From 7bc5f685186b1ff03ce0f7c99e7bec411beabaeb Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Sun, 6 Sep 2026 22:04:54 -0400 Subject: feat: make ui suck less, frfr --- src/components/ConfigurationSection.tsx | 10 +-- src/components/ConfigurationTab.tsx | 86 +++++++++++++++------ src/components/Content.tsx | 45 +++++++++-- src/components/FgmodClipboardButton.tsx | 110 --------------------------- src/components/FpsMultiplierControl.tsx | 70 ++++------------- src/components/GameConfigurationControls.tsx | 17 +++++ src/components/GameConfigurationSelector.tsx | 67 +++++++++++----- src/components/NowPlayingTab.tsx | 32 ++++++++ src/components/index.ts | 3 +- 9 files changed, 221 insertions(+), 219 deletions(-) delete mode 100644 src/components/FgmodClipboardButton.tsx create mode 100644 src/components/GameConfigurationControls.tsx create mode 100644 src/components/NowPlayingTab.tsx (limited to 'src/components') diff --git a/src/components/ConfigurationSection.tsx b/src/components/ConfigurationSection.tsx index c2bba7e..1f17264 100644 --- a/src/components/ConfigurationSection.tsx +++ b/src/components/ConfigurationSection.tsx @@ -10,19 +10,19 @@ interface ConfigurationSectionProps { export function ConfigurationSection({ config, onConfigChange }: ConfigurationSectionProps) { return <> - onConfigChange(FLOW_SCALE, value)} /> + onConfigChange(FLOW_SCALE, value)} /> - onConfigChange(NO_FP16, !value)} /> + onConfigChange(NO_FP16, !value)} /> - onConfigChange(PERFORMANCE_MODE, value)} /> + onConfigChange(PERFORMANCE_MODE, value)} /> - onConfigChange(OVERRIDE_PRESENT_MODE, value)} /> + onConfigChange(OVERRIDE_PRESENT_MODE, value)} /> - onConfigChange(PRESERVE_SWAPCHAIN_IMAGE_COUNT, value)} /> + onConfigChange(PRESERVE_SWAPCHAIN_IMAGE_COUNT, value)} /> ; } diff --git a/src/components/ConfigurationTab.tsx b/src/components/ConfigurationTab.tsx index 6f3f474..dab6f19 100644 --- a/src/components/ConfigurationTab.tsx +++ b/src/components/ConfigurationTab.tsx @@ -1,19 +1,17 @@ -import { PanelSection } from "@decky/ui"; +import { ButtonItem, Field, Focusable, PanelSection, PanelSectionRow } from "@decky/ui"; +import { useEffect, useRef, useState } from "react"; import { ConfigurationData } from "../config/configSchema"; import { GameTarget } from "../hooks/useGameConfiguration"; -import { ConfigurationSection } from "./ConfigurationSection"; -import { FgmodClipboardButton } from "./FgmodClipboardButton"; -import { FpsMultiplierControl } from "./FpsMultiplierControl"; +import { GameConfigurationControls } from "./GameConfigurationControls"; import { GameConfigurationSelector } from "./GameConfigurationSelector"; -import t from "../i18n/i18n"; interface ConfigurationTabProps { config: ConfigurationData; targets: GameTarget[]; runningGame: GameTarget | null; - selectedAppId: string; onSelect: (appid: string) => void; onConfigChange: (fieldName: keyof ConfigurationData, value: boolean | number | string | string[]) => Promise; + onEnable: (appid: string) => Promise; onReset: () => Promise; onResetAll: () => Promise; } @@ -22,33 +20,77 @@ export function ConfigurationTab({ config, targets, runningGame, - selectedAppId, onSelect, onConfigChange, + onEnable, onReset, onResetAll, }: ConfigurationTabProps) { - return ( - <> - - - - + const [detailAppId, setDetailAppId] = useState(null); + const promptedRunningAppId = useRef(null); + + useEffect(() => { + if (!runningGame || runningGame.configured) { + promptedRunningAppId.current = null; + return; + } + if (promptedRunningAppId.current !== runningGame.appid && detailAppId === null) { + promptedRunningAppId.current = runningGame.appid; + setDetailAppId(runningGame.appid); + } + }, [detailAppId, runningGame?.appid, runningGame?.configured]); + + const selectedTarget = detailAppId ? targets.find((target) => target.appid === detailAppId) : null; + + if (detailAppId === null) { + return ( + { + onSelect(appid); + setDetailAppId(appid); + }} onResetAll={onResetAll} /> - - - - - + ); + } + + const profileLabel = selectedTarget?.name || "Game profile"; + const running = selectedTarget?.appid === runningGame?.appid; + const profileDescription = selectedTarget + ? `${selectedTarget.nonSteam ? "Non-Steam" : "Steam"} · App ID ${selectedTarget.appid} · ${selectedTarget.configured ? running && !runningGame?.configured ? "Profile saved · applies next launch" : "Profile active" : "Not configured · changes apply next launch"}` + : "Game is no longer available"; + + return ( + setDetailAppId(null)}> + + + + + + setDetailAppId(null)}>Back to games + - + + + { + if (selectedTarget?.configured) { + promptedRunningAppId.current = detailAppId; + await onReset(); + setDetailAppId(null); + } else if (detailAppId) { + await onEnable(detailAppId); + } + }} + > + {selectedTarget?.configured ? "Remove profile" : "Enable for next launch"} + + + ); } diff --git a/src/components/Content.tsx b/src/components/Content.tsx index de8f996..9281d39 100644 --- a/src/components/Content.tsx +++ b/src/components/Content.tsx @@ -1,6 +1,6 @@ import { Tabs } from "@decky/ui"; -import { useEffect, useState } from "react"; -import { FaFileAlt, FaGamepad, FaLayerGroup, FaTools } from "react-icons/fa"; +import { useEffect, useRef, useState } from "react"; +import { FaFileAlt, FaGamepad, FaLayerGroup, FaList, FaTools } from "react-icons/fa"; import { ConfigurationData } from "../config/configSchema"; import { tabStyles } from "../styles"; import { useGameConfiguration } from "../hooks/useGameConfiguration"; @@ -9,10 +9,12 @@ import { useInstallationStatus } from "../hooks/useLsfgHooks"; import { ConfigFileTab } from "./ConfigFileTab"; import { ConfigurationTab } from "./ConfigurationTab"; import { FlatpaksTab } from "./FlatpaksTab"; +import { NowPlayingTab } from "./NowPlayingTab"; import { SetupTab } from "./SetupTab"; const tabIcons = { - configuration: , + nowPlaying: , + configuration: , flatpak: , configFile: , setup: , @@ -33,9 +35,9 @@ export function Content() { config, targets, runningGame, - selectedAppId, setSelectedAppId, save, + enable, resetSelected, resetAll, reload, @@ -48,10 +50,27 @@ export function Content() { steamBranchStatus?.success === true && steamBranchStatus.installed && !steamBranchStatus.needs_switch; + const previousRunningState = useRef<{ appid: string; configured: boolean } | null>(null); useEffect(() => { - setTab(setupComplete ? "Configuration" : "Setup"); - }, [setupComplete]); + if (!setupComplete) { + setTab("Setup"); + return; + } + setTab((current) => current === "Setup" ? (runningGame?.configured ? "NowPlaying" : "Configuration") : current); + }, [runningGame?.configured, setupComplete]); + + useEffect(() => { + if (!setupComplete) return; + const current = runningGame ? { appid: runningGame.appid, configured: runningGame.configured } : null; + const previous = previousRunningState.current; + previousRunningState.current = current; + if (current?.appid && (current.appid !== previous?.appid || current.configured !== previous?.configured)) { + setTab(current.configured ? "NowPlaying" : "Configuration"); + } else if (!current && previous) { + setTab((currentTab) => currentTab === "NowPlaying" ? "Configuration" : currentTab); + } + }, [runningGame?.appid, runningGame?.configured, setupComplete]); useEffect(() => { if (isInstalled) void reload(); @@ -88,6 +107,18 @@ export function Content() { const tabs = setupComplete ? [ + ...(runningGame?.configured ? [{ + id: "NowPlaying", + title: tabIcons.nowPlaying, + content: ( + + ), + }] : []), { id: "Configuration", title: tabIcons.configuration, @@ -96,9 +127,9 @@ export function Content() { config={config} targets={targets} runningGame={runningGame} - selectedAppId={selectedAppId} onSelect={setSelectedAppId} onConfigChange={handleConfigChange} + onEnable={enable} onReset={resetSelected} onResetAll={resetAll} /> diff --git a/src/components/FgmodClipboardButton.tsx b/src/components/FgmodClipboardButton.tsx deleted file mode 100644 index efc5490..0000000 --- a/src/components/FgmodClipboardButton.tsx +++ /dev/null @@ -1,110 +0,0 @@ -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"; -import t from '../i18n/i18n'; - -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 ( - - -
- {showSuccess ? ( - - ) : isLoading ? ( - - ) : ( - - )} -
- {showSuccess ? t('CLIPBOARD_COPIED', 'Copied to clipboard') : isLoading ? t('CLIPBOARD_COPYING', 'Copying...') : t('CLIPBOARD_LSFG_FGMOD', 'LSFG + DeckyFG')} -
-
-
- -
- ); -} diff --git a/src/components/FpsMultiplierControl.tsx b/src/components/FpsMultiplierControl.tsx index 5069c9a..e197dc3 100644 --- a/src/components/FpsMultiplierControl.tsx +++ b/src/components/FpsMultiplierControl.tsx @@ -1,4 +1,4 @@ -import { PanelSectionRow, DialogButton, Focusable } from "@decky/ui"; +import { PanelSectionRow, SliderField } from "@decky/ui"; import { ConfigurationData } from "../config/configSchema"; import { MULTIPLIER } from "../config/generatedConfigSchema"; import t from "../i18n/i18n"; @@ -12,62 +12,22 @@ export function FpsMultiplierControl({ config, onConfigChange }: FpsMultiplierControlProps) { + const multiplierLabel = config.multiplier === 1 + ? t("MULTIPLIER_OFF", "Off") + : `${config.multiplier}x`; + return ( - - onConfigChange(MULTIPLIER, Math.max(1, config.multiplier - 1))} - disabled={config.multiplier <= 1} - > - − - -
4 ? "red" : "white", - minWidth: "60px", - textAlign: "center" - }} - > - {config.multiplier === 1 ? t('MULTIPLIER_OFF', 'OFF') : `${config.multiplier}X`} -
- onConfigChange(MULTIPLIER, Math.min(4, config.multiplier + 1))} - disabled={config.multiplier >= 4} - > - + - -
+ void onConfigChange(MULTIPLIER, value)} + />
); } diff --git a/src/components/GameConfigurationControls.tsx b/src/components/GameConfigurationControls.tsx new file mode 100644 index 0000000..bdf9985 --- /dev/null +++ b/src/components/GameConfigurationControls.tsx @@ -0,0 +1,17 @@ +import { ConfigurationData } from "../config/configSchema"; +import { ConfigurationSection } from "./ConfigurationSection"; +import { FpsMultiplierControl } from "./FpsMultiplierControl"; + +interface Props { + config: ConfigurationData; + onConfigChange: (fieldName: keyof ConfigurationData, value: boolean | number | string | string[]) => Promise; +} + +export function GameConfigurationControls({ config, onConfigChange }: Props) { + return ( + <> + + + + ); +} diff --git a/src/components/GameConfigurationSelector.tsx b/src/components/GameConfigurationSelector.tsx index a231477..fcdd0aa 100644 --- a/src/components/GameConfigurationSelector.tsx +++ b/src/components/GameConfigurationSelector.tsx @@ -1,29 +1,58 @@ -import { Dropdown, DropdownOption, PanelSectionRow, ButtonItem } from "@decky/ui"; +import { ButtonItem, Field, PanelSectionRow } from "@decky/ui"; import { GameTarget } from "../hooks/useGameConfiguration"; interface Props { targets: GameTarget[]; runningGame: GameTarget | null; - selectedAppId: string; onSelect: (appid: string) => void; - onReset: () => Promise; onResetAll: () => Promise; } -export function GameConfigurationSelector({ targets, runningGame, selectedAppId, onSelect, onReset, onResetAll }: Props) { - const options: DropdownOption[] = [ - { data: "", label: runningGame ? `Default (editing template) · ${runningGame.name}` : "Default" }, - ...targets.map((target) => ({ data: target.appid, label: `${target.nonSteam ? "Non-Steam · " : ""}${target.name} · ${target.appid}` })), - ]; - return <> - - onSelect(String(option.data))} /> - - - void onReset()} disabled={!selectedAppId}>Reset selected game - - - void onResetAll()} disabled={!targets.some((target) => target.configured)}>Reset all game profiles - - ; +const profileDescription = (target: GameTarget, running: boolean, active: boolean) => [ + running ? "Now playing" : "", + target.nonSteam ? "Non-Steam" : "Steam", + target.configured + ? active ? "Profile active" : "Profile saved · applies next launch" + : "Not configured · changes apply next launch", +].filter(Boolean).join(" · "); + +export function GameConfigurationSelector({ targets, runningGame, onSelect, onResetAll }: Props) { + const games = [...targets].sort((a, b) => { + if (a.appid === runningGame?.appid) return -1; + if (b.appid === runningGame?.appid) return 1; + return a.name.localeCompare(b.name); + }); + + return ( + <> + {games.length === 0 && ( + + + + )} + {games.map((game) => ( + + onSelect(game.appid)} + highlightOnFocus + /> + + ))} + + void onResetAll()} + disabled={!targets.some((target) => target.configured)} + > + Remove all profiles + + + + ); } diff --git a/src/components/NowPlayingTab.tsx b/src/components/NowPlayingTab.tsx new file mode 100644 index 0000000..956db4a --- /dev/null +++ b/src/components/NowPlayingTab.tsx @@ -0,0 +1,32 @@ +import { ButtonItem, Field, Focusable, PanelSection, PanelSectionRow } from "@decky/ui"; +import { ConfigurationData } from "../config/configSchema"; +import { GameTarget } from "../hooks/useGameConfiguration"; +import { GameConfigurationControls } from "./GameConfigurationControls"; + +interface Props { + game: GameTarget; + config: ConfigurationData; + onConfigChange: (fieldName: keyof ConfigurationData, value: boolean | number | string | string[]) => Promise; + onRemove: () => Promise; +} + +export function NowPlayingTab({ game, config, onConfigChange, onRemove }: Props) { + return ( + + + + + + + + + void onRemove()}> + Remove profile + + + + ); +} diff --git a/src/components/index.ts b/src/components/index.ts index 5089b6f..424a360 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -3,9 +3,10 @@ export { StatusDisplay } from "./StatusDisplay"; export { InstallationButton } from "./InstallationButton"; export { ConfigurationSection } from "./ConfigurationSection"; export { FpsMultiplierControl } from "./FpsMultiplierControl"; -export { FgmodClipboardButton } from "./FgmodClipboardButton"; export { ConfigurationTab } from "./ConfigurationTab"; export { SetupTab } from "./SetupTab"; export { ConfigFileTab } from "./ConfigFileTab"; export { FlatpaksTab } from "./FlatpaksTab"; export { GameConfigurationSelector } from "./GameConfigurationSelector"; +export { GameConfigurationControls } from "./GameConfigurationControls"; +export { NowPlayingTab } from "./NowPlayingTab"; -- cgit v1.2.3