Fix SPA transitions: title, cover image, and slideshow not updating
Three bugs in the Up Next / playlist SPA transition for music-type videos: 1. Title selector was '.audio-title' (doesn't exist) instead of '.video-title span' 2. Cover image only updated #audioCoverImg — missed when video has a slideshow 3. Slideshow SLIDE_URLS lived in a closed IIFE and couldn't be updated cross-video Fix: always render both #audioCoverImg and #slideshowWrap in the DOM (toggle display via inline style), hoist slideshow state variables outside the if-block, and expose window._audioPlayerUpdate(d) that both recTransitionTo and plTransitionTo call. The hook handles all cases: cover-to-cover, cover-to-slideshow, slideshow-to-cover, slideshow-to-slideshow. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
2c0888088d
commit
6e7d5d178a
@ -11,15 +11,12 @@
|
||||
<div class="ytp-wrap" id="ytpWrap">
|
||||
<div class="ytp audio-ytp" id="audioContainer" tabindex="0">
|
||||
|
||||
{{-- Cover art / slideshow --}}
|
||||
@if(count($slideUrls) > 1)
|
||||
<div class="slideshow-wrap" id="slideshowWrap">
|
||||
<img src="{{ $slideUrls[0] }}" alt="" class="slide-img slide-a" id="slideA">
|
||||
<img src="{{ $slideUrls[1] }}" alt="" class="slide-img slide-b" id="slideB">
|
||||
{{-- Cover art / slideshow — both always in DOM so SPA transitions can switch between them --}}
|
||||
<img src="{{ $coverUrl }}" alt="{{ $video->title }}" class="audio-cover-img" id="audioCoverImg"@if(count($slideUrls) > 1) style="display:none"@endif>
|
||||
<div class="slideshow-wrap" id="slideshowWrap"@if(count($slideUrls) <= 1) style="display:none"@endif>
|
||||
<img src="{{ $slideUrls[0] ?? $coverUrl }}" alt="" class="slide-img slide-a" id="slideA">
|
||||
<img src="{{ count($slideUrls) > 1 ? $slideUrls[1] : ($slideUrls[0] ?? $coverUrl) }}" alt="" class="slide-img slide-b" id="slideB">
|
||||
</div>
|
||||
@else
|
||||
<img src="{{ $coverUrl }}" alt="{{ $video->title }}" class="audio-cover-img" id="audioCoverImg">
|
||||
@endif
|
||||
|
||||
{{-- Bars canvas overlay --}}
|
||||
<canvas id="audioAnimCanvas" style="position:absolute;inset:0;width:100%;height:100%;pointer-events:none;display:none;z-index:3;"></canvas>
|
||||
@ -726,53 +723,37 @@ 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)
|
||||
|
||||
// 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;
|
||||
});
|
||||
let aIsTop = true;
|
||||
let slideshowTimer = null;
|
||||
const slideOrientations = new Array(Math.max(SLIDE_URLS.length, 1)).fill(false);
|
||||
|
||||
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';
|
||||
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';
|
||||
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());
|
||||
@ -782,51 +763,79 @@ if (SLIDE_URLS.length > 1) {
|
||||
slideshowTimer = null;
|
||||
}
|
||||
|
||||
// Always attach listeners; functions guard themselves with SLIDE_URLS.length check
|
||||
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;
|
||||
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
|
||||
);
|
||||
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.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);
|
||||
slideB.src = SLIDE_URLS[nextIdx]; applyOrientation(slideB, nextIdx);
|
||||
aIsTop = true;
|
||||
requestAnimationFrame(() => {
|
||||
slideA.style.transition = '';
|
||||
slideB.style.transition = '';
|
||||
});
|
||||
requestAnimationFrame(() => { slideA.style.transition = ''; slideB.style.transition = ''; });
|
||||
if (!audio.paused) startSlideshow();
|
||||
});
|
||||
|
||||
// Extract colors + apply orientation for first slide
|
||||
// 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();
|
||||
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 {
|
||||
slideA.addEventListener('load', initSlideA, { once: true });
|
||||
}
|
||||
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');
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user