fix: 3 watch mode defects — crossfade race, filter-aware next, countdown position
1. Crossfade race condition: when a DJ crossfade is active and the user clicks a new song, cancelDjCrossfade resets djCrossfadeActive but the old song is still near its end — timeupdate immediately re-triggers startDjCrossfade, racing with the in-flight fetch. Added navigationPending flag to block DJ crossfade and ended handler while a user-initiated navigation is in progress. 2. getNextItem/getNextUnwatchedItem now respect media type filters. Previously, filtering to "video only" still showed an image in the countdown because the ring walked by position without checking data-media-type. Now skips filtered items. 3. Countdown overlay moved from position:absolute inside the video/audio container (covering native controls) to a flow-positioned element between the media and title. Removed watch-countdown-static class since all countdown instances now use the same in-flow layout.
This commit is contained in:
parent
a71a3d4c6e
commit
5e0d263b7c
4 changed files with 97 additions and 92 deletions
|
|
@ -3187,26 +3187,22 @@ textarea {
|
|||
|
||||
/* Watch countdown overlay — compact bottom bar with frosted glass */
|
||||
.watch-countdown {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: rgba(255, 255, 255, 0.75);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: 60px 1fr auto auto;
|
||||
gap: 10px;
|
||||
padding: 10px 14px;
|
||||
align-items: center;
|
||||
text-align: left;
|
||||
z-index: 30;
|
||||
border-top: 1px solid var(--border-color, #dee2e6);
|
||||
background: var(--surface-secondary, #f6f8fa);
|
||||
border: 1px solid var(--border-color, #dee2e6);
|
||||
border-radius: var(--radius-md, 8px);
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .watch-countdown {
|
||||
background: rgba(13, 17, 23, 0.80);
|
||||
border-top: 1px solid var(--border-color, #7ab9ff);
|
||||
background: var(--surface-secondary, #161b22);
|
||||
border-color: var(--border-color, #7ab9ff);
|
||||
}
|
||||
|
||||
.countdown-thumb {
|
||||
|
|
@ -3249,15 +3245,6 @@ textarea {
|
|||
grid-column: 4;
|
||||
}
|
||||
|
||||
/* Static content countdown: flows in-place, not overlaid */
|
||||
.watch-countdown-static {
|
||||
position: relative;
|
||||
border-top: none;
|
||||
border: 1px solid var(--border-color, #dee2e6);
|
||||
border-radius: 6px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
/* Queue UI */
|
||||
#watch-queue {
|
||||
margin-bottom: 16px;
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@
|
|||
var PROGRESS_KEY = 'mps_watch_progress';
|
||||
var PROGRESS_SAVE_INTERVAL = 7000;
|
||||
var lastProgressSave = 0;
|
||||
var navigationPending = false;
|
||||
|
||||
// --- Karaoke state ---
|
||||
var karaokeMode = 0; // 0=original, 1=instrumentals, 2=vocals
|
||||
|
|
@ -448,9 +449,12 @@
|
|||
if (djCrossfadeActive) cancelDjCrossfade();
|
||||
cancelCountdown();
|
||||
if (staticTimer) { clearTimeout(staticTimer); staticTimer = null; }
|
||||
navigationPending = true;
|
||||
fetchWatchData(productId).then(function(data) {
|
||||
navigationPending = false;
|
||||
transitionTo(data);
|
||||
}).catch(function() {
|
||||
navigationPending = false;
|
||||
// Fallback: hard navigate to the canonical URL
|
||||
// Try to find the link in the related items
|
||||
var link = document.querySelector('[data-watch-id="' + productId + '"]');
|
||||
|
|
@ -637,10 +641,8 @@
|
|||
|
||||
standbyMedia = createStandbyMedia(newType);
|
||||
|
||||
// Place countdown inside the positioned wrapper (video or audio container)
|
||||
var countdown = document.getElementById('watch-countdown');
|
||||
var positionedWrap = container.querySelector('.watch-video-container, .watch-audio-container');
|
||||
if (countdown && positionedWrap) positionedWrap.appendChild(countdown);
|
||||
// Countdown lives outside the media container (between media and title)
|
||||
// — no need to move it during hard swaps.
|
||||
|
||||
updatePageContent(data);
|
||||
setupMediaEvents();
|
||||
|
|
@ -1046,55 +1048,69 @@
|
|||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// --- Helper: check if a product's media type is filtered out ---
|
||||
function isItemFiltered(productId) {
|
||||
var el = document.querySelector('[data-watch-id="' + productId + '"]');
|
||||
if (!el) return false; // unknown — don't filter
|
||||
var row = el.closest('.related-content-row');
|
||||
if (!row) return false;
|
||||
var type = row.getAttribute('data-media-type');
|
||||
return type ? !mediaFilters[type] : false;
|
||||
}
|
||||
|
||||
function itemFromElement(el, id) {
|
||||
return {
|
||||
id: id || el.getAttribute('data-watch-id'),
|
||||
title: el.querySelector('span') ? el.querySelector('span').textContent : '',
|
||||
thumbnail_url: el.querySelector('img') ? el.querySelector('img').src : null,
|
||||
url: el.getAttribute('href') || ('/c/' + (id || el.getAttribute('data-watch-id')))
|
||||
};
|
||||
}
|
||||
|
||||
// --- Ring-based getNextItem ---
|
||||
function getNextItem() {
|
||||
// Queue items splice into current position — play queue first
|
||||
if (queue.length > 0) return queue[0];
|
||||
|
||||
// Advance ring position by direction, return that item
|
||||
// Advance ring position by direction, skip filtered items
|
||||
if (!ringProductIds.length) {
|
||||
// No ring — fall back to first related item in DOM
|
||||
// No ring — fall back to first visible related item in DOM
|
||||
var allRelated = document.querySelectorAll('.related-content-item[data-watch-id]');
|
||||
if (allRelated.length > 0) {
|
||||
var el = allRelated[0];
|
||||
return {
|
||||
id: el.getAttribute('data-watch-id'),
|
||||
title: el.querySelector('span') ? el.querySelector('span').textContent : '',
|
||||
thumbnail_url: el.querySelector('img') ? el.querySelector('img').src : null,
|
||||
url: el.getAttribute('href')
|
||||
};
|
||||
for (var i = 0; i < allRelated.length; i++) {
|
||||
var el = allRelated[i];
|
||||
var row = el.closest('.related-content-row');
|
||||
var mtype = row ? row.getAttribute('data-media-type') : null;
|
||||
if (mtype && !mediaFilters[mtype]) continue;
|
||||
return itemFromElement(el);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
var nextPos = (ringPosition + ringDirection + ringProductIds.length) % ringProductIds.length;
|
||||
var nextId = ringProductIds[nextPos];
|
||||
|
||||
// Find the related item in the DOM for title/thumbnail
|
||||
var el = document.querySelector('[data-watch-id="' + nextId + '"]');
|
||||
return {
|
||||
id: nextId,
|
||||
title: el ? (el.querySelector('span') ? el.querySelector('span').textContent : '') : '',
|
||||
thumbnail_url: el ? (el.querySelector('img') ? el.querySelector('img').src : null) : null,
|
||||
url: el ? el.getAttribute('href') : '/c/' + nextId
|
||||
};
|
||||
// Walk ring in travel direction, skipping filtered items
|
||||
for (var i = 1; i <= ringProductIds.length; i++) {
|
||||
var nextPos = (ringPosition + i * ringDirection + ringProductIds.length) % ringProductIds.length;
|
||||
var nextId = ringProductIds[nextPos];
|
||||
if (isItemFiltered(nextId)) continue;
|
||||
var el = document.querySelector('[data-watch-id="' + nextId + '"]');
|
||||
return el ? itemFromElement(el, nextId) : {
|
||||
id: nextId, title: '', thumbnail_url: null, url: '/c/' + nextId
|
||||
};
|
||||
}
|
||||
return null; // all items filtered
|
||||
}
|
||||
|
||||
// --- Skip to next unwatched ring item ---
|
||||
function getNextUnwatchedItem() {
|
||||
if (!ringProductIds.length) return getNextItem();
|
||||
|
||||
// Walk forward through the ring, skipping watched items
|
||||
// Walk forward through the ring, skipping watched and filtered items
|
||||
for (var i = 1; i < ringProductIds.length; i++) {
|
||||
var pos = (ringPosition + i * ringDirection + ringProductIds.length * ringProductIds.length) % ringProductIds.length;
|
||||
var id = ringProductIds[pos];
|
||||
if (!isWatched(id)) {
|
||||
if (!isWatched(id) && !isItemFiltered(id)) {
|
||||
var el = document.querySelector('[data-watch-id="' + id + '"]');
|
||||
return {
|
||||
id: id,
|
||||
title: el ? (el.querySelector('span') ? el.querySelector('span').textContent : '') : '',
|
||||
thumbnail_url: el ? (el.querySelector('img') ? el.querySelector('img').src : null) : null,
|
||||
url: el ? el.getAttribute('href') : '/c/' + id
|
||||
return el ? itemFromElement(el, id) : {
|
||||
id: id, title: '', thumbnail_url: null, url: '/c/' + id
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1631,7 +1647,8 @@
|
|||
function onMediaEnded() {
|
||||
clearProgress();
|
||||
// Fallback: only start countdown if DJ crossfade didn't handle it
|
||||
if (!djCrossfadeActive) {
|
||||
// Skip if user navigation is in-flight — the new song will take over
|
||||
if (!djCrossfadeActive && !navigationPending) {
|
||||
// Short clips: instant hard-cut to next if preloaded, no countdown
|
||||
if (activeMedia && activeMedia.duration <= COUNTDOWN_SECONDS
|
||||
&& preloadedData && autoplayEnabled) {
|
||||
|
|
@ -1656,8 +1673,9 @@
|
|||
}
|
||||
// DJ crossfade: start blending 7 seconds before end
|
||||
// Skip for short clips — they play fully then hard-cut on ended
|
||||
// Skip when user navigation is in-flight to prevent re-triggering
|
||||
if (remaining <= COUNTDOWN_SECONDS && remaining > 0 && !djCrossfadeActive
|
||||
&& activeMedia.duration > COUNTDOWN_SECONDS) {
|
||||
&& !navigationPending && activeMedia.duration > COUNTDOWN_SECONDS) {
|
||||
startDjCrossfade();
|
||||
}
|
||||
// Save progress every 7 seconds
|
||||
|
|
|
|||
|
|
@ -44,17 +44,17 @@
|
|||
<div class="watch-video-container">
|
||||
<video id="watch-video" src="{{ watch_video_url }}" autoplay controls playsinline class="product-main"></video>
|
||||
<button class="unmute-overlay">Tap to unmute</button>
|
||||
<div id="watch-countdown" class="watch-countdown js-only" style="display:none">
|
||||
<img id="countdown-thumb" class="countdown-thumb" />
|
||||
<div class="countdown-text">
|
||||
<span id="countdown-title">Up next: ...</span>
|
||||
<span id="countdown-number">7</span>
|
||||
</div>
|
||||
<button id="countdown-play-now" class="mps-button mps-button-blue">Play Now</button>
|
||||
<button id="countdown-cancel" class="mps-button">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="watch-countdown" class="watch-countdown js-only" style="display:none">
|
||||
<img id="countdown-thumb" class="countdown-thumb" />
|
||||
<div class="countdown-text">
|
||||
<span id="countdown-title">Up next: ...</span>
|
||||
<span id="countdown-number">7</span>
|
||||
</div>
|
||||
<button id="countdown-play-now" class="mps-button mps-button-blue">Play Now</button>
|
||||
<button id="countdown-cancel" class="mps-button">Cancel</button>
|
||||
</div>
|
||||
<noscript>
|
||||
<a href="{{ watch_video_url }}" target="_blank">Open video</a>
|
||||
</noscript>
|
||||
|
|
@ -67,17 +67,17 @@
|
|||
<img src="{{ request.shop_cdn_endpoint }}/{{ product.s3_path }}/thumbnail1?ts={{ product.updated_timestamp }}" class="product-main audio-cover" />
|
||||
{% endif %}
|
||||
<audio id="watch-audio" src="{{ watch_audio_url }}" autoplay controls></audio>
|
||||
<div id="watch-countdown" class="watch-countdown js-only" style="display:none">
|
||||
<img id="countdown-thumb" class="countdown-thumb" />
|
||||
<div class="countdown-text">
|
||||
<span id="countdown-title">Up next: ...</span>
|
||||
<span id="countdown-number">7</span>
|
||||
</div>
|
||||
<button id="countdown-play-now" class="mps-button mps-button-blue">Play Now</button>
|
||||
<button id="countdown-cancel" class="mps-button">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="watch-countdown" class="watch-countdown js-only" style="display:none">
|
||||
<img id="countdown-thumb" class="countdown-thumb" />
|
||||
<div class="countdown-text">
|
||||
<span id="countdown-title">Up next: ...</span>
|
||||
<span id="countdown-number">7</span>
|
||||
</div>
|
||||
<button id="countdown-play-now" class="mps-button mps-button-blue">Play Now</button>
|
||||
<button id="countdown-cancel" class="mps-button">Cancel</button>
|
||||
</div>
|
||||
<noscript>
|
||||
<a href="{{ watch_audio_url }}" target="_blank">Listen</a>
|
||||
</noscript>
|
||||
|
|
@ -102,7 +102,7 @@
|
|||
{% endif %}
|
||||
{% if request.shop.watch_mode_enabled and product.extensions.get("product") not in video_extensions and product.extensions.get("product") not in audio_extensions %}
|
||||
{# Countdown overlay for static content (PDFs, images) #}
|
||||
<div id="watch-countdown" class="watch-countdown watch-countdown-static js-only" style="display:none">
|
||||
<div id="watch-countdown" class="watch-countdown js-only" style="display:none">
|
||||
<img id="countdown-thumb" class="countdown-thumb" />
|
||||
<div class="countdown-text">
|
||||
<span id="countdown-title">Up next: ...</span>
|
||||
|
|
|
|||
|
|
@ -48,17 +48,17 @@
|
|||
<div class="watch-video-container">
|
||||
<video id="watch-video" src="{{ watch_video_url }}" autoplay controls playsinline class="product-main"></video>
|
||||
<button class="unmute-overlay">Tap to unmute</button>
|
||||
<div id="watch-countdown" class="watch-countdown js-only" style="display:none">
|
||||
<img id="countdown-thumb" class="countdown-thumb" />
|
||||
<div class="countdown-text">
|
||||
<span id="countdown-title">Up next: ...</span>
|
||||
<span id="countdown-number">7</span>
|
||||
</div>
|
||||
<button id="countdown-play-now" class="mps-button mps-button-blue">Play Now</button>
|
||||
<button id="countdown-cancel" class="mps-button">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="watch-countdown" class="watch-countdown js-only" style="display:none">
|
||||
<img id="countdown-thumb" class="countdown-thumb" />
|
||||
<div class="countdown-text">
|
||||
<span id="countdown-title">Up next: ...</span>
|
||||
<span id="countdown-number">7</span>
|
||||
</div>
|
||||
<button id="countdown-play-now" class="mps-button mps-button-blue">Play Now</button>
|
||||
<button id="countdown-cancel" class="mps-button">Cancel</button>
|
||||
</div>
|
||||
<noscript>
|
||||
<a href="{{ watch_video_url }}" target="_blank">Open video</a>
|
||||
</noscript>
|
||||
|
|
@ -75,17 +75,17 @@
|
|||
<img src="{{ request.shop_cdn_endpoint }}/{{ product.s3_path }}/thumbnail1?ts={{ product.updated_timestamp }}" class="product-main audio-cover" />
|
||||
{% endif %}
|
||||
<audio id="watch-audio" src="{{ watch_audio_url }}" autoplay controls></audio>
|
||||
<div id="watch-countdown" class="watch-countdown js-only" style="display:none">
|
||||
<img id="countdown-thumb" class="countdown-thumb" />
|
||||
<div class="countdown-text">
|
||||
<span id="countdown-title">Up next: ...</span>
|
||||
<span id="countdown-number">7</span>
|
||||
</div>
|
||||
<button id="countdown-play-now" class="mps-button mps-button-blue">Play Now</button>
|
||||
<button id="countdown-cancel" class="mps-button">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="watch-countdown" class="watch-countdown js-only" style="display:none">
|
||||
<img id="countdown-thumb" class="countdown-thumb" />
|
||||
<div class="countdown-text">
|
||||
<span id="countdown-title">Up next: ...</span>
|
||||
<span id="countdown-number">7</span>
|
||||
</div>
|
||||
<button id="countdown-play-now" class="mps-button mps-button-blue">Play Now</button>
|
||||
<button id="countdown-cancel" class="mps-button">Cancel</button>
|
||||
</div>
|
||||
<noscript>
|
||||
<a href="{{ watch_audio_url }}" target="_blank">Listen</a>
|
||||
</noscript>
|
||||
|
|
@ -114,7 +114,7 @@
|
|||
{% endif %}
|
||||
{% if request.shop.watch_mode_enabled and product.extensions.get("product") not in video_extensions and product.extensions.get("product") not in audio_extensions %}
|
||||
{# Countdown overlay for static content (PDFs, images) #}
|
||||
<div id="watch-countdown" class="watch-countdown watch-countdown-static js-only" style="display:none">
|
||||
<div id="watch-countdown" class="watch-countdown js-only" style="display:none">
|
||||
<img id="countdown-thumb" class="countdown-thumb" />
|
||||
<div class="countdown-text">
|
||||
<span id="countdown-title">Up next: ...</span>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue