diff options
Diffstat (limited to 'src/components')
| -rw-r--r-- | src/components/ConfigurationTab.tsx | 22 | ||||
| -rw-r--r-- | src/components/Content.tsx | 41 | ||||
| -rw-r--r-- | src/components/FlatpaksTab.tsx | 196 | ||||
| -rw-r--r-- | src/components/GameConfigurationControls.tsx | 3 | ||||
| -rw-r--r-- | src/components/GameConfigurationSelector.tsx | 101 | ||||
| -rw-r--r-- | src/components/NowPlayingTab.tsx | 93 | ||||
| -rw-r--r-- | src/components/SetupTab.tsx | 142 | ||||
| -rw-r--r-- | src/components/WorkaroundsSection.tsx | 6 | ||||
| -rw-r--r-- | src/components/index.ts | 2 |
9 files changed, 274 insertions, 332 deletions
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"; |
