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/components/FGModInstallerSection.tsx | 103 ++++++++++++++++++++++++++ src/components/InstalledGamesSection.tsx | 123 +++++++++++++++++++++++++++++++ src/components/ResultDisplay.tsx | 37 ++++++++++ 3 files changed, 263 insertions(+) create mode 100644 src/components/FGModInstallerSection.tsx create mode 100644 src/components/InstalledGamesSection.tsx create mode 100644 src/components/ResultDisplay.tsx (limited to 'src/components') 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} +
+
+ ); +}; -- cgit v1.2.3