import { useCallback, useEffect, useRef, useState } from 'react'; import { invoke } from '@tauri-apps/api/core'; import { listen } from '@tauri-apps/api/event'; import { open } from '@tauri-apps/plugin-dialog'; import { RotateCcw, Save } from 'lucide-react'; import { useI18n } from '../i18n'; import { templateMessages, templateText } from '../i18n/templateConfig'; import { useCoreRuntime, type CoreStatus } from '../coreRuntime'; import { SettingsHelp } from './SettingsHelp'; import { applyTemplateChanges, draftTemplateField, readTemplatePath, templateFieldKey, templateFieldValidation, templateFieldSaveValue, type TemplateConfigChange, type TemplateConfigDraft, type TemplateConfigField, type TemplateConfigGroup, type TemplateConfigSaveResult, type TemplateText, } from '../services/templateConfig'; import '../styles/template-config.css'; const restartHint = { zh: '需重启内核', en: 'Core restart required', ja: 'コアの再起動が必要' } satisfies TemplateText; export function TemplateConfigSection({ groups, visibleGroups, onDirtyGroupsChange }: { groups: readonly TemplateConfigGroup[]; visibleGroups?: readonly string[]; onDirtyGroupsChange?: (groupIds: readonly string[]) => void; }) { const { locale } = useI18n(); const { status, publishStatus, refreshStatus } = useCoreRuntime(); const [config, setConfig] = useState | null>(null); const [drafts, setDrafts] = useState>({}); const [loadError, setLoadError] = useState(''); const [groupErrors, setGroupErrors] = useState>({}); const [saved, setSaved] = useState>({}); const [restartNeeded, setRestartNeeded] = useState(false); const [busy, setBusy] = useState(null); const mounted = useRef(true); const revision = useRef(0); const reportedDirtyGroups = useRef(null); const tr = (message: keyof typeof templateMessages) => templateText(templateMessages[message], locale); useEffect(() => { if (!onDirtyGroupsChange) return; const dirtyGroups = groups.filter((group) => group.fields.some((field) => drafts[templateFieldKey(field.path)])).map((group) => group.id); const signature = JSON.stringify(dirtyGroups); if (reportedDirtyGroups.current === signature) return; reportedDirtyGroups.current = signature; onDirtyGroupsChange(dirtyGroups); }, [drafts, groups, onDirtyGroupsChange]); const reload = useCallback(async () => { const request = ++revision.current; try { const value = await invoke>('get_extended_core_config'); if (mounted.current && request === revision.current) { setConfig(value); setLoadError(''); } } catch (error) { if (mounted.current && request === revision.current) setLoadError(String(error)); } }, []); useEffect(() => { mounted.current = true; let disposed = false; let stop: (() => void) | undefined; void reload(); void listen('config-files-changed', () => { if (!disposed) void reload(); }).then((unlisten) => { if (!disposed) stop = unlisten; else unlisten(); }).catch((error) => { if (!disposed) setLoadError(String(error)); }); return () => { disposed = true; mounted.current = false; ++revision.current; stop?.(); }; }, [reload]); const changeField = (group: TemplateConfigGroup, field: TemplateConfigField, value: unknown, remove = false) => { if (!config) return; const key = templateFieldKey(field.path); setDrafts((previous) => { const next = { ...previous }; const draft = draftTemplateField(field, config, previous[key], value, remove); if (draft) next[key] = draft; else delete next[key]; return next; }); setGroupErrors((previous) => ({ ...previous, [group.id]: '' })); setSaved((previous) => ({ ...previous, [group.id]: false })); }; const discardGroup = (group: TemplateConfigGroup) => { setDrafts((previous) => { const next = { ...previous }; for (const field of group.fields) delete next[templateFieldKey(field.path)]; return next; }); setGroupErrors((previous) => ({ ...previous, [group.id]: '' })); setSaved((previous) => ({ ...previous, [group.id]: false })); }; const saveGroup = async (group: TemplateConfigGroup) => { if (!config || busy) return; const changes: TemplateConfigChange[] = []; for (const field of group.fields) { const draft = drafts[templateFieldKey(field.path)]; if (!draft) continue; const error = !draft.remove && templateFieldValidation(field, draft.value); if (error) { setGroupErrors((previous) => ({ ...previous, [group.id]: `${templateText(field.label, locale)}: ${templateText(error, locale)}` })); return; } changes.push({ ...draft, path: [...field.path], value: draft.remove ? null : templateFieldSaveValue(field, draft.value) }); } if (!changes.length) return; const groupError = group.validate?.(applyTemplateChanges(config, changes)); if (groupError) { setGroupErrors((previous) => ({ ...previous, [group.id]: templateText(groupError, locale) })); return; } setBusy(group.id); setGroupErrors((previous) => ({ ...previous, [group.id]: '' })); try { const result = await invoke('save_extended_core_config', { changes }); ++revision.current; setConfig(result.config); setLoadError(''); setDrafts((previous) => { const next = { ...previous }; for (const change of changes) delete next[templateFieldKey(change.path)]; return next; }); setSaved((previous) => ({ ...previous, [group.id]: true })); if (result.restartRequired) setRestartNeeded(true); } catch (error) { setGroupErrors((previous) => ({ ...previous, [group.id]: String(error) })); // Refresh untouched values while retaining both the draft and its original conflict check. await reload(); } finally { if (mounted.current) setBusy(null); } }; const restart = async () => { setBusy('restart'); try { publishStatus(await invoke('restart_core_process')); setRestartNeeded(false); setLoadError(''); } catch (error) { setLoadError(String(error)); await refreshStatus(); } finally { setBusy(null); } }; const chooseDirectory = async (group: TemplateConfigGroup, field: TemplateConfigField) => { setBusy('directory'); try { const selected = await open({ directory: true, multiple: false, title: templateText(field.label, locale) }); if (mounted.current && typeof selected === 'string') changeField(group, field, selected); } catch (error) { if (mounted.current) setGroupErrors((previous) => ({ ...previous, [group.id]: String(error) })); } finally { if (mounted.current) setBusy(null); } }; const control = (group: TemplateConfigGroup, field: TemplateConfigField, value: unknown, id: string, invalid: boolean) => { const disabled = busy !== null || config === null; const onChange = (next: unknown) => changeField(group, field, next); const common = { id, disabled, 'aria-invalid': invalid || undefined, 'aria-describedby': invalid ? `${id}-error` : undefined }; if (field.type === 'custom' && field.render) return field.render({ value, onChange, disabled, id }); if (field.type === 'boolean') return ; if (field.type === 'select') { const selected = field.options?.findIndex((option) => Object.is(option.value, value)) ?? -1; return ; } if (field.type === 'string-list' || field.type === 'json') return