From 4fe21381b6c694d5eb615447c4827e4921593a52 Mon Sep 17 00:00:00 2001 From: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> Date: Sat, 5 Sep 2026 15:30:42 -0400 Subject: feat: migrate native runtime to lsfg-vk v2 --- src/components/ConfigurationSection.tsx | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) (limited to 'src/components') diff --git a/src/components/ConfigurationSection.tsx b/src/components/ConfigurationSection.tsx index a10aab2..e83bd58 100644 --- a/src/components/ConfigurationSection.tsx +++ b/src/components/ConfigurationSection.tsx @@ -3,7 +3,7 @@ import { useState, useEffect } from "react"; import { RiArrowDownSFill, RiArrowUpSFill } from "react-icons/ri"; import { ConfigurationData } from "../config/configSchema"; import { - FLOW_SCALE, NO_FP16, PERFORMANCE_MODE, HDR_MODE, + FLOW_SCALE, NO_FP16, PERFORMANCE_MODE, EXPERIMENTAL_PRESENT_MODE, DXVK_FRAME_RATE, DISABLE_STEAMDECK_MODE, MANGOHUD_WORKAROUND, DISABLE_VKBASALT, FORCE_ENABLE_VKBASALT, ENABLE_WSI, ENABLE_ZINK } from "../config/generatedConfigSchema"; @@ -149,8 +149,8 @@ export function ConfigurationSection({ onConfigChange(EXPERIMENTAL_PRESENT_MODE, value ? "fifo" : "mailbox")} /> @@ -165,14 +165,6 @@ export function ConfigurationSection({ /> - - onConfigChange(HDR_MODE, value)} - /> - )} -- cgit v1.2.3 From e64e87e9eb9e3c183ad7807dffa356f47d14e825 Mon Sep 17 00:00:00 2001 From: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> Date: Sat, 5 Sep 2026 15:33:30 -0400 Subject: fix: migrate flatpak integration to v2 --- src/components/FlatpaksModal.tsx | 40 ---------------------------------------- 1 file changed, 40 deletions(-) (limited to 'src/components') diff --git a/src/components/FlatpaksModal.tsx b/src/components/FlatpaksModal.tsx index 16d0369..479b919 100644 --- a/src/components/FlatpaksModal.tsx +++ b/src/components/FlatpaksModal.tsx @@ -172,46 +172,6 @@ export const FlatpaksModal: FC = ({ closeModal }) => { {extensionStatus && extensionStatus.success ? ( <> - {/* 23.08 Runtime */} - - : } - > - { - const operation = extensionStatus.installed_23_08 ? 'uninstall' : 'install'; - const action = () => handleExtensionOperation(operation, '23.08'); - - if (operation === 'uninstall') { - confirmOperation( - action, - t('FLATPAK_UNINSTALL_TITLE', 'Uninstall Runtime Extension'), - `${t('FLATPAK_UNINSTALL_CONFIRM_PREFIX', 'Are you sure you want to uninstall the')} 23.08 ${t('FLATPAK_UNINSTALL_CONFIRM_SUFFIX', 'runtime extension?')}` - ); - } else { - action(); - } - }} - disabled={operationInProgress === 'install-23.08' || operationInProgress === 'uninstall-23.08'} - > - {operationInProgress === 'install-23.08' || operationInProgress === 'uninstall-23.08' ? ( - - ) : extensionStatus.installed_23_08 ? ( - <> - {t('FLATPAK_UNINSTALL_BTN', 'Uninstall')} - - ) : ( - <> - {t('FLATPAK_INSTALL_BTN', 'Install')} - - )} - - - - {/* 24.08 Runtime */} Date: Sat, 5 Sep 2026 23:44:37 -0400 Subject: refactor: delegate runtime checks to lsfg-vk --- src/components/Content.tsx | 21 ++++++------ src/components/FlatpaksModal.tsx | 67 +++++++++++++++++++++++++++++++++++++++ src/components/NerdStuffModal.tsx | 55 +++++--------------------------- src/components/StatusDisplay.tsx | 21 +++++++----- 4 files changed, 99 insertions(+), 65 deletions(-) (limited to 'src/components') diff --git a/src/components/Content.tsx b/src/components/Content.tsx index 5988e79..aab2fb8 100644 --- a/src/components/Content.tsx +++ b/src/components/Content.tsx @@ -1,6 +1,6 @@ import { useEffect } from "react"; import { PanelSection, showModal, ButtonItem, PanelSectionRow } from "@decky/ui"; -import { useInstallationStatus, useDllDetection, useLsfgConfig } from "../hooks/useLsfgHooks"; +import { useInstallationStatus, useLsfgConfig } from "../hooks/useLsfgHooks"; import { useProfileManagement } from "../hooks/useProfileManagement"; import { useInstallationActions } from "../hooks/useInstallationActions"; import { StatusDisplay } from "./StatusDisplay"; @@ -21,11 +21,12 @@ export function Content() { isInstalled, installationStatus, setIsInstalled, - setInstallationStatus + setInstallationStatus, + losslessScalingInstalled, + losslessScalingStatus, + checkInstallation } = useInstallationStatus(); - const { dllDetected, dllDetectionStatus } = useDllDetection(); - const { config, loadLsfgConfig, @@ -59,11 +60,11 @@ export function Content() { }; const onInstall = () => { - handleInstall(setIsInstalled, setInstallationStatus, loadLsfgConfig); + handleInstall(setIsInstalled, setInstallationStatus, loadLsfgConfig, checkInstallation); }; const onUninstall = () => { - handleUninstall(setIsInstalled, setInstallationStatus); + handleUninstall(setIsInstalled, setInstallationStatus, checkInstallation); }; const handleShowNerdStuff = () => { @@ -87,10 +88,10 @@ export function Content() { /> )} @@ -167,10 +168,10 @@ export function Content() { {isInstalled && ( <> void; @@ -38,6 +39,7 @@ export const FlatpaksModal: FC = ({ closeModal }) => { const [flatpakApps, setFlatpakApps] = useState(null); const [loading, setLoading] = useState(true); const [operationInProgress, setOperationInProgress] = useState(null); + const [operationError, setOperationError] = useState(null); const loadData = async () => { setLoading(true); @@ -63,6 +65,7 @@ export const FlatpaksModal: FC = ({ closeModal }) => { const handleExtensionOperation = async (operation: 'install' | 'uninstall', version: string) => { const operationId = `${operation}-${version}`; setOperationInProgress(operationId); + setOperationError(null); try { const result = operation === 'install' @@ -73,9 +76,16 @@ export const FlatpaksModal: FC = ({ closeModal }) => { // Reload status after operation const newStatus = await checkFlatpakExtensionStatus(); setExtensionStatus(newStatus); + } else { + const message = result.error || result.message || 'Flatpak operation failed'; + setOperationError(message); + showErrorToast('Flatpak operation failed', message); } } catch (error) { console.error(`Error ${operation}ing extension:`, error); + const message = String(error); + setOperationError(message); + showErrorToast('Flatpak operation failed', message); } finally { setOperationInProgress(null); } @@ -85,6 +95,7 @@ export const FlatpaksModal: FC = ({ closeModal }) => { const hasOverrides = app.has_filesystem_override && app.has_env_override; const operationId = `app-${app.app_id}`; setOperationInProgress(operationId); + setOperationError(null); try { const result = hasOverrides @@ -95,9 +106,16 @@ export const FlatpaksModal: FC = ({ closeModal }) => { // Reload apps data after operation const newApps = await getFlatpakApps(); setFlatpakApps(newApps); + } else { + const message = result.error || result.message || 'Flatpak override failed'; + setOperationError(message); + showErrorToast('Flatpak override failed', message); } } catch (error) { console.error('Error toggling app override:', error); + const message = String(error); + setOperationError(message); + showErrorToast('Flatpak override failed', message); } finally { setOperationInProgress(null); } @@ -170,8 +188,57 @@ export const FlatpaksModal: FC = ({ closeModal }) => { {t('FLATPAK_RUNTIME_INSTALLER', 'Runtime Extension Installer')} + {operationError && ( + + } + /> + + )} + {extensionStatus && extensionStatus.success ? ( <> + + : } + > + { + const operation = extensionStatus.installed_23_08 ? 'uninstall' : 'install'; + const action = () => handleExtensionOperation(operation, '23.08'); + + if (operation === 'uninstall') { + confirmOperation( + action, + t('FLATPAK_UNINSTALL_TITLE', 'Uninstall Runtime Extension'), + `${t('FLATPAK_UNINSTALL_CONFIRM_PREFIX', 'Are you sure you want to uninstall the')} 23.08 ${t('FLATPAK_UNINSTALL_CONFIRM_SUFFIX', 'runtime extension?')}` + ); + } else { + action(); + } + }} + disabled={operationInProgress === 'install-23.08' || operationInProgress === 'uninstall-23.08'} + > + {operationInProgress === 'install-23.08' || operationInProgress === 'uninstall-23.08' ? ( + + ) : extensionStatus.installed_23_08 ? ( + <> + {t('FLATPAK_UNINSTALL_BTN', 'Uninstall')} + + ) : ( + <> + {t('FLATPAK_INSTALL_BTN', 'Install')} + + )} + + + + {/* 24.08 Runtime */} (null); const [configContent, setConfigContent] = useState(null); const [scriptContent, setScriptContent] = useState(null); const [loading, setLoading] = useState(true); @@ -28,13 +31,11 @@ export function NerdStuffModal({ closeModal }: NerdStuffModalProps) { setError(null); // Load all data in parallel - const [dllResult, configResult, scriptResult] = await Promise.all([ - getDllStats(), + const [configResult, scriptResult] = await Promise.all([ getConfigFileContent(), - getLaunchScriptContent() + getLaunchScriptContent(), ]); - setDllStats(dllResult); setConfigContent(configResult); setScriptContent(scriptResult); } catch (err) { @@ -47,11 +48,6 @@ export function NerdStuffModal({ closeModal }: NerdStuffModalProps) { loadData(); }, []); - const formatSHA256 = (hash: string) => { - // Format SHA256 hash for better readability (add spaces every 8 characters) - return hash.replace(/(.{8})/g, '$1 ').trim(); - }; - const copyToClipboard = async (text: string) => { try { await navigator.clipboard.writeText(text); @@ -73,41 +69,6 @@ export function NerdStuffModal({ closeModal }: NerdStuffModalProps) { {!loading && !error && ( <> - {/* DLL Stats Section */} - {dllStats && ( - <> - {!dllStats.success ? ( -
{dllStats.error || "Failed to get DLL stats"}
- ) : ( -
- - dllStats.dll_path && copyToClipboard(dllStats.dll_path)} - onActivate={() => dllStats.dll_path && copyToClipboard(dllStats.dll_path)} - > - {dllStats.dll_path || t('NERD_NOT_AVAILABLE', 'Not available')} - - - - - dllStats.dll_sha256 && copyToClipboard(dllStats.dll_sha256)} - onActivate={() => dllStats.dll_sha256 && copyToClipboard(dllStats.dll_sha256)} - > - {dllStats.dll_sha256 ? formatSHA256(dllStats.dll_sha256) : t('NERD_NOT_AVAILABLE', 'Not available')} - - - - {dllStats.dll_source && ( - -
{dllStats.dll_source}
-
- )} -
- )} - - )} - {/* Launch Script Section */} {scriptContent && ( @@ -168,7 +129,7 @@ export function NerdStuffModal({ closeModal }: NerdStuffModalProps) { )} )} - + {/* Close Button */} diff --git a/src/components/StatusDisplay.tsx b/src/components/StatusDisplay.tsx index 3a48a15..31584eb 100644 --- a/src/components/StatusDisplay.tsx +++ b/src/components/StatusDisplay.tsx @@ -1,24 +1,24 @@ import { PanelSectionRow } from "@decky/ui"; interface StatusDisplayProps { - dllDetected: boolean; - dllDetectionStatus: string; isInstalled: boolean; installationStatus: string; + losslessScalingInstalled: boolean; + losslessScalingStatus: string; } export function StatusDisplay({ - dllDetected, - dllDetectionStatus, isInstalled, - installationStatus + installationStatus, + losslessScalingInstalled, + losslessScalingStatus }: StatusDisplayProps) { return (
- {dllDetected ? "✅" : "❌"} + {losslessScalingInstalled ? "✅" : "❌"} - {dllDetectionStatus} + {losslessScalingInstalled ? "Lossless Scaling Installed" : "Lossless Scaling Not Installed"}
+ {!losslessScalingInstalled && losslessScalingStatus && ( +
+ {losslessScalingStatus} +
+ )}
Date: Sat, 5 Sep 2026 23:58:16 -0400 Subject: feat: select Lossless Scaling lsfg-vk branch --- src/components/Content.tsx | 22 ++++++++ src/components/StatusDisplay.tsx | 115 ++++++++++++++++++++++++++------------- 2 files changed, 99 insertions(+), 38 deletions(-) (limited to 'src/components') diff --git a/src/components/Content.tsx b/src/components/Content.tsx index aab2fb8..4e4e33a 100644 --- a/src/components/Content.tsx +++ b/src/components/Content.tsx @@ -15,6 +15,7 @@ import { NerdStuffModal } from "./NerdStuffModal"; import { FlatpaksModal } from "./FlatpaksModal"; import { ConfigurationData } from "../config/configSchema"; import t from '../i18n/i18n'; +import { showErrorToast, showSuccessToast } from "../utils/toastUtils"; export function Content() { const { @@ -24,6 +25,9 @@ export function Content() { setInstallationStatus, losslessScalingInstalled, losslessScalingStatus, + steamBranchStatus, + isSwitchingSteamBranch, + selectLosslessScalingBranch, checkInstallation } = useInstallationStatus(); @@ -67,6 +71,18 @@ export function Content() { handleUninstall(setIsInstalled, setInstallationStatus, checkInstallation); }; + const onSelectLosslessScalingBranch = async () => { + const result = await selectLosslessScalingBranch(); + if (result.success) { + showSuccessToast("Steam branch selected", result.message); + } else { + showErrorToast( + "Steam branch selection failed", + result.error || "Unable to select the lsfg-vk Steam branch" + ); + } + }; + const handleShowNerdStuff = () => { showModal(); }; @@ -92,6 +108,9 @@ export function Content() { installationStatus={installationStatus} losslessScalingInstalled={losslessScalingInstalled} losslessScalingStatus={losslessScalingStatus} + steamBranchStatus={steamBranchStatus} + isSwitchingSteamBranch={isSwitchingSteamBranch} + onSelectLosslessScalingBranch={onSelectLosslessScalingBranch} /> )} @@ -172,6 +191,9 @@ export function Content() { installationStatus={installationStatus} losslessScalingInstalled={losslessScalingInstalled} losslessScalingStatus={losslessScalingStatus} + steamBranchStatus={steamBranchStatus} + isSwitchingSteamBranch={isSwitchingSteamBranch} + onSelectLosslessScalingBranch={onSelectLosslessScalingBranch} /> void; } export function StatusDisplay({ isInstalled, installationStatus, losslessScalingInstalled, - losslessScalingStatus + losslessScalingStatus, + steamBranchStatus, + isSwitchingSteamBranch, + onSelectLosslessScalingBranch }: StatusDisplayProps) { return ( - -
-
- - {losslessScalingInstalled ? "✅" : "❌"} - - {losslessScalingInstalled ? "Lossless Scaling Installed" : "Lossless Scaling Not Installed"} -
- {!losslessScalingInstalled && losslessScalingStatus && ( -
- {losslessScalingStatus} + <> + +
+
+ + {losslessScalingInstalled ? "✅" : "❌"} + + {losslessScalingInstalled ? "Lossless Scaling Installed" : "Lossless Scaling Not Installed"} +
+ {!losslessScalingInstalled && losslessScalingStatus && ( +
+ {losslessScalingStatus} +
+ )} +
+ + {isInstalled ? "✅" : "❌"} + + {installationStatus}
- )} -
- - {isInstalled ? "✅" : "❌"} - - {installationStatus}
-
-
+ + + {losslessScalingInstalled && steamBranchStatus?.installed && ( + +
+
+ Steam branch: {steamBranchStatus.current_branch || "public"} + {steamBranchStatus.needs_switch && ( +
+ {steamBranchStatus.message} +
+ )} +
+ {steamBranchStatus.needs_switch && ( + + {isSwitchingSteamBranch ? "Selecting lsfg-vk..." : "Use lsfg-vk Steam branch"} + + )} +
+
+ )} + ); } -- cgit v1.2.3 From f9352694a78a2bcfd00d237398a2b30de14c70be Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Sun, 6 Sep 2026 00:06:58 -0400 Subject: fix: show Steam branch recovery action --- src/components/StatusDisplay.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) (limited to 'src/components') diff --git a/src/components/StatusDisplay.tsx b/src/components/StatusDisplay.tsx index b6290b0..5eb1823 100644 --- a/src/components/StatusDisplay.tsx +++ b/src/components/StatusDisplay.tsx @@ -20,13 +20,15 @@ export function StatusDisplay({ isSwitchingSteamBranch, onSelectLosslessScalingBranch }: StatusDisplayProps) { + const losslessScalingAppInstalled = losslessScalingInstalled || steamBranchStatus?.installed === true; + return ( <>
- {losslessScalingInstalled ? "✅" : "❌"} + {losslessScalingAppInstalled ? "✅" : "❌"} - {losslessScalingInstalled ? "Lossless Scaling Installed" : "Lossless Scaling Not Installed"} + {losslessScalingAppInstalled ? "Lossless Scaling Installed" : "Lossless Scaling Not Installed"}
- {!losslessScalingInstalled && losslessScalingStatus && ( + {!losslessScalingAppInstalled && losslessScalingStatus && (
{losslessScalingStatus}
@@ -61,7 +63,7 @@ export function StatusDisplay({
- {losslessScalingInstalled && steamBranchStatus?.installed && ( + {steamBranchStatus?.installed && (
Date: Sun, 6 Sep 2026 00:16:19 -0400 Subject: refactor: make Steam branch integration read-only --- src/components/Content.tsx | 19 ------------------- src/components/StatusDisplay.tsx | 23 ++--------------------- 2 files changed, 2 insertions(+), 40 deletions(-) (limited to 'src/components') diff --git a/src/components/Content.tsx b/src/components/Content.tsx index 4e4e33a..01f94c9 100644 --- a/src/components/Content.tsx +++ b/src/components/Content.tsx @@ -15,7 +15,6 @@ import { NerdStuffModal } from "./NerdStuffModal"; import { FlatpaksModal } from "./FlatpaksModal"; import { ConfigurationData } from "../config/configSchema"; import t from '../i18n/i18n'; -import { showErrorToast, showSuccessToast } from "../utils/toastUtils"; export function Content() { const { @@ -26,8 +25,6 @@ export function Content() { losslessScalingInstalled, losslessScalingStatus, steamBranchStatus, - isSwitchingSteamBranch, - selectLosslessScalingBranch, checkInstallation } = useInstallationStatus(); @@ -71,18 +68,6 @@ export function Content() { handleUninstall(setIsInstalled, setInstallationStatus, checkInstallation); }; - const onSelectLosslessScalingBranch = async () => { - const result = await selectLosslessScalingBranch(); - if (result.success) { - showSuccessToast("Steam branch selected", result.message); - } else { - showErrorToast( - "Steam branch selection failed", - result.error || "Unable to select the lsfg-vk Steam branch" - ); - } - }; - const handleShowNerdStuff = () => { showModal(); }; @@ -109,8 +94,6 @@ export function Content() { losslessScalingInstalled={losslessScalingInstalled} losslessScalingStatus={losslessScalingStatus} steamBranchStatus={steamBranchStatus} - isSwitchingSteamBranch={isSwitchingSteamBranch} - onSelectLosslessScalingBranch={onSelectLosslessScalingBranch} /> )} @@ -192,8 +175,6 @@ export function Content() { losslessScalingInstalled={losslessScalingInstalled} losslessScalingStatus={losslessScalingStatus} steamBranchStatus={steamBranchStatus} - isSwitchingSteamBranch={isSwitchingSteamBranch} - onSelectLosslessScalingBranch={onSelectLosslessScalingBranch} /> void; } export function StatusDisplay({ @@ -16,9 +14,7 @@ export function StatusDisplay({ installationStatus, losslessScalingInstalled, losslessScalingStatus, - steamBranchStatus, - isSwitchingSteamBranch, - onSelectLosslessScalingBranch + steamBranchStatus }: StatusDisplayProps) { const losslessScalingAppInstalled = losslessScalingInstalled || steamBranchStatus?.installed === true; @@ -36,9 +32,6 @@ export function StatusDisplay({ gap: "6px" }} > - - {losslessScalingAppInstalled ? "✅" : "❌"} - {losslessScalingAppInstalled ? "Lossless Scaling Installed" : "Lossless Scaling Not Installed"}
{!losslessScalingAppInstalled && losslessScalingStatus && ( @@ -55,9 +48,6 @@ export function StatusDisplay({ gap: "6px" }} > - - {isInstalled ? "✅" : "❌"} - {installationStatus}
@@ -80,15 +70,6 @@ export function StatusDisplay({
)}
- {steamBranchStatus.needs_switch && ( - - {isSwitchingSteamBranch ? "Selecting lsfg-vk..." : "Use lsfg-vk Steam branch"} - - )}
)} -- cgit v1.2.3 From ad2b182777bfd0a5ceef6e654df75ff13eb8b503 Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Sun, 6 Sep 2026 15:08:45 -0400 Subject: refactor: offload configuration to lsfg-vk --- src/components/ConfigurationSection.tsx | 307 ++--------------- src/components/Content.tsx | 48 +-- src/components/FpsMultiplierControl.tsx | 6 +- src/components/GameConfigurationSelector.tsx | 29 ++ src/components/ProfileManagement.tsx | 477 --------------------------- src/components/index.ts | 2 +- 6 files changed, 65 insertions(+), 804 deletions(-) create mode 100644 src/components/GameConfigurationSelector.tsx delete mode 100644 src/components/ProfileManagement.tsx (limited to 'src/components') diff --git a/src/components/ConfigurationSection.tsx b/src/components/ConfigurationSection.tsx index e83bd58..c2bba7e 100644 --- a/src/components/ConfigurationSection.tsx +++ b/src/components/ConfigurationSection.tsx @@ -1,293 +1,28 @@ -import { PanelSectionRow, ToggleField, SliderField, ButtonItem } from "@decky/ui"; -import { useState, useEffect } from "react"; -import { RiArrowDownSFill, RiArrowUpSFill } from "react-icons/ri"; +import { PanelSectionRow, ToggleField, SliderField } from "@decky/ui"; import { ConfigurationData } from "../config/configSchema"; -import { - FLOW_SCALE, NO_FP16, PERFORMANCE_MODE, - EXPERIMENTAL_PRESENT_MODE, DXVK_FRAME_RATE, DISABLE_STEAMDECK_MODE, - MANGOHUD_WORKAROUND, DISABLE_VKBASALT, FORCE_ENABLE_VKBASALT, ENABLE_WSI, ENABLE_ZINK -} from "../config/generatedConfigSchema"; -import t from '../i18n/i18n'; +import { FLOW_SCALE, PERFORMANCE_MODE, OVERRIDE_PRESENT_MODE, PRESERVE_SWAPCHAIN_IMAGE_COUNT, NO_FP16 } from "../config/configSchema"; interface ConfigurationSectionProps { config: ConfigurationData; - onConfigChange: (fieldName: keyof ConfigurationData, value: boolean | number | string) => Promise; + onConfigChange: (fieldName: keyof ConfigurationData, value: boolean | number | string | string[]) => Promise; } -const WORKAROUNDS_COLLAPSED_KEY = "lsfg-workarounds-collapsed"; -const CONFIG_COLLAPSED_KEY = "lsfg-config-collapsed"; - -export function ConfigurationSection({ - config, - onConfigChange -}: ConfigurationSectionProps) { - // Initialize with localStorage value, fallback to true if not found - const [configCollapsed, setConfigCollapsed] = useState(() => { - try { - const saved = localStorage.getItem(CONFIG_COLLAPSED_KEY); - return saved !== null ? JSON.parse(saved) : false; - } catch { - return false; - } - }); - - const [workaroundsCollapsed, setWorkaroundsCollapsed] = useState(() => { - try { - const saved = localStorage.getItem(WORKAROUNDS_COLLAPSED_KEY); - return saved !== null ? JSON.parse(saved) : true; - } catch { - return true; - } - }); - - // Persist workarounds collapse state to localStorage - useEffect(() => { - try { - localStorage.setItem(CONFIG_COLLAPSED_KEY, JSON.stringify(configCollapsed)); - } catch (error) { - console.warn("Failed to save config collapse state:", error); - } - }, [configCollapsed]); - - useEffect(() => { - try { - localStorage.setItem(WORKAROUNDS_COLLAPSED_KEY, JSON.stringify(workaroundsCollapsed)); - } catch (error) { - console.warn("Failed to save workarounds collapse state:", error); - } - }, [workaroundsCollapsed]); - - return ( - <> - - - {/* Config Section */} - -
- {t('CONFIG_SECTION_TITLE', 'Config')} -
-
- - -
- setConfigCollapsed(!configCollapsed)} - > - {configCollapsed ? ( - - ) : ( - - )} - -
-
- - {!configCollapsed && ( - <> - - onConfigChange(FLOW_SCALE, value)} - /> - - - - onConfigChange(NO_FP16, !value)} - /> - - - - 0 ? ` (${config.dxvk_frame_rate} FPS)` : ` (${t('CONFIG_BASE_FPS_CAP_OFF', 'Off')})`}`} - description={t('CONFIG_BASE_FPS_CAP_DESC', 'Base framerate cap for DirectX games, before frame multiplier. (Requires game restart to apply)')} - value={config.dxvk_frame_rate} - min={0} - max={60} - step={1} - onChange={(value) => onConfigChange(DXVK_FRAME_RATE, value)} - /> - - - - onConfigChange(EXPERIMENTAL_PRESENT_MODE, value ? "fifo" : "mailbox")} - /> - - - - onConfigChange(PERFORMANCE_MODE, value)} - /> - - - - )} - - {/* Workarounds Section */} - -
- {t('CONFIG_WORKAROUNDS_TITLE', 'Workarounds')} -
-
- - -
- setWorkaroundsCollapsed(!workaroundsCollapsed)} - > - {workaroundsCollapsed ? ( - - ) : ( - - )} - -
-
- - {!workaroundsCollapsed && ( - <> - - onConfigChange(ENABLE_WSI, value)} - /> - - - - onConfigChange('enable_wow64', value)} - /> - - - - - - - onConfigChange(MANGOHUD_WORKAROUND, value)} - /> - - - - { - if (value && config.force_enable_vkbasalt) { - // Turn off force enable when enabling disable - onConfigChange(FORCE_ENABLE_VKBASALT, false); - } - onConfigChange(DISABLE_VKBASALT, value); - }} - /> - - - - { - if (value && config.disable_vkbasalt) { - // Turn off disable when enabling force enable - onConfigChange(DISABLE_VKBASALT, false); - } - onConfigChange(FORCE_ENABLE_VKBASALT, value); - }} - /> - - - - onConfigChange(ENABLE_ZINK, value)} - /> - - - )} - - ); +export function ConfigurationSection({ config, onConfigChange }: ConfigurationSectionProps) { + return <> + + onConfigChange(FLOW_SCALE, value)} /> + + + onConfigChange(NO_FP16, !value)} /> + + + onConfigChange(PERFORMANCE_MODE, value)} /> + + + onConfigChange(OVERRIDE_PRESENT_MODE, value)} /> + + + onConfigChange(PRESERVE_SWAPCHAIN_IMAGE_COUNT, value)} /> + + ; } diff --git a/src/components/Content.tsx b/src/components/Content.tsx index 01f94c9..bdb3a04 100644 --- a/src/components/Content.tsx +++ b/src/components/Content.tsx @@ -1,12 +1,12 @@ import { useEffect } from "react"; import { PanelSection, showModal, ButtonItem, PanelSectionRow } from "@decky/ui"; -import { useInstallationStatus, useLsfgConfig } from "../hooks/useLsfgHooks"; -import { useProfileManagement } from "../hooks/useProfileManagement"; +import { useInstallationStatus } from "../hooks/useLsfgHooks"; +import { useGameConfiguration } from "../hooks/useGameConfiguration"; import { useInstallationActions } from "../hooks/useInstallationActions"; import { StatusDisplay } from "./StatusDisplay"; import { InstallationButton } from "./InstallationButton"; import { ConfigurationSection } from "./ConfigurationSection"; -import { ProfileManagement } from "./ProfileManagement"; +import { GameConfigurationSelector } from "./GameConfigurationSelector"; import { UsageInstructions } from "./UsageInstructions"; import { SmartClipboardButton } from "./SmartClipboardButton"; import { FgmodClipboardButton } from "./FgmodClipboardButton"; @@ -28,40 +28,20 @@ export function Content() { checkInstallation } = useInstallationStatus(); - const { - config, - loadLsfgConfig, - updateField - } = useLsfgConfig(); - - const { - currentProfile, - updateProfileConfig, - loadProfiles - } = useProfileManagement(); + const { config, targets, runningGame, selectedAppId, setSelectedAppId, save, resetSelected, resetAll, reload } = useGameConfiguration(); const { isInstalling, isUninstalling, handleInstall, handleUninstall } = useInstallationActions(); useEffect(() => { - if (isInstalled) { - loadLsfgConfig(); - } - }, [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); - } + if (isInstalled) void reload(); + }, [isInstalled, reload]); + + const handleConfigChange = async (fieldName: keyof ConfigurationData, value: boolean | number | string | string[]) => { + await save({ ...config, [fieldName]: value }); }; const onInstall = () => { - handleInstall(setIsInstalled, setInstallationStatus, loadLsfgConfig, checkInstallation); + handleInstall(setIsInstalled, setInstallationStatus, reload, checkInstallation); }; const onUninstall = () => { @@ -124,13 +104,7 @@ export function Content() { )} {isInstalled && ( - { - await loadProfiles(); - await loadLsfgConfig(); - }} - /> + )} {isInstalled && ( diff --git a/src/components/FpsMultiplierControl.tsx b/src/components/FpsMultiplierControl.tsx index 206643e..5069c9a 100644 --- a/src/components/FpsMultiplierControl.tsx +++ b/src/components/FpsMultiplierControl.tsx @@ -1,11 +1,11 @@ import { PanelSectionRow, DialogButton, Focusable } from "@decky/ui"; import { ConfigurationData } from "../config/configSchema"; import { MULTIPLIER } from "../config/generatedConfigSchema"; -import t from '../i18n/i18n'; +import t from "../i18n/i18n"; interface FpsMultiplierControlProps { config: ConfigurationData; - onConfigChange: (fieldName: keyof ConfigurationData, value: boolean | number | string) => Promise; + onConfigChange: (fieldName: keyof ConfigurationData, value: boolean | number | string | string[]) => Promise; } export function FpsMultiplierControl({ @@ -50,7 +50,7 @@ export function FpsMultiplierControl({ textAlign: "center" }} > - {config.multiplier < 2 ? t('MULTIPLIER_OFF', 'OFF') : `${config.multiplier}X`} + {config.multiplier === 1 ? t('MULTIPLIER_OFF', 'OFF') : `${config.multiplier}X`} void; + onReset: () => Promise; + onResetAll: () => Promise; +} + +export function GameConfigurationSelector({ targets, runningGame, selectedAppId, onSelect, onReset, onResetAll }: Props) { + const options: DropdownOption[] = [ + { data: "", label: runningGame ? `Default (editing template) · ${runningGame.name}` : "Default" }, + ...targets.map((target) => ({ data: target.appid, label: `${target.name} · ${target.appid}` })), + ]; + return <> + + onSelect(String(option.data))} /> + + + void onReset()} disabled={!selectedAppId}>Reset selected game + + + void onResetAll()} disabled={!targets.some((target) => target.configured)}>Reset all game profiles + + ; +} diff --git a/src/components/ProfileManagement.tsx b/src/components/ProfileManagement.tsx deleted file mode 100644 index 626d54c..0000000 --- a/src/components/ProfileManagement.tsx +++ /dev/null @@ -1,477 +0,0 @@ -import { useState, useEffect } from "react"; -import { - PanelSectionRow, - Dropdown, - DropdownOption, - showModal, - ConfirmModal, - Field, - DialogButton, - ButtonItem, - ModalRoot, - TextField, - Focusable, - AppOverview, - Router -} from "@decky/ui"; -import { RiArrowDownSFill, RiArrowUpSFill, RiEditLine, RiDeleteBinLine } from "react-icons/ri"; -import { - getProfiles, - createProfile, - deleteProfile, - renameProfile, - setCurrentProfile, - ProfilesResult, - ProfileResult -} from "../api/lsfgApi"; -import { showSuccessToast, showErrorToast } from "../utils/toastUtils"; -import t from '../i18n/i18n'; - -const PROFILES_COLLAPSED_KEY = 'lsfg-profiles-collapsed'; - -interface TextInputModalProps { - title: string; - description: string; - defaultValue?: string; - okText?: string; - cancelText?: string; - onOK: (value: string) => void; - closeModal?: () => void; -} - -function TextInputModal({ - title, - description, - defaultValue = "", - okText = "OK", - cancelText = "Cancel", - onOK, - closeModal -}: TextInputModalProps) { - const [value, setValue] = useState(defaultValue); - - const handleOK = () => { - if (value.trim()) { - onOK(value); - closeModal?.(); - } - }; - - return ( - -
-

{title}

-

{description}

- -
- - setValue(e?.target?.value || "")} - style={{ width: "100%" }} - /> - -
- - - - {cancelText} - - - {okText} - - -
-
- ); -} - -interface ProfileManagementProps { - currentProfile?: string; - onProfileChange?: (profileName: string) => void; -} - -export function ProfileManagement({ currentProfile, onProfileChange }: ProfileManagementProps) { - const [profiles, setProfiles] = useState([]); - const [selectedProfile, setSelectedProfile] = useState(currentProfile || "decky-lsfg-vk"); - const [isLoading, setIsLoading] = useState(false); - const [mainRunningApp, setMainRunningApp] = useState(undefined); - - // Initialize with localStorage value, fallback to false (expanded) if not found - const [profilesCollapsed, setProfilesCollapsed] = useState(() => { - try { - const saved = localStorage.getItem(PROFILES_COLLAPSED_KEY); - return saved !== null ? JSON.parse(saved) : false; - } catch { - return false; - } - }); - - // Persist profiles collapse state to localStorage - useEffect(() => { - try { - localStorage.setItem(PROFILES_COLLAPSED_KEY, JSON.stringify(profilesCollapsed)); - } catch (error) { - console.warn('Failed to save profiles collapse state:', error); - } - }, [profilesCollapsed]); - - // Load profiles on component mount - useEffect(() => { - loadProfiles(); - }, []); - - // Update selected profile when prop changes - useEffect(() => { - if (currentProfile) { - setSelectedProfile(currentProfile); - } - }, [currentProfile]); - - // Poll for running app every 2 seconds - useEffect(() => { - const checkRunningApp = () => { - setMainRunningApp(Router.MainRunningApp); - }; - - // Check immediately - checkRunningApp(); - - // Set up polling interval - const interval = setInterval(checkRunningApp, 2000); - - // Cleanup interval on unmount - return () => clearInterval(interval); - }, []); - - const loadProfiles = async () => { - try { - const result: ProfilesResult = await getProfiles(); - if (result.success && result.profiles) { - setProfiles(result.profiles); - if (result.current_profile) { - setSelectedProfile(result.current_profile); - } - } else { - console.error("Failed to load profiles:", result.error); - showErrorToast("Failed to load profiles", result.error || "Unknown error"); - } - } catch (error) { - console.error("Error loading profiles:", error); - showErrorToast("Error loading profiles", String(error)); - } - }; - - const handleProfileChange = async (profileName: string) => { - setIsLoading(true); - try { - const result: ProfileResult = await setCurrentProfile(profileName); - if (result.success) { - setSelectedProfile(profileName); - showSuccessToast("Profile switched", `Switched to profile: ${profileName}`); - onProfileChange?.(profileName); - } else { - console.error("Failed to switch profile:", result.error); - showErrorToast("Failed to switch profile", result.error || "Unknown error"); - } - } catch (error) { - console.error("Error switching profile:", error); - showErrorToast("Error switching profile", String(error)); - } finally { - setIsLoading(false); - } - }; - - const handleCreateProfile = () => { - showModal( - { - if (name.trim()) { - createNewProfile(name.trim()); - } - }} - /> - ); - }; - - const createNewProfile = async (profileName: string) => { - setIsLoading(true); - try { - const result: ProfileResult = await createProfile(profileName, selectedProfile); - if (result.success) { - // Use the normalized name returned from backend (spaces converted to dashes) - const actualProfileName = result.profile_name || profileName; - showSuccessToast("Profile created", `Created profile: ${actualProfileName}`); - await loadProfiles(); - // Automatically switch to the newly created profile using the normalized name - await handleProfileChange(actualProfileName); - } else { - console.error("Failed to create profile:", result.error); - showErrorToast("Failed to create profile", result.error || "Unknown error"); - } - } catch (error) { - console.error("Error creating profile:", error); - showErrorToast("Error creating profile", String(error)); - } finally { - setIsLoading(false); - } - }; - - const handleDeleteProfile = () => { - if (selectedProfile === "decky-lsfg-vk") { - showErrorToast(t('PROFILE_CANNOT_DELETE_TITLE', 'Cannot delete default profile'), t('PROFILE_CANNOT_DELETE_MSG', 'The default profile cannot be deleted')); - return; - } - - showModal( - deleteSelectedProfile()} - /> - ); - }; - - const deleteSelectedProfile = async () => { - setIsLoading(true); - try { - const result: ProfileResult = await deleteProfile(selectedProfile); - if (result.success) { - showSuccessToast("Profile deleted", `Deleted profile: ${selectedProfile}`); - await loadProfiles(); - // If we deleted the current profile, it should have switched to default - setSelectedProfile("decky-lsfg-vk"); - onProfileChange?.("decky-lsfg-vk"); - } else { - console.error("Failed to delete profile:", result.error); - showErrorToast("Failed to delete profile", result.error || "Unknown error"); - } - } catch (error) { - console.error("Error deleting profile:", error); - showErrorToast("Error deleting profile", String(error)); - } finally { - setIsLoading(false); - } - }; - - const handleDropdownChange = (option: DropdownOption) => { - if (option.data === "__NEW_PROFILE__") { - handleCreateProfile(); - } else { - handleProfileChange(option.data); - } - }; - - const handleRenameProfile = () => { - if (selectedProfile === "decky-lsfg-vk") { - showErrorToast(t('PROFILE_CANNOT_RENAME_TITLE', 'Cannot rename default profile'), t('PROFILE_CANNOT_RENAME_MSG', 'The default profile cannot be renamed')); - return; - } - - showModal( - { - if (newName.trim() && newName.trim() !== selectedProfile) { - renameSelectedProfile(newName.trim()); - } - }} - /> - ); - }; - - const renameSelectedProfile = async (newName: string) => { - setIsLoading(true); - try { - const result: ProfileResult = await renameProfile(selectedProfile, newName); - if (result.success) { - // Use the normalized name returned from backend (spaces converted to dashes) - const actualNewName = result.profile_name || newName; - showSuccessToast("Profile renamed", `Renamed profile to: ${actualNewName}`); - await loadProfiles(); - setSelectedProfile(actualNewName); - onProfileChange?.(actualNewName); - } else { - console.error("Failed to rename profile:", result.error); - showErrorToast("Failed to rename profile", result.error || "Unknown error"); - } - } catch (error) { - console.error("Error renaming profile:", error); - showErrorToast("Error renaming profile", String(error)); - } finally { - setIsLoading(false); - } - }; - - const profileOptions: DropdownOption[] = [ - ...profiles.map((profile: string) => ({ - data: profile, - label: profile === "decky-lsfg-vk" ? t('PROFILE_DEFAULT', 'Default') : profile - })), - { - data: "__NEW_PROFILE__", - label: t('PROFILE_NEW', 'New Profile') - } - ]; - - return ( - <> - - - {/* Display currently running game info - always visible */} - {mainRunningApp && ( - -
- {mainRunningApp.display_name} running. {t('PROFILE_CLOSE_GAME', 'Close game to change profile.')} -
-
- )} - - -
- {t('PROFILE_SECTION_TITLE', 'Profile:')} {selectedProfile === "decky-lsfg-vk" ? t('PROFILE_DEFAULT', 'Default') : selectedProfile} -
-
- - -
- setProfilesCollapsed(!profilesCollapsed)} - > - {profilesCollapsed ? ( - - ) : ( - - )} - -
-
- - {!profilesCollapsed && ( - <> - - - - - - - - - - - - - - - - - - - )} - - ); -} diff --git a/src/components/index.ts b/src/components/index.ts index bec45ae..4284aee 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -8,4 +8,4 @@ export { SmartClipboardButton } from "./SmartClipboardButton"; export { FgmodClipboardButton } from "./FgmodClipboardButton"; export { NerdStuffModal } from "./NerdStuffModal"; export { FlatpaksModal } from "./FlatpaksModal"; -export { ProfileManagement } from "./ProfileManagement"; +export { GameConfigurationSelector } from "./GameConfigurationSelector"; -- cgit v1.2.3 From c9be32287ad5b72fcd86b10fa726d09dbd97d7fd Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Sun, 6 Sep 2026 17:09:54 -0400 Subject: refactor: organize plugin into native tabs --- src/components/ConfigFileTab.tsx | 46 +++ src/components/ConfigurationTab.tsx | 54 ++++ src/components/Content.tsx | 223 ++++++------- src/components/FlatpaksModal.tsx | 460 --------------------------- src/components/FlatpaksTab.tsx | 195 ++++++++++++ src/components/GameConfigurationSelector.tsx | 2 +- src/components/NerdStuffModal.tsx | 148 --------- src/components/SetupTab.tsx | 47 +++ src/components/SmartClipboardButton.tsx | 91 ------ src/components/UsageInstructions.tsx | 69 ---- src/components/index.ts | 8 +- 11 files changed, 438 insertions(+), 905 deletions(-) create mode 100644 src/components/ConfigFileTab.tsx create mode 100644 src/components/ConfigurationTab.tsx delete mode 100644 src/components/FlatpaksModal.tsx create mode 100644 src/components/FlatpaksTab.tsx delete mode 100644 src/components/NerdStuffModal.tsx create mode 100644 src/components/SetupTab.tsx delete mode 100644 src/components/SmartClipboardButton.tsx delete mode 100644 src/components/UsageInstructions.tsx (limited to 'src/components') diff --git a/src/components/ConfigFileTab.tsx b/src/components/ConfigFileTab.tsx new file mode 100644 index 0000000..60c5c7e --- /dev/null +++ b/src/components/ConfigFileTab.tsx @@ -0,0 +1,46 @@ +import { useEffect, useState } from "react"; +import { Field, Focusable, PanelSection, PanelSectionRow, Spinner } from "@decky/ui"; +import { getConfigFileContent, FileContentResult } from "../api/lsfgApi"; +import t from "../i18n/i18n"; + +export function ConfigFileTab() { + const [result, setResult] = useState(null); + + const copy = async (content: string) => { + try { + await navigator.clipboard.writeText(content); + } catch { + // Clipboard access is unavailable in some Deck UI contexts. + } + }; + + useEffect(() => { + getConfigFileContent().then(setResult).catch((error) => { + setResult({ success: false, error: String(error) }); + }); + }, []); + + if (!result) { + return ( + + + + ); + } + + return ( + + + + {result.success && result.content && ( + void copy(result.content || "")}> +
+                {result.content}
+              
+
+ )} +
+
+
+ ); +} diff --git a/src/components/ConfigurationTab.tsx b/src/components/ConfigurationTab.tsx new file mode 100644 index 0000000..6f3f474 --- /dev/null +++ b/src/components/ConfigurationTab.tsx @@ -0,0 +1,54 @@ +import { PanelSection } from "@decky/ui"; +import { ConfigurationData } from "../config/configSchema"; +import { GameTarget } from "../hooks/useGameConfiguration"; +import { ConfigurationSection } from "./ConfigurationSection"; +import { FgmodClipboardButton } from "./FgmodClipboardButton"; +import { FpsMultiplierControl } from "./FpsMultiplierControl"; +import { GameConfigurationSelector } from "./GameConfigurationSelector"; +import t from "../i18n/i18n"; + +interface ConfigurationTabProps { + config: ConfigurationData; + targets: GameTarget[]; + runningGame: GameTarget | null; + selectedAppId: string; + onSelect: (appid: string) => void; + onConfigChange: (fieldName: keyof ConfigurationData, value: boolean | number | string | string[]) => Promise; + onReset: () => Promise; + onResetAll: () => Promise; +} + +export function ConfigurationTab({ + config, + targets, + runningGame, + selectedAppId, + onSelect, + onConfigChange, + onReset, + onResetAll, +}: ConfigurationTabProps) { + return ( + <> + + + + + + + + + + + + + + ); +} diff --git a/src/components/Content.tsx b/src/components/Content.tsx index bdb3a04..de8f996 100644 --- a/src/components/Content.tsx +++ b/src/components/Content.tsx @@ -1,20 +1,22 @@ -import { useEffect } from "react"; -import { PanelSection, showModal, ButtonItem, PanelSectionRow } from "@decky/ui"; -import { useInstallationStatus } from "../hooks/useLsfgHooks"; +import { Tabs } from "@decky/ui"; +import { useEffect, useState } from "react"; +import { FaFileAlt, FaGamepad, FaLayerGroup, FaTools } from "react-icons/fa"; +import { ConfigurationData } from "../config/configSchema"; +import { tabStyles } from "../styles"; import { useGameConfiguration } from "../hooks/useGameConfiguration"; import { useInstallationActions } from "../hooks/useInstallationActions"; -import { StatusDisplay } from "./StatusDisplay"; -import { InstallationButton } from "./InstallationButton"; -import { ConfigurationSection } from "./ConfigurationSection"; -import { GameConfigurationSelector } from "./GameConfigurationSelector"; -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 { ConfigurationData } from "../config/configSchema"; -import t from '../i18n/i18n'; +import { useInstallationStatus } from "../hooks/useLsfgHooks"; +import { ConfigFileTab } from "./ConfigFileTab"; +import { ConfigurationTab } from "./ConfigurationTab"; +import { FlatpaksTab } from "./FlatpaksTab"; +import { SetupTab } from "./SetupTab"; + +const tabIcons = { + configuration: , + flatpak: , + configFile: , + setup: , +}; export function Content() { const { @@ -25,141 +27,98 @@ export function Content() { losslessScalingInstalled, losslessScalingStatus, steamBranchStatus, - checkInstallation + checkInstallation, } = useInstallationStatus(); - - const { config, targets, runningGame, selectedAppId, setSelectedAppId, save, resetSelected, resetAll, reload } = useGameConfiguration(); - + const { + config, + targets, + runningGame, + selectedAppId, + setSelectedAppId, + save, + resetSelected, + resetAll, + reload, + } = useGameConfiguration(); const { isInstalling, isUninstalling, handleInstall, handleUninstall } = useInstallationActions(); + const [tab, setTab] = useState("Setup"); + const setupComplete = + isInstalled && + losslessScalingInstalled && + steamBranchStatus?.success === true && + steamBranchStatus.installed && + !steamBranchStatus.needs_switch; + + useEffect(() => { + setTab(setupComplete ? "Configuration" : "Setup"); + }, [setupComplete]); useEffect(() => { if (isInstalled) void reload(); }, [isInstalled, reload]); - const handleConfigChange = async (fieldName: keyof ConfigurationData, value: boolean | number | string | string[]) => { + const handleConfigChange = async ( + fieldName: keyof ConfigurationData, + value: boolean | number | string | string[], + ) => { await save({ ...config, [fieldName]: value }); }; const onInstall = () => { - handleInstall(setIsInstalled, setInstallationStatus, reload, checkInstallation); + void handleInstall(setIsInstalled, setInstallationStatus, reload, checkInstallation); }; const onUninstall = () => { - handleUninstall(setIsInstalled, setInstallationStatus, checkInstallation); + void handleUninstall(setIsInstalled, setInstallationStatus, checkInstallation); }; - const handleShowNerdStuff = () => { - showModal(); - }; + const setupContent = ( + + ); - const handleShowFlatpaks = () => { - showModal(); - }; + const tabs = setupComplete + ? [ + { + id: "Configuration", + title: tabIcons.configuration, + content: ( + + ), + }, + { id: "Flatpak", title: tabIcons.flatpak, content: }, + { id: "ConfigFile", title: tabIcons.configFile, content: }, + { id: "Setup", title: tabIcons.setup, content: setupContent }, + ] + : [ + { id: "Setup", title: tabIcons.setup, content: setupContent }, + ]; return ( - - {!isInstalled && ( - <> - - - - - )} - - {isInstalled && ( - <> - -
- {t('CONTENT_FPS_MULTIPLIER', 'FPS Multiplier')} -
-
- - - - )} - - {isInstalled && ( - - )} - - {isInstalled && ( - - )} - - {isInstalled && ( - <> - - - - )} - - - - - - {t('CONTENT_NERD_STUFF', 'Nerd Stuff')} - - - - - - {t('CONTENT_FLATPAK_SETUP', 'Flatpak Setup')} - - - - {isInstalled && ( - <> - - - - - )} -
+
+ + +
); } diff --git a/src/components/FlatpaksModal.tsx b/src/components/FlatpaksModal.tsx deleted file mode 100644 index 8245160..0000000 --- a/src/components/FlatpaksModal.tsx +++ /dev/null @@ -1,460 +0,0 @@ -import { FC, useState, useEffect, CSSProperties } from 'react'; -import { - ModalRoot, - DialogBody, - DialogHeader, - DialogControlsSection, - DialogControlsSectionHeader, - ButtonItem, - PanelSectionRow, - Field, - Toggle, - Spinner, - Focusable, - showModal, - ConfirmModal -} from '@decky/ui'; -import { FaCheck, FaTimes, FaDownload, FaTrash, FaCog } from 'react-icons/fa'; -import flatpakTargetImage from '../../assets/flatpak-target.png'; -import { - checkFlatpakExtensionStatus, - installFlatpakExtension, - uninstallFlatpakExtension, - getFlatpakApps, - setFlatpakAppOverride, - removeFlatpakAppOverride, - FlatpakExtensionStatus, - FlatpakApp, - FlatpakAppInfo -} from '../api/lsfgApi'; -import t from '../i18n/i18n'; -import { showErrorToast } from '../utils/toastUtils'; - -interface FlatpaksModalProps { - closeModal?: () => void; -} - -export const FlatpaksModal: FC = ({ closeModal }) => { - const [extensionStatus, setExtensionStatus] = useState(null); - const [flatpakApps, setFlatpakApps] = useState(null); - const [loading, setLoading] = useState(true); - const [operationInProgress, setOperationInProgress] = useState(null); - const [operationError, setOperationError] = useState(null); - - const loadData = async () => { - setLoading(true); - try { - const [statusResult, appsResult] = await Promise.all([ - checkFlatpakExtensionStatus(), - getFlatpakApps() - ]); - - setExtensionStatus(statusResult); - setFlatpakApps(appsResult); - } catch (error) { - console.error('Error loading Flatpak data:', error); - } finally { - setLoading(false); - } - }; - - useEffect(() => { - loadData(); - }, []); - - const handleExtensionOperation = async (operation: 'install' | 'uninstall', version: string) => { - const operationId = `${operation}-${version}`; - setOperationInProgress(operationId); - setOperationError(null); - - try { - const result = operation === 'install' - ? await installFlatpakExtension(version) - : await uninstallFlatpakExtension(version); - - if (result.success) { - // Reload status after operation - const newStatus = await checkFlatpakExtensionStatus(); - setExtensionStatus(newStatus); - } else { - const message = result.error || result.message || 'Flatpak operation failed'; - setOperationError(message); - showErrorToast('Flatpak operation failed', message); - } - } catch (error) { - console.error(`Error ${operation}ing extension:`, error); - const message = String(error); - setOperationError(message); - showErrorToast('Flatpak operation failed', message); - } finally { - setOperationInProgress(null); - } - }; - - const handleAppOverrideToggle = async (app: FlatpakApp) => { - const hasOverrides = app.has_filesystem_override && app.has_env_override; - const operationId = `app-${app.app_id}`; - setOperationInProgress(operationId); - setOperationError(null); - - try { - const result = hasOverrides - ? await removeFlatpakAppOverride(app.app_id) - : await setFlatpakAppOverride(app.app_id); - - if (result.success) { - // Reload apps data after operation - const newApps = await getFlatpakApps(); - setFlatpakApps(newApps); - } else { - const message = result.error || result.message || 'Flatpak override failed'; - setOperationError(message); - showErrorToast('Flatpak override failed', message); - } - } catch (error) { - console.error('Error toggling app override:', error); - const message = String(error); - setOperationError(message); - showErrorToast('Flatpak override failed', message); - } finally { - setOperationInProgress(null); - } - }; - - const confirmOperation = (operation: () => void, title: string, description: string) => { - showModal( - {}} - /> - ); - }; - - if (loading) { - return ( - - {t('FLATPAK_MODAL_TITLE', 'Flatpak Extensions')} - -
- -
-
-
- ); - } - - const instructionSteps = [ - { - id: 'try-first', - title: t('FLATPAK_STEP_TRY_FIRST', 'Try first:'), - command: '~/lsfg' - }, - { - id: 'try-full-path', - title: t('FLATPAK_STEP_TRY_FULL_PATH', "If that doesn't work, try full path:"), - command: '/home/(username)/lsfg' - }, - { - id: 'final-result', - title: t('FLATPAK_STEP_FINAL', 'Final result should look like:'), - command: '~/lsfg "usr/bin/flatpak"' - } - ]; - - const focusableInstructionStyle: CSSProperties = { - padding: '10px', - background: 'rgba(0, 0, 0, 0.3)', - borderRadius: '6px', - marginBottom: '12px' - }; - - const commandStyle: CSSProperties = { - fontFamily: 'monospace', - fontSize: '0.85em', - background: 'rgba(0, 0, 0, 0.45)', - padding: '8px', - borderRadius: '4px', - marginTop: '6px' - }; - - return ( - - {t('FLATPAK_MODAL_TITLE', 'Flatpak Extensions')} - - - {/* Extension Status Section */} - - {t('FLATPAK_RUNTIME_INSTALLER', 'Runtime Extension Installer')} - - {operationError && ( - - } - /> - - )} - - {extensionStatus && extensionStatus.success ? ( - <> - - : } - > - { - const operation = extensionStatus.installed_23_08 ? 'uninstall' : 'install'; - const action = () => handleExtensionOperation(operation, '23.08'); - - if (operation === 'uninstall') { - confirmOperation( - action, - t('FLATPAK_UNINSTALL_TITLE', 'Uninstall Runtime Extension'), - `${t('FLATPAK_UNINSTALL_CONFIRM_PREFIX', 'Are you sure you want to uninstall the')} 23.08 ${t('FLATPAK_UNINSTALL_CONFIRM_SUFFIX', 'runtime extension?')}` - ); - } else { - action(); - } - }} - disabled={operationInProgress === 'install-23.08' || operationInProgress === 'uninstall-23.08'} - > - {operationInProgress === 'install-23.08' || operationInProgress === 'uninstall-23.08' ? ( - - ) : extensionStatus.installed_23_08 ? ( - <> - {t('FLATPAK_UNINSTALL_BTN', 'Uninstall')} - - ) : ( - <> - {t('FLATPAK_INSTALL_BTN', 'Install')} - - )} - - - - - {/* 24.08 Runtime */} - - : } - > - { - const operation = extensionStatus.installed_24_08 ? 'uninstall' : 'install'; - const action = () => handleExtensionOperation(operation, '24.08'); - - if (operation === 'uninstall') { - confirmOperation( - action, - t('FLATPAK_UNINSTALL_TITLE', 'Uninstall Runtime Extension'), - `${t('FLATPAK_UNINSTALL_CONFIRM_PREFIX', 'Are you sure you want to uninstall the')} 24.08 ${t('FLATPAK_UNINSTALL_CONFIRM_SUFFIX', 'runtime extension?')}` - ); - } else { - action(); - } - }} - disabled={operationInProgress === 'install-24.08' || operationInProgress === 'uninstall-24.08'} - > - {operationInProgress === 'install-24.08' || operationInProgress === 'uninstall-24.08' ? ( - - ) : extensionStatus.installed_24_08 ? ( - <> - {t('FLATPAK_UNINSTALL_BTN', 'Uninstall')} - - ) : ( - <> - {t('FLATPAK_INSTALL_BTN', 'Install')} - - )} - - - - - {/* 25.08 Runtime */} - - : } - > - { - const operation = extensionStatus.installed_25_08 ? 'uninstall' : 'install'; - const action = () => handleExtensionOperation(operation, '25.08'); - - if (operation === 'uninstall') { - confirmOperation( - action, - t('FLATPAK_UNINSTALL_TITLE', 'Uninstall Runtime Extension'), - `${t('FLATPAK_UNINSTALL_CONFIRM_PREFIX', 'Are you sure you want to uninstall the')} 25.08 ${t('FLATPAK_UNINSTALL_CONFIRM_SUFFIX', 'runtime extension?')}` - ); - } else { - action(); - } - }} - disabled={operationInProgress === 'install-25.08' || operationInProgress === 'uninstall-25.08'} - > - {operationInProgress === 'install-25.08' || operationInProgress === 'uninstall-25.08' ? ( - - ) : extensionStatus.installed_25_08 ? ( - <> - {t('FLATPAK_UNINSTALL_BTN', 'Uninstall')} - - ) : ( - <> - {t('FLATPAK_INSTALL_BTN', 'Install')} - - )} - - - - - ) : ( - - } - /> - - )} - - - {/* Flatpak Apps Section */} - - {t('FLATPAK_APPS_TITLE', 'Flatpak Applications')} - - {flatpakApps && flatpakApps.success ? ( - flatpakApps.apps.length > 0 ? ( - flatpakApps.apps.map((app) => { - const hasOverrides = app.has_filesystem_override && app.has_env_override; - const partialOverrides = app.has_filesystem_override || app.has_env_override; - - let statusColor = 'red'; - let statusText = t('FLATPAK_STATUS_NO_OVERRIDES', 'No overrides'); - - if (hasOverrides) { - statusColor = 'green'; - statusText = t('FLATPAK_STATUS_CONFIGURED', 'Configured'); - } else if (partialOverrides) { - statusColor = 'orange'; - statusText = t('FLATPAK_STATUS_PARTIAL', 'Partial'); - } - - return ( - - } - > - handleAppOverrideToggle(app)} - disabled={operationInProgress === `app-${app.app_id}`} - /> - - - ); - }) - ) : ( - - - - ) - ) : ( - - } - /> - - )} - - - {/* Steam Configuration Instructions */} - - {t('FLATPAK_STEAM_CONFIG_TITLE', 'Steam Configuration')} -
-
- {t('FLATPAK_STEAM_CONFIG_HEADER', 'Configure Steam Flatpak Shortcuts')} -
-
- {t('FLATPAK_STEAM_CONFIG_DESC', 'In Steam, open your flatpak game and click the cog wheel.')} -
-
- IMPORTANT: {t('FLATPAK_STEAM_CONFIG_IMPORTANT', 'Set this in TARGET (NOT LAUNCH OPTIONS)')} -
- - {instructionSteps.map((step) => ( - {}} - style={focusableInstructionStyle} - > -
{step.title}
-
{step.command}
-
- ))} - - {}} - style={{ marginTop: '4px' }} - > -
- Steam Properties Target Field Example -
-
-
-
- - {/* Close Button */} - - - - {t('FLATPAK_CLOSE', 'Close')} - - - -
-
-
- ); -}; diff --git a/src/components/FlatpaksTab.tsx b/src/components/FlatpaksTab.tsx new file mode 100644 index 0000000..dc73858 --- /dev/null +++ b/src/components/FlatpaksTab.tsx @@ -0,0 +1,195 @@ +import { useEffect, useState } from "react"; +import { + ButtonItem, + ConfirmModal, + Field, + PanelSection, + PanelSectionRow, + Spinner, + Toggle, + showModal, +} from "@decky/ui"; +import { FaCheck, FaCog, FaDownload, FaTimes, FaTrash } from "react-icons/fa"; +import { + checkFlatpakExtensionStatus, + FlatpakApp, + FlatpakAppInfo, + FlatpakExtensionStatus, + getFlatpakApps, + installFlatpakExtension, + removeFlatpakAppOverride, + setFlatpakAppOverride, + uninstallFlatpakExtension, +} from "../api/lsfgApi"; +import { showErrorToast } from "../utils/toastUtils"; +import t from "../i18n/i18n"; + +const runtimeVersions = [ + { version: "23.08", key: "installed_23_08" }, + { version: "24.08", key: "installed_24_08" }, + { version: "25.08", key: "installed_25_08" }, +] as const; + +interface RuntimeRowProps { + version: string; + installed: boolean; + busy: boolean; + onAction: () => void; +} + +function RuntimeRow({ version, installed, busy, onAction }: RuntimeRowProps) { + return ( + + : } + > + + {busy ? : installed ? <> {t("FLATPAK_UNINSTALL_BTN", "Uninstall")} : <> {t("FLATPAK_INSTALL_BTN", "Install")}} + + + + ); +} + +interface AppRowProps { + app: FlatpakApp; + busy: boolean; + onToggle: () => void; +} + +function AppRow({ app, busy, onToggle }: AppRowProps) { + const configured = app.has_filesystem_override && app.has_env_override; + const partial = app.has_filesystem_override || app.has_env_override; + const status = configured + ? t("FLATPAK_STATUS_CONFIGURED", "Configured") + : partial + ? t("FLATPAK_STATUS_PARTIAL", "Partial") + : t("FLATPAK_STATUS_NO_OVERRIDES", "No overrides"); + + return ( + + } + > + + + + ); +} + +export function FlatpaksTab() { + const [extensionStatus, setExtensionStatus] = useState(null); + const [apps, setApps] = useState(null); + const [loading, setLoading] = useState(true); + const [operation, setOperation] = useState(null); + const [error, setError] = useState(null); + + const load = async () => { + setLoading(true); + try { + const [nextStatus, nextApps] = await Promise.all([ + checkFlatpakExtensionStatus(), + getFlatpakApps(), + ]); + setExtensionStatus(nextStatus); + setApps(nextApps); + } catch (loadError) { + setError(String(loadError)); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + void load(); + }, []); + + const runExtensionOperation = async (version: string, installed: boolean) => { + const action = installed ? "uninstall" : "install"; + setOperation(`${action}-${version}`); + setError(null); + try { + const result = installed + ? await uninstallFlatpakExtension(version) + : await installFlatpakExtension(version); + if (!result.success) throw new Error(result.error || result.message); + setExtensionStatus(await checkFlatpakExtensionStatus()); + } catch (operationError) { + const message = String(operationError); + setError(message); + showErrorToast("Flatpak operation failed", message); + } finally { + setOperation(null); + } + }; + + const confirmExtensionOperation = (version: string, installed: boolean) => { + if (!installed) { + void runExtensionOperation(version, false); + return; + } + showModal( + void runExtensionOperation(version, true)} + onCancel={() => {}} + />, + ); + }; + + const toggleApp = async (app: FlatpakApp) => { + const configured = app.has_filesystem_override && app.has_env_override; + setOperation(`app-${app.app_id}`); + setError(null); + try { + const result = configured + ? await removeFlatpakAppOverride(app.app_id) + : await setFlatpakAppOverride(app.app_id); + if (!result.success) throw new Error(result.error || result.message); + setApps(await getFlatpakApps()); + } catch (operationError) { + const message = String(operationError); + setError(message); + showErrorToast("Flatpak override failed", message); + } finally { + setOperation(null); + } + }; + + if (loading) { + return ; + } + + return ( + <> + + {error && } />} + {extensionStatus?.success ? runtimeVersions.map(({ version, key }) => ( + confirmExtensionOperation(version, extensionStatus[key])} + /> + )) : } + + + + {apps?.success ? apps.apps.length ? apps.apps.map((app) => ( + void toggleApp(app)} + /> + )) : : } + + + ); +} diff --git a/src/components/GameConfigurationSelector.tsx b/src/components/GameConfigurationSelector.tsx index 8a0df6c..a231477 100644 --- a/src/components/GameConfigurationSelector.tsx +++ b/src/components/GameConfigurationSelector.tsx @@ -13,7 +13,7 @@ interface Props { export function GameConfigurationSelector({ targets, runningGame, selectedAppId, onSelect, onReset, onResetAll }: Props) { const options: DropdownOption[] = [ { data: "", label: runningGame ? `Default (editing template) · ${runningGame.name}` : "Default" }, - ...targets.map((target) => ({ data: target.appid, label: `${target.name} · ${target.appid}` })), + ...targets.map((target) => ({ data: target.appid, label: `${target.nonSteam ? "Non-Steam · " : ""}${target.name} · ${target.appid}` })), ]; return <> diff --git a/src/components/NerdStuffModal.tsx b/src/components/NerdStuffModal.tsx deleted file mode 100644 index f075ccb..0000000 --- a/src/components/NerdStuffModal.tsx +++ /dev/null @@ -1,148 +0,0 @@ -import { useState, useEffect } from "react"; -import { - ModalRoot, - Field, - Focusable, - DialogControlsSection, - PanelSectionRow, - ButtonItem -} from "@decky/ui"; -import { - getConfigFileContent, - getLaunchScriptContent, - FileContentResult, -} from "../api/lsfgApi"; -import t from '../i18n/i18n'; - -interface NerdStuffModalProps { - closeModal?: () => void; -} - -export function NerdStuffModal({ closeModal }: NerdStuffModalProps) { - const [configContent, setConfigContent] = useState(null); - const [scriptContent, setScriptContent] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - useEffect(() => { - const loadData = async () => { - try { - setLoading(true); - setError(null); - - // Load all data in parallel - const [configResult, scriptResult] = await Promise.all([ - getConfigFileContent(), - getLaunchScriptContent(), - ]); - - setConfigContent(configResult); - setScriptContent(scriptResult); - } catch (err) { - setError(err instanceof Error ? err.message : "Failed to load data"); - } finally { - setLoading(false); - } - }; - - loadData(); - }, []); - - const copyToClipboard = async (text: string) => { - try { - await navigator.clipboard.writeText(text); - // Could add a toast notification here if desired - } catch (err) { - console.error("Failed to copy to clipboard:", err); - } - }; - - return ( - - {loading && ( -
{t('NERD_LOADING', 'Loading information...')}
- )} - - {error && ( -
Error: {error}
- )} - - {!loading && !error && ( - <> - {/* Launch Script Section */} - {scriptContent && ( - - {!scriptContent.success ? ( -
{t('NERD_SCRIPT_NOT_FOUND_PREFIX', 'Script not found:')} {scriptContent.error}
- ) : ( -
-
- {t('NERD_PATH_PREFIX', 'Path:')} {scriptContent.path} -
- scriptContent.content && copyToClipboard(scriptContent.content)} - onActivate={() => scriptContent.content && copyToClipboard(scriptContent.content)} - > -
-                      {scriptContent.content || t('NERD_NO_CONTENT', 'No content')}
-                    
-
-
- )} -
- )} - - {/* Config File Section */} - {configContent && ( - - {!configContent.success ? ( -
{t('NERD_CONFIG_NOT_FOUND_PREFIX', 'Config not found:')} {configContent.error}
- ) : ( -
-
- {t('NERD_PATH_PREFIX', 'Path:')} {configContent.path} -
- configContent.content && copyToClipboard(configContent.content)} - onActivate={() => configContent.content && copyToClipboard(configContent.content)} - > -
-                      {configContent.content || t('NERD_NO_CONTENT', 'No content')}
-                    
-
-
- )} -
- )} - - {/* Close Button */} - - - - {t('NERD_CLOSE', 'Close')} - - - - - )} -
- ); -} diff --git a/src/components/SetupTab.tsx b/src/components/SetupTab.tsx new file mode 100644 index 0000000..34106ed --- /dev/null +++ b/src/components/SetupTab.tsx @@ -0,0 +1,47 @@ +import { PanelSection } from "@decky/ui"; +import type { SteamBranchStatus } from "../api/lsfgApi"; +import { InstallationButton } from "./InstallationButton"; +import { StatusDisplay } from "./StatusDisplay"; + +interface SetupTabProps { + isInstalled: boolean; + installationStatus: string; + losslessScalingInstalled: boolean; + losslessScalingStatus: string; + steamBranchStatus: SteamBranchStatus | null; + isInstalling: boolean; + isUninstalling: boolean; + onInstall: () => void; + onUninstall: () => void; +} + +export function SetupTab({ + isInstalled, + installationStatus, + losslessScalingInstalled, + losslessScalingStatus, + steamBranchStatus, + isInstalling, + isUninstalling, + onInstall, + onUninstall, +}: SetupTabProps) { + return ( + + + + + ); +} diff --git a/src/components/SmartClipboardButton.tsx b/src/components/SmartClipboardButton.tsx deleted file mode 100644 index 8da239a..0000000 --- a/src/components/SmartClipboardButton.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { useState, useEffect } from "react"; -import { PanelSectionRow, ButtonItem } from "@decky/ui"; -import { FaClipboard, FaCheck } from "react-icons/fa"; -import { getLaunchOption } from "../api/lsfgApi"; -import { showClipboardErrorToast } from "../utils/toastUtils"; -import { copyWithVerification } from "../utils/clipboardUtils"; -import t from '../i18n/i18n'; - -export function SmartClipboardButton() { - const [isLoading, setIsLoading] = useState(false); - const [showSuccess, setShowSuccess] = useState(false); - - useEffect(() => { - if (showSuccess) { - const timer = setTimeout(() => { - setShowSuccess(false); - }, 3000); - return () => clearTimeout(timer); - } - return undefined; - }, [showSuccess]); - - const getLaunchOptionText = async (): Promise => { - try { - const result = await getLaunchOption(); - return result.launch_option || "~/lsfg %command%"; - } catch (error) { - return "~/lsfg %command%"; - } - }; - - const copyToClipboard = async () => { - if (isLoading || showSuccess) return; - - setIsLoading(true); - try { - const text = await getLaunchOptionText(); - const { success, verified } = await copyWithVerification(text); - - if (success) { - setShowSuccess(true); - if (!verified) { - console.log('Copy verification failed but copy likely worked'); - } - } else { - showClipboardErrorToast(); - } - - } catch (error) { - showClipboardErrorToast(); - } finally { - setIsLoading(false); - } - }; - - return ( - - -
- {showSuccess ? ( - - ) : isLoading ? ( - - ) : ( - - )} -
- {showSuccess ? t('CLIPBOARD_COPIED', 'Copied to clipboard') : isLoading ? t('CLIPBOARD_COPYING', 'Copying...') : t('CLIPBOARD_COPY_LAUNCH', 'Copy Launch Option')} -
-
-
- -
- ); -} diff --git a/src/components/UsageInstructions.tsx b/src/components/UsageInstructions.tsx deleted file mode 100644 index 36e31cf..0000000 --- a/src/components/UsageInstructions.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { PanelSectionRow } from "@decky/ui"; -import t from '../i18n/i18n'; - -export function UsageInstructions() { - return ( - <> - -
- {t('USAGE_TITLE', 'Usage Instructions')} -
-
- - -
- {t('USAGE_DESC', 'Click "Copy Launch Option" button, then paste it into your Steam game\'s launch options to enable frame generation.')} -
-
- - -
- ~/lsfg %command% -
-
- - -
- {t('USAGE_CONFIG_NOTE', 'The configuration is stored in ~/.config/lsfg-vk/conf.toml and hot-reloads while games are running.')} -
-
- - ); -} diff --git a/src/components/index.ts b/src/components/index.ts index 4284aee..5089b6f 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -3,9 +3,9 @@ export { StatusDisplay } from "./StatusDisplay"; export { InstallationButton } from "./InstallationButton"; export { ConfigurationSection } from "./ConfigurationSection"; export { FpsMultiplierControl } from "./FpsMultiplierControl"; -export { UsageInstructions } from "./UsageInstructions"; -export { SmartClipboardButton } from "./SmartClipboardButton"; export { FgmodClipboardButton } from "./FgmodClipboardButton"; -export { NerdStuffModal } from "./NerdStuffModal"; -export { FlatpaksModal } from "./FlatpaksModal"; +export { ConfigurationTab } from "./ConfigurationTab"; +export { SetupTab } from "./SetupTab"; +export { ConfigFileTab } from "./ConfigFileTab"; +export { FlatpaksTab } from "./FlatpaksTab"; export { GameConfigurationSelector } from "./GameConfigurationSelector"; -- cgit v1.2.3 From 170d183fdafc1ec1a686c006fd6a2431c1f30c71 Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Sun, 6 Sep 2026 19:06:52 -0400 Subject: refactor: align secondary tabs with Decky UI --- src/components/ConfigFileTab.tsx | 42 ++++++++++++------------ src/components/FlatpaksTab.tsx | 37 +++++++++------------ src/components/InstallationButton.tsx | 43 +++++------------------- src/components/SetupTab.tsx | 1 - src/components/StatusDisplay.tsx | 61 +++++++---------------------------- 5 files changed, 57 insertions(+), 127 deletions(-) (limited to 'src/components') diff --git a/src/components/ConfigFileTab.tsx b/src/components/ConfigFileTab.tsx index 60c5c7e..e3cc09d 100644 --- a/src/components/ConfigFileTab.tsx +++ b/src/components/ConfigFileTab.tsx @@ -1,19 +1,11 @@ import { useEffect, useState } from "react"; -import { Field, Focusable, PanelSection, PanelSectionRow, Spinner } from "@decky/ui"; +import { Field, PanelSection, PanelSectionRow, Spinner } from "@decky/ui"; import { getConfigFileContent, FileContentResult } from "../api/lsfgApi"; import t from "../i18n/i18n"; export function ConfigFileTab() { const [result, setResult] = useState(null); - const copy = async (content: string) => { - try { - await navigator.clipboard.writeText(content); - } catch { - // Clipboard access is unavailable in some Deck UI contexts. - } - }; - useEffect(() => { getConfigFileContent().then(setResult).catch((error) => { setResult({ success: false, error: String(error) }); @@ -23,24 +15,32 @@ export function ConfigFileTab() { if (!result) { return ( - + + + ); } return ( - - - {result.success && result.content && ( - void copy(result.content || "")}> -
-                {result.content}
-              
-
- )} -
-
+ {result.error && ( + + + + )} + {result.success && result.content && ( + <> + + + + +
+              {result.content}
+            
+
+ + )}
); } diff --git a/src/components/FlatpaksTab.tsx b/src/components/FlatpaksTab.tsx index dc73858..b4d5e4d 100644 --- a/src/components/FlatpaksTab.tsx +++ b/src/components/FlatpaksTab.tsx @@ -1,15 +1,12 @@ import { useEffect, useState } from "react"; import { - ButtonItem, ConfirmModal, Field, PanelSection, PanelSectionRow, - Spinner, - Toggle, + ToggleField, showModal, } from "@decky/ui"; -import { FaCheck, FaCog, FaDownload, FaTimes, FaTrash } from "react-icons/fa"; import { checkFlatpakExtensionStatus, FlatpakApp, @@ -40,15 +37,13 @@ interface RuntimeRowProps { function RuntimeRow({ version, installed, busy, onAction }: RuntimeRowProps) { return ( - : } - > - - {busy ? : installed ? <> {t("FLATPAK_UNINSTALL_BTN", "Uninstall")} : <> {t("FLATPAK_INSTALL_BTN", "Install")}} - - + description={busy ? "Updating..." : installed ? t("FLATPAK_INSTALLED", "Installed") : t("FLATPAK_NOT_INSTALLED", "Not installed")} + checked={installed} + onChange={() => onAction()} + disabled={busy} + /> ); } @@ -70,13 +65,13 @@ function AppRow({ app, busy, onToggle }: AppRowProps) { return ( - } - > - - + checked={configured} + onChange={onToggle} + disabled={busy} + /> ); } @@ -162,13 +157,13 @@ export function FlatpaksTab() { }; if (loading) { - return ; + return ; } return ( <> - - {error && } />} + + {error && } {extensionStatus?.success ? runtimeVersions.map(({ version, key }) => (
} - + {apps?.success ? apps.apps.length ? apps.apps.map((app) => ( { - if (isInstalling) { - return ( -
-
{t('INSTALL_INSTALLING', 'Installing...')}
-
- ); - } - - if (isUninstalling) { - return ( -
-
{t('INSTALL_UNINSTALLING', 'Uninstalling...')}
-
- ); - } - - if (isInstalled) { - return ( -
- -
{t('INSTALL_UNINSTALL_BTN', 'Uninstall LSFG-VK')}
-
- ); - } - - return ( -
- -
{t('INSTALL_INSTALL_BTN', 'Install LSFG-VK')}
-
- ); - }; + const label = 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 ( @@ -58,7 +31,7 @@ export function InstallationButton({ onClick={isInstalled ? onUninstall : onInstall} disabled={isInstalling || isUninstalling} > - {renderButtonContent()} + {label} ); diff --git a/src/components/SetupTab.tsx b/src/components/SetupTab.tsx index 34106ed..98d6e78 100644 --- a/src/components/SetupTab.tsx +++ b/src/components/SetupTab.tsx @@ -29,7 +29,6 @@ export function SetupTab({ return ( -
-
- {losslessScalingAppInstalled ? "Lossless Scaling Installed" : "Lossless Scaling Not Installed"} -
- {!losslessScalingAppInstalled && losslessScalingStatus && ( -
- {losslessScalingStatus} -
- )} -
- {installationStatus} -
-
+ +
+ + {steamBranchStatus?.installed && ( -
-
- Steam branch: {steamBranchStatus.current_branch || "public"} - {steamBranchStatus.needs_switch && ( -
- {steamBranchStatus.message} -
- )} -
-
+
)} -- cgit v1.2.3 From 7bc5f685186b1ff03ce0f7c99e7bec411beabaeb Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Sun, 6 Sep 2026 22:04:54 -0400 Subject: feat: make ui suck less, frfr --- src/components/ConfigurationSection.tsx | 10 +-- src/components/ConfigurationTab.tsx | 86 +++++++++++++++------ src/components/Content.tsx | 45 +++++++++-- src/components/FgmodClipboardButton.tsx | 110 --------------------------- src/components/FpsMultiplierControl.tsx | 70 ++++------------- src/components/GameConfigurationControls.tsx | 17 +++++ src/components/GameConfigurationSelector.tsx | 67 +++++++++++----- src/components/NowPlayingTab.tsx | 32 ++++++++ src/components/index.ts | 3 +- 9 files changed, 221 insertions(+), 219 deletions(-) delete mode 100644 src/components/FgmodClipboardButton.tsx create mode 100644 src/components/GameConfigurationControls.tsx create mode 100644 src/components/NowPlayingTab.tsx (limited to 'src/components') diff --git a/src/components/ConfigurationSection.tsx b/src/components/ConfigurationSection.tsx index c2bba7e..1f17264 100644 --- a/src/components/ConfigurationSection.tsx +++ b/src/components/ConfigurationSection.tsx @@ -10,19 +10,19 @@ interface ConfigurationSectionProps { export function ConfigurationSection({ config, onConfigChange }: ConfigurationSectionProps) { return <> - onConfigChange(FLOW_SCALE, value)} /> + onConfigChange(FLOW_SCALE, value)} /> - onConfigChange(NO_FP16, !value)} /> + onConfigChange(NO_FP16, !value)} /> - onConfigChange(PERFORMANCE_MODE, value)} /> + onConfigChange(PERFORMANCE_MODE, value)} /> - onConfigChange(OVERRIDE_PRESENT_MODE, value)} /> + onConfigChange(OVERRIDE_PRESENT_MODE, value)} /> - onConfigChange(PRESERVE_SWAPCHAIN_IMAGE_COUNT, value)} /> + onConfigChange(PRESERVE_SWAPCHAIN_IMAGE_COUNT, value)} /> ; } diff --git a/src/components/ConfigurationTab.tsx b/src/components/ConfigurationTab.tsx index 6f3f474..dab6f19 100644 --- a/src/components/ConfigurationTab.tsx +++ b/src/components/ConfigurationTab.tsx @@ -1,19 +1,17 @@ -import { PanelSection } from "@decky/ui"; +import { ButtonItem, Field, Focusable, PanelSection, PanelSectionRow } from "@decky/ui"; +import { useEffect, useRef, useState } from "react"; import { ConfigurationData } from "../config/configSchema"; import { GameTarget } from "../hooks/useGameConfiguration"; -import { ConfigurationSection } from "./ConfigurationSection"; -import { FgmodClipboardButton } from "./FgmodClipboardButton"; -import { FpsMultiplierControl } from "./FpsMultiplierControl"; +import { GameConfigurationControls } from "./GameConfigurationControls"; import { GameConfigurationSelector } from "./GameConfigurationSelector"; -import t from "../i18n/i18n"; interface ConfigurationTabProps { config: ConfigurationData; targets: GameTarget[]; runningGame: GameTarget | null; - selectedAppId: string; onSelect: (appid: string) => void; onConfigChange: (fieldName: keyof ConfigurationData, value: boolean | number | string | string[]) => Promise; + onEnable: (appid: string) => Promise; onReset: () => Promise; onResetAll: () => Promise; } @@ -22,33 +20,77 @@ export function ConfigurationTab({ config, targets, runningGame, - selectedAppId, onSelect, onConfigChange, + onEnable, onReset, onResetAll, }: ConfigurationTabProps) { - return ( - <> - - - - + const [detailAppId, setDetailAppId] = useState(null); + const promptedRunningAppId = useRef(null); + + useEffect(() => { + if (!runningGame || runningGame.configured) { + promptedRunningAppId.current = null; + return; + } + if (promptedRunningAppId.current !== runningGame.appid && detailAppId === null) { + promptedRunningAppId.current = runningGame.appid; + setDetailAppId(runningGame.appid); + } + }, [detailAppId, runningGame?.appid, runningGame?.configured]); + + const selectedTarget = detailAppId ? targets.find((target) => target.appid === detailAppId) : null; + + if (detailAppId === null) { + return ( + { + onSelect(appid); + setDetailAppId(appid); + }} onResetAll={onResetAll} /> - - - - - + ); + } + + const profileLabel = selectedTarget?.name || "Game profile"; + const running = selectedTarget?.appid === runningGame?.appid; + const profileDescription = selectedTarget + ? `${selectedTarget.nonSteam ? "Non-Steam" : "Steam"} · App ID ${selectedTarget.appid} · ${selectedTarget.configured ? running && !runningGame?.configured ? "Profile saved · applies next launch" : "Profile active" : "Not configured · changes apply next launch"}` + : "Game is no longer available"; + + return ( + setDetailAppId(null)}> + + + + + + setDetailAppId(null)}>Back to games + - + + + { + if (selectedTarget?.configured) { + promptedRunningAppId.current = detailAppId; + await onReset(); + setDetailAppId(null); + } else if (detailAppId) { + await onEnable(detailAppId); + } + }} + > + {selectedTarget?.configured ? "Remove profile" : "Enable for next launch"} + + + ); } diff --git a/src/components/Content.tsx b/src/components/Content.tsx index de8f996..9281d39 100644 --- a/src/components/Content.tsx +++ b/src/components/Content.tsx @@ -1,6 +1,6 @@ import { Tabs } from "@decky/ui"; -import { useEffect, useState } from "react"; -import { FaFileAlt, FaGamepad, FaLayerGroup, FaTools } from "react-icons/fa"; +import { useEffect, useRef, useState } from "react"; +import { FaFileAlt, FaGamepad, FaLayerGroup, FaList, FaTools } from "react-icons/fa"; import { ConfigurationData } from "../config/configSchema"; import { tabStyles } from "../styles"; import { useGameConfiguration } from "../hooks/useGameConfiguration"; @@ -9,10 +9,12 @@ import { useInstallationStatus } from "../hooks/useLsfgHooks"; import { ConfigFileTab } from "./ConfigFileTab"; import { ConfigurationTab } from "./ConfigurationTab"; import { FlatpaksTab } from "./FlatpaksTab"; +import { NowPlayingTab } from "./NowPlayingTab"; import { SetupTab } from "./SetupTab"; const tabIcons = { - configuration: , + nowPlaying: , + configuration: , flatpak: , configFile: , setup: , @@ -33,9 +35,9 @@ export function Content() { config, targets, runningGame, - selectedAppId, setSelectedAppId, save, + enable, resetSelected, resetAll, reload, @@ -48,10 +50,27 @@ export function Content() { steamBranchStatus?.success === true && steamBranchStatus.installed && !steamBranchStatus.needs_switch; + const previousRunningState = useRef<{ appid: string; configured: boolean } | null>(null); useEffect(() => { - setTab(setupComplete ? "Configuration" : "Setup"); - }, [setupComplete]); + if (!setupComplete) { + setTab("Setup"); + return; + } + setTab((current) => current === "Setup" ? (runningGame?.configured ? "NowPlaying" : "Configuration") : current); + }, [runningGame?.configured, setupComplete]); + + useEffect(() => { + if (!setupComplete) return; + const current = runningGame ? { appid: runningGame.appid, configured: runningGame.configured } : null; + const previous = previousRunningState.current; + previousRunningState.current = current; + if (current?.appid && (current.appid !== previous?.appid || current.configured !== previous?.configured)) { + setTab(current.configured ? "NowPlaying" : "Configuration"); + } else if (!current && previous) { + setTab((currentTab) => currentTab === "NowPlaying" ? "Configuration" : currentTab); + } + }, [runningGame?.appid, runningGame?.configured, setupComplete]); useEffect(() => { if (isInstalled) void reload(); @@ -88,6 +107,18 @@ export function Content() { const tabs = setupComplete ? [ + ...(runningGame?.configured ? [{ + id: "NowPlaying", + title: tabIcons.nowPlaying, + content: ( + + ), + }] : []), { id: "Configuration", title: tabIcons.configuration, @@ -96,9 +127,9 @@ export function Content() { config={config} targets={targets} runningGame={runningGame} - selectedAppId={selectedAppId} onSelect={setSelectedAppId} onConfigChange={handleConfigChange} + onEnable={enable} onReset={resetSelected} onResetAll={resetAll} /> diff --git a/src/components/FgmodClipboardButton.tsx b/src/components/FgmodClipboardButton.tsx deleted file mode 100644 index efc5490..0000000 --- a/src/components/FgmodClipboardButton.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { useState, useEffect } from "react"; -import { PanelSectionRow, ButtonItem } from "@decky/ui"; -import { FaClipboard, FaCheck } from "react-icons/fa"; -import { checkFgmodDirectory } from "../api/lsfgApi"; -import { showClipboardErrorToast } from "../utils/toastUtils"; -import { copyWithVerification } from "../utils/clipboardUtils"; -import t from '../i18n/i18n'; - -export function FgmodClipboardButton() { - const [isLoading, setIsLoading] = useState(false); - const [showSuccess, setShowSuccess] = useState(false); - const [fgmodExists, setFgmodExists] = useState(false); - const [checkingFgmod, setCheckingFgmod] = useState(true); - - // Check for fgmod directory on component mount - useEffect(() => { - const checkFgmod = async () => { - try { - const result = await checkFgmodDirectory(); - setFgmodExists(result.exists); - } catch (error) { - console.error("Error checking fgmod directory:", error); - setFgmodExists(false); - } finally { - setCheckingFgmod(false); - } - }; - - checkFgmod(); - }, []); - - // Reset success state after 3 seconds - useEffect(() => { - if (showSuccess) { - const timer = setTimeout(() => { - setShowSuccess(false); - }, 3000); - return () => clearTimeout(timer); - } - return undefined; - }, [showSuccess]); - - const copyToClipboard = async () => { - if (isLoading || showSuccess) return; - - setIsLoading(true); - try { - const text = "~/fgmod/fgmod ~/lsfg %command%"; - const { success, verified } = await copyWithVerification(text); - - if (success) { - // Show success feedback in the button instead of toast - setShowSuccess(true); - if (!verified) { - // Copy worked but verification failed - still show success - console.log('Copy verification failed but copy likely worked'); - } - } else { - showClipboardErrorToast(); - } - } catch (error) { - showClipboardErrorToast(); - } finally { - setIsLoading(false); - } - }; - - // Don't render if fgmod directory doesn't exist or we're still checking - if (checkingFgmod || !fgmodExists) { - return null; - } - - return ( - - -
- {showSuccess ? ( - - ) : isLoading ? ( - - ) : ( - - )} -
- {showSuccess ? t('CLIPBOARD_COPIED', 'Copied to clipboard') : isLoading ? t('CLIPBOARD_COPYING', 'Copying...') : t('CLIPBOARD_LSFG_FGMOD', 'LSFG + DeckyFG')} -
-
-
- -
- ); -} diff --git a/src/components/FpsMultiplierControl.tsx b/src/components/FpsMultiplierControl.tsx index 5069c9a..e197dc3 100644 --- a/src/components/FpsMultiplierControl.tsx +++ b/src/components/FpsMultiplierControl.tsx @@ -1,4 +1,4 @@ -import { PanelSectionRow, DialogButton, Focusable } from "@decky/ui"; +import { PanelSectionRow, SliderField } from "@decky/ui"; import { ConfigurationData } from "../config/configSchema"; import { MULTIPLIER } from "../config/generatedConfigSchema"; import t from "../i18n/i18n"; @@ -12,62 +12,22 @@ export function FpsMultiplierControl({ config, onConfigChange }: FpsMultiplierControlProps) { + const multiplierLabel = config.multiplier === 1 + ? t("MULTIPLIER_OFF", "Off") + : `${config.multiplier}x`; + return ( - - onConfigChange(MULTIPLIER, Math.max(1, config.multiplier - 1))} - disabled={config.multiplier <= 1} - > - − - -
4 ? "red" : "white", - minWidth: "60px", - textAlign: "center" - }} - > - {config.multiplier === 1 ? t('MULTIPLIER_OFF', 'OFF') : `${config.multiplier}X`} -
- onConfigChange(MULTIPLIER, Math.min(4, config.multiplier + 1))} - disabled={config.multiplier >= 4} - > - + - -
+ void onConfigChange(MULTIPLIER, value)} + />
); } diff --git a/src/components/GameConfigurationControls.tsx b/src/components/GameConfigurationControls.tsx new file mode 100644 index 0000000..bdf9985 --- /dev/null +++ b/src/components/GameConfigurationControls.tsx @@ -0,0 +1,17 @@ +import { ConfigurationData } from "../config/configSchema"; +import { ConfigurationSection } from "./ConfigurationSection"; +import { FpsMultiplierControl } from "./FpsMultiplierControl"; + +interface Props { + config: ConfigurationData; + onConfigChange: (fieldName: keyof ConfigurationData, value: boolean | number | string | string[]) => Promise; +} + +export function GameConfigurationControls({ config, onConfigChange }: Props) { + return ( + <> + + + + ); +} diff --git a/src/components/GameConfigurationSelector.tsx b/src/components/GameConfigurationSelector.tsx index a231477..fcdd0aa 100644 --- a/src/components/GameConfigurationSelector.tsx +++ b/src/components/GameConfigurationSelector.tsx @@ -1,29 +1,58 @@ -import { Dropdown, DropdownOption, PanelSectionRow, ButtonItem } from "@decky/ui"; +import { ButtonItem, Field, PanelSectionRow } from "@decky/ui"; import { GameTarget } from "../hooks/useGameConfiguration"; interface Props { targets: GameTarget[]; runningGame: GameTarget | null; - selectedAppId: string; onSelect: (appid: string) => void; - onReset: () => Promise; onResetAll: () => Promise; } -export function GameConfigurationSelector({ targets, runningGame, selectedAppId, onSelect, onReset, onResetAll }: Props) { - const options: DropdownOption[] = [ - { data: "", label: runningGame ? `Default (editing template) · ${runningGame.name}` : "Default" }, - ...targets.map((target) => ({ data: target.appid, label: `${target.nonSteam ? "Non-Steam · " : ""}${target.name} · ${target.appid}` })), - ]; - return <> - - onSelect(String(option.data))} /> - - - void onReset()} disabled={!selectedAppId}>Reset selected game - - - void onResetAll()} disabled={!targets.some((target) => target.configured)}>Reset all game profiles - - ; +const profileDescription = (target: GameTarget, running: boolean, active: boolean) => [ + running ? "Now playing" : "", + target.nonSteam ? "Non-Steam" : "Steam", + target.configured + ? active ? "Profile active" : "Profile saved · applies next launch" + : "Not configured · changes apply next launch", +].filter(Boolean).join(" · "); + +export function GameConfigurationSelector({ targets, runningGame, onSelect, onResetAll }: Props) { + const games = [...targets].sort((a, b) => { + if (a.appid === runningGame?.appid) return -1; + if (b.appid === runningGame?.appid) return 1; + return a.name.localeCompare(b.name); + }); + + return ( + <> + {games.length === 0 && ( + + + + )} + {games.map((game) => ( + + onSelect(game.appid)} + highlightOnFocus + /> + + ))} + + void onResetAll()} + disabled={!targets.some((target) => target.configured)} + > + Remove all profiles + + + + ); } diff --git a/src/components/NowPlayingTab.tsx b/src/components/NowPlayingTab.tsx new file mode 100644 index 0000000..956db4a --- /dev/null +++ b/src/components/NowPlayingTab.tsx @@ -0,0 +1,32 @@ +import { ButtonItem, Field, Focusable, PanelSection, PanelSectionRow } from "@decky/ui"; +import { ConfigurationData } from "../config/configSchema"; +import { GameTarget } from "../hooks/useGameConfiguration"; +import { GameConfigurationControls } from "./GameConfigurationControls"; + +interface Props { + game: GameTarget; + config: ConfigurationData; + onConfigChange: (fieldName: keyof ConfigurationData, value: boolean | number | string | string[]) => Promise; + onRemove: () => Promise; +} + +export function NowPlayingTab({ game, config, onConfigChange, onRemove }: Props) { + return ( + + + + + + + + + void onRemove()}> + Remove profile + + + + ); +} diff --git a/src/components/index.ts b/src/components/index.ts index 5089b6f..424a360 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -3,9 +3,10 @@ export { StatusDisplay } from "./StatusDisplay"; export { InstallationButton } from "./InstallationButton"; export { ConfigurationSection } from "./ConfigurationSection"; export { FpsMultiplierControl } from "./FpsMultiplierControl"; -export { FgmodClipboardButton } from "./FgmodClipboardButton"; export { ConfigurationTab } from "./ConfigurationTab"; export { SetupTab } from "./SetupTab"; export { ConfigFileTab } from "./ConfigFileTab"; export { FlatpaksTab } from "./FlatpaksTab"; export { GameConfigurationSelector } from "./GameConfigurationSelector"; +export { GameConfigurationControls } from "./GameConfigurationControls"; +export { NowPlayingTab } from "./NowPlayingTab"; -- cgit v1.2.3 From d1f8263bf9f22753ad53e1b0105172a3bd0faf5f Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Sun, 6 Sep 2026 23:12:28 -0400 Subject: feat: organize game profiles and focus enabled settings --- src/components/ConfigurationTab.tsx | 28 +++++-- src/components/FpsMultiplierControl.tsx | 45 ++++++++--- src/components/GameConfigurationControls.tsx | 16 +++- src/components/GameConfigurationSelector.tsx | 107 +++++++++++++++++++++------ src/components/NowPlayingTab.tsx | 2 +- 5 files changed, 152 insertions(+), 46 deletions(-) (limited to 'src/components') diff --git a/src/components/ConfigurationTab.tsx b/src/components/ConfigurationTab.tsx index dab6f19..6c4a99a 100644 --- a/src/components/ConfigurationTab.tsx +++ b/src/components/ConfigurationTab.tsx @@ -1,5 +1,5 @@ import { ButtonItem, Field, Focusable, PanelSection, PanelSectionRow } from "@decky/ui"; -import { useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { ConfigurationData } from "../config/configSchema"; import { GameTarget } from "../hooks/useGameConfiguration"; import { GameConfigurationControls } from "./GameConfigurationControls"; @@ -27,7 +27,13 @@ export function ConfigurationTab({ onResetAll, }: ConfigurationTabProps) { const [detailAppId, setDetailAppId] = useState(null); + const [focusFpsMultiplier, setFocusFpsMultiplier] = useState(false); const promptedRunningAppId = useRef(null); + const closeDetails = useCallback(() => { + setFocusFpsMultiplier(false); + setDetailAppId(null); + }, []); + const clearFpsFocusRequest = useCallback(() => setFocusFpsMultiplier(false), []); useEffect(() => { if (!runningGame || runningGame.configured) { @@ -59,22 +65,28 @@ export function ConfigurationTab({ } const profileLabel = selectedTarget?.name || "Game profile"; - const running = selectedTarget?.appid === runningGame?.appid; const profileDescription = selectedTarget - ? `${selectedTarget.nonSteam ? "Non-Steam" : "Steam"} · App ID ${selectedTarget.appid} · ${selectedTarget.configured ? running && !runningGame?.configured ? "Profile saved · applies next launch" : "Profile active" : "Not configured · changes apply next launch"}` + ? `${selectedTarget.nonSteam ? "Non-Steam" : "Steam"} · App ID ${selectedTarget.appid} · ${selectedTarget.configured ? "Configured" : "Not configured"}` : "Game is no longer available"; return ( - setDetailAppId(null)}> + - setDetailAppId(null)}>Back to games + Back to games - + {selectedTarget?.configured && ( + + )} diff --git a/src/components/FpsMultiplierControl.tsx b/src/components/FpsMultiplierControl.tsx index e197dc3..1fd0e78 100644 --- a/src/components/FpsMultiplierControl.tsx +++ b/src/components/FpsMultiplierControl.tsx @@ -1,4 +1,5 @@ -import { PanelSectionRow, SliderField } from "@decky/ui"; +import { Focusable, PanelSectionRow, SliderField } from "@decky/ui"; +import { useEffect, useRef } from "react"; import { ConfigurationData } from "../config/configSchema"; import { MULTIPLIER } from "../config/generatedConfigSchema"; import t from "../i18n/i18n"; @@ -6,28 +7,48 @@ import t from "../i18n/i18n"; interface FpsMultiplierControlProps { config: ConfigurationData; onConfigChange: (fieldName: keyof ConfigurationData, value: boolean | number | string | string[]) => Promise; + autoFocus?: boolean; + onAutoFocus?: () => void; } export function FpsMultiplierControl({ config, - onConfigChange + onConfigChange, + autoFocus = false, + onAutoFocus, }: FpsMultiplierControlProps) { + const focusableRef = useRef(null); + + useEffect(() => { + if (!autoFocus) return; + const frame = requestAnimationFrame(() => { + const target = focusableRef.current?.querySelector( + '[role="button"], [role="slider"]', + ); + target?.focus(); + onAutoFocus?.(); + }); + return () => cancelAnimationFrame(frame); + }, [autoFocus, onAutoFocus]); + const multiplierLabel = config.multiplier === 1 ? t("MULTIPLIER_OFF", "Off") : `${config.multiplier}x`; return ( - void onConfigChange(MULTIPLIER, value)} - /> + + void onConfigChange(MULTIPLIER, value)} + /> + ); } diff --git a/src/components/GameConfigurationControls.tsx b/src/components/GameConfigurationControls.tsx index bdf9985..34b82c5 100644 --- a/src/components/GameConfigurationControls.tsx +++ b/src/components/GameConfigurationControls.tsx @@ -5,12 +5,24 @@ import { FpsMultiplierControl } from "./FpsMultiplierControl"; interface Props { config: ConfigurationData; onConfigChange: (fieldName: keyof ConfigurationData, value: boolean | number | string | string[]) => Promise; + autoFocusFpsMultiplier?: boolean; + onFpsMultiplierFocused?: () => void; } -export function GameConfigurationControls({ config, onConfigChange }: Props) { +export function GameConfigurationControls({ + config, + onConfigChange, + autoFocusFpsMultiplier, + onFpsMultiplierFocused, +}: Props) { return ( <> - + ); diff --git a/src/components/GameConfigurationSelector.tsx b/src/components/GameConfigurationSelector.tsx index fcdd0aa..982fcef 100644 --- a/src/components/GameConfigurationSelector.tsx +++ b/src/components/GameConfigurationSelector.tsx @@ -1,4 +1,6 @@ import { ButtonItem, Field, PanelSectionRow } from "@decky/ui"; +import { useEffect, useState } from "react"; +import { RiArrowDownSFill, RiArrowUpSFill } from "react-icons/ri"; import { GameTarget } from "../hooks/useGameConfiguration"; interface Props { @@ -8,42 +10,101 @@ interface Props { onResetAll: () => Promise; } -const profileDescription = (target: GameTarget, running: boolean, active: boolean) => [ - running ? "Now playing" : "", - target.nonSteam ? "Non-Steam" : "Steam", - target.configured - ? active ? "Profile active" : "Profile saved · applies next launch" - : "Not configured · changes apply next launch", -].filter(Boolean).join(" · "); +const CONFIGURED_COLLAPSED_KEY = "lsfg-configured-games-collapsed"; +const AVAILABLE_COLLAPSED_KEY = "lsfg-available-games-collapsed"; -export function GameConfigurationSelector({ targets, runningGame, onSelect, onResetAll }: Props) { - const games = [...targets].sort((a, b) => { - if (a.appid === runningGame?.appid) return -1; - if (b.appid === runningGame?.appid) return 1; - return a.name.localeCompare(b.name); +function usePersistentCollapsed(key: string) { + const [collapsed, setCollapsed] = useState(() => { + try { + return localStorage.getItem(key) === "true"; + } catch { + return false; + } }); + useEffect(() => { + try { + localStorage.setItem(key, String(collapsed)); + } catch { + // Persisting the view preference is optional. + } + }, [collapsed, key]); + + return [collapsed, () => setCollapsed((value) => !value)] as const; +} + +function GameGroup({ + title, + games, + collapsed, + onToggle, + onSelect, +}: { + title: string; + games: GameTarget[]; + collapsed: boolean; + onToggle: () => void; + onSelect: (appid: string) => void; +}) { + if (games.length === 0) return null; + return ( <> - {games.length === 0 && ( - - - - )} - {games.map((game) => ( + + + {collapsed ? : } {title} ({games.length}) + + + {!collapsed && games.map((game) => ( onSelect(game.appid)} highlightOnFocus /> ))} + + ); +} + +export function GameConfigurationSelector({ targets, runningGame, onSelect, onResetAll }: Props) { + const sortGames = (games: GameTarget[]) => [...games].sort((a, b) => { + if (a.appid === runningGame?.appid) return -1; + if (b.appid === runningGame?.appid) return 1; + return a.name.localeCompare(b.name); + }); + const configuredGames = sortGames(targets.filter((game) => game.configured)); + const availableGames = sortGames(targets.filter((game) => !game.configured)); + const [configuredCollapsed, toggleConfigured] = usePersistentCollapsed(CONFIGURED_COLLAPSED_KEY); + const [availableCollapsed, toggleAvailable] = usePersistentCollapsed(AVAILABLE_COLLAPSED_KEY); + + return ( + <> + {targets.length === 0 && ( + + + + )} + +
-- cgit v1.2.3 From 50810a08e5a767ec6d774996ce13244121b9f3c3 Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Mon, 7 Sep 2026 00:39:16 -0400 Subject: fix: improve profile management focus and reset confirmation --- src/components/ConfigurationTab.tsx | 17 ++++++++++++++++- src/components/GameConfigurationSelector.tsx | 15 +++++++++++++-- 2 files changed, 29 insertions(+), 3 deletions(-) (limited to 'src/components') diff --git a/src/components/ConfigurationTab.tsx b/src/components/ConfigurationTab.tsx index 6c4a99a..0d8553c 100644 --- a/src/components/ConfigurationTab.tsx +++ b/src/components/ConfigurationTab.tsx @@ -28,13 +28,25 @@ export function ConfigurationTab({ }: ConfigurationTabProps) { const [detailAppId, setDetailAppId] = useState(null); const [focusFpsMultiplier, setFocusFpsMultiplier] = useState(false); + const [focusBackToGames, setFocusBackToGames] = useState(false); + const backToGamesRef = useRef(null); const promptedRunningAppId = useRef(null); const closeDetails = useCallback(() => { setFocusFpsMultiplier(false); + setFocusBackToGames(false); setDetailAppId(null); }, []); const clearFpsFocusRequest = useCallback(() => setFocusFpsMultiplier(false), []); + useEffect(() => { + if (!focusBackToGames) return; + const frame = requestAnimationFrame(() => { + backToGamesRef.current?.querySelector('[role="button"]')?.focus(); + setFocusBackToGames(false); + }); + return () => cancelAnimationFrame(frame); + }, [focusBackToGames]); + useEffect(() => { if (!runningGame || runningGame.configured) { promptedRunningAppId.current = null; @@ -55,6 +67,7 @@ export function ConfigurationTab({ targets={targets} runningGame={runningGame} onSelect={(appid) => { + setFocusBackToGames(true); onSelect(appid); setDetailAppId(appid); }} @@ -76,7 +89,9 @@ export function ConfigurationTab({
- Back to games + + Back to games + {selectedTarget?.configured && ( diff --git a/src/components/GameConfigurationSelector.tsx b/src/components/GameConfigurationSelector.tsx index 982fcef..91fdc39 100644 --- a/src/components/GameConfigurationSelector.tsx +++ b/src/components/GameConfigurationSelector.tsx @@ -1,4 +1,4 @@ -import { ButtonItem, Field, PanelSectionRow } from "@decky/ui"; +import { ButtonItem, ConfirmModal, Field, PanelSectionRow, showModal } from "@decky/ui"; import { useEffect, useState } from "react"; import { RiArrowDownSFill, RiArrowUpSFill } from "react-icons/ri"; import { GameTarget } from "../hooks/useGameConfiguration"; @@ -83,6 +83,17 @@ export function GameConfigurationSelector({ targets, runningGame, onSelect, onRe const availableGames = sortGames(targets.filter((game) => !game.configured)); const [configuredCollapsed, toggleConfigured] = usePersistentCollapsed(CONFIGURED_COLLAPSED_KEY); const [availableCollapsed, toggleAvailable] = usePersistentCollapsed(AVAILABLE_COLLAPSED_KEY); + const confirmResetAll = () => { + showModal( + void onResetAll()} + onCancel={() => {}} + />, + ); + }; return ( <> @@ -108,7 +119,7 @@ export function GameConfigurationSelector({ targets, runningGame, onSelect, onRe void onResetAll()} + onClick={confirmResetAll} disabled={!targets.some((target) => target.configured)} > Remove all profiles -- cgit v1.2.3 From 62531dd08e5f12f1f0533ccf8fa6b84c81eb63d4 Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Mon, 7 Sep 2026 00:50:43 -0400 Subject: chore: hide config file tab and clarify flatpak runtimes --- src/components/Content.tsx | 5 +++-- src/components/FlatpaksTab.tsx | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) (limited to 'src/components') diff --git a/src/components/Content.tsx b/src/components/Content.tsx index 9281d39..1bc6e95 100644 --- a/src/components/Content.tsx +++ b/src/components/Content.tsx @@ -6,7 +6,7 @@ import { tabStyles } from "../styles"; import { useGameConfiguration } from "../hooks/useGameConfiguration"; import { useInstallationActions } from "../hooks/useInstallationActions"; import { useInstallationStatus } from "../hooks/useLsfgHooks"; -import { ConfigFileTab } from "./ConfigFileTab"; +// import { ConfigFileTab } from "./ConfigFileTab"; import { ConfigurationTab } from "./ConfigurationTab"; import { FlatpaksTab } from "./FlatpaksTab"; import { NowPlayingTab } from "./NowPlayingTab"; @@ -136,7 +136,8 @@ export function Content() { ), }, { id: "Flatpak", title: tabIcons.flatpak, content: }, - { id: "ConfigFile", title: tabIcons.configFile, content: }, + // Keep the configuration-file view available for future use without exposing it in the UI. + // { id: "ConfigFile", title: tabIcons.configFile, content: }, { id: "Setup", title: tabIcons.setup, content: setupContent }, ] : [ diff --git a/src/components/FlatpaksTab.tsx b/src/components/FlatpaksTab.tsx index b4d5e4d..27204f4 100644 --- a/src/components/FlatpaksTab.tsx +++ b/src/components/FlatpaksTab.tsx @@ -157,12 +157,12 @@ export function FlatpaksTab() { }; if (loading) { - return ; + return ; } return ( <> - + {error && } {extensionStatus?.success ? runtimeVersions.map(({ version, key }) => ( Date: Mon, 7 Sep 2026 09:30:54 -0400 Subject: feat: restore multiplier buttons --- src/components/FpsMultiplierControl.tsx | 72 +++++++++++++++++++++++++-------- 1 file changed, 56 insertions(+), 16 deletions(-) (limited to 'src/components') diff --git a/src/components/FpsMultiplierControl.tsx b/src/components/FpsMultiplierControl.tsx index 1fd0e78..2c50be1 100644 --- a/src/components/FpsMultiplierControl.tsx +++ b/src/components/FpsMultiplierControl.tsx @@ -1,4 +1,4 @@ -import { Focusable, PanelSectionRow, SliderField } from "@decky/ui"; +import { DialogButton, Focusable, PanelSectionRow } from "@decky/ui"; import { useEffect, useRef } from "react"; import { ConfigurationData } from "../config/configSchema"; import { MULTIPLIER } from "../config/generatedConfigSchema"; @@ -31,23 +31,63 @@ export function FpsMultiplierControl({ return () => cancelAnimationFrame(frame); }, [autoFocus, onAutoFocus]); - const multiplierLabel = config.multiplier === 1 - ? t("MULTIPLIER_OFF", "Off") - : `${config.multiplier}x`; - return ( - - void onConfigChange(MULTIPLIER, value)} - /> + + void onConfigChange(MULTIPLIER, Math.max(1, config.multiplier - 1))} + disabled={config.multiplier <= 1} + > + − + +
4 ? "red" : "white", + minWidth: "60px", + textAlign: "center", + }} + > + {config.multiplier < 2 ? t("MULTIPLIER_OFF", "OFF") : `${config.multiplier}X`} +
+ void onConfigChange(MULTIPLIER, Math.min(4, config.multiplier + 1))} + disabled={config.multiplier >= 4} + > + + +
); -- cgit v1.2.3 From 22db1125238e54b29538102727c36284e122e703 Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Mon, 7 Sep 2026 16:02:26 -0400 Subject: feat: refine game discovery and profile UX --- src/components/ConfigurationTab.tsx | 59 +++++++++++++++++----------- src/components/Content.tsx | 8 ++-- src/components/GameConfigurationSelector.tsx | 50 ++++++++++++++++++++--- src/components/NowPlayingTab.tsx | 14 ++----- 4 files changed, 88 insertions(+), 43 deletions(-) (limited to 'src/components') diff --git a/src/components/ConfigurationTab.tsx b/src/components/ConfigurationTab.tsx index 0d8553c..83f46c1 100644 --- a/src/components/ConfigurationTab.tsx +++ b/src/components/ConfigurationTab.tsx @@ -12,6 +12,7 @@ interface ConfigurationTabProps { onSelect: (appid: string) => void; onConfigChange: (fieldName: keyof ConfigurationData, value: boolean | number | string | string[]) => Promise; onEnable: (appid: string) => Promise; + onEnableAll: () => Promise; onReset: () => Promise; onResetAll: () => Promise; } @@ -23,29 +24,32 @@ export function ConfigurationTab({ onSelect, onConfigChange, onEnable, + onEnableAll, onReset, onResetAll, }: ConfigurationTabProps) { const [detailAppId, setDetailAppId] = useState(null); const [focusFpsMultiplier, setFocusFpsMultiplier] = useState(false); - const [focusBackToGames, setFocusBackToGames] = useState(false); + const [focusDetailAction, setFocusDetailAction] = useState<"enable" | "back" | null>(null); const backToGamesRef = useRef(null); + const enableRef = useRef(null); const promptedRunningAppId = useRef(null); const closeDetails = useCallback(() => { setFocusFpsMultiplier(false); - setFocusBackToGames(false); + setFocusDetailAction(null); setDetailAppId(null); }, []); const clearFpsFocusRequest = useCallback(() => setFocusFpsMultiplier(false), []); useEffect(() => { - if (!focusBackToGames) return; + if (!focusDetailAction) return; const frame = requestAnimationFrame(() => { - backToGamesRef.current?.querySelector('[role="button"]')?.focus(); - setFocusBackToGames(false); + const ref = focusDetailAction === "enable" ? enableRef : backToGamesRef; + ref.current?.querySelector('[role="button"]')?.focus(); + setFocusDetailAction(null); }); return () => cancelAnimationFrame(frame); - }, [focusBackToGames]); + }, [focusDetailAction]); useEffect(() => { if (!runningGame || runningGame.configured) { @@ -54,6 +58,7 @@ export function ConfigurationTab({ } if (promptedRunningAppId.current !== runningGame.appid && detailAppId === null) { promptedRunningAppId.current = runningGame.appid; + setFocusDetailAction(runningGame.configured ? "back" : "enable"); setDetailAppId(runningGame.appid); } }, [detailAppId, runningGame?.appid, runningGame?.configured]); @@ -67,10 +72,11 @@ export function ConfigurationTab({ targets={targets} runningGame={runningGame} onSelect={(appid) => { - setFocusBackToGames(true); + setFocusDetailAction(targets.find((target) => target.appid === appid)?.configured ? "back" : "enable"); onSelect(appid); setDetailAppId(appid); }} + onEnableAll={onEnableAll} onResetAll={onResetAll} />
@@ -79,8 +85,17 @@ export function ConfigurationTab({ const profileLabel = selectedTarget?.name || "Game profile"; const profileDescription = selectedTarget - ? `${selectedTarget.nonSteam ? "Non-Steam" : "Steam"} · App ID ${selectedTarget.appid} · ${selectedTarget.configured ? "Configured" : "Not configured"}` + ? `${selectedTarget.nonSteam ? "Non-Steam" : "Steam"} · App ID ${selectedTarget.appid} · ${selectedTarget.configured ? "LSFG-VK Enabled" : "LSFG-VK not enabled"}` : "Game is no longer available"; + const handleProfileAction = async () => { + if (selectedTarget?.configured) { + promptedRunningAppId.current = detailAppId; + await onReset(); + closeDetails(); + } else if (detailAppId && await onEnable(detailAppId)) { + setFocusFpsMultiplier(true); + } + }; return ( @@ -88,6 +103,13 @@ export function ConfigurationTab({ + {!selectedTarget?.configured && selectedTarget && ( + + + Enable for next launch + + + )} Back to games @@ -102,22 +124,11 @@ export function ConfigurationTab({ onFpsMultiplierFocused={clearFpsFocusRequest} /> )} - - { - if (selectedTarget?.configured) { - promptedRunningAppId.current = detailAppId; - await onReset(); - closeDetails(); - } else if (detailAppId) { - if (await onEnable(detailAppId)) setFocusFpsMultiplier(true); - } - }} - > - {selectedTarget?.configured ? "Remove profile" : "Enable for next launch"} - - + {selectedTarget?.configured && ( + + Remove profile + + )} ); } diff --git a/src/components/Content.tsx b/src/components/Content.tsx index 1bc6e95..f1c8c11 100644 --- a/src/components/Content.tsx +++ b/src/components/Content.tsx @@ -6,7 +6,7 @@ import { tabStyles } from "../styles"; import { useGameConfiguration } from "../hooks/useGameConfiguration"; import { useInstallationActions } from "../hooks/useInstallationActions"; import { useInstallationStatus } from "../hooks/useLsfgHooks"; -// import { ConfigFileTab } from "./ConfigFileTab"; +import { ConfigFileTab } from "./ConfigFileTab"; import { ConfigurationTab } from "./ConfigurationTab"; import { FlatpaksTab } from "./FlatpaksTab"; import { NowPlayingTab } from "./NowPlayingTab"; @@ -38,6 +38,7 @@ export function Content() { setSelectedAppId, save, enable, + enableAll, resetSelected, resetAll, reload, @@ -115,7 +116,6 @@ export function Content() { game={runningGame} config={config} onConfigChange={handleConfigChange} - onRemove={resetSelected} /> ), }] : []), @@ -130,14 +130,14 @@ export function Content() { onSelect={setSelectedAppId} onConfigChange={handleConfigChange} onEnable={enable} + onEnableAll={enableAll} onReset={resetSelected} onResetAll={resetAll} /> ), }, { id: "Flatpak", title: tabIcons.flatpak, content: }, - // Keep the configuration-file view available for future use without exposing it in the UI. - // { id: "ConfigFile", title: tabIcons.configFile, content: }, + { id: "ConfigFile", title: tabIcons.configFile, content: }, // comment out for prod { id: "Setup", title: tabIcons.setup, content: setupContent }, ] : [ diff --git a/src/components/GameConfigurationSelector.tsx b/src/components/GameConfigurationSelector.tsx index 91fdc39..a046594 100644 --- a/src/components/GameConfigurationSelector.tsx +++ b/src/components/GameConfigurationSelector.tsx @@ -7,10 +7,11 @@ interface Props { targets: GameTarget[]; runningGame: GameTarget | null; onSelect: (appid: string) => void; + onEnableAll: () => Promise; onResetAll: () => Promise; } -const CONFIGURED_COLLAPSED_KEY = "lsfg-configured-games-collapsed"; +const CONFIGURED_COLLAPSED_KEY = "lsfg-configured-games-collapsed-v2"; const AVAILABLE_COLLAPSED_KEY = "lsfg-available-games-collapsed"; function usePersistentCollapsed(key: string) { @@ -73,7 +74,7 @@ function GameGroup({ ); } -export function GameConfigurationSelector({ targets, runningGame, onSelect, onResetAll }: Props) { +export function GameConfigurationSelector({ targets, runningGame, onSelect, onEnableAll, onResetAll }: Props) { const sortGames = (games: GameTarget[]) => [...games].sort((a, b) => { if (a.appid === runningGame?.appid) return -1; if (b.appid === runningGame?.appid) return 1; @@ -81,8 +82,14 @@ export function GameConfigurationSelector({ targets, runningGame, onSelect, onRe }); const configuredGames = sortGames(targets.filter((game) => game.configured)); const availableGames = sortGames(targets.filter((game) => !game.configured)); + const configuredSteamGames = configuredGames.filter((game) => !game.nonSteam); + const configuredNonSteamGames = configuredGames.filter((game) => game.nonSteam); + const availableSteamGames = availableGames.filter((game) => !game.nonSteam); + const availableNonSteamGames = availableGames.filter((game) => game.nonSteam); const [configuredCollapsed, toggleConfigured] = usePersistentCollapsed(CONFIGURED_COLLAPSED_KEY); + const [configuredNonSteamCollapsed, toggleConfiguredNonSteam] = usePersistentCollapsed(`${CONFIGURED_COLLAPSED_KEY}-non-steam`); const [availableCollapsed, toggleAvailable] = usePersistentCollapsed(AVAILABLE_COLLAPSED_KEY); + const [availableNonSteamCollapsed, toggleAvailableNonSteam] = usePersistentCollapsed(`${AVAILABLE_COLLAPSED_KEY}-non-steam`); const confirmResetAll = () => { showModal( , ); }; + const confirmEnableAll = () => { + showModal( + void onEnableAll()} + onCancel={() => {}} + />, + ); + }; return ( <> @@ -102,20 +121,41 @@ export function GameConfigurationSelector({ targets, runningGame, onSelect, onRe )} + {availableGames.length > 0 && ( + + + Enable all available games + + + )} + + Promise; - onRemove: () => Promise; } -export function NowPlayingTab({ game, config, onConfigChange, onRemove }: Props) { +export function NowPlayingTab({ game, config, onConfigChange }: Props) { return ( - + - - void onRemove()}> - Remove profile - - ); } -- cgit v1.2.3 From e21eee83fc58fee3481aa0e17feb7cd9a8d8750e Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Mon, 7 Sep 2026 20:16:09 -0400 Subject: Support lowercase Steam shortcut names and collapsible profile details --- src/components/ConfigurationTab.tsx | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) (limited to 'src/components') diff --git a/src/components/ConfigurationTab.tsx b/src/components/ConfigurationTab.tsx index 83f46c1..2175eb9 100644 --- a/src/components/ConfigurationTab.tsx +++ b/src/components/ConfigurationTab.tsx @@ -1,5 +1,6 @@ import { ButtonItem, Field, Focusable, PanelSection, PanelSectionRow } from "@decky/ui"; import { useCallback, useEffect, useRef, useState } from "react"; +import { RiArrowDownSFill, RiArrowUpSFill } from "react-icons/ri"; import { ConfigurationData } from "../config/configSchema"; import { GameTarget } from "../hooks/useGameConfiguration"; import { GameConfigurationControls } from "./GameConfigurationControls"; @@ -29,6 +30,7 @@ export function ConfigurationTab({ onResetAll, }: ConfigurationTabProps) { const [detailAppId, setDetailAppId] = useState(null); + const [detailsExpanded, setDetailsExpanded] = useState(false); const [focusFpsMultiplier, setFocusFpsMultiplier] = useState(false); const [focusDetailAction, setFocusDetailAction] = useState<"enable" | "back" | null>(null); const backToGamesRef = useRef(null); @@ -41,6 +43,8 @@ export function ConfigurationTab({ }, []); const clearFpsFocusRequest = useCallback(() => setFocusFpsMultiplier(false), []); + useEffect(() => setDetailsExpanded(false), [detailAppId]); + useEffect(() => { if (!focusDetailAction) return; const frame = requestAnimationFrame(() => { @@ -101,7 +105,7 @@ export function ConfigurationTab({ - + {!selectedTarget?.configured && selectedTarget && ( @@ -129,6 +133,20 @@ export function ConfigurationTab({ Remove profile )} + + setDetailsExpanded((expanded) => !expanded)} + > + {detailsExpanded ? : } Details + + + {detailsExpanded && ( + + + + )} ); } -- cgit v1.2.3 From 54acc36f16e1336772354f979a618cce65061f82 Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Mon, 7 Sep 2026 20:57:26 -0400 Subject: refactor: make runtime installation explicit --- src/components/ConfigurationTab.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) (limited to 'src/components') diff --git a/src/components/ConfigurationTab.tsx b/src/components/ConfigurationTab.tsx index 2175eb9..b3c0281 100644 --- a/src/components/ConfigurationTab.tsx +++ b/src/components/ConfigurationTab.tsx @@ -1,5 +1,6 @@ -import { ButtonItem, Field, Focusable, PanelSection, PanelSectionRow } from "@decky/ui"; +import { ButtonItem, DialogButton, Field, Focusable, PanelSection, PanelSectionRow } from "@decky/ui"; import { useCallback, useEffect, useRef, useState } from "react"; +import { FaArrowLeft } from "react-icons/fa"; import { RiArrowDownSFill, RiArrowUpSFill } from "react-icons/ri"; import { ConfigurationData } from "../config/configSchema"; import { GameTarget } from "../hooks/useGameConfiguration"; @@ -116,7 +117,13 @@ export function ConfigurationTab({ )} - Back to games + + +
-- cgit v1.2.3 From 5b84a7ab2782ce672b51dc9d608c63c9ac8ff3ab Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Mon, 7 Sep 2026 21:04:26 -0400 Subject: feat: show Flatpak runtime readiness --- src/components/FlatpaksTab.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) (limited to 'src/components') diff --git a/src/components/FlatpaksTab.tsx b/src/components/FlatpaksTab.tsx index 27204f4..8aab940 100644 --- a/src/components/FlatpaksTab.tsx +++ b/src/components/FlatpaksTab.tsx @@ -50,18 +50,21 @@ function RuntimeRow({ version, installed, busy, onAction }: RuntimeRowProps) { interface AppRowProps { app: FlatpakApp; + runtimeReady: boolean; busy: boolean; onToggle: () => void; } -function AppRow({ app, busy, onToggle }: AppRowProps) { +function AppRow({ app, runtimeReady, busy, onToggle }: AppRowProps) { const configured = app.has_filesystem_override && app.has_env_override; const partial = app.has_filesystem_override || app.has_env_override; const status = configured - ? t("FLATPAK_STATUS_CONFIGURED", "Configured") + ? runtimeReady + ? t("FLATPAK_STATUS_READY", "Ready") + : t("FLATPAK_STATUS_RUNTIME_MISSING", "Runtime missing") : partial ? t("FLATPAK_STATUS_PARTIAL", "Partial") - : t("FLATPAK_STATUS_NO_OVERRIDES", "No overrides"); + : t("FLATPAK_STATUS_NOT_ENABLED", "Not enabled"); return ( @@ -82,6 +85,8 @@ export function FlatpaksTab() { const [loading, setLoading] = useState(true); const [operation, setOperation] = useState(null); const [error, setError] = useState(null); + const runtimeReady = extensionStatus?.success === true + && runtimeVersions.some(({ key }) => extensionStatus[key]); const load = async () => { setLoading(true); @@ -180,6 +185,7 @@ export function FlatpaksTab() { void toggleApp(app)} /> -- cgit v1.2.3 From 85bb5c9da5bb5ea64ddca1aeca1dc6ba656bb86c Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Mon, 7 Sep 2026 23:40:11 -0400 Subject: fix: focus configured game controls --- src/components/ConfigurationTab.tsx | 58 +++++++++++++++---------------------- 1 file changed, 24 insertions(+), 34 deletions(-) (limited to 'src/components') diff --git a/src/components/ConfigurationTab.tsx b/src/components/ConfigurationTab.tsx index b3c0281..b94522a 100644 --- a/src/components/ConfigurationTab.tsx +++ b/src/components/ConfigurationTab.tsx @@ -1,7 +1,6 @@ import { ButtonItem, DialogButton, Field, Focusable, PanelSection, PanelSectionRow } from "@decky/ui"; import { useCallback, useEffect, useRef, useState } from "react"; import { FaArrowLeft } from "react-icons/fa"; -import { RiArrowDownSFill, RiArrowUpSFill } from "react-icons/ri"; import { ConfigurationData } from "../config/configSchema"; import { GameTarget } from "../hooks/useGameConfiguration"; import { GameConfigurationControls } from "./GameConfigurationControls"; @@ -31,10 +30,8 @@ export function ConfigurationTab({ onResetAll, }: ConfigurationTabProps) { const [detailAppId, setDetailAppId] = useState(null); - const [detailsExpanded, setDetailsExpanded] = useState(false); const [focusFpsMultiplier, setFocusFpsMultiplier] = useState(false); - const [focusDetailAction, setFocusDetailAction] = useState<"enable" | "back" | null>(null); - const backToGamesRef = useRef(null); + const [focusDetailAction, setFocusDetailAction] = useState<"enable" | "fps" | null>(null); const enableRef = useRef(null); const promptedRunningAppId = useRef(null); const closeDetails = useCallback(() => { @@ -44,13 +41,15 @@ export function ConfigurationTab({ }, []); const clearFpsFocusRequest = useCallback(() => setFocusFpsMultiplier(false), []); - useEffect(() => setDetailsExpanded(false), [detailAppId]); - useEffect(() => { if (!focusDetailAction) return; + if (focusDetailAction === "fps") { + setFocusFpsMultiplier(true); + setFocusDetailAction(null); + return; + } const frame = requestAnimationFrame(() => { - const ref = focusDetailAction === "enable" ? enableRef : backToGamesRef; - ref.current?.querySelector('[role="button"]')?.focus(); + enableRef.current?.querySelector('[role="button"]')?.focus(); setFocusDetailAction(null); }); return () => cancelAnimationFrame(frame); @@ -63,7 +62,7 @@ export function ConfigurationTab({ } if (promptedRunningAppId.current !== runningGame.appid && detailAppId === null) { promptedRunningAppId.current = runningGame.appid; - setFocusDetailAction(runningGame.configured ? "back" : "enable"); + setFocusDetailAction("enable"); setDetailAppId(runningGame.appid); } }, [detailAppId, runningGame?.appid, runningGame?.configured]); @@ -77,7 +76,7 @@ export function ConfigurationTab({ targets={targets} runningGame={runningGame} onSelect={(appid) => { - setFocusDetailAction(targets.find((target) => target.appid === appid)?.configured ? "back" : "enable"); + setFocusDetailAction(targets.find((target) => target.appid === appid)?.configured ? "fps" : "enable"); onSelect(appid); setDetailAppId(appid); }} @@ -104,7 +103,20 @@ export function ConfigurationTab({ return ( - + + + + + + + + + + @@ -115,17 +127,6 @@ export function ConfigurationTab({ )} - - - - - - -
{selectedTarget?.configured && ( )} - setDetailsExpanded((expanded) => !expanded)} - > - {detailsExpanded ? : } Details - + - {detailsExpanded && ( - - - - )} ); } -- cgit v1.2.3 From c6c24524b46e237bee796cb57b9a27c437c458ae Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Tue, 8 Sep 2026 09:07:51 -0400 Subject: fix: make game group collapse controls compact --- src/components/GameConfigurationSelector.tsx | 32 ++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) (limited to 'src/components') diff --git a/src/components/GameConfigurationSelector.tsx b/src/components/GameConfigurationSelector.tsx index a046594..15f2465 100644 --- a/src/components/GameConfigurationSelector.tsx +++ b/src/components/GameConfigurationSelector.tsx @@ -52,13 +52,25 @@ function GameGroup({ return ( <> - + + +
- {collapsed ? : } {title} ({games.length}) - + + {collapsed ? ( + + ) : ( + + )} + +
{!collapsed && games.map((game) => ( @@ -116,6 +128,14 @@ export function GameConfigurationSelector({ targets, runningGame, onSelect, onEn return ( <> + {targets.length === 0 && ( -- cgit v1.2.3 From b3749ecc4b094a46d2ab9f638ee810f70840f67a Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Tue, 8 Sep 2026 11:31:46 -0400 Subject: update layout and config and hash --- src/components/ConfigurationTab.tsx | 33 +++++++++++++++++-------- src/components/GameConfigurationSelector.tsx | 23 +++++++++++------ src/components/NowPlayingTab.tsx | 1 - src/components/ProfileDetails.tsx | 37 ++++++++++++++++++++++++++++ 4 files changed, 76 insertions(+), 18 deletions(-) create mode 100644 src/components/ProfileDetails.tsx (limited to 'src/components') diff --git a/src/components/ConfigurationTab.tsx b/src/components/ConfigurationTab.tsx index b94522a..6ba8f5d 100644 --- a/src/components/ConfigurationTab.tsx +++ b/src/components/ConfigurationTab.tsx @@ -1,10 +1,11 @@ -import { ButtonItem, DialogButton, Field, Focusable, PanelSection, PanelSectionRow } from "@decky/ui"; +import { ButtonItem, DialogButton, Focusable, PanelSection, PanelSectionRow, gamepadDialogClasses } 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 { GameConfigurationControls } from "./GameConfigurationControls"; import { GameConfigurationSelector } from "./GameConfigurationSelector"; +import { ProfileDetails } from "./ProfileDetails"; interface ConfigurationTabProps { config: ConfigurationData; @@ -105,21 +106,35 @@ export function ConfigurationTab({ - +
+ - + +
+ {profileLabel} +
+
- - - {!selectedTarget?.configured && selectedTarget && ( @@ -141,9 +156,7 @@ export function ConfigurationTab({ Remove profile )} - - - +
); } diff --git a/src/components/GameConfigurationSelector.tsx b/src/components/GameConfigurationSelector.tsx index 15f2465..5f23b91 100644 --- a/src/components/GameConfigurationSelector.tsx +++ b/src/components/GameConfigurationSelector.tsx @@ -11,15 +11,15 @@ interface Props { onResetAll: () => Promise; } -const CONFIGURED_COLLAPSED_KEY = "lsfg-configured-games-collapsed-v2"; -const AVAILABLE_COLLAPSED_KEY = "lsfg-available-games-collapsed"; +const CONFIGURED_COLLAPSED_KEY = "lsfg-configured-games-collapsed-v3"; +const AVAILABLE_COLLAPSED_KEY = "lsfg-available-games-collapsed-v2"; function usePersistentCollapsed(key: string) { const [collapsed, setCollapsed] = useState(() => { try { - return localStorage.getItem(key) === "true"; + return localStorage.getItem(key) !== "false"; } catch { - return false; + return true; } }); @@ -65,9 +65,9 @@ function GameGroup({ onClick={onToggle} > {collapsed ? ( - + ) : ( - + )}
@@ -132,7 +132,16 @@ export function GameConfigurationSelector({ targets, runningGame, onSelect, onEn {` .LSFG_GameGroupCollapseButton_Container > div > div > div > button, .LSFG_GameGroupCollapseButton_Container > div > div > div > div > button { - height: 10px !important; + 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; } `} diff --git a/src/components/NowPlayingTab.tsx b/src/components/NowPlayingTab.tsx index 8d2d779..2ec12ca 100644 --- a/src/components/NowPlayingTab.tsx +++ b/src/components/NowPlayingTab.tsx @@ -16,7 +16,6 @@ export function NowPlayingTab({ game, config, onConfigChange }: Props) { diff --git a/src/components/ProfileDetails.tsx b/src/components/ProfileDetails.tsx new file mode 100644 index 0000000..056abbb --- /dev/null +++ b/src/components/ProfileDetails.tsx @@ -0,0 +1,37 @@ +import { ButtonItem, Field, Focusable, PanelSectionRow } from "@decky/ui"; +import { useEffect, useRef, useState } from "react"; +import { RiArrowDownSFill, RiArrowUpSFill } from "react-icons/ri"; + +interface ProfileDetailsProps { + description: string; +} + +export function ProfileDetails({ description }: ProfileDetailsProps) { + const [expanded, setExpanded] = useState(false); + const detailsRef = useRef(null); + + useEffect(() => { + if (!expanded) return; + const frame = requestAnimationFrame(() => detailsRef.current?.scrollIntoView({ block: "nearest" })); + return () => cancelAnimationFrame(frame); + }, [expanded]); + + return ( + + + setExpanded((value) => !value)} + > + {expanded ? : } Details + + + {expanded && ( + + + + )} + + ); +} -- cgit v1.2.3 From 8352ee74e8a437c1f4a4dadb75d63049f45b164b Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Tue, 8 Sep 2026 16:25:11 -0400 Subject: add back workarounds sections, scope out of now playing --- src/components/ConfigurationTab.tsx | 2 + src/components/GameConfigurationControls.tsx | 9 ++ src/components/NowPlayingTab.tsx | 2 +- src/components/WorkaroundsSection.tsx | 190 +++++++++++++++++++++++++++ src/components/index.ts | 1 + 5 files changed, 203 insertions(+), 1 deletion(-) create mode 100644 src/components/WorkaroundsSection.tsx (limited to 'src/components') diff --git a/src/components/ConfigurationTab.tsx b/src/components/ConfigurationTab.tsx index 6ba8f5d..2bb0f26 100644 --- a/src/components/ConfigurationTab.tsx +++ b/src/components/ConfigurationTab.tsx @@ -149,6 +149,8 @@ export function ConfigurationTab({ onConfigChange={onConfigChange} autoFocusFpsMultiplier={focusFpsMultiplier} onFpsMultiplierFocused={clearFpsFocusRequest} + showWorkarounds + workaroundTarget={selectedTarget || undefined} /> )} {selectedTarget?.configured && ( diff --git a/src/components/GameConfigurationControls.tsx b/src/components/GameConfigurationControls.tsx index 34b82c5..58ccd02 100644 --- a/src/components/GameConfigurationControls.tsx +++ b/src/components/GameConfigurationControls.tsx @@ -1,12 +1,16 @@ import { ConfigurationData } from "../config/configSchema"; +import type { GameTarget } from "../hooks/useGameConfiguration"; import { ConfigurationSection } from "./ConfigurationSection"; import { FpsMultiplierControl } from "./FpsMultiplierControl"; +import { WorkaroundsSection } from "./WorkaroundsSection"; interface Props { config: ConfigurationData; onConfigChange: (fieldName: keyof ConfigurationData, value: boolean | number | string | string[]) => Promise; autoFocusFpsMultiplier?: boolean; onFpsMultiplierFocused?: () => void; + showWorkarounds?: boolean; + workaroundTarget?: Pick; } export function GameConfigurationControls({ @@ -14,6 +18,8 @@ export function GameConfigurationControls({ onConfigChange, autoFocusFpsMultiplier, onFpsMultiplierFocused, + showWorkarounds = false, + workaroundTarget, }: Props) { return ( <> @@ -24,6 +30,9 @@ export function GameConfigurationControls({ onAutoFocus={onFpsMultiplierFocused} /> + {showWorkarounds && workaroundTarget && ( + + )} ); } diff --git a/src/components/NowPlayingTab.tsx b/src/components/NowPlayingTab.tsx index 2ec12ca..188c56a 100644 --- a/src/components/NowPlayingTab.tsx +++ b/src/components/NowPlayingTab.tsx @@ -19,7 +19,7 @@ export function NowPlayingTab({ game, config, onConfigChange }: Props) { />
- + ); } diff --git a/src/components/WorkaroundsSection.tsx b/src/components/WorkaroundsSection.tsx new file mode 100644 index 0000000..e990784 --- /dev/null +++ b/src/components/WorkaroundsSection.tsx @@ -0,0 +1,190 @@ +import { ButtonItem, Field, PanelSectionRow, SliderField, ToggleField } from "@decky/ui"; +import { useEffect, useState } from "react"; +import { RiArrowDownSFill, RiArrowUpSFill } from "react-icons/ri"; +import { usePerAppWorkarounds } from "../hooks/usePerAppWorkarounds"; +import t from "../i18n/i18n"; +import type { WorkaroundField } from "../utils/steamLaunchOptions"; + +interface WorkaroundsSectionProps { + appId: string; + nonSteam: boolean; +} + +const WORKAROUNDS_COLLAPSED_KEY = "lsfg-workarounds-collapsed"; +type ToggleWorkaroundField = Exclude; + +const TOGGLE_ROWS: readonly { + field: ToggleWorkaroundField; + labelKey: string; + label: string; + descriptionKey: string; + description: string; +}[] = [ + { + field: "disableGamescopeWsi", + labelKey: "CONFIG_DISABLE_GAMESCOPE_WSI", + label: "Disable Gamescope WSI", + descriptionKey: "CONFIG_DISABLE_GAMESCOPE_WSI_DESC", + description: "Adds ENABLE_GAMESCOPE_WSI=0 without changing HDR. Requires game restart to apply.", + }, + { + field: "disableSteamdeckMode", + labelKey: "CONFIG_DISABLE_STEAMDECK_MODE", + label: "Disable Steam Deck Mode", + descriptionKey: "CONFIG_DISABLE_STEAMDECK_MODE_DESC", + description: "Disables a game-specific Steam Deck compatibility switch. Requires game restart to apply.", + }, + { + field: "disableVkbasalt", + labelKey: "CONFIG_DISABLE_VKBASALT", + label: "Disable vkBasalt", + descriptionKey: "CONFIG_DISABLE_VKBASALT_DESC", + description: "Disables vkBasalt layer which can conflict with LSFG (Reshade, some Decky plugins)", + }, + { + field: "enableZink", + labelKey: "CONFIG_ENABLE_ZINK", + label: "Force Zink for OpenGL Games", + descriptionKey: "CONFIG_ENABLE_ZINK_DESC", + description: "Uses Mesa's Zink OpenGL-to-Vulkan driver. May cause crashes or freezes with some games. Requires game restart to apply.", + }, +]; + +function usePersistentCollapsed() { + const [collapsed, setCollapsed] = useState(() => { + try { + const saved = localStorage.getItem(WORKAROUNDS_COLLAPSED_KEY); + return saved !== null ? JSON.parse(saved) === true : true; + } catch { + return true; + } + }); + + useEffect(() => { + try { + localStorage.setItem(WORKAROUNDS_COLLAPSED_KEY, JSON.stringify(collapsed)); + } catch { + // Persisting the view preference is optional. + } + }, [collapsed]); + + return [collapsed, () => setCollapsed((value) => !value)] as const; +} + +export function WorkaroundsSection({ appId, nonSteam }: WorkaroundsSectionProps) { + const [collapsed, toggleCollapsed] = usePersistentCollapsed(); + const { status, snapshot, refresh, update, error } = usePerAppWorkarounds(appId, nonSteam); + const state = snapshot?.parsed.state; + const issues = snapshot?.parsed.issues || []; + const controlsDisabled = status !== "ready" || state === undefined; + const [fpsValue, setFpsValue] = useState(null); + const effectiveFpsValue = fpsValue ?? state?.dxvkFrameRate ?? 0; + const fpsLabel = effectiveFpsValue > 0 + ? `${effectiveFpsValue} FPS` + : t("CONFIG_BASE_FPS_CAP_OFF", "Off"); + + useEffect(() => { + setFpsValue(state?.dxvkFrameRate ?? null); + }, [state?.dxvkFrameRate, status]); + + return ( + <> + + +
+ {t("CONFIG_WORKAROUNDS_TITLE", "Workarounds")} +
+
+ +
+ + {collapsed ? : } + +
+
+ + {!collapsed && ( + <> + {status === "loading" && ( + + + + )} + {status === "error" && ( + <> + + + + + void refresh()}>Retry + + + )} + {status === "ready" && issues.length > 0 && ( + + + + )} + + + { + setFpsValue(value); + void update("dxvkFrameRate", value); + }} + disabled={controlsDisabled} + /> + + {TOGGLE_ROWS.map((row) => ( + + void update(row.field, value)} + disabled={controlsDisabled} + /> + + ))} + + )} + + ); +} diff --git a/src/components/index.ts b/src/components/index.ts index 424a360..37a8edb 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -10,3 +10,4 @@ export { FlatpaksTab } from "./FlatpaksTab"; export { GameConfigurationSelector } from "./GameConfigurationSelector"; export { GameConfigurationControls } from "./GameConfigurationControls"; export { NowPlayingTab } from "./NowPlayingTab"; +export { WorkaroundsSection } from "./WorkaroundsSection"; -- cgit v1.2.3 From bec26fe025c97c00d398e7a4fb571195706b9e76 Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Tue, 8 Sep 2026 16:56:57 -0400 Subject: feat: more launch arg janitoring --- src/components/Content.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'src/components') diff --git a/src/components/Content.tsx b/src/components/Content.tsx index f1c8c11..1d7d2d1 100644 --- a/src/components/Content.tsx +++ b/src/components/Content.tsx @@ -80,8 +80,9 @@ export function Content() { const handleConfigChange = async ( fieldName: keyof ConfigurationData, value: boolean | number | string | string[], + cleanupLaunchOptions = false, ) => { - await save({ ...config, [fieldName]: value }); + await save({ ...config, [fieldName]: value }, cleanupLaunchOptions); }; const onInstall = () => { @@ -115,7 +116,7 @@ export function Content() { handleConfigChange(fieldName, value)} /> ), }] : []), @@ -128,7 +129,7 @@ export function Content() { targets={targets} runningGame={runningGame} onSelect={setSelectedAppId} - onConfigChange={handleConfigChange} + onConfigChange={(fieldName, value) => handleConfigChange(fieldName, value, true)} onEnable={enable} onEnableAll={enableAll} onReset={resetSelected} -- cgit v1.2.3 From 790132668c4421c68c32bdc8fc9792b0d6028f97 Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Tue, 8 Sep 2026 22:18:13 -0400 Subject: I really dont want to but here you go little guy --- src/components/ConfigurationTab.tsx | 6 ++++++ src/components/GameConfigurationSelector.tsx | 29 ++++++++++++++++++++++++++-- src/components/ProfileDetails.tsx | 4 ++-- src/components/WorkaroundsSection.tsx | 21 +++++++++++++------- 4 files changed, 49 insertions(+), 11 deletions(-) (limited to 'src/components') diff --git a/src/components/ConfigurationTab.tsx b/src/components/ConfigurationTab.tsx index 2bb0f26..c41c941 100644 --- a/src/components/ConfigurationTab.tsx +++ b/src/components/ConfigurationTab.tsx @@ -33,6 +33,7 @@ export function ConfigurationTab({ const [detailAppId, setDetailAppId] = useState(null); const [focusFpsMultiplier, setFocusFpsMultiplier] = useState(false); const [focusDetailAction, setFocusDetailAction] = useState<"enable" | "fps" | null>(null); + const [focusConfiguredToggle, setFocusConfiguredToggle] = useState(false); const enableRef = useRef(null); const promptedRunningAppId = useRef(null); const closeDetails = useCallback(() => { @@ -41,6 +42,7 @@ export function ConfigurationTab({ setDetailAppId(null); }, []); const clearFpsFocusRequest = useCallback(() => setFocusFpsMultiplier(false), []); + const clearConfiguredToggleFocusRequest = useCallback(() => setFocusConfiguredToggle(false), []); useEffect(() => { if (!focusDetailAction) return; @@ -77,12 +79,15 @@ export function ConfigurationTab({ targets={targets} runningGame={runningGame} onSelect={(appid) => { + setFocusConfiguredToggle(false); setFocusDetailAction(targets.find((target) => target.appid === appid)?.configured ? "fps" : "enable"); onSelect(appid); setDetailAppId(appid); }} onEnableAll={onEnableAll} onResetAll={onResetAll} + focusConfiguredToggle={focusConfiguredToggle} + onConfiguredToggleFocused={clearConfiguredToggleFocusRequest} /> ); @@ -96,6 +101,7 @@ export function ConfigurationTab({ if (selectedTarget?.configured) { promptedRunningAppId.current = detailAppId; await onReset(); + setFocusConfiguredToggle(true); closeDetails(); } else if (detailAppId && await onEnable(detailAppId)) { setFocusFpsMultiplier(true); diff --git a/src/components/GameConfigurationSelector.tsx b/src/components/GameConfigurationSelector.tsx index 5f23b91..50b2cf8 100644 --- a/src/components/GameConfigurationSelector.tsx +++ b/src/components/GameConfigurationSelector.tsx @@ -1,5 +1,5 @@ import { ButtonItem, ConfirmModal, Field, PanelSectionRow, showModal } from "@decky/ui"; -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState, type RefObject } from "react"; import { RiArrowDownSFill, RiArrowUpSFill } from "react-icons/ri"; import { GameTarget } from "../hooks/useGameConfiguration"; @@ -9,6 +9,8 @@ interface Props { onSelect: (appid: string) => void; onEnableAll: () => Promise; onResetAll: () => Promise; + focusConfiguredToggle?: boolean; + onConfiguredToggleFocused?: () => void; } const CONFIGURED_COLLAPSED_KEY = "lsfg-configured-games-collapsed-v3"; @@ -40,12 +42,14 @@ function GameGroup({ collapsed, onToggle, onSelect, + toggleRef, }: { title: string; games: GameTarget[]; collapsed: boolean; onToggle: () => void; onSelect: (appid: string) => void; + toggleRef?: RefObject; }) { if (games.length === 0) return null; @@ -56,6 +60,7 @@ function GameGroup({
@@ -86,7 +91,15 @@ function GameGroup({ ); } -export function GameConfigurationSelector({ targets, runningGame, onSelect, onEnableAll, onResetAll }: Props) { +export function GameConfigurationSelector({ + targets, + runningGame, + onSelect, + onEnableAll, + onResetAll, + focusConfiguredToggle = false, + onConfiguredToggleFocused, +}: Props) { const sortGames = (games: GameTarget[]) => [...games].sort((a, b) => { if (a.appid === runningGame?.appid) return -1; if (b.appid === runningGame?.appid) return 1; @@ -102,6 +115,17 @@ export function GameConfigurationSelector({ targets, runningGame, onSelect, onEn const [configuredNonSteamCollapsed, toggleConfiguredNonSteam] = usePersistentCollapsed(`${CONFIGURED_COLLAPSED_KEY}-non-steam`); const [availableCollapsed, toggleAvailable] = usePersistentCollapsed(AVAILABLE_COLLAPSED_KEY); const [availableNonSteamCollapsed, toggleAvailableNonSteam] = usePersistentCollapsed(`${AVAILABLE_COLLAPSED_KEY}-non-steam`); + const configuredToggleRef = useRef(null); + + useEffect(() => { + if (!focusConfiguredToggle) return; + const frame = requestAnimationFrame(() => { + configuredToggleRef.current?.querySelector('[role="button"], button')?.focus(); + onConfiguredToggleFocused?.(); + }); + return () => cancelAnimationFrame(frame); + }, [configuredGames.length, focusConfiguredToggle, onConfiguredToggleFocused]); + const confirmResetAll = () => { showModal( setExpanded((value) => !value)} > - {expanded ? : } Details + {expanded ? : } Game Details {expanded && ( - + )} diff --git a/src/components/WorkaroundsSection.tsx b/src/components/WorkaroundsSection.tsx index e990784..d783620 100644 --- a/src/components/WorkaroundsSection.tsx +++ b/src/components/WorkaroundsSection.tsx @@ -10,7 +10,7 @@ interface WorkaroundsSectionProps { nonSteam: boolean; } -const WORKAROUNDS_COLLAPSED_KEY = "lsfg-workarounds-collapsed"; +const WORKAROUNDS_COLLAPSED_KEY = "lsfg-workarounds-collapsed-v2"; type ToggleWorkaroundField = Exclude; const TOGGLE_ROWS: readonly { @@ -20,19 +20,26 @@ const TOGGLE_ROWS: readonly { descriptionKey: string; description: string; }[] = [ + { + field: "disableSteamdeckMode", + labelKey: "CONFIG_DISABLE_STEAMDECK_MODE", + label: "Disable Steam Deck Mode", + descriptionKey: "CONFIG_DISABLE_STEAMDECK_MODE_DESC", + description: "Disables a game-specific Steam Deck compatibility switch. Requires game restart to apply.", + }, { field: "disableGamescopeWsi", labelKey: "CONFIG_DISABLE_GAMESCOPE_WSI", label: "Disable Gamescope WSI", descriptionKey: "CONFIG_DISABLE_GAMESCOPE_WSI_DESC", - description: "Adds ENABLE_GAMESCOPE_WSI=0 without changing HDR. Requires game restart to apply.", + description: "Adds ENABLE_GAMESCOPE_WSI=0. Requires game restart to apply.", }, { - field: "disableSteamdeckMode", - labelKey: "CONFIG_DISABLE_STEAMDECK_MODE", - label: "Disable Steam Deck Mode", - descriptionKey: "CONFIG_DISABLE_STEAMDECK_MODE_DESC", - description: "Disables a game-specific Steam Deck compatibility switch. Requires game restart to apply.", + field: "disableHdr", + labelKey: "CONFIG_DISABLE_HDR", + label: "Disable HDR", + descriptionKey: "CONFIG_DISABLE_HDR_DESC", + description: "Prevents DXVK from exposing HDR to the game. Requires game restart to apply.", }, { field: "disableVkbasalt", -- cgit v1.2.3 From 1b932fd69c3dba925e0cbf027e05508b2daf5e8c Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Wed, 9 Sep 2026 01:01:42 -0400 Subject: add back launcher script and correct pathing --- src/components/ConfigurationTab.tsx | 3 ++ src/components/Content.tsx | 2 ++ src/components/GameConfigurationControls.tsx | 8 +++++- src/components/WorkaroundsSection.tsx | 41 ++++++++++++++++++++-------- 4 files changed, 41 insertions(+), 13 deletions(-) (limited to 'src/components') diff --git a/src/components/ConfigurationTab.tsx b/src/components/ConfigurationTab.tsx index c41c941..62a971b 100644 --- a/src/components/ConfigurationTab.tsx +++ b/src/components/ConfigurationTab.tsx @@ -15,6 +15,7 @@ interface ConfigurationTabProps { onConfigChange: (fieldName: keyof ConfigurationData, value: boolean | number | string | string[]) => Promise; onEnable: (appid: string) => Promise; onEnableAll: () => Promise; + onRepair: (appid: string) => Promise; onReset: () => Promise; onResetAll: () => Promise; } @@ -27,6 +28,7 @@ export function ConfigurationTab({ onConfigChange, onEnable, onEnableAll, + onRepair, onReset, onResetAll, }: ConfigurationTabProps) { @@ -157,6 +159,7 @@ export function ConfigurationTab({ onFpsMultiplierFocused={clearFpsFocusRequest} showWorkarounds workaroundTarget={selectedTarget || undefined} + onRepairWorkaround={selectedTarget ? () => onRepair(selectedTarget.appid) : undefined} /> )} {selectedTarget?.configured && ( diff --git a/src/components/Content.tsx b/src/components/Content.tsx index 1d7d2d1..59dc514 100644 --- a/src/components/Content.tsx +++ b/src/components/Content.tsx @@ -39,6 +39,7 @@ export function Content() { save, enable, enableAll, + repair, resetSelected, resetAll, reload, @@ -132,6 +133,7 @@ export function Content() { onConfigChange={(fieldName, value) => handleConfigChange(fieldName, value, true)} onEnable={enable} onEnableAll={enableAll} + onRepair={repair} onReset={resetSelected} onResetAll={resetAll} /> diff --git a/src/components/GameConfigurationControls.tsx b/src/components/GameConfigurationControls.tsx index 58ccd02..7025f78 100644 --- a/src/components/GameConfigurationControls.tsx +++ b/src/components/GameConfigurationControls.tsx @@ -11,6 +11,7 @@ interface Props { onFpsMultiplierFocused?: () => void; showWorkarounds?: boolean; workaroundTarget?: Pick; + onRepairWorkaround?: () => Promise; } export function GameConfigurationControls({ @@ -20,6 +21,7 @@ export function GameConfigurationControls({ onFpsMultiplierFocused, showWorkarounds = false, workaroundTarget, + onRepairWorkaround, }: Props) { return ( <> @@ -31,7 +33,11 @@ export function GameConfigurationControls({ /> {showWorkarounds && workaroundTarget && ( - + )} ); diff --git a/src/components/WorkaroundsSection.tsx b/src/components/WorkaroundsSection.tsx index d783620..dbdd6a3 100644 --- a/src/components/WorkaroundsSection.tsx +++ b/src/components/WorkaroundsSection.tsx @@ -3,11 +3,12 @@ import { useEffect, useState } from "react"; import { RiArrowDownSFill, RiArrowUpSFill } from "react-icons/ri"; import { usePerAppWorkarounds } from "../hooks/usePerAppWorkarounds"; import t from "../i18n/i18n"; -import type { WorkaroundField } from "../utils/steamLaunchOptions"; +import type { WorkaroundField } from "../hooks/usePerAppWorkarounds"; interface WorkaroundsSectionProps { appId: string; nonSteam: boolean; + onRepair?: () => Promise; } const WORKAROUNDS_COLLAPSED_KEY = "lsfg-workarounds-collapsed-v2"; @@ -78,18 +79,28 @@ function usePersistentCollapsed() { return [collapsed, () => setCollapsed((value) => !value)] as const; } -export function WorkaroundsSection({ appId, nonSteam }: WorkaroundsSectionProps) { +export function WorkaroundsSection({ appId, nonSteam, onRepair }: WorkaroundsSectionProps) { const [collapsed, toggleCollapsed] = usePersistentCollapsed(); const { status, snapshot, refresh, update, error } = usePerAppWorkarounds(appId, nonSteam); - const state = snapshot?.parsed.state; - const issues = snapshot?.parsed.issues || []; - const controlsDisabled = status !== "ready" || state === undefined; + const [repairing, setRepairing] = useState(false); + const state = snapshot?.state; + const controlsDisabled = status !== "ready" || state === undefined || snapshot?.wrapperOwned !== true || snapshot.integrationInstalled !== true; const [fpsValue, setFpsValue] = useState(null); const effectiveFpsValue = fpsValue ?? state?.dxvkFrameRate ?? 0; const fpsLabel = effectiveFpsValue > 0 ? `${effectiveFpsValue} FPS` : t("CONFIG_BASE_FPS_CAP_OFF", "Off"); + const handleRepair = async () => { + if (!onRepair || repairing) return; + setRepairing(true); + try { + if (await onRepair()) await refresh(); + } finally { + setRepairing(false); + } + }; + useEffect(() => { setFpsValue(state?.dxvkFrameRate ?? null); }, [state?.dxvkFrameRate, status]); @@ -155,13 +166,19 @@ export function WorkaroundsSection({ appId, nonSteam }: WorkaroundsSectionProps) )} - {status === "ready" && issues.length > 0 && ( - - - + {status === "ready" && snapshot && (!snapshot.wrapperOwned || !snapshot.integrationInstalled) && ( + <> + + + + {onRepair && ( + + void handleRepair()}> + {repairing ? "Reinstalling..." : "Reinstall wrapper"} + + + )} + )} -- cgit v1.2.3 From 102a4a0f0ce4af303a12e7f6f1a4454f4e572a17 Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Wed, 9 Sep 2026 09:51:21 -0400 Subject: move all games --- src/components/GameConfigurationSelector.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) (limited to 'src/components') diff --git a/src/components/GameConfigurationSelector.tsx b/src/components/GameConfigurationSelector.tsx index 50b2cf8..df0c3e9 100644 --- a/src/components/GameConfigurationSelector.tsx +++ b/src/components/GameConfigurationSelector.tsx @@ -174,13 +174,6 @@ export function GameConfigurationSelector({ )} - {availableGames.length > 0 && ( - - - Enable all available games - - - )} + {availableGames.length > 0 && ( + + + Enable all available games + + + )} Date: Wed, 9 Sep 2026 19:16:30 -0400 Subject: refactor: unify flatpak targets with steam profiles --- src/components/ConfigurationTab.tsx | 22 ++- src/components/Content.tsx | 41 +++--- src/components/FlatpaksTab.tsx | 196 --------------------------- src/components/GameConfigurationControls.tsx | 3 +- src/components/GameConfigurationSelector.tsx | 101 +++----------- src/components/NowPlayingTab.tsx | 93 +++++++++++-- src/components/SetupTab.tsx | 142 ++++++++++++++++--- src/components/WorkaroundsSection.tsx | 6 +- src/components/index.ts | 2 - 9 files changed, 274 insertions(+), 332 deletions(-) delete mode 100644 src/components/FlatpaksTab.tsx (limited to 'src/components') diff --git a/src/components/ConfigurationTab.tsx b/src/components/ConfigurationTab.tsx index 62a971b..90c1a7b 100644 --- a/src/components/ConfigurationTab.tsx +++ b/src/components/ConfigurationTab.tsx @@ -14,7 +14,6 @@ interface ConfigurationTabProps { onSelect: (appid: string) => void; onConfigChange: (fieldName: keyof ConfigurationData, value: boolean | number | string | string[]) => Promise; onEnable: (appid: string) => Promise; - onEnableAll: () => Promise; onRepair: (appid: string) => Promise; onReset: () => Promise; onResetAll: () => Promise; @@ -27,7 +26,6 @@ export function ConfigurationTab({ onSelect, onConfigChange, onEnable, - onEnableAll, onRepair, onReset, onResetAll, @@ -86,7 +84,6 @@ export function ConfigurationTab({ onSelect(appid); setDetailAppId(appid); }} - onEnableAll={onEnableAll} onResetAll={onResetAll} focusConfiguredToggle={focusConfiguredToggle} onConfiguredToggleFocused={clearConfiguredToggleFocusRequest} @@ -96,8 +93,13 @@ export function ConfigurationTab({ } const profileLabel = selectedTarget?.name || "Game profile"; + const profileTransport = selectedTarget + ? selectedTarget.transport.kind === "flatpak" + ? "Non-Steam · Flatpak" + : selectedTarget.nonSteam ? "Non-Steam" : "Steam" + : "Game"; const profileDescription = selectedTarget - ? `${selectedTarget.nonSteam ? "Non-Steam" : "Steam"} · 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"}` : "Game is no longer available"; const handleProfileAction = async () => { if (selectedTarget?.configured) { @@ -151,6 +153,18 @@ export function ConfigurationTab({ )} + {selectedTarget?.configured && selectedTarget.transport.kind === "flatpak" && selectedTarget.flatpakSupport?.support_status !== "ready" && ( + + + void onRepair(selectedTarget.appid)} + > + Repair Flatpak support + + + + )} {selectedTarget?.configured && ( , - configuration: , - flatpak: , - configFile: , + games: , setup: , }; @@ -38,7 +34,6 @@ export function Content() { setSelectedAppId, save, enable, - enableAll, repair, resetSelected, resetAll, @@ -52,25 +47,25 @@ export function Content() { steamBranchStatus?.success === true && steamBranchStatus.installed && !steamBranchStatus.needs_switch; - const previousRunningState = useRef<{ appid: string; configured: boolean } | null>(null); + const previousRunningAppId = useRef(null); useEffect(() => { if (!setupComplete) { setTab("Setup"); return; } - setTab((current) => current === "Setup" ? (runningGame?.configured ? "NowPlaying" : "Configuration") : current); - }, [runningGame?.configured, setupComplete]); + setTab((current) => current === "Setup" ? (runningGame ? "NowPlaying" : "Games") : current); + }, [runningGame?.appid, setupComplete]); useEffect(() => { if (!setupComplete) return; - const current = runningGame ? { appid: runningGame.appid, configured: runningGame.configured } : null; - const previous = previousRunningState.current; - previousRunningState.current = current; - if (current?.appid && (current.appid !== previous?.appid || current.configured !== previous?.configured)) { - setTab(current.configured ? "NowPlaying" : "Configuration"); - } else if (!current && previous) { - setTab((currentTab) => currentTab === "NowPlaying" ? "Configuration" : currentTab); + const appid = runningGame?.appid || null; + const previous = previousRunningAppId.current; + previousRunningAppId.current = appid; + if (appid && appid !== previous) { + setTab("NowPlaying"); + } else if (!appid && previous) { + setTab((currentTab) => currentTab === "NowPlaying" ? "Games" : currentTab); } }, [runningGame?.appid, runningGame?.configured, setupComplete]); @@ -105,12 +100,13 @@ export function Content() { isUninstalling={isUninstalling} onInstall={onInstall} onUninstall={onUninstall} + flatpakRelevant={targets.some((target) => target.transport.kind === "flatpak")} /> ); const tabs = setupComplete ? [ - ...(runningGame?.configured ? [{ + ...(runningGame ? [{ id: "NowPlaying", title: tabIcons.nowPlaying, content: ( @@ -118,12 +114,14 @@ export function Content() { game={runningGame} config={config} onConfigChange={(fieldName, value) => handleConfigChange(fieldName, value)} + onEnable={enable} + onRepair={repair} /> ), }] : []), { - id: "Configuration", - title: tabIcons.configuration, + id: "Games", + title: tabIcons.games, content: ( handleConfigChange(fieldName, value, true)} onEnable={enable} - onEnableAll={enableAll} onRepair={repair} onReset={resetSelected} onResetAll={resetAll} /> ), }, - { id: "Flatpak", title: tabIcons.flatpak, content: }, - { id: "ConfigFile", title: tabIcons.configFile, content: }, // comment out for prod { id: "Setup", title: tabIcons.setup, content: setupContent }, ] : [ diff --git a/src/components/FlatpaksTab.tsx b/src/components/FlatpaksTab.tsx deleted file mode 100644 index 8aab940..0000000 --- a/src/components/FlatpaksTab.tsx +++ /dev/null @@ -1,196 +0,0 @@ -import { useEffect, useState } from "react"; -import { - ConfirmModal, - Field, - PanelSection, - PanelSectionRow, - ToggleField, - showModal, -} from "@decky/ui"; -import { - checkFlatpakExtensionStatus, - FlatpakApp, - FlatpakAppInfo, - FlatpakExtensionStatus, - getFlatpakApps, - installFlatpakExtension, - removeFlatpakAppOverride, - setFlatpakAppOverride, - uninstallFlatpakExtension, -} from "../api/lsfgApi"; -import { showErrorToast } from "../utils/toastUtils"; -import t from "../i18n/i18n"; - -const runtimeVersions = [ - { version: "23.08", key: "installed_23_08" }, - { version: "24.08", key: "installed_24_08" }, - { version: "25.08", key: "installed_25_08" }, -] as const; - -interface RuntimeRowProps { - version: string; - installed: boolean; - busy: boolean; - onAction: () => void; -} - -function RuntimeRow({ version, installed, busy, onAction }: RuntimeRowProps) { - return ( - - onAction()} - disabled={busy} - /> - - ); -} - -interface AppRowProps { - app: FlatpakApp; - runtimeReady: boolean; - busy: boolean; - onToggle: () => void; -} - -function AppRow({ app, runtimeReady, busy, onToggle }: AppRowProps) { - const configured = app.has_filesystem_override && app.has_env_override; - const partial = app.has_filesystem_override || app.has_env_override; - const status = configured - ? runtimeReady - ? t("FLATPAK_STATUS_READY", "Ready") - : t("FLATPAK_STATUS_RUNTIME_MISSING", "Runtime missing") - : partial - ? t("FLATPAK_STATUS_PARTIAL", "Partial") - : t("FLATPAK_STATUS_NOT_ENABLED", "Not enabled"); - - return ( - - - - ); -} - -export function FlatpaksTab() { - const [extensionStatus, setExtensionStatus] = useState(null); - const [apps, setApps] = useState(null); - const [loading, setLoading] = useState(true); - const [operation, setOperation] = useState(null); - const [error, setError] = useState(null); - const runtimeReady = extensionStatus?.success === true - && runtimeVersions.some(({ key }) => extensionStatus[key]); - - const load = async () => { - setLoading(true); - try { - const [nextStatus, nextApps] = await Promise.all([ - checkFlatpakExtensionStatus(), - getFlatpakApps(), - ]); - setExtensionStatus(nextStatus); - setApps(nextApps); - } catch (loadError) { - setError(String(loadError)); - } finally { - setLoading(false); - } - }; - - useEffect(() => { - void load(); - }, []); - - const runExtensionOperation = async (version: string, installed: boolean) => { - const action = installed ? "uninstall" : "install"; - setOperation(`${action}-${version}`); - setError(null); - try { - const result = installed - ? await uninstallFlatpakExtension(version) - : await installFlatpakExtension(version); - if (!result.success) throw new Error(result.error || result.message); - setExtensionStatus(await checkFlatpakExtensionStatus()); - } catch (operationError) { - const message = String(operationError); - setError(message); - showErrorToast("Flatpak operation failed", message); - } finally { - setOperation(null); - } - }; - - const confirmExtensionOperation = (version: string, installed: boolean) => { - if (!installed) { - void runExtensionOperation(version, false); - return; - } - showModal( - void runExtensionOperation(version, true)} - onCancel={() => {}} - />, - ); - }; - - const toggleApp = async (app: FlatpakApp) => { - const configured = app.has_filesystem_override && app.has_env_override; - setOperation(`app-${app.app_id}`); - setError(null); - try { - const result = configured - ? await removeFlatpakAppOverride(app.app_id) - : await setFlatpakAppOverride(app.app_id); - if (!result.success) throw new Error(result.error || result.message); - setApps(await getFlatpakApps()); - } catch (operationError) { - const message = String(operationError); - setError(message); - showErrorToast("Flatpak override failed", message); - } finally { - setOperation(null); - } - }; - - if (loading) { - return ; - } - - return ( - <> - - {error && } - {extensionStatus?.success ? runtimeVersions.map(({ version, key }) => ( - confirmExtensionOperation(version, extensionStatus[key])} - /> - )) : } - - - - {apps?.success ? apps.apps.length ? apps.apps.map((app) => ( - void toggleApp(app)} - /> - )) : : } - - - ); -} diff --git a/src/components/GameConfigurationControls.tsx b/src/components/GameConfigurationControls.tsx index 7025f78..6bea2e0 100644 --- a/src/components/GameConfigurationControls.tsx +++ b/src/components/GameConfigurationControls.tsx @@ -10,7 +10,7 @@ interface Props { autoFocusFpsMultiplier?: boolean; onFpsMultiplierFocused?: () => void; showWorkarounds?: boolean; - workaroundTarget?: Pick; + workaroundTarget?: Pick; onRepairWorkaround?: () => Promise; } @@ -36,6 +36,7 @@ export function GameConfigurationControls({ )} diff --git a/src/components/GameConfigurationSelector.tsx b/src/components/GameConfigurationSelector.tsx index df0c3e9..2a92c67 100644 --- a/src/components/GameConfigurationSelector.tsx +++ b/src/components/GameConfigurationSelector.tsx @@ -7,14 +7,13 @@ interface Props { targets: GameTarget[]; runningGame: GameTarget | null; onSelect: (appid: string) => void; - onEnableAll: () => Promise; onResetAll: () => Promise; focusConfiguredToggle?: boolean; onConfiguredToggleFocused?: () => void; } -const CONFIGURED_COLLAPSED_KEY = "lsfg-configured-games-collapsed-v3"; -const AVAILABLE_COLLAPSED_KEY = "lsfg-available-games-collapsed-v2"; +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(() => { @@ -36,6 +35,11 @@ function usePersistentCollapsed(key: string) { return [collapsed, () => setCollapsed((value) => !value)] as const; } +function targetDescription(game: GameTarget): string { + if (game.transport.kind === "flatpak") return "Non-Steam · Flatpak"; + return game.nonSteam ? "Non-Steam" : "Steam"; +} + function GameGroup({ title, games, @@ -56,7 +60,7 @@ function GameGroup({ return ( <> - +
- {collapsed ? ( - - ) : ( - - )} + {collapsed ? : }
@@ -81,7 +81,7 @@ function GameGroup({ onSelect(game.appid)} highlightOnFocus /> @@ -95,7 +95,6 @@ export function GameConfigurationSelector({ targets, runningGame, onSelect, - onEnableAll, onResetAll, focusConfiguredToggle = false, onConfiguredToggleFocused, @@ -105,26 +104,20 @@ export function GameConfigurationSelector({ if (b.appid === runningGame?.appid) return 1; return a.name.localeCompare(b.name); }); - const configuredGames = sortGames(targets.filter((game) => game.configured)); + const enabledGames = sortGames(targets.filter((game) => game.configured)); const availableGames = sortGames(targets.filter((game) => !game.configured)); - const configuredSteamGames = configuredGames.filter((game) => !game.nonSteam); - const configuredNonSteamGames = configuredGames.filter((game) => game.nonSteam); - const availableSteamGames = availableGames.filter((game) => !game.nonSteam); - const availableNonSteamGames = availableGames.filter((game) => game.nonSteam); - const [configuredCollapsed, toggleConfigured] = usePersistentCollapsed(CONFIGURED_COLLAPSED_KEY); - const [configuredNonSteamCollapsed, toggleConfiguredNonSteam] = usePersistentCollapsed(`${CONFIGURED_COLLAPSED_KEY}-non-steam`); + const [enabledCollapsed, toggleEnabled] = usePersistentCollapsed(ENABLED_COLLAPSED_KEY); const [availableCollapsed, toggleAvailable] = usePersistentCollapsed(AVAILABLE_COLLAPSED_KEY); - const [availableNonSteamCollapsed, toggleAvailableNonSteam] = usePersistentCollapsed(`${AVAILABLE_COLLAPSED_KEY}-non-steam`); - const configuredToggleRef = useRef(null); + const enabledToggleRef = useRef(null); useEffect(() => { if (!focusConfiguredToggle) return; const frame = requestAnimationFrame(() => { - configuredToggleRef.current?.querySelector('[role="button"], button')?.focus(); + enabledToggleRef.current?.querySelector('[role="button"], button')?.focus(); onConfiguredToggleFocused?.(); }); return () => cancelAnimationFrame(frame); - }, [configuredGames.length, focusConfiguredToggle, onConfiguredToggleFocused]); + }, [enabledGames.length, focusConfiguredToggle, onConfiguredToggleFocused]); const confirmResetAll = () => { showModal( @@ -137,79 +130,29 @@ export function GameConfigurationSelector({ />, ); }; - const confirmEnableAll = () => { - showModal( - void onEnableAll()} - onCancel={() => {}} - />, - ); - }; return ( <> - {targets.length === 0 && ( )} - - - {availableGames.length > 0 && ( - - - Enable all available games - - - )} Promise; + onConfigChange: ( + fieldName: keyof ConfigurationData, + value: boolean | number | string | string[], + ) => Promise; + onEnable: (appid: string) => Promise; + onRepair: (appid: string) => Promise; } -export function NowPlayingTab({ game, config, onConfigChange }: Props) { +function targetDescription(game: GameTarget): string { + if (game.transport.kind === "flatpak") return "Non-Steam · Flatpak"; + return game.nonSteam ? "Non-Steam" : "Steam"; +} + +export function NowPlayingTab({ + game, + config, + onConfigChange, + onEnable, + onRepair, +}: Props) { + const [busy, setBusy] = useState(false); + const supportNeedsRepair = + game.configured && + game.transport.kind === "flatpak" && + game.flatpakSupport?.support_status !== "ready"; + + const handleEnable = async () => { + if (busy) return; + setBusy(true); + try { + await onEnable(game.appid); + } finally { + setBusy(false); + } + }; + + const handleRepair = async () => { + if (busy) return; + setBusy(true); + try { + await onRepair(game.appid); + } finally { + setBusy(false); + } + }; + return ( - + - + - + {!game.configured && ( + + + + + + void handleEnable()}> + {busy ? "Enabling..." : "Enable LSFG-VK"} + + + + )} + {game.configured && supportNeedsRepair && ( + + + + + + void handleRepair()}> + {busy ? "Repairing..." : "Repair Flatpak support"} + + + + )} + {game.configured && ( + + )} ); } diff --git a/src/components/SetupTab.tsx b/src/components/SetupTab.tsx index 98d6e78..9c854e1 100644 --- a/src/components/SetupTab.tsx +++ b/src/components/SetupTab.tsx @@ -1,5 +1,11 @@ -import { PanelSection } from "@decky/ui"; -import type { SteamBranchStatus } from "../api/lsfgApi"; +import { ButtonItem, ConfirmModal, Field, PanelSection, PanelSectionRow, showModal } from "@decky/ui"; +import { useEffect, useState } from "react"; +import { + getFlatpakSupportStatus, + removePluginOwnedFlatpakExtensions, + type FlatpakExtensionStatus, + type SteamBranchStatus, +} from "../api/lsfgApi"; import { InstallationButton } from "./InstallationButton"; import { StatusDisplay } from "./StatusDisplay"; @@ -13,6 +19,104 @@ interface SetupTabProps { isUninstalling: boolean; onInstall: () => void; onUninstall: () => void; + flatpakRelevant: boolean; +} + +function FlatpakSupportDiagnostics({ relevant }: { relevant: boolean }) { + const [status, setStatus] = useState(null); + const [advanced, setAdvanced] = useState(false); + const [busy, setBusy] = useState(false); + + const refresh = async () => { + try { + setStatus(await getFlatpakSupportStatus()); + } catch (error) { + setStatus({ + success: false, + message: "", + error: String(error), + available: false, + extension_id: "", + supported_branches: [], + installed_branches: [], + owned_branches: [], + ownership_uncertain: false, + }); + } + }; + + useEffect(() => { + if (relevant) void refresh(); + }, [relevant]); + + if (!relevant || !status?.available) return null; + + const confirmCleanup = () => { + showModal( + { + setBusy(true); + try { + await removePluginOwnedFlatpakExtensions(); + await refresh(); + } finally { + setBusy(false); + } + }} + onCancel={() => {}} + />, + ); + }; + + return ( + + + + + + setAdvanced((value) => !value)}> + {advanced ? "Hide runtime details" : "Show runtime details"} + + + {advanced && ( + <> + {status.supported_branches.map((branch) => ( + + + + ))} + {status.ownership_uncertain && ( + + + + )} + + + {busy ? "Removing..." : "Remove plugin-installed extensions"} + + + + )} + + ); } export function SetupTab({ @@ -25,22 +129,26 @@ export function SetupTab({ isUninstalling, onInstall, onUninstall, + flatpakRelevant, }: SetupTabProps) { return ( - - - - + <> + + + + + + ); } diff --git a/src/components/WorkaroundsSection.tsx b/src/components/WorkaroundsSection.tsx index dbdd6a3..5587392 100644 --- a/src/components/WorkaroundsSection.tsx +++ b/src/components/WorkaroundsSection.tsx @@ -1,6 +1,7 @@ import { ButtonItem, Field, PanelSectionRow, SliderField, ToggleField } from "@decky/ui"; import { useEffect, useState } from "react"; import { RiArrowDownSFill, RiArrowUpSFill } from "react-icons/ri"; +import type { TargetTransport } from "../api/lsfgApi"; import { usePerAppWorkarounds } from "../hooks/usePerAppWorkarounds"; import t from "../i18n/i18n"; import type { WorkaroundField } from "../hooks/usePerAppWorkarounds"; @@ -8,6 +9,7 @@ import type { WorkaroundField } from "../hooks/usePerAppWorkarounds"; interface WorkaroundsSectionProps { appId: string; nonSteam: boolean; + transport: TargetTransport; onRepair?: () => Promise; } @@ -79,9 +81,9 @@ function usePersistentCollapsed() { return [collapsed, () => setCollapsed((value) => !value)] as const; } -export function WorkaroundsSection({ appId, nonSteam, onRepair }: WorkaroundsSectionProps) { +export function WorkaroundsSection({ appId, nonSteam, transport, onRepair }: WorkaroundsSectionProps) { const [collapsed, toggleCollapsed] = usePersistentCollapsed(); - const { status, snapshot, refresh, update, error } = usePerAppWorkarounds(appId, nonSteam); + const { status, snapshot, refresh, update, error } = usePerAppWorkarounds(appId, nonSteam, transport); const [repairing, setRepairing] = useState(false); const state = snapshot?.state; const controlsDisabled = status !== "ready" || state === undefined || snapshot?.wrapperOwned !== true || snapshot.integrationInstalled !== true; diff --git a/src/components/index.ts b/src/components/index.ts index 37a8edb..7c3ee0a 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -5,8 +5,6 @@ export { ConfigurationSection } from "./ConfigurationSection"; export { FpsMultiplierControl } from "./FpsMultiplierControl"; export { ConfigurationTab } from "./ConfigurationTab"; export { SetupTab } from "./SetupTab"; -export { ConfigFileTab } from "./ConfigFileTab"; -export { FlatpaksTab } from "./FlatpaksTab"; export { GameConfigurationSelector } from "./GameConfigurationSelector"; export { GameConfigurationControls } from "./GameConfigurationControls"; export { NowPlayingTab } from "./NowPlayingTab"; -- cgit v1.2.3 From fb4d053213bdbda271a54b517a11a89c4780f80a Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Wed, 9 Sep 2026 20:45:20 -0400 Subject: fix: restore profile and Flatpak controls --- src/components/ConfigurationTab.tsx | 3 ++ src/components/Content.tsx | 11 +++- src/components/GameConfigurationSelector.tsx | 40 ++++++++++++++ src/components/SetupTab.tsx | 81 ++++++++++++++++++++++++---- src/components/index.ts | 1 + 5 files changed, 124 insertions(+), 12 deletions(-) (limited to 'src/components') diff --git a/src/components/ConfigurationTab.tsx b/src/components/ConfigurationTab.tsx index 90c1a7b..8f8690c 100644 --- a/src/components/ConfigurationTab.tsx +++ b/src/components/ConfigurationTab.tsx @@ -14,6 +14,7 @@ interface ConfigurationTabProps { onSelect: (appid: string) => void; onConfigChange: (fieldName: keyof ConfigurationData, value: boolean | number | string | string[]) => Promise; onEnable: (appid: string) => Promise; + onEnableAll: () => Promise; onRepair: (appid: string) => Promise; onReset: () => Promise; onResetAll: () => Promise; @@ -26,6 +27,7 @@ export function ConfigurationTab({ onSelect, onConfigChange, onEnable, + onEnableAll, onRepair, onReset, onResetAll, @@ -84,6 +86,7 @@ export function ConfigurationTab({ onSelect(appid); setDetailAppId(appid); }} + onEnableAll={onEnableAll} onResetAll={onResetAll} focusConfiguredToggle={focusConfiguredToggle} onConfiguredToggleFocused={clearConfiguredToggleFocusRequest} diff --git a/src/components/Content.tsx b/src/components/Content.tsx index ff2376b..58512d5 100644 --- a/src/components/Content.tsx +++ b/src/components/Content.tsx @@ -1,18 +1,20 @@ import { Tabs } from "@decky/ui"; import { useEffect, useRef, useState } from "react"; -import { FaGamepad, FaList, FaTools } from "react-icons/fa"; +import { FaFileAlt, FaGamepad, FaList, FaTools } from "react-icons/fa"; import { ConfigurationData } from "../config/configSchema"; import { tabStyles } from "../styles"; import { useGameConfiguration } from "../hooks/useGameConfiguration"; import { useInstallationActions } from "../hooks/useInstallationActions"; import { useInstallationStatus } from "../hooks/useLsfgHooks"; import { ConfigurationTab } from "./ConfigurationTab"; +import { ConfigFileTab } from "./ConfigFileTab"; import { NowPlayingTab } from "./NowPlayingTab"; import { SetupTab } from "./SetupTab"; const tabIcons = { nowPlaying: , games: , + configFile: , setup: , }; @@ -34,6 +36,7 @@ export function Content() { setSelectedAppId, save, enable, + enableAll, repair, resetSelected, resetAll, @@ -130,12 +133,18 @@ export function Content() { onSelect={setSelectedAppId} onConfigChange={(fieldName, value) => handleConfigChange(fieldName, value, true)} onEnable={enable} + onEnableAll={enableAll} onRepair={repair} onReset={resetSelected} onResetAll={resetAll} /> ), }, + { + id: "ConfigFile", + title: tabIcons.configFile, + content: , + }, { id: "Setup", title: tabIcons.setup, content: setupContent }, ] : [ diff --git a/src/components/GameConfigurationSelector.tsx b/src/components/GameConfigurationSelector.tsx index 2a92c67..92eacba 100644 --- a/src/components/GameConfigurationSelector.tsx +++ b/src/components/GameConfigurationSelector.tsx @@ -7,6 +7,7 @@ interface Props { targets: GameTarget[]; runningGame: GameTarget | null; onSelect: (appid: string) => void; + onEnableAll: () => Promise; onResetAll: () => Promise; focusConfiguredToggle?: boolean; onConfiguredToggleFocused?: () => void; @@ -95,6 +96,7 @@ export function GameConfigurationSelector({ targets, runningGame, onSelect, + onEnableAll, onResetAll, focusConfiguredToggle = false, onConfiguredToggleFocused, @@ -131,8 +133,39 @@ export function GameConfigurationSelector({ ); }; + const confirmEnableAll = () => { + showModal( + void onEnableAll()} + onCancel={() => {}} + />, + ); + }; + return ( <> + {targets.length === 0 && ( @@ -153,6 +186,13 @@ export function GameConfigurationSelector({ onToggle={toggleAvailable} onSelect={onSelect} /> + {availableGames.length > 0 && ( + + + Enable all available games + + + )} (null); const [advanced, setAdvanced] = useState(false); - const [busy, setBusy] = useState(false); + const [operation, setOperation] = useState(null); const refresh = async () => { try { @@ -51,6 +53,51 @@ function FlatpakSupportDiagnostics({ relevant }: { relevant: boolean }) { if (!relevant || !status?.available) return null; + const runExtensionOperation = async (version: string, enabled: boolean) => { + const operationKey = `${enabled ? "enable" : "disable"}-${version}`; + setOperation(operationKey); + try { + const result = await setFlatpakExtensionEnabled(version, enabled); + if (!result.success) throw new Error(result.error || result.message || "Flatpak runtime update failed"); + await refresh(); + } catch (error) { + showErrorToast("Flatpak runtime update failed", String(error)); + } finally { + setOperation(null); + } + }; + + const confirmDisable = (version: string) => { + showModal( + void runExtensionOperation(version, false)} + onCancel={() => {}} + />, + ); + }; + + const handleExtensionToggle = (version: string, enabled: boolean) => { + const installed = status.installed_branches.includes(version); + const owned = status.owned_branches.includes(version); + if (!enabled && installed && !owned) { + showErrorToast( + "Flatpak runtime preserved", + `${version} was not installed by this plugin, so it will remain installed.`, + ); + void refresh(); + return; + } + if (!enabled && installed && owned) { + confirmDisable(version); + return; + } + void runExtensionOperation(version, enabled); + }; + const confirmCleanup = () => { showModal( { - setBusy(true); + setOperation("cleanup"); try { - await removePluginOwnedFlatpakExtensions(); + const result = await removePluginOwnedFlatpakExtensions(); + if (!result.success) throw new Error(result.error || result.message || "Flatpak cleanup failed"); await refresh(); + } catch (error) { + showErrorToast("Flatpak cleanup failed", String(error)); } finally { - setBusy(false); + setOperation(null); } }} onCancel={() => {}} @@ -89,13 +139,22 @@ function FlatpakSupportDiagnostics({ relevant }: { relevant: boolean }) { <> {status.supported_branches.map((branch) => ( - handleExtensionToggle(branch, enabled)} + disabled={operation !== null || status.ownership_uncertain} /> ))} @@ -107,10 +166,10 @@ function FlatpakSupportDiagnostics({ relevant }: { relevant: boolean }) { - {busy ? "Removing..." : "Remove plugin-installed extensions"} + {operation === "cleanup" ? "Removing..." : "Remove plugin-installed extensions"} diff --git a/src/components/index.ts b/src/components/index.ts index 7c3ee0a..6856e76 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -4,6 +4,7 @@ export { InstallationButton } from "./InstallationButton"; export { ConfigurationSection } from "./ConfigurationSection"; export { FpsMultiplierControl } from "./FpsMultiplierControl"; export { ConfigurationTab } from "./ConfigurationTab"; +export { ConfigFileTab } from "./ConfigFileTab"; export { SetupTab } from "./SetupTab"; export { GameConfigurationSelector } from "./GameConfigurationSelector"; export { GameConfigurationControls } from "./GameConfigurationControls"; -- cgit v1.2.3 From bc75bb93d5aa9aa176262a138f47d3a1d53afbcb Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Thu, 10 Sep 2026 00:25:58 -0400 Subject: fix: simplify Flatpak runtime toggles --- src/components/SetupTab.tsx | 76 ++------------------------------------------- 1 file changed, 3 insertions(+), 73 deletions(-) (limited to 'src/components') diff --git a/src/components/SetupTab.tsx b/src/components/SetupTab.tsx index df43c5e..aec0e93 100644 --- a/src/components/SetupTab.tsx +++ b/src/components/SetupTab.tsx @@ -1,8 +1,7 @@ -import { ButtonItem, ConfirmModal, Field, PanelSection, PanelSectionRow, ToggleField, showModal } from "@decky/ui"; +import { ButtonItem, Field, PanelSection, PanelSectionRow, ToggleField } from "@decky/ui"; import { useEffect, useState } from "react"; import { getFlatpakSupportStatus, - removePluginOwnedFlatpakExtensions, setFlatpakExtensionEnabled, type FlatpakExtensionStatus, type SteamBranchStatus, @@ -41,8 +40,6 @@ function FlatpakSupportDiagnostics({ relevant }: { relevant: boolean }) { extension_id: "", supported_branches: [], installed_branches: [], - owned_branches: [], - ownership_uncertain: false, }); } }; @@ -67,61 +64,10 @@ function FlatpakSupportDiagnostics({ relevant }: { relevant: boolean }) { } }; - const confirmDisable = (version: string) => { - showModal( - void runExtensionOperation(version, false)} - onCancel={() => {}} - />, - ); - }; - const handleExtensionToggle = (version: string, enabled: boolean) => { - const installed = status.installed_branches.includes(version); - const owned = status.owned_branches.includes(version); - if (!enabled && installed && !owned) { - showErrorToast( - "Flatpak runtime preserved", - `${version} was not installed by this plugin, so it will remain installed.`, - ); - void refresh(); - return; - } - if (!enabled && installed && owned) { - confirmDisable(version); - return; - } void runExtensionOperation(version, enabled); }; - const confirmCleanup = () => { - showModal( - { - setOperation("cleanup"); - try { - const result = await removePluginOwnedFlatpakExtensions(); - if (!result.success) throw new Error(result.error || result.message || "Flatpak cleanup failed"); - await refresh(); - } catch (error) { - showErrorToast("Flatpak cleanup failed", String(error)); - } finally { - setOperation(null); - } - }} - onCancel={() => {}} - />, - ); - }; - return ( @@ -147,31 +93,15 @@ function FlatpakSupportDiagnostics({ relevant }: { relevant: boolean }) { : operation === `disable-${branch}` ? "Uninstalling..." : status.installed_branches.includes(branch) - ? status.owned_branches.includes(branch) - ? "Installed · plugin-owned" - : "Installed · pre-existing (preserved)" + ? "Installed" : "Not installed" } checked={status.installed_branches.includes(branch)} onChange={(enabled) => handleExtensionToggle(branch, enabled)} - disabled={operation !== null || status.ownership_uncertain} + disabled={operation !== null} /> ))} - {status.ownership_uncertain && ( - - - - )} - - - {operation === "cleanup" ? "Removing..." : "Remove plugin-installed extensions"} - - )} -- cgit v1.2.3 From 28ebc17785a8cca52f289b74261d1f310fd35904 Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Thu, 10 Sep 2026 07:08:19 -0400 Subject: fix: detect wrapped Flatpak shortcuts --- src/components/Content.tsx | 1 - src/components/SetupTab.tsx | 68 +++++++++++++++++++-------------------------- 2 files changed, 28 insertions(+), 41 deletions(-) (limited to 'src/components') diff --git a/src/components/Content.tsx b/src/components/Content.tsx index 58512d5..d4f54e9 100644 --- a/src/components/Content.tsx +++ b/src/components/Content.tsx @@ -103,7 +103,6 @@ export function Content() { isUninstalling={isUninstalling} onInstall={onInstall} onUninstall={onUninstall} - flatpakRelevant={targets.some((target) => target.transport.kind === "flatpak")} /> ); diff --git a/src/components/SetupTab.tsx b/src/components/SetupTab.tsx index aec0e93..6bf1fad 100644 --- a/src/components/SetupTab.tsx +++ b/src/components/SetupTab.tsx @@ -1,4 +1,4 @@ -import { ButtonItem, Field, PanelSection, PanelSectionRow, ToggleField } from "@decky/ui"; +import { Field, PanelSection, PanelSectionRow, ToggleField } from "@decky/ui"; import { useEffect, useState } from "react"; import { getFlatpakSupportStatus, @@ -20,12 +20,10 @@ interface SetupTabProps { isUninstalling: boolean; onInstall: () => void; onUninstall: () => void; - flatpakRelevant: boolean; } -function FlatpakSupportDiagnostics({ relevant }: { relevant: boolean }) { +function FlatpakSupportDiagnostics() { const [status, setStatus] = useState(null); - const [advanced, setAdvanced] = useState(false); const [operation, setOperation] = useState(null); const refresh = async () => { @@ -45,10 +43,10 @@ function FlatpakSupportDiagnostics({ relevant }: { relevant: boolean }) { }; useEffect(() => { - if (relevant) void refresh(); - }, [relevant]); + void refresh(); + }, []); - if (!relevant || !status?.available) return null; + if (!status?.available) return null; const runExtensionOperation = async (version: string, enabled: boolean) => { const operationKey = `${enabled ? "enable" : "disable"}-${version}`; @@ -69,41 +67,32 @@ function FlatpakSupportDiagnostics({ relevant }: { relevant: boolean }) { }; return ( - + - - setAdvanced((value) => !value)}> - {advanced ? "Hide runtime details" : "Show runtime details"} - - - {advanced && ( - <> - {status.supported_branches.map((branch) => ( - - handleExtensionToggle(branch, enabled)} - disabled={operation !== null} - /> - - ))} - - )} + {status.supported_branches.map((branch) => ( + + handleExtensionToggle(branch, enabled)} + disabled={operation !== null} + /> + + ))} ); } @@ -118,7 +107,6 @@ export function SetupTab({ isUninstalling, onInstall, onUninstall, - flatpakRelevant, }: SetupTabProps) { return ( <> @@ -137,7 +125,7 @@ export function SetupTab({ onUninstall={onUninstall} /> - + ); } -- cgit v1.2.3 From 450d3e5e6612d467a00bb937538fded640c66ecb Mon Sep 17 00:00:00 2001 From: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> Date: Thu, 10 Sep 2026 07:24:05 -0400 Subject: refactor: simplify migration implementation --- src/components/Content.tsx | 73 +++++++---------- src/components/InstallationButton.tsx | 38 --------- src/components/SetupTab.tsx | 145 +++++++++++++++++++--------------- src/components/StatusDisplay.tsx | 41 ---------- src/components/index.ts | 2 - 5 files changed, 111 insertions(+), 188 deletions(-) delete mode 100644 src/components/InstallationButton.tsx delete mode 100644 src/components/StatusDisplay.tsx (limited to 'src/components') diff --git a/src/components/Content.tsx b/src/components/Content.tsx index d4f54e9..d92ef86 100644 --- a/src/components/Content.tsx +++ b/src/components/Content.tsx @@ -2,12 +2,11 @@ import { Tabs } from "@decky/ui"; import { useEffect, useRef, useState } from "react"; import { FaFileAlt, FaGamepad, FaList, FaTools } from "react-icons/fa"; import { ConfigurationData } from "../config/configSchema"; -import { tabStyles } from "../styles"; import { useGameConfiguration } from "../hooks/useGameConfiguration"; -import { useInstallationActions } from "../hooks/useInstallationActions"; -import { useInstallationStatus } from "../hooks/useLsfgHooks"; -import { ConfigurationTab } from "./ConfigurationTab"; +import { useInstallation } from "../hooks/useLsfgHooks"; +import { tabStyles } from "../styles"; import { ConfigFileTab } from "./ConfigFileTab"; +import { ConfigurationTab } from "./ConfigurationTab"; import { NowPlayingTab } from "./NowPlayingTab"; import { SetupTab } from "./SetupTab"; @@ -19,16 +18,6 @@ const tabIcons = { }; export function Content() { - const { - isInstalled, - installationStatus, - setIsInstalled, - setInstallationStatus, - losslessScalingInstalled, - losslessScalingStatus, - steamBranchStatus, - checkInstallation, - } = useInstallationStatus(); const { config, targets, @@ -42,15 +31,25 @@ export function Content() { resetAll, reload, } = useGameConfiguration(); - const { isInstalling, isUninstalling, handleInstall, handleUninstall } = useInstallationActions(); + const { + isInstalled, + installationStatus, + losslessScalingInstalled, + losslessScalingStatus, + steamBranchStatus, + isInstalling, + isUninstalling, + install, + uninstall, + } = useInstallation(reload); const [tab, setTab] = useState("Setup"); + const previousRunningAppId = useRef(null); const setupComplete = isInstalled && losslessScalingInstalled && steamBranchStatus?.success === true && steamBranchStatus.installed && !steamBranchStatus.needs_switch; - const previousRunningAppId = useRef(null); useEffect(() => { if (!setupComplete) { @@ -65,10 +64,9 @@ export function Content() { const appid = runningGame?.appid || null; const previous = previousRunningAppId.current; previousRunningAppId.current = appid; - if (appid && appid !== previous) { - setTab("NowPlaying"); - } else if (!appid && previous) { - setTab((currentTab) => currentTab === "NowPlaying" ? "Games" : currentTab); + if (appid && appid !== previous) setTab("NowPlaying"); + else if (!appid && previous) { + setTab((current) => current === "NowPlaying" ? "Games" : current); } }, [runningGame?.appid, runningGame?.configured, setupComplete]); @@ -80,19 +78,9 @@ export function Content() { fieldName: keyof ConfigurationData, value: boolean | number | string | string[], cleanupLaunchOptions = false, - ) => { - await save({ ...config, [fieldName]: value }, cleanupLaunchOptions); - }; - - const onInstall = () => { - void handleInstall(setIsInstalled, setInstallationStatus, reload, checkInstallation); - }; - - const onUninstall = () => { - void handleUninstall(setIsInstalled, setInstallationStatus, checkInstallation); - }; + ) => save({ ...config, [fieldName]: value }, cleanupLaunchOptions); - const setupContent = ( + const setup = ( void install()} + onUninstall={() => void uninstall()} + flatpakRelevant={targets.some((target) => target.transport.kind === "flatpak")} /> ); @@ -115,7 +104,7 @@ export function Content() { handleConfigChange(fieldName, value)} + onConfigChange={(field, value) => handleConfigChange(field, value)} onEnable={enable} onRepair={repair} /> @@ -130,7 +119,7 @@ export function Content() { targets={targets} runningGame={runningGame} onSelect={setSelectedAppId} - onConfigChange={(fieldName, value) => handleConfigChange(fieldName, value, true)} + onConfigChange={(field, value) => handleConfigChange(field, value, true)} onEnable={enable} onEnableAll={enableAll} onRepair={repair} @@ -139,16 +128,10 @@ export function Content() { /> ), }, - { - id: "ConfigFile", - title: tabIcons.configFile, - content: , - }, - { id: "Setup", title: tabIcons.setup, content: setupContent }, + { id: "ConfigFile", title: tabIcons.configFile, content: }, + { id: "Setup", title: tabIcons.setup, content: setup }, ] - : [ - { id: "Setup", title: tabIcons.setup, content: setupContent }, - ]; + : [{ id: "Setup", title: tabIcons.setup, content: setup }]; return (
void; - onUninstall: () => void; -} - -export function InstallationButton({ - isInstalled, - isInstalling, - isUninstalling, - onInstall, - onUninstall -}: InstallationButtonProps) { - const label = 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 ( - - - {label} - - - ); -} diff --git a/src/components/SetupTab.tsx b/src/components/SetupTab.tsx index 6bf1fad..e936049 100644 --- a/src/components/SetupTab.tsx +++ b/src/components/SetupTab.tsx @@ -1,4 +1,4 @@ -import { Field, PanelSection, PanelSectionRow, ToggleField } from "@decky/ui"; +import { ButtonItem, Field, PanelSection, PanelSectionRow, ToggleField } from "@decky/ui"; import { useEffect, useState } from "react"; import { getFlatpakSupportStatus, @@ -6,8 +6,7 @@ import { type FlatpakExtensionStatus, type SteamBranchStatus, } from "../api/lsfgApi"; -import { InstallationButton } from "./InstallationButton"; -import { StatusDisplay } from "./StatusDisplay"; +import t from "../i18n/i18n"; import { showErrorToast } from "../utils/toastUtils"; interface SetupTabProps { @@ -20,10 +19,12 @@ interface SetupTabProps { isUninstalling: boolean; onInstall: () => void; onUninstall: () => void; + flatpakRelevant: boolean; } -function FlatpakSupportDiagnostics() { +function FlatpakSupportDiagnostics({ relevant }: { relevant: boolean }) { const [status, setStatus] = useState(null); + const [advanced, setAdvanced] = useState(false); const [operation, setOperation] = useState(null); const refresh = async () => { @@ -43,16 +44,15 @@ function FlatpakSupportDiagnostics() { }; useEffect(() => { - void refresh(); - }, []); + if (relevant) void refresh(); + }, [relevant]); - if (!status?.available) return null; + if (!relevant || !status?.available) return null; - const runExtensionOperation = async (version: string, enabled: boolean) => { - const operationKey = `${enabled ? "enable" : "disable"}-${version}`; - setOperation(operationKey); + const setEnabled = async (branch: string, enabled: boolean) => { + setOperation(`${enabled ? "enable" : "disable"}-${branch}`); try { - const result = await setFlatpakExtensionEnabled(version, enabled); + const result = await setFlatpakExtensionEnabled(branch, enabled); if (!result.success) throw new Error(result.error || result.message || "Flatpak runtime update failed"); await refresh(); } catch (error) { @@ -62,70 +62,91 @@ function FlatpakSupportDiagnostics() { } }; - const handleExtensionToggle = (version: string, enabled: boolean) => { - void runExtensionOperation(version, enabled); - }; - return ( - + - {status.supported_branches.map((branch) => ( - - handleExtensionToggle(branch, enabled)} - disabled={operation !== null} - /> - - ))} + + setAdvanced((value) => !value)}> + {advanced ? "Hide runtime details" : "Show runtime details"} + + + {advanced && status.supported_branches.map((branch) => { + const installed = status.installed_branches.includes(branch); + const pending = operation?.endsWith(`-${branch}`); + return ( + + void setEnabled(branch, enabled)} + disabled={operation !== null} + /> + + ); + })} ); } -export function SetupTab({ - isInstalled, - installationStatus, - losslessScalingInstalled, - losslessScalingStatus, - steamBranchStatus, - isInstalling, - isUninstalling, - onInstall, - onUninstall, -}: SetupTabProps) { +export function SetupTab(props: SetupTabProps) { + const { + isInstalled, + installationStatus, + losslessScalingInstalled, + losslessScalingStatus, + steamBranchStatus, + isInstalling, + isUninstalling, + onInstall, + onUninstall, + flatpakRelevant, + } = 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} + + - + ); } diff --git a/src/components/StatusDisplay.tsx b/src/components/StatusDisplay.tsx deleted file mode 100644 index b1a98e5..0000000 --- a/src/components/StatusDisplay.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { Field, PanelSectionRow } from "@decky/ui"; -import type { SteamBranchStatus } from "../api/lsfgApi"; - -interface StatusDisplayProps { - installationStatus: string; - losslessScalingInstalled: boolean; - losslessScalingStatus: string; - steamBranchStatus: SteamBranchStatus | null; -} - -export function StatusDisplay({ - installationStatus, - losslessScalingInstalled, - losslessScalingStatus, - steamBranchStatus -}: StatusDisplayProps) { - const losslessScalingAppInstalled = losslessScalingInstalled || steamBranchStatus?.installed === true; - - return ( - <> - - - - - - - - {steamBranchStatus?.installed && ( - - - - )} - - ); -} diff --git a/src/components/index.ts b/src/components/index.ts index 6856e76..bca6f6f 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1,6 +1,4 @@ export { Content } from "./Content"; -export { StatusDisplay } from "./StatusDisplay"; -export { InstallationButton } from "./InstallationButton"; export { ConfigurationSection } from "./ConfigurationSection"; export { FpsMultiplierControl } from "./FpsMultiplierControl"; export { ConfigurationTab } from "./ConfigurationTab"; -- cgit v1.2.3 From 840ea2802551a786de9b59f7aa00f04c5f335e2a Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Thu, 10 Sep 2026 07:57:03 -0400 Subject: fix: keep Flatpak runtime controls simple --- src/components/Content.tsx | 1 - src/components/SetupTab.tsx | 26 +++++++++----------------- 2 files changed, 9 insertions(+), 18 deletions(-) (limited to 'src/components') diff --git a/src/components/Content.tsx b/src/components/Content.tsx index d92ef86..e5216ca 100644 --- a/src/components/Content.tsx +++ b/src/components/Content.tsx @@ -91,7 +91,6 @@ export function Content() { isUninstalling={isUninstalling} onInstall={() => void install()} onUninstall={() => void uninstall()} - flatpakRelevant={targets.some((target) => target.transport.kind === "flatpak")} /> ); diff --git a/src/components/SetupTab.tsx b/src/components/SetupTab.tsx index e936049..624be54 100644 --- a/src/components/SetupTab.tsx +++ b/src/components/SetupTab.tsx @@ -19,12 +19,10 @@ interface SetupTabProps { isUninstalling: boolean; onInstall: () => void; onUninstall: () => void; - flatpakRelevant: boolean; } -function FlatpakSupportDiagnostics({ relevant }: { relevant: boolean }) { +function FlatpakSupportDiagnostics() { const [status, setStatus] = useState(null); - const [advanced, setAdvanced] = useState(false); const [operation, setOperation] = useState(null); const refresh = async () => { @@ -44,10 +42,10 @@ function FlatpakSupportDiagnostics({ relevant }: { relevant: boolean }) { }; useEffect(() => { - if (relevant) void refresh(); - }, [relevant]); + void refresh(); + }, []); - if (!relevant || !status?.available) return null; + if (!status?.available) return null; const setEnabled = async (branch: string, enabled: boolean) => { setOperation(`${enabled ? "enable" : "disable"}-${branch}`); @@ -63,19 +61,14 @@ function FlatpakSupportDiagnostics({ relevant }: { relevant: boolean }) { }; return ( - + - - setAdvanced((value) => !value)}> - {advanced ? "Hide runtime details" : "Show runtime details"} - - - {advanced && status.supported_branches.map((branch) => { + {status.supported_branches.map((branch) => { const installed = status.installed_branches.includes(branch); const pending = operation?.endsWith(`-${branch}`); return ( @@ -105,7 +98,6 @@ export function SetupTab(props: SetupTabProps) { isUninstalling, onInstall, onUninstall, - flatpakRelevant, } = props; const losslessScalingAppInstalled = losslessScalingInstalled || steamBranchStatus?.installed === true; const buttonLabel = isInstalling @@ -146,7 +138,7 @@ export function SetupTab(props: SetupTabProps) { - + ); } -- cgit v1.2.3 From e6efa4f0ff32d520982860180a29472068aab8e2 Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Thu, 10 Sep 2026 09:53:55 -0400 Subject: simplify unhooked game behavior on mount --- src/components/ConfigurationTab.tsx | 14 ------------- src/components/Content.tsx | 9 ++++---- src/components/NowPlayingTab.tsx | 42 ++++++------------------------------- 3 files changed, 10 insertions(+), 55 deletions(-) (limited to 'src/components') diff --git a/src/components/ConfigurationTab.tsx b/src/components/ConfigurationTab.tsx index 8f8690c..8150819 100644 --- a/src/components/ConfigurationTab.tsx +++ b/src/components/ConfigurationTab.tsx @@ -37,7 +37,6 @@ export function ConfigurationTab({ const [focusDetailAction, setFocusDetailAction] = useState<"enable" | "fps" | null>(null); const [focusConfiguredToggle, setFocusConfiguredToggle] = useState(false); const enableRef = useRef(null); - const promptedRunningAppId = useRef(null); const closeDetails = useCallback(() => { setFocusFpsMultiplier(false); setFocusDetailAction(null); @@ -60,18 +59,6 @@ export function ConfigurationTab({ return () => cancelAnimationFrame(frame); }, [focusDetailAction]); - useEffect(() => { - if (!runningGame || runningGame.configured) { - promptedRunningAppId.current = null; - return; - } - if (promptedRunningAppId.current !== runningGame.appid && detailAppId === null) { - promptedRunningAppId.current = runningGame.appid; - setFocusDetailAction("enable"); - setDetailAppId(runningGame.appid); - } - }, [detailAppId, runningGame?.appid, runningGame?.configured]); - const selectedTarget = detailAppId ? targets.find((target) => target.appid === detailAppId) : null; if (detailAppId === null) { @@ -106,7 +93,6 @@ export function ConfigurationTab({ : "Game is no longer available"; const handleProfileAction = async () => { if (selectedTarget?.configured) { - promptedRunningAppId.current = detailAppId; await onReset(); setFocusConfiguredToggle(true); closeDetails(); diff --git a/src/components/Content.tsx b/src/components/Content.tsx index e5216ca..578f984 100644 --- a/src/components/Content.tsx +++ b/src/components/Content.tsx @@ -56,15 +56,15 @@ export function Content() { setTab("Setup"); return; } - setTab((current) => current === "Setup" ? (runningGame ? "NowPlaying" : "Games") : current); - }, [runningGame?.appid, setupComplete]); + setTab((current) => current === "Setup" ? (runningGame?.configured ? "NowPlaying" : "Games") : current); + }, [runningGame?.appid, runningGame?.configured, setupComplete]); useEffect(() => { if (!setupComplete) return; const appid = runningGame?.appid || null; const previous = previousRunningAppId.current; previousRunningAppId.current = appid; - if (appid && appid !== previous) setTab("NowPlaying"); + if (appid && appid !== previous) setTab(runningGame?.configured ? "NowPlaying" : "Games"); else if (!appid && previous) { setTab((current) => current === "NowPlaying" ? "Games" : current); } @@ -96,7 +96,7 @@ export function Content() { const tabs = setupComplete ? [ - ...(runningGame ? [{ + ...(runningGame?.configured ? [{ id: "NowPlaying", title: tabIcons.nowPlaying, content: ( @@ -104,7 +104,6 @@ export function Content() { game={runningGame} config={config} onConfigChange={(field, value) => handleConfigChange(field, value)} - onEnable={enable} onRepair={repair} /> ), diff --git a/src/components/NowPlayingTab.tsx b/src/components/NowPlayingTab.tsx index 5bce5c4..57858db 100644 --- a/src/components/NowPlayingTab.tsx +++ b/src/components/NowPlayingTab.tsx @@ -11,7 +11,6 @@ interface Props { fieldName: keyof ConfigurationData, value: boolean | number | string | string[], ) => Promise; - onEnable: (appid: string) => Promise; onRepair: (appid: string) => Promise; } @@ -24,25 +23,13 @@ export function NowPlayingTab({ game, config, onConfigChange, - onEnable, onRepair, }: Props) { const [busy, setBusy] = useState(false); const supportNeedsRepair = - game.configured && game.transport.kind === "flatpak" && game.flatpakSupport?.support_status !== "ready"; - const handleEnable = async () => { - if (busy) return; - setBusy(true); - try { - await onEnable(game.appid); - } finally { - setBusy(false); - } - }; - const handleRepair = async () => { if (busy) return; setBusy(true); @@ -60,22 +47,7 @@ export function NowPlayingTab({ - {!game.configured && ( - - - - - - void handleEnable()}> - {busy ? "Enabling..." : "Enable LSFG-VK"} - - - - )} - {game.configured && supportNeedsRepair && ( + {supportNeedsRepair && ( )} - {game.configured && ( - - )} + ); } -- cgit v1.2.3 From 209ab92cc3baeb15ce808fc53ce9041a761d8df4 Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Thu, 10 Sep 2026 10:33:08 -0400 Subject: only warn quit on real steam games --- src/components/ConfigurationTab.tsx | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) (limited to 'src/components') diff --git a/src/components/ConfigurationTab.tsx b/src/components/ConfigurationTab.tsx index 8150819..5ae4a87 100644 --- a/src/components/ConfigurationTab.tsx +++ b/src/components/ConfigurationTab.tsx @@ -1,4 +1,4 @@ -import { ButtonItem, DialogButton, Focusable, PanelSection, PanelSectionRow, gamepadDialogClasses } from "@decky/ui"; +import { ButtonItem, ConfirmModal, DialogButton, 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"; @@ -91,13 +91,37 @@ export function ConfigurationTab({ const profileDescription = selectedTarget ? `${profileTransport} · App ID ${selectedTarget.appid} · ${selectedTarget.configured ? "LSFG-VK Enabled" : "LSFG-VK not enabled"}` : "Game is no longer available"; + const enableProfile = async (appid: string, quitRunningGame = false) => { + if (!(await onEnable(appid))) return; + if (quitRunningGame) SteamClient.Apps.TerminateApp(appid, false); + setFocusFpsMultiplier(true); + }; const handleProfileAction = async () => { if (selectedTarget?.configured) { await onReset(); setFocusConfiguredToggle(true); closeDetails(); - } else if (detailAppId && await onEnable(detailAppId)) { - setFocusFpsMultiplier(true); + } else if (detailAppId) { + const isRunningUnconfigured = runningGame?.appid === detailAppId + && runningGame.nonSteam === false + && runningGame.transport.kind === "host" + && selectedTarget?.nonSteam === false + && selectedTarget?.transport.kind === "host" + && !runningGame.configured; + if (isRunningUnconfigured) { + showModal( + void enableProfile(detailAppId, true)} + onCancel={() => void enableProfile(detailAppId)} + />, + ); + } else { + await enableProfile(detailAppId); + } } }; -- cgit v1.2.3 From b197e25b45d53c6c7175a45dd0b14642f2aab198 Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Thu, 10 Sep 2026 11:51:22 -0400 Subject: fixes for appimage and flatpak --- src/components/SetupTab.tsx | 128 +++++++++----------------------------------- 1 file changed, 26 insertions(+), 102 deletions(-) (limited to 'src/components') diff --git a/src/components/SetupTab.tsx b/src/components/SetupTab.tsx index 624be54..d769200 100644 --- a/src/components/SetupTab.tsx +++ b/src/components/SetupTab.tsx @@ -1,13 +1,6 @@ -import { ButtonItem, Field, PanelSection, PanelSectionRow, ToggleField } from "@decky/ui"; -import { useEffect, useState } from "react"; -import { - getFlatpakSupportStatus, - setFlatpakExtensionEnabled, - type FlatpakExtensionStatus, - type SteamBranchStatus, -} from "../api/lsfgApi"; +import { ButtonItem, Field, PanelSection, PanelSectionRow } from "@decky/ui"; +import { type SteamBranchStatus } from "../api/lsfgApi"; import t from "../i18n/i18n"; -import { showErrorToast } from "../utils/toastUtils"; interface SetupTabProps { isInstalled: boolean; @@ -21,72 +14,6 @@ interface SetupTabProps { onUninstall: () => void; } -function FlatpakSupportDiagnostics() { - const [status, setStatus] = useState(null); - const [operation, setOperation] = useState(null); - - const refresh = async () => { - try { - setStatus(await getFlatpakSupportStatus()); - } catch (error) { - setStatus({ - success: false, - message: "", - error: String(error), - available: false, - extension_id: "", - supported_branches: [], - installed_branches: [], - }); - } - }; - - useEffect(() => { - void refresh(); - }, []); - - if (!status?.available) return null; - - const setEnabled = async (branch: string, enabled: boolean) => { - setOperation(`${enabled ? "enable" : "disable"}-${branch}`); - try { - const result = await setFlatpakExtensionEnabled(branch, enabled); - if (!result.success) throw new Error(result.error || result.message || "Flatpak runtime update failed"); - await refresh(); - } catch (error) { - showErrorToast("Flatpak runtime update failed", String(error)); - } finally { - setOperation(null); - } - }; - - return ( - - - - - {status.supported_branches.map((branch) => { - const installed = status.installed_branches.includes(branch); - const pending = operation?.endsWith(`-${branch}`); - return ( - - void setEnabled(branch, enabled)} - disabled={operation !== null} - /> - - ); - })} - - ); -} - export function SetupTab(props: SetupTabProps) { const { isInstalled, @@ -109,36 +36,33 @@ export function SetupTab(props: SetupTabProps) { : t("INSTALL_INSTALL_BTN", "Install LSFG-VK"); return ( - <> - + + + + + + + + {steamBranchStatus?.installed && ( - - - - {steamBranchStatus?.installed && ( - - - - )} - - - {buttonLabel} - - - - - + )} + + + {buttonLabel} + + + ); } -- cgit v1.2.3 From de74f0d2499159ed1cf8f628a166302146ae1f13 Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Thu, 10 Sep 2026 12:58:44 -0400 Subject: fix flatpak disable leftovers --- src/components/ConfigFileTab.tsx | 119 +++++++++++++++++++++++++++++------- src/components/ConfigurationTab.tsx | 50 +++++++++------ src/components/Content.tsx | 34 ++++++++++- 3 files changed, 163 insertions(+), 40 deletions(-) (limited to 'src/components') diff --git a/src/components/ConfigFileTab.tsx b/src/components/ConfigFileTab.tsx index e3cc09d..07408d7 100644 --- a/src/components/ConfigFileTab.tsx +++ b/src/components/ConfigFileTab.tsx @@ -1,13 +1,79 @@ +import { ButtonItem, Field, PanelSection, PanelSectionRow, Spinner } from "@decky/ui"; import { useEffect, useState } from "react"; -import { Field, PanelSection, PanelSectionRow, Spinner } from "@decky/ui"; -import { getConfigFileContent, FileContentResult } from "../api/lsfgApi"; +import { RiArrowDownSFill, RiArrowUpSFill } from "react-icons/ri"; +import { getDebugFileContents, type DebugFileContent, type DebugFileContentsResult } from "../api/lsfgApi"; import t from "../i18n/i18n"; +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 { + // Persisting the view preference is optional. + } + }, [collapsed, key]); + + return [collapsed, () => setCollapsed((value) => !value)] as const; +} + +function DebugFileSection({ file }: { file: DebugFileContent }) { + const [collapsed, toggleCollapsed] = usePersistentCollapsed(`lsfg-debug-file-${file.id}-collapsed-v1`); + const status = file.exists ? "Present" : "Not present"; + + return ( + <> + + + + +
+ + {collapsed ? : } + +
+
+ {!collapsed && ( + + {file.exists && file.content !== null && file.content !== undefined ? ( +
+              {file.content}
+            
+ ) : ( + + )} +
+ )} + + ); +} + export function ConfigFileTab() { - const [result, setResult] = useState(null); + const [result, setResult] = useState(null); useEffect(() => { - getConfigFileContent().then(setResult).catch((error) => { + getDebugFileContents().then(setResult).catch((error) => { setResult({ success: false, error: String(error) }); }); }, []); @@ -23,24 +89,35 @@ export function ConfigFileTab() { } return ( - - {result.error && ( - - - - )} - {result.success && result.content && ( - <> - - - + <> + + + {result.error && ( -
-              {result.content}
-            
+
- - )} -
+ )} + {result.success && result.files?.map((file) => ( + + ))} +
+ ); } diff --git a/src/components/ConfigurationTab.tsx b/src/components/ConfigurationTab.tsx index 5ae4a87..4e2d93d 100644 --- a/src/components/ConfigurationTab.tsx +++ b/src/components/ConfigurationTab.tsx @@ -1,4 +1,4 @@ -import { ButtonItem, ConfirmModal, DialogButton, Focusable, PanelSection, PanelSectionRow, gamepadDialogClasses, showModal } from "@decky/ui"; +import { ButtonItem, ConfirmModal, DialogButton, Focusable, PanelSection, PanelSectionRow, ToggleField, gamepadDialogClasses, showModal } from "@decky/ui"; import { useCallback, useEffect, useRef, useState } from "react"; import { FaArrowLeft } from "react-icons/fa"; import { ConfigurationData } from "../config/configSchema"; @@ -11,6 +11,8 @@ interface ConfigurationTabProps { config: ConfigurationData; targets: GameTarget[]; runningGame: GameTarget | null; + showDebugTab: boolean; + onShowDebugTabChange: (value: boolean) => void; onSelect: (appid: string) => void; onConfigChange: (fieldName: keyof ConfigurationData, value: boolean | number | string | string[]) => Promise; onEnable: (appid: string) => Promise; @@ -24,6 +26,8 @@ export function ConfigurationTab({ config, targets, runningGame, + showDebugTab, + onShowDebugTabChange, onSelect, onConfigChange, onEnable, @@ -63,22 +67,34 @@ export function ConfigurationTab({ if (detailAppId === null) { return ( - - { - setFocusConfiguredToggle(false); - setFocusDetailAction(targets.find((target) => target.appid === appid)?.configured ? "fps" : "enable"); - onSelect(appid); - setDetailAppId(appid); - }} - onEnableAll={onEnableAll} - onResetAll={onResetAll} - focusConfiguredToggle={focusConfiguredToggle} - onConfiguredToggleFocused={clearConfiguredToggleFocusRequest} - /> - + <> + + { + setFocusConfiguredToggle(false); + setFocusDetailAction(targets.find((target) => target.appid === appid)?.configured ? "fps" : "enable"); + onSelect(appid); + setDetailAppId(appid); + }} + onEnableAll={onEnableAll} + onResetAll={onResetAll} + focusConfiguredToggle={focusConfiguredToggle} + onConfiguredToggleFocused={clearConfiguredToggleFocusRequest} + /> + + + + + + + ); } diff --git a/src/components/Content.tsx b/src/components/Content.tsx index 578f984..43720c0 100644 --- a/src/components/Content.tsx +++ b/src/components/Content.tsx @@ -17,6 +17,29 @@ const tabIcons = { setup: , }; +const DEBUG_TAB_VISIBILITY_KEY = "lsfg-debug-tab-visible-v1"; + +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 { + // Persisting the visibility preference is optional. + } + }, [key, value]); + + return [value, setValue] as const; +} + export function Content() { const { config, @@ -43,6 +66,7 @@ export function Content() { uninstall, } = useInstallation(reload); const [tab, setTab] = useState("Setup"); + const [showDebugTab, setShowDebugTab] = usePersistentBoolean(DEBUG_TAB_VISIBILITY_KEY, true); const previousRunningAppId = useRef(null); const setupComplete = isInstalled && @@ -74,6 +98,10 @@ export function Content() { if (isInstalled) void reload(); }, [isInstalled, reload]); + useEffect(() => { + if (!showDebugTab && tab === "ConfigFile") setTab("Games"); + }, [showDebugTab, tab]); + const handleConfigChange = async ( fieldName: keyof ConfigurationData, value: boolean | number | string | string[], @@ -116,6 +144,8 @@ export function Content() { config={config} targets={targets} runningGame={runningGame} + showDebugTab={showDebugTab} + onShowDebugTabChange={setShowDebugTab} onSelect={setSelectedAppId} onConfigChange={(field, value) => handleConfigChange(field, value, true)} onEnable={enable} @@ -126,7 +156,7 @@ export function Content() { /> ), }, - { id: "ConfigFile", title: tabIcons.configFile, content: }, + ...(showDebugTab ? [{ id: "ConfigFile", title: tabIcons.configFile, content: }] : []), { id: "Setup", title: tabIcons.setup, content: setup }, ] : [{ id: "Setup", title: tabIcons.setup, content: setup }]; @@ -137,7 +167,7 @@ export function Content() { style={{ height: "95%", width: "300px", position: "fixed", marginTop: "-12px", overflow: "hidden" }} > - +
); } -- cgit v1.2.3 From c46d41b22df8a6830fe4483ba1b71e246b4ae5e6 Mon Sep 17 00:00:00 2001 From: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> Date: Thu, 10 Sep 2026 15:44:26 -0400 Subject: refactor: pass direct flatpak launch shape --- src/components/GameConfigurationControls.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/components') diff --git a/src/components/GameConfigurationControls.tsx b/src/components/GameConfigurationControls.tsx index 6bea2e0..a7e3fec 100644 --- a/src/components/GameConfigurationControls.tsx +++ b/src/components/GameConfigurationControls.tsx @@ -10,7 +10,7 @@ interface Props { autoFocusFpsMultiplier?: boolean; onFpsMultiplierFocused?: () => void; showWorkarounds?: boolean; - workaroundTarget?: Pick; + workaroundTarget?: Pick; onRepairWorkaround?: () => Promise; } @@ -36,7 +36,7 @@ export function GameConfigurationControls({ )} -- cgit v1.2.3 From 1ce6d55a1d44eeb0eb04751e124970f2f1f9acbb Mon Sep 17 00:00:00 2001 From: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> Date: Thu, 10 Sep 2026 15:45:03 -0400 Subject: refactor: decouple workaround controls from flatpak transport --- src/components/WorkaroundsSection.tsx | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) (limited to 'src/components') diff --git a/src/components/WorkaroundsSection.tsx b/src/components/WorkaroundsSection.tsx index 5587392..050992c 100644 --- a/src/components/WorkaroundsSection.tsx +++ b/src/components/WorkaroundsSection.tsx @@ -1,7 +1,6 @@ import { ButtonItem, Field, PanelSectionRow, SliderField, ToggleField } from "@decky/ui"; import { useEffect, useState } from "react"; import { RiArrowDownSFill, RiArrowUpSFill } from "react-icons/ri"; -import type { TargetTransport } from "../api/lsfgApi"; import { usePerAppWorkarounds } from "../hooks/usePerAppWorkarounds"; import t from "../i18n/i18n"; import type { WorkaroundField } from "../hooks/usePerAppWorkarounds"; @@ -9,7 +8,7 @@ import type { WorkaroundField } from "../hooks/usePerAppWorkarounds"; interface WorkaroundsSectionProps { appId: string; nonSteam: boolean; - transport: TargetTransport; + directFlatpak?: boolean; onRepair?: () => Promise; } @@ -73,17 +72,15 @@ function usePersistentCollapsed() { useEffect(() => { try { localStorage.setItem(WORKAROUNDS_COLLAPSED_KEY, JSON.stringify(collapsed)); - } catch { - // Persisting the view preference is optional. - } + } catch {} }, [collapsed]); return [collapsed, () => setCollapsed((value) => !value)] as const; } -export function WorkaroundsSection({ appId, nonSteam, transport, onRepair }: WorkaroundsSectionProps) { +export function WorkaroundsSection({ appId, nonSteam, directFlatpak = false, onRepair }: WorkaroundsSectionProps) { const [collapsed, toggleCollapsed] = usePersistentCollapsed(); - const { status, snapshot, refresh, update, error } = usePerAppWorkarounds(appId, nonSteam, transport); + const { status, snapshot, refresh, update, error } = usePerAppWorkarounds(appId, nonSteam, directFlatpak); const [repairing, setRepairing] = useState(false); const state = snapshot?.state; const controlsDisabled = status !== "ready" || state === undefined || snapshot?.wrapperOwned !== true || snapshot.integrationInstalled !== true; -- cgit v1.2.3 From da9166ca8c6d90f402352e47ec28d67f2a02cf14 Mon Sep 17 00:00:00 2001 From: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> Date: Thu, 10 Sep 2026 15:45:27 -0400 Subject: refactor: remove per-game flatpak support ui --- src/components/ConfigurationTab.tsx | 50 +++++++++++++------------------------ 1 file changed, 18 insertions(+), 32 deletions(-) (limited to 'src/components') diff --git a/src/components/ConfigurationTab.tsx b/src/components/ConfigurationTab.tsx index 4e2d93d..18575b4 100644 --- a/src/components/ConfigurationTab.tsx +++ b/src/components/ConfigurationTab.tsx @@ -100,8 +100,8 @@ export function ConfigurationTab({ const profileLabel = selectedTarget?.name || "Game profile"; const profileTransport = selectedTarget - ? selectedTarget.transport.kind === "flatpak" - ? "Non-Steam · Flatpak" + ? selectedTarget.directFlatpak + ? "Non-Steam · Direct Flatpak" : selectedTarget.nonSteam ? "Non-Steam" : "Steam" : "Game"; const profileDescription = selectedTarget @@ -120,9 +120,7 @@ export function ConfigurationTab({ } else if (detailAppId) { const isRunningUnconfigured = runningGame?.appid === detailAppId && runningGame.nonSteam === false - && runningGame.transport.kind === "host" && selectedTarget?.nonSteam === false - && selectedTarget?.transport.kind === "host" && !runningGame.configured; if (isRunningUnconfigured) { showModal( @@ -147,22 +145,22 @@ export function ConfigurationTab({
- - - + + +
)} - {selectedTarget?.configured && selectedTarget.transport.kind === "flatpak" && selectedTarget.flatpakSupport?.support_status !== "ready" && ( - - - void onRepair(selectedTarget.appid)} - > - Repair Flatpak support - - - - )} {selectedTarget?.configured && ( Date: Thu, 10 Sep 2026 15:45:36 -0400 Subject: refactor: remove per-game flatpak repair state --- src/components/NowPlayingTab.tsx | 37 ++----------------------------------- 1 file changed, 2 insertions(+), 35 deletions(-) (limited to 'src/components') diff --git a/src/components/NowPlayingTab.tsx b/src/components/NowPlayingTab.tsx index 57858db..1bc331f 100644 --- a/src/components/NowPlayingTab.tsx +++ b/src/components/NowPlayingTab.tsx @@ -1,5 +1,4 @@ -import { ButtonItem, Field, Focusable, PanelSection, PanelSectionRow } from "@decky/ui"; -import { useState } from "react"; +import { Field, Focusable, PanelSection, PanelSectionRow } from "@decky/ui"; import { ConfigurationData } from "../config/configSchema"; import { GameTarget } from "../hooks/useGameConfiguration"; import { GameConfigurationControls } from "./GameConfigurationControls"; @@ -11,11 +10,10 @@ interface Props { fieldName: keyof ConfigurationData, value: boolean | number | string | string[], ) => Promise; - onRepair: (appid: string) => Promise; } function targetDescription(game: GameTarget): string { - if (game.transport.kind === "flatpak") return "Non-Steam · Flatpak"; + if (game.directFlatpak) return "Non-Steam · Direct Flatpak"; return game.nonSteam ? "Non-Steam" : "Steam"; } @@ -23,23 +21,7 @@ export function NowPlayingTab({ game, config, onConfigChange, - onRepair, }: Props) { - const [busy, setBusy] = useState(false); - const supportNeedsRepair = - game.transport.kind === "flatpak" && - game.flatpakSupport?.support_status !== "ready"; - - const handleRepair = async () => { - if (busy) return; - setBusy(true); - try { - await onRepair(game.appid); - } finally { - setBusy(false); - } - }; - return ( @@ -47,21 +29,6 @@ export function NowPlayingTab({ - {supportNeedsRepair && ( - - - - - - void handleRepair()}> - {busy ? "Repairing..." : "Repair Flatpak support"} - - - - )} Date: Thu, 10 Sep 2026 15:45:58 -0400 Subject: feat: add explicit flatpak setup --- src/components/FlatpakSetupSection.tsx | 110 +++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 src/components/FlatpakSetupSection.tsx (limited to 'src/components') diff --git a/src/components/FlatpakSetupSection.tsx b/src/components/FlatpakSetupSection.tsx new file mode 100644 index 0000000..0119250 --- /dev/null +++ b/src/components/FlatpakSetupSection.tsx @@ -0,0 +1,110 @@ +import { ButtonItem, Field, PanelSection, PanelSectionRow } from "@decky/ui"; +import { useCallback, useEffect, useState } from "react"; +import { + getFlatpakApps, + prepareFlatpakApp, + removeFlatpakApp, + type FlatpakApp, +} from "../api/lsfgApi"; +import { showErrorToast } from "../utils/toastUtils"; + +interface Props { + enabled: boolean; +} + +export function FlatpakSetupSection({ enabled }: Props) { + const [apps, setApps] = useState([]); + const [loading, setLoading] = useState(false); + const [busyApp, setBusyApp] = useState(""); + const [error, setError] = useState(null); + + const load = useCallback(async () => { + if (!enabled) { + setApps([]); + setError(null); + return; + } + setLoading(true); + try { + const result = await getFlatpakApps(); + if (!result.success) throw new Error(result.error || "Could not list Flatpak applications"); + setApps(result.apps || []); + setError(null); + } catch (loadError) { + const message = loadError instanceof Error ? loadError.message : String(loadError); + setError(message); + } finally { + setLoading(false); + } + }, [enabled]); + + useEffect(() => { + void load(); + }, [load]); + + const toggle = async (app: FlatpakApp) => { + if (busyApp || (app.prepared && !app.owned)) return; + setBusyApp(app.app_id); + try { + const result = app.prepared + ? await removeFlatpakApp(app.app_id) + : await prepareFlatpakApp(app.app_id); + if (!result.success) throw new Error(result.error || "Flatpak setup failed"); + await load(); + } catch (operationError) { + const message = operationError instanceof Error ? operationError.message : String(operationError); + showErrorToast("Flatpak setup failed", message); + } finally { + setBusyApp(""); + } + }; + + if (!enabled) return null; + + return ( + + + + + {error && ( + + + + )} + {apps.map((app) => { + const busy = busyApp === app.app_id; + const description = [ + app.app_id, + app.runtime_branch ? `runtime ${app.runtime_branch}` : null, + app.error, + ].filter(Boolean).join(" · "); + const label = busy + ? "Working..." + : app.prepared + ? app.owned ? "Remove" : "Prepared externally" + : app.error ? "Unavailable" : "Prepare"; + return ( + + + void toggle(app)} + > + {label} + + + + ); + })} + + void load()}> + {loading ? "Refreshing..." : "Refresh Flatpaks"} + + + + ); +} -- cgit v1.2.3 From 644e624e6e8745f58b4f3bee4a672f180992ad1b Mon Sep 17 00:00:00 2001 From: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> Date: Thu, 10 Sep 2026 15:46:18 -0400 Subject: feat: add flatpak setup to setup tab --- src/components/SetupTab.tsx | 52 ++++++++++++++++++++++++--------------------- 1 file changed, 28 insertions(+), 24 deletions(-) (limited to 'src/components') diff --git a/src/components/SetupTab.tsx b/src/components/SetupTab.tsx index d769200..1a62c2a 100644 --- a/src/components/SetupTab.tsx +++ b/src/components/SetupTab.tsx @@ -1,6 +1,7 @@ import { ButtonItem, Field, PanelSection, PanelSectionRow } from "@decky/ui"; import { type SteamBranchStatus } from "../api/lsfgApi"; import t from "../i18n/i18n"; +import { FlatpakSetupSection } from "./FlatpakSetupSection"; interface SetupTabProps { isInstalled: boolean; @@ -36,33 +37,36 @@ export function SetupTab(props: SetupTabProps) { : t("INSTALL_INSTALL_BTN", "Install LSFG-VK"); return ( - - - - - - - - {steamBranchStatus?.installed && ( + <> + - )} - - - {buttonLabel} - - - + + + + {steamBranchStatus?.installed && ( + + + + )} + + + {buttonLabel} + + + + + ); } -- cgit v1.2.3 From f83e6cacec84ccc8e17be50d68e228aab996cee5 Mon Sep 17 00:00:00 2001 From: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> Date: Thu, 10 Sep 2026 15:46:51 -0400 Subject: refactor: remove now-playing flatpak repair coupling --- src/components/Content.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) (limited to 'src/components') diff --git a/src/components/Content.tsx b/src/components/Content.tsx index 43720c0..8f83e85 100644 --- a/src/components/Content.tsx +++ b/src/components/Content.tsx @@ -32,9 +32,7 @@ function usePersistentBoolean(key: string, defaultValue: boolean) { useEffect(() => { try { localStorage.setItem(key, String(value)); - } catch { - // Persisting the visibility preference is optional. - } + } catch {} }, [key, value]); return [value, setValue] as const; @@ -132,7 +130,6 @@ export function Content() { game={runningGame} config={config} onConfigChange={(field, value) => handleConfigChange(field, value)} - onRepair={repair} /> ), }] : []), -- cgit v1.2.3 From 91365a978911e17097f7363216271b98354399ec Mon Sep 17 00:00:00 2001 From: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> Date: Thu, 10 Sep 2026 15:50:35 -0400 Subject: refactor: remove flatpak transport labels --- src/components/GameConfigurationSelector.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) (limited to 'src/components') diff --git a/src/components/GameConfigurationSelector.tsx b/src/components/GameConfigurationSelector.tsx index 92eacba..1c95820 100644 --- a/src/components/GameConfigurationSelector.tsx +++ b/src/components/GameConfigurationSelector.tsx @@ -28,16 +28,14 @@ function usePersistentCollapsed(key: string) { useEffect(() => { try { localStorage.setItem(key, String(collapsed)); - } catch { - // Persisting the view preference is optional. - } + } catch {} }, [collapsed, key]); return [collapsed, () => setCollapsed((value) => !value)] as const; } function targetDescription(game: GameTarget): string { - if (game.transport.kind === "flatpak") return "Non-Steam · Flatpak"; + if (game.directFlatpak) return "Non-Steam · Direct Flatpak"; return game.nonSteam ? "Non-Steam" : "Steam"; } @@ -137,7 +135,7 @@ export function GameConfigurationSelector({ showModal( void onEnableAll()} -- cgit v1.2.3 From 4823f1a17d647d4e6e387107a2260a8ebd3015de Mon Sep 17 00:00:00 2001 From: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> Date: Thu, 10 Sep 2026 16:31:35 -0400 Subject: feat: add flatpak workaround controls --- src/components/FlatpakWorkaroundsSection.tsx | 144 +++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 src/components/FlatpakWorkaroundsSection.tsx (limited to 'src/components') diff --git a/src/components/FlatpakWorkaroundsSection.tsx b/src/components/FlatpakWorkaroundsSection.tsx new file mode 100644 index 0000000..7d9d880 --- /dev/null +++ b/src/components/FlatpakWorkaroundsSection.tsx @@ -0,0 +1,144 @@ +import { ButtonItem, PanelSectionRow, SliderField, ToggleField } from "@decky/ui"; +import { useEffect, useRef, useState } from "react"; +import { RiArrowDownSFill, RiArrowUpSFill } from "react-icons/ri"; +import type { WorkaroundState } from "../api/lsfgApi"; +import t from "../i18n/i18n"; + +interface Props { + state: WorkaroundState; + disabled?: boolean; + onChange: (state: WorkaroundState) => Promise; +} + +const WORKAROUNDS_COLLAPSED_KEY = "lsfg-flatpak-workarounds-collapsed-v1"; + +export function FlatpakWorkaroundsSection({ state, disabled = false, onChange }: Props) { + const [collapsed, setCollapsed] = useState(() => { + try { + return localStorage.getItem(WORKAROUNDS_COLLAPSED_KEY) !== "false"; + } catch { + return true; + } + }); + const [fpsValue, setFpsValue] = useState(state.dxvkFrameRate); + const timer = useRef(null); + + useEffect(() => { + try { + localStorage.setItem(WORKAROUNDS_COLLAPSED_KEY, String(collapsed)); + } catch {} + }, [collapsed]); + + useEffect(() => { + setFpsValue(state.dxvkFrameRate); + }, [state.dxvkFrameRate]); + + useEffect(() => () => { + if (timer.current !== null) window.clearTimeout(timer.current); + }, []); + + const update = (field: keyof WorkaroundState, value: boolean | number) => { + void onChange({ ...state, [field]: value }); + }; + + const updateFps = (value: number) => { + setFpsValue(value); + if (timer.current !== null) window.clearTimeout(timer.current); + timer.current = window.setTimeout(() => { + timer.current = null; + void onChange({ ...state, dxvkFrameRate: value }); + }, 250); + }; + + const fpsLabel = fpsValue > 0 ? `${fpsValue} FPS` : t("CONFIG_BASE_FPS_CAP_OFF", "Off"); + + return ( + <> + +
+ {t("CONFIG_WORKAROUNDS_TITLE", "Workarounds")} +
+
+ + setCollapsed((value) => !value)} + > + {collapsed ? : } + + + {!collapsed && ( + <> + + + + + update("disableSteamdeckMode", value)} + disabled={disabled} + /> + + + update("disableGamescopeWsi", value)} + disabled={disabled} + /> + + + update("disableHdr", value)} + disabled={disabled} + /> + + + update("disableVkbasalt", value)} + disabled={disabled} + /> + + + update("enableZink", value)} + disabled={disabled} + /> + + + )} + + ); +} -- cgit v1.2.3 From 6671a5c51fef98c07369f5e5be5ce498a317e868 Mon Sep 17 00:00:00 2001 From: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> Date: Thu, 10 Sep 2026 16:31:58 -0400 Subject: feat: add flatpak profiles tab --- src/components/FlatpakTab.tsx | 178 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 src/components/FlatpakTab.tsx (limited to 'src/components') diff --git a/src/components/FlatpakTab.tsx b/src/components/FlatpakTab.tsx new file mode 100644 index 0000000..6392d59 --- /dev/null +++ b/src/components/FlatpakTab.tsx @@ -0,0 +1,178 @@ +import { ButtonItem, DialogButton, Field, Focusable, PanelSection, PanelSectionRow, gamepadDialogClasses } from "@decky/ui"; +import { useCallback, useMemo, useState } from "react"; +import { FaArrowLeft } from "react-icons/fa"; +import type { FlatpakApp, LsfgConfig, WorkaroundState } from "../api/lsfgApi"; +import { ConfigurationSection } from "./ConfigurationSection"; +import { FlatpakWorkaroundsSection } from "./FlatpakWorkaroundsSection"; +import { FpsMultiplierControl } from "./FpsMultiplierControl"; +import { ProfileDetails } from "./ProfileDetails"; + +interface Props { + apps: FlatpakApp[]; + runningApp: FlatpakApp | null; + loading: boolean; + busyAppId: string; + onRefresh: () => Promise; + onEnable: (appId: string) => Promise; + onRemove: (appId: string) => Promise; + onConfigChange: (appId: string, config: LsfgConfig) => Promise; + onWorkaroundChange: (appId: string, state: WorkaroundState) => Promise; +} + +export function FlatpakTab({ + apps, + runningApp, + loading, + busyAppId, + onRefresh, + onEnable, + onRemove, + onConfigChange, + onWorkaroundChange, +}: Props) { + const [selectedAppId, setSelectedAppId] = useState(null); + const selected = useMemo( + () => selectedAppId ? apps.find((app) => app.app_id === selectedAppId) || null : null, + [apps, selectedAppId], + ); + const close = useCallback(() => setSelectedAppId(null), []); + + if (!selectedAppId) { + return ( + + + + + {apps.map((app) => { + const status = app.enabled + ? app.app_id === runningApp?.app_id ? "Enabled · Running" : "Enabled" + : app.prepared && !app.owned ? "Prepared externally" : "Available"; + return ( + + setSelectedAppId(app.app_id)} + highlightOnFocus + /> + + ); + })} + {apps.length === 0 && !loading && ( + + + + )} + + void onRefresh()}> + {loading ? "Refreshing..." : "Refresh Flatpaks"} + + + + ); + } + + if (!selected) { + return ( + + + Back + + + + + + ); + } + + const busy = busyAppId === selected.app_id; + const config = selected.config; + const external = selected.prepared && !selected.owned; + const profileDescription = [ + selected.app_id, + selected.runtime_branch ? `runtime ${selected.runtime_branch}` : null, + selected.enabled ? `profile ${selected.profile}` : null, + selected.app_id === runningApp?.app_id ? "Running" : null, + ].filter(Boolean).join(" · "); + + const changeConfig = async ( + field: keyof LsfgConfig, + value: boolean | number | string | string[], + ) => { + if (!config) return; + await onConfigChange(selected.app_id, { ...config, [field]: value }); + }; + + return ( + + + +
+ + + + + +
+ {selected.app_name} +
+
+
+
+ {!selected.enabled && ( + + + void onEnable(selected.app_id)} + > + {busy ? "Enabling..." : external ? "Prepared externally" : "Enable LSFG-VK"} + + + {selected.error && ( + + + + )} + + )} + {selected.enabled && config && ( + <> + + + onWorkaroundChange(selected.app_id, state)} + /> + + void onRemove(selected.app_id)}> + {busy ? "Removing..." : "Remove Flatpak profile"} + + + + )} + +
+ ); +} -- cgit v1.2.3 From 8090b237a34140343b13efc845f8123730989cbf Mon Sep 17 00:00:00 2001 From: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> Date: Thu, 10 Sep 2026 16:32:15 -0400 Subject: feat: show running flatpak profiles --- src/components/FlatpakNowPlayingTab.tsx | 39 +++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 src/components/FlatpakNowPlayingTab.tsx (limited to 'src/components') diff --git a/src/components/FlatpakNowPlayingTab.tsx b/src/components/FlatpakNowPlayingTab.tsx new file mode 100644 index 0000000..9e5a0db --- /dev/null +++ b/src/components/FlatpakNowPlayingTab.tsx @@ -0,0 +1,39 @@ +import { Field, Focusable, PanelSection, PanelSectionRow } from "@decky/ui"; +import type { FlatpakApp, LsfgConfig, WorkaroundState } from "../api/lsfgApi"; +import { ConfigurationSection } from "./ConfigurationSection"; +import { FlatpakWorkaroundsSection } from "./FlatpakWorkaroundsSection"; +import { FpsMultiplierControl } from "./FpsMultiplierControl"; + +interface Props { + app: FlatpakApp; + busy: boolean; + onConfigChange: (appId: string, config: LsfgConfig) => Promise; + onWorkaroundChange: (appId: string, state: WorkaroundState) => Promise; +} + +export function FlatpakNowPlayingTab({ app, busy, onConfigChange, onWorkaroundChange }: Props) { + if (!app.config) return null; + const changeConfig = async ( + field: keyof LsfgConfig, + value: boolean | number | string | string[], + ) => { + await onConfigChange(app.app_id, { ...app.config!, [field]: value }); + }; + + return ( + + + + + + + + + onWorkaroundChange(app.app_id, state)} + /> + + ); +} -- cgit v1.2.3 From 0e51abafa2fed9fc829dceb48728d0e8bbf0dcaf Mon Sep 17 00:00:00 2001 From: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> Date: Thu, 10 Sep 2026 16:32:25 -0400 Subject: refactor: move flatpak setup into profiles tab --- src/components/SetupTab.tsx | 52 +++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 28 deletions(-) (limited to 'src/components') diff --git a/src/components/SetupTab.tsx b/src/components/SetupTab.tsx index 1a62c2a..d769200 100644 --- a/src/components/SetupTab.tsx +++ b/src/components/SetupTab.tsx @@ -1,7 +1,6 @@ import { ButtonItem, Field, PanelSection, PanelSectionRow } from "@decky/ui"; import { type SteamBranchStatus } from "../api/lsfgApi"; import t from "../i18n/i18n"; -import { FlatpakSetupSection } from "./FlatpakSetupSection"; interface SetupTabProps { isInstalled: boolean; @@ -37,36 +36,33 @@ export function SetupTab(props: SetupTabProps) { : t("INSTALL_INSTALL_BTN", "Install LSFG-VK"); return ( - <> - + + + + + + + + {steamBranchStatus?.installed && ( - - - - {steamBranchStatus?.installed && ( - - - - )} - - - {buttonLabel} - - - - - + )} + + + {buttonLabel} + + + ); } -- cgit v1.2.3 From 64e3a2db3a538b4b538e6b0520943b5a90eff5a5 Mon Sep 17 00:00:00 2001 From: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> Date: Thu, 10 Sep 2026 16:32:58 -0400 Subject: feat: add flatpak profiles tab and now playing --- src/components/Content.tsx | 84 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 59 insertions(+), 25 deletions(-) (limited to 'src/components') diff --git a/src/components/Content.tsx b/src/components/Content.tsx index 8f83e85..ffb6eef 100644 --- a/src/components/Content.tsx +++ b/src/components/Content.tsx @@ -1,18 +1,22 @@ import { Tabs } from "@decky/ui"; import { useEffect, useRef, useState } from "react"; -import { FaFileAlt, FaGamepad, FaList, FaTools } from "react-icons/fa"; +import { FaCube, FaFileAlt, FaGamepad, FaList, 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 { ConfigFileTab } from "./ConfigFileTab"; import { ConfigurationTab } from "./ConfigurationTab"; +import { FlatpakNowPlayingTab } from "./FlatpakNowPlayingTab"; +import { FlatpakTab } from "./FlatpakTab"; import { NowPlayingTab } from "./NowPlayingTab"; import { SetupTab } from "./SetupTab"; const tabIcons = { nowPlaying: , games: , + flatpak: , configFile: , setup: , }; @@ -63,38 +67,46 @@ export function Content() { install, uninstall, } = useInstallation(reload); - const [tab, setTab] = useState("Setup"); - const [showDebugTab, setShowDebugTab] = usePersistentBoolean(DEBUG_TAB_VISIBILITY_KEY, true); - const previousRunningAppId = useRef(null); const setupComplete = isInstalled && losslessScalingInstalled && steamBranchStatus?.success === true && steamBranchStatus.installed && !steamBranchStatus.needs_switch; + const flatpak = useFlatpakConfiguration(setupComplete); + const [tab, setTab] = useState("Setup"); + const [showDebugTab, setShowDebugTab] = usePersistentBoolean(DEBUG_TAB_VISIBILITY_KEY, true); + const previousRunningWorkload = useRef(null); + const runningFlatpak = flatpak.runningApp; + const hasNowPlaying = Boolean(runningGame?.configured || runningFlatpak); + const runningWorkload = runningGame?.configured + ? `steam:${runningGame.appid}` + : runningFlatpak ? `flatpak:${runningFlatpak.app_id}` : null; useEffect(() => { if (!setupComplete) { setTab("Setup"); return; } - setTab((current) => current === "Setup" ? (runningGame?.configured ? "NowPlaying" : "Games") : current); - }, [runningGame?.appid, runningGame?.configured, setupComplete]); + setTab((current) => current === "Setup" ? (hasNowPlaying ? "NowPlaying" : "Games") : current); + }, [hasNowPlaying, setupComplete]); useEffect(() => { if (!setupComplete) return; - const appid = runningGame?.appid || null; - const previous = previousRunningAppId.current; - previousRunningAppId.current = appid; - if (appid && appid !== previous) setTab(runningGame?.configured ? "NowPlaying" : "Games"); - else if (!appid && previous) { + const previous = previousRunningWorkload.current; + previousRunningWorkload.current = runningWorkload; + if (runningWorkload && runningWorkload !== previous) setTab("NowPlaying"); + else if (!runningWorkload && previous) { setTab((current) => current === "NowPlaying" ? "Games" : current); } - }, [runningGame?.appid, runningGame?.configured, setupComplete]); + }, [runningWorkload, setupComplete]); useEffect(() => { - if (isInstalled) void reload(); - }, [isInstalled, reload]); + if (isInstalled) { + void reload(); + void flatpak.reload(); + } + }, [isInstalled, reload, flatpak.reload]); useEffect(() => { if (!showDebugTab && tab === "ConfigFile") setTab("Games"); @@ -120,19 +132,24 @@ export function Content() { /> ); + const nowPlaying = runningGame?.configured ? ( + handleConfigChange(field, value)} + /> + ) : runningFlatpak ? ( + + ) : null; + const tabs = setupComplete ? [ - ...(runningGame?.configured ? [{ - id: "NowPlaying", - title: tabIcons.nowPlaying, - content: ( - handleConfigChange(field, value)} - /> - ), - }] : []), + ...(nowPlaying ? [{ id: "NowPlaying", title: tabIcons.nowPlaying, content: nowPlaying }] : []), { id: "Games", title: tabIcons.games, @@ -153,6 +170,23 @@ export function Content() { /> ), }, + { + id: "Flatpak", + title: tabIcons.flatpak, + content: ( + + ), + }, ...(showDebugTab ? [{ id: "ConfigFile", title: tabIcons.configFile, content: }] : []), { id: "Setup", title: tabIcons.setup, content: setup }, ] -- cgit v1.2.3 From 1de34909f7caa0afc9c5b2745112ce29173eda69 Mon Sep 17 00:00:00 2001 From: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> Date: Thu, 10 Sep 2026 16:35:25 -0400 Subject: refactor: keep steam workarounds transport agnostic --- src/components/WorkaroundsSection.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) (limited to 'src/components') diff --git a/src/components/WorkaroundsSection.tsx b/src/components/WorkaroundsSection.tsx index 050992c..3de5ec1 100644 --- a/src/components/WorkaroundsSection.tsx +++ b/src/components/WorkaroundsSection.tsx @@ -8,7 +8,6 @@ import type { WorkaroundField } from "../hooks/usePerAppWorkarounds"; interface WorkaroundsSectionProps { appId: string; nonSteam: boolean; - directFlatpak?: boolean; onRepair?: () => Promise; } @@ -78,9 +77,9 @@ function usePersistentCollapsed() { return [collapsed, () => setCollapsed((value) => !value)] as const; } -export function WorkaroundsSection({ appId, nonSteam, directFlatpak = false, onRepair }: WorkaroundsSectionProps) { +export function WorkaroundsSection({ appId, nonSteam, onRepair }: WorkaroundsSectionProps) { const [collapsed, toggleCollapsed] = usePersistentCollapsed(); - const { status, snapshot, refresh, update, error } = usePerAppWorkarounds(appId, nonSteam, directFlatpak); + const { status, snapshot, refresh, update, error } = usePerAppWorkarounds(appId, nonSteam); const [repairing, setRepairing] = useState(false); const state = snapshot?.state; const controlsDisabled = status !== "ready" || state === undefined || snapshot?.wrapperOwned !== true || snapshot.integrationInstalled !== true; -- cgit v1.2.3 From f4b67511ddbe32948927ce73b5f3e067755a904d Mon Sep 17 00:00:00 2001 From: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> Date: Thu, 10 Sep 2026 16:35:33 -0400 Subject: refactor: simplify steam workaround target --- src/components/GameConfigurationControls.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'src/components') diff --git a/src/components/GameConfigurationControls.tsx b/src/components/GameConfigurationControls.tsx index a7e3fec..7025f78 100644 --- a/src/components/GameConfigurationControls.tsx +++ b/src/components/GameConfigurationControls.tsx @@ -10,7 +10,7 @@ interface Props { autoFocusFpsMultiplier?: boolean; onFpsMultiplierFocused?: () => void; showWorkarounds?: boolean; - workaroundTarget?: Pick; + workaroundTarget?: Pick; onRepairWorkaround?: () => Promise; } @@ -36,7 +36,6 @@ export function GameConfigurationControls({ )} -- cgit v1.2.3 From 1949dd018bd7492c77736e19f9a25b14367187ff Mon Sep 17 00:00:00 2001 From: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> Date: Thu, 10 Sep 2026 16:36:31 -0400 Subject: refactor: separate flatpak from games selector --- src/components/GameConfigurationSelector.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'src/components') diff --git a/src/components/GameConfigurationSelector.tsx b/src/components/GameConfigurationSelector.tsx index 1c95820..1f0bc73 100644 --- a/src/components/GameConfigurationSelector.tsx +++ b/src/components/GameConfigurationSelector.tsx @@ -35,7 +35,6 @@ function usePersistentCollapsed(key: string) { } function targetDescription(game: GameTarget): string { - if (game.directFlatpak) return "Non-Steam · Direct Flatpak"; return game.nonSteam ? "Non-Steam" : "Steam"; } @@ -135,7 +134,7 @@ export function GameConfigurationSelector({ showModal( void onEnableAll()} -- cgit v1.2.3 From 59b9148054e2f0c7dcd33b0e5393899d7a92f93e Mon Sep 17 00:00:00 2001 From: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> Date: Thu, 10 Sep 2026 16:36:50 -0400 Subject: refactor: keep game profiles steam scoped --- src/components/ConfigurationTab.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) (limited to 'src/components') diff --git a/src/components/ConfigurationTab.tsx b/src/components/ConfigurationTab.tsx index 18575b4..a6d4c2a 100644 --- a/src/components/ConfigurationTab.tsx +++ b/src/components/ConfigurationTab.tsx @@ -99,11 +99,7 @@ export function ConfigurationTab({ } const profileLabel = selectedTarget?.name || "Game profile"; - const profileTransport = selectedTarget - ? selectedTarget.directFlatpak - ? "Non-Steam · Direct Flatpak" - : selectedTarget.nonSteam ? "Non-Steam" : "Steam" - : "Game"; + const profileTransport = selectedTarget?.nonSteam ? "Non-Steam" : "Steam"; const profileDescription = selectedTarget ? `${profileTransport} · App ID ${selectedTarget.appid} · ${selectedTarget.configured ? "LSFG-VK Enabled" : "LSFG-VK not enabled"}` : "Game is no longer available"; -- cgit v1.2.3 From eb714e77e494e3af8163ffceac7cb6e6d788440c Mon Sep 17 00:00:00 2001 From: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> Date: Thu, 10 Sep 2026 16:38:30 -0400 Subject: refactor: remove flatpak transport from steam now playing --- src/components/NowPlayingTab.tsx | 1 - 1 file changed, 1 deletion(-) (limited to 'src/components') diff --git a/src/components/NowPlayingTab.tsx b/src/components/NowPlayingTab.tsx index 1bc331f..c067dc0 100644 --- a/src/components/NowPlayingTab.tsx +++ b/src/components/NowPlayingTab.tsx @@ -13,7 +13,6 @@ interface Props { } function targetDescription(game: GameTarget): string { - if (game.directFlatpak) return "Non-Steam · Direct Flatpak"; return game.nonSteam ? "Non-Steam" : "Steam"; } -- cgit v1.2.3 From 08404e49b9f21c16f3c69cdcd9eefe0250f594e2 Mon Sep 17 00:00:00 2001 From: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> Date: Thu, 10 Sep 2026 16:39:02 -0400 Subject: refactor: remove obsolete flatpak setup section --- src/components/FlatpakSetupSection.tsx | 110 --------------------------------- 1 file changed, 110 deletions(-) delete mode 100644 src/components/FlatpakSetupSection.tsx (limited to 'src/components') diff --git a/src/components/FlatpakSetupSection.tsx b/src/components/FlatpakSetupSection.tsx deleted file mode 100644 index 0119250..0000000 --- a/src/components/FlatpakSetupSection.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { ButtonItem, Field, PanelSection, PanelSectionRow } from "@decky/ui"; -import { useCallback, useEffect, useState } from "react"; -import { - getFlatpakApps, - prepareFlatpakApp, - removeFlatpakApp, - type FlatpakApp, -} from "../api/lsfgApi"; -import { showErrorToast } from "../utils/toastUtils"; - -interface Props { - enabled: boolean; -} - -export function FlatpakSetupSection({ enabled }: Props) { - const [apps, setApps] = useState([]); - const [loading, setLoading] = useState(false); - const [busyApp, setBusyApp] = useState(""); - const [error, setError] = useState(null); - - const load = useCallback(async () => { - if (!enabled) { - setApps([]); - setError(null); - return; - } - setLoading(true); - try { - const result = await getFlatpakApps(); - if (!result.success) throw new Error(result.error || "Could not list Flatpak applications"); - setApps(result.apps || []); - setError(null); - } catch (loadError) { - const message = loadError instanceof Error ? loadError.message : String(loadError); - setError(message); - } finally { - setLoading(false); - } - }, [enabled]); - - useEffect(() => { - void load(); - }, [load]); - - const toggle = async (app: FlatpakApp) => { - if (busyApp || (app.prepared && !app.owned)) return; - setBusyApp(app.app_id); - try { - const result = app.prepared - ? await removeFlatpakApp(app.app_id) - : await prepareFlatpakApp(app.app_id); - if (!result.success) throw new Error(result.error || "Flatpak setup failed"); - await load(); - } catch (operationError) { - const message = operationError instanceof Error ? operationError.message : String(operationError); - showErrorToast("Flatpak setup failed", message); - } finally { - setBusyApp(""); - } - }; - - if (!enabled) return null; - - return ( - - - - - {error && ( - - - - )} - {apps.map((app) => { - const busy = busyApp === app.app_id; - const description = [ - app.app_id, - app.runtime_branch ? `runtime ${app.runtime_branch}` : null, - app.error, - ].filter(Boolean).join(" · "); - const label = busy - ? "Working..." - : app.prepared - ? app.owned ? "Remove" : "Prepared externally" - : app.error ? "Unavailable" : "Prepare"; - return ( - - - void toggle(app)} - > - {label} - - - - ); - })} - - void load()}> - {loading ? "Refreshing..." : "Refresh Flatpaks"} - - - - ); -} -- cgit v1.2.3 From 69f5b6f884064e93f15570d8c7ceda5d610c39dd Mon Sep 17 00:00:00 2001 From: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> Date: Thu, 10 Sep 2026 16:46:41 -0400 Subject: fix: use running steam config in now playing --- src/components/Content.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) (limited to 'src/components') diff --git a/src/components/Content.tsx b/src/components/Content.tsx index ffb6eef..241ab92 100644 --- a/src/components/Content.tsx +++ b/src/components/Content.tsx @@ -45,6 +45,7 @@ function usePersistentBoolean(key: string, defaultValue: boolean) { export function Content() { const { config, + runningConfig, targets, runningGame, setSelectedAppId, @@ -135,8 +136,10 @@ export function Content() { const nowPlaying = runningGame?.configured ? ( handleConfigChange(field, value)} + config={runningConfig} + onConfigChange={async (field, value) => { + await save({ ...runningConfig, [field]: value }, true); + }} /> ) : runningFlatpak ? ( Date: Thu, 10 Sep 2026 16:47:38 -0400 Subject: fix: save now playing steam profile by app id --- src/components/Content.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'src/components') diff --git a/src/components/Content.tsx b/src/components/Content.tsx index 241ab92..a300f6f 100644 --- a/src/components/Content.tsx +++ b/src/components/Content.tsx @@ -50,6 +50,7 @@ export function Content() { runningGame, setSelectedAppId, save, + saveFor, enable, enableAll, repair, @@ -138,7 +139,7 @@ export function Content() { game={runningGame} config={runningConfig} onConfigChange={async (field, value) => { - await save({ ...runningConfig, [field]: value }, true); + await saveFor(runningGame.appid, { ...runningConfig, [field]: value }, true); }} /> ) : runningFlatpak ? ( -- cgit v1.2.3 From c3778706ab420494479740e19df08c0c3c5b2693 Mon Sep 17 00:00:00 2001 From: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> Date: Thu, 10 Sep 2026 16:48:39 -0400 Subject: fix: preserve config callback return type --- src/components/Content.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'src/components') diff --git a/src/components/Content.tsx b/src/components/Content.tsx index a300f6f..ce3c018 100644 --- a/src/components/Content.tsx +++ b/src/components/Content.tsx @@ -118,7 +118,9 @@ export function Content() { fieldName: keyof ConfigurationData, value: boolean | number | string | string[], cleanupLaunchOptions = false, - ) => save({ ...config, [fieldName]: value }, cleanupLaunchOptions); + ) => { + await save({ ...config, [fieldName]: value }, cleanupLaunchOptions); + }; const setup = ( Date: Thu, 10 Sep 2026 20:49:05 -0400 Subject: fix; correct flatpak extension sources --- src/components/FlatpakTab.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/components') diff --git a/src/components/FlatpakTab.tsx b/src/components/FlatpakTab.tsx index 6392d59..c5e27f5 100644 --- a/src/components/FlatpakTab.tsx +++ b/src/components/FlatpakTab.tsx @@ -40,12 +40,12 @@ export function FlatpakTab({ if (!selectedAppId) { return ( - + {/* - + */} {apps.map((app) => { const status = app.enabled ? app.app_id === runningApp?.app_id ? "Enabled · Running" : "Enabled" -- cgit v1.2.3 From d2bfdafa92f3d31cc5392bf8a5a1f1df5c2358ce Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Fri, 11 Sep 2026 09:05:14 -0400 Subject: flatpak correctness, ui alignment, tests --- src/components/CollapsibleItemGroup.tsx | 79 ++++++++++++++++++++++++++ src/components/Content.tsx | 56 +++++++++++++----- src/components/FlatpakNowPlayingTab.tsx | 26 +++++---- src/components/FlatpakTab.tsx | 79 +++++++++++++++++++------- src/components/GameConfigurationSelector.tsx | 85 ++++------------------------ 5 files changed, 204 insertions(+), 121 deletions(-) create mode 100644 src/components/CollapsibleItemGroup.tsx (limited to 'src/components') 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; +} + +export function CollapsibleItemGroup({ + title, + items, + collapsed, + onToggle, + onSelect, + toggleRef, +}: Props) { + if (items.length === 0) return null; + + return ( + <> + + + + +
+ + {collapsed ? : } + +
+
+ {!collapsed && items.map((item) => ( + + onSelect(item.id)} + highlightOnFocus + /> + + ))} + + ); +} 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(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" ? ( { - await saveFor(runningGame.appid, { ...runningConfig, [field]: value }, true); + await saveFor(nowPlayingTarget.game.appid, { ...runningConfig, [field]: value }, true); }} /> - ) : runningFlatpak ? ( + ) : nowPlayingTarget?.kind === "flatpak" ? ( - ) : null; + ) : ( + + ); 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 (
- + { + if (availableTabIds.has(nextTab)) setTab(nextTab); + }} + tabs={tabs} + /> +
+ ); +} + +function NowPlayingTabPlaceholder() { + return ( +
+ + + + +
); } 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; - onWorkaroundChange: (appId: string, state: WorkaroundState) => Promise; } -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 - + + {launcher && ( + + + + )} - onWorkaroundChange(app.app_id, state)} - /> ); } 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; } +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(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 ( - {/* - - */} - {apps.map((app) => { - const status = app.enabled - ? app.app_id === runningApp?.app_id ? "Enabled · Running" : "Enabled" - : app.prepared && !app.owned ? "Prepared externally" : "Available"; - return ( - - setSelectedAppId(app.app_id)} - highlightOnFocus - /> - - ); - })} + + ({ + 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} + /> + {apps.length === 0 && !loading && ( 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; -}) { - if (games.length === 0) return null; - - return ( - <> - - - - -
- - {collapsed ? : } - -
-
- {!collapsed && games.map((game) => ( - - onSelect(game.appid)} - highlightOnFocus - /> - - ))} - - ); -} - 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(null); @@ -146,39 +100,24 @@ export function GameConfigurationSelector({ return ( <> {targets.length === 0 && ( )} - - Date: Fri, 11 Sep 2026 09:09:29 -0400 Subject: fix tab change jutter --- src/components/ConfigurationTab.tsx | 1 - src/components/Content.tsx | 30 +++++++++++------------------- 2 files changed, 11 insertions(+), 20 deletions(-) (limited to 'src/components') diff --git a/src/components/ConfigurationTab.tsx b/src/components/ConfigurationTab.tsx index a6d4c2a..12f36a5 100644 --- a/src/components/ConfigurationTab.tsx +++ b/src/components/ConfigurationTab.tsx @@ -88,7 +88,6 @@ export function ConfigurationTab({ diff --git a/src/components/Content.tsx b/src/components/Content.tsx index 0d49f89..dc8c54f 100644 --- a/src/components/Content.tsx +++ b/src/components/Content.tsx @@ -1,5 +1,5 @@ -import { Field, PanelSection, PanelSectionRow, Tabs } from "@decky/ui"; -import { useEffect, useRef, useState } from "react"; +import { Tabs } from "@decky/ui"; +import { useEffect, useRef, useState, type FocusEvent } from "react"; import { FaCube, FaFileAlt, FaGamepad, FaList, FaTools } from "react-icons/fa"; import { ConfigurationData } from "../config/configSchema"; import { useFlatpakConfiguration } from "../hooks/useFlatpakConfiguration"; @@ -79,6 +79,7 @@ export function Content() { const flatpak = useFlatpakConfiguration(setupComplete); const [tab, setTab] = useState("Setup"); const [showDebugTab, setShowDebugTab] = usePersistentBoolean(DEBUG_TAB_VISIBILITY_KEY, true); + const [contentFocused, setContentFocused] = useState(false); const previousRunningWorkload = useRef(null); const runningFlatpak = flatpak.runningApp; const nowPlayingTarget = resolveNowPlayingTarget(runningGame, runningFlatpak); @@ -154,13 +155,11 @@ export function Content() { launcher={nowPlayingTarget.launcher} onConfigChange={flatpak.updateConfig} /> - ) : ( - - ); + ) : null; const tabs = setupComplete ? [ - { id: "NowPlaying", title: tabIcons.nowPlaying, content: nowPlaying }, + ...(nowPlaying ? [{ id: "NowPlaying", title: tabIcons.nowPlaying, content: nowPlaying }] : []), { id: "Games", title: tabIcons.games, @@ -205,11 +204,16 @@ export function Content() { const availableTabIds = new Set(tabs.map(({ id }) => id)); const activeTab = availableTabIds.has(tab) ? tab : setupComplete ? "Games" : "Setup"; + const handleFocusCapture = (event: FocusEvent) => { + const focusedElement = event.target as HTMLElement | null; + setContentFocused(!focusedElement?.closest?.('[role="tab"]')); + }; return (
); } - -function NowPlayingTabPlaceholder() { - return ( -
- - - - - -
- ); -} -- cgit v1.2.3 From 9138886fd5315eb0e8c6a4a549a9d44f93ba3cde Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Fri, 11 Sep 2026 09:30:30 -0400 Subject: consistent tab collapsed toggles on flatpak --- src/components/FlatpakTab.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) (limited to 'src/components') diff --git a/src/components/FlatpakTab.tsx b/src/components/FlatpakTab.tsx index 31bfc2d..b46eb28 100644 --- a/src/components/FlatpakTab.tsx +++ b/src/components/FlatpakTab.tsx @@ -20,6 +20,9 @@ interface Props { onWorkaroundChange: (appId: string, state: WorkaroundState) => Promise; } +const ENABLED_COLLAPSED_KEY = "lsfg-flatpak-enabled-collapsed-v2"; +const AVAILABLE_COLLAPSED_KEY = "lsfg-flatpak-available-collapsed-v2"; + function usePersistentCollapsed(key: string) { const [collapsed, setCollapsed] = useState(() => { try { @@ -55,8 +58,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 [enabledCollapsed, toggleEnabled] = usePersistentCollapsed(ENABLED_COLLAPSED_KEY); + const [availableCollapsed, toggleAvailable] = usePersistentCollapsed(AVAILABLE_COLLAPSED_KEY); const enabledToggleRef = useRef(null); const enabledApps = useMemo( -- cgit v1.2.3 From a8388009b2ba141caafe7f2e35f7029d93e7811f Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Fri, 11 Sep 2026 09:38:35 -0400 Subject: handle edge cases on flatpaks and ordering --- src/components/CollapsibleItemGroup.tsx | 20 +++++++++++++++++++- src/components/Content.tsx | 2 +- src/components/FlatpakTab.tsx | 24 ++---------------------- src/components/GameConfigurationSelector.tsx | 22 ++-------------------- 4 files changed, 24 insertions(+), 44 deletions(-) (limited to 'src/components') diff --git a/src/components/CollapsibleItemGroup.tsx b/src/components/CollapsibleItemGroup.tsx index a66a8ba..037b34f 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 { @@ -25,6 +25,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[]; diff --git a/src/components/Content.tsx b/src/components/Content.tsx index dc8c54f..422eec8 100644 --- a/src/components/Content.tsx +++ b/src/components/Content.tsx @@ -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; diff --git a/src/components/FlatpakTab.tsx b/src/components/FlatpakTab.tsx index b46eb28..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"; @@ -23,24 +23,6 @@ interface Props { const ENABLED_COLLAPSED_KEY = "lsfg-flatpak-enabled-collapsed-v2"; const AVAILABLE_COLLAPSED_KEY = "lsfg-flatpak-available-collapsed-v2"; -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, @@ -60,7 +42,6 @@ export function FlatpakTab({ const close = useCallback(() => setSelectedAppId(null), []); const [enabledCollapsed, toggleEnabled] = usePersistentCollapsed(ENABLED_COLLAPSED_KEY); const [availableCollapsed, toggleAvailable] = usePersistentCollapsed(AVAILABLE_COLLAPSED_KEY); - const enabledToggleRef = useRef(null); const enabledApps = useMemo( () => apps.filter((app) => app.enabled).sort((a, b) => a.app_name.localeCompare(b.app_name)), @@ -90,7 +71,6 @@ export function FlatpakTab({ collapsed={enabledCollapsed} onToggle={toggleEnabled} onSelect={setSelectedAppId} - toggleRef={enabledToggleRef} /> { - 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"; } -- cgit v1.2.3 From bc2669b6050da7acdf9e1dda2b77444734852559 Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Fri, 11 Sep 2026 10:00:19 -0400 Subject: workaround for new client update, ui cuts off in game --- src/components/Content.tsx | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) (limited to 'src/components') diff --git a/src/components/Content.tsx b/src/components/Content.tsx index 422eec8..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"; @@ -141,6 +141,10 @@ export function Content() { /> ); + const tabContent = (content: ReactNode) => ( +
{content}
+ ); + const nowPlaying = nowPlayingTarget?.kind === "steam" ? ( + />, ), }, { id: "Flatpak", title: tabIcons.flatpak, - content: ( + content: tabContent( + />, ), }, - ...(showDebugTab ? [{ id: "ConfigFile", title: tabIcons.configFile, content: }] : []), - { id: "Setup", title: tabIcons.setup, content: setup }, + ...(showDebugTab ? [{ id: "ConfigFile", title: tabIcons.configFile, content: tabContent() }] : []), + { 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"; -- cgit v1.2.3 From 0be6ccdd1a8ecfe386281451cf4c49d052f6dda4 Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Fri, 11 Sep 2026 10:13:22 -0400 Subject: move inline styping --- src/components/CollapsibleItemGroup.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'src/components') diff --git a/src/components/CollapsibleItemGroup.tsx b/src/components/CollapsibleItemGroup.tsx index 037b34f..29fe945 100644 --- a/src/components/CollapsibleItemGroup.tsx +++ b/src/components/CollapsibleItemGroup.tsx @@ -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; @@ -71,7 +76,6 @@ export function CollapsibleItemGroup({
Date: Fri, 11 Sep 2026 10:23:23 -0400 Subject: compact now playing info --- src/components/FlatpakNowPlayingTab.tsx | 27 +++++++++++---------------- src/components/NowPlayingSummary.tsx | 16 ++++++++++++++++ src/components/NowPlayingTab.tsx | 12 ++++++------ 3 files changed, 33 insertions(+), 22 deletions(-) create mode 100644 src/components/NowPlayingSummary.tsx (limited to 'src/components') 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 ( - - - - - {launcher && ( - - - - )} - + detail !== null)} + /> 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 ( + + + + + + ); +} 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 ( - - - - - + Date: Fri, 11 Sep 2026 11:08:12 -0400 Subject: better uninstall cleanup --- src/components/ConfigurationSection.tsx | 5 +- src/components/ConfigurationTab.tsx | 15 +----- src/components/Content.tsx | 13 ++++-- src/components/SetupTab.tsx | 82 +++++++++++++++++++++++---------- 4 files changed, 68 insertions(+), 47 deletions(-) (limited to 'src/components') diff --git a/src/components/ConfigurationSection.tsx b/src/components/ConfigurationSection.tsx index 1f17264..6996bcd 100644 --- a/src/components/ConfigurationSection.tsx +++ b/src/components/ConfigurationSection.tsx @@ -1,6 +1,6 @@ import { PanelSectionRow, ToggleField, SliderField } from "@decky/ui"; import { ConfigurationData } from "../config/configSchema"; -import { FLOW_SCALE, PERFORMANCE_MODE, OVERRIDE_PRESENT_MODE, PRESERVE_SWAPCHAIN_IMAGE_COUNT, NO_FP16 } from "../config/configSchema"; +import { FLOW_SCALE, PERFORMANCE_MODE, OVERRIDE_PRESENT_MODE, PRESERVE_SWAPCHAIN_IMAGE_COUNT } from "../config/configSchema"; interface ConfigurationSectionProps { config: ConfigurationData; @@ -12,9 +12,6 @@ export function ConfigurationSection({ config, onConfigChange }: ConfigurationSe onConfigChange(FLOW_SCALE, value)} /> - - onConfigChange(NO_FP16, !value)} /> - onConfigChange(PERFORMANCE_MODE, value)} /> diff --git a/src/components/ConfigurationTab.tsx b/src/components/ConfigurationTab.tsx index 12f36a5..37a7867 100644 --- a/src/components/ConfigurationTab.tsx +++ b/src/components/ConfigurationTab.tsx @@ -1,4 +1,4 @@ -import { ButtonItem, ConfirmModal, DialogButton, Focusable, PanelSection, PanelSectionRow, ToggleField, gamepadDialogClasses, showModal } from "@decky/ui"; +import { ButtonItem, ConfirmModal, DialogButton, 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"; @@ -11,8 +11,6 @@ interface ConfigurationTabProps { config: ConfigurationData; targets: GameTarget[]; runningGame: GameTarget | null; - showDebugTab: boolean; - onShowDebugTabChange: (value: boolean) => void; onSelect: (appid: string) => void; onConfigChange: (fieldName: keyof ConfigurationData, value: boolean | number | string | string[]) => Promise; onEnable: (appid: string) => Promise; @@ -26,8 +24,6 @@ export function ConfigurationTab({ config, targets, runningGame, - showDebugTab, - onShowDebugTabChange, onSelect, onConfigChange, onEnable, @@ -84,15 +80,6 @@ export function ConfigurationTab({ onConfiguredToggleFocused={clearConfiguredToggleFocusRequest} /> - - - - - ); } diff --git a/src/components/Content.tsx b/src/components/Content.tsx index 32e9bf8..91775df 100644 --- a/src/components/Content.tsx +++ b/src/components/Content.tsx @@ -47,16 +47,19 @@ export function Content() { const { config, runningConfig, + globalConfig, targets, runningGame, setSelectedAppId, save, saveFor, + updateGlobal, enable, enableAll, repair, resetSelected, resetAll, + cleanupAllWorkarounds, reload, } = useGameConfiguration(); const { @@ -69,7 +72,7 @@ export function Content() { isUninstalling, install, uninstall, - } = useInstallation(reload); + } = useInstallation(reload, cleanupAllWorkarounds); const setupComplete = isInstalled && losslessScalingInstalled && @@ -78,7 +81,7 @@ export function Content() { !steamBranchStatus.needs_switch; const flatpak = useFlatpakConfiguration(setupComplete); const [tab, setTab] = useState("Setup"); - const [showDebugTab, setShowDebugTab] = usePersistentBoolean(DEBUG_TAB_VISIBILITY_KEY, true); + const [showDebugTab, setShowDebugTab] = usePersistentBoolean(DEBUG_TAB_VISIBILITY_KEY, false); const [contentFocused, setContentFocused] = useState(false); const previousRunningWorkload = useRef(null); const runningFlatpak = flatpak.runningApp; @@ -136,6 +139,10 @@ export function Content() { steamBranchStatus={steamBranchStatus} isInstalling={isInstalling} isUninstalling={isUninstalling} + globalConfig={globalConfig} + showDebugTab={showDebugTab} + onGlobalConfigChange={updateGlobal} + onShowDebugTabChange={setShowDebugTab} onInstall={() => void install()} onUninstall={() => void uninstall()} /> @@ -172,8 +179,6 @@ export function Content() { config={config} targets={targets} runningGame={runningGame} - showDebugTab={showDebugTab} - onShowDebugTabChange={setShowDebugTab} onSelect={setSelectedAppId} onConfigChange={(field, value) => handleConfigChange(field, value, true)} onEnable={enable} diff --git a/src/components/SetupTab.tsx b/src/components/SetupTab.tsx index d769200..ff072cb 100644 --- a/src/components/SetupTab.tsx +++ b/src/components/SetupTab.tsx @@ -1,5 +1,5 @@ -import { ButtonItem, Field, PanelSection, PanelSectionRow } from "@decky/ui"; -import { type SteamBranchStatus } from "../api/lsfgApi"; +import { ButtonItem, Field, PanelSection, PanelSectionRow, ToggleField } from "@decky/ui"; +import { type GlobalConfig, type SteamBranchStatus } from "../api/lsfgApi"; import t from "../i18n/i18n"; interface SetupTabProps { @@ -10,6 +10,10 @@ interface SetupTabProps { steamBranchStatus: SteamBranchStatus | null; isInstalling: boolean; isUninstalling: boolean; + globalConfig: GlobalConfig; + showDebugTab: boolean; + onGlobalConfigChange: (config: GlobalConfig) => Promise; + onShowDebugTabChange: (value: boolean) => void; onInstall: () => void; onUninstall: () => void; } @@ -23,6 +27,10 @@ export function SetupTab(props: SetupTabProps) { steamBranchStatus, isInstalling, isUninstalling, + globalConfig, + showDebugTab, + onGlobalConfigChange, + onShowDebugTabChange, onInstall, onUninstall, } = props; @@ -36,33 +44,57 @@ export function SetupTab(props: SetupTabProps) { : t("INSTALL_INSTALL_BTN", "Install LSFG-VK"); return ( - - - - - - - - {steamBranchStatus?.installed && ( + <> + + + + + {steamBranchStatus?.installed && ( + + + + )} + + + {buttonLabel} + + + + {isInstalled && ( + <> + + + void onGlobalConfigChange({ ...globalConfig, no_fp16: !value })} + /> + + + + + + + + )} - - - {buttonLabel} - - - + ); } -- cgit v1.2.3 From 4d14bc1086a1ba05acd01d7466749ff3cffba75f Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Fri, 11 Sep 2026 12:58:16 -0400 Subject: enable all and disable all for flatpak --- src/components/Content.tsx | 2 ++ src/components/FlatpakTab.tsx | 53 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 54 insertions(+), 1 deletion(-) (limited to 'src/components') diff --git a/src/components/Content.tsx b/src/components/Content.tsx index 91775df..3f25320 100644 --- a/src/components/Content.tsx +++ b/src/components/Content.tsx @@ -200,7 +200,9 @@ export function Content() { busyAppId={flatpak.busyAppId} onRefresh={flatpak.reload} onEnable={flatpak.enableApp} + onEnableAll={flatpak.enableAll} onRemove={flatpak.removeApp} + onRemoveAll={flatpak.removeAll} onConfigChange={flatpak.updateConfig} onWorkaroundChange={flatpak.updateWorkarounds} />, diff --git a/src/components/FlatpakTab.tsx b/src/components/FlatpakTab.tsx index 0b20da0..35721f6 100644 --- a/src/components/FlatpakTab.tsx +++ b/src/components/FlatpakTab.tsx @@ -1,4 +1,4 @@ -import { ButtonItem, DialogButton, Field, Focusable, PanelSection, PanelSectionRow, gamepadDialogClasses } from "@decky/ui"; +import { ButtonItem, ConfirmModal, DialogButton, Field, Focusable, PanelSection, PanelSectionRow, gamepadDialogClasses, showModal } from "@decky/ui"; import { useCallback, useMemo, useState } from "react"; import { FaArrowLeft } from "react-icons/fa"; import type { FlatpakApp, LsfgConfig, WorkaroundState } from "../api/lsfgApi"; @@ -15,7 +15,9 @@ interface Props { busyAppId: string; onRefresh: () => Promise; onEnable: (appId: string) => Promise; + onEnableAll: () => Promise; onRemove: (appId: string) => Promise; + onRemoveAll: () => Promise; onConfigChange: (appId: string, config: LsfgConfig) => Promise; onWorkaroundChange: (appId: string, state: WorkaroundState) => Promise; } @@ -30,7 +32,9 @@ export function FlatpakTab({ busyAppId, onRefresh, onEnable, + onEnableAll, onRemove, + onRemoveAll, onConfigChange, onWorkaroundChange, }: Props) { @@ -51,6 +55,33 @@ export function FlatpakTab({ () => apps.filter((app) => !app.enabled).sort((a, b) => a.app_name.localeCompare(b.app_name)), [apps], ); + const enableableApps = useMemo( + () => availableApps.filter((app) => !(app.prepared && !app.owned) && !app.error), + [availableApps], + ); + const confirmEnableAll = () => { + showModal( + void onEnableAll()} + onCancel={() => {}} + />, + ); + }; + const confirmRemoveAll = () => { + showModal( + void onRemoveAll()} + onCancel={() => {}} + />, + ); + }; const itemFor = (app: FlatpakApp) => ({ id: app.app_id, label: app.app_name, @@ -79,6 +110,26 @@ export function FlatpakTab({ onToggle={toggleAvailable} onSelect={setSelectedAppId} /> + {enableableApps.length > 0 && ( + + + Enable all available Flatpaks + + + )} + + + Remove all profiles + + {apps.length === 0 && !loading && ( -- cgit v1.2.3 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 From f185d26f4d37894183cdfa8c3e6ffc718cb3b103 Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Fri, 11 Sep 2026 14:16:06 -0400 Subject: labeled slider for multiplier --- src/components/FpsMultiplierControl.tsx | 81 ++++++++++----------------------- 1 file changed, 25 insertions(+), 56 deletions(-) (limited to 'src/components') diff --git a/src/components/FpsMultiplierControl.tsx b/src/components/FpsMultiplierControl.tsx index 2c50be1..523e8b3 100644 --- a/src/components/FpsMultiplierControl.tsx +++ b/src/components/FpsMultiplierControl.tsx @@ -1,4 +1,4 @@ -import { DialogButton, Focusable, PanelSectionRow } from "@decky/ui"; +import { Focusable, PanelSectionRow, SliderField } from "@decky/ui"; import { useEffect, useRef } from "react"; import { ConfigurationData } from "../config/configSchema"; import { MULTIPLIER } from "../config/generatedConfigSchema"; @@ -31,63 +31,32 @@ export function FpsMultiplierControl({ return () => cancelAnimationFrame(frame); }, [autoFocus, onAutoFocus]); + const multiplierLabel = config.multiplier === 1 + ? t("MULTIPLIER_OFF", "Off") + : `${config.multiplier}x`; + return ( - - void onConfigChange(MULTIPLIER, Math.max(1, config.multiplier - 1))} - disabled={config.multiplier <= 1} - > - − - -
4 ? "red" : "white", - minWidth: "60px", - textAlign: "center", - }} - > - {config.multiplier < 2 ? t("MULTIPLIER_OFF", "OFF") : `${config.multiplier}X`} -
- void onConfigChange(MULTIPLIER, Math.min(4, config.multiplier + 1))} - disabled={config.multiplier >= 4} - > - + - + + void onConfigChange(MULTIPLIER, value)} + />
); -- cgit v1.2.3 From 81feb03288166755545401b9df2ff2c9bc3ae7d7 Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Fri, 11 Sep 2026 16:48:35 -0400 Subject: handle flatpak grey, id proton and exclusions --- src/components/CollapsibleItemGroup.tsx | 4 +++- src/components/GameConfigurationSelector.tsx | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) (limited to 'src/components') diff --git a/src/components/CollapsibleItemGroup.tsx b/src/components/CollapsibleItemGroup.tsx index 29fe945..a4e4092 100644 --- a/src/components/CollapsibleItemGroup.tsx +++ b/src/components/CollapsibleItemGroup.tsx @@ -6,6 +6,7 @@ export interface CollapsibleItem { id: string; label: string; description: string; + disabled?: boolean; } export const collapsibleItemGroupStyles = ` @@ -91,7 +92,8 @@ export function CollapsibleItemGroup({ onSelect(item.id)} + disabled={item.disabled} + onActivate={item.disabled ? undefined : () => onSelect(item.id)} highlightOnFocus />
diff --git a/src/components/GameConfigurationSelector.tsx b/src/components/GameConfigurationSelector.tsx index ad3537d..91dc3df 100644 --- a/src/components/GameConfigurationSelector.tsx +++ b/src/components/GameConfigurationSelector.tsx @@ -20,6 +20,7 @@ const ENABLED_COLLAPSED_KEY = "lsfg-enabled-games-collapsed-v4"; const AVAILABLE_COLLAPSED_KEY = "lsfg-available-games-collapsed-v3"; function targetDescription(game: GameTarget): string { + if (game.isFlatpakShortcut) return "Non-Steam | Use Flatpak Tab"; return game.source === "unknown" ? "Unknown source · excluded from bulk actions" : sourceLabel(game.source); @@ -43,8 +44,8 @@ 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 enableableGames = availableGames.filter((game) => game.source === source && !game.isFlatpakShortcut); + const removableGames = enabledGames.filter((game) => game.source === source && !game.isFlatpakShortcut); const sourceName = source === "nonSteam" ? "non-Steam shortcuts" : "Steam games"; const emptyDescription = source === "nonSteam" ? "Steam has not reported any eligible non-Steam shortcuts" @@ -53,6 +54,7 @@ export function GameConfigurationSelector({ id: game.appid, label: game.name, description: targetDescription(game), + disabled: game.isFlatpakShortcut, }); const [enabledCollapsed, toggleEnabled] = usePersistentCollapsed(`${ENABLED_COLLAPSED_KEY}-${source}`); const [availableCollapsed, toggleAvailable] = usePersistentCollapsed(`${AVAILABLE_COLLAPSED_KEY}-${source}`); -- cgit v1.2.3