summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorxXJSONDeruloXx <danielhimebauch@gmail.com>2025-08-16 12:05:10 -0400
committerxXJSONDeruloXx <danielhimebauch@gmail.com>2025-08-16 12:05:10 -0400
commit4104e28053fc03b3875958c7bf56ec6fbc5aab84 (patch)
tree1095b374ab453ce98ab35dfbfa79d810ad99fdab /src
parent6489f2273fc246fcca25e95d913e60ea214e0d31 (diff)
downloaddecky-lsfg-vk-4104e28053fc03b3875958c7bf56ec6fbc5aab84.tar.gz
decky-lsfg-vk-4104e28053fc03b3875958c7bf56ec6fbc5aab84.zip
initial prof selector and creator ui
Diffstat (limited to 'src')
-rw-r--r--src/api/lsfgApi.ts26
-rw-r--r--src/components/Content.tsx26
-rw-r--r--src/components/ProfileManagement.tsx307
-rw-r--r--src/components/index.ts1
-rw-r--r--src/hooks/useProfileManagement.ts194
5 files changed, 553 insertions, 1 deletions
diff --git a/src/api/lsfgApi.ts b/src/api/lsfgApi.ts
index 8d14da6..6c535af 100644
--- a/src/api/lsfgApi.ts
+++ b/src/api/lsfgApi.ts
@@ -55,6 +55,8 @@ export interface ConfigSchemaResult {
field_names: string[];
field_types: Record<string, string>;
defaults: ConfigurationData;
+ profiles?: string[];
+ current_profile?: string;
}
export interface UpdateCheckResult {
@@ -87,6 +89,22 @@ export interface FileContentResult {
error?: 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");
@@ -113,3 +131,11 @@ export const updateLsfgConfigFromObject = async (config: ConfigurationData): Pro
// Self-updater API functions
export const checkForPluginUpdate = callable<[], UpdateCheckResult>("check_for_plugin_update");
export const downloadPluginUpdate = callable<[string], UpdateDownloadResult>("download_plugin_update");
+
+// 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/Content.tsx b/src/components/Content.tsx
index a075574..c7c757b 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,11 @@ export function Content() {
updateField
} = useLsfgConfig();
+ const {
+ currentProfile,
+ updateProfileConfig
+ } = useProfileManagement();
+
const { isInstalling, isUninstalling, handleInstall, handleUninstall } = useInstallationActions();
// Reload config when installation status changes
@@ -40,7 +47,16 @@ 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 };
+ await updateProfileConfig(currentProfile, newConfig);
+ // Also update local config state
+ await updateField(fieldName, value);
+ } else {
+ // Fallback to the original method
+ await updateField(fieldName, value);
+ }
};
const onInstall = () => {
@@ -74,6 +90,14 @@ export function Content() {
<SmartClipboardButton />
+ {/* Profile Management - only show if installed */}
+ {isInstalled && (
+ <ProfileManagement
+ currentProfile={currentProfile}
+ onProfileChange={() => 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..a499836
--- /dev/null
+++ b/src/components/ProfileManagement.tsx
@@ -0,0 +1,307 @@
+import { useState, useEffect } from "react";
+import {
+ PanelSection,
+ PanelSectionRow,
+ Dropdown,
+ DropdownOption,
+ showModal,
+ ConfirmModal,
+ Field,
+ DialogButton,
+ Focusable,
+ ModalRoot,
+ TextField
+} 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 = () => {
+ onOK(value);
+ closeModal?.();
+ };
+
+ return (
+ <ModalRoot>
+ <div style={{ padding: "16px", minWidth: "300px" }}>
+ <h2 style={{ marginBottom: "16px" }}>{title}</h2>
+ <p style={{ marginBottom: "16px" }}>{description}</p>
+
+ <Field label="Name">
+ <TextField
+ value={value}
+ onChange={(e) => setValue(e?.target?.value || "")}
+ />
+ </Field>
+
+ <div style={{ display: "flex", gap: "8px", marginTop: "16px" }}>
+ <DialogButton onClick={handleOK} disabled={!value.trim()}>
+ {okText}
+ </DialogButton>
+ <DialogButton onClick={closeModal}>
+ {cancelText}
+ </DialogButton>
+ </div>
+ </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();
+ } 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 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 => ({
+ data: profile,
+ label: profile === "decky-lsfg-vk" ? `${profile} (default)` : profile
+ }));
+
+ return (
+ <PanelSection title="Profile Management">
+ <PanelSectionRow>
+ <Field label="Current Profile">
+ <Dropdown
+ rgOptions={profileOptions}
+ selectedOption={selectedProfile}
+ onChange={(option) => handleProfileChange(option.data)}
+ disabled={isLoading}
+ />
+ </Field>
+ </PanelSectionRow>
+
+ <PanelSectionRow>
+ <Focusable style={{ display: "flex", gap: "8px", flexWrap: "wrap" }}>
+ <DialogButton
+ onClick={handleCreateProfile}
+ disabled={isLoading}
+ style={{ flex: "1", minWidth: "120px" }}
+ >
+ New Profile
+ </DialogButton>
+
+ <DialogButton
+ onClick={handleRenameProfile}
+ disabled={isLoading || selectedProfile === "decky-lsfg-vk"}
+ style={{ flex: "1", minWidth: "120px" }}
+ >
+ Rename
+ </DialogButton>
+
+ <DialogButton
+ onClick={handleDeleteProfile}
+ disabled={isLoading || selectedProfile === "decky-lsfg-vk"}
+ style={{ flex: "1", minWidth: "120px" }}
+ >
+ Delete
+ </DialogButton>
+ </Focusable>
+ </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";
diff --git a/src/hooks/useProfileManagement.ts b/src/hooks/useProfileManagement.ts
new file mode 100644
index 0000000..bb6cd6f
--- /dev/null
+++ b/src/hooks/useProfileManagement.ts
@@ -0,0 +1,194 @@
+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<string[]>([]);
+ const [currentProfile, setCurrentProfileState] = useState<string>("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) {
+ showSuccessToast("Profile created", `Created profile: ${profileName}`);
+ 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) {
+ showSuccessToast("Profile renamed", `Renamed profile to: ${newName}`);
+ await loadProfiles();
+ // Update current profile if it was renamed
+ if (currentProfile === oldName) {
+ setCurrentProfileState(newName);
+ }
+ 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) {
+ // Only show success toast if this is the current profile
+ if (profileName === currentProfile) {
+ showSuccessToast("Configuration updated", `Updated configuration for profile: ${profileName}`);
+ }
+ 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
+ };
+}