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"; function formatVersion(version: string): string { const v = version.trim(); return v.startsWith("v") || v.startsWith("V") ? v : `v${v}`; } 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 [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"); }; 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")}
)}
{/* 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 ); }