From 1fbad3cd37d9f09a69ff26167901b846dd3f7697 Mon Sep 17 00:00:00 2001 From: Tomas Dvorak Date: Sat, 19 Sep 2026 00:33:34 +0200 Subject: [PATCH] feat(uploader): show estimated remaining time (#2393) The processing progress line now appends an ETA computed from remaining bytes over the displayed speed (instant or average per the existing toggle). Empty while speed is unknown. Authored By: TDvorak Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- frontend/public/locales/en-US/application.json | 5 +++-- frontend/public/locales/zh-CN/application.json | 5 +++-- .../src/component/Uploader/Popup/UploadTask.tsx | 14 ++++++++++++++ 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/frontend/public/locales/en-US/application.json b/frontend/public/locales/en-US/application.json index 150fa3ae..2f8f2d09 100644 --- a/frontend/public/locales/en-US/application.json +++ b/frontend/public/locales/en-US/application.json @@ -666,7 +666,7 @@ "preparing": "Preparing...", "processing": "Processing...", "progressDescription": "{{uploaded}} uploaded, {{total}} total - {{percentage}}%", - "progressDescriptionFull": "{{uploaded}} uploaded, {{total}} total - {{percentage}}% ({{speed}})", + "progressDescriptionFull": "{{uploaded}} uploaded, {{total}} total - {{percentage}}% ({{speed}}){{eta}}", "progressDescriptionPlaceHolder": " - uploaded", "uploaded": "Uploaded", "rootFolder": "Root folder", @@ -725,7 +725,8 @@ "upyunUploadFailed": "Upload failed: {{msg}}", "parseResponseError": "Unable to parse response: {{msg}} ({{content}})", "concurrentTaskNumber": "Concurrent task limit", - "dropFileHere": "Drop file to upload" + "dropFileHere": "Drop file to upload", + "etaSuffix": ", ETA {{eta}}" }, "share": { "statistics": "Statistics", diff --git a/frontend/public/locales/zh-CN/application.json b/frontend/public/locales/zh-CN/application.json index aa23bb53..0649856d 100644 --- a/frontend/public/locales/zh-CN/application.json +++ b/frontend/public/locales/zh-CN/application.json @@ -666,7 +666,7 @@ "preparing": "准备中...", "processing": "处理中...", "progressDescription": "已上传 {{uploaded}},共 {{total}} - {{percentage}}%", - "progressDescriptionFull": "{{speed}} 已上传 {{uploaded}},共 {{total}} - {{percentage}}%", + "progressDescriptionFull": "{{speed}} 已上传 {{uploaded}},共 {{total}} - {{percentage}}%{{eta}}", "progressDescriptionPlaceHolder": "已上传 - ", "uploaded": "已上传", "rootFolder": "根目录", @@ -725,7 +725,8 @@ "upyunUploadFailed": "上传失败:{{msg}}", "parseResponseError": "无法解析响应:{{msg}} ({{content}})", "concurrentTaskNumber": "同时上传的任务数量", - "dropFileHere": "松开鼠标开始上传" + "dropFileHere": "松开鼠标开始上传", + "etaSuffix": ",预计剩余 {{eta}}" }, "share": { "statistics": "统计", diff --git a/frontend/src/component/Uploader/Popup/UploadTask.tsx b/frontend/src/component/Uploader/Popup/UploadTask.tsx index 11385594..831fe21b 100644 --- a/frontend/src/component/Uploader/Popup/UploadTask.tsx +++ b/frontend/src/component/Uploader/Popup/UploadTask.tsx @@ -5,11 +5,13 @@ import MuiAccordionSummary from "@mui/material/AccordionSummary"; import Chip from "@mui/material/Chip"; import { lighten, useTheme } from "@mui/material/styles"; import useMediaQuery from "@mui/material/useMediaQuery"; +import dayjs from "dayjs"; import React, { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { FileType } from "../../../api/explorer.ts"; import { navigateToPath } from "../../../redux/thunks/filemanager.ts"; import { sizeToString } from "../../../util"; +import { formatDuration } from "../../../util/datetime.ts"; import { NoWrapBox } from "../../Common/StyledComponents.tsx"; import FileTypeIcon from "../../FileManager/Explorer/FileTypeIcon.tsx"; import ArrowClockwiseFilled from "../../Icons/ArrowClockwiseFilled.tsx"; @@ -70,6 +72,16 @@ const getSpeedText = (speed: number, speedAvg: number, useSpeedAvg: boolean) => return `${sizeToString(displayedSpeed ? displayedSpeed : 0)}/s`; }; +// ETA from remaining bytes over the displayed speed; empty when speed is +// unknown or zero. +const getEtaText = (speed: number, speedAvg: number, useSpeedAvg: boolean, loaded: number, total: number) => { + const displayedSpeed = useSpeedAvg ? speedAvg : speed; + if (!displayedSpeed || displayedSpeed <= 0 || total <= loaded) { + return ""; + } + return formatDuration(dayjs.duration(Math.ceil((total - loaded) / displayedSpeed), "seconds")); +}; + const getErrMsg = (error?: Error) => { if (error) { let errMsg = error.message; @@ -165,6 +177,7 @@ export default function UploadTask({ ); case Status.processing: if (progress) { + const eta = getEtaText(speed, speedAvg, useAvgSpeed, progress.total.loaded, progress.total.size); return (
{t("progressDescriptionFull", { @@ -172,6 +185,7 @@ export default function UploadTask({ uploaded: sizeToString(progress.total.loaded), total: sizeToString(progress.total.size), percentage: progress.total.percent.toFixed(2), + eta: eta ? t("etaSuffix", { eta }) : "", })}
);