mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 07:15:47 +00:00
4b1dd3c29f
Findings 5-8 of the dedup audit: - F5 byte formatters: appUpdaterHelpers.fmtBytes + ZipDownloadOverlay .formatMB route through the existing formatBytes; a new formatMb (always-MB) backs playlistDetailHelpers.formatSize, AlbumHeader and the 4 inline DeviceSyncPreSyncModal expressions. SongInfoModal.format Size is intentionally left — it uses decimal (1e6) divisors, not 1024. - F6 sanitizeHtml: extracted to utils/sanitizeHtml.ts; AlbumHeader, ComposerDetail and the (now-empty, deleted) artistDetailHelpers use it directly. nowPlayingHelpers keeps its own export but now delegates to the shared sanitiser and only adds its trailing-link strip on top. - F7 album duration: BecauseYouLikeRail's formatAlbumDuration drops in favour of the shared formatHumanHoursMinutes. Behaviour note: total minutes now floor instead of round (<=1 min display difference, matches every other caller). - F8 clock time: extracted to utils/format/formatClockTime.ts; PlaybackDelayModal + QueueHeader use it (toLocaleTimeString and Intl.DateTimeFormat produced identical output). Behaviour preserved except the two explicitly noted divergences (F7 round->floor; F5 appUpdater/Zip now show GB above 1 GB instead of a large MB number).
75 lines
2.6 KiB
TypeScript
75 lines
2.6 KiB
TypeScript
import { useEffect } from 'react';
|
|
import { createPortal } from 'react-dom';
|
|
import { HardDriveDownload, Check, X } from 'lucide-react';
|
|
import { useZipDownloadStore } from '../store/zipDownloadStore';
|
|
import { formatBytes } from '../utils/format/formatBytes';
|
|
|
|
function ZipDownloadItem({ id }: { id: string }) {
|
|
const dismiss = useZipDownloadStore(s => s.dismiss);
|
|
const item = useZipDownloadStore(s => s.downloads.find(d => d.id === id));
|
|
|
|
// Auto-dismiss 3 s after completion or error.
|
|
useEffect(() => {
|
|
if (!item?.done && !item?.error) return;
|
|
const timer = setTimeout(() => dismiss(id), 3000);
|
|
return () => clearTimeout(timer);
|
|
}, [item?.done, item?.error, id, dismiss]);
|
|
|
|
if (!item) return null;
|
|
|
|
const pct = item.total && item.total > 0
|
|
? Math.min(100, (item.bytes / item.total) * 100)
|
|
: null;
|
|
|
|
const isIndeterminate = !item.done && !item.error && (item.total === null || item.total === 0);
|
|
|
|
return (
|
|
<div className={`zip-dl-item${item.done ? ' zip-dl-done' : item.error ? ' zip-dl-error' : ''}`}>
|
|
<div className="zip-dl-header">
|
|
{item.done
|
|
? <Check size={13} />
|
|
: item.error
|
|
? <X size={13} />
|
|
: <HardDriveDownload size={13} className="spin-slow" />
|
|
}
|
|
<span className="zip-dl-name" data-tooltip={item.filename} data-tooltip-pos="top">{item.filename}</span>
|
|
{(item.done || item.error) && (
|
|
<button className="zip-dl-close" onClick={() => dismiss(id)} aria-label="Close">
|
|
<X size={10} />
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{!item.done && !item.error && (
|
|
<>
|
|
<div className="zip-dl-info">
|
|
{formatBytes(item.bytes)}
|
|
{item.total !== null && item.total > 0 && (
|
|
<> / {formatBytes(item.total)} ({pct!.toFixed(0)}%)</>
|
|
)}
|
|
</div>
|
|
<div className={`zip-dl-track${isIndeterminate ? ' zip-dl-indeterminate' : ''}`}>
|
|
{!isIndeterminate && pct !== null && (
|
|
<div className="zip-dl-fill" style={{ width: `${pct}%` }} />
|
|
)}
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function ZipDownloadOverlay() {
|
|
// Subscribe to the array reference directly — never derive a new array in the selector
|
|
// (selector returning new array on every call causes an infinite re-render loop).
|
|
const downloads = useZipDownloadStore(s => s.downloads);
|
|
if (downloads.length === 0) return null;
|
|
|
|
return createPortal(
|
|
<div className="zip-dl-overlay">
|
|
{downloads.map(d => <ZipDownloadItem key={d.id} id={d.id} />)}
|
|
</div>,
|
|
document.body,
|
|
);
|
|
}
|