import { useState, useEffect } from "react"; import { useTranslation } from "react-i18next"; import { X, RefreshCw, FolderOpen, Download, ExternalLink, Check, Undo2, FolderSearch, Save, } from "lucide-react"; import { getVersion } from "@tauri-apps/api/app"; import { ImportProgressModal } from "./ImportProgressModal"; import { homeDir, join } from "@tauri-apps/api/path"; import "../lib/tauri-api"; import { relaunchApp } from "../lib/updater"; import { useUpdate } from "../contexts/UpdateContext"; import type { Settings } from "../types"; import type { AppType } from "../lib/tauri-api"; import { isLinux } from "../lib/platform"; interface SettingsModalProps { onClose: () => void; onImportSuccess?: () => void | Promise; onNotify?: ( message: string, type: "success" | "error", duration?: number, ) => void; } export default function SettingsModal({ onClose, onImportSuccess, onNotify, }: SettingsModalProps) { const { t, i18n } = useTranslation(); const normalizeLanguage = (lang?: string | null): "zh" | "en" => lang === "en" ? "en" : "zh"; const readPersistedLanguage = (): "zh" | "en" => { if (typeof window !== "undefined") { const stored = window.localStorage.getItem("language"); if (stored === "en" || stored === "zh") { return stored; } } return normalizeLanguage(i18n.language); }; const persistedLanguage = readPersistedLanguage(); const [settings, setSettings] = useState({ showInTray: true, minimizeToTrayOnClose: true, enableClaudePluginIntegration: false, claudeConfigDir: undefined, codexConfigDir: undefined, language: persistedLanguage, }); const [initialLanguage, setInitialLanguage] = useState<"zh" | "en">( persistedLanguage, ); const [configPath, setConfigPath] = useState(""); const [version, setVersion] = useState(""); const [isCheckingUpdate, setIsCheckingUpdate] = useState(false); const [isDownloading, setIsDownloading] = useState(false); const [showUpToDate, setShowUpToDate] = useState(false); const [resolvedClaudeDir, setResolvedClaudeDir] = useState(""); const [resolvedCodexDir, setResolvedCodexDir] = useState(""); const [isPortable, setIsPortable] = useState(false); const { hasUpdate, updateInfo, updateHandle, checkUpdate, resetDismiss } = useUpdate(); // 导入/导出相关状态 const [isImporting, setIsImporting] = useState(false); const [importStatus, setImportStatus] = useState< "idle" | "importing" | "success" | "error" >("idle"); const [importError, setImportError] = useState(""); const [importBackupId, setImportBackupId] = useState(""); const [selectedImportFile, setSelectedImportFile] = useState(""); useEffect(() => { loadSettings(); loadConfigPath(); loadVersion(); loadResolvedDirs(); loadPortableFlag(); }, []); const loadVersion = async () => { try { const appVersion = await getVersion(); setVersion(appVersion); } catch (error) { console.error(t("console.getVersionFailed"), error); // 失败时不硬编码版本号,显示为未知 setVersion(t("common.unknown")); } }; const loadSettings = async () => { try { const loadedSettings = await window.api.getSettings(); const showInTray = (loadedSettings as any)?.showInTray ?? (loadedSettings as any)?.showInDock ?? true; const minimizeToTrayOnClose = (loadedSettings as any)?.minimizeToTrayOnClose ?? (loadedSettings as any)?.minimize_to_tray_on_close ?? true; const storedLanguage = normalizeLanguage( typeof (loadedSettings as any)?.language === "string" ? (loadedSettings as any).language : persistedLanguage, ); setSettings({ showInTray, minimizeToTrayOnClose, enableClaudePluginIntegration: typeof (loadedSettings as any)?.enableClaudePluginIntegration === "boolean" ? (loadedSettings as any).enableClaudePluginIntegration : false, claudeConfigDir: typeof (loadedSettings as any)?.claudeConfigDir === "string" ? (loadedSettings as any).claudeConfigDir : undefined, codexConfigDir: typeof (loadedSettings as any)?.codexConfigDir === "string" ? (loadedSettings as any).codexConfigDir : undefined, language: storedLanguage, }); setInitialLanguage(storedLanguage); if (i18n.language !== storedLanguage) { void i18n.changeLanguage(storedLanguage); } } catch (error) { console.error(t("console.loadSettingsFailed"), error); } }; const loadConfigPath = async () => { try { const path = await window.api.getAppConfigPath(); if (path) { setConfigPath(path); } } catch (error) { console.error(t("console.getConfigPathFailed"), error); } }; const loadResolvedDirs = async () => { try { const [claudeDir, codexDir] = await Promise.all([ window.api.getConfigDir("claude"), window.api.getConfigDir("codex"), ]); setResolvedClaudeDir(claudeDir || ""); setResolvedCodexDir(codexDir || ""); } catch (error) { console.error(t("console.getConfigDirFailed"), error); } }; const loadPortableFlag = async () => { try { const portable = await window.api.isPortable(); setIsPortable(portable); } catch (error) { console.error(t("console.detectPortableFailed"), error); } }; const saveSettings = async () => { try { const selectedLanguage = settings.language === "en" ? "en" : "zh"; const payload: Settings = { ...settings, claudeConfigDir: settings.claudeConfigDir && settings.claudeConfigDir.trim() !== "" ? settings.claudeConfigDir.trim() : undefined, codexConfigDir: settings.codexConfigDir && settings.codexConfigDir.trim() !== "" ? settings.codexConfigDir.trim() : undefined, language: selectedLanguage, }; await window.api.saveSettings(payload); // 立即生效:根据开关无条件写入/移除 ~/.claude/config.json try { if (payload.enableClaudePluginIntegration) { await window.api.applyClaudePluginConfig({ official: false }); } else { await window.api.applyClaudePluginConfig({ official: true }); } } catch (e) { console.warn("[Settings] Apply Claude plugin config on save failed", e); } setSettings(payload); try { window.localStorage.setItem("language", selectedLanguage); } catch (error) { console.warn("[Settings] Failed to persist language preference", error); } setInitialLanguage(selectedLanguage); if (i18n.language !== selectedLanguage) { void i18n.changeLanguage(selectedLanguage); } onClose(); } catch (error) { console.error(t("console.saveSettingsFailed"), error); } }; const handleLanguageChange = (lang: "zh" | "en") => { setSettings((prev) => ({ ...prev, language: lang })); if (i18n.language !== lang) { void i18n.changeLanguage(lang); } }; const handleCancel = () => { if (settings.language !== initialLanguage) { setSettings((prev) => ({ ...prev, language: initialLanguage })); if (i18n.language !== initialLanguage) { void i18n.changeLanguage(initialLanguage); } } onClose(); }; const handleCheckUpdate = async () => { if (hasUpdate && updateHandle) { if (isPortable) { await window.api.checkForUpdates(); return; } // 已检测到更新:直接复用 updateHandle 下载并安装,避免重复检查 setIsDownloading(true); try { resetDismiss(); await updateHandle.downloadAndInstall(); await relaunchApp(); } catch (error) { console.error(t("console.updateFailed"), error); // 更新失败时回退到打开 Releases 页面 await window.api.checkForUpdates(); } finally { setIsDownloading(false); } } else { // 尚未检测到更新:先检查 setIsCheckingUpdate(true); setShowUpToDate(false); try { const hasNewUpdate = await checkUpdate(); // 检查完成后,如果没有更新,显示"已是最新" if (!hasNewUpdate) { setShowUpToDate(true); // 3秒后恢复按钮文字 setTimeout(() => { setShowUpToDate(false); }, 3000); } } catch (error) { console.error(t("console.checkUpdateFailed"), error); // 在开发模式下,模拟已是最新版本的响应 if (import.meta.env.DEV) { setShowUpToDate(true); setTimeout(() => { setShowUpToDate(false); }, 3000); } else { // 生产环境下如果更新插件不可用,回退到打开 Releases 页面 await window.api.checkForUpdates(); } } finally { setIsCheckingUpdate(false); } } }; const handleOpenConfigFolder = async () => { try { await window.api.openAppConfigFolder(); } catch (error) { console.error(t("console.openConfigFolderFailed"), error); } }; const handleBrowseConfigDir = async (app: AppType) => { try { const currentResolved = app === "claude" ? (settings.claudeConfigDir ?? resolvedClaudeDir) : (settings.codexConfigDir ?? resolvedCodexDir); const selected = await window.api.selectConfigDirectory(currentResolved); if (!selected) { return; } const sanitized = selected.trim(); if (sanitized === "") { return; } if (app === "claude") { setSettings((prev) => ({ ...prev, claudeConfigDir: sanitized })); setResolvedClaudeDir(sanitized); } else { setSettings((prev) => ({ ...prev, codexConfigDir: sanitized })); setResolvedCodexDir(sanitized); } } catch (error) { console.error(t("console.selectConfigDirFailed"), error); } }; const computeDefaultConfigDir = async (app: AppType) => { try { const home = await homeDir(); const folder = app === "claude" ? ".claude" : ".codex"; return await join(home, folder); } catch (error) { console.error(t("console.getDefaultConfigDirFailed"), error); return ""; } }; const handleResetConfigDir = async (app: AppType) => { setSettings((prev) => ({ ...prev, ...(app === "claude" ? { claudeConfigDir: undefined } : { codexConfigDir: undefined }), })); const defaultDir = await computeDefaultConfigDir(app); if (!defaultDir) { return; } if (app === "claude") { setResolvedClaudeDir(defaultDir); } else { setResolvedCodexDir(defaultDir); } }; const handleOpenReleaseNotes = async () => { try { const targetVersion = updateInfo?.availableVersion || version; const unknownLabel = t("common.unknown"); // 如果未知或为空,回退到 releases 首页 if (!targetVersion || targetVersion === unknownLabel) { await window.api.openExternal( "https://github.com/farion1231/cc-switch/releases", ); return; } const tag = targetVersion.startsWith("v") ? targetVersion : `v${targetVersion}`; await window.api.openExternal( `https://github.com/farion1231/cc-switch/releases/tag/${tag}`, ); } catch (error) { console.error(t("console.openReleaseNotesFailed"), error); } }; // 导出配置处理函数 const handleExportConfig = async () => { try { const defaultName = `cc-switch-config-${new Date().toISOString().split("T")[0]}.json`; const filePath = await window.api.saveFileDialog(defaultName); if (!filePath) { onNotify?.(`${t("settings.exportFailed")}: ${t("settings.selectFileFailed")}`, "error", 4000); return; } const result = await window.api.exportConfigToFile(filePath); if (result.success) { onNotify?.( `${t("settings.configExported")}\n${result.filePath}`, "success", 4000, ); } } catch (error) { console.error(t("settings.exportFailedError"), error); onNotify?.( `${t("settings.exportFailed")}: ${String(error)}`, "error", 5000, ); } }; // 选择要导入的文件 const handleSelectImportFile = async () => { try { const filePath = await window.api.openFileDialog(); if (filePath) { setSelectedImportFile(filePath); setImportStatus("idle"); // 重置状态 setImportError(""); } } catch (error) { console.error(t("settings.selectFileFailed") + ":", error); onNotify?.( `${t("settings.selectFileFailed")}: ${String(error)}`, "error", 5000, ); } }; // 执行导入 const handleExecuteImport = async () => { if (!selectedImportFile || isImporting) return; setIsImporting(true); setImportStatus("importing"); try { const result = await window.api.importConfigFromFile(selectedImportFile); if (result.success) { setImportBackupId(result.backupId || ""); setImportStatus("success"); // ImportProgressModal 会在2秒后触发数据刷新回调 } else { setImportError(result.message || t("settings.configCorrupted")); setImportStatus("error"); } } catch (error) { setImportError(String(error)); setImportStatus("error"); } finally { setIsImporting(false); } }; return (
{ if (e.target === e.currentTarget) handleCancel(); }} >
{/* 标题栏 */}

