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 AppId } from "@/lib/api"; import type { SettingsFormState } from "./useSettingsForm"; type DirectoryKey = "appConfig" | "claude" | "codex" | "gemini"; export interface ResolvedDirectories { appConfig: string; claude: string; codex: string; gemini: 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 => { 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: AppId, ): Promise => { try { const home = await homeDir(); const folder = app === "claude" ? ".claude" : app === "codex" ? ".codex" : ".gemini"; 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) => void; } export interface UseDirectorySettingsResult { appConfigDir?: string; resolvedDirs: ResolvedDirectories; isLoading: boolean; initialAppConfigDir?: string; updateDirectory: (app: AppId, value?: string) => void; updateAppConfigDir: (value?: string) => void; browseDirectory: (app: AppId) => Promise; browseAppConfigDir: () => Promise; resetDirectory: (app: AppId) => Promise; resetAppConfigDir: () => Promise; resetAllDirectories: ( claudeDir?: string, codexDir?: string, geminiDir?: string, ) => void; } /** * useDirectorySettings - 目录管理 * 负责: * - appConfigDir 状态 * - resolvedDirs 状态 * - 目录选择(browse) * - 目录重置 * - 默认值计算 */ export function useDirectorySettings({ settings, onUpdateSettings, }: UseDirectorySettingsProps): UseDirectorySettingsResult { const { t } = useTranslation(); const [appConfigDir, setAppConfigDir] = useState( undefined, ); const [resolvedDirs, setResolvedDirs] = useState({ appConfig: "", claude: "", codex: "", gemini: "", }); const [isLoading, setIsLoading] = useState(true); const defaultsRef = useRef({ appConfig: "", claude: "", codex: "", gemini: "", }); const initialAppConfigDirRef = useRef(undefined); // 加载目录信息 useEffect(() => { let active = true; setIsLoading(true); const load = async () => { try { const [ overrideRaw, claudeDir, codexDir, geminiDir, defaultAppConfig, defaultClaudeDir, defaultCodexDir, defaultGeminiDir, ] = await Promise.all([ settingsApi.getAppConfigDirOverride(), settingsApi.getConfigDir("claude"), settingsApi.getConfigDir("codex"), settingsApi.getConfigDir("gemini"), computeDefaultAppConfigDir(), computeDefaultConfigDir("claude"), computeDefaultConfigDir("codex"), computeDefaultConfigDir("gemini"), ]); if (!active) return; const normalizedOverride = sanitizeDir(overrideRaw ?? undefined); defaultsRef.current = { appConfig: defaultAppConfig ?? "", claude: defaultClaudeDir ?? "", codex: defaultCodexDir ?? "", gemini: defaultGeminiDir ?? "", }; setAppConfigDir(normalizedOverride); initialAppConfigDirRef.current = normalizedOverride; setResolvedDirs({ appConfig: normalizedOverride ?? defaultsRef.current.appConfig, claude: claudeDir || defaultsRef.current.claude, codex: codexDir || defaultsRef.current.codex, gemini: geminiDir || defaultsRef.current.gemini, }); } 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 } : key === "codex" ? { codexConfigDir: sanitized } : { geminiConfigDir: sanitized }, ); } setResolvedDirs((prev) => ({ ...prev, [key]: sanitized ?? defaultsRef.current[key], })); }, [onUpdateSettings], ); const updateAppConfigDir = useCallback( (value?: string) => { updateDirectoryState("appConfig", value); }, [updateDirectoryState], ); const updateDirectory = useCallback( (app: AppId, value?: string) => { updateDirectoryState( app === "claude" ? "claude" : app === "codex" ? "codex" : "gemini", value, ); }, [updateDirectoryState], ); const browseDirectory = useCallback( async (app: AppId) => { const key: DirectoryKey = app === "claude" ? "claude" : app === "codex" ? "codex" : "gemini"; const currentValue = key === "claude" ? (settings?.claudeConfigDir ?? resolvedDirs.claude) : key === "codex" ? (settings?.codexConfigDir ?? resolvedDirs.codex) : (settings?.geminiConfigDir ?? resolvedDirs.gemini); 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: AppId) => { const key: DirectoryKey = app === "claude" ? "claude" : app === "codex" ? "codex" : "gemini"; 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, geminiDir?: string) => { setAppConfigDir(initialAppConfigDirRef.current); setResolvedDirs({ appConfig: initialAppConfigDirRef.current ?? defaultsRef.current.appConfig, claude: claudeDir ?? defaultsRef.current.claude, codex: codexDir ?? defaultsRef.current.codex, gemini: geminiDir ?? defaultsRef.current.gemini, }); }, [], ); return { appConfigDir, resolvedDirs, isLoading, initialAppConfigDir: initialAppConfigDirRef.current, updateDirectory, updateAppConfigDir, browseDirectory, browseAppConfigDir, resetDirectory, resetAppConfigDir, resetAllDirectories, }; }