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
-
+
@@ -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");
}