diff --git a/frontend/src/components/upload/FileList.tsx b/frontend/src/components/upload/FileList.tsx index 98468e8a0..560670f33 100644 --- a/frontend/src/components/upload/FileList.tsx +++ b/frontend/src/components/upload/FileList.tsx @@ -1,10 +1,20 @@ -import { ActionIcon, Table } from "@mantine/core"; +import { + ActionIcon, + Badge, + Box, + Group, + Table, + Text, + Tooltip, +} from "@mantine/core"; import { TbTrash } from "react-icons/tb"; import { GrUndo } from "react-icons/gr"; -import { FileListItem } from "../../types/File.type"; +import { FileListItem, FileUpload } from "../../types/File.type"; import { byteToHumanSizeString } from "../../utils/fileSize.util"; import UploadProgressIndicator from "./UploadProgressIndicator"; import { FormattedMessage } from "react-intl"; +import useTranslate from "../../hooks/useTranslate.hook"; +import { formatTimeRemaining } from "../../utils/time.util"; const FileListRow = ({ file, @@ -15,6 +25,7 @@ const FileListRow = ({ onRemove?: () => void; onRestore?: () => void; }) => { + const t = useTranslate(); { const uploadable = "uploadingProgress" in file; const uploading = uploadable && file.uploadingProgress !== 0; @@ -33,6 +44,17 @@ const FileListRow = ({ > {file.name} {byteToHumanSizeString(+file.size)} + + {uploading && + uploadable && + file.estimatedTimeRemaining !== undefined && ( + + + {formatTimeRemaining(file.estimatedTimeRemaining)} + + + )} + {removable && ( ({ /> )); + // Calculate upload statistics + const uploadableFiles = files.filter( + (file) => "uploadingProgress" in file, + ) as unknown as FileUpload[]; + + const inProgressFiles = uploadableFiles.filter( + (file) => file.uploadingProgress > 0 && file.uploadingProgress < 100, + ).length; + + const pendingFiles = uploadableFiles.filter( + (file) => file.uploadingProgress === 0, + ).length; + + const remainingFiles = inProgressFiles + pendingFiles; + + // Calculate overall estimated time + let maxEstimatedTime = 0; + if (inProgressFiles > 0) { + for (const file of uploadableFiles) { + if ( + file.estimatedTimeRemaining && + file.estimatedTimeRemaining > maxEstimatedTime + ) { + maxEstimatedTime = file.estimatedTimeRemaining; + } + } + } + return ( - - - - - - - - - {rows} -
- - - -
+ <> + {uploadableFiles.length > 0 && remainingFiles > 0 && ( + + + + + + {maxEstimatedTime > 0 && ( + + + + )} + + + )} + + + + + + + + + + {rows} +
+ + + + + +
+ ); }; diff --git a/frontend/src/components/upload/UploadProgressIndicator.tsx b/frontend/src/components/upload/UploadProgressIndicator.tsx index a26b740da..8b848f4da 100644 --- a/frontend/src/components/upload/UploadProgressIndicator.tsx +++ b/frontend/src/components/upload/UploadProgressIndicator.tsx @@ -1,6 +1,13 @@ import { Loader, RingProgress } from "@mantine/core"; import { TbCircleCheck } from "react-icons/tb"; -const UploadProgressIndicator = ({ progress }: { progress: number }) => { + +type UploadProgressIndicatorProps = { + progress: number; +}; + +const UploadProgressIndicator = ({ + progress, +}: UploadProgressIndicatorProps) => { if (progress > 0 && progress < 100) { return ( files.map((file, callbackIndex) => { if (fileIndex == callbackIndex) { + if (progress === 1 && !file.uploadStartTime) { + file.uploadStartTime = Date.now(); + } + file.uploadingProgress = progress; + + if (progress > 0 && progress < 100) { + const elapsedMs = + Date.now() - (file.uploadStartTime || Date.now()); + const estimatedTotalMs = (elapsedMs / progress) * 100; + const remainingMs = estimatedTotalMs - elapsedMs; + file.estimatedTimeRemaining = Math.max(0, remainingMs); + } else if (progress >= 100) { + file.estimatedTimeRemaining = 0; + } } return file; }), diff --git a/frontend/src/types/File.type.ts b/frontend/src/types/File.type.ts index f50cd9db9..cb0b0c15f 100644 --- a/frontend/src/types/File.type.ts +++ b/frontend/src/types/File.type.ts @@ -1,4 +1,8 @@ -export type FileUpload = File & { uploadingProgress: number }; +export type FileUpload = File & { + uploadingProgress: number; + uploadStartTime?: number; + estimatedTimeRemaining?: number; +}; export type FileUploadResponse = { id: string; name: string }; diff --git a/frontend/src/utils/time.util.ts b/frontend/src/utils/time.util.ts new file mode 100644 index 000000000..80f667f6f --- /dev/null +++ b/frontend/src/utils/time.util.ts @@ -0,0 +1,20 @@ +/** + * Formats a time in milliseconds to a human-readable string + * @param ms Time in milliseconds + * @returns Formatted time string (e.g. "5h 30m" or "2m 15s") + */ +export const formatTimeRemaining = (ms: number): string => { + if (ms <= 0) return "0s"; + + const seconds = Math.floor(ms / 1000) % 60; + const minutes = Math.floor(ms / (1000 * 60)) % 60; + const hours = Math.floor(ms / (1000 * 60 * 60)); + + if (hours > 0) { + return `${hours}h ${minutes}m`; + } else if (minutes > 0) { + return `${minutes}m ${seconds}s`; + } else { + return `${seconds}s`; + } +};