- Fix negative margin overflow in all dialog content areas - Standardize dialog structure with flex-col layout - Add consistent py-4 spacing to all content areas - Ensure proper spacing between header, content, and footer Affected components: - AddProviderDialog, EditProviderDialog - McpFormModal, McpPanel - UsageScriptModal - SettingsDialog All dialogs now follow unified layout pattern: - DialogContent: flex flex-col max-h-[90vh] - Content area: flex-1 overflow-y-auto px-6 py-4 - No negative margins that cause content overflow
206 lines
5.4 KiB
TypeScript
206 lines
5.4 KiB
TypeScript
import { useCallback, useMemo } from "react";
|
||
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";
|
||
import { useSettingsForm, type SettingsFormState } from "./useSettingsForm";
|
||
import {
|
||
useDirectorySettings,
|
||
type ResolvedDirectories,
|
||
} from "./useDirectorySettings";
|
||
import { useSettingsMetadata } from "./useSettingsMetadata";
|
||
|
||
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;
|
||
}
|
||
|
||
export type { SettingsFormState, ResolvedDirectories };
|
||
|
||
const sanitizeDir = (value?: string | null): string | undefined => {
|
||
if (!value) return undefined;
|
||
const trimmed = value.trim();
|
||
return trimmed.length > 0 ? trimmed : undefined;
|
||
};
|
||
|
||
/**
|
||
* useSettings - 组合层
|
||
* 负责:
|
||
* - 组合 useSettingsForm、useDirectorySettings、useSettingsMetadata
|
||
* - 保存设置逻辑
|
||
* - 重置设置逻辑
|
||
*/
|
||
export function useSettings(): UseSettingsResult {
|
||
const { t } = useTranslation();
|
||
const { data } = useSettingsQuery();
|
||
const saveMutation = useSaveSettingsMutation();
|
||
|
||
// 1️⃣ 表单状态管理
|
||
const {
|
||
settings,
|
||
isLoading: isFormLoading,
|
||
initialLanguage,
|
||
updateSettings,
|
||
resetSettings: resetForm,
|
||
syncLanguage,
|
||
} = useSettingsForm();
|
||
|
||
// 2️⃣ 目录管理
|
||
const {
|
||
appConfigDir,
|
||
resolvedDirs,
|
||
isLoading: isDirectoryLoading,
|
||
initialAppConfigDir,
|
||
updateDirectory,
|
||
updateAppConfigDir,
|
||
browseDirectory,
|
||
browseAppConfigDir,
|
||
resetDirectory,
|
||
resetAppConfigDir,
|
||
resetAllDirectories,
|
||
} = useDirectorySettings({
|
||
settings,
|
||
onUpdateSettings: updateSettings,
|
||
});
|
||
|
||
// 3️⃣ 元数据管理
|
||
const {
|
||
isPortable,
|
||
requiresRestart,
|
||
isLoading: isMetadataLoading,
|
||
acknowledgeRestart,
|
||
setRequiresRestart,
|
||
} = useSettingsMetadata();
|
||
|
||
// 重置设置
|
||
const resetSettings = useCallback(() => {
|
||
resetForm(data ?? null);
|
||
syncLanguage(initialLanguage);
|
||
resetAllDirectories(
|
||
sanitizeDir(data?.claudeConfigDir),
|
||
sanitizeDir(data?.codexConfigDir),
|
||
);
|
||
setRequiresRestart(false);
|
||
}, [
|
||
data,
|
||
initialLanguage,
|
||
resetForm,
|
||
syncLanguage,
|
||
resetAllDirectories,
|
||
setRequiresRestart,
|
||
]);
|
||
|
||
// 保存设置
|
||
const saveSettings = useCallback(async (): Promise<SaveResult | null> => {
|
||
if (!settings) return null;
|
||
try {
|
||
const sanitizedAppDir = sanitizeDir(appConfigDir);
|
||
const sanitizedClaudeDir = sanitizeDir(settings.claudeConfigDir);
|
||
const sanitizedCodexDir = sanitizeDir(settings.codexConfigDir);
|
||
const previousAppDir = initialAppConfigDir;
|
||
|
||
const payload: Settings = {
|
||
...settings,
|
||
claudeConfigDir: sanitizedClaudeDir,
|
||
codexConfigDir: sanitizedCodexDir,
|
||
language: settings.language,
|
||
};
|
||
|
||
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) {
|
||
console.warn(
|
||
"[useSettings] Failed to sync Claude plugin config",
|
||
error,
|
||
);
|
||
toast.error(
|
||
t("notifications.syncClaudePluginFailed", {
|
||
defaultValue: "同步 Claude 插件失败",
|
||
}),
|
||
);
|
||
}
|
||
|
||
try {
|
||
if (typeof window !== "undefined") {
|
||
window.localStorage.setItem("language", payload.language as Language);
|
||
}
|
||
} catch (error) {
|
||
console.warn(
|
||
"[useSettings] Failed to persist language preference",
|
||
error,
|
||
);
|
||
}
|
||
|
||
const appDirChanged = sanitizedAppDir !== (previousAppDir ?? undefined);
|
||
setRequiresRestart(appDirChanged);
|
||
|
||
return { requiresRestart: appDirChanged };
|
||
} catch (error) {
|
||
console.error("[useSettings] Failed to save settings", error);
|
||
throw error;
|
||
}
|
||
}, [
|
||
appConfigDir,
|
||
initialAppConfigDir,
|
||
saveMutation,
|
||
settings,
|
||
setRequiresRestart,
|
||
t,
|
||
]);
|
||
|
||
const isLoading = useMemo(
|
||
() => isFormLoading || isDirectoryLoading || isMetadataLoading,
|
||
[isFormLoading, isDirectoryLoading, isMetadataLoading],
|
||
);
|
||
|
||
return {
|
||
settings,
|
||
isLoading,
|
||
isSaving: saveMutation.isPending,
|
||
isPortable,
|
||
appConfigDir,
|
||
resolvedDirs,
|
||
requiresRestart,
|
||
updateSettings,
|
||
updateDirectory,
|
||
updateAppConfigDir,
|
||
browseDirectory,
|
||
browseAppConfigDir,
|
||
resetDirectory,
|
||
resetAppConfigDir,
|
||
saveSettings,
|
||
resetSettings,
|
||
acknowledgeRestart,
|
||
};
|
||
}
|