From 1b7d752e4e465cbc833f25797ced5f0edf28811e Mon Sep 17 00:00:00 2001 From: Nose Date: Wed, 22 Apr 2026 00:16:20 -0700 Subject: [PATCH] updated files with new features and improvements: caching proper --- app/card-images.js | 38 +++++++++++++++++--- app/ui-tarot-lightbox.js | 78 ++++++++++++++++++++++++++++++++++------ app/ui-tarot.js | 4 +++ 3 files changed, 106 insertions(+), 14 deletions(-) diff --git a/app/card-images.js b/app/card-images.js index 7f1bf7e..b27d3bc 100644 --- a/app/card-images.js +++ b/app/card-images.js @@ -140,6 +140,7 @@ const cardBackCache = new Map(); const cardBackThumbnailCache = new Map(); const imagePreloadCache = new Map(); + const loadedImageCache = new Map(); const deckImagePreloadCache = new Map(); const deckPreloadStatus = { activeDeckId: DEFAULT_DECK_ID, @@ -574,6 +575,7 @@ cardBackCache.clear(); cardBackThumbnailCache.clear(); imagePreloadCache.clear(); + loadedImageCache.clear(); deckImagePreloadCache.clear(); backgroundDeckWarmupPromise = null; setDeckPreloadStatus({ @@ -948,7 +950,11 @@ function preloadImageUrl(url) { const normalizedUrl = String(url || "").trim(); if (!normalizedUrl) { - return Promise.resolve(false); + return Promise.resolve(null); + } + + if (loadedImageCache.has(normalizedUrl)) { + return Promise.resolve(loadedImageCache.get(normalizedUrl)); } if (imagePreloadCache.has(normalizedUrl)) { @@ -971,12 +977,20 @@ } image.decoding = "async"; - image.onload = () => finalize(true); - image.onerror = () => finalize(false); + image.onload = () => { + loadedImageCache.set(normalizedUrl, image); + finalize(image); + }; + image.onerror = () => finalize(null); image.src = normalizedUrl; if (image.complete) { - finalize(Boolean(image.naturalWidth)); + if (image.naturalWidth) { + loadedImageCache.set(normalizedUrl, image); + finalize(image); + } else { + finalize(null); + } } }); @@ -984,6 +998,20 @@ return preloadPromise; } + function ensureImageLoaded(url) { + return preloadImageUrl(url); + } + + function isImageLoaded(url) { + const normalizedUrl = String(url || "").trim(); + if (!normalizedUrl) { + return false; + } + + const cachedImage = loadedImageCache.get(normalizedUrl); + return Boolean(cachedImage?.complete && cachedImage?.naturalWidth); + } + async function preloadImageUrls(urls, concurrency = 6) { const queue = Array.from(new Set(Array.isArray(urls) ? urls.map((entry) => String(entry || "").trim()).filter(Boolean) : [])); if (queue.length === 0) { @@ -1268,6 +1296,8 @@ resolveTarotCardBackThumbnail, preloadDeckImages, preloadAllDeckImages, + ensureImageLoaded, + isImageLoaded, getDeckPreloadStatus: () => emitDeckPreloadStatus(), getTarotCardDisplayName, getTarotCardSearchAliases, diff --git a/app/ui-tarot-lightbox.js b/app/ui-tarot-lightbox.js index 2934981..32834c7 100644 --- a/app/ui-tarot-lightbox.js +++ b/app/ui-tarot-lightbox.js @@ -54,6 +54,8 @@ let activePinchGesture = null; let suppressNextCardClick = false; let suppressDeckCompareToggleUntil = 0; + let primaryImageRequestToken = 0; + let overlayImageRequestToken = 0; const LIGHTBOX_ZOOM_SCALE = 6.66; const LIGHTBOX_ZOOM_STEP = 0.1; @@ -837,6 +839,7 @@ const label = String(normalized.label || normalized.altText || "Tarot card enlarged image").trim() || "Tarot card enlarged image"; return { src: String(normalized.src || "").trim(), + previewSrc: String(normalized.previewSrc || "").trim(), altText: String(normalized.altText || label).trim() || label, label, cardId: String(normalized.cardId || "").trim(), @@ -847,6 +850,66 @@ }; } + function getImageRequestToken(layer = "primary") { + if (layer === "overlay") { + overlayImageRequestToken += 1; + return overlayImageRequestToken; + } + + primaryImageRequestToken += 1; + return primaryImageRequestToken; + } + + function getCurrentImageRequestToken(layer = "primary") { + return layer === "overlay" ? overlayImageRequestToken : primaryImageRequestToken; + } + + function applyCardImageToElement(targetImageEl, cardRequest, layer = "primary") { + if (!(targetImageEl instanceof HTMLImageElement)) { + return; + } + + const normalizedCard = normalizeCardRequest(cardRequest); + const fullSrc = String(normalizedCard.src || "").trim(); + const previewSrc = String(normalizedCard.previewSrc || "").trim(); + const normalizedPreviewSrc = previewSrc && previewSrc !== fullSrc ? previewSrc : ""; + const imageCache = window.TarotCardImages || {}; + const fullImageLoaded = typeof imageCache.isImageLoaded === "function" + ? imageCache.isImageLoaded(fullSrc) + : false; + const requestToken = getImageRequestToken(layer); + const initialSrc = fullSrc && (!normalizedPreviewSrc || fullImageLoaded) + ? fullSrc + : (normalizedPreviewSrc || fullSrc); + + if (initialSrc) { + targetImageEl.src = initialSrc; + targetImageEl.alt = normalizedCard.altText; + } else { + targetImageEl.removeAttribute("src"); + targetImageEl.alt = normalizedCard.altText; + return; + } + + if (!fullSrc || initialSrc === fullSrc || typeof imageCache.ensureImageLoaded !== "function") { + return; + } + + imageCache.ensureImageLoaded(fullSrc) + .then((loadedImage) => { + if (!loadedImage || getCurrentImageRequestToken(layer) !== requestToken || !lightboxState.isOpen) { + return; + } + + if (targetImageEl.src !== fullSrc) { + targetImageEl.src = fullSrc; + targetImageEl.alt = normalizedCard.altText; + } + }) + .catch(() => { + }); + } + function normalizeDeckOptions(deckOptions) { if (!Array.isArray(deckOptions)) { return []; @@ -3198,8 +3261,7 @@ if (isCompactLightboxLayout()) { lightboxState.mobileInfoView = "overlay"; } - overlayImageEl.src = normalizedCard.src; - overlayImageEl.alt = normalizedCard.altText; + applyCardImageToElement(overlayImageEl, normalizedCard, "overlay"); overlayImageEl.style.display = "block"; overlayImageEl.style.opacity = String(lightboxState.overlayOpacity); if (syncSelection && typeof lightboxState.onSelectCardId === "function") { @@ -3254,8 +3316,7 @@ } lightboxState.primaryCard = nextCard; - imageEl.src = nextCard.src; - imageEl.alt = nextCard.altText; + applyCardImageToElement(imageEl, nextCard, "primary"); resetZoom(); if (lightboxState.deckCompareMode) { syncDeckCompareCards(); @@ -3279,10 +3340,8 @@ lightboxState.primaryCard = nextPrimaryCard; lightboxState.secondaryCard = nextSecondaryCard; - imageEl.src = nextPrimaryCard.src; - imageEl.alt = nextPrimaryCard.altText; - overlayImageEl.src = nextSecondaryCard.src; - overlayImageEl.alt = nextSecondaryCard.altText; + applyCardImageToElement(imageEl, nextPrimaryCard, "primary"); + applyCardImageToElement(overlayImageEl, nextSecondaryCard, "overlay"); overlayImageEl.style.display = "block"; overlayImageEl.style.opacity = String(lightboxState.overlayOpacity); @@ -3723,8 +3782,7 @@ setInfoPanelOpen(getPersistedInfoPanelVisibility(), { persist: false }); lightboxState.mobileInfoView = "primary"; - imageEl.src = normalizedPrimary.src; - imageEl.alt = normalizedPrimary.altText; + applyCardImageToElement(imageEl, normalizedPrimary, "primary"); clearSecondaryCard(); resetZoom(); previousFocusedEl = document.activeElement instanceof HTMLElement ? document.activeElement : null; diff --git a/app/ui-tarot.js b/app/ui-tarot.js index 65ae547..527d8ff 100644 --- a/app/ui-tarot.js +++ b/app/ui-tarot.js @@ -770,6 +770,9 @@ const src = typeof resolveTarotCardImage === "function" ? resolveTarotCardImage(card.name, deckOptions) : ""; + const previewSrc = typeof resolveTarotCardThumbnail === "function" + ? (resolveTarotCardThumbnail(card.name, deckOptions) || src) + : src; const deckMeta = resolvedDeckId ? getRegisteredDeckOptionMap().get(resolvedDeckId) : null; const label = (typeof getTarotCardDisplayName === "function" ? getTarotCardDisplayName(card.name, deckOptions) @@ -777,6 +780,7 @@ return { src, + previewSrc, altText: label, label, cardId: card.id,