summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorKurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com>2025-09-17 15:27:15 -0400
committerGitHub <noreply@github.com>2025-09-17 15:27:15 -0400
commit295831a14b002604d2a1f207338c6034ab743c8e (patch)
treed7b2b78a89b50a95ffec63c6880ac1e39c8a5406 /src
parent21b076df45f542fdc02e8b5574abcd91e9d68f89 (diff)
parent26b8d1933821805fcef9275519884214fb1bc175 (diff)
downloaddecky-lsfg-vk-295831a14b002604d2a1f207338c6034ab743c8e.tar.gz
decky-lsfg-vk-295831a14b002604d2a1f207338c6034ab743c8e.zip
Merge pull request #172 from xXJSONDeruloXx/decky-fg-crossoverv0.10.9
Decky fg crossover
Diffstat (limited to 'src')
-rw-r--r--src/api/lsfgApi.ts8
-rw-r--r--src/components/ClipboardDisplay.tsx139
-rw-r--r--src/components/Content.tsx13
-rw-r--r--src/components/FgmodClipboardButton.tsx109
-rw-r--r--src/components/index.ts2
5 files changed, 270 insertions, 1 deletions
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";