summaryrefslogtreecommitdiff
path: root/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'src/components')
-rw-r--r--src/components/ClipboardCommands.tsx11
-rw-r--r--src/components/CustomPathOverride.tsx17
-rw-r--r--src/components/InstalledGamesSection.tsx8
-rw-r--r--src/components/OptiScalerControls.tsx34
-rw-r--r--src/components/ResultDisplay.tsx4
-rw-r--r--src/components/SteamGamePatcher.tsx277
-rw-r--r--src/components/index.ts1
7 files changed, 326 insertions, 26 deletions
diff --git a/src/components/ClipboardCommands.tsx b/src/components/ClipboardCommands.tsx
index 5a6f38f..7bbd12d 100644
--- a/src/components/ClipboardCommands.tsx
+++ b/src/components/ClipboardCommands.tsx
@@ -2,19 +2,20 @@ import { SmartClipboardButton } from "./SmartClipboardButton";
interface ClipboardCommandsProps {
pathExists: boolean | null;
+ dllName: string;
}
-export function ClipboardCommands({ pathExists }: ClipboardCommandsProps) {
+export function ClipboardCommands({ pathExists, dllName }: ClipboardCommandsProps) {
if (pathExists !== true) return null;
return (
<>
- <SmartClipboardButton
- command="~/fgmod/fgmod %command%"
+ <SmartClipboardButton
+ command={`DLL=${dllName} ~/fgmod/fgmod %command%`}
buttonText="Copy Patch Command"
/>
-
- <SmartClipboardButton
+
+ <SmartClipboardButton
command="~/fgmod/fgmod-uninstaller.sh %command%"
buttonText="Copy Unpatch Command"
/>
diff --git a/src/components/CustomPathOverride.tsx b/src/components/CustomPathOverride.tsx
index ffc4b1f..4effc6c 100644
--- a/src/components/CustomPathOverride.tsx
+++ b/src/components/CustomPathOverride.tsx
@@ -36,6 +36,7 @@ const ensureDirectory = (value: string) => {
interface ManualPatchControlsProps {
isAvailable: boolean;
onManualModeChange?: (enabled: boolean) => void;
+ dllName: string;
}
interface PickerState {
@@ -56,7 +57,7 @@ const formatResultMessage = (result: ApiResponse | null) => {
return result.message || result.output || "Operation failed.";
};
-export const ManualPatchControls = ({ isAvailable, onManualModeChange }: ManualPatchControlsProps) => {
+export const ManualPatchControls = ({ isAvailable, onManualModeChange, dllName }: ManualPatchControlsProps) => {
const [isEnabled, setEnabled] = useState(false);
const [defaults, setDefaults] = useState<PathDefaults>(INITIAL_DEFAULTS);
const [pickerState, setPickerState] = useState<PickerState>(INITIAL_PICKER_STATE);
@@ -165,7 +166,7 @@ export const ManualPatchControls = ({ isAvailable, onManualModeChange }: ManualP
try {
const response =
action === "patch"
- ? await runManualPatch(selectedPath)
+ ? await runManualPatch(selectedPath, dllName)
: await runManualUnpatch(selectedPath);
setOperationResult(response ?? { status: "error", message: "No response from backend." });
} catch (err) {
@@ -177,7 +178,7 @@ export const ManualPatchControls = ({ isAvailable, onManualModeChange }: ManualP
setBusy(false);
}
},
- [selectedPath]
+ [selectedPath, dllName]
);
const handleToggle = (value: boolean) => {
@@ -216,7 +217,11 @@ export const ManualPatchControls = ({ isAvailable, onManualModeChange }: ManualP
{canInteract && (
<>
<SmartClipboardButton
- command='WINEDLLOVERRIDES="dxgi=n,b" SteamDeck=0 %command%'
+ command={
+ dllName === "OptiScaler.asi"
+ ? "SteamDeck=0 %command%"
+ : `WINEDLLOVERRIDES="${dllName.replace(".dll", "")}=n,b" SteamDeck=0 %command%`
+ }
buttonText="Manual launch cmd"
/>
<PanelSectionRow>
@@ -234,9 +239,7 @@ export const ManualPatchControls = ({ isAvailable, onManualModeChange }: ManualP
<Field
label="Picker error"
description={pickerState.lastError}
- >
- ⚠️
- </Field>
+ />
</PanelSectionRow>
)}
diff --git a/src/components/InstalledGamesSection.tsx b/src/components/InstalledGamesSection.tsx
index 71278d7..04d653b 100644
--- a/src/components/InstalledGamesSection.tsx
+++ b/src/components/InstalledGamesSection.tsx
@@ -42,14 +42,14 @@ export function InstalledGamesSection() {
<ConfirmModal
strTitle={`Enable Frame Generation for ${selectedGame.name}?`}
strDescription={
- "⚠️ Important: This plugin does not automatically unpatch games when uninstalled. If you uninstall this plugin or experience game issues, use the 'Disable Frame Generation' option or verify game file integrity through Steam."
+ "Important: This plugin does not automatically unpatch games when uninstalled. If you uninstall this plugin or experience game issues, use the 'Disable Frame Generation' option or verify game file integrity through Steam."
}
strOKButtonText="Enable Frame Generation"
strCancelButtonText="Cancel"
onOK={async () => {
try {
await SteamClient.Apps.SetAppLaunchOptions(selectedGame.appid, '~/fgmod/fgmod %COMMAND%');
- setResult(`✓ Frame generation enabled for ${selectedGame.name}. Launch the game, enable DLSS in graphics settings, then press Insert to access OptiScaler options.`);
+ setResult(`Frame generation enabled for ${selectedGame.name}. Launch the game, enable DLSS in graphics settings, then press Insert to access OptiScaler options.`);
} catch (error) {
logError('handlePatchClick: ' + String(error));
setResult(error instanceof Error ? `Error: ${error.message}` : 'Error enabling frame generation');
@@ -64,7 +64,7 @@ export function InstalledGamesSection() {
try {
await SteamClient.Apps.SetAppLaunchOptions(selectedGame.appid, '~/fgmod/fgmod-uninstaller.sh %COMMAND%');
- setResult(`✓ Frame generation will be disabled on next launch of ${selectedGame.name}.`);
+ setResult(`Frame generation will be disabled on next launch of ${selectedGame.name}.`);
} catch (error) {
logError('handleUnpatchClick: ' + String(error));
setResult(error instanceof Error ? `Error: ${error.message}` : 'Error disabling frame generation');
@@ -96,7 +96,7 @@ export function InstalledGamesSection() {
...STYLES.preWrap,
...(result.includes('Error') ? STYLES.statusNotInstalled : STYLES.statusInstalled)
}}>
- {result.includes('Error') ? '❌' : '✅'} {result}
+ {result}
</div>
</PanelSectionRow>
) : null}
diff --git a/src/components/OptiScalerControls.tsx b/src/components/OptiScalerControls.tsx
index 468683c..f88e8f9 100644
--- a/src/components/OptiScalerControls.tsx
+++ b/src/components/OptiScalerControls.tsx
@@ -1,9 +1,9 @@
import { useState, useEffect } from "react";
-import { PanelSection } from "@decky/ui";
+import { DropdownItem, PanelSection, PanelSectionRow } from "@decky/ui";
import { runInstallFGMod, runUninstallFGMod } from "../api";
import { OperationResult } from "./ResultDisplay";
import { createAutoCleanupTimer } from "../utils";
-import { TIMEOUTS } from "../utils/constants";
+import { TIMEOUTS, PROXY_DLL_OPTIONS, DEFAULT_PROXY_DLL } from "../utils/constants";
import { InstallationStatus } from "./InstallationStatus";
import { OptiScalerHeader } from "./OptiScalerHeader";
import { ClipboardCommands } from "./ClipboardCommands";
@@ -11,6 +11,7 @@ import { InstructionCard } from "./InstructionCard";
import { OptiScalerWiki } from "./OptiScalerWiki";
import { UninstallButton } from "./UninstallButton";
import { ManualPatchControls } from "./CustomPathOverride";
+import { SteamGamePatcher } from "./SteamGamePatcher";
interface OptiScalerControlsProps {
pathExists: boolean | null;
@@ -23,6 +24,7 @@ export function OptiScalerControls({ pathExists, setPathExists }: OptiScalerCont
const [installResult, setInstallResult] = useState<OperationResult | null>(null);
const [uninstallResult, setUninstallResult] = useState<OperationResult | null>(null);
const [manualModeEnabled, setManualModeEnabled] = useState(false);
+ const [dllName, setDllName] = useState<string>(DEFAULT_PROXY_DLL);
useEffect(() => {
if (installResult) {
return createAutoCleanupTimer(() => setInstallResult(null), TIMEOUTS.resultDisplay);
@@ -76,18 +78,34 @@ export function OptiScalerControls({ pathExists, setPathExists }: OptiScalerCont
/>
<OptiScalerHeader pathExists={pathExists} />
-
+
+ {pathExists === true && (
+ <PanelSectionRow>
+ <DropdownItem
+ label="Proxy DLL name"
+ description={PROXY_DLL_OPTIONS.find((o) => o.value === dllName)?.hint}
+ menuLabel="Proxy DLL name"
+ selectedOption={dllName}
+ rgOptions={PROXY_DLL_OPTIONS.map((o) => ({ data: o.value, label: o.label }))}
+ onChange={(option) => setDllName(String(option.data))}
+ />
+ </PanelSectionRow>
+ )}
+
+ {pathExists === true && (
+ <SteamGamePatcher dllName={dllName} />
+ )}
+
+ <ClipboardCommands pathExists={pathExists} dllName={dllName} />
+
<ManualPatchControls
isAvailable={pathExists === true}
onManualModeChange={setManualModeEnabled}
+ dllName={dllName}
/>
{!manualModeEnabled && (
- <>
- <ClipboardCommands pathExists={pathExists} />
-
- <InstructionCard pathExists={pathExists} />
- </>
+ <InstructionCard pathExists={pathExists} />
)}
<OptiScalerWiki pathExists={pathExists} />
diff --git a/src/components/ResultDisplay.tsx b/src/components/ResultDisplay.tsx
index bcd66c0..b54e41d 100644
--- a/src/components/ResultDisplay.tsx
+++ b/src/components/ResultDisplay.tsx
@@ -19,13 +19,13 @@ export const ResultDisplay: FC<ResultDisplayProps> = ({ result }) => {
<div style={isSuccess ? STYLES.statusInstalled : STYLES.statusNotInstalled}>
{isSuccess ? (
<>
- ✅ {result.output?.includes("uninstall") || result.output?.includes("remov")
+ {result.output?.includes("uninstall") || result.output?.includes("remov")
? "OptiScaler mod removed successfully"
: "OptiScaler mod installed successfully"}
</>
) : (
<>
- ❌ <strong>Error:</strong> {result.message || "Operation failed"}
+ <strong>Error:</strong> {result.message || "Operation failed"}
</>
)}
{result.output && !isSuccess && (
diff --git a/src/components/SteamGamePatcher.tsx b/src/components/SteamGamePatcher.tsx
new file mode 100644
index 0000000..06c373c
--- /dev/null
+++ b/src/components/SteamGamePatcher.tsx
@@ -0,0 +1,277 @@
+import { useCallback, useEffect, useMemo, useState } from "react";
+import { ButtonItem, DropdownItem, Field, PanelSectionRow } from "@decky/ui";
+import { listInstalledGames } from "../api";
+import { createAutoCleanupTimer } from "../utils";
+import { TIMEOUTS } from "../utils/constants";
+
+// ─── SteamClient helpers ─────────────────────────────────────────────────────
+
+/**
+ * Wrap the callback-based RegisterForAppDetails in a Promise.
+ * Resolves with the current launch options string, or "" if SteamClient is
+ * unavailable (e.g. desktop / dev mode). Times out after 5 seconds.
+ */
+const getSteamLaunchOptions = (appId: number): Promise<string> =>
+ new Promise((resolve, reject) => {
+ if (
+ typeof SteamClient === "undefined" ||
+ !SteamClient?.Apps?.RegisterForAppDetails
+ ) {
+ resolve("");
+ return;
+ }
+
+ let settled = false;
+ let unregister = () => {};
+
+ const timeout = window.setTimeout(() => {
+ if (settled) return;
+ settled = true;
+ unregister();
+ reject(new Error("Timed out reading launch options."));
+ }, 5000);
+
+ const registration = SteamClient.Apps.RegisterForAppDetails(
+ appId,
+ (details: { strLaunchOptions?: string }) => {
+ if (settled) return;
+ settled = true;
+ window.clearTimeout(timeout);
+ unregister();
+ resolve(details?.strLaunchOptions ?? "");
+ }
+ );
+
+ unregister = registration.unregister;
+ });
+
+const setSteamLaunchOptions = (appId: number, options: string): void => {
+ if (
+ typeof SteamClient === "undefined" ||
+ !SteamClient?.Apps?.SetAppLaunchOptions
+ ) {
+ throw new Error("SteamClient.Apps.SetAppLaunchOptions is not available.");
+ }
+ SteamClient.Apps.SetAppLaunchOptions(appId, options);
+};
+
+// ─── Helpers ─────────────────────────────────────────────────────────────────
+
+/** Remove any fgmod invocation from a launch options string, keeping the rest. */
+const stripFgmod = (opts: string): string =>
+ opts
+ .replace(/DLL=\S+\s+~\/fgmod\/fgmod\s+%command%/g, "")
+ .replace(/~\/fgmod\/fgmod\s+%command%/g, "")
+ .trim();
+
+/** Extract the DLL= value from a launch options string, if present. */
+const extractDllName = (opts: string): string | null => {
+ const m = opts.match(/DLL=(\S+)\s+~\/fgmod\/fgmod/);
+ return m ? m[1] : null;
+};
+
+// ─── Component ───────────────────────────────────────────────────────────────
+
+interface SteamGamePatcherProps {
+ dllName: string;
+}
+
+type GameEntry = { appid: string; name: string };
+
+export function SteamGamePatcher({ dllName }: SteamGamePatcherProps) {
+ const [games, setGames] = useState<GameEntry[]>([]);
+ const [gamesLoading, setGamesLoading] = useState(true);
+ const [selectedAppId, setSelectedAppId] = useState<string>("");
+ const [launchOptions, setLaunchOptions] = useState<string>("");
+ const [launchOptionsLoading, setLaunchOptionsLoading] = useState(false);
+ const [busy, setBusy] = useState(false);
+ const [resultMessage, setResultMessage] = useState<string>("");
+
+ // Auto-clear result message
+ useEffect(() => {
+ if (resultMessage) {
+ return createAutoCleanupTimer(
+ () => setResultMessage(""),
+ TIMEOUTS.resultDisplay
+ );
+ }
+ return undefined;
+ }, [resultMessage]);
+
+ // Load game list on mount
+ useEffect(() => {
+ let cancelled = false;
+ (async () => {
+ setGamesLoading(true);
+ try {
+ const result = await listInstalledGames();
+ if (cancelled) return;
+ if (result.status === "success" && result.games.length > 0) {
+ setGames(result.games);
+ setSelectedAppId(result.games[0].appid);
+ }
+ } catch (e) {
+ console.error("SteamGamePatcher: failed to load games", e);
+ } finally {
+ if (!cancelled) setGamesLoading(false);
+ }
+ })();
+ return () => {
+ cancelled = true;
+ };
+ }, []);
+
+ // Reload launch options when selected game changes
+ useEffect(() => {
+ if (!selectedAppId) {
+ setLaunchOptions("");
+ return;
+ }
+ let cancelled = false;
+ (async () => {
+ setLaunchOptionsLoading(true);
+ try {
+ const opts = await getSteamLaunchOptions(Number(selectedAppId));
+ if (!cancelled) setLaunchOptions(opts);
+ } catch {
+ if (!cancelled) setLaunchOptions("");
+ } finally {
+ if (!cancelled) setLaunchOptionsLoading(false);
+ }
+ })();
+ return () => {
+ cancelled = true;
+ };
+ }, [selectedAppId]);
+
+ const targetCommand = `DLL=${dllName} ~/fgmod/fgmod %command%`;
+ const isManaged = launchOptions.includes("fgmod/fgmod");
+ const activeDll = useMemo(() => extractDllName(launchOptions), [launchOptions]);
+ const selectedGame = useMemo(
+ () => games.find((g) => g.appid === selectedAppId) ?? null,
+ [games, selectedAppId]
+ );
+
+ const handleSet = useCallback(() => {
+ if (!selectedAppId || busy) return;
+ setBusy(true);
+ try {
+ setSteamLaunchOptions(Number(selectedAppId), targetCommand);
+ setLaunchOptions(targetCommand);
+ setResultMessage(
+ `Launch options set for ${selectedGame?.name ?? selectedAppId}`
+ );
+ } catch (e) {
+ setResultMessage(`Error: ${e instanceof Error ? e.message : String(e)}`);
+ } finally {
+ setBusy(false);
+ }
+ }, [selectedAppId, targetCommand, selectedGame, busy]);
+
+ const handleRemove = useCallback(() => {
+ if (!selectedAppId || busy) return;
+ setBusy(true);
+ try {
+ const stripped = stripFgmod(launchOptions);
+ setSteamLaunchOptions(Number(selectedAppId), stripped);
+ setLaunchOptions(stripped);
+ setResultMessage(
+ `Removed fgmod from ${selectedGame?.name ?? selectedAppId}`
+ );
+ } catch (e) {
+ setResultMessage(`Error: ${e instanceof Error ? e.message : String(e)}`);
+ } finally {
+ setBusy(false);
+ }
+ }, [selectedAppId, launchOptions, selectedGame, busy]);
+
+ // ── Status display ──────────────────────────────────────────────────────────
+ const statusText = useMemo(() => {
+ if (!selectedGame) return "—";
+ if (launchOptionsLoading) return "Loading...";
+ if (!isManaged) return "Not set";
+ if (activeDll && activeDll !== dllName)
+ return `Active — ${activeDll} · switch to apply ${dllName}`;
+ return `Active — ${activeDll ?? dllName}`;
+ }, [selectedGame, launchOptionsLoading, isManaged, activeDll, dllName]);
+
+ const statusColor = useMemo(() => {
+ if (!isManaged || launchOptionsLoading) return undefined;
+ if (activeDll && activeDll !== dllName) return "#ffd866"; // yellow — different DLL selected
+ return "#3fb950"; // green — active and matching
+ }, [isManaged, launchOptionsLoading, activeDll, dllName]);
+
+ const setButtonLabel = useMemo(() => {
+ if (busy) return "Applying...";
+ if (!isManaged) return "Enable for this game";
+ if (activeDll && activeDll !== dllName) return `Switch to ${dllName}`;
+ return "Re-apply";
+ }, [busy, isManaged, activeDll, dllName]);
+
+ return (
+ <>
+ <PanelSectionRow>
+ <DropdownItem
+ label="Steam game"
+ menuLabel="Select a Steam game"
+ strDefaultLabel={
+ gamesLoading ? "Loading games..." : "Choose a game"
+ }
+ disabled={gamesLoading || games.length === 0}
+ selectedOption={selectedAppId}
+ rgOptions={games.map((g) => ({ data: g.appid, label: g.name }))}
+ onChange={(option) => {
+ setSelectedAppId(String(option.data));
+ setResultMessage("");
+ }}
+ />
+ </PanelSectionRow>
+
+ {selectedGame && (
+ <>
+ <PanelSectionRow>
+ <Field focusable label="Launch options status">
+ {statusColor ? (
+ <span style={{ color: statusColor, fontWeight: 600 }}>
+ {statusText}
+ </span>
+ ) : (
+ statusText
+ )}
+ </Field>
+ </PanelSectionRow>
+
+ <PanelSectionRow>
+ <ButtonItem
+ layout="below"
+ disabled={busy || launchOptionsLoading}
+ onClick={handleSet}
+ >
+ {setButtonLabel}
+ </ButtonItem>
+ </PanelSectionRow>
+
+ {isManaged && (
+ <PanelSectionRow>
+ <ButtonItem
+ layout="below"
+ disabled={busy}
+ onClick={handleRemove}
+ >
+ {busy ? "Removing..." : "Remove from launch options"}
+ </ButtonItem>
+ </PanelSectionRow>
+ )}
+
+ {resultMessage && (
+ <PanelSectionRow>
+ <Field focusable label="Result">
+ {resultMessage}
+ </Field>
+ </PanelSectionRow>
+ )}
+ </>
+ )}
+ </>
+ );
+}
diff --git a/src/components/index.ts b/src/components/index.ts
index cd599ba..ad47347 100644
--- a/src/components/index.ts
+++ b/src/components/index.ts
@@ -9,3 +9,4 @@ export { UninstallButton } from './UninstallButton';
export { SmartClipboardButton } from './SmartClipboardButton';
export { ResultDisplay } from './ResultDisplay';
export { ManualPatchControls } from './CustomPathOverride';
+export { SteamGamePatcher } from './SteamGamePatcher';