{t("settings.title")}

{/* 设置内容 */}
{/* 语言设置 */}

{t("settings.language")}

{/* 窗口行为设置 */}

{t("settings.windowBehavior")}

{/* Claude 插件联动开关 */}
{/* 配置文件位置 */}

{t("settings.configFileLocation")}

{configPath || t("common.loading")}
{/* 配置目录覆盖 */}

{t("settings.configDirectoryOverride")}

{t("settings.configDirectoryDescription")}

setSettings({ ...settings, claudeConfigDir: e.target.value, }) } placeholder={t("settings.browsePlaceholderClaude")} className="flex-1 px-3 py-2 text-xs font-mono bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/40" />
setSettings({ ...settings, codexConfigDir: e.target.value, }) } placeholder={t("settings.browsePlaceholderCodex")} className="flex-1 px-3 py-2 text-xs font-mono bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/40" />
{/* 导入导出 */}

{t("settings.importExport")}

{/* 导出按钮 */} {/* 导入区域 */}
{/* 显示选择的文件 */} {selectedImportFile && (
{selectedImportFile.split("/").pop() || selectedImportFile.split("\\").pop() || selectedImportFile}
)}
{/* 关于 */}

{t("common.about")}

CC Switch

{t("common.version")} {version}

{/* 底部按钮 */}
{/* Import Progress Modal */} {importStatus !== "idle" && ( { setImportStatus("idle"); setImportError(""); setSelectedImportFile(""); }} onSuccess={() => { if (onImportSuccess) { void onImportSuccess(); } void window.api .updateTrayMenu() .catch((error) => console.error( "[SettingsModal] Failed to refresh tray menu", error, ), ); }} /> )}
); }