feat: skip presigned URL refresh when paused + SPA shop settings
watch.js: presigned URL refresh now skips src swap when media is paused. Tracks lastUrlRefreshTs so that when the user resumes playback after a long pause, the play listener detects the stale URL and refreshes before continuing. No more re-buffering or disruption for idle media. shop.py: detect X-Requested-With header on settings POST and return JSON with flash messages instead of redirect, enabling SPA behavior. shop-settings.js: new progressive enhancement script intercepts settings form submits via fetch, renders flash messages inline into #alerts div. Falls back to normal POST + redirect when JS is disabled or on error.
This commit is contained in:
parent
b3aa6078b2
commit
6a175dad29
4 changed files with 106 additions and 1 deletions
78
make_post_sell/static/js/shop-settings.js
Normal file
78
make_post_sell/static/js/shop-settings.js
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
// AJAX shop settings — progressive enhancement.
|
||||
// When JS is available, intercepts settings form POSTs and submits
|
||||
// via fetch so the page does not reload. Flash messages are rendered
|
||||
// inline into the #alerts div. When JS is disabled, forms fall back
|
||||
// to the normal POST + redirect flow.
|
||||
(function() {
|
||||
var forms = document.querySelectorAll('form[action*="/settings"]');
|
||||
if (!forms.length) return;
|
||||
|
||||
forms.forEach(function(form) {
|
||||
// Skip file upload forms — they POST directly to S3, not /settings
|
||||
if (form.querySelector('input[type="file"]')) return;
|
||||
|
||||
form.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
var formData = new FormData(form);
|
||||
var submitBtn = form.querySelector('input[type="submit"], button[type="submit"]');
|
||||
if (!submitBtn) return;
|
||||
var originalLabel = submitBtn.value;
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.value = 'Saving...';
|
||||
|
||||
fetch(form.action, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
redirect: 'manual',
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||
})
|
||||
.then(function(response) {
|
||||
if (!response.ok) {
|
||||
// Auth redirect or server error — fall back to regular form submit
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.value = originalLabel;
|
||||
form.submit();
|
||||
return;
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(function(data) {
|
||||
if (!data) return;
|
||||
showFlashMessages(data.messages);
|
||||
})
|
||||
.catch(function() {
|
||||
// Network error — fall back to regular form submit
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.value = originalLabel;
|
||||
form.submit();
|
||||
})
|
||||
.finally(function() {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.value = originalLabel;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function showFlashMessages(messages) {
|
||||
var alerts = document.getElementById('alerts');
|
||||
if (!alerts) return;
|
||||
alerts.innerHTML = '';
|
||||
if (!messages || !messages.length) return;
|
||||
messages.forEach(function(msg) {
|
||||
var text = msg[0], level = msg[1];
|
||||
var div = document.createElement('div');
|
||||
div.className = 'alert alert-' + level;
|
||||
div.setAttribute('onclick', "this.style.display='none'");
|
||||
div.innerHTML = '<p class="alert-message">' + escapeHtml(text) + '</p>'
|
||||
+ '<label class="close" alt="dismiss" title="Mark as read">X</label>';
|
||||
alerts.appendChild(div);
|
||||
});
|
||||
alerts.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
var div = document.createElement('div');
|
||||
div.textContent = str;
|
||||
return div.innerHTML;
|
||||
}
|
||||
})();
|
||||
|
|
@ -25,6 +25,7 @@
|
|||
var currentThumbnailUrl = null;
|
||||
var urlRefreshTimer = null;
|
||||
var URL_REFRESH_MS = 7 * 60 * 1000; // 7 minutes
|
||||
var lastUrlRefreshTs = Date.now();
|
||||
var PROGRESS_KEY = 'mps_watch_progress';
|
||||
var PROGRESS_SAVE_INTERVAL = 7000;
|
||||
var lastProgressSave = 0;
|
||||
|
|
@ -660,6 +661,7 @@
|
|||
currentThumbnailUrl = data.thumbnail_url || null;
|
||||
markWatched(data.product_id);
|
||||
syncRingPosition(data.product_id);
|
||||
lastUrlRefreshTs = Date.now();
|
||||
scheduleUrlRefresh();
|
||||
|
||||
// Reset karaoke state for the new product
|
||||
|
|
@ -1529,6 +1531,10 @@
|
|||
// Skip refresh during crossfade — the new media already has a fresh URL
|
||||
if (djCrossfadeActive) { scheduleUrlRefresh(); return; }
|
||||
|
||||
// Skip refresh while paused — no point re-buffering idle media.
|
||||
// The play listener will refresh stale URLs when the user resumes.
|
||||
if (activeMedia.paused) { scheduleUrlRefresh(); return; }
|
||||
|
||||
fetchWatchData(currentProductId).then(function(data) {
|
||||
if (!data || !data.media_url) { scheduleUrlRefresh(); return; }
|
||||
// Only refresh if still on the same product
|
||||
|
|
@ -1550,6 +1556,7 @@
|
|||
else if (karaokeMode === 2 && karaokeUrls.vocals) freshSrc = karaokeUrls.vocals;
|
||||
else freshSrc = data.media_url;
|
||||
activeMedia.src = freshSrc;
|
||||
lastUrlRefreshTs = Date.now();
|
||||
|
||||
activeMedia.addEventListener('loadeddata', function onLoaded() {
|
||||
activeMedia.removeEventListener('loadeddata', onLoaded);
|
||||
|
|
@ -1651,6 +1658,15 @@
|
|||
}
|
||||
}
|
||||
|
||||
// --- Stale URL check on play: if paused long enough for the presigned URL
|
||||
// to expire, refresh before resuming playback ---
|
||||
function onMediaPlay() {
|
||||
if (Date.now() - lastUrlRefreshTs > URL_REFRESH_MS) {
|
||||
activeMedia.pause();
|
||||
refreshMediaUrl();
|
||||
}
|
||||
}
|
||||
|
||||
// --- Media event setup ---
|
||||
function setupMediaEvents() {
|
||||
if (!activeMedia) return;
|
||||
|
|
@ -1659,16 +1675,19 @@
|
|||
activeMedia.removeEventListener('ended', onMediaEnded);
|
||||
activeMedia.removeEventListener('loadedmetadata', onMediaLoadedMetadata);
|
||||
activeMedia.removeEventListener('timeupdate', onMediaTimeUpdate);
|
||||
activeMedia.removeEventListener('play', onMediaPlay);
|
||||
if (standbyMedia) {
|
||||
standbyMedia.removeEventListener('ended', onMediaEnded);
|
||||
standbyMedia.removeEventListener('loadedmetadata', onMediaLoadedMetadata);
|
||||
standbyMedia.removeEventListener('timeupdate', onMediaTimeUpdate);
|
||||
standbyMedia.removeEventListener('play', onMediaPlay);
|
||||
}
|
||||
|
||||
// Add fresh listeners to active element only
|
||||
activeMedia.addEventListener('ended', onMediaEnded);
|
||||
activeMedia.addEventListener('loadedmetadata', onMediaLoadedMetadata);
|
||||
activeMedia.addEventListener('timeupdate', onMediaTimeUpdate);
|
||||
activeMedia.addEventListener('play', onMediaPlay);
|
||||
}
|
||||
|
||||
// Initial setup
|
||||
|
|
|
|||
|
|
@ -1156,5 +1156,6 @@ function toggleCryptoWallets() {
|
|||
wallets.style.display = checkbox.checked ? 'block' : 'none';
|
||||
}
|
||||
</script>
|
||||
<script src="/static/js/shop-settings.js"></script>
|
||||
|
||||
{%- endblock -%}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ from ..lib.phone_numbers import is_phone_number_valid
|
|||
from ..lib.currency import dollars_to_cents, cents_to_dollars
|
||||
|
||||
from pyramid.httpexceptions import HTTPFound
|
||||
from pyramid.response import Response
|
||||
|
||||
# feel free to come up with a better plan, GPT-4 made this regex.
|
||||
DOMAIN_NAME_REGEX = re.compile(
|
||||
|
|
@ -1021,8 +1022,14 @@ def shop_settings(request):
|
|||
except (ValueError, TypeError):
|
||||
request.session.flash(("Invalid high risk threshold", "error"))
|
||||
|
||||
# If we processed any form submission, redirect to prevent re-submission
|
||||
# If we processed any form submission, respond accordingly
|
||||
if form_section:
|
||||
if request.headers.get("X-Requested-With") == "XMLHttpRequest":
|
||||
messages = list(request.session.pop_flash())
|
||||
return Response(
|
||||
json={"status": "ok", "messages": messages},
|
||||
content_type="application/json",
|
||||
)
|
||||
return HTTPFound(f"/s/{shop.id}/settings")
|
||||
|
||||
# TODO: Dry out this block, it's a copy pasta from views/product.py
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue