From 56eac1ac74e9f32bbf2541929e80b79cdb6cbcb1 Mon Sep 17 00:00:00 2001 From: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> Date: Mon, 28 Jul 2025 07:21:34 -0700 Subject: refined copy to clipboard ui feedback (#122) * copy feedback * add opti logo and update wording * branding updates * hide check mark when installed --- assets/opti-graph.png | Bin 0 -> 100615 bytes assets/optiscaler.png | Bin 0 -> 43729 bytes package.json | 2 +- plugin.json | 4 +- src/components/FGModInstallerSection.tsx | 29 ++++++++++++--- src/components/SmartClipboardButton.tsx | 62 +++++++++++++++++-------------- src/index.tsx | 6 +-- src/utils/constants.ts | 6 +-- 8 files changed, 67 insertions(+), 42 deletions(-) create mode 100644 assets/opti-graph.png create mode 100644 assets/optiscaler.png diff --git a/assets/opti-graph.png b/assets/opti-graph.png new file mode 100644 index 0000000..1a601fb Binary files /dev/null and b/assets/opti-graph.png differ diff --git a/assets/optiscaler.png b/assets/optiscaler.png new file mode 100644 index 0000000..21af233 Binary files /dev/null and b/assets/optiscaler.png differ diff --git a/package.json b/package.json index 2f22876..42ccb30 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "decky-framegen", - "version": "0.11.5", + "version": "0.11.6", "description": "plugin to install OptiScaler bleeding-edge and enable upscaling and framegen in a large variety of games.", "type": "module", "scripts": { diff --git a/plugin.json b/plugin.json index 125d5ad..1e4c179 100644 --- a/plugin.json +++ b/plugin.json @@ -1,6 +1,6 @@ { - "name": "Decky-Framegen", - "author": "JSON Derulo", + "name": "Decky Framegen", + "author": "Kurt Himebauch", "flags": [], "api_version": 1, "publish": { diff --git a/src/components/FGModInstallerSection.tsx b/src/components/FGModInstallerSection.tsx index 6777301..b82e749 100644 --- a/src/components/FGModInstallerSection.tsx +++ b/src/components/FGModInstallerSection.tsx @@ -5,6 +5,7 @@ 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; @@ -63,10 +64,10 @@ export function FGModInstallerSection({ pathExists, setPathExists }: FGModInstal return ( - {pathExists !== null ? ( + {pathExists === false ? ( -
- {pathExists ? MESSAGES.modInstalled : MESSAGES.modNotInstalled} +
+ {MESSAGES.modNotInstalled}
) : null} @@ -87,6 +88,26 @@ export function FGModInstallerSection({ pathExists, setPathExists }: FGModInstal ) : null} + {pathExists === true ? ( + +
+ OptiScaler +
+
+ ) : null} + {pathExists === true ? (
@@ -104,7 +125,6 @@ export function FGModInstallerSection({ pathExists, setPathExists }: FGModInstal ) : null} @@ -112,7 +132,6 @@ export function FGModInstallerSection({ pathExists, setPathExists }: FGModInstal ) : null} diff --git a/src/components/SmartClipboardButton.tsx b/src/components/SmartClipboardButton.tsx index 7d250f5..095da15 100644 --- a/src/components/SmartClipboardButton.tsx +++ b/src/components/SmartClipboardButton.tsx @@ -1,31 +1,37 @@ -import { useState } from "react"; +import { useState, useEffect } from "react"; import { PanelSectionRow, ButtonItem } from "@decky/ui"; -import { FaClipboard } from "react-icons/fa"; +import { FaClipboard, FaCheck } from "react-icons/fa"; import { toaster } from "@decky/api"; interface SmartClipboardButtonProps { command?: string; buttonText?: string; - successMessage?: string; } export function SmartClipboardButton({ command = "~/fgmod/fgmod %command%", - buttonText = "Copy Launch Command", - successMessage = "Launch option ready to paste" + buttonText = "Copy Launch Command" }: SmartClipboardButtonProps) { const [isLoading, setIsLoading] = useState(false); + const [showSuccess, setShowSuccess] = useState(false); - const getLaunchOptionText = (): string => { - return command; - }; + // 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) return; + if (isLoading || showSuccess) return; setIsLoading(true); try { - const text = getLaunchOptionText(); + const text = command; // Use the proven input simulation method const tempInput = document.createElement('input'); @@ -58,27 +64,18 @@ export function SmartClipboardButton({ document.body.removeChild(tempInput); if (copySuccess) { + // Show success feedback in the button instead of toast + setShowSuccess(true); // Verify the copy worked by reading back try { const readBack = await navigator.clipboard.readText(); - if (readBack === text) { - toaster.toast({ - title: "Copied to Clipboard!", - body: successMessage - }); - } else { - // Copy worked but verification failed - still consider it success - toaster.toast({ - title: "Copied to Clipboard!", - body: "Launch option copied (verification unavailable)" - }); + if (readBack !== text) { + // Copy worked but verification failed - still show success + console.log('Copy verification failed but copy likely worked'); } } catch (e) { // Verification failed but copy likely worked - toaster.toast({ - title: "Copied to Clipboard!", - body: "Launch option copied successfully" - }); + console.log('Copy verification unavailable but copy likely worked'); } } else { toaster.toast({ @@ -102,10 +99,14 @@ export function SmartClipboardButton({
- {isLoading ? ( + {showSuccess ? ( + + ) : isLoading ? ( )} -
{isLoading ? "Copying..." : buttonText}
+
+ {showSuccess ? "Copied to clipboard" : isLoading ? "Copying..." : buttonText} +