feat: autoplay visible videos in Dashboard via IntersectionObserver
Replaces hover-to-play with viewport-visibility autoplay for a more Instagram-like browsing experience. Videos auto-play when their card enters the viewport (25% threshold) and pause+reset when scrolled off. - New videoAutoplayObserver in logic_virtual.js with threshold: 0.25 - Each rendered video is registered with the observer - Pool/removal flow unobserves and pauses the video to prevent leaks - createCard() in logic_ui.js drops the hover-play listeners; keeps click-to-fullscreen and loadedmetadata aspect ratio handlers - Observer also lazy-loads src on first intersection as a safety net Uses native IntersectionObserver so it respects zoom, pan, and any viewport transform automatically — no manual bounds math needed. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
b641beb94f
commit
dbd9833fee
@@ -558,7 +558,7 @@ function initFilters() {
|
||||
if (!activeData || activeData.length === 0) return;
|
||||
|
||||
// Ensure all filter Sets exist (add new ones if missing)
|
||||
const filterKeys = ['model', 'sampler', 'scheduler', 'denoise', 'lora', 'positive', 'negative', 'size', 'seed', 'steps', 'cfg', 'upscaleMethod'];
|
||||
const filterKeys = ['model', 'sampler', 'scheduler', 'denoise', 'lora', 'positive', 'negative', 'size', 'seed', 'steps', 'cfg', 'upscaleMethod', 'mediaType'];
|
||||
filterKeys.forEach(key => {
|
||||
if (!filters.hasOwnProperty(key) || !(filters[key] instanceof Set)) {
|
||||
filters[key] = new Set();
|
||||
@@ -586,6 +586,7 @@ function initFilters() {
|
||||
const shortModel = model ? String(model).replace(/\\/g, '/').split('/').pop().replace(/\.[^.]+$/, '') : '';
|
||||
return shortModel ? `${mode} + ${shortModel}` : mode || 'Upscaled';
|
||||
}
|
||||
if (key === 'mediaType') return d.media_type || 'image';
|
||||
return d[key];
|
||||
}))].sort();
|
||||
|
||||
@@ -1284,14 +1285,11 @@ function createCard(d) {
|
||||
<div class="stat"><b>Size:</b> ${d.width}x${d.height} <b>Seed:</b> ${d.seed}</div>
|
||||
</div>`;
|
||||
|
||||
// For videos: hover-play, click for fullscreen, update aspect ratio once metadata loads
|
||||
// For videos: autoplay when visible is handled by videoAutoplayObserver in logic_virtual.js.
|
||||
// This block wires click-to-fullscreen and loadedmetadata aspect ratio update.
|
||||
if (isVideo) {
|
||||
const videoEl = card.querySelector('video');
|
||||
if (videoEl) {
|
||||
card.addEventListener('mouseenter', function() { videoEl.play().catch(function() {}); });
|
||||
card.addEventListener('mouseleave', function() {
|
||||
if (!document.fullscreenElement) { videoEl.pause(); videoEl.currentTime = 0; }
|
||||
});
|
||||
// Click: open native fullscreen with browser controls
|
||||
videoEl.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
@@ -1301,7 +1299,7 @@ function createCard(d) {
|
||||
});
|
||||
// Remove controls when exiting fullscreen
|
||||
videoEl.addEventListener('fullscreenchange', function() {
|
||||
if (!document.fullscreenElement) { videoEl.controls = false; videoEl.pause(); }
|
||||
if (!document.fullscreenElement) { videoEl.controls = false; }
|
||||
});
|
||||
videoEl.addEventListener('loadedmetadata', function() {
|
||||
if (videoEl.videoWidth && videoEl.videoHeight) {
|
||||
|
||||
@@ -145,6 +145,25 @@ const imageObserver = new IntersectionObserver((entries) => {
|
||||
});
|
||||
}, { rootMargin: '400px' });
|
||||
|
||||
// --- VIDEO AUTOPLAY OBSERVER ---
|
||||
// Autoplay videos whose cards are visible in the viewport.
|
||||
// Pauses (and resets) videos that scroll off screen to save bandwidth/CPU.
|
||||
const videoAutoplayObserver = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
const vid = entry.target;
|
||||
if (entry.isIntersecting) {
|
||||
// Ensure src is loaded (lazy loader may not have fired yet)
|
||||
if (vid.dataset.src && !vid.src) {
|
||||
vid.src = vid.dataset.src;
|
||||
}
|
||||
vid.play().catch(() => { /* browser may block until user interaction */ });
|
||||
} else {
|
||||
vid.pause();
|
||||
vid.currentTime = 0;
|
||||
}
|
||||
});
|
||||
}, { rootMargin: '0px', threshold: 0.25 });
|
||||
|
||||
// --- CALCULATE VISIBLE RANGE ---
|
||||
function calculateVisibleRange() {
|
||||
const viewport = document.getElementById('viewport');
|
||||
@@ -246,6 +265,12 @@ function renderVisibleItems(forcePositionUpdate = false) {
|
||||
toRemove.forEach(id => {
|
||||
const node = nodeMap.get(id);
|
||||
if (node) {
|
||||
// Stop observing any video in this card and pause playback
|
||||
const vid = node.querySelector('video');
|
||||
if (vid) {
|
||||
try { videoAutoplayObserver.unobserve(vid); } catch (e) {}
|
||||
vid.pause();
|
||||
}
|
||||
if (_cardPool.length < MAX_POOL_SIZE) {
|
||||
// Pool the card for reuse instead of destroying
|
||||
node.style.display = 'none';
|
||||
@@ -309,10 +334,11 @@ function renderVisibleItems(forcePositionUpdate = false) {
|
||||
img.src = img.dataset.src;
|
||||
img.onload = () => img.style.opacity = '1';
|
||||
}
|
||||
// Lazy-load videos too (same data-src → src pattern)
|
||||
const vid = card.querySelector('video[data-src]');
|
||||
if (vid && !vid.src) {
|
||||
vid.src = vid.dataset.src;
|
||||
// Videos: observe for autoplay-when-visible. Observer handles
|
||||
// lazy src loading on first intersection too.
|
||||
const vid = card.querySelector('video');
|
||||
if (vid) {
|
||||
videoAutoplayObserver.observe(vid);
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user