2025-10-17 18:12:03 +08:00
|
|
|
|
import { useCallback, useMemo } from "react";
|
2025-10-16 11:40:02 +08:00
|
|
|
|
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";
|
2025-10-17 18:12:03 +08:00
|
|
|
|
import {
|
|
|
|
|
|
useSettingsForm,
|
|
|
|
|
|
type SettingsFormState,
|
|
|
|
|
|
} from "./useSettingsForm";
|
|
|
|
|
|
import {
|
|
|
|
|
|
useDirectorySettings,
|
|
|
|
|
|
type ResolvedDirectories,
|
|
|
|
|
|
} from "./useDirectorySettings";
|
|
|
|
|
|
import { useSettingsMetadata } from "./useSettingsMetadata";
|
2025-10-16 11:40:02 +08:00
|
|
|
|
|
|
|
|
|
|
type Language = "zh" | "en";
|
|
|
|
|
|
|
|
|
|
|
|
interface SaveResult {
|
|
|
|
|
|
requiresRestart: boolean;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export interface UseSettingsResult {
|
|
|
|
|
|
settings: SettingsFormState | null;
|
|
|
|
|
|
isLoading: boolean;
|
|
|
|
|
|
isSaving: boolean;
|
|
|
|
|
|
isPortable: boolean;
|
|
|
|
|
|
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>;
|
|
|
|
|
|
saveSettings: () => Promise<SaveResult | null>;
|
|
|
|
|
|
resetSettings: () => void;
|
|
|
|
|
|
acknowledgeRestart: () => void;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-17 18:12:03 +08:00
|
|
|
|
export type { SettingsFormState, ResolvedDirectories };
|
2025-10-16 11:40:02 +08:00
|
|
|
|
|
|
|
|
|
|
const sanitizeDir = (value?: string | null): string | undefined => {
|
|
|
|
|
|
if (!value) return undefined;
|
|
|
|
|
|
const trimmed = value.trim();
|
|
|
|
|
|
return trimmed.length > 0 ? trimmed : undefined;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-10-17 18:12:03 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* useSettings - 组合层
|
|
|
|
|
|
* 负责:
|
|
|
|
|
|
* - 组合 useSettingsForm、useDirectorySettings、useSettingsMetadata
|
|
|
|
|
|
* - 保存设置逻辑
|
|
|
|
|
|
* - 重置设置逻辑
|
|
|
|
|
|
*/
|
2025-10-16 11:40:02 +08:00
|
|
|
|
export function useSettings(): UseSettingsResult {
|
2025-10-17 18:12:03 +08:00
|
|
|
|
const { t } = useTranslation();
|
|
|
|
|
|
const { data } = useSettingsQuery();
|
2025-10-16 11:40:02 +08:00
|
|
|
|
const saveMutation = useSaveSettingsMutation();
|
|
|
|
|
|
|
2025-10-17 18:12:03 +08:00
|
|
|
|
// 1️⃣ 表单状态管理
|
|
|
|
|
|
const {
|
|
|
|
|
|
settings,
|
|
|
|
|
|
isLoading: isFormLoading,
|
|
|
|
|
|
initialLanguage,
|
|
|
|
|
|
updateSettings,
|
|
|
|
|
|
resetSettings: resetForm,
|
|
|
|
|
|
syncLanguage,
|
|
|
|
|
|
} = useSettingsForm();
|
2025-10-16 11:40:02 +08:00
|
|
|
|
|
2025-10-17 18:12:03 +08:00
|
|
|
|
// 2️⃣ 目录管理
|
|
|
|
|
|
const {
|
|
|
|
|
|
appConfigDir,
|
|
|
|
|
|
resolvedDirs,
|
|
|
|
|
|
isLoading: isDirectoryLoading,
|
|
|
|
|
|
initialAppConfigDir,
|
|
|
|
|
|
updateDirectory,
|
|
|
|
|
|
updateAppConfigDir,
|
|
|
|
|
|
browseDirectory,
|
|
|
|
|
|
browseAppConfigDir,
|
|
|
|
|
|
resetDirectory,
|
|
|
|
|
|
resetAppConfigDir,
|
|
|
|
|
|
resetAllDirectories,
|
|
|
|
|
|
} = useDirectorySettings({
|
|
|
|
|
|
settings,
|
|
|
|
|
|
onUpdateSettings: updateSettings,
|
2025-10-16 11:40:02 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2025-10-17 18:12:03 +08:00
|
|
|
|
// 3️⃣ 元数据管理
|
|
|
|
|
|
const {
|
|
|
|
|
|
isPortable,
|
|
|
|
|
|
requiresRestart,
|
|
|
|
|
|
isLoading: isMetadataLoading,
|
|
|
|
|
|
acknowledgeRestart,
|
|
|
|
|
|
setRequiresRestart,
|
|
|
|
|
|
} = useSettingsMetadata();
|
2025-10-16 11:40:02 +08:00
|
|
|
|
|
2025-10-17 18:12:03 +08:00
|
|
|
|
// 重置设置
|
2025-10-16 11:40:02 +08:00
|
|
|
|
const resetSettings = useCallback(() => {
|
2025-10-17 18:12:03 +08:00
|
|
|
|
resetForm(data ?? null);
|
|
|
|
|
|
syncLanguage(initialLanguage);
|
|
|
|
|
|
resetAllDirectories(
|
|
|
|
|
|
sanitizeDir(data?.claudeConfigDir),
|
|
|
|
|
|
sanitizeDir(data?.codexConfigDir),
|
2025-10-16 11:40:02 +08:00
|
|
|
|
);
|
|
|
|
|
|
setRequiresRestart(false);
|
2025-10-17 18:12:03 +08:00
|
|
|
|
}, [
|
|
|
|
|
|
data,
|
|
|
|
|
|
initialLanguage,
|
|
|
|
|
|
resetForm,
|
|
|
|
|
|
syncLanguage,
|
|
|
|
|
|
resetAllDirectories,
|
|
|
|
|
|
setRequiresRestart,
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
// 保存设置
|
2025-10-16 11:40:02 +08:00
|
|
|
|
const saveSettings = useCallback(async (): Promise<SaveResult | null> => {
|
2025-10-17 18:12:03 +08:00
|
|
|
|
if (!settings) return null;
|
2025-10-16 11:40:02 +08:00
|
|
|
|
try {
|
|
|
|
|
|
const sanitizedAppDir = sanitizeDir(appConfigDir);
|
2025-10-17 18:12:03 +08:00
|
|
|
|
const sanitizedClaudeDir = sanitizeDir(settings.claudeConfigDir);
|
|
|
|
|
|
const sanitizedCodexDir = sanitizeDir(settings.codexConfigDir);
|
|
|
|
|
|
const previousAppDir = initialAppConfigDir;
|
|
|
|
|
|
|
2025-10-16 11:40:02 +08:00
|
|
|
|
const payload: Settings = {
|
2025-10-17 18:12:03 +08:00
|
|
|
|
...settings,
|
2025-10-16 11:40:02 +08:00
|
|
|
|
claudeConfigDir: sanitizedClaudeDir,
|
|
|
|
|
|
codexConfigDir: sanitizedCodexDir,
|
2025-10-17 18:12:03 +08:00
|
|
|
|
language: settings.language,
|
2025-10-16 11:40:02 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
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) {
|
2025-10-16 12:13:51 +08:00
|
|
|
|
console.warn(
|
|
|
|
|
|
"[useSettings] Failed to sync Claude plugin config",
|
|
|
|
|
|
error,
|
|
|
|
|
|
);
|
2025-10-16 11:40:02 +08:00
|
|
|
|
toast.error(
|
|
|
|
|
|
t("notifications.syncClaudePluginFailed", {
|
|
|
|
|
|
defaultValue: "同步 Claude 插件失败",
|
|
|
|
|
|
}),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (typeof window !== "undefined") {
|
|
|
|
|
|
window.localStorage.setItem("language", payload.language as Language);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
2025-10-16 12:13:51 +08:00
|
|
|
|
console.warn(
|
|
|
|
|
|
"[useSettings] Failed to persist language preference",
|
|
|
|
|
|
error,
|
|
|
|
|
|
);
|
2025-10-16 11:40:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const appDirChanged = sanitizedAppDir !== (previousAppDir ?? undefined);
|
|
|
|
|
|
setRequiresRestart(appDirChanged);
|
|
|
|
|
|
|
|
|
|
|
|
return { requiresRestart: appDirChanged };
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("[useSettings] Failed to save settings", error);
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
}
|
2025-10-17 18:12:03 +08:00
|
|
|
|
}, [
|
|
|
|
|
|
appConfigDir,
|
|
|
|
|
|
initialAppConfigDir,
|
|
|
|
|
|
saveMutation,
|
|
|
|
|
|
settings,
|
|
|
|
|
|
setRequiresRestart,
|
|
|
|
|
|
t,
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
const isLoading = useMemo(
|
|
|
|
|
|
() => isFormLoading || isDirectoryLoading || isMetadataLoading,
|
|
|
|
|
|
[isFormLoading, isDirectoryLoading, isMetadataLoading],
|
2025-10-16 11:40:02 +08:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
2025-10-17 18:12:03 +08:00
|
|
|
|
settings,
|
|
|
|
|
|
isLoading,
|
2025-10-16 11:40:02 +08:00
|
|
|
|
isSaving: saveMutation.isPending,
|
|
|
|
|
|
isPortable,
|
|
|
|
|
|
appConfigDir,
|
|
|
|
|
|
resolvedDirs,
|
|
|
|
|
|
requiresRestart,
|
|
|
|
|
|
updateSettings,
|
|
|
|
|
|
updateDirectory,
|
|
|
|
|
|
updateAppConfigDir,
|
|
|
|
|
|
browseDirectory,
|
|
|
|
|
|
browseAppConfigDir,
|
|
|
|
|
|
resetDirectory,
|
|
|
|
|
|
resetAppConfigDir,
|
|
|
|
|
|
saveSettings,
|
|
|
|
|
|
resetSettings,
|
|
|
|
|
|
acknowledgeRestart,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|