summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--main.py16
-rw-r--r--package.json4
-rw-r--r--src/components/ClipboardCommands.tsx23
-rw-r--r--src/components/DocumentationButton.tsx41
-rw-r--r--src/components/FGModInstallerSection.tsx139
-rw-r--r--src/components/InstallationStatus.tsx28
-rw-r--r--src/components/InstructionCard.tsx23
-rw-r--r--src/components/OptiScalerControls.tsx92
-rw-r--r--src/components/OptiScalerHeader.tsx30
-rw-r--r--src/components/OptiScalerWiki.tsx28
-rw-r--r--src/components/README.md55
-rw-r--r--src/components/UninstallButton.tsx29
-rw-r--r--src/components/index.ts10
-rw-r--r--src/exports.ts2
-rw-r--r--src/index.tsx10
-rw-r--r--src/utils/constants.ts2
16 files changed, 339 insertions, 193 deletions
diff --git a/main.py b/main.py
index 44d02a4..4eb769d 100644
--- a/main.py
+++ b/main.py
@@ -94,12 +94,17 @@ class Plugin:
def _setup_flatpak_compatibility(self, fgmod_path):
"""Set up Flatpak compatibility if needed"""
try:
+ # Create a clean environment to avoid PyInstaller issues
+ clean_env = os.environ.copy()
+ clean_env["LD_LIBRARY_PATH"] = ""
+
# Check if Flatpak Steam is installed
flatpak_check = subprocess.run(
["flatpak", "list"],
capture_output=True,
text=True,
- check=False
+ check=False,
+ env=clean_env
)
if flatpak_check.returncode == 0 and "com.valvesoftware.Steam" in flatpak_check.stdout:
@@ -109,7 +114,7 @@ class Plugin:
"flatpak", "override", "--user",
f"--filesystem={fgmod_path}",
"com.valvesoftware.Steam"
- ], check=False)
+ ], check=False, env=clean_env)
decky.logger.info("Added Flatpak filesystem access")
return True
@@ -153,11 +158,16 @@ class Plugin:
str(optiscaler_archive)
]
+ # Create a clean environment to avoid PyInstaller issues
+ clean_env = os.environ.copy()
+ clean_env["LD_LIBRARY_PATH"] = ""
+
extract_result = subprocess.run(
extract_cmd,
capture_output=True,
text=True,
- check=False
+ check=False,
+ env=clean_env
)
if extract_result.returncode != 0:
diff --git a/package.json b/package.json
index 42ccb30..eae30d4 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "decky-framegen",
- "version": "0.11.6",
+ "version": "0.11.8",
"description": "plugin to install OptiScaler bleeding-edge and enable upscaling and framegen in a large variety of games.",
"type": "module",
"scripts": {
@@ -49,7 +49,7 @@
]
}
},
- "remote_binary_bundling" : true,
+ "remote_binary_bundling" : false,
"remote_binary":
[
{
diff --git a/src/components/ClipboardCommands.tsx b/src/components/ClipboardCommands.tsx
new file mode 100644
index 0000000..5a6f38f
--- /dev/null
+++ b/src/components/ClipboardCommands.tsx
@@ -0,0 +1,23 @@
+import { SmartClipboardButton } from "./SmartClipboardButton";
+
+interface ClipboardCommandsProps {
+ pathExists: boolean | null;
+}
+
+export function ClipboardCommands({ pathExists }: ClipboardCommandsProps) {
+ if (pathExists !== true) return null;
+
+ return (
+ <>
+ <SmartClipboardButton
+ command="~/fgmod/fgmod %command%"
+ buttonText="Copy Patch Command"
+ />
+
+ <SmartClipboardButton
+ command="~/fgmod/fgmod-uninstaller.sh %command%"
+ buttonText="Copy Unpatch Command"
+ />
+ </>
+ );
+}
diff --git a/src/components/DocumentationButton.tsx b/src/components/DocumentationButton.tsx
deleted file mode 100644
index 7069bc2..0000000
--- a/src/components/DocumentationButton.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-import { PanelSection, PanelSectionRow, ButtonItem } from "@decky/ui";
-import { FaClipboard, FaBook } from "react-icons/fa";
-
-export function DocumentationButton() {
- const handleDocClick = () => {
- window.open("https://github.com/xXJSONDeruloXx/Decky-Framegen/wiki", "_blank");
- };
-
- const handleOptiScalerClick = () => {
- window.open("https://github.com/optiscaler/OptiScaler/wiki", "_blank");
- };
-
- return (
- <PanelSection>
- {/*
- <PanelSectionRow>
- <ButtonItem
- layout="below"
- onClick={handleDocClick}
- >
- <div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
- <FaClipboard />
- <div>Copy Launch Command</div>
- </div>
- </ButtonItem>
- </PanelSectionRow>
- */}
- <PanelSectionRow>
- <ButtonItem
- layout="below"
- onClick={handleOptiScalerClick}
- >
- <div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
- <FaBook />
- <div>OptiScaler Wiki</div>
- </div>
- </ButtonItem>
- </PanelSectionRow>
- </PanelSection>
- );
-}
diff --git a/src/components/FGModInstallerSection.tsx b/src/components/FGModInstallerSection.tsx
deleted file mode 100644
index b82e749..0000000
--- a/src/components/FGModInstallerSection.tsx
+++ /dev/null
@@ -1,139 +0,0 @@
-import { useState, useEffect } from "react";
-import { PanelSection, PanelSectionRow, ButtonItem } from "@decky/ui";
-import { runInstallFGMod, runUninstallFGMod } from "../api";
-import { OperationResult } from "./ResultDisplay";
-import { SmartClipboardButton } from "./SmartClipboardButton";
-import { createAutoCleanupTimer } from "../utils";
-import { TIMEOUTS, MESSAGES, STYLES } from "../utils/constants";
-import optiScalerImage from "../../assets/optiscaler.png";
-
-interface FGModInstallerSectionProps {
- pathExists: boolean | null;
- setPathExists: (exists: boolean | null) => void;
-}
-
-export function FGModInstallerSection({ pathExists, setPathExists }: FGModInstallerSectionProps) {
- const [installing, setInstalling] = useState(false);
- const [uninstalling, setUninstalling] = useState(false);
- const [installResult, setInstallResult] = useState<OperationResult | null>(null);
- const [uninstallResult, setUninstallResult] = useState<OperationResult | null>(null);
-
- 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);
- if (result.status === "success") {
- setPathExists(true);
- }
- } catch (e) {
- console.error(e);
- } finally {
- setInstalling(false);
- }
- };
-
- const handleUninstallClick = async () => {
- try {
- setUninstalling(true);
- const result = await runUninstallFGMod();
- setUninstallResult(result);
- if (result.status === "success") {
- setPathExists(false);
- }
- } catch (e) {
- console.error(e);
- } finally {
- setUninstalling(false);
- }
- };
-
- return (
- <PanelSection>
- {pathExists === false ? (
- <PanelSectionRow>
- <div style={STYLES.statusNotInstalled}>
- {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}
-
- {pathExists === true ? (
- <PanelSectionRow>
- <div style={{
- display: 'flex',
- justifyContent: 'center',
- marginBottom: '16px'
- }}>
- <img
- src={optiScalerImage}
- alt="OptiScaler"
- style={{
- maxWidth: '100%',
- height: 'auto',
- borderRadius: '8px'
- }}
- />
- </div>
- </PanelSectionRow>
- ) : null}
-
- {pathExists === true ? (
- <PanelSectionRow>
- <div style={STYLES.instructionCard}>
- <div style={{ fontWeight: 'bold', marginBottom: '8px', color: 'var(--decky-accent-text)' }}>
- {MESSAGES.instructionTitle}
- </div>
- <div style={{ whiteSpace: 'pre-line' }}>
- {MESSAGES.instructionText}
- </div>
- </div>
- </PanelSectionRow>
- ) : null}
-
- {pathExists === true ? (
- <SmartClipboardButton
- command="~/fgmod/fgmod %command%"
- buttonText="Copy Patch Command"
- />
- ) : null}
-
- {pathExists === true ? (
- <SmartClipboardButton
- command="~/fgmod/fgmod-uninstaller.sh %command%"
- buttonText="Copy Unpatch Command"
- />
- ) : null}
- </PanelSection>
- );
-}
diff --git a/src/components/InstallationStatus.tsx b/src/components/InstallationStatus.tsx
new file mode 100644
index 0000000..713b5e1
--- /dev/null
+++ b/src/components/InstallationStatus.tsx
@@ -0,0 +1,28 @@
+import { PanelSectionRow, ButtonItem } from "@decky/ui";
+import { MESSAGES, STYLES } from "../utils/constants";
+
+interface InstallationStatusProps {
+ pathExists: boolean | null;
+ installing: boolean;
+ onInstallClick: () => void;
+}
+
+export function InstallationStatus({ pathExists, installing, onInstallClick }: InstallationStatusProps) {
+ if (pathExists !== false) return null;
+
+ return (
+ <>
+ <PanelSectionRow>
+ <div style={STYLES.statusNotInstalled}>
+ {MESSAGES.modNotInstalled}
+ </div>
+ </PanelSectionRow>
+
+ <PanelSectionRow>
+ <ButtonItem layout="below" onClick={onInstallClick} disabled={installing}>
+ {installing ? MESSAGES.installing : MESSAGES.installButton}
+ </ButtonItem>
+ </PanelSectionRow>
+ </>
+ );
+}
diff --git a/src/components/InstructionCard.tsx b/src/components/InstructionCard.tsx
new file mode 100644
index 0000000..fdf6755
--- /dev/null
+++ b/src/components/InstructionCard.tsx
@@ -0,0 +1,23 @@
+import { PanelSectionRow } from "@decky/ui";
+import { MESSAGES, STYLES } from "../utils/constants";
+
+interface InstructionCardProps {
+ pathExists: boolean | null;
+}
+
+export function InstructionCard({ pathExists }: InstructionCardProps) {
+ if (pathExists !== true) return null;
+
+ return (
+ <PanelSectionRow>
+ <div style={STYLES.instructionCard}>
+ <div style={{ fontWeight: 'bold', marginBottom: '8px', color: 'var(--decky-accent-text)' }}>
+ {MESSAGES.instructionTitle}
+ </div>
+ <div style={{ whiteSpace: 'pre-line' }}>
+ {MESSAGES.instructionText}
+ </div>
+ </div>
+ </PanelSectionRow>
+ );
+}
diff --git a/src/components/OptiScalerControls.tsx b/src/components/OptiScalerControls.tsx
new file mode 100644
index 0000000..7b2db0e
--- /dev/null
+++ b/src/components/OptiScalerControls.tsx
@@ -0,0 +1,92 @@
+import { useState, useEffect } from "react";
+import { PanelSection } from "@decky/ui";
+import { runInstallFGMod, runUninstallFGMod } from "../api";
+import { OperationResult } from "./ResultDisplay";
+import { createAutoCleanupTimer } from "../utils";
+import { TIMEOUTS } from "../utils/constants";
+import { InstallationStatus } from "./InstallationStatus";
+import { OptiScalerHeader } from "./OptiScalerHeader";
+import { ClipboardCommands } from "./ClipboardCommands";
+import { InstructionCard } from "./InstructionCard";
+import { OptiScalerWiki } from "./OptiScalerWiki";
+import { UninstallButton } from "./UninstallButton";
+
+interface OptiScalerControlsProps {
+ pathExists: boolean | null;
+ setPathExists: (exists: boolean | null) => void;
+}
+
+export function OptiScalerControls({ pathExists, setPathExists }: OptiScalerControlsProps) {
+ const [installing, setInstalling] = useState(false);
+ const [uninstalling, setUninstalling] = useState(false);
+ const [installResult, setInstallResult] = useState<OperationResult | null>(null);
+ const [uninstallResult, setUninstallResult] = useState<OperationResult | null>(null);
+
+ 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);
+ if (result.status === "success") {
+ setPathExists(true);
+ }
+ } catch (e) {
+ console.error(e);
+ } finally {
+ setInstalling(false);
+ }
+ };
+
+ const handleUninstallClick = async () => {
+ try {
+ setUninstalling(true);
+ const result = await runUninstallFGMod();
+ setUninstallResult(result);
+ if (result.status === "success") {
+ setPathExists(false);
+ }
+ } catch (e) {
+ console.error(e);
+ } finally {
+ setUninstalling(false);
+ }
+ };
+
+ return (
+ <PanelSection>
+ <InstallationStatus
+ pathExists={pathExists}
+ installing={installing}
+ onInstallClick={handleInstallClick}
+ />
+
+ <OptiScalerHeader pathExists={pathExists} />
+
+ <ClipboardCommands pathExists={pathExists} />
+
+ <InstructionCard pathExists={pathExists} />
+
+ <OptiScalerWiki pathExists={pathExists} />
+
+ <UninstallButton
+ pathExists={pathExists}
+ uninstalling={uninstalling}
+ onUninstallClick={handleUninstallClick}
+ />
+ </PanelSection>
+ );
+}
diff --git a/src/components/OptiScalerHeader.tsx b/src/components/OptiScalerHeader.tsx
new file mode 100644
index 0000000..27dc501
--- /dev/null
+++ b/src/components/OptiScalerHeader.tsx
@@ -0,0 +1,30 @@
+import { PanelSectionRow } from "@decky/ui";
+import optiScalerImage from "../../assets/optiscaler.png";
+
+interface OptiScalerHeaderProps {
+ pathExists: boolean | null;
+}
+
+export function OptiScalerHeader({ pathExists }: OptiScalerHeaderProps) {
+ if (pathExists !== true) return null;
+
+ return (
+ <PanelSectionRow>
+ <div style={{
+ display: 'flex',
+ justifyContent: 'center',
+ marginBottom: '16px'
+ }}>
+ <img
+ src={optiScalerImage}
+ alt="OptiScaler"
+ style={{
+ maxWidth: '100%',
+ height: 'auto',
+ borderRadius: '8px'
+ }}
+ />
+ </div>
+ </PanelSectionRow>
+ );
+}
diff --git a/src/components/OptiScalerWiki.tsx b/src/components/OptiScalerWiki.tsx
new file mode 100644
index 0000000..f8545f9
--- /dev/null
+++ b/src/components/OptiScalerWiki.tsx
@@ -0,0 +1,28 @@
+import { PanelSectionRow, ButtonItem } from "@decky/ui";
+import { FaBook } from "react-icons/fa";
+
+interface OptiScalerWikiProps {
+ pathExists: boolean | null;
+}
+
+export function OptiScalerWiki({ pathExists }: OptiScalerWikiProps) {
+ if (pathExists !== true) return null;
+
+ const handleWikiClick = () => {
+ window.open("https://github.com/optiscaler/OptiScaler/wiki", "_blank");
+ };
+
+ return (
+ <PanelSectionRow>
+ <ButtonItem
+ layout="below"
+ onClick={handleWikiClick}
+ >
+ <div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
+ <FaBook />
+ <div>OptiScaler Wiki</div>
+ </div>
+ </ButtonItem>
+ </PanelSectionRow>
+ );
+}
diff --git a/src/components/README.md b/src/components/README.md
new file mode 100644
index 0000000..19c50a7
--- /dev/null
+++ b/src/components/README.md
@@ -0,0 +1,55 @@
+# Components Structure
+
+This directory contains the organized component structure for the Decky Framegen plugin.
+
+## Component Hierarchy
+
+### Main Orchestrator
+- **`OptiScalerControls.tsx`** - Main component that orchestrates all OptiScaler functionality and state management
+
+### Sub-components (Logical Sections)
+- **`InstallationStatus.tsx`** - Shows installation status and setup button when mod is not installed
+- **`OptiScalerHeader.tsx`** - Displays the OptiScaler logo/image when installed
+- **`ClipboardCommands.tsx`** - Contains the patch/unpatch command copy buttons
+- **`InstructionCard.tsx`** - Shows usage instructions and help text
+- **`OptiScalerWiki.tsx`** - Wiki documentation button
+- **`UninstallButton.tsx`** - Red remove/uninstall button
+
+### Utility Components
+- **`SmartClipboardButton.tsx`** - Reusable clipboard copy button component
+- **`ResultDisplay.tsx`** - Display component for operation results
+
+### Other Components
+- **`InstalledGamesSection.tsx`** - Games section component (currently commented out)
+
+## State Management
+
+All state is managed in the main `OptiScalerControls` component:
+- `pathExists` - Whether the mod is installed
+- `installing` - Installation progress state
+- `uninstalling` - Uninstallation progress state
+- `installResult` - Result of installation operation
+- `uninstallResult` - Result of uninstallation operation
+
+## Component Flow
+
+1. **Not Installed State**: Shows `InstallationStatus` with setup button
+2. **Installed State**: Shows all components in order:
+ - OptiScaler header image
+ - Clipboard command buttons
+ - Instruction card
+ - Wiki button
+ - Uninstall button (red)
+
+## Benefits of This Structure
+
+- **Separation of Concerns**: Each component has a single responsibility
+- **Reusability**: Components can be easily reused or rearranged
+- **Maintainability**: Easier to find and modify specific functionality
+- **Testability**: Smaller components are easier to test
+- **State Management**: Centralized state in the orchestrator component
+- **Clean Imports**: Barrel exports through `index.ts` for cleaner imports
+
+## Clean Architecture
+
+All legacy components have been removed and replaced with this organized structure. The codebase is now clean and follows modern React patterns.
diff --git a/src/components/UninstallButton.tsx b/src/components/UninstallButton.tsx
new file mode 100644
index 0000000..1f55548
--- /dev/null
+++ b/src/components/UninstallButton.tsx
@@ -0,0 +1,29 @@
+import { PanelSectionRow, ButtonItem } from "@decky/ui";
+import { MESSAGES } from "../utils/constants";
+
+interface UninstallButtonProps {
+ pathExists: boolean | null;
+ uninstalling: boolean;
+ onUninstallClick: () => void;
+}
+
+export function UninstallButton({ pathExists, uninstalling, onUninstallClick }: UninstallButtonProps) {
+ if (pathExists !== true) return null;
+
+ return (
+ <PanelSectionRow>
+ <ButtonItem
+ layout="below"
+ onClick={onUninstallClick}
+ disabled={uninstalling}
+ >
+ <div style={{
+ color: '#ef4444',
+ fontWeight: 'bold'
+ }}>
+ {uninstalling ? MESSAGES.uninstalling : MESSAGES.uninstallButton}
+ </div>
+ </ButtonItem>
+ </PanelSectionRow>
+ );
+}
diff --git a/src/components/index.ts b/src/components/index.ts
new file mode 100644
index 0000000..3f67341
--- /dev/null
+++ b/src/components/index.ts
@@ -0,0 +1,10 @@
+// Component exports for cleaner imports
+export { OptiScalerControls } from './OptiScalerControls';
+export { InstallationStatus } from './InstallationStatus';
+export { OptiScalerHeader } from './OptiScalerHeader';
+export { ClipboardCommands } from './ClipboardCommands';
+export { InstructionCard } from './InstructionCard';
+export { OptiScalerWiki } from './OptiScalerWiki';
+export { UninstallButton } from './UninstallButton';
+export { SmartClipboardButton } from './SmartClipboardButton';
+export { ResultDisplay } from './ResultDisplay';
diff --git a/src/exports.ts b/src/exports.ts
index a95f7b6..56c2292 100644
--- a/src/exports.ts
+++ b/src/exports.ts
@@ -1,5 +1,5 @@
// Re-export components
-export { FGModInstallerSection } from './components/FGModInstallerSection';
+export { OptiScalerControls } from './components';
export { InstalledGamesSection } from './components/InstalledGamesSection';
export { ResultDisplay } from './components/ResultDisplay';
diff --git a/src/index.tsx b/src/index.tsx
index 5cf59e4..0ea93e0 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,9 +1,8 @@
import { definePlugin } from "@decky/api";
import { MdOutlineAutoAwesomeMotion } from "react-icons/md";
import { useState, useEffect } from "react";
-import { FGModInstallerSection } from "./components/FGModInstallerSection";
+import { OptiScalerControls } from "./components";
// import { InstalledGamesSection } from "./components/InstalledGamesSection";
-import { DocumentationButton } from "./components/DocumentationButton";
import { checkFGModPath } from "./api";
import { safeAsyncOperation } from "./utils";
import { TIMEOUTS } from "./utils/constants";
@@ -27,11 +26,10 @@ function MainContent() {
return (
<>
- <FGModInstallerSection pathExists={pathExists} setPathExists={setPathExists} />
+ <OptiScalerControls pathExists={pathExists} setPathExists={setPathExists} />
{pathExists === true ? (
<>
{/* <InstalledGamesSection /> */}
- <DocumentationButton />
</>
) : null}
</>
@@ -39,12 +37,12 @@ function MainContent() {
}
export default definePlugin(() => ({
- name: "Framegen Plugin",
+ name: "Decky Framegen",
titleView: <div>Decky Framegen</div>,
alwaysRender: true,
content: <MainContent />,
icon: <MdOutlineAutoAwesomeMotion />,
onDismount() {
- console.log("Framegen Plugin unmounted");
+ console.log("Decky Framegen Plugin unmounted");
},
}));
diff --git a/src/utils/constants.ts b/src/utils/constants.ts
index c406e5b..1f583c0 100644
--- a/src/utils/constants.ts
+++ b/src/utils/constants.ts
@@ -62,5 +62,5 @@ export const MESSAGES = {
installSuccess: "✅ OptiScaler mod setup successfully!",
uninstallSuccess: "✅ OptiScaler mod removed successfully.",
instructionTitle: "How to Use:",
- instructionText: "Click 'Copy Patch Command' or 'Copy Unpatch Command', then go to your game's properties, and paste the command into the Launch Options field.\n\nIn-game: Enable DLSS in graphics settings, or assign a back button to keyboard's 'Insert' key for extended OptiScaler options."
+ instructionText: "Click 'Copy Patch Command' or 'Copy Unpatch Command', then go to your game's properties, and paste the command into the Launch Options field.\n\nIn-game: Enable DLSS in graphics settings to unlock FSR 3.1/XeSS 2.0 in DirectX12 Games.\n\nFor extended OptiScaler options, assign a back button to a keyboard's 'Insert' key."
};