diff --git a/webui/static/video/video-calendar.js b/webui/static/video/video-calendar.js
index 5a976a74..6b327932 100644
--- a/webui/static/video/video-calendar.js
+++ b/webui/static/video/video-calendar.js
@@ -11,14 +11,14 @@
'use strict';
var PAGE_ID = 'video-calendar';
- var URL = '/api/video/calendar?days=7';
+ var URL = '/api/video/calendar';
var COLS = 7;
var WD = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
var WD_FULL = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
var MO = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
var MO_FULL = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August',
'September', 'October', 'November', 'December'];
- var state = { loaded: false, eps: {} };
+ var state = { loaded: false, eps: {}, data: null, offset: 0, filter: 'all' };
function $(s) { return document.querySelector(s); }
function esc(s) {
@@ -161,7 +161,8 @@
(bg ? '
' : '') +
'
' +
'
' +
- '
NEXT UP · ' + esc(whenLabel(ep, d.today)) + '
' +
+ '
' +
+ (state.offset === 0 ? 'NEXT UP' : 'FEATURED') + ' · ' + esc(whenLabel(ep, d.today)) + '
' +
'
' + esc(ep.show_title) + '
' +
'
' + se + '' + (epTitle ? ' · ' + esc(epTitle) : '') + '
' +
'
View details' + owned + '
' +
@@ -169,20 +170,51 @@
'
';
}
+ function filterEps(eps) {
+ if (state.filter === 'owned') return eps.filter(function (e) { return e.has_file; });
+ if (state.filter === 'missing') return eps.filter(function (e) { return !e.has_file; });
+ return eps;
+ }
+ function updateChrome() {
+ var t = $('[data-video-cal-title]');
+ if (t) t.textContent = state.offset === 0 ? 'This Week'
+ : state.offset === 1 ? 'Next Week' : state.offset === -1 ? 'Last Week'
+ : state.offset > 0 ? 'In ' + state.offset + ' weeks' : Math.abs(state.offset) + ' weeks ago';
+ var today = $('[data-video-cal-today]');
+ if (today) today.disabled = state.offset === 0;
+ var fbs = document.querySelectorAll('[data-video-cal-filter]');
+ for (var i = 0; i < fbs.length; i++)
+ fbs[i].classList.toggle('vcal-filter-btn--on', fbs[i].getAttribute('data-video-cal-filter') === state.filter);
+ }
+ // Render from the cached payload + the active filter (no refetch).
+ function render() {
+ var d = state.data;
+ updateChrome();
+ var hero = $('[data-video-cal-hero]'), cols = $('[data-video-cal-cols]');
+ if (!d) { showEmpty(true); if (hero) hero.innerHTML = ''; if (cols) cols.innerHTML = ''; return; }
+ var eps = filterEps(d.episodes || []);
+ var view = { episodes: eps, total: eps.length, today: d.today, start: d.start, end: d.end, days: d.days };
+ var has = eps.length > 0;
+ showEmpty(!has);
+ if (has) { renderHero(view); renderGrid(view); }
+ else { if (hero) hero.innerHTML = ''; if (cols) cols.innerHTML = ''; }
+ setSub(view);
+ }
function load() {
state.loaded = true;
showEmpty(false); showLoading(true);
- fetch(URL, { headers: { 'Accept': 'application/json' } })
+ var base = new Date(); base.setHours(0, 0, 0, 0); base.setDate(base.getDate() + state.offset * 7);
+ fetch(URL + '?days=7&start=' + isoOf(base), { headers: { 'Accept': 'application/json' } })
.then(function (r) { return r.ok ? r.json() : null; })
.then(function (d) {
showLoading(false);
- if (!d || d.error) { showEmpty(true); return; }
- if (!(d.episodes && d.episodes.length)) { showEmpty(true); return; }
+ if (!d || d.error) { state.data = null; render(); return; }
+ state.data = d;
state.eps = {};
- d.episodes.forEach(function (e) { state.eps[e.id] = e; });
- renderHero(d); renderGrid(d); setSub(d);
+ (d.episodes || []).forEach(function (e) { state.eps[e.id] = e; });
+ render();
})
- .catch(function () { showLoading(false); showEmpty(true); });
+ .catch(function () { showLoading(false); state.data = null; render(); });
}
function openFrom(target) {
@@ -225,6 +257,15 @@
wireTilt(hero, '.vcal-bb', 3);
}
wireTilt(cols, '.vcal-cell', 7);
+
+ var prev = $('[data-video-cal-prev]'); if (prev) prev.addEventListener('click', function () { state.offset--; load(); });
+ var next = $('[data-video-cal-next]'); if (next) next.addEventListener('click', function () { state.offset++; load(); });
+ var today = $('[data-video-cal-today]');
+ if (today) today.addEventListener('click', function () { if (state.offset !== 0) { state.offset = 0; load(); } });
+ var fbs = document.querySelectorAll('[data-video-cal-filter]');
+ for (var i = 0; i < fbs.length; i++) (function (b) {
+ b.addEventListener('click', function () { state.filter = b.getAttribute('data-video-cal-filter'); render(); });
+ })(fbs[i]);
}
// ── episode modal ─────────────────────────────────────────────────────────
diff --git a/webui/static/video/video-side.css b/webui/static/video/video-side.css
index 8ec7243e..278ec164 100644
--- a/webui/static/video/video-side.css
+++ b/webui/static/video/video-side.css
@@ -1716,3 +1716,23 @@ body[data-side="video"] #soulsync-toggle { display: none; }
.vcal-hero-wrap { padding-left: 18px; padding-right: 18px; }
.vcal-bb-title { font-size: 30px; } .vcal-bb { min-height: 240px; }
}
+
+/* ── Calendar — header controls (week nav + filter) ───────────────────────── */
+.vcal-head-row { position: relative; z-index: 1; display: flex; align-items: flex-end; justify-content: space-between; gap: 20px; flex-wrap: wrap; }
+.vcal-controls { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
+.vcal-nav { display: inline-flex; align-items: center; gap: 2px; padding: 4px; border-radius: 12px;
+ background: rgba(255, 255, 255, 0.06); border: 1px solid rgba(255, 255, 255, 0.08); }
+.vcal-nav-btn { width: 34px; height: 32px; border: 0; background: transparent; color: rgba(255, 255, 255, 0.75);
+ font-size: 20px; font-weight: 700; cursor: pointer; border-radius: 8px; line-height: 1; transition: all 0.15s ease; }
+.vcal-nav-btn:hover { background: rgba(255, 255, 255, 0.1); color: #fff; }
+.vcal-nav-today { border: 0; background: transparent; color: rgba(255, 255, 255, 0.82); font-size: 12.5px; font-weight: 800;
+ padding: 7px 12px; border-radius: 8px; cursor: pointer; transition: all 0.15s ease; }
+.vcal-nav-today:hover:not(:disabled) { background: rgba(255, 255, 255, 0.1); color: #fff; }
+.vcal-nav-today:disabled { opacity: 0.34; cursor: default; }
+.vcal-filter { display: inline-flex; gap: 3px; padding: 4px; border-radius: 12px;
+ background: rgba(255, 255, 255, 0.06); border: 1px solid rgba(255, 255, 255, 0.08); }
+.vcal-filter-btn { border: 0; background: transparent; color: rgba(255, 255, 255, 0.7); font-size: 12.5px; font-weight: 700;
+ padding: 7px 14px; border-radius: 8px; cursor: pointer; transition: all 0.15s ease; }
+.vcal-filter-btn:hover { color: #fff; background: rgba(255, 255, 255, 0.08); }
+.vcal-filter-btn--on { color: #fff; background: rgba(var(--vcal-accent), 0.9); box-shadow: 0 4px 14px rgba(var(--vcal-accent), 0.32); }
+@media (max-width: 720px) { .vcal-head-row { flex-direction: column; align-items: stretch; } .vcal-controls { justify-content: space-between; } }