mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-25 07:05:43 -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;
|
max-height: 70vh;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
border-radius: var(--border-radius-sm);
|
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 {
|
.showcase__media.blurred {
|
||||||
@@ -384,6 +389,51 @@
|
|||||||
font-size: 2rem;
|
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 */
|
/* Empty state */
|
||||||
.showcase__empty {
|
.showcase__empty {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -36,7 +36,19 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
border: 2px solid transparent;
|
border: 2px solid transparent;
|
||||||
transition: border-color 0.2s ease, transform 0.2s ease;
|
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 {
|
.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);
|
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 */
|
/* NSFW blur for thumbnails - BEM naming to avoid conflicts with global .nsfw-blur */
|
||||||
.thumbnail-rail__item--nsfw-blurred img {
|
.thumbnail-rail__item--nsfw-blurred img {
|
||||||
filter: blur(8px);
|
filter: blur(8px);
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export class Showcase {
|
|||||||
this.isUploading = false;
|
this.isUploading = false;
|
||||||
this.localFiles = [];
|
this.localFiles = [];
|
||||||
this.globalBlurEnabled = true; // Will be initialized based on user settings
|
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';
|
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
|
* Get the HTML template
|
||||||
*/
|
*/
|
||||||
@@ -239,6 +279,7 @@ export class Showcase {
|
|||||||
const needsBlur = nsfwLevel > NSFW_LEVELS.PG13;
|
const needsBlur = nsfwLevel > NSFW_LEVELS.PG13;
|
||||||
const shouldBlur = this.globalBlurEnabled && needsBlur;
|
const shouldBlur = this.globalBlurEnabled && needsBlur;
|
||||||
const isVideo = this.isVideo(img, localFile);
|
const isVideo = this.isVideo(img, localFile);
|
||||||
|
const blurClass = shouldBlur ? 'blurred' : '';
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="thumbnail-rail__item ${index === 0 ? 'active' : ''} ${shouldBlur ? 'thumbnail-rail__item--nsfw-blurred' : ''}"
|
<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>
|
<i class="fas fa-play-circle"></i>
|
||||||
</div>
|
</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>' : ''}
|
${shouldBlur ? '<span class="thumbnail-rail__nsfw-badge">NSFW</span>' : ''}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -539,7 +580,7 @@ export class Showcase {
|
|||||||
/**
|
/**
|
||||||
* Load and display an image by index
|
* Load and display an image by index
|
||||||
*/
|
*/
|
||||||
loadImage(index) {
|
async loadImage(index) {
|
||||||
if (index < 0 || index >= this.images.length) return;
|
if (index < 0 || index >= this.images.length) return;
|
||||||
|
|
||||||
this.currentIndex = index;
|
this.currentIndex = index;
|
||||||
@@ -556,7 +597,32 @@ export class Showcase {
|
|||||||
// Update main media container
|
// Update main media container
|
||||||
const mediaContainer = this.element.querySelector('.showcase__media-container');
|
const mediaContainer = this.element.querySelector('.showcase__media-container');
|
||||||
if (mediaContainer) {
|
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
|
// Update global blur toggle button visibility
|
||||||
|
|||||||
Reference in New Issue
Block a user