summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/api/lsfgApi.ts19
-rw-r--r--src/components/ConfigurationSection.tsx5
-rw-r--r--src/components/ConfigurationTab.tsx15
-rw-r--r--src/components/Content.tsx13
-rw-r--r--src/components/SetupTab.tsx82
-rw-r--r--src/hooks/useGameConfiguration.ts46
-rw-r--r--src/hooks/useLsfgHooks.ts9
-rw-r--r--src/hooks/usePerAppWorkarounds.ts2
8 files changed, 141 insertions, 50 deletions
diff --git a/src/api/lsfgApi.ts b/src/api/lsfgApi.ts
index f3b7fa1..e050f62 100644
--- a/src/api/lsfgApi.ts
+++ b/src/api/lsfgApi.ts
@@ -64,6 +64,18 @@ export interface WorkaroundStateResult extends ApiResult {
wrapper_path?: string;
wrapper_owned?: boolean;
command_token_added?: boolean;
+ non_steam?: boolean;
+}
+
+export interface WorkaroundApp {
+ appid: string;
+ non_steam: boolean;
+ command_token_added: boolean;
+}
+
+export interface WorkaroundAppsResult extends ApiResult {
+ apps?: WorkaroundApp[];
+ wrapper_path?: string;
}
export interface GameConfigsResult extends ApiResult {
@@ -71,6 +83,10 @@ export interface GameConfigsResult extends ApiResult {
games?: GameConfigEntry[];
}
+export interface GlobalConfigResult extends ApiResult {
+ global_config?: GlobalConfig;
+}
+
export interface GameConfigResult extends ApiResult {
appid?: string;
exists?: boolean;
@@ -149,11 +165,14 @@ export const getInstalledGames = callable<[], InstalledGamesResult>("get_install
export const updateGameConfig = callable<[string, string, LsfgConfig], GameConfigResult>("update_game_config");
export const resetGameConfig = callable<[string], GameConfigResult>("reset_game_config");
export const resetAllGameConfigs = callable<[], GameConfigsResult>("reset_all_game_configs");
+export const updateGlobalConfig = callable<[GlobalConfig], GlobalConfigResult>("update_global_config");
export const getWorkaroundState = callable<[string], WorkaroundStateResult>("get_workaround_state");
export const setWorkaroundState = callable<[
string,
WorkaroundState,
boolean,
+ boolean,
], WorkaroundStateResult>("set_workaround_state");
export const removeWorkaroundState = callable<[string], WorkaroundStateResult>("remove_workaround_state");
+export const getWorkaroundApps = callable<[], WorkaroundAppsResult>("get_workaround_apps");
export const getDebugFileContents = callable<[], DebugFileContentsResult>("get_debug_file_contents");
diff --git a/src/components/ConfigurationSection.tsx b/src/components/ConfigurationSection.tsx
index 1f17264..6996bcd 100644
--- a/src/components/ConfigurationSection.tsx
+++ b/src/components/ConfigurationSection.tsx
@@ -1,6 +1,6 @@
import { PanelSectionRow, ToggleField, SliderField } from "@decky/ui";
import { ConfigurationData } from "../config/configSchema";
-import { FLOW_SCALE, PERFORMANCE_MODE, OVERRIDE_PRESENT_MODE, PRESERVE_SWAPCHAIN_IMAGE_COUNT, NO_FP16 } from "../config/configSchema";
+import { FLOW_SCALE, PERFORMANCE_MODE, OVERRIDE_PRESENT_MODE, PRESERVE_SWAPCHAIN_IMAGE_COUNT } from "../config/configSchema";
interface ConfigurationSectionProps {
config: ConfigurationData;
@@ -13,9 +13,6 @@ export function ConfigurationSection({ config, onConfigChange }: ConfigurationSe
<SliderField label={`Flow Scale (${Math.round(config.flow_scale * 100)}%)`} value={config.flow_scale} min={0.25} max={1} step={0.01} onChange={(value) => onConfigChange(FLOW_SCALE, value)} />
</PanelSectionRow>
<PanelSectionRow>
- <ToggleField label="FP16 Acceleration" checked={!config.no_fp16} onChange={(value) => onConfigChange(NO_FP16, !value)} />
- </PanelSectionRow>
- <PanelSectionRow>
<ToggleField label="Performance Mode" checked={config.performance_mode} onChange={(value) => onConfigChange(PERFORMANCE_MODE, value)} />
</PanelSectionRow>
<PanelSectionRow>
diff --git a/src/components/ConfigurationTab.tsx b/src/components/ConfigurationTab.tsx
index 12f36a5..37a7867 100644
--- a/src/components/ConfigurationTab.tsx
+++ b/src/components/ConfigurationTab.tsx
@@ -1,4 +1,4 @@
-import { ButtonItem, ConfirmModal, DialogButton, Focusable, PanelSection, PanelSectionRow, ToggleField, gamepadDialogClasses, showModal } from "@decky/ui";
+import { ButtonItem, ConfirmModal, DialogButton, Focusable, PanelSection, PanelSectionRow, gamepadDialogClasses, showModal } from "@decky/ui";
import { useCallback, useEffect, useRef, useState } from "react";
import { FaArrowLeft } from "react-icons/fa";
import { ConfigurationData } from "../config/configSchema";
@@ -11,8 +11,6 @@ interface ConfigurationTabProps {
config: ConfigurationData;
targets: GameTarget[];
runningGame: GameTarget | null;
- showDebugTab: boolean;
- onShowDebugTabChange: (value: boolean) => void;
onSelect: (appid: string) => void;
onConfigChange: (fieldName: keyof ConfigurationData, value: boolean | number | string | string[]) => Promise<void>;
onEnable: (appid: string) => Promise<boolean>;
@@ -26,8 +24,6 @@ export function ConfigurationTab({
config,
targets,
runningGame,
- showDebugTab,
- onShowDebugTabChange,
onSelect,
onConfigChange,
onEnable,
@@ -84,15 +80,6 @@ export function ConfigurationTab({
onConfiguredToggleFocused={clearConfiguredToggleFocusRequest}
/>
</PanelSection>
- <PanelSection title="Settings">
- <PanelSectionRow>
- <ToggleField
- label="Show debug tab"
- checked={showDebugTab}
- onChange={onShowDebugTabChange}
- />
- </PanelSectionRow>
- </PanelSection>
</>
);
}
diff --git a/src/components/Content.tsx b/src/components/Content.tsx
index 32e9bf8..91775df 100644
--- a/src/components/Content.tsx
+++ b/src/components/Content.tsx
@@ -47,16 +47,19 @@ export function Content() {
const {
config,
runningConfig,
+ globalConfig,
targets,
runningGame,
setSelectedAppId,
save,
saveFor,
+ updateGlobal,
enable,
enableAll,
repair,
resetSelected,
resetAll,
+ cleanupAllWorkarounds,
reload,
} = useGameConfiguration();
const {
@@ -69,7 +72,7 @@ export function Content() {
isUninstalling,
install,
uninstall,
- } = useInstallation(reload);
+ } = useInstallation(reload, cleanupAllWorkarounds);
const setupComplete =
isInstalled &&
losslessScalingInstalled &&
@@ -78,7 +81,7 @@ export function Content() {
!steamBranchStatus.needs_switch;
const flatpak = useFlatpakConfiguration(setupComplete);
const [tab, setTab] = useState("Setup");
- const [showDebugTab, setShowDebugTab] = usePersistentBoolean(DEBUG_TAB_VISIBILITY_KEY, true);
+ const [showDebugTab, setShowDebugTab] = usePersistentBoolean(DEBUG_TAB_VISIBILITY_KEY, false);
const [contentFocused, setContentFocused] = useState(false);
const previousRunningWorkload = useRef<string | null>(null);
const runningFlatpak = flatpak.runningApp;
@@ -136,6 +139,10 @@ export function Content() {
steamBranchStatus={steamBranchStatus}
isInstalling={isInstalling}
isUninstalling={isUninstalling}
+ globalConfig={globalConfig}
+ showDebugTab={showDebugTab}
+ onGlobalConfigChange={updateGlobal}
+ onShowDebugTabChange={setShowDebugTab}
onInstall={() => void install()}
onUninstall={() => void uninstall()}
/>
@@ -172,8 +179,6 @@ export function Content() {
config={config}
targets={targets}
runningGame={runningGame}
- showDebugTab={showDebugTab}
- onShowDebugTabChange={setShowDebugTab}
onSelect={setSelectedAppId}
onConfigChange={(field, value) => handleConfigChange(field, value, true)}
onEnable={enable}
diff --git a/src/components/SetupTab.tsx b/src/components/SetupTab.tsx
index d769200..ff072cb 100644
--- a/src/components/SetupTab.tsx
+++ b/src/components/SetupTab.tsx
@@ -1,5 +1,5 @@
-import { ButtonItem, Field, PanelSection, PanelSectionRow } from "@decky/ui";
-import { type SteamBranchStatus } from "../api/lsfgApi";
+import { ButtonItem, Field, PanelSection, PanelSectionRow, ToggleField } from "@decky/ui";
+import { type GlobalConfig, type SteamBranchStatus } from "../api/lsfgApi";
import t from "../i18n/i18n";
interface SetupTabProps {
@@ -10,6 +10,10 @@ interface SetupTabProps {
steamBranchStatus: SteamBranchStatus | null;
isInstalling: boolean;
isUninstalling: boolean;
+ globalConfig: GlobalConfig;
+ showDebugTab: boolean;
+ onGlobalConfigChange: (config: GlobalConfig) => Promise<boolean>;
+ onShowDebugTabChange: (value: boolean) => void;
onInstall: () => void;
onUninstall: () => void;
}
@@ -23,6 +27,10 @@ export function SetupTab(props: SetupTabProps) {
steamBranchStatus,
isInstalling,
isUninstalling,
+ globalConfig,
+ showDebugTab,
+ onGlobalConfigChange,
+ onShowDebugTabChange,
onInstall,
onUninstall,
} = props;
@@ -36,33 +44,57 @@ export function SetupTab(props: SetupTabProps) {
: t("INSTALL_INSTALL_BTN", "Install LSFG-VK");
return (
- <PanelSection title="Setup">
- <PanelSectionRow>
- <Field
- label="Lossless Scaling"
- description={losslessScalingAppInstalled ? "Installed" : losslessScalingStatus || "Not installed"}
- />
- </PanelSectionRow>
- <PanelSectionRow>
- <Field label="LSFG-VK" description={installationStatus} />
- </PanelSectionRow>
- {steamBranchStatus?.installed && (
+ <>
+ <PanelSection title="Setup">
<PanelSectionRow>
<Field
- label="Steam branch"
- description={`${steamBranchStatus.current_branch || "public"}${steamBranchStatus.needs_switch ? ` - ${steamBranchStatus.message}` : ""}`}
+ label="Lossless Scaling"
+ description={losslessScalingAppInstalled ? "Installed" : losslessScalingStatus || "Not installed"}
/>
</PanelSectionRow>
+ <PanelSectionRow>
+ <Field label="LSFG-VK" description={installationStatus} />
+ </PanelSectionRow>
+ {steamBranchStatus?.installed && (
+ <PanelSectionRow>
+ <Field
+ label="Steam branch"
+ description={`${steamBranchStatus.current_branch || "public"}${steamBranchStatus.needs_switch ? ` - ${steamBranchStatus.message}` : ""}`}
+ />
+ </PanelSectionRow>
+ )}
+ <PanelSectionRow>
+ <ButtonItem
+ layout="below"
+ onClick={isInstalled ? onUninstall : onInstall}
+ disabled={isInstalling || isUninstalling}
+ >
+ {buttonLabel}
+ </ButtonItem>
+ </PanelSectionRow>
+ </PanelSection>
+ {isInstalled && (
+ <>
+ <PanelSection title="Global settings">
+ <PanelSectionRow>
+ <ToggleField
+ label="FP16 Acceleration"
+ checked={!globalConfig.no_fp16}
+ onChange={(value) => void onGlobalConfigChange({ ...globalConfig, no_fp16: !value })}
+ />
+ </PanelSectionRow>
+ </PanelSection>
+ <PanelSection title="Advanced">
+ <PanelSectionRow>
+ <ToggleField
+ label="Show config file tab"
+ checked={showDebugTab}
+ onChange={onShowDebugTabChange}
+ />
+ </PanelSectionRow>
+ </PanelSection>
+ </>
)}
- <PanelSectionRow>
- <ButtonItem
- layout="below"
- onClick={isInstalled ? onUninstall : onInstall}
- disabled={isInstalling || isUninstalling}
- >
- {buttonLabel}
- </ButtonItem>
- </PanelSectionRow>
- </PanelSection>
+ </>
);
}
diff --git a/src/hooks/useGameConfiguration.ts b/src/hooks/useGameConfiguration.ts
index 2e39f5e..fd8dfb1 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 { getGameConfigs, getInstalledGames, getWorkaroundApps, getWorkaroundState, removeWorkaroundState, resetGameConfig, resetAllGameConfigs, setWorkaroundState, updateGameConfig, updateGlobalConfig as saveGlobalConfig, 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";
@@ -153,6 +153,7 @@ export function useGameConfiguration() {
target.appid,
state,
integration.commandTokenAdded,
+ target.nonSteam,
);
if (!saved.success) throw new Error(saved.error || "Could not save workaround state");
return true;
@@ -205,6 +206,40 @@ export function useGameConfiguration() {
}
}, [installedGames]);
+ const cleanupAllWorkarounds = useCallback(async (): Promise<boolean> => {
+ try {
+ const result = await getWorkaroundApps();
+ if (!result.success) throw new Error(result.error || "Could not read workaround state");
+ const targetsByAppId = new Map(targets.map((target) => [target.appid, target]));
+ const cleaned = new Set<string>();
+ const wrapperPath = result.wrapper_path || getDefaultWrapperPath();
+
+ for (const entry of result.apps || []) {
+ const target = targetsByAppId.get(entry.appid);
+ const nonSteam = target?.nonSteam ?? entry.non_steam;
+ await removeWrapperIntegration(
+ Number(entry.appid),
+ nonSteam,
+ wrapperPath,
+ entry.command_token_added,
+ );
+ const removed = await removeWorkaroundState(entry.appid);
+ if (!removed.success) throw new Error(removed.error || "Could not remove workaround state");
+ cleaned.add(entry.appid);
+ }
+
+ // Also clean configured targets whose sidecar entry was lost. This
+ // removes an old wrapper and only the plugin-managed launch pieces.
+ for (const target of targets.filter((item) => item.configured && installedGames.some((game) => game.appid === item.appid))) {
+ if (!cleaned.has(target.appid) && !(await removeTargetWorkarounds(target))) return false;
+ }
+ return true;
+ } catch (error) {
+ showErrorToast("Could not clean up game launch options", asError(error).message);
+ return false;
+ }
+ }, [installedGames, removeTargetWorkarounds, targets]);
+
const saveFor = useCallback(async (appid: string, next: ConfigurationData, cleanupLaunchOptions = false) => {
const target = targets.find((item) => item.appid === appid);
if (!target?.name) return false;
@@ -222,6 +257,13 @@ export function useGameConfiguration() {
[saveFor, selectedAppId],
);
+ const updateGlobal = useCallback(async (next: GlobalConfig): Promise<boolean> => {
+ const result = await saveGlobalConfig(next);
+ if (!result.success) return false;
+ setGlobalConfig(result.global_config || next);
+ return true;
+ }, []);
+
const enable = useCallback(async (appid: string) => {
const target = targets.find((item) => item.appid === appid);
if (!target?.name) return false;
@@ -283,5 +325,5 @@ export function useGameConfiguration() {
}
}, [load, removeTargetWorkarounds, targets]);
- return { config, runningConfig, games, targets, runningGame, selectedAppId, setSelectedAppId, save, saveFor, enable, enableAll, repair, resetSelected, resetAll, reload: load };
+ return { config, runningConfig, globalConfig, targets, runningGame, selectedAppId, setSelectedAppId, save, saveFor, updateGlobal, enable, enableAll, repair, resetSelected, resetAll, cleanupAllWorkarounds, reload: load };
}
diff --git a/src/hooks/useLsfgHooks.ts b/src/hooks/useLsfgHooks.ts
index 9beb749..0f51e90 100644
--- a/src/hooks/useLsfgHooks.ts
+++ b/src/hooks/useLsfgHooks.ts
@@ -13,7 +13,10 @@ import {
showUninstallSuccessToast,
} from "../utils/toastUtils";
-export function useInstallation(reloadConfig?: () => Promise<void>) {
+export function useInstallation(
+ reloadConfig?: () => Promise<void>,
+ beforeUninstall?: () => Promise<boolean>,
+) {
const [isInstalled, setIsInstalled] = useState(false);
const [installationStatus, setInstallationStatus] = useState("");
const [losslessScalingInstalled, setLosslessScalingInstalled] = useState(false);
@@ -77,6 +80,10 @@ export function useInstallation(reloadConfig?: () => Promise<void>) {
setIsUninstalling(true);
setInstallationStatus("Uninstalling lsfg-vk...");
try {
+ if (beforeUninstall && !(await beforeUninstall())) {
+ setInstallationStatus("Uninstallation cancelled: could not clean up launch options");
+ return;
+ }
const result = await uninstallLsfgVk();
if (!result.success) {
setInstallationStatus(`Uninstallation failed: ${result.error}`);
diff --git a/src/hooks/usePerAppWorkarounds.ts b/src/hooks/usePerAppWorkarounds.ts
index c2b8904..ab33bb2 100644
--- a/src/hooks/usePerAppWorkarounds.ts
+++ b/src/hooks/usePerAppWorkarounds.ts
@@ -86,6 +86,7 @@ async function adoptWorkaroundState(
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);
@@ -206,6 +207,7 @@ export function usePerAppWorkarounds(appId: string, nonSteam: boolean): PerAppWo
appId,
nextState,
current.commandTokenAdded,
+ nonSteam,
);
if (!result.success || !result.state) throw new Error(result.error || "Could not save workaround state");
applySnapshot({