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 --- 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 +++ 9 files changed, 401 insertions(+), 300 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 (limited to 'src') 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