From ad2b182777bfd0a5ceef6e654df75ff13eb8b503 Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Sun, 6 Sep 2026 15:08:45 -0400 Subject: refactor: offload configuration to lsfg-vk --- src/hooks/useGameConfiguration.ts | 68 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 src/hooks/useGameConfiguration.ts (limited to 'src/hooks/useGameConfiguration.ts') diff --git a/src/hooks/useGameConfiguration.ts b/src/hooks/useGameConfiguration.ts new file mode 100644 index 0000000..e0ba360 --- /dev/null +++ b/src/hooks/useGameConfiguration.ts @@ -0,0 +1,68 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { Router } from "@decky/ui"; +import { getGameConfigs, getInstalledGames, updateGameConfig, updateLsfgConfig, resetGameConfig, resetAllGameConfigs, type GameConfigEntry, type InstalledGame } from "../api/lsfgApi"; +import { ConfigurationData, getDefaults } from "../config/configSchema"; + +export interface GameTarget { appid: string; name: string; configured: boolean; } + +export function useGameConfiguration() { + const [defaultConfig, setDefaultConfig] = useState(getDefaults()); + const [games, setGames] = useState([]); + const [installedGames, setInstalledGames] = useState([]); + const [selectedAppId, setSelectedAppId] = useState(""); + const [runningGame, setRunningGame] = useState(null); + const autoSelected = useRef(false); + + const load = useCallback(async () => { + const [result, installed] = await Promise.all([getGameConfigs(), getInstalledGames()]); + if (result.success) { + setDefaultConfig(result.default || getDefaults()); + setGames(result.games || []); + } + if (installed.success) setInstalledGames(installed.games || []); + }, []); + + useEffect(() => { load(); }, [load]); + useEffect(() => { + const poll = () => { + const app = Router.MainRunningApp as any; + if (app?.appid) setRunningGame({ appid: String(app.appid), name: app.display_name || `App ${app.appid}`, configured: games.some((game) => game.appid === String(app.appid)) }); + else setRunningGame(null); + }; + poll(); + const interval = window.setInterval(poll, 2000); + return () => window.clearInterval(interval); + }, [games]); + useEffect(() => { + if (!autoSelected.current && runningGame) { + autoSelected.current = true; + setSelectedAppId(runningGame.appid); + } + }, [runningGame]); + + const targets = useMemo(() => { + const configured = installedGames.map((game) => ({ appid: game.appid, name: game.name, configured: games.some((item) => item.appid === game.appid) })); + for (const game of games) if (!configured.some((item) => item.appid === game.appid)) configured.push({ appid: game.appid, name: `App ${game.appid}`, configured: true }); + if (runningGame && !configured.some((game) => game.appid === runningGame.appid)) configured.unshift(runningGame); + return configured; + }, [games, installedGames, runningGame]); + const selected = selectedAppId ? games.find((game) => game.appid === selectedAppId)?.config : defaultConfig; + const config = selected || defaultConfig; + + const save = useCallback(async (next: ConfigurationData) => { + if (!selectedAppId) { + const result = await updateLsfgConfig(next); + if (result.success) setDefaultConfig(next); + return; + } + const result = await updateGameConfig(selectedAppId, next); + if (result.success) await load(); + }, [load, selectedAppId]); + + const resetSelected = useCallback(async () => { + if (selectedAppId) { await resetGameConfig(selectedAppId); setSelectedAppId(""); await load(); } + }, [load, selectedAppId]); + const resetAll = useCallback(async () => { await resetAllGameConfigs(); setSelectedAppId(""); await load(); }, [load]); + + return { config, defaultConfig, games, targets, runningGame, selectedAppId, setSelectedAppId, save, resetSelected, resetAll, reload: load }; +} -- cgit v1.2.3 From c9be32287ad5b72fcd86b10fa726d09dbd97d7fd Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Sun, 6 Sep 2026 17:09:54 -0400 Subject: refactor: organize plugin into native tabs --- src/hooks/useGameConfiguration.ts | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) (limited to 'src/hooks/useGameConfiguration.ts') diff --git a/src/hooks/useGameConfiguration.ts b/src/hooks/useGameConfiguration.ts index e0ba360..b177551 100644 --- a/src/hooks/useGameConfiguration.ts +++ b/src/hooks/useGameConfiguration.ts @@ -3,7 +3,7 @@ import { Router } from "@decky/ui"; import { getGameConfigs, getInstalledGames, updateGameConfig, updateLsfgConfig, resetGameConfig, resetAllGameConfigs, type GameConfigEntry, type InstalledGame } from "../api/lsfgApi"; import { ConfigurationData, getDefaults } from "../config/configSchema"; -export interface GameTarget { appid: string; name: string; configured: boolean; } +export interface GameTarget extends InstalledGame { configured: boolean; } export function useGameConfiguration() { const [defaultConfig, setDefaultConfig] = useState(getDefaults()); @@ -26,13 +26,21 @@ export function useGameConfiguration() { useEffect(() => { const poll = () => { const app = Router.MainRunningApp as any; - if (app?.appid) setRunningGame({ appid: String(app.appid), name: app.display_name || `App ${app.appid}`, configured: games.some((game) => game.appid === String(app.appid)) }); - else setRunningGame(null); + if (!app?.appid) return setRunningGame(null); + const appid = String(app.appid); + const installed = installedGames.find((game) => game.appid === appid); + const name = app.display_name || installed?.name; + if (!name) return setRunningGame(null); + setRunningGame({ + ...(installed || { appid, name, nonSteam: false }), + name, + configured: games.some((game) => game.appid === appid), + }); }; poll(); const interval = window.setInterval(poll, 2000); return () => window.clearInterval(interval); - }, [games]); + }, [games, installedGames]); useEffect(() => { if (!autoSelected.current && runningGame) { autoSelected.current = true; @@ -41,8 +49,8 @@ export function useGameConfiguration() { }, [runningGame]); const targets = useMemo(() => { - const configured = installedGames.map((game) => ({ appid: game.appid, name: game.name, configured: games.some((item) => item.appid === game.appid) })); - for (const game of games) if (!configured.some((item) => item.appid === game.appid)) configured.push({ appid: game.appid, name: `App ${game.appid}`, configured: true }); + const configured = installedGames.map((game) => ({ ...game, configured: games.some((item) => item.appid === game.appid) })); + for (const game of games) if (!configured.some((item) => item.appid === game.appid)) configured.push({ appid: game.appid, name: game.profile, nonSteam: false, configured: true }); if (runningGame && !configured.some((game) => game.appid === runningGame.appid)) configured.unshift(runningGame); return configured; }, [games, installedGames, runningGame]); @@ -55,9 +63,11 @@ export function useGameConfiguration() { if (result.success) setDefaultConfig(next); return; } - const result = await updateGameConfig(selectedAppId, next); + const selectedTarget = targets.find((target) => target.appid === selectedAppId); + if (!selectedTarget?.name) return; + const result = await updateGameConfig(selectedAppId, selectedTarget.name, next); if (result.success) await load(); - }, [load, selectedAppId]); + }, [load, selectedAppId, targets]); const resetSelected = useCallback(async () => { if (selectedAppId) { await resetGameConfig(selectedAppId); setSelectedAppId(""); await load(); } -- cgit v1.2.3 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/hooks/useGameConfiguration.ts | 61 ++++++++++++++++++++++++++------------- 1 file changed, 41 insertions(+), 20 deletions(-) (limited to 'src/hooks/useGameConfiguration.ts') diff --git a/src/hooks/useGameConfiguration.ts b/src/hooks/useGameConfiguration.ts index b177551..d279a8c 100644 --- a/src/hooks/useGameConfiguration.ts +++ b/src/hooks/useGameConfiguration.ts @@ -1,37 +1,40 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Router } from "@decky/ui"; -import { getGameConfigs, getInstalledGames, updateGameConfig, updateLsfgConfig, resetGameConfig, resetAllGameConfigs, type GameConfigEntry, type InstalledGame } from "../api/lsfgApi"; +import { getGameConfigs, getInstalledGames, updateGameConfig, resetGameConfig, resetAllGameConfigs, type GameConfigEntry, type GlobalConfig, type InstalledGame } from "../api/lsfgApi"; import { ConfigurationData, getDefaults } from "../config/configSchema"; export interface GameTarget extends InstalledGame { configured: boolean; } export function useGameConfiguration() { - const [defaultConfig, setDefaultConfig] = useState(getDefaults()); const [games, setGames] = useState([]); + const [globalConfig, setGlobalConfig] = useState({ dll: "", no_fp16: false }); const [installedGames, setInstalledGames] = useState([]); + const [configsLoaded, setConfigsLoaded] = useState(false); const [selectedAppId, setSelectedAppId] = useState(""); const [runningGame, setRunningGame] = useState(null); - const autoSelected = useRef(false); + const previousRunningAppId = useRef(null); const load = useCallback(async () => { const [result, installed] = await Promise.all([getGameConfigs(), getInstalledGames()]); if (result.success) { - setDefaultConfig(result.default || getDefaults()); + setGlobalConfig(result.global_config || { dll: "", no_fp16: false }); setGames(result.games || []); } if (installed.success) setInstalledGames(installed.games || []); + setConfigsLoaded(true); }, []); useEffect(() => { load(); }, [load]); useEffect(() => { const poll = () => { + if (!configsLoaded) return; const app = Router.MainRunningApp as any; if (!app?.appid) return setRunningGame(null); const appid = String(app.appid); const installed = installedGames.find((game) => game.appid === appid); const name = app.display_name || installed?.name; if (!name) return setRunningGame(null); - setRunningGame({ + setRunningGame((current) => current?.appid === appid ? current : { ...(installed || { appid, name, nonSteam: false }), name, configured: games.some((game) => game.appid === appid), @@ -40,13 +43,14 @@ export function useGameConfiguration() { poll(); const interval = window.setInterval(poll, 2000); return () => window.clearInterval(interval); - }, [games, installedGames]); + }, [configsLoaded, games, installedGames]); useEffect(() => { - if (!autoSelected.current && runningGame) { - autoSelected.current = true; - setSelectedAppId(runningGame.appid); + const appid = runningGame?.appid || null; + if (appid !== previousRunningAppId.current) { + previousRunningAppId.current = appid; + setSelectedAppId(appid || ""); } - }, [runningGame]); + }, [runningGame?.appid]); const targets = useMemo(() => { const configured = installedGames.map((game) => ({ ...game, configured: games.some((item) => item.appid === game.appid) })); @@ -54,25 +58,42 @@ export function useGameConfiguration() { if (runningGame && !configured.some((game) => game.appid === runningGame.appid)) configured.unshift(runningGame); return configured; }, [games, installedGames, runningGame]); - const selected = selectedAppId ? games.find((game) => game.appid === selectedAppId)?.config : defaultConfig; - const config = selected || defaultConfig; + const template = useMemo(() => ({ ...getDefaults(), ...globalConfig }), [globalConfig]); + const config = games.find((game) => game.appid === selectedAppId)?.config || template; const save = useCallback(async (next: ConfigurationData) => { - if (!selectedAppId) { - const result = await updateLsfgConfig(next); - if (result.success) setDefaultConfig(next); - return; - } const selectedTarget = targets.find((target) => target.appid === selectedAppId); if (!selectedTarget?.name) return; const result = await updateGameConfig(selectedAppId, selectedTarget.name, next); if (result.success) await load(); }, [load, selectedAppId, targets]); + const enable = useCallback(async (appid: string) => { + const target = targets.find((item) => item.appid === appid); + if (!target?.name) return false; + const result = await updateGameConfig(appid, target.name, template); + if (result.success) await load(); + return result.success; + }, [load, targets, template]); + const resetSelected = useCallback(async () => { - if (selectedAppId) { await resetGameConfig(selectedAppId); setSelectedAppId(""); await load(); } + if (selectedAppId) { + const result = await resetGameConfig(selectedAppId); + if (result.success) { + setRunningGame((current) => current?.appid === selectedAppId ? { ...current, configured: false } : current); + setSelectedAppId(""); + await load(); + } + } }, [load, selectedAppId]); - const resetAll = useCallback(async () => { await resetAllGameConfigs(); setSelectedAppId(""); await load(); }, [load]); + const resetAll = useCallback(async () => { + const result = await resetAllGameConfigs(); + if (result.success) { + setRunningGame((current) => current ? { ...current, configured: false } : current); + setSelectedAppId(""); + await load(); + } + }, [load]); - return { config, defaultConfig, games, targets, runningGame, selectedAppId, setSelectedAppId, save, resetSelected, resetAll, reload: load }; + return { config, games, targets, runningGame, selectedAppId, setSelectedAppId, save, enable, resetSelected, resetAll, reload: load }; } -- cgit v1.2.3 From 22db1125238e54b29538102727c36284e122e703 Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Mon, 7 Sep 2026 16:02:26 -0400 Subject: feat: refine game discovery and profile UX --- src/hooks/useGameConfiguration.ts | 53 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 49 insertions(+), 4 deletions(-) (limited to 'src/hooks/useGameConfiguration.ts') 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 { + 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([]); const [globalConfig, setGlobalConfig] = useState({ dll: "", no_fp16: false }); @@ -13,18 +39,25 @@ export function useGameConfiguration() { const [selectedAppId, setSelectedAppId] = useState(""); const [runningGame, setRunningGame] = useState(null); const previousRunningAppId = useRef(null); + const previousQuickAccessVisible = useRef(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 => { + 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 }; } -- cgit v1.2.3 From 8352ee74e8a437c1f4a4dadb75d63049f45b164b Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Tue, 8 Sep 2026 16:25:11 -0400 Subject: add back workarounds sections, scope out of now playing --- src/hooks/useGameConfiguration.ts | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) (limited to 'src/hooks/useGameConfiguration.ts') diff --git a/src/hooks/useGameConfiguration.ts b/src/hooks/useGameConfiguration.ts index 597edca..7a572f9 100644 --- a/src/hooks/useGameConfiguration.ts +++ b/src/hooks/useGameConfiguration.ts @@ -3,6 +3,7 @@ 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 { cleanupSteamLaunchOptions } from "../utils/steamLaunchOptions"; import { showErrorToast } from "../utils/toastUtils"; export interface GameTarget extends InstalledGame { configured: boolean; } @@ -94,6 +95,17 @@ export function useGameConfiguration() { const template = useMemo(() => ({ ...getDefaults(), ...globalConfig }), [globalConfig]); const config = games.find((game) => game.appid === selectedAppId)?.config || template; + const cleanupTargetLaunchOptions = useCallback(async (target: GameTarget): Promise => { + if (!installedGames.some((game) => game.appid === target.appid)) return true; + try { + await cleanupSteamLaunchOptions(Number(target.appid), target.nonSteam); + return true; + } catch (error) { + showErrorToast("Could not update Steam launch options", error instanceof Error ? error.message : String(error)); + return false; + } + }, [installedGames]); + const save = useCallback(async (next: ConfigurationData) => { const selectedTarget = targets.find((target) => target.appid === selectedAppId); if (!selectedTarget?.name) return; @@ -104,14 +116,16 @@ export function useGameConfiguration() { const enable = useCallback(async (appid: string) => { const target = targets.find((item) => item.appid === appid); if (!target?.name) return false; + if (!(await cleanupTargetLaunchOptions(target))) return false; const result = await updateGameConfig(appid, target.name, template); if (result.success) await load(); return result.success; - }, [load, targets, template]); + }, [cleanupTargetLaunchOptions, load, targets, template]); const enableAll = useCallback(async (): Promise => { const available = targets.filter((target) => !target.configured && target.name); if (available.length === 0) return; for (const target of available) { + if (!(await cleanupTargetLaunchOptions(target))) return; 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"); @@ -119,10 +133,12 @@ export function useGameConfiguration() { } } await load(); - }, [load, targets, template]); + }, [cleanupTargetLaunchOptions, load, targets, template]); const resetSelected = useCallback(async () => { if (selectedAppId) { + const selectedTarget = targets.find((target) => target.appid === selectedAppId); + if (selectedTarget && !(await cleanupTargetLaunchOptions(selectedTarget))) return; const result = await resetGameConfig(selectedAppId); if (result.success) { setRunningGame((current) => current?.appid === selectedAppId ? { ...current, configured: false } : current); @@ -130,15 +146,18 @@ export function useGameConfiguration() { await load(); } } - }, [load, selectedAppId]); + }, [cleanupTargetLaunchOptions, load, selectedAppId, targets]); const resetAll = useCallback(async () => { + for (const target of targets.filter((item) => item.configured)) { + if (!(await cleanupTargetLaunchOptions(target))) return; + } const result = await resetAllGameConfigs(); if (result.success) { setRunningGame((current) => current ? { ...current, configured: false } : current); setSelectedAppId(""); await load(); } - }, [load]); + }, [cleanupTargetLaunchOptions, load, targets]); return { config, games, targets, runningGame, selectedAppId, setSelectedAppId, save, enable, enableAll, resetSelected, resetAll, reload: load }; } -- cgit v1.2.3 From bec26fe025c97c00d398e7a4fb571195706b9e76 Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Tue, 8 Sep 2026 16:56:57 -0400 Subject: feat: more launch arg janitoring --- src/hooks/useGameConfiguration.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'src/hooks/useGameConfiguration.ts') diff --git a/src/hooks/useGameConfiguration.ts b/src/hooks/useGameConfiguration.ts index 7a572f9..46607cb 100644 --- a/src/hooks/useGameConfiguration.ts +++ b/src/hooks/useGameConfiguration.ts @@ -106,12 +106,13 @@ export function useGameConfiguration() { } }, [installedGames]); - const save = useCallback(async (next: ConfigurationData) => { + const save = useCallback(async (next: ConfigurationData, cleanupLaunchOptions = false) => { const selectedTarget = targets.find((target) => target.appid === selectedAppId); if (!selectedTarget?.name) return; + if (cleanupLaunchOptions && !(await cleanupTargetLaunchOptions(selectedTarget))) return; const result = await updateGameConfig(selectedAppId, selectedTarget.name, next); if (result.success) await load(); - }, [load, selectedAppId, targets]); + }, [cleanupTargetLaunchOptions, load, selectedAppId, targets]); const enable = useCallback(async (appid: string) => { const target = targets.find((item) => item.appid === appid); -- cgit v1.2.3 From 790132668c4421c68c32bdc8fc9792b0d6028f97 Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Tue, 8 Sep 2026 22:18:13 -0400 Subject: I really dont want to but here you go little guy --- src/hooks/useGameConfiguration.ts | 46 ++++++++++++++++++++++++++++++--------- 1 file changed, 36 insertions(+), 10 deletions(-) (limited to 'src/hooks/useGameConfiguration.ts') diff --git a/src/hooks/useGameConfiguration.ts b/src/hooks/useGameConfiguration.ts index 46607cb..6d1fe6a 100644 --- a/src/hooks/useGameConfiguration.ts +++ b/src/hooks/useGameConfiguration.ts @@ -3,7 +3,7 @@ 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 { cleanupSteamLaunchOptions } from "../utils/steamLaunchOptions"; +import { applyWorkaroundState, cleanupLegacySteamLaunchOptions, cleanupSteamLaunchOptions, getDefaultWorkaroundState, updateSteamLaunchOptions } from "../utils/steamLaunchOptions"; import { showErrorToast } from "../utils/toastUtils"; export interface GameTarget extends InstalledGame { configured: boolean; } @@ -98,7 +98,7 @@ export function useGameConfiguration() { const cleanupTargetLaunchOptions = useCallback(async (target: GameTarget): Promise => { if (!installedGames.some((game) => game.appid === target.appid)) return true; try { - await cleanupSteamLaunchOptions(Number(target.appid), target.nonSteam); + await cleanupLegacySteamLaunchOptions(Number(target.appid), target.nonSteam); return true; } catch (error) { showErrorToast("Could not update Steam launch options", error instanceof Error ? error.message : String(error)); @@ -106,6 +106,32 @@ export function useGameConfiguration() { } }, [installedGames]); + const removeTargetLaunchOptions = useCallback(async (target: GameTarget): Promise => { + if (!installedGames.some((game) => game.appid === target.appid)) return true; + try { + await cleanupSteamLaunchOptions(Number(target.appid), target.nonSteam); + return true; + } catch (error) { + showErrorToast("Could not clean up Steam launch options", error instanceof Error ? error.message : String(error)); + return false; + } + }, [installedGames]); + + const initializeTargetLaunchOptions = useCallback(async (target: GameTarget): Promise => { + if (!installedGames.some((game) => game.appid === target.appid)) return true; + try { + await updateSteamLaunchOptions( + Number(target.appid), + target.nonSteam, + (options) => applyWorkaroundState(options, getDefaultWorkaroundState()), + ); + return true; + } catch (error) { + showErrorToast("Could not initialize Steam launch options", error instanceof Error ? error.message : String(error)); + return false; + } + }, [installedGames]); + const save = useCallback(async (next: ConfigurationData, cleanupLaunchOptions = false) => { const selectedTarget = targets.find((target) => target.appid === selectedAppId); if (!selectedTarget?.name) return; @@ -117,16 +143,16 @@ export function useGameConfiguration() { const enable = useCallback(async (appid: string) => { const target = targets.find((item) => item.appid === appid); if (!target?.name) return false; - if (!(await cleanupTargetLaunchOptions(target))) return false; + if (!(await initializeTargetLaunchOptions(target))) return false; const result = await updateGameConfig(appid, target.name, template); if (result.success) await load(); return result.success; - }, [cleanupTargetLaunchOptions, load, targets, template]); + }, [initializeTargetLaunchOptions, load, targets, template]); const enableAll = useCallback(async (): Promise => { const available = targets.filter((target) => !target.configured && target.name); if (available.length === 0) return; for (const target of available) { - if (!(await cleanupTargetLaunchOptions(target))) return; + if (!(await initializeTargetLaunchOptions(target))) return; 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"); @@ -134,12 +160,12 @@ export function useGameConfiguration() { } } await load(); - }, [cleanupTargetLaunchOptions, load, targets, template]); + }, [initializeTargetLaunchOptions, load, targets, template]); const resetSelected = useCallback(async () => { if (selectedAppId) { const selectedTarget = targets.find((target) => target.appid === selectedAppId); - if (selectedTarget && !(await cleanupTargetLaunchOptions(selectedTarget))) return; + if (selectedTarget && !(await removeTargetLaunchOptions(selectedTarget))) return; const result = await resetGameConfig(selectedAppId); if (result.success) { setRunningGame((current) => current?.appid === selectedAppId ? { ...current, configured: false } : current); @@ -147,10 +173,10 @@ export function useGameConfiguration() { await load(); } } - }, [cleanupTargetLaunchOptions, load, selectedAppId, targets]); + }, [load, removeTargetLaunchOptions, selectedAppId, targets]); const resetAll = useCallback(async () => { for (const target of targets.filter((item) => item.configured)) { - if (!(await cleanupTargetLaunchOptions(target))) return; + if (!(await removeTargetLaunchOptions(target))) return; } const result = await resetAllGameConfigs(); if (result.success) { @@ -158,7 +184,7 @@ export function useGameConfiguration() { setSelectedAppId(""); await load(); } - }, [cleanupTargetLaunchOptions, load, targets]); + }, [load, removeTargetLaunchOptions, targets]); return { config, games, targets, runningGame, selectedAppId, setSelectedAppId, save, enable, enableAll, resetSelected, resetAll, reload: load }; } -- cgit v1.2.3 From 1b932fd69c3dba925e0cbf027e05508b2daf5e8c Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Wed, 9 Sep 2026 01:01:42 -0400 Subject: add back launcher script and correct pathing --- src/hooks/useGameConfiguration.ts | 158 ++++++++++++++++++++++++++++++-------- 1 file changed, 124 insertions(+), 34 deletions(-) (limited to 'src/hooks/useGameConfiguration.ts') diff --git a/src/hooks/useGameConfiguration.ts b/src/hooks/useGameConfiguration.ts index 6d1fe6a..c66596a 100644 --- a/src/hooks/useGameConfiguration.ts +++ b/src/hooks/useGameConfiguration.ts @@ -1,9 +1,9 @@ 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 { getGameConfigs, getInstalledGames, getWorkaroundState, removeWorkaroundState, resetGameConfig, resetAllGameConfigs, setWorkaroundState, updateGameConfig, type GameConfigEntry, type GlobalConfig, type InstalledGame, type WorkaroundState } from "../api/lsfgApi"; import { ConfigurationData, getDefaults } from "../config/configSchema"; -import { applyWorkaroundState, cleanupLegacySteamLaunchOptions, cleanupSteamLaunchOptions, getDefaultWorkaroundState, updateSteamLaunchOptions } from "../utils/steamLaunchOptions"; +import { cleanupLegacySteamLaunchOptions, getDefaultWrapperPath, hasWrapperLaunchIntegration, installWrapperIntegration, isLegacyWrapperToken, readSteamLaunchOptions, removeWrapperIntegration } from "../utils/steamLaunchOptions"; import { showErrorToast } from "../utils/toastUtils"; export interface GameTarget extends InstalledGame { configured: boolean; } @@ -32,6 +32,19 @@ function mergeInstalledGames(backendGames: InstalledGame[], shortcutGames: Insta return Array.from(games.values()); } +const DEFAULT_WORKAROUND_STATE: WorkaroundState = { + dxvkFrameRate: 0, + disableGamescopeWsi: true, + disableHdr: true, + disableSteamdeckMode: false, + disableVkbasalt: false, + enableZink: false, +}; + +function asError(error: unknown): Error { + return error instanceof Error ? error : new Error(String(error)); +} + export function useGameConfiguration() { const [games, setGames] = useState([]); const [globalConfig, setGlobalConfig] = useState({ dll: "", no_fp16: false }); @@ -95,39 +108,107 @@ export function useGameConfiguration() { const template = useMemo(() => ({ ...getDefaults(), ...globalConfig }), [globalConfig]); const config = games.find((game) => game.appid === selectedAppId)?.config || template; - const cleanupTargetLaunchOptions = useCallback(async (target: GameTarget): Promise => { + const ensureTargetWorkarounds = useCallback(async (target: GameTarget): Promise => { if (!installedGames.some((game) => game.appid === target.appid)) return true; + const appId = Number(target.appid); try { - await cleanupLegacySteamLaunchOptions(Number(target.appid), target.nonSteam); - return true; - } catch (error) { - showErrorToast("Could not update Steam launch options", error instanceof Error ? error.message : String(error)); - return false; - } - }, [installedGames]); + const existing = await getWorkaroundState(target.appid); + if (!existing.success) throw new Error(existing.error || "Could not read workaround state"); + const current = await readSteamLaunchOptions(appId, target.nonSteam); + const wrapperPath = existing.wrapper_path || getDefaultWrapperPath(); + const oldState = existing.state; + const oldShortcutExe = existing.shortcut_exe || undefined; + const oldCommandTokenAdded = existing.command_token_added === true; + if (target.nonSteam && oldState && current.target === wrapperPath && !oldShortcutExe) { + throw new Error("Managed shortcut Target has no saved original executable"); + } + if (target.nonSteam && oldState && current.target !== wrapperPath && current.target !== oldShortcutExe) { + throw new Error("Shortcut Target changed externally; refusing to replace it"); + } + if (target.nonSteam && !oldState && current.target === wrapperPath) { + throw new Error("Shortcut Target is already the managed wrapper but its original Target is unknown"); + } + const state = oldState || { ...DEFAULT_WORKAROUND_STATE }; + const originalExecutable = target.nonSteam ? (oldShortcutExe || current.target) : undefined; + const initialIntegration = target.nonSteam + ? current.target === wrapperPath + : hasWrapperLaunchIntegration(current.options, wrapperPath); + const initialStateResult = await setWorkaroundState( + target.appid, + state, + originalExecutable || null, + oldCommandTokenAdded, + ); + if (!initialStateResult.success) throw new Error(initialStateResult.error || "Could not create workaround state"); - const removeTargetLaunchOptions = useCallback(async (target: GameTarget): Promise => { - if (!installedGames.some((game) => game.appid === target.appid)) return true; - try { - await cleanupSteamLaunchOptions(Number(target.appid), target.nonSteam); - return true; + let integration: Awaited> | null = null; + try { + integration = await installWrapperIntegration(appId, target.nonSteam, wrapperPath, oldCommandTokenAdded); + const finalStateResult = await setWorkaroundState( + target.appid, + state, + target.nonSteam ? (integration.originalExecutable || originalExecutable || null) : null, + integration.commandTokenAdded, + ); + if (!finalStateResult.success) throw new Error(finalStateResult.error || "Could not finalize workaround state"); + return true; + } catch (error) { + let rollbackSucceeded = true; + if (!initialIntegration && integration) { + try { + await removeWrapperIntegration( + appId, + target.nonSteam, + wrapperPath, + target.nonSteam ? (integration?.originalExecutable || originalExecutable) : undefined, + integration?.commandTokenAdded ?? oldCommandTokenAdded, + ); + } catch (rollbackError) { + showErrorToast("Workaround rollback failed", asError(rollbackError).message); + rollbackSucceeded = false; + } + } + if (rollbackSucceeded) { + const restored = oldState + ? await setWorkaroundState(target.appid, oldState, oldShortcutExe || null, oldCommandTokenAdded) + : await removeWorkaroundState(target.appid); + if (!restored.success) throw new Error(restored.error || "Could not roll back workaround state"); + } + throw error; + } } catch (error) { - showErrorToast("Could not clean up Steam launch options", error instanceof Error ? error.message : String(error)); + showErrorToast("Could not initialize workarounds", asError(error).message); return false; } }, [installedGames]); - const initializeTargetLaunchOptions = useCallback(async (target: GameTarget): Promise => { + const removeTargetWorkarounds = useCallback(async (target: GameTarget): Promise => { if (!installedGames.some((game) => game.appid === target.appid)) return true; + const appId = Number(target.appid); try { - await updateSteamLaunchOptions( - Number(target.appid), - target.nonSteam, - (options) => applyWorkaroundState(options, getDefaultWorkaroundState()), - ); + const existing = await getWorkaroundState(target.appid); + if (!existing.success) throw new Error(existing.error || "Could not read workaround state"); + const wrapperPath = existing.wrapper_path || getDefaultWrapperPath(); + if (existing.state) { + await removeWrapperIntegration( + appId, + target.nonSteam, + wrapperPath, + existing.shortcut_exe || undefined, + existing.command_token_added === true, + ); + } else { + const current = await readSteamLaunchOptions(appId, target.nonSteam); + if (target.nonSteam && (current.target === wrapperPath || isLegacyWrapperToken(current.target))) { + throw new Error("Shortcut Target is a frame-generation wrapper but its original Target is unknown"); + } + await cleanupLegacySteamLaunchOptions(appId, target.nonSteam, wrapperPath); + } + const removed = await removeWorkaroundState(target.appid); + if (!removed.success) throw new Error(removed.error || "Could not remove workaround state"); return true; } catch (error) { - showErrorToast("Could not initialize Steam launch options", error instanceof Error ? error.message : String(error)); + showErrorToast("Could not clean up game workarounds", asError(error).message); return false; } }, [installedGames]); @@ -135,37 +216,46 @@ export function useGameConfiguration() { const save = useCallback(async (next: ConfigurationData, cleanupLaunchOptions = false) => { const selectedTarget = targets.find((target) => target.appid === selectedAppId); if (!selectedTarget?.name) return; - if (cleanupLaunchOptions && !(await cleanupTargetLaunchOptions(selectedTarget))) return; + // The profile owns its wrapper integration. Keep this check on every + // configuration save so an external edit is detected before the profile + // is changed; toggles update the sidecar only. + if (cleanupLaunchOptions && !(await ensureTargetWorkarounds(selectedTarget))) return; const result = await updateGameConfig(selectedAppId, selectedTarget.name, next); if (result.success) await load(); - }, [cleanupTargetLaunchOptions, load, selectedAppId, targets]); + }, [ensureTargetWorkarounds, load, selectedAppId, targets]); const enable = useCallback(async (appid: string) => { const target = targets.find((item) => item.appid === appid); if (!target?.name) return false; - if (!(await initializeTargetLaunchOptions(target))) return false; + if (!(await ensureTargetWorkarounds(target))) return false; const result = await updateGameConfig(appid, target.name, template); if (result.success) await load(); + else await removeTargetWorkarounds(target); return result.success; - }, [initializeTargetLaunchOptions, load, targets, template]); + }, [ensureTargetWorkarounds, load, removeTargetWorkarounds, targets, template]); const enableAll = useCallback(async (): Promise => { const available = targets.filter((target) => !target.configured && target.name); if (available.length === 0) return; for (const target of available) { - if (!(await initializeTargetLaunchOptions(target))) return; + if (!(await ensureTargetWorkarounds(target))) return; 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"); + await removeTargetWorkarounds(target); return; } } await load(); - }, [initializeTargetLaunchOptions, load, targets, template]); + }, [ensureTargetWorkarounds, load, removeTargetWorkarounds, targets, template]); + const repair = useCallback(async (appid: string): Promise => { + const target = targets.find((item) => item.appid === appid); + return target ? ensureTargetWorkarounds(target) : false; + }, [ensureTargetWorkarounds, targets]); const resetSelected = useCallback(async () => { if (selectedAppId) { const selectedTarget = targets.find((target) => target.appid === selectedAppId); - if (selectedTarget && !(await removeTargetLaunchOptions(selectedTarget))) return; + if (selectedTarget && !(await removeTargetWorkarounds(selectedTarget))) return; const result = await resetGameConfig(selectedAppId); if (result.success) { setRunningGame((current) => current?.appid === selectedAppId ? { ...current, configured: false } : current); @@ -173,10 +263,10 @@ export function useGameConfiguration() { await load(); } } - }, [load, removeTargetLaunchOptions, selectedAppId, targets]); + }, [load, removeTargetWorkarounds, selectedAppId, targets]); const resetAll = useCallback(async () => { for (const target of targets.filter((item) => item.configured)) { - if (!(await removeTargetLaunchOptions(target))) return; + if (!(await removeTargetWorkarounds(target))) return; } const result = await resetAllGameConfigs(); if (result.success) { @@ -184,7 +274,7 @@ export function useGameConfiguration() { setSelectedAppId(""); await load(); } - }, [load, removeTargetLaunchOptions, targets]); + }, [load, removeTargetWorkarounds, targets]); - return { config, games, targets, runningGame, selectedAppId, setSelectedAppId, save, enable, enableAll, resetSelected, resetAll, reload: load }; + return { config, games, targets, runningGame, selectedAppId, setSelectedAppId, save, enable, enableAll, repair, resetSelected, resetAll, reload: load }; } -- cgit v1.2.3 From cc1e6f47dd9838b066822162a607d2859c043aff Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Wed, 9 Sep 2026 19:16:30 -0400 Subject: refactor: unify flatpak targets with steam profiles --- src/hooks/useGameConfiguration.ts | 78 +++++++++++++++++++++++++++------------ 1 file changed, 54 insertions(+), 24 deletions(-) (limited to 'src/hooks/useGameConfiguration.ts') diff --git a/src/hooks/useGameConfiguration.ts b/src/hooks/useGameConfiguration.ts index c66596a..b59d592 100644 --- a/src/hooks/useGameConfiguration.ts +++ b/src/hooks/useGameConfiguration.ts @@ -1,7 +1,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useQuickAccessVisible } from "@decky/api"; import { Router } from "@decky/ui"; -import { getGameConfigs, getInstalledGames, getWorkaroundState, removeWorkaroundState, resetGameConfig, resetAllGameConfigs, setWorkaroundState, updateGameConfig, type GameConfigEntry, type GlobalConfig, type InstalledGame, type WorkaroundState } from "../api/lsfgApi"; +import { ensureFlatpakSupport, getGameConfigs, getInstalledGames, getWorkaroundState, removeWorkaroundState, repairFlatpakSupport, resetGameConfig, resetAllGameConfigs, setWorkaroundState, updateGameConfig, type GameConfigEntry, type GlobalConfig, type InstalledGame, type WorkaroundState } from "../api/lsfgApi"; import { ConfigurationData, getDefaults } from "../config/configSchema"; import { cleanupLegacySteamLaunchOptions, getDefaultWrapperPath, hasWrapperLaunchIntegration, installWrapperIntegration, isLegacyWrapperToken, readSteamLaunchOptions, removeWrapperIntegration } from "../utils/steamLaunchOptions"; import { showErrorToast } from "../utils/toastUtils"; @@ -19,7 +19,12 @@ async function getSteamShortcuts(): Promise { 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 }]; + return [{ + appid: String(appid >>> 0), + name, + nonSteam: true, + transport: { kind: "host" }, + }]; }); } catch { return []; @@ -28,7 +33,10 @@ async function getSteamShortcuts(): Promise { 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); + for (const game of shortcutGames) { + const existing = games.get(game.appid); + games.set(game.appid, existing ? { ...existing, name: game.name, nonSteam: true } : game); + } return Array.from(games.values()); } @@ -82,7 +90,7 @@ export function useGameConfiguration() { const name = app.display_name || installed?.name; if (!name) return setRunningGame(null); setRunningGame((current) => current?.appid === appid ? current : { - ...(installed || { appid, name, nonSteam: false }), + ...(installed || { appid, name, nonSteam: false, transport: { kind: "host" } }), name, configured: games.some((game) => game.appid === appid), }); @@ -101,13 +109,26 @@ export function useGameConfiguration() { const targets = useMemo(() => { const configured = installedGames.map((game) => ({ ...game, configured: games.some((item) => item.appid === game.appid) })); - for (const game of games) if (!configured.some((item) => item.appid === game.appid)) configured.push({ appid: game.appid, name: game.profile, nonSteam: false, configured: true }); + for (const game of games) if (!configured.some((item) => item.appid === game.appid)) configured.push({ appid: game.appid, name: game.profile, nonSteam: false, transport: { kind: "host" }, configured: true }); if (runningGame && !configured.some((game) => game.appid === runningGame.appid)) configured.unshift(runningGame); return configured; }, [games, installedGames, runningGame]); const template = useMemo(() => ({ ...getDefaults(), ...globalConfig }), [globalConfig]); const config = games.find((game) => game.appid === selectedAppId)?.config || template; + const ensureTargetFlatpakSupport = useCallback(async (target: GameTarget): Promise => { + if (target.transport.kind !== "flatpak") return true; + const result = await ensureFlatpakSupport(target.transport.flatpakAppId); + if (!result.success || result.support_status !== "ready") { + showErrorToast( + "Flatpak support unavailable", + result.error || result.message || "The required Flatpak runtime extension is not ready", + ); + return false; + } + return true; + }, []); + const ensureTargetWorkarounds = useCallback(async (target: GameTarget): Promise => { if (!installedGames.some((game) => game.appid === target.appid)) return true; const appId = Number(target.appid); @@ -119,6 +140,7 @@ export function useGameConfiguration() { const oldState = existing.state; const oldShortcutExe = existing.shortcut_exe || undefined; const oldCommandTokenAdded = existing.command_token_added === true; + const oldTransport = existing.transport || target.transport; if (target.nonSteam && oldState && current.target === wrapperPath && !oldShortcutExe) { throw new Error("Managed shortcut Target has no saved original executable"); } @@ -138,6 +160,7 @@ export function useGameConfiguration() { state, originalExecutable || null, oldCommandTokenAdded, + target.transport, ); if (!initialStateResult.success) throw new Error(initialStateResult.error || "Could not create workaround state"); @@ -149,6 +172,7 @@ export function useGameConfiguration() { state, target.nonSteam ? (integration.originalExecutable || originalExecutable || null) : null, integration.commandTokenAdded, + target.transport, ); if (!finalStateResult.success) throw new Error(finalStateResult.error || "Could not finalize workaround state"); return true; @@ -170,7 +194,13 @@ export function useGameConfiguration() { } if (rollbackSucceeded) { const restored = oldState - ? await setWorkaroundState(target.appid, oldState, oldShortcutExe || null, oldCommandTokenAdded) + ? await setWorkaroundState( + target.appid, + oldState, + oldShortcutExe || null, + oldCommandTokenAdded, + oldTransport, + ) : await removeWorkaroundState(target.appid); if (!restored.success) throw new Error(restored.error || "Could not roll back workaround state"); } @@ -227,30 +257,30 @@ export function useGameConfiguration() { const enable = useCallback(async (appid: string) => { const target = targets.find((item) => item.appid === appid); if (!target?.name) return false; + if (!(await ensureTargetFlatpakSupport(target))) return false; if (!(await ensureTargetWorkarounds(target))) return false; const result = await updateGameConfig(appid, target.name, template); if (result.success) await load(); else await removeTargetWorkarounds(target); return result.success; - }, [ensureTargetWorkarounds, load, removeTargetWorkarounds, targets, template]); - const enableAll = useCallback(async (): Promise => { - const available = targets.filter((target) => !target.configured && target.name); - if (available.length === 0) return; - for (const target of available) { - if (!(await ensureTargetWorkarounds(target))) return; - 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"); - await removeTargetWorkarounds(target); - return; - } - } - await load(); - }, [ensureTargetWorkarounds, load, removeTargetWorkarounds, targets, template]); + }, [ensureTargetFlatpakSupport, ensureTargetWorkarounds, load, removeTargetWorkarounds, targets, template]); const repair = useCallback(async (appid: string): Promise => { const target = targets.find((item) => item.appid === appid); - return target ? ensureTargetWorkarounds(target) : false; - }, [ensureTargetWorkarounds, targets]); + if (!target) return false; + if (target.transport.kind === "flatpak") { + const support = await repairFlatpakSupport(target.transport.flatpakAppId); + if (!support.success || support.support_status !== "ready") { + showErrorToast( + "Flatpak support unavailable", + support.error || support.message || "The required Flatpak runtime extension is not ready", + ); + return false; + } + } + const success = await ensureTargetWorkarounds(target); + if (success) await load(); + return success; + }, [ensureTargetWorkarounds, load, targets]); const resetSelected = useCallback(async () => { if (selectedAppId) { @@ -276,5 +306,5 @@ export function useGameConfiguration() { } }, [load, removeTargetWorkarounds, targets]); - return { config, games, targets, runningGame, selectedAppId, setSelectedAppId, save, enable, enableAll, repair, resetSelected, resetAll, reload: load }; + return { config, games, targets, runningGame, selectedAppId, setSelectedAppId, save, enable, repair, resetSelected, resetAll, reload: load }; } -- cgit v1.2.3 From fb4d053213bdbda271a54b517a11a89c4780f80a Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Wed, 9 Sep 2026 20:45:20 -0400 Subject: fix: restore profile and Flatpak controls --- src/hooks/useGameConfiguration.ts | 65 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 61 insertions(+), 4 deletions(-) (limited to 'src/hooks/useGameConfiguration.ts') diff --git a/src/hooks/useGameConfiguration.ts b/src/hooks/useGameConfiguration.ts index b59d592..c70d589 100644 --- a/src/hooks/useGameConfiguration.ts +++ b/src/hooks/useGameConfiguration.ts @@ -40,6 +40,23 @@ function mergeInstalledGames(backendGames: InstalledGame[], shortcutGames: Insta return Array.from(games.values()); } +function selectShortcutExecutable( + target: GameTarget, + ...candidates: Array +): string | undefined { + const absolute = candidates + .map((candidate) => candidate?.trim()) + .find((candidate) => candidate && candidate.startsWith("/")); + if (absolute) return absolute; + + // Steam's app-details API can report a Flatpak Target as just "flatpak" + // even when the shortcut's canonical VDF executable is /usr/bin/flatpak. + // Keep the stored original executable absolute so SetShortcutExe and the + // generated dispatcher agree on the same direct transport. + if (target.transport.kind === "flatpak") return "/usr/bin/flatpak"; + return candidates.map((candidate) => candidate?.trim()).find(Boolean); +} + const DEFAULT_WORKAROUND_STATE: WorkaroundState = { dxvkFrameRate: 0, disableGamescopeWsi: true, @@ -151,7 +168,14 @@ export function useGameConfiguration() { throw new Error("Shortcut Target is already the managed wrapper but its original Target is unknown"); } const state = oldState || { ...DEFAULT_WORKAROUND_STATE }; - const originalExecutable = target.nonSteam ? (oldShortcutExe || current.target) : undefined; + const originalExecutable = target.nonSteam + ? selectShortcutExecutable( + target, + oldShortcutExe, + target.transport.kind === "flatpak" ? target.executable : undefined, + current.target, + ) + : undefined; const initialIntegration = target.nonSteam ? current.target === wrapperPath : hasWrapperLaunchIntegration(current.options, wrapperPath); @@ -170,7 +194,14 @@ export function useGameConfiguration() { const finalStateResult = await setWorkaroundState( target.appid, state, - target.nonSteam ? (integration.originalExecutable || originalExecutable || null) : null, + target.nonSteam + ? (selectShortcutExecutable( + target, + integration.originalExecutable, + originalExecutable, + target.transport.kind === "flatpak" ? target.executable : undefined, + ) || null) + : null, integration.commandTokenAdded, target.transport, ); @@ -184,7 +215,14 @@ export function useGameConfiguration() { appId, target.nonSteam, wrapperPath, - target.nonSteam ? (integration?.originalExecutable || originalExecutable) : undefined, + target.nonSteam + ? (selectShortcutExecutable( + target, + integration?.originalExecutable, + originalExecutable, + target.transport.kind === "flatpak" ? target.executable : undefined, + ) || undefined) + : undefined, integration?.commandTokenAdded ?? oldCommandTokenAdded, ); } catch (rollbackError) { @@ -264,6 +302,25 @@ export function useGameConfiguration() { else await removeTargetWorkarounds(target); return result.success; }, [ensureTargetFlatpakSupport, ensureTargetWorkarounds, load, removeTargetWorkarounds, targets, template]); + const enableAll = useCallback(async (): Promise => { + const available = targets.filter((target) => !target.configured && target.name); + if (available.length === 0) return; + + for (const target of available) { + if (!(await ensureTargetFlatpakSupport(target))) return; + if (!(await ensureTargetWorkarounds(target))) return; + const result = await updateGameConfig(target.appid, target.name, template); + if (!result.success) { + await removeTargetWorkarounds(target); + showErrorToast( + "Could not enable all games", + result.error || `Could not create a profile for ${target.name}`, + ); + return; + } + } + await load(); + }, [ensureTargetFlatpakSupport, ensureTargetWorkarounds, load, removeTargetWorkarounds, targets, template]); const repair = useCallback(async (appid: string): Promise => { const target = targets.find((item) => item.appid === appid); if (!target) return false; @@ -306,5 +363,5 @@ export function useGameConfiguration() { } }, [load, removeTargetWorkarounds, targets]); - return { config, games, targets, runningGame, selectedAppId, setSelectedAppId, save, enable, repair, resetSelected, resetAll, reload: load }; + return { config, games, targets, runningGame, selectedAppId, setSelectedAppId, save, enable, enableAll, repair, resetSelected, resetAll, reload: load }; } -- cgit v1.2.3 From b197e25b45d53c6c7175a45dd0b14642f2aab198 Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Thu, 10 Sep 2026 11:51:22 -0400 Subject: fixes for appimage and flatpak --- src/hooks/useGameConfiguration.ts | 39 ++++++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 17 deletions(-) (limited to 'src/hooks/useGameConfiguration.ts') diff --git a/src/hooks/useGameConfiguration.ts b/src/hooks/useGameConfiguration.ts index c70d589..e120426 100644 --- a/src/hooks/useGameConfiguration.ts +++ b/src/hooks/useGameConfiguration.ts @@ -48,11 +48,6 @@ function selectShortcutExecutable( .map((candidate) => candidate?.trim()) .find((candidate) => candidate && candidate.startsWith("/")); if (absolute) return absolute; - - // Steam's app-details API can report a Flatpak Target as just "flatpak" - // even when the shortcut's canonical VDF executable is /usr/bin/flatpak. - // Keep the stored original executable absolute so SetShortcutExe and the - // generated dispatcher agree on the same direct transport. if (target.transport.kind === "flatpak") return "/usr/bin/flatpak"; return candidates.map((candidate) => candidate?.trim()).find(Boolean); } @@ -158,25 +153,26 @@ export function useGameConfiguration() { const oldShortcutExe = existing.shortcut_exe || undefined; const oldCommandTokenAdded = existing.command_token_added === true; const oldTransport = existing.transport || target.transport; - if (target.nonSteam && oldState && current.target === wrapperPath && !oldShortcutExe) { + const usesShortcutTarget = target.nonSteam && target.transport.kind === "flatpak"; + if (usesShortcutTarget && oldState && current.target === wrapperPath && !oldShortcutExe) { throw new Error("Managed shortcut Target has no saved original executable"); } - if (target.nonSteam && oldState && current.target !== wrapperPath && current.target !== oldShortcutExe) { + if (usesShortcutTarget && oldState && current.target !== wrapperPath && current.target !== oldShortcutExe) { throw new Error("Shortcut Target changed externally; refusing to replace it"); } - if (target.nonSteam && !oldState && current.target === wrapperPath) { + if (usesShortcutTarget && !oldState && current.target === wrapperPath) { throw new Error("Shortcut Target is already the managed wrapper but its original Target is unknown"); } const state = oldState || { ...DEFAULT_WORKAROUND_STATE }; - const originalExecutable = target.nonSteam + const originalExecutable = usesShortcutTarget ? selectShortcutExecutable( target, oldShortcutExe, - target.transport.kind === "flatpak" ? target.executable : undefined, + target.executable, current.target, ) : undefined; - const initialIntegration = target.nonSteam + const initialIntegration = usesShortcutTarget ? current.target === wrapperPath : hasWrapperLaunchIntegration(current.options, wrapperPath); const initialStateResult = await setWorkaroundState( @@ -190,16 +186,22 @@ export function useGameConfiguration() { let integration: Awaited> | null = null; try { - integration = await installWrapperIntegration(appId, target.nonSteam, wrapperPath, oldCommandTokenAdded); + integration = await installWrapperIntegration( + appId, + target.nonSteam, + wrapperPath, + oldCommandTokenAdded, + target.transport.kind, + ); const finalStateResult = await setWorkaroundState( target.appid, state, - target.nonSteam + usesShortcutTarget ? (selectShortcutExecutable( target, integration.originalExecutable, originalExecutable, - target.transport.kind === "flatpak" ? target.executable : undefined, + target.executable, ) || null) : null, integration.commandTokenAdded, @@ -215,15 +217,16 @@ export function useGameConfiguration() { appId, target.nonSteam, wrapperPath, - target.nonSteam + usesShortcutTarget ? (selectShortcutExecutable( target, integration?.originalExecutable, originalExecutable, - target.transport.kind === "flatpak" ? target.executable : undefined, + target.executable, ) || undefined) : undefined, integration?.commandTokenAdded ?? oldCommandTokenAdded, + target.transport.kind, ); } catch (rollbackError) { showErrorToast("Workaround rollback failed", asError(rollbackError).message); @@ -257,6 +260,7 @@ export function useGameConfiguration() { const existing = await getWorkaroundState(target.appid); if (!existing.success) throw new Error(existing.error || "Could not read workaround state"); const wrapperPath = existing.wrapper_path || getDefaultWrapperPath(); + const usesShortcutTarget = target.nonSteam && target.transport.kind === "flatpak"; if (existing.state) { await removeWrapperIntegration( appId, @@ -264,10 +268,11 @@ export function useGameConfiguration() { wrapperPath, existing.shortcut_exe || undefined, existing.command_token_added === true, + target.transport.kind, ); } else { const current = await readSteamLaunchOptions(appId, target.nonSteam); - if (target.nonSteam && (current.target === wrapperPath || isLegacyWrapperToken(current.target))) { + if (usesShortcutTarget && (current.target === wrapperPath || isLegacyWrapperToken(current.target))) { throw new Error("Shortcut Target is a frame-generation wrapper but its original Target is unknown"); } await cleanupLegacySteamLaunchOptions(appId, target.nonSteam, wrapperPath); -- cgit v1.2.3 From 670f36e8cc75da9c8b1b174c24e722657bbf2a56 Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Thu, 10 Sep 2026 12:15:05 -0400 Subject: cleanup flatpak handles --- src/hooks/useGameConfiguration.ts | 176 +++++++++++--------------------------- 1 file changed, 52 insertions(+), 124 deletions(-) (limited to 'src/hooks/useGameConfiguration.ts') diff --git a/src/hooks/useGameConfiguration.ts b/src/hooks/useGameConfiguration.ts index e120426..3260019 100644 --- a/src/hooks/useGameConfiguration.ts +++ b/src/hooks/useGameConfiguration.ts @@ -3,7 +3,7 @@ import { useQuickAccessVisible } from "@decky/api"; import { Router } from "@decky/ui"; import { ensureFlatpakSupport, getGameConfigs, getInstalledGames, getWorkaroundState, removeWorkaroundState, repairFlatpakSupport, resetGameConfig, resetAllGameConfigs, setWorkaroundState, updateGameConfig, type GameConfigEntry, type GlobalConfig, type InstalledGame, type WorkaroundState } from "../api/lsfgApi"; import { ConfigurationData, getDefaults } from "../config/configSchema"; -import { cleanupLegacySteamLaunchOptions, getDefaultWrapperPath, hasWrapperLaunchIntegration, installWrapperIntegration, isLegacyWrapperToken, readSteamLaunchOptions, removeWrapperIntegration } from "../utils/steamLaunchOptions"; +import { getDefaultWrapperPath, installWrapperIntegration, removeWrapperIntegration } from "../utils/steamLaunchOptions"; import { showErrorToast } from "../utils/toastUtils"; export interface GameTarget extends InstalledGame { configured: boolean; } @@ -40,18 +40,6 @@ function mergeInstalledGames(backendGames: InstalledGame[], shortcutGames: Insta return Array.from(games.values()); } -function selectShortcutExecutable( - target: GameTarget, - ...candidates: Array -): string | undefined { - const absolute = candidates - .map((candidate) => candidate?.trim()) - .find((candidate) => candidate && candidate.startsWith("/")); - if (absolute) return absolute; - if (target.transport.kind === "flatpak") return "/usr/bin/flatpak"; - return candidates.map((candidate) => candidate?.trim()).find(Boolean); -} - const DEFAULT_WORKAROUND_STATE: WorkaroundState = { dxvkFrameRate: 0, disableGamescopeWsi: true, @@ -144,110 +132,59 @@ export function useGameConfiguration() { const ensureTargetWorkarounds = useCallback(async (target: GameTarget): Promise => { if (!installedGames.some((game) => game.appid === target.appid)) return true; const appId = Number(target.appid); + let integration: Awaited> | null = null; + let newState = false; + let stateWriteAttempted = false; + let wrapperPath = getDefaultWrapperPath(); try { const existing = await getWorkaroundState(target.appid); if (!existing.success) throw new Error(existing.error || "Could not read workaround state"); - const current = await readSteamLaunchOptions(appId, target.nonSteam); - const wrapperPath = existing.wrapper_path || getDefaultWrapperPath(); - const oldState = existing.state; - const oldShortcutExe = existing.shortcut_exe || undefined; - const oldCommandTokenAdded = existing.command_token_added === true; - const oldTransport = existing.transport || target.transport; - const usesShortcutTarget = target.nonSteam && target.transport.kind === "flatpak"; - if (usesShortcutTarget && oldState && current.target === wrapperPath && !oldShortcutExe) { - throw new Error("Managed shortcut Target has no saved original executable"); - } - if (usesShortcutTarget && oldState && current.target !== wrapperPath && current.target !== oldShortcutExe) { - throw new Error("Shortcut Target changed externally; refusing to replace it"); - } - if (usesShortcutTarget && !oldState && current.target === wrapperPath) { - throw new Error("Shortcut Target is already the managed wrapper but its original Target is unknown"); - } - const state = oldState || { ...DEFAULT_WORKAROUND_STATE }; - const originalExecutable = usesShortcutTarget - ? selectShortcutExecutable( - target, - oldShortcutExe, - target.executable, - current.target, - ) - : undefined; - const initialIntegration = usesShortcutTarget - ? current.target === wrapperPath - : hasWrapperLaunchIntegration(current.options, wrapperPath); - const initialStateResult = await setWorkaroundState( + wrapperPath = existing.wrapper_path || getDefaultWrapperPath(); + const state = existing.state || { ...DEFAULT_WORKAROUND_STATE }; + const commandTokenAdded = existing.command_token_added === true; + newState = !existing.state; + integration = await installWrapperIntegration( + appId, + target.nonSteam, + wrapperPath, + commandTokenAdded, + target.transport, + target.transport.kind === "flatpak" ? existing.shortcut_exe || undefined : undefined, + ); + stateWriteAttempted = true; + const saved = await setWorkaroundState( target.appid, state, - originalExecutable || null, - oldCommandTokenAdded, + integration.originalExecutable ?? null, + integration.commandTokenAdded, target.transport, ); - if (!initialStateResult.success) throw new Error(initialStateResult.error || "Could not create workaround state"); - - let integration: Awaited> | null = null; - try { - integration = await installWrapperIntegration( - appId, - target.nonSteam, - wrapperPath, - oldCommandTokenAdded, - target.transport.kind, - ); - const finalStateResult = await setWorkaroundState( - target.appid, - state, - usesShortcutTarget - ? (selectShortcutExecutable( - target, - integration.originalExecutable, - originalExecutable, - target.executable, - ) || null) - : null, - integration.commandTokenAdded, - target.transport, - ); - if (!finalStateResult.success) throw new Error(finalStateResult.error || "Could not finalize workaround state"); - return true; - } catch (error) { - let rollbackSucceeded = true; - if (!initialIntegration && integration) { - try { - await removeWrapperIntegration( - appId, - target.nonSteam, - wrapperPath, - usesShortcutTarget - ? (selectShortcutExecutable( - target, - integration?.originalExecutable, - originalExecutable, - target.executable, - ) || undefined) - : undefined, - integration?.commandTokenAdded ?? oldCommandTokenAdded, - target.transport.kind, - ); - } catch (rollbackError) { - showErrorToast("Workaround rollback failed", asError(rollbackError).message); - rollbackSucceeded = false; - } + if (!saved.success) throw new Error(saved.error || "Could not save workaround state"); + return true; + } catch (error) { + let rollbackSucceeded = true; + if (integration?.changed) { + try { + await removeWrapperIntegration( + appId, + target.nonSteam, + wrapperPath, + integration.originalExecutable, + integration.commandTokenAdded, + target.transport, + ); + } catch (rollbackError) { + showErrorToast("Workaround rollback failed", asError(rollbackError).message); + rollbackSucceeded = false; } - if (rollbackSucceeded) { - const restored = oldState - ? await setWorkaroundState( - target.appid, - oldState, - oldShortcutExe || null, - oldCommandTokenAdded, - oldTransport, - ) - : await removeWorkaroundState(target.appid); - if (!restored.success) throw new Error(restored.error || "Could not roll back workaround state"); + } + if (rollbackSucceeded && newState && stateWriteAttempted) { + const restored = await removeWorkaroundState(target.appid); + if (!restored.success) { + showErrorToast("Workaround rollback failed", restored.error || "Could not roll back workaround state"); + rollbackSucceeded = false; } - throw error; } - } catch (error) { showErrorToast("Could not initialize workarounds", asError(error).message); return false; } @@ -260,23 +197,14 @@ export function useGameConfiguration() { const existing = await getWorkaroundState(target.appid); if (!existing.success) throw new Error(existing.error || "Could not read workaround state"); const wrapperPath = existing.wrapper_path || getDefaultWrapperPath(); - const usesShortcutTarget = target.nonSteam && target.transport.kind === "flatpak"; - if (existing.state) { - await removeWrapperIntegration( - appId, - target.nonSteam, - wrapperPath, - existing.shortcut_exe || undefined, - existing.command_token_added === true, - target.transport.kind, - ); - } else { - const current = await readSteamLaunchOptions(appId, target.nonSteam); - if (usesShortcutTarget && (current.target === wrapperPath || isLegacyWrapperToken(current.target))) { - throw new Error("Shortcut Target is a frame-generation wrapper but its original Target is unknown"); - } - await cleanupLegacySteamLaunchOptions(appId, target.nonSteam, wrapperPath); - } + await removeWrapperIntegration( + appId, + target.nonSteam, + wrapperPath, + target.transport.kind === "flatpak" ? existing.shortcut_exe || undefined : undefined, + existing.command_token_added === true, + target.transport, + ); const removed = await removeWorkaroundState(target.appid); if (!removed.success) throw new Error(removed.error || "Could not remove workaround state"); return true; -- cgit v1.2.3 From 0df91099f4199dada43a7c804bb848722a493df2 Mon Sep 17 00:00:00 2001 From: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> Date: Thu, 10 Sep 2026 15:42:13 -0400 Subject: refactor: decouple game profiles from flatpak setup --- src/hooks/useGameConfiguration.ts | 57 ++++++++++----------------------------- 1 file changed, 14 insertions(+), 43 deletions(-) (limited to 'src/hooks/useGameConfiguration.ts') diff --git a/src/hooks/useGameConfiguration.ts b/src/hooks/useGameConfiguration.ts index 3260019..4131d0f 100644 --- a/src/hooks/useGameConfiguration.ts +++ b/src/hooks/useGameConfiguration.ts @@ -1,7 +1,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useQuickAccessVisible } from "@decky/api"; import { Router } from "@decky/ui"; -import { ensureFlatpakSupport, getGameConfigs, getInstalledGames, getWorkaroundState, removeWorkaroundState, repairFlatpakSupport, resetGameConfig, resetAllGameConfigs, setWorkaroundState, updateGameConfig, type GameConfigEntry, type GlobalConfig, type InstalledGame, type WorkaroundState } from "../api/lsfgApi"; +import { getGameConfigs, getInstalledGames, getWorkaroundState, removeWorkaroundState, resetGameConfig, resetAllGameConfigs, setWorkaroundState, updateGameConfig, type GameConfigEntry, type GlobalConfig, type InstalledGame, type WorkaroundState } from "../api/lsfgApi"; import { ConfigurationData, getDefaults } from "../config/configSchema"; import { getDefaultWrapperPath, installWrapperIntegration, removeWrapperIntegration } from "../utils/steamLaunchOptions"; import { showErrorToast } from "../utils/toastUtils"; @@ -23,7 +23,7 @@ async function getSteamShortcuts(): Promise { appid: String(appid >>> 0), name, nonSteam: true, - transport: { kind: "host" }, + directFlatpak: false, }]; }); } catch { @@ -80,6 +80,7 @@ export function useGameConfiguration() { previousQuickAccessVisible.current = quickAccessVisible; if (initialLoad || becameVisible) void load(); }, [load, quickAccessVisible]); + useEffect(() => { const poll = () => { if (!configsLoaded) return; @@ -90,7 +91,7 @@ export function useGameConfiguration() { const name = app.display_name || installed?.name; if (!name) return setRunningGame(null); setRunningGame((current) => current?.appid === appid ? current : { - ...(installed || { appid, name, nonSteam: false, transport: { kind: "host" } }), + ...(installed || { appid, name, nonSteam: false, directFlatpak: false }), name, configured: games.some((game) => game.appid === appid), }); @@ -99,6 +100,7 @@ export function useGameConfiguration() { const interval = window.setInterval(poll, 2000); return () => window.clearInterval(interval); }, [configsLoaded, games, installedGames]); + useEffect(() => { const appid = runningGame?.appid || null; if (appid !== previousRunningAppId.current) { @@ -109,26 +111,13 @@ export function useGameConfiguration() { const targets = useMemo(() => { const configured = installedGames.map((game) => ({ ...game, configured: games.some((item) => item.appid === game.appid) })); - for (const game of games) if (!configured.some((item) => item.appid === game.appid)) configured.push({ appid: game.appid, name: game.profile, nonSteam: false, transport: { kind: "host" }, configured: true }); + for (const game of games) if (!configured.some((item) => item.appid === game.appid)) configured.push({ appid: game.appid, name: game.profile, nonSteam: false, directFlatpak: false, configured: true }); if (runningGame && !configured.some((game) => game.appid === runningGame.appid)) configured.unshift(runningGame); return configured; }, [games, installedGames, runningGame]); const template = useMemo(() => ({ ...getDefaults(), ...globalConfig }), [globalConfig]); const config = games.find((game) => game.appid === selectedAppId)?.config || template; - const ensureTargetFlatpakSupport = useCallback(async (target: GameTarget): Promise => { - if (target.transport.kind !== "flatpak") return true; - const result = await ensureFlatpakSupport(target.transport.flatpakAppId); - if (!result.success || result.support_status !== "ready") { - showErrorToast( - "Flatpak support unavailable", - result.error || result.message || "The required Flatpak runtime extension is not ready", - ); - return false; - } - return true; - }, []); - const ensureTargetWorkarounds = useCallback(async (target: GameTarget): Promise => { if (!installedGames.some((game) => game.appid === target.appid)) return true; const appId = Number(target.appid); @@ -148,16 +137,13 @@ export function useGameConfiguration() { target.nonSteam, wrapperPath, commandTokenAdded, - target.transport, - target.transport.kind === "flatpak" ? existing.shortcut_exe || undefined : undefined, + target.directFlatpak === true, ); stateWriteAttempted = true; const saved = await setWorkaroundState( target.appid, state, - integration.originalExecutable ?? null, integration.commandTokenAdded, - target.transport, ); if (!saved.success) throw new Error(saved.error || "Could not save workaround state"); return true; @@ -169,9 +155,8 @@ export function useGameConfiguration() { appId, target.nonSteam, wrapperPath, - integration.originalExecutable, integration.commandTokenAdded, - target.transport, + target.directFlatpak === true, ); } catch (rollbackError) { showErrorToast("Workaround rollback failed", asError(rollbackError).message); @@ -201,9 +186,8 @@ export function useGameConfiguration() { appId, target.nonSteam, wrapperPath, - target.transport.kind === "flatpak" ? existing.shortcut_exe || undefined : undefined, existing.command_token_added === true, - target.transport, + target.directFlatpak === true, ); const removed = await removeWorkaroundState(target.appid); if (!removed.success) throw new Error(removed.error || "Could not remove workaround state"); @@ -217,9 +201,6 @@ export function useGameConfiguration() { const save = useCallback(async (next: ConfigurationData, cleanupLaunchOptions = false) => { const selectedTarget = targets.find((target) => target.appid === selectedAppId); if (!selectedTarget?.name) return; - // The profile owns its wrapper integration. Keep this check on every - // configuration save so an external edit is detected before the profile - // is changed; toggles update the sidecar only. if (cleanupLaunchOptions && !(await ensureTargetWorkarounds(selectedTarget))) return; const result = await updateGameConfig(selectedAppId, selectedTarget.name, next); if (result.success) await load(); @@ -228,19 +209,17 @@ export function useGameConfiguration() { const enable = useCallback(async (appid: string) => { const target = targets.find((item) => item.appid === appid); if (!target?.name) return false; - if (!(await ensureTargetFlatpakSupport(target))) return false; if (!(await ensureTargetWorkarounds(target))) return false; const result = await updateGameConfig(appid, target.name, template); if (result.success) await load(); else await removeTargetWorkarounds(target); return result.success; - }, [ensureTargetFlatpakSupport, ensureTargetWorkarounds, load, removeTargetWorkarounds, targets, template]); + }, [ensureTargetWorkarounds, load, removeTargetWorkarounds, targets, template]); + const enableAll = useCallback(async (): Promise => { const available = targets.filter((target) => !target.configured && target.name); if (available.length === 0) return; - for (const target of available) { - if (!(await ensureTargetFlatpakSupport(target))) return; if (!(await ensureTargetWorkarounds(target))) return; const result = await updateGameConfig(target.appid, target.name, template); if (!result.success) { @@ -253,20 +232,11 @@ export function useGameConfiguration() { } } await load(); - }, [ensureTargetFlatpakSupport, ensureTargetWorkarounds, load, removeTargetWorkarounds, targets, template]); + }, [ensureTargetWorkarounds, load, removeTargetWorkarounds, targets, template]); + const repair = useCallback(async (appid: string): Promise => { const target = targets.find((item) => item.appid === appid); if (!target) return false; - if (target.transport.kind === "flatpak") { - const support = await repairFlatpakSupport(target.transport.flatpakAppId); - if (!support.success || support.support_status !== "ready") { - showErrorToast( - "Flatpak support unavailable", - support.error || support.message || "The required Flatpak runtime extension is not ready", - ); - return false; - } - } const success = await ensureTargetWorkarounds(target); if (success) await load(); return success; @@ -284,6 +254,7 @@ export function useGameConfiguration() { } } }, [load, removeTargetWorkarounds, selectedAppId, targets]); + const resetAll = useCallback(async () => { for (const target of targets.filter((item) => item.configured)) { if (!(await removeTargetWorkarounds(target))) return; -- cgit v1.2.3 From e6d6ec2a0944be5f3d348e06c5031c94159910df Mon Sep 17 00:00:00 2001 From: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> Date: Thu, 10 Sep 2026 16:36:02 -0400 Subject: refactor: keep steam profiles flatpak agnostic --- src/hooks/useGameConfiguration.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) (limited to 'src/hooks/useGameConfiguration.ts') diff --git a/src/hooks/useGameConfiguration.ts b/src/hooks/useGameConfiguration.ts index 4131d0f..8d56a62 100644 --- a/src/hooks/useGameConfiguration.ts +++ b/src/hooks/useGameConfiguration.ts @@ -23,7 +23,6 @@ async function getSteamShortcuts(): Promise { appid: String(appid >>> 0), name, nonSteam: true, - directFlatpak: false, }]; }); } catch { @@ -91,7 +90,7 @@ export function useGameConfiguration() { const name = app.display_name || installed?.name; if (!name) return setRunningGame(null); setRunningGame((current) => current?.appid === appid ? current : { - ...(installed || { appid, name, nonSteam: false, directFlatpak: false }), + ...(installed || { appid, name, nonSteam: false }), name, configured: games.some((game) => game.appid === appid), }); @@ -111,7 +110,7 @@ export function useGameConfiguration() { const targets = useMemo(() => { const configured = installedGames.map((game) => ({ ...game, configured: games.some((item) => item.appid === game.appid) })); - for (const game of games) if (!configured.some((item) => item.appid === game.appid)) configured.push({ appid: game.appid, name: game.profile, nonSteam: false, directFlatpak: false, configured: true }); + for (const game of games) if (!configured.some((item) => item.appid === game.appid)) configured.push({ appid: game.appid, name: game.profile, nonSteam: false, configured: true }); if (runningGame && !configured.some((game) => game.appid === runningGame.appid)) configured.unshift(runningGame); return configured; }, [games, installedGames, runningGame]); @@ -137,7 +136,6 @@ export function useGameConfiguration() { target.nonSteam, wrapperPath, commandTokenAdded, - target.directFlatpak === true, ); stateWriteAttempted = true; const saved = await setWorkaroundState( @@ -156,7 +154,6 @@ export function useGameConfiguration() { target.nonSteam, wrapperPath, integration.commandTokenAdded, - target.directFlatpak === true, ); } catch (rollbackError) { showErrorToast("Workaround rollback failed", asError(rollbackError).message); @@ -187,7 +184,6 @@ export function useGameConfiguration() { target.nonSteam, wrapperPath, existing.command_token_added === true, - target.directFlatpak === true, ); const removed = await removeWorkaroundState(target.appid); if (!removed.success) throw new Error(removed.error || "Could not remove workaround state"); -- cgit v1.2.3 From 817cdf3d5344e961df05f408354d0387c731f5ce Mon Sep 17 00:00:00 2001 From: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> Date: Thu, 10 Sep 2026 16:46:19 -0400 Subject: fix: derive now playing config from running app --- src/hooks/useGameConfiguration.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'src/hooks/useGameConfiguration.ts') diff --git a/src/hooks/useGameConfiguration.ts b/src/hooks/useGameConfiguration.ts index 8d56a62..17eb867 100644 --- a/src/hooks/useGameConfiguration.ts +++ b/src/hooks/useGameConfiguration.ts @@ -116,6 +116,9 @@ export function useGameConfiguration() { }, [games, installedGames, runningGame]); const template = useMemo(() => ({ ...getDefaults(), ...globalConfig }), [globalConfig]); const config = games.find((game) => game.appid === selectedAppId)?.config || template; + const runningConfig = runningGame + ? games.find((game) => game.appid === runningGame.appid)?.config || template + : template; const ensureTargetWorkarounds = useCallback(async (target: GameTarget): Promise => { if (!installedGames.some((game) => game.appid === target.appid)) return true; @@ -263,5 +266,5 @@ export function useGameConfiguration() { } }, [load, removeTargetWorkarounds, targets]); - return { config, games, targets, runningGame, selectedAppId, setSelectedAppId, save, enable, enableAll, repair, resetSelected, resetAll, reload: load }; + return { config, runningConfig, games, targets, runningGame, selectedAppId, setSelectedAppId, save, enable, enableAll, repair, resetSelected, resetAll, reload: load }; } -- cgit v1.2.3 From 2a1c019b328bac6a2e8c12debeb1c832f8193bda Mon Sep 17 00:00:00 2001 From: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> Date: Thu, 10 Sep 2026 16:47:17 -0400 Subject: fix: make steam profile saves app scoped --- src/hooks/useGameConfiguration.ts | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) (limited to 'src/hooks/useGameConfiguration.ts') diff --git a/src/hooks/useGameConfiguration.ts b/src/hooks/useGameConfiguration.ts index 17eb867..2e39f5e 100644 --- a/src/hooks/useGameConfiguration.ts +++ b/src/hooks/useGameConfiguration.ts @@ -89,11 +89,19 @@ export function useGameConfiguration() { const installed = installedGames.find((game) => game.appid === appid); const name = app.display_name || installed?.name; if (!name) return setRunningGame(null); - setRunningGame((current) => current?.appid === appid ? current : { + const next: GameTarget = { ...(installed || { appid, name, nonSteam: false }), name, configured: games.some((game) => game.appid === appid), - }); + }; + setRunningGame((current) => ( + current?.appid === next.appid + && current.name === next.name + && current.nonSteam === next.nonSteam + && current.configured === next.configured + ? current + : next + )); }; poll(); const interval = window.setInterval(poll, 2000); @@ -197,13 +205,22 @@ export function useGameConfiguration() { } }, [installedGames]); - const save = useCallback(async (next: ConfigurationData, cleanupLaunchOptions = false) => { - const selectedTarget = targets.find((target) => target.appid === selectedAppId); - if (!selectedTarget?.name) return; - if (cleanupLaunchOptions && !(await ensureTargetWorkarounds(selectedTarget))) return; - const result = await updateGameConfig(selectedAppId, selectedTarget.name, next); + const saveFor = useCallback(async (appid: string, next: ConfigurationData, cleanupLaunchOptions = false) => { + const target = targets.find((item) => item.appid === appid); + if (!target?.name) return false; + if (cleanupLaunchOptions && !(await ensureTargetWorkarounds(target))) return false; + const result = await updateGameConfig(appid, target.name, next); if (result.success) await load(); - }, [ensureTargetWorkarounds, load, selectedAppId, targets]); + return result.success; + }, [ensureTargetWorkarounds, load, targets]); + + const save = useCallback( + async (next: ConfigurationData, cleanupLaunchOptions = false) => { + if (!selectedAppId) return false; + return saveFor(selectedAppId, next, cleanupLaunchOptions); + }, + [saveFor, selectedAppId], + ); const enable = useCallback(async (appid: string) => { const target = targets.find((item) => item.appid === appid); @@ -266,5 +283,5 @@ export function useGameConfiguration() { } }, [load, removeTargetWorkarounds, targets]); - return { config, runningConfig, games, targets, runningGame, selectedAppId, setSelectedAppId, save, enable, enableAll, repair, resetSelected, resetAll, reload: load }; + return { config, runningConfig, games, targets, runningGame, selectedAppId, setSelectedAppId, save, saveFor, enable, enableAll, repair, resetSelected, resetAll, reload: load }; } -- cgit v1.2.3 From 954b2abc47d8772211cb8ecee523900f823184e8 Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Fri, 11 Sep 2026 11:08:12 -0400 Subject: better uninstall cleanup --- src/hooks/useGameConfiguration.ts | 46 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) (limited to 'src/hooks/useGameConfiguration.ts') diff --git a/src/hooks/useGameConfiguration.ts b/src/hooks/useGameConfiguration.ts index 2e39f5e..fd8dfb1 100644 --- a/src/hooks/useGameConfiguration.ts +++ b/src/hooks/useGameConfiguration.ts @@ -1,7 +1,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useQuickAccessVisible } from "@decky/api"; import { Router } from "@decky/ui"; -import { getGameConfigs, getInstalledGames, getWorkaroundState, removeWorkaroundState, resetGameConfig, resetAllGameConfigs, setWorkaroundState, updateGameConfig, type GameConfigEntry, type GlobalConfig, type InstalledGame, type WorkaroundState } from "../api/lsfgApi"; +import { getGameConfigs, getInstalledGames, getWorkaroundApps, getWorkaroundState, removeWorkaroundState, resetGameConfig, resetAllGameConfigs, setWorkaroundState, updateGameConfig, updateGlobalConfig as saveGlobalConfig, type GameConfigEntry, type GlobalConfig, type InstalledGame, type WorkaroundState } from "../api/lsfgApi"; import { ConfigurationData, getDefaults } from "../config/configSchema"; import { getDefaultWrapperPath, installWrapperIntegration, removeWrapperIntegration } from "../utils/steamLaunchOptions"; import { showErrorToast } from "../utils/toastUtils"; @@ -153,6 +153,7 @@ export function useGameConfiguration() { target.appid, state, integration.commandTokenAdded, + target.nonSteam, ); if (!saved.success) throw new Error(saved.error || "Could not save workaround state"); return true; @@ -205,6 +206,40 @@ export function useGameConfiguration() { } }, [installedGames]); + const cleanupAllWorkarounds = useCallback(async (): Promise => { + try { + const result = await getWorkaroundApps(); + if (!result.success) throw new Error(result.error || "Could not read workaround state"); + const targetsByAppId = new Map(targets.map((target) => [target.appid, target])); + const cleaned = new Set(); + const wrapperPath = result.wrapper_path || getDefaultWrapperPath(); + + for (const entry of result.apps || []) { + const target = targetsByAppId.get(entry.appid); + const nonSteam = target?.nonSteam ?? entry.non_steam; + await removeWrapperIntegration( + Number(entry.appid), + nonSteam, + wrapperPath, + entry.command_token_added, + ); + const removed = await removeWorkaroundState(entry.appid); + if (!removed.success) throw new Error(removed.error || "Could not remove workaround state"); + cleaned.add(entry.appid); + } + + // Also clean configured targets whose sidecar entry was lost. This + // removes an old wrapper and only the plugin-managed launch pieces. + for (const target of targets.filter((item) => item.configured && installedGames.some((game) => game.appid === item.appid))) { + if (!cleaned.has(target.appid) && !(await removeTargetWorkarounds(target))) return false; + } + return true; + } catch (error) { + showErrorToast("Could not clean up game launch options", asError(error).message); + return false; + } + }, [installedGames, removeTargetWorkarounds, targets]); + const saveFor = useCallback(async (appid: string, next: ConfigurationData, cleanupLaunchOptions = false) => { const target = targets.find((item) => item.appid === appid); if (!target?.name) return false; @@ -222,6 +257,13 @@ export function useGameConfiguration() { [saveFor, selectedAppId], ); + const updateGlobal = useCallback(async (next: GlobalConfig): Promise => { + const result = await saveGlobalConfig(next); + if (!result.success) return false; + setGlobalConfig(result.global_config || next); + return true; + }, []); + const enable = useCallback(async (appid: string) => { const target = targets.find((item) => item.appid === appid); if (!target?.name) return false; @@ -283,5 +325,5 @@ export function useGameConfiguration() { } }, [load, removeTargetWorkarounds, targets]); - return { config, runningConfig, games, targets, runningGame, selectedAppId, setSelectedAppId, save, saveFor, enable, enableAll, repair, resetSelected, resetAll, reload: load }; + return { config, runningConfig, globalConfig, targets, runningGame, selectedAppId, setSelectedAppId, save, saveFor, updateGlobal, enable, enableAll, repair, resetSelected, resetAll, cleanupAllWorkarounds, reload: load }; } -- cgit v1.2.3 From f3074fbe1427411dc3b5597d2e87918383299e3f Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Fri, 11 Sep 2026 13:47:52 -0400 Subject: feat: non steam tab, faster bulk actions --- src/hooks/useGameConfiguration.ts | 139 ++++++++++++++++++++++++++------------ 1 file changed, 94 insertions(+), 45 deletions(-) (limited to 'src/hooks/useGameConfiguration.ts') diff --git a/src/hooks/useGameConfiguration.ts b/src/hooks/useGameConfiguration.ts index fd8dfb1..deb68ff 100644 --- a/src/hooks/useGameConfiguration.ts +++ b/src/hooks/useGameConfiguration.ts @@ -1,12 +1,13 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useQuickAccessVisible } from "@decky/api"; import { Router } from "@decky/ui"; -import { getGameConfigs, getInstalledGames, getWorkaroundApps, getWorkaroundState, removeWorkaroundState, resetGameConfig, resetAllGameConfigs, setWorkaroundState, updateGameConfig, updateGlobalConfig as saveGlobalConfig, type GameConfigEntry, type GlobalConfig, type InstalledGame, type WorkaroundState } from "../api/lsfgApi"; +import { getGameConfigs, getInstalledGames, getWorkaroundApps, getWorkaroundState, removeWorkaroundState, resetGameConfig, resetGameConfigs, setWorkaroundState, updateGameConfig, updateGlobalConfig as saveGlobalConfig, type GameConfigEntry, type GlobalConfig, type InstalledGame, type WorkaroundApp, type WorkaroundState } from "../api/lsfgApi"; import { ConfigurationData, getDefaults } from "../config/configSchema"; import { getDefaultWrapperPath, installWrapperIntegration, removeWrapperIntegration } from "../utils/steamLaunchOptions"; +import { getTargetSource, mergeGameTargets, type GameTarget, type KnownGameSource } from "../utils/gameTargets"; import { showErrorToast } from "../utils/toastUtils"; -export interface GameTarget extends InstalledGame { configured: boolean; } +export type { GameSource, GameTarget, KnownGameSource } from "../utils/gameTargets"; async function getSteamShortcuts(): Promise { const apps = (globalThis as any).SteamClient?.Apps; @@ -56,20 +57,29 @@ export function useGameConfiguration() { const [games, setGames] = useState([]); const [globalConfig, setGlobalConfig] = useState({ dll: "", no_fp16: false }); const [installedGames, setInstalledGames] = useState([]); + const [workaroundApps, setWorkaroundApps] = useState([]); const [configsLoaded, setConfigsLoaded] = useState(false); const [selectedAppId, setSelectedAppId] = useState(""); const [runningGame, setRunningGame] = useState(null); + const [bulkOperationBusy, setBulkOperationBusy] = useState(false); + const bulkOperationLock = useRef(false); const previousRunningAppId = useRef(null); const previousQuickAccessVisible = useRef(null); const quickAccessVisible = useQuickAccessVisible(); const load = useCallback(async () => { - const [result, installed, shortcuts] = await Promise.all([getGameConfigs(), getInstalledGames(), getSteamShortcuts()]); + const [result, installed, shortcuts, workaroundResult] = await Promise.all([ + getGameConfigs(), + getInstalledGames(), + getSteamShortcuts(), + getWorkaroundApps(), + ]); if (result.success) { setGlobalConfig(result.global_config || { dll: "", no_fp16: false }); setGames(result.games || []); } setInstalledGames(mergeInstalledGames(installed.success ? installed.games || [] : [], shortcuts)); + setWorkaroundApps(workaroundResult.success ? workaroundResult.apps || [] : []); setConfigsLoaded(true); }, []); @@ -89,15 +99,19 @@ export function useGameConfiguration() { const installed = installedGames.find((game) => game.appid === appid); const name = app.display_name || installed?.name; if (!name) return setRunningGame(null); + const source = getTargetSource(appid, installedGames, workaroundApps); const next: GameTarget = { - ...(installed || { appid, name, nonSteam: false }), + ...(installed || { appid, name, nonSteam: source === "nonSteam" }), name, + nonSteam: source === "nonSteam", + source, configured: games.some((game) => game.appid === appid), }; setRunningGame((current) => ( current?.appid === next.appid && current.name === next.name && current.nonSteam === next.nonSteam + && current.source === next.source && current.configured === next.configured ? current : next @@ -106,7 +120,7 @@ export function useGameConfiguration() { poll(); const interval = window.setInterval(poll, 2000); return () => window.clearInterval(interval); - }, [configsLoaded, games, installedGames]); + }, [configsLoaded, games, installedGames, workaroundApps]); useEffect(() => { const appid = runningGame?.appid || null; @@ -117,11 +131,8 @@ export function useGameConfiguration() { }, [runningGame?.appid]); const targets = useMemo(() => { - const configured = installedGames.map((game) => ({ ...game, configured: games.some((item) => item.appid === game.appid) })); - for (const game of games) if (!configured.some((item) => item.appid === game.appid)) configured.push({ appid: game.appid, name: game.profile, nonSteam: false, configured: true }); - if (runningGame && !configured.some((game) => game.appid === runningGame.appid)) configured.unshift(runningGame); - return configured; - }, [games, installedGames, runningGame]); + return mergeGameTargets(games, installedGames, workaroundApps, runningGame); + }, [games, installedGames, runningGame, workaroundApps]); const template = useMemo(() => ({ ...getDefaults(), ...globalConfig }), [globalConfig]); const config = games.find((game) => game.appid === selectedAppId)?.config || template; const runningConfig = runningGame @@ -129,6 +140,10 @@ export function useGameConfiguration() { : template; const ensureTargetWorkarounds = useCallback(async (target: GameTarget): Promise => { + if (target.source === "unknown") { + showErrorToast("Could not initialize workarounds", "The target source is unknown; re-discover the game before enabling it"); + return false; + } if (!installedGames.some((game) => game.appid === target.appid)) return true; const appId = Number(target.appid); let integration: Awaited> | null = null; @@ -185,18 +200,21 @@ export function useGameConfiguration() { }, [installedGames]); const removeTargetWorkarounds = useCallback(async (target: GameTarget): Promise => { - if (!installedGames.some((game) => game.appid === target.appid)) return true; + const installed = installedGames.some((game) => game.appid === target.appid); + if (target.source === "unknown" && installed) return true; const appId = Number(target.appid); try { const existing = await getWorkaroundState(target.appid); if (!existing.success) throw new Error(existing.error || "Could not read workaround state"); const wrapperPath = existing.wrapper_path || getDefaultWrapperPath(); - await removeWrapperIntegration( - appId, - target.nonSteam, - wrapperPath, - existing.command_token_added === true, - ); + if (installed) { + await removeWrapperIntegration( + appId, + target.nonSteam, + wrapperPath, + existing.command_token_added === true, + ); + } const removed = await removeWorkaroundState(target.appid); if (!removed.success) throw new Error(removed.error || "Could not remove workaround state"); return true; @@ -206,20 +224,29 @@ export function useGameConfiguration() { } }, [installedGames]); + const acquireBulkOperation = useCallback(() => { + if (bulkOperationLock.current) return false; + bulkOperationLock.current = true; + setBulkOperationBusy(true); + return true; + }, []); + + const releaseBulkOperation = useCallback(() => { + bulkOperationLock.current = false; + setBulkOperationBusy(false); + }, []); + const cleanupAllWorkarounds = useCallback(async (): Promise => { try { const result = await getWorkaroundApps(); if (!result.success) throw new Error(result.error || "Could not read workaround state"); - const targetsByAppId = new Map(targets.map((target) => [target.appid, target])); const cleaned = new Set(); const wrapperPath = result.wrapper_path || getDefaultWrapperPath(); for (const entry of result.apps || []) { - const target = targetsByAppId.get(entry.appid); - const nonSteam = target?.nonSteam ?? entry.non_steam; await removeWrapperIntegration( Number(entry.appid), - nonSteam, + entry.non_steam, wrapperPath, entry.command_token_added, ); @@ -274,23 +301,32 @@ export function useGameConfiguration() { return result.success; }, [ensureTargetWorkarounds, load, removeTargetWorkarounds, targets, template]); - const enableAll = useCallback(async (): Promise => { - const available = targets.filter((target) => !target.configured && target.name); - if (available.length === 0) return; - for (const target of available) { - if (!(await ensureTargetWorkarounds(target))) return; - const result = await updateGameConfig(target.appid, target.name, template); - if (!result.success) { - await removeTargetWorkarounds(target); - showErrorToast( - "Could not enable all games", - result.error || `Could not create a profile for ${target.name}`, - ); - return; + const enableAll = useCallback(async (source: KnownGameSource): Promise => { + if (!acquireBulkOperation()) return; + try { + const available = targets.filter((target) => target.source === source && !target.configured && target.name); + if (available.length === 0) return; + for (const target of available) { + if (!(await ensureTargetWorkarounds(target))) { + await load(); + return; + } + const result = await updateGameConfig(target.appid, target.name, template); + if (!result.success) { + await removeTargetWorkarounds(target); + showErrorToast( + "Could not enable all games", + result.error || `Could not create a profile for ${target.name}`, + ); + await load(); + return; + } } + await load(); + } finally { + releaseBulkOperation(); } - await load(); - }, [ensureTargetWorkarounds, load, removeTargetWorkarounds, targets, template]); + }, [acquireBulkOperation, ensureTargetWorkarounds, load, removeTargetWorkarounds, releaseBulkOperation, targets, template]); const repair = useCallback(async (appid: string): Promise => { const target = targets.find((item) => item.appid === appid); @@ -313,17 +349,30 @@ export function useGameConfiguration() { } }, [load, removeTargetWorkarounds, selectedAppId, targets]); - const resetAll = useCallback(async () => { - for (const target of targets.filter((item) => item.configured)) { - if (!(await removeTargetWorkarounds(target))) return; - } - const result = await resetAllGameConfigs(); - if (result.success) { - setRunningGame((current) => current ? { ...current, configured: false } : current); + const resetAll = useCallback(async (source: KnownGameSource) => { + if (!acquireBulkOperation()) return; + try { + const selectedTargets = targets.filter((item) => item.configured && item.source === source); + if (selectedTargets.length === 0) return; + for (const target of selectedTargets) { + if (!(await removeTargetWorkarounds(target))) { + await load(); + return; + } + } + const result = await resetGameConfigs(selectedTargets.map((target) => target.appid)); + if (!result.success) { + showErrorToast("Could not remove all profiles", result.error || "Could not remove the selected profiles"); + await load(); + return; + } + setRunningGame((current) => current?.source === source ? { ...current, configured: false } : current); setSelectedAppId(""); await load(); + } finally { + releaseBulkOperation(); } - }, [load, removeTargetWorkarounds, targets]); + }, [acquireBulkOperation, load, removeTargetWorkarounds, releaseBulkOperation, targets]); - return { config, runningConfig, globalConfig, targets, runningGame, selectedAppId, setSelectedAppId, save, saveFor, updateGlobal, enable, enableAll, repair, resetSelected, resetAll, cleanupAllWorkarounds, reload: load }; + return { config, runningConfig, globalConfig, targets, runningGame, selectedAppId, setSelectedAppId, save, saveFor, updateGlobal, enable, enableAll, repair, resetSelected, resetAll, bulkOperationBusy, cleanupAllWorkarounds, reload: load }; } -- cgit v1.2.3