summaryrefslogtreecommitdiff
path: root/src/components
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 /src/components
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
Diffstat (limited to 'src/components')
-rw-r--r--src/components/FGModInstallerSection.tsx103
-rw-r--r--src/components/InstalledGamesSection.tsx123
-rw-r--r--src/components/ResultDisplay.tsx37
3 files changed, 263 insertions, 0 deletions
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>
+ );
+};