mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 21:22:11 -03:00
feat(showcase): improve remote image loading with skeleton animation and fade-in effects
- Add preloadMedia() for async image/video loading before display - Implement renderLoadingSkeleton() with fa-circle-notch fa-spin animation - Add fadeIn transition (opacity 0→1) for main media elements - Remove shimmer gradient animation from thumbnails for cleaner look - Use solid background color placeholder with subtle fade-in for thumbnails - Fixes progressive rendering of remote images from top to bottom - Prevents black flash during loading with proper loading states
This commit is contained in:
@@ -77,7 +77,12 @@
|
||||
max-height: 70vh;
|
||||
object-fit: contain;
|
||||
border-radius: var(--border-radius-sm);
|
||||
transition: filter 0.2s ease;
|
||||
transition: filter 0.2s ease, opacity 0.3s ease;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.showcase__media.loaded {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.showcase__media.blurred {
|
||||
@@ -384,6 +389,51 @@
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
/* Skeleton loading state */
|
||||
.showcase__skeleton {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--lora-surface);
|
||||
}
|
||||
|
||||
.skeleton-animation {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--space-3);
|
||||
color: var(--text-color);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.skeleton-spinner {
|
||||
font-size: 2.5rem;
|
||||
color: var(--lora-accent);
|
||||
}
|
||||
|
||||
/* Error state */
|
||||
.showcase__error {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: var(--lora-error);
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.showcase__error i {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.showcase__error p {
|
||||
margin: 0;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
/* Empty state */
|
||||
.showcase__empty {
|
||||
display: flex;
|
||||
|
||||
@@ -36,7 +36,19 @@
|
||||
position: relative;
|
||||
border: 2px solid transparent;
|
||||
transition: border-color 0.2s ease, transform 0.2s ease;
|
||||
background: var(--bg-color);
|
||||
background: var(--lora-surface);
|
||||
}
|
||||
|
||||
.thumbnail-rail__item img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.thumbnail-rail__item img.loaded {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.thumbnail-rail__item:hover {
|
||||
@@ -49,12 +61,6 @@
|
||||
box-shadow: 0 0 0 2px oklch(var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h) / 0.3);
|
||||
}
|
||||
|
||||
.thumbnail-rail__item img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
/* NSFW blur for thumbnails - BEM naming to avoid conflicts with global .nsfw-blur */
|
||||
.thumbnail-rail__item--nsfw-blurred img {
|
||||
filter: blur(8px);
|
||||
|
||||
@@ -28,6 +28,7 @@ export class Showcase {
|
||||
this.isUploading = false;
|
||||
this.localFiles = [];
|
||||
this.globalBlurEnabled = true; // Will be initialized based on user settings
|
||||
this.isLoading = false; // Track loading state
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -155,6 +156,45 @@ export class Showcase {
|
||||
return 'Mature Content';
|
||||
}
|
||||
|
||||
/**
|
||||
* Preload media (image or video)
|
||||
* @param {string} url - Media URL
|
||||
* @param {boolean} isVideo - Whether media is video
|
||||
* @returns {Promise} Resolves when media is loaded
|
||||
*/
|
||||
preloadMedia(url, isVideo = false) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (isVideo) {
|
||||
const video = document.createElement('video');
|
||||
video.preload = 'metadata';
|
||||
video.src = url;
|
||||
video.addEventListener('loadeddata', () => resolve(url));
|
||||
video.addEventListener('error', reject);
|
||||
} else {
|
||||
const img = new Image();
|
||||
img.crossOrigin = 'anonymous';
|
||||
img.referrerPolicy = 'no-referrer';
|
||||
img.onload = () => resolve(url);
|
||||
img.onerror = reject;
|
||||
img.src = url;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Render loading skeleton
|
||||
* @returns {string} Skeleton HTML
|
||||
*/
|
||||
renderLoadingSkeleton() {
|
||||
return `
|
||||
<div class="showcase__skeleton">
|
||||
<div class="skeleton-animation">
|
||||
<i class="fas fa-circle-notch fa-spin skeleton-spinner"></i>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the HTML template
|
||||
*/
|
||||
@@ -239,6 +279,7 @@ export class Showcase {
|
||||
const needsBlur = nsfwLevel > NSFW_LEVELS.PG13;
|
||||
const shouldBlur = this.globalBlurEnabled && needsBlur;
|
||||
const isVideo = this.isVideo(img, localFile);
|
||||
const blurClass = shouldBlur ? 'blurred' : '';
|
||||
|
||||
return `
|
||||
<div class="thumbnail-rail__item ${index === 0 ? 'active' : ''} ${shouldBlur ? 'thumbnail-rail__item--nsfw-blurred' : ''}"
|
||||
@@ -250,7 +291,7 @@ export class Showcase {
|
||||
<i class="fas fa-play-circle"></i>
|
||||
</div>
|
||||
` : ''}
|
||||
<img src="${url}" loading="lazy" alt="" ${shouldBlur ? 'class="blurred"' : ''}>
|
||||
<img src="${url}" loading="lazy" alt="" class="${blurClass}" onload="this.classList.add('loaded')">
|
||||
${shouldBlur ? '<span class="thumbnail-rail__nsfw-badge">NSFW</span>' : ''}
|
||||
</div>
|
||||
`;
|
||||
@@ -539,7 +580,7 @@ export class Showcase {
|
||||
/**
|
||||
* Load and display an image by index
|
||||
*/
|
||||
loadImage(index) {
|
||||
async loadImage(index) {
|
||||
if (index < 0 || index >= this.images.length) return;
|
||||
|
||||
this.currentIndex = index;
|
||||
@@ -556,7 +597,32 @@ export class Showcase {
|
||||
// Update main media container
|
||||
const mediaContainer = this.element.querySelector('.showcase__media-container');
|
||||
if (mediaContainer) {
|
||||
mediaContainer.innerHTML = this.renderMediaElement(url, isVideo, shouldBlur, nsfwText, nsfwLevel);
|
||||
// Show skeleton while loading
|
||||
mediaContainer.innerHTML = this.renderLoadingSkeleton();
|
||||
|
||||
try {
|
||||
// Preload media
|
||||
await this.preloadMedia(url, isVideo);
|
||||
|
||||
// Render media with fade-in effect
|
||||
mediaContainer.innerHTML = this.renderMediaElement(url, isVideo, shouldBlur, nsfwText, nsfwLevel);
|
||||
|
||||
// Trigger fade-in animation
|
||||
const media = mediaContainer.querySelector('.showcase__media');
|
||||
if (media) {
|
||||
requestAnimationFrame(() => {
|
||||
media.classList.add('loaded');
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load media:', error);
|
||||
mediaContainer.innerHTML = `
|
||||
<div class="showcase__error">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
<p>${translate('modals.model.examples.loadError', {}, 'Failed to load media')}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// Update global blur toggle button visibility
|
||||
|
||||
Reference in New Issue
Block a user