From cc1e6f47dd9838b066822162a607d2859c043aff Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Wed, 9 Sep 2026 19:16:30 -0400 Subject: refactor: unify flatpak targets with steam profiles --- src/components/ConfigurationTab.tsx | 22 ++- src/components/Content.tsx | 41 +++--- src/components/FlatpaksTab.tsx | 196 --------------------------- src/components/GameConfigurationControls.tsx | 3 +- src/components/GameConfigurationSelector.tsx | 101 +++----------- src/components/NowPlayingTab.tsx | 93 +++++++++++-- src/components/SetupTab.tsx | 142 ++++++++++++++++--- src/components/WorkaroundsSection.tsx | 6 +- src/components/index.ts | 2 - 9 files changed, 274 insertions(+), 332 deletions(-) delete mode 100644 src/components/FlatpaksTab.tsx (limited to 'src/components') 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; onEnable: (appid: string) => Promise; - onEnableAll: () => Promise; onRepair: (appid: string) => Promise; onReset: () => Promise; onResetAll: () => Promise; @@ -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({ )} + {selectedTarget?.configured && selectedTarget.transport.kind === "flatpak" && selectedTarget.flatpakSupport?.support_status !== "ready" && ( + + + void onRepair(selectedTarget.appid)} + > + Repair Flatpak support + + + + )} {selectedTarget?.configured && ( , - configuration: , - flatpak: , - configFile: , + games: , setup: , }; @@ -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(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: ( handleConfigChange(fieldName, value, true)} onEnable={enable} - onEnableAll={enableAll} onRepair={repair} onReset={resetSelected} onResetAll={resetAll} /> ), }, - { id: "Flatpak", title: tabIcons.flatpak, content: }, - { id: "ConfigFile", title: tabIcons.configFile, content: }, // 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 ( - - onAction()} - disabled={busy} - /> - - ); -} - -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 ( - - - - ); -} - -export function FlatpaksTab() { - const [extensionStatus, setExtensionStatus] = useState(null); - const [apps, setApps] = useState(null); - const [loading, setLoading] = useState(true); - const [operation, setOperation] = useState(null); - const [error, setError] = useState(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( - 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 ; - } - - return ( - <> - - {error && } - {extensionStatus?.success ? runtimeVersions.map(({ version, key }) => ( - confirmExtensionOperation(version, extensionStatus[key])} - /> - )) : } - - - - {apps?.success ? apps.apps.length ? apps.apps.map((app) => ( - void toggleApp(app)} - /> - )) : : } - - - ); -} 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; + workaroundTarget?: Pick; onRepairWorkaround?: () => Promise; } @@ -36,6 +36,7 @@ export function GameConfigurationControls({ )} 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; onResetAll: () => Promise; 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 ( <> - +
- {collapsed ? ( - - ) : ( - - )} + {collapsed ? : }
@@ -81,7 +81,7 @@ function GameGroup({ 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(null); + const enabledToggleRef = useRef(null); useEffect(() => { if (!focusConfiguredToggle) return; const frame = requestAnimationFrame(() => { - configuredToggleRef.current?.querySelector('[role="button"], button')?.focus(); + enabledToggleRef.current?.querySelector('[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( - void onEnableAll()} - onCancel={() => {}} - />, - ); - }; return ( <> - {targets.length === 0 && ( )} - - - {availableGames.length > 0 && ( - - - Enable all available games - - - )} Promise; + onConfigChange: ( + fieldName: keyof ConfigurationData, + value: boolean | number | string | string[], + ) => Promise; + onEnable: (appid: string) => Promise; + onRepair: (appid: string) => Promise; } -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 ( - + - + - + {!game.configured && ( + + + + + + void handleEnable()}> + {busy ? "Enabling..." : "Enable LSFG-VK"} + + + + )} + {game.configured && supportNeedsRepair && ( + + + + + + void handleRepair()}> + {busy ? "Repairing..." : "Repair Flatpak support"} + + + + )} + {game.configured && ( + + )} ); } 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(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( + { + setBusy(true); + try { + await removePluginOwnedFlatpakExtensions(); + await refresh(); + } finally { + setBusy(false); + } + }} + onCancel={() => {}} + />, + ); + }; + + return ( + + + + + + setAdvanced((value) => !value)}> + {advanced ? "Hide runtime details" : "Show runtime details"} + + + {advanced && ( + <> + {status.supported_branches.map((branch) => ( + + + + ))} + {status.ownership_uncertain && ( + + + + )} + + + {busy ? "Removing..." : "Remove plugin-installed extensions"} + + + + )} + + ); } export function SetupTab({ @@ -25,22 +129,26 @@ export function SetupTab({ isUninstalling, onInstall, onUninstall, + flatpakRelevant, }: SetupTabProps) { return ( - - - - + <> + + + + + + ); } 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; } @@ -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"; -- cgit v1.2.3