diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/components/ConfigurationTab.tsx | 59 | ||||
| -rw-r--r-- | src/components/Content.tsx | 8 | ||||
| -rw-r--r-- | src/components/GameConfigurationSelector.tsx | 50 | ||||
| -rw-r--r-- | src/components/NowPlayingTab.tsx | 14 | ||||
| -rw-r--r-- | src/config/generatedConfigSchema.ts | 4 | ||||
| -rw-r--r-- | src/hooks/useGameConfiguration.ts | 53 |
6 files changed, 139 insertions, 49 deletions
diff --git a/src/components/ConfigurationTab.tsx b/src/components/ConfigurationTab.tsx index 0d8553c..83f46c1 100644 --- a/src/components/ConfigurationTab.tsx +++ b/src/components/ConfigurationTab.tsx @@ -12,6 +12,7 @@ interface ConfigurationTabProps { onSelect: (appid: string) => void; onConfigChange: (fieldName: keyof ConfigurationData, value: boolean | number | string | string[]) => Promise<void>; onEnable: (appid: string) => Promise<boolean>; + onEnableAll: () => Promise<void>; onReset: () => Promise<void>; onResetAll: () => Promise<void>; } @@ -23,29 +24,32 @@ export function ConfigurationTab({ onSelect, onConfigChange, onEnable, + onEnableAll, onReset, onResetAll, }: ConfigurationTabProps) { const [detailAppId, setDetailAppId] = useState<string | null>(null); const [focusFpsMultiplier, setFocusFpsMultiplier] = useState(false); - const [focusBackToGames, setFocusBackToGames] = useState(false); + const [focusDetailAction, setFocusDetailAction] = useState<"enable" | "back" | null>(null); const backToGamesRef = useRef<HTMLDivElement>(null); + const enableRef = useRef<HTMLDivElement>(null); const promptedRunningAppId = useRef<string | null>(null); const closeDetails = useCallback(() => { setFocusFpsMultiplier(false); - setFocusBackToGames(false); + setFocusDetailAction(null); setDetailAppId(null); }, []); const clearFpsFocusRequest = useCallback(() => setFocusFpsMultiplier(false), []); useEffect(() => { - if (!focusBackToGames) return; + if (!focusDetailAction) return; const frame = requestAnimationFrame(() => { - backToGamesRef.current?.querySelector<HTMLElement>('[role="button"]')?.focus(); - setFocusBackToGames(false); + const ref = focusDetailAction === "enable" ? enableRef : backToGamesRef; + ref.current?.querySelector<HTMLElement>('[role="button"]')?.focus(); + setFocusDetailAction(null); }); return () => cancelAnimationFrame(frame); - }, [focusBackToGames]); + }, [focusDetailAction]); useEffect(() => { if (!runningGame || runningGame.configured) { @@ -54,6 +58,7 @@ export function ConfigurationTab({ } if (promptedRunningAppId.current !== runningGame.appid && detailAppId === null) { promptedRunningAppId.current = runningGame.appid; + setFocusDetailAction(runningGame.configured ? "back" : "enable"); setDetailAppId(runningGame.appid); } }, [detailAppId, runningGame?.appid, runningGame?.configured]); @@ -67,10 +72,11 @@ export function ConfigurationTab({ targets={targets} runningGame={runningGame} onSelect={(appid) => { - setFocusBackToGames(true); + setFocusDetailAction(targets.find((target) => target.appid === appid)?.configured ? "back" : "enable"); onSelect(appid); setDetailAppId(appid); }} + onEnableAll={onEnableAll} onResetAll={onResetAll} /> </PanelSection> @@ -79,8 +85,17 @@ export function ConfigurationTab({ const profileLabel = selectedTarget?.name || "Game profile"; const profileDescription = selectedTarget - ? `${selectedTarget.nonSteam ? "Non-Steam" : "Steam"} · App ID ${selectedTarget.appid} · ${selectedTarget.configured ? "Configured" : "Not configured"}` + ? `${selectedTarget.nonSteam ? "Non-Steam" : "Steam"} · App ID ${selectedTarget.appid} · ${selectedTarget.configured ? "LSFG-VK Enabled" : "LSFG-VK not enabled"}` : "Game is no longer available"; + const handleProfileAction = async () => { + if (selectedTarget?.configured) { + promptedRunningAppId.current = detailAppId; + await onReset(); + closeDetails(); + } else if (detailAppId && await onEnable(detailAppId)) { + setFocusFpsMultiplier(true); + } + }; return ( <Focusable onCancelButton={closeDetails}> @@ -88,6 +103,13 @@ export function ConfigurationTab({ <PanelSectionRow> <Field label={profileLabel} description={profileDescription} /> </PanelSectionRow> + {!selectedTarget?.configured && selectedTarget && ( + <PanelSectionRow> + <Focusable ref={enableRef} noFocusRing> + <ButtonItem layout="below" onClick={handleProfileAction}>Enable for next launch</ButtonItem> + </Focusable> + </PanelSectionRow> + )} <PanelSectionRow> <Focusable ref={backToGamesRef} noFocusRing> <ButtonItem layout="below" onClick={closeDetails}>Back to games</ButtonItem> @@ -102,22 +124,11 @@ export function ConfigurationTab({ onFpsMultiplierFocused={clearFpsFocusRequest} /> )} - <PanelSectionRow> - <ButtonItem - layout="below" - onClick={async () => { - if (selectedTarget?.configured) { - promptedRunningAppId.current = detailAppId; - await onReset(); - closeDetails(); - } else if (detailAppId) { - if (await onEnable(detailAppId)) setFocusFpsMultiplier(true); - } - }} - > - {selectedTarget?.configured ? "Remove profile" : "Enable for next launch"} - </ButtonItem> - </PanelSectionRow> + {selectedTarget?.configured && ( + <PanelSectionRow> + <ButtonItem layout="below" onClick={handleProfileAction}>Remove profile</ButtonItem> + </PanelSectionRow> + )} </Focusable> ); } diff --git a/src/components/Content.tsx b/src/components/Content.tsx index 1bc6e95..f1c8c11 100644 --- a/src/components/Content.tsx +++ b/src/components/Content.tsx @@ -6,7 +6,7 @@ import { tabStyles } from "../styles"; import { useGameConfiguration } from "../hooks/useGameConfiguration"; import { useInstallationActions } from "../hooks/useInstallationActions"; import { useInstallationStatus } from "../hooks/useLsfgHooks"; -// import { ConfigFileTab } from "./ConfigFileTab"; +import { ConfigFileTab } from "./ConfigFileTab"; import { ConfigurationTab } from "./ConfigurationTab"; import { FlatpaksTab } from "./FlatpaksTab"; import { NowPlayingTab } from "./NowPlayingTab"; @@ -38,6 +38,7 @@ export function Content() { setSelectedAppId, save, enable, + enableAll, resetSelected, resetAll, reload, @@ -115,7 +116,6 @@ export function Content() { game={runningGame} config={config} onConfigChange={handleConfigChange} - onRemove={resetSelected} /> ), }] : []), @@ -130,14 +130,14 @@ export function Content() { onSelect={setSelectedAppId} onConfigChange={handleConfigChange} onEnable={enable} + onEnableAll={enableAll} onReset={resetSelected} onResetAll={resetAll} /> ), }, { id: "Flatpak", title: tabIcons.flatpak, content: <FlatpaksTab /> }, - // Keep the configuration-file view available for future use without exposing it in the UI. - // { id: "ConfigFile", title: tabIcons.configFile, content: <ConfigFileTab /> }, + { id: "ConfigFile", title: tabIcons.configFile, content: <ConfigFileTab /> }, // comment out for prod { id: "Setup", title: tabIcons.setup, content: setupContent }, ] : [ diff --git a/src/components/GameConfigurationSelector.tsx b/src/components/GameConfigurationSelector.tsx index 91fdc39..a046594 100644 --- a/src/components/GameConfigurationSelector.tsx +++ b/src/components/GameConfigurationSelector.tsx @@ -7,10 +7,11 @@ interface Props { targets: GameTarget[]; runningGame: GameTarget | null; onSelect: (appid: string) => void; + onEnableAll: () => Promise<void>; onResetAll: () => Promise<void>; } -const CONFIGURED_COLLAPSED_KEY = "lsfg-configured-games-collapsed"; +const CONFIGURED_COLLAPSED_KEY = "lsfg-configured-games-collapsed-v2"; const AVAILABLE_COLLAPSED_KEY = "lsfg-available-games-collapsed"; function usePersistentCollapsed(key: string) { @@ -73,7 +74,7 @@ function GameGroup({ ); } -export function GameConfigurationSelector({ targets, runningGame, onSelect, onResetAll }: Props) { +export function GameConfigurationSelector({ targets, runningGame, onSelect, onEnableAll, onResetAll }: Props) { const sortGames = (games: GameTarget[]) => [...games].sort((a, b) => { if (a.appid === runningGame?.appid) return -1; if (b.appid === runningGame?.appid) return 1; @@ -81,8 +82,14 @@ export function GameConfigurationSelector({ targets, runningGame, onSelect, onRe }); const configuredGames = sortGames(targets.filter((game) => game.configured)); const availableGames = sortGames(targets.filter((game) => !game.configured)); + const configuredSteamGames = configuredGames.filter((game) => !game.nonSteam); + const configuredNonSteamGames = configuredGames.filter((game) => game.nonSteam); + const availableSteamGames = availableGames.filter((game) => !game.nonSteam); + const availableNonSteamGames = availableGames.filter((game) => game.nonSteam); const [configuredCollapsed, toggleConfigured] = usePersistentCollapsed(CONFIGURED_COLLAPSED_KEY); + const [configuredNonSteamCollapsed, toggleConfiguredNonSteam] = usePersistentCollapsed(`${CONFIGURED_COLLAPSED_KEY}-non-steam`); const [availableCollapsed, toggleAvailable] = usePersistentCollapsed(AVAILABLE_COLLAPSED_KEY); + const [availableNonSteamCollapsed, toggleAvailableNonSteam] = usePersistentCollapsed(`${AVAILABLE_COLLAPSED_KEY}-non-steam`); const confirmResetAll = () => { showModal( <ConfirmModal @@ -94,6 +101,18 @@ export function GameConfigurationSelector({ targets, runningGame, onSelect, onRe />, ); }; + const confirmEnableAll = () => { + showModal( + <ConfirmModal + strTitle="Enable all available games?" + strDescription="Create individual LSFG-VK profiles for every available game using the plugin defaults." + strOKButtonText="Enable all" + strCancelButtonText="Cancel" + onOK={() => void onEnableAll()} + onCancel={() => {}} + />, + ); + }; return ( <> @@ -102,20 +121,41 @@ export function GameConfigurationSelector({ targets, runningGame, onSelect, onRe <Field label="No installed games" description="Steam has not reported any eligible games" /> </PanelSectionRow> )} + {availableGames.length > 0 && ( + <PanelSectionRow> + <ButtonItem layout="below" onClick={confirmEnableAll}> + Enable all available games + </ButtonItem> + </PanelSectionRow> + )} <GameGroup - title="Configured" - games={configuredGames} + title="LSFG-VK Enabled" + games={configuredSteamGames} collapsed={configuredCollapsed} onToggle={toggleConfigured} onSelect={onSelect} /> <GameGroup + title="LSFG-VK Enabled (Non-Steam)" + games={configuredNonSteamGames} + collapsed={configuredNonSteamCollapsed} + onToggle={toggleConfiguredNonSteam} + onSelect={onSelect} + /> + <GameGroup title="Available games" - games={availableGames} + games={availableSteamGames} collapsed={availableCollapsed} onToggle={toggleAvailable} onSelect={onSelect} /> + <GameGroup + title="Available games (Non-Steam)" + games={availableNonSteamGames} + collapsed={availableNonSteamCollapsed} + onToggle={toggleAvailableNonSteam} + onSelect={onSelect} + /> <PanelSectionRow> <ButtonItem layout="below" diff --git a/src/components/NowPlayingTab.tsx b/src/components/NowPlayingTab.tsx index b4f0557..8d2d779 100644 --- a/src/components/NowPlayingTab.tsx +++ b/src/components/NowPlayingTab.tsx @@ -1,4 +1,4 @@ -import { ButtonItem, Field, Focusable, PanelSection, PanelSectionRow } from "@decky/ui"; +import { Field, Focusable, PanelSection, PanelSectionRow } from "@decky/ui"; import { ConfigurationData } from "../config/configSchema"; import { GameTarget } from "../hooks/useGameConfiguration"; import { GameConfigurationControls } from "./GameConfigurationControls"; @@ -7,26 +7,20 @@ interface Props { game: GameTarget; config: ConfigurationData; onConfigChange: (fieldName: keyof ConfigurationData, value: boolean | number | string | string[]) => Promise<void>; - onRemove: () => Promise<void>; } -export function NowPlayingTab({ game, config, onConfigChange, onRemove }: Props) { +export function NowPlayingTab({ game, config, onConfigChange }: Props) { return ( <Focusable> - <PanelSection title="Now Playing"> + <PanelSection> <PanelSectionRow> <Field label={game.name} - description={`${game.nonSteam ? "Non-Steam" : "Steam"} · App ID ${game.appid} · Configured`} + description={`${game.nonSteam ? "Non-Steam" : "Steam"} | App ID ${game.appid}`} /> </PanelSectionRow> </PanelSection> <GameConfigurationControls config={config} onConfigChange={onConfigChange} /> - <PanelSectionRow> - <ButtonItem layout="below" onClick={() => void onRemove()}> - Remove profile - </ButtonItem> - </PanelSectionRow> </Focusable> ); } diff --git a/src/config/generatedConfigSchema.ts b/src/config/generatedConfigSchema.ts index 71459eb..3668b5b 100644 --- a/src/config/generatedConfigSchema.ts +++ b/src/config/generatedConfigSchema.ts @@ -21,11 +21,11 @@ export const CONFIG_SCHEMA: Record<string, ConfigField> = { active_in: { name: "active_in", fieldType: ConfigFieldType.ARRAY, default: [], description: "Steam AppID or executable identifiers" }, pacing_mode: { name: "pacing_mode", fieldType: ConfigFieldType.STRING, default: "vsync", description: "Frame pacing mode" }, multiplier: { name: "multiplier", fieldType: ConfigFieldType.INTEGER, default: 2, description: "Frame generation multiplier" }, - flow_scale: { name: "flow_scale", fieldType: ConfigFieldType.FLOAT, default: 1, description: "Motion estimation resolution scale" }, + flow_scale: { name: "flow_scale", fieldType: ConfigFieldType.FLOAT, default: 0.8, description: "Motion estimation resolution scale" }, performance_mode: { name: "performance_mode", fieldType: ConfigFieldType.BOOLEAN, default: false, description: "Use the lighter frame generation model" }, override_present_mode: { name: "override_present_mode", fieldType: ConfigFieldType.BOOLEAN, default: true, description: "Override present mode" }, preserve_swapchain_image_count: { name: "preserve_swapchain_image_count", fieldType: ConfigFieldType.BOOLEAN, default: false, description: "Preserve the swapchain image count" }, }; export function getFieldNames(): string[] { return Object.keys(CONFIG_SCHEMA); } -export function getDefaults(): ConfigurationData { return { dll: "", no_fp16: false, active_in: [], pacing_mode: "vsync", multiplier: 2, flow_scale: 1, performance_mode: false, override_present_mode: true, preserve_swapchain_image_count: false }; } +export function getDefaults(): ConfigurationData { return { dll: "", no_fp16: false, active_in: [], pacing_mode: "vsync", multiplier: 2, flow_scale: 0.8, performance_mode: false, override_present_mode: true, preserve_swapchain_image_count: false }; } export function getFieldTypes(): Record<string, ConfigFieldType> { return Object.fromEntries(Object.entries(CONFIG_SCHEMA).map(([key, value]) => [key, value.fieldType])); } diff --git a/src/hooks/useGameConfiguration.ts b/src/hooks/useGameConfiguration.ts index d279a8c..597edca 100644 --- a/src/hooks/useGameConfiguration.ts +++ b/src/hooks/useGameConfiguration.ts @@ -1,10 +1,36 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useQuickAccessVisible } from "@decky/api"; import { Router } from "@decky/ui"; import { getGameConfigs, getInstalledGames, updateGameConfig, resetGameConfig, resetAllGameConfigs, type GameConfigEntry, type GlobalConfig, type InstalledGame } from "../api/lsfgApi"; import { ConfigurationData, getDefaults } from "../config/configSchema"; +import { showErrorToast } from "../utils/toastUtils"; export interface GameTarget extends InstalledGame { configured: boolean; } +async function getSteamShortcuts(): Promise<InstalledGame[]> { + const apps = (globalThis as any).SteamClient?.Apps; + if (typeof apps?.GetAllShortcuts !== "function") return []; + + try { + const shortcuts = await apps.GetAllShortcuts(); + if (!Array.isArray(shortcuts)) return []; + return shortcuts.flatMap((shortcut: any) => { + const appid = Number(shortcut?.appid); + const name = shortcut?.data?.strAppName; + if (!Number.isInteger(appid) || appid === 0 || typeof name !== "string" || !name) return []; + return [{ appid: String(appid >>> 0), name, nonSteam: true }]; + }); + } catch { + return []; + } +} + +function mergeInstalledGames(backendGames: InstalledGame[], shortcutGames: InstalledGame[]) { + const games = new Map(backendGames.map((game) => [game.appid, game])); + for (const game of shortcutGames) games.set(game.appid, game); + return Array.from(games.values()); +} + export function useGameConfiguration() { const [games, setGames] = useState<GameConfigEntry[]>([]); const [globalConfig, setGlobalConfig] = useState<GlobalConfig>({ dll: "", no_fp16: false }); @@ -13,18 +39,25 @@ export function useGameConfiguration() { const [selectedAppId, setSelectedAppId] = useState(""); const [runningGame, setRunningGame] = useState<GameTarget | null>(null); const previousRunningAppId = useRef<string | null>(null); + const previousQuickAccessVisible = useRef<boolean | null>(null); + const quickAccessVisible = useQuickAccessVisible(); const load = useCallback(async () => { - const [result, installed] = await Promise.all([getGameConfigs(), getInstalledGames()]); + const [result, installed, shortcuts] = await Promise.all([getGameConfigs(), getInstalledGames(), getSteamShortcuts()]); if (result.success) { setGlobalConfig(result.global_config || { dll: "", no_fp16: false }); setGames(result.games || []); } - if (installed.success) setInstalledGames(installed.games || []); + setInstalledGames(mergeInstalledGames(installed.success ? installed.games || [] : [], shortcuts)); setConfigsLoaded(true); }, []); - useEffect(() => { load(); }, [load]); + useEffect(() => { + const initialLoad = previousQuickAccessVisible.current === null; + const becameVisible = quickAccessVisible && previousQuickAccessVisible.current === false; + previousQuickAccessVisible.current = quickAccessVisible; + if (initialLoad || becameVisible) void load(); + }, [load, quickAccessVisible]); useEffect(() => { const poll = () => { if (!configsLoaded) return; @@ -75,6 +108,18 @@ export function useGameConfiguration() { if (result.success) await load(); return result.success; }, [load, targets, template]); + const enableAll = useCallback(async (): Promise<void> => { + const available = targets.filter((target) => !target.configured && target.name); + if (available.length === 0) return; + for (const target of available) { + const result = await updateGameConfig(target.appid, target.name, template); + if (!result.success) { + showErrorToast("Could not enable all games", result.error || "A game profile could not be created"); + return; + } + } + await load(); + }, [load, targets, template]); const resetSelected = useCallback(async () => { if (selectedAppId) { @@ -95,5 +140,5 @@ export function useGameConfiguration() { } }, [load]); - return { config, games, targets, runningGame, selectedAppId, setSelectedAppId, save, enable, resetSelected, resetAll, reload: load }; + return { config, games, targets, runningGame, selectedAppId, setSelectedAppId, save, enable, enableAll, resetSelected, resetAll, reload: load }; } |
