Files
cc-switch/src/hooks/useDirectorySettings.ts
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

298 lines
8.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
};
}