diff options
| author | xXJSONDeruloXx <danielhimebauch@gmail.com> | 2026-09-06 15:08:45 -0400 |
|---|---|---|
| committer | xXJSONDeruloXx <danielhimebauch@gmail.com> | 2026-09-06 15:09:20 -0400 |
| commit | ad2b182777bfd0a5ceef6e654df75ff13eb8b503 (patch) | |
| tree | 7b98bfe322f18e59c74d7b3a0745608390a0a0fd /src/components | |
| parent | a9fd67d05d6819a839a60b581c1dc6eb2792a9be (diff) | |
| download | decky-lsfg-vk-ad2b182777bfd0a5ceef6e654df75ff13eb8b503.tar.gz decky-lsfg-vk-ad2b182777bfd0a5ceef6e654df75ff13eb8b503.zip | |
refactor: offload configuration to lsfg-vk
Diffstat (limited to 'src/components')
| -rw-r--r-- | src/components/ConfigurationSection.tsx | 307 | ||||
| -rw-r--r-- | src/components/Content.tsx | 48 | ||||
| -rw-r--r-- | src/components/FpsMultiplierControl.tsx | 6 | ||||
| -rw-r--r-- | src/components/GameConfigurationSelector.tsx | 29 | ||||
| -rw-r--r-- | src/components/ProfileManagement.tsx | 477 | ||||
| -rw-r--r-- | src/components/index.ts | 2 |
6 files changed, 65 insertions, 804 deletions
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<void>; + onConfigChange: (fieldName: keyof ConfigurationData, value: boolean | number | string | string[]) => Promise<void>; } -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 ( - <> - <style> - {` - .LSFG_ConfigCollapseButton_Container > div > div > div > button, - .LSFG_ConfigCollapseButton_Container > div > div > div > div > button, - .LSFG_WorkaroundsCollapseButton_Container > div > div > div > button { - height: 10px !important; - } - .LSFG_WorkaroundsCollapseButton_Container > div > div > div > div > button { - height: 10px !important; - } - `} - </style> - - {/* Config Section */} - <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('CONFIG_SECTION_TITLE', 'Config')} - </div> - </PanelSectionRow> - - <PanelSectionRow> - <div - className="LSFG_ConfigCollapseButton_Container" - style={{ marginTop: "-2px", marginBottom: "4px" }} - > - <ButtonItem - layout="below" - bottomSeparator={configCollapsed ? "standard" : "none"} - onClick={() => setConfigCollapsed(!configCollapsed)} - > - {configCollapsed ? ( - <RiArrowDownSFill - style={{ transform: "translate(0, -13px)", fontSize: "1.5em" }} - /> - ) : ( - <RiArrowUpSFill - style={{ transform: "translate(0, -12px)", fontSize: "1.5em" }} - /> - )} - </ButtonItem> - </div> - </PanelSectionRow> - - {!configCollapsed && ( - <> - <PanelSectionRow> - <SliderField - label={`${t('CONFIG_FLOW_SCALE', 'Flow Scale')} (${Math.round(config.flow_scale * 100)}%)`} - description={t('CONFIG_FLOW_SCALE_DESC', 'Lowers internal motion estimation resolution, improving performance slightly')} - value={config.flow_scale} - min={0.25} - max={1.0} - step={0.01} - onChange={(value) => onConfigChange(FLOW_SCALE, value)} - /> - </PanelSectionRow> - - <PanelSectionRow> - <ToggleField - label="FP16 Acceleration" - description="Use FP16 shaders when supported" - checked={!config.no_fp16} - onChange={(value) => onConfigChange(NO_FP16, !value)} - /> - </PanelSectionRow> - - <PanelSectionRow> - <SliderField - label={`${t('CONFIG_BASE_FPS_CAP', 'Base FPS Cap')}${config.dxvk_frame_rate > 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)} - /> - </PanelSectionRow> - - <PanelSectionRow> - <ToggleField - label={`${t('CONFIG_PRESENT_MODE', 'Present Mode Override')} (${(config.experimental_present_mode || "fifo") === "fifo" ? t('CONFIG_PRESENT_MODE_FIFO', 'FIFO - VSync') : 'App Default'})`} - description={t('CONFIG_PRESENT_MODE_DESC', 'Force FIFO/VSync for v2 frame pacing, or leave the game present mode unchanged')} - checked={(config.experimental_present_mode || "fifo") === "fifo"} - onChange={(value) => onConfigChange(EXPERIMENTAL_PRESENT_MODE, value ? "fifo" : "mailbox")} - /> - </PanelSectionRow> - - <PanelSectionRow> - <ToggleField - label={t('CONFIG_PERFORMANCE_MODE', 'Performance Mode')} - description={t('CONFIG_PERFORMANCE_MODE_DESC', 'Uses a lighter model for FG (Recommended for most games)')} - checked={config.performance_mode} - onChange={(value) => onConfigChange(PERFORMANCE_MODE, value)} - /> - </PanelSectionRow> - - </> - )} - - {/* Workarounds Section */} - <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('CONFIG_WORKAROUNDS_TITLE', 'Workarounds')} - </div> - </PanelSectionRow> - - <PanelSectionRow> - <div - className="LSFG_WorkaroundsCollapseButton_Container" - style={{ marginTop: "-2px", marginBottom: "4px" }} - > - <ButtonItem - layout="below" - bottomSeparator={workaroundsCollapsed ? "standard" : "none"} - onClick={() => setWorkaroundsCollapsed(!workaroundsCollapsed)} - > - {workaroundsCollapsed ? ( - <RiArrowDownSFill - style={{ transform: "translate(0, -13px)", fontSize: "1.5em" }} - /> - ) : ( - <RiArrowUpSFill - style={{ transform: "translate(0, -12px)", fontSize: "1.5em" }} - /> - )} - </ButtonItem> - </div> - </PanelSectionRow> - - {!workaroundsCollapsed && ( - <> - <PanelSectionRow> - <ToggleField - label={t('CONFIG_ENABLE_WSI', 'Enable WSI')} - description={t('CONFIG_ENABLE_WSI_DESC', 'Re-Enable Gamescope WSI Layer. Requires game restart to apply.')} - checked={config.enable_wsi} - onChange={(value) => onConfigChange(ENABLE_WSI, value)} - /> - </PanelSectionRow> - - <PanelSectionRow> - <ToggleField - label={t('CONFIG_ENABLE_WOW64', 'Enable WOW64 for 32-bit games')} - description={t('CONFIG_ENABLE_WOW64_DESC', 'Enables PROTON_USE_WOW64=1 for 32-bit games (Use with ProtonGE to fix crashing)')} - checked={config.enable_wow64} - onChange={(value) => onConfigChange('enable_wow64', value)} - /> - </PanelSectionRow> - - <PanelSectionRow> - <ToggleField - label={t('CONFIG_DISABLE_STEAMDECK_MODE', 'Disable Steam Deck Mode')} - description={t('CONFIG_DISABLE_STEAMDECK_MODE_DESC', 'Disables Steam Deck mode (Unlocks hidden settings in some games)')} - checked={config.disable_steamdeck_mode} - onChange={(value) => onConfigChange(DISABLE_STEAMDECK_MODE, value)} - /> - </PanelSectionRow> - - <PanelSectionRow> - <ToggleField - label={t('CONFIG_MANGOHUD_WORKAROUND', 'MangoHud Workaround')} - description={t('CONFIG_MANGOHUD_WORKAROUND_DESC', 'Enables a transparent mangohud overlay, sometimes fixes issues with 2X multiplier in game mode')} - checked={config.mangohud_workaround} - onChange={(value) => onConfigChange(MANGOHUD_WORKAROUND, value)} - /> - </PanelSectionRow> - - <PanelSectionRow> - <ToggleField - label={t('CONFIG_DISABLE_VKBASALT', 'Disable vkBasalt')} - description={t('CONFIG_DISABLE_VKBASALT_DESC', 'Disables vkBasalt layer which can conflict with LSFG (Reshade, some Decky plugins)')} - checked={config.disable_vkbasalt} - disabled={config.force_enable_vkbasalt} - onChange={(value) => { - if (value && config.force_enable_vkbasalt) { - // Turn off force enable when enabling disable - onConfigChange(FORCE_ENABLE_VKBASALT, false); - } - onConfigChange(DISABLE_VKBASALT, value); - }} - /> - </PanelSectionRow> - - <PanelSectionRow> - <ToggleField - label={t('CONFIG_FORCE_ENABLE_VKBASALT', 'Force Enable vkBasalt')} - description={t('CONFIG_FORCE_ENABLE_VKBASALT_DESC', 'Force vkBasalt to engage to fix framepacing issues in gamemode')} - checked={config.force_enable_vkbasalt} - disabled={config.disable_vkbasalt} - onChange={(value) => { - if (value && config.disable_vkbasalt) { - // Turn off disable when enabling force enable - onConfigChange(DISABLE_VKBASALT, false); - } - onConfigChange(FORCE_ENABLE_VKBASALT, value); - }} - /> - </PanelSectionRow> - - <PanelSectionRow> - <ToggleField - label={t('CONFIG_ENABLE_ZINK', 'Enable Zink for OpenGL Games')} - description={t('CONFIG_ENABLE_ZINK_DESC', 'Use Vulkan-based OpenGL implementation for OpenGL games (may cause crashes or freezes with some games)')} - checked={config.enable_zink} - onChange={(value) => onConfigChange(ENABLE_ZINK, value)} - /> - </PanelSectionRow> - </> - )} - </> - ); +export function ConfigurationSection({ config, onConfigChange }: ConfigurationSectionProps) { + return <> + <PanelSectionRow> + <SliderField label={`Flow Scale (${Math.round(config.flow_scale * 100)}%)`} description="Motion estimation resolution scale" value={config.flow_scale} min={0.25} max={1} step={0.01} onChange={(value) => onConfigChange(FLOW_SCALE, value)} /> + </PanelSectionRow> + <PanelSectionRow> + <ToggleField label="FP16 Acceleration" description="Use FP16 shaders when supported" checked={!config.no_fp16} onChange={(value) => onConfigChange(NO_FP16, !value)} /> + </PanelSectionRow> + <PanelSectionRow> + <ToggleField label="Performance Mode" description="Use the lighter frame generation model" checked={config.performance_mode} onChange={(value) => onConfigChange(PERFORMANCE_MODE, value)} /> + </PanelSectionRow> + <PanelSectionRow> + <ToggleField label="Present Mode Override" description="Force FIFO/VSync pacing" checked={config.override_present_mode} onChange={(value) => onConfigChange(OVERRIDE_PRESENT_MODE, value)} /> + </PanelSectionRow> + <PanelSectionRow> + <ToggleField label="Preserve Swapchain Image Count" description="Do not change the application's swapchain image count" checked={config.preserve_swapchain_image_count} onChange={(value) => onConfigChange(PRESERVE_SWAPCHAIN_IMAGE_COUNT, value)} /> + </PanelSectionRow> + </>; } 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 && ( - <ProfileManagement - currentProfile={currentProfile} - onProfileChange={async () => { - await loadProfiles(); - await loadLsfgConfig(); - }} - /> + <GameConfigurationSelector targets={targets} runningGame={runningGame} selectedAppId={selectedAppId} onSelect={setSelectedAppId} onReset={resetSelected} onResetAll={resetAll} /> )} {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<void>; + onConfigChange: (fieldName: keyof ConfigurationData, value: boolean | number | string | string[]) => Promise<void>; } 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`} </div> <DialogButton style={{ diff --git a/src/components/GameConfigurationSelector.tsx b/src/components/GameConfigurationSelector.tsx new file mode 100644 index 0000000..8a0df6c --- /dev/null +++ b/src/components/GameConfigurationSelector.tsx @@ -0,0 +1,29 @@ +import { Dropdown, DropdownOption, PanelSectionRow, ButtonItem } from "@decky/ui"; +import { GameTarget } from "../hooks/useGameConfiguration"; + +interface Props { + targets: GameTarget[]; + runningGame: GameTarget | null; + selectedAppId: string; + onSelect: (appid: string) => void; + onReset: () => Promise<void>; + onResetAll: () => Promise<void>; +} + +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 <> + <PanelSectionRow> + <Dropdown rgOptions={options} selectedOption={selectedAppId} onChange={(option) => onSelect(String(option.data))} /> + </PanelSectionRow> + <PanelSectionRow> + <ButtonItem layout="below" onClick={() => void onReset()} disabled={!selectedAppId}>Reset selected game</ButtonItem> + </PanelSectionRow> + <PanelSectionRow> + <ButtonItem layout="below" onClick={() => void onResetAll()} disabled={!targets.some((target) => target.configured)}>Reset all game profiles</ButtonItem> + </PanelSectionRow> + </>; +} 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 ( - <ModalRoot> - <div style={{ padding: "16px", minWidth: "400px" }}> - <h2 style={{ marginBottom: "16px" }}>{title}</h2> - <p style={{ marginBottom: "24px" }}>{description}</p> - - <div style={{ marginBottom: "24px" }}> - <Field - label={t('PROFILE_NAME_LABEL', 'Name')} - childrenLayout="below" - childrenContainerWidth="max" - > - <TextField - value={value} - onChange={(e) => setValue(e?.target?.value || "")} - style={{ width: "100%" }} - /> - </Field> - </div> - - <Focusable - style={{ - display: "flex", - justifyContent: "flex-end", - gap: "8px", - marginTop: "16px" - }} - flow-children="horizontal" - > - <DialogButton onClick={closeModal}> - {cancelText} - </DialogButton> - <DialogButton - onClick={handleOK} - disabled={!value.trim()} - > - {okText} - </DialogButton> - </Focusable> - </div> - </ModalRoot> - ); -} - -interface ProfileManagementProps { - currentProfile?: string; - onProfileChange?: (profileName: string) => void; -} - -export function ProfileManagement({ currentProfile, onProfileChange }: ProfileManagementProps) { - const [profiles, setProfiles] = useState<string[]>([]); - const [selectedProfile, setSelectedProfile] = useState<string>(currentProfile || "decky-lsfg-vk"); - const [isLoading, setIsLoading] = useState(false); - const [mainRunningApp, setMainRunningApp] = useState<AppOverview | undefined>(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( - <TextInputModal - title={t('PROFILE_CREATE_TITLE', 'Create New Profile')} - description={t('PROFILE_CREATE_DESC', "Enter a name for the new profile. The current profile's settings will be copied.")} - okText={t('PROFILE_CREATE_BTN', 'Create')} - cancelText={t('PROFILE_CANCEL_BTN', 'Cancel')} - onOK={(name: string) => { - 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( - <ConfirmModal - strTitle={t('PROFILE_DELETE_TITLE', 'Delete Profile')} - strDescription={`${t('PROFILE_DELETE_DESC_PREFIX', 'Are you sure you want to delete the profile')} "${selectedProfile}"${t('PROFILE_DELETE_DESC_SUFFIX', '? This action cannot be undone.')}`} - strOKButtonText={t('PROFILE_DELETE_BTN', 'Delete')} - strCancelButtonText={t('PROFILE_CANCEL_BTN', 'Cancel')} - onOK={() => 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( - <TextInputModal - title={t('PROFILE_RENAME_TITLE', 'Rename Profile')} - description={`${t('PROFILE_RENAME_DESC_PREFIX', 'Enter a new name for the profile')} "${selectedProfile}".`} - defaultValue={selectedProfile} - okText={t('PROFILE_RENAME_BTN', 'Rename')} - cancelText={t('PROFILE_CANCEL_BTN', 'Cancel')} - onOK={(newName: string) => { - 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 ( - <> - <style> - {` - .LSFG_ProfilesCollapseButton_Container > div > div > div > button { - height: 10px !important; - } - .LSFG_ProfilesCollapseButton_Container > div > div > div > div > button { - height: 10px !important; - } - `} - </style> - - {/* Display currently running game info - always visible */} - {mainRunningApp && ( - <PanelSectionRow> - <div style={{ - padding: "8px 12px", - backgroundColor: "rgba(0, 255, 0, 0.1)", - borderRadius: "4px", - border: "1px solid rgba(0, 255, 0, 0.3)", - fontSize: "13px" - }}> - <strong>{mainRunningApp.display_name}</strong> running. {t('PROFILE_CLOSE_GAME', 'Close game to change profile.')} - </div> - </PanelSectionRow> - )} - - <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('PROFILE_SECTION_TITLE', 'Profile:')} {selectedProfile === "decky-lsfg-vk" ? t('PROFILE_DEFAULT', 'Default') : selectedProfile} - </div> - </PanelSectionRow> - - <PanelSectionRow> - <div - className="LSFG_ProfilesCollapseButton_Container" - style={{ marginTop: "-2px", marginBottom: "4px" }} - > - <ButtonItem - layout="below" - bottomSeparator={profilesCollapsed ? "standard" : "none"} - onClick={() => setProfilesCollapsed(!profilesCollapsed)} - > - {profilesCollapsed ? ( - <RiArrowDownSFill - style={{ transform: "translate(0, -13px)", fontSize: "1.5em" }} - /> - ) : ( - <RiArrowUpSFill - style={{ transform: "translate(0, -12px)", fontSize: "1.5em" }} - /> - )} - </ButtonItem> - </div> - </PanelSectionRow> - - {!profilesCollapsed && ( - <> - <PanelSectionRow> - <Field - label="" - childrenLayout="below" - childrenContainerWidth="max" - bottomSeparator="none" - > - <Dropdown - rgOptions={profileOptions} - selectedOption={selectedProfile} - onChange={handleDropdownChange} - disabled={isLoading || !!mainRunningApp} - /> - </Field> - </PanelSectionRow> - - <PanelSectionRow> - <Focusable - style={{ - display: "flex", - alignItems: "center", - gap: "8px", - width: "100%", - padding: "0", - margin: "0", - marginTop: "8px" - }} - flow-children="horizontal" - > - <DialogButton - style={{ - height: "40px", - flex: 1, - display: "flex", - alignItems: "center", - justifyContent: "center", - padding: "10px", - minWidth: "0", - }} - onClick={handleRenameProfile} - disabled={isLoading || selectedProfile === "decky-lsfg-vk" || !!mainRunningApp} - > - <RiEditLine size={20} /> - </DialogButton> - - <DialogButton - style={{ - height: "40px", - flex: 1, - display: "flex", - alignItems: "center", - justifyContent: "center", - padding: "10px", - minWidth: "0", - }} - onClick={handleDeleteProfile} - disabled={isLoading || selectedProfile === "decky-lsfg-vk" || !!mainRunningApp} - > - <RiDeleteBinLine size={20} /> - </DialogButton> - </Focusable> - </PanelSectionRow> - </> - )} - </> - ); -} 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"; |
