feat: show release updates in system info
This commit is contained in:
1 parent
7f830403bb
commit
fa69a9cb89
6 files changed
+114
-4
No files matched your search
@@ -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">
|
||||
|
||||
@@ -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
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "edgeever",
|
||||
"version": "1.5.11",
|
||||
"version": "1.5.12",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"workspaces": [
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -205,6 +205,10 @@ export const zhCN = {
|
||||
browserMode: "浏览器",
|
||||
unknown: "未知",
|
||||
copy: "复制信息",
|
||||
updateAvailableTitle: "发现新版本",
|
||||
updateAvailableDescription: "当前版本较旧,最新版本为 {{version}}。",
|
||||
viewRelease: "查看更新",
|
||||
latestVersion: "当前已是最新版本",
|
||||
},
|
||||
feedback: {
|
||||
title: "意见反馈",
|
||||
|
||||
Reference in new issue
Block a user