diff --git a/webui/index.html b/webui/index.html index 1d875e8a..4f97106a 100644 --- a/webui/index.html +++ b/webui/index.html @@ -1035,6 +1035,7 @@ + π« Ignore List diff --git a/webui/static/video/video-discover.js b/webui/static/video/video-discover.js index 90988f11..84a20d45 100644 --- a/webui/static/video/video-discover.js +++ b/webui/static/video/video-discover.js @@ -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) ? 'β' : ''; return '' + cb + + '" style="--vgm-h:' + hueOf(it.title) + '">' + cb + notInt + '' + img + ribbon + rating + 'i' + '' + 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 '' + + (it.poster ? '' : '' + (it.kind === 'movie' ? 'π¬' : 'πΊ') + '') + + '' + esc(it.title || 'Untitled') + (it.year ? ' (' + it.year + ')' : '') + '' + + '' + btnLabel + ''; + } + 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 = 'π' + + 'Nothing hidden yet.Hover any Discover card and hit β, or search above to hide a title.'; + return; + } + box.innerHTML = '' + items.length + ' hidden' + + items.map(function (it) { + return '' + + (it.poster ? '' + : '' + (it.kind === 'movie' ? 'π¬' : 'πΊ') + '') + + '' + esc(it.title || 'Untitled') + '' + + (it.year ? '' + it.year + '' : '') + '' + + 'Un-hide'; + }).join('') + ''; + }).catch(function () { box.innerHTML = 'Could not load the list.'; }); + } + 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 = + '' + + 'Not Interested' + + 'Titles hidden from Discover β they won\'t show in any rail or recommendation.' + + 'β' + + '' + + '' + + 'Loadingβ¦' + + ''; + 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('') + : 'No matches'; + }).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); }); diff --git a/webui/static/video/video-side.css b/webui/static/video/video-side.css index 1830d196..259d2e6a 100644 --- a/webui/static/video/video-side.css +++ b/webui/static/video/video-side.css @@ -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; } }