import { useEffect, useState, type ReactNode } from "react"; import { createPortal } from "react-dom"; import { invoke } from "@tauri-apps/api/core"; import { listen } from "@tauri-apps/api/event"; import { useTranslation, type Language } from "../i18n"; import { useTheme, type Theme } from "../hooks/useTheme"; import type { UpdateInfo } from "../hooks/useUpdateCheck"; import { useKimiCodeVersion, isNewerVersion, type KimiCodeStatus, } from "../hooks/useKimiCodeVersion"; function formatVersion(version: string): string { const v = version.trim(); return v.startsWith("v") || v.startsWith("V") ? v : `v${v}`; } /** 上游 kimi-code releases 页面。 */ const KIMI_CODE_RELEASES_URL = "https://github.com/MoonshotAI/kimi-code/releases"; interface SettingsModalProps { open: boolean; onClose: () => void; updateInfo: UpdateInfo | null; checking: boolean; onCheckUpdate: () => void; lastChecked: number | null; } export function SettingsModal({ open, onClose, updateInfo, checking, onCheckUpdate, lastChecked, }: SettingsModalProps) { const { t, lang, setLang } = useTranslation(); const { theme, setTheme } = useTheme(); const { info: kimiCodeInfo, checking: kimiCodeChecking, checkNow: checkKimiCode, } = useKimiCodeVersion(); const [appVersion, setAppVersion] = useState(""); const [downloadProgress, setDownloadProgress] = useState(null); const [downloadedPath, setDownloadedPath] = useState(null); const [downloadError, setDownloadError] = useState(null); useEffect(() => { if (open) { invoke("get_app_version").then(setAppVersion).catch(() => {}); } }, [open]); // Listen to download progress events useEffect(() => { const unlisteners: (() => void)[] = []; const setupProgress = async () => { const un1 = await listen<{ downloaded: number; total: number; progress: number }>( "download-progress", (event) => { setDownloadProgress(event.payload.progress); } ); unlisteners.push(() => un1()); const un2 = await listen<{ path: string }>( "download-complete", (event) => { setDownloadedPath(event.payload.path); setDownloadProgress(100); } ); unlisteners.push(() => un2()); }; setupProgress(); return () => unlisteners.forEach((fn) => fn()); }, []); useEffect(() => { if (!open) return; const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [open, onClose]); useEffect(() => { if (!open) return; const prev = document.body.style.overflow; document.body.style.overflow = "hidden"; return () => { document.body.style.overflow = prev; }; }, [open]); const handleDownload = async () => { if (!updateInfo?.downloadUrl) return; setDownloadProgress(0); setDownloadError(null); setDownloadedPath(null); try { const path = await invoke("download_update", { url: updateInfo.downloadUrl, }); setDownloadedPath(path); setDownloadProgress(100); } catch (e) { setDownloadError(e instanceof Error ? e.message : String(e)); setDownloadProgress(null); } }; const handleInstall = () => { if (!downloadedPath) return; invoke("open_installer", { path: downloadedPath }).catch(() => {}); }; // Open an external link via the Rust-side opener command (bypasses the JS // plugin scope); fall back to a new tab when even that fails. const openExternal = (url: string) => { invoke("open_external_url", { url }).catch(() => { const w = window.open(url, "_blank"); if (!w) console.error(`failed to open ${url}`); }); }; if (!open) return null; const themeOptions: { value: Theme; icon: ReactNode; label: string }[] = [ { value: "light", icon: ( ), label: t("themeLight"), }, { value: "dark", icon: ( ), label: t("themeDark"), }, { value: "auto", icon: ( ), label: t("themeAuto"), }, ]; const fmtLastChecked = (ts: number | null) => { if (!ts) return "—"; const d = new Date(ts); return d.toLocaleString(lang === "zh" ? "zh-CN" : "en-US"); }; // 状态徽标:绿=已适配 / 黄=建议升级 KimiSwitch / 橙=部分兼容 / 灰=未知或未安装 const kimiCodeBadge = (status: KimiCodeStatus) => { switch (status) { case "current": return { label: t("kimiCodeStatusCurrent"), className: "bg-green-100 dark:bg-green-900/30 text-green-600 dark:text-green-400 border-green-300 dark:border-green-500/30", }; case "outdated": return { label: t("kimiCodeStatusOutdated"), className: "bg-yellow-100 dark:bg-yellow-900/30 text-yellow-700 dark:text-yellow-400 border-yellow-300 dark:border-yellow-500/30", }; case "partial": return { label: t("kimiCodeStatusPartial"), className: "bg-orange-100 dark:bg-orange-900/30 text-orange-600 dark:text-orange-400 border-orange-300 dark:border-orange-500/30", }; case "unknown": return { label: t("kimiCodeStatusUnknown"), className: "bg-hover-2 text-content-muted border-border", }; default: return { label: t("kimiCodeStatusNotInstalled"), className: "bg-hover-2 text-content-muted border-border", }; } }; const kimiCodeUpdateAvailable = !!kimiCodeInfo?.installed && !!kimiCodeInfo?.latest && isNewerVersion(kimiCodeInfo.latest, kimiCodeInfo.installed); const kimiCodeBadgeInfo = kimiCodeInfo ? kimiCodeBadge(kimiCodeInfo.status) : null; return createPortal(
e.stopPropagation()} > {/* Header */}

{t("settingsTitle")}

{/* Appearance */}

{t("settingsAppearance")}

{/* Theme */}
{t("themeLight")} / {t("themeDark")}
{themeOptions.map((opt) => ( ))}
{/* Language */}
{t("language")}
{/* Updates */}

{t("settingsUpdates")}

{t("currentVersion")} v{appVersion || "—"}
{t("latestVersion")} {updateInfo ? formatVersion(updateInfo.latest) : "—"}
{t("lastChecked")} {fmtLastChecked(lastChecked)}
{updateInfo?.updateAvailable && downloadProgress === null && !downloadedPath && ( )} {downloadedPath && ( )}
{/* Download progress bar */} {downloadProgress !== null && downloadProgress < 100 && (
{t("downloading")} {downloadProgress}%
)} {downloadedPath && (
✓ {t("downloadComplete")}
)} {downloadError && (
{t("updateCheckFailed")}: {downloadError}
)} {updateInfo?.updateAvailable && (
✦ {t("updateAvailable")}: {formatVersion(updateInfo.latest)}
)} {updateInfo && !updateInfo.updateAvailable && (
✓ {t("upToDate")}
)}
{/* Kimi Code CLI version & compatibility */}

{t("kimiCodeSection")}

{t("kimiCodeInstalled")} {kimiCodeInfo?.installed ? formatVersion(kimiCodeInfo.installed) : "—"}
{t("kimiCodeLatest")} {kimiCodeInfo?.latest ? formatVersion(kimiCodeInfo.latest) : "—"}
{t("kimiCodeStatusLabel")} {kimiCodeBadgeInfo ? ( {kimiCodeBadgeInfo.label} ) : ( — )}
{kimiCodeUpdateAvailable && ( )}
{/* About */}

{t("settingsAbout")}

Kimi Switch
{t("appDescription")}
v{appVersion} · MIT License
{t("settingsWebsite")}:{" "}
{t("kimiEyesSite")}:{" "}
{t("attributionPorted")}{" "} {t("attributionSuffix")}
{t("modelDataSourcePrefix")}{" "}
{t("referenceProject")}: (MIT, © Jason Young)
, document.body ); }