summaryrefslogtreecommitdiff
path: root/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'src/components')
-rw-r--r--src/components/ConfigurationSection.tsx307
-rw-r--r--src/components/Content.tsx48
-rw-r--r--src/components/FpsMultiplierControl.tsx6
-rw-r--r--src/components/GameConfigurationSelector.tsx29
-rw-r--r--src/components/ProfileManagement.tsx477
-rw-r--r--src/components/index.ts2
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";