feat: show release updates in system info

This commit is contained in:
tianma-if committed 2026-07-23 22:11:34 +08:00
1 parent 7f830403bb
commit fa69a9cb89
6 files changed
+114 -4

No files matched your search

+1 -1
View File
@@ -422,7 +422,7 @@ export const NotebookPane = ({
/>
{demoMode && onResetDemo && (
<SidebarNavButton
icon={<RotateCcw className={cn("h-4 w-4 text-amber-600", isResettingDemo && "animate-spin")} />}
icon={<RotateCcw className={cn("h-4 w-4 text-slate-500", isResettingDemo && "animate-spin text-emerald-600")} />}
label={isResettingDemo ? t("demo.resetting") : t("demo.resetButton")}
onClick={onResetDemo}
/>
@@ -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<LatestRelease | null>(null);
const [releaseCheck, setReleaseCheck] = useState<"idle" | "checking" | "latest" | "outdated" | "error">("idle");
const infoItems = useMemo<SystemInfoItem[]>(
() => 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")}
</Button>
</div>
{releaseCheck === "outdated" && latestRelease ? (
<div className="flex items-start gap-2 rounded-md border border-emerald-200 border-l-2 border-l-emerald-500 bg-emerald-50/40 px-3 py-2 text-slate-800" role="status">
<ArrowUpCircle className="mt-0.5 h-4 w-4 shrink-0 text-emerald-600" />
<div className="min-w-0 flex-1 text-xs leading-5">
<div className="font-semibold">{t("systemInfo.updateAvailableTitle")}</div>
<div className="text-slate-500">{t("systemInfo.updateAvailableDescription", { version: latestRelease.tagName })}</div>
</div>
<a
className="inline-flex shrink-0 items-center gap-1 text-xs font-semibold text-emerald-700 underline underline-offset-2 hover:text-emerald-900"
href={latestRelease.url}
target="_blank"
rel="noreferrer"
>
{t("systemInfo.viewRelease")}
<ExternalLink className="h-3 w-3" />
</a>
</div>
) : releaseCheck === "latest" ? (
<div className="flex items-center gap-2 text-xs text-emerald-700" role="status">
<CheckCircle2 className="h-4 w-4" />
{t("systemInfo.latestVersion")}
</div>
) : null}
<dl className="grid border-y border-slate-200 sm:grid-cols-3">
{infoItems.map((item) => (
<div key={item.label} className="min-w-0 border-b border-slate-200 px-2 py-2.5 sm:px-3">
+59
View File
@@ -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<LatestRelease> => {
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;
};
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "edgeever",
"version": "1.5.11",
"version": "1.5.12",
"private": true,
"type": "module",
"workspaces": [
+4
View File
@@ -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",
+4
View File
@@ -205,6 +205,10 @@ export const zhCN = {
browserMode: "浏览器",
unknown: "未知",
copy: "复制信息",
updateAvailableTitle: "发现新版本",
updateAvailableDescription: "当前版本较旧,最新版本为 {{version}}。",
viewRelease: "查看更新",
latestVersion: "当前已是最新版本",
},
feedback: {
title: "意见反馈",