summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/components/ConfigurationTab.tsx59
-rw-r--r--src/components/Content.tsx8
-rw-r--r--src/components/GameConfigurationSelector.tsx50
-rw-r--r--src/components/NowPlayingTab.tsx14
-rw-r--r--src/config/generatedConfigSchema.ts4
-rw-r--r--src/hooks/useGameConfiguration.ts53
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 };
}