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 <info@tdvorak.dev>

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
pull/3582/head
Tomas Dvorak 2 weeks ago
parent 2c8e686b4d
commit 1fbad3cd37

@ -666,7 +666,7 @@
"preparing": "Preparing...", "preparing": "Preparing...",
"processing": "Processing...", "processing": "Processing...",
"progressDescription": "{{uploaded}} uploaded, {{total}} total - {{percentage}}%", "progressDescription": "{{uploaded}} uploaded, {{total}} total - {{percentage}}%",
"progressDescriptionFull": "{{uploaded}} uploaded, {{total}} total - {{percentage}}% ({{speed}})", "progressDescriptionFull": "{{uploaded}} uploaded, {{total}} total - {{percentage}}% ({{speed}}){{eta}}",
"progressDescriptionPlaceHolder": " - uploaded", "progressDescriptionPlaceHolder": " - uploaded",
"uploaded": "Uploaded", "uploaded": "Uploaded",
"rootFolder": "Root folder", "rootFolder": "Root folder",
@ -725,7 +725,8 @@
"upyunUploadFailed": "Upload failed: {{msg}}", "upyunUploadFailed": "Upload failed: {{msg}}",
"parseResponseError": "Unable to parse response: {{msg}} ({{content}})", "parseResponseError": "Unable to parse response: {{msg}} ({{content}})",
"concurrentTaskNumber": "Concurrent task limit", "concurrentTaskNumber": "Concurrent task limit",
"dropFileHere": "Drop file to upload" "dropFileHere": "Drop file to upload",
"etaSuffix": ", ETA {{eta}}"
}, },
"share": { "share": {
"statistics": "Statistics", "statistics": "Statistics",

@ -666,7 +666,7 @@
"preparing": "准备中...", "preparing": "准备中...",
"processing": "处理中...", "processing": "处理中...",
"progressDescription": "已上传 {{uploaded}},共 {{total}} - {{percentage}}%", "progressDescription": "已上传 {{uploaded}},共 {{total}} - {{percentage}}%",
"progressDescriptionFull": "{{speed}} 已上传 {{uploaded}},共 {{total}} - {{percentage}}%", "progressDescriptionFull": "{{speed}} 已上传 {{uploaded}},共 {{total}} - {{percentage}}%{{eta}}",
"progressDescriptionPlaceHolder": "已上传 - ", "progressDescriptionPlaceHolder": "已上传 - ",
"uploaded": "已上传", "uploaded": "已上传",
"rootFolder": "根目录", "rootFolder": "根目录",
@ -725,7 +725,8 @@
"upyunUploadFailed": "上传失败:{{msg}}", "upyunUploadFailed": "上传失败:{{msg}}",
"parseResponseError": "无法解析响应:{{msg}} ({{content}})", "parseResponseError": "无法解析响应:{{msg}} ({{content}})",
"concurrentTaskNumber": "同时上传的任务数量", "concurrentTaskNumber": "同时上传的任务数量",
"dropFileHere": "松开鼠标开始上传" "dropFileHere": "松开鼠标开始上传",
"etaSuffix": ",预计剩余 {{eta}}"
}, },
"share": { "share": {
"statistics": "统计", "statistics": "统计",

@ -5,11 +5,13 @@ import MuiAccordionSummary from "@mui/material/AccordionSummary";
import Chip from "@mui/material/Chip"; import Chip from "@mui/material/Chip";
import { lighten, useTheme } from "@mui/material/styles"; import { lighten, useTheme } from "@mui/material/styles";
import useMediaQuery from "@mui/material/useMediaQuery"; import useMediaQuery from "@mui/material/useMediaQuery";
import dayjs from "dayjs";
import React, { useEffect, useMemo, useState } from "react"; import React, { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { FileType } from "../../../api/explorer.ts"; import { FileType } from "../../../api/explorer.ts";
import { navigateToPath } from "../../../redux/thunks/filemanager.ts"; import { navigateToPath } from "../../../redux/thunks/filemanager.ts";
import { sizeToString } from "../../../util"; import { sizeToString } from "../../../util";
import { formatDuration } from "../../../util/datetime.ts";
import { NoWrapBox } from "../../Common/StyledComponents.tsx"; import { NoWrapBox } from "../../Common/StyledComponents.tsx";
import FileTypeIcon from "../../FileManager/Explorer/FileTypeIcon.tsx"; import FileTypeIcon from "../../FileManager/Explorer/FileTypeIcon.tsx";
import ArrowClockwiseFilled from "../../Icons/ArrowClockwiseFilled.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`; 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) => { const getErrMsg = (error?: Error) => {
if (error) { if (error) {
let errMsg = error.message; let errMsg = error.message;
@ -165,6 +177,7 @@ export default function UploadTask({
); );
case Status.processing: case Status.processing:
if (progress) { if (progress) {
const eta = getEtaText(speed, speedAvg, useAvgSpeed, progress.total.loaded, progress.total.size);
return ( return (
<div> <div>
{t("progressDescriptionFull", { {t("progressDescriptionFull", {
@ -172,6 +185,7 @@ export default function UploadTask({
uploaded: sizeToString(progress.total.loaded), uploaded: sizeToString(progress.total.loaded),
total: sizeToString(progress.total.size), total: sizeToString(progress.total.size),
percentage: progress.total.percent.toFixed(2), percentage: progress.total.percent.toFixed(2),
eta: eta ? t("etaSuffix", { eta }) : "",
})} })}
</div> </div>
); );

Loading…
Cancel
Save