summaryrefslogtreecommitdiff
path: root/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'src/components')
-rw-r--r--src/components/ConfigFileTab.tsx119
-rw-r--r--src/components/ConfigurationTab.tsx114
-rw-r--r--src/components/Content.tsx122
-rw-r--r--src/components/FlatpaksTab.tsx196
-rw-r--r--src/components/GameConfigurationControls.tsx11
-rw-r--r--src/components/GameConfigurationSelector.tsx102
-rw-r--r--src/components/InstallationButton.tsx38
-rw-r--r--src/components/NowPlayingTab.tsx63
-rw-r--r--src/components/ProfileDetails.tsx4
-rw-r--r--src/components/SetupTab.tsx78
-rw-r--r--src/components/StatusDisplay.tsx41
-rw-r--r--src/components/WorkaroundsSection.tsx66
-rw-r--r--src/components/index.ts5
13 files changed, 468 insertions, 491 deletions
diff --git a/src/components/ConfigFileTab.tsx b/src/components/ConfigFileTab.tsx
index e3cc09d..07408d7 100644
--- a/src/components/ConfigFileTab.tsx
+++ b/src/components/ConfigFileTab.tsx
@@ -1,13 +1,79 @@
+import { ButtonItem, Field, PanelSection, PanelSectionRow, Spinner } from "@decky/ui";
import { useEffect, useState } from "react";
-import { Field, PanelSection, PanelSectionRow, Spinner } from "@decky/ui";
-import { getConfigFileContent, FileContentResult } from "../api/lsfgApi";
+import { RiArrowDownSFill, RiArrowUpSFill } from "react-icons/ri";
+import { getDebugFileContents, type DebugFileContent, type DebugFileContentsResult } from "../api/lsfgApi";
import t from "../i18n/i18n";
+function usePersistentCollapsed(key: string) {
+ const [collapsed, setCollapsed] = useState(() => {
+ try {
+ return localStorage.getItem(key) !== "false";
+ } catch {
+ return true;
+ }
+ });
+
+ useEffect(() => {
+ try {
+ localStorage.setItem(key, String(collapsed));
+ } catch {
+ // Persisting the view preference is optional.
+ }
+ }, [collapsed, key]);
+
+ return [collapsed, () => setCollapsed((value) => !value)] as const;
+}
+
+function DebugFileSection({ file }: { file: DebugFileContent }) {
+ const [collapsed, toggleCollapsed] = usePersistentCollapsed(`lsfg-debug-file-${file.id}-collapsed-v1`);
+ const status = file.exists ? "Present" : "Not present";
+
+ return (
+ <>
+ <PanelSectionRow>
+ <Field
+ label={file.label}
+ description={`${file.path} · ${status}`}
+ bottomSeparator="none"
+ />
+ </PanelSectionRow>
+ <PanelSectionRow>
+ <div
+ className="LSFG_DebugFileCollapseButton_Container"
+ style={{ marginTop: "-2px", marginBottom: "4px" }}
+ >
+ <ButtonItem
+ layout="below"
+ bottomSeparator={collapsed ? "standard" : "none"}
+ onClick={toggleCollapsed}
+ >
+ {collapsed ? <RiArrowDownSFill /> : <RiArrowUpSFill />}
+ </ButtonItem>
+ </div>
+ </PanelSectionRow>
+ {!collapsed && (
+ <PanelSectionRow>
+ {file.exists && file.content !== null && file.content !== undefined ? (
+ <pre style={{ whiteSpace: "pre-wrap", overflowWrap: "anywhere" }}>
+ {file.content}
+ </pre>
+ ) : (
+ <Field
+ label="File unavailable"
+ description={file.error || "The file has not been created yet."}
+ />
+ )}
+ </PanelSectionRow>
+ )}
+ </>
+ );
+}
+
export function ConfigFileTab() {
- const [result, setResult] = useState<FileContentResult | null>(null);
+ const [result, setResult] = useState<DebugFileContentsResult | null>(null);
useEffect(() => {
- getConfigFileContent().then(setResult).catch((error) => {
+ getDebugFileContents().then(setResult).catch((error) => {
setResult({ success: false, error: String(error) });
});
}, []);
@@ -23,24 +89,35 @@ export function ConfigFileTab() {
}
return (
- <PanelSection title={t("NERD_CONFIG_FILE", "Configuration File")}>
- {result.error && (
- <PanelSectionRow>
- <Field label="Error" description={result.error} />
- </PanelSectionRow>
- )}
- {result.success && result.content && (
- <>
- <PanelSectionRow>
- <Field label="Config file" description={result.path} />
- </PanelSectionRow>
+ <>
+ <style>
+ {`
+ .LSFG_DebugFileCollapseButton_Container > div > div > div > button,
+ .LSFG_DebugFileCollapseButton_Container > div > div > div > div > button {
+ height: 24px !important;
+ min-height: 24px !important;
+ padding: 0 !important;
+ display: flex !important;
+ align-items: center !important;
+ justify-content: center !important;
+ }
+
+ .LSFG_DebugFileCollapseButton_Container svg {
+ display: block;
+ margin: 0;
+ }
+ `}
+ </style>
+ <PanelSection title={t("NERD_CONFIG_FILE", "Configuration File")}>
+ {result.error && (
<PanelSectionRow>
- <pre style={{ whiteSpace: "pre-wrap", overflowWrap: "anywhere" }}>
- {result.content}
- </pre>
+ <Field label="Error" description={result.error} />
</PanelSectionRow>
- </>
- )}
- </PanelSection>
+ )}
+ {result.success && result.files?.map((file) => (
+ <DebugFileSection key={file.id} file={file} />
+ ))}
+ </PanelSection>
+ </>
);
}
diff --git a/src/components/ConfigurationTab.tsx b/src/components/ConfigurationTab.tsx
index 2bb0f26..4e2d93d 100644
--- a/src/components/ConfigurationTab.tsx
+++ b/src/components/ConfigurationTab.tsx
@@ -1,4 +1,4 @@
-import { ButtonItem, DialogButton, Focusable, PanelSection, PanelSectionRow, gamepadDialogClasses } from "@decky/ui";
+import { ButtonItem, ConfirmModal, DialogButton, Focusable, PanelSection, PanelSectionRow, ToggleField, gamepadDialogClasses, showModal } from "@decky/ui";
import { useCallback, useEffect, useRef, useState } from "react";
import { FaArrowLeft } from "react-icons/fa";
import { ConfigurationData } from "../config/configSchema";
@@ -11,10 +11,13 @@ 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>;
onEnableAll: () => Promise<void>;
+ onRepair: (appid: string) => Promise<boolean>;
onReset: () => Promise<void>;
onResetAll: () => Promise<void>;
}
@@ -23,24 +26,28 @@ export function ConfigurationTab({
config,
targets,
runningGame,
+ showDebugTab,
+ onShowDebugTabChange,
onSelect,
onConfigChange,
onEnable,
onEnableAll,
+ onRepair,
onReset,
onResetAll,
}: ConfigurationTabProps) {
const [detailAppId, setDetailAppId] = useState<string | null>(null);
const [focusFpsMultiplier, setFocusFpsMultiplier] = useState(false);
const [focusDetailAction, setFocusDetailAction] = useState<"enable" | "fps" | null>(null);
+ const [focusConfiguredToggle, setFocusConfiguredToggle] = useState(false);
const enableRef = useRef<HTMLDivElement>(null);
- const promptedRunningAppId = useRef<string | null>(null);
const closeDetails = useCallback(() => {
setFocusFpsMultiplier(false);
setFocusDetailAction(null);
setDetailAppId(null);
}, []);
const clearFpsFocusRequest = useCallback(() => setFocusFpsMultiplier(false), []);
+ const clearConfiguredToggleFocusRequest = useCallback(() => setFocusConfiguredToggle(false), []);
useEffect(() => {
if (!focusDetailAction) return;
@@ -56,49 +63,81 @@ export function ConfigurationTab({
return () => cancelAnimationFrame(frame);
}, [focusDetailAction]);
- useEffect(() => {
- if (!runningGame || runningGame.configured) {
- promptedRunningAppId.current = null;
- return;
- }
- if (promptedRunningAppId.current !== runningGame.appid && detailAppId === null) {
- promptedRunningAppId.current = runningGame.appid;
- setFocusDetailAction("enable");
- setDetailAppId(runningGame.appid);
- }
- }, [detailAppId, runningGame?.appid, runningGame?.configured]);
-
const selectedTarget = detailAppId ? targets.find((target) => target.appid === detailAppId) : null;
if (detailAppId === null) {
return (
- <PanelSection title="Games">
- <GameConfigurationSelector
- targets={targets}
- runningGame={runningGame}
- onSelect={(appid) => {
- setFocusDetailAction(targets.find((target) => target.appid === appid)?.configured ? "fps" : "enable");
- onSelect(appid);
- setDetailAppId(appid);
- }}
- onEnableAll={onEnableAll}
- onResetAll={onResetAll}
- />
- </PanelSection>
+ <>
+ <PanelSection title="Games">
+ <GameConfigurationSelector
+ targets={targets}
+ runningGame={runningGame}
+ onSelect={(appid) => {
+ setFocusConfiguredToggle(false);
+ setFocusDetailAction(targets.find((target) => target.appid === appid)?.configured ? "fps" : "enable");
+ onSelect(appid);
+ setDetailAppId(appid);
+ }}
+ onEnableAll={onEnableAll}
+ onResetAll={onResetAll}
+ focusConfiguredToggle={focusConfiguredToggle}
+ onConfiguredToggleFocused={clearConfiguredToggleFocusRequest}
+ />
+ </PanelSection>
+ <PanelSection title="Settings">
+ <PanelSectionRow>
+ <ToggleField
+ label="Show debug tab"
+ description="Show the raw configuration and generated files tab for troubleshooting."
+ checked={showDebugTab}
+ onChange={onShowDebugTabChange}
+ />
+ </PanelSectionRow>
+ </PanelSection>
+ </>
);
}
const profileLabel = selectedTarget?.name || "Game profile";
+ const profileTransport = selectedTarget
+ ? selectedTarget.transport.kind === "flatpak"
+ ? "Non-Steam · Flatpak"
+ : selectedTarget.nonSteam ? "Non-Steam" : "Steam"
+ : "Game";
const profileDescription = selectedTarget
- ? `${selectedTarget.nonSteam ? "Non-Steam" : "Steam"} · App ID ${selectedTarget.appid} · ${selectedTarget.configured ? "LSFG-VK Enabled" : "LSFG-VK not enabled"}`
+ ? `${profileTransport} · App ID ${selectedTarget.appid} · ${selectedTarget.configured ? "LSFG-VK Enabled" : "LSFG-VK not enabled"}`
: "Game is no longer available";
+ const enableProfile = async (appid: string, quitRunningGame = false) => {
+ if (!(await onEnable(appid))) return;
+ if (quitRunningGame) SteamClient.Apps.TerminateApp(appid, false);
+ setFocusFpsMultiplier(true);
+ };
const handleProfileAction = async () => {
if (selectedTarget?.configured) {
- promptedRunningAppId.current = detailAppId;
await onReset();
+ setFocusConfiguredToggle(true);
closeDetails();
- } else if (detailAppId && await onEnable(detailAppId)) {
- setFocusFpsMultiplier(true);
+ } else if (detailAppId) {
+ const isRunningUnconfigured = runningGame?.appid === detailAppId
+ && runningGame.nonSteam === false
+ && runningGame.transport.kind === "host"
+ && selectedTarget?.nonSteam === false
+ && selectedTarget?.transport.kind === "host"
+ && !runningGame.configured;
+ if (isRunningUnconfigured) {
+ showModal(
+ <ConfirmModal
+ strTitle="Game is running"
+ strDescription="Quit the game now so LSFG-VK is used on its next launch?"
+ strOKButtonText="Quit and enable"
+ strCancelButtonText="Enable without quitting"
+ onOK={() => void enableProfile(detailAppId, true)}
+ onCancel={() => void enableProfile(detailAppId)}
+ />,
+ );
+ } else {
+ await enableProfile(detailAppId);
+ }
}
};
@@ -143,6 +182,18 @@ export function ConfigurationTab({
</PanelSectionRow>
)}
</PanelSection>
+ {selectedTarget?.configured && selectedTarget.transport.kind === "flatpak" && selectedTarget.flatpakSupport?.support_status !== "ready" && (
+ <PanelSection>
+ <PanelSectionRow>
+ <ButtonItem
+ layout="below"
+ onClick={() => void onRepair(selectedTarget.appid)}
+ >
+ Repair Flatpak support
+ </ButtonItem>
+ </PanelSectionRow>
+ </PanelSection>
+ )}
{selectedTarget?.configured && (
<GameConfigurationControls
config={config}
@@ -151,6 +202,7 @@ export function ConfigurationTab({
onFpsMultiplierFocused={clearFpsFocusRequest}
showWorkarounds
workaroundTarget={selectedTarget || undefined}
+ onRepairWorkaround={selectedTarget ? () => onRepair(selectedTarget.appid) : undefined}
/>
)}
{selectedTarget?.configured && (
diff --git a/src/components/Content.tsx b/src/components/Content.tsx
index 1d7d2d1..43720c0 100644
--- a/src/components/Content.tsx
+++ b/src/components/Content.tsx
@@ -1,37 +1,47 @@
import { Tabs } from "@decky/ui";
import { useEffect, useRef, useState } from "react";
-import { FaFileAlt, FaGamepad, FaLayerGroup, FaList, FaTools } from "react-icons/fa";
+import { FaFileAlt, FaGamepad, FaList, FaTools } from "react-icons/fa";
import { ConfigurationData } from "../config/configSchema";
-import { tabStyles } from "../styles";
import { useGameConfiguration } from "../hooks/useGameConfiguration";
-import { useInstallationActions } from "../hooks/useInstallationActions";
-import { useInstallationStatus } from "../hooks/useLsfgHooks";
+import { useInstallation } from "../hooks/useLsfgHooks";
+import { tabStyles } from "../styles";
import { ConfigFileTab } from "./ConfigFileTab";
import { ConfigurationTab } from "./ConfigurationTab";
-import { FlatpaksTab } from "./FlatpaksTab";
import { NowPlayingTab } from "./NowPlayingTab";
import { SetupTab } from "./SetupTab";
const tabIcons = {
nowPlaying: <FaGamepad size={18} />,
- configuration: <FaList size={18} />,
- flatpak: <FaLayerGroup size={18} />,
+ games: <FaList size={18} />,
configFile: <FaFileAlt size={18} />,
setup: <FaTools size={18} />,
};
+const DEBUG_TAB_VISIBILITY_KEY = "lsfg-debug-tab-visible-v1";
+
+function usePersistentBoolean(key: string, defaultValue: boolean) {
+ const [value, setValue] = useState(() => {
+ try {
+ const stored = localStorage.getItem(key);
+ return stored === null ? defaultValue : stored === "true";
+ } catch {
+ return defaultValue;
+ }
+ });
+
+ useEffect(() => {
+ try {
+ localStorage.setItem(key, String(value));
+ } catch {
+ // Persisting the visibility preference is optional.
+ }
+ }, [key, value]);
+
+ return [value, setValue] as const;
+}
+
export function Content() {
const {
- isInstalled,
- installationStatus,
- setIsInstalled,
- setInstallationStatus,
- losslessScalingInstalled,
- losslessScalingStatus,
- steamBranchStatus,
- checkInstallation,
- } = useInstallationStatus();
- const {
config,
targets,
runningGame,
@@ -39,37 +49,48 @@ export function Content() {
save,
enable,
enableAll,
+ repair,
resetSelected,
resetAll,
reload,
} = useGameConfiguration();
- const { isInstalling, isUninstalling, handleInstall, handleUninstall } = useInstallationActions();
+ const {
+ isInstalled,
+ installationStatus,
+ losslessScalingInstalled,
+ losslessScalingStatus,
+ steamBranchStatus,
+ isInstalling,
+ isUninstalling,
+ install,
+ uninstall,
+ } = useInstallation(reload);
const [tab, setTab] = useState("Setup");
+ const [showDebugTab, setShowDebugTab] = usePersistentBoolean(DEBUG_TAB_VISIBILITY_KEY, true);
+ const previousRunningAppId = useRef<string | null>(null);
const setupComplete =
isInstalled &&
losslessScalingInstalled &&
steamBranchStatus?.success === true &&
steamBranchStatus.installed &&
!steamBranchStatus.needs_switch;
- const previousRunningState = useRef<{ appid: string; configured: boolean } | null>(null);
useEffect(() => {
if (!setupComplete) {
setTab("Setup");
return;
}
- setTab((current) => current === "Setup" ? (runningGame?.configured ? "NowPlaying" : "Configuration") : current);
- }, [runningGame?.configured, setupComplete]);
+ setTab((current) => current === "Setup" ? (runningGame?.configured ? "NowPlaying" : "Games") : current);
+ }, [runningGame?.appid, runningGame?.configured, setupComplete]);
useEffect(() => {
if (!setupComplete) return;
- const current = runningGame ? { appid: runningGame.appid, configured: runningGame.configured } : null;
- const previous = previousRunningState.current;
- previousRunningState.current = current;
- if (current?.appid && (current.appid !== previous?.appid || current.configured !== previous?.configured)) {
- setTab(current.configured ? "NowPlaying" : "Configuration");
- } else if (!current && previous) {
- setTab((currentTab) => currentTab === "NowPlaying" ? "Configuration" : currentTab);
+ const appid = runningGame?.appid || null;
+ const previous = previousRunningAppId.current;
+ previousRunningAppId.current = appid;
+ if (appid && appid !== previous) setTab(runningGame?.configured ? "NowPlaying" : "Games");
+ else if (!appid && previous) {
+ setTab((current) => current === "NowPlaying" ? "Games" : current);
}
}, [runningGame?.appid, runningGame?.configured, setupComplete]);
@@ -77,23 +98,17 @@ export function Content() {
if (isInstalled) void reload();
}, [isInstalled, reload]);
+ useEffect(() => {
+ if (!showDebugTab && tab === "ConfigFile") setTab("Games");
+ }, [showDebugTab, tab]);
+
const handleConfigChange = async (
fieldName: keyof ConfigurationData,
value: boolean | number | string | string[],
cleanupLaunchOptions = false,
- ) => {
- await save({ ...config, [fieldName]: value }, cleanupLaunchOptions);
- };
-
- const onInstall = () => {
- void handleInstall(setIsInstalled, setInstallationStatus, reload, checkInstallation);
- };
-
- const onUninstall = () => {
- void handleUninstall(setIsInstalled, setInstallationStatus, checkInstallation);
- };
+ ) => save({ ...config, [fieldName]: value }, cleanupLaunchOptions);
- const setupContent = (
+ const setup = (
<SetupTab
isInstalled={isInstalled}
installationStatus={installationStatus}
@@ -102,8 +117,8 @@ export function Content() {
steamBranchStatus={steamBranchStatus}
isInstalling={isInstalling}
isUninstalling={isUninstalling}
- onInstall={onInstall}
- onUninstall={onUninstall}
+ onInstall={() => void install()}
+ onUninstall={() => void uninstall()}
/>
);
@@ -116,34 +131,35 @@ export function Content() {
<NowPlayingTab
game={runningGame}
config={config}
- onConfigChange={(fieldName, value) => handleConfigChange(fieldName, value)}
+ onConfigChange={(field, value) => handleConfigChange(field, value)}
+ onRepair={repair}
/>
),
}] : []),
{
- id: "Configuration",
- title: tabIcons.configuration,
+ id: "Games",
+ title: tabIcons.games,
content: (
<ConfigurationTab
config={config}
targets={targets}
runningGame={runningGame}
+ showDebugTab={showDebugTab}
+ onShowDebugTabChange={setShowDebugTab}
onSelect={setSelectedAppId}
- onConfigChange={(fieldName, value) => handleConfigChange(fieldName, value, true)}
+ onConfigChange={(field, value) => handleConfigChange(field, value, true)}
onEnable={enable}
onEnableAll={enableAll}
+ onRepair={repair}
onReset={resetSelected}
onResetAll={resetAll}
/>
),
},
- { id: "Flatpak", title: tabIcons.flatpak, content: <FlatpaksTab /> },
- { id: "ConfigFile", title: tabIcons.configFile, content: <ConfigFileTab /> }, // comment out for prod
- { id: "Setup", title: tabIcons.setup, content: setupContent },
+ ...(showDebugTab ? [{ id: "ConfigFile", title: tabIcons.configFile, content: <ConfigFileTab /> }] : []),
+ { id: "Setup", title: tabIcons.setup, content: setup },
]
- : [
- { id: "Setup", title: tabIcons.setup, content: setupContent },
- ];
+ : [{ id: "Setup", title: tabIcons.setup, content: setup }];
return (
<div
@@ -151,7 +167,7 @@ export function Content() {
style={{ height: "95%", width: "300px", position: "fixed", marginTop: "-12px", overflow: "hidden" }}
>
<style>{tabStyles}</style>
- <Tabs activeTab={tab} onShowTab={setTab} tabs={tabs} />
+ <Tabs activeTab={!showDebugTab && tab === "ConfigFile" ? "Games" : tab} onShowTab={setTab} tabs={tabs} />
</div>
);
}
diff --git a/src/components/FlatpaksTab.tsx b/src/components/FlatpaksTab.tsx
deleted file mode 100644
index 8aab940..0000000
--- a/src/components/FlatpaksTab.tsx
+++ /dev/null
@@ -1,196 +0,0 @@
-import { useEffect, useState } from "react";
-import {
- ConfirmModal,
- Field,
- PanelSection,
- PanelSectionRow,
- ToggleField,
- showModal,
-} from "@decky/ui";
-import {
- checkFlatpakExtensionStatus,
- FlatpakApp,
- FlatpakAppInfo,
- FlatpakExtensionStatus,
- getFlatpakApps,
- installFlatpakExtension,
- removeFlatpakAppOverride,
- setFlatpakAppOverride,
- uninstallFlatpakExtension,
-} from "../api/lsfgApi";
-import { showErrorToast } from "../utils/toastUtils";
-import t from "../i18n/i18n";
-
-const runtimeVersions = [
- { version: "23.08", key: "installed_23_08" },
- { version: "24.08", key: "installed_24_08" },
- { version: "25.08", key: "installed_25_08" },
-] as const;
-
-interface RuntimeRowProps {
- version: string;
- installed: boolean;
- busy: boolean;
- onAction: () => void;
-}
-
-function RuntimeRow({ version, installed, busy, onAction }: RuntimeRowProps) {
- return (
- <PanelSectionRow>
- <ToggleField
- label={`Runtime ${version}`}
- description={busy ? "Updating..." : installed ? t("FLATPAK_INSTALLED", "Installed") : t("FLATPAK_NOT_INSTALLED", "Not installed")}
- checked={installed}
- onChange={() => onAction()}
- disabled={busy}
- />
- </PanelSectionRow>
- );
-}
-
-interface AppRowProps {
- app: FlatpakApp;
- runtimeReady: boolean;
- busy: boolean;
- onToggle: () => void;
-}
-
-function AppRow({ app, runtimeReady, busy, onToggle }: AppRowProps) {
- const configured = app.has_filesystem_override && app.has_env_override;
- const partial = app.has_filesystem_override || app.has_env_override;
- const status = configured
- ? runtimeReady
- ? t("FLATPAK_STATUS_READY", "Ready")
- : t("FLATPAK_STATUS_RUNTIME_MISSING", "Runtime missing")
- : partial
- ? t("FLATPAK_STATUS_PARTIAL", "Partial")
- : t("FLATPAK_STATUS_NOT_ENABLED", "Not enabled");
-
- return (
- <PanelSectionRow>
- <ToggleField
- label={app.app_name || app.app_id}
- description={`${app.app_id} - ${status}`}
- checked={configured}
- onChange={onToggle}
- disabled={busy}
- />
- </PanelSectionRow>
- );
-}
-
-export function FlatpaksTab() {
- const [extensionStatus, setExtensionStatus] = useState<FlatpakExtensionStatus | null>(null);
- const [apps, setApps] = useState<FlatpakAppInfo | null>(null);
- const [loading, setLoading] = useState(true);
- const [operation, setOperation] = useState<string | null>(null);
- const [error, setError] = useState<string | null>(null);
- const runtimeReady = extensionStatus?.success === true
- && runtimeVersions.some(({ key }) => extensionStatus[key]);
-
- const load = async () => {
- setLoading(true);
- try {
- const [nextStatus, nextApps] = await Promise.all([
- checkFlatpakExtensionStatus(),
- getFlatpakApps(),
- ]);
- setExtensionStatus(nextStatus);
- setApps(nextApps);
- } catch (loadError) {
- setError(String(loadError));
- } finally {
- setLoading(false);
- }
- };
-
- useEffect(() => {
- void load();
- }, []);
-
- const runExtensionOperation = async (version: string, installed: boolean) => {
- const action = installed ? "uninstall" : "install";
- setOperation(`${action}-${version}`);
- setError(null);
- try {
- const result = installed
- ? await uninstallFlatpakExtension(version)
- : await installFlatpakExtension(version);
- if (!result.success) throw new Error(result.error || result.message);
- setExtensionStatus(await checkFlatpakExtensionStatus());
- } catch (operationError) {
- const message = String(operationError);
- setError(message);
- showErrorToast("Flatpak operation failed", message);
- } finally {
- setOperation(null);
- }
- };
-
- const confirmExtensionOperation = (version: string, installed: boolean) => {
- if (!installed) {
- void runExtensionOperation(version, false);
- return;
- }
- showModal(
- <ConfirmModal
- strTitle={t("FLATPAK_UNINSTALL_TITLE", "Uninstall Runtime Extension")}
- strDescription={`${t("FLATPAK_UNINSTALL_CONFIRM_PREFIX", "Are you sure you want to uninstall the")} ${version} ${t("FLATPAK_UNINSTALL_CONFIRM_SUFFIX", "runtime extension?")}`}
- onOK={() => void runExtensionOperation(version, true)}
- onCancel={() => {}}
- />,
- );
- };
-
- const toggleApp = async (app: FlatpakApp) => {
- const configured = app.has_filesystem_override && app.has_env_override;
- setOperation(`app-${app.app_id}`);
- setError(null);
- try {
- const result = configured
- ? await removeFlatpakAppOverride(app.app_id)
- : await setFlatpakAppOverride(app.app_id);
- if (!result.success) throw new Error(result.error || result.message);
- setApps(await getFlatpakApps());
- } catch (operationError) {
- const message = String(operationError);
- setError(message);
- showErrorToast("Flatpak override failed", message);
- } finally {
- setOperation(null);
- }
- };
-
- if (loading) {
- return <PanelSection title="Flatpak Runtimes" spinner />;
- }
-
- return (
- <>
- <PanelSection title="Flatpak Runtimes">
- {error && <PanelSectionRow><Field label={t("FLATPAK_OPERATION_ERROR", "Operation failed")} description={error} /></PanelSectionRow>}
- {extensionStatus?.success ? runtimeVersions.map(({ version, key }) => (
- <RuntimeRow
- key={version}
- version={version}
- installed={extensionStatus[key]}
- busy={operation === `${extensionStatus[key] ? "uninstall" : "install"}-${version}`}
- onAction={() => confirmExtensionOperation(version, extensionStatus[key])}
- />
- )) : <PanelSectionRow><Field label={t("FLATPAK_ERROR", "Error")} description={extensionStatus?.error || error || t("FLATPAK_ERROR_STATUS", "Failed to check extension status")} /></PanelSectionRow>}
- </PanelSection>
-
- <PanelSection title="Applications">
- {apps?.success ? apps.apps.length ? apps.apps.map((app) => (
- <AppRow
- key={app.app_id}
- app={app}
- runtimeReady={runtimeReady}
- busy={operation === `app-${app.app_id}`}
- onToggle={() => void toggleApp(app)}
- />
- )) : <PanelSectionRow><Field label={t("FLATPAK_NO_APPS", "No Flatpak Apps Found")} description={t("FLATPAK_NO_APPS_DESC", "No Flatpak applications are currently installed")} /></PanelSectionRow> : <PanelSectionRow><Field label={t("FLATPAK_ERROR", "Error")} description={apps?.error || error || t("FLATPAK_ERROR_APPS", "Failed to load Flatpak applications")} /></PanelSectionRow>}
- </PanelSection>
- </>
- );
-}
diff --git a/src/components/GameConfigurationControls.tsx b/src/components/GameConfigurationControls.tsx
index 58ccd02..6bea2e0 100644
--- a/src/components/GameConfigurationControls.tsx
+++ b/src/components/GameConfigurationControls.tsx
@@ -10,7 +10,8 @@ interface Props {
autoFocusFpsMultiplier?: boolean;
onFpsMultiplierFocused?: () => void;
showWorkarounds?: boolean;
- workaroundTarget?: Pick<GameTarget, "appid" | "nonSteam">;
+ workaroundTarget?: Pick<GameTarget, "appid" | "nonSteam" | "transport">;
+ onRepairWorkaround?: () => Promise<boolean>;
}
export function GameConfigurationControls({
@@ -20,6 +21,7 @@ export function GameConfigurationControls({
onFpsMultiplierFocused,
showWorkarounds = false,
workaroundTarget,
+ onRepairWorkaround,
}: Props) {
return (
<>
@@ -31,7 +33,12 @@ export function GameConfigurationControls({
/>
<ConfigurationSection config={config} onConfigChange={onConfigChange} />
{showWorkarounds && workaroundTarget && (
- <WorkaroundsSection appId={workaroundTarget.appid} nonSteam={workaroundTarget.nonSteam} />
+ <WorkaroundsSection
+ appId={workaroundTarget.appid}
+ nonSteam={workaroundTarget.nonSteam}
+ transport={workaroundTarget.transport}
+ onRepair={onRepairWorkaround}
+ />
)}
</>
);
diff --git a/src/components/GameConfigurationSelector.tsx b/src/components/GameConfigurationSelector.tsx
index 5f23b91..92eacba 100644
--- a/src/components/GameConfigurationSelector.tsx
+++ b/src/components/GameConfigurationSelector.tsx
@@ -1,5 +1,5 @@
import { ButtonItem, ConfirmModal, Field, PanelSectionRow, showModal } from "@decky/ui";
-import { useEffect, useState } from "react";
+import { useEffect, useRef, useState, type RefObject } from "react";
import { RiArrowDownSFill, RiArrowUpSFill } from "react-icons/ri";
import { GameTarget } from "../hooks/useGameConfiguration";
@@ -9,10 +9,12 @@ interface Props {
onSelect: (appid: string) => void;
onEnableAll: () => Promise<void>;
onResetAll: () => Promise<void>;
+ focusConfiguredToggle?: boolean;
+ onConfiguredToggleFocused?: () => void;
}
-const CONFIGURED_COLLAPSED_KEY = "lsfg-configured-games-collapsed-v3";
-const AVAILABLE_COLLAPSED_KEY = "lsfg-available-games-collapsed-v2";
+const ENABLED_COLLAPSED_KEY = "lsfg-enabled-games-collapsed-v4";
+const AVAILABLE_COLLAPSED_KEY = "lsfg-available-games-collapsed-v3";
function usePersistentCollapsed(key: string) {
const [collapsed, setCollapsed] = useState(() => {
@@ -34,28 +36,36 @@ function usePersistentCollapsed(key: string) {
return [collapsed, () => setCollapsed((value) => !value)] as const;
}
+function targetDescription(game: GameTarget): string {
+ if (game.transport.kind === "flatpak") return "Non-Steam · Flatpak";
+ return game.nonSteam ? "Non-Steam" : "Steam";
+}
+
function GameGroup({
title,
games,
collapsed,
onToggle,
onSelect,
+ toggleRef,
}: {
title: string;
games: GameTarget[];
collapsed: boolean;
onToggle: () => void;
onSelect: (appid: string) => void;
+ toggleRef?: RefObject<HTMLDivElement>;
}) {
if (games.length === 0) return null;
return (
<>
<PanelSectionRow>
- <Field label={`${title} (${games.length})`} bottomSeparator="none" />
+ <Field label={title + " (" + games.length + ")"} bottomSeparator="none" />
</PanelSectionRow>
<PanelSectionRow>
<div
+ ref={toggleRef}
className="LSFG_GameGroupCollapseButton_Container"
style={{ marginTop: "-2px", marginBottom: "4px" }}
>
@@ -64,11 +74,7 @@ function GameGroup({
bottomSeparator={collapsed ? "standard" : "none"}
onClick={onToggle}
>
- {collapsed ? (
- <RiArrowDownSFill />
- ) : (
- <RiArrowUpSFill />
- )}
+ {collapsed ? <RiArrowDownSFill /> : <RiArrowUpSFill />}
</ButtonItem>
</div>
</PanelSectionRow>
@@ -76,7 +82,7 @@ function GameGroup({
<PanelSectionRow key={game.appid}>
<Field
label={game.name}
- description={game.nonSteam ? "Non-Steam" : "Steam"}
+ description={targetDescription(game)}
onActivate={() => onSelect(game.appid)}
highlightOnFocus
/>
@@ -86,22 +92,35 @@ function GameGroup({
);
}
-export function GameConfigurationSelector({ targets, runningGame, onSelect, onEnableAll, onResetAll }: Props) {
+export function GameConfigurationSelector({
+ targets,
+ runningGame,
+ onSelect,
+ onEnableAll,
+ onResetAll,
+ focusConfiguredToggle = false,
+ onConfiguredToggleFocused,
+}: Props) {
const sortGames = (games: GameTarget[]) => [...games].sort((a, b) => {
if (a.appid === runningGame?.appid) return -1;
if (b.appid === runningGame?.appid) return 1;
return a.name.localeCompare(b.name);
});
- const configuredGames = sortGames(targets.filter((game) => game.configured));
+ const enabledGames = sortGames(targets.filter((game) => game.configured));
const availableGames = sortGames(targets.filter((game) => !game.configured));
- const configuredSteamGames = configuredGames.filter((game) => !game.nonSteam);
- const configuredNonSteamGames = configuredGames.filter((game) => game.nonSteam);
- const availableSteamGames = availableGames.filter((game) => !game.nonSteam);
- const availableNonSteamGames = availableGames.filter((game) => game.nonSteam);
- const [configuredCollapsed, toggleConfigured] = usePersistentCollapsed(CONFIGURED_COLLAPSED_KEY);
- const [configuredNonSteamCollapsed, toggleConfiguredNonSteam] = usePersistentCollapsed(`${CONFIGURED_COLLAPSED_KEY}-non-steam`);
+ const [enabledCollapsed, toggleEnabled] = usePersistentCollapsed(ENABLED_COLLAPSED_KEY);
const [availableCollapsed, toggleAvailable] = usePersistentCollapsed(AVAILABLE_COLLAPSED_KEY);
- const [availableNonSteamCollapsed, toggleAvailableNonSteam] = usePersistentCollapsed(`${AVAILABLE_COLLAPSED_KEY}-non-steam`);
+ const enabledToggleRef = useRef<HTMLDivElement>(null);
+
+ useEffect(() => {
+ if (!focusConfiguredToggle) return;
+ const frame = requestAnimationFrame(() => {
+ enabledToggleRef.current?.querySelector<HTMLElement>('[role="button"], button')?.focus();
+ onConfiguredToggleFocused?.();
+ });
+ return () => cancelAnimationFrame(frame);
+ }, [enabledGames.length, focusConfiguredToggle, onConfiguredToggleFocused]);
+
const confirmResetAll = () => {
showModal(
<ConfirmModal
@@ -113,11 +132,12 @@ export function GameConfigurationSelector({ targets, runningGame, onSelect, onEn
/>,
);
};
+
const confirmEnableAll = () => {
showModal(
<ConfirmModal
strTitle="Enable all available games?"
- strDescription="Create individual LSFG-VK profiles for every available game using the plugin defaults."
+ strDescription="Create individual LSFG-VK profiles for every available game using the plugin defaults. Flatpak targets will be provisioned as needed."
strOKButtonText="Enable all"
strCancelButtonText="Cancel"
onOK={() => void onEnableAll()}
@@ -133,6 +153,7 @@ export function GameConfigurationSelector({ targets, runningGame, onSelect, onEn
.LSFG_GameGroupCollapseButton_Container > div > div > div > button,
.LSFG_GameGroupCollapseButton_Container > div > div > div > div > button {
height: 24px !important;
+ min-height: 24px !important;
padding: 0 !important;
display: flex !important;
align-items: center !important;
@@ -150,41 +171,28 @@ export function GameConfigurationSelector({ targets, runningGame, onSelect, onEn
<Field label="No installed games" description="Steam has not reported any eligible games" />
</PanelSectionRow>
)}
- {availableGames.length > 0 && (
- <PanelSectionRow>
- <ButtonItem layout="below" onClick={confirmEnableAll}>
- Enable all available games
- </ButtonItem>
- </PanelSectionRow>
- )}
<GameGroup
- title="LSFG-VK Enabled"
- games={configuredSteamGames}
- collapsed={configuredCollapsed}
- onToggle={toggleConfigured}
+ title="Enabled"
+ games={enabledGames}
+ collapsed={enabledCollapsed}
+ onToggle={toggleEnabled}
onSelect={onSelect}
+ toggleRef={enabledToggleRef}
/>
<GameGroup
- title="LSFG-VK Enabled (Non-Steam)"
- games={configuredNonSteamGames}
- collapsed={configuredNonSteamCollapsed}
- onToggle={toggleConfiguredNonSteam}
- onSelect={onSelect}
- />
- <GameGroup
- title="Available games"
- games={availableSteamGames}
+ title="Available"
+ games={availableGames}
collapsed={availableCollapsed}
onToggle={toggleAvailable}
onSelect={onSelect}
/>
- <GameGroup
- title="Available games (Non-Steam)"
- games={availableNonSteamGames}
- collapsed={availableNonSteamCollapsed}
- onToggle={toggleAvailableNonSteam}
- onSelect={onSelect}
- />
+ {availableGames.length > 0 && (
+ <PanelSectionRow>
+ <ButtonItem layout="below" onClick={confirmEnableAll}>
+ Enable all available games
+ </ButtonItem>
+ </PanelSectionRow>
+ )}
<PanelSectionRow>
<ButtonItem
layout="below"
diff --git a/src/components/InstallationButton.tsx b/src/components/InstallationButton.tsx
deleted file mode 100644
index 1bf10ac..0000000
--- a/src/components/InstallationButton.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import { ButtonItem, PanelSectionRow } from "@decky/ui";
-import t from '../i18n/i18n';
-
-interface InstallationButtonProps {
- isInstalled: boolean;
- isInstalling: boolean;
- isUninstalling: boolean;
- onInstall: () => void;
- onUninstall: () => void;
-}
-
-export function InstallationButton({
- isInstalled,
- isInstalling,
- isUninstalling,
- onInstall,
- onUninstall
-}: InstallationButtonProps) {
- const label = isInstalling
- ? t('INSTALL_INSTALLING', 'Installing...')
- : isUninstalling
- ? t('INSTALL_UNINSTALLING', 'Uninstalling...')
- : isInstalled
- ? t('INSTALL_UNINSTALL_BTN', 'Uninstall LSFG-VK')
- : t('INSTALL_INSTALL_BTN', 'Install LSFG-VK');
-
- return (
- <PanelSectionRow>
- <ButtonItem
- layout="below"
- onClick={isInstalled ? onUninstall : onInstall}
- disabled={isInstalling || isUninstalling}
- >
- {label}
- </ButtonItem>
- </PanelSectionRow>
- );
-}
diff --git a/src/components/NowPlayingTab.tsx b/src/components/NowPlayingTab.tsx
index 188c56a..57858db 100644
--- a/src/components/NowPlayingTab.tsx
+++ b/src/components/NowPlayingTab.tsx
@@ -1,4 +1,5 @@
-import { Field, Focusable, PanelSection, PanelSectionRow } from "@decky/ui";
+import { ButtonItem, Field, Focusable, PanelSection, PanelSectionRow } from "@decky/ui";
+import { useState } from "react";
import { ConfigurationData } from "../config/configSchema";
import { GameTarget } from "../hooks/useGameConfiguration";
import { GameConfigurationControls } from "./GameConfigurationControls";
@@ -6,20 +7,66 @@ import { GameConfigurationControls } from "./GameConfigurationControls";
interface Props {
game: GameTarget;
config: ConfigurationData;
- onConfigChange: (fieldName: keyof ConfigurationData, value: boolean | number | string | string[]) => Promise<void>;
+ onConfigChange: (
+ fieldName: keyof ConfigurationData,
+ value: boolean | number | string | string[],
+ ) => Promise<void>;
+ onRepair: (appid: string) => Promise<boolean>;
}
-export function NowPlayingTab({ game, config, onConfigChange }: Props) {
+function targetDescription(game: GameTarget): string {
+ if (game.transport.kind === "flatpak") return "Non-Steam · Flatpak";
+ return game.nonSteam ? "Non-Steam" : "Steam";
+}
+
+export function NowPlayingTab({
+ game,
+ config,
+ onConfigChange,
+ onRepair,
+}: Props) {
+ const [busy, setBusy] = useState(false);
+ const supportNeedsRepair =
+ game.transport.kind === "flatpak" &&
+ game.flatpakSupport?.support_status !== "ready";
+
+ const handleRepair = async () => {
+ if (busy) return;
+ setBusy(true);
+ try {
+ await onRepair(game.appid);
+ } finally {
+ setBusy(false);
+ }
+ };
+
return (
<Focusable>
- <PanelSection>
+ <PanelSection title="Now Playing">
<PanelSectionRow>
- <Field
- label={game.name}
- />
+ <Field label={game.name} description={targetDescription(game)} />
</PanelSectionRow>
</PanelSection>
- <GameConfigurationControls config={config} onConfigChange={onConfigChange} showWorkarounds={false} />
+ {supportNeedsRepair && (
+ <PanelSection>
+ <PanelSectionRow>
+ <Field
+ label="Flatpak support needs repair"
+ description={game.flatpakSupport?.error || "The target runtime extension is not ready."}
+ />
+ </PanelSectionRow>
+ <PanelSectionRow>
+ <ButtonItem layout="below" disabled={busy} onClick={() => void handleRepair()}>
+ {busy ? "Repairing..." : "Repair Flatpak support"}
+ </ButtonItem>
+ </PanelSectionRow>
+ </PanelSection>
+ )}
+ <GameConfigurationControls
+ config={config}
+ onConfigChange={onConfigChange}
+ showWorkarounds={false}
+ />
</Focusable>
);
}
diff --git a/src/components/ProfileDetails.tsx b/src/components/ProfileDetails.tsx
index 056abbb..4dd8891 100644
--- a/src/components/ProfileDetails.tsx
+++ b/src/components/ProfileDetails.tsx
@@ -24,12 +24,12 @@ export function ProfileDetails({ description }: ProfileDetailsProps) {
bottomSeparator={expanded ? "none" : "standard"}
onClick={() => setExpanded((value) => !value)}
>
- {expanded ? <RiArrowUpSFill /> : <RiArrowDownSFill />} Details
+ {expanded ? <RiArrowUpSFill /> : <RiArrowDownSFill />} Game Details
</ButtonItem>
</PanelSectionRow>
{expanded && (
<PanelSectionRow>
- <Field label="Details" description={description} />
+ <Field label="Game Details" description={description} />
</PanelSectionRow>
)}
</Focusable>
diff --git a/src/components/SetupTab.tsx b/src/components/SetupTab.tsx
index 98d6e78..d769200 100644
--- a/src/components/SetupTab.tsx
+++ b/src/components/SetupTab.tsx
@@ -1,7 +1,6 @@
-import { PanelSection } from "@decky/ui";
-import type { SteamBranchStatus } from "../api/lsfgApi";
-import { InstallationButton } from "./InstallationButton";
-import { StatusDisplay } from "./StatusDisplay";
+import { ButtonItem, Field, PanelSection, PanelSectionRow } from "@decky/ui";
+import { type SteamBranchStatus } from "../api/lsfgApi";
+import t from "../i18n/i18n";
interface SetupTabProps {
isInstalled: boolean;
@@ -15,32 +14,55 @@ interface SetupTabProps {
onUninstall: () => void;
}
-export function SetupTab({
- isInstalled,
- installationStatus,
- losslessScalingInstalled,
- losslessScalingStatus,
- steamBranchStatus,
- isInstalling,
- isUninstalling,
- onInstall,
- onUninstall,
-}: SetupTabProps) {
+export function SetupTab(props: SetupTabProps) {
+ const {
+ isInstalled,
+ installationStatus,
+ losslessScalingInstalled,
+ losslessScalingStatus,
+ steamBranchStatus,
+ isInstalling,
+ isUninstalling,
+ onInstall,
+ onUninstall,
+ } = props;
+ const losslessScalingAppInstalled = losslessScalingInstalled || steamBranchStatus?.installed === true;
+ const buttonLabel = isInstalling
+ ? t("INSTALL_INSTALLING", "Installing...")
+ : isUninstalling
+ ? t("INSTALL_UNINSTALLING", "Uninstalling...")
+ : isInstalled
+ ? t("INSTALL_UNINSTALL_BTN", "Uninstall LSFG-VK")
+ : t("INSTALL_INSTALL_BTN", "Install LSFG-VK");
+
return (
<PanelSection title="Setup">
- <StatusDisplay
- installationStatus={installationStatus}
- losslessScalingInstalled={losslessScalingInstalled}
- losslessScalingStatus={losslessScalingStatus}
- steamBranchStatus={steamBranchStatus}
- />
- <InstallationButton
- isInstalled={isInstalled}
- isInstalling={isInstalling}
- isUninstalling={isUninstalling}
- onInstall={onInstall}
- onUninstall={onUninstall}
- />
+ <PanelSectionRow>
+ <Field
+ 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>
);
}
diff --git a/src/components/StatusDisplay.tsx b/src/components/StatusDisplay.tsx
deleted file mode 100644
index b1a98e5..0000000
--- a/src/components/StatusDisplay.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-import { Field, PanelSectionRow } from "@decky/ui";
-import type { SteamBranchStatus } from "../api/lsfgApi";
-
-interface StatusDisplayProps {
- installationStatus: string;
- losslessScalingInstalled: boolean;
- losslessScalingStatus: string;
- steamBranchStatus: SteamBranchStatus | null;
-}
-
-export function StatusDisplay({
- installationStatus,
- losslessScalingInstalled,
- losslessScalingStatus,
- steamBranchStatus
-}: StatusDisplayProps) {
- const losslessScalingAppInstalled = losslessScalingInstalled || steamBranchStatus?.installed === true;
-
- return (
- <>
- <PanelSectionRow>
- <Field
- 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>
- )}
- </>
- );
-}
diff --git a/src/components/WorkaroundsSection.tsx b/src/components/WorkaroundsSection.tsx
index e990784..5587392 100644
--- a/src/components/WorkaroundsSection.tsx
+++ b/src/components/WorkaroundsSection.tsx
@@ -1,16 +1,19 @@
import { ButtonItem, Field, PanelSectionRow, SliderField, ToggleField } from "@decky/ui";
import { useEffect, useState } from "react";
import { RiArrowDownSFill, RiArrowUpSFill } from "react-icons/ri";
+import type { TargetTransport } from "../api/lsfgApi";
import { usePerAppWorkarounds } from "../hooks/usePerAppWorkarounds";
import t from "../i18n/i18n";
-import type { WorkaroundField } from "../utils/steamLaunchOptions";
+import type { WorkaroundField } from "../hooks/usePerAppWorkarounds";
interface WorkaroundsSectionProps {
appId: string;
nonSteam: boolean;
+ transport: TargetTransport;
+ onRepair?: () => Promise<boolean>;
}
-const WORKAROUNDS_COLLAPSED_KEY = "lsfg-workarounds-collapsed";
+const WORKAROUNDS_COLLAPSED_KEY = "lsfg-workarounds-collapsed-v2";
type ToggleWorkaroundField = Exclude<WorkaroundField, "dxvkFrameRate">;
const TOGGLE_ROWS: readonly {
@@ -21,18 +24,25 @@ const TOGGLE_ROWS: readonly {
description: string;
}[] = [
{
+ field: "disableSteamdeckMode",
+ labelKey: "CONFIG_DISABLE_STEAMDECK_MODE",
+ label: "Disable Steam Deck Mode",
+ descriptionKey: "CONFIG_DISABLE_STEAMDECK_MODE_DESC",
+ description: "Disables a game-specific Steam Deck compatibility switch. Requires game restart to apply.",
+ },
+ {
field: "disableGamescopeWsi",
labelKey: "CONFIG_DISABLE_GAMESCOPE_WSI",
label: "Disable Gamescope WSI",
descriptionKey: "CONFIG_DISABLE_GAMESCOPE_WSI_DESC",
- description: "Adds ENABLE_GAMESCOPE_WSI=0 without changing HDR. Requires game restart to apply.",
+ description: "Adds ENABLE_GAMESCOPE_WSI=0. Requires game restart to apply.",
},
{
- field: "disableSteamdeckMode",
- labelKey: "CONFIG_DISABLE_STEAMDECK_MODE",
- label: "Disable Steam Deck Mode",
- descriptionKey: "CONFIG_DISABLE_STEAMDECK_MODE_DESC",
- description: "Disables a game-specific Steam Deck compatibility switch. Requires game restart to apply.",
+ field: "disableHdr",
+ labelKey: "CONFIG_DISABLE_HDR",
+ label: "Disable HDR",
+ descriptionKey: "CONFIG_DISABLE_HDR_DESC",
+ description: "Prevents DXVK from exposing HDR to the game. Requires game restart to apply.",
},
{
field: "disableVkbasalt",
@@ -71,18 +81,28 @@ function usePersistentCollapsed() {
return [collapsed, () => setCollapsed((value) => !value)] as const;
}
-export function WorkaroundsSection({ appId, nonSteam }: WorkaroundsSectionProps) {
+export function WorkaroundsSection({ appId, nonSteam, transport, onRepair }: WorkaroundsSectionProps) {
const [collapsed, toggleCollapsed] = usePersistentCollapsed();
- const { status, snapshot, refresh, update, error } = usePerAppWorkarounds(appId, nonSteam);
- const state = snapshot?.parsed.state;
- const issues = snapshot?.parsed.issues || [];
- const controlsDisabled = status !== "ready" || state === undefined;
+ const { status, snapshot, refresh, update, error } = usePerAppWorkarounds(appId, nonSteam, transport);
+ const [repairing, setRepairing] = useState(false);
+ const state = snapshot?.state;
+ const controlsDisabled = status !== "ready" || state === undefined || snapshot?.wrapperOwned !== true || snapshot.integrationInstalled !== true;
const [fpsValue, setFpsValue] = useState<number | null>(null);
const effectiveFpsValue = fpsValue ?? state?.dxvkFrameRate ?? 0;
const fpsLabel = effectiveFpsValue > 0
? `${effectiveFpsValue} FPS`
: t("CONFIG_BASE_FPS_CAP_OFF", "Off");
+ const handleRepair = async () => {
+ if (!onRepair || repairing) return;
+ setRepairing(true);
+ try {
+ if (await onRepair()) await refresh();
+ } finally {
+ setRepairing(false);
+ }
+ };
+
useEffect(() => {
setFpsValue(state?.dxvkFrameRate ?? null);
}, [state?.dxvkFrameRate, status]);
@@ -148,13 +168,19 @@ export function WorkaroundsSection({ appId, nonSteam }: WorkaroundsSectionProps)
</PanelSectionRow>
</>
)}
- {status === "ready" && issues.length > 0 && (
- <PanelSectionRow>
- <Field
- label="Launch options need attention"
- description={`${issues.join(" ")} Adjusting a workaround will normalize its managed values.`}
- />
- </PanelSectionRow>
+ {status === "ready" && snapshot && (!snapshot.wrapperOwned || !snapshot.integrationInstalled) && (
+ <>
+ <PanelSectionRow>
+ <Field label="Wrapper needs to be reinstalled" />
+ </PanelSectionRow>
+ {onRepair && (
+ <PanelSectionRow>
+ <ButtonItem layout="below" disabled={repairing} onClick={() => void handleRepair()}>
+ {repairing ? "Reinstalling..." : "Reinstall wrapper"}
+ </ButtonItem>
+ </PanelSectionRow>
+ )}
+ </>
)}
<PanelSectionRow>
diff --git a/src/components/index.ts b/src/components/index.ts
index 37a8edb..bca6f6f 100644
--- a/src/components/index.ts
+++ b/src/components/index.ts
@@ -1,12 +1,9 @@
export { Content } from "./Content";
-export { StatusDisplay } from "./StatusDisplay";
-export { InstallationButton } from "./InstallationButton";
export { ConfigurationSection } from "./ConfigurationSection";
export { FpsMultiplierControl } from "./FpsMultiplierControl";
export { ConfigurationTab } from "./ConfigurationTab";
-export { SetupTab } from "./SetupTab";
export { ConfigFileTab } from "./ConfigFileTab";
-export { FlatpaksTab } from "./FlatpaksTab";
+export { SetupTab } from "./SetupTab";
export { GameConfigurationSelector } from "./GameConfigurationSelector";
export { GameConfigurationControls } from "./GameConfigurationControls";
export { NowPlayingTab } from "./NowPlayingTab";