mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 21:22:11 -03:00
feat(video): enhance video handling in model cards with lazy loading and autoplay settings, see #446
This commit is contained in:
@@ -435,7 +435,18 @@ export function createModelCard(model, modelType) {
|
|||||||
// Check if autoplayOnHover is enabled for video previews
|
// Check if autoplayOnHover is enabled for video previews
|
||||||
const autoplayOnHover = state.global?.settings?.autoplay_on_hover || false;
|
const autoplayOnHover = state.global?.settings?.autoplay_on_hover || false;
|
||||||
const isVideo = previewUrl.endsWith('.mp4');
|
const isVideo = previewUrl.endsWith('.mp4');
|
||||||
const videoAttrs = autoplayOnHover ? 'controls muted loop' : 'controls autoplay muted loop';
|
const videoAttrs = [
|
||||||
|
'controls',
|
||||||
|
'muted',
|
||||||
|
'loop',
|
||||||
|
'playsinline',
|
||||||
|
'preload="none"',
|
||||||
|
`data-src="${versionedPreviewUrl}"`
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!autoplayOnHover) {
|
||||||
|
videoAttrs.push('data-autoplay="true"');
|
||||||
|
}
|
||||||
|
|
||||||
// Get favorite status from model data
|
// Get favorite status from model data
|
||||||
const isFavorite = model.favorite === true;
|
const isFavorite = model.favorite === true;
|
||||||
@@ -473,9 +484,7 @@ export function createModelCard(model, modelType) {
|
|||||||
card.innerHTML = `
|
card.innerHTML = `
|
||||||
<div class="card-preview ${shouldBlur ? 'blurred' : ''}">
|
<div class="card-preview ${shouldBlur ? 'blurred' : ''}">
|
||||||
${isVideo ?
|
${isVideo ?
|
||||||
`<video ${videoAttrs} style="pointer-events: none;">
|
`<video ${videoAttrs.join(' ')} style="pointer-events: none;"></video>` :
|
||||||
<source src="${versionedPreviewUrl}" type="video/mp4">
|
|
||||||
</video>` :
|
|
||||||
`<img src="${versionedPreviewUrl}" alt="${model.model_name}">`
|
`<img src="${versionedPreviewUrl}" alt="${model.model_name}">`
|
||||||
}
|
}
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
@@ -514,21 +523,155 @@ export function createModelCard(model, modelType) {
|
|||||||
|
|
||||||
// Add video auto-play on hover functionality if needed
|
// Add video auto-play on hover functionality if needed
|
||||||
const videoElement = card.querySelector('video');
|
const videoElement = card.querySelector('video');
|
||||||
if (videoElement && autoplayOnHover) {
|
if (videoElement) {
|
||||||
const cardPreview = card.querySelector('.card-preview');
|
configureModelCardVideo(videoElement, autoplayOnHover);
|
||||||
|
|
||||||
// Remove autoplay attribute and pause initially
|
|
||||||
videoElement.removeAttribute('autoplay');
|
|
||||||
videoElement.pause();
|
|
||||||
|
|
||||||
// Add mouse events to trigger play/pause using event attributes
|
|
||||||
cardPreview.setAttribute('onmouseenter', 'this.querySelector("video")?.play()');
|
|
||||||
cardPreview.setAttribute('onmouseleave', 'const v=this.querySelector("video"); if(v){v.pause();v.currentTime=0;}');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return card;
|
return card;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const VIDEO_LAZY_ROOT_MARGIN = '200px 0px';
|
||||||
|
let videoLazyObserver = null;
|
||||||
|
|
||||||
|
function ensureVideoLazyObserver() {
|
||||||
|
if (videoLazyObserver) {
|
||||||
|
return videoLazyObserver;
|
||||||
|
}
|
||||||
|
|
||||||
|
videoLazyObserver = new IntersectionObserver((entries, observer) => {
|
||||||
|
entries.forEach(entry => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
const target = entry.target;
|
||||||
|
observer.unobserve(target);
|
||||||
|
loadVideoSource(target);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, {
|
||||||
|
root: null,
|
||||||
|
rootMargin: VIDEO_LAZY_ROOT_MARGIN,
|
||||||
|
threshold: 0.01
|
||||||
|
});
|
||||||
|
|
||||||
|
return videoLazyObserver;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanupHoverHandlers(videoElement) {
|
||||||
|
const handlers = videoElement._hoverHandlers;
|
||||||
|
if (!handlers) return;
|
||||||
|
|
||||||
|
const { cardPreview, mouseEnter, mouseLeave } = handlers;
|
||||||
|
if (cardPreview) {
|
||||||
|
cardPreview.removeEventListener('mouseenter', mouseEnter);
|
||||||
|
cardPreview.removeEventListener('mouseleave', mouseLeave);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete videoElement._hoverHandlers;
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestSafePlay(videoElement) {
|
||||||
|
const playPromise = videoElement.play();
|
||||||
|
if (playPromise && typeof playPromise.catch === 'function') {
|
||||||
|
playPromise.catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadVideoSource(videoElement) {
|
||||||
|
if (!videoElement || videoElement.dataset.loaded === 'true') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceElement = videoElement.querySelector('source');
|
||||||
|
const dataSrc = videoElement.dataset.src || sourceElement?.dataset?.src;
|
||||||
|
|
||||||
|
if (!dataSrc) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure src attributes are reset before applying
|
||||||
|
videoElement.removeAttribute('src');
|
||||||
|
if (sourceElement) {
|
||||||
|
sourceElement.src = dataSrc;
|
||||||
|
} else {
|
||||||
|
videoElement.src = dataSrc;
|
||||||
|
}
|
||||||
|
|
||||||
|
videoElement.load();
|
||||||
|
videoElement.dataset.loaded = 'true';
|
||||||
|
|
||||||
|
if (videoElement.dataset.autoplay === 'true') {
|
||||||
|
videoElement.setAttribute('autoplay', '');
|
||||||
|
requestSafePlay(videoElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function configureModelCardVideo(videoElement, autoplayOnHover) {
|
||||||
|
if (!videoElement) return;
|
||||||
|
|
||||||
|
cleanupHoverHandlers(videoElement);
|
||||||
|
|
||||||
|
const sourceElement = videoElement.querySelector('source');
|
||||||
|
const existingSrc = videoElement.dataset.src || sourceElement?.dataset?.src || videoElement.currentSrc;
|
||||||
|
|
||||||
|
if (existingSrc && !videoElement.dataset.src) {
|
||||||
|
videoElement.dataset.src = existingSrc;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sourceElement && !sourceElement.dataset.src) {
|
||||||
|
sourceElement.dataset.src = videoElement.dataset.src || sourceElement.src;
|
||||||
|
}
|
||||||
|
|
||||||
|
videoElement.removeAttribute('autoplay');
|
||||||
|
videoElement.removeAttribute('src');
|
||||||
|
videoElement.setAttribute('preload', 'none');
|
||||||
|
videoElement.setAttribute('muted', '');
|
||||||
|
videoElement.setAttribute('loop', '');
|
||||||
|
videoElement.setAttribute('playsinline', '');
|
||||||
|
videoElement.setAttribute('controls', '');
|
||||||
|
videoElement.dataset.loaded = 'false';
|
||||||
|
|
||||||
|
if (sourceElement) {
|
||||||
|
sourceElement.removeAttribute('src');
|
||||||
|
if (videoElement.dataset.src) {
|
||||||
|
sourceElement.dataset.src = videoElement.dataset.src;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!autoplayOnHover) {
|
||||||
|
videoElement.dataset.autoplay = 'true';
|
||||||
|
} else {
|
||||||
|
delete videoElement.dataset.autoplay;
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = ensureVideoLazyObserver();
|
||||||
|
observer.observe(videoElement);
|
||||||
|
|
||||||
|
// Pause the video until it is either hovered or autoplay kicks in
|
||||||
|
try {
|
||||||
|
videoElement.pause();
|
||||||
|
} catch (err) {
|
||||||
|
// Ignore pause errors (e.g., if not loaded yet)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (autoplayOnHover) {
|
||||||
|
const cardPreview = videoElement.closest('.card-preview');
|
||||||
|
if (cardPreview) {
|
||||||
|
const mouseEnter = () => {
|
||||||
|
loadVideoSource(videoElement);
|
||||||
|
requestSafePlay(videoElement);
|
||||||
|
};
|
||||||
|
const mouseLeave = () => {
|
||||||
|
videoElement.pause();
|
||||||
|
videoElement.currentTime = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
cardPreview.addEventListener('mouseenter', mouseEnter);
|
||||||
|
cardPreview.addEventListener('mouseleave', mouseLeave);
|
||||||
|
|
||||||
|
videoElement._hoverHandlers = { cardPreview, mouseEnter, mouseLeave };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Add a method to update card appearance based on bulk mode (LoRA only)
|
// Add a method to update card appearance based on bulk mode (LoRA only)
|
||||||
export function updateCardsForBulkMode(isBulkMode) {
|
export function updateCardsForBulkMode(isBulkMode) {
|
||||||
// Update the state
|
// Update the state
|
||||||
@@ -567,4 +710,6 @@ export function updateCardsForBulkMode(isBulkMode) {
|
|||||||
if (isBulkMode) {
|
if (isBulkMode) {
|
||||||
bulkManager.applySelectionState();
|
bulkManager.applySelectionState();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { resetAndReload } from '../api/modelApiFactory.js';
|
|||||||
import { DOWNLOAD_PATH_TEMPLATES, MAPPABLE_BASE_MODELS, PATH_TEMPLATE_PLACEHOLDERS, DEFAULT_PATH_TEMPLATES } from '../utils/constants.js';
|
import { DOWNLOAD_PATH_TEMPLATES, MAPPABLE_BASE_MODELS, PATH_TEMPLATE_PLACEHOLDERS, DEFAULT_PATH_TEMPLATES } from '../utils/constants.js';
|
||||||
import { translate } from '../utils/i18nHelpers.js';
|
import { translate } from '../utils/i18nHelpers.js';
|
||||||
import { i18n } from '../i18n/index.js';
|
import { i18n } from '../i18n/index.js';
|
||||||
|
import { configureModelCardVideo } from '../components/shared/ModelCard.js';
|
||||||
|
|
||||||
export class SettingsManager {
|
export class SettingsManager {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -1222,29 +1223,7 @@ export class SettingsManager {
|
|||||||
// Apply autoplay setting to existing videos in card previews
|
// Apply autoplay setting to existing videos in card previews
|
||||||
const autoplayOnHover = state.global.settings.autoplay_on_hover;
|
const autoplayOnHover = state.global.settings.autoplay_on_hover;
|
||||||
document.querySelectorAll('.card-preview video').forEach(video => {
|
document.querySelectorAll('.card-preview video').forEach(video => {
|
||||||
// Remove previous event listeners by cloning and replacing the element
|
configureModelCardVideo(video, autoplayOnHover);
|
||||||
const videoParent = video.parentElement;
|
|
||||||
const videoClone = video.cloneNode(true);
|
|
||||||
|
|
||||||
if (autoplayOnHover) {
|
|
||||||
// Pause video initially and set up mouse events for hover playback
|
|
||||||
videoClone.removeAttribute('autoplay');
|
|
||||||
videoClone.pause();
|
|
||||||
|
|
||||||
// Add mouse events to the parent element
|
|
||||||
videoParent.onmouseenter = () => videoClone.play();
|
|
||||||
videoParent.onmouseleave = () => {
|
|
||||||
videoClone.pause();
|
|
||||||
videoClone.currentTime = 0;
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
// Use default autoplay behavior
|
|
||||||
videoClone.setAttribute('autoplay', '');
|
|
||||||
videoParent.onmouseenter = null;
|
|
||||||
videoParent.onmouseleave = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
videoParent.replaceChild(videoClone, video);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Apply display density class to grid
|
// Apply display density class to grid
|
||||||
|
|||||||
Reference in New Issue
Block a user