Files
cc-switch/src/hooks/useDirectorySettings.ts

298 lines
8.2 KiB
TypeScript
Raw Normal View History

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,
};
}