summaryrefslogtreecommitdiff
path: root/src/hooks/usePerAppWorkarounds.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/hooks/usePerAppWorkarounds.ts')
-rw-r--r--src/hooks/usePerAppWorkarounds.ts266
1 files changed, 266 insertions, 0 deletions
diff --git a/src/hooks/usePerAppWorkarounds.ts b/src/hooks/usePerAppWorkarounds.ts
new file mode 100644
index 0000000..ab33bb2
--- /dev/null
+++ b/src/hooks/usePerAppWorkarounds.ts
@@ -0,0 +1,266 @@
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
+import {
+ getWorkaroundState,
+ removeWorkaroundState,
+ setWorkaroundState,
+ type WorkaroundState,
+} from "../api/lsfgApi";
+import {
+ getDefaultWrapperPath,
+ installWrapperIntegration,
+ isWrapperIntegrationInstalled,
+ readSteamLaunchOptions,
+ removeWrapperIntegration,
+ subscribeSteamLaunchOptions,
+ type SteamLaunchOptionsSnapshot,
+} from "../utils/steamLaunchOptions";
+import { showErrorToast } from "../utils/toastUtils";
+
+export type WorkaroundField = keyof WorkaroundState;
+export type WorkaroundLoadStatus = "loading" | "ready" | "error";
+
+const SLIDER_DEBOUNCE_MS = 250;
+
+const DEFAULT_WORKAROUND_STATE: WorkaroundState = {
+ dxvkFrameRate: 0,
+ disableGamescopeWsi: true,
+ disableHdr: true,
+ disableSteamdeckMode: false,
+ disableVkbasalt: false,
+ enableZink: false,
+};
+
+interface PendingSliderUpdate {
+ timer: number;
+ value: number;
+ waiters: Array<(success: boolean) => void>;
+}
+
+export interface WorkaroundSnapshot {
+ steam: SteamLaunchOptionsSnapshot;
+ state: WorkaroundState;
+ wrapperPath: string;
+ wrapperOwned: boolean;
+ integrationInstalled: boolean;
+ commandTokenAdded: boolean;
+}
+
+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,
+ result: Awaited<ReturnType<typeof getWorkaroundState>>,
+ nonSteam: boolean,
+): WorkaroundSnapshot {
+ if (!result.state) throw new Error("Workaround state is not initialized for this profile");
+ const wrapperPath = result.wrapper_path || getDefaultWrapperPath();
+ return {
+ steam,
+ state: result.state,
+ wrapperPath,
+ wrapperOwned: result.wrapper_owned === true,
+ integrationInstalled: isWrapperIntegrationInstalled(steam, nonSteam, wrapperPath),
+ commandTokenAdded: result.command_token_added === true,
+ };
+}
+
+async function adoptWorkaroundState(
+ appId: string,
+ nonSteam: boolean,
+ wrapperPath: string,
+): Promise<WorkaroundSnapshot> {
+ let integration: Awaited<ReturnType<typeof installWrapperIntegration>> | null = null;
+ try {
+ integration = await installWrapperIntegration(Number(appId), nonSteam, wrapperPath, false);
+ const finalized = await setWorkaroundState(
+ appId,
+ DEFAULT_WORKAROUND_STATE,
+ integration.commandTokenAdded,
+ nonSteam,
+ );
+ if (!finalized.success) throw new Error(finalized.error || "Could not finalize workaround state");
+ return makeSnapshot(integration.snapshot, finalized, nonSteam);
+ } catch (error) {
+ let rollbackSucceeded = true;
+ if (integration?.changed) {
+ try {
+ await removeWrapperIntegration(
+ Number(appId),
+ nonSteam,
+ wrapperPath,
+ integration.commandTokenAdded,
+ );
+ } catch {
+ rollbackSucceeded = false;
+ }
+ }
+ if (rollbackSucceeded) {
+ const removed = await removeWorkaroundState(appId);
+ if (!removed.success) throw new Error(removed.error || "Could not roll back workaround state");
+ }
+ throw error;
+ }
+}
+
+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 loadSnapshot = useCallback(async () => {
+ const [result, steam] = await Promise.all([
+ getWorkaroundState(appId),
+ readSteamLaunchOptions(numericAppId, nonSteam),
+ ]);
+ if (!result.success) throw new Error(result.error || "Could not read workaround state");
+ if (!result.state) {
+ return adoptWorkaroundState(
+ appId,
+ nonSteam,
+ result.wrapper_path || getDefaultWrapperPath(),
+ );
+ }
+ return makeSnapshot(steam, result, nonSteam);
+ }, [appId, nonSteam, numericAppId]);
+
+ const applySnapshot = useCallback((next: WorkaroundSnapshot) => {
+ setSnapshot(next);
+ setStatus("ready");
+ setError(null);
+ }, []);
+
+ const refresh = useCallback(async () => {
+ setStatus("loading");
+ setError(null);
+ try {
+ applySnapshot(await loadSnapshot());
+ } catch (refreshError) {
+ const nextError = asError(refreshError);
+ setStatus("error");
+ setError(nextError.message);
+ }
+ }, [applySnapshot, loadSnapshot]);
+
+ useEffect(() => {
+ let active = true;
+ setStatus("loading");
+ setSnapshot(null);
+ setError(null);
+ let unsubscribe = () => {};
+ try {
+ unsubscribe = subscribeSteamLaunchOptions(
+ numericAppId,
+ nonSteam,
+ (steam) => {
+ if (!active) return;
+ setSnapshot((current) => current ? {
+ ...current,
+ steam,
+ integrationInstalled: isWrapperIntegrationInstalled(steam, nonSteam, current.wrapperPath),
+ } : current);
+ },
+ (subscriptionError) => {
+ if (!active) return;
+ setStatus("error");
+ setError(subscriptionError.message);
+ },
+ );
+ } catch (subscriptionError) {
+ if (active) {
+ setStatus("error");
+ setError(asError(subscriptionError).message);
+ }
+ }
+ void loadSnapshot()
+ .then((next) => { if (active) applySnapshot(next); })
+ .catch((readError) => {
+ if (active) {
+ setStatus("error");
+ setError(asError(readError).message);
+ }
+ });
+ return () => {
+ active = false;
+ unsubscribe();
+ };
+ }, [applySnapshot, loadSnapshot, nonSteam, numericAppId]);
+
+ const persistUpdate = useCallback(async (field: WorkaroundField, value: boolean | number): Promise<boolean> => {
+ const current = snapshot;
+ if (!current) return false;
+ setError(null);
+ const nextState = { ...current.state, [field]: value } as WorkaroundState;
+ try {
+ const result = await setWorkaroundState(
+ appId,
+ nextState,
+ current.commandTokenAdded,
+ nonSteam,
+ );
+ if (!result.success || !result.state) throw new Error(result.error || "Could not save workaround state");
+ applySnapshot({
+ ...current,
+ state: result.state,
+ wrapperPath: result.wrapper_path || current.wrapperPath,
+ wrapperOwned: result.wrapper_owned === true,
+ commandTokenAdded: result.command_token_added === true,
+ });
+ return true;
+ } catch (updateError) {
+ const nextError = asError(updateError);
+ setStatus("error");
+ setError(nextError.message);
+ showErrorToast("Workaround update failed", nextError.message);
+ return false;
+ }
+ }, [appId, applySnapshot, snapshot]);
+
+ const flushSliderUpdate = useCallback(async (): Promise<boolean> => {
+ const pending = pendingSliderUpdate.current;
+ if (!pending) return true;
+ pendingSliderUpdate.current = null;
+ 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(() => () => {
+ 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]);
+}