Files
cc-switch/src/hooks/useSettings.ts
T
Jason edfb61186d 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
2025-10-17 18:12:03 +08:00

216 lines
5.6 KiB
TypeScript

import { useCallback, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { settingsApi, type AppType } from "@/lib/api";
import { useSettingsQuery, useSaveSettingsMutation } from "@/lib/query";
import type { Settings } from "@/types";
import {
useSettingsForm,
type SettingsFormState,
} from "./useSettingsForm";
import {
useDirectorySettings,
type ResolvedDirectories,
} from "./useDirectorySettings";
import { useSettingsMetadata } from "./useSettingsMetadata";
type Language = "zh" | "en";
interface SaveResult {
requiresRestart: boolean;
}
export interface UseSettingsResult {
settings: SettingsFormState | null;
isLoading: boolean;
isSaving: boolean;
isPortable: boolean;
configPath: string;
appConfigDir?: string;
resolvedDirs: ResolvedDirectories;
requiresRestart: boolean;
updateSettings: (updates: Partial<SettingsFormState>) => void;
updateDirectory: (app: AppType, value?: string) => void;
updateAppConfigDir: (value?: string) => void;
browseDirectory: (app: AppType) => Promise<void>;
browseAppConfigDir: () => Promise<void>;
resetDirectory: (app: AppType) => Promise<void>;
resetAppConfigDir: () => Promise<void>;
openConfigFolder: () => Promise<void>;
saveSettings: () => Promise<SaveResult | null>;
resetSettings: () => void;
acknowledgeRestart: () => void;
}
export type { SettingsFormState, ResolvedDirectories };
const sanitizeDir = (value?: string | null): string | undefined => {
if (!value) return undefined;
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : undefined;
};
/**
* useSettings - 组合层
* 负责:
* - 组合 useSettingsForm、useDirectorySettings、useSettingsMetadata
* - 保存设置逻辑
* - 重置设置逻辑
*/
export function useSettings(): UseSettingsResult {
const { t } = useTranslation();
const { data } = useSettingsQuery();
const saveMutation = useSaveSettingsMutation();
// 1️⃣ 表单状态管理
const {
settings,
isLoading: isFormLoading,
initialLanguage,
updateSettings,
resetSettings: resetForm,
readPersistedLanguage,
syncLanguage,
} = useSettingsForm();
// 2️⃣ 目录管理
const {
appConfigDir,
resolvedDirs,
isLoading: isDirectoryLoading,
initialAppConfigDir,
updateDirectory,
updateAppConfigDir,
browseDirectory,
browseAppConfigDir,
resetDirectory,
resetAppConfigDir,
resetAllDirectories,
} = useDirectorySettings({
settings,
onUpdateSettings: updateSettings,
});
// 3️⃣ 元数据管理
const {
configPath,
isPortable,
requiresRestart,
isLoading: isMetadataLoading,
openConfigFolder,
acknowledgeRestart,
setRequiresRestart,
} = useSettingsMetadata();
// 重置设置
const resetSettings = useCallback(() => {
resetForm(data ?? null);
syncLanguage(initialLanguage);
resetAllDirectories(
sanitizeDir(data?.claudeConfigDir),
sanitizeDir(data?.codexConfigDir),
);
setRequiresRestart(false);
}, [
data,
initialLanguage,
resetForm,
syncLanguage,
resetAllDirectories,
setRequiresRestart,
]);
// 保存设置
const saveSettings = useCallback(async (): Promise<SaveResult | null> => {
if (!settings) return null;
try {
const sanitizedAppDir = sanitizeDir(appConfigDir);
const sanitizedClaudeDir = sanitizeDir(settings.claudeConfigDir);
const sanitizedCodexDir = sanitizeDir(settings.codexConfigDir);
const previousAppDir = initialAppConfigDir;
const payload: Settings = {
...settings,
claudeConfigDir: sanitizedClaudeDir,
codexConfigDir: sanitizedCodexDir,
language: settings.language,
};
await saveMutation.mutateAsync(payload);
await settingsApi.setAppConfigDirOverride(sanitizedAppDir ?? null);
try {
if (payload.enableClaudePluginIntegration) {
await settingsApi.applyClaudePluginConfig({ official: false });
} else {
await settingsApi.applyClaudePluginConfig({ official: true });
}
} catch (error) {
console.warn(
"[useSettings] Failed to sync Claude plugin config",
error,
);
toast.error(
t("notifications.syncClaudePluginFailed", {
defaultValue: "同步 Claude 插件失败",
}),
);
}
try {
if (typeof window !== "undefined") {
window.localStorage.setItem("language", payload.language as Language);
}
} catch (error) {
console.warn(
"[useSettings] Failed to persist language preference",
error,
);
}
const appDirChanged = sanitizedAppDir !== (previousAppDir ?? undefined);
setRequiresRestart(appDirChanged);
return { requiresRestart: appDirChanged };
} catch (error) {
console.error("[useSettings] Failed to save settings", error);
throw error;
}
}, [
appConfigDir,
initialAppConfigDir,
saveMutation,
settings,
setRequiresRestart,
t,
]);
const isLoading = useMemo(
() => isFormLoading || isDirectoryLoading || isMetadataLoading,
[isFormLoading, isDirectoryLoading, isMetadataLoading],
);
return {
settings,
isLoading,
isSaving: saveMutation.isPending,
isPortable,
configPath,
appConfigDir,
resolvedDirs,
requiresRestart,
updateSettings,
updateDirectory,
updateAppConfigDir,
browseDirectory,
browseAppConfigDir,
resetDirectory,
resetAppConfigDir,
openConfigFolder,
saveSettings,
resetSettings,
acknowledgeRestart,
};
}