import {useState, useEffect, useRef} from 'react' import PropTypes from 'prop-types' import List from '@mui/material/List' 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 {useColorScheme, styled} from '@mui/material/styles' import Table from '@mui/material/Table' import TableHead from '@mui/material/TableHead' import TableBody from '@mui/material/TableBody' 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 DescriptionIcon from '@mui/icons-material/Description' import LibraryMusicIcon from '@mui/icons-material/LibraryMusic' import {Link} from '@mui/material' const apiUrl = process.env.NEXT_PUBLIC_API_URL_ROOT || 'http://localhost:1337' const audioMetaCache = {} const StyledTableCell = styled(TableCell)(({theme}) => ({ [`&.${tableCellClasses.head}`]: { backgroundColor: theme.palette.common.black, color: theme.palette.common.white, }, [`&.${tableCellClasses.body}`]: { fontSize: 14, }, })) const StyledTableRow = styled(TableRow)(({theme}) => ({ '&:nth-of-type(odd)': { backgroundColor: theme.palette.action.hover, }, '&:last-child td, &:last-child th': { border: 0, }, })) function formatSize(size) { if (size < 1000) { return Math.round(size) + ' Kb' } const mbSize = size / 1000 return Math.round(mbSize) + ' Mb' } export default function FilesList({files}) { const {mode} = useColorScheme() const [audioMeta, setAudioMeta] = useState(audioMetaCache) const [downloading, setDownloading] = useState({}) const controllersRef = useRef({}) const musicFiles = files.filter(file => file.mime.startsWith('audio')) const pdfFiles = files.filter(file => file.mime === 'application/pdf') const sortedMusicFiles = musicFiles.sort((a, b) => { const extensionOrder = { '.flac': 0, '.ogg': 1, '.aac': 2, '.mp3': 3 } return extensionOrder[a.ext.toLowerCase()] - extensionOrder[b.ext.toLowerCase()] }) useEffect(() => { const audioFiles = files.filter(f => f.mime.startsWith('audio')) if (audioFiles.length === 0) return let cancelled = false async function fetchAllMeta() { const mm = await import('music-metadata-browser') const results = {} await Promise.all( audioFiles.map(async file => { if (file.id in audioMetaCache) { results[file.id] = audioMetaCache[file.id] return } try { const response = await fetch(`${apiUrl}${file.url}`, { headers: {Range: 'bytes=0-32767'}, }) const buffer = await response.arrayBuffer() const meta = await mm.parseBuffer(new Uint8Array(buffer), {mimeType: file.mime, skipCovers: true}) audioMetaCache[file.id] = meta.format results[file.id] = meta.format } catch { audioMetaCache[file.id] = null results[file.id] = null } }) ) if (!cancelled) setAudioMeta(results) } fetchAllMeta() return () => { cancelled = true } }, [files]) const losslessFormats = new Set(['.flac', '.wav', '.aiff', '.alac']) const qualityBadge = ext => { const isHaute = losslessFormats.has(ext.toLowerCase()) return ( {isHaute ? 'Haute' : 'Faible'} ) } const renderMeta = file => { const format = file.ext.replace('.', '').toUpperCase() if (!(file.id in audioMeta)) { return } const meta = audioMeta[file.id] const sampleRate = meta?.sampleRate ? `${meta.sampleRate / 1000} kHz` : null const bitDepth = meta?.bitsPerSample ? `${meta.bitsPerSample} bits` : null const bitrate = meta?.bitrate ? `${Math.round(meta.bitrate / 1000)} kbps` : null const details = [sampleRate, bitDepth ?? bitrate].filter(Boolean).join(' ยท ') return ( {qualityBadge(file.ext)} {format} {details && {details}} ) } useEffect(() => () => { Object.values(controllersRef.current).forEach(c => c.abort()) }, []) const handleClick = async (e, url, fileName, fileId) => { e.stopPropagation() if (fileId in downloading) return const controller = new AbortController() controllersRef.current[fileId] = controller setDownloading(prev => ({...prev, [fileId]: 0})) try { const response = await fetch(url, {signal: controller.signal}) 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) } catch (error) { if (error.name !== 'AbortError') throw error } finally { delete controllersRef.current[fileId] setDownloading(prev => { const next = {...prev} delete next[fileId] return next }) } } const handleCancel = (e, fileId) => { e.stopPropagation() controllersRef.current[fileId]?.abort() } return ( <> {musicFiles.length > 0 && ( Musiques FORMAT {sortedMusicFiles.map(file => ( {renderMeta(file)} 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]} % handleCancel(e, file.id)} > Annuler )} ))}
)} {pdfFiles.length > 0 && ( Paroles LANGUE {pdfFiles.map(file => ( {file.caption} handleClick(e, `${apiUrl}${file.url}`, file.name)} > {file.name} ({formatSize(file.size)}) ))}
)} ) } FilesList.propTypes = { files: PropTypes.array.isRequired }