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/api/lsfgApi.ts | 54 +-- 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 +- src/config/configSchema.ts | 6 +- src/config/generatedConfigSchema.ts | 193 ++--------- src/hooks/useGameConfiguration.ts | 68 ++++ src/hooks/useProfileManagement.ts | 194 ----------- 11 files changed, 183 insertions(+), 1201 deletions(-) create mode 100644 src/components/GameConfigurationSelector.tsx delete mode 100644 src/components/ProfileManagement.tsx create mode 100644 src/hooks/useGameConfiguration.ts delete mode 100644 src/hooks/useProfileManagement.ts (limited to 'src') diff --git a/src/api/lsfgApi.ts b/src/api/lsfgApi.ts index c0d6528..b38fa3b 100644 --- a/src/api/lsfgApi.ts +++ b/src/api/lsfgApi.ts @@ -44,12 +44,31 @@ export interface ConfigUpdateResult { error?: string; } +export interface GameConfigEntry { + appid: string; + profile: string; + config: LsfgConfig; +} +export interface InstalledGame { appid: string; name: string; } +export interface InstalledGamesResult { success: boolean; games?: InstalledGame[]; error?: string; } + +export interface GameConfigsResult { + success: boolean; + default?: LsfgConfig; + games?: GameConfigEntry[]; + error?: string; +} + +export interface GameConfigResult extends ConfigUpdateResult { + appid?: string; + exists?: boolean; + config?: LsfgConfig; +} + export interface ConfigSchemaResult { field_names: string[]; field_types: Record; defaults: ConfigurationData; - profiles?: string[]; - current_profile?: string; } export interface LaunchOptionResult { @@ -105,22 +124,6 @@ export interface FlatpakOperationResult { operation?: string; } -// Profile management interfaces -export interface ProfilesResult { - success: boolean; - profiles?: string[]; - current_profile?: string; - message?: string; - error?: string; -} - -export interface ProfileResult { - success: boolean; - profile_name?: string; - message?: string; - error?: string; -} - // API functions export const installLsfgVk = callable<[], InstallationResult>("install_lsfg_vk"); export const uninstallLsfgVk = callable<[], InstallationResult>("uninstall_lsfg_vk"); @@ -146,17 +149,14 @@ export const updateLsfgConfig = callable< [ConfigurationData], ConfigUpdateResult >("update_lsfg_config"); +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 resetGameConfig = callable<[string], GameConfigResult>("reset_game_config"); +export const resetAllGameConfigs = callable<[], GameConfigsResult>("reset_all_game_configs"); // Legacy helper function for backward compatibility export const updateLsfgConfigFromObject = async (config: ConfigurationData): Promise => { return updateLsfgConfig(config); }; - -// Self-updater API functions -// Profile management API functions -export const getProfiles = callable<[], ProfilesResult>("get_profiles"); -export const createProfile = callable<[string, string?], ProfileResult>("create_profile"); -export const deleteProfile = callable<[string], ProfileResult>("delete_profile"); -export const renameProfile = callable<[string, string], ProfileResult>("rename_profile"); -export const setCurrentProfile = callable<[string], ProfileResult>("set_current_profile"); -export const updateProfileConfig = callable<[string, ConfigurationData], ConfigUpdateResult>("update_profile_config"); 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"; diff --git a/src/config/configSchema.ts b/src/config/configSchema.ts index befbd8d..bd72e8c 100644 --- a/src/config/configSchema.ts +++ b/src/config/configSchema.ts @@ -6,8 +6,6 @@ export { getFieldNames, getDefaults, getFieldTypes, - DLL, NO_FP16, MULTIPLIER, FLOW_SCALE, PERFORMANCE_MODE, - EXPERIMENTAL_PRESENT_MODE, DXVK_FRAME_RATE, ENABLE_WOW64, - DISABLE_STEAMDECK_MODE, MANGOHUD_WORKAROUND, DISABLE_VKBASALT, - FORCE_ENABLE_VKBASALT, ENABLE_WSI, ENABLE_ZINK + DLL, NO_FP16, ACTIVE_IN, PACING_MODE, MULTIPLIER, FLOW_SCALE, + PERFORMANCE_MODE, OVERRIDE_PRESENT_MODE, PRESERVE_SWAPCHAIN_IMAGE_COUNT } from './generatedConfigSchema'; diff --git a/src/config/generatedConfigSchema.ts b/src/config/generatedConfigSchema.ts index edbde88..71459eb 100644 --- a/src/config/generatedConfigSchema.ts +++ b/src/config/generatedConfigSchema.ts @@ -1,182 +1,31 @@ -// src/config/generatedConfigSchema.ts -// Configuration field type enum - matches Python -export enum ConfigFieldType { - BOOLEAN = "boolean", - INTEGER = "integer", - FLOAT = "float", - STRING = "string" -} +export enum ConfigFieldType { BOOLEAN = "boolean", INTEGER = "integer", FLOAT = "float", STRING = "string", ARRAY = "array" } -// Field name constants for type-safe access export const DLL = "dll" as const; export const NO_FP16 = "no_fp16" as const; +export const ACTIVE_IN = "active_in" as const; +export const PACING_MODE = "pacing_mode" as const; export const MULTIPLIER = "multiplier" as const; export const FLOW_SCALE = "flow_scale" as const; export const PERFORMANCE_MODE = "performance_mode" as const; -export const EXPERIMENTAL_PRESENT_MODE = "experimental_present_mode" as const; -export const DXVK_FRAME_RATE = "dxvk_frame_rate" as const; -export const ENABLE_WOW64 = "enable_wow64" as const; -export const DISABLE_STEAMDECK_MODE = "disable_steamdeck_mode" as const; -export const MANGOHUD_WORKAROUND = "mangohud_workaround" as const; -export const DISABLE_VKBASALT = "disable_vkbasalt" as const; -export const FORCE_ENABLE_VKBASALT = "force_enable_vkbasalt" as const; -export const ENABLE_WSI = "enable_wsi" as const; -export const ENABLE_ZINK = "enable_zink" as const; +export const OVERRIDE_PRESENT_MODE = "override_present_mode" as const; +export const PRESERVE_SWAPCHAIN_IMAGE_COUNT = "preserve_swapchain_image_count" as const; -// Configuration field definition -export interface ConfigField { - name: string; - fieldType: ConfigFieldType; - default: boolean | number | string; - description: string; +export interface ConfigField { name: string; fieldType: ConfigFieldType; default: boolean | number | string | string[]; description: string; } +export interface ConfigurationData { + dll: string; no_fp16: boolean; active_in: string[]; pacing_mode: string; multiplier: number; + flow_scale: number; performance_mode: boolean; override_present_mode: boolean; preserve_swapchain_image_count: boolean; } - -// Configuration schema - auto-generated from Python export const CONFIG_SCHEMA: Record = { - dll: { - name: "dll", - fieldType: ConfigFieldType.STRING, - default: "", - description: "override the lsfg-vk.dll path" - }, - no_fp16: { - name: "no_fp16", - fieldType: ConfigFieldType.BOOLEAN, - default: false, - description: "disable FP16 acceleration" - }, - multiplier: { - name: "multiplier", - fieldType: ConfigFieldType.INTEGER, - default: 1, - description: "frame generation multiplier" - }, - flow_scale: { - name: "flow_scale", - fieldType: ConfigFieldType.FLOAT, - default: 1, - description: "motion estimation resolution scale" - }, - performance_mode: { - name: "performance_mode", - fieldType: ConfigFieldType.BOOLEAN, - default: false, - description: "use the lighter frame generation model" - }, - experimental_present_mode: { - name: "experimental_present_mode", - fieldType: ConfigFieldType.STRING, - default: "fifo", - description: "control the v2 present mode override" - }, - dxvk_frame_rate: { - name: "dxvk_frame_rate", - fieldType: ConfigFieldType.INTEGER, - default: 0, - description: "base framerate cap for DirectX games before frame multiplier" - }, - enable_wow64: { - name: "enable_wow64", - fieldType: ConfigFieldType.BOOLEAN, - default: false, - description: "enable PROTON_USE_WOW64=1 for 32-bit games" - }, - disable_steamdeck_mode: { - name: "disable_steamdeck_mode", - fieldType: ConfigFieldType.BOOLEAN, - default: false, - description: "disable Steam Deck mode" - }, - mangohud_workaround: { - name: "mangohud_workaround", - fieldType: ConfigFieldType.BOOLEAN, - default: false, - description: "enable a transparent MangoHud overlay workaround" - }, - disable_vkbasalt: { - name: "disable_vkbasalt", - fieldType: ConfigFieldType.BOOLEAN, - default: false, - description: "disable vkBasalt for games where it conflicts with lsfg-vk" - }, - force_enable_vkbasalt: { - name: "force_enable_vkbasalt", - fieldType: ConfigFieldType.BOOLEAN, - default: false, - description: "force-enable vkBasalt" - }, - enable_wsi: { - name: "enable_wsi", - fieldType: ConfigFieldType.BOOLEAN, - default: false, - description: "enable the Gamescope WSI layer" - }, - enable_zink: { - name: "enable_zink", - fieldType: ConfigFieldType.BOOLEAN, - default: false, - description: "enable Zink for OpenGL games" - }, + dll: { name: "dll", fieldType: ConfigFieldType.STRING, default: "", description: "Override the lsfg-vk.dll path" }, + no_fp16: { name: "no_fp16", fieldType: ConfigFieldType.BOOLEAN, default: false, description: "Disable FP16 acceleration" }, + active_in: { name: "active_in", fieldType: ConfigFieldType.ARRAY, default: [], description: "Steam AppID or executable identifiers" }, + pacing_mode: { name: "pacing_mode", fieldType: ConfigFieldType.STRING, default: "vsync", description: "Frame pacing mode" }, + multiplier: { name: "multiplier", fieldType: ConfigFieldType.INTEGER, default: 2, description: "Frame generation multiplier" }, + flow_scale: { name: "flow_scale", fieldType: ConfigFieldType.FLOAT, default: 1, description: "Motion estimation resolution scale" }, + performance_mode: { name: "performance_mode", fieldType: ConfigFieldType.BOOLEAN, default: false, description: "Use the lighter frame generation model" }, + override_present_mode: { name: "override_present_mode", fieldType: ConfigFieldType.BOOLEAN, default: true, description: "Override present mode" }, + preserve_swapchain_image_count: { name: "preserve_swapchain_image_count", fieldType: ConfigFieldType.BOOLEAN, default: false, description: "Preserve the swapchain image count" }, }; - -// Type-safe configuration data structure -export interface ConfigurationData { - dll: string; - no_fp16: boolean; - multiplier: number; - flow_scale: number; - performance_mode: boolean; - experimental_present_mode: string; - dxvk_frame_rate: number; - enable_wow64: boolean; - disable_steamdeck_mode: boolean; - mangohud_workaround: boolean; - disable_vkbasalt: boolean; - force_enable_vkbasalt: boolean; - enable_wsi: boolean; - enable_zink: boolean; -} - -// Helper functions -export function getFieldNames(): string[] { - return Object.keys(CONFIG_SCHEMA); -} - -export function getDefaults(): ConfigurationData { - return { - dll: "", - no_fp16: false, - multiplier: 1, - flow_scale: 1, - performance_mode: false, - experimental_present_mode: "fifo", - dxvk_frame_rate: 0, - enable_wow64: false, - disable_steamdeck_mode: false, - mangohud_workaround: false, - disable_vkbasalt: false, - force_enable_vkbasalt: false, - enable_wsi: false, - enable_zink: false, - }; -} - -export function getFieldTypes(): Record { - return { - dll: ConfigFieldType.STRING, - no_fp16: ConfigFieldType.BOOLEAN, - multiplier: ConfigFieldType.INTEGER, - flow_scale: ConfigFieldType.FLOAT, - performance_mode: ConfigFieldType.BOOLEAN, - experimental_present_mode: ConfigFieldType.STRING, - dxvk_frame_rate: ConfigFieldType.INTEGER, - enable_wow64: ConfigFieldType.BOOLEAN, - disable_steamdeck_mode: ConfigFieldType.BOOLEAN, - mangohud_workaround: ConfigFieldType.BOOLEAN, - disable_vkbasalt: ConfigFieldType.BOOLEAN, - force_enable_vkbasalt: ConfigFieldType.BOOLEAN, - enable_wsi: ConfigFieldType.BOOLEAN, - enable_zink: ConfigFieldType.BOOLEAN, - }; -} - +export function getFieldNames(): string[] { return Object.keys(CONFIG_SCHEMA); } +export function getDefaults(): ConfigurationData { return { dll: "", no_fp16: false, active_in: [], pacing_mode: "vsync", multiplier: 2, flow_scale: 1, performance_mode: false, override_present_mode: true, preserve_swapchain_image_count: false }; } +export function getFieldTypes(): Record { return Object.fromEntries(Object.entries(CONFIG_SCHEMA).map(([key, value]) => [key, value.fieldType])); } diff --git a/src/hooks/useGameConfiguration.ts b/src/hooks/useGameConfiguration.ts new file mode 100644 index 0000000..e0ba360 --- /dev/null +++ b/src/hooks/useGameConfiguration.ts @@ -0,0 +1,68 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +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 function useGameConfiguration() { + const [defaultConfig, setDefaultConfig] = useState(getDefaults()); + const [games, setGames] = useState([]); + const [installedGames, setInstalledGames] = useState([]); + const [selectedAppId, setSelectedAppId] = useState(""); + const [runningGame, setRunningGame] = useState(null); + const autoSelected = useRef(false); + + const load = useCallback(async () => { + const [result, installed] = await Promise.all([getGameConfigs(), getInstalledGames()]); + if (result.success) { + setDefaultConfig(result.default || getDefaults()); + setGames(result.games || []); + } + if (installed.success) setInstalledGames(installed.games || []); + }, []); + + useEffect(() => { load(); }, [load]); + 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); + }; + poll(); + const interval = window.setInterval(poll, 2000); + return () => window.clearInterval(interval); + }, [games]); + useEffect(() => { + if (!autoSelected.current && runningGame) { + autoSelected.current = true; + setSelectedAppId(runningGame.appid); + } + }, [runningGame]); + + const targets = useMemo(() => { + 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 }); + if (runningGame && !configured.some((game) => game.appid === runningGame.appid)) configured.unshift(runningGame); + return configured; + }, [games, installedGames, runningGame]); + const selected = selectedAppId ? games.find((game) => game.appid === selectedAppId)?.config : defaultConfig; + const config = selected || defaultConfig; + + const save = useCallback(async (next: ConfigurationData) => { + if (!selectedAppId) { + const result = await updateLsfgConfig(next); + if (result.success) setDefaultConfig(next); + return; + } + const result = await updateGameConfig(selectedAppId, next); + if (result.success) await load(); + }, [load, selectedAppId]); + + const resetSelected = useCallback(async () => { + if (selectedAppId) { await resetGameConfig(selectedAppId); setSelectedAppId(""); await load(); } + }, [load, selectedAppId]); + const resetAll = useCallback(async () => { await resetAllGameConfigs(); setSelectedAppId(""); await load(); }, [load]); + + return { config, defaultConfig, games, targets, runningGame, selectedAppId, setSelectedAppId, save, resetSelected, resetAll, reload: load }; +} diff --git a/src/hooks/useProfileManagement.ts b/src/hooks/useProfileManagement.ts deleted file mode 100644 index a5f2a07..0000000 --- a/src/hooks/useProfileManagement.ts +++ /dev/null @@ -1,194 +0,0 @@ -import { useState, useEffect, useCallback } from "react"; -import { - getProfiles, - createProfile, - deleteProfile, - renameProfile, - setCurrentProfile, - updateProfileConfig, - type ProfilesResult, - type ProfileResult, - type ConfigUpdateResult -} from "../api/lsfgApi"; -import { ConfigurationData } from "../config/configSchema"; -import { showSuccessToast, showErrorToast } from "../utils/toastUtils"; - -export function useProfileManagement() { - const [profiles, setProfiles] = useState([]); - const [currentProfile, setCurrentProfileState] = useState("decky-lsfg-vk"); - const [isLoading, setIsLoading] = useState(false); - - // Load profiles on hook initialization - const loadProfiles = useCallback(async () => { - try { - const result: ProfilesResult = await getProfiles(); - if (result.success && result.profiles) { - setProfiles(result.profiles); - if (result.current_profile) { - setCurrentProfileState(result.current_profile); - } - return result; - } else { - console.error("Failed to load profiles:", result.error); - showErrorToast("Failed to load profiles", result.error || "Unknown error"); - return result; - } - } catch (error) { - console.error("Error loading profiles:", error); - showErrorToast("Error loading profiles", String(error)); - return { success: false, error: String(error) }; - } - }, []); - - // Create a new profile - const handleCreateProfile = useCallback(async (profileName: string, sourceProfile?: string) => { - setIsLoading(true); - try { - const result: ProfileResult = await createProfile(profileName, sourceProfile || currentProfile); - 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(); - return result; - } else { - console.error("Failed to create profile:", result.error); - showErrorToast("Failed to create profile", result.error || "Unknown error"); - return result; - } - } catch (error) { - console.error("Error creating profile:", error); - showErrorToast("Error creating profile", String(error)); - return { success: false, error: String(error) }; - } finally { - setIsLoading(false); - } - }, [currentProfile, loadProfiles]); - - // Delete a profile - const handleDeleteProfile = useCallback(async (profileName: string) => { - if (profileName === "decky-lsfg-vk") { - showErrorToast("Cannot delete default profile", "The default profile cannot be deleted"); - return { success: false, error: "Cannot delete default profile" }; - } - - setIsLoading(true); - try { - const result: ProfileResult = await deleteProfile(profileName); - if (result.success) { - showSuccessToast("Profile deleted", `Deleted profile: ${profileName}`); - await loadProfiles(); - // If we deleted the current profile, it should have switched to default - if (currentProfile === profileName) { - setCurrentProfileState("decky-lsfg-vk"); - } - return result; - } else { - console.error("Failed to delete profile:", result.error); - showErrorToast("Failed to delete profile", result.error || "Unknown error"); - return result; - } - } catch (error) { - console.error("Error deleting profile:", error); - showErrorToast("Error deleting profile", String(error)); - return { success: false, error: String(error) }; - } finally { - setIsLoading(false); - } - }, [currentProfile, loadProfiles]); - - // Rename a profile - const handleRenameProfile = useCallback(async (oldName: string, newName: string) => { - if (oldName === "decky-lsfg-vk") { - showErrorToast("Cannot rename default profile", "The default profile cannot be renamed"); - return { success: false, error: "Cannot rename default profile" }; - } - - setIsLoading(true); - try { - const result: ProfileResult = await renameProfile(oldName, 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(); - // Update current profile if it was renamed - if (currentProfile === oldName) { - setCurrentProfileState(actualNewName); - } - return result; - } else { - console.error("Failed to rename profile:", result.error); - showErrorToast("Failed to rename profile", result.error || "Unknown error"); - return result; - } - } catch (error) { - console.error("Error renaming profile:", error); - showErrorToast("Error renaming profile", String(error)); - return { success: false, error: String(error) }; - } finally { - setIsLoading(false); - } - }, [currentProfile, loadProfiles]); - - // Set the current active profile - const handleSetCurrentProfile = useCallback(async (profileName: string) => { - setIsLoading(true); - try { - const result: ProfileResult = await setCurrentProfile(profileName); - if (result.success) { - setCurrentProfileState(profileName); - showSuccessToast("Profile switched", `Switched to profile: ${profileName}`); - return result; - } else { - console.error("Failed to switch profile:", result.error); - showErrorToast("Failed to switch profile", result.error || "Unknown error"); - return result; - } - } catch (error) { - console.error("Error switching profile:", error); - showErrorToast("Error switching profile", String(error)); - return { success: false, error: String(error) }; - } finally { - setIsLoading(false); - } - }, []); - - // Update configuration for a specific profile - const handleUpdateProfileConfig = useCallback(async (profileName: string, config: ConfigurationData) => { - setIsLoading(true); - try { - const result: ConfigUpdateResult = await updateProfileConfig(profileName, config); - if (result.success) { - return result; - } else { - console.error("Failed to update profile config:", result.error); - showErrorToast("Failed to update profile config", result.error || "Unknown error"); - return result; - } - } catch (error) { - console.error("Error updating profile config:", error); - showErrorToast("Error updating profile config", String(error)); - return { success: false, error: String(error) }; - } finally { - setIsLoading(false); - } - }, [currentProfile]); - - // Initialize profiles on mount - useEffect(() => { - loadProfiles(); - }, [loadProfiles]); - - return { - profiles, - currentProfile, - isLoading, - loadProfiles, - createProfile: handleCreateProfile, - deleteProfile: handleDeleteProfile, - renameProfile: handleRenameProfile, - setCurrentProfile: handleSetCurrentProfile, - updateProfileConfig: handleUpdateProfileConfig - }; -} -- cgit v1.2.3