summaryrefslogtreecommitdiff
path: root/src/hooks
diff options
context:
space:
mode:
authorxXJSONDeruloXx <danielhimebauch@gmail.com>2026-09-09 19:16:30 -0400
committerxXJSONDeruloXx <danielhimebauch@gmail.com>2026-09-09 20:20:58 -0400
commitcc1e6f47dd9838b066822162a607d2859c043aff (patch)
tree532915c1e98c068ab8324d71a0dab160bb53de68 /src/hooks
parent102a4a0f0ce4af303a12e7f6f1a4454f4e572a17 (diff)
downloaddecky-lsfg-vk-cc1e6f47dd9838b066822162a607d2859c043aff.tar.gz
decky-lsfg-vk-cc1e6f47dd9838b066822162a607d2859c043aff.zip
refactor: unify flatpak targets with steam profiles
Diffstat (limited to 'src/hooks')
-rw-r--r--src/hooks/useGameConfiguration.ts78
-rw-r--r--src/hooks/usePerAppWorkarounds.ts24
2 files changed, 75 insertions, 27 deletions
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<InstalledGame[]> {
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<InstalledGame[]> {
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<GameTarget[]>(() => {
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<boolean> => {
+ 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<boolean> => {
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<void> => {
- 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<boolean> => {
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 };
}
diff --git a/src/hooks/usePerAppWorkarounds.ts b/src/hooks/usePerAppWorkarounds.ts
index 9e283db..c7413b0 100644
--- a/src/hooks/usePerAppWorkarounds.ts
+++ b/src/hooks/usePerAppWorkarounds.ts
@@ -3,6 +3,7 @@ import {
getWorkaroundState,
removeWorkaroundState,
setWorkaroundState,
+ type TargetTransport,
type WorkaroundState,
} from "../api/lsfgApi";
import {
@@ -45,6 +46,7 @@ export interface WorkaroundSnapshot {
integrationInstalled: boolean;
commandTokenAdded: boolean;
shortcutExe?: string | null;
+ transport: TargetTransport;
}
interface PerAppWorkarounds {
@@ -85,12 +87,14 @@ function makeSnapshot(
integrationInstalled: integrationIsInstalled(steam, nonSteam, wrapperPath),
commandTokenAdded: result.command_token_added === true,
shortcutExe: result.shortcut_exe,
+ transport: result.transport || { kind: "host" },
};
}
async function adoptWorkaroundState(
appId: string,
nonSteam: boolean,
+ transport: TargetTransport,
steam: SteamLaunchOptionsSnapshot,
wrapperPath: string,
): Promise<WorkaroundSnapshot> {
@@ -98,7 +102,13 @@ async function adoptWorkaroundState(
throw new Error("Shortcut Target is a wrapper but its original Target is unknown");
}
const originalExecutable = nonSteam ? steam.target : null;
- const initial = await setWorkaroundState(appId, DEFAULT_WORKAROUND_STATE, originalExecutable, false);
+ const initial = await setWorkaroundState(
+ appId,
+ DEFAULT_WORKAROUND_STATE,
+ originalExecutable,
+ false,
+ transport,
+ );
if (!initial.success) throw new Error(initial.error || "Could not create workaround state");
let integration: Awaited<ReturnType<typeof installWrapperIntegration>> | null = null;
try {
@@ -112,6 +122,7 @@ async function adoptWorkaroundState(
DEFAULT_WORKAROUND_STATE,
nonSteam ? (integration.originalExecutable || originalExecutable) : null,
integration.commandTokenAdded,
+ transport,
);
if (!finalized.success) throw new Error(finalized.error || "Could not finalize workaround state");
return makeSnapshot(integration.snapshot, finalized, nonSteam);
@@ -139,7 +150,11 @@ async function adoptWorkaroundState(
}
}
-export function usePerAppWorkarounds(appId: string, nonSteam: boolean): PerAppWorkarounds {
+export function usePerAppWorkarounds(
+ appId: string,
+ nonSteam: boolean,
+ transport: TargetTransport = { kind: "host" },
+): PerAppWorkarounds {
const [status, setStatus] = useState<WorkaroundLoadStatus>("loading");
const [snapshot, setSnapshot] = useState<WorkaroundSnapshot | null>(null);
const [error, setError] = useState<string | null>(null);
@@ -156,12 +171,13 @@ export function usePerAppWorkarounds(appId: string, nonSteam: boolean): PerAppWo
return adoptWorkaroundState(
appId,
nonSteam,
+ transport,
steam,
result.wrapper_path || getDefaultWrapperPath(),
);
}
return makeSnapshot(steam, result, nonSteam);
- }, [appId, nonSteam, numericAppId]);
+ }, [appId, nonSteam, numericAppId, transport]);
const applySnapshot = useCallback((next: WorkaroundSnapshot) => {
setSnapshot(next);
@@ -236,6 +252,7 @@ export function usePerAppWorkarounds(appId: string, nonSteam: boolean): PerAppWo
nextState,
current.shortcutExe ?? null,
current.commandTokenAdded,
+ current.transport,
);
if (!result.success || !result.state) throw new Error(result.error || "Could not save workaround state");
applySnapshot({
@@ -245,6 +262,7 @@ export function usePerAppWorkarounds(appId: string, nonSteam: boolean): PerAppWo
wrapperOwned: result.wrapper_owned === true,
shortcutExe: result.shortcut_exe,
commandTokenAdded: result.command_token_added === true,
+ transport: result.transport || current.transport,
});
return true;
} catch (updateError) {