From cd3cc2a697eac3996946911566b52cb87f28c9e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20FAMIBELLE-PRONZOLA?= Date: Mon, 1 Jun 2026 00:52:41 +0400 Subject: [PATCH] fix: explicit download --- components/files/files-list.js | 55 ++++++++++++++++++++++++++++++---- 1 file changed, 50 insertions(+), 5 deletions(-) diff --git a/components/files/files-list.js b/components/files/files-list.js index 9355e51..66ffd8f 100644 --- a/components/files/files-list.js +++ b/components/files/files-list.js @@ -5,6 +5,7 @@ import ListSubheader from '@mui/material/ListSubheader' import Typography from '@mui/material/Typography' import Box from '@mui/material/Box' import Skeleton from '@mui/material/Skeleton' +import LinearProgress from '@mui/material/LinearProgress' import {useTheme, useColorScheme, styled} from '@mui/material/styles' import Table from '@mui/material/Table' import TableHead from '@mui/material/TableHead' @@ -13,7 +14,6 @@ import TableCell, {tableCellClasses} from '@mui/material/TableCell' import TableRow from '@mui/material/TableRow' import TableContainer from '@mui/material/TableContainer' import Paper from '@mui/material/Paper' -import FileSaver from 'file-saver' import DescriptionIcon from '@mui/icons-material/Description' import LibraryMusicIcon from '@mui/icons-material/LibraryMusic' import {Link} from '@mui/material' @@ -53,6 +53,7 @@ export default function FilesList({files}) { const theme = useTheme() const {mode} = useColorScheme() const [audioMeta, setAudioMeta] = useState(audioMetaCache) + const [downloading, setDownloading] = useState({}) const musicFiles = files.filter(file => file.mime.startsWith('audio')) const pdfFiles = files.filter(file => file.mime === 'application/pdf') @@ -148,9 +149,42 @@ export default function FilesList({files}) { ) } - const handleClick = (e, url, fileName) => { + const handleClick = async (e, url, fileName, fileId) => { e.stopPropagation() - FileSaver.saveAs(url, fileName) + if (fileId in downloading) return + + setDownloading(prev => ({...prev, [fileId]: 0})) + try { + const response = await fetch(url) + const contentLength = +response.headers.get('content-length') + const reader = response.body.getReader() + const chunks = [] + let received = 0 + + while (true) { + const {done, value} = await reader.read() + if (done) break + chunks.push(value) + received += value.length + if (contentLength) { + setDownloading(prev => ({...prev, [fileId]: Math.round(received / contentLength * 100)})) + } + } + + const blob = new Blob(chunks, {type: response.headers.get('content-type')}) + const blobUrl = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = blobUrl + a.download = fileName + a.click() + URL.revokeObjectURL(blobUrl) + } finally { + setDownloading(prev => { + const next = {...prev} + delete next[fileId] + return next + }) + } } return ( @@ -185,13 +219,24 @@ export default function FilesList({files}) { handleClick(e, `${apiUrl}${file.url}`, file.name)} + onClick={e => handleClick(e, `${apiUrl}${file.url}`, file.name, file.id)} > {file.name} ({formatSize(file.size)}) + {file.id in downloading && ( + + 0 ? 'determinate' : 'indeterminate'} + value={downloading[file.id]} + /> + + {downloading[file.id]} % + + + )} ))}