diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/components/CollapsibleItemGroup.tsx | 26 | ||||
| -rw-r--r-- | src/components/Content.tsx | 24 | ||||
| -rw-r--r-- | src/components/FlatpakNowPlayingTab.tsx | 27 | ||||
| -rw-r--r-- | src/components/FlatpakTab.tsx | 29 | ||||
| -rw-r--r-- | src/components/GameConfigurationSelector.tsx | 22 | ||||
| -rw-r--r-- | src/components/NowPlayingSummary.tsx | 16 | ||||
| -rw-r--r-- | src/components/NowPlayingTab.tsx | 12 | ||||
| -rw-r--r-- | src/hooks/useFlatpakConfiguration.ts | 4 | ||||
| -rw-r--r-- | src/styles.ts | 4 | ||||
| -rw-r--r-- | src/utils/nowPlaying.ts | 31 |
10 files changed, 104 insertions, 91 deletions
diff --git a/src/components/CollapsibleItemGroup.tsx b/src/components/CollapsibleItemGroup.tsx index a66a8ba..29fe945 100644 --- a/src/components/CollapsibleItemGroup.tsx +++ b/src/components/CollapsibleItemGroup.tsx @@ -1,5 +1,5 @@ import { ButtonItem, Field, PanelSectionRow } from "@decky/ui"; -import { type RefObject } from "react"; +import { useEffect, useState, type RefObject } from "react"; import { RiArrowDownSFill, RiArrowUpSFill } from "react-icons/ri"; export interface CollapsibleItem { @@ -9,6 +9,11 @@ export interface CollapsibleItem { } export const collapsibleItemGroupStyles = ` + .LSFG_GameGroupCollapseButton_Container { + margin-top: -2px; + margin-bottom: 4px; + } + .LSFG_GameGroupCollapseButton_Container > div > div > div > button, .LSFG_GameGroupCollapseButton_Container > div > div > div > div > button { height: 24px !important; @@ -25,6 +30,24 @@ export const collapsibleItemGroupStyles = ` } `; +export 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; +} + interface Props { title: string; items: CollapsibleItem[]; @@ -53,7 +76,6 @@ export function CollapsibleItemGroup({ <div ref={toggleRef} className="LSFG_GameGroupCollapseButton_Container" - style={{ marginTop: "-2px", marginBottom: "4px" }} > <ButtonItem layout="below" diff --git a/src/components/Content.tsx b/src/components/Content.tsx index dc8c54f..32e9bf8 100644 --- a/src/components/Content.tsx +++ b/src/components/Content.tsx @@ -1,5 +1,5 @@ import { Tabs } from "@decky/ui"; -import { useEffect, useRef, useState, type FocusEvent } from "react"; +import { useEffect, useRef, useState, type FocusEvent, type ReactNode } from "react"; import { FaCube, FaFileAlt, FaGamepad, FaList, FaTools } from "react-icons/fa"; import { ConfigurationData } from "../config/configSchema"; import { useFlatpakConfiguration } from "../hooks/useFlatpakConfiguration"; @@ -86,7 +86,7 @@ export function Content() { const hasNowPlaying = Boolean(nowPlayingTarget); const runningWorkload = nowPlayingTarget ? nowPlayingTarget.kind === "flatpak" - ? `flatpak:${nowPlayingTarget.app.app_id}` + ? `flatpak:${nowPlayingTarget.app.app_id}:${nowPlayingTarget.launcher?.appid ?? ""}` : `steam:${nowPlayingTarget.game.appid}` : null; @@ -141,6 +141,10 @@ export function Content() { /> ); + const tabContent = (content: ReactNode) => ( + <div className="lsfg-vk-tab-content">{content}</div> + ); + const nowPlaying = nowPlayingTarget?.kind === "steam" ? ( <NowPlayingTab game={nowPlayingTarget.game} @@ -159,11 +163,11 @@ export function Content() { const tabs = setupComplete ? [ - ...(nowPlaying ? [{ id: "NowPlaying", title: tabIcons.nowPlaying, content: nowPlaying }] : []), + ...(nowPlaying ? [{ id: "NowPlaying", title: tabIcons.nowPlaying, content: tabContent(nowPlaying) }] : []), { id: "Games", title: tabIcons.games, - content: ( + content: tabContent( <ConfigurationTab config={config} targets={targets} @@ -177,13 +181,13 @@ export function Content() { onRepair={repair} onReset={resetSelected} onResetAll={resetAll} - /> + />, ), }, { id: "Flatpak", title: tabIcons.flatpak, - content: ( + content: tabContent( <FlatpakTab apps={flatpak.apps} runningApp={runningFlatpak} @@ -194,13 +198,13 @@ export function Content() { onRemove={flatpak.removeApp} onConfigChange={flatpak.updateConfig} onWorkaroundChange={flatpak.updateWorkarounds} - /> + />, ), }, - ...(showDebugTab ? [{ id: "ConfigFile", title: tabIcons.configFile, content: <ConfigFileTab /> }] : []), - { id: "Setup", title: tabIcons.setup, content: setup }, + ...(showDebugTab ? [{ id: "ConfigFile", title: tabIcons.configFile, content: tabContent(<ConfigFileTab />) }] : []), + { id: "Setup", title: tabIcons.setup, content: tabContent(setup) }, ] - : [{ id: "Setup", title: tabIcons.setup, content: setup }]; + : [{ id: "Setup", title: tabIcons.setup, content: tabContent(setup) }]; const availableTabIds = new Set(tabs.map(({ id }) => id)); const activeTab = availableTabIds.has(tab) ? tab : setupComplete ? "Games" : "Setup"; diff --git a/src/components/FlatpakNowPlayingTab.tsx b/src/components/FlatpakNowPlayingTab.tsx index 9623b11..89f7f49 100644 --- a/src/components/FlatpakNowPlayingTab.tsx +++ b/src/components/FlatpakNowPlayingTab.tsx @@ -1,8 +1,9 @@ -import { Field, Focusable, PanelSection, PanelSectionRow } from "@decky/ui"; +import { Focusable } from "@decky/ui"; import type { FlatpakApp, LsfgConfig } from "../api/lsfgApi"; import type { GameTarget } from "../hooks/useGameConfiguration"; import { ConfigurationSection } from "./ConfigurationSection"; import { FpsMultiplierControl } from "./FpsMultiplierControl"; +import { NowPlayingSummary } from "./NowPlayingSummary"; interface Props { app: FlatpakApp; @@ -21,21 +22,15 @@ export function FlatpakNowPlayingTab({ app, launcher, onConfigChange }: Props) { return ( <Focusable> - <PanelSection title="Now Playing"> - <PanelSectionRow> - <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> + <NowPlayingSummary + title={launcher?.name || app.app_name} + details={[ + launcher ? (launcher.nonSteam ? "Steam shortcut" : "Steam") : "Flatpak", + launcher && launcher.name !== app.app_name ? `Running in ${app.app_name}` : null, + launcher ? "Flatpak" : null, + `Controls: ${app.app_name} profile`, + ].filter((detail): detail is string => detail !== null)} + /> <FpsMultiplierControl config={app.config} onConfigChange={changeConfig} /> <ConfigurationSection config={app.config} onConfigChange={changeConfig} /> </Focusable> diff --git a/src/components/FlatpakTab.tsx b/src/components/FlatpakTab.tsx index 31bfc2d..0b20da0 100644 --- a/src/components/FlatpakTab.tsx +++ b/src/components/FlatpakTab.tsx @@ -1,8 +1,8 @@ import { ButtonItem, DialogButton, Field, Focusable, PanelSection, PanelSectionRow, gamepadDialogClasses } from "@decky/ui"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useCallback, useMemo, useState } from "react"; import { FaArrowLeft } from "react-icons/fa"; import type { FlatpakApp, LsfgConfig, WorkaroundState } from "../api/lsfgApi"; -import { CollapsibleItemGroup, collapsibleItemGroupStyles } from "./CollapsibleItemGroup"; +import { CollapsibleItemGroup, collapsibleItemGroupStyles, usePersistentCollapsed } from "./CollapsibleItemGroup"; import { ConfigurationSection } from "./ConfigurationSection"; import { FlatpakWorkaroundsSection } from "./FlatpakWorkaroundsSection"; import { FpsMultiplierControl } from "./FpsMultiplierControl"; @@ -20,23 +20,8 @@ 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; -} +const ENABLED_COLLAPSED_KEY = "lsfg-flatpak-enabled-collapsed-v2"; +const AVAILABLE_COLLAPSED_KEY = "lsfg-flatpak-available-collapsed-v2"; export function FlatpakTab({ apps, @@ -55,9 +40,8 @@ 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 [enabledCollapsed, toggleEnabled] = usePersistentCollapsed(ENABLED_COLLAPSED_KEY); + const [availableCollapsed, toggleAvailable] = usePersistentCollapsed(AVAILABLE_COLLAPSED_KEY); const enabledApps = useMemo( () => apps.filter((app) => app.enabled).sort((a, b) => a.app_name.localeCompare(b.app_name)), @@ -87,7 +71,6 @@ export function FlatpakTab({ collapsed={enabledCollapsed} onToggle={toggleEnabled} onSelect={setSelectedAppId} - toggleRef={enabledToggleRef} /> <CollapsibleItemGroup title="Available" diff --git a/src/components/GameConfigurationSelector.tsx b/src/components/GameConfigurationSelector.tsx index ee87423..2c683ae 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 } from "react"; +import { useEffect, useRef } from "react"; import { GameTarget } from "../hooks/useGameConfiguration"; -import { CollapsibleItemGroup, collapsibleItemGroupStyles } from "./CollapsibleItemGroup"; +import { CollapsibleItemGroup, collapsibleItemGroupStyles, usePersistentCollapsed } from "./CollapsibleItemGroup"; interface Props { targets: GameTarget[]; @@ -16,24 +16,6 @@ interface Props { 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(() => { - 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; -} - function targetDescription(game: GameTarget): string { return game.nonSteam ? "Non-Steam" : "Steam"; } diff --git a/src/components/NowPlayingSummary.tsx b/src/components/NowPlayingSummary.tsx new file mode 100644 index 0000000..adc5d10 --- /dev/null +++ b/src/components/NowPlayingSummary.tsx @@ -0,0 +1,16 @@ +import { Field, PanelSection, PanelSectionRow } from "@decky/ui"; + +interface Props { + title: string; + details: string[]; +} + +export function NowPlayingSummary({ title, details }: Props) { + return ( + <PanelSection> + <PanelSectionRow> + <Field label={title} description={details.filter(Boolean).join(" | ")} /> + </PanelSectionRow> + </PanelSection> + ); +} diff --git a/src/components/NowPlayingTab.tsx b/src/components/NowPlayingTab.tsx index c067dc0..4c22fb7 100644 --- a/src/components/NowPlayingTab.tsx +++ b/src/components/NowPlayingTab.tsx @@ -1,7 +1,8 @@ -import { Field, Focusable, PanelSection, PanelSectionRow } from "@decky/ui"; +import { Focusable } from "@decky/ui"; import { ConfigurationData } from "../config/configSchema"; import { GameTarget } from "../hooks/useGameConfiguration"; import { GameConfigurationControls } from "./GameConfigurationControls"; +import { NowPlayingSummary } from "./NowPlayingSummary"; interface Props { game: GameTarget; @@ -23,11 +24,10 @@ export function NowPlayingTab({ }: Props) { return ( <Focusable> - <PanelSection title="Now Playing"> - <PanelSectionRow> - <Field label={game.name} description={targetDescription(game)} /> - </PanelSectionRow> - </PanelSection> + <NowPlayingSummary + title={game.name} + details={[targetDescription(game), `Controls: ${game.name} profile`]} + /> <GameConfigurationControls config={config} onConfigChange={onConfigChange} diff --git a/src/hooks/useFlatpakConfiguration.ts b/src/hooks/useFlatpakConfiguration.ts index ce881f0..6ff7a79 100644 --- a/src/hooks/useFlatpakConfiguration.ts +++ b/src/hooks/useFlatpakConfiguration.ts @@ -120,9 +120,7 @@ export function useFlatpakConfiguration(enabled: boolean) { [operate], ); - const runningApp = useMemo(() => { - return selectMostRecentRunningFlatpak(apps, runningApps); - }, [apps, runningApps]); + const runningApp = useMemo(() => selectMostRecentRunningFlatpak(apps, runningApps), [apps, runningApps]); return { apps, diff --git a/src/styles.ts b/src/styles.ts index d197451..58c6b01 100644 --- a/src/styles.ts +++ b/src/styles.ts @@ -10,6 +10,10 @@ export const tabStyles = ` padding-right: 8px !important; } + .lsfg-vk-tabs .lsfg-vk-tab-content { + padding-bottom: 96px; // workaround for in-game bottom bar padding behaving differently than in launcher, remove later? + } + .lsfg-vk-tabs [role="tablist"] { display: flex; flex-wrap: nowrap; diff --git a/src/utils/nowPlaying.ts b/src/utils/nowPlaying.ts index a207321..2e0a876 100644 --- a/src/utils/nowPlaying.ts +++ b/src/utils/nowPlaying.ts @@ -29,25 +29,34 @@ export function selectMostRecentRunningFlatpak( 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); - }); + .filter((candidate): candidate is { running: RunningFlatpakApp; app: FlatpakApp } => candidate.app !== null); + const activeCandidates = candidates.filter(({ running }) => running.active); + const eligibleCandidates = activeCandidates.length > 0 + ? activeCandidates + : candidates.length === 1 + ? candidates + : []; - return candidates[0]?.app || null; + eligibleCandidates.sort((a, b) => { + 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 eligibleCandidates[0]?.app || null; } export function resolveNowPlayingTarget( runningGame: GameTarget | null, runningFlatpak: FlatpakApp | null, ): NowPlayingTarget | null { + if (runningGame && !runningGame.nonSteam) { + return runningGame.configured ? { kind: "steam", game: runningGame } : null; + } if (runningFlatpak) { - return { kind: "flatpak", app: runningFlatpak, launcher: runningGame }; + return { kind: "flatpak", app: runningFlatpak, launcher: runningGame?.nonSteam ? runningGame : null }; } if (runningGame?.configured) return { kind: "steam", game: runningGame }; return null; |
