From ca0d5f0ec1f4ba21f4bf51f0f773d2b6bad45c93 Mon Sep 17 00:00:00 2001 From: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> Date: Thu, 17 Jul 2025 08:49:12 -0400 Subject: reorganize for readability and DRY (#115) * reorganize for readability and DRY * rm backup files * ver bump --- main.py | 202 ++++++++++++--------- package.json | 2 +- src/api/index.ts | 23 +++ src/components/FGModInstallerSection.tsx | 103 +++++++++++ src/components/InstalledGamesSection.tsx | 123 +++++++++++++ src/components/ResultDisplay.tsx | 37 ++++ src/exports.ts | 14 ++ src/index.tsx | 303 +------------------------------ src/types/index.ts | 26 +++ src/utils/constants.ts | 42 +++++ src/utils/index.ts | 30 +++ 11 files changed, 515 insertions(+), 390 deletions(-) create mode 100644 src/api/index.ts create mode 100644 src/components/FGModInstallerSection.tsx create mode 100644 src/components/InstalledGamesSection.tsx create mode 100644 src/components/ResultDisplay.tsx create mode 100644 src/exports.ts mode change 100755 => 100644 src/index.tsx create mode 100644 src/types/index.ts create mode 100644 src/utils/constants.ts create mode 100644 src/utils/index.ts 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(null); + const [uninstallResult, setUninstallResult] = useState(null); + const [pathExists, setPathExists] = useState(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 ( + + {pathExists !== null ? ( + +
+ {pathExists ? MESSAGES.modInstalled : MESSAGES.modNotInstalled} +
+
+ ) : null} + + {pathExists === false ? ( + + + {installing ? MESSAGES.installing : MESSAGES.installButton} + + + ) : null} + + {pathExists === true ? ( + + + {uninstalling ? MESSAGES.uninstalling : MESSAGES.uninstallButton} + + + ) : null} + + + + + +
+ {MESSAGES.instructionText} +
+
+
+ ); +} 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([]); + const [selectedGame, setSelectedGame] = useState(null); + const [result, setResult] = useState(''); + + 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( + { + 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 ( + + + ({ + 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" + /> + + + {result ? ( + +
+ {result} +
+
+ ) : null} + + {selectedGame ? ( + <> + + + Patch + + + + + Unpatch + + + + ) : null} +
+ ); +} 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 = ({ result }) => { + if (!result) return null; + + return ( + +
+ Status:{" "} + + {result.status === "success" ? "Success" : "Error"} + +
+ {result.output ? ( + <> + Output: +
{result.output}
+ + ) : null} + {result.message ? ( + <> + Error: {result.message} + + ) : null} +
+
+ ); +}; 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 old mode 100755 new mode 100644 index 64dabdb..12150d2 --- 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(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 ( - - {pathExists !== null ? ( - -
- {pathExists ? "OptiScaler Mod Is Installed" : "OptiScaler Mod Not Installed"} -
-
- ) : null} - {pathExists === false ? ( - - - {installing ? "Installing..." : "Install OptiScaler FG Mod"} - - - ) : null} - {pathExists === true ? ( - - - {uninstalling ? "Uninstalling..." : "Uninstall OptiScaler FG Mod"} - - - ) : null} - {installResult ? ( - -
- Status:{" "} - {installResult.status === "success" ? "Success" : "Error"} -
- {installResult.output ? ( - <> - Output: -
{installResult.output}
- - ) : null} - {installResult.message ? ( - <> - Error: {installResult.message} - - ) : null} -
-
- ) : null} - {uninstallResult ? ( - -
- Status:{" "} - {uninstallResult.status === "success" ? "Success" : "Error"} -
- {uninstallResult.output ? ( - <> - Output: -
{uninstallResult.output}
- - ) : null} - {uninstallResult.message ? ( - <> - Error: {uninstallResult.message} - - ) : null} -
-
- ) : null} - -
- 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. -
-
-
- ); -} - -function InstalledGamesSection() { - const [games, setGames] = useState<{ appid: number; name: string }[]>([]); - const [selectedGame, setSelectedGame] = useState<{ appid: number; name: string } | null>(null); - const [result, setResult] = useState(''); - - 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( - { - 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 ( - - - ({ - 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" - /> - - - {result ? ( - -
- {result} -
-
- ) : null} - - {selectedGame ? ( - <> - - - Patch - - - - - Unpatch - - - - ) : null} -
- ); -} +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 ( + operation: () => Promise, + errorContext: string +): Promise => { + try { + return await operation(); + } catch (e) { + logError(`${errorContext}: ${String(e)}`); + console.error(e); + return undefined; + } +}; -- cgit v1.2.3