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.ts173
1 files changed, 173 insertions, 0 deletions
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]);
+}