diff options
| author | xXJSONDeruloXx <danielhimebauch@gmail.com> | 2025-07-21 12:28:36 -0400 |
|---|---|---|
| committer | xXJSONDeruloXx <danielhimebauch@gmail.com> | 2025-07-21 12:28:36 -0400 |
| commit | 0668428a5ebc221d39b907f251dc0dc43e30a2df (patch) | |
| tree | b60414cb79057b3e1f38c7e114ab9ec0f6c3d61d /src | |
| parent | a7fb5ee69c8d74534f2994263558ddcd9c8c0d41 (diff) | |
| download | decky-lsfg-vk-0668428a5ebc221d39b907f251dc0dc43e30a2df.tar.gz decky-lsfg-vk-0668428a5ebc221d39b907f251dc0dc43e30a2df.zip | |
testing alt keyboard copy methods
Diffstat (limited to 'src')
| -rw-r--r-- | src/api/lsfgApi.ts | 10 | ||||
| -rw-r--r-- | src/components/ClipboardExperiments.tsx | 509 | ||||
| -rw-r--r-- | src/components/Content.tsx | 6 | ||||
| -rw-r--r-- | src/components/SmartClipboardButton.tsx | 245 | ||||
| -rw-r--r-- | src/components/index.ts | 2 |
5 files changed, 772 insertions, 0 deletions
diff --git a/src/api/lsfgApi.ts b/src/api/lsfgApi.ts index 74caa57..b984612 100644 --- a/src/api/lsfgApi.ts +++ b/src/api/lsfgApi.ts @@ -36,6 +36,13 @@ export interface DllStatsResult { error?: string; } +export interface ClipboardResult { + success: boolean; + method?: string; + message?: string; + error?: string; +} + // Use centralized configuration data type export type LsfgConfig = ConfigurationData; @@ -99,6 +106,9 @@ export const getLaunchOption = callable<[], LaunchOptionResult>("get_launch_opti export const getConfigFileContent = callable<[], FileContentResult>("get_config_file_content"); export const getLaunchScriptContent = callable<[], FileContentResult>("get_launch_script_content"); +// Clipboard API +export const copyToSystemClipboard = callable<[string], ClipboardResult>("copy_to_system_clipboard"); + // Updated config function using centralized configuration export const updateLsfgConfig = callable< [string, number, number, boolean, boolean, string, number, boolean, boolean], diff --git a/src/components/ClipboardExperiments.tsx b/src/components/ClipboardExperiments.tsx new file mode 100644 index 0000000..ec7d9ab --- /dev/null +++ b/src/components/ClipboardExperiments.tsx @@ -0,0 +1,509 @@ +import { useState } from "react"; +import { PanelSectionRow, ButtonItem, Field, Focusable } from "@decky/ui"; +import { FaClipboard, FaFlask, FaRocket, FaCog, FaTerminal } from "react-icons/fa"; +import { toaster } from "@decky/api"; +import { getLaunchOption, copyToSystemClipboard } from "../api/lsfgApi"; + +interface ExperimentResult { + success: boolean; + method: string; + error?: string; + details?: string; +} + +export function ClipboardExperiments() { + const [results, setResults] = useState<ExperimentResult[]>([]); + const [isLoading, setIsLoading] = useState<string | null>(null); + + const addResult = (result: ExperimentResult) => { + setResults(prev => [...prev, { ...result, timestamp: Date.now() }]); + }; + + const getLaunchOptionText = async (): Promise<string> => { + try { + const result = await getLaunchOption(); + return result.launch_option || "~/lsfg %command%"; + } catch (error) { + return "~/lsfg %command%"; + } + }; + + // Approach 1: Direct Navigator Clipboard API + const testDirectClipboard = async () => { + setIsLoading("direct"); + try { + const text = await getLaunchOptionText(); + await navigator.clipboard.writeText(text); + + // Test if it actually worked by reading back + const readBack = await navigator.clipboard.readText(); + const success = readBack === text; + + addResult({ + success, + method: "Direct Navigator Clipboard", + details: success ? `Successfully copied: "${text}"` : `Mismatch: wrote "${text}", read "${readBack}"` + }); + + if (success) { + toaster.toast({ + title: "Clipboard Success!", + body: "Direct navigator.clipboard API worked" + }); + } + } catch (error) { + addResult({ + success: false, + method: "Direct Navigator Clipboard", + error: String(error) + }); + } finally { + setIsLoading(null); + } + }; + + // Approach 2: CEF Browser with Data URL + const testDataUrlApproach = async () => { + setIsLoading("dataurl"); + try { + const text = await getLaunchOptionText(); + const htmlContent = ` + <!DOCTYPE html> + <html> + <head> + <meta charset="utf-8"> + <title>Clipboard Helper</title> + <style> + body { + font-family: 'Motiva Sans', Arial, sans-serif; + background: #1e2328; + color: white; + padding: 20px; + text-align: center; + } + .content { background: #2a475e; padding: 20px; border-radius: 8px; margin: 20px; } + .success { color: #66bb6a; font-weight: bold; } + .error { color: #f44336; font-weight: bold; } + code { background: rgba(255,255,255,0.1); padding: 4px 8px; border-radius: 4px; } + </style> + </head> + <body> + <div class="content"> + <h2>๐ Clipboard Automation Test</h2> + <p>Attempting to copy launch option: <code>${text}</code></p> + <div id="status">Working...</div> + <div id="details"></div> + <button onclick="window.close()" style="margin-top: 20px; padding: 8px 16px;">Close</button> + </div> + <script> + (async function() { + const statusEl = document.getElementById('status'); + const detailsEl = document.getElementById('details'); + const textToCopy = ${JSON.stringify(text)}; + + try { + await navigator.clipboard.writeText(textToCopy); + + // Verify it worked + const readBack = await navigator.clipboard.readText(); + if (readBack === textToCopy) { + statusEl.innerHTML = '<span class="success">โ
Success! Text copied to clipboard</span>'; + detailsEl.innerHTML = 'The launch option is now in your clipboard. You can close this window.'; + } else { + statusEl.innerHTML = '<span class="error">โ ๏ธ Partial Success</span>'; + detailsEl.innerHTML = 'Text was written but verification failed. Check clipboard manually.'; + } + } catch (error) { + statusEl.innerHTML = '<span class="error">โ Failed</span>'; + detailsEl.innerHTML = 'Error: ' + error.message; + } + })(); + </script> + </body> + </html> + `; + + const dataUrl = 'data:text/html;charset=utf-8,' + encodeURIComponent(htmlContent); + window.open(dataUrl, '_blank'); + + addResult({ + success: true, + method: "Data URL Browser Window", + details: "Opened data URL with auto-copy script" + }); + } catch (error) { + addResult({ + success: false, + method: "Data URL Browser Window", + error: String(error) + }); + } finally { + setIsLoading(null); + } + }; + + // Approach 3: Focused Element + Selection + Input API + const testInputSimulation = async () => { + setIsLoading("input"); + try { + const text = await getLaunchOptionText(); + + // Create a temporary input element + const tempInput = document.createElement('input'); + tempInput.value = text; + tempInput.style.position = 'absolute'; + tempInput.style.left = '-9999px'; + document.body.appendChild(tempInput); + + // Focus and select the text + tempInput.focus(); + tempInput.select(); + + // Try different copy methods + let copySuccess = false; + let method = ''; + + // Method 1: execCommand (deprecated but might work) + try { + if (document.execCommand('copy')) { + copySuccess = true; + method = 'execCommand'; + } + } catch (e) {} + + // Method 2: Navigator clipboard on selected text + if (!copySuccess) { + try { + await navigator.clipboard.writeText(text); + copySuccess = true; + method = 'navigator.clipboard'; + } catch (e) {} + } + + // Clean up + document.body.removeChild(tempInput); + + if (copySuccess) { + // Verify + try { + const readBack = await navigator.clipboard.readText(); + const verified = readBack === text; + addResult({ + success: verified, + method: `Input Simulation (${method})`, + details: verified ? "Successfully copied and verified" : "Copy worked but verification failed" + }); + } catch (e) { + addResult({ + success: true, + method: `Input Simulation (${method})`, + details: "Copy appeared to work but couldn't verify" + }); + } + } else { + addResult({ + success: false, + method: "Input Simulation", + error: "All copy methods failed" + }); + } + } catch (error) { + addResult({ + success: false, + method: "Input Simulation", + error: String(error) + }); + } finally { + setIsLoading(null); + } + }; + + // Approach 4: Backend Clipboard + const testBackendClipboard = async () => { + setIsLoading("backend"); + try { + const text = await getLaunchOptionText(); + + const result = await copyToSystemClipboard(text); + + if (result.success) { + addResult({ + success: true, + method: `Backend System Clipboard (${result.method})`, + details: result.message || "Successfully copied to system clipboard" + }); + + toaster.toast({ + title: "Clipboard Success!", + body: `Copied using ${result.method}` + }); + } else { + addResult({ + success: false, + method: "Backend System Clipboard", + error: result.error || "Unknown error" + }); + } + } catch (error) { + addResult({ + success: false, + method: "Backend System Clipboard", + error: String(error) + }); + } finally { + setIsLoading(null); + } + }; + + // Approach 5: Hybrid approach with immediate feedback + const testHybridApproach = async () => { + setIsLoading("hybrid"); + try { + const text = await getLaunchOptionText(); + + // Try direct first + let directWorked = false; + try { + await navigator.clipboard.writeText(text); + const readBack = await navigator.clipboard.readText(); + directWorked = readBack === text; + } catch (e) {} + + if (directWorked) { + addResult({ + success: true, + method: "Hybrid (Direct Success)", + details: "Direct clipboard API worked, no browser needed" + }); + + toaster.toast({ + title: "Clipboard Success!", + body: "Launch option copied to clipboard" + }); + } else { + // Fall back to optimized browser approach + const htmlContent = ` + <!DOCTYPE html> + <html> + <head> + <meta charset="utf-8"> + <title>Quick Copy</title> + <style> + body { font-family: system-ui; background: #1a1a1a; color: white; padding: 20px; } + .container { max-width: 400px; margin: 0 auto; text-align: center; } + .success { color: #4CAF50; } + button { padding: 12px 24px; font-size: 16px; margin: 10px; } + </style> + </head> + <body> + <div class="container"> + <h3>๐ Clipboard Helper</h3> + <p>Copying: <strong>${text}</strong></p> + <div id="status">โณ Working...</div> + <button onclick="copyAndClose()" id="copyBtn">Copy & Close</button> + <button onclick="window.close()">Just Close</button> + </div> + <script> + const textToCopy = ${JSON.stringify(text)}; + let copied = false; + + async function autoCopy() { + try { + await navigator.clipboard.writeText(textToCopy); + document.getElementById('status').innerHTML = '<span class="success">โ
Copied successfully!</span>'; + copied = true; + setTimeout(() => window.close(), 1500); + } catch (e) { + document.getElementById('status').innerHTML = 'โ Auto-copy failed. Use button below.'; + } + } + + async function copyAndClose() { + try { + await navigator.clipboard.writeText(textToCopy); + window.close(); + } catch (e) { + alert('Copy failed: ' + e.message); + } + } + + // Auto-copy on load + autoCopy(); + </script> + </body> + </html> + `; + + const dataUrl = 'data:text/html;charset=utf-8,' + encodeURIComponent(htmlContent); + window.open(dataUrl, '_blank', 'width=500,height=300'); + + addResult({ + success: true, + method: "Hybrid (Browser Fallback)", + details: "Direct failed, opened optimized browser window" + }); + } + } catch (error) { + addResult({ + success: false, + method: "Hybrid Approach", + error: String(error) + }); + } finally { + setIsLoading(null); + } + }; + + const clearResults = () => { + setResults([]); + }; + + return ( + <> + <PanelSectionRow> + <div + style={{ + fontSize: "14px", + fontWeight: "bold", + marginTop: "16px", + marginBottom: "8px", + borderBottom: "1px solid rgba(255, 255, 255, 0.2)", + paddingBottom: "4px", + color: "white" + }} + > + ๐งช Clipboard Automation Experiments + </div> + </PanelSectionRow> + + <PanelSectionRow> + <div style={{ fontSize: "12px", opacity: 0.8, marginBottom: "8px" }}> + Testing different approaches to automate clipboard access in Steam Deck gaming mode: + </div> + </PanelSectionRow> + + {/* Test Buttons */} + <PanelSectionRow> + <ButtonItem + layout="below" + onClick={testDirectClipboard} + disabled={isLoading === "direct"} + > + <div style={{ display: "flex", alignItems: "center", gap: "8px" }}> + <FaClipboard /> + <div>Test Direct Clipboard API</div> + {isLoading === "direct" && <div>โณ</div>} + </div> + </ButtonItem> + </PanelSectionRow> + + <PanelSectionRow> + <ButtonItem + layout="below" + onClick={testDataUrlApproach} + disabled={isLoading === "dataurl"} + > + <div style={{ display: "flex", alignItems: "center", gap: "8px" }}> + <FaRocket /> + <div>Test Data URL Browser</div> + {isLoading === "dataurl" && <div>โณ</div>} + </div> + </ButtonItem> + </PanelSectionRow> + + <PanelSectionRow> + <ButtonItem + layout="below" + onClick={testInputSimulation} + disabled={isLoading === "input"} + > + <div style={{ display: "flex", alignItems: "center", gap: "8px" }}> + <FaCog /> + <div>Test Input Simulation</div> + {isLoading === "input" && <div>โณ</div>} + </div> + </ButtonItem> + </PanelSectionRow> + + <PanelSectionRow> + <ButtonItem + layout="below" + onClick={testBackendClipboard} + disabled={isLoading === "backend"} + > + <div style={{ display: "flex", alignItems: "center", gap: "8px" }}> + <FaTerminal /> + <div>Test Backend Clipboard</div> + {isLoading === "backend" && <div>โณ</div>} + </div> + </ButtonItem> + </PanelSectionRow> + + <PanelSectionRow> + <ButtonItem + layout="below" + onClick={testHybridApproach} + disabled={isLoading === "hybrid"} + > + <div style={{ display: "flex", alignItems: "center", gap: "8px" }}> + <FaFlask /> + <div>Test Hybrid Approach (Recommended)</div> + {isLoading === "hybrid" && <div>โณ</div>} + </div> + </ButtonItem> + </PanelSectionRow> + + {/* Results Section */} + {results.length > 0 && ( + <> + <PanelSectionRow> + <Field + label={`Test Results (${results.length})`} + bottomSeparator="none" + > + <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}> + <div style={{ fontSize: "12px", opacity: 0.8 }}> + {results.filter(r => r.success).length} successful, {results.filter(r => !r.success).length} failed + </div> + <ButtonItem + layout="inline" + onClick={clearResults} + > + Clear + </ButtonItem> + </div> + </Field> + </PanelSectionRow> + + {results.slice(-5).map((result, index) => ( + <PanelSectionRow key={index}> + <Focusable> + <div style={{ + padding: "8px", + backgroundColor: result.success ? "rgba(76, 175, 80, 0.1)" : "rgba(244, 67, 54, 0.1)", + borderLeft: `3px solid ${result.success ? "#4CAF50" : "#f44336"}`, + borderRadius: "4px", + fontSize: "11px" + }}> + <div style={{ fontWeight: "bold", marginBottom: "4px" }}> + {result.success ? "โ
" : "โ"} {result.method} + </div> + {result.details && ( + <div style={{ color: "#4CAF50", marginBottom: "2px" }}> + {result.details} + </div> + )} + {result.error && ( + <div style={{ color: "#f44336" }}> + Error: {result.error} + </div> + )} + </div> + </Focusable> + </PanelSectionRow> + ))} + </> + )} + </> + ); +} diff --git a/src/components/Content.tsx b/src/components/Content.tsx index 9ce4c35..f6f24dc 100644 --- a/src/components/Content.tsx +++ b/src/components/Content.tsx @@ -8,6 +8,8 @@ import { ConfigurationSection } from "./ConfigurationSection"; import { UsageInstructions } from "./UsageInstructions"; import { WikiButton } from "./WikiButton"; import { ClipboardButton } from "./ClipboardButton"; +import { SmartClipboardButton } from "./SmartClipboardButton"; +import { ClipboardExperiments } from "./ClipboardExperiments"; import { PluginUpdateChecker } from "./PluginUpdateChecker"; import { NerdStuffModal } from "./NerdStuffModal"; import { ConfigurationData } from "../config/configSchema"; @@ -83,6 +85,10 @@ export function Content() { <WikiButton /> <ClipboardButton /> + <SmartClipboardButton /> + + {/* Experimental Clipboard Automation */} + <ClipboardExperiments /> {/* Plugin Update Checker */} <PluginUpdateChecker /> diff --git a/src/components/SmartClipboardButton.tsx b/src/components/SmartClipboardButton.tsx new file mode 100644 index 0000000..229560d --- /dev/null +++ b/src/components/SmartClipboardButton.tsx @@ -0,0 +1,245 @@ +import { useState } from "react"; +import { PanelSectionRow, ButtonItem } from "@decky/ui"; +import { FaClipboard, FaRocket } from "react-icons/fa"; +import { toaster } from "@decky/api"; +import { getLaunchOption, copyToSystemClipboard } from "../api/lsfgApi"; + +export function SmartClipboardButton() { + const [isLoading, setIsLoading] = useState(false); + + const getLaunchOptionText = async (): Promise<string> => { + try { + const result = await getLaunchOption(); + return result.launch_option || "~/lsfg %command%"; + } catch (error) { + return "~/lsfg %command%"; + } + }; + + const copyToClipboard = async () => { + if (isLoading) return; + + setIsLoading(true); + try { + const text = await getLaunchOptionText(); + + // Strategy 1: Try direct navigator.clipboard first (fastest) + let directSuccess = false; + try { + await navigator.clipboard.writeText(text); + // Verify it worked + const readBack = await navigator.clipboard.readText(); + directSuccess = readBack === text; + } catch (e) { + // Direct clipboard failed, will try alternatives + } + + if (directSuccess) { + toaster.toast({ + title: "Copied to Clipboard!", + body: "Launch option ready to paste" + }); + return; + } + + // Strategy 2: Try backend system clipboard + try { + const result = await copyToSystemClipboard(text); + if (result.success) { + toaster.toast({ + title: "Copied to Clipboard!", + body: `Using ${result.method || "system clipboard"}` + }); + return; + } + } catch (e) { + // Backend failed, fall back to browser + } + + // Strategy 3: Fall back to optimized browser window + const htmlContent = ` + <!DOCTYPE html> + <html> + <head> + <meta charset="utf-8"> + <title>Quick Copy - Steam Deck Clipboard Helper</title> + <style> + body { + font-family: 'Motiva Sans', system-ui, sans-serif; + background: linear-gradient(135deg, #1e2328 0%, #2a475e 100%); + color: white; + padding: 20px; + margin: 0; + display: flex; + align-items: center; + justify-content: center; + min-height: 100vh; + } + .container { + background: rgba(42, 71, 94, 0.9); + padding: 30px; + border-radius: 12px; + text-align: center; + box-shadow: 0 8px 32px rgba(0,0,0,0.3); + border: 1px solid rgba(255,255,255,0.1); + max-width: 500px; + width: 100%; + } + h2 { + margin-top: 0; + color: #66c0f4; + font-size: 24px; + } + .launch-option { + background: rgba(0,0,0,0.3); + padding: 15px; + border-radius: 8px; + font-family: 'Fira Code', 'Courier New', monospace; + font-size: 16px; + margin: 20px 0; + word-break: break-all; + border: 1px solid rgba(102, 192, 244, 0.3); + } + .status { + margin: 20px 0; + font-size: 16px; + min-height: 24px; + } + .success { color: #66bb6a; } + .error { color: #f44336; } + button { + background: linear-gradient(135deg, #417a9b 0%, #67c1f5 100%); + color: white; + border: none; + padding: 12px 24px; + font-size: 16px; + border-radius: 6px; + cursor: pointer; + margin: 8px; + transition: all 0.2s; + font-family: inherit; + } + button:hover { + background: linear-gradient(135deg, #4e8bb8 0%, #7bc8f7 100%); + transform: translateY(-1px); + } + button:active { + transform: translateY(0px); + } + .close-timer { + font-size: 14px; + opacity: 0.7; + margin-top: 15px; + } + </style> + </head> + <body> + <div class="container"> + <h2>๐ Steam Deck Clipboard Helper</h2> + <div>Copy this launch option for your Steam games:</div> + <div class="launch-option">${text}</div> + <div id="status" class="status">โณ Copying to clipboard...</div> + <div> + <button onclick="copyAndClose()" id="copyBtn">Copy & Close</button> + <button onclick="window.close()">Close</button> + </div> + <div class="close-timer" id="timer"></div> + </div> + <script> + const textToCopy = ${JSON.stringify(text)}; + let copied = false; + let autoCloseTimer = null; + + async function autoCopy() { + try { + await navigator.clipboard.writeText(textToCopy); + // Verify it worked + const readBack = await navigator.clipboard.readText(); + if (readBack === textToCopy) { + document.getElementById('status').innerHTML = '<span class="success">โ
Successfully copied to clipboard!</span>'; + copied = true; + startAutoClose(); + } else { + document.getElementById('status').innerHTML = '<span class="error">โ ๏ธ Copy may have failed - use button below</span>'; + } + } catch (e) { + document.getElementById('status').innerHTML = '<span class="error">โ Auto-copy failed - click "Copy & Close" below</span>'; + } + } + + async function copyAndClose() { + try { + await navigator.clipboard.writeText(textToCopy); + const readBack = await navigator.clipboard.readText(); + if (readBack === textToCopy) { + window.close(); + } else { + alert('Copy verification failed. Please try again or copy manually.'); + } + } catch (e) { + alert('Copy failed: ' + e.message); + } + } + + function startAutoClose() { + let seconds = 3; + const timerEl = document.getElementById('timer'); + timerEl.textContent = \`Window will close in \${seconds} seconds...\`; + + autoCloseTimer = setInterval(() => { + seconds--; + if (seconds <= 0) { + clearInterval(autoCloseTimer); + window.close(); + } else { + timerEl.textContent = \`Window will close in \${seconds} seconds...\`; + } + }, 1000); + } + + // Auto-copy on load + window.addEventListener('load', autoCopy); + </script> + </body> + </html> + `; + + const dataUrl = 'data:text/html;charset=utf-8,' + encodeURIComponent(htmlContent); + window.open(dataUrl, '_blank', 'width=600,height=400,scrollbars=no,resizable=yes'); + + toaster.toast({ + title: "Browser Helper Opened", + body: "Clipboard helper window opened with auto-copy" + }); + + } catch (error) { + toaster.toast({ + title: "Copy Failed", + body: `Error: ${String(error)}` + }); + } finally { + setIsLoading(false); + } + }; + + return ( + <PanelSectionRow> + <ButtonItem + layout="below" + onClick={copyToClipboard} + disabled={isLoading} + > + <div style={{ display: "flex", alignItems: "center", gap: "8px" }}> + {isLoading ? <FaRocket style={{ animation: "spin 1s linear infinite" }} /> : <FaClipboard />} + <div>{isLoading ? "Copying..." : "Smart Clipboard Copy"}</div> + </div> + </ButtonItem> + <style>{` + @keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } + } + `}</style> + </PanelSectionRow> + ); +} diff --git a/src/components/index.ts b/src/components/index.ts index c0c4804..1a36327 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -5,6 +5,8 @@ export { ConfigurationSection } from "./ConfigurationSection"; // export { UsageInstructions } from "./UsageInstructions"; export { WikiButton } from "./WikiButton"; export { ClipboardButton } from "./ClipboardButton"; +export { SmartClipboardButton } from "./SmartClipboardButton"; +export { ClipboardExperiments } from "./ClipboardExperiments"; export { LaunchOptionInfo } from "./LaunchOptionInfo"; export { PluginUpdateChecker } from "./PluginUpdateChecker"; export { NerdStuffModal } from "./NerdStuffModal"; |
