summaryrefslogtreecommitdiff
path: root/src/components
diff options
context:
space:
mode:
authorKurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com>2025-08-18 11:51:56 -0400
committerGitHub <noreply@github.com>2025-08-18 11:51:56 -0400
commit3d75e193791c18b1a0bcde5c8e80bdc24492c031 (patch)
treece232300a4779078c28ed549a8ce678a1f7974c2 /src/components
parent6489f2273fc246fcca25e95d913e60ea214e0d31 (diff)
parent1fcde377b970e0e04d11ca68892c3e04481471ac (diff)
downloaddecky-lsfg-vk-3d75e193791c18b1a0bcde5c8e80bdc24492c031.tar.gz
decky-lsfg-vk-3d75e193791c18b1a0bcde5c8e80bdc24492c031.zip
Merge pull request #142 from xXJSONDeruloXx/feat/manual-profilesv0.10.3v0.10.2
Feat/manual profiles
Diffstat (limited to 'src/components')
-rw-r--r--src/components/ConfigurationSection.tsx6
-rw-r--r--src/components/Content.tsx32
-rw-r--r--src/components/ProfileManagement.tsx340
-rw-r--r--src/components/index.ts1
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";