fix: make desktop updates reliably discoverable
This commit is contained in:
1 parent
0bb405aeb7
commit
6faffbc2f2
12 files changed
+234
-16
No files matched your search
@@ -2,14 +2,35 @@ import { readFileSync } from "node:fs";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
|
||||
const mainSource = readFileSync(new URL("./index.mjs", import.meta.url), "utf8");
|
||||
const preloadSource = readFileSync(new URL("../preload/index.cjs", import.meta.url), "utf8");
|
||||
const noticeSource = readFileSync(new URL("../../../web/src/components/DesktopUpdateNotice.tsx", import.meta.url), "utf8");
|
||||
const systemInfoSource = readFileSync(new URL("../../../web/src/components/settings/SystemInfoPanel.tsx", import.meta.url), "utf8");
|
||||
|
||||
describe("desktop update flow", () => {
|
||||
test("downloads updates in the background and relaunches after installation", () => {
|
||||
expect(mainSource).toContain("autoUpdater.autoDownload = true");
|
||||
expect(mainSource).toContain("autoUpdater.autoRunAppAfterInstall = true");
|
||||
expect(mainSource).toContain("isQuitting = true;\n autoUpdater.quitAndInstall(false, true)");
|
||||
expect(mainSource).toContain("result?.downloadPromise?.catch");
|
||||
expect(mainSource).toContain("result?.downloadPromise");
|
||||
});
|
||||
|
||||
test("offers a manual update check in desktop system settings", () => {
|
||||
expect(mainSource).toContain('ipcMain.handle("desktop:check-update"');
|
||||
expect(mainSource).toContain('checkForDesktopUpdate("manual", { force: true, throwOnError: true })');
|
||||
expect(preloadSource).toContain('checkUpdate: () => ipcRenderer.invoke("desktop:check-update")');
|
||||
expect(systemInfoSource).toContain('t("systemInfo.desktopCheckForUpdates")');
|
||||
expect(systemInfoSource).toContain("desktopBridge!.checkUpdate()");
|
||||
expect(systemInfoSource).toContain('t("systemInfo.desktopUpdateCurrent")');
|
||||
});
|
||||
|
||||
test("rechecks for updates while a packaged app remains open", () => {
|
||||
expect(mainSource).toContain('checkForDesktopUpdate("startup", { force: true })');
|
||||
expect(mainSource).toContain('checkForDesktopUpdate("interval", { force: true })');
|
||||
expect(mainSource).toContain('powerMonitor.on("resume"');
|
||||
expect(mainSource).toContain('checkForDesktopUpdate("activate")');
|
||||
expect(mainSource).toContain("if (updateCheckInFlight) {");
|
||||
expect(mainSource).toContain("return throwOnError ? updateCheckInFlight : updateCheckInFlight.catch(() => null)");
|
||||
expect(mainSource).toContain("clearInterval(updateCheckTimer)");
|
||||
});
|
||||
|
||||
test("only offers the restart action after the update is downloaded", () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { app, BrowserWindow, Menu, Tray, nativeImage, ipcMain, session, net, protocol, shell, dialog, safeStorage, clipboard } from "electron";
|
||||
import { app, BrowserWindow, Menu, Tray, nativeImage, ipcMain, session, net, protocol, shell, dialog, safeStorage, clipboard, powerMonitor } from "electron";
|
||||
import { existsSync } from "node:fs";
|
||||
import { appendFile, mkdir, readdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
||||
import { basename, join } from "node:path";
|
||||
@@ -76,6 +76,10 @@ let sidecar;
|
||||
let tray;
|
||||
let isQuitting = false;
|
||||
let updateState = "idle";
|
||||
let updateCheckInFlight = null;
|
||||
let updateDownloadInFlight = null;
|
||||
let updateCheckTimer = null;
|
||||
let lastUpdateCheckAt = 0;
|
||||
let sidecarScopeKey = "anonymous";
|
||||
let activeAccountId = null;
|
||||
let shutdownCleanupStarted = false;
|
||||
@@ -84,6 +88,8 @@ let sidecarRestartAttempts = 0;
|
||||
let sidecarRestartInFlight = false;
|
||||
let localDataResetScheduled = false;
|
||||
let rendererCrashDialogOpen = false;
|
||||
const updateCheckIntervalMs = 60 * 60 * 1_000;
|
||||
const updateCheckFocusThrottleMs = 15 * 60 * 1_000;
|
||||
const hasSingleInstanceLock = app.requestSingleInstanceLock();
|
||||
const windowStatePath = () => join(app.getPath("userData"), "window-state.json");
|
||||
const instanceUrlPath = () => join(app.getPath("userData"), "instance-url");
|
||||
@@ -508,18 +514,61 @@ const installDownloadedUpdate = () => {
|
||||
return { started: true };
|
||||
};
|
||||
|
||||
const checkForDesktopUpdate = (reason, { force = false, throwOnError = false } = {}) => {
|
||||
if (!app.isPackaged || process.env.EDGE_EVER_DISABLE_AUTO_UPDATE === "1" || updateState === "downloaded") {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
if (updateCheckInFlight) {
|
||||
return throwOnError ? updateCheckInFlight : updateCheckInFlight.catch(() => null);
|
||||
}
|
||||
if (updateDownloadInFlight) return Promise.resolve(null);
|
||||
const now = Date.now();
|
||||
if (!force && now - lastUpdateCheckAt < updateCheckFocusThrottleMs) return Promise.resolve(null);
|
||||
lastUpdateCheckAt = now;
|
||||
void writeDiagnostic("update.check-started", { reason });
|
||||
updateCheckInFlight = autoUpdater.checkForUpdates()
|
||||
.then((result) => {
|
||||
if (result?.downloadPromise) {
|
||||
updateDownloadInFlight = result.downloadPromise
|
||||
.catch(async (error) => {
|
||||
updateState = "idle";
|
||||
refreshTrayMenu();
|
||||
await writeDiagnostic("update.download-failed", { reason, message: error.message });
|
||||
})
|
||||
.finally(() => { updateDownloadInFlight = null; });
|
||||
}
|
||||
return result;
|
||||
})
|
||||
.catch(async (error) => {
|
||||
await writeDiagnostic("update.check-failed", { reason, message: error.message });
|
||||
throw error;
|
||||
})
|
||||
.finally(() => { updateCheckInFlight = null; });
|
||||
return throwOnError ? updateCheckInFlight : updateCheckInFlight.catch(() => null);
|
||||
};
|
||||
|
||||
const configureAutoUpdater = () => {
|
||||
if (!app.isPackaged || process.env.EDGE_EVER_DISABLE_AUTO_UPDATE === "1") return;
|
||||
autoUpdater.autoDownload = true;
|
||||
autoUpdater.autoInstallOnAppQuit = true;
|
||||
autoUpdater.autoRunAppAfterInstall = true;
|
||||
autoUpdater.on("update-available", () => { updateState = "available"; refreshTrayMenu(); void writeDiagnostic("update.available"); });
|
||||
autoUpdater.on("update-not-available", () => { updateState = "idle"; refreshTrayMenu(); void writeDiagnostic("update.not-available"); });
|
||||
autoUpdater.on("download-progress", (progress) => { void writeDiagnostic("update.download-progress", { percent: progress.percent }); });
|
||||
autoUpdater.on("update-downloaded", () => { updateState = "downloaded"; refreshTrayMenu(); void writeDiagnostic("update.downloaded"); });
|
||||
autoUpdater.on("error", (error) => { isQuitting = false; void writeDiagnostic("update.error", { message: error.message }); });
|
||||
void autoUpdater.checkForUpdates()
|
||||
.then((result) => result?.downloadPromise?.catch((error) => writeDiagnostic("update.download-failed", { message: error.message })))
|
||||
.catch((error) => writeDiagnostic("update.check-failed", { message: error.message }));
|
||||
autoUpdater.on("error", (error) => {
|
||||
isQuitting = false;
|
||||
if (updateState !== "downloaded") updateState = "idle";
|
||||
refreshTrayMenu();
|
||||
void writeDiagnostic("update.error", { message: error.message });
|
||||
});
|
||||
void checkForDesktopUpdate("startup", { force: true });
|
||||
updateCheckTimer = setInterval(() => {
|
||||
void checkForDesktopUpdate("interval", { force: true });
|
||||
}, updateCheckIntervalMs);
|
||||
powerMonitor.on("resume", () => {
|
||||
void checkForDesktopUpdate("resume", { force: true });
|
||||
});
|
||||
};
|
||||
|
||||
const startSidecar = async (accountId = null) => {
|
||||
@@ -907,6 +956,10 @@ app.whenReady().then(async () => {
|
||||
return configuredApiBaseUrl;
|
||||
});
|
||||
ipcMain.handle("desktop:update-status", () => ({ state: updateState }));
|
||||
ipcMain.handle("desktop:check-update", async () => {
|
||||
await checkForDesktopUpdate("manual", { force: true, throwOnError: true });
|
||||
return { state: updateState };
|
||||
});
|
||||
ipcMain.handle("desktop:download-update", () => autoUpdater.downloadUpdate());
|
||||
ipcMain.handle("desktop:install-update", () => installDownloadedUpdate());
|
||||
ipcMain.handle("desktop:stage-resource", async (_event, input) => {
|
||||
@@ -979,6 +1032,7 @@ app.whenReady().then(async () => {
|
||||
handleOpenTarget(process.argv);
|
||||
app.on("activate", () => {
|
||||
if (!showWindow(mainWindow)) void createWindow();
|
||||
void checkForDesktopUpdate("activate");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1005,6 +1059,10 @@ app.on("before-quit", (event) => {
|
||||
clearTimeout(sidecarRestartTimer);
|
||||
sidecarRestartTimer = null;
|
||||
}
|
||||
if (updateCheckTimer) {
|
||||
clearInterval(updateCheckTimer);
|
||||
updateCheckTimer = null;
|
||||
}
|
||||
tray?.destroy();
|
||||
void (async () => {
|
||||
await stopSidecar();
|
||||
|
||||
@@ -16,6 +16,7 @@ contextBridge.exposeInMainWorld("edgeeverDesktop", Object.freeze({
|
||||
recordRendererError: (details) => ipcRenderer.invoke("desktop:record-renderer-error", details),
|
||||
openRendererIssue: (details) => ipcRenderer.invoke("desktop:open-renderer-issue", details),
|
||||
updateStatus: () => ipcRenderer.invoke("desktop:update-status"),
|
||||
checkUpdate: () => ipcRenderer.invoke("desktop:check-update"),
|
||||
downloadUpdate: () => ipcRenderer.invoke("desktop:download-update"),
|
||||
installUpdate: () => ipcRenderer.invoke("desktop:install-update"),
|
||||
sidecarRequest: (method, params = {}) => ipcRenderer.invoke("desktop:sidecar-request", method, params),
|
||||
|
||||
@@ -12,6 +12,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { useParams } from "react-router";
|
||||
import { api } from "@/lib/api";
|
||||
import { EdgeEverCodeBlock, codeBlockLowlight } from "@/lib/code-block";
|
||||
import { withEnvironmentTitlePrefix } from "@/lib/environment-title";
|
||||
import {
|
||||
parseImageWidth,
|
||||
getImageReferrerPolicy,
|
||||
@@ -119,7 +120,12 @@ export const PublicSharePage = () => {
|
||||
robots.name = "robots";
|
||||
robots.content = "noindex,nofollow,noarchive";
|
||||
document.head.appendChild(robots);
|
||||
if (share) document.title = `${share.title?.trim() || t("common.untitledMemo")} · EdgeEver`;
|
||||
if (share) {
|
||||
document.title = withEnvironmentTitlePrefix(
|
||||
`${share.title?.trim() || t("common.untitledMemo")} · EdgeEver`,
|
||||
{ development: import.meta.env.DEV, profile: __EDGEEVER_DEVELOPMENT_PROFILE__ },
|
||||
);
|
||||
}
|
||||
return () => {
|
||||
document.title = previousTitle;
|
||||
robots.remove();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { CircleCheck, Copy, ExternalLink } from "lucide-react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { CircleCheck, Copy, ExternalLink, LoaderCircle, RefreshCw, RotateCcw } from "lucide-react";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useDeployedUpdateNotice } from "@/hooks/useDeployedUpdateNotice";
|
||||
@@ -84,8 +84,12 @@ export const getWebSystemInfoItems = (
|
||||
export const SystemInfoPanel = ({ active = true }: { active?: boolean }) => {
|
||||
const { t, i18n } = useTranslation();
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [desktopUpdateChecked, setDesktopUpdateChecked] = useState(false);
|
||||
const queryClient = useQueryClient();
|
||||
const { release } = useDeployedUpdateNotice();
|
||||
const instanceUrl = window.edgeeverDesktop?.isAvailable === true ? getConfiguredDesktopApiBaseUrl() : window.location.origin;
|
||||
const desktopBridge = window.edgeeverDesktop;
|
||||
const desktopAvailable = desktopBridge?.isAvailable === true;
|
||||
const instanceUrl = desktopAvailable ? getConfiguredDesktopApiBaseUrl() : window.location.origin;
|
||||
const healthQuery = useQuery({
|
||||
queryKey: ["instance-health", instanceUrl],
|
||||
queryFn: () => api.getInstanceHealth(),
|
||||
@@ -93,6 +97,23 @@ export const SystemInfoPanel = ({ active = true }: { active?: boolean }) => {
|
||||
staleTime: 5 * 60 * 1000,
|
||||
retry: 1,
|
||||
});
|
||||
const desktopUpdateStatusQuery = useQuery({
|
||||
queryKey: ["desktop-update-status"],
|
||||
queryFn: () => desktopBridge!.updateStatus(),
|
||||
enabled: active && desktopAvailable,
|
||||
refetchInterval: (query) => query.state.data?.state === "available" ? 1_000 : false,
|
||||
retry: 1,
|
||||
});
|
||||
const desktopUpdateCheckMutation = useMutation({
|
||||
mutationFn: () => desktopBridge!.checkUpdate(),
|
||||
onSuccess: (status) => {
|
||||
queryClient.setQueryData(["desktop-update-status"], status);
|
||||
setDesktopUpdateChecked(true);
|
||||
},
|
||||
});
|
||||
const desktopUpdateInstallMutation = useMutation({
|
||||
mutationFn: () => desktopBridge!.installUpdate(),
|
||||
});
|
||||
const infoItems = useMemo(
|
||||
() => getWebSystemInfoItems(t, i18n.language, healthQuery.data?.runtime),
|
||||
[healthQuery.data?.runtime, i18n.language, t],
|
||||
@@ -112,14 +133,72 @@ export const SystemInfoPanel = ({ active = true }: { active?: boolean }) => {
|
||||
window.setTimeout(() => setCopied(false), 1600);
|
||||
};
|
||||
|
||||
const desktopUpdateState = desktopUpdateStatusQuery.data?.state ?? "idle";
|
||||
const desktopUpdateBusy = desktopUpdateCheckMutation.isPending || desktopUpdateInstallMutation.isPending;
|
||||
const desktopUpdateStatus = desktopUpdateInstallMutation.isError || desktopUpdateCheckMutation.isError || desktopUpdateStatusQuery.isError
|
||||
? t("systemInfo.desktopUpdateFailed")
|
||||
: desktopUpdateInstallMutation.isPending
|
||||
? t("systemInfo.desktopUpdateInstalling")
|
||||
: desktopUpdateCheckMutation.isPending
|
||||
? t("systemInfo.desktopUpdateChecking")
|
||||
: desktopUpdateState === "available"
|
||||
? t("systemInfo.desktopUpdateDownloading")
|
||||
: desktopUpdateState === "downloaded"
|
||||
? t("systemInfo.desktopUpdateReady")
|
||||
: desktopUpdateChecked
|
||||
? t("systemInfo.desktopUpdateCurrent")
|
||||
: null;
|
||||
|
||||
const handleDesktopUpdate = () => {
|
||||
if (desktopUpdateState === "downloaded") desktopUpdateInstallMutation.mutate();
|
||||
else desktopUpdateCheckMutation.mutate();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid gap-3">
|
||||
<div className="flex justify-end">
|
||||
<div className="flex flex-wrap justify-end gap-2">
|
||||
{desktopAvailable ? (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-8 bg-white px-3 text-xs"
|
||||
type="button"
|
||||
disabled={desktopUpdateBusy || desktopUpdateState === "available"}
|
||||
onClick={handleDesktopUpdate}
|
||||
>
|
||||
{desktopUpdateBusy || desktopUpdateState === "available"
|
||||
? <LoaderCircle className="h-3.5 w-3.5 animate-spin" />
|
||||
: desktopUpdateState === "downloaded"
|
||||
? <RotateCcw className="h-3.5 w-3.5" />
|
||||
: <RefreshCw className="h-3.5 w-3.5" />}
|
||||
{desktopUpdateState === "downloaded"
|
||||
? t("systemInfo.desktopUpdateRestart")
|
||||
: desktopUpdateState === "available"
|
||||
? t("systemInfo.desktopUpdateDownloading")
|
||||
: desktopUpdateCheckMutation.isPending
|
||||
? t("systemInfo.desktopUpdateChecking")
|
||||
: t("systemInfo.desktopCheckForUpdates")}
|
||||
</Button>
|
||||
) : null}
|
||||
<Button size="sm" variant="outline" className="h-8 w-full bg-white px-3 text-xs sm:w-auto" type="button" onClick={() => void handleCopy()}>
|
||||
<Copy className="h-3.5 w-3.5" />
|
||||
{copied ? t("common.copied") : t("systemInfo.copy")}
|
||||
</Button>
|
||||
</div>
|
||||
{desktopAvailable && desktopUpdateStatus ? (
|
||||
<p
|
||||
className={cn(
|
||||
"text-right text-xs",
|
||||
desktopUpdateInstallMutation.isError || desktopUpdateCheckMutation.isError || desktopUpdateStatusQuery.isError
|
||||
? "text-red-600"
|
||||
: "text-slate-500",
|
||||
)}
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
>
|
||||
{desktopUpdateStatus}
|
||||
</p>
|
||||
) : null}
|
||||
{active && release ? (
|
||||
<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">
|
||||
<CircleCheck className="mt-0.5 h-4 w-4 shrink-0 text-emerald-600" />
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { withEnvironmentTitlePrefix } from "./environment-title.ts";
|
||||
|
||||
describe("environment tab title", () => {
|
||||
test("marks local development tabs as DEV", () => {
|
||||
expect(withEnvironmentTitlePrefix("EdgeEver", { development: true, profile: "local" }))
|
||||
.toBe("[DEV] EdgeEver");
|
||||
expect(withEnvironmentTitlePrefix("EdgeEver", { development: true, profile: "" }))
|
||||
.toBe("[DEV] EdgeEver");
|
||||
});
|
||||
|
||||
test("marks the local demo profile as DEMO", () => {
|
||||
expect(withEnvironmentTitlePrefix("EdgeEver", { development: true, profile: "demo" }))
|
||||
.toBe("[DEMO] EdgeEver");
|
||||
});
|
||||
|
||||
test("is idempotent during hot reloads and leaves production titles unchanged", () => {
|
||||
expect(withEnvironmentTitlePrefix("[LOCAL DEMO] EdgeEver", { development: true, profile: "demo" }))
|
||||
.toBe("[DEMO] EdgeEver");
|
||||
expect(withEnvironmentTitlePrefix("EdgeEver", { development: false, profile: "demo" }))
|
||||
.toBe("EdgeEver");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
type DevelopmentProfile = "" | "local" | "demo";
|
||||
|
||||
const existingEnvironmentPrefix = /^\[(?:DEV|DEMO|LOCAL|LOCAL DEMO)\]\s*/;
|
||||
|
||||
export const withEnvironmentTitlePrefix = (
|
||||
title: string,
|
||||
options: { development: boolean; profile: DevelopmentProfile },
|
||||
) => {
|
||||
if (!options.development) return title;
|
||||
const label = options.profile === "demo" ? "DEMO" : "DEV";
|
||||
return `[${label}] ${title.replace(existingEnvironmentPrefix, "")}`;
|
||||
};
|
||||
@@ -6,6 +6,7 @@ import { registerSW } from "virtual:pwa-register";
|
||||
import { App } from "./app/App";
|
||||
import "./i18n";
|
||||
import { emitPwaUpdateNotice } from "./lib/pwa-update-notice";
|
||||
import { withEnvironmentTitlePrefix } from "./lib/environment-title";
|
||||
import { initializeTheme, ThemeProvider } from "./components/ThemeProvider";
|
||||
import { DesktopRendererErrorBoundary } from "./components/DesktopRendererErrorBoundary";
|
||||
import "./styles/globals.css";
|
||||
@@ -13,10 +14,14 @@ import "./styles/globals.css";
|
||||
const PWA_UPDATE_CHECK_INTERVAL_MS = 10 * 60 * 1_000;
|
||||
const DEVELOPMENT_PWA_RELOAD_KEY = "edgeever.dev-pwa-reset";
|
||||
|
||||
if (import.meta.env.DEV && __EDGEEVER_DEVELOPMENT_PROFILE__) {
|
||||
document.documentElement.dataset.edgeeverEnvironment = __EDGEEVER_DEVELOPMENT_PROFILE__;
|
||||
const environmentLabel = __EDGEEVER_DEVELOPMENT_PROFILE__ === "demo" ? "LOCAL DEMO" : "LOCAL";
|
||||
document.title = `[${environmentLabel}] ${document.title}`;
|
||||
if (import.meta.env.DEV) {
|
||||
if (__EDGEEVER_DEVELOPMENT_PROFILE__) {
|
||||
document.documentElement.dataset.edgeeverEnvironment = __EDGEEVER_DEVELOPMENT_PROFILE__;
|
||||
}
|
||||
document.title = withEnvironmentTitlePrefix(document.title, {
|
||||
development: true,
|
||||
profile: __EDGEEVER_DEVELOPMENT_PROFILE__,
|
||||
});
|
||||
}
|
||||
|
||||
const clearDevelopmentPwaState = async () => {
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
type NotePrintPayload,
|
||||
} from "@/lib/note-print";
|
||||
import { getMessageTargetOrigin } from "@/lib/app-page-path";
|
||||
import { withEnvironmentTitlePrefix } from "@/lib/environment-title";
|
||||
import katexStyles from "katex/dist/katex.min.css?inline";
|
||||
import printStyles from "@/styles/note-print.css?inline";
|
||||
import "@fontsource-variable/noto-sans-sc/wght.css";
|
||||
@@ -110,7 +111,10 @@ const buildSource = (payload: NotePrintPayload) => {
|
||||
|
||||
const renderPreview = async (payload: NotePrintPayload) => {
|
||||
document.documentElement.lang = payload.language;
|
||||
document.title = `${payload.title} · EdgeEver`;
|
||||
document.title = withEnvironmentTitlePrefix(`${payload.title} · EdgeEver`, {
|
||||
development: import.meta.env.DEV,
|
||||
profile: __EDGEEVER_DEVELOPMENT_PROFILE__,
|
||||
});
|
||||
setText(status, payload.labels.preparing);
|
||||
setText(hint, "");
|
||||
setText(closeButton, payload.labels.close);
|
||||
|
||||
Vendored
+1
@@ -32,6 +32,7 @@ interface EdgeEverDesktopBridge {
|
||||
sidecarStatus(): Promise<{ available: boolean; path: string; scope: string }>;
|
||||
setAccountScope(accountId: string | null): Promise<{ ready: true; scope: string }>;
|
||||
updateStatus(): Promise<{ state: "idle" | "available" | "downloaded" }>;
|
||||
checkUpdate(): Promise<{ state: "idle" | "available" | "downloaded" }>;
|
||||
downloadUpdate(): Promise<unknown>;
|
||||
installUpdate(): Promise<unknown>;
|
||||
sidecarRequest<T = unknown>(method: string, params?: Record<string, unknown>): Promise<T>;
|
||||
|
||||
@@ -609,10 +609,14 @@ export const enUS = {
|
||||
releaseNotesUnavailable: "This build does not include an update summary. You can view the full Release notes.",
|
||||
clientUpdatesNote: "Download desktop updates from <releases>Releases</releases>, or update mobile apps through the corresponding app store.",
|
||||
viewReleaseNotes: "Full notes",
|
||||
desktopCheckForUpdates: "Check for updates",
|
||||
desktopUpdateChecking: "Checking for updates…",
|
||||
desktopUpdateCurrent: "You're up to date.",
|
||||
desktopUpdateDownloading: "Downloading update…",
|
||||
desktopUpdateReady: "Update downloaded",
|
||||
desktopUpdateDownload: "Download update",
|
||||
desktopUpdateRestart: "Restart to update",
|
||||
desktopUpdateInstalling: "Restarting to install update…",
|
||||
desktopUpdateFailed: "Update failed. Please try again later.",
|
||||
},
|
||||
feedback: {
|
||||
|
||||
@@ -609,10 +609,14 @@ export const zhCN = {
|
||||
releaseNotesUnavailable: "本次构建未包含更新摘要,可查看完整 Release 说明。",
|
||||
clientUpdatesNote: "桌面端可前往 <releases>Releases</releases> 下载更新,移动端请通过对应的应用商店更新。",
|
||||
viewReleaseNotes: "完整说明",
|
||||
desktopCheckForUpdates: "检查更新",
|
||||
desktopUpdateChecking: "正在检查更新…",
|
||||
desktopUpdateCurrent: "当前已是最新版本。",
|
||||
desktopUpdateDownloading: "正在下载更新…",
|
||||
desktopUpdateReady: "更新已下载完成",
|
||||
desktopUpdateDownload: "下载更新",
|
||||
desktopUpdateRestart: "重启并更新",
|
||||
desktopUpdateInstalling: "正在重启并安装更新…",
|
||||
desktopUpdateFailed: "更新失败,请稍后重试。",
|
||||
},
|
||||
feedback: {
|
||||
|
||||
Reference in new issue
Block a user