diff options
| author | Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> | 2025-08-18 11:51:56 -0400 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-08-18 11:51:56 -0400 |
| commit | 3d75e193791c18b1a0bcde5c8e80bdc24492c031 (patch) | |
| tree | ce232300a4779078c28ed549a8ce678a1f7974c2 /src/components | |
| parent | 6489f2273fc246fcca25e95d913e60ea214e0d31 (diff) | |
| parent | 1fcde377b970e0e04d11ca68892c3e04481471ac (diff) | |
| download | decky-lsfg-vk-3d75e193791c18b1a0bcde5c8e80bdc24492c031.tar.gz decky-lsfg-vk-3d75e193791c18b1a0bcde5c8e80bdc24492c031.zip | |
Feat/manual profiles
Diffstat (limited to 'src/components')
| -rw-r--r-- | src/components/ConfigurationSection.tsx | 6 | ||||
| -rw-r--r-- | src/components/Content.tsx | 32 | ||||
| -rw-r--r-- | src/components/ProfileManagement.tsx | 340 | ||||
| -rw-r--r-- | src/components/index.ts | 1 |
4 files changed, 375 insertions, 4 deletions
diff --git a/src/components/ConfigurationSection.tsx b/src/components/ConfigurationSection.tsx index 31ce278..92d1867 100644 --- a/src/components/ConfigurationSection.tsx +++ b/src/components/ConfigurationSection.tsx @@ -4,7 +4,7 @@ import { RiArrowDownSFill, RiArrowUpSFill } from "react-icons/ri"; import { ConfigurationData } from "../config/configSchema"; import { FpsMultiplierControl } from "./FpsMultiplierControl"; import { - NO_FP16, FLOW_SCALE, PERFORMANCE_MODE, HDR_MODE, + FLOW_SCALE, PERFORMANCE_MODE, HDR_MODE, EXPERIMENTAL_PRESENT_MODE, DXVK_FRAME_RATE, DISABLE_STEAMDECK_MODE, MANGOHUD_WORKAROUND, DISABLE_VKBASALT, FORCE_ENABLE_VKBASALT, ENABLE_WSI } from "../config/generatedConfigSchema"; @@ -113,14 +113,14 @@ export function ConfigurationSection({ /> </PanelSectionRow> - <PanelSectionRow> + {/* <PanelSectionRow> <ToggleField label="Force Disable FP16" description="Force-disable FP16 acceleration" checked={config.no_fp16} onChange={(value) => onConfigChange(NO_FP16, value)} /> - </PanelSectionRow> + </PanelSectionRow> */} <PanelSectionRow> <ToggleField diff --git a/src/components/Content.tsx b/src/components/Content.tsx index a075574..7815951 100644 --- a/src/components/Content.tsx +++ b/src/components/Content.tsx @@ -1,10 +1,12 @@ import { useEffect } from "react"; import { PanelSection, showModal, ButtonItem, PanelSectionRow } from "@decky/ui"; import { useInstallationStatus, useDllDetection, useLsfgConfig } from "../hooks/useLsfgHooks"; +import { useProfileManagement } from "../hooks/useProfileManagement"; import { useInstallationActions } from "../hooks/useInstallationActions"; import { StatusDisplay } from "./StatusDisplay"; import { InstallationButton } from "./InstallationButton"; import { ConfigurationSection } from "./ConfigurationSection"; +import { ProfileManagement } from "./ProfileManagement"; import { UsageInstructions } from "./UsageInstructions"; import { WikiButton } from "./WikiButton"; import { ClipboardButton } from "./ClipboardButton"; @@ -29,6 +31,12 @@ export function Content() { updateField } = useLsfgConfig(); + const { + currentProfile, + updateProfileConfig, + loadProfiles + } = useProfileManagement(); + const { isInstalling, isUninstalling, handleInstall, handleUninstall } = useInstallationActions(); // Reload config when installation status changes @@ -40,7 +48,18 @@ export function Content() { // Generic configuration change handler const handleConfigChange = async (fieldName: keyof ConfigurationData, value: boolean | number | string) => { - await updateField(fieldName, value); + // If we have a current profile, update that profile specifically + if (currentProfile) { + const newConfig = { ...config, [fieldName]: value }; + const result = await updateProfileConfig(currentProfile, newConfig); + if (result.success) { + // Reload config to reflect the changes from the backend + await loadLsfgConfig(); + } + } else { + // Fallback to the original method for backward compatibility + await updateField(fieldName, value); + } }; const onInstall = () => { @@ -74,6 +93,17 @@ export function Content() { <SmartClipboardButton /> + {/* Profile Management - only show if installed */} + {isInstalled && ( + <ProfileManagement + currentProfile={currentProfile} + onProfileChange={async () => { + await loadProfiles(); + await loadLsfgConfig(); + }} + /> + )} + {/* Configuration Section - only show if installed */} {isInstalled && ( <ConfigurationSection diff --git a/src/components/ProfileManagement.tsx b/src/components/ProfileManagement.tsx new file mode 100644 index 0000000..67f0645 --- /dev/null +++ b/src/components/ProfileManagement.tsx @@ -0,0 +1,340 @@ +import { useState, useEffect } from "react"; +import { + PanelSection, + PanelSectionRow, + Dropdown, + DropdownOption, + showModal, + ConfirmModal, + Field, + DialogButton, + ButtonItem, + ModalRoot, + TextField, + Focusable +} from "@decky/ui"; +import { + getProfiles, + createProfile, + deleteProfile, + renameProfile, + setCurrentProfile, + ProfilesResult, + ProfileResult +} from "../api/lsfgApi"; +import { showSuccessToast, showErrorToast } from "../utils/toastUtils"; + +interface ProfileManagementProps { + currentProfile?: string; + onProfileChange?: (profileName: string) => void; +} + +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="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); + + // Load profiles on component mount + useEffect(() => { + loadProfiles(); + }, []); + + // Update selected profile when prop changes + useEffect(() => { + if (currentProfile) { + setSelectedProfile(currentProfile); + } + }, [currentProfile]); + + 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="Create New Profile" + description="Enter a name for the new profile. The current profile's settings will be copied." + okText="Create" + cancelText="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) { + showSuccessToast("Profile created", `Created profile: ${profileName}`); + await loadProfiles(); + // Automatically switch to the newly created profile + await handleProfileChange(profileName); + } 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("Cannot delete default profile", "The default profile cannot be deleted"); + return; + } + + showModal( + <ConfirmModal + strTitle="Delete Profile" + strDescription={`Are you sure you want to delete the profile "${selectedProfile}"? This action cannot be undone.`} + strOKButtonText="Delete" + strCancelButtonText="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("Cannot rename default profile", "The default profile cannot be renamed"); + return; + } + + showModal( + <TextInputModal + title="Rename Profile" + description={`Enter a new name for the profile "${selectedProfile}".`} + defaultValue={selectedProfile} + okText="Rename" + cancelText="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) { + showSuccessToast("Profile renamed", `Renamed profile to: ${newName}`); + await loadProfiles(); + setSelectedProfile(newName); + onProfileChange?.(newName); + } 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" ? "Default" : profile + })), + { + data: "__NEW_PROFILE__", + label: "New Profile" + } + ]; + + return ( + <PanelSection title="Select Profile"> + <PanelSectionRow> + <Field + label="" + childrenLayout="below" + childrenContainerWidth="max" + > + <Dropdown + rgOptions={profileOptions} + selectedOption={selectedProfile} + onChange={handleDropdownChange} + disabled={isLoading} + /> + </Field> + </PanelSectionRow> + + <PanelSectionRow> + <ButtonItem + layout="below" + onClick={handleRenameProfile} + disabled={isLoading || selectedProfile === "decky-lsfg-vk"} + > + Rename + </ButtonItem> + </PanelSectionRow> + + <PanelSectionRow> + <ButtonItem + layout="below" + onClick={handleDeleteProfile} + disabled={isLoading || selectedProfile === "decky-lsfg-vk"} + > + Delete + </ButtonItem> + </PanelSectionRow> + </PanelSection> + ); +} diff --git a/src/components/index.ts b/src/components/index.ts index 305911d..bf60423 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -8,3 +8,4 @@ export { WikiButton } from "./WikiButton"; export { SmartClipboardButton } from "./SmartClipboardButton"; export { PluginUpdateChecker } from "./PluginUpdateChecker"; export { NerdStuffModal } from "./NerdStuffModal"; +export { ProfileManagement } from "./ProfileManagement"; |
