video discover phase 4 (UI): 'Not interested' card button + ignore-list modal
- A ✕ 'Not interested' button on every un-owned Discover card (hover) — adds to the ignore list
and fades the card out instantly.
- A '🚫 Ignore List' button top-right of the hero opens a vibey glassmorphic modal: a header
explaining what it is, a search box to hide any movie/show directly (TMDB search), and a poster
grid of everything hidden with one-click 'Un-hide'. Empty state guides the user.
- Card button + modal both POST /discover/ignore; ignored titles vanish from every rail (via
_stamp_owned). Video-only, additive.
This commit is contained in:
parent
3f344a8cd9
commit
6734b6ab22
3 changed files with 191 additions and 1 deletions
|
|
@ -1035,6 +1035,7 @@
|
|||
<section class="video-subpage" data-video-subpage="video-discover" hidden>
|
||||
<div class="vdsc-page" data-vdsc-page>
|
||||
<div class="vdsc-amb" data-vdsc-amb aria-hidden="true"></div>
|
||||
<button class="vdsc-ignore-btn" type="button" data-vdsc-ignore-open title="Manage your Not-interested list"><span aria-hidden="true">🚫</span> Ignore List</button>
|
||||
<div class="vdsc-hero hidden" data-vdsc-hero></div>
|
||||
<div class="vdsc-browse">
|
||||
<div class="vdsc-browse-top">
|
||||
|
|
|
|||
|
|
@ -133,9 +133,14 @@
|
|||
var sub = [it.year, it.kind === 'movie' ? 'Movie' : 'TV'].filter(Boolean).join(' · ');
|
||||
var cb = window.VideoGet ? VideoGet.cardButton({ kind: it.kind, tmdbId: it.tmdb_id,
|
||||
libraryId: it.library_id, title: it.title, poster: it.poster, status: it.status, source: source }) : '';
|
||||
// 'Not interested' — un-owned cards only (you can't be uninterested in what you own).
|
||||
var notInt = (!owned && it.tmdb_id) ? '<button class="vsr-notint" type="button" title="Not interested" ' +
|
||||
'aria-label="Not interested" data-ig-kind="' + it.kind + '" data-ig-id="' + it.tmdb_id + '" ' +
|
||||
'data-ig-title="' + esc(it.title || '') + '" data-ig-year="' + (it.year || '') + '" ' +
|
||||
'data-ig-poster="' + esc(it.poster || '') + '">✕</button>' : '';
|
||||
return '<a class="vsr-card' + (owned ? ' vsr-card--owned' : '') + '" href="' + href + '" ' +
|
||||
'data-vsr-open="' + it.kind + '" data-vsr-source="' + source + '" data-vsr-id="' + id +
|
||||
'" style="--vgm-h:' + hueOf(it.title) + '">' + cb +
|
||||
'" style="--vgm-h:' + hueOf(it.title) + '">' + cb + notInt +
|
||||
'<div class="vsr-poster">' + img + ribbon + rating +
|
||||
'<span class="vsr-peek" aria-hidden="true">i</span></div>' +
|
||||
'<div class="vsr-info"><span class="vsr-name" title="' + esc(it.title) + '">' + esc(it.title) +
|
||||
|
|
@ -143,6 +148,114 @@
|
|||
}
|
||||
function hydrateGet(root) { if (window.VideoWatchlist) VideoWatchlist.hydrate(root); }
|
||||
|
||||
// ── 'Not interested' + ignore-list management ─────────────────────────────
|
||||
function postIgnore(payload) {
|
||||
return fetch('/api/video/discover/ignore', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
}).then(function (r) { return r.ok ? r.json() : null; });
|
||||
}
|
||||
function wireNotInterested() {
|
||||
if (state._notIntWired) return; state._notIntWired = true;
|
||||
document.addEventListener('click', function (e) {
|
||||
var b = e.target.closest('.vsr-notint'); if (!b) return;
|
||||
e.preventDefault(); e.stopPropagation();
|
||||
postIgnore({ action: 'add', kind: b.getAttribute('data-ig-kind'),
|
||||
tmdb_id: parseInt(b.getAttribute('data-ig-id'), 10),
|
||||
title: b.getAttribute('data-ig-title'),
|
||||
year: parseInt(b.getAttribute('data-ig-year'), 10) || null,
|
||||
poster: b.getAttribute('data-ig-poster') || null });
|
||||
var card = b.closest('.vsr-card');
|
||||
if (card) { card.style.transition = 'opacity .2s, transform .2s'; card.style.opacity = '0'; card.style.transform = 'scale(.92)'; setTimeout(function () { card.remove(); }, 200); }
|
||||
}, true);
|
||||
}
|
||||
function igRowHtml(it, btnLabel) {
|
||||
return '<button class="vdsc-ig-res" type="button" data-ig-kind="' + it.kind + '" data-ig-id="' + it.tmdb_id +
|
||||
'" data-ig-title="' + esc(it.title || '') + '" data-ig-year="' + (it.year || '') + '" data-ig-poster="' + esc(it.poster || '') + '">' +
|
||||
(it.poster ? '<img src="' + esc(it.poster) + '" alt="">' : '<span class="vdsc-ig-ph">' + (it.kind === 'movie' ? '🎬' : '📺') + '</span>') +
|
||||
'<span class="vdsc-ig-rn">' + esc(it.title || 'Untitled') + (it.year ? ' (' + it.year + ')' : '') + '</span>' +
|
||||
'<span class="vdsc-ig-add">' + btnLabel + '</span></button>';
|
||||
}
|
||||
function renderIgnoreList(ov) {
|
||||
var box = ov.querySelector('[data-ig-list]');
|
||||
fetch('/api/video/discover/ignore', { headers: { Accept: 'application/json' } })
|
||||
.then(function (r) { return r.ok ? r.json() : null; })
|
||||
.then(function (d) {
|
||||
var items = (d && d.items) || [];
|
||||
if (!items.length) {
|
||||
box.innerHTML = '<div class="vdsc-ig-empty"><div class="vdsc-ig-empty-ic">🙈</div>' +
|
||||
'<div>Nothing hidden yet.</div><div class="vdsc-ig-empty-sub">Hover any Discover card and hit ✕, or search above to hide a title.</div></div>';
|
||||
return;
|
||||
}
|
||||
box.innerHTML = '<div class="vdsc-ig-count">' + items.length + ' hidden</div><div class="vdsc-ig-grid">' +
|
||||
items.map(function (it) {
|
||||
return '<div class="vdsc-ig-card" data-ig-kind="' + it.kind + '" data-ig-id="' + it.tmdb_id + '">' +
|
||||
(it.poster ? '<img class="vdsc-ig-poster" src="' + esc(it.poster) + '" alt="">'
|
||||
: '<div class="vdsc-ig-poster vdsc-ig-ph">' + (it.kind === 'movie' ? '🎬' : '📺') + '</div>') +
|
||||
'<div class="vdsc-ig-meta"><span class="vdsc-ig-name">' + esc(it.title || 'Untitled') + '</span>' +
|
||||
(it.year ? '<span class="vdsc-ig-yr">' + it.year + '</span>' : '') + '</div>' +
|
||||
'<button class="vdsc-ig-remove" type="button">Un-hide</button></div>';
|
||||
}).join('') + '</div>';
|
||||
}).catch(function () { box.innerHTML = '<div class="vdsc-ig-empty">Could not load the list.</div>'; });
|
||||
}
|
||||
function openIgnoreModal() {
|
||||
var ex = document.getElementById('vdsc-ignore-modal'); if (ex) ex.remove();
|
||||
var ov = document.createElement('div');
|
||||
ov.id = 'vdsc-ignore-modal';
|
||||
ov.className = 'vdsc-ig-overlay';
|
||||
ov.innerHTML =
|
||||
'<div class="vdsc-ig-modal" role="dialog" aria-label="Not interested list">' +
|
||||
'<div class="vdsc-ig-head"><div><div class="vdsc-ig-title">Not Interested</div>' +
|
||||
'<div class="vdsc-ig-sub">Titles hidden from Discover — they won\'t show in any rail or recommendation.</div></div>' +
|
||||
'<button class="vdsc-ig-close" type="button" aria-label="Close">✕</button></div>' +
|
||||
'<div class="vdsc-ig-search"><input type="text" class="vdsc-ig-input" placeholder="Search a movie or show to hide…">' +
|
||||
'<div class="vdsc-ig-results" data-ig-results></div></div>' +
|
||||
'<div class="vdsc-ig-list" data-ig-list><div class="vdsc-ig-empty">Loading…</div></div>' +
|
||||
'</div>';
|
||||
document.body.appendChild(ov);
|
||||
requestAnimationFrame(function () { ov.classList.add('vdsc-ig-in'); });
|
||||
var close = function () { ov.classList.remove('vdsc-ig-in'); setTimeout(function () { ov.remove(); }, 180); };
|
||||
ov.addEventListener('click', function (e) { if (e.target === ov || e.target.closest('.vdsc-ig-close')) close(); });
|
||||
document.addEventListener('keydown', function esc(e) { if (e.key === 'Escape') { close(); document.removeEventListener('keydown', esc); } });
|
||||
|
||||
renderIgnoreList(ov);
|
||||
|
||||
var input = ov.querySelector('.vdsc-ig-input');
|
||||
var resBox = ov.querySelector('[data-ig-results]');
|
||||
var t = null;
|
||||
input.addEventListener('input', function () {
|
||||
clearTimeout(t);
|
||||
var q = input.value.trim();
|
||||
if (q.length < 2) { resBox.innerHTML = ''; return; }
|
||||
t = setTimeout(function () {
|
||||
fetch('/api/video/search?q=' + encodeURIComponent(q), { headers: { Accept: 'application/json' } })
|
||||
.then(function (r) { return r.ok ? r.json() : null; })
|
||||
.then(function (d) {
|
||||
var items = ((d && d.results) || []).filter(function (x) {
|
||||
return x.tmdb_id && (x.kind === 'movie' || x.kind === 'show');
|
||||
}).slice(0, 8);
|
||||
resBox.innerHTML = items.length ? items.map(function (x) { return igRowHtml(x, '+ Hide'); }).join('')
|
||||
: '<div class="vdsc-ig-nores">No matches</div>';
|
||||
}).catch(function () { resBox.innerHTML = ''; });
|
||||
}, 300);
|
||||
});
|
||||
resBox.addEventListener('click', function (e) {
|
||||
var b = e.target.closest('.vdsc-ig-res'); if (!b) return;
|
||||
postIgnore({ action: 'add', kind: b.getAttribute('data-ig-kind'),
|
||||
tmdb_id: parseInt(b.getAttribute('data-ig-id'), 10), title: b.getAttribute('data-ig-title'),
|
||||
year: parseInt(b.getAttribute('data-ig-year'), 10) || null, poster: b.getAttribute('data-ig-poster') || null })
|
||||
.then(function () { input.value = ''; resBox.innerHTML = ''; renderIgnoreList(ov); });
|
||||
});
|
||||
ov.querySelector('[data-ig-list]').addEventListener('click', function (e) {
|
||||
var b = e.target.closest('.vdsc-ig-remove'); if (!b) return;
|
||||
var c = b.closest('.vdsc-ig-card');
|
||||
postIgnore({ action: 'remove', kind: c.getAttribute('data-ig-kind'),
|
||||
tmdb_id: parseInt(c.getAttribute('data-ig-id'), 10) })
|
||||
.then(function () { c.style.opacity = '0'; c.style.transform = 'scale(.9)';
|
||||
setTimeout(function () { renderIgnoreList(ov); }, 160); });
|
||||
});
|
||||
}
|
||||
|
||||
// ── hero slideshow ────────────────────────────────────────────────────────
|
||||
function loadHero() {
|
||||
fetch('/api/video/discover/hero', { headers: { Accept: 'application/json' } })
|
||||
|
|
@ -549,6 +662,8 @@
|
|||
var hero = $('[data-vdsc-hero]');
|
||||
if (hero) { hero.addEventListener('mouseenter', stopHeroTimer); hero.addEventListener('mouseleave', startHeroTimer); }
|
||||
|
||||
var igBtn = $('[data-vdsc-ignore-open]'); if (igBtn && !igBtn._wired) { igBtn._wired = 1; igBtn.addEventListener('click', openIgnoreModal); }
|
||||
wireNotInterested();
|
||||
var apply = $('[data-vdsc-apply]'); if (apply) apply.addEventListener('click', applyFilter);
|
||||
var clear = $('[data-vdsc-clear]'); if (clear) clear.addEventListener('click', closeCategory);
|
||||
var more = $('[data-vdsc-more]'); if (more) more.addEventListener('click', function () { loadGrid(false); });
|
||||
|
|
|
|||
|
|
@ -2364,6 +2364,80 @@ body[data-side="video"] #soulsync-toggle { display: none; }
|
|||
.vdsc-lang:hover { color: rgba(255, 255, 255, 0.85); }
|
||||
.vdsc-lang--on { color: #fff; background: rgba(var(--accent-rgb, 88 101 242), 0.85); border-color: transparent; }
|
||||
|
||||
/* ── 'Not interested' card button ──────────────────────────────────────────── */
|
||||
.vsr-card { position: relative; }
|
||||
.vsr-notint { position: absolute; top: 6px; left: 6px; z-index: 6; width: 24px; height: 24px;
|
||||
border-radius: 50%; border: none; cursor: pointer; font-size: 12px; line-height: 1;
|
||||
display: flex; align-items: center; justify-content: center; opacity: 0;
|
||||
background: rgba(10, 10, 14, 0.78); color: rgba(255, 255, 255, 0.85);
|
||||
backdrop-filter: blur(4px); transition: opacity .15s ease, background .15s ease, transform .15s ease; }
|
||||
.vsr-card:hover .vsr-notint { opacity: 1; }
|
||||
.vsr-notint:hover { background: rgba(220, 38, 38, 0.92); color: #fff; transform: scale(1.12); }
|
||||
|
||||
/* ── Ignore-list button (top-right of the hero) ────────────────────────────── */
|
||||
.vdsc-ignore-btn { position: absolute; top: 16px; right: 18px; z-index: 8;
|
||||
display: inline-flex; align-items: center; gap: 6px; padding: 7px 13px; border-radius: 999px;
|
||||
font-size: 12.5px; font-weight: 600; cursor: pointer; color: rgba(255, 255, 255, 0.82);
|
||||
background: rgba(12, 12, 16, 0.55); border: 1px solid rgba(255, 255, 255, 0.14);
|
||||
backdrop-filter: blur(10px); transition: all .18s ease; }
|
||||
.vdsc-ignore-btn:hover { color: #fff; background: rgba(220, 38, 38, 0.5); border-color: rgba(220, 38, 38, 0.6);
|
||||
transform: translateY(-1px); }
|
||||
|
||||
/* ── Ignore-list modal ─────────────────────────────────────────────────────── */
|
||||
.vdsc-ig-overlay { position: fixed; inset: 0; z-index: 10000; display: flex; align-items: center;
|
||||
justify-content: center; background: rgba(0, 0, 0, 0.62); backdrop-filter: blur(6px);
|
||||
opacity: 0; transition: opacity .18s ease; padding: 20px; }
|
||||
.vdsc-ig-overlay.vdsc-ig-in { opacity: 1; }
|
||||
.vdsc-ig-modal { width: 720px; max-width: 96vw; max-height: 86vh; display: flex; flex-direction: column;
|
||||
border-radius: 22px; overflow: hidden; transform: translateY(10px) scale(.98); transition: transform .2s ease;
|
||||
background: linear-gradient(160deg, rgba(24, 24, 30, 0.98), rgba(15, 15, 20, 0.99));
|
||||
border: 1px solid rgba(var(--accent-rgb, 88 101 242), 0.28);
|
||||
box-shadow: 0 30px 80px rgba(0, 0, 0, 0.6); }
|
||||
.vdsc-ig-in .vdsc-ig-modal { transform: translateY(0) scale(1); }
|
||||
.vdsc-ig-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px;
|
||||
padding: 22px 24px 14px; }
|
||||
.vdsc-ig-title { font-size: 19px; font-weight: 800; color: #fff; letter-spacing: -0.3px; }
|
||||
.vdsc-ig-sub { font-size: 12.5px; color: rgba(255, 255, 255, 0.5); margin-top: 3px; max-width: 480px; line-height: 1.5; }
|
||||
.vdsc-ig-close { width: 32px; height: 32px; flex-shrink: 0; border-radius: 50%; border: none; cursor: pointer;
|
||||
background: rgba(255, 255, 255, 0.06); color: rgba(255, 255, 255, 0.6); font-size: 14px; transition: all .15s ease; }
|
||||
.vdsc-ig-close:hover { background: rgba(255, 255, 255, 0.12); color: #fff; transform: rotate(90deg); }
|
||||
.vdsc-ig-search { padding: 0 24px 12px; position: relative; }
|
||||
.vdsc-ig-input { width: 100%; box-sizing: border-box; padding: 11px 15px; border-radius: 12px;
|
||||
background: rgba(255, 255, 255, 0.05); border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
color: #fff; font-size: 14px; outline: none; transition: border-color .15s ease; }
|
||||
.vdsc-ig-input:focus { border-color: rgba(var(--accent-rgb, 88 101 242), 0.6); }
|
||||
.vdsc-ig-results { display: flex; flex-direction: column; gap: 4px; margin-top: 8px; max-height: 240px; overflow-y: auto; }
|
||||
.vdsc-ig-res { display: flex; align-items: center; gap: 11px; padding: 7px 9px; border-radius: 10px; cursor: pointer;
|
||||
border: 1px solid transparent; background: rgba(255, 255, 255, 0.03); color: rgba(255, 255, 255, 0.85);
|
||||
text-align: left; width: 100%; transition: background .12s ease, border-color .12s ease; }
|
||||
.vdsc-ig-res:hover { background: rgba(255, 255, 255, 0.08); border-color: rgba(255, 255, 255, 0.1); }
|
||||
.vdsc-ig-res img { width: 32px; height: 48px; object-fit: cover; border-radius: 4px; flex-shrink: 0; }
|
||||
.vdsc-ig-res .vdsc-ig-ph { width: 32px; height: 48px; display: flex; align-items: center; justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.06); border-radius: 4px; flex-shrink: 0; }
|
||||
.vdsc-ig-rn { flex: 1; font-size: 13.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.vdsc-ig-add { font-size: 11px; font-weight: 700; color: rgba(var(--accent-rgb, 88 101 242), 1);
|
||||
padding: 3px 9px; border-radius: 999px; background: rgba(var(--accent-rgb, 88 101 242), 0.14); flex-shrink: 0; }
|
||||
.vdsc-ig-nores { font-size: 12.5px; color: rgba(255, 255, 255, 0.4); padding: 8px 4px; }
|
||||
.vdsc-ig-list { padding: 6px 24px 24px; overflow-y: auto; }
|
||||
.vdsc-ig-count { font-size: 11px; font-weight: 700; letter-spacing: 0.5px; text-transform: uppercase;
|
||||
color: rgba(255, 255, 255, 0.35); margin: 6px 2px 12px; }
|
||||
.vdsc-ig-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(96px, 1fr)); gap: 14px; }
|
||||
.vdsc-ig-card { position: relative; display: flex; flex-direction: column; gap: 6px; transition: opacity .15s ease, transform .15s ease; }
|
||||
.vdsc-ig-poster { width: 100%; aspect-ratio: 2/3; object-fit: cover; border-radius: 10px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08); }
|
||||
.vdsc-ig-card .vdsc-ig-ph { display: flex; align-items: center; justify-content: center; font-size: 28px;
|
||||
background: rgba(255, 255, 255, 0.05); }
|
||||
.vdsc-ig-meta { display: flex; flex-direction: column; }
|
||||
.vdsc-ig-name { font-size: 12px; color: rgba(255, 255, 255, 0.85); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.vdsc-ig-yr { font-size: 10.5px; color: rgba(255, 255, 255, 0.4); }
|
||||
.vdsc-ig-remove { margin-top: 2px; padding: 4px 0; border-radius: 8px; border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
background: rgba(255, 255, 255, 0.04); color: rgba(255, 255, 255, 0.65); font-size: 11px; font-weight: 600; cursor: pointer;
|
||||
transition: all .14s ease; }
|
||||
.vdsc-ig-remove:hover { background: rgba(var(--accent-rgb, 88 101 242), 0.85); color: #fff; border-color: transparent; }
|
||||
.vdsc-ig-empty { text-align: center; padding: 44px 20px; color: rgba(255, 255, 255, 0.55); }
|
||||
.vdsc-ig-empty-ic { font-size: 40px; margin-bottom: 10px; }
|
||||
.vdsc-ig-empty-sub { font-size: 12.5px; color: rgba(255, 255, 255, 0.38); margin-top: 6px; }
|
||||
|
||||
/* rails: a gentle fade-in when filled + per-title hue on hover */
|
||||
.vdsc-shelf--in .vdsc-rail { animation: vdscFade 0.45s ease both; }
|
||||
@keyframes vdscFade { from { opacity: 0.35; } to { opacity: 1; } }
|
||||
|
|
|
|||
Loading…
Reference in a new issue