From 5e0d263b7c89f747ab3cda484984704b26032268 Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Wed, 11 Mar 2026 13:03:18 -0400 Subject: [PATCH] =?UTF-8?q?fix:=203=20watch=20mode=20defects=20=E2=80=94?= =?UTF-8?q?=20crossfade=20race,=20filter-aware=20next,=20countdown=20posit?= =?UTF-8?q?ion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- make_post_sell/static/css/common.css | 27 +++------ make_post_sell/static/js/watch.js | 86 +++++++++++++++++----------- make_post_sell/templates/content.j2 | 38 ++++++------ make_post_sell/templates/product.j2 | 38 ++++++------ 4 files changed, 97 insertions(+), 92 deletions(-) diff --git a/make_post_sell/static/css/common.css b/make_post_sell/static/css/common.css index 4b3a489..4c126f5 100644 --- a/make_post_sell/static/css/common.css +++ b/make_post_sell/static/css/common.css @@ -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; diff --git a/make_post_sell/static/js/watch.js b/make_post_sell/static/js/watch.js index 1e22031..8fd42e8 100644 --- a/make_post_sell/static/js/watch.js +++ b/make_post_sell/static/js/watch.js @@ -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 diff --git a/make_post_sell/templates/content.j2 b/make_post_sell/templates/content.j2 index 79e570a..bd9b432 100644 --- a/make_post_sell/templates/content.j2 +++ b/make_post_sell/templates/content.j2 @@ -44,17 +44,17 @@
-
+ @@ -67,17 +67,17 @@ {% endif %} - + @@ -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) #} - + @@ -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) #} -