From f3074fbe1427411dc3b5597d2e87918383299e3f Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Fri, 11 Sep 2026 13:47:52 -0400 Subject: feat: non steam tab, faster bulk actions --- src/components/ConfigFileTab.tsx | 4 +- src/components/ConfigurationTab.tsx | 56 ++++++++++----- src/components/Content.tsx | 98 ++++++++++++++++++-------- src/components/FlatpakNowPlayingTab.tsx | 4 +- src/components/GameConfigurationSelector.tsx | 47 ++++++++----- src/components/NowPlayingTab.tsx | 5 +- src/components/SettingsTab.tsx | 100 +++++++++++++++++++++++++++ src/components/SetupTab.tsx | 100 --------------------------- src/components/index.ts | 2 +- 9 files changed, 245 insertions(+), 171 deletions(-) create mode 100644 src/components/SettingsTab.tsx delete mode 100644 src/components/SetupTab.tsx (limited to 'src/components') diff --git a/src/components/ConfigFileTab.tsx b/src/components/ConfigFileTab.tsx index 07408d7..fd6d716 100644 --- a/src/components/ConfigFileTab.tsx +++ b/src/components/ConfigFileTab.tsx @@ -80,7 +80,7 @@ export function ConfigFileTab() { if (!result) { return ( - + @@ -108,7 +108,7 @@ export function ConfigFileTab() { } `} - + {result.error && ( diff --git a/src/components/ConfigurationTab.tsx b/src/components/ConfigurationTab.tsx index 37a7867..37555e0 100644 --- a/src/components/ConfigurationTab.tsx +++ b/src/components/ConfigurationTab.tsx @@ -1,26 +1,36 @@ -import { ButtonItem, ConfirmModal, DialogButton, Focusable, PanelSection, PanelSectionRow, gamepadDialogClasses, showModal } from "@decky/ui"; +import { ButtonItem, ConfirmModal, DialogButton, Field, Focusable, PanelSection, PanelSectionRow, gamepadDialogClasses, showModal } from "@decky/ui"; import { useCallback, useEffect, useRef, useState } from "react"; import { FaArrowLeft } from "react-icons/fa"; import { ConfigurationData } from "../config/configSchema"; -import { GameTarget } from "../hooks/useGameConfiguration"; +import type { GameTarget, KnownGameSource } from "../utils/gameTargets"; +import { sourceLabel } from "../utils/gameTargets"; import { GameConfigurationControls } from "./GameConfigurationControls"; import { GameConfigurationSelector } from "./GameConfigurationSelector"; import { ProfileDetails } from "./ProfileDetails"; interface ConfigurationTabProps { + title: string; + source: KnownGameSource; config: ConfigurationData; targets: GameTarget[]; runningGame: GameTarget | null; onSelect: (appid: string) => void; - onConfigChange: (fieldName: keyof ConfigurationData, value: boolean | number | string | string[]) => Promise; + onConfigChange: ( + fieldName: keyof ConfigurationData, + value: boolean | number | string | string[], + cleanupLaunchOptions?: boolean, + ) => Promise; onEnable: (appid: string) => Promise; - onEnableAll: () => Promise; + onEnableAll: (source: KnownGameSource) => Promise; + bulkOperationBusy: boolean; onRepair: (appid: string) => Promise; onReset: () => Promise; - onResetAll: () => Promise; + onResetAll: (source: KnownGameSource) => Promise; } export function ConfigurationTab({ + title, + source, config, targets, runningGame, @@ -28,6 +38,7 @@ export function ConfigurationTab({ onConfigChange, onEnable, onEnableAll, + bulkOperationBusy, onRepair, onReset, onResetAll, @@ -64,10 +75,12 @@ export function ConfigurationTab({ if (detailAppId === null) { return ( <> - + { setFocusConfiguredToggle(false); setFocusDetailAction(targets.find((target) => target.appid === appid)?.configured ? "fps" : "enable"); @@ -85,9 +98,9 @@ export function ConfigurationTab({ } const profileLabel = selectedTarget?.name || "Game profile"; - const profileTransport = selectedTarget?.nonSteam ? "Non-Steam" : "Steam"; + const profileTransport = selectedTarget ? sourceLabel(selectedTarget.source) : sourceLabel(source); const profileDescription = selectedTarget - ? `${profileTransport} · 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"}${selectedTarget.source === "unknown" ? " · Bulk actions exclude this profile" : ""}` : "Game is no longer available"; const enableProfile = async (appid: string, quitRunningGame = false) => { if (!(await onEnable(appid))) return; @@ -101,8 +114,8 @@ export function ConfigurationTab({ closeDetails(); } else if (detailAppId) { const isRunningUnconfigured = runningGame?.appid === detailAppId - && runningGame.nonSteam === false - && selectedTarget?.nonSteam === false + && runningGame.source === "steam" + && selectedTarget?.source === "steam" && !runningGame.configured; if (isRunningUnconfigured) { showModal( @@ -128,7 +141,7 @@ export function ConfigurationTab({
{!selectedTarget?.configured && selectedTarget && ( - - Enable for next launch - + {selectedTarget.source === "unknown" ? ( + + ) : ( + + Enable for next launch + + )} )} {selectedTarget?.configured && ( onConfigChange(field, value, selectedTarget?.source !== "unknown")} autoFocusFpsMultiplier={focusFpsMultiplier} onFpsMultiplierFocused={clearFpsFocusRequest} - showWorkarounds - workaroundTarget={selectedTarget || undefined} - onRepairWorkaround={selectedTarget ? () => onRepair(selectedTarget.appid) : undefined} + showWorkarounds={selectedTarget.source !== "unknown"} + workaroundTarget={selectedTarget.source !== "unknown" ? selectedTarget : undefined} + onRepairWorkaround={selectedTarget.source !== "unknown" ? () => onRepair(selectedTarget.appid) : undefined} /> )} {selectedTarget?.configured && ( diff --git a/src/components/Content.tsx b/src/components/Content.tsx index 3f25320..470db95 100644 --- a/src/components/Content.tsx +++ b/src/components/Content.tsx @@ -1,28 +1,37 @@ import { Tabs } from "@decky/ui"; import { useEffect, useRef, useState, type FocusEvent, type ReactNode } from "react"; -import { FaCube, FaFileAlt, FaGamepad, FaList, FaTools } from "react-icons/fa"; +import { FaCube, FaExternalLinkAlt, FaFileAlt, FaGamepad, FaSteam, FaTools } from "react-icons/fa"; import { ConfigurationData } from "../config/configSchema"; 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 { targetsForSource } from "../utils/gameTargets"; +import { resolveNowPlayingTarget, type NowPlayingTarget } from "../utils/nowPlaying"; import { ConfigFileTab } from "./ConfigFileTab"; import { ConfigurationTab } from "./ConfigurationTab"; import { FlatpakNowPlayingTab } from "./FlatpakNowPlayingTab"; import { FlatpakTab } from "./FlatpakTab"; import { NowPlayingTab } from "./NowPlayingTab"; -import { SetupTab } from "./SetupTab"; +import { SettingsTab } from "./SettingsTab"; const tabIcons = { nowPlaying: , - games: , + steam: , + nonSteam: , flatpak: , configFile: , - setup: , + settings: , }; const DEBUG_TAB_VISIBILITY_KEY = "lsfg-debug-tab-visible-v1"; +type GameTabId = "Steam" | "NonSteam" | "Flatpak"; + +function tabForNowPlaying(target: NowPlayingTarget | null): GameTabId { + if (!target) return "Steam"; + if (target.kind === "flatpak") return target.launcher?.source === "nonSteam" ? "NonSteam" : "Flatpak"; + return target.game.source === "nonSteam" ? "NonSteam" : "Steam"; +} function usePersistentBoolean(key: string, defaultValue: boolean) { const [value, setValue] = useState(() => { @@ -56,6 +65,7 @@ export function Content() { updateGlobal, enable, enableAll, + bulkOperationBusy, repair, resetSelected, resetAll, @@ -80,34 +90,40 @@ export function Content() { steamBranchStatus.installed && !steamBranchStatus.needs_switch; const flatpak = useFlatpakConfiguration(setupComplete); - const [tab, setTab] = useState("Setup"); + const [tab, setTab] = useState("Settings"); const [showDebugTab, setShowDebugTab] = usePersistentBoolean(DEBUG_TAB_VISIBILITY_KEY, false); const [contentFocused, setContentFocused] = useState(false); const previousRunningWorkload = useRef(null); + const previousNowPlayingTab = useRef("Steam"); const runningFlatpak = flatpak.runningApp; const nowPlayingTarget = resolveNowPlayingTarget(runningGame, runningFlatpak); const hasNowPlaying = Boolean(nowPlayingTarget); const runningWorkload = nowPlayingTarget ? nowPlayingTarget.kind === "flatpak" ? `flatpak:${nowPlayingTarget.app.app_id}:${nowPlayingTarget.launcher?.appid ?? ""}` - : `steam:${nowPlayingTarget.game.appid}` + : `${nowPlayingTarget.game.source}:${nowPlayingTarget.game.appid}` : null; + const steamTargets = targetsForSource(targets, "steam"); + const nonSteamTargets = targetsForSource(targets, "nonSteam"); useEffect(() => { if (!setupComplete) { - setTab("Setup"); + setTab("Settings"); return; } - setTab((current) => current === "Setup" ? (hasNowPlaying ? "NowPlaying" : "Games") : current); + setTab((current) => current === "Settings" ? (hasNowPlaying ? "NowPlaying" : "Steam") : current); }, [hasNowPlaying, setupComplete]); useEffect(() => { if (!setupComplete) return; const previous = previousRunningWorkload.current; previousRunningWorkload.current = runningWorkload; - if (runningWorkload && runningWorkload !== previous) setTab("NowPlaying"); + if (runningWorkload && runningWorkload !== previous) { + previousNowPlayingTab.current = tabForNowPlaying(nowPlayingTarget); + setTab("NowPlaying"); + } else if (!runningWorkload && previous) { - setTab((current) => current === "NowPlaying" ? "Games" : current); + setTab((current) => current === "NowPlaying" ? previousNowPlayingTab.current : current); } }, [runningWorkload, setupComplete]); @@ -119,8 +135,8 @@ export function Content() { }, [isInstalled, reload, flatpak.reload]); useEffect(() => { - if (!showDebugTab && tab === "ConfigFile") setTab("Games"); - }, [showDebugTab, tab]); + if (!showDebugTab && tab === "ConfigFile") setTab(setupComplete ? "Steam" : "Settings"); + }, [setupComplete, showDebugTab, tab]); const handleConfigChange = async ( fieldName: keyof ConfigurationData, @@ -130,8 +146,8 @@ export function Content() { await save({ ...config, [fieldName]: value }, cleanupLaunchOptions); }; - const setup = ( - {content}
); - const nowPlaying = nowPlayingTarget?.kind === "steam" ? ( + const nowPlaying = nowPlayingTarget?.kind === "flatpak" ? ( + + ) : nowPlayingTarget ? ( - ) : nowPlayingTarget?.kind === "flatpak" ? ( - ) : null; const tabs = setupComplete ? [ ...(nowPlaying ? [{ id: "NowPlaying", title: tabIcons.nowPlaying, content: tabContent(nowPlaying) }] : []), { - id: "Games", - title: tabIcons.games, + id: "Steam", + title: tabIcons.steam, content: tabContent( handleConfigChange(field, value, true)} + onConfigChange={handleConfigChange} onEnable={enable} onEnableAll={enableAll} + bulkOperationBusy={bulkOperationBusy} onRepair={repair} onReset={resetSelected} onResetAll={resetAll} />, ), }, + { + id: "NonSteam", + title: tabIcons.nonSteam, + content: tabContent( + + ), + }, { id: "Flatpak", title: tabIcons.flatpak, @@ -209,12 +249,12 @@ export function Content() { ), }, ...(showDebugTab ? [{ id: "ConfigFile", title: tabIcons.configFile, content: tabContent() }] : []), - { id: "Setup", title: tabIcons.setup, content: tabContent(setup) }, + { id: "Settings", title: tabIcons.settings, content: tabContent(settings) }, ] - : [{ id: "Setup", title: tabIcons.setup, content: tabContent(setup) }]; + : [{ id: "Settings", title: tabIcons.settings, content: tabContent(settings) }]; const availableTabIds = new Set(tabs.map(({ id }) => id)); - const activeTab = availableTabIds.has(tab) ? tab : setupComplete ? "Games" : "Setup"; + const activeTab = availableTabIds.has(tab) ? tab : setupComplete ? "Steam" : "Settings"; const handleFocusCapture = (event: FocusEvent) => { const focusedElement = event.target as HTMLElement | null; setContentFocused(!focusedElement?.closest?.('[role="tab"]')); diff --git a/src/components/FlatpakNowPlayingTab.tsx b/src/components/FlatpakNowPlayingTab.tsx index 89f7f49..449e4b5 100644 --- a/src/components/FlatpakNowPlayingTab.tsx +++ b/src/components/FlatpakNowPlayingTab.tsx @@ -1,6 +1,6 @@ import { Focusable } from "@decky/ui"; import type { FlatpakApp, LsfgConfig } from "../api/lsfgApi"; -import type { GameTarget } from "../hooks/useGameConfiguration"; +import type { GameTarget } from "../utils/gameTargets"; import { ConfigurationSection } from "./ConfigurationSection"; import { FpsMultiplierControl } from "./FpsMultiplierControl"; import { NowPlayingSummary } from "./NowPlayingSummary"; @@ -25,7 +25,7 @@ export function FlatpakNowPlayingTab({ app, launcher, onConfigChange }: Props) { void; - onEnableAll: () => Promise; - onResetAll: () => Promise; + onEnableAll: (source: KnownGameSource) => Promise; + onResetAll: (source: KnownGameSource) => Promise; focusConfiguredToggle?: boolean; onConfiguredToggleFocused?: () => void; } @@ -17,12 +20,16 @@ const ENABLED_COLLAPSED_KEY = "lsfg-enabled-games-collapsed-v4"; const AVAILABLE_COLLAPSED_KEY = "lsfg-available-games-collapsed-v3"; function targetDescription(game: GameTarget): string { - return game.nonSteam ? "Non-Steam" : "Steam"; + return game.source === "unknown" + ? "Unknown source · excluded from bulk actions" + : sourceLabel(game.source); } export function GameConfigurationSelector({ targets, runningGame, + source, + bulkOperationBusy, onSelect, onEnableAll, onResetAll, @@ -36,13 +43,19 @@ export function GameConfigurationSelector({ }); const enabledGames = sortGames(targets.filter((game) => game.configured)); const availableGames = sortGames(targets.filter((game) => !game.configured)); + const enableableGames = availableGames.filter((game) => game.source === source); + const removableGames = enabledGames.filter((game) => game.source === source); + const sourceName = source === "nonSteam" ? "non-Steam shortcuts" : "Steam games"; + const emptyDescription = source === "nonSteam" + ? "Steam has not reported any eligible non-Steam shortcuts" + : "Steam has not reported any eligible installed games"; 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 [enabledCollapsed, toggleEnabled] = usePersistentCollapsed(`${ENABLED_COLLAPSED_KEY}-${source}`); + const [availableCollapsed, toggleAvailable] = usePersistentCollapsed(`${AVAILABLE_COLLAPSED_KEY}-${source}`); const enabledToggleRef = useRef(null); useEffect(() => { @@ -57,10 +70,10 @@ export function GameConfigurationSelector({ const confirmResetAll = () => { showModal( void onResetAll()} + onOK={() => void onResetAll(source)} onCancel={() => {}} />, ); @@ -69,11 +82,11 @@ export function GameConfigurationSelector({ const confirmEnableAll = () => { showModal( void onEnableAll()} + onOK={() => void onEnableAll(source)} onCancel={() => {}} />, ); @@ -86,7 +99,7 @@ export function GameConfigurationSelector({ {targets.length === 0 && ( - + )} - {availableGames.length > 0 && ( + {enableableGames.length > 0 && ( - - Enable all available games + + {`Enable all ${sourceName}`} )} @@ -115,9 +128,9 @@ export function GameConfigurationSelector({ target.configured)} + disabled={bulkOperationBusy || removableGames.length === 0} > - Remove all profiles + {`Remove all ${sourceLabel(source)} profiles`}
diff --git a/src/components/NowPlayingTab.tsx b/src/components/NowPlayingTab.tsx index 4c22fb7..69e72d7 100644 --- a/src/components/NowPlayingTab.tsx +++ b/src/components/NowPlayingTab.tsx @@ -1,6 +1,7 @@ import { Focusable } from "@decky/ui"; import { ConfigurationData } from "../config/configSchema"; -import { GameTarget } from "../hooks/useGameConfiguration"; +import type { GameTarget } from "../utils/gameTargets"; +import { sourceLabel } from "../utils/gameTargets"; import { GameConfigurationControls } from "./GameConfigurationControls"; import { NowPlayingSummary } from "./NowPlayingSummary"; @@ -14,7 +15,7 @@ interface Props { } function targetDescription(game: GameTarget): string { - return game.nonSteam ? "Non-Steam" : "Steam"; + return sourceLabel(game.source); } export function NowPlayingTab({ diff --git a/src/components/SettingsTab.tsx b/src/components/SettingsTab.tsx new file mode 100644 index 0000000..35a74cc --- /dev/null +++ b/src/components/SettingsTab.tsx @@ -0,0 +1,100 @@ +import { ButtonItem, Field, PanelSection, PanelSectionRow, ToggleField } from "@decky/ui"; +import { type GlobalConfig, type SteamBranchStatus } from "../api/lsfgApi"; +import t from "../i18n/i18n"; + +interface SettingsTabProps { + isInstalled: boolean; + installationStatus: string; + losslessScalingInstalled: boolean; + losslessScalingStatus: string; + steamBranchStatus: SteamBranchStatus | null; + isInstalling: boolean; + isUninstalling: boolean; + globalConfig: GlobalConfig; + showDebugTab: boolean; + onGlobalConfigChange: (config: GlobalConfig) => Promise; + onShowDebugTabChange: (value: boolean) => void; + onInstall: () => void; + onUninstall: () => void; +} + +export function SettingsTab(props: SettingsTabProps) { + const { + isInstalled, + installationStatus, + losslessScalingInstalled, + losslessScalingStatus, + steamBranchStatus, + isInstalling, + isUninstalling, + globalConfig, + showDebugTab, + onGlobalConfigChange, + onShowDebugTabChange, + onInstall, + onUninstall, + } = props; + const losslessScalingAppInstalled = losslessScalingInstalled || steamBranchStatus?.installed === true; + const buttonLabel = isInstalling + ? t("INSTALL_INSTALLING", "Installing...") + : isUninstalling + ? t("INSTALL_UNINSTALLING", "Uninstalling...") + : isInstalled + ? t("INSTALL_UNINSTALL_BTN", "Uninstall LSFG-VK") + : t("INSTALL_INSTALL_BTN", "Install LSFG-VK"); + + return ( + <> + + + + + + + + {steamBranchStatus?.installed && ( + + + + )} + + + {buttonLabel} + + + + {isInstalled && ( + <> + + + void onGlobalConfigChange({ ...globalConfig, no_fp16: !value })} + /> + + + + + + + + + )} + + ); +} diff --git a/src/components/SetupTab.tsx b/src/components/SetupTab.tsx deleted file mode 100644 index ff072cb..0000000 --- a/src/components/SetupTab.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import { ButtonItem, Field, PanelSection, PanelSectionRow, ToggleField } from "@decky/ui"; -import { type GlobalConfig, type SteamBranchStatus } from "../api/lsfgApi"; -import t from "../i18n/i18n"; - -interface SetupTabProps { - isInstalled: boolean; - installationStatus: string; - losslessScalingInstalled: boolean; - losslessScalingStatus: string; - steamBranchStatus: SteamBranchStatus | null; - isInstalling: boolean; - isUninstalling: boolean; - globalConfig: GlobalConfig; - showDebugTab: boolean; - onGlobalConfigChange: (config: GlobalConfig) => Promise; - onShowDebugTabChange: (value: boolean) => void; - onInstall: () => void; - onUninstall: () => void; -} - -export function SetupTab(props: SetupTabProps) { - const { - isInstalled, - installationStatus, - losslessScalingInstalled, - losslessScalingStatus, - steamBranchStatus, - isInstalling, - isUninstalling, - globalConfig, - showDebugTab, - onGlobalConfigChange, - onShowDebugTabChange, - onInstall, - onUninstall, - } = props; - const losslessScalingAppInstalled = losslessScalingInstalled || steamBranchStatus?.installed === true; - const buttonLabel = isInstalling - ? t("INSTALL_INSTALLING", "Installing...") - : isUninstalling - ? t("INSTALL_UNINSTALLING", "Uninstalling...") - : isInstalled - ? t("INSTALL_UNINSTALL_BTN", "Uninstall LSFG-VK") - : t("INSTALL_INSTALL_BTN", "Install LSFG-VK"); - - return ( - <> - - - - - - - - {steamBranchStatus?.installed && ( - - - - )} - - - {buttonLabel} - - - - {isInstalled && ( - <> - - - void onGlobalConfigChange({ ...globalConfig, no_fp16: !value })} - /> - - - - - - - - - )} - - ); -} diff --git a/src/components/index.ts b/src/components/index.ts index bca6f6f..5459974 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -3,7 +3,7 @@ export { ConfigurationSection } from "./ConfigurationSection"; export { FpsMultiplierControl } from "./FpsMultiplierControl"; export { ConfigurationTab } from "./ConfigurationTab"; export { ConfigFileTab } from "./ConfigFileTab"; -export { SetupTab } from "./SetupTab"; +export { SettingsTab } from "./SettingsTab"; export { GameConfigurationSelector } from "./GameConfigurationSelector"; export { GameConfigurationControls } from "./GameConfigurationControls"; export { NowPlayingTab } from "./NowPlayingTab"; -- cgit v1.2.3