diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/api/lsfgApi.ts | 12 | ||||
| -rw-r--r-- | src/components/ConfigFileTab.tsx | 46 | ||||
| -rw-r--r-- | src/components/ConfigurationTab.tsx | 54 | ||||
| -rw-r--r-- | src/components/Content.tsx | 223 | ||||
| -rw-r--r-- | src/components/FlatpaksModal.tsx | 460 | ||||
| -rw-r--r-- | src/components/FlatpaksTab.tsx | 195 | ||||
| -rw-r--r-- | src/components/GameConfigurationSelector.tsx | 2 | ||||
| -rw-r--r-- | src/components/NerdStuffModal.tsx | 148 | ||||
| -rw-r--r-- | src/components/SetupTab.tsx | 47 | ||||
| -rw-r--r-- | src/components/SmartClipboardButton.tsx | 91 | ||||
| -rw-r--r-- | src/components/UsageInstructions.tsx | 69 | ||||
| -rw-r--r-- | src/components/index.ts | 8 | ||||
| -rw-r--r-- | src/hooks/useGameConfiguration.ts | 26 | ||||
| -rw-r--r-- | src/i18n/languages.json | 51 | ||||
| -rw-r--r-- | src/styles.ts | 34 |
15 files changed, 504 insertions, 962 deletions
diff --git a/src/api/lsfgApi.ts b/src/api/lsfgApi.ts index b38fa3b..04d1309 100644 --- a/src/api/lsfgApi.ts +++ b/src/api/lsfgApi.ts @@ -49,7 +49,7 @@ export interface GameConfigEntry { profile: string; config: LsfgConfig; } -export interface InstalledGame { appid: string; name: string; } +export interface InstalledGame { appid: string; name: string; nonSteam: boolean; } export interface InstalledGamesResult { success: boolean; games?: InstalledGame[]; error?: string; } export interface GameConfigsResult { @@ -71,12 +71,6 @@ export interface ConfigSchemaResult { defaults: ConfigurationData; } -export interface LaunchOptionResult { - launch_option: string; - instructions: string; - explanation: string; -} - export interface FileContentResult { success: boolean; content?: string; @@ -131,9 +125,7 @@ export const checkLsfgVkInstalled = callable<[], InstallationStatus>("check_lsfg export const getLosslessScalingBranchStatus = callable<[], SteamBranchStatus>("get_lossless_scaling_branch_status"); export const getLsfgConfig = callable<[], ConfigResult>("get_lsfg_config"); export const getConfigSchema = callable<[], ConfigSchemaResult>("get_config_schema"); -export const getLaunchOption = callable<[], LaunchOptionResult>("get_launch_option"); export const getConfigFileContent = callable<[], FileContentResult>("get_config_file_content"); -export const getLaunchScriptContent = callable<[], FileContentResult>("get_launch_script_content"); export const checkFgmodDirectory = callable<[], FgmodCheckResult>("check_fgmod_directory"); // Flatpak management API functions @@ -152,7 +144,7 @@ export const updateLsfgConfig = callable< export const getGameConfigs = callable<[], GameConfigsResult>("get_game_configs"); export const getInstalledGames = callable<[], InstalledGamesResult>("get_installed_games"); export const getGameConfig = callable<[string], GameConfigResult>("get_game_config"); -export const updateGameConfig = callable<[string, LsfgConfig], GameConfigResult>("update_game_config"); +export const updateGameConfig = callable<[string, string, LsfgConfig], GameConfigResult>("update_game_config"); export const resetGameConfig = callable<[string], GameConfigResult>("reset_game_config"); export const resetAllGameConfigs = callable<[], GameConfigsResult>("reset_all_game_configs"); 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<FileContentResult | null>(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 ( + <PanelSection title={t("NERD_CONFIG_FILE", "Configuration File")}> + <PanelSectionRow><Spinner /></PanelSectionRow> + </PanelSection> + ); + } + + return ( + <PanelSection title={t("NERD_CONFIG_FILE", "Configuration File")}> + <PanelSectionRow> + <Field label={result.path || t("NERD_CONFIG_FILE", "Configuration File")} description={result.error || "Tap the file to copy it."}> + {result.success && result.content && ( + <Focusable onActivate={() => void copy(result.content || "")}> + <pre style={{ maxHeight: "420px", overflow: "auto", whiteSpace: "pre-wrap", fontSize: "12px" }}> + {result.content} + </pre> + </Focusable> + )} + </Field> + </PanelSectionRow> + </PanelSection> + ); +} 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<void>; + onReset: () => Promise<void>; + onResetAll: () => Promise<void>; +} + +export function ConfigurationTab({ + config, + targets, + runningGame, + selectedAppId, + onSelect, + onConfigChange, + onReset, + onResetAll, +}: ConfigurationTabProps) { + return ( + <> + <PanelSection title={t("CONTENT_FPS_MULTIPLIER", "FPS Multiplier")}> + <FpsMultiplierControl config={config} onConfigChange={onConfigChange} /> + </PanelSection> + <PanelSection title="Game Profile"> + <GameConfigurationSelector + targets={targets} + runningGame={runningGame} + selectedAppId={selectedAppId} + onSelect={onSelect} + onReset={onReset} + onResetAll={onResetAll} + /> + </PanelSection> + <PanelSection title="Options"> + <ConfigurationSection config={config} onConfigChange={onConfigChange} /> + </PanelSection> + <PanelSection> + <FgmodClipboardButton /> + </PanelSection> + </> + ); +} 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: <FaGamepad size={18} />, + flatpak: <FaLayerGroup size={18} />, + configFile: <FaFileAlt size={18} />, + setup: <FaTools size={18} />, +}; 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(<NerdStuffModal />); - }; + const setupContent = ( + <SetupTab + isInstalled={isInstalled} + installationStatus={installationStatus} + losslessScalingInstalled={losslessScalingInstalled} + losslessScalingStatus={losslessScalingStatus} + steamBranchStatus={steamBranchStatus} + isInstalling={isInstalling} + isUninstalling={isUninstalling} + onInstall={onInstall} + onUninstall={onUninstall} + /> + ); - const handleShowFlatpaks = () => { - showModal(<FlatpaksModal />); - }; + const tabs = setupComplete + ? [ + { + id: "Configuration", + title: tabIcons.configuration, + content: ( + <ConfigurationTab + config={config} + targets={targets} + runningGame={runningGame} + selectedAppId={selectedAppId} + onSelect={setSelectedAppId} + onConfigChange={handleConfigChange} + onReset={resetSelected} + onResetAll={resetAll} + /> + ), + }, + { id: "Flatpak", title: tabIcons.flatpak, content: <FlatpaksTab /> }, + { id: "ConfigFile", title: tabIcons.configFile, content: <ConfigFileTab /> }, + { id: "Setup", title: tabIcons.setup, content: setupContent }, + ] + : [ + { id: "Setup", title: tabIcons.setup, content: setupContent }, + ]; return ( - <PanelSection> - {!isInstalled && ( - <> - <InstallationButton - isInstalled={isInstalled} - isInstalling={isInstalling} - isUninstalling={isUninstalling} - onInstall={onInstall} - onUninstall={onUninstall} - /> - - <StatusDisplay - isInstalled={isInstalled} - installationStatus={installationStatus} - losslessScalingInstalled={losslessScalingInstalled} - losslessScalingStatus={losslessScalingStatus} - steamBranchStatus={steamBranchStatus} - /> - </> - )} - - {isInstalled && ( - <> - <PanelSectionRow> - <div - style={{ - fontSize: "14px", - fontWeight: "bold", - marginTop: "8px", - marginBottom: "6px", - borderBottom: "1px solid rgba(255, 255, 255, 0.2)", - paddingBottom: "3px", - color: "white" - }} - > - {t('CONTENT_FPS_MULTIPLIER', 'FPS Multiplier')} - </div> - </PanelSectionRow> - - <FpsMultiplierControl - config={config} - onConfigChange={handleConfigChange} - /> - </> - )} - - {isInstalled && ( - <GameConfigurationSelector targets={targets} runningGame={runningGame} selectedAppId={selectedAppId} onSelect={setSelectedAppId} onReset={resetSelected} onResetAll={resetAll} /> - )} - - {isInstalled && ( - <ConfigurationSection - config={config} - onConfigChange={handleConfigChange} - /> - )} - - {isInstalled && ( - <> - <SmartClipboardButton /> - <FgmodClipboardButton /> - </> - )} - - <UsageInstructions /> - - <PanelSectionRow> - <ButtonItem - layout="below" - onClick={handleShowNerdStuff} - > - {t('CONTENT_NERD_STUFF', 'Nerd Stuff')} - </ButtonItem> - </PanelSectionRow> - - <PanelSectionRow> - <ButtonItem - layout="below" - onClick={handleShowFlatpaks} - > - {t('CONTENT_FLATPAK_SETUP', 'Flatpak Setup')} - </ButtonItem> - </PanelSectionRow> - - {isInstalled && ( - <> - <StatusDisplay - isInstalled={isInstalled} - installationStatus={installationStatus} - losslessScalingInstalled={losslessScalingInstalled} - losslessScalingStatus={losslessScalingStatus} - steamBranchStatus={steamBranchStatus} - /> - - <InstallationButton - isInstalled={isInstalled} - isInstalling={isInstalling} - isUninstalling={isUninstalling} - onInstall={onInstall} - onUninstall={onUninstall} - /> - </> - )} - </PanelSection> + <div + className="lsfg-vk-tabs" + style={{ height: "95%", width: "300px", position: "fixed", marginTop: "-12px", overflow: "hidden" }} + > + <style>{tabStyles}</style> + <Tabs activeTab={tab} onShowTab={setTab} tabs={tabs} /> + </div> ); } 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<FlatpaksModalProps> = ({ closeModal }) => { - const [extensionStatus, setExtensionStatus] = useState<FlatpakExtensionStatus | null>(null); - const [flatpakApps, setFlatpakApps] = useState<FlatpakAppInfo | null>(null); - const [loading, setLoading] = useState(true); - const [operationInProgress, setOperationInProgress] = useState<string | null>(null); - const [operationError, setOperationError] = useState<string | null>(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( - <ConfirmModal - strTitle={title} - strDescription={description} - onOK={operation} - onCancel={() => {}} - /> - ); - }; - - if (loading) { - return ( - <ModalRoot closeModal={closeModal}> - <DialogHeader>{t('FLATPAK_MODAL_TITLE', 'Flatpak Extensions')}</DialogHeader> - <DialogBody> - <div style={{ display: 'flex', justifyContent: 'center', padding: '20px' }}> - <Spinner /> - </div> - </DialogBody> - </ModalRoot> - ); - } - - 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 ( - <ModalRoot closeModal={closeModal}> - <DialogHeader>{t('FLATPAK_MODAL_TITLE', 'Flatpak Extensions')}</DialogHeader> - <DialogBody> - <Focusable> - {/* Extension Status Section */} - <DialogControlsSection> - <DialogControlsSectionHeader>{t('FLATPAK_RUNTIME_INSTALLER', 'Runtime Extension Installer')}</DialogControlsSectionHeader> - - {operationError && ( - <PanelSectionRow> - <Field - label={t('FLATPAK_OPERATION_ERROR', 'Operation failed')} - description={operationError} - icon={<FaTimes style={{color: 'red'}} />} - /> - </PanelSectionRow> - )} - - {extensionStatus && extensionStatus.success ? ( - <> - <PanelSectionRow> - <Field - label={t('FLATPAK_RUNTIME_23', 'Runtime 23.08')} - description={extensionStatus.installed_23_08 ? t('FLATPAK_INSTALLED', 'Installed') : t('FLATPAK_NOT_INSTALLED', 'Not installed')} - icon={extensionStatus.installed_23_08 ? <FaCheck style={{color: 'green'}} /> : <FaTimes style={{color: 'red'}} />} - > - <ButtonItem - layout="below" - onClick={() => { - 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' ? ( - <Spinner /> - ) : extensionStatus.installed_23_08 ? ( - <> - <FaTrash /> {t('FLATPAK_UNINSTALL_BTN', 'Uninstall')} - </> - ) : ( - <> - <FaDownload /> {t('FLATPAK_INSTALL_BTN', 'Install')} - </> - )} - </ButtonItem> - </Field> - </PanelSectionRow> - - {/* 24.08 Runtime */} - <PanelSectionRow> - <Field - label={t('FLATPAK_RUNTIME_24', 'Runtime 24.08')} - description={extensionStatus.installed_24_08 ? t('FLATPAK_INSTALLED', 'Installed') : t('FLATPAK_NOT_INSTALLED', 'Not installed')} - icon={extensionStatus.installed_24_08 ? <FaCheck style={{color: 'green'}} /> : <FaTimes style={{color: 'red'}} />} - > - <ButtonItem - layout="below" - onClick={() => { - 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' ? ( - <Spinner /> - ) : extensionStatus.installed_24_08 ? ( - <> - <FaTrash /> {t('FLATPAK_UNINSTALL_BTN', 'Uninstall')} - </> - ) : ( - <> - <FaDownload /> {t('FLATPAK_INSTALL_BTN', 'Install')} - </> - )} - </ButtonItem> - </Field> - </PanelSectionRow> - - {/* 25.08 Runtime */} - <PanelSectionRow> - <Field - label={t('FLATPAK_RUNTIME_25', 'Runtime 25.08')} - description={extensionStatus.installed_25_08 ? t('FLATPAK_INSTALLED', 'Installed') : t('FLATPAK_NOT_INSTALLED', 'Not installed')} - icon={extensionStatus.installed_25_08 ? <FaCheck style={{color: 'green'}} /> : <FaTimes style={{color: 'red'}} />} - > - <ButtonItem - layout="below" - onClick={() => { - 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' ? ( - <Spinner /> - ) : extensionStatus.installed_25_08 ? ( - <> - <FaTrash /> {t('FLATPAK_UNINSTALL_BTN', 'Uninstall')} - </> - ) : ( - <> - <FaDownload /> {t('FLATPAK_INSTALL_BTN', 'Install')} - </> - )} - </ButtonItem> - </Field> - </PanelSectionRow> - </> - ) : ( - <PanelSectionRow> - <Field - label={t('FLATPAK_ERROR', 'Error')} - description={extensionStatus?.error || t('FLATPAK_ERROR_STATUS', 'Failed to check extension status')} - icon={<FaTimes style={{color: 'red'}} />} - /> - </PanelSectionRow> - )} - </DialogControlsSection> - - {/* Flatpak Apps Section */} - <DialogControlsSection> - <DialogControlsSectionHeader>{t('FLATPAK_APPS_TITLE', 'Flatpak Applications')}</DialogControlsSectionHeader> - - {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 ( - <PanelSectionRow key={app.app_id}> - <Field - label={app.app_name || app.app_id} - description={`${app.app_id} - ${statusText}`} - icon={<FaCog style={{color: statusColor}} />} - > - <Toggle - value={hasOverrides} - onChange={() => handleAppOverrideToggle(app)} - disabled={operationInProgress === `app-${app.app_id}`} - /> - </Field> - </PanelSectionRow> - ); - }) - ) : ( - <PanelSectionRow> - <Field - label={t('FLATPAK_NO_APPS', 'No Flatpak Apps Found')} - description={t('FLATPAK_NO_APPS_DESC', 'No Flatpak applications are currently installed')} - /> - </PanelSectionRow> - ) - ) : ( - <PanelSectionRow> - <Field - label={t('FLATPAK_ERROR', 'Error')} - description={flatpakApps?.error || t('FLATPAK_ERROR_APPS', 'Failed to load Flatpak applications')} - icon={<FaTimes style={{color: 'red'}} />} - /> - </PanelSectionRow> - )} - </DialogControlsSection> - - {/* Steam Configuration Instructions */} - <DialogControlsSection> - <DialogControlsSectionHeader>{t('FLATPAK_STEAM_CONFIG_TITLE', 'Steam Configuration')}</DialogControlsSectionHeader> - <div - style={{ - padding: '12px', - background: 'rgba(255, 255, 255, 0.1)', - borderRadius: '8px', - margin: '8px 0', - display: 'flex', - flexDirection: 'column' - }} - > - <div style={{ fontWeight: 'bold', marginBottom: '8px', color: '#fff' }}> - {t('FLATPAK_STEAM_CONFIG_HEADER', 'Configure Steam Flatpak Shortcuts')} - </div> - <div style={{ fontSize: '0.9em', lineHeight: '1.4', marginBottom: '8px' }}> - {t('FLATPAK_STEAM_CONFIG_DESC', 'In Steam, open your flatpak game and click the cog wheel.')} - </div> - <div style={{ fontSize: '0.9em', lineHeight: '1.4', marginBottom: '12px', color: '#ffa500' }}> - <strong>IMPORTANT:</strong> {t('FLATPAK_STEAM_CONFIG_IMPORTANT', 'Set this in TARGET (NOT LAUNCH OPTIONS)')} - </div> - - {instructionSteps.map((step) => ( - <Focusable - key={step.id} - focusWithinClassName="gpfocuswithin" - onActivate={() => {}} - style={focusableInstructionStyle} - > - <div style={{ fontWeight: 'bold' }}>{step.title}</div> - <div style={commandStyle}>{step.command}</div> - </Focusable> - ))} - - <Focusable - focusWithinClassName="gpfocuswithin" - onActivate={() => {}} - style={{ marginTop: '4px' }} - > - <div style={{ textAlign: 'center' }}> - <img - src={flatpakTargetImage.replace(/ /g, '%20')} - alt="Steam Properties Target Field Example" - style={{ - maxWidth: '100%', - height: 'auto', - border: '1px solid rgba(255, 255, 255, 0.2)', - borderRadius: '4px' - }} - /> - </div> - </Focusable> - </div> - </DialogControlsSection> - - {/* Close Button */} - <DialogControlsSection> - <PanelSectionRow> - <ButtonItem - layout="below" - onClick={closeModal} - > - {t('FLATPAK_CLOSE', 'Close')} - </ButtonItem> - </PanelSectionRow> - </DialogControlsSection> - </Focusable> - </DialogBody> - </ModalRoot> - ); -}; 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 ( + <PanelSectionRow> + <Field + label={`Runtime ${version}`} + description={installed ? t("FLATPAK_INSTALLED", "Installed") : t("FLATPAK_NOT_INSTALLED", "Not installed")} + icon={installed ? <FaCheck style={{ color: "green" }} /> : <FaTimes style={{ color: "red" }} />} + > + <ButtonItem layout="below" onClick={onAction} disabled={busy}> + {busy ? <Spinner /> : installed ? <><FaTrash /> {t("FLATPAK_UNINSTALL_BTN", "Uninstall")}</> : <><FaDownload /> {t("FLATPAK_INSTALL_BTN", "Install")}</>} + </ButtonItem> + </Field> + </PanelSectionRow> + ); +} + +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 ( + <PanelSectionRow> + <Field + label={app.app_name || app.app_id} + description={`${app.app_id} - ${status}`} + icon={<FaCog style={{ color: configured ? "green" : partial ? "orange" : "red" }} />} + > + <Toggle value={configured} onChange={onToggle} disabled={busy} /> + </Field> + </PanelSectionRow> + ); +} + +export function FlatpaksTab() { + const [extensionStatus, setExtensionStatus] = useState<FlatpakExtensionStatus | null>(null); + const [apps, setApps] = useState<FlatpakAppInfo | null>(null); + const [loading, setLoading] = useState(true); + const [operation, setOperation] = useState<string | null>(null); + const [error, setError] = useState<string | null>(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( + <ConfirmModal + strTitle={t("FLATPAK_UNINSTALL_TITLE", "Uninstall Runtime Extension")} + strDescription={`${t("FLATPAK_UNINSTALL_CONFIRM_PREFIX", "Are you sure you want to uninstall the")} ${version} ${t("FLATPAK_UNINSTALL_CONFIRM_SUFFIX", "runtime extension?")}`} + onOK={() => 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 <PanelSection title={t("FLATPAK_MODAL_TITLE", "Flatpak Extensions")}><PanelSectionRow><Spinner /></PanelSectionRow></PanelSection>; + } + + return ( + <> + <PanelSection title={t("FLATPAK_RUNTIME_INSTALLER", "Runtime Extension Installer")}> + {error && <PanelSectionRow><Field label={t("FLATPAK_OPERATION_ERROR", "Operation failed")} description={error} icon={<FaTimes style={{ color: "red" }} />} /></PanelSectionRow>} + {extensionStatus?.success ? runtimeVersions.map(({ version, key }) => ( + <RuntimeRow + key={version} + version={version} + installed={extensionStatus[key]} + busy={operation === `${extensionStatus[key] ? "uninstall" : "install"}-${version}`} + onAction={() => confirmExtensionOperation(version, extensionStatus[key])} + /> + )) : <PanelSectionRow><Field label={t("FLATPAK_ERROR", "Error")} description={extensionStatus?.error || error || t("FLATPAK_ERROR_STATUS", "Failed to check extension status")} /></PanelSectionRow>} + </PanelSection> + + <PanelSection title={t("FLATPAK_APPS_TITLE", "Flatpak Applications")}> + {apps?.success ? apps.apps.length ? apps.apps.map((app) => ( + <AppRow + key={app.app_id} + app={app} + busy={operation === `app-${app.app_id}`} + onToggle={() => void toggleApp(app)} + /> + )) : <PanelSectionRow><Field label={t("FLATPAK_NO_APPS", "No Flatpak Apps Found")} description={t("FLATPAK_NO_APPS_DESC", "No Flatpak applications are currently installed")} /></PanelSectionRow> : <PanelSectionRow><Field label={t("FLATPAK_ERROR", "Error")} description={apps?.error || error || t("FLATPAK_ERROR_APPS", "Failed to load Flatpak applications")} /></PanelSectionRow>} + </PanelSection> + </> + ); +} 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 <> <PanelSectionRow> 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<FileContentResult | null>(null); - const [scriptContent, setScriptContent] = useState<FileContentResult | null>(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState<string | null>(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 ( - <ModalRoot onCancel={closeModal} onOK={closeModal}> - {loading && ( - <div>{t('NERD_LOADING', 'Loading information...')}</div> - )} - - {error && ( - <div>Error: {error}</div> - )} - - {!loading && !error && ( - <> - {/* Launch Script Section */} - {scriptContent && ( - <Field label={t('NERD_LAUNCH_SCRIPT', 'Launch Script')}> - {!scriptContent.success ? ( - <div>{t('NERD_SCRIPT_NOT_FOUND_PREFIX', 'Script not found:')} {scriptContent.error}</div> - ) : ( - <div> - <div style={{ marginBottom: "8px", fontSize: "0.9em", opacity: 0.8 }}> - {t('NERD_PATH_PREFIX', 'Path:')} {scriptContent.path} - </div> - <Focusable - onClick={() => scriptContent.content && copyToClipboard(scriptContent.content)} - onActivate={() => scriptContent.content && copyToClipboard(scriptContent.content)} - > - <pre style={{ - background: "rgba(255, 255, 255, 0.1)", - padding: "8px", - borderRadius: "4px", - fontSize: "0.8em", - whiteSpace: "pre-wrap", - overflow: "auto", - maxHeight: "150px" - }}> - {scriptContent.content || t('NERD_NO_CONTENT', 'No content')} - </pre> - </Focusable> - </div> - )} - </Field> - )} - - {/* Config File Section */} - {configContent && ( - <Field label={t('NERD_CONFIG_FILE', 'Configuration File')}> - {!configContent.success ? ( - <div>{t('NERD_CONFIG_NOT_FOUND_PREFIX', 'Config not found:')} {configContent.error}</div> - ) : ( - <div> - <div style={{ marginBottom: "8px", fontSize: "0.9em", opacity: 0.8 }}> - {t('NERD_PATH_PREFIX', 'Path:')} {configContent.path} - </div> - <Focusable - onClick={() => configContent.content && copyToClipboard(configContent.content)} - onActivate={() => configContent.content && copyToClipboard(configContent.content)} - > - <pre style={{ - background: "rgba(255, 255, 255, 0.1)", - padding: "8px", - borderRadius: "4px", - fontSize: "0.8em", - whiteSpace: "pre-wrap", - overflow: "auto" - }}> - {configContent.content || t('NERD_NO_CONTENT', 'No content')} - </pre> - </Focusable> - </div> - )} - </Field> - )} - - {/* Close Button */} - <DialogControlsSection> - <PanelSectionRow> - <ButtonItem - layout="below" - onClick={closeModal} - > - {t('NERD_CLOSE', 'Close')} - </ButtonItem> - </PanelSectionRow> - </DialogControlsSection> - </> - )} - </ModalRoot> - ); -} 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 ( + <PanelSection title="Setup"> + <StatusDisplay + isInstalled={isInstalled} + installationStatus={installationStatus} + losslessScalingInstalled={losslessScalingInstalled} + losslessScalingStatus={losslessScalingStatus} + steamBranchStatus={steamBranchStatus} + /> + <InstallationButton + isInstalled={isInstalled} + isInstalling={isInstalling} + isUninstalling={isUninstalling} + onInstall={onInstall} + onUninstall={onUninstall} + /> + </PanelSection> + ); +} 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<string> => { - 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 ( - <PanelSectionRow> - <ButtonItem - layout="below" - onClick={copyToClipboard} - disabled={isLoading || showSuccess} - > - <div style={{ display: "flex", alignItems: "center", gap: "8px" }}> - {showSuccess ? ( - <FaCheck style={{ color: "#4CAF50" }} /> - ) : isLoading ? ( - <FaClipboard style={{ - animation: "pulse 1s ease-in-out infinite", - opacity: 0.7 - }} /> - ) : ( - <FaClipboard /> - )} - <div style={{ - color: showSuccess ? "#4CAF50" : "inherit", - fontWeight: showSuccess ? "bold" : "normal" - }}> - {showSuccess ? t('CLIPBOARD_COPIED', 'Copied to clipboard') : isLoading ? t('CLIPBOARD_COPYING', 'Copying...') : t('CLIPBOARD_COPY_LAUNCH', 'Copy Launch Option')} - </div> - </div> - </ButtonItem> - <style>{` - @keyframes pulse { - 0% { opacity: 0.7; } - 50% { opacity: 1; } - 100% { opacity: 0.7; } - } - `}</style> - </PanelSectionRow> - ); -} 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 ( - <> - <PanelSectionRow> - <div - style={{ - fontSize: "14px", - fontWeight: "bold", - marginTop: "16px", - marginBottom: "8px", - borderBottom: "1px solid rgba(255, 255, 255, 0.2)", - paddingBottom: "4px", - color: "white" - }} - > - {t('USAGE_TITLE', 'Usage Instructions')} - </div> - </PanelSectionRow> - - <PanelSectionRow> - <div - style={{ - fontSize: "12px", - lineHeight: "1.4", - opacity: "0.8", - whiteSpace: "pre-wrap" - }} - > - {t('USAGE_DESC', 'Click "Copy Launch Option" button, then paste it into your Steam game\'s launch options to enable frame generation.')} - </div> - </PanelSectionRow> - - <PanelSectionRow> - <div - style={{ - fontSize: "12px", - lineHeight: "1.4", - opacity: "0.8", - backgroundColor: "rgba(255, 255, 255, 0.1)", - padding: "8px", - borderRadius: "4px", - fontFamily: "monospace", - marginTop: "8px", - marginBottom: "8px", - textAlign: "center" - }} - > - <strong>~/lsfg %command%</strong> - </div> - </PanelSectionRow> - - <PanelSectionRow> - <div - style={{ - fontSize: "11px", - lineHeight: "1.3", - opacity: "0.6", - marginTop: "8px" - }} - > - {t('USAGE_CONFIG_NOTE', 'The configuration is stored in ~/.config/lsfg-vk/conf.toml and hot-reloads while games are running.')} - </div> - </PanelSectionRow> - </> - ); -} 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"; diff --git a/src/hooks/useGameConfiguration.ts b/src/hooks/useGameConfiguration.ts index e0ba360..b177551 100644 --- a/src/hooks/useGameConfiguration.ts +++ b/src/hooks/useGameConfiguration.ts @@ -3,7 +3,7 @@ import { Router } from "@decky/ui"; import { getGameConfigs, getInstalledGames, updateGameConfig, updateLsfgConfig, resetGameConfig, resetAllGameConfigs, type GameConfigEntry, type InstalledGame } from "../api/lsfgApi"; import { ConfigurationData, getDefaults } from "../config/configSchema"; -export interface GameTarget { appid: string; name: string; configured: boolean; } +export interface GameTarget extends InstalledGame { configured: boolean; } export function useGameConfiguration() { const [defaultConfig, setDefaultConfig] = useState<ConfigurationData>(getDefaults()); @@ -26,13 +26,21 @@ export function useGameConfiguration() { useEffect(() => { const poll = () => { const app = Router.MainRunningApp as any; - if (app?.appid) setRunningGame({ appid: String(app.appid), name: app.display_name || `App ${app.appid}`, configured: games.some((game) => game.appid === String(app.appid)) }); - else setRunningGame(null); + if (!app?.appid) return setRunningGame(null); + const appid = String(app.appid); + const installed = installedGames.find((game) => game.appid === appid); + const name = app.display_name || installed?.name; + if (!name) return setRunningGame(null); + setRunningGame({ + ...(installed || { appid, name, nonSteam: false }), + name, + configured: games.some((game) => game.appid === appid), + }); }; poll(); const interval = window.setInterval(poll, 2000); return () => window.clearInterval(interval); - }, [games]); + }, [games, installedGames]); useEffect(() => { if (!autoSelected.current && runningGame) { autoSelected.current = true; @@ -41,8 +49,8 @@ export function useGameConfiguration() { }, [runningGame]); const targets = useMemo<GameTarget[]>(() => { - const configured = installedGames.map((game) => ({ appid: game.appid, name: game.name, configured: games.some((item) => item.appid === game.appid) })); - for (const game of games) if (!configured.some((item) => item.appid === game.appid)) configured.push({ appid: game.appid, name: `App ${game.appid}`, configured: true }); + const configured = installedGames.map((game) => ({ ...game, configured: games.some((item) => item.appid === game.appid) })); + for (const game of games) if (!configured.some((item) => item.appid === game.appid)) configured.push({ appid: game.appid, name: game.profile, nonSteam: false, configured: true }); if (runningGame && !configured.some((game) => game.appid === runningGame.appid)) configured.unshift(runningGame); return configured; }, [games, installedGames, runningGame]); @@ -55,9 +63,11 @@ export function useGameConfiguration() { if (result.success) setDefaultConfig(next); return; } - const result = await updateGameConfig(selectedAppId, next); + const selectedTarget = targets.find((target) => target.appid === selectedAppId); + if (!selectedTarget?.name) return; + const result = await updateGameConfig(selectedAppId, selectedTarget.name, next); if (result.success) await load(); - }, [load, selectedAppId]); + }, [load, selectedAppId, targets]); const resetSelected = useCallback(async () => { if (selectedAppId) { await resetGameConfig(selectedAppId); setSelectedAppId(""); await load(); } diff --git a/src/i18n/languages.json b/src/i18n/languages.json index 46b4cd9..0528341 100644 --- a/src/i18n/languages.json +++ b/src/i18n/languages.json @@ -58,17 +58,12 @@ "FLATPAK_ERROR": "エラー", "FLATPAK_ERROR_STATUS": "拡張ステータスの確認に失敗しました", "FLATPAK_ERROR_APPS": "Flatpakアプリケーションの読み込みに失敗しました", - "FLATPAK_STEAM_CONFIG_TITLE": "Steam設定", - "FLATPAK_STEAM_CONFIG_HEADER": "Steam Flatpakショートカットの設定", - "FLATPAK_STEAM_CONFIG_DESC": "Steamでflatpakゲームを開き、歯車アイコンをクリックしてください。", - "FLATPAK_STEAM_CONFIG_IMPORTANT": "重要: 起動オプションではなくターゲット(TARGET)に設定してください", - "FLATPAK_STEP_TRY_FIRST": "まず試す:", - "FLATPAK_STEP_TRY_FULL_PATH": "うまくいかない場合、フルパスを試す:", - "FLATPAK_STEP_FINAL": "最終的な結果はこのようになります:", "FLATPAK_CLOSE": "閉じる", "NERD_LOADING": "情報を読み込み中...", - "NERD_LAUNCH_SCRIPT": "起動スクリプト", - "NERD_SCRIPT_NOT_FOUND_PREFIX": "スクリプトが見つかりません:", + "NERD_DLL_PATH": "DLLパス", + "NERD_NOT_AVAILABLE": "利用不可", + "NERD_DLL_HASH": "DLL SHA256ハッシュ", + "NERD_DETECTION_SOURCE": "検出ソース", "NERD_PATH_PREFIX": "パス:", "NERD_NO_CONTENT": "コンテンツなし", "NERD_CONFIG_FILE": "設定ファイル", @@ -94,12 +89,8 @@ "PROFILE_DELETE_BTN": "削除", "PROFILE_CANNOT_RENAME_TITLE": "デフォルトプロファイルの名前は変更できません", "PROFILE_CANNOT_RENAME_MSG": "デフォルトプロファイルの名前は変更できません", - "USAGE_TITLE": "使用方法", - "USAGE_DESC": "「起動オプションをコピー」ボタンをクリックし、Steamゲームの起動オプションに貼り付けてフレーム生成を有効化してください。", - "USAGE_CONFIG_NOTE": "設定は~/.config/lsfg-vk/conf.tomlに保存され、ゲーム実行中もホットリロードされます。", "CLIPBOARD_COPIED": "クリップボードにコピーしました", "CLIPBOARD_COPYING": "コピー中...", - "CLIPBOARD_COPY_LAUNCH": "起動オプションをコピー", "CLIPBOARD_LSFG_FGMOD": "LSFG + DeckyFG" }, "ko": { @@ -161,17 +152,12 @@ "FLATPAK_ERROR": "오류", "FLATPAK_ERROR_STATUS": "확장 상태 확인 실패", "FLATPAK_ERROR_APPS": "Flatpak 애플리케이션 로드 실패", - "FLATPAK_STEAM_CONFIG_TITLE": "Steam 설정", - "FLATPAK_STEAM_CONFIG_HEADER": "Steam Flatpak 단축키 설정", - "FLATPAK_STEAM_CONFIG_DESC": "Steam에서 Flatpak 게임을 열고 톱니바퀴를 클릭하세요.", - "FLATPAK_STEAM_CONFIG_IMPORTANT": "중요: 실행 옵션이 아닌 대상(TARGET)에 설정하세요", - "FLATPAK_STEP_TRY_FIRST": "먼저 시도:", - "FLATPAK_STEP_TRY_FULL_PATH": "작동하지 않으면 전체 경로 시도:", - "FLATPAK_STEP_FINAL": "최종 결과는 다음과 같아야 합니다:", "FLATPAK_CLOSE": "닫기", "NERD_LOADING": "정보 불러오는 중...", - "NERD_LAUNCH_SCRIPT": "실행 스크립트", - "NERD_SCRIPT_NOT_FOUND_PREFIX": "스크립트 없음:", + "NERD_DLL_PATH": "DLL 경로", + "NERD_NOT_AVAILABLE": "사용 불가", + "NERD_DLL_HASH": "DLL SHA256 해시", + "NERD_DETECTION_SOURCE": "감지 소스", "NERD_PATH_PREFIX": "경로:", "NERD_NO_CONTENT": "내용 없음", "NERD_CONFIG_FILE": "설정 파일", @@ -197,12 +183,8 @@ "PROFILE_DELETE_BTN": "삭제", "PROFILE_CANNOT_RENAME_TITLE": "기본 프로필 이름 변경 불가", "PROFILE_CANNOT_RENAME_MSG": "기본 프로필의 이름은 변경할 수 없습니다", - "USAGE_TITLE": "사용 방법", - "USAGE_DESC": "\"실행 옵션 복사\" 버튼을 클릭한 후, Steam 게임의 실행 옵션에 붙여넣어 프레임 생성을 활성화하세요.", - "USAGE_CONFIG_NOTE": "설정은 ~/.config/lsfg-vk/conf.toml에 저장되며 게임 실행 중에도 즉시 반영됩니다.", "CLIPBOARD_COPIED": "클립보드에 복사됨", "CLIPBOARD_COPYING": "복사 중...", - "CLIPBOARD_COPY_LAUNCH": "실행 옵션 복사", "CLIPBOARD_LSFG_FGMOD": "LSFG + DeckyFG" }, "language_metadata": { @@ -292,17 +274,12 @@ "FLATPAK_ERROR": "Error", "FLATPAK_ERROR_STATUS": "Failed to check extension status", "FLATPAK_ERROR_APPS": "Failed to load Flatpak applications", - "FLATPAK_STEAM_CONFIG_TITLE": "Steam Configuration", - "FLATPAK_STEAM_CONFIG_HEADER": "Configure Steam Flatpak Shortcuts", - "FLATPAK_STEAM_CONFIG_DESC": "In Steam, open your flatpak game and click the cog wheel.", - "FLATPAK_STEAM_CONFIG_IMPORTANT": "IMPORTANT: Set this in TARGET (NOT LAUNCH OPTIONS)", - "FLATPAK_STEP_TRY_FIRST": "Try first:", - "FLATPAK_STEP_TRY_FULL_PATH": "If that doesn't work, try full path:", - "FLATPAK_STEP_FINAL": "Final result should look like:", "FLATPAK_CLOSE": "Close", "NERD_LOADING": "Loading information...", - "NERD_LAUNCH_SCRIPT": "Launch Script", - "NERD_SCRIPT_NOT_FOUND_PREFIX": "Script not found:", + "NERD_DLL_PATH": "DLL Path", + "NERD_NOT_AVAILABLE": "Not available", + "NERD_DLL_HASH": "DLL SHA256 Hash", + "NERD_DETECTION_SOURCE": "Detection Source", "NERD_PATH_PREFIX": "Path:", "NERD_NO_CONTENT": "No content", "NERD_CONFIG_FILE": "Configuration File", @@ -328,12 +305,8 @@ "PROFILE_DELETE_BTN": "Delete", "PROFILE_CANNOT_RENAME_TITLE": "Cannot rename default profile", "PROFILE_CANNOT_RENAME_MSG": "The default profile cannot be renamed", - "USAGE_TITLE": "Usage Instructions", - "USAGE_DESC": "Click \"Copy Launch Option\" button, then paste it into your Steam game's launch options to enable frame generation.", - "USAGE_CONFIG_NOTE": "The configuration is stored in ~/.config/lsfg-vk/conf.toml and hot-reloads while games are running.", "CLIPBOARD_COPIED": "Copied to clipboard", "CLIPBOARD_COPYING": "Copying...", - "CLIPBOARD_COPY_LAUNCH": "Copy Launch Option", "CLIPBOARD_LSFG_FGMOD": "LSFG + DeckyFG" } } diff --git a/src/styles.ts b/src/styles.ts new file mode 100644 index 0000000..fe40a59 --- /dev/null +++ b/src/styles.ts @@ -0,0 +1,34 @@ +export const tabStyles = ` + .lsfg-vk-tabs > div > div:first-child::before { + background: #0D141C; + box-shadow: none; + backdrop-filter: none; + } + + .lsfg-vk-tabs [role="tabpanel"] { + padding-left: 8px !important; + padding-right: 8px !important; + } + + .lsfg-vk-tabs [role="tablist"] { + display: flex; + flex-wrap: nowrap; + justify-content: center; + } + + .lsfg-vk-tabs [role="tab"] { + flex: 0 1 auto; + min-width: 0; + box-sizing: border-box; + padding-left: 6px !important; + padding-right: 6px !important; + display: flex !important; + align-items: center; + justify-content: center; + } + + .lsfg-vk-tabs [role="tab"] svg { + display: block; + margin: 0; + } +`; |
