diff options
| author | xXJSONDeruloXx <danielhimebauch@gmail.com> | 2026-09-11 09:05:14 -0400 |
|---|---|---|
| committer | xXJSONDeruloXx <danielhimebauch@gmail.com> | 2026-09-11 09:05:14 -0400 |
| commit | d2bfdafa92f3d31cc5392bf8a5a1f1df5c2358ce (patch) | |
| tree | 102c12dc843ce64dd21599c2cf8ae3d5a3bd0104 /src | |
| parent | 904e2e6131071c3b132d3148947b613c2830b1bb (diff) | |
| download | decky-lsfg-vk-d2bfdafa92f3d31cc5392bf8a5a1f1df5c2358ce.tar.gz decky-lsfg-vk-d2bfdafa92f3d31cc5392bf8a5a1f1df5c2358ce.zip | |
flatpak correctness, ui alignment, tests
Diffstat (limited to 'src')
| -rw-r--r-- | src/api/lsfgApi.ts | 1 | ||||
| -rw-r--r-- | src/components/CollapsibleItemGroup.tsx | 79 | ||||
| -rw-r--r-- | src/components/Content.tsx | 56 | ||||
| -rw-r--r-- | src/components/FlatpakNowPlayingTab.tsx | 26 | ||||
| -rw-r--r-- | src/components/FlatpakTab.tsx | 79 | ||||
| -rw-r--r-- | src/components/GameConfigurationSelector.tsx | 85 | ||||
| -rw-r--r-- | src/hooks/useFlatpakConfiguration.ts | 59 | ||||
| -rw-r--r-- | src/utils/nowPlaying.ts | 54 |
8 files changed, 304 insertions, 135 deletions
diff --git a/src/api/lsfgApi.ts b/src/api/lsfgApi.ts index 3d4890e..f3b7fa1 100644 --- a/src/api/lsfgApi.ts +++ b/src/api/lsfgApi.ts @@ -118,6 +118,7 @@ export interface RunningFlatpakApp { app_id: string; active: boolean; pid?: string; + start_time?: number | null; } export interface FlatpakAppsResult extends ApiResult { diff --git a/src/components/CollapsibleItemGroup.tsx b/src/components/CollapsibleItemGroup.tsx new file mode 100644 index 0000000..a66a8ba --- /dev/null +++ b/src/components/CollapsibleItemGroup.tsx @@ -0,0 +1,79 @@ +import { ButtonItem, Field, PanelSectionRow } from "@decky/ui"; +import { type RefObject } from "react"; +import { RiArrowDownSFill, RiArrowUpSFill } from "react-icons/ri"; + +export interface CollapsibleItem { + id: string; + label: string; + description: string; +} + +export const collapsibleItemGroupStyles = ` + .LSFG_GameGroupCollapseButton_Container > div > div > div > button, + .LSFG_GameGroupCollapseButton_Container > div > div > div > div > button { + height: 24px !important; + min-height: 24px !important; + padding: 0 !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + } + + .LSFG_GameGroupCollapseButton_Container svg { + display: block; + margin: 0; + } +`; + +interface Props { + title: string; + items: CollapsibleItem[]; + collapsed: boolean; + onToggle: () => void; + onSelect: (id: string) => void; + toggleRef?: RefObject<HTMLDivElement>; +} + +export function CollapsibleItemGroup({ + title, + items, + collapsed, + onToggle, + onSelect, + toggleRef, +}: Props) { + if (items.length === 0) return null; + + return ( + <> + <PanelSectionRow> + <Field label={`${title} (${items.length})`} bottomSeparator="none" /> + </PanelSectionRow> + <PanelSectionRow> + <div + ref={toggleRef} + className="LSFG_GameGroupCollapseButton_Container" + style={{ marginTop: "-2px", marginBottom: "4px" }} + > + <ButtonItem + layout="below" + bottomSeparator={collapsed ? "standard" : "none"} + onClick={onToggle} + > + {collapsed ? <RiArrowDownSFill /> : <RiArrowUpSFill />} + </ButtonItem> + </div> + </PanelSectionRow> + {!collapsed && items.map((item) => ( + <PanelSectionRow key={item.id}> + <Field + label={item.label} + description={item.description} + onActivate={() => onSelect(item.id)} + highlightOnFocus + /> + </PanelSectionRow> + ))} + </> + ); +} diff --git a/src/components/Content.tsx b/src/components/Content.tsx index ce3c018..0d49f89 100644 --- a/src/components/Content.tsx +++ b/src/components/Content.tsx @@ -1,4 +1,4 @@ -import { Tabs } from "@decky/ui"; +import { Field, PanelSection, PanelSectionRow, Tabs } from "@decky/ui"; import { useEffect, useRef, useState } from "react"; import { FaCube, FaFileAlt, FaGamepad, FaList, FaTools } from "react-icons/fa"; import { ConfigurationData } from "../config/configSchema"; @@ -6,6 +6,7 @@ import { useFlatpakConfiguration } from "../hooks/useFlatpakConfiguration"; import { useGameConfiguration } from "../hooks/useGameConfiguration"; import { useInstallation } from "../hooks/useLsfgHooks"; import { tabStyles } from "../styles"; +import { resolveNowPlayingTarget } from "../utils/nowPlaying"; import { ConfigFileTab } from "./ConfigFileTab"; import { ConfigurationTab } from "./ConfigurationTab"; import { FlatpakNowPlayingTab } from "./FlatpakNowPlayingTab"; @@ -80,10 +81,13 @@ export function Content() { const [showDebugTab, setShowDebugTab] = usePersistentBoolean(DEBUG_TAB_VISIBILITY_KEY, true); const previousRunningWorkload = useRef<string | null>(null); const runningFlatpak = flatpak.runningApp; - const hasNowPlaying = Boolean(runningGame?.configured || runningFlatpak); - const runningWorkload = runningGame?.configured - ? `steam:${runningGame.appid}` - : runningFlatpak ? `flatpak:${runningFlatpak.app_id}` : null; + const nowPlayingTarget = resolveNowPlayingTarget(runningGame, runningFlatpak); + const hasNowPlaying = Boolean(nowPlayingTarget); + const runningWorkload = nowPlayingTarget + ? nowPlayingTarget.kind === "flatpak" + ? `flatpak:${nowPlayingTarget.app.app_id}` + : `steam:${nowPlayingTarget.game.appid}` + : null; useEffect(() => { if (!setupComplete) { @@ -136,26 +140,27 @@ export function Content() { /> ); - const nowPlaying = runningGame?.configured ? ( + const nowPlaying = nowPlayingTarget?.kind === "steam" ? ( <NowPlayingTab - game={runningGame} + game={nowPlayingTarget.game} config={runningConfig} onConfigChange={async (field, value) => { - await saveFor(runningGame.appid, { ...runningConfig, [field]: value }, true); + await saveFor(nowPlayingTarget.game.appid, { ...runningConfig, [field]: value }, true); }} /> - ) : runningFlatpak ? ( + ) : nowPlayingTarget?.kind === "flatpak" ? ( <FlatpakNowPlayingTab - app={runningFlatpak} - busy={flatpak.busyAppId === runningFlatpak.app_id} + app={nowPlayingTarget.app} + launcher={nowPlayingTarget.launcher} onConfigChange={flatpak.updateConfig} - onWorkaroundChange={flatpak.updateWorkarounds} /> - ) : null; + ) : ( + <NowPlayingTabPlaceholder /> + ); const tabs = setupComplete ? [ - ...(nowPlaying ? [{ id: "NowPlaying", title: tabIcons.nowPlaying, content: nowPlaying }] : []), + { id: "NowPlaying", title: tabIcons.nowPlaying, content: nowPlaying }, { id: "Games", title: tabIcons.games, @@ -198,13 +203,34 @@ export function Content() { ] : [{ id: "Setup", title: tabIcons.setup, content: setup }]; + const availableTabIds = new Set(tabs.map(({ id }) => id)); + const activeTab = availableTabIds.has(tab) ? tab : setupComplete ? "Games" : "Setup"; + return ( <div className="lsfg-vk-tabs" style={{ height: "95%", width: "300px", position: "fixed", marginTop: "-12px", overflow: "hidden" }} > <style>{tabStyles}</style> - <Tabs activeTab={!showDebugTab && tab === "ConfigFile" ? "Games" : tab} onShowTab={setTab} tabs={tabs} /> + <Tabs + activeTab={activeTab} + onShowTab={(nextTab: string) => { + if (availableTabIds.has(nextTab)) setTab(nextTab); + }} + tabs={tabs} + /> + </div> + ); +} + +function NowPlayingTabPlaceholder() { + return ( + <div> + <PanelSection title="Now Playing"> + <PanelSectionRow> + <Field label="Nothing running" description="Start an enabled Steam, non-Steam, or Flatpak target to configure it here." /> + </PanelSectionRow> + </PanelSection> </div> ); } diff --git a/src/components/FlatpakNowPlayingTab.tsx b/src/components/FlatpakNowPlayingTab.tsx index 9e5a0db..9623b11 100644 --- a/src/components/FlatpakNowPlayingTab.tsx +++ b/src/components/FlatpakNowPlayingTab.tsx @@ -1,17 +1,16 @@ import { Field, Focusable, PanelSection, PanelSectionRow } from "@decky/ui"; -import type { FlatpakApp, LsfgConfig, WorkaroundState } from "../api/lsfgApi"; +import type { FlatpakApp, LsfgConfig } from "../api/lsfgApi"; +import type { GameTarget } from "../hooks/useGameConfiguration"; import { ConfigurationSection } from "./ConfigurationSection"; -import { FlatpakWorkaroundsSection } from "./FlatpakWorkaroundsSection"; import { FpsMultiplierControl } from "./FpsMultiplierControl"; interface Props { app: FlatpakApp; - busy: boolean; + launcher: GameTarget | null; onConfigChange: (appId: string, config: LsfgConfig) => Promise<boolean>; - onWorkaroundChange: (appId: string, state: WorkaroundState) => Promise<boolean>; } -export function FlatpakNowPlayingTab({ app, busy, onConfigChange, onWorkaroundChange }: Props) { +export function FlatpakNowPlayingTab({ app, launcher, onConfigChange }: Props) { if (!app.config) return null; const changeConfig = async ( field: keyof LsfgConfig, @@ -24,16 +23,21 @@ export function FlatpakNowPlayingTab({ app, busy, onConfigChange, onWorkaroundCh <Focusable> <PanelSection title="Now Playing"> <PanelSectionRow> - <Field label={app.app_name} description={`Flatpak · ${app.app_id}`} /> + <Field + label={launcher?.name || app.app_name} + description={launcher + ? `${launcher.nonSteam ? "Steam shortcut" : "Steam"} · Running in ${app.app_name} · Flatpak` + : `Flatpak · ${app.app_id}`} + /> </PanelSectionRow> + {launcher && ( + <PanelSectionRow> + <Field label="Controls" description={`${app.app_name} profile · ${app.app_id}`} /> + </PanelSectionRow> + )} </PanelSection> <FpsMultiplierControl config={app.config} onConfigChange={changeConfig} /> <ConfigurationSection config={app.config} onConfigChange={changeConfig} /> - <FlatpakWorkaroundsSection - state={app.workarounds} - disabled={busy} - onChange={(state) => onWorkaroundChange(app.app_id, state)} - /> </Focusable> ); } diff --git a/src/components/FlatpakTab.tsx b/src/components/FlatpakTab.tsx index c5e27f5..31bfc2d 100644 --- a/src/components/FlatpakTab.tsx +++ b/src/components/FlatpakTab.tsx @@ -1,7 +1,8 @@ import { ButtonItem, DialogButton, Field, Focusable, PanelSection, PanelSectionRow, gamepadDialogClasses } from "@decky/ui"; -import { useCallback, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { FaArrowLeft } from "react-icons/fa"; import type { FlatpakApp, LsfgConfig, WorkaroundState } from "../api/lsfgApi"; +import { CollapsibleItemGroup, collapsibleItemGroupStyles } from "./CollapsibleItemGroup"; import { ConfigurationSection } from "./ConfigurationSection"; import { FlatpakWorkaroundsSection } from "./FlatpakWorkaroundsSection"; import { FpsMultiplierControl } from "./FpsMultiplierControl"; @@ -19,6 +20,24 @@ interface Props { onWorkaroundChange: (appId: string, state: WorkaroundState) => Promise<boolean>; } +function usePersistentCollapsed(key: string) { + const [collapsed, setCollapsed] = useState(() => { + try { + return localStorage.getItem(key) !== "false"; + } catch { + return true; + } + }); + + useEffect(() => { + try { + localStorage.setItem(key, String(collapsed)); + } catch {} + }, [collapsed, key]); + + return [collapsed, () => setCollapsed((value) => !value)] as const; +} + export function FlatpakTab({ apps, runningApp, @@ -36,31 +55,47 @@ export function FlatpakTab({ [apps, selectedAppId], ); const close = useCallback(() => setSelectedAppId(null), []); + const [enabledCollapsed, toggleEnabled] = usePersistentCollapsed("lsfg-flatpak-enabled-collapsed-v1"); + const [availableCollapsed, toggleAvailable] = usePersistentCollapsed("lsfg-flatpak-available-collapsed-v1"); + const enabledToggleRef = useRef<HTMLDivElement>(null); + + const enabledApps = useMemo( + () => apps.filter((app) => app.enabled).sort((a, b) => a.app_name.localeCompare(b.app_name)), + [apps], + ); + const availableApps = useMemo( + () => apps.filter((app) => !app.enabled).sort((a, b) => a.app_name.localeCompare(b.app_name)), + [apps], + ); + const itemFor = (app: FlatpakApp) => ({ + id: app.app_id, + label: app.app_name, + description: `${app.app_id} · ${app.prepared && !app.owned ? "Prepared externally" : "Available"}`, + }); if (!selectedAppId) { return ( <PanelSection title="Flatpak"> - {/* <PanelSectionRow> - <Field - label="Flatpak applications" - description="Enable LSFG-VK directly for a Flatpak. Steam shortcuts and launcher scripts are not modified." - /> - </PanelSectionRow> */} - {apps.map((app) => { - const status = app.enabled - ? app.app_id === runningApp?.app_id ? "Enabled · Running" : "Enabled" - : app.prepared && !app.owned ? "Prepared externally" : "Available"; - return ( - <PanelSectionRow key={app.app_id}> - <Field - label={app.app_name} - description={`${app.app_id} · ${status}`} - onActivate={() => setSelectedAppId(app.app_id)} - highlightOnFocus - /> - </PanelSectionRow> - ); - })} + <style>{collapsibleItemGroupStyles}</style> + <CollapsibleItemGroup + title="Enabled" + items={enabledApps.map((app) => ({ + id: app.app_id, + label: app.app_name, + description: `${app.app_id}${app.app_id === runningApp?.app_id ? " · Running" : ""}`, + }))} + collapsed={enabledCollapsed} + onToggle={toggleEnabled} + onSelect={setSelectedAppId} + toggleRef={enabledToggleRef} + /> + <CollapsibleItemGroup + title="Available" + items={availableApps.map(itemFor)} + collapsed={availableCollapsed} + onToggle={toggleAvailable} + onSelect={setSelectedAppId} + /> {apps.length === 0 && !loading && ( <PanelSectionRow> <Field label="No Flatpak applications found" /> diff --git a/src/components/GameConfigurationSelector.tsx b/src/components/GameConfigurationSelector.tsx index 1f0bc73..ee87423 100644 --- a/src/components/GameConfigurationSelector.tsx +++ b/src/components/GameConfigurationSelector.tsx @@ -1,7 +1,7 @@ import { ButtonItem, ConfirmModal, Field, PanelSectionRow, showModal } from "@decky/ui"; -import { useEffect, useRef, useState, type RefObject } from "react"; -import { RiArrowDownSFill, RiArrowUpSFill } from "react-icons/ri"; +import { useEffect, useRef, useState } from "react"; import { GameTarget } from "../hooks/useGameConfiguration"; +import { CollapsibleItemGroup, collapsibleItemGroupStyles } from "./CollapsibleItemGroup"; interface Props { targets: GameTarget[]; @@ -38,57 +38,6 @@ function targetDescription(game: GameTarget): string { return game.nonSteam ? "Non-Steam" : "Steam"; } -function GameGroup({ - title, - games, - collapsed, - onToggle, - onSelect, - toggleRef, -}: { - title: string; - games: GameTarget[]; - collapsed: boolean; - onToggle: () => void; - onSelect: (appid: string) => void; - toggleRef?: RefObject<HTMLDivElement>; -}) { - if (games.length === 0) return null; - - return ( - <> - <PanelSectionRow> - <Field label={title + " (" + games.length + ")"} bottomSeparator="none" /> - </PanelSectionRow> - <PanelSectionRow> - <div - ref={toggleRef} - className="LSFG_GameGroupCollapseButton_Container" - style={{ marginTop: "-2px", marginBottom: "4px" }} - > - <ButtonItem - layout="below" - bottomSeparator={collapsed ? "standard" : "none"} - onClick={onToggle} - > - {collapsed ? <RiArrowDownSFill /> : <RiArrowUpSFill />} - </ButtonItem> - </div> - </PanelSectionRow> - {!collapsed && games.map((game) => ( - <PanelSectionRow key={game.appid}> - <Field - label={game.name} - description={targetDescription(game)} - onActivate={() => onSelect(game.appid)} - highlightOnFocus - /> - </PanelSectionRow> - ))} - </> - ); -} - export function GameConfigurationSelector({ targets, runningGame, @@ -105,6 +54,11 @@ export function GameConfigurationSelector({ }); const enabledGames = sortGames(targets.filter((game) => game.configured)); const availableGames = sortGames(targets.filter((game) => !game.configured)); + const toItem = (game: GameTarget) => ({ + id: game.appid, + label: game.name, + description: targetDescription(game), + }); const [enabledCollapsed, toggleEnabled] = usePersistentCollapsed(ENABLED_COLLAPSED_KEY); const [availableCollapsed, toggleAvailable] = usePersistentCollapsed(AVAILABLE_COLLAPSED_KEY); const enabledToggleRef = useRef<HTMLDivElement>(null); @@ -146,39 +100,24 @@ export function GameConfigurationSelector({ return ( <> <style> - {` - .LSFG_GameGroupCollapseButton_Container > div > div > div > button, - .LSFG_GameGroupCollapseButton_Container > div > div > div > div > button { - height: 24px !important; - min-height: 24px !important; - padding: 0 !important; - display: flex !important; - align-items: center !important; - justify-content: center !important; - } - - .LSFG_GameGroupCollapseButton_Container svg { - display: block; - margin: 0; - } - `} + {collapsibleItemGroupStyles} </style> {targets.length === 0 && ( <PanelSectionRow> <Field label="No installed games" description="Steam has not reported any eligible games" /> </PanelSectionRow> )} - <GameGroup + <CollapsibleItemGroup title="Enabled" - games={enabledGames} + items={enabledGames.map(toItem)} collapsed={enabledCollapsed} onToggle={toggleEnabled} onSelect={onSelect} toggleRef={enabledToggleRef} /> - <GameGroup + <CollapsibleItemGroup title="Available" - games={availableGames} + items={availableGames.map(toItem)} collapsed={availableCollapsed} onToggle={toggleAvailable} onSelect={onSelect} diff --git a/src/hooks/useFlatpakConfiguration.ts b/src/hooks/useFlatpakConfiguration.ts index d5e6f7e..ce881f0 100644 --- a/src/hooks/useFlatpakConfiguration.ts +++ b/src/hooks/useFlatpakConfiguration.ts @@ -11,8 +11,16 @@ import { type RunningFlatpakApp, type WorkaroundState, } from "../api/lsfgApi"; +import { selectMostRecentRunningFlatpak } from "../utils/nowPlaying"; import { showErrorToast } from "../utils/toastUtils"; +type FlatpakOperationResult = { + success: boolean; + error?: string | null; + config?: LsfgConfig | null; + state?: WorkaroundState | null; +}; + export function useFlatpakConfiguration(enabled: boolean) { const [apps, setApps] = useState<FlatpakApp[]>([]); const [runningApps, setRunningApps] = useState<RunningFlatpakApp[]>([]); @@ -58,39 +66,62 @@ export function useFlatpakConfiguration(enabled: boolean) { return () => window.clearInterval(interval); }, [enabled, pollRunning]); - const operate = useCallback(async (appId: string, operation: () => Promise<{ success: boolean; error?: string | null }>) => { - if (busyAppId) return false; + const operate = useCallback(async ( + appId: string, + operation: () => Promise<FlatpakOperationResult>, + refresh = true, + ): Promise<FlatpakOperationResult> => { + if (busyAppId) return { success: false }; setBusyAppId(appId); try { const result = await operation(); if (!result.success) throw new Error(result.error || "Flatpak operation failed"); - await reload(); - await pollRunning(); - return true; + if (refresh) { + await reload(); + await pollRunning(); + } + return result; } catch (error) { showErrorToast("Flatpak operation failed", error instanceof Error ? error.message : String(error)); - return false; + return { success: false, error: error instanceof Error ? error.message : String(error) }; } finally { setBusyAppId(""); } }, [busyAppId, pollRunning, reload]); - const enableApp = useCallback((appId: string) => operate(appId, () => enableFlatpakApp(appId)), [operate]); - const removeApp = useCallback((appId: string) => operate(appId, () => removeFlatpakApp(appId)), [operate]); + const enableApp = useCallback(async (appId: string) => ( + await operate(appId, () => enableFlatpakApp(appId)) + ).success, [operate]); + const removeApp = useCallback(async (appId: string) => ( + await operate(appId, () => removeFlatpakApp(appId)) + ).success, [operate]); const updateConfig = useCallback( - (appId: string, config: LsfgConfig) => operate(appId, () => updateFlatpakConfig(appId, config)), + async (appId: string, config: LsfgConfig) => { + const result = await operate(appId, () => updateFlatpakConfig(appId, config), false); + if (result.success) { + setApps((current) => current.map((app) => ( + app.app_id === appId ? { ...app, config: result.config || config } : app + ))); + } + return result.success; + }, [operate], ); const updateWorkarounds = useCallback( - (appId: string, state: WorkaroundState) => operate(appId, () => setFlatpakWorkaroundState(appId, state)), + async (appId: string, state: WorkaroundState) => { + const result = await operate(appId, () => setFlatpakWorkaroundState(appId, state), false); + if (result.success) { + setApps((current) => current.map((app) => ( + app.app_id === appId ? { ...app, workarounds: result.state || state } : app + ))); + } + return result.success; + }, [operate], ); const runningApp = useMemo(() => { - if (runningApps.length === 0) return null; - const running = runningApps.find((app) => app.active) || (runningApps.length === 1 ? runningApps[0] : null); - if (!running) return null; - return apps.find((app) => app.app_id === running.app_id) || null; + return selectMostRecentRunningFlatpak(apps, runningApps); }, [apps, runningApps]); return { diff --git a/src/utils/nowPlaying.ts b/src/utils/nowPlaying.ts new file mode 100644 index 0000000..a207321 --- /dev/null +++ b/src/utils/nowPlaying.ts @@ -0,0 +1,54 @@ +import type { FlatpakApp, RunningFlatpakApp } from "../api/lsfgApi"; +import type { GameTarget } from "../hooks/useGameConfiguration"; + +export type NowPlayingTarget = + | { + kind: "flatpak"; + app: FlatpakApp; + launcher: GameTarget | null; + } + | { + kind: "steam"; + game: GameTarget; + }; + +function numericValue(value: number | null | undefined): number { + return typeof value === "number" && Number.isFinite(value) ? value : -1; +} + +function numericPid(value: string | undefined): number { + return value && /^\d+$/.test(value) ? Number(value) : -1; +} + +export function selectMostRecentRunningFlatpak( + apps: FlatpakApp[], + runningApps: RunningFlatpakApp[], +): FlatpakApp | null { + const candidates = runningApps + .map((running) => ({ + running, + app: apps.find((app) => app.app_id === running.app_id) || null, + })) + .filter((candidate): candidate is { running: RunningFlatpakApp; app: FlatpakApp } => candidate.app !== null) + .sort((a, b) => { + if (a.running.active !== b.running.active) return a.running.active ? -1 : 1; + const startDifference = numericValue(b.running.start_time) - numericValue(a.running.start_time); + if (startDifference !== 0) return startDifference; + const pidDifference = numericPid(b.running.pid) - numericPid(a.running.pid); + if (pidDifference !== 0) return pidDifference; + return a.running.app_id.localeCompare(b.running.app_id); + }); + + return candidates[0]?.app || null; +} + +export function resolveNowPlayingTarget( + runningGame: GameTarget | null, + runningFlatpak: FlatpakApp | null, +): NowPlayingTarget | null { + if (runningFlatpak) { + return { kind: "flatpak", app: runningFlatpak, launcher: runningGame }; + } + if (runningGame?.configured) return { kind: "steam", game: runningGame }; + return null; +} |
