diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 6fde274..8ad1866 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -559,7 +559,7 @@ dependencies = [ [[package]] name = "cc-switch" -version = "3.1.1" +version = "3.1.2" dependencies = [ "dirs 5.0.1", "log", diff --git a/src/App.tsx b/src/App.tsx index fb3ceb8..901130e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,6 +7,7 @@ import EditProviderModal from "./components/EditProviderModal"; import { ConfirmDialog } from "./components/ConfirmDialog"; import { AppSwitcher } from "./components/AppSwitcher"; import SettingsModal from "./components/SettingsModal"; +import { UpdateBadge } from "./components/UpdateBadge"; import { Plus, Settings, Moon, Sun } from "lucide-react"; import { buttonStyles } from "./lib/styles"; import { useDarkMode } from "./hooks/useDarkMode"; @@ -220,13 +221,16 @@ function App() { > {isDarkMode ? : } - +
+ + setIsSettingsOpen(true)} /> +
diff --git a/src/components/ProviderForm.tsx b/src/components/ProviderForm.tsx index 2aa5e59..015581e 100644 --- a/src/components/ProviderForm.tsx +++ b/src/components/ProviderForm.tsx @@ -419,14 +419,12 @@ const ProviderForm: React.FC = ({ // 初始时从配置中同步 API Key(编辑模式) useEffect(() => { - if (initialData) { - const parsedKey = getApiKeyFromConfig( - JSON.stringify(initialData.settingsConfig), - ); - if (parsedKey) setApiKey(parsedKey); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + if (!initialData) return; + const parsedKey = getApiKeyFromConfig( + JSON.stringify(initialData.settingsConfig), + ); + if (parsedKey) setApiKey(parsedKey); + }, [initialData]); // 支持按下 ESC 关闭弹窗 useEffect(() => { diff --git a/src/components/SettingsModal.tsx b/src/components/SettingsModal.tsx index 2e7c746..95f993b 100644 --- a/src/components/SettingsModal.tsx +++ b/src/components/SettingsModal.tsx @@ -1,8 +1,9 @@ import { useState, useEffect } from "react"; -import { X, Info, RefreshCw, FolderOpen } from "lucide-react"; +import { X, Info, RefreshCw, FolderOpen, Download, ExternalLink } from "lucide-react"; import { getVersion } from "@tauri-apps/api/app"; import "../lib/tauri-api"; -import { runUpdateFlow } from "../lib/updater"; +import { relaunchApp } from "../lib/updater"; +import { useUpdate } from "../contexts/UpdateContext"; import type { Settings } from "../types"; interface SettingsModalProps { @@ -16,6 +17,8 @@ export default function SettingsModal({ onClose }: SettingsModalProps) { const [configPath, setConfigPath] = useState(""); const [version, setVersion] = useState(""); const [isCheckingUpdate, setIsCheckingUpdate] = useState(false); + const [isDownloading, setIsDownloading] = useState(false); + const { hasUpdate, updateInfo, updateHandle, checkUpdate, resetDismiss } = useUpdate(); useEffect(() => { loadSettings(); @@ -29,7 +32,8 @@ export default function SettingsModal({ onClose }: SettingsModalProps) { setVersion(appVersion); } catch (error) { console.error("获取版本信息失败:", error); - setVersion("3.1.1"); // 降级使用默认版本 + // 失败时不硬编码版本号,显示为未知 + setVersion("未知"); } }; @@ -65,15 +69,32 @@ export default function SettingsModal({ onClose }: SettingsModalProps) { }; const handleCheckUpdate = async () => { - setIsCheckingUpdate(true); - try { - // 优先使用 Tauri Updater 流程;失败时回退到打开 Releases 页面 - await runUpdateFlow({ timeout: 30000 }); - } catch (error) { - console.error("检查更新失败,回退到 Releases 页面:", error); - await window.api.checkForUpdates(); - } finally { - setIsCheckingUpdate(false); + if (hasUpdate && updateHandle) { + // 已检测到更新:直接复用 updateHandle 下载并安装,避免重复检查 + setIsDownloading(true); + try { + resetDismiss(); + await updateHandle.downloadAndInstall(); + await relaunchApp(); + } catch (error) { + console.error("更新失败:", error); + // 更新失败时回退到打开 Releases 页面 + await window.api.checkForUpdates(); + } finally { + setIsDownloading(false); + } + } else { + // 尚未检测到更新:先检查 + setIsCheckingUpdate(true); + try { + await checkUpdate(); + // 检查后若有更新,让用户再次点击执行 + } catch (error) { + console.error("检查更新失败,回退到 Releases 页面:", error); + await window.api.checkForUpdates(); + } finally { + setIsCheckingUpdate(false); + } } }; @@ -85,6 +106,23 @@ export default function SettingsModal({ onClose }: SettingsModalProps) { } }; + const handleOpenReleaseNotes = async () => { + try { + const targetVersion = updateInfo?.availableVersion || version; + // 如果未知或为空,回退到 releases 首页 + if (!targetVersion || targetVersion === "未知") { + 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("打开更新日志失败:", error); + } + }; + return (
@@ -168,24 +206,48 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {

- + + +
diff --git a/src/components/UpdateBadge.tsx b/src/components/UpdateBadge.tsx new file mode 100644 index 0000000..f217c48 --- /dev/null +++ b/src/components/UpdateBadge.tsx @@ -0,0 +1,59 @@ +import { X, Sparkles } from "lucide-react"; +import { useUpdate } from "../contexts/UpdateContext"; + +interface UpdateBadgeProps { + className?: string; + onClick?: () => void; // 点击徽标的回调(例如打开设置) +} + +export function UpdateBadge({ className = "", onClick }: UpdateBadgeProps) { + const { hasUpdate, updateInfo, isDismissed, dismissUpdate } = useUpdate(); + + // 如果没有更新或已关闭,不显示 + if (!hasUpdate || isDismissed || !updateInfo) { + return null; + } + + return ( +
{ + if (!onClick) return; + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + onClick(); + } + }} + > + + + 新版本 {updateInfo.availableVersion} + + +
+ ); +} diff --git a/src/contexts/UpdateContext.tsx b/src/contexts/UpdateContext.tsx new file mode 100644 index 0000000..259cd49 --- /dev/null +++ b/src/contexts/UpdateContext.tsx @@ -0,0 +1,147 @@ +import React, { createContext, useContext, useState, useEffect, useCallback, useRef } from "react"; +import type { UpdateInfo, UpdateHandle } from "../lib/updater"; +import { checkForUpdate } from "../lib/updater"; + +interface UpdateContextValue { + // 更新状态 + hasUpdate: boolean; + updateInfo: UpdateInfo | null; + updateHandle: UpdateHandle | null; + isChecking: boolean; + error: string | null; + + // 提示状态 + isDismissed: boolean; + dismissUpdate: () => void; + + // 操作方法 + checkUpdate: () => Promise; + resetDismiss: () => void; +} + +const UpdateContext = createContext(undefined); + +export function UpdateProvider({ children }: { children: React.ReactNode }) { + const DISMISSED_VERSION_KEY = "ccswitch:update:dismissedVersion"; + const LEGACY_DISMISSED_KEY = "dismissedUpdateVersion"; // 兼容旧键 + + const [hasUpdate, setHasUpdate] = useState(false); + const [updateInfo, setUpdateInfo] = useState(null); + const [updateHandle, setUpdateHandle] = useState(null); + const [isChecking, setIsChecking] = useState(false); + const [error, setError] = useState(null); + const [isDismissed, setIsDismissed] = useState(false); + + // 从 localStorage 读取已关闭的版本 + useEffect(() => { + const current = updateInfo?.availableVersion; + if (!current) return; + + // 读取新键;若不存在,尝试迁移旧键 + let dismissedVersion = localStorage.getItem(DISMISSED_VERSION_KEY); + if (!dismissedVersion) { + const legacy = localStorage.getItem(LEGACY_DISMISSED_KEY); + if (legacy) { + localStorage.setItem(DISMISSED_VERSION_KEY, legacy); + localStorage.removeItem(LEGACY_DISMISSED_KEY); + dismissedVersion = legacy; + } + } + + setIsDismissed(dismissedVersion === current); + }, [updateInfo?.availableVersion]); + + const isCheckingRef = useRef(false); + + const checkUpdate = useCallback(async () => { + if (isCheckingRef.current) return; + isCheckingRef.current = true; + setIsChecking(true); + setError(null); + + try { + const result = await checkForUpdate({ timeout: 30000 }); + + if (result.status === "available") { + setHasUpdate(true); + setUpdateInfo(result.info); + setUpdateHandle(result.update); + + // 检查是否已经关闭过这个版本的提醒 + let dismissedVersion = localStorage.getItem(DISMISSED_VERSION_KEY); + if (!dismissedVersion) { + const legacy = localStorage.getItem(LEGACY_DISMISSED_KEY); + if (legacy) { + localStorage.setItem(DISMISSED_VERSION_KEY, legacy); + localStorage.removeItem(LEGACY_DISMISSED_KEY); + dismissedVersion = legacy; + } + } + setIsDismissed(dismissedVersion === result.info.availableVersion); + } else { + setHasUpdate(false); + setUpdateInfo(null); + setUpdateHandle(null); + setIsDismissed(false); + } + } catch (err) { + console.error("检查更新失败:", err); + setError(err instanceof Error ? err.message : "检查更新失败"); + setHasUpdate(false); + } finally { + setIsChecking(false); + isCheckingRef.current = false; + } + }, []); + + const dismissUpdate = useCallback(() => { + setIsDismissed(true); + if (updateInfo?.availableVersion) { + localStorage.setItem(DISMISSED_VERSION_KEY, updateInfo.availableVersion); + // 清理旧键 + localStorage.removeItem(LEGACY_DISMISSED_KEY); + } + }, [updateInfo?.availableVersion]); + + const resetDismiss = useCallback(() => { + setIsDismissed(false); + localStorage.removeItem(DISMISSED_VERSION_KEY); + localStorage.removeItem(LEGACY_DISMISSED_KEY); + }, []); + + // 应用启动时自动检查更新 + useEffect(() => { + // 延迟1秒后检查,避免影响启动体验 + const timer = setTimeout(() => { + checkUpdate().catch(console.error); + }, 1000); + + return () => clearTimeout(timer); + }, [checkUpdate]); + + const value: UpdateContextValue = { + hasUpdate, + updateInfo, + updateHandle, + isChecking, + error, + isDismissed, + dismissUpdate, + checkUpdate, + resetDismiss, + }; + + return ( + + {children} + + ); +} + +export function useUpdate() { + const context = useContext(UpdateContext); + if (!context) { + throw new Error("useUpdate must be used within UpdateProvider"); + } + return context; +} diff --git a/src/lib/updater.ts b/src/lib/updater.ts index 017406b..2926cb1 100644 --- a/src/lib/updater.ts +++ b/src/lib/updater.ts @@ -122,25 +122,5 @@ export async function relaunchApp(): Promise { await relaunch(); } -export async function runUpdateFlow( - opts: CheckOptions = {}, -): Promise<{ status: "up-to-date" | "done" }> { - const result = await checkForUpdate(opts); - if (result.status === "up-to-date") return result; - - let downloaded = 0; - let total = 0; - await result.update.downloadAndInstall((e) => { - if (e.event === "Started") { - total = e.total ?? 0; - downloaded = 0; - } else if (e.event === "Progress") { - downloaded += e.downloaded ?? 0; - // 调用方可监听此处并更新 UI(目前设置页仅显示加载态) - console.debug("update progress", { downloaded, total }); - } - }); - - await relaunchApp(); - return { status: "done" }; -} +// 旧的聚合更新流程已由调用方直接使用 updateHandle 取代 +// 如需单函数封装,可在需要时基于 checkForUpdate + updateHandle 复合调用 diff --git a/src/main.tsx b/src/main.tsx index caefd86..5d77b49 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,6 +1,7 @@ import React from "react"; import ReactDOM from "react-dom/client"; import App from "./App"; +import { UpdateProvider } from "./contexts/UpdateContext"; import "./index.css"; // 导入 Tauri API(自动绑定到 window.api) import "./lib/tauri-api"; @@ -19,6 +20,8 @@ try { ReactDOM.createRoot(document.getElementById("root")!).render( - + + + , );