refactor: split useSettings hook into specialized hooks

Before optimization:
- useSettings.ts: 516 lines (single monolithic hook)

After optimization:
- useSettingsForm.ts: 158 lines (form state management)
- useDirectorySettings.ts: 297 lines (directory management)
- useSettingsMetadata.ts: 95 lines (metadata management)
- useSettings.ts: 215 lines (composition layer)
- Total: 765 lines (+249 lines, but with clear separation of concerns)

Benefits:
 Single Responsibility Principle: each hook focuses on one domain
 Testability: independent hooks are easier to unit test
 Reusability: specialized hooks can be reused in other components
 Maintainability: reduced cognitive load per file
 Zero breaking changes: SettingsDialog auto-adapted to new interface

Technical details:
- useSettingsForm: pure form state + language sync
- useDirectorySettings: directory selection/reset + default value computation
- useSettingsMetadata: config path + portable mode + restart flag
- useSettings: composition layer + save logic + reset logic
This commit is contained in:
Jason
2025-10-17 18:12:03 +08:00
parent f963d58e6a
commit edfb61186d
4 changed files with 642 additions and 393 deletions
+158
View File
@@ -0,0 +1,158 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { useSettingsQuery } from "@/lib/query";
import type { Settings } from "@/types";
type Language = "zh" | "en";
export type SettingsFormState = Omit<Settings, "language"> & {
language: Language;
};
const normalizeLanguage = (lang?: string | null): Language => {
if (!lang) return "zh";
return lang === "en" ? "en" : "zh";
};
const sanitizeDir = (value?: string | null): string | undefined => {
if (!value) return undefined;
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : undefined;
};
export interface UseSettingsFormResult {
settings: SettingsFormState | null;
isLoading: boolean;
initialLanguage: Language;
updateSettings: (updates: Partial<SettingsFormState>) => void;
resetSettings: (serverData: Settings | null) => void;
readPersistedLanguage: () => Language;
syncLanguage: (lang: Language) => void;
}
/**
* useSettingsForm - 表单状态管理
* 负责:
* - 表单数据状态
* - 表单字段更新
* - 语言同步
* - 表单重置
*/
export function useSettingsForm(): UseSettingsFormResult {
const { i18n } = useTranslation();
const { data, isLoading } = useSettingsQuery();
const [settingsState, setSettingsState] = useState<SettingsFormState | null>(
null,
);
const initialLanguageRef = useRef<Language>("zh");
const readPersistedLanguage = useCallback((): Language => {
if (typeof window !== "undefined") {
const stored = window.localStorage.getItem("language");
if (stored === "en" || stored === "zh") {
return stored;
}
}
return normalizeLanguage(i18n.language);
}, [i18n.language]);
const syncLanguage = useCallback(
(lang: Language) => {
const current = normalizeLanguage(i18n.language);
if (current !== lang) {
void i18n.changeLanguage(lang);
}
},
[i18n],
);
// 初始化设置数据
useEffect(() => {
if (!data) return;
const normalizedLanguage = normalizeLanguage(
data.language ?? readPersistedLanguage(),
);
const normalized: SettingsFormState = {
...data,
showInTray: data.showInTray ?? true,
minimizeToTrayOnClose: data.minimizeToTrayOnClose ?? true,
enableClaudePluginIntegration:
data.enableClaudePluginIntegration ?? false,
claudeConfigDir: sanitizeDir(data.claudeConfigDir),
codexConfigDir: sanitizeDir(data.codexConfigDir),
language: normalizedLanguage,
};
setSettingsState(normalized);
initialLanguageRef.current = normalizedLanguage;
syncLanguage(normalizedLanguage);
}, [data, readPersistedLanguage, syncLanguage]);
const updateSettings = useCallback(
(updates: Partial<SettingsFormState>) => {
setSettingsState((prev) => {
const base =
prev ??
({
showInTray: true,
minimizeToTrayOnClose: true,
enableClaudePluginIntegration: false,
language: readPersistedLanguage(),
} as SettingsFormState);
const next: SettingsFormState = {
...base,
...updates,
};
if (updates.language) {
const normalized = normalizeLanguage(updates.language);
next.language = normalized;
syncLanguage(normalized);
}
return next;
});
},
[readPersistedLanguage, syncLanguage],
);
const resetSettings = useCallback(
(serverData: Settings | null) => {
if (!serverData) return;
const normalizedLanguage = normalizeLanguage(
serverData.language ?? readPersistedLanguage(),
);
const normalized: SettingsFormState = {
...serverData,
showInTray: serverData.showInTray ?? true,
minimizeToTrayOnClose: serverData.minimizeToTrayOnClose ?? true,
enableClaudePluginIntegration:
serverData.enableClaudePluginIntegration ?? false,
claudeConfigDir: sanitizeDir(serverData.claudeConfigDir),
codexConfigDir: sanitizeDir(serverData.codexConfigDir),
language: normalizedLanguage,
};
setSettingsState(normalized);
syncLanguage(initialLanguageRef.current);
},
[readPersistedLanguage, syncLanguage],
);
return {
settings: settingsState,
isLoading,
initialLanguage: initialLanguageRef.current,
updateSettings,
resetSettings,
readPersistedLanguage,
syncLanguage,
};
}