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
|
||||
const autoplayOnHover = state.global?.settings?.autoplay_on_hover || false;
|
||||
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
|
||||
const isFavorite = model.favorite === true;
|
||||
@@ -473,9 +484,7 @@ export function createModelCard(model, modelType) {
|
||||
card.innerHTML = `
|
||||
<div class="card-preview ${shouldBlur ? 'blurred' : ''}">
|
||||
${isVideo ?
|
||||
`<video ${videoAttrs} style="pointer-events: none;">
|
||||
<source src="${versionedPreviewUrl}" type="video/mp4">
|
||||
</video>` :
|
||||
`<video ${videoAttrs.join(' ')} style="pointer-events: none;"></video>` :
|
||||
`<img src="${versionedPreviewUrl}" alt="${model.model_name}">`
|
||||
}
|
||||
<div class="card-header">
|
||||
@@ -514,21 +523,155 @@ export function createModelCard(model, modelType) {
|
||||
|
||||
// Add video auto-play on hover functionality if needed
|
||||
const videoElement = card.querySelector('video');
|
||||
if (videoElement && autoplayOnHover) {
|
||||
const cardPreview = card.querySelector('.card-preview');
|
||||
|
||||
// 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;}');
|
||||
if (videoElement) {
|
||||
configureModelCardVideo(videoElement, autoplayOnHover);
|
||||
}
|
||||
|
||||
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)
|
||||
export function updateCardsForBulkMode(isBulkMode) {
|
||||
// Update the state
|
||||
@@ -567,4 +710,6 @@ export function updateCardsForBulkMode(isBulkMode) {
|
||||
if (isBulkMode) {
|
||||
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 { translate } from '../utils/i18nHelpers.js';
|
||||
import { i18n } from '../i18n/index.js';
|
||||
import { configureModelCardVideo } from '../components/shared/ModelCard.js';
|
||||
|
||||
export class SettingsManager {
|
||||
constructor() {
|
||||
@@ -1222,29 +1223,7 @@ export class SettingsManager {
|
||||
// Apply autoplay setting to existing videos in card previews
|
||||
const autoplayOnHover = state.global.settings.autoplay_on_hover;
|
||||
document.querySelectorAll('.card-preview video').forEach(video => {
|
||||
// Remove previous event listeners by cloning and replacing the element
|
||||
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);
|
||||
configureModelCardVideo(video, autoplayOnHover);
|
||||
});
|
||||
|
||||
// Apply display density class to grid
|
||||
|
||||
Reference in New Issue
Block a user