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-wrap" id="ytpWrap">
|
||||||
<div class="ytp audio-ytp" id="audioContainer" tabindex="0">
|
<div class="ytp audio-ytp" id="audioContainer" tabindex="0">
|
||||||
|
|
||||||
{{-- Cover art / slideshow --}}
|
{{-- Cover art / slideshow — both always in DOM so SPA transitions can switch between them --}}
|
||||||
@if(count($slideUrls) > 1)
|
<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">
|
<div class="slideshow-wrap" id="slideshowWrap"@if(count($slideUrls) <= 1) style="display:none"@endif>
|
||||||
<img src="{{ $slideUrls[0] }}" alt="" class="slide-img slide-a" id="slideA">
|
<img src="{{ $slideUrls[0] ?? $coverUrl }}" alt="" class="slide-img slide-a" id="slideA">
|
||||||
<img src="{{ $slideUrls[1] }}" alt="" class="slide-img slide-b" id="slideB">
|
<img src="{{ count($slideUrls) > 1 ? $slideUrls[1] : ($slideUrls[0] ?? $coverUrl) }}" alt="" class="slide-img slide-b" id="slideB">
|
||||||
</div>
|
</div>
|
||||||
@else
|
|
||||||
<img src="{{ $coverUrl }}" alt="{{ $video->title }}" class="audio-cover-img" id="audioCoverImg">
|
|
||||||
@endif
|
|
||||||
|
|
||||||
{{-- Bars canvas overlay --}}
|
{{-- Bars canvas overlay --}}
|
||||||
<canvas id="audioAnimCanvas" style="position:absolute;inset:0;width:100%;height:100%;pointer-events:none;display:none;z-index:3;"></canvas>
|
<canvas id="audioAnimCanvas" style="position:absolute;inset:0;width:100%;height:100%;pointer-events:none;display:none;z-index:3;"></canvas>
|
||||||
@ -726,107 +723,119 @@ function stopBars() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Crossfade slideshow ───────────────────────────────────────
|
// ── Crossfade slideshow ───────────────────────────────────────
|
||||||
|
// Variables hoisted outside the if-block so the SPA update hook can access them
|
||||||
const SLIDE_URLS = @json($slideUrls);
|
const SLIDE_URLS = @json($slideUrls);
|
||||||
if (SLIDE_URLS.length > 1) {
|
const slideA = document.getElementById('slideA');
|
||||||
const slideA = document.getElementById('slideA');
|
const slideB = document.getElementById('slideB');
|
||||||
const slideB = document.getElementById('slideB');
|
let currentSlide = 0;
|
||||||
let currentSlide = 0;
|
let aIsTop = true;
|
||||||
let aIsTop = true; // slideA starts on top (opacity 1)
|
let slideshowTimer = null;
|
||||||
|
const slideOrientations = new Array(Math.max(SLIDE_URLS.length, 1)).fill(false);
|
||||||
|
|
||||||
// Preload all images and detect portrait orientation
|
function applyOrientation(el, idx) {
|
||||||
const slideOrientations = new Array(SLIDE_URLS.length).fill(false); // true = portrait
|
if (!el) return;
|
||||||
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]);
|
el.classList.toggle('portrait', !!slideOrientations[idx]);
|
||||||
}
|
}
|
||||||
|
function getSlideInterval() {
|
||||||
function getSlideInterval() {
|
|
||||||
return audio.duration ? (audio.duration / SLIDE_URLS.length) * 1000 : 8000;
|
return audio.duration ? (audio.duration / SLIDE_URLS.length) * 1000 : 8000;
|
||||||
}
|
}
|
||||||
|
function advanceSlide() {
|
||||||
function advanceSlide() {
|
if (!slideA || !slideB || SLIDE_URLS.length <= 1) return;
|
||||||
currentSlide = (currentSlide + 1) % SLIDE_URLS.length;
|
currentSlide = (currentSlide + 1) % SLIDE_URLS.length;
|
||||||
const next = SLIDE_URLS[currentSlide];
|
const next = SLIDE_URLS[currentSlide];
|
||||||
|
|
||||||
if (aIsTop) {
|
if (aIsTop) {
|
||||||
slideB.src = next;
|
slideB.src = next; applyOrientation(slideB, currentSlide);
|
||||||
applyOrientation(slideB, currentSlide);
|
slideB.style.zIndex = '2'; slideB.style.opacity = '1';
|
||||||
slideB.style.zIndex = '2';
|
slideA.style.opacity = '0'; slideA.style.zIndex = '1';
|
||||||
slideB.style.opacity = '1';
|
|
||||||
slideA.style.opacity = '0';
|
|
||||||
slideA.style.zIndex = '1';
|
|
||||||
} else {
|
} else {
|
||||||
slideA.src = next;
|
slideA.src = next; applyOrientation(slideA, currentSlide);
|
||||||
applyOrientation(slideA, currentSlide);
|
slideA.style.zIndex = '2'; slideA.style.opacity = '1';
|
||||||
slideA.style.zIndex = '2';
|
slideB.style.opacity = '0'; slideB.style.zIndex = '1';
|
||||||
slideA.style.opacity = '1';
|
|
||||||
slideB.style.opacity = '0';
|
|
||||||
slideB.style.zIndex = '1';
|
|
||||||
}
|
}
|
||||||
aIsTop = !aIsTop;
|
aIsTop = !aIsTop;
|
||||||
}
|
}
|
||||||
|
function startSlideshow() {
|
||||||
let slideshowTimer = null;
|
|
||||||
|
|
||||||
function startSlideshow() {
|
|
||||||
if (slideshowTimer || SLIDE_URLS.length <= 1) return;
|
if (slideshowTimer || SLIDE_URLS.length <= 1) return;
|
||||||
slideshowTimer = setInterval(advanceSlide, getSlideInterval());
|
slideshowTimer = setInterval(advanceSlide, getSlideInterval());
|
||||||
}
|
}
|
||||||
function stopSlideshow() {
|
function stopSlideshow() {
|
||||||
clearInterval(slideshowTimer);
|
clearInterval(slideshowTimer);
|
||||||
slideshowTimer = null;
|
slideshowTimer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
audio.addEventListener('play', startSlideshow);
|
// Always attach listeners; functions guard themselves with SLIDE_URLS.length check
|
||||||
audio.addEventListener('pause', stopSlideshow);
|
audio.addEventListener('play', startSlideshow);
|
||||||
audio.addEventListener('ended', stopSlideshow);
|
audio.addEventListener('pause', stopSlideshow);
|
||||||
|
audio.addEventListener('ended', stopSlideshow);
|
||||||
|
|
||||||
// Seek: jump to the correct slide instantly (no transition flash)
|
audio.addEventListener('seeked', () => {
|
||||||
audio.addEventListener('seeked', () => {
|
if (SLIDE_URLS.length <= 1 || !slideA || !slideB || !audio.duration) return;
|
||||||
if (!audio.duration) return;
|
|
||||||
stopSlideshow();
|
stopSlideshow();
|
||||||
const idx = Math.min(
|
const idx = Math.min(Math.floor((audio.currentTime / audio.duration) * SLIDE_URLS.length), SLIDE_URLS.length - 1);
|
||||||
Math.floor((audio.currentTime / audio.duration) * SLIDE_URLS.length),
|
|
||||||
SLIDE_URLS.length - 1
|
|
||||||
);
|
|
||||||
currentSlide = idx;
|
currentSlide = idx;
|
||||||
// Reset: show correct slide without animation
|
slideA.style.transition = 'none'; slideB.style.transition = 'none';
|
||||||
slideA.style.transition = 'none';
|
slideA.src = SLIDE_URLS[idx]; applyOrientation(slideA, idx);
|
||||||
slideB.style.transition = 'none';
|
|
||||||
slideA.src = SLIDE_URLS[idx];
|
|
||||||
applyOrientation(slideA, idx);
|
|
||||||
slideA.style.opacity = '1'; slideA.style.zIndex = '2';
|
slideA.style.opacity = '1'; slideA.style.zIndex = '2';
|
||||||
slideB.style.opacity = '0'; slideB.style.zIndex = '1';
|
slideB.style.opacity = '0'; slideB.style.zIndex = '1';
|
||||||
const nextIdx = (idx + 1) % SLIDE_URLS.length;
|
const nextIdx = (idx + 1) % SLIDE_URLS.length;
|
||||||
slideB.src = SLIDE_URLS[nextIdx];
|
slideB.src = SLIDE_URLS[nextIdx]; applyOrientation(slideB, nextIdx);
|
||||||
applyOrientation(slideB, nextIdx);
|
|
||||||
aIsTop = true;
|
aIsTop = true;
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => { slideA.style.transition = ''; slideB.style.transition = ''; });
|
||||||
slideA.style.transition = '';
|
|
||||||
slideB.style.transition = '';
|
|
||||||
});
|
|
||||||
if (!audio.paused) startSlideshow();
|
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() {
|
function initSlideA() {
|
||||||
extractColors(slideA);
|
extractColors(slideA);
|
||||||
const portrait = slideA.naturalHeight > slideA.naturalWidth;
|
const portrait = slideA.naturalHeight > slideA.naturalWidth;
|
||||||
slideA.classList.toggle('portrait', portrait);
|
slideA.classList.toggle('portrait', portrait);
|
||||||
slideOrientations[0] = portrait;
|
slideOrientations[0] = portrait;
|
||||||
}
|
}
|
||||||
if (slideA.complete && slideA.naturalWidth) {
|
if (slideA.complete && slideA.naturalWidth) initSlideA();
|
||||||
initSlideA();
|
else slideA.addEventListener('load', initSlideA, { once: true });
|
||||||
} 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 ─────────────────────────────────────────────────────
|
// ── Init ─────────────────────────────────────────────────────
|
||||||
const savedVol = localStorage.getItem('ytpVolume');
|
const savedVol = localStorage.getItem('ytpVolume');
|
||||||
const savedMuted = localStorage.getItem('ytpMuted');
|
const savedMuted = localStorage.getItem('ytpMuted');
|
||||||
|
|||||||
@ -559,24 +559,8 @@
|
|||||||
var audio = document.getElementById('audioEl');
|
var audio = document.getElementById('audioEl');
|
||||||
if (audio) { audio.src=d.stream_url; audio.load(); }
|
if (audio) { audio.src=d.stream_url; audio.load(); }
|
||||||
|
|
||||||
// update cover / slideshow
|
// update cover / slideshow / title / progress via shared hook
|
||||||
var ci = document.getElementById('audioCoverImg');
|
if (window._audioPlayerUpdate) window._audioPlayerUpdate(d);
|
||||||
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 state
|
// update state
|
||||||
PL_CURRENT = d.id;
|
PL_CURRENT = d.id;
|
||||||
@ -762,17 +746,11 @@
|
|||||||
var d = await resp.json();
|
var d = await resp.json();
|
||||||
|
|
||||||
var audio = document.getElementById('audioEl');
|
var audio = document.getElementById('audioEl');
|
||||||
if (audio) {
|
if (audio) { audio.src = d.stream_url; audio.load(); }
|
||||||
audio.src = d.stream_url;
|
|
||||||
audio.load();
|
|
||||||
audio.play().catch(function(){});
|
|
||||||
}
|
|
||||||
|
|
||||||
var coverImg = document.querySelector('.audio-cover-img');
|
if (window._audioPlayerUpdate) window._audioPlayerUpdate(d);
|
||||||
if (coverImg) coverImg.src = d.cover_url || '';
|
|
||||||
|
|
||||||
var titleEl = document.querySelector('.audio-title');
|
if (audio) audio.play().catch(function(){});
|
||||||
if (titleEl) titleEl.textContent = d.title || '';
|
|
||||||
|
|
||||||
if (pushHist !== false) history.pushState({ url: url }, '', url);
|
if (pushHist !== false) history.pushState({ url: url }, '', url);
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user