diff options
| author | Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> | 2026-09-12 21:29:08 -0400 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-09-12 21:29:08 -0400 |
| commit | e580c6bd60c92af36f92d4c29b5d9a44f73d47d7 (patch) | |
| tree | 7fc6799626519e5b01fb137f5440c99fda5faca7 /src/components/Content.tsx | |
| parent | e997a3fb74fa70f5e60b60807fb0897120e98313 (diff) | |
| parent | 81feb03288166755545401b9df2ff2c9bc3ae7d7 (diff) | |
| download | decky-lsfg-vk-e580c6bd60c92af36f92d4c29b5d9a44f73d47d7.tar.gz decky-lsfg-vk-e580c6bd60c92af36f92d4c29b5d9a44f73d47d7.zip | |
Merge pull request #264 from xXJSONDeruloXx/chore/migrate
the big one
Diffstat (limited to 'src/components/Content.tsx')
| -rw-r--r-- | src/components/Content.tsx | 422 |
1 files changed, 257 insertions, 165 deletions
diff --git a/src/components/Content.tsx b/src/components/Content.tsx index 5988e79..470db95 100644 --- a/src/components/Content.tsx +++ b/src/components/Content.tsx @@ -1,187 +1,279 @@ -import { useEffect } from "react"; -import { PanelSection, showModal, ButtonItem, PanelSectionRow } from "@decky/ui"; -import { useInstallationStatus, useDllDetection, useLsfgConfig } from "../hooks/useLsfgHooks"; -import { useProfileManagement } from "../hooks/useProfileManagement"; -import { useInstallationActions } from "../hooks/useInstallationActions"; -import { StatusDisplay } from "./StatusDisplay"; -import { InstallationButton } from "./InstallationButton"; -import { ConfigurationSection } from "./ConfigurationSection"; -import { ProfileManagement } from "./ProfileManagement"; -import { UsageInstructions } from "./UsageInstructions"; -import { SmartClipboardButton } from "./SmartClipboardButton"; -import { FgmodClipboardButton } from "./FgmodClipboardButton"; -import { FpsMultiplierControl } from "./FpsMultiplierControl"; -import { NerdStuffModal } from "./NerdStuffModal"; -import { FlatpaksModal } from "./FlatpaksModal"; +import { Tabs } from "@decky/ui"; +import { useEffect, useRef, useState, type FocusEvent, type ReactNode } from "react"; +import { FaCube, FaExternalLinkAlt, FaFileAlt, FaGamepad, FaSteam, FaTools } from "react-icons/fa"; import { ConfigurationData } from "../config/configSchema"; -import t from '../i18n/i18n'; +import { useFlatpakConfiguration } from "../hooks/useFlatpakConfiguration"; +import { useGameConfiguration } from "../hooks/useGameConfiguration"; +import { useInstallation } from "../hooks/useLsfgHooks"; +import { tabStyles } from "../styles"; +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 { SettingsTab } from "./SettingsTab"; -export function Content() { - const { - isInstalled, - installationStatus, - setIsInstalled, - setInstallationStatus - } = useInstallationStatus(); +const tabIcons = { + nowPlaying: <FaGamepad size={18} />, + steam: <FaSteam size={18} />, + nonSteam: <FaExternalLinkAlt size={18} />, + flatpak: <FaCube size={18} />, + configFile: <FaFileAlt size={18} />, + settings: <FaTools size={18} />, +}; + +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(() => { + try { + const stored = localStorage.getItem(key); + return stored === null ? defaultValue : stored === "true"; + } catch { + return defaultValue; + } + }); + + useEffect(() => { + try { + localStorage.setItem(key, String(value)); + } catch {} + }, [key, value]); - const { dllDetected, dllDetectionStatus } = useDllDetection(); + return [value, setValue] as const; +} +export function Content() { const { config, - loadLsfgConfig, - updateField - } = useLsfgConfig(); - + runningConfig, + globalConfig, + targets, + runningGame, + setSelectedAppId, + save, + saveFor, + updateGlobal, + enable, + enableAll, + bulkOperationBusy, + repair, + resetSelected, + resetAll, + cleanupAllWorkarounds, + reload, + } = useGameConfiguration(); const { - currentProfile, - updateProfileConfig, - loadProfiles - } = useProfileManagement(); + isInstalled, + installationStatus, + losslessScalingInstalled, + losslessScalingStatus, + steamBranchStatus, + isInstalling, + isUninstalling, + install, + uninstall, + } = useInstallation(reload, cleanupAllWorkarounds); + const setupComplete = + isInstalled && + losslessScalingInstalled && + steamBranchStatus?.success === true && + steamBranchStatus.installed && + !steamBranchStatus.needs_switch; + const flatpak = useFlatpakConfiguration(setupComplete); + const [tab, setTab] = useState("Settings"); + const [showDebugTab, setShowDebugTab] = usePersistentBoolean(DEBUG_TAB_VISIBILITY_KEY, false); + const [contentFocused, setContentFocused] = useState(false); + const previousRunningWorkload = useRef<string | null>(null); + const previousNowPlayingTab = useRef<GameTabId>("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 ?? ""}` + : `${nowPlayingTarget.game.source}:${nowPlayingTarget.game.appid}` + : null; + const steamTargets = targetsForSource(targets, "steam"); + const nonSteamTargets = targetsForSource(targets, "nonSteam"); - const { isInstalling, isUninstalling, handleInstall, handleUninstall } = useInstallationActions(); + useEffect(() => { + if (!setupComplete) { + setTab("Settings"); + return; + } + setTab((current) => current === "Settings" ? (hasNowPlaying ? "NowPlaying" : "Steam") : current); + }, [hasNowPlaying, setupComplete]); useEffect(() => { - if (isInstalled) { - loadLsfgConfig(); + if (!setupComplete) return; + const previous = previousRunningWorkload.current; + previousRunningWorkload.current = runningWorkload; + if (runningWorkload && runningWorkload !== previous) { + previousNowPlayingTab.current = tabForNowPlaying(nowPlayingTarget); + setTab("NowPlaying"); } - }, [isInstalled, loadLsfgConfig]); - - const handleConfigChange = async (fieldName: keyof ConfigurationData, value: boolean | number | string) => { - if (currentProfile) { - const newConfig = { ...config, [fieldName]: value }; - const result = await updateProfileConfig(currentProfile, newConfig); - if (result.success) { - await loadLsfgConfig(); - } - } else { - await updateField(fieldName, value); + else if (!runningWorkload && previous) { + setTab((current) => current === "NowPlaying" ? previousNowPlayingTab.current : current); } - }; + }, [runningWorkload, setupComplete]); - const onInstall = () => { - handleInstall(setIsInstalled, setInstallationStatus, loadLsfgConfig); - }; + useEffect(() => { + if (isInstalled) { + void reload(); + void flatpak.reload(); + } + }, [isInstalled, reload, flatpak.reload]); - const onUninstall = () => { - handleUninstall(setIsInstalled, setInstallationStatus); - }; + useEffect(() => { + if (!showDebugTab && tab === "ConfigFile") setTab(setupComplete ? "Steam" : "Settings"); + }, [setupComplete, showDebugTab, tab]); - const handleShowNerdStuff = () => { - showModal(<NerdStuffModal />); + const handleConfigChange = async ( + fieldName: keyof ConfigurationData, + value: boolean | number | string | string[], + cleanupLaunchOptions = false, + ) => { + await save({ ...config, [fieldName]: value }, cleanupLaunchOptions); }; - const handleShowFlatpaks = () => { - showModal(<FlatpaksModal />); + const settings = ( + <SettingsTab + isInstalled={isInstalled} + installationStatus={installationStatus} + losslessScalingInstalled={losslessScalingInstalled} + losslessScalingStatus={losslessScalingStatus} + steamBranchStatus={steamBranchStatus} + isInstalling={isInstalling} + isUninstalling={isUninstalling} + globalConfig={globalConfig} + showDebugTab={showDebugTab} + onGlobalConfigChange={updateGlobal} + onShowDebugTabChange={setShowDebugTab} + onInstall={() => void install()} + onUninstall={() => void uninstall()} + /> + ); + + const tabContent = (content: ReactNode) => ( + <div className="lsfg-vk-tab-content">{content}</div> + ); + + const nowPlaying = nowPlayingTarget?.kind === "flatpak" ? ( + <FlatpakNowPlayingTab + app={nowPlayingTarget.app} + launcher={nowPlayingTarget.launcher} + onConfigChange={flatpak.updateConfig} + /> + ) : nowPlayingTarget ? ( + <NowPlayingTab + game={nowPlayingTarget.game} + config={runningConfig} + onConfigChange={async (field, value) => { + await saveFor(nowPlayingTarget.game.appid, { ...runningConfig, [field]: value }, true); + }} + /> + ) : null; + + const tabs = setupComplete + ? [ + ...(nowPlaying ? [{ id: "NowPlaying", title: tabIcons.nowPlaying, content: tabContent(nowPlaying) }] : []), + { + id: "Steam", + title: tabIcons.steam, + content: tabContent( + <ConfigurationTab + title="Steam games" + source="steam" + config={config} + targets={steamTargets} + runningGame={runningGame} + onSelect={setSelectedAppId} + onConfigChange={handleConfigChange} + onEnable={enable} + onEnableAll={enableAll} + bulkOperationBusy={bulkOperationBusy} + onRepair={repair} + onReset={resetSelected} + onResetAll={resetAll} + />, + ), + }, + { + id: "NonSteam", + title: tabIcons.nonSteam, + content: tabContent( + <ConfigurationTab + title="Non-Steam games" + source="nonSteam" + config={config} + targets={nonSteamTargets} + runningGame={runningGame} + onSelect={setSelectedAppId} + onConfigChange={handleConfigChange} + onEnable={enable} + onEnableAll={enableAll} + bulkOperationBusy={bulkOperationBusy} + onRepair={repair} + onReset={resetSelected} + onResetAll={resetAll} + /> + ), + }, + { + id: "Flatpak", + title: tabIcons.flatpak, + content: tabContent( + <FlatpakTab + apps={flatpak.apps} + runningApp={runningFlatpak} + loading={flatpak.loading} + busyAppId={flatpak.busyAppId} + onRefresh={flatpak.reload} + onEnable={flatpak.enableApp} + onEnableAll={flatpak.enableAll} + onRemove={flatpak.removeApp} + onRemoveAll={flatpak.removeAll} + onConfigChange={flatpak.updateConfig} + onWorkaroundChange={flatpak.updateWorkarounds} + />, + ), + }, + ...(showDebugTab ? [{ id: "ConfigFile", title: tabIcons.configFile, content: tabContent(<ConfigFileTab />) }] : []), + { id: "Settings", title: tabIcons.settings, content: tabContent(settings) }, + ] + : [{ id: "Settings", title: tabIcons.settings, content: tabContent(settings) }]; + + const availableTabIds = new Set(tabs.map(({ id }) => id)); + const activeTab = availableTabIds.has(tab) ? tab : setupComplete ? "Steam" : "Settings"; + const handleFocusCapture = (event: FocusEvent<HTMLDivElement>) => { + const focusedElement = event.target as HTMLElement | null; + setContentFocused(!focusedElement?.closest?.('[role="tab"]')); }; return ( - <PanelSection> - {!isInstalled && ( - <> - <InstallationButton - isInstalled={isInstalled} - isInstalling={isInstalling} - isUninstalling={isUninstalling} - onInstall={onInstall} - onUninstall={onUninstall} - /> - - <StatusDisplay - dllDetected={dllDetected} - dllDetectionStatus={dllDetectionStatus} - isInstalled={isInstalled} - installationStatus={installationStatus} - /> - </> - )} - - {isInstalled && ( - <> - <PanelSectionRow> - <div - style={{ - fontSize: "14px", - fontWeight: "bold", - marginTop: "8px", - marginBottom: "6px", - borderBottom: "1px solid rgba(255, 255, 255, 0.2)", - paddingBottom: "3px", - color: "white" - }} - > - {t('CONTENT_FPS_MULTIPLIER', 'FPS Multiplier')} - </div> - </PanelSectionRow> - - <FpsMultiplierControl - config={config} - onConfigChange={handleConfigChange} - /> - </> - )} - - {isInstalled && ( - <ProfileManagement - currentProfile={currentProfile} - onProfileChange={async () => { - await loadProfiles(); - await loadLsfgConfig(); - }} - /> - )} - - {isInstalled && ( - <ConfigurationSection - config={config} - onConfigChange={handleConfigChange} - /> - )} - - {isInstalled && ( - <> - <SmartClipboardButton /> - <FgmodClipboardButton /> - </> - )} - - <UsageInstructions /> - - <PanelSectionRow> - <ButtonItem - layout="below" - onClick={handleShowNerdStuff} - > - {t('CONTENT_NERD_STUFF', 'Nerd Stuff')} - </ButtonItem> - </PanelSectionRow> - - <PanelSectionRow> - <ButtonItem - layout="below" - onClick={handleShowFlatpaks} - > - {t('CONTENT_FLATPAK_SETUP', 'Flatpak Setup')} - </ButtonItem> - </PanelSectionRow> - - {isInstalled && ( - <> - <StatusDisplay - dllDetected={dllDetected} - dllDetectionStatus={dllDetectionStatus} - isInstalled={isInstalled} - installationStatus={installationStatus} - /> - - <InstallationButton - isInstalled={isInstalled} - isInstalling={isInstalling} - isUninstalling={isUninstalling} - onInstall={onInstall} - onUninstall={onUninstall} - /> - </> - )} - </PanelSection> + <div + className={`lsfg-vk-tabs${contentFocused ? " lsfg-vk-tabs--content-focused" : ""}`} + style={{ height: "95%", width: "300px", position: "fixed", marginTop: "-12px", overflow: "hidden" }} + onFocusCapture={handleFocusCapture} + > + <style>{tabStyles}</style> + <Tabs + activeTab={activeTab} + onShowTab={(nextTab: string) => { + if (availableTabIds.has(nextTab)) setTab(nextTab); + }} + tabs={tabs} + /> + </div> ); } |
