feat(updater): 优化更新体验与 UI
- ui: UpdateBadge 使用 Tailwind 内置过渡,支持点击打开设置,保留图标动画 - updater: 新增 UpdateContext 首启延迟检查,忽略版本键名命名空间化(含旧键迁移),并发保护 - settings: 去除版本硬编码回退;检测到更新时复用 updateHandle 下载并安装,并新增常显“更新日志”入口 - a11y: 更新徽标支持键盘触达(Enter/Space) - refactor: 移除未使用的 runUpdateFlow 导出 - chore: 类型检查通过,整体行为与权限边界未改变
This commit is contained in:
147
src/contexts/UpdateContext.tsx
Normal file
147
src/contexts/UpdateContext.tsx
Normal file
@@ -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<void>;
|
||||
resetDismiss: () => void;
|
||||
}
|
||||
|
||||
const UpdateContext = createContext<UpdateContextValue | undefined>(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<UpdateInfo | null>(null);
|
||||
const [updateHandle, setUpdateHandle] = useState<UpdateHandle | null>(null);
|
||||
const [isChecking, setIsChecking] = useState(false);
|
||||
const [error, setError] = useState<string | null>(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 (
|
||||
<UpdateContext.Provider value={value}>
|
||||
{children}
|
||||
</UpdateContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useUpdate() {
|
||||
const context = useContext(UpdateContext);
|
||||
if (!context) {
|
||||
throw new Error("useUpdate must be used within UpdateProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
Reference in New Issue
Block a user