diff --git a/static/js/components/shared/showcase/ShowcaseView.js b/static/js/components/shared/showcase/ShowcaseView.js index 2b852216..3406cc12 100644 --- a/static/js/components/shared/showcase/ShowcaseView.js +++ b/static/js/components/shared/showcase/ShowcaseView.js @@ -22,7 +22,7 @@ import { } from './MediaUtils.js'; import { generateMetadataPanel } from './MetadataPanel.js'; import { generateImageWrapper, generateVideoWrapper } from './MediaRenderers.js'; -import { getShowcaseUrl, getGalleryThumbnailUrl } from '../../../utils/civitaiUtils.js'; +import { getShowcaseUrl, getDisplayUrl, getGalleryThumbnailUrl } from '../../../utils/civitaiUtils.js'; import { openMediaViewer } from '../MediaViewer.js'; import { escapeAttribute } from '../utils.js'; @@ -311,8 +311,9 @@ function renderMediaItem(img, index, exampleFiles) { originalRemoteUrl.endsWith('.mp4') || originalRemoteUrl.endsWith('.webm'); const mediaType = isVideo ? 'video' : 'image'; - // Optimize CivitAI URLs for showcase display (full quality) - const remoteUrl = getShowcaseUrl(originalRemoteUrl, mediaType); + // Optimize CivitAI URLs for in-modal display (images capped at width=2400; + // the full-size media viewer uses getShowcaseUrl separately) + const remoteUrl = getDisplayUrl(originalRemoteUrl, mediaType); const localUrl = localFile ? localFile.path : ''; @@ -466,7 +467,9 @@ function prefetchAdjacentMedia() { const isVideo = img.url.endsWith('.mp4') || img.url.endsWith('.webm'); if (isVideo) return; - const url = getShowcaseUrl(img.url, 'image'); + // Must match the main viewer's URL (display mode) or the warmed + // cache entry is never used + const url = getDisplayUrl(img.url, 'image'); if (prefetchedUrls.has(url)) return; prefetchedUrls.add(url); diff --git a/static/js/utils/civitaiUtils.js b/static/js/utils/civitaiUtils.js index 4007aad0..2f7f30fd 100644 --- a/static/js/utils/civitaiUtils.js +++ b/static/js/utils/civitaiUtils.js @@ -9,6 +9,9 @@ export const OptimizationMode = { /** Full quality for showcase/display - uses /optimized=true only */ SHOWCASE: 'showcase', + /** In-modal display - caps image width at 2400 (covers the ~1200 CSS px + * main viewer at DPR 2); videos stay full quality */ + DISPLAY: 'display', /** Thumbnail size for cards - uses /width=450,optimized=true */ THUMBNAIL: 'thumbnail', /** Small thumbnails for the showcase gallery strip (72px display) - uses /width=160,optimized=true */ @@ -97,15 +100,20 @@ export function rewriteCivitaiUrl(sourceUrl, mediaType = null, mode = Optimizati } // Determine replacement based on mode and media type + const isVideo = Boolean(mediaType && mediaType.toLowerCase() === 'video'); let replacement; if (mode === OptimizationMode.SHOWCASE) { // Full quality for showcase - no width restriction replacement = '/optimized=true'; + } else if (mode === OptimizationMode.DISPLAY) { + // Display mode caps image width for in-modal viewing; videos stay + // full quality (CDN transcoding costs more than it saves here) + replacement = isVideo ? '/optimized=true' : '/width=2400,optimized=true'; } else { // Thumbnail modes with width restriction const width = mode === OptimizationMode.GALLERY_THUMBNAIL ? 160 : 450; replacement = `/width=${width},optimized=true`; - if (mediaType && mediaType.toLowerCase() === 'video') { + if (isVideo) { replacement = `/transcode=true,width=${width},optimized=true`; } } @@ -153,6 +161,19 @@ export function getShowcaseUrl(url, type = 'image') { return getOptimizedUrl(url, type, OptimizationMode.SHOWCASE); } +/** + * Get display-optimized URL for the in-modal main viewer (images capped at + * width=2400; videos full quality). Use getShowcaseUrl for full-size viewing + * (e.g. the media viewer overlay) + * + * @param {string} url - Original URL + * @param {string} type - Media type ("image" or "video") + * @returns {string} - Optimized URL for in-modal display + */ +export function getDisplayUrl(url, type = 'image') { + return getOptimizedUrl(url, type, OptimizationMode.DISPLAY); +} + /** * Get thumbnail-optimized URL (width=450) * diff --git a/tests/frontend/utils/civitaiUtils.test.js b/tests/frontend/utils/civitaiUtils.test.js index 12b81c88..04ae081e 100644 --- a/tests/frontend/utils/civitaiUtils.test.js +++ b/tests/frontend/utils/civitaiUtils.test.js @@ -8,6 +8,7 @@ import { rewriteCivitaiUrl, getOptimizedUrl, getShowcaseUrl, + getDisplayUrl, getThumbnailUrl, getGalleryThumbnailUrl, extractCivitaiImageId, @@ -22,6 +23,7 @@ describe('civitaiUtils', () => { describe('OptimizationMode', () => { it('should have correct mode values', () => { expect(OptimizationMode.SHOWCASE).toBe('showcase'); + expect(OptimizationMode.DISPLAY).toBe('display'); expect(OptimizationMode.THUMBNAIL).toBe('thumbnail'); expect(OptimizationMode.GALLERY_THUMBNAIL).toBe('gallery-thumbnail'); }); @@ -125,6 +127,22 @@ describe('civitaiUtils', () => { expect(rewritten).toBe('https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/abc123/transcode=true,width=160,optimized=true/12345.mp4'); }); + it('should rewrite image URLs with /original=true for display mode (width=2400)', () => { + const originalUrl = 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/abc123/original=true/12345.jpeg'; + const [rewritten, wasRewritten] = rewriteCivitaiUrl(originalUrl, 'image', OptimizationMode.DISPLAY); + + expect(wasRewritten).toBe(true); + expect(rewritten).toBe('https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/abc123/width=2400,optimized=true/12345.jpeg'); + }); + + it('should keep videos full quality in display mode (no transcode/width)', () => { + const originalUrl = 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/abc123/original=true/12345.mp4'; + const [rewritten, wasRewritten] = rewriteCivitaiUrl(originalUrl, 'video', OptimizationMode.DISPLAY); + + expect(wasRewritten).toBe(true); + expect(rewritten).toBe('https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/abc123/optimized=true/12345.mp4'); + }); + it('should not rewrite URLs without /original=true', () => { const originalUrl = 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/abc123/width=450/12345.jpeg'; const [rewritten, wasRewritten] = rewriteCivitaiUrl(originalUrl, 'image', OptimizationMode.THUMBNAIL); @@ -250,6 +268,22 @@ describe('civitaiUtils', () => { }); }); + describe('getDisplayUrl', () => { + it('should return display-optimized URL (width=2400) for images', () => { + const originalUrl = 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/abc123/original=true/12345.jpeg'; + const displayUrl = getDisplayUrl(originalUrl, 'image'); + + expect(displayUrl).toBe('https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/abc123/width=2400,optimized=true/12345.jpeg'); + }); + + it('should keep videos full quality', () => { + const originalUrl = 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/abc123/original=true/12345.mp4'; + const displayUrl = getDisplayUrl(originalUrl, 'video'); + + expect(displayUrl).toBe('https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/abc123/optimized=true/12345.mp4'); + }); + }); + describe('getGalleryThumbnailUrl', () => { it('should return gallery-thumbnail-optimized URL (width=160)', () => { const originalUrl = 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/abc123/original=true/12345.jpeg';