From fa69a9cb896fb4c3feb455535e63fb61de06075c Mon Sep 17 00:00:00 2001 From: tianma-if Date: Thu, 23 Jul 2026 22:11:34 +0800 Subject: [PATCH] feat: show release updates in system info --- apps/web/src/components/NotebookPane.tsx | 2 +- .../components/settings/SystemInfoCard.tsx | 47 ++++++++++++++- apps/web/src/lib/version-check.ts | 59 +++++++++++++++++++ package.json | 2 +- packages/shared/src/i18n/en-US.ts | 4 ++ packages/shared/src/i18n/zh-CN.ts | 4 ++ 6 files changed, 114 insertions(+), 4 deletions(-) create mode 100644 apps/web/src/lib/version-check.ts diff --git a/apps/web/src/components/NotebookPane.tsx b/apps/web/src/components/NotebookPane.tsx index f25c9095..d9465022 100644 --- a/apps/web/src/components/NotebookPane.tsx +++ b/apps/web/src/components/NotebookPane.tsx @@ -422,7 +422,7 @@ export const NotebookPane = ({ /> {demoMode && onResetDemo && ( } + icon={} label={isResettingDemo ? t("demo.resetting") : t("demo.resetButton")} onClick={onResetDemo} /> diff --git a/apps/web/src/components/settings/SystemInfoCard.tsx b/apps/web/src/components/settings/SystemInfoCard.tsx index 74246142..8775f540 100644 --- a/apps/web/src/components/settings/SystemInfoCard.tsx +++ b/apps/web/src/components/settings/SystemInfoCard.tsx @@ -1,10 +1,11 @@ -import { useMemo, useState } from "react"; -import { ChevronDown, Copy, Info } from "lucide-react"; +import { useEffect, useMemo, useState } from "react"; +import { ArrowUpCircle, CheckCircle2, ChevronDown, Copy, ExternalLink, Info } from "lucide-react"; import { useTranslation } from "react-i18next"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { cn } from "@/lib/utils"; +import { fetchLatestRelease, isVersionOutdated, type LatestRelease } from "@/lib/version-check"; import { copyTextToClipboard } from "./settings-utils"; export type SystemInfoItem = { @@ -93,12 +94,31 @@ export const SystemInfoCard = () => { const { t, i18n } = useTranslation(); const [copied, setCopied] = useState(false); const [expanded, setExpanded] = useState(false); + const [latestRelease, setLatestRelease] = useState(null); + const [releaseCheck, setReleaseCheck] = useState<"idle" | "checking" | "latest" | "outdated" | "error">("idle"); const infoItems = useMemo( () => getWebSystemInfoItems(t, i18n.language), [i18n.language, t] ); + useEffect(() => { + if (!expanded || releaseCheck !== "idle") return; + + const controller = new AbortController(); + setReleaseCheck("checking"); + void fetchLatestRelease(controller.signal) + .then((release) => { + setLatestRelease(release); + setReleaseCheck(isVersionOutdated(__EDGEEVER_APP_VERSION__, release.tagName) ? "outdated" : "latest"); + }) + .catch(() => { + if (!controller.signal.aborted) setReleaseCheck("error"); + }); + + return () => controller.abort(); + }, [expanded]); + const handleCopy = async () => { const details = infoItems.map((item) => `${item.label}: ${item.value}`).join("\n"); @@ -146,6 +166,29 @@ export const SystemInfoCard = () => { {copied ? t("common.copied") : t("systemInfo.copy")} + {releaseCheck === "outdated" && latestRelease ? ( +
+ +
+
{t("systemInfo.updateAvailableTitle")}
+
{t("systemInfo.updateAvailableDescription", { version: latestRelease.tagName })}
+
+ + {t("systemInfo.viewRelease")} + + +
+ ) : releaseCheck === "latest" ? ( +
+ + {t("systemInfo.latestVersion")} +
+ ) : null}
{infoItems.map((item) => (
diff --git a/apps/web/src/lib/version-check.ts b/apps/web/src/lib/version-check.ts new file mode 100644 index 00000000..b044e397 --- /dev/null +++ b/apps/web/src/lib/version-check.ts @@ -0,0 +1,59 @@ +const LATEST_RELEASE_URL = "https://api.github.com/repos/tianma-if/edgeever/releases/latest"; +const CACHE_KEY = "edgeever:latest-release"; +const CACHE_TTL_MS = 60 * 60 * 1000; + +export type LatestRelease = { + tagName: string; + url: string; +}; + +const parseVersion = (value: string) => { + const match = value.match(/v?(\d+)\.(\d+)\.(\d+)/i); + return match ? match.slice(1).map(Number) : null; +}; + +export const isVersionOutdated = (currentVersion: string, latestVersion: string) => { + const current = parseVersion(currentVersion); + const latest = parseVersion(latestVersion); + if (!current || !latest) return false; + + for (let index = 0; index < 3; index += 1) { + if (current[index] !== latest[index]) return current[index] < latest[index]; + } + + return false; +}; + +const readCachedRelease = (): LatestRelease | null => { + try { + const cached = JSON.parse(window.sessionStorage.getItem(CACHE_KEY) ?? "null") as { + expiresAt?: number; + release?: LatestRelease; + } | null; + return cached?.expiresAt && cached.expiresAt > Date.now() && cached.release ? cached.release : null; + } catch { + return null; + } +}; + +export const fetchLatestRelease = async (signal?: AbortSignal): Promise => { + const cached = readCachedRelease(); + if (cached) return cached; + + const response = await fetch(LATEST_RELEASE_URL, { + headers: { Accept: "application/vnd.github+json" }, + signal, + }); + if (!response.ok) throw new Error(`Release lookup failed with ${response.status}`); + + const payload = (await response.json()) as { html_url?: string; tag_name?: string }; + if (!payload.tag_name || !payload.html_url) throw new Error("Release response is incomplete"); + + const release = { tagName: payload.tag_name, url: payload.html_url }; + try { + window.sessionStorage.setItem(CACHE_KEY, JSON.stringify({ expiresAt: Date.now() + CACHE_TTL_MS, release })); + } catch { + // Storage can be unavailable in restricted browsing modes. + } + return release; +}; diff --git a/package.json b/package.json index 3e0242db..5d84aaf6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "edgeever", - "version": "1.5.11", + "version": "1.5.12", "private": true, "type": "module", "workspaces": [ diff --git a/packages/shared/src/i18n/en-US.ts b/packages/shared/src/i18n/en-US.ts index 5c6bc979..1767661c 100644 --- a/packages/shared/src/i18n/en-US.ts +++ b/packages/shared/src/i18n/en-US.ts @@ -205,6 +205,10 @@ export const enUS = { browserMode: "Browser", unknown: "Unknown", copy: "Copy info", + updateAvailableTitle: "New version available", + updateAvailableDescription: "This version is out of date. The latest version is {{version}}.", + viewRelease: "View update", + latestVersion: "You are running the latest version", }, feedback: { title: "Feedback", diff --git a/packages/shared/src/i18n/zh-CN.ts b/packages/shared/src/i18n/zh-CN.ts index a50983df..b9a638f3 100644 --- a/packages/shared/src/i18n/zh-CN.ts +++ b/packages/shared/src/i18n/zh-CN.ts @@ -205,6 +205,10 @@ export const zhCN = { browserMode: "浏览器", unknown: "未知", copy: "复制信息", + updateAvailableTitle: "发现新版本", + updateAvailableDescription: "当前版本较旧,最新版本为 {{version}}。", + viewRelease: "查看更新", + latestVersion: "当前已是最新版本", }, feedback: { title: "意见反馈",