diff --git a/resources/views/videos/partials/audio-player.blade.php b/resources/views/videos/partials/audio-player.blade.php index e49170d..48b2e90 100644 --- a/resources/views/videos/partials/audio-player.blade.php +++ b/resources/views/videos/partials/audio-player.blade.php @@ -11,15 +11,12 @@
- {{-- Cover art / slideshow --}} - @if(count($slideUrls) > 1) -
- - + {{-- Cover art / slideshow — both always in DOM so SPA transitions can switch between them --}} + {{ $video->title }} 1) style="display:none"@endif> + - @else - {{ $video->title }} - @endif {{-- Bars canvas overlay --}} @@ -726,107 +723,119 @@ function stopBars() { } // ── Crossfade slideshow ─────────────────────────────────────── +// Variables hoisted outside the if-block so the SPA update hook can access them const SLIDE_URLS = @json($slideUrls); -if (SLIDE_URLS.length > 1) { - const slideA = document.getElementById('slideA'); - const slideB = document.getElementById('slideB'); - let currentSlide = 0; - let aIsTop = true; // slideA starts on top (opacity 1) +const slideA = document.getElementById('slideA'); +const slideB = document.getElementById('slideB'); +let currentSlide = 0; +let aIsTop = true; +let slideshowTimer = null; +const slideOrientations = new Array(Math.max(SLIDE_URLS.length, 1)).fill(false); - // Preload all images and detect portrait orientation - const slideOrientations = new Array(SLIDE_URLS.length).fill(false); // true = portrait - SLIDE_URLS.forEach((url, idx) => { - const img = new Image(); - img.onload = () => { slideOrientations[idx] = img.naturalHeight > img.naturalWidth; }; - img.src = url; - }); - - function applyOrientation(el, idx) { - el.classList.toggle('portrait', !!slideOrientations[idx]); - } - - function getSlideInterval() { - return audio.duration ? (audio.duration / SLIDE_URLS.length) * 1000 : 8000; - } - - function advanceSlide() { - currentSlide = (currentSlide + 1) % SLIDE_URLS.length; - const next = SLIDE_URLS[currentSlide]; - - if (aIsTop) { - slideB.src = next; - applyOrientation(slideB, currentSlide); - slideB.style.zIndex = '2'; - slideB.style.opacity = '1'; - slideA.style.opacity = '0'; - slideA.style.zIndex = '1'; - } else { - slideA.src = next; - applyOrientation(slideA, currentSlide); - slideA.style.zIndex = '2'; - slideA.style.opacity = '1'; - slideB.style.opacity = '0'; - slideB.style.zIndex = '1'; - } - aIsTop = !aIsTop; - } - - let slideshowTimer = null; - - function startSlideshow() { - if (slideshowTimer || SLIDE_URLS.length <= 1) return; - slideshowTimer = setInterval(advanceSlide, getSlideInterval()); - } - function stopSlideshow() { - clearInterval(slideshowTimer); - slideshowTimer = null; - } - - audio.addEventListener('play', startSlideshow); - audio.addEventListener('pause', stopSlideshow); - audio.addEventListener('ended', stopSlideshow); - - // Seek: jump to the correct slide instantly (no transition flash) - audio.addEventListener('seeked', () => { - if (!audio.duration) return; - stopSlideshow(); - const idx = Math.min( - Math.floor((audio.currentTime / audio.duration) * SLIDE_URLS.length), - SLIDE_URLS.length - 1 - ); - currentSlide = idx; - // Reset: show correct slide without animation - slideA.style.transition = 'none'; - slideB.style.transition = 'none'; - slideA.src = SLIDE_URLS[idx]; - applyOrientation(slideA, idx); - slideA.style.opacity = '1'; slideA.style.zIndex = '2'; +function applyOrientation(el, idx) { + if (!el) return; + el.classList.toggle('portrait', !!slideOrientations[idx]); +} +function getSlideInterval() { + return audio.duration ? (audio.duration / SLIDE_URLS.length) * 1000 : 8000; +} +function advanceSlide() { + if (!slideA || !slideB || SLIDE_URLS.length <= 1) return; + currentSlide = (currentSlide + 1) % SLIDE_URLS.length; + const next = SLIDE_URLS[currentSlide]; + if (aIsTop) { + slideB.src = next; applyOrientation(slideB, currentSlide); + slideB.style.zIndex = '2'; slideB.style.opacity = '1'; + slideA.style.opacity = '0'; slideA.style.zIndex = '1'; + } else { + slideA.src = next; applyOrientation(slideA, currentSlide); + slideA.style.zIndex = '2'; slideA.style.opacity = '1'; slideB.style.opacity = '0'; slideB.style.zIndex = '1'; - const nextIdx = (idx + 1) % SLIDE_URLS.length; - slideB.src = SLIDE_URLS[nextIdx]; - applyOrientation(slideB, nextIdx); - aIsTop = true; - requestAnimationFrame(() => { - slideA.style.transition = ''; - slideB.style.transition = ''; - }); - if (!audio.paused) startSlideshow(); - }); + } + aIsTop = !aIsTop; +} +function startSlideshow() { + if (slideshowTimer || SLIDE_URLS.length <= 1) return; + slideshowTimer = setInterval(advanceSlide, getSlideInterval()); +} +function stopSlideshow() { + clearInterval(slideshowTimer); + slideshowTimer = null; +} - // Extract colors + apply orientation for first slide +// Always attach listeners; functions guard themselves with SLIDE_URLS.length check +audio.addEventListener('play', startSlideshow); +audio.addEventListener('pause', stopSlideshow); +audio.addEventListener('ended', stopSlideshow); + +audio.addEventListener('seeked', () => { + if (SLIDE_URLS.length <= 1 || !slideA || !slideB || !audio.duration) return; + stopSlideshow(); + const idx = Math.min(Math.floor((audio.currentTime / audio.duration) * SLIDE_URLS.length), SLIDE_URLS.length - 1); + currentSlide = idx; + slideA.style.transition = 'none'; slideB.style.transition = 'none'; + slideA.src = SLIDE_URLS[idx]; applyOrientation(slideA, idx); + slideA.style.opacity = '1'; slideA.style.zIndex = '2'; + slideB.style.opacity = '0'; slideB.style.zIndex = '1'; + const nextIdx = (idx + 1) % SLIDE_URLS.length; + slideB.src = SLIDE_URLS[nextIdx]; applyOrientation(slideB, nextIdx); + aIsTop = true; + requestAnimationFrame(() => { slideA.style.transition = ''; slideB.style.transition = ''; }); + if (!audio.paused) startSlideshow(); +}); + +// Preload orientations +SLIDE_URLS.forEach((url, idx) => { + const img = new Image(); + img.onload = () => { slideOrientations[idx] = img.naturalHeight > img.naturalWidth; }; + img.src = url; +}); + +if (SLIDE_URLS.length > 1 && slideA) { function initSlideA() { extractColors(slideA); const portrait = slideA.naturalHeight > slideA.naturalWidth; slideA.classList.toggle('portrait', portrait); slideOrientations[0] = portrait; } - if (slideA.complete && slideA.naturalWidth) { - initSlideA(); - } else { - slideA.addEventListener('load', initSlideA, { once: true }); - } + if (slideA.complete && slideA.naturalWidth) initSlideA(); + else slideA.addEventListener('load', initSlideA, { once: true }); } +// ── SPA update hook — called by recTransitionTo / plTransitionTo ────────── +window._audioPlayerUpdate = function(d) { + var newSlides = (d.slides && d.slides.length > 1) ? d.slides : []; + var coverEl = document.getElementById('audioCoverImg'); + var slideshowEl = document.getElementById('slideshowWrap'); + + stopSlideshow(); + SLIDE_URLS.length = 0; + + if (newSlides.length > 1) { + newSlides.forEach(function(s) { SLIDE_URLS.push(s); }); + if (coverEl) coverEl.style.display = 'none'; + if (slideshowEl) slideshowEl.style.display = ''; + currentSlide = 0; aIsTop = true; + if (slideA) { slideA.style.transition='none'; slideA.src=newSlides[0]; slideA.style.opacity='1'; slideA.style.zIndex='2'; } + if (slideB) { slideB.style.transition='none'; slideB.src=newSlides[1]||newSlides[0]; slideB.style.opacity='0'; slideB.style.zIndex='1'; } + requestAnimationFrame(function() { if(slideA) slideA.style.transition=''; if(slideB) slideB.style.transition=''; }); + } else { + if (slideshowEl) slideshowEl.style.display = 'none'; + if (coverEl) { coverEl.style.display = ''; coverEl.src = d.cover_url || ''; } + } + + // Update page title + var titleEl = document.querySelector('.video-title span'); + if (titleEl) titleEl.textContent = d.title || ''; + document.title = (d.title || '') + ' | {{ config("app.name") }}'; + + // Reset progress bar + if (played) played.style.width = '0%'; + if (scrubber) scrubber.parentElement.style.left = '0%'; + if (timeCur) timeCur.textContent = '0:00'; + if (timeDur && d.duration) timeDur.textContent = fmt(d.duration); +}; + // ── Init ───────────────────────────────────────────────────── const savedVol = localStorage.getItem('ytpVolume'); const savedMuted = localStorage.getItem('ytpMuted'); diff --git a/resources/views/videos/types/music.blade.php b/resources/views/videos/types/music.blade.php index 67b49bc..ee06206 100644 --- a/resources/views/videos/types/music.blade.php +++ b/resources/views/videos/types/music.blade.php @@ -559,24 +559,8 @@ var audio = document.getElementById('audioEl'); if (audio) { audio.src=d.stream_url; audio.load(); } - // update cover / slideshow - var ci = document.getElementById('audioCoverImg'); - if (ci) ci.src = d.cover_url; - var sa = document.getElementById('slideA'); - if (sa) sa.src = d.cover_url; - - // reset progress - var pl=document.getElementById('ytpPlayed'), sc=document.getElementById('ytpScrubber'); - var cu=document.getElementById('ytpCurrent'), dr=document.getElementById('ytpDuration'); - if(pl) pl.style.width='0%'; - if(sc) sc.style.left='0%'; - if(cu) cu.textContent='0:00'; - if(dr&&d.duration) dr.textContent=plFmt(d.duration); - - // update visible title - var ts=document.querySelector('.video-title span'); - if(ts) ts.textContent=d.title; - document.title=d.title+' | {{ config("app.name") }}'; + // update cover / slideshow / title / progress via shared hook + if (window._audioPlayerUpdate) window._audioPlayerUpdate(d); // update state PL_CURRENT = d.id; @@ -762,17 +746,11 @@ var d = await resp.json(); var audio = document.getElementById('audioEl'); - if (audio) { - audio.src = d.stream_url; - audio.load(); - audio.play().catch(function(){}); - } + if (audio) { audio.src = d.stream_url; audio.load(); } - var coverImg = document.querySelector('.audio-cover-img'); - if (coverImg) coverImg.src = d.cover_url || ''; + if (window._audioPlayerUpdate) window._audioPlayerUpdate(d); - var titleEl = document.querySelector('.audio-title'); - if (titleEl) titleEl.textContent = d.title || ''; + if (audio) audio.play().catch(function(){}); if (pushHist !== false) history.pushState({ url: url }, '', url);