summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com>2025-07-17 08:49:12 -0400
committerGitHub <noreply@github.com>2025-07-17 08:49:12 -0400
commitca0d5f0ec1f4ba21f4bf51f0f773d2b6bad45c93 (patch)
tree8374652b65877b10bce3ea6e073165a02b6af0ba
parent74ac6e7b7a18c2ae969b08242a5919f903d294e2 (diff)
downloadDecky-Framegen-ca0d5f0ec1f4ba21f4bf51f0f773d2b6bad45c93.tar.gz
Decky-Framegen-ca0d5f0ec1f4ba21f4bf51f0f773d2b6bad45c93.zip
reorganize for readability and DRY (#115)v0.10.1
* reorganize for readability and DRY * rm backup files * ver bump
-rw-r--r--main.py202
-rw-r--r--package.json2
-rw-r--r--src/api/index.ts23
-rw-r--r--src/components/FGModInstallerSection.tsx103
-rw-r--r--src/components/InstalledGamesSection.tsx123
-rw-r--r--src/components/ResultDisplay.tsx37
-rw-r--r--src/exports.ts14
-rw-r--r--[-rwxr-xr-x]src/index.tsx303
-rw-r--r--src/types/index.ts26
-rw-r--r--src/utils/constants.ts42
-rw-r--r--src/utils/index.ts30
11 files changed, 515 insertions, 390 deletions
diff --git a/main.py b/main.py
index 12aa702..05f4de5 100644
--- a/main.py
+++ b/main.py
@@ -1,4 +1,4 @@
-import decky # Old-style Decky import
+import decky
import os
import subprocess
import json
@@ -12,6 +12,109 @@ class Plugin:
async def _unload(self):
decky.logger.info("Framegen plugin unloaded.")
+
+ def _create_renamed_copies(self, source_file, renames_dir):
+ """Create renamed copies of the OptiScaler.dll file"""
+ try:
+ renames_dir.mkdir(exist_ok=True)
+
+ rename_files = [
+ "dxgi.dll",
+ "winmm.dll",
+ "dbghelp.dll",
+ "version.dll",
+ "wininet.dll",
+ "winhttp.dll",
+ "OptiScaler.asi"
+ ]
+
+ if source_file.exists():
+ for rename_file in rename_files:
+ dest_file = renames_dir / rename_file
+ shutil.copy2(source_file, dest_file)
+ decky.logger.info(f"Created renamed copy: {dest_file}")
+ return True
+ else:
+ decky.logger.error(f"Source file {source_file} does not exist")
+ return False
+
+ except Exception as e:
+ decky.logger.error(f"Failed to create renamed copies: {e}")
+ return False
+
+ def _copy_launcher_scripts(self, assets_dir, extract_path):
+ """Copy launcher scripts from assets directory"""
+ try:
+ # Copy fgmod script
+ fgmod_script_src = assets_dir / "fgmod.sh"
+ fgmod_script_dest = extract_path / "fgmod"
+ if fgmod_script_src.exists():
+ shutil.copy2(fgmod_script_src, fgmod_script_dest)
+ fgmod_script_dest.chmod(0o755)
+ decky.logger.info(f"Copied fgmod script to {fgmod_script_dest}")
+
+ # Copy uninstaller script
+ uninstaller_src = assets_dir / "fgmod-uninstaller.sh"
+ uninstaller_dest = extract_path / "fgmod-uninstaller.sh"
+ if uninstaller_src.exists():
+ shutil.copy2(uninstaller_src, uninstaller_dest)
+ uninstaller_dest.chmod(0o755)
+ decky.logger.info(f"Copied uninstaller script to {uninstaller_dest}")
+
+ return True
+ except Exception as e:
+ decky.logger.error(f"Failed to copy launcher scripts: {e}")
+ return False
+
+ def _modify_optiscaler_ini(self, ini_file):
+ """Modify OptiScaler.ini to set FGType=nukems"""
+ try:
+ if ini_file.exists():
+ with open(ini_file, 'r') as f:
+ content = f.read()
+
+ # Replace FGType=auto with FGType=nukems
+ updated_content = re.sub(r'FGType\s*=\s*auto', 'FGType=nukems', content)
+
+ with open(ini_file, 'w') as f:
+ f.write(updated_content)
+
+ decky.logger.info("Modified OptiScaler.ini to set FGType=nukems")
+ return True
+ else:
+ decky.logger.warning(f"OptiScaler.ini not found at {ini_file}")
+ return False
+ except Exception as e:
+ decky.logger.error(f"Failed to modify OptiScaler.ini: {e}")
+ return False
+
+ def _setup_flatpak_compatibility(self, fgmod_path):
+ """Set up Flatpak compatibility if needed"""
+ try:
+ # Check if Flatpak Steam is installed
+ flatpak_check = subprocess.run(
+ ["flatpak", "list"],
+ capture_output=True,
+ text=True,
+ check=False
+ )
+
+ if flatpak_check.returncode == 0 and "com.valvesoftware.Steam" in flatpak_check.stdout:
+ decky.logger.info("Flatpak Steam detected, adding filesystem access")
+
+ subprocess.run([
+ "flatpak", "override", "--user",
+ f"--filesystem={fgmod_path}",
+ "com.valvesoftware.Steam"
+ ], check=False)
+
+ decky.logger.info("Added Flatpak filesystem access")
+ return True
+
+ return False
+ except Exception as e:
+ decky.logger.warning(f"Flatpak setup had issues (this is OK): {e}")
+ return False
async def extract_static_optiscaler(self) -> dict:
"""Extract OptiScaler from the plugin's bin directory."""
@@ -62,55 +165,13 @@ class Plugin:
}
# Create renamed copies of OptiScaler.dll
- try:
- renames_dir = extract_path / "renames"
- renames_dir.mkdir(exist_ok=True)
-
- source_file = extract_path / "OptiScaler.dll"
-
- rename_files = [
- "dxgi.dll",
- "winmm.dll",
- "dbghelp.dll",
- "version.dll",
- "wininet.dll",
- "winhttp.dll",
- "OptiScaler.asi"
- ]
-
- if source_file.exists():
- for rename_file in rename_files:
- dest_file = renames_dir / rename_file
- shutil.copy2(source_file, dest_file)
- decky.logger.info(f"Created renamed copy: {dest_file}")
- else:
- decky.logger.error(f"Source file {source_file} does not exist")
-
- except Exception as e:
- decky.logger.error(f"Failed to create renamed copies: {e}")
+ source_file = extract_path / "OptiScaler.dll"
+ renames_dir = extract_path / "renames"
+ self._create_renamed_copies(source_file, renames_dir)
# Copy launcher scripts from assets
- try:
- assets_dir = Path(decky.DECKY_PLUGIN_DIR) / "assets"
-
- # Copy fgmod script
- fgmod_script_src = assets_dir / "fgmod.sh"
- fgmod_script_dest = extract_path / "fgmod"
- if fgmod_script_src.exists():
- shutil.copy2(fgmod_script_src, fgmod_script_dest)
- fgmod_script_dest.chmod(0o755)
- decky.logger.info(f"Copied fgmod script to {fgmod_script_dest}")
-
- # Copy uninstaller script
- uninstaller_src = assets_dir / "fgmod-uninstaller.sh"
- uninstaller_dest = extract_path / "fgmod-uninstaller.sh"
- if uninstaller_src.exists():
- shutil.copy2(uninstaller_src, uninstaller_dest)
- uninstaller_dest.chmod(0o755)
- decky.logger.info(f"Copied uninstaller script to {uninstaller_dest}")
-
- except Exception as e:
- decky.logger.error(f"Failed to copy launcher scripts: {e}")
+ assets_dir = Path(decky.DECKY_PLUGIN_DIR) / "assets"
+ self._copy_launcher_scripts(assets_dir, extract_path)
# Extract version from filename
version_match = optiscaler_archive.name.replace('.7z', '')
@@ -129,23 +190,8 @@ class Plugin:
decky.logger.error(f"Failed to create version file: {e}")
# Modify OptiScaler.ini to set FGType=nukems
- try:
- ini_file = extract_path / "OptiScaler.ini"
- if ini_file.exists():
- with open(ini_file, 'r') as f:
- content = f.read()
-
- # Replace FGType=auto with FGType=nukems
- updated_content = re.sub(r'FGType\s*=\s*auto', 'FGType=nukems', content)
-
- with open(ini_file, 'w') as f:
- f.write(updated_content)
-
- decky.logger.info("Modified OptiScaler.ini to set FGType=nukems")
- else:
- decky.logger.warning(f"OptiScaler.ini not found at {ini_file}")
- except Exception as e:
- decky.logger.error(f"Failed to modify OptiScaler.ini: {e}")
+ ini_file = extract_path / "OptiScaler.ini"
+ self._modify_optiscaler_ini(ini_file)
return {
"status": "success",
@@ -197,30 +243,8 @@ class Plugin:
}
# Handle Flatpak compatibility
- try:
- fgmod_path = Path(decky.HOME) / "fgmod"
-
- # Check if Flatpak Steam is installed
- flatpak_check = subprocess.run(
- ["flatpak", "list"],
- capture_output=True,
- text=True,
- check=False
- )
-
- if flatpak_check.returncode == 0 and "com.valvesoftware.Steam" in flatpak_check.stdout:
- decky.logger.info("Flatpak Steam detected, adding filesystem access")
-
- subprocess.run([
- "flatpak", "override", "--user",
- f"--filesystem={fgmod_path}",
- "com.valvesoftware.Steam"
- ], check=False)
-
- decky.logger.info("Added Flatpak filesystem access")
-
- except Exception as e:
- decky.logger.warning(f"Flatpak setup had issues (this is OK): {e}")
+ fgmod_path = Path(decky.HOME) / "fgmod"
+ self._setup_flatpak_compatibility(fgmod_path)
return {
"status": "success",
diff --git a/package.json b/package.json
index 6d7f0d2..cd2ff67 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "decky-framegen",
- "version": "0.10.0",
+ "version": "0.10.1",
"description": "plugin to install OptiScaler bleeding-edge and enable upscaling and framegen in a large variety of games.",
"type": "module",
"scripts": {
diff --git a/src/api/index.ts b/src/api/index.ts
new file mode 100644
index 0000000..11e4213
--- /dev/null
+++ b/src/api/index.ts
@@ -0,0 +1,23 @@
+import { callable } from "@decky/api";
+
+export const runInstallFGMod = callable<
+ [],
+ { status: string; message?: string; output?: string }
+>("run_install_fgmod");
+
+export const runUninstallFGMod = callable<
+ [],
+ { status: string; message?: string; output?: string }
+>("run_uninstall_fgmod");
+
+export const checkFGModPath = callable<
+ [],
+ { exists: boolean }
+>("check_fgmod_path");
+
+export const listInstalledGames = callable<
+ [],
+ { status: string; games: { appid: string; name: string }[] }
+>("list_installed_games");
+
+export const logError = callable<[string], void>("log_error");
diff --git a/src/components/FGModInstallerSection.tsx b/src/components/FGModInstallerSection.tsx
new file mode 100644
index 0000000..f9f905e
--- /dev/null
+++ b/src/components/FGModInstallerSection.tsx
@@ -0,0 +1,103 @@
+import { useState, useEffect } from "react";
+import { PanelSection, PanelSectionRow, ButtonItem } from "@decky/ui";
+import { checkFGModPath, runInstallFGMod, runUninstallFGMod } from "../api";
+import { ResultDisplay, OperationResult } from "./ResultDisplay";
+import { createAutoCleanupTimer, safeAsyncOperation } from "../utils";
+import { TIMEOUTS, MESSAGES } from "../utils/constants";
+
+export function FGModInstallerSection() {
+ const [installing, setInstalling] = useState(false);
+ const [uninstalling, setUninstalling] = useState(false);
+ const [installResult, setInstallResult] = useState<OperationResult | null>(null);
+ const [uninstallResult, setUninstallResult] = useState<OperationResult | null>(null);
+ const [pathExists, setPathExists] = useState<boolean | null>(null);
+
+ useEffect(() => {
+ const checkPath = async () => {
+ const result = await safeAsyncOperation(
+ async () => await checkFGModPath(),
+ 'useEffect -> checkPath'
+ );
+ if (result) setPathExists(result.exists);
+ };
+
+ checkPath(); // Initial check
+ const intervalId = setInterval(checkPath, TIMEOUTS.pathCheck); // Check every 3 seconds
+ return () => clearInterval(intervalId); // Cleanup interval on component unmount
+ }, []);
+
+ useEffect(() => {
+ if (installResult) {
+ return createAutoCleanupTimer(() => setInstallResult(null), TIMEOUTS.resultDisplay);
+ }
+ return () => {}; // Ensure a cleanup function is always returned
+ }, [installResult]);
+
+ useEffect(() => {
+ if (uninstallResult) {
+ return createAutoCleanupTimer(() => setUninstallResult(null), TIMEOUTS.resultDisplay);
+ }
+ return () => {}; // Ensure a cleanup function is always returned
+ }, [uninstallResult]);
+
+ const handleInstallClick = async () => {
+ try {
+ setInstalling(true);
+ const result = await runInstallFGMod();
+ setInstallResult(result);
+ } catch (e) {
+ console.error(e);
+ } finally {
+ setInstalling(false);
+ }
+ };
+
+ const handleUninstallClick = async () => {
+ try {
+ setUninstalling(true);
+ const result = await runUninstallFGMod();
+ setUninstallResult(result);
+ } catch (e) {
+ console.error(e);
+ } finally {
+ setUninstalling(false);
+ }
+ };
+
+ return (
+ <PanelSection>
+ {pathExists !== null ? (
+ <PanelSectionRow>
+ <div style={{ color: pathExists ? "green" : "red" }}>
+ {pathExists ? MESSAGES.modInstalled : MESSAGES.modNotInstalled}
+ </div>
+ </PanelSectionRow>
+ ) : null}
+
+ {pathExists === false ? (
+ <PanelSectionRow>
+ <ButtonItem layout="below" onClick={handleInstallClick} disabled={installing}>
+ {installing ? MESSAGES.installing : MESSAGES.installButton}
+ </ButtonItem>
+ </PanelSectionRow>
+ ) : null}
+
+ {pathExists === true ? (
+ <PanelSectionRow>
+ <ButtonItem layout="below" onClick={handleUninstallClick} disabled={uninstalling}>
+ {uninstalling ? MESSAGES.uninstalling : MESSAGES.uninstallButton}
+ </ButtonItem>
+ </PanelSectionRow>
+ ) : null}
+
+ <ResultDisplay result={installResult} />
+ <ResultDisplay result={uninstallResult} />
+
+ <PanelSectionRow>
+ <div>
+ {MESSAGES.instructionText}
+ </div>
+ </PanelSectionRow>
+ </PanelSection>
+ );
+}
diff --git a/src/components/InstalledGamesSection.tsx b/src/components/InstalledGamesSection.tsx
new file mode 100644
index 0000000..30ca2a4
--- /dev/null
+++ b/src/components/InstalledGamesSection.tsx
@@ -0,0 +1,123 @@
+import { useState, useEffect } from "react";
+import { PanelSection, PanelSectionRow, ButtonItem, DropdownItem, ConfirmModal, showModal } from "@decky/ui";
+import { listInstalledGames, logError } from "../api";
+import { safeAsyncOperation } from "../utils";
+import { STYLES } from "../utils/constants";
+import { GameInfo } from "../types/index";
+
+export function InstalledGamesSection() {
+ const [games, setGames] = useState<GameInfo[]>([]);
+ const [selectedGame, setSelectedGame] = useState<GameInfo | null>(null);
+ const [result, setResult] = useState<string>('');
+
+ useEffect(() => {
+ const fetchGames = async () => {
+ const response = await safeAsyncOperation(
+ async () => await listInstalledGames(),
+ 'fetchGames'
+ );
+
+ if (response?.status === "success") {
+ const sortedGames = [...response.games]
+ .map(game => ({
+ ...game,
+ appid: parseInt(game.appid, 10),
+ }))
+ .sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
+ setGames(sortedGames);
+ } else if (response) {
+ logError('fetchGames: ' + JSON.stringify(response));
+ console.error('fetchGames: ' + JSON.stringify(response));
+ }
+ };
+
+ fetchGames();
+ }, []);
+
+ const handlePatchClick = async () => {
+ if (!selectedGame) return;
+
+ // Show confirmation modal
+ showModal(
+ <ConfirmModal
+ strTitle={`Patch ${selectedGame.name}?`}
+ strDescription={
+ "WARNING: Decky Framegen does not unpatch games when uninstalled. Be sure to unpatch the game or verify the integrity of your game files if you choose to uninstall the plugin or the game has issues."
+ }
+ strOKButtonText="Yeah man, I wanna do it"
+ strCancelButtonText="Cancel"
+ onOK={async () => {
+ try {
+ await SteamClient.Apps.SetAppLaunchOptions(selectedGame.appid, '~/fgmod/fgmod %COMMAND%');
+ setResult(`Launch options set for ${selectedGame.name}. You can now select DLSS in the game's menu, and access OptiScaler with Insert key.`);
+ } catch (error) {
+ logError('handlePatchClick: ' + String(error));
+ setResult(error instanceof Error ? `Error setting launch options: ${error.message}` : 'Error setting launch options');
+ }
+ }}
+ />
+ );
+ };
+
+ const handleUnpatchClick = async () => {
+ if (!selectedGame) return;
+
+ try {
+ await SteamClient.Apps.SetAppLaunchOptions(selectedGame.appid, '~/fgmod/fgmod-uninstaller.sh %COMMAND%');
+ setResult(`OptiScaler will uninstall on next launch of ${selectedGame.name}.`);
+ } catch (error) {
+ logError('handleUnpatchClick: ' + String(error));
+ setResult(error instanceof Error ? `Error clearing launch options: ${error.message}` : 'Error clearing launch options');
+ }
+ };
+
+ return (
+ <PanelSection title="Select a game to patch:">
+ <PanelSectionRow>
+ <DropdownItem
+ rgOptions={games.map(game => ({
+ data: game.appid,
+ label: game.name
+ }))}
+ selectedOption={selectedGame?.appid}
+ onChange={(option) => {
+ const game = games.find(g => g.appid === option.data);
+ setSelectedGame(game || null);
+ setResult('');
+ }}
+ strDefaultLabel="Select a game..."
+ menuLabel="Installed Games"
+ />
+ </PanelSectionRow>
+
+ {result ? (
+ <PanelSectionRow>
+ <div style={STYLES.resultBox}>
+ {result}
+ </div>
+ </PanelSectionRow>
+ ) : null}
+
+ {selectedGame ? (
+ <>
+ <PanelSectionRow>
+ <ButtonItem
+ layout="below"
+ onClick={handlePatchClick}
+ >
+ Patch
+ </ButtonItem>
+ </PanelSectionRow>
+ <PanelSectionRow>
+ <ButtonItem
+ layout="below"
+ onClick={handleUnpatchClick}
+ >
+ Unpatch
+ </ButtonItem>
+ </PanelSectionRow>
+ </>
+ ) : null}
+ </PanelSection>
+ );
+}
diff --git a/src/components/ResultDisplay.tsx b/src/components/ResultDisplay.tsx
new file mode 100644
index 0000000..0f58f0e
--- /dev/null
+++ b/src/components/ResultDisplay.tsx
@@ -0,0 +1,37 @@
+import { PanelSectionRow } from "@decky/ui";
+import { FC } from "react";
+import { STYLES } from "../utils/constants";
+import { ApiResponse } from "../types/index";
+
+export type OperationResult = ApiResponse;
+
+interface ResultDisplayProps {
+ result: OperationResult | null;
+}
+
+export const ResultDisplay: FC<ResultDisplayProps> = ({ result }) => {
+ if (!result) return null;
+
+ return (
+ <PanelSectionRow>
+ <div>
+ <strong>Status:</strong>{" "}
+ <span style={result.status === "success" ? STYLES.statusSuccess : STYLES.statusError}>
+ {result.status === "success" ? "Success" : "Error"}
+ </span>
+ <br />
+ {result.output ? (
+ <>
+ <strong>Output:</strong>
+ <pre style={STYLES.preWrap}>{result.output}</pre>
+ </>
+ ) : null}
+ {result.message ? (
+ <>
+ <strong>Error:</strong> {result.message}
+ </>
+ ) : null}
+ </div>
+ </PanelSectionRow>
+ );
+};
diff --git a/src/exports.ts b/src/exports.ts
new file mode 100644
index 0000000..a95f7b6
--- /dev/null
+++ b/src/exports.ts
@@ -0,0 +1,14 @@
+// Re-export components
+export { FGModInstallerSection } from './components/FGModInstallerSection';
+export { InstalledGamesSection } from './components/InstalledGamesSection';
+export { ResultDisplay } from './components/ResultDisplay';
+
+// Re-export utilities
+export * from './utils';
+export * from './utils/constants';
+
+// Re-export types
+export * from './types/index';
+
+// Re-export API
+export * from './api';
diff --git a/src/index.tsx b/src/index.tsx
index 64dabdb..12150d2 100755..100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,304 +1,7 @@
-import { useState, useEffect } from "react";
-import {
- PanelSection,
- PanelSectionRow,
- ButtonItem,
- DropdownItem,
- ConfirmModal,
- showModal
-} from "@decky/ui";
-import { definePlugin, callable } from "@decky/api";
+import { definePlugin } from "@decky/api";
import { RiAiGenerate } from "react-icons/ri";
-
-const runInstallFGMod = callable<
- [],
- { status: string; message?: string; output?: string }
->("run_install_fgmod");
-
-const runUninstallFGMod = callable<
- [],
- { status: string; message?: string; output?: string }
->("run_uninstall_fgmod");
-
-const checkFGModPath = callable<
- [],
- { exists: boolean }
->("check_fgmod_path");
-
-const listInstalledGames = callable<
- [],
- { status: string; games: { appid: string; name: string }[] }
->("list_installed_games");
-
-const logError = callable<[string], void>("log_error");
-
-function FGModInstallerSection() {
- const [installing, setInstalling] = useState(false);
- const [uninstalling, setUninstalling] = useState(false);
- const [installResult, setInstallResult] = useState<{
- status: string;
- output?: string;
- message?: string;
- } | null>(null);
- const [uninstallResult, setUninstallResult] = useState<{
- status: string;
- output?: string;
- message?: string;
- } | null>(null);
- const [pathExists, setPathExists] = useState<boolean | null>(null);
-
- useEffect(() => {
- const checkPath = async () => {
- try {
- const result = await checkFGModPath();
- setPathExists(result.exists);
- } catch (e) {
- logError('useEffect -> checkPath' + String(e));
- console.error(e);
- }
- };
- checkPath(); // Initial check
- const intervalId = setInterval(checkPath, 3000); // Check every 3 seconds
- return () => clearInterval(intervalId); // Cleanup interval on component unmount
- }, []);
-
- useEffect(() => {
- if (installResult) {
- const timer = setTimeout(() => {
- setInstallResult(null);
- }, 5000);
- return () => clearTimeout(timer);
- }
- return () => {}; // Ensure a cleanup function is always returned
- }, [installResult]);
-
- useEffect(() => {
- if (uninstallResult) {
- const timer = setTimeout(() => {
- setUninstallResult(null);
- }, 5000);
- return () => clearTimeout(timer);
- }
- return () => {};
- }, [uninstallResult]);
-
- const handleInstallClick = async () => {
- try {
- setInstalling(true);
- const result = await runInstallFGMod();
- setInstalling(false);
- setInstallResult(result);
- } catch (e) {
- logError('handleInstallClick: ' + String(e));
- console.error(e)
- }
- };
-
- const handleUninstallClick = async () => {
- try {
- setUninstalling(true);
- const result = await runUninstallFGMod();
- setUninstalling(false);
- setUninstallResult(result);
- } catch (e) {
- logError('handleUninstallClick' + String(e));
- console.error(e)
- }
- };
-
- return (
- <PanelSection>
- {pathExists !== null ? (
- <PanelSectionRow>
- <div style={{ color: pathExists ? "green" : "red" }}>
- {pathExists ? "OptiScaler Mod Is Installed" : "OptiScaler Mod Not Installed"}
- </div>
- </PanelSectionRow>
- ) : null}
- {pathExists === false ? (
- <PanelSectionRow>
- <ButtonItem layout="below" onClick={handleInstallClick} disabled={installing}>
- {installing ? "Installing..." : "Install OptiScaler FG Mod"}
- </ButtonItem>
- </PanelSectionRow>
- ) : null}
- {pathExists === true ? (
- <PanelSectionRow>
- <ButtonItem layout="below" onClick={handleUninstallClick} disabled={uninstalling}>
- {uninstalling ? "Uninstalling..." : "Uninstall OptiScaler FG Mod"}
- </ButtonItem>
- </PanelSectionRow>
- ) : null}
- {installResult ? (
- <PanelSectionRow>
- <div>
- <strong>Status:</strong>{" "}
- {installResult.status === "success" ? "Success" : "Error"}
- <br />
- {installResult.output ? (
- <>
- <strong>Output:</strong>
- <pre style={{ whiteSpace: "pre-wrap" }}>{installResult.output}</pre>
- </>
- ) : null}
- {installResult.message ? (
- <>
- <strong>Error:</strong> {installResult.message}
- </>
- ) : null}
- </div>
- </PanelSectionRow>
- ) : null}
- {uninstallResult ? (
- <PanelSectionRow>
- <div>
- <strong>Status:</strong>{" "}
- {uninstallResult.status === "success" ? "Success" : "Error"}
- <br />
- {uninstallResult.output ? (
- <>
- <strong>Output:</strong>
- <pre style={{ whiteSpace: "pre-wrap" }}>{uninstallResult.output}</pre>
- </>
- ) : null}
- {uninstallResult.message ? (
- <>
- <strong>Error:</strong> {uninstallResult.message}
- </>
- ) : null}
- </div>
- </PanelSectionRow>
- ) : null}
- <PanelSectionRow>
- <div>
- Install the OptiScaler-based mod above, then select and patch a game below to enable DLSS replacement with FSR Frame Generation. Map a button to "insert" key to bring up the OptiScaler menu in-game.
- </div>
- </PanelSectionRow>
- </PanelSection>
- );
-}
-
-function InstalledGamesSection() {
- const [games, setGames] = useState<{ appid: number; name: string }[]>([]);
- const [selectedGame, setSelectedGame] = useState<{ appid: number; name: string } | null>(null);
- const [result, setResult] = useState<string>('');
-
- useEffect(() => {
- const fetchGames = async () => {
- try {
- const response = await listInstalledGames();
- if (response.status === "success") {
- const sortedGames = [...response.games]
- .map(game => ({
- ...game,
- appid: parseInt(game.appid, 10),
- }))
- .sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
- setGames(sortedGames);
- } else {
- logError('fetchGames: ' + JSON.stringify(response));
- console.error('fetchGames: ' + JSON.stringify(response));
- }
- } catch (error) {
- logError("Error fetching games:" + String(error));
- console.error("Error fetching games:", String(error));
- }
- };
- fetchGames();
- }, []);
-
- const handlePatchClick = async () => {
- if (!selectedGame) return;
-
- // Show confirmation modal
- showModal(
- <ConfirmModal
- strTitle={`Patch ${selectedGame.name}?`}
- strDescription={
- "WARNING: Decky Framegen does not unpatch games when uninstalled. Be sure to unpatch the game or verify the integrity of your game files if you choose to uninstall the plugin or the game has issues."
- }
- strOKButtonText="Yeah man, I wanna do it"
- strCancelButtonText="Cancel"
- onOK={async () => {
- try {
- await SteamClient.Apps.SetAppLaunchOptions(selectedGame.appid, '~/fgmod/fgmod %COMMAND%');
- setResult(`Launch options set for ${selectedGame.name}. You can now select DLSS in the game's menu, and access OptiScaler with Insert key.`);
- } catch (error) {
- logError('handlePatchClick: ' + String(error));
- setResult(error instanceof Error ? `Error setting launch options: ${error.message}` : 'Error setting launch options');
- }
- }}
- />
- );
- };
-
- const handleUnpatchClick = async () => {
- if (!selectedGame) return;
-
- try {
- await SteamClient.Apps.SetAppLaunchOptions(selectedGame.appid, '~/fgmod/fgmod-uninstaller.sh %COMMAND%');
- setResult(`OptiScaler will uninstall on next launch of ${selectedGame.name}.`);
- } catch (error) {
- logError('handleUnpatchClick: ' + String(error));
- setResult(error instanceof Error ? `Error clearing launch options: ${error.message}` : 'Error clearing launch options');
- }
- };
-
- return (
- <PanelSection title="Select a game to patch:">
- <PanelSectionRow>
- <DropdownItem
- rgOptions={games.map(game => ({
- data: game.appid,
- label: game.name
- }))}
- selectedOption={selectedGame?.appid}
- onChange={(option) => {
- const game = games.find(g => g.appid === option.data);
- setSelectedGame(game || null);
- setResult('');
- }}
- strDefaultLabel="Select a game..."
- menuLabel="Installed Games"
- />
- </PanelSectionRow>
-
- {result ? (
- <PanelSectionRow>
- <div style={{
- padding: '12px',
- marginTop: '16px',
- backgroundColor: 'var(--decky-selected-ui-bg)',
- borderRadius: '4px'
- }}>
- {result}
- </div>
- </PanelSectionRow>
- ) : null}
-
- {selectedGame ? (
- <>
- <PanelSectionRow>
- <ButtonItem
- layout="below"
- onClick={handlePatchClick}
- >
- Patch
- </ButtonItem>
- </PanelSectionRow>
- <PanelSectionRow>
- <ButtonItem
- layout="below"
- onClick={handleUnpatchClick}
- >
- Unpatch
- </ButtonItem>
- </PanelSectionRow>
- </>
- ) : null}
- </PanelSection>
- );
-}
+import { FGModInstallerSection } from "./components/FGModInstallerSection";
+import { InstalledGamesSection } from "./components/InstalledGamesSection";
export default definePlugin(() => ({
name: "Framegen Plugin",
diff --git a/src/types/index.ts b/src/types/index.ts
new file mode 100644
index 0000000..c810754
--- /dev/null
+++ b/src/types/index.ts
@@ -0,0 +1,26 @@
+// Common types used throughout the application
+
+export interface ApiResponse {
+ status: string;
+ message?: string;
+ output?: string;
+}
+
+export interface GameInfo {
+ appid: string | number;
+ name: string;
+}
+
+export interface LaunchOptions {
+ command: string;
+ arguments?: string[];
+}
+
+export interface ModInstallationConfig {
+ files: string[];
+ paths: {
+ fgmod: string;
+ assets: string;
+ bin: string;
+ };
+}
diff --git a/src/utils/constants.ts b/src/utils/constants.ts
new file mode 100644
index 0000000..fe78dd0
--- /dev/null
+++ b/src/utils/constants.ts
@@ -0,0 +1,42 @@
+// Common types for the application
+
+export interface ResultType {
+ status: string;
+ message?: string;
+ output?: string;
+}
+
+export interface GameType {
+ appid: number;
+ name: string;
+}
+
+// Common style definitions
+export const STYLES = {
+ resultBox: {
+ padding: '12px',
+ marginTop: '16px',
+ backgroundColor: 'var(--decky-selected-ui-bg)',
+ borderRadius: '4px'
+ },
+ statusSuccess: { color: "green" },
+ statusError: { color: "red" },
+ preWrap: { whiteSpace: "pre-wrap" as const }
+};
+
+// Common timeout values
+export const TIMEOUTS = {
+ resultDisplay: 5000, // 5 seconds
+ pathCheck: 3000 // 3 seconds
+};
+
+// Message strings
+export const MESSAGES = {
+ modInstalled: "OptiScaler Mod Is Installed",
+ modNotInstalled: "OptiScaler Mod Not Installed",
+ installing: "Installing...",
+ installButton: "Install OptiScaler FG Mod",
+ uninstalling: "Uninstalling...",
+ uninstallButton: "Uninstall OptiScaler FG Mod",
+ instructionText: "Install the OptiScaler-based mod above, then select and patch a game below to enable DLSS replacement with FSR Frame Generation. Map a button to \"insert\" key to bring up the OptiScaler menu in-game."
+};
diff --git a/src/utils/index.ts b/src/utils/index.ts
new file mode 100644
index 0000000..d969cb6
--- /dev/null
+++ b/src/utils/index.ts
@@ -0,0 +1,30 @@
+import { logError } from "../api";
+
+/**
+ * Utility for creating a timer that automatically clears after specified timeout
+ * @param callback Function to call when timer completes
+ * @param timeout Timeout in milliseconds
+ * @returns Cleanup function that can be used in useEffect
+ */
+export const createAutoCleanupTimer = (callback: () => void, timeout: number): (() => void) => {
+ const timer = setTimeout(callback, timeout);
+ return () => clearTimeout(timer);
+};
+
+/**
+ * Safe wrapper for async operations to handle errors consistently
+ * @param operation Async operation to perform
+ * @param errorContext Context string for error logging
+ */
+export const safeAsyncOperation = async <T,>(
+ operation: () => Promise<T>,
+ errorContext: string
+): Promise<T | undefined> => {
+ try {
+ return await operation();
+ } catch (e) {
+ logError(`${errorContext}: ${String(e)}`);
+ console.error(e);
+ return undefined;
+ }
+};