summaryrefslogtreecommitdiff
path: root/src/hooks
diff options
context:
space:
mode:
authorxXJSONDeruloXx <danielhimebauch@gmail.com>2026-09-08 16:25:11 -0400
committerxXJSONDeruloXx <danielhimebauch@gmail.com>2026-09-08 16:25:11 -0400
commit8352ee74e8a437c1f4a4dadb75d63049f45b164b (patch)
treebd59691eb01f5fbf4b5a6aede5ae4d0295e6643d /src/hooks
parentb3749ecc4b094a46d2ab9f638ee810f70840f67a (diff)
downloaddecky-lsfg-vk-8352ee74e8a437c1f4a4dadb75d63049f45b164b.tar.gz
decky-lsfg-vk-8352ee74e8a437c1f4a4dadb75d63049f45b164b.zip
add back workarounds sections, scope out of now playing
Diffstat (limited to 'src/hooks')
-rw-r--r--src/hooks/useGameConfiguration.ts27
-rw-r--r--src/hooks/usePerAppWorkarounds.ts173
2 files changed, 196 insertions, 4 deletions
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<boolean> => {
+ 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<void> => {
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 };
}
diff --git a/src/hooks/usePerAppWorkarounds.ts b/src/hooks/usePerAppWorkarounds.ts
new file mode 100644
index 0000000..a937780
--- /dev/null
+++ b/src/hooks/usePerAppWorkarounds.ts
@@ -0,0 +1,173 @@
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
+import {
+ applyWorkaroundChange,
+ parseWorkaroundOptions,
+ readSteamLaunchOptions,
+ subscribeSteamLaunchOptions,
+ updateSteamLaunchOptions,
+ type ParsedWorkaroundOptions,
+ type SteamLaunchOptionsSnapshot,
+ type WorkaroundField,
+} from "../utils/steamLaunchOptions";
+import { showErrorToast } from "../utils/toastUtils";
+
+export type WorkaroundLoadStatus = "loading" | "ready" | "error";
+
+const SLIDER_DEBOUNCE_MS = 250;
+
+interface PendingSliderUpdate {
+ timer: number;
+ value: number;
+ waiters: Array<(success: boolean) => void>;
+}
+
+interface WorkaroundSnapshot {
+ steam: SteamLaunchOptionsSnapshot;
+ parsed: ParsedWorkaroundOptions;
+}
+
+interface PerAppWorkarounds {
+ status: WorkaroundLoadStatus;
+ snapshot: WorkaroundSnapshot | null;
+ refresh: () => Promise<void>;
+ update: (field: WorkaroundField, value: boolean | number) => Promise<boolean>;
+ error: string | null;
+}
+
+function asError(error: unknown): Error {
+ return error instanceof Error ? error : new Error(String(error));
+}
+
+function makeSnapshot(steam: SteamLaunchOptionsSnapshot): WorkaroundSnapshot {
+ return { steam, parsed: parseWorkaroundOptions(steam.options) };
+}
+
+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);
+ const pendingSliderUpdate = useRef<PendingSliderUpdate | null>(null);
+ const numericAppId = Number(appId);
+
+ const applySnapshot = useCallback((steam: SteamLaunchOptionsSnapshot) => {
+ setSnapshot(makeSnapshot(steam));
+ setStatus("ready");
+ setError(null);
+ }, []);
+
+ const refresh = useCallback(async () => {
+ setStatus("loading");
+ setError(null);
+ try {
+ applySnapshot(await readSteamLaunchOptions(numericAppId, nonSteam));
+ } catch (refreshError) {
+ const nextError = asError(refreshError);
+ setStatus("error");
+ setError(nextError.message);
+ }
+ }, [applySnapshot, nonSteam, numericAppId]);
+
+ useEffect(() => {
+ let active = true;
+ setStatus("loading");
+ setSnapshot(null);
+ setError(null);
+
+ const handleSnapshot = (nextSnapshot: SteamLaunchOptionsSnapshot) => {
+ if (!active) return;
+ applySnapshot(nextSnapshot);
+ };
+ const handleSubscriptionError = (subscriptionError: Error) => {
+ if (!active) return;
+ setStatus("error");
+ setError(subscriptionError.message);
+ };
+
+ let unsubscribe = () => {};
+ try {
+ unsubscribe = subscribeSteamLaunchOptions(
+ numericAppId,
+ nonSteam,
+ handleSnapshot,
+ handleSubscriptionError,
+ );
+ } catch (subscriptionError) {
+ handleSubscriptionError(asError(subscriptionError));
+ }
+
+ void readSteamLaunchOptions(numericAppId, nonSteam)
+ .then((nextSnapshot) => {
+ if (active) applySnapshot(nextSnapshot);
+ })
+ .catch((readError) => {
+ if (active) handleSubscriptionError(asError(readError));
+ });
+
+ return () => {
+ active = false;
+ unsubscribe();
+ };
+ }, [applySnapshot, nonSteam, numericAppId]);
+
+ const persistUpdate = useCallback(async (field: WorkaroundField, value: boolean | number): Promise<boolean> => {
+ setError(null);
+ try {
+ const nextSnapshot = await updateSteamLaunchOptions(
+ numericAppId,
+ nonSteam,
+ (options) => applyWorkaroundChange(options, field, value),
+ );
+ applySnapshot(nextSnapshot);
+ return true;
+ } catch (updateError) {
+ const nextError = asError(updateError);
+ setStatus("error");
+ setError(nextError.message);
+ showErrorToast("Workaround update failed", nextError.message);
+ return false;
+ }
+ }, [applySnapshot, nonSteam, numericAppId]);
+
+ const flushSliderUpdate = useCallback(async (): Promise<boolean> => {
+ const pending = pendingSliderUpdate.current;
+ if (!pending) return true;
+
+ pendingSliderUpdate.current = null;
+ window.clearTimeout(pending.timer);
+ const success = await persistUpdate("dxvkFrameRate", pending.value);
+ pending.waiters.forEach((resolve) => resolve(success));
+ return success;
+ }, [persistUpdate]);
+
+ const update = useCallback(async (field: WorkaroundField, value: boolean | number): Promise<boolean> => {
+ if (field === "dxvkFrameRate") {
+ setError(null);
+ return new Promise<boolean>((resolve) => {
+ const pending = pendingSliderUpdate.current ?? { timer: 0, value: 0, waiters: [] };
+ window.clearTimeout(pending.timer);
+ pending.value = Number(value);
+ pending.waiters.push(resolve);
+ pending.timer = window.setTimeout(() => {
+ void flushSliderUpdate();
+ }, SLIDER_DEBOUNCE_MS);
+ pendingSliderUpdate.current = pending;
+ });
+ }
+
+ const sliderSuccess = await flushSliderUpdate();
+ if (!sliderSuccess) return false;
+ return persistUpdate(field, value);
+ }, [flushSliderUpdate, persistUpdate]);
+
+ useEffect(() => {
+ return () => {
+ const pending = pendingSliderUpdate.current;
+ if (!pending) return;
+ window.clearTimeout(pending.timer);
+ pendingSliderUpdate.current = null;
+ pending.waiters.forEach((resolve) => resolve(false));
+ };
+ }, [numericAppId, nonSteam]);
+
+ return useMemo(() => ({ status, snapshot, refresh, update, error }), [error, refresh, snapshot, status, update]);
+}