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:
297
src/hooks/useDirectorySettings.ts
Normal file
297
src/hooks/useDirectorySettings.ts
Normal file
@@ -0,0 +1,297 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
import { homeDir, join } from "@tauri-apps/api/path";
|
||||
import { settingsApi, type AppType } from "@/lib/api";
|
||||
import type { SettingsFormState } from "./useSettingsForm";
|
||||
|
||||
type DirectoryKey = "appConfig" | "claude" | "codex";
|
||||
|
||||
export interface ResolvedDirectories {
|
||||
appConfig: string;
|
||||
claude: string;
|
||||
codex: string;
|
||||
}
|
||||
|
||||
const sanitizeDir = (value?: string | null): string | undefined => {
|
||||
if (!value) return undefined;
|
||||
const trimmed = value.trim();
|
||||
return trimmed.length > 0 ? trimmed : undefined;
|
||||
};
|
||||
|
||||
const computeDefaultAppConfigDir = async (): Promise<string | undefined> => {
|
||||
try {
|
||||
const home = await homeDir();
|
||||
return await join(home, ".cc-switch");
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"[useDirectorySettings] Failed to resolve default app config dir",
|
||||
error,
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const computeDefaultConfigDir = async (
|
||||
app: AppType,
|
||||
): Promise<string | undefined> => {
|
||||
try {
|
||||
const home = await homeDir();
|
||||
const folder = app === "claude" ? ".claude" : ".codex";
|
||||
return await join(home, folder);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"[useDirectorySettings] Failed to resolve default config dir",
|
||||
error,
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
export interface UseDirectorySettingsProps {
|
||||
settings: SettingsFormState | null;
|
||||
onUpdateSettings: (updates: Partial<SettingsFormState>) => void;
|
||||
}
|
||||
|
||||
export interface UseDirectorySettingsResult {
|
||||
appConfigDir?: string;
|
||||
resolvedDirs: ResolvedDirectories;
|
||||
isLoading: boolean;
|
||||
initialAppConfigDir?: string;
|
||||
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>;
|
||||
resetAllDirectories: (claudeDir?: string, codexDir?: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* useDirectorySettings - 目录管理
|
||||
* 负责:
|
||||
* - appConfigDir 状态
|
||||
* - resolvedDirs 状态
|
||||
* - 目录选择(browse)
|
||||
* - 目录重置
|
||||
* - 默认值计算
|
||||
*/
|
||||
export function useDirectorySettings({
|
||||
settings,
|
||||
onUpdateSettings,
|
||||
}: UseDirectorySettingsProps): UseDirectorySettingsResult {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [appConfigDir, setAppConfigDir] = useState<string | undefined>(
|
||||
undefined,
|
||||
);
|
||||
const [resolvedDirs, setResolvedDirs] = useState<ResolvedDirectories>({
|
||||
appConfig: "",
|
||||
claude: "",
|
||||
codex: "",
|
||||
});
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
const defaultsRef = useRef<ResolvedDirectories>({
|
||||
appConfig: "",
|
||||
claude: "",
|
||||
codex: "",
|
||||
});
|
||||
const initialAppConfigDirRef = useRef<string | undefined>(undefined);
|
||||
|
||||
// 加载目录信息
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
setIsLoading(true);
|
||||
|
||||
const load = async () => {
|
||||
try {
|
||||
const [
|
||||
overrideRaw,
|
||||
claudeDir,
|
||||
codexDir,
|
||||
defaultAppConfig,
|
||||
defaultClaudeDir,
|
||||
defaultCodexDir,
|
||||
] = await Promise.all([
|
||||
settingsApi.getAppConfigDirOverride(),
|
||||
settingsApi.getConfigDir("claude"),
|
||||
settingsApi.getConfigDir("codex"),
|
||||
computeDefaultAppConfigDir(),
|
||||
computeDefaultConfigDir("claude"),
|
||||
computeDefaultConfigDir("codex"),
|
||||
]);
|
||||
|
||||
if (!active) return;
|
||||
|
||||
const normalizedOverride = sanitizeDir(overrideRaw ?? undefined);
|
||||
|
||||
defaultsRef.current = {
|
||||
appConfig: defaultAppConfig ?? "",
|
||||
claude: defaultClaudeDir ?? "",
|
||||
codex: defaultCodexDir ?? "",
|
||||
};
|
||||
|
||||
setAppConfigDir(normalizedOverride);
|
||||
initialAppConfigDirRef.current = normalizedOverride;
|
||||
|
||||
setResolvedDirs({
|
||||
appConfig: normalizedOverride ?? defaultsRef.current.appConfig,
|
||||
claude: claudeDir || defaultsRef.current.claude,
|
||||
codex: codexDir || defaultsRef.current.codex,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"[useDirectorySettings] Failed to load directory info",
|
||||
error,
|
||||
);
|
||||
} finally {
|
||||
if (active) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void load();
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const updateDirectoryState = useCallback(
|
||||
(key: DirectoryKey, value?: string) => {
|
||||
const sanitized = sanitizeDir(value);
|
||||
if (key === "appConfig") {
|
||||
setAppConfigDir(sanitized);
|
||||
} else {
|
||||
onUpdateSettings(
|
||||
key === "claude"
|
||||
? { claudeConfigDir: sanitized }
|
||||
: { codexConfigDir: sanitized },
|
||||
);
|
||||
}
|
||||
|
||||
setResolvedDirs((prev) => ({
|
||||
...prev,
|
||||
[key]: sanitized ?? defaultsRef.current[key],
|
||||
}));
|
||||
},
|
||||
[onUpdateSettings],
|
||||
);
|
||||
|
||||
const updateAppConfigDir = useCallback(
|
||||
(value?: string) => {
|
||||
updateDirectoryState("appConfig", value);
|
||||
},
|
||||
[updateDirectoryState],
|
||||
);
|
||||
|
||||
const updateDirectory = useCallback(
|
||||
(app: AppType, value?: string) => {
|
||||
updateDirectoryState(app === "claude" ? "claude" : "codex", value);
|
||||
},
|
||||
[updateDirectoryState],
|
||||
);
|
||||
|
||||
const browseDirectory = useCallback(
|
||||
async (app: AppType) => {
|
||||
const key: DirectoryKey = app === "claude" ? "claude" : "codex";
|
||||
const currentValue =
|
||||
key === "claude"
|
||||
? (settings?.claudeConfigDir ?? resolvedDirs.claude)
|
||||
: (settings?.codexConfigDir ?? resolvedDirs.codex);
|
||||
|
||||
try {
|
||||
const picked = await settingsApi.selectConfigDirectory(currentValue);
|
||||
const sanitized = sanitizeDir(picked ?? undefined);
|
||||
if (!sanitized) return;
|
||||
updateDirectoryState(key, sanitized);
|
||||
} catch (error) {
|
||||
console.error("[useDirectorySettings] Failed to pick directory", error);
|
||||
toast.error(
|
||||
t("settings.selectFileFailed", {
|
||||
defaultValue: "选择目录失败",
|
||||
}),
|
||||
);
|
||||
}
|
||||
},
|
||||
[settings, resolvedDirs, t, updateDirectoryState],
|
||||
);
|
||||
|
||||
const browseAppConfigDir = useCallback(async () => {
|
||||
const currentValue = appConfigDir ?? resolvedDirs.appConfig;
|
||||
try {
|
||||
const picked = await settingsApi.selectConfigDirectory(currentValue);
|
||||
const sanitized = sanitizeDir(picked ?? undefined);
|
||||
if (!sanitized) return;
|
||||
updateDirectoryState("appConfig", sanitized);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"[useDirectorySettings] Failed to pick app config directory",
|
||||
error,
|
||||
);
|
||||
toast.error(
|
||||
t("settings.selectFileFailed", {
|
||||
defaultValue: "选择目录失败",
|
||||
}),
|
||||
);
|
||||
}
|
||||
}, [appConfigDir, resolvedDirs.appConfig, t, updateDirectoryState]);
|
||||
|
||||
const resetDirectory = useCallback(
|
||||
async (app: AppType) => {
|
||||
const key: DirectoryKey = app === "claude" ? "claude" : "codex";
|
||||
if (!defaultsRef.current[key]) {
|
||||
const fallback = await computeDefaultConfigDir(app);
|
||||
if (fallback) {
|
||||
defaultsRef.current = {
|
||||
...defaultsRef.current,
|
||||
[key]: fallback,
|
||||
};
|
||||
}
|
||||
}
|
||||
updateDirectoryState(key, undefined);
|
||||
},
|
||||
[updateDirectoryState],
|
||||
);
|
||||
|
||||
const resetAppConfigDir = useCallback(async () => {
|
||||
if (!defaultsRef.current.appConfig) {
|
||||
const fallback = await computeDefaultAppConfigDir();
|
||||
if (fallback) {
|
||||
defaultsRef.current = {
|
||||
...defaultsRef.current,
|
||||
appConfig: fallback,
|
||||
};
|
||||
}
|
||||
}
|
||||
updateDirectoryState("appConfig", undefined);
|
||||
}, [updateDirectoryState]);
|
||||
|
||||
const resetAllDirectories = useCallback(
|
||||
(claudeDir?: string, codexDir?: string) => {
|
||||
setAppConfigDir(initialAppConfigDirRef.current);
|
||||
setResolvedDirs({
|
||||
appConfig:
|
||||
initialAppConfigDirRef.current ?? defaultsRef.current.appConfig,
|
||||
claude: claudeDir ?? defaultsRef.current.claude,
|
||||
codex: codexDir ?? defaultsRef.current.codex,
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return {
|
||||
appConfigDir,
|
||||
resolvedDirs,
|
||||
isLoading,
|
||||
initialAppConfigDir: initialAppConfigDirRef.current,
|
||||
updateDirectory,
|
||||
updateAppConfigDir,
|
||||
browseDirectory,
|
||||
browseAppConfigDir,
|
||||
resetDirectory,
|
||||
resetAppConfigDir,
|
||||
resetAllDirectories,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user