summaryrefslogtreecommitdiff
path: root/src/components/Content.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/Content.tsx')
-rw-r--r--src/components/Content.tsx422
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>
);
}