summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/api/lsfgApi.ts81
-rw-r--r--src/components/ConfigurationTab.tsx22
-rw-r--r--src/components/Content.tsx41
-rw-r--r--src/components/FlatpaksTab.tsx196
-rw-r--r--src/components/GameConfigurationControls.tsx3
-rw-r--r--src/components/GameConfigurationSelector.tsx101
-rw-r--r--src/components/NowPlayingTab.tsx93
-rw-r--r--src/components/SetupTab.tsx142
-rw-r--r--src/components/WorkaroundsSection.tsx6
-rw-r--r--src/components/index.ts2
-rw-r--r--src/hooks/useGameConfiguration.ts78
-rw-r--r--src/hooks/usePerAppWorkarounds.ts24
-rw-r--r--src/types.d.ts1
13 files changed, 399 insertions, 391 deletions
diff --git a/src/api/lsfgApi.ts b/src/api/lsfgApi.ts
index 087228c..f487dff 100644
--- a/src/api/lsfgApi.ts
+++ b/src/api/lsfgApi.ts
@@ -43,7 +43,34 @@ export interface GameConfigEntry {
profile: string;
config: LsfgConfig;
}
-export interface InstalledGame { appid: string; name: string; nonSteam: boolean; }
+export type TargetTransport =
+ | { kind: "host" }
+ | { kind: "flatpak"; flatpakAppId: string };
+
+export type FlatpakTargetSupportStatus = "ready" | "needs-runtime" | "unsupported" | "error";
+
+export interface FlatpakTargetSupport {
+ success: boolean;
+ message?: string;
+ error?: string | null;
+ flatpak_app_id?: string;
+ runtime?: string | null;
+ runtime_branch?: string | null;
+ support_status: FlatpakTargetSupportStatus;
+ extension_installed: boolean;
+ installed_branches: string[];
+}
+
+export interface InstalledGame {
+ appid: string;
+ name: string;
+ nonSteam: boolean;
+ transport: TargetTransport;
+ executable?: string;
+ arguments?: string;
+ startDir?: string;
+ flatpakSupport?: FlatpakTargetSupport;
+}
export interface InstalledGamesResult { success: boolean; games?: InstalledGame[]; error?: string; }
export interface GlobalConfig { dll: string; no_fp16: boolean; }
@@ -79,6 +106,7 @@ export interface WorkaroundStateResult {
wrapper_owned?: boolean;
shortcut_exe?: string | null;
command_token_added?: boolean;
+ transport?: TargetTransport | null;
}
export interface FileContentResult {
@@ -88,37 +116,25 @@ export interface FileContentResult {
error?: string;
}
-// Flatpak management interfaces
export interface FlatpakExtensionStatus {
success: boolean;
message: string;
- error?: string;
- installed_23_08: boolean;
- installed_24_08: boolean;
- installed_25_08: boolean;
+ error?: string | null;
+ available: boolean;
+ extension_id: string;
+ supported_branches: string[];
+ installed_branches: string[];
+ owned_branches: string[];
+ ownership_uncertain: boolean;
}
-export interface FlatpakApp {
- app_id: string;
- app_name: string;
- has_filesystem_override: boolean;
- has_env_override: boolean;
-}
-
-export interface FlatpakAppInfo {
- success: boolean;
- message: string;
- error?: string;
- apps: FlatpakApp[];
- total_apps: number;
-}
-
-export interface FlatpakOperationResult {
+export interface FlatpakCleanupResult {
success: boolean;
message: string;
- error?: string;
- app_id?: string;
- operation?: string;
+ error?: string | null;
+ removed_branches: string[];
+ preserved_branches: string[];
+ ownership_uncertain: boolean;
}
// API functions
@@ -128,13 +144,13 @@ export const checkLsfgVkInstalled = callable<[], InstallationStatus>("check_lsfg
export const getLosslessScalingBranchStatus = callable<[], SteamBranchStatus>("get_lossless_scaling_branch_status");
export const getConfigFileContent = callable<[], FileContentResult>("get_config_file_content");
-// Flatpak management API functions
-export const checkFlatpakExtensionStatus = callable<[], FlatpakExtensionStatus>("check_flatpak_extension_status");
-export const installFlatpakExtension = callable<[string], FlatpakOperationResult>("install_flatpak_extension");
-export const uninstallFlatpakExtension = callable<[string], FlatpakOperationResult>("uninstall_flatpak_extension");
-export const getFlatpakApps = callable<[], FlatpakAppInfo>("get_flatpak_apps");
-export const setFlatpakAppOverride = callable<[string], FlatpakOperationResult>("set_flatpak_app_override");
-export const removeFlatpakAppOverride = callable<[string], FlatpakOperationResult>("remove_flatpak_app_override");
+export const getFlatpakSupportStatus = callable<[], FlatpakExtensionStatus>("get_flatpak_support_status");
+export const ensureFlatpakSupport = callable<[string], FlatpakTargetSupport>("ensure_flatpak_support");
+export const repairFlatpakSupport = callable<[string], FlatpakTargetSupport>("repair_flatpak_support");
+export const removePluginOwnedFlatpakExtensions = callable<
+ [],
+ FlatpakCleanupResult
+>("remove_plugin_owned_flatpak_extensions");
export const getGameConfigs = callable<[], GameConfigsResult>("get_game_configs");
export const getInstalledGames = callable<[], InstalledGamesResult>("get_installed_games");
@@ -147,5 +163,6 @@ export const setWorkaroundState = callable<[
WorkaroundState,
string | null | undefined,
boolean,
+ TargetTransport | null | undefined,
], WorkaroundStateResult>("set_workaround_state");
export const removeWorkaroundState = callable<[string], WorkaroundStateResult>("remove_workaround_state");
diff --git a/src/components/ConfigurationTab.tsx b/src/components/ConfigurationTab.tsx
index 62a971b..90c1a7b 100644
--- a/src/components/ConfigurationTab.tsx
+++ b/src/components/ConfigurationTab.tsx
@@ -14,7 +14,6 @@ interface ConfigurationTabProps {
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>;
@@ -27,7 +26,6 @@ export function ConfigurationTab({
onSelect,
onConfigChange,
onEnable,
- onEnableAll,
onRepair,
onReset,
onResetAll,
@@ -86,7 +84,6 @@ export function ConfigurationTab({
onSelect(appid);
setDetailAppId(appid);
}}
- onEnableAll={onEnableAll}
onResetAll={onResetAll}
focusConfiguredToggle={focusConfiguredToggle}
onConfiguredToggleFocused={clearConfiguredToggleFocusRequest}
@@ -96,8 +93,13 @@ export function ConfigurationTab({
}
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 handleProfileAction = async () => {
if (selectedTarget?.configured) {
@@ -151,6 +153,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}
diff --git a/src/components/Content.tsx b/src/components/Content.tsx
index 59dc514..ff2376b 100644
--- a/src/components/Content.tsx
+++ b/src/components/Content.tsx
@@ -1,22 +1,18 @@
import { Tabs } from "@decky/ui";
import { useEffect, useRef, useState } from "react";
-import { FaFileAlt, FaGamepad, FaLayerGroup, FaList, FaTools } from "react-icons/fa";
+import { 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 { 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} />,
- configFile: <FaFileAlt size={18} />,
+ games: <FaList size={18} />,
setup: <FaTools size={18} />,
};
@@ -38,7 +34,6 @@ export function Content() {
setSelectedAppId,
save,
enable,
- enableAll,
repair,
resetSelected,
resetAll,
@@ -52,25 +47,25 @@ export function Content() {
steamBranchStatus?.success === true &&
steamBranchStatus.installed &&
!steamBranchStatus.needs_switch;
- const previousRunningState = useRef<{ appid: string; configured: boolean } | null>(null);
+ const previousRunningAppId = useRef<string | 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 ? "NowPlaying" : "Games") : current);
+ }, [runningGame?.appid, 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("NowPlaying");
+ } else if (!appid && previous) {
+ setTab((currentTab) => currentTab === "NowPlaying" ? "Games" : currentTab);
}
}, [runningGame?.appid, runningGame?.configured, setupComplete]);
@@ -105,12 +100,13 @@ export function Content() {
isUninstalling={isUninstalling}
onInstall={onInstall}
onUninstall={onUninstall}
+ flatpakRelevant={targets.some((target) => target.transport.kind === "flatpak")}
/>
);
const tabs = setupComplete
? [
- ...(runningGame?.configured ? [{
+ ...(runningGame ? [{
id: "NowPlaying",
title: tabIcons.nowPlaying,
content: (
@@ -118,12 +114,14 @@ export function Content() {
game={runningGame}
config={config}
onConfigChange={(fieldName, value) => handleConfigChange(fieldName, value)}
+ onEnable={enable}
+ onRepair={repair}
/>
),
}] : []),
{
- id: "Configuration",
- title: tabIcons.configuration,
+ id: "Games",
+ title: tabIcons.games,
content: (
<ConfigurationTab
config={config}
@@ -132,15 +130,12 @@ export function Content() {
onSelect={setSelectedAppId}
onConfigChange={(fieldName, value) => handleConfigChange(fieldName, 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 },
]
: [
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 7025f78..6bea2e0 100644
--- a/src/components/GameConfigurationControls.tsx
+++ b/src/components/GameConfigurationControls.tsx
@@ -10,7 +10,7 @@ interface Props {
autoFocusFpsMultiplier?: boolean;
onFpsMultiplierFocused?: () => void;
showWorkarounds?: boolean;
- workaroundTarget?: Pick<GameTarget, "appid" | "nonSteam">;
+ workaroundTarget?: Pick<GameTarget, "appid" | "nonSteam" | "transport">;
onRepairWorkaround?: () => Promise<boolean>;
}
@@ -36,6 +36,7 @@ export function GameConfigurationControls({
<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 df0c3e9..2a92c67 100644
--- a/src/components/GameConfigurationSelector.tsx
+++ b/src/components/GameConfigurationSelector.tsx
@@ -7,14 +7,13 @@ interface Props {
targets: GameTarget[];
runningGame: GameTarget | null;
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(() => {
@@ -36,6 +35,11 @@ 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,
@@ -56,7 +60,7 @@ function GameGroup({
return (
<>
<PanelSectionRow>
- <Field label={`${title} (${games.length})`} bottomSeparator="none" />
+ <Field label={title + " (" + games.length + ")"} bottomSeparator="none" />
</PanelSectionRow>
<PanelSectionRow>
<div
@@ -69,11 +73,7 @@ function GameGroup({
bottomSeparator={collapsed ? "standard" : "none"}
onClick={onToggle}
>
- {collapsed ? (
- <RiArrowDownSFill />
- ) : (
- <RiArrowUpSFill />
- )}
+ {collapsed ? <RiArrowDownSFill /> : <RiArrowUpSFill />}
</ButtonItem>
</div>
</PanelSectionRow>
@@ -81,7 +81,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
/>
@@ -95,7 +95,6 @@ export function GameConfigurationSelector({
targets,
runningGame,
onSelect,
- onEnableAll,
onResetAll,
focusConfiguredToggle = false,
onConfiguredToggleFocused,
@@ -105,26 +104,20 @@ export function GameConfigurationSelector({
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 configuredToggleRef = useRef<HTMLDivElement>(null);
+ const enabledToggleRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!focusConfiguredToggle) return;
const frame = requestAnimationFrame(() => {
- configuredToggleRef.current?.querySelector<HTMLElement>('[role="button"], button')?.focus();
+ enabledToggleRef.current?.querySelector<HTMLElement>('[role="button"], button')?.focus();
onConfiguredToggleFocused?.();
});
return () => cancelAnimationFrame(frame);
- }, [configuredGames.length, focusConfiguredToggle, onConfiguredToggleFocused]);
+ }, [enabledGames.length, focusConfiguredToggle, onConfiguredToggleFocused]);
const confirmResetAll = () => {
showModal(
@@ -137,79 +130,29 @@ export function GameConfigurationSelector({
/>,
);
};
- const confirmEnableAll = () => {
- showModal(
- <ConfirmModal
- strTitle="Enable all available games?"
- strDescription="Create individual LSFG-VK profiles for every available game using the plugin defaults."
- strOKButtonText="Enable all"
- strCancelButtonText="Cancel"
- onOK={() => void onEnableAll()}
- onCancel={() => {}}
- />,
- );
- };
return (
<>
- <style>
- {`
- .LSFG_GameGroupCollapseButton_Container > div > div > div > button,
- .LSFG_GameGroupCollapseButton_Container > div > div > div > div > button {
- height: 24px !important;
- padding: 0 !important;
- display: flex !important;
- align-items: center !important;
- justify-content: center !important;
- }
-
- .LSFG_GameGroupCollapseButton_Container svg {
- display: block;
- margin: 0;
- }
- `}
- </style>
{targets.length === 0 && (
<PanelSectionRow>
<Field label="No installed games" description="Steam has not reported any eligible games" />
</PanelSectionRow>
)}
<GameGroup
- title="LSFG-VK Enabled"
- games={configuredSteamGames}
- collapsed={configuredCollapsed}
- onToggle={toggleConfigured}
+ title="Enabled"
+ games={enabledGames}
+ collapsed={enabledCollapsed}
+ onToggle={toggleEnabled}
onSelect={onSelect}
- toggleRef={configuredToggleRef}
+ 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/NowPlayingTab.tsx b/src/components/NowPlayingTab.tsx
index 188c56a..5bce5c4 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,96 @@ 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>;
+ onEnable: (appid: string) => Promise<boolean>;
+ 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,
+ onEnable,
+ onRepair,
+}: Props) {
+ const [busy, setBusy] = useState(false);
+ const supportNeedsRepair =
+ game.configured &&
+ game.transport.kind === "flatpak" &&
+ game.flatpakSupport?.support_status !== "ready";
+
+ const handleEnable = async () => {
+ if (busy) return;
+ setBusy(true);
+ try {
+ await onEnable(game.appid);
+ } finally {
+ setBusy(false);
+ }
+ };
+
+ 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} />
+ {!game.configured && (
+ <PanelSection>
+ <PanelSectionRow>
+ <Field
+ label="LSFG-VK is available"
+ description="This target is not enabled yet. Create its AppID profile before the next launch."
+ />
+ </PanelSectionRow>
+ <PanelSectionRow>
+ <ButtonItem layout="below" disabled={busy} onClick={() => void handleEnable()}>
+ {busy ? "Enabling..." : "Enable LSFG-VK"}
+ </ButtonItem>
+ </PanelSectionRow>
+ </PanelSection>
+ )}
+ {game.configured && 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>
+ )}
+ {game.configured && (
+ <GameConfigurationControls
+ config={config}
+ onConfigChange={onConfigChange}
+ showWorkarounds={false}
+ />
+ )}
</Focusable>
);
}
diff --git a/src/components/SetupTab.tsx b/src/components/SetupTab.tsx
index 98d6e78..9c854e1 100644
--- a/src/components/SetupTab.tsx
+++ b/src/components/SetupTab.tsx
@@ -1,5 +1,11 @@
-import { PanelSection } from "@decky/ui";
-import type { SteamBranchStatus } from "../api/lsfgApi";
+import { ButtonItem, ConfirmModal, Field, PanelSection, PanelSectionRow, showModal } from "@decky/ui";
+import { useEffect, useState } from "react";
+import {
+ getFlatpakSupportStatus,
+ removePluginOwnedFlatpakExtensions,
+ type FlatpakExtensionStatus,
+ type SteamBranchStatus,
+} from "../api/lsfgApi";
import { InstallationButton } from "./InstallationButton";
import { StatusDisplay } from "./StatusDisplay";
@@ -13,6 +19,104 @@ interface SetupTabProps {
isUninstalling: boolean;
onInstall: () => void;
onUninstall: () => void;
+ flatpakRelevant: boolean;
+}
+
+function FlatpakSupportDiagnostics({ relevant }: { relevant: boolean }) {
+ const [status, setStatus] = useState<FlatpakExtensionStatus | null>(null);
+ const [advanced, setAdvanced] = useState(false);
+ const [busy, setBusy] = useState(false);
+
+ const refresh = async () => {
+ try {
+ setStatus(await getFlatpakSupportStatus());
+ } catch (error) {
+ setStatus({
+ success: false,
+ message: "",
+ error: String(error),
+ available: false,
+ extension_id: "",
+ supported_branches: [],
+ installed_branches: [],
+ owned_branches: [],
+ ownership_uncertain: false,
+ });
+ }
+ };
+
+ useEffect(() => {
+ if (relevant) void refresh();
+ }, [relevant]);
+
+ if (!relevant || !status?.available) return null;
+
+ const confirmCleanup = () => {
+ showModal(
+ <ConfirmModal
+ strTitle="Remove plugin-installed Flatpak extensions?"
+ strDescription="Shared runtime branches recorded as installed by this plugin will be removed. Existing unowned branches are preserved."
+ strOKButtonText="Remove extensions"
+ strCancelButtonText="Cancel"
+ onOK={async () => {
+ setBusy(true);
+ try {
+ await removePluginOwnedFlatpakExtensions();
+ await refresh();
+ } finally {
+ setBusy(false);
+ }
+ }}
+ onCancel={() => {}}
+ />,
+ );
+ };
+
+ return (
+ <PanelSection title="Flatpak support">
+ <PanelSectionRow>
+ <Field
+ label="Runtime extension support"
+ description={status.message || "Flatpak is available for classified targets."}
+ />
+ </PanelSectionRow>
+ <PanelSectionRow>
+ <ButtonItem layout="below" onClick={() => setAdvanced((value) => !value)}>
+ {advanced ? "Hide runtime details" : "Show runtime details"}
+ </ButtonItem>
+ </PanelSectionRow>
+ {advanced && (
+ <>
+ {status.supported_branches.map((branch) => (
+ <PanelSectionRow key={branch}>
+ <Field
+ label={branch}
+ description={
+ status.installed_branches.includes(branch)
+ ? "Installed" + (status.owned_branches.includes(branch) ? " · plugin-owned" : "")
+ : "Not installed"
+ }
+ />
+ </PanelSectionRow>
+ ))}
+ {status.ownership_uncertain && (
+ <PanelSectionRow>
+ <Field label="Ownership metadata is uncertain" description="Cleanup is disabled until the metadata is repaired." />
+ </PanelSectionRow>
+ )}
+ <PanelSectionRow>
+ <ButtonItem
+ layout="below"
+ disabled={busy || status.ownership_uncertain || status.owned_branches.length === 0}
+ onClick={confirmCleanup}
+ >
+ {busy ? "Removing..." : "Remove plugin-installed extensions"}
+ </ButtonItem>
+ </PanelSectionRow>
+ </>
+ )}
+ </PanelSection>
+ );
}
export function SetupTab({
@@ -25,22 +129,26 @@ export function SetupTab({
isUninstalling,
onInstall,
onUninstall,
+ flatpakRelevant,
}: SetupTabProps) {
return (
- <PanelSection title="Setup">
- <StatusDisplay
- installationStatus={installationStatus}
- losslessScalingInstalled={losslessScalingInstalled}
- losslessScalingStatus={losslessScalingStatus}
- steamBranchStatus={steamBranchStatus}
- />
- <InstallationButton
- isInstalled={isInstalled}
- isInstalling={isInstalling}
- isUninstalling={isUninstalling}
- onInstall={onInstall}
- onUninstall={onUninstall}
- />
- </PanelSection>
+ <>
+ <PanelSection title="Setup">
+ <StatusDisplay
+ installationStatus={installationStatus}
+ losslessScalingInstalled={losslessScalingInstalled}
+ losslessScalingStatus={losslessScalingStatus}
+ steamBranchStatus={steamBranchStatus}
+ />
+ <InstallationButton
+ isInstalled={isInstalled}
+ isInstalling={isInstalling}
+ isUninstalling={isUninstalling}
+ onInstall={onInstall}
+ onUninstall={onUninstall}
+ />
+ </PanelSection>
+ <FlatpakSupportDiagnostics relevant={flatpakRelevant} />
+ </>
);
}
diff --git a/src/components/WorkaroundsSection.tsx b/src/components/WorkaroundsSection.tsx
index dbdd6a3..5587392 100644
--- a/src/components/WorkaroundsSection.tsx
+++ b/src/components/WorkaroundsSection.tsx
@@ -1,6 +1,7 @@
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 "../hooks/usePerAppWorkarounds";
@@ -8,6 +9,7 @@ import type { WorkaroundField } from "../hooks/usePerAppWorkarounds";
interface WorkaroundsSectionProps {
appId: string;
nonSteam: boolean;
+ transport: TargetTransport;
onRepair?: () => Promise<boolean>;
}
@@ -79,9 +81,9 @@ function usePersistentCollapsed() {
return [collapsed, () => setCollapsed((value) => !value)] as const;
}
-export function WorkaroundsSection({ appId, nonSteam, onRepair }: WorkaroundsSectionProps) {
+export function WorkaroundsSection({ appId, nonSteam, transport, onRepair }: WorkaroundsSectionProps) {
const [collapsed, toggleCollapsed] = usePersistentCollapsed();
- const { status, snapshot, refresh, update, error } = usePerAppWorkarounds(appId, nonSteam);
+ 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;
diff --git a/src/components/index.ts b/src/components/index.ts
index 37a8edb..7c3ee0a 100644
--- a/src/components/index.ts
+++ b/src/components/index.ts
@@ -5,8 +5,6 @@ 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 { GameConfigurationSelector } from "./GameConfigurationSelector";
export { GameConfigurationControls } from "./GameConfigurationControls";
export { NowPlayingTab } from "./NowPlayingTab";
diff --git a/src/hooks/useGameConfiguration.ts b/src/hooks/useGameConfiguration.ts
index c66596a..b59d592 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 { ensureFlatpakSupport, getGameConfigs, getInstalledGames, getWorkaroundState, removeWorkaroundState, repairFlatpakSupport, resetGameConfig, resetAllGameConfigs, setWorkaroundState, updateGameConfig, type GameConfigEntry, type GlobalConfig, type InstalledGame, type WorkaroundState } from "../api/lsfgApi";
import { ConfigurationData, getDefaults } from "../config/configSchema";
import { cleanupLegacySteamLaunchOptions, getDefaultWrapperPath, hasWrapperLaunchIntegration, installWrapperIntegration, isLegacyWrapperToken, readSteamLaunchOptions, removeWrapperIntegration } from "../utils/steamLaunchOptions";
import { showErrorToast } from "../utils/toastUtils";
@@ -19,7 +19,12 @@ async function getSteamShortcuts(): Promise<InstalledGame[]> {
const appid = Number(shortcut?.appid);
const name = shortcut?.data?.strAppName;
if (!Number.isInteger(appid) || appid === 0 || typeof name !== "string" || !name) return [];
- return [{ appid: String(appid >>> 0), name, nonSteam: true }];
+ return [{
+ appid: String(appid >>> 0),
+ name,
+ nonSteam: true,
+ transport: { kind: "host" },
+ }];
});
} catch {
return [];
@@ -28,7 +33,10 @@ async function getSteamShortcuts(): Promise<InstalledGame[]> {
function mergeInstalledGames(backendGames: InstalledGame[], shortcutGames: InstalledGame[]) {
const games = new Map(backendGames.map((game) => [game.appid, game]));
- for (const game of shortcutGames) games.set(game.appid, game);
+ for (const game of shortcutGames) {
+ const existing = games.get(game.appid);
+ games.set(game.appid, existing ? { ...existing, name: game.name, nonSteam: true } : game);
+ }
return Array.from(games.values());
}
@@ -82,7 +90,7 @@ export function useGameConfiguration() {
const name = app.display_name || installed?.name;
if (!name) return setRunningGame(null);
setRunningGame((current) => current?.appid === appid ? current : {
- ...(installed || { appid, name, nonSteam: false }),
+ ...(installed || { appid, name, nonSteam: false, transport: { kind: "host" } }),
name,
configured: games.some((game) => game.appid === appid),
});
@@ -101,13 +109,26 @@ export function useGameConfiguration() {
const targets = useMemo<GameTarget[]>(() => {
const configured = installedGames.map((game) => ({ ...game, configured: games.some((item) => item.appid === game.appid) }));
- for (const game of games) if (!configured.some((item) => item.appid === game.appid)) configured.push({ appid: game.appid, name: game.profile, nonSteam: false, configured: true });
+ for (const game of games) if (!configured.some((item) => item.appid === game.appid)) configured.push({ appid: game.appid, name: game.profile, nonSteam: false, transport: { kind: "host" }, configured: true });
if (runningGame && !configured.some((game) => game.appid === runningGame.appid)) configured.unshift(runningGame);
return configured;
}, [games, installedGames, runningGame]);
const template = useMemo(() => ({ ...getDefaults(), ...globalConfig }), [globalConfig]);
const config = games.find((game) => game.appid === selectedAppId)?.config || template;
+ const ensureTargetFlatpakSupport = useCallback(async (target: GameTarget): Promise<boolean> => {
+ if (target.transport.kind !== "flatpak") return true;
+ const result = await ensureFlatpakSupport(target.transport.flatpakAppId);
+ if (!result.success || result.support_status !== "ready") {
+ showErrorToast(
+ "Flatpak support unavailable",
+ result.error || result.message || "The required Flatpak runtime extension is not ready",
+ );
+ return false;
+ }
+ return true;
+ }, []);
+
const ensureTargetWorkarounds = useCallback(async (target: GameTarget): Promise<boolean> => {
if (!installedGames.some((game) => game.appid === target.appid)) return true;
const appId = Number(target.appid);
@@ -119,6 +140,7 @@ export function useGameConfiguration() {
const oldState = existing.state;
const oldShortcutExe = existing.shortcut_exe || undefined;
const oldCommandTokenAdded = existing.command_token_added === true;
+ const oldTransport = existing.transport || target.transport;
if (target.nonSteam && oldState && current.target === wrapperPath && !oldShortcutExe) {
throw new Error("Managed shortcut Target has no saved original executable");
}
@@ -138,6 +160,7 @@ export function useGameConfiguration() {
state,
originalExecutable || null,
oldCommandTokenAdded,
+ target.transport,
);
if (!initialStateResult.success) throw new Error(initialStateResult.error || "Could not create workaround state");
@@ -149,6 +172,7 @@ export function useGameConfiguration() {
state,
target.nonSteam ? (integration.originalExecutable || originalExecutable || null) : null,
integration.commandTokenAdded,
+ target.transport,
);
if (!finalStateResult.success) throw new Error(finalStateResult.error || "Could not finalize workaround state");
return true;
@@ -170,7 +194,13 @@ export function useGameConfiguration() {
}
if (rollbackSucceeded) {
const restored = oldState
- ? await setWorkaroundState(target.appid, oldState, oldShortcutExe || null, oldCommandTokenAdded)
+ ? await setWorkaroundState(
+ target.appid,
+ oldState,
+ oldShortcutExe || null,
+ oldCommandTokenAdded,
+ oldTransport,
+ )
: await removeWorkaroundState(target.appid);
if (!restored.success) throw new Error(restored.error || "Could not roll back workaround state");
}
@@ -227,30 +257,30 @@ export function useGameConfiguration() {
const enable = useCallback(async (appid: string) => {
const target = targets.find((item) => item.appid === appid);
if (!target?.name) return false;
+ if (!(await ensureTargetFlatpakSupport(target))) return false;
if (!(await ensureTargetWorkarounds(target))) return false;
const result = await updateGameConfig(appid, target.name, template);
if (result.success) await load();
else await removeTargetWorkarounds(target);
return result.success;
- }, [ensureTargetWorkarounds, load, removeTargetWorkarounds, 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 ensureTargetWorkarounds(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");
- await removeTargetWorkarounds(target);
- return;
- }
- }
- await load();
- }, [ensureTargetWorkarounds, load, removeTargetWorkarounds, targets, template]);
+ }, [ensureTargetFlatpakSupport, ensureTargetWorkarounds, load, removeTargetWorkarounds, targets, template]);
const repair = useCallback(async (appid: string): Promise<boolean> => {
const target = targets.find((item) => item.appid === appid);
- return target ? ensureTargetWorkarounds(target) : false;
- }, [ensureTargetWorkarounds, targets]);
+ if (!target) return false;
+ if (target.transport.kind === "flatpak") {
+ const support = await repairFlatpakSupport(target.transport.flatpakAppId);
+ if (!support.success || support.support_status !== "ready") {
+ showErrorToast(
+ "Flatpak support unavailable",
+ support.error || support.message || "The required Flatpak runtime extension is not ready",
+ );
+ return false;
+ }
+ }
+ const success = await ensureTargetWorkarounds(target);
+ if (success) await load();
+ return success;
+ }, [ensureTargetWorkarounds, load, targets]);
const resetSelected = useCallback(async () => {
if (selectedAppId) {
@@ -276,5 +306,5 @@ export function useGameConfiguration() {
}
}, [load, removeTargetWorkarounds, targets]);
- return { config, games, targets, runningGame, selectedAppId, setSelectedAppId, save, enable, enableAll, repair, resetSelected, resetAll, reload: load };
+ return { config, games, targets, runningGame, selectedAppId, setSelectedAppId, save, enable, repair, resetSelected, resetAll, reload: load };
}
diff --git a/src/hooks/usePerAppWorkarounds.ts b/src/hooks/usePerAppWorkarounds.ts
index 9e283db..c7413b0 100644
--- a/src/hooks/usePerAppWorkarounds.ts
+++ b/src/hooks/usePerAppWorkarounds.ts
@@ -3,6 +3,7 @@ import {
getWorkaroundState,
removeWorkaroundState,
setWorkaroundState,
+ type TargetTransport,
type WorkaroundState,
} from "../api/lsfgApi";
import {
@@ -45,6 +46,7 @@ export interface WorkaroundSnapshot {
integrationInstalled: boolean;
commandTokenAdded: boolean;
shortcutExe?: string | null;
+ transport: TargetTransport;
}
interface PerAppWorkarounds {
@@ -85,12 +87,14 @@ function makeSnapshot(
integrationInstalled: integrationIsInstalled(steam, nonSteam, wrapperPath),
commandTokenAdded: result.command_token_added === true,
shortcutExe: result.shortcut_exe,
+ transport: result.transport || { kind: "host" },
};
}
async function adoptWorkaroundState(
appId: string,
nonSteam: boolean,
+ transport: TargetTransport,
steam: SteamLaunchOptionsSnapshot,
wrapperPath: string,
): Promise<WorkaroundSnapshot> {
@@ -98,7 +102,13 @@ async function adoptWorkaroundState(
throw new Error("Shortcut Target is a wrapper but its original Target is unknown");
}
const originalExecutable = nonSteam ? steam.target : null;
- const initial = await setWorkaroundState(appId, DEFAULT_WORKAROUND_STATE, originalExecutable, false);
+ const initial = await setWorkaroundState(
+ appId,
+ DEFAULT_WORKAROUND_STATE,
+ originalExecutable,
+ false,
+ transport,
+ );
if (!initial.success) throw new Error(initial.error || "Could not create workaround state");
let integration: Awaited<ReturnType<typeof installWrapperIntegration>> | null = null;
try {
@@ -112,6 +122,7 @@ async function adoptWorkaroundState(
DEFAULT_WORKAROUND_STATE,
nonSteam ? (integration.originalExecutable || originalExecutable) : null,
integration.commandTokenAdded,
+ transport,
);
if (!finalized.success) throw new Error(finalized.error || "Could not finalize workaround state");
return makeSnapshot(integration.snapshot, finalized, nonSteam);
@@ -139,7 +150,11 @@ async function adoptWorkaroundState(
}
}
-export function usePerAppWorkarounds(appId: string, nonSteam: boolean): PerAppWorkarounds {
+export function usePerAppWorkarounds(
+ appId: string,
+ nonSteam: boolean,
+ transport: TargetTransport = { kind: "host" },
+): PerAppWorkarounds {
const [status, setStatus] = useState<WorkaroundLoadStatus>("loading");
const [snapshot, setSnapshot] = useState<WorkaroundSnapshot | null>(null);
const [error, setError] = useState<string | null>(null);
@@ -156,12 +171,13 @@ export function usePerAppWorkarounds(appId: string, nonSteam: boolean): PerAppWo
return adoptWorkaroundState(
appId,
nonSteam,
+ transport,
steam,
result.wrapper_path || getDefaultWrapperPath(),
);
}
return makeSnapshot(steam, result, nonSteam);
- }, [appId, nonSteam, numericAppId]);
+ }, [appId, nonSteam, numericAppId, transport]);
const applySnapshot = useCallback((next: WorkaroundSnapshot) => {
setSnapshot(next);
@@ -236,6 +252,7 @@ export function usePerAppWorkarounds(appId: string, nonSteam: boolean): PerAppWo
nextState,
current.shortcutExe ?? null,
current.commandTokenAdded,
+ current.transport,
);
if (!result.success || !result.state) throw new Error(result.error || "Could not save workaround state");
applySnapshot({
@@ -245,6 +262,7 @@ export function usePerAppWorkarounds(appId: string, nonSteam: boolean): PerAppWo
wrapperOwned: result.wrapper_owned === true,
shortcutExe: result.shortcut_exe,
commandTokenAdded: result.command_token_added === true,
+ transport: result.transport || current.transport,
});
return true;
} catch (updateError) {
diff --git a/src/types.d.ts b/src/types.d.ts
index 7b5d055..df433e0 100644
--- a/src/types.d.ts
+++ b/src/types.d.ts
@@ -17,6 +17,7 @@ interface SteamAppDetails {
strLaunchOptions?: string;
strShortcutLaunchOptions?: string;
strShortcutExe?: string;
+ strShortcutStartDir?: string;
}
interface SteamAppDetailsRegistration {