Video wishlist: the page (Movies grid + grouped TV tree)
Tabbed Movies / TV page (mirrors the watchlist chrome). Movies render as a poster grid with status pill + hover remove. TV groups into collapsible show -> season -> episode rows with wanted/done roll-ups and a remove (x) at every level (episode / season / whole show). Server-paged + searchable; updates the nav + hero badges and listens for soulsync:video-wishlist-changed. Movie cards open detail. Wires the pre-existing Wishlist nav button (added its badge) + subpage container + .vwsh-* styles.
This commit is contained in:
parent
6d3a59c8dc
commit
261416fede
3 changed files with 388 additions and 0 deletions
|
|
@ -349,6 +349,7 @@
|
|||
<a class="nav-button" data-video-page="video-wishlist" href="#">
|
||||
<span class="nav-icon"><svg class="nav-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg></span>
|
||||
<span class="nav-text">Wishlist</span>
|
||||
<span class="dl-nav-badge hidden" data-video-wishlist-badge>0</span>
|
||||
</a>
|
||||
<a class="nav-button" data-video-page="video-downloads" href="#">
|
||||
<span class="nav-icon"><svg class="nav-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg></span>
|
||||
|
|
@ -1038,6 +1039,53 @@
|
|||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- Wishlist (nav page): movies + wanted episodes, split by a
|
||||
Movies/TV tab. TV groups show -> season -> episode. Built by
|
||||
video/video-wishlist.js, styled by .vwsh-* in video-side.css. -->
|
||||
<section class="video-subpage" data-video-subpage="video-wishlist" hidden>
|
||||
<div class="vwsh-page">
|
||||
<div class="vwsh-head">
|
||||
<div class="vwsh-head-glow" aria-hidden="true"></div>
|
||||
<div class="vwsh-head-row">
|
||||
<div class="vwsh-head-titles">
|
||||
<h1 class="vwsh-title">Wishlist</h1>
|
||||
<p class="vwsh-sub">Movies & episodes you want to grab</p>
|
||||
</div>
|
||||
<div class="vwsh-tabs" role="tablist">
|
||||
<button class="vwsh-tab vwsh-tab--on" type="button" role="tab" data-vwsh-tab="movie">
|
||||
Movies <span class="vwsh-tab-n" data-vwsh-count-movie>0</span>
|
||||
</button>
|
||||
<button class="vwsh-tab" type="button" role="tab" data-vwsh-tab="show">
|
||||
TV <span class="vwsh-tab-n" data-vwsh-count-show>0</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="vwsh-toolbar">
|
||||
<div class="vwsh-search">
|
||||
<svg class="vwsh-search-ic" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg>
|
||||
<input type="text" class="vwsh-search-input" data-vwsh-search placeholder="Search wishlist…" autocomplete="off" spellcheck="false">
|
||||
</div>
|
||||
</div>
|
||||
<div class="vwsh-body">
|
||||
<div class="vwsh-loading hidden" data-vwsh-loading>
|
||||
<div class="loading-spinner"></div>
|
||||
<div class="loading-text">Loading wishlist…</div>
|
||||
</div>
|
||||
<div class="vwsh-grid" data-vwsh-grid></div>
|
||||
<div class="vwsh-state hidden" data-vwsh-empty>
|
||||
<div class="vwsh-state-ic">📥</div>
|
||||
<div class="vwsh-state-title" data-vwsh-empty-title>Your wishlist is empty</div>
|
||||
<div class="vwsh-state-sub">Add a movie or episodes from any title’s get button — they queue up here to grab.</div>
|
||||
</div>
|
||||
<div class="vwsh-pagination hidden" data-vwsh-pagination>
|
||||
<button class="vwsh-page-btn" type="button" data-vwsh-prev>‹ Prev</button>
|
||||
<span class="vwsh-page-info" data-vwsh-pageinfo></span>
|
||||
<button class="vwsh-page-btn" type="button" data-vwsh-next>Next ›</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- TV-show detail (drill-in from a show card; not a nav page).
|
||||
Isolated: built by video/video-detail.js, styled by .vd-* in
|
||||
video-side.css. Inspired by the music artist-detail vibe. -->
|
||||
|
|
@ -9658,6 +9706,8 @@
|
|||
<script src="{{ url_for('static', filename='video/video-person.js', v=static_v) }}"></script>
|
||||
<!-- Video watchlist page (isolated; tabbed shows / people you follow) -->
|
||||
<script src="{{ url_for('static', filename='video/video-watchlist.js', v=static_v) }}"></script>
|
||||
<!-- Video wishlist page (isolated; tabbed movies / TV, grouped show tree) -->
|
||||
<script src="{{ url_for('static', filename='video/video-wishlist.js', v=static_v) }}"></script>
|
||||
<!-- Video worker orbs (isolated copy of the music orbs; idles into the
|
||||
floating-orb animation around the SoulSync logo on the video dashboard) -->
|
||||
<script src="{{ url_for('static', filename='video/video-worker-orbs.js', v=static_v) }}"></script>
|
||||
|
|
|
|||
|
|
@ -2415,3 +2415,105 @@ body[data-side="video"] #soulsync-toggle { display: none; }
|
|||
/* keep the trailer close button on-screen (it sits above the frame on desktop) */
|
||||
.vdsc-tr-close { top: 6px; right: 6px; background: rgba(0, 0, 0, 0.65); }
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════════
|
||||
Wishlist page (.vwsh-*) — Movies grid + grouped TV show→season→episode tree
|
||||
══════════════════════════════════════════════════════════════════════════ */
|
||||
.vwsh-page { max-width: 1180px; margin: 0 auto; padding: 0 0 60px; }
|
||||
.vwsh-head { position: relative; padding: 26px 0 16px; }
|
||||
.vwsh-head-glow { position: absolute; top: -20px; left: 0; width: 320px; height: 180px; pointer-events: none;
|
||||
background: radial-gradient(60% 80% at 0% 0%, rgba(var(--accent-rgb, 29, 185, 84), 0.16), transparent 70%); }
|
||||
.vwsh-head-row { position: relative; display: flex; align-items: flex-end; justify-content: space-between; gap: 20px; flex-wrap: wrap; }
|
||||
.vwsh-title { font-size: 30px; font-weight: 900; letter-spacing: -0.025em; color: #fff; margin: 0; }
|
||||
.vwsh-sub { font-size: 14px; color: rgba(255, 255, 255, 0.5); margin: 6px 0 0; }
|
||||
.vwsh-tabs { display: inline-flex; gap: 6px; background: rgba(255, 255, 255, 0.04); padding: 5px;
|
||||
border-radius: 13px; border: 1px solid rgba(255, 255, 255, 0.07); }
|
||||
.vwsh-tab { padding: 9px 18px; border: none; background: none; cursor: pointer; border-radius: 9px;
|
||||
font-size: 13.5px; font-weight: 800; color: rgba(255, 255, 255, 0.55); transition: color 0.15s ease, background 0.15s ease; }
|
||||
.vwsh-tab:hover { color: #fff; }
|
||||
.vwsh-tab--on { color: #fff; background: rgba(var(--accent-rgb, 29, 185, 84), 0.9);
|
||||
box-shadow: 0 6px 18px -6px rgba(var(--accent-rgb, 29, 185, 84), 0.7); }
|
||||
.vwsh-tab-n { font-size: 11px; font-weight: 800; opacity: 0.7; margin-left: 4px; }
|
||||
|
||||
.vwsh-toolbar { display: flex; gap: 12px; margin: 6px 0 22px; }
|
||||
.vwsh-search { position: relative; flex: 1; max-width: 360px; }
|
||||
.vwsh-search-ic { position: absolute; left: 13px; top: 50%; transform: translateY(-50%); color: rgba(255, 255, 255, 0.35); }
|
||||
.vwsh-search-input { width: 100%; padding: 11px 14px 11px 38px; border-radius: 11px; background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1); color: #fff; font-size: 13.5px; outline: none; transition: border-color 0.15s ease; }
|
||||
.vwsh-search-input:focus { border-color: rgba(var(--accent-rgb, 29, 185, 84), 0.5); }
|
||||
|
||||
.vwsh-loading { padding: 50px; text-align: center; }
|
||||
.vwsh-state { padding: 70px 24px; text-align: center; }
|
||||
.vwsh-state.hidden, .vwsh-loading.hidden, .vwsh-pagination.hidden { display: none; }
|
||||
.vwsh-state-ic { font-size: 42px; margin-bottom: 12px; opacity: 0.8; }
|
||||
.vwsh-state-title { font-size: 19px; font-weight: 800; color: #fff; margin-bottom: 7px; }
|
||||
.vwsh-state-sub { font-size: 13.5px; color: rgba(255, 255, 255, 0.5); max-width: 430px; margin: 0 auto; line-height: 1.55; }
|
||||
|
||||
.vwsh-pagination { display: flex; align-items: center; justify-content: center; gap: 16px; margin-top: 26px; }
|
||||
.vwsh-page-btn { padding: 9px 16px; border-radius: 10px; background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1); color: #fff; font-size: 13px; font-weight: 700; cursor: pointer; }
|
||||
.vwsh-page-btn:disabled { opacity: 0.4; cursor: default; }
|
||||
.vwsh-page-info { font-size: 13px; color: rgba(255, 255, 255, 0.5); }
|
||||
|
||||
/* movies grid */
|
||||
.vwsh-grid--movies { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 22px 18px; }
|
||||
.vwsh-movie { cursor: pointer; }
|
||||
.vwsh-movie-art { position: relative; aspect-ratio: 2 / 3; border-radius: 12px; overflow: hidden; background: #16161d;
|
||||
border: 1px solid rgba(255, 255, 255, 0.07); transition: border-color 0.18s ease, transform 0.18s ease; }
|
||||
.vwsh-movie:hover .vwsh-movie-art { border-color: rgba(var(--accent-rgb, 29, 185, 84), 0.4); transform: translateY(-3px); }
|
||||
.vwsh-movie-img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
||||
.vwsh-movie-ph { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; font-size: 34px; opacity: 0.4; }
|
||||
.vwsh-movie-scrim { position: absolute; inset: 0; background: linear-gradient(0deg, rgba(0, 0, 0, 0.5), transparent 45%); opacity: 0; transition: opacity 0.2s; }
|
||||
.vwsh-movie:hover .vwsh-movie-scrim { opacity: 1; }
|
||||
.vwsh-movie-info { padding: 8px 2px 0; }
|
||||
.vwsh-movie-title { display: block; font-size: 13.5px; font-weight: 700; color: #fff; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.vwsh-movie-meta { display: block; font-size: 11.5px; color: rgba(255, 255, 255, 0.45); margin-top: 2px; }
|
||||
|
||||
/* shows tree */
|
||||
.vwsh-grid--shows { display: flex; flex-direction: column; gap: 12px; }
|
||||
.vwsh-show { border: 1px solid rgba(255, 255, 255, 0.07); border-radius: 14px; background: rgba(255, 255, 255, 0.02); overflow: hidden; }
|
||||
.vwsh-show-head { display: flex; align-items: center; gap: 13px; padding: 12px 14px; cursor: pointer; transition: background 0.15s; }
|
||||
.vwsh-show-head:hover { background: rgba(255, 255, 255, 0.03); }
|
||||
.vwsh-show-chev { color: rgba(255, 255, 255, 0.4); transition: transform 0.2s; font-size: 14px; }
|
||||
.vwsh-show--open .vwsh-show-chev { transform: rotate(180deg); }
|
||||
.vwsh-show-art { width: 42px; height: 60px; border-radius: 7px; overflow: hidden; flex-shrink: 0; background: #16161d; }
|
||||
.vwsh-show-img { width: 100%; height: 100%; object-fit: cover; }
|
||||
.vwsh-show-ph { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; font-size: 20px; opacity: 0.4; }
|
||||
.vwsh-show-titles { flex: 1; min-width: 0; }
|
||||
.vwsh-show-title { display: block; font-size: 15px; font-weight: 800; color: #fff; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.vwsh-show-meta { display: block; font-size: 12.5px; color: rgba(255, 255, 255, 0.45); margin-top: 3px; }
|
||||
.vwsh-show-body { display: none; padding: 0 14px 10px; }
|
||||
.vwsh-show--open .vwsh-show-body { display: block; }
|
||||
|
||||
.vwsh-season { border-top: 1px solid rgba(255, 255, 255, 0.05); }
|
||||
.vwsh-season-head { display: flex; align-items: center; gap: 10px; padding: 10px 4px; cursor: pointer; }
|
||||
.vwsh-season-chev { color: rgba(255, 255, 255, 0.35); transition: transform 0.2s; font-size: 12px; }
|
||||
.vwsh-season--open .vwsh-season-chev { transform: rotate(180deg); }
|
||||
.vwsh-season-name { font-size: 13.5px; font-weight: 700; color: rgba(255, 255, 255, 0.9); }
|
||||
.vwsh-season-meta { font-size: 12px; color: rgba(255, 255, 255, 0.4); margin-left: auto; }
|
||||
.vwsh-season-eps { display: none; padding: 2px 0 6px; }
|
||||
.vwsh-season--open .vwsh-season-eps { display: block; }
|
||||
.vwsh-ep { display: flex; align-items: center; gap: 10px; padding: 7px 8px 7px 22px; border-radius: 8px; }
|
||||
.vwsh-ep:hover { background: rgba(255, 255, 255, 0.03); }
|
||||
.vwsh-ep-code { font-size: 11.5px; font-weight: 800; color: rgba(255, 255, 255, 0.4); min-width: 56px; flex-shrink: 0; }
|
||||
.vwsh-ep-title { flex: 1; min-width: 0; font-size: 13px; color: rgba(255, 255, 255, 0.85); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
|
||||
/* status pills + remove button */
|
||||
.vwsh-st { font-size: 10px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.03em; padding: 3px 8px; border-radius: 6px; flex-shrink: 0; }
|
||||
.vwsh-st--wanted { background: rgba(var(--accent-rgb, 29, 185, 84), 0.16); color: rgb(var(--accent-rgb, 29, 185, 84)); }
|
||||
.vwsh-st--searching { background: rgba(245, 158, 11, 0.16); color: #f59e0b; }
|
||||
.vwsh-st--downloading { background: rgba(100, 149, 237, 0.18); color: #8ab0ff; }
|
||||
.vwsh-st--done { background: rgba(34, 197, 94, 0.18); color: #4ade80; }
|
||||
.vwsh-st--failed { background: rgba(248, 113, 113, 0.16); color: #f87171; }
|
||||
.vwsh-movie-art .vwsh-st { position: absolute; top: 8px; left: 8px; z-index: 2; backdrop-filter: blur(6px); }
|
||||
|
||||
.vwsh-rm { flex-shrink: 0; width: 26px; height: 26px; border-radius: 7px; border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
background: rgba(0, 0, 0, 0.35); color: rgba(255, 255, 255, 0.6); font-size: 17px; line-height: 1; cursor: pointer;
|
||||
display: flex; align-items: center; justify-content: center; transition: all 0.15s ease; }
|
||||
.vwsh-rm:hover { background: rgba(248, 113, 113, 0.85); border-color: transparent; color: #fff; }
|
||||
.vwsh-rm:disabled { opacity: 0.4; cursor: default; }
|
||||
.vwsh-movie-art .vwsh-rm { position: absolute; top: 8px; right: 8px; z-index: 3; opacity: 0; }
|
||||
.vwsh-movie:hover .vwsh-movie-art .vwsh-rm { opacity: 1; }
|
||||
.vwsh-season-head .vwsh-rm, .vwsh-ep .vwsh-rm { width: 24px; height: 24px; font-size: 15px; }
|
||||
.vwsh-season-head .vwsh-rm { margin-left: 10px; }
|
||||
@media (hover: none) { .vwsh-movie-art .vwsh-rm { opacity: 1; } }
|
||||
|
|
|
|||
236
webui/static/video/video-wishlist.js
Normal file
236
webui/static/video/video-wishlist.js
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
/*
|
||||
* SoulSync — Video Wishlist page (isolated).
|
||||
*
|
||||
* The curated 'get this' list, split by a Movies / TV tab. Movies render as a
|
||||
* poster grid; TV groups into collapsible show → season → episode rows with
|
||||
* wanted/done roll-ups and a remove (✕) at every level. Server-paged + searchable
|
||||
* like the other pages. Reads /api/video/wishlist; removes via /wishlist/remove.
|
||||
* Self-contained IIFE, no globals.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var PAGE_ID = 'video-wishlist';
|
||||
var LIMIT = 60;
|
||||
var state = { loaded: false, tab: 'movie', search: '', page: 1, counts: { movie: 0, show: 0 } };
|
||||
var searchTimer = null;
|
||||
|
||||
function $(s, r) { return (r || document).querySelector(s); }
|
||||
function esc(s) {
|
||||
return String(s == null ? '' : s)
|
||||
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
}
|
||||
|
||||
var STATUS = {
|
||||
wanted: ['Wanted', 'vwsh-st--wanted'], searching: ['Searching', 'vwsh-st--searching'],
|
||||
downloading: ['Downloading', 'vwsh-st--downloading'], downloaded: ['Done', 'vwsh-st--done'],
|
||||
failed: ['Failed', 'vwsh-st--failed'],
|
||||
};
|
||||
function statusPill(status) {
|
||||
var s = STATUS[status] || STATUS.wanted;
|
||||
return '<span class="vwsh-st ' + s[1] + '">' + s[0] + '</span>';
|
||||
}
|
||||
function rmBtn(scope, attrs) {
|
||||
return '<button class="vwsh-rm" type="button" title="Remove" aria-label="Remove" ' +
|
||||
'data-vwsh-rm="' + scope + '"' + attrs + '>×</button>';
|
||||
}
|
||||
|
||||
// ── movie card ────────────────────────────────────────────────────────────
|
||||
function movieCard(it) {
|
||||
var owned = it.library_id != null;
|
||||
var art = it.poster_url
|
||||
? '<img class="vwsh-movie-img" src="' + esc(it.poster_url) + '" alt="" loading="lazy" ' +
|
||||
'onerror="this.style.display=\'none\'">'
|
||||
: '<div class="vwsh-movie-ph">🎬</div>';
|
||||
var meta = [it.year, owned ? 'In library' : null].filter(Boolean).join(' · ');
|
||||
return '<div class="vwsh-movie" data-vwsh-open-movie="' + esc(it.tmdb_id) +
|
||||
'" data-vwsh-src="' + (owned ? 'library' : 'tmdb') + '" data-vwsh-id="' + esc(owned ? it.library_id : it.tmdb_id) + '">' +
|
||||
'<div class="vwsh-movie-art">' + art + '<div class="vwsh-movie-scrim"></div>' +
|
||||
statusPill(it.status) + rmBtn('movie', ' data-tmdb="' + esc(it.tmdb_id) + '"') + '</div>' +
|
||||
'<div class="vwsh-movie-info"><span class="vwsh-movie-title" title="' + esc(it.title) + '">' +
|
||||
esc(it.title) + '</span>' + (meta ? '<span class="vwsh-movie-meta">' + esc(meta) + '</span>' : '') +
|
||||
'</div></div>';
|
||||
}
|
||||
|
||||
// ── show group (collapsible show → season → episode) ──────────────────────
|
||||
function showGroup(sh) {
|
||||
var poster = sh.poster_url
|
||||
? '<img class="vwsh-show-img" src="' + esc(sh.poster_url) + '" alt="" loading="lazy" onerror="this.style.display=\'none\'">'
|
||||
: '<div class="vwsh-show-ph">📺</div>';
|
||||
var done = sh.done ? ' · ' + sh.done + ' done' : '';
|
||||
var seasons = (sh.seasons || []).map(function (se) {
|
||||
var eps = (se.episodes || []).map(function (e) {
|
||||
return '<div class="vwsh-ep">' +
|
||||
'<span class="vwsh-ep-code">S' + se.season_number + '·E' + e.episode_number + '</span>' +
|
||||
'<span class="vwsh-ep-title">' + esc(e.title || ('Episode ' + e.episode_number)) + '</span>' +
|
||||
statusPill(e.status) +
|
||||
rmBtn('episode', ' data-tmdb="' + esc(sh.tmdb_id) + '" data-s="' + se.season_number + '" data-e="' + e.episode_number + '"') +
|
||||
'</div>';
|
||||
}).join('');
|
||||
return '<div class="vwsh-season">' +
|
||||
'<div class="vwsh-season-head" data-vwsh-season-toggle>' +
|
||||
'<span class="vwsh-season-chev" aria-hidden="true">⌄</span>' +
|
||||
'<span class="vwsh-season-name">Season ' + se.season_number + '</span>' +
|
||||
'<span class="vwsh-season-meta">' + se.episodes.length + ' wanted</span>' +
|
||||
rmBtn('season', ' data-tmdb="' + esc(sh.tmdb_id) + '" data-s="' + se.season_number + '"') +
|
||||
'</div>' +
|
||||
'<div class="vwsh-season-eps">' + eps + '</div>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
return '<div class="vwsh-show" data-vwsh-show="' + esc(sh.tmdb_id) + '">' +
|
||||
'<div class="vwsh-show-head" data-vwsh-show-toggle>' +
|
||||
'<span class="vwsh-show-chev" aria-hidden="true">⌄</span>' +
|
||||
'<div class="vwsh-show-art">' + poster + '</div>' +
|
||||
'<div class="vwsh-show-titles"><span class="vwsh-show-title" title="' + esc(sh.title) + '">' + esc(sh.title) + '</span>' +
|
||||
'<span class="vwsh-show-meta">' + sh.wanted + ' wanted' + done + '</span></div>' +
|
||||
rmBtn('show', ' data-tmdb="' + esc(sh.tmdb_id) + '"') +
|
||||
'</div>' +
|
||||
'<div class="vwsh-show-body">' + seasons + '</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
function render(items) {
|
||||
var grid = $('[data-vwsh-grid]'); if (!grid) return;
|
||||
var shows = state.tab === 'show';
|
||||
grid.classList.toggle('vwsh-grid--shows', shows);
|
||||
grid.classList.toggle('vwsh-grid--movies', !shows);
|
||||
grid.innerHTML = items.map(shows ? showGroup : movieCard).join('');
|
||||
}
|
||||
|
||||
// ── counts / badges / pager ───────────────────────────────────────────────
|
||||
function setCounts(counts) {
|
||||
state.counts = { movie: (counts && counts.movie) || 0, show: (counts && counts.show) || 0 };
|
||||
var cm = $('[data-vwsh-count-movie]'); if (cm) cm.textContent = state.counts.movie;
|
||||
var cs = $('[data-vwsh-count-show]'); if (cs) cs.textContent = state.counts.show;
|
||||
updateBadges(counts && counts.total != null ? counts.total : (state.counts.movie + state.counts.show));
|
||||
}
|
||||
function updateBadges(total) {
|
||||
var n = total || 0;
|
||||
['[data-video-wishlist-badge]', '[data-video-badge="wishlist"]'].forEach(function (sel) {
|
||||
document.querySelectorAll(sel).forEach(function (b) {
|
||||
b.textContent = n; b.classList.toggle('hidden', !n);
|
||||
});
|
||||
});
|
||||
}
|
||||
function refreshBadge() {
|
||||
fetch('/api/video/wishlist/counts', { headers: { Accept: 'application/json' } })
|
||||
.then(function (r) { return r.ok ? r.json() : null; })
|
||||
.then(function (d) { if (d && d.success) updateBadges(d.total); })
|
||||
.catch(function () { /* ignore */ });
|
||||
}
|
||||
function updatePagination(p) {
|
||||
var box = $('[data-vwsh-pagination]'), prev = $('[data-vwsh-prev]'),
|
||||
next = $('[data-vwsh-next]'), info = $('[data-vwsh-pageinfo]');
|
||||
if (!box) return;
|
||||
if (!p || p.total_pages <= 1) { box.classList.add('hidden'); return; }
|
||||
if (prev) prev.disabled = !p.has_prev;
|
||||
if (next) next.disabled = !p.has_next;
|
||||
if (info) info.textContent = 'Page ' + p.page + ' of ' + p.total_pages;
|
||||
box.classList.remove('hidden');
|
||||
}
|
||||
function updateEmpty(total) {
|
||||
var empty = $('[data-vwsh-empty]'); if (empty) empty.classList.toggle('hidden', total > 0);
|
||||
var et = $('[data-vwsh-empty-title]');
|
||||
if (et && total === 0) {
|
||||
et.textContent = state.search ? 'No matches'
|
||||
: (state.tab === 'movie' ? 'No movies on your wishlist yet' : 'No TV episodes on your wishlist yet');
|
||||
}
|
||||
}
|
||||
|
||||
function load() {
|
||||
state.loaded = true;
|
||||
var ld = $('[data-vwsh-loading]'); if (ld) ld.classList.remove('hidden');
|
||||
var params = new URLSearchParams({ kind: state.tab, search: state.search, page: state.page, limit: LIMIT });
|
||||
fetch('/api/video/wishlist?' + params.toString(), { headers: { Accept: 'application/json' } })
|
||||
.then(function (r) { return r.ok ? r.json() : null; })
|
||||
.then(function (d) {
|
||||
if (ld) ld.classList.add('hidden');
|
||||
if (!d || !d.success) { render([]); updatePagination(null); updateEmpty(0); return; }
|
||||
setCounts(d.counts);
|
||||
var p = d.pagination || { page: 1, total_pages: 1, total_count: (d.items || []).length };
|
||||
state.page = p.page;
|
||||
render(d.items || []);
|
||||
updatePagination(p);
|
||||
updateEmpty(p.total_count);
|
||||
})
|
||||
.catch(function () { if (ld) ld.classList.add('hidden'); render([]); updatePagination(null); updateEmpty(0); });
|
||||
}
|
||||
|
||||
function setTab(tab) {
|
||||
if (tab !== 'movie' && tab !== 'show') return;
|
||||
state.tab = tab; state.page = 1; state.search = '';
|
||||
var si = $('[data-vwsh-search]'); if (si) si.value = '';
|
||||
var tabs = document.querySelectorAll('[data-vwsh-tab]');
|
||||
for (var i = 0; i < tabs.length; i++)
|
||||
tabs[i].classList.toggle('vwsh-tab--on', tabs[i].getAttribute('data-vwsh-tab') === tab);
|
||||
load();
|
||||
}
|
||||
|
||||
// ── remove ────────────────────────────────────────────────────────────────
|
||||
function doRemove(btn) {
|
||||
var scope = btn.getAttribute('data-vwsh-rm');
|
||||
var body = { scope: scope, tmdb_id: parseInt(btn.getAttribute('data-tmdb'), 10) };
|
||||
if (btn.hasAttribute('data-s')) body.season_number = parseInt(btn.getAttribute('data-s'), 10);
|
||||
if (btn.hasAttribute('data-e')) body.episode_number = parseInt(btn.getAttribute('data-e'), 10);
|
||||
btn.disabled = true;
|
||||
fetch('/api/video/wishlist/remove', { method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body) })
|
||||
.then(function (r) { return r.ok ? r.json() : null; })
|
||||
.then(function (d) {
|
||||
if (!d || !d.success) { btn.disabled = false; return; }
|
||||
if (typeof showToast === 'function') showToast('Removed from wishlist', 'info');
|
||||
load(); // re-render + counts + pager stay correct
|
||||
})
|
||||
.catch(function () { btn.disabled = false; });
|
||||
}
|
||||
|
||||
function onGridClick(e) {
|
||||
var rm = e.target.closest('[data-vwsh-rm]');
|
||||
if (rm) { e.preventDefault(); e.stopPropagation(); doRemove(rm); return; }
|
||||
var st = e.target.closest('[data-vwsh-season-toggle]');
|
||||
if (st) { st.parentNode.classList.toggle('vwsh-season--open'); return; }
|
||||
var sh = e.target.closest('[data-vwsh-show-toggle]');
|
||||
if (sh) { sh.parentNode.classList.toggle('vwsh-show--open'); return; }
|
||||
var mv = e.target.closest('[data-vwsh-open-movie]');
|
||||
if (mv) {
|
||||
if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
|
||||
document.dispatchEvent(new CustomEvent('soulsync:video-open-detail', {
|
||||
detail: { kind: 'movie', id: parseInt(mv.getAttribute('data-vwsh-id'), 10),
|
||||
source: mv.getAttribute('data-vwsh-src') || 'tmdb' },
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
function wire() {
|
||||
var tabs = document.querySelectorAll('[data-vwsh-tab]');
|
||||
for (var i = 0; i < tabs.length; i++) (function (b) {
|
||||
b.addEventListener('click', function () { setTab(b.getAttribute('data-vwsh-tab')); });
|
||||
})(tabs[i]);
|
||||
var grid = $('[data-vwsh-grid]'); if (grid) grid.addEventListener('click', onGridClick);
|
||||
var search = $('[data-vwsh-search]');
|
||||
if (search) search.addEventListener('input', function () {
|
||||
if (searchTimer) clearTimeout(searchTimer);
|
||||
searchTimer = setTimeout(function () { state.search = search.value.trim(); state.page = 1; load(); }, 250);
|
||||
});
|
||||
var prev = $('[data-vwsh-prev]');
|
||||
if (prev) prev.addEventListener('click', function () { if (state.page > 1) { state.page--; load(); } });
|
||||
var next = $('[data-vwsh-next]');
|
||||
if (next) next.addEventListener('click', function () { state.page++; load(); });
|
||||
// Adds elsewhere (the get-modal) refresh the badge + page if visible.
|
||||
document.addEventListener('soulsync:video-wishlist-changed', function () {
|
||||
var g = $('[data-vwsh-grid]');
|
||||
if (g && g.offsetParent !== null) load(); else refreshBadge();
|
||||
});
|
||||
}
|
||||
|
||||
function onShown(e) { if (e && e.detail === PAGE_ID) { state.page = 1; load(); } }
|
||||
|
||||
function init() {
|
||||
wire();
|
||||
document.addEventListener('soulsync:video-page-shown', onShown);
|
||||
refreshBadge();
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
|
||||
else init();
|
||||
})();
|
||||
Loading…
Reference in a new issue