60 lines
5.3 KiB
JavaScript
60 lines
5.3 KiB
JavaScript
/* Download controls and device storage accounting. */
|
|
(function(root) {
|
|
'use strict';
|
|
const actionButtons = typeof module !== 'undefined' ? require('./download-actions.js') : root.DownloadActions;
|
|
const errors = typeof module !== 'undefined' ? require('./download-errors.js') : root.DownloadErrors;
|
|
const bytes = n => n >= 1073741824 ? `${(n / 1073741824).toFixed(1)} GB` : `${(n / 1048576).toFixed(1)} MB`;
|
|
function breakdown(files, playlists) {
|
|
const kinds = { video: 0, audio: 0, eq: 0 }, byId = new Map();
|
|
for (const f of files) { const size = Math.max(0, Number(f.size) || 0); kinds[f.kind in kinds ? f.kind : 'video'] += size; byId.set(f.id, (byId.get(f.id) || 0) + size); }
|
|
return { total: Object.values(kinds).reduce((a, b) => a + b, 0), kinds, playlists: playlists.map(p => ({ id: p.id, name: p.name || p.title || 'Playlist', size: [...new Set((p.videos || []).map(v => v.id))].reduce((n, id) => n + (byId.get(id) || 0), 0) })) };
|
|
}
|
|
let adapter, total = 0;
|
|
function configure(options) {
|
|
adapter = options;
|
|
root.SettingsSections.register({ id: 'downloads-storage', title: 'Downloads & storage', cluster: 'Library & storage', description: 'Active queued paused failed saves, disk usage and playlist sizes', icon: '<path d="M12 3v12m-5-5 5 5 5-5M4 17v4h16v-4"/>', summary: () => `${adapter.slots()} at once · ${bytes(total)}`, render(container) {
|
|
const doc = container.ownerDocument;
|
|
const heading = doc.createElement('h3'); heading.textContent = 'Downloads & storage';
|
|
const jobs = doc.createElement('div'); jobs.className = 'download-manager'; jobs.setAttribute('aria-label', 'Download manager');
|
|
const storage = doc.createElement('div'); storage.className = 'download-storage';
|
|
container.append(heading, jobs, storage);
|
|
let alive = true, signature = '';
|
|
function paint() {
|
|
const items = adapter.jobs(); const next = JSON.stringify(items); if (next === signature) return; signature = next;
|
|
jobs.replaceChildren();
|
|
if (!items.length) { const p = doc.createElement('p'); p.textContent = 'No pending downloads.'; jobs.append(p); }
|
|
for (const job of items) {
|
|
const row = doc.createElement('div'); row.className = 'download-job';
|
|
const title = doc.createElement('b'); title.textContent = job.title || job.id;
|
|
const status = doc.createElement('small'); status.textContent = job.error ? errors.message(job.error) : job.status;
|
|
const progress = doc.createElement('progress'); progress.max = job.progress?.total || 1; if (job.progress?.total) progress.value = job.progress.received || 0; progress.setAttribute('aria-label', `Saving ${job.title || job.id}`);
|
|
const actions = doc.createElement('div');
|
|
for (const action of [...(['active', 'queued'].includes(job.status) ? ['pause'] : [job.status === 'failed' ? 'retry' : 'resume']), 'cancel']) {
|
|
const button = actionButtons.create(doc, action, job.title || job.id); button.onclick = () => { adapter.action(job.id, action); signature = ''; paint(); }; actions.append(button);
|
|
}
|
|
row.append(title, status, progress, actions);
|
|
if (job.error) errors.appendDetails(row, job.error);
|
|
jobs.append(row);
|
|
}
|
|
}
|
|
async function disk() {
|
|
try {
|
|
const [files, estimate] = await Promise.all([adapter.files(), root.navigator.storage?.estimate?.() || {}]); if (!alive) return;
|
|
const stats = breakdown(files, adapter.playlists()); total = stats.total; storage.replaceChildren();
|
|
for (const text of [`Device media: ${bytes(total)}`, `Video ${bytes(stats.kinds.video)} · Audio ${bytes(stats.kinds.audio)} · EQ renders ${bytes(stats.kinds.eq)}`, estimate.quota ? `Available browser storage: ${bytes(Math.max(0, estimate.quota - (estimate.usage || 0)))}` : 'Available browser storage is not reported.', ...stats.playlists.map(p => `${p.name}: ${bytes(p.size)}`), 'Playlist totals include shared files; device totals count each file once. Browser quota is not total device free space.']) { const p = doc.createElement('p'); p.textContent = text; storage.append(p); }
|
|
storage.dispatchEvent(new Event('change', { bubbles: true }));
|
|
} catch { storage.textContent = 'Storage details are unavailable on this device.'; }
|
|
}
|
|
paint(); disk(); const timer = setInterval(paint, 1000), diskTimer = setInterval(disk, 15000);
|
|
return () => { alive = false; clearInterval(timer); clearInterval(diskTimer); };
|
|
} });
|
|
}
|
|
function getJobs() { return adapter && typeof adapter.jobs === 'function' ? adapter.jobs() : []; }
|
|
function action(id, act) { if (adapter && typeof adapter.action === 'function') return adapter.action(id, act); }
|
|
function getFiles() { return adapter && typeof adapter.files === 'function' ? adapter.files() : Promise.resolve([]); }
|
|
function getPlaylists() { return adapter && typeof adapter.playlists === 'function' ? adapter.playlists() : []; }
|
|
function getSlots() { return adapter && typeof adapter.slots === 'function' ? adapter.slots() : 4; }
|
|
function snapshot() { return { jobs: getJobs(), slots: getSlots(), playlists: getPlaylists() }; }
|
|
const api = { configure, breakdown, bytes, getJobs, action, getFiles, getPlaylists, getSlots, snapshot }; if (typeof module !== 'undefined') module.exports = api; else root.Downloads = api;
|
|
})(typeof window !== 'undefined' ? window : globalThis);
|