diff options
| author | Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> | 2025-09-17 15:27:15 -0400 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-09-17 15:27:15 -0400 |
| commit | 295831a14b002604d2a1f207338c6034ab743c8e (patch) | |
| tree | d7b2b78a89b50a95ffec63c6880ac1e39c8a5406 | |
| parent | 21b076df45f542fdc02e8b5574abcd91e9d68f89 (diff) | |
| parent | 26b8d1933821805fcef9275519884214fb1bc175 (diff) | |
| download | decky-lsfg-vk-295831a14b002604d2a1f207338c6034ab743c8e.tar.gz decky-lsfg-vk-295831a14b002604d2a1f207338c6034ab743c8e.zip | |
Merge pull request #172 from xXJSONDeruloXx/decky-fg-crossoverv0.10.9
Decky fg crossover
| -rw-r--r-- | package.json | 2 | ||||
| -rw-r--r-- | py_modules/lsfg_vk/plugin.py | 58 | ||||
| -rw-r--r-- | src/api/lsfgApi.ts | 8 | ||||
| -rw-r--r-- | src/components/ClipboardDisplay.tsx | 139 | ||||
| -rw-r--r-- | src/components/Content.tsx | 13 | ||||
| -rw-r--r-- | src/components/FgmodClipboardButton.tsx | 109 | ||||
| -rw-r--r-- | src/components/index.ts | 2 |
7 files changed, 316 insertions, 15 deletions
diff --git a/package.json b/package.json index 8714f2f..22e80d5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "decky-lsfg-vk", - "version": "0.10.7", + "version": "0.10.8", "description": "Use Lossless Scaling on the Steam Deck using the lsfg-vk vulkan layer", "type": "module", "scripts": { diff --git a/py_modules/lsfg_vk/plugin.py b/py_modules/lsfg_vk/plugin.py index c1ffdf6..f08aa18 100644 --- a/py_modules/lsfg_vk/plugin.py +++ b/py_modules/lsfg_vk/plugin.py @@ -553,36 +553,68 @@ class Plugin: } async def get_launch_script_content(self) -> Dict[str, Any]: - """Get the current launch script content + """Get the content of the launch script file Returns: - Dict containing the launch script content or error message + FileContentResponse dict with file content or error information """ try: - script_path = self.installation_service.lsfg_script_path - if not script_path.exists(): + script_path = self.installation_service.get_launch_script_path() + + if not os.path.exists(script_path): return { "success": False, - "content": None, - "path": str(script_path), - "error": "Launch script does not exist" + "error": f"Launch script not found at {script_path}", + "path": str(script_path) } - content = script_path.read_text(encoding='utf-8') + with open(script_path, 'r') as file: + content = file.read() + return { "success": True, "content": content, - "path": str(script_path), - "error": None + "path": str(script_path) } + except Exception as e: + import decky + decky.logger.error(f"Error reading launch script: {e}") return { "success": False, - "content": None, - "path": str(script_path) if 'script_path' in locals() else "unknown", - "error": f"Error reading launch script: {str(e)}" + "error": str(e) } + async def check_fgmod_directory(self) -> Dict[str, Any]: + """Check if the fgmod directory exists in the home directory + + Returns: + Dict with exists status and directory path + """ + try: + import decky + home_path = Path(decky.DECKY_USER_HOME) + fgmod_path = home_path / "fgmod" + + exists = fgmod_path.exists() and fgmod_path.is_dir() + + return { + "success": True, + "exists": exists, + "path": str(fgmod_path) + } + + except Exception as e: + import decky + decky.logger.error(f"Error checking fgmod directory: {e}") + return { + "success": False, + "exists": False, + "error": str(e) + } + + # Decky Loader lifecycle methods + # Lifecycle methods async def _main(self): """ diff --git a/src/api/lsfgApi.ts b/src/api/lsfgApi.ts index 6c535af..8db0c82 100644 --- a/src/api/lsfgApi.ts +++ b/src/api/lsfgApi.ts @@ -89,6 +89,13 @@ export interface FileContentResult { error?: string; } +export interface FgmodCheckResult { + success: boolean; + exists: boolean; + path?: string; + error?: string; +} + // Profile management interfaces export interface ProfilesResult { success: boolean; @@ -116,6 +123,7 @@ export const getConfigSchema = callable<[], ConfigSchemaResult>("get_config_sche export const getLaunchOption = callable<[], LaunchOptionResult>("get_launch_option"); export const getConfigFileContent = callable<[], FileContentResult>("get_config_file_content"); export const getLaunchScriptContent = callable<[], FileContentResult>("get_launch_script_content"); +export const checkFgmodDirectory = callable<[], FgmodCheckResult>("check_fgmod_directory"); // Updated config function using object-based configuration (single source of truth) export const updateLsfgConfig = callable< diff --git a/src/components/ClipboardDisplay.tsx b/src/components/ClipboardDisplay.tsx new file mode 100644 index 0000000..852a50f --- /dev/null +++ b/src/components/ClipboardDisplay.tsx @@ -0,0 +1,139 @@ +import { useState, useEffect } from "react"; +import { PanelSectionRow } from "@decky/ui"; +import { FaClipboard, FaEye } from "react-icons/fa"; + +export function ClipboardDisplay() { + const [clipboardContent, setClipboardContent] = useState<string>(""); + const [isReading, setIsReading] = useState(false); + + const readClipboard = async () => { + if (isReading) return; // Prevent concurrent reads + + setIsReading(true); + try { + console.log("ClipboardDisplay: Attempting to read clipboard..."); + + if (!navigator.clipboard) { + console.log("ClipboardDisplay: navigator.clipboard not available"); + setClipboardContent("Clipboard API not available"); + return; + } + + if (!navigator.clipboard.readText) { + console.log("ClipboardDisplay: navigator.clipboard.readText not available"); + setClipboardContent("Clipboard read not supported"); + return; + } + + console.log("ClipboardDisplay: Calling navigator.clipboard.readText()..."); + const content = await navigator.clipboard.readText(); + console.log("ClipboardDisplay: Successfully read clipboard:", content.length, "characters"); + setClipboardContent(content); + } catch (error) { + // This is expected if user hasn't granted clipboard permissions + // or if we're in a context where reading isn't allowed + console.log("ClipboardDisplay: Error reading clipboard:", error); + console.log("ClipboardDisplay: Error name:", (error as Error).name); + console.log("ClipboardDisplay: Error message:", (error as Error).message); + + // More specific error messages based on error type + if (error instanceof DOMException) { + switch (error.name) { + case 'NotAllowedError': + setClipboardContent("Clipboard access denied - check permissions"); + break; + case 'NotFoundError': + setClipboardContent("No clipboard data found"); + break; + case 'SecurityError': + setClipboardContent("Clipboard access blocked by security policy"); + break; + default: + setClipboardContent(`Clipboard error: ${error.name}`); + } + } else { + setClipboardContent("Unable to read clipboard"); + } + } finally { + setIsReading(false); + } + }; + + // Read clipboard on mount and then every 3 seconds + useEffect(() => { + readClipboard(); + + const interval = setInterval(() => { + readClipboard(); + }, 3000); + + return () => clearInterval(interval); + }, []); + + const truncateText = (text: string, maxLength: number = 60) => { + if (text.length <= maxLength) return text; + return text.substring(0, maxLength) + "..."; + }; + + const displayText = truncateText(clipboardContent); + + return ( + <PanelSectionRow> + <div style={{ + padding: "8px", + backgroundColor: "rgba(255, 255, 255, 0.05)", + borderRadius: "4px", + border: "1px solid rgba(255, 255, 255, 0.1)", + marginBottom: "8px" + }}> + <div style={{ + display: "flex", + alignItems: "center", + gap: "8px", + marginBottom: "4px" + }}> + <FaClipboard style={{ color: "#888", fontSize: "12px" }} /> + <div style={{ + fontSize: "12px", + fontWeight: "bold", + color: "#888", + textTransform: "uppercase" + }}> + Current Clipboard + </div> + {isReading && ( + <FaEye style={{ + color: "#888", + fontSize: "10px", + animation: "pulse 1s ease-in-out infinite" + }} /> + )} + </div> + <div style={{ + fontSize: "11px", + color: clipboardContent.includes("error") || + clipboardContent.includes("denied") || + clipboardContent.includes("not available") || + clipboardContent.includes("not supported") || + clipboardContent.includes("blocked") || + clipboardContent === "Unable to read clipboard" + ? "#ff6b6b" + : "#fff", + fontFamily: "monospace", + wordBreak: "break-word", + lineHeight: "1.3", + minHeight: "14px" + }}> + {displayText || "Reading clipboard..."} + </div> + </div> + <style>{` + @keyframes pulse { + 0% { opacity: 0.5; } + 50% { opacity: 1; } + 100% { opacity: 0.5; } + } + `}</style> + </PanelSectionRow> + ); +} diff --git a/src/components/Content.tsx b/src/components/Content.tsx index e0adf3f..e82a7c3 100644 --- a/src/components/Content.tsx +++ b/src/components/Content.tsx @@ -11,6 +11,8 @@ import { UsageInstructions } from "./UsageInstructions"; import { WikiButton } from "./WikiButton"; import { ClipboardButton } from "./ClipboardButton"; import { SmartClipboardButton } from "./SmartClipboardButton"; +import { FgmodClipboardButton } from "./FgmodClipboardButton"; +import { ClipboardDisplay } from "./ClipboardDisplay"; import { PluginUpdateChecker } from "./PluginUpdateChecker"; import { NerdStuffModal } from "./NerdStuffModal"; import { ConfigurationData } from "../config/configSchema"; @@ -96,7 +98,14 @@ export function Content() { </> )} - <SmartClipboardButton /> + {/* Clipboard buttons - only show if installed */} + {isInstalled && ( + <> + <ClipboardDisplay /> + <SmartClipboardButton /> + <FgmodClipboardButton /> + </> + )} {/* Profile Management - only show if installed */} {isInstalled && ( @@ -117,8 +126,10 @@ export function Content() { /> )} + {/* Usage instructions - always visible for user guidance */} <UsageInstructions config={config} /> + {/* Wiki and clipboard buttons - always available for documentation */} <WikiButton /> <ClipboardButton /> diff --git a/src/components/FgmodClipboardButton.tsx b/src/components/FgmodClipboardButton.tsx new file mode 100644 index 0000000..6f65955 --- /dev/null +++ b/src/components/FgmodClipboardButton.tsx @@ -0,0 +1,109 @@ +import { useState, useEffect } from "react"; +import { PanelSectionRow, ButtonItem } from "@decky/ui"; +import { FaClipboard, FaCheck } from "react-icons/fa"; +import { checkFgmodDirectory } from "../api/lsfgApi"; +import { showClipboardErrorToast } from "../utils/toastUtils"; +import { copyWithVerification } from "../utils/clipboardUtils"; + +export function FgmodClipboardButton() { + const [isLoading, setIsLoading] = useState(false); + const [showSuccess, setShowSuccess] = useState(false); + const [fgmodExists, setFgmodExists] = useState(false); + const [checkingFgmod, setCheckingFgmod] = useState(true); + + // Check for fgmod directory on component mount + useEffect(() => { + const checkFgmod = async () => { + try { + const result = await checkFgmodDirectory(); + setFgmodExists(result.exists); + } catch (error) { + console.error("Error checking fgmod directory:", error); + setFgmodExists(false); + } finally { + setCheckingFgmod(false); + } + }; + + checkFgmod(); + }, []); + + // Reset success state after 3 seconds + useEffect(() => { + if (showSuccess) { + const timer = setTimeout(() => { + setShowSuccess(false); + }, 3000); + return () => clearTimeout(timer); + } + return undefined; + }, [showSuccess]); + + const copyToClipboard = async () => { + if (isLoading || showSuccess) return; + + setIsLoading(true); + try { + const text = "~/fgmod/fgmod ~/lsfg %command%"; + const { success, verified } = await copyWithVerification(text); + + if (success) { + // Show success feedback in the button instead of toast + setShowSuccess(true); + if (!verified) { + // Copy worked but verification failed - still show success + console.log('Copy verification failed but copy likely worked'); + } + } else { + showClipboardErrorToast(); + } + } catch (error) { + showClipboardErrorToast(); + } finally { + setIsLoading(false); + } + }; + + // Don't render if fgmod directory doesn't exist or we're still checking + if (checkingFgmod || !fgmodExists) { + return null; + } + + return ( + <PanelSectionRow> + <ButtonItem + layout="below" + onClick={copyToClipboard} + disabled={isLoading || showSuccess} + > + <div style={{ display: "flex", alignItems: "center", gap: "8px" }}> + {showSuccess ? ( + <FaCheck style={{ + color: "#4CAF50" // Green color for success + }} /> + ) : isLoading ? ( + <FaClipboard style={{ + animation: "pulse 1s ease-in-out infinite", + opacity: 0.7 + }} /> + ) : ( + <FaClipboard /> + )} + <div style={{ + color: showSuccess ? "#4CAF50" : "inherit", + fontWeight: showSuccess ? "bold" : "normal" + }}> + {showSuccess ? "Copied to clipboard" : isLoading ? "Copying..." : "LSFG + DeckyFG"} + </div> + </div> + </ButtonItem> + <style>{` + @keyframes pulse { + 0% { opacity: 0.7; } + 50% { opacity: 1; } + 100% { opacity: 0.7; } + } + `}</style> + </PanelSectionRow> + ); +} diff --git a/src/components/index.ts b/src/components/index.ts index bf60423..c3ace84 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -6,6 +6,8 @@ export { FpsMultiplierControl } from "./FpsMultiplierControl"; export { UsageInstructions } from "./UsageInstructions"; export { WikiButton } from "./WikiButton"; export { SmartClipboardButton } from "./SmartClipboardButton"; +export { FgmodClipboardButton } from "./FgmodClipboardButton"; +export { ClipboardDisplay } from "./ClipboardDisplay"; export { PluginUpdateChecker } from "./PluginUpdateChecker"; export { NerdStuffModal } from "./NerdStuffModal"; export { ProfileManagement } from "./ProfileManagement"; |
