mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 07:15:47 +00:00
ba670bd1e8
ZIP downloads (PlaylistDetail, Albums, NewReleases, RandomAlbums) now use
invoke('download_zip') via Rust streaming instead of fetch+blob+arrayBuffer,
eliminating JS heap saturation on large files. Progress shown in ZipDownloadOverlay.
Offline cache downloads (600+ songs) no longer freeze the UI: transient job
state moved to a separate non-persisted offlineJobStore, reducing localStorage
writes from ~1200 down to 2 for an entire download. Also adds playlist offline
toggle — clicking the cache button when already cached removes it from cache.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
79 lines
2.7 KiB
TypeScript
79 lines
2.7 KiB
TypeScript
import { useEffect } from 'react';
|
|
import { createPortal } from 'react-dom';
|
|
import { HardDriveDownload, Check, X } from 'lucide-react';
|
|
import { useZipDownloadStore } from '../store/zipDownloadStore';
|
|
|
|
function formatMB(bytes: number): string {
|
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
|
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
}
|
|
|
|
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">
|
|
{formatMB(item.bytes)}
|
|
{item.total !== null && item.total > 0 && (
|
|
<> / {formatMB(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,
|
|
);
|
|
}
|