diff options
| author | xXJSONDeruloXx <danielhimebauch@gmail.com> | 2026-09-06 23:12:28 -0400 |
|---|---|---|
| committer | xXJSONDeruloXx <danielhimebauch@gmail.com> | 2026-09-06 23:12:28 -0400 |
| commit | d1f8263bf9f22753ad53e1b0105172a3bd0faf5f (patch) | |
| tree | 5e744fd0d2d61f6ea118d1d53b94a602fed3111b | |
| parent | 7bc5f685186b1ff03ce0f7c99e7bec411beabaeb (diff) | |
| download | decky-lsfg-vk-d1f8263bf9f22753ad53e1b0105172a3bd0faf5f.tar.gz decky-lsfg-vk-d1f8263bf9f22753ad53e1b0105172a3bd0faf5f.zip | |
feat: organize game profiles and focus enabled settings
| -rw-r--r-- | src/components/ConfigurationTab.tsx | 28 | ||||
| -rw-r--r-- | src/components/FpsMultiplierControl.tsx | 45 | ||||
| -rw-r--r-- | src/components/GameConfigurationControls.tsx | 16 | ||||
| -rw-r--r-- | src/components/GameConfigurationSelector.tsx | 107 | ||||
| -rw-r--r-- | src/components/NowPlayingTab.tsx | 2 |
5 files changed, 152 insertions, 46 deletions
diff --git a/src/components/ConfigurationTab.tsx b/src/components/ConfigurationTab.tsx index dab6f19..6c4a99a 100644 --- a/src/components/ConfigurationTab.tsx +++ b/src/components/ConfigurationTab.tsx @@ -1,5 +1,5 @@ import { ButtonItem, Field, Focusable, PanelSection, PanelSectionRow } from "@decky/ui"; -import { useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { ConfigurationData } from "../config/configSchema"; import { GameTarget } from "../hooks/useGameConfiguration"; import { GameConfigurationControls } from "./GameConfigurationControls"; @@ -27,7 +27,13 @@ export function ConfigurationTab({ onResetAll, }: ConfigurationTabProps) { const [detailAppId, setDetailAppId] = useState<string | null>(null); + const [focusFpsMultiplier, setFocusFpsMultiplier] = useState(false); const promptedRunningAppId = useRef<string | null>(null); + const closeDetails = useCallback(() => { + setFocusFpsMultiplier(false); + setDetailAppId(null); + }, []); + const clearFpsFocusRequest = useCallback(() => setFocusFpsMultiplier(false), []); useEffect(() => { if (!runningGame || runningGame.configured) { @@ -59,22 +65,28 @@ export function ConfigurationTab({ } 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"}` + ? `${selectedTarget.nonSteam ? "Non-Steam" : "Steam"} · App ID ${selectedTarget.appid} · ${selectedTarget.configured ? "Configured" : "Not configured"}` : "Game is no longer available"; return ( - <Focusable onCancelButton={() => setDetailAppId(null)}> + <Focusable onCancelButton={closeDetails}> <PanelSection title="Game Profile"> <PanelSectionRow> <Field label={profileLabel} description={profileDescription} /> </PanelSectionRow> <PanelSectionRow> - <ButtonItem layout="below" onClick={() => setDetailAppId(null)}>Back to games</ButtonItem> + <ButtonItem layout="below" onClick={closeDetails}>Back to games</ButtonItem> </PanelSectionRow> </PanelSection> - <GameConfigurationControls config={config} onConfigChange={onConfigChange} /> + {selectedTarget?.configured && ( + <GameConfigurationControls + config={config} + onConfigChange={onConfigChange} + autoFocusFpsMultiplier={focusFpsMultiplier} + onFpsMultiplierFocused={clearFpsFocusRequest} + /> + )} <PanelSectionRow> <ButtonItem layout="below" @@ -82,9 +94,9 @@ export function ConfigurationTab({ if (selectedTarget?.configured) { promptedRunningAppId.current = detailAppId; await onReset(); - setDetailAppId(null); + closeDetails(); } else if (detailAppId) { - await onEnable(detailAppId); + if (await onEnable(detailAppId)) setFocusFpsMultiplier(true); } }} > diff --git a/src/components/FpsMultiplierControl.tsx b/src/components/FpsMultiplierControl.tsx index e197dc3..1fd0e78 100644 --- a/src/components/FpsMultiplierControl.tsx +++ b/src/components/FpsMultiplierControl.tsx @@ -1,4 +1,5 @@ -import { PanelSectionRow, SliderField } from "@decky/ui"; +import { Focusable, PanelSectionRow, SliderField } from "@decky/ui"; +import { useEffect, useRef } from "react"; import { ConfigurationData } from "../config/configSchema"; import { MULTIPLIER } from "../config/generatedConfigSchema"; import t from "../i18n/i18n"; @@ -6,28 +7,48 @@ import t from "../i18n/i18n"; interface FpsMultiplierControlProps { config: ConfigurationData; onConfigChange: (fieldName: keyof ConfigurationData, value: boolean | number | string | string[]) => Promise<void>; + autoFocus?: boolean; + onAutoFocus?: () => void; } export function FpsMultiplierControl({ config, - onConfigChange + onConfigChange, + autoFocus = false, + onAutoFocus, }: FpsMultiplierControlProps) { + const focusableRef = useRef<HTMLDivElement>(null); + + useEffect(() => { + if (!autoFocus) return; + const frame = requestAnimationFrame(() => { + const target = focusableRef.current?.querySelector<HTMLElement>( + '[role="button"], [role="slider"]', + ); + target?.focus(); + onAutoFocus?.(); + }); + return () => cancelAnimationFrame(frame); + }, [autoFocus, onAutoFocus]); + const multiplierLabel = config.multiplier === 1 ? t("MULTIPLIER_OFF", "Off") : `${config.multiplier}x`; return ( <PanelSectionRow> - <SliderField - label={`FPS multiplier · ${multiplierLabel}`} - value={config.multiplier} - min={1} - max={4} - step={1} - notchCount={4} - showValue={false} - onChange={(value) => void onConfigChange(MULTIPLIER, value)} - /> + <Focusable ref={focusableRef} noFocusRing> + <SliderField + label={`FPS multiplier · ${multiplierLabel}`} + value={config.multiplier} + min={1} + max={4} + step={1} + notchCount={4} + showValue={false} + onChange={(value) => void onConfigChange(MULTIPLIER, value)} + /> + </Focusable> </PanelSectionRow> ); } diff --git a/src/components/GameConfigurationControls.tsx b/src/components/GameConfigurationControls.tsx index bdf9985..34b82c5 100644 --- a/src/components/GameConfigurationControls.tsx +++ b/src/components/GameConfigurationControls.tsx @@ -5,12 +5,24 @@ import { FpsMultiplierControl } from "./FpsMultiplierControl"; interface Props { config: ConfigurationData; onConfigChange: (fieldName: keyof ConfigurationData, value: boolean | number | string | string[]) => Promise<void>; + autoFocusFpsMultiplier?: boolean; + onFpsMultiplierFocused?: () => void; } -export function GameConfigurationControls({ config, onConfigChange }: Props) { +export function GameConfigurationControls({ + config, + onConfigChange, + autoFocusFpsMultiplier, + onFpsMultiplierFocused, +}: Props) { return ( <> - <FpsMultiplierControl config={config} onConfigChange={onConfigChange} /> + <FpsMultiplierControl + config={config} + onConfigChange={onConfigChange} + autoFocus={autoFocusFpsMultiplier} + onAutoFocus={onFpsMultiplierFocused} + /> <ConfigurationSection config={config} onConfigChange={onConfigChange} /> </> ); diff --git a/src/components/GameConfigurationSelector.tsx b/src/components/GameConfigurationSelector.tsx index fcdd0aa..982fcef 100644 --- a/src/components/GameConfigurationSelector.tsx +++ b/src/components/GameConfigurationSelector.tsx @@ -1,4 +1,6 @@ import { ButtonItem, Field, PanelSectionRow } from "@decky/ui"; +import { useEffect, useState } from "react"; +import { RiArrowDownSFill, RiArrowUpSFill } from "react-icons/ri"; import { GameTarget } from "../hooks/useGameConfiguration"; interface Props { @@ -8,42 +10,101 @@ interface Props { onResetAll: () => Promise<void>; } -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(" · "); +const CONFIGURED_COLLAPSED_KEY = "lsfg-configured-games-collapsed"; +const AVAILABLE_COLLAPSED_KEY = "lsfg-available-games-collapsed"; -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); +function usePersistentCollapsed(key: string) { + const [collapsed, setCollapsed] = useState(() => { + try { + return localStorage.getItem(key) === "true"; + } catch { + return false; + } }); + useEffect(() => { + try { + localStorage.setItem(key, String(collapsed)); + } catch { + // Persisting the view preference is optional. + } + }, [collapsed, key]); + + return [collapsed, () => setCollapsed((value) => !value)] as const; +} + +function GameGroup({ + title, + games, + collapsed, + onToggle, + onSelect, +}: { + title: string; + games: GameTarget[]; + collapsed: boolean; + onToggle: () => void; + onSelect: (appid: string) => void; +}) { + if (games.length === 0) return null; + return ( <> - {games.length === 0 && ( - <PanelSectionRow> - <Field label="No installed games" description="Steam has not reported any eligible games" /> - </PanelSectionRow> - )} - {games.map((game) => ( + <PanelSectionRow> + <ButtonItem + layout="below" + bottomSeparator={collapsed ? "standard" : "none"} + onClick={onToggle} + > + {collapsed ? <RiArrowDownSFill /> : <RiArrowUpSFill />} {title} ({games.length}) + </ButtonItem> + </PanelSectionRow> + {!collapsed && games.map((game) => ( <PanelSectionRow key={game.appid}> <Field label={game.name} - description={profileDescription( - game, - game.appid === runningGame?.appid, - game.appid === runningGame?.appid ? runningGame.configured : game.configured, - )} + description={game.nonSteam ? "Non-Steam" : "Steam"} onActivate={() => onSelect(game.appid)} highlightOnFocus /> </PanelSectionRow> ))} + </> + ); +} + +export function GameConfigurationSelector({ targets, runningGame, onSelect, onResetAll }: Props) { + const sortGames = (games: GameTarget[]) => [...games].sort((a, b) => { + if (a.appid === runningGame?.appid) return -1; + if (b.appid === runningGame?.appid) return 1; + return a.name.localeCompare(b.name); + }); + const configuredGames = sortGames(targets.filter((game) => game.configured)); + const availableGames = sortGames(targets.filter((game) => !game.configured)); + const [configuredCollapsed, toggleConfigured] = usePersistentCollapsed(CONFIGURED_COLLAPSED_KEY); + const [availableCollapsed, toggleAvailable] = usePersistentCollapsed(AVAILABLE_COLLAPSED_KEY); + + return ( + <> + {targets.length === 0 && ( + <PanelSectionRow> + <Field label="No installed games" description="Steam has not reported any eligible games" /> + </PanelSectionRow> + )} + <GameGroup + title="Configured" + games={configuredGames} + collapsed={configuredCollapsed} + onToggle={toggleConfigured} + onSelect={onSelect} + /> + <GameGroup + title="Available games" + games={availableGames} + collapsed={availableCollapsed} + onToggle={toggleAvailable} + onSelect={onSelect} + /> <PanelSectionRow> <ButtonItem layout="below" diff --git a/src/components/NowPlayingTab.tsx b/src/components/NowPlayingTab.tsx index 956db4a..b4f0557 100644 --- a/src/components/NowPlayingTab.tsx +++ b/src/components/NowPlayingTab.tsx @@ -17,7 +17,7 @@ export function NowPlayingTab({ game, config, onConfigChange, onRemove }: Props) <PanelSectionRow> <Field label={game.name} - description={`${game.nonSteam ? "Non-Steam" : "Steam"} · App ID ${game.appid} · Profile active`} + description={`${game.nonSteam ? "Non-Steam" : "Steam"} · App ID ${game.appid} · Configured`} /> </PanelSectionRow> </PanelSection> |
