From daea02d0fc097455d9c22d5623b7a34b0c8f564a Mon Sep 17 00:00:00 2001 From: 4DBug <4DBug@github.com> Date: Thu, 27 Aug 2026 11:10:56 -0500 Subject: [PATCH] push --- web/css/components.css | 2 +- web/css/pages.css | 544 +++++++++++++++++++++++------------------ web/js/cards.js | 1 - web/js/config.js | 2 +- web/js/watch.js | 334 ++++++++++++++++++------- web/watch.html | 80 +++--- 6 files changed, 592 insertions(+), 371 deletions(-) diff --git a/web/css/components.css b/web/css/components.css index fa84507..277d9d0 100644 --- a/web/css/components.css +++ b/web/css/components.css @@ -246,7 +246,7 @@ select.field { } .card__ep { - color: var(--color-text); + color: var(--color-text-dim); font-weight: var(--font-weight-semibold); letter-spacing: 0.03em; white-space: nowrap; diff --git a/web/css/pages.css b/web/css/pages.css index 3ac7fdf..4f316c9 100644 --- a/web/css/pages.css +++ b/web/css/pages.css @@ -258,99 +258,6 @@ color: var(--color-text-faint); } -/* ---- watch: hero ------------------------------------------------------------- */ - -.watch-hero { - position: relative; - display: flex; - gap: var(--space-5); - align-items: flex-end; - margin-bottom: var(--space-5); - padding: var(--space-5); - border: 1px solid var(--color-border); - border-radius: var(--radius-lg); - overflow: hidden; - background: var(--color-surface); -} - -.watch-hero__backdrop { - position: absolute; - inset: 0; - z-index: 0; - pointer-events: none; -} - -.watch-hero__backdrop img { - width: 100%; - height: 100%; - object-fit: cover; - opacity: 0.25; -} - -.watch-hero__poster { - position: relative; - z-index: 1; - flex: 0 0 180px; - width: 180px; - aspect-ratio: var(--poster-aspect); - border-radius: var(--radius-md); - overflow: hidden; - border: 1px solid var(--color-border-strong); - box-shadow: var(--shadow-2); - background: var(--color-surface-2); -} - -.watch-hero__poster img { - width: 100%; - height: 100%; - object-fit: cover; -} - -.watch-hero__body { - position: relative; - z-index: 1; - min-width: 0; - flex: 1; -} - -.watch-hero__title { - font-size: var(--text-3xl); - letter-spacing: -0.02em; - margin-bottom: var(--space-2); -} - -.watch-hero__facts { - display: flex; - align-items: center; - flex-wrap: wrap; - gap: var(--space-3); - font-size: var(--text-sm); - color: var(--color-text-dim); -} - -.watch-hero__rating { - color: var(--color-star); - font-weight: var(--font-weight-semibold); -} - -.watch-hero__genres { - display: flex; - flex-wrap: wrap; - gap: var(--space-2); - margin-top: var(--space-3); -} - -.watch-hero__overview { - margin-top: var(--space-4); - max-width: 72ch; - color: var(--color-text-dim); - font-size: var(--text-sm); - display: -webkit-box; - -webkit-line-clamp: 4; - -webkit-box-orient: vertical; - overflow: hidden; -} - /* ---- watch: player (.player-wrap + controls live in components.css) ------------- */ .player-status { @@ -399,138 +306,6 @@ flex: 1; } -.loading-cell { - grid-column: 1 / -1; - display: flex; - justify-content: center; - padding: var(--space-8) 0; -} - -/* ---- watch: episodes --------------------------------------------------------- */ - -.episodes { - margin-top: var(--space-7); -} - -.episodes__toolbar { - display: flex; - align-items: center; - gap: var(--space-3); - flex-wrap: wrap; - margin-bottom: var(--space-4); -} - -.episodes__title { - font-size: var(--text-xl); - font-weight: var(--font-weight-semibold); - margin-right: auto; -} - -.episodes__toolbar select.field { - width: auto; -} - -.episode-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); - gap: var(--space-3); -} - -.episode { - display: flex; - gap: var(--space-3); - padding: var(--space-3); - border: 1px solid var(--color-border); - border-radius: var(--radius-md); - background: var(--color-surface); - cursor: pointer; - transition: border-color 120ms ease, background 120ms ease; -} - -.episode:hover { - border-color: var(--color-border-strong); - background: var(--color-surface-hover); -} - -.episode--current { - border-color: var(--color-accent); -} - -.episode__thumb { - position: relative; - flex: 0 0 108px; - width: 108px; - aspect-ratio: 16 / 9; - border-radius: var(--radius-sm); - overflow: hidden; - background: var(--color-surface-2); -} - -.episode__thumb img { - width: 100%; - height: 100%; - object-fit: cover; -} - -.episode__thumb--empty { - display: grid; - place-items: center; - color: var(--color-text-faint); - font-size: var(--text-lg); - font-weight: var(--font-weight-semibold); - background: linear-gradient(135deg, var(--color-surface-2), var(--color-surface)); -} - -.episode__play { - position: absolute; - inset: 0; - display: flex; - align-items: center; - justify-content: center; - font-size: var(--text-lg); - color: #fff; - background: rgba(0, 0, 0, 0.35); - opacity: 0; - transition: opacity 120ms ease; -} - -.episode:hover .episode__play { - opacity: 1; -} - -.episode--current .episode__play { - opacity: 1; - background: color-mix(in srgb, var(--color-accent) 35%, rgba(0, 0, 0, 0.35)); -} - -.episode__body { - min-width: 0; - display: flex; - flex-direction: column; - justify-content: center; -} - -.episode__num { - font-size: var(--text-xs); - color: var(--color-text-faint); -} - -.episode__title { - font-size: var(--text-sm); - font-weight: var(--font-weight-medium); - line-height: 1.3; - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; -} - -.episode__meta { - margin-top: 2px; - font-size: var(--text-xs); - color: var(--color-text-faint); -} - /* ---- settings ------------------------------------------------------------------ */ .settings { @@ -677,21 +452,6 @@ display: none; } - .watch-hero { - flex-direction: column; - align-items: flex-start; - padding: var(--space-4); - } - - .watch-hero__poster { - flex-basis: auto; - width: 132px; - } - - .watch-hero__title { - font-size: var(--text-2xl); - } - .search-bar__form { flex-direction: column; } @@ -711,3 +471,307 @@ padding: var(--space-4); } } + +/* ---- watch v2: full-bleed hero + overlay player -------------------------------- */ + +.watch-page .page--watch { + padding-top: 0; +} + +.watch-page .site-header { + background: transparent; + border-bottom-color: transparent; + backdrop-filter: none; +} + +.watch-page.is-scrolled .site-header { + background: color-mix(in srgb, var(--color-bg) 85%, transparent); + border-bottom-color: var(--color-border); + backdrop-filter: blur(12px); +} + +.watch-hero { + position: relative; + isolation: isolate; + margin-top: calc(var(--header-height) * -1); + min-height: min(85svh, 900px); + display: flex; + align-items: flex-end; + overflow: hidden; + background: var(--color-surface); +} + +.watch-hero__backdrop { + position: absolute; + inset: 0; + z-index: -2; + background-size: cover; + background-position: center; + background-color: var(--color-surface); +} + +.watch-hero__scrim { + position: absolute; + inset: 0; + z-index: -1; + background: + linear-gradient( + 180deg, + color-mix(in srgb, var(--color-bg) 20%, transparent) 0%, + color-mix(in srgb, var(--color-bg) 55%, transparent) 55%, + var(--color-bg) 100% + ), + linear-gradient( + 90deg, + color-mix(in srgb, var(--color-bg) 85%, transparent) 0%, + transparent 65% + ); +} + +.watch-hero__inner { + width: 100%; + max-width: var(--container-max); + margin: 0 auto; + padding: calc(var(--header-height) + var(--space-8)) var(--container-pad) var(--space-8); +} + +.watch-hero__logo { + max-width: min(420px, 60vw); + max-height: 140px; + object-fit: contain; + margin-bottom: var(--space-4); +} + +.watch-hero__title { + margin: 0; + font-size: clamp(2.2rem, 6vw, 4.5rem); + line-height: 1.02; + letter-spacing: -0.035em; +} + +.watch-hero__meta { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--space-2) var(--space-3); + margin-top: var(--space-4); + color: var(--color-text-dim); + font-size: var(--text-sm); +} + +.watch-hero__rating { + color: var(--color-star); + letter-spacing: 2px; +} + +.watch-hero__dot { + color: var(--color-text-faint); +} + +.watch-hero__genres { + display: flex; + flex-wrap: wrap; + gap: var(--space-2); + margin-top: var(--space-4); +} + +.watch-hero__overview { + max-width: 72ch; + margin: var(--space-4) 0 0; + color: var(--color-text-dim); + line-height: 1.55; + display: -webkit-box; + -webkit-line-clamp: 4; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.watch-hero__actions { + margin-top: var(--space-6); + display: flex; +} + +.watch-hero__actions .btn--primary { + padding: var(--space-3) var(--space-6); + font-size: var(--text-md); +} + +.watch-episodes { + padding: var(--space-8) 0; +} + +.watch-episodes__title { + margin: 0 0 var(--space-4); + font-size: var(--text-xl); +} + +.season-tabs { + display: flex; + gap: var(--space-2); + overflow-x: auto; + padding-bottom: var(--space-2); + margin-bottom: var(--space-5); +} + +.episode-shelf { + display: grid; + grid-auto-flow: column; + grid-auto-columns: minmax(220px, 1fr); + gap: var(--space-4); + overflow-x: auto; + scroll-snap-type: x mandatory; + padding-bottom: var(--space-2); +} + +.episode-shelf__loading, +.episode-shelf__empty { + grid-column: 1 / -1; + display: grid; + place-items: center; + min-height: 180px; +} + +.episode-card { + scroll-snap-align: start; + text-align: left; + border: 1px solid var(--color-border); + background: var(--color-surface); + border-radius: var(--radius-lg); + overflow: hidden; + padding: 0; + cursor: pointer; +} + +.episode-card--current { + outline: 2px solid var(--color-accent); + outline-offset: 2px; + border-color: var(--color-accent); +} + +.episode-card__thumb { + position: relative; + display: block; + aspect-ratio: 16 / 9; + width: 100%; + background: var(--color-surface-2); +} + +.episode-card__thumb img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} + +.episode-card__thumb--empty { + display: grid; + place-items: center; + font-size: var(--text-2xl); + color: var(--color-text-faint); +} + +.episode-card__progress { + position: absolute; + left: 0; + bottom: 0; + height: 3px; + background: var(--color-accent); +} + +.episode-card__body { + display: grid; + gap: var(--space-1); + padding: var(--space-3); +} + +.episode-card__num { + color: var(--color-text-faint); + font-size: var(--text-xs); + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.episode-card__title { + font-weight: 600; + color: var(--color-text-dim); +} + +.episode-card__meta { + color: var(--color-text-dim); + font-size: var(--text-xs); +} + +.player-overlay { + position: fixed; + inset: 0; + z-index: 100; + background: #000; + display: grid; + place-items: center; +} + +.player-overlay[hidden] { + display: none; +} + +body.player-open { + overflow: hidden; +} + +body.player-open .player-overlay { + display: grid; +} + +.player-overlay__close { + position: absolute; + top: var(--space-4); + right: var(--space-4); + z-index: 20; + background: color-mix(in srgb, #000 60%, transparent); + color: #fff; + border: 1px solid rgb(255 255 255 / 20%); +} + +#playerOverlay .player-wrap { + width: 100vw; + height: 100vh; + height: 100dvh; + aspect-ratio: auto; + border: none; + border-radius: 0; +} + +#playerOverlay .watch-toolbar { + position: absolute; + left: 50%; + transform: translateX(-50%); + bottom: var(--space-5); + z-index: 2; + background: color-mix(in srgb, #000 65%, transparent); + border: 1px solid rgb(255 255 255 / 10%); + padding: var(--space-2) var(--space-3); + border-radius: var(--radius-md); +} + +@media (max-width: 720px) { + .watch-hero { + min-height: 70svh; + } + + .watch-hero__inner { + padding-bottom: var(--space-6); + } + + .watch-hero__actions .btn--primary { + width: 100%; + } + + .episode-shelf { + grid-auto-columns: minmax(160px, 80vw); + } + + .player-overlay__close { + top: var(--space-2); + right: var(--space-2); + } +} diff --git a/web/js/cards.js b/web/js/cards.js index 7ecc986..cff28fd 100644 --- a/web/js/cards.js +++ b/web/js/cards.js @@ -151,7 +151,6 @@
${esc(item.overview)}
` : ''} diff --git a/web/js/config.js b/web/js/config.js index fe4145e..49f0ae2 100644 --- a/web/js/config.js +++ b/web/js/config.js @@ -82,7 +82,7 @@ } } - const PREFERRED_PROVIDERS = ['cineby', 'videasy', 'vidfast']; + const PREFERRED_PROVIDERS = ['cineby', 'playimdb', 'vidrock']; // Merge the server's provider lists with the user's toggles. // Returns the enabled provider names, in the server's `all` order. diff --git a/web/js/watch.js b/web/js/watch.js index 253d6ab..0357cc9 100644 --- a/web/js/watch.js +++ b/web/js/watch.js @@ -19,15 +19,29 @@ return; } + const heroEl = document.getElementById('hero'); + const heroBackdrop = document.getElementById('heroBackdrop'); + const heroTitleCard = document.getElementById('heroTitleCard'); + const heroTitle = document.getElementById('heroTitle'); + const heroMeta = document.getElementById('heroMeta'); + const heroGenres = document.getElementById('heroGenres'); + const heroOverview = document.getElementById('heroOverview'); + const playButton = document.getElementById('playButton'); + const playLabel = document.getElementById('playLabel'); + const episodeSection = document.getElementById('episodeSection'); + const seasonTabs = document.getElementById('seasonTabs'); + const episodeShelf = document.getElementById('episodeShelf'); + const playerOverlay = document.getElementById('playerOverlay'); + const playerClose = document.getElementById('playerClose'); + + // custom player controls const video = document.getElementById('video'); const statusEl = document.getElementById('playerStatus'); const sourceLabel = document.getElementById('sourceLabel'); const resumeNote = document.getElementById('resumeNote'); const sourcesPanel = document.getElementById('sourcesPanel'); const sourcesClose = document.getElementById('sourcesClose'); - const episodeSection = document.getElementById('episodes'); - // custom player controls const playerWrap = document.getElementById('playerWrap'); const bigPlay = document.getElementById('bigPlay'); const pcSeek = document.getElementById('pcSeek'); @@ -40,10 +54,6 @@ const pcGear = document.getElementById('pcGear'); const pcPip = document.getElementById('pcPip'); const pcFull = document.getElementById('pcFull'); - const seasonSelect = document.getElementById('seasonSelect'); - const episodeGrid = document.getElementById('episodeGrid'); - const prevBtn = document.getElementById('prevEpisode'); - const nextBtn = document.getElementById('nextEpisode'); const state = { details: null, @@ -57,8 +67,13 @@ season: null, episode: null, loadedKey: null, + loadToken: 0, + overlayOpen: false, + lastFocusedElement: null, + pendingClose: false, }; + /* ---- status overlay ------------------------------------------------------ */ function closeSources() { @@ -323,7 +338,16 @@ break; case 'Escape': if (!sourcesPanel.hidden) closeSources(); + else if (state.overlayOpen) { + if (document.fullscreenElement) { + state.pendingClose = true; + document.exitFullscreen().catch(() => {}); + } else { + closePlayer(); + } + } break; + } if (video.src || video.currentSrc) showControls(); }); @@ -334,36 +358,120 @@ /* ---- hero ------------------------------------------------------------------ */ + function openPlayer() { + if (state.overlayOpen) return; + state.overlayOpen = true; + state.lastFocusedElement = document.activeElement; + playerOverlay.hidden = false; + document.body.classList.add('player-open'); + playerClose.focus(); + } + + function closePlayer() { + if (!state.overlayOpen) return; + state.overlayOpen = false; + state.pendingClose = false; + video.pause(); + playerOverlay.hidden = true; + document.body.classList.remove('player-open'); + if (state.lastFocusedElement && typeof state.lastFocusedElement.focus === 'function') { + state.lastFocusedElement.focus(); + } + } + + document.addEventListener('fullscreenchange', () => { + if (!document.fullscreenElement && state.pendingClose && state.overlayOpen) closePlayer(); + }); + + function syncHeader() { + document.body.classList.toggle('is-scrolled', window.scrollY > 8); + } + window.addEventListener('scroll', syncHeader, { passive: true }); + syncHeader(); + + function setPlayLabel() { + if (type === 'tv' && state.season != null && state.episode != null) { + playLabel.textContent = `Play S${state.season} E${state.episode}`; + } else { + playLabel.textContent = 'Play'; + } + } + function renderHero(d) { document.title = `${d.name || d.title} — bug.tv`; const isTv = type === 'tv'; const year = String((isTv ? d.first_air_date : d.release_date) || '').slice(0, 4); - const facts = [ - isTv ? `${d.number_of_seasons || 0} seasons` : SR.util.runtimeLabel(d.runtime), - year, - ] - .filter(Boolean) - .map(SR.util.escapeHtml) - .join('·'); - const rating = d.vote_average - ? `` + const facts = []; + if (isTv) { + if (d.number_of_seasons) { + facts.push(`${d.number_of_seasons} season${d.number_of_seasons === 1 ? '' : 's'}`); + } + } else { + const runtime = SR.util.runtimeLabel(d.runtime); + if (runtime) facts.push(runtime); + } + if (year) facts.push(year); + + const vote = Number(d.vote_average || 0); + const filled = Math.max(0, Math.min(5, Math.round(vote / 2))); + const rating = vote + ? `` : ''; - const genres = (d.genres || []) - .map((g) => `${SR.util.escapeHtml(g.name)}`) - .join(''); - const poster = SR.util.tmdbResize(d.poster_url, 'w500'); - const backdrop = SR.util.tmdbResize(d.backdrop_url, 'w1280'); - document.getElementById('hero').innerHTML = ` - ${backdrop ? `${SR.util.escapeHtml(d.overview)}
` : ''} -