import { useState, useEffect, useCallback } from 'react' import { invoke } from '@tauri-apps/api/core' import Dialog from './Dialog' import DialogHeader from './DialogHeader' import DialogBody from './DialogBody' import DialogFooter from './DialogFooter' import DialogActions from './DialogActions' import { Button, AnimatedCheckbox } from '@/components/buttons' import { Dropdown, DropdownOption } from '@/components/common' export type ApiSettingsField = | { kind: 'dropdown' key: string label: string description: string options: DropdownOption[] } | { kind: 'checkbox' key: string label: string sublabel: string } export interface ApiSettingsSection { title: string fields: ApiSettingsField[] } /** * Describes an unlocker's config dialog: what sections/fields to render and * which Tauri commands read/write/delete its config file. One dialog * component renders any config that fits this shape, instead of a * near-identical component per unlocker. */ export interface ApiSettingsSpec { dialogTitle: string enableLabel: string enableSublabel: string defaultConfig: Record sections: ApiSettingsSection[] readCommand: string writeCommand: string deleteCommand: string } export interface ApiSettingsDialogProps { visible: boolean onClose: () => void gamePath: string gameTitle: string spec: ApiSettingsSpec } /** * Generic settings dialog for an unlocker's config file (SmokeAPI, * ScreamAPI, ...). Renders whatever sections/fields the spec describes and * reads/writes/deletes the config through the spec's Tauri commands. */ const ApiSettingsDialog = ({ visible, onClose, gamePath, gameTitle, spec }: ApiSettingsDialogProps) => { const [enabled, setEnabled] = useState(false) const [config, setConfig] = useState>(spec.defaultConfig) const [isLoading, setIsLoading] = useState(false) const [hasChanges, setHasChanges] = useState(false) const loadConfig = useCallback(async () => { setIsLoading(true) try { const existingConfig = await invoke | null>(spec.readCommand, { gamePath, }) if (existingConfig) { setConfig(existingConfig) setEnabled(true) } else { setConfig(spec.defaultConfig) setEnabled(false) } setHasChanges(false) } catch (error) { console.error(`Failed to load ${spec.dialogTitle}:`, error) setConfig(spec.defaultConfig) setEnabled(false) } finally { setIsLoading(false) } }, [gamePath, spec]) useEffect(() => { if (visible && gamePath) { loadConfig() } }, [visible, gamePath, loadConfig]) const handleSave = async () => { setIsLoading(true) try { if (enabled) { await invoke(spec.writeCommand, { gamePath, config }) } else { await invoke(spec.deleteCommand, { gamePath }) } setHasChanges(false) onClose() } catch (error) { console.error(`Failed to save ${spec.dialogTitle}:`, error) } finally { setIsLoading(false) } } const handleCancel = () => { setHasChanges(false) onClose() } const updateConfig = (key: string, value: unknown) => { setConfig((prev) => ({ ...prev, [key]: value })) setHasChanges(true) } return (

{spec.dialogTitle}

{gameTitle}

{ setEnabled(!enabled) setHasChanges(true) }} label={spec.enableLabel} sublabel={spec.enableSublabel} />
{spec.sections.map((section) => (

{section.title}

{section.fields.map((field) => field.kind === 'dropdown' ? ( updateConfig(field.key, value)} disabled={!enabled} /> ) : (
updateConfig(field.key, !config[field.key])} label={field.label} sublabel={field.sublabel} />
) )}
))}
) } export default ApiSettingsDialog