feat(desktop): implement startup and scheduled auto update flow
This commit is contained in:
@@ -132,6 +132,12 @@ const updateState = reactive({
|
|||||||
lastCheckedAt: "",
|
lastCheckedAt: "",
|
||||||
message: "",
|
message: "",
|
||||||
});
|
});
|
||||||
|
const AUTO_UPDATE_CHECK_INTERVAL_MS = 30 * 60 * 1000;
|
||||||
|
const updateRuntime = reactive({
|
||||||
|
downloading: false,
|
||||||
|
promptedVersion: "",
|
||||||
|
mandatoryInstalledVersion: "",
|
||||||
|
});
|
||||||
const contextMenu = reactive({
|
const contextMenu = reactive({
|
||||||
visible: false,
|
visible: false,
|
||||||
x: 0,
|
x: 0,
|
||||||
@@ -147,6 +153,7 @@ const dropState = reactive({
|
|||||||
});
|
});
|
||||||
let unlistenDragDrop: UnlistenFn | null = null;
|
let unlistenDragDrop: UnlistenFn | null = null;
|
||||||
let syncTimer: ReturnType<typeof setInterval> | null = null;
|
let syncTimer: ReturnType<typeof setInterval> | null = null;
|
||||||
|
let updateCheckTimer: ReturnType<typeof setInterval> | null = null;
|
||||||
|
|
||||||
const toast = reactive({
|
const toast = reactive({
|
||||||
visible: false,
|
visible: false,
|
||||||
@@ -557,6 +564,13 @@ function clearSyncScheduler() {
|
|||||||
syncState.nextRunAt = "";
|
syncState.nextRunAt = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clearUpdateScheduler() {
|
||||||
|
if (updateCheckTimer) {
|
||||||
|
clearInterval(updateCheckTimer);
|
||||||
|
updateCheckTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function syncFingerprint(item: LocalSyncFileItem) {
|
function syncFingerprint(item: LocalSyncFileItem) {
|
||||||
return `${Number(item.size || 0)}:${Number(item.modifiedMs || 0)}`;
|
return `${Number(item.size || 0)}:${Number(item.modifiedMs || 0)}`;
|
||||||
}
|
}
|
||||||
@@ -600,7 +614,11 @@ async function initClientVersion() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkClientUpdate(showResultToast = true) {
|
async function checkClientUpdate(showResultToast = true): Promise<boolean> {
|
||||||
|
if (updateState.checking) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
updateState.checking = true;
|
updateState.checking = true;
|
||||||
updateState.message = "";
|
updateState.message = "";
|
||||||
const response = await invokeBridge("api_check_client_update", {
|
const response = await invokeBridge("api_check_client_update", {
|
||||||
@@ -626,22 +644,34 @@ async function checkClientUpdate(showResultToast = true) {
|
|||||||
showToast("当前已是最新版本", "info");
|
showToast("当前已是最新版本", "info");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateState.available = false;
|
updateState.available = false;
|
||||||
|
updateState.downloadUrl = "";
|
||||||
updateState.message = String(response.data?.message || "检查更新失败");
|
updateState.message = String(response.data?.message || "检查更新失败");
|
||||||
if (showResultToast) {
|
if (showResultToast) {
|
||||||
showToast(updateState.message, "error");
|
showToast(updateState.message, "error");
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function installLatestUpdate() {
|
async function installLatestUpdate(trigger: "manual" | "auto" = "manual"): Promise<boolean> {
|
||||||
if (!updateState.downloadUrl) {
|
if (updateRuntime.downloading) {
|
||||||
showToast("当前没有可用的更新下载地址", "info");
|
if (trigger === "manual") {
|
||||||
return;
|
showToast("更新包正在下载,请稍候", "info");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!updateState.downloadUrl) {
|
||||||
|
if (trigger === "manual") {
|
||||||
|
showToast("当前没有可用的更新下载地址", "info");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateRuntime.downloading = true;
|
||||||
const taskId = `UPD-${Date.now()}`;
|
const taskId = `UPD-${Date.now()}`;
|
||||||
const installerName = `wanwan-cloud-desktop_v${updateState.latestVersion || updateState.currentVersion}.exe`;
|
const installerName = `wanwan-cloud-desktop_v${updateState.latestVersion || updateState.currentVersion}.exe`;
|
||||||
prependTransferTask({
|
prependTransferTask({
|
||||||
@@ -661,6 +691,7 @@ async function installLatestUpdate() {
|
|||||||
fileName: installerName,
|
fileName: installerName,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
if (response.ok && response.data?.success) {
|
if (response.ok && response.data?.success) {
|
||||||
updateTransferTask(taskId, {
|
updateTransferTask(taskId, {
|
||||||
speed: "-",
|
speed: "-",
|
||||||
@@ -677,7 +708,7 @@ async function installLatestUpdate() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
showToast("更新包已下载,已尝试启动安装程序", "success");
|
showToast("更新包已下载,已尝试启动安装程序", "success");
|
||||||
return;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const message = String(response.data?.message || "下载更新包失败");
|
const message = String(response.data?.message || "下载更新包失败");
|
||||||
@@ -688,6 +719,50 @@ async function installLatestUpdate() {
|
|||||||
note: message,
|
note: message,
|
||||||
});
|
});
|
||||||
showToast(message, "error");
|
showToast(message, "error");
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
updateRuntime.downloading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runAutoUpdateCycle(trigger: "startup" | "login" | "timer" = "startup") {
|
||||||
|
const checked = await checkClientUpdate(false);
|
||||||
|
if (!checked) return;
|
||||||
|
|
||||||
|
if (!updateState.available || !updateState.downloadUrl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const latestVersion = String(updateState.latestVersion || "").trim();
|
||||||
|
if (!latestVersion) return;
|
||||||
|
|
||||||
|
if (updateState.mandatory) {
|
||||||
|
if (updateRuntime.mandatoryInstalledVersion === latestVersion) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showToast(`检测到强制更新 v${latestVersion},正在下载升级包`, "info");
|
||||||
|
const installed = await installLatestUpdate("auto");
|
||||||
|
if (installed) {
|
||||||
|
updateRuntime.mandatoryInstalledVersion = latestVersion;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateRuntime.promptedVersion === latestVersion) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateRuntime.promptedVersion = latestVersion;
|
||||||
|
if (trigger === "timer") {
|
||||||
|
showToast(`发现新版本 v${latestVersion},可在“版本更新”中安装`, "info");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmed = window.confirm(`发现新版本 v${latestVersion},是否立即下载并安装?`);
|
||||||
|
if (confirmed) {
|
||||||
|
await installLatestUpdate("auto");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function chooseSyncDirectory() {
|
async function chooseSyncDirectory() {
|
||||||
@@ -747,6 +822,16 @@ function rebuildSyncScheduler() {
|
|||||||
}, intervalMinutes * 60 * 1000);
|
}, intervalMinutes * 60 * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function rebuildUpdateScheduler() {
|
||||||
|
clearUpdateScheduler();
|
||||||
|
if (!authenticated.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updateCheckTimer = setInterval(() => {
|
||||||
|
void runAutoUpdateCycle("timer");
|
||||||
|
}, AUTO_UPDATE_CHECK_INTERVAL_MS);
|
||||||
|
}
|
||||||
|
|
||||||
async function clearSyncSnapshot() {
|
async function clearSyncSnapshot() {
|
||||||
if (!syncState.localDir.trim()) {
|
if (!syncState.localDir.trim()) {
|
||||||
showToast("请先配置本地同步目录", "info");
|
showToast("请先配置本地同步目录", "info");
|
||||||
@@ -1034,9 +1119,10 @@ async function restoreSession() {
|
|||||||
authenticated.value = true;
|
authenticated.value = true;
|
||||||
loadSyncConfig();
|
loadSyncConfig();
|
||||||
rebuildSyncScheduler();
|
rebuildSyncScheduler();
|
||||||
|
rebuildUpdateScheduler();
|
||||||
await loadFiles("/");
|
await loadFiles("/");
|
||||||
await loadShares(true);
|
await loadShares(true);
|
||||||
await checkClientUpdate(false);
|
await runAutoUpdateCycle("startup");
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildItemPath(item: FileItem) {
|
function buildItemPath(item: FileItem) {
|
||||||
@@ -1101,11 +1187,12 @@ async function handleLogin() {
|
|||||||
showToast("登录成功,正在同步文件目录", "success");
|
showToast("登录成功,正在同步文件目录", "success");
|
||||||
loadSyncConfig();
|
loadSyncConfig();
|
||||||
rebuildSyncScheduler();
|
rebuildSyncScheduler();
|
||||||
|
rebuildUpdateScheduler();
|
||||||
await loadFiles("/");
|
await loadFiles("/");
|
||||||
if (!user.value) {
|
if (!user.value) {
|
||||||
await loadProfile();
|
await loadProfile();
|
||||||
}
|
}
|
||||||
await checkClientUpdate(false);
|
await runAutoUpdateCycle("login");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1117,6 +1204,7 @@ async function handleLogin() {
|
|||||||
async function handleLogout() {
|
async function handleLogout() {
|
||||||
await invokeBridge("api_logout", { baseUrl: appConfig.baseUrl });
|
await invokeBridge("api_logout", { baseUrl: appConfig.baseUrl });
|
||||||
clearSyncScheduler();
|
clearSyncScheduler();
|
||||||
|
clearUpdateScheduler();
|
||||||
authenticated.value = false;
|
authenticated.value = false;
|
||||||
user.value = null;
|
user.value = null;
|
||||||
files.value = [];
|
files.value = [];
|
||||||
@@ -1137,6 +1225,9 @@ async function handleLogout() {
|
|||||||
syncState.lastRunAt = "";
|
syncState.lastRunAt = "";
|
||||||
syncState.lastSummary = "";
|
syncState.lastSummary = "";
|
||||||
syncState.nextRunAt = "";
|
syncState.nextRunAt = "";
|
||||||
|
updateRuntime.downloading = false;
|
||||||
|
updateRuntime.promptedVersion = "";
|
||||||
|
updateRuntime.mandatoryInstalledVersion = "";
|
||||||
showToast("已退出客户端", "info");
|
showToast("已退出客户端", "info");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1611,6 +1702,7 @@ onBeforeUnmount(() => {
|
|||||||
window.removeEventListener("click", handleGlobalClick);
|
window.removeEventListener("click", handleGlobalClick);
|
||||||
window.removeEventListener("keydown", handleGlobalKey);
|
window.removeEventListener("keydown", handleGlobalKey);
|
||||||
clearSyncScheduler();
|
clearSyncScheduler();
|
||||||
|
clearUpdateScheduler();
|
||||||
if (unlistenDragDrop) {
|
if (unlistenDragDrop) {
|
||||||
unlistenDragDrop();
|
unlistenDragDrop();
|
||||||
unlistenDragDrop = null;
|
unlistenDragDrop = null;
|
||||||
@@ -1753,10 +1845,12 @@ onBeforeUnmount(() => {
|
|||||||
<button class="action-btn" @click="clearSyncSnapshot">重建索引</button>
|
<button class="action-btn" @click="clearSyncSnapshot">重建索引</button>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="nav === 'updates'">
|
<template v-else-if="nav === 'updates'">
|
||||||
<button class="action-btn" :disabled="updateState.checking" @click="checkClientUpdate()">
|
<button class="action-btn" :disabled="updateState.checking || updateRuntime.downloading" @click="checkClientUpdate()">
|
||||||
{{ updateState.checking ? "检查中..." : "检查更新" }}
|
{{ updateState.checking ? "检查中..." : "检查更新" }}
|
||||||
</button>
|
</button>
|
||||||
<button class="action-btn" :disabled="!updateState.available || !updateState.downloadUrl" @click="installLatestUpdate">立即更新</button>
|
<button class="action-btn" :disabled="updateRuntime.downloading || !updateState.available || !updateState.downloadUrl" @click="installLatestUpdate('manual')">
|
||||||
|
{{ updateRuntime.downloading ? "下载中..." : "立即更新" }}
|
||||||
|
</button>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -1959,6 +2053,7 @@ onBeforeUnmount(() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="update-meta">
|
<div class="update-meta">
|
||||||
|
<span>自动检查:已开启(每 30 分钟)</span>
|
||||||
<span>上次检查:{{ updateState.lastCheckedAt ? formatDate(updateState.lastCheckedAt) : "-" }}</span>
|
<span>上次检查:{{ updateState.lastCheckedAt ? formatDate(updateState.lastCheckedAt) : "-" }}</span>
|
||||||
<span>提示:{{ updateState.message || "可手动点击“检查更新”获取最新信息" }}</span>
|
<span>提示:{{ updateState.message || "可手动点击“检查更新”获取最新信息" }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user