diff --git a/static/app.css b/static/app.css index fade374..82b6729 100644 --- a/static/app.css +++ b/static/app.css @@ -623,6 +623,14 @@ footer .audio-player-container { border-radius: 12px; background: linear-gradient(145deg, #ffffff, #f6f8fc); box-shadow: var(--card-shadow); + aspect-ratio: 1 / 1; +} + +.image-item .image-link { + display: block; + width: 100%; + height: 100%; + line-height: 0; } /* the filename overlay, centered at bottom */ @@ -651,11 +659,12 @@ footer .audio-player-container { .thumbnail { width: 100%; height: 100%; + display: block; object-fit: cover; } .thumbnail-fallback { - object-fit: contain; + object-fit: cover; background: #e5e5e5; } diff --git a/static/gallery.css b/static/gallery.css index c757e5e..f907350 100644 --- a/static/gallery.css +++ b/static/gallery.css @@ -22,6 +22,7 @@ position: fixed; z-index: 3000; inset: 0; + --gallery-edge-gap: 10px; background-color: rgba(0, 0, 0, 0.82); background-image: radial-gradient(circle at 50% 15%, rgba(42, 42, 42, 0.52), rgba(8, 8, 8, 0.95) 56%, #000 100%); @@ -33,7 +34,7 @@ #gallery-stage { position: absolute; - inset: 82px 22px 96px; + inset: var(--gallery-edge-gap); cursor: pointer; } @@ -42,8 +43,8 @@ top: 50%; left: 50%; transform: translate(-50%, -50%); - max-width: min(96vw, calc(100vw - 80px)); - max-height: calc(100vh - 190px); + max-width: calc(100vw - (var(--gallery-edge-gap) * 2)); + max-height: calc(100vh - (var(--gallery-edge-gap) * 2)); object-fit: contain; margin: auto; display: block; @@ -258,7 +259,7 @@ #gallery-filename, #gallery-position { color: #fff; - background: rgba(0, 0, 0, 0.6); + background: rgba(0, 0, 0, 0.42); border: 1px solid rgba(255, 255, 255, 0.16); border-radius: 8px; padding: 0.45rem 0.72rem; @@ -285,10 +286,10 @@ background: linear-gradient( 90deg, - rgba(255, 255, 255, 0.95) 0%, - rgba(255, 255, 255, 0.95) var(--filename-progress), - rgba(0, 0, 0, 0.62) var(--filename-progress), - rgba(0, 0, 0, 0.62) 100% + rgba(255, 255, 255, 0.56) 0%, + rgba(255, 255, 255, 0.56) var(--filename-progress), + rgba(0, 0, 0, 0.44) var(--filename-progress), + rgba(0, 0, 0, 0.44) 100% ); z-index: 0; } @@ -310,7 +311,7 @@ left: 0; width: 100%; height: 100%; - padding: 0.45rem 0.72rem; + padding: inherit; box-sizing: border-box; z-index: 2; overflow: hidden; @@ -365,13 +366,17 @@ } @media (max-width: 860px) { + #gallery-modal { + --gallery-edge-gap: 8px; + } + #gallery-stage { - inset: 74px 10px 110px; + inset: var(--gallery-edge-gap); } #gallery-modal-content { - max-width: calc(100vw - 20px); - max-height: calc(100vh - 210px); + max-width: calc(100vw - (var(--gallery-edge-gap) * 2)); + max-height: calc(100vh - (var(--gallery-edge-gap) * 2)); border-radius: 10px; } @@ -453,13 +458,17 @@ } @media (max-height: 520px) and (orientation: landscape) { + #gallery-modal { + --gallery-edge-gap: 6px; + } + #gallery-stage { - inset: 44px 10px 60px; + inset: var(--gallery-edge-gap); } #gallery-modal-content { - max-width: calc(100vw - 20px); - max-height: calc(100vh - 112px); + max-width: calc(100vw - (var(--gallery-edge-gap) * 2)); + max-height: calc(100vh - (var(--gallery-edge-gap) * 2)); border-radius: 8px; } diff --git a/static/gallery.js b/static/gallery.js index 656c830..e34138f 100644 --- a/static/gallery.js +++ b/static/gallery.js @@ -29,6 +29,7 @@ if (typeof currentGalleryIndex === "undefined") { } let currentExifRequestId = 0; +const previewRefreshInFlight = new Set(); function encodeGalleryPath(relUrl) { if (!relUrl) { @@ -40,6 +41,73 @@ function encodeGalleryPath(relUrl) { .join('/'); } +function findBrowserThumbnailByRelUrl(relUrl) { + if (!relUrl) { + return null; + } + const links = document.querySelectorAll('.image-link[data-url]'); + for (const link of links) { + if ((link.dataset.url || '') === relUrl) { + return link.querySelector('img.thumbnail'); + } + } + return null; +} + +function isFallbackThumbnail(imgEl) { + if (!imgEl) { + return false; + } + const src = imgEl.getAttribute('src') || ''; + return imgEl.classList.contains('thumbnail-fallback') || src.startsWith('data:image/'); +} + +function delay(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +async function refreshBrowserThumbnailIfMissing(relUrl) { + if (!relUrl || previewRefreshInFlight.has(relUrl)) { + return; + } + + const thumbImg = findBrowserThumbnailByRelUrl(relUrl); + if (!thumbImg || !isFallbackThumbnail(thumbImg)) { + return; + } + + const baseThumbUrl = thumbImg.dataset.thumbUrl; + if (!baseThumbUrl) { + return; + } + + previewRefreshInFlight.add(relUrl); + const separator = baseThumbUrl.includes('?') ? '&' : '?'; + + try { + for (let attempt = 0; attempt < 2; attempt++) { + const refreshUrl = `${baseThumbUrl}${separator}_=${Date.now()}-${attempt}`; + const response = await fetch(refreshUrl, { method: 'GET', cache: 'no-store' }).catch(() => null); + if (response && response.ok && response.status !== 204) { + thumbImg.classList.remove('thumbnail-fallback'); + if (typeof handleThumbnailLoad === 'function') { + thumbImg.onload = () => handleThumbnailLoad(thumbImg); + } + if (typeof handleThumbnailError === 'function') { + thumbImg.onerror = () => handleThumbnailError(thumbImg); + } + thumbImg.src = refreshUrl; + return; + } + if (attempt === 0) { + await delay(250); + } + } + } finally { + previewRefreshInFlight.delete(relUrl); + } +} + function escapeHtml(value) { return String(value === undefined || value === null ? '' : value).replace(/[&<>"']/g, (char) => ({ '&': '&', @@ -276,6 +344,11 @@ function showGalleryNav() { }, 1000); } +function hideGalleryNav() { + clearTimeout(navHideTimer); + document.querySelectorAll('.gallery-nav').forEach(btn => btn.classList.add('nav-hidden')); +} + function clamp(value, min, max) { return Math.min(Math.max(value, min), max); } @@ -375,6 +448,7 @@ function showGalleryImage(relUrl) { clearTimeout(loaderTimeout); loader.style.display = 'none'; updateFilenameDisplay(relUrl); + refreshBrowserThumbnailIfMissing(relUrl); // Select all current images in the gallery stage const existingImages = stage.querySelectorAll('img'); @@ -449,8 +523,12 @@ function showGalleryImage(relUrl) { function preloadNextImage() { if (currentGalleryIndex < currentGalleryImages.length - 1) { + const nextRelUrl = currentGalleryImages[currentGalleryIndex + 1]; const nextImage = new Image(); - nextImage.src = '/media/' + currentGalleryImages[currentGalleryIndex + 1]; + nextImage.onload = function () { + refreshBrowserThumbnailIfMissing(nextRelUrl); + }; + nextImage.src = '/media/' + nextRelUrl; } } @@ -703,7 +781,6 @@ function handlePinchStart(e) { pinchStartMidY = (e.touches[0].pageY + e.touches[1].pageY) / 2; pinchStartTranslateX = currentTranslateX; pinchStartTranslateY = currentTranslateY; - showGalleryNav(); return; } @@ -736,7 +813,11 @@ function handlePinchMove(e) { currentTranslateX = pinchStartTranslateX + (midX - pinchStartMidX); currentTranslateY = pinchStartTranslateY + (midY - pinchStartMidY); applyImageTransform(e.currentTarget); - showGalleryNav(); + if (currentScale > 1.01) { + showGalleryNav(); + } else { + hideGalleryNav(); + } return; } @@ -776,6 +857,9 @@ function handlePinchEnd(e) { currentScale = 1; currentTranslateX = 0; currentTranslateY = 0; + applyImageTransform(image); + hideGalleryNav(); + return; } applyImageTransform(image); showGalleryNav();