返回 DeepSeek-Reasonix
UpdateBanner.tsx
根目录 / desktop / frontend / src / components / UpdateBanner.tsx
1 import { useEffect, useState } from "react";
2 import { useT } from "../lib/i18n";
3 import { useUpdater } from "../lib/useUpdater";
4
5 const MB = 1024 * 1024;
6 const UPDATE_REFRESH_INTERVAL_MS = 60 * 60 * 1000;
7 const mb = (n: number) => (n / MB).toFixed(1);
8
9 export function subscribeToUpdateRefresh(
10 refresh: () => void,
11 intervalMs = UPDATE_REFRESH_INTERVAL_MS,
12 ): () => void {
13 const refreshVisible = () => {
14 if (document.visibilityState === "visible") refresh();
15 };
16 const interval = window.setInterval(refreshVisible, intervalMs);
17 window.addEventListener("focus", refreshVisible);
18 document.addEventListener("visibilitychange", refreshVisible);
19 return () => {
20 window.clearInterval(interval);
21 window.removeEventListener("focus", refreshVisible);
22 document.removeEventListener("visibilitychange", refreshVisible);
23 };
24 }
25
26 // UpdateBanner checks on mount and while the app remains open and, when one is available,
27 // shows a dismissible top banner with a single "update and restart" action
28 // (or, on macOS manual builds, links out to the download page). It renders
29 // nothing while idle, checking, or already current. A failed check can be
30 // dismissed here; Settings is where a manual check shows errors inline.
31 export function UpdateBanner({
32 enabled = true,
33 onShowReleaseNotes,
34 }: {
35 enabled?: boolean;
36 onShowReleaseNotes?: (version: string) => void;
37 }) {
38 const t = useT();
39 const { status, check, refresh, apply, openDownload, abandonPending, reset } = useUpdater();
40 const [dismissed, setDismissed] = useState<string | null>(null);
41
42 useEffect(() => {
43 if (!enabled) return;
44 void refresh();
45 }, [enabled, refresh]);
46
47 useEffect(() => {
48 if (!enabled) return;
49 return subscribeToUpdateRefresh(() => {
50 void refresh();
51 });
52 }, [enabled, refresh]);
53
54 if (!enabled) return null;
55
56 switch (status.kind) {
57 case "available": {
58 const info = status.info;
59 if (info.latest === dismissed) return null;
60 return (
61 <div className="banner banner--update">
62 <span className="banner__msg">{t("updater.available", { v: info.latest })}</span>
63 {!info.canSelfUpdate && <span className="banner__hint">{info.manualReason || t("updater.macHint")}</span>}
64 <span className="banner__spacer" />
65 {onShowReleaseNotes && (
66 <button className="btn btn--small" onClick={() => onShowReleaseNotes(info.latest)}>
67 {t("updater.releaseNotes")}
68 </button>
69 )}
70 <button className="btn btn--small btn--primary" onClick={() => apply(info)}>
71 {info.canSelfUpdate ? t("updater.updateAndRestart") : t("updater.goToDownload")}
72 </button>
73 <button className="btn btn--small" onClick={() => setDismissed(info.latest)}>
74 {t("updater.dismiss")}
75 </button>
76 </div>
77 );
78 }
79 case "downloading": {
80 const pct = status.total > 0 ? Math.round((status.received / status.total) * 100) : 0;
81 return (
82 <div className="banner banner--update">
83 <span className="banner__msg">
84 {t("updater.downloading", { done: mb(status.received), total: mb(status.total), pct })}
85 </span>
86 <span className="banner__spacer" />
87 <progress className="banner__progress" value={status.received} max={status.total || undefined} />
88 </div>
89 );
90 }
91 case "verifying":
92 return <div className="banner banner--update">{t("updater.verifying")}</div>;
93 case "authorizing":
94 return <div className="banner banner--update">{t("updater.authorizing")}</div>;
95 case "installing":
96 return (
97 <div className="banner banner--update">
98 {status.info?.requiresElevation || status.info?.installMode === "deb"
99 ? t("updater.installingPackage")
100 : t("updater.installing")}
101 </div>
102 );
103 case "relaunching":
104 case "done":
105 return <div className="banner banner--update">{t("updater.done")}</div>;
106 case "error": {
107 const failedMessage = status.disposition === "recovery"
108 ? t("updater.recoveryBlocked")
109 : status.disposition === "manual"
110 ? t("updater.manualUpdateRequired")
111 : t("updater.failed", { msg: status.message });
112 const downloadFirst = status.disposition !== "retryable";
113 return (
114 <div className="banner banner--update banner--error banner--actionable">
115 <span className="banner__msg" title={failedMessage}>
116 {failedMessage}
117 </span>
118 <span className="banner__spacer" />
119 {status.disposition === "recovery" && (
120 <button
121 className="btn btn--small"
122 type="button"
123 onClick={() => void abandonPending()}
124 >
125 {t("updater.discardPrevious")}
126 </button>
127 )}
128 {downloadFirst && (
129 <button className="btn btn--small btn--primary" type="button" onClick={openDownload}>
130 {t("updater.officialDownload")}
131 </button>
132 )}
133 <button
134 className={`btn btn--small${downloadFirst ? "" : " btn--primary"}`}
135 type="button"
136 onClick={() => {
137 if (status.info) apply(status.info);
138 else void check();
139 }}
140 >
141 {t("updater.retry")}
142 </button>
143 <button className="btn btn--small" onClick={() => reset()}>
144 {t("updater.dismiss")}
145 </button>
146 </div>
147 );
148 }
149 default:
150 // idle | checking | upToDate — nothing to show.
151 return null;
152 }
153 }
154
154 lines Plain Text