Scrapped the bespoke centered .vdpg- design and reused the music downloads page's .adl-* classes for real parity: full-width .adl-layout, the segmented .adl-filter-pills, the title with the accent download glyph, and the compact .adl-row (44px art tile + .adl-row-info title/meta/error + .adl-row-status dot+label) — driven by the video JS via data-vdpg-* hooks (no #id clashes with the music page). Per-row cancel reuses the music hover-reveal .adl-row-cancel; retry mirrors it in accent. Kept the smooth in-place patching (slim progress line for active rows). Removed the old .vdpg-* block; balance clean.
219 lines
12 KiB
JavaScript
219 lines
12 KiB
JavaScript
/*
|
|
* SoulSync — Video Downloads page.
|
|
*
|
|
* Reuses the music downloads page's .adl-* layout + look (full-width, segmented
|
|
* filter pills, compact rows, status dots) for visual parity, driven by video data
|
|
* via data-vdpg-* hooks. Filter tabs, per-row cancel + retry, cancel-all, clear.
|
|
* Rows are created ONCE and patched in place so progress glides and nothing blinks.
|
|
*/
|
|
(function () {
|
|
'use strict';
|
|
|
|
var URL_ACTIVE = '/api/video/downloads/active';
|
|
var URL_CLEAR = '/api/video/downloads/clear';
|
|
var URL_CANCEL = '/api/video/downloads/cancel';
|
|
var URL_RETRY = '/api/video/downloads/retry';
|
|
var _timer = null, _wired = false, _filter = 'all';
|
|
var _cards = {};
|
|
|
|
function esc(s) {
|
|
return String(s == null ? '' : s)
|
|
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
}
|
|
function toast(m, t) { if (typeof showToast === 'function') showToast(m, t); }
|
|
function getJSON(u) {
|
|
return fetch(u, { headers: { Accept: 'application/json' } })
|
|
.then(function (r) { return r.ok ? r.json() : null; }).catch(function () { return null; });
|
|
}
|
|
function postJSON(u, b) {
|
|
return fetch(u, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
body: JSON.stringify(b || {}) }).then(function (r) { return r.ok ? r.json() : null; }).catch(function () { return null; });
|
|
}
|
|
|
|
var KIND_ICON = { movie: '🎬', show: '📺', episode: '📺', season: '📺', series: '📺', youtube: '▶️' };
|
|
// status -> { label, cls } where cls is the music .adl-row-/.adl-status-dot class
|
|
var STATUS = {
|
|
downloading: { label: 'Downloading', cls: 'active' },
|
|
queued: { label: 'Queued', cls: 'queued' },
|
|
completed: { label: 'Completed', cls: 'completed' },
|
|
failed: { label: 'Failed', cls: 'failed' },
|
|
cancelled: { label: 'Cancelled', cls: 'cancelled' }
|
|
};
|
|
function isActive(s) { return s === 'downloading' || s === 'queued'; }
|
|
function isFail(s) { return s === 'failed' || s === 'cancelled'; }
|
|
function matches(s) {
|
|
return _filter === 'all' || (_filter === 'active' && isActive(s)) ||
|
|
(_filter === 'completed' && s === 'completed') || (_filter === 'failed' && isFail(s));
|
|
}
|
|
function fmtSize(bytes) {
|
|
var gb = (bytes || 0) / (1024 * 1024 * 1024);
|
|
return gb >= 0.1 ? (Math.round(gb * 10) / 10) + ' GB' : Math.round((bytes || 0) / (1024 * 1024)) + ' MB';
|
|
}
|
|
|
|
var X_SVG = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>';
|
|
var R_SVG = '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>';
|
|
|
|
function makeCard(d) {
|
|
var el = document.createElement('div');
|
|
el.className = 'adl-row';
|
|
el.setAttribute('data-dl-id', d.id);
|
|
el.innerHTML =
|
|
'<div class="adl-row-art adl-row-art-empty vdpg-art" data-f="ic"></div>' +
|
|
'<div class="adl-row-info">' +
|
|
'<div class="adl-row-title" data-f="name"></div>' +
|
|
'<div class="adl-row-meta" data-f="meta"></div>' +
|
|
'<div class="adl-row-error" data-f="error" style="display:none"></div>' +
|
|
'<div class="vdpg-prog" data-f="bar" style="display:none"><div class="vdpg-prog-fill" data-f="fill"></div></div>' +
|
|
'</div>' +
|
|
'<div class="adl-row-status" data-f="status"><span class="adl-status-dot" data-f="dot"></span><span data-f="label"></span></div>' +
|
|
'<div class="vdpg-rowact" data-f="actions"></div>';
|
|
return el;
|
|
}
|
|
|
|
function patchCard(el, d) {
|
|
var info = STATUS[d.status] || STATUS.downloading;
|
|
var cls = info.cls, active = isActive(d.status);
|
|
var pct = Math.max(0, Math.min(100, d.progress || 0));
|
|
var q = function (f) { return el.querySelector('[data-f="' + f + '"]'); };
|
|
|
|
var want = 'adl-row adl-row-' + cls;
|
|
if (el.className !== want) el.className = want;
|
|
var ic = q('ic'); var icon = KIND_ICON[(d.kind || '').toLowerCase()] || '🎬';
|
|
if (ic.textContent !== icon) ic.textContent = icon;
|
|
var name = d.title || d.release_title || 'Download';
|
|
var nm = q('name'); if (nm.textContent !== name) nm.textContent = name;
|
|
|
|
// one compact meta line (music-style), context-dependent
|
|
var meta;
|
|
if (d.status === 'completed' && d.dest_path) meta = '→ ' + d.dest_path;
|
|
else {
|
|
var bits = [];
|
|
if (d.release_title && d.release_title !== name) bits.push(d.release_title);
|
|
else bits.push(fmtSize(d.size_bytes));
|
|
if (d.username) bits.push('👤 ' + d.username);
|
|
if (active) bits.push(Math.round(pct) + '%');
|
|
meta = bits.join(' · ');
|
|
}
|
|
var mt = q('meta');
|
|
if (mt.textContent !== meta) mt.textContent = meta;
|
|
mt.classList.toggle('vdpg-dest', d.status === 'completed' && !!d.dest_path);
|
|
|
|
var err = q('error');
|
|
var errTxt = isFail(d.status) && d.error ? d.error : '';
|
|
if (err.textContent !== errTxt) err.textContent = errTxt;
|
|
err.style.display = errTxt ? '' : 'none';
|
|
|
|
var bar = q('bar');
|
|
bar.style.display = active ? '' : 'none';
|
|
if (active) q('fill').style.width = pct + '%';
|
|
|
|
var st = q('status'); var stWant = 'adl-row-status ' + cls;
|
|
if (st.className !== stWant) st.className = stWant;
|
|
var dot = q('dot'); var dotWant = 'adl-status-dot ' + cls;
|
|
if (dot.className !== dotWant) dot.className = dotWant;
|
|
var lab = q('label'); if (lab.textContent !== info.label) lab.textContent = info.label;
|
|
|
|
var act = q('actions');
|
|
var actHTML = active
|
|
? '<button class="adl-row-cancel" type="button" data-vdpg-cancel="' + d.id + '" title="Cancel">' + X_SVG + '</button>'
|
|
: isFail(d.status)
|
|
? '<button class="vdpg-row-retry" type="button" data-vdpg-retry="' + d.id + '" title="Retry">' + R_SVG + '</button>'
|
|
: '';
|
|
if (act.innerHTML !== actHTML) act.innerHTML = actHTML;
|
|
}
|
|
|
|
function render(list) {
|
|
var host = document.querySelector('[data-vdpg-list]'); if (!host) return;
|
|
list = list || [];
|
|
var empty = host.querySelector('[data-vdpg-empty]');
|
|
|
|
var counts = { all: list.length, active: 0, completed: 0, failed: 0 };
|
|
list.forEach(function (d) {
|
|
if (isActive(d.status)) counts.active++;
|
|
else if (d.status === 'completed') counts.completed++;
|
|
else counts.failed++;
|
|
});
|
|
var cancelAll = document.querySelector('[data-vdpg-cancel-all]'); if (cancelAll) cancelAll.style.display = counts.active ? '' : 'none';
|
|
var clearBtn = document.querySelector('[data-vdpg-clear]'); if (clearBtn) clearBtn.style.display = (counts.completed + counts.failed) ? '' : 'none';
|
|
var sub = document.querySelector('[data-vdpg-sub]');
|
|
if (sub) {
|
|
var parts = [];
|
|
if (counts.active) parts.push(counts.active + ' active');
|
|
if (counts.completed) parts.push(counts.completed + ' done');
|
|
if (counts.failed) parts.push(counts.failed + ' failed');
|
|
sub.textContent = parts.join(' · ');
|
|
}
|
|
|
|
var seen = {}, shown = 0;
|
|
list.forEach(function (d) {
|
|
seen[d.id] = true;
|
|
var el = _cards[d.id] || (_cards[d.id] = makeCard(d));
|
|
patchCard(el, d);
|
|
var vis = matches(d.status);
|
|
el.style.display = vis ? '' : 'none';
|
|
if (vis) shown++;
|
|
host.appendChild(el); // keep order = server order (active first); no re-anim
|
|
});
|
|
Object.keys(_cards).forEach(function (id) {
|
|
if (!seen[id]) { var e = _cards[id]; if (e && e.parentNode) e.parentNode.removeChild(e); delete _cards[id]; }
|
|
});
|
|
|
|
if (empty) {
|
|
host.appendChild(empty); // keep the empty element last
|
|
empty.style.display = shown === 0 ? '' : 'none';
|
|
empty.textContent = !list.length ? "No downloads yet. Hit Grab on a search result and it'll show up here."
|
|
: 'Nothing ' + (_filter === 'all' ? 'here' : _filter) + ' right now.';
|
|
}
|
|
}
|
|
|
|
function setFilter(f) {
|
|
_filter = f;
|
|
Array.prototype.forEach.call(document.querySelectorAll('[data-vdpg-filter]'), function (b) {
|
|
b.classList.toggle('active', b.getAttribute('data-vdpg-filter') === f);
|
|
});
|
|
getJSON(URL_ACTIVE).then(function (d) { if (d) render(d.downloads || []); });
|
|
}
|
|
|
|
function anyActive() { return !!document.querySelector('.adl-row.adl-row-active, .adl-row.adl-row-queued'); }
|
|
|
|
function poll() { getJSON(URL_ACTIVE).then(function (d) { if (d) render(d.downloads || []); schedule(); }); }
|
|
function schedule() { if (_timer) clearTimeout(_timer); _timer = setTimeout(poll, anyActive() ? 1500 : 6000); }
|
|
function start() { wire(); if (_timer) clearTimeout(_timer); poll(); }
|
|
function stop() { if (_timer) { clearTimeout(_timer); _timer = null; } }
|
|
|
|
function wire() {
|
|
if (_wired) return; _wired = true;
|
|
var clearBtn = document.querySelector('[data-vdpg-clear]');
|
|
if (clearBtn) clearBtn.addEventListener('click', function () {
|
|
postJSON(URL_CLEAR, {}).then(function () { toast('Cleared finished downloads', 'success'); poll(); });
|
|
});
|
|
var cancelAll = document.querySelector('[data-vdpg-cancel-all]');
|
|
if (cancelAll) cancelAll.addEventListener('click', function () {
|
|
getJSON(URL_ACTIVE).then(function (d) {
|
|
var ids = ((d && d.downloads) || []).filter(function (x) { return isActive(x.status); }).map(function (x) { return x.id; });
|
|
Promise.all(ids.map(function (id) { return postJSON(URL_CANCEL, { id: id }); }))
|
|
.then(function () { toast('Cancelled ' + ids.length + ' download' + (ids.length === 1 ? '' : 's'), 'info'); poll(); });
|
|
});
|
|
});
|
|
var pills = document.querySelector('[data-vdpg-pills]');
|
|
if (pills) pills.addEventListener('click', function (e) {
|
|
var b = e.target.closest('[data-vdpg-filter]'); if (b) setFilter(b.getAttribute('data-vdpg-filter'));
|
|
});
|
|
var list = document.querySelector('[data-vdpg-list]');
|
|
if (list) list.addEventListener('click', function (e) {
|
|
var c = e.target.closest('[data-vdpg-cancel]');
|
|
if (c) { c.disabled = true; c.classList.add('adl-row-cancel-pending'); postJSON(URL_CANCEL, { id: +c.getAttribute('data-vdpg-cancel') }).then(function () { poll(); }); return; }
|
|
var r = e.target.closest('[data-vdpg-retry]');
|
|
if (r) { r.disabled = true; postJSON(URL_RETRY, { id: +r.getAttribute('data-vdpg-retry') }).then(function (res) {
|
|
if (res && res.ok) toast('Retrying', 'info'); else toast((res && res.error) || 'Retry failed', 'error'); poll(); }); }
|
|
});
|
|
}
|
|
|
|
document.addEventListener('soulsync:video-page-shown', function (e) {
|
|
if (e.detail === 'video-downloads') start(); else stop();
|
|
});
|
|
document.addEventListener('soulsync:video-download-started', function () {
|
|
if (document.querySelector('[data-video-subpage="video-downloads"]:not([hidden])')) setTimeout(poll, 350);
|
|
});
|
|
window._vdpgAnyActive = anyActive;
|
|
})();
|