From 5d3b91aa41845f58507de77fb24be93fbd26c314 Mon Sep 17 00:00:00 2001 From: Arne Baeumler Date: Sat, 13 Jun 2026 07:29:44 +0200 Subject: [PATCH] feat: add overlay image loading state --- src/server/request-handler.ts | 84 ++++++++++++++++++++++++++++++++++- 1 file changed, 82 insertions(+), 2 deletions(-) diff --git a/src/server/request-handler.ts b/src/server/request-handler.ts index a7da8d1..509be34 100644 --- a/src/server/request-handler.ts +++ b/src/server/request-handler.ts @@ -619,6 +619,12 @@ function htmlPage(): string { color: var(--timeline-text); } + .overlay-media { + position: relative; + min-height: 48vh; + background: var(--timeline-surface-strong); + } + .overlay-card header { padding: 16px 18px; border-bottom: 1px solid var(--timeline-border); @@ -635,6 +641,43 @@ function htmlPage(): string { max-height: 78vh; object-fit: contain; background: var(--timeline-surface-strong); + transition: opacity 120ms ease; + } + + .overlay-card.loading img { + opacity: 0; + } + + .overlay-loading { + position: absolute; + inset: 0; + display: none; + place-items: center; + gap: 10px; + padding: 24px; + text-align: center; + color: var(--timeline-text-muted); + } + + .overlay-loading.active { + display: grid; + } + + .overlay-loading .spinner { + opacity: 1; + transform: scale(1); + animation: spin 0.8s linear infinite; + } + + .overlay-loading.error .spinner { + animation: none; + opacity: 0.45; + } + + .overlay-loading strong { + color: var(--timeline-text); + font-size: 0.92rem; + font-weight: 600; } .close { @@ -1015,7 +1058,13 @@ function htmlPage(): string { Close - +
+
+ + Loading full-size image... +
+ +
@@ -1079,6 +1128,8 @@ function htmlPage(): string { const overlay = mustGet("overlay"); const overlayTitle = mustGet("overlay-title"); const overlayImage = mustGet("overlay-image"); + const overlayLoading = mustGet("overlay-loading"); + const overlayLoadingText = mustGet("overlay-loading-text"); const closeOverlay = mustGet("close-overlay"); const photoList = mustGet("photo-list"); const emptyState = mustGet("empty-state"); @@ -1104,6 +1155,7 @@ function htmlPage(): string { const importStatus = mustGet("share-status"); const importButton = mustGet("load-share"); const stopImportButton = mustGet("cancel-share"); + let overlayLoadToken = 0; function mustGet(id) { const element = document.getElementById(id); @@ -1568,11 +1620,34 @@ function htmlPage(): string { } function openOverlay(photo) { + const requestToken = ++overlayLoadToken; overlay.classList.add("open"); overlay.setAttribute("aria-hidden", "false"); overlayTitle.textContent = photo.name; - overlayImage.src = photo.fullUrl; overlayImage.alt = photo.name; + overlay.classList.add("loading"); + overlayLoading.classList.add("active"); + overlayLoading.classList.remove("error"); + overlayLoadingText.textContent = "Loading full-size image..."; + overlayImage.onload = () => { + if (requestToken !== overlayLoadToken) { + return; + } + overlay.classList.remove("loading"); + overlayLoading.classList.remove("active"); + overlayLoading.classList.remove("error"); + }; + overlayImage.onerror = () => { + if (requestToken !== overlayLoadToken) { + return; + } + overlayLoadingText.textContent = "Could not load full-size image."; + overlay.classList.remove("loading"); + overlayLoading.classList.add("error"); + overlayLoading.classList.add("active"); + }; + overlayImage.removeAttribute("src"); + overlayImage.src = photo.fullUrl; } function createPhotoMarkerIcon(photo) { @@ -1588,8 +1663,13 @@ function htmlPage(): string { } function closeOverlayView() { + overlayLoadToken += 1; overlay.classList.remove("open"); overlay.setAttribute("aria-hidden", "true"); + overlay.classList.remove("loading"); + overlayLoading.classList.remove("active"); + overlayLoading.classList.remove("error"); + overlayLoadingText.textContent = "Loading full-size image..."; overlayImage.removeAttribute("src"); }