feat(desktop): stream download progress and auto-launch installer
This commit is contained in:
@@ -4,7 +4,8 @@ import { invoke } from "@tauri-apps/api/core";
|
||||
import { openPath, openUrl } from "@tauri-apps/plugin-opener";
|
||||
import { open as openDialog } from "@tauri-apps/plugin-dialog";
|
||||
import { getVersion } from "@tauri-apps/api/app";
|
||||
import type { UnlistenFn } from "@tauri-apps/api/event";
|
||||
import { listen, type UnlistenFn } from "@tauri-apps/api/event";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import { getCurrentWebview } from "@tauri-apps/api/webview";
|
||||
|
||||
type NavKey = "files" | "transfers" | "shares" | "sync" | "updates";
|
||||
@@ -40,6 +41,15 @@ type BridgeResponse = {
|
||||
data: Record<string, any>;
|
||||
};
|
||||
|
||||
type NativeDownloadProgressEvent = {
|
||||
taskId?: string;
|
||||
downloadedBytes?: number;
|
||||
totalBytes?: number | null;
|
||||
progress?: number | null;
|
||||
resumedBytes?: number;
|
||||
done?: boolean;
|
||||
};
|
||||
|
||||
type LocalSyncFileItem = {
|
||||
path: string;
|
||||
relativePath: string;
|
||||
@@ -149,6 +159,7 @@ const dropState = reactive({
|
||||
failed: 0,
|
||||
});
|
||||
let unlistenDragDrop: UnlistenFn | null = null;
|
||||
let unlistenNativeDownloadProgress: UnlistenFn | null = null;
|
||||
let syncTimer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
const toast = reactive({
|
||||
@@ -347,6 +358,35 @@ function fileTypeLabel(item: FileItem) {
|
||||
return "文件";
|
||||
}
|
||||
|
||||
function applyNativeDownloadProgress(payload: NativeDownloadProgressEvent) {
|
||||
const taskId = String(payload?.taskId || "").trim();
|
||||
if (!taskId) return;
|
||||
|
||||
const downloadedBytes = Number(payload?.downloadedBytes || 0);
|
||||
const totalBytesRaw = payload?.totalBytes;
|
||||
const totalBytes = totalBytesRaw === null || totalBytesRaw === undefined ? NaN : Number(totalBytesRaw);
|
||||
const eventProgress = Number(payload?.progress);
|
||||
const calculatedProgress = Number.isFinite(eventProgress)
|
||||
? eventProgress
|
||||
: (Number.isFinite(totalBytes) && totalBytes > 0 ? (downloadedBytes / totalBytes) * 100 : NaN);
|
||||
const boundedProgress = Number.isFinite(calculatedProgress)
|
||||
? Math.max(1, Math.min(payload?.done ? 100 : 99.8, calculatedProgress))
|
||||
: NaN;
|
||||
|
||||
const patch: Partial<TransferTask> = {
|
||||
speed: "下载中",
|
||||
status: "downloading",
|
||||
note: Number.isFinite(totalBytes) && totalBytes > 0
|
||||
? `${formatBytes(downloadedBytes)} / ${formatBytes(totalBytes)}`
|
||||
: `已下载 ${formatBytes(downloadedBytes)}`,
|
||||
};
|
||||
if (Number.isFinite(boundedProgress)) {
|
||||
patch.progress = Number(boundedProgress.toFixed(1));
|
||||
}
|
||||
|
||||
updateTransferTask(taskId, patch);
|
||||
}
|
||||
|
||||
function fileIcon(item: FileItem) {
|
||||
if (item.isDirectory || item.type === "directory") return "📁";
|
||||
const name = String(item.name || "").toLowerCase();
|
||||
@@ -674,6 +714,7 @@ async function installLatestUpdate(): Promise<boolean> {
|
||||
const response = await invokeBridge("api_native_download", {
|
||||
url: updateState.downloadUrl,
|
||||
fileName: installerName,
|
||||
taskId,
|
||||
});
|
||||
|
||||
try {
|
||||
@@ -686,13 +727,30 @@ async function installLatestUpdate(): Promise<boolean> {
|
||||
});
|
||||
const savePath = String(response.data?.savePath || "").trim();
|
||||
if (savePath) {
|
||||
const launchResponse = await invokeBridge("api_launch_installer", {
|
||||
installerPath: savePath,
|
||||
});
|
||||
if (launchResponse.ok && launchResponse.data?.success) {
|
||||
updateTransferTask(taskId, {
|
||||
speed: "-",
|
||||
progress: 100,
|
||||
status: "done",
|
||||
note: "安装程序已启动,客户端即将退出",
|
||||
});
|
||||
showToast("安装程序已启动,客户端即将退出", "success");
|
||||
setTimeout(() => {
|
||||
void getCurrentWindow().close();
|
||||
}, 400);
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
await openPath(savePath);
|
||||
} catch (error) {
|
||||
console.error("open installer failed", error);
|
||||
console.error("open installer fallback failed", error);
|
||||
}
|
||||
}
|
||||
showToast("更新包已下载,已尝试启动安装程序", "success");
|
||||
showToast("更新包已下载,请手动运行安装程序", "info");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1258,6 +1316,7 @@ async function downloadSelected(target?: FileItem | null) {
|
||||
const nativeResponse = await invokeBridge("api_native_download", {
|
||||
url: signedUrl,
|
||||
fileName: current.displayName || current.name,
|
||||
taskId,
|
||||
});
|
||||
|
||||
if (nativeResponse.ok && nativeResponse.data?.success) {
|
||||
@@ -1387,6 +1446,7 @@ async function retryTransferTask(taskId: string) {
|
||||
const response = await invokeBridge("api_native_download", {
|
||||
url: task.downloadUrl,
|
||||
fileName: task.fileName || task.name,
|
||||
taskId,
|
||||
});
|
||||
if (response.ok && response.data?.success) {
|
||||
const resumedBytes = Number(response.data?.resumedBytes || 0);
|
||||
@@ -1595,6 +1655,16 @@ async function registerDragDropListener() {
|
||||
}
|
||||
}
|
||||
|
||||
async function registerNativeDownloadProgressListener() {
|
||||
try {
|
||||
unlistenNativeDownloadProgress = await listen<NativeDownloadProgressEvent>("native-download-progress", (event) => {
|
||||
applyNativeDownloadProgress(event.payload || {});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("register native download progress listener failed", error);
|
||||
}
|
||||
}
|
||||
|
||||
watch(nav, async (next) => {
|
||||
if (next === "shares" && authenticated.value) {
|
||||
await loadShares();
|
||||
@@ -1622,6 +1692,7 @@ onMounted(async () => {
|
||||
window.addEventListener("click", handleGlobalClick);
|
||||
window.addEventListener("keydown", handleGlobalKey);
|
||||
await registerDragDropListener();
|
||||
await registerNativeDownloadProgressListener();
|
||||
await initClientVersion();
|
||||
await restoreSession();
|
||||
});
|
||||
@@ -1634,6 +1705,10 @@ onBeforeUnmount(() => {
|
||||
unlistenDragDrop();
|
||||
unlistenDragDrop = null;
|
||||
}
|
||||
if (unlistenNativeDownloadProgress) {
|
||||
unlistenNativeDownloadProgress();
|
||||
unlistenNativeDownloadProgress = null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1843,6 +1918,7 @@ onBeforeUnmount(() => {
|
||||
<div class="progress">
|
||||
<div class="bar" :style="{ width: `${task.progress}%` }" />
|
||||
</div>
|
||||
<small class="task-percent">{{ Math.max(0, Math.min(100, Math.round(task.progress))) }}%</small>
|
||||
<div class="task-actions">
|
||||
<button v-if="task.status === 'failed'" class="mini-btn" @click="retryTransferTask(task.id)">重试</button>
|
||||
<button v-if="task.status === 'done' || task.status === 'failed'" class="mini-btn ghost" @click="removeTransferTask(task.id)">移除</button>
|
||||
@@ -2793,8 +2869,9 @@ select:focus {
|
||||
}
|
||||
|
||||
.progress {
|
||||
height: 8px;
|
||||
background: #e4edf9;
|
||||
height: 10px;
|
||||
background: #dbe7f8;
|
||||
border: 1px solid #cadef6;
|
||||
border-radius: 999px;
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -2802,6 +2879,14 @@ select:focus {
|
||||
.bar {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #1d6fff, #57a2ff);
|
||||
transition: width 0.14s linear;
|
||||
}
|
||||
|
||||
.task-percent {
|
||||
justify-self: end;
|
||||
color: #3f5f86;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.share-list {
|
||||
|
||||
Reference in New Issue
Block a user