summaryrefslogtreecommitdiff
path: root/src/hooks
diff options
context:
space:
mode:
Diffstat (limited to 'src/hooks')
-rw-r--r--src/hooks/useFlatpakConfiguration.ts108
-rw-r--r--src/hooks/useGameConfiguration.ts91
-rw-r--r--src/hooks/usePerAppWorkarounds.ts47
3 files changed, 154 insertions, 92 deletions
diff --git a/src/hooks/useFlatpakConfiguration.ts b/src/hooks/useFlatpakConfiguration.ts
new file mode 100644
index 0000000..d5e6f7e
--- /dev/null
+++ b/src/hooks/useFlatpakConfiguration.ts
@@ -0,0 +1,108 @@
+import { useCallback, useEffect, useMemo, useState } from "react";
+import {
+ enableFlatpakApp,
+ getFlatpakApps,
+ getRunningFlatpakApps,
+ removeFlatpakApp,
+ setFlatpakWorkaroundState,
+ updateFlatpakConfig,
+ type FlatpakApp,
+ type LsfgConfig,
+ type RunningFlatpakApp,
+ type WorkaroundState,
+} from "../api/lsfgApi";
+import { showErrorToast } from "../utils/toastUtils";
+
+export function useFlatpakConfiguration(enabled: boolean) {
+ const [apps, setApps] = useState<FlatpakApp[]>([]);
+ const [runningApps, setRunningApps] = useState<RunningFlatpakApp[]>([]);
+ const [loading, setLoading] = useState(false);
+ const [busyAppId, setBusyAppId] = useState("");
+
+ const reload = useCallback(async () => {
+ if (!enabled) {
+ setApps([]);
+ return;
+ }
+ setLoading(true);
+ try {
+ const result = await getFlatpakApps();
+ if (!result.success) throw new Error(result.error || "Could not list Flatpak applications");
+ setApps(result.apps || []);
+ } catch (error) {
+ showErrorToast("Flatpak unavailable", error instanceof Error ? error.message : String(error));
+ } finally {
+ setLoading(false);
+ }
+ }, [enabled]);
+
+ const pollRunning = useCallback(async () => {
+ if (!enabled) {
+ setRunningApps([]);
+ return;
+ }
+ try {
+ const result = await getRunningFlatpakApps();
+ if (result.success) setRunningApps(result.apps || []);
+ } catch {}
+ }, [enabled]);
+
+ useEffect(() => {
+ void reload();
+ }, [reload]);
+
+ useEffect(() => {
+ void pollRunning();
+ if (!enabled) return;
+ const interval = window.setInterval(() => void pollRunning(), 2000);
+ return () => window.clearInterval(interval);
+ }, [enabled, pollRunning]);
+
+ const operate = useCallback(async (appId: string, operation: () => Promise<{ success: boolean; error?: string | null }>) => {
+ if (busyAppId) return false;
+ setBusyAppId(appId);
+ try {
+ const result = await operation();
+ if (!result.success) throw new Error(result.error || "Flatpak operation failed");
+ await reload();
+ await pollRunning();
+ return true;
+ } catch (error) {
+ showErrorToast("Flatpak operation failed", error instanceof Error ? error.message : String(error));
+ return false;
+ } finally {
+ setBusyAppId("");
+ }
+ }, [busyAppId, pollRunning, reload]);
+
+ const enableApp = useCallback((appId: string) => operate(appId, () => enableFlatpakApp(appId)), [operate]);
+ const removeApp = useCallback((appId: string) => operate(appId, () => removeFlatpakApp(appId)), [operate]);
+ const updateConfig = useCallback(
+ (appId: string, config: LsfgConfig) => operate(appId, () => updateFlatpakConfig(appId, config)),
+ [operate],
+ );
+ const updateWorkarounds = useCallback(
+ (appId: string, state: WorkaroundState) => operate(appId, () => setFlatpakWorkaroundState(appId, state)),
+ [operate],
+ );
+
+ const runningApp = useMemo(() => {
+ if (runningApps.length === 0) return null;
+ const running = runningApps.find((app) => app.active) || (runningApps.length === 1 ? runningApps[0] : null);
+ if (!running) return null;
+ return apps.find((app) => app.app_id === running.app_id) || null;
+ }, [apps, runningApps]);
+
+ return {
+ apps,
+ runningApps,
+ runningApp,
+ loading,
+ busyAppId,
+ reload,
+ enableApp,
+ removeApp,
+ updateConfig,
+ updateWorkarounds,
+ };
+}
diff --git a/src/hooks/useGameConfiguration.ts b/src/hooks/useGameConfiguration.ts
index 3260019..2e39f5e 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,6 @@ async function getSteamShortcuts(): Promise<InstalledGame[]> {
appid: String(appid >>> 0),
name,
nonSteam: true,
- transport: { kind: "host" },
}];
});
} catch {
@@ -80,6 +79,7 @@ export function useGameConfiguration() {
previousQuickAccessVisible.current = quickAccessVisible;
if (initialLoad || becameVisible) void load();
}, [load, quickAccessVisible]);
+
useEffect(() => {
const poll = () => {
if (!configsLoaded) return;
@@ -89,16 +89,25 @@ 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 : {
- ...(installed || { appid, name, nonSteam: false, transport: { kind: "host" } }),
+ 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);
return () => window.clearInterval(interval);
}, [configsLoaded, games, installedGames]);
+
useEffect(() => {
const appid = runningGame?.appid || null;
if (appid !== previousRunningAppId.current) {
@@ -109,25 +118,15 @@ 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, 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, 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 runningConfig = runningGame
+ ? games.find((game) => game.appid === runningGame.appid)?.config || template
+ : template;
const ensureTargetWorkarounds = useCallback(async (target: GameTarget): Promise<boolean> => {
if (!installedGames.some((game) => game.appid === target.appid)) return true;
@@ -148,16 +147,12 @@ export function useGameConfiguration() {
target.nonSteam,
wrapperPath,
commandTokenAdded,
- target.transport,
- target.transport.kind === "flatpak" ? existing.shortcut_exe || undefined : undefined,
);
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 +164,7 @@ export function useGameConfiguration() {
appId,
target.nonSteam,
wrapperPath,
- integration.originalExecutable,
integration.commandTokenAdded,
- target.transport,
);
} catch (rollbackError) {
showErrorToast("Workaround rollback failed", asError(rollbackError).message);
@@ -201,9 +194,7 @@ export function useGameConfiguration() {
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");
@@ -214,33 +205,37 @@ export function useGameConfiguration() {
}
}, [installedGames]);
- 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);
+ 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);
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<void> => {
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 +248,11 @@ export function useGameConfiguration() {
}
}
await load();
- }, [ensureTargetFlatpakSupport, ensureTargetWorkarounds, load, removeTargetWorkarounds, targets, template]);
+ }, [ensureTargetWorkarounds, load, removeTargetWorkarounds, targets, template]);
+
const repair = useCallback(async (appid: string): Promise<boolean> => {
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 +270,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;
@@ -296,5 +283,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, saveFor, enable, enableAll, repair, resetSelected, resetAll, reload: load };
}
diff --git a/src/hooks/usePerAppWorkarounds.ts b/src/hooks/usePerAppWorkarounds.ts
index 29e1181..c2b8904 100644
--- a/src/hooks/usePerAppWorkarounds.ts
+++ b/src/hooks/usePerAppWorkarounds.ts
@@ -3,11 +3,9 @@ import {
getWorkaroundState,
removeWorkaroundState,
setWorkaroundState,
- type TargetTransport,
type WorkaroundState,
} from "../api/lsfgApi";
import {
- assertKnownShortcutTarget,
getDefaultWrapperPath,
installWrapperIntegration,
isWrapperIntegrationInstalled,
@@ -45,8 +43,6 @@ export interface WorkaroundSnapshot {
wrapperOwned: boolean;
integrationInstalled: boolean;
commandTokenAdded: boolean;
- shortcutExe?: string | null;
- transport: TargetTransport;
}
interface PerAppWorkarounds {
@@ -65,49 +61,34 @@ function makeSnapshot(
steam: SteamLaunchOptionsSnapshot,
result: Awaited<ReturnType<typeof getWorkaroundState>>,
nonSteam: boolean,
- transport: TargetTransport,
): WorkaroundSnapshot {
if (!result.state) throw new Error("Workaround state is not initialized for this profile");
const wrapperPath = result.wrapper_path || getDefaultWrapperPath();
- const selectedTransport = result.transport || transport;
- const shortcutExe = selectedTransport.kind === "flatpak" ? result.shortcut_exe : undefined;
- assertKnownShortcutTarget(steam, nonSteam, selectedTransport, wrapperPath, shortcutExe);
return {
steam,
state: result.state,
wrapperPath,
wrapperOwned: result.wrapper_owned === true,
- integrationInstalled: isWrapperIntegrationInstalled(steam, nonSteam, selectedTransport, wrapperPath),
+ integrationInstalled: isWrapperIntegrationInstalled(steam, nonSteam, wrapperPath),
commandTokenAdded: result.command_token_added === true,
- shortcutExe,
- transport: selectedTransport,
};
}
async function adoptWorkaroundState(
appId: string,
nonSteam: boolean,
- transport: TargetTransport,
wrapperPath: string,
): Promise<WorkaroundSnapshot> {
let integration: Awaited<ReturnType<typeof installWrapperIntegration>> | null = null;
try {
- integration = await installWrapperIntegration(
- Number(appId),
- nonSteam,
- wrapperPath,
- false,
- transport,
- );
+ integration = await installWrapperIntegration(Number(appId), nonSteam, wrapperPath, false);
const finalized = await setWorkaroundState(
appId,
DEFAULT_WORKAROUND_STATE,
- integration.originalExecutable ?? null,
integration.commandTokenAdded,
- transport,
);
if (!finalized.success) throw new Error(finalized.error || "Could not finalize workaround state");
- return makeSnapshot(integration.snapshot, finalized, nonSteam, transport);
+ return makeSnapshot(integration.snapshot, finalized, nonSteam);
} catch (error) {
let rollbackSucceeded = true;
if (integration?.changed) {
@@ -116,12 +97,9 @@ async function adoptWorkaroundState(
Number(appId),
nonSteam,
wrapperPath,
- integration.originalExecutable,
integration.commandTokenAdded,
- transport,
);
} catch {
- // Leave the owned integration in place rather than guessing at cleanup.
rollbackSucceeded = false;
}
}
@@ -133,11 +111,7 @@ async function adoptWorkaroundState(
}
}
-export function usePerAppWorkarounds(
- appId: string,
- nonSteam: boolean,
- transport: TargetTransport = { kind: "host" },
-): PerAppWorkarounds {
+export function usePerAppWorkarounds(appId: string, nonSteam: boolean): PerAppWorkarounds {
const [status, setStatus] = useState<WorkaroundLoadStatus>("loading");
const [snapshot, setSnapshot] = useState<WorkaroundSnapshot | null>(null);
const [error, setError] = useState<string | null>(null);
@@ -154,12 +128,11 @@ export function usePerAppWorkarounds(
return adoptWorkaroundState(
appId,
nonSteam,
- transport,
result.wrapper_path || getDefaultWrapperPath(),
);
}
- return makeSnapshot(steam, result, nonSteam, transport);
- }, [appId, nonSteam, numericAppId, transport]);
+ return makeSnapshot(steam, result, nonSteam);
+ }, [appId, nonSteam, numericAppId]);
const applySnapshot = useCallback((next: WorkaroundSnapshot) => {
setSnapshot(next);
@@ -194,7 +167,7 @@ export function usePerAppWorkarounds(
setSnapshot((current) => current ? {
...current,
steam,
- integrationInstalled: isWrapperIntegrationInstalled(steam, nonSteam, current.transport, current.wrapperPath),
+ integrationInstalled: isWrapperIntegrationInstalled(steam, nonSteam, current.wrapperPath),
} : current);
},
(subscriptionError) => {
@@ -229,24 +202,18 @@ export function usePerAppWorkarounds(
setError(null);
const nextState = { ...current.state, [field]: value } as WorkaroundState;
try {
- const shortcutExe = current.transport.kind === "flatpak" ? current.shortcutExe ?? null : null;
const result = await setWorkaroundState(
appId,
nextState,
- shortcutExe,
current.commandTokenAdded,
- current.transport,
);
if (!result.success || !result.state) throw new Error(result.error || "Could not save workaround state");
- const selectedTransport = result.transport || current.transport;
applySnapshot({
...current,
state: result.state,
wrapperPath: result.wrapper_path || current.wrapperPath,
wrapperOwned: result.wrapper_owned === true,
- shortcutExe: selectedTransport.kind === "flatpak" ? result.shortcut_exe : undefined,
commandTokenAdded: result.command_token_added === true,
- transport: selectedTransport,
});
return true;
} catch (updateError) {