Video get-modal: owned episodes re-downloadable + 'Add to watchlist' offer
- Owned episodes are now SELECTABLE (checkbox, not pre-checked) so you can re-download a season/episode you already have; flip 'Missing only' off to see them. Every season (incl. fully-owned) gets a select-all. Select-all + the season checkbox state respect the 'Missing only' filter (no silently grabbing hidden owned episodes). - Airing show you don't follow yet → an accent 'Add to watchlist' tickbox (default on) so grabbing episodes also starts you watching for new ones. Hidden for ended shows + shows you already follow. The Add-to-Wishlist stub toast notes '+ Watchlist' when it's ticked.
This commit is contained in:
parent
e24e9bfe1e
commit
419b939999
2 changed files with 67 additions and 16 deletions
|
|
@ -91,6 +91,7 @@
|
||||||
'<div class="vgm-body">' +
|
'<div class="vgm-body">' +
|
||||||
'<p class="vgm-overview" data-vgm-overview>Loading details…</p>' +
|
'<p class="vgm-overview" data-vgm-overview>Loading details…</p>' +
|
||||||
'<div class="vgm-eps" data-vgm-eps hidden></div>' +
|
'<div class="vgm-eps" data-vgm-eps hidden></div>' +
|
||||||
|
'<div class="vgm-follow" data-vgm-follow hidden></div>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'<div class="vgm-actions">' +
|
'<div class="vgm-actions">' +
|
||||||
'<span class="vgm-sel-count" data-vgm-count></span>' +
|
'<span class="vgm-sel-count" data-vgm-count></span>' +
|
||||||
|
|
@ -123,19 +124,22 @@
|
||||||
if (e.target.closest('[data-vgm-wishlist]') || e.target.closest('[data-vgm-download]')) {
|
if (e.target.closest('[data-vgm-wishlist]') || e.target.closest('[data-vgm-download]')) {
|
||||||
// v1: visual only — real wishlist/download is a later phase.
|
// v1: visual only — real wishlist/download is a later phase.
|
||||||
var n = modalState ? modalState.sel.size : 0;
|
var n = modalState ? modalState.sel.size : 0;
|
||||||
var what = e.target.closest('[data-vgm-download]') ? 'Download' : 'Wishlist';
|
var isDl = !!e.target.closest('[data-vgm-download]');
|
||||||
toast((modalState ? (n + ' episode' + (n === 1 ? '' : 's') + ' — ') : '') + what + ' coming soon', 'info');
|
var aw = ov.querySelector('[data-vgm-add-watch]');
|
||||||
|
var follow = (!isDl && aw && aw.checked) ? ' + Watchlist' : '';
|
||||||
|
toast((modalState ? (n + ' episode' + (n === 1 ? '' : 's') + ' — ') : '') +
|
||||||
|
(isDl ? 'Download' : 'Wishlist') + follow + ' coming soon', 'info');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
ov.addEventListener('change', function (e) {
|
ov.addEventListener('change', function (e) {
|
||||||
var sa = e.target.closest('[data-vgm-season-all]');
|
var sa = e.target.closest('[data-vgm-season-all]');
|
||||||
if (sa) { // season select-all → toggle all its selectable episodes
|
if (sa) { // season select-all → toggle its currently-actionable episodes
|
||||||
var sn = sa.getAttribute('data-vgm-season-all');
|
var sn = sa.getAttribute('data-vgm-season-all');
|
||||||
var boxes = ov.querySelectorAll('.vgm-ep-cb[data-vgm-ep^="' + sn + '_"]');
|
sa.indeterminate = false;
|
||||||
for (var i = 0; i < boxes.length; i++) {
|
seasonBoxes(ov, sn).forEach(function (b) {
|
||||||
boxes[i].checked = sa.checked;
|
b.checked = sa.checked;
|
||||||
toggleSel(boxes[i].getAttribute('data-vgm-ep'), sa.checked);
|
toggleSel(b.getAttribute('data-vgm-ep'), sa.checked);
|
||||||
}
|
});
|
||||||
updateFooter();
|
updateFooter();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -149,6 +153,9 @@
|
||||||
if (e.target.closest('[data-vgm-missing-only]')) {
|
if (e.target.closest('[data-vgm-missing-only]')) {
|
||||||
var wrap = ov.querySelector('[data-vgm-eps]');
|
var wrap = ov.querySelector('[data-vgm-eps]');
|
||||||
if (wrap) wrap.classList.toggle('vgm-eps--missing-only', e.target.checked);
|
if (wrap) wrap.classList.toggle('vgm-eps--missing-only', e.target.checked);
|
||||||
|
// the actionable set changed → re-derive every season checkbox
|
||||||
|
var sas = ov.querySelectorAll('[data-vgm-season-all]');
|
||||||
|
for (var k = 0; k < sas.length; k++) syncSeasonCheck(ov, sas[k].getAttribute('data-vgm-season-all'));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
keyHandler = function (e) { if (e.key === 'Escape') closeModal(); };
|
keyHandler = function (e) { if (e.key === 'Escape') closeModal(); };
|
||||||
|
|
@ -165,13 +172,26 @@
|
||||||
|
|
||||||
function toggleSel(key, on) { if (!modalState) return; if (on) modalState.sel.add(key); else modalState.sel.delete(key); }
|
function toggleSel(key, on) { if (!modalState) return; if (on) modalState.sel.add(key); else modalState.sel.delete(key); }
|
||||||
|
|
||||||
|
function missingOnlyOn(ov) { var m = ov.querySelector('[data-vgm-missing-only]'); return !m || m.checked; }
|
||||||
|
// Episode checkboxes for a season that the season-select-all should act on:
|
||||||
|
// when "Missing only" is on, owned episodes are hidden, so exclude them.
|
||||||
|
function seasonBoxes(ov, sn) {
|
||||||
|
var skipOwned = missingOnlyOn(ov);
|
||||||
|
var all = ov.querySelectorAll('.vgm-ep-cb[data-vgm-ep^="' + sn + '_"]'), out = [];
|
||||||
|
for (var i = 0; i < all.length; i++) {
|
||||||
|
if (skipOwned && all[i].closest('.vgm-ep--owned')) continue;
|
||||||
|
out.push(all[i]);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
function syncSeasonCheck(ov, sn) {
|
function syncSeasonCheck(ov, sn) {
|
||||||
var all = ov.querySelector('[data-vgm-season-all="' + sn + '"]'); if (!all) return;
|
var all = ov.querySelector('[data-vgm-season-all="' + sn + '"]'); if (!all) return;
|
||||||
var boxes = ov.querySelectorAll('.vgm-ep-cb[data-vgm-ep^="' + sn + '_"]');
|
var boxes = seasonBoxes(ov, sn);
|
||||||
var checked = 0;
|
var checked = 0;
|
||||||
for (var i = 0; i < boxes.length; i++) if (boxes[i].checked) checked++;
|
for (var i = 0; i < boxes.length; i++) if (boxes[i].checked) checked++;
|
||||||
all.checked = checked === boxes.length && boxes.length > 0;
|
all.checked = boxes.length > 0 && checked === boxes.length;
|
||||||
all.indeterminate = checked > 0 && checked < boxes.length;
|
all.indeterminate = checked > 0 && checked < boxes.length;
|
||||||
|
all.disabled = boxes.length === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateFooter() {
|
function updateFooter() {
|
||||||
|
|
@ -195,9 +215,11 @@
|
||||||
var st = epState(e, today);
|
var st = epState(e, today);
|
||||||
var key = snum + '_' + e.episode_number;
|
var key = snum + '_' + e.episode_number;
|
||||||
var date = e.air_date ? fmtDate(e.air_date) : '';
|
var date = e.air_date ? fmtDate(e.air_date) : '';
|
||||||
var ctrl = (st === 'missing')
|
// missing-aired -> pre-checked; owned -> selectable for re-download (not
|
||||||
? '<input type="checkbox" class="vgm-ep-cb" data-vgm-ep="' + esc(key) + '" checked>'
|
// pre-checked); upcoming -> locked (can't grab what hasn't aired).
|
||||||
: '<span class="vgm-ep-lock">' + (st === 'owned' ? '✓' : '◷') + '</span>';
|
var ctrl = (st === 'upcoming')
|
||||||
|
? '<span class="vgm-ep-lock">◷</span>'
|
||||||
|
: '<input type="checkbox" class="vgm-ep-cb" data-vgm-ep="' + esc(key) + '"' + (st === 'missing' ? ' checked' : '') + '>';
|
||||||
var badge = st === 'owned' ? '<span class="vgm-ep-badge vgm-ep-badge--owned">In library</span>'
|
var badge = st === 'owned' ? '<span class="vgm-ep-badge vgm-ep-badge--owned">In library</span>'
|
||||||
: st === 'upcoming' ? '<span class="vgm-ep-badge vgm-ep-badge--soon">' + (date || 'Upcoming') + '</span>'
|
: st === 'upcoming' ? '<span class="vgm-ep-badge vgm-ep-badge--soon">' + (date || 'Upcoming') + '</span>'
|
||||||
: '<span class="vgm-ep-badge vgm-ep-badge--missing">Missing</span>';
|
: '<span class="vgm-ep-badge vgm-ep-badge--missing">Missing</span>';
|
||||||
|
|
@ -223,8 +245,7 @@
|
||||||
eps.forEach(function (e) { if (epState(e, today) === 'missing') { missing++; modalState.sel.add(s.season_number + '_' + e.episode_number); } });
|
eps.forEach(function (e) { if (epState(e, today) === 'missing') { missing++; modalState.sel.add(s.season_number + '_' + e.episode_number); } });
|
||||||
html += '<div class="vgm-season">' +
|
html += '<div class="vgm-season">' +
|
||||||
'<div class="vgm-season-head" data-vgm-season-toggle>' +
|
'<div class="vgm-season-head" data-vgm-season-toggle>' +
|
||||||
(missing ? '<span class="vgm-season-check"><input type="checkbox" data-vgm-season-all="' + s.season_number + '" checked></span>'
|
'<span class="vgm-season-check"><input type="checkbox" data-vgm-season-all="' + s.season_number + '"' + (missing && missing === eps.length ? ' checked' : '') + '></span>' +
|
||||||
: '<span class="vgm-season-check vgm-season-check--lock">✓</span>') +
|
|
||||||
'<span class="vgm-season-name">' + esc(s.title || ('Season ' + s.season_number)) + '</span>' +
|
'<span class="vgm-season-name">' + esc(s.title || ('Season ' + s.season_number)) + '</span>' +
|
||||||
'<span class="vgm-season-meta">' + (missing ? missing + ' missing · ' : '') + eps.length + ' eps</span>' +
|
'<span class="vgm-season-meta">' + (missing ? missing + ' missing · ' : '') + eps.length + ' eps</span>' +
|
||||||
'<span class="vgm-season-chev" aria-hidden="true">⌄</span>' +
|
'<span class="vgm-season-chev" aria-hidden="true">⌄</span>' +
|
||||||
|
|
@ -236,6 +257,8 @@
|
||||||
wrap.innerHTML = html;
|
wrap.innerHTML = html;
|
||||||
wrap.classList.add('vgm-eps--missing-only');
|
wrap.classList.add('vgm-eps--missing-only');
|
||||||
wrap.hidden = false;
|
wrap.hidden = false;
|
||||||
|
// initial season-checkbox states reflect the (missing-only) selection
|
||||||
|
d.seasons.forEach(function (s) { syncSeasonCheck(modalEl, s.season_number); });
|
||||||
}
|
}
|
||||||
|
|
||||||
function fill(d, o) {
|
function fill(d, o) {
|
||||||
|
|
@ -270,10 +293,28 @@
|
||||||
else { ov.textContent = 'No synopsis available yet.'; ov.classList.add('vgm-overview--none'); }
|
else { ov.textContent = 'No synopsis available yet.'; ov.classList.add('vgm-overview--none'); }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (o.kind === 'show') renderEpisodes(d); // the season/episode selector
|
if (o.kind === 'show') { renderEpisodes(d); maybeFollow(d); } // selector + follow offer
|
||||||
updateFooter();
|
updateFooter();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Airing show you don't follow yet → offer to start watching it (default on),
|
||||||
|
// so grabbing episodes also keeps you current. Hidden for ended shows (can't
|
||||||
|
// "watch" for new) and shows you already follow.
|
||||||
|
function maybeFollow(d) {
|
||||||
|
var fw = modalEl.querySelector('[data-vgm-follow]'); if (!fw) return;
|
||||||
|
if (!d.tmdb_id || !isAiring(d.status)) { fw.hidden = true; return; }
|
||||||
|
fetch('/api/video/watchlist/check', { method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ kind: 'show', tmdb_ids: [d.tmdb_id] }) })
|
||||||
|
.then(function (r) { return r.ok ? r.json() : null; })
|
||||||
|
.then(function (res) {
|
||||||
|
if (!modalEl) return;
|
||||||
|
if (res && res.results && res.results[String(d.tmdb_id)]) { fw.hidden = true; return; }
|
||||||
|
fw.innerHTML = '<label class="vgm-follow-row"><input type="checkbox" data-vgm-add-watch checked>' +
|
||||||
|
'<span class="vgm-follow-txt"><strong>Add to watchlist</strong> — automatically get new episodes as they air</span></label>';
|
||||||
|
fw.hidden = false;
|
||||||
|
}).catch(function () { /* skip the row */ });
|
||||||
|
}
|
||||||
|
|
||||||
// One capture-phase handler — the get button sits inside a card <a>.
|
// One capture-phase handler — the get button sits inside a card <a>.
|
||||||
document.addEventListener('click', function (e) {
|
document.addEventListener('click', function (e) {
|
||||||
var b = e.target.closest && e.target.closest('.vget-btn');
|
var b = e.target.closest && e.target.closest('.vget-btn');
|
||||||
|
|
|
||||||
|
|
@ -2024,3 +2024,13 @@ body[data-side="video"] #soulsync-toggle { display: none; }
|
||||||
.vgm-btn--text { background: transparent; border-color: transparent; color: rgba(255, 255, 255, 0.6); padding: 11px 8px; }
|
.vgm-btn--text { background: transparent; border-color: transparent; color: rgba(255, 255, 255, 0.6); padding: 11px 8px; }
|
||||||
.vgm-btn--text:hover { color: #fff; }
|
.vgm-btn--text:hover { color: #fff; }
|
||||||
.vgm-btn:disabled { opacity: 0.4; cursor: default; transform: none !important; filter: none !important; }
|
.vgm-btn:disabled { opacity: 0.4; cursor: default; transform: none !important; filter: none !important; }
|
||||||
|
|
||||||
|
/* "Add to watchlist" offer in the show get-modal (airing + not-yet-followed) */
|
||||||
|
.vgm-follow { margin-top: 14px; }
|
||||||
|
.vgm-follow[hidden] { display: none; }
|
||||||
|
.vgm-follow-row { display: flex; align-items: center; gap: 11px; padding: 13px 14px; border-radius: 12px; cursor: pointer;
|
||||||
|
background: rgba(var(--accent-rgb, 88 101 242), 0.1); border: 1px solid rgba(var(--accent-rgb, 88 101 242), 0.28); }
|
||||||
|
.vgm-follow-row:hover { background: rgba(var(--accent-rgb, 88 101 242), 0.14); }
|
||||||
|
.vgm-follow-row input { accent-color: rgb(var(--accent-rgb, 88 101 242)); width: 17px; height: 17px; cursor: pointer; flex-shrink: 0; }
|
||||||
|
.vgm-follow-txt { font-size: 13px; color: rgba(255, 255, 255, 0.82); }
|
||||||
|
.vgm-follow-txt strong { color: #fff; font-weight: 800; }
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue