import { ref } from 'vue' import { db } from 'src/db' const THUMB_SIZE = 200 // In-memory URL cache: avoids repeated IndexedDB reads and blob URL creation // Shared across all component instances const memoryCache = new Map() /** * Create a thumbnail (THUMB_SIZE x THUMB_SIZE) from a source image blob. * Returns a new Blob (JPEG, quality 0.8). */ function createThumbnail(blob) { return new Promise((resolve, reject) => { const img = new Image() const url = URL.createObjectURL(blob) img.onload = () => { const canvas = document.createElement('canvas') canvas.width = THUMB_SIZE canvas.height = THUMB_SIZE const ctx = canvas.getContext('2d') // Cover crop: center the image const scale = Math.max(THUMB_SIZE / img.width, THUMB_SIZE / img.height) const w = img.width * scale const h = img.height * scale const x = (THUMB_SIZE - w) / 2 const y = (THUMB_SIZE - h) / 2 ctx.drawImage(img, x, y, w, h) canvas.toBlob( (thumbBlob) => { URL.revokeObjectURL(url) if (thumbBlob) resolve(thumbBlob) else reject(new Error('Canvas toBlob failed')) }, 'image/jpeg', 0.8 ) } img.onerror = () => { URL.revokeObjectURL(url) reject(new Error('Image load failed')) } img.src = url }) } /** * Fetch an image from URL, cache thumbnail in IndexedDB, return blob URL. */ async function fetchAndCache(imageUrl, eventId) { const response = await fetch(imageUrl) if (!response.ok) throw new Error(`Fetch failed: ${response.status}`) const blob = await response.blob() // Create thumbnail const thumbBlob = await createThumbnail(blob) // Store in IndexedDB await db.imageCache.put({ url: imageUrl, eventId, type: 'thumbnail', blob: thumbBlob, cachedAt: Date.now() }) const blobUrl = URL.createObjectURL(thumbBlob) memoryCache.set(imageUrl, blobUrl) return blobUrl } /** * Get a cached thumbnail blob URL from IndexedDB. * Returns null if not cached. */ async function getCachedImage(imageUrl) { // Check memory first if (memoryCache.has(imageUrl)) return memoryCache.get(imageUrl) try { const entry = await db.imageCache.get(imageUrl) if (entry?.blob) { const blobUrl = URL.createObjectURL(entry.blob) memoryCache.set(imageUrl, blobUrl) return blobUrl } } catch (e) { console.warn('Image cache read failed:', e) } return null } /** * Composable: resolves an event's image to a displayable src. * - Checks memory cache → IndexedDB cache → fetches & caches thumbnail. * - Returns reactive `resolvedSrc` ref. */ export function useImageCache(imageUrl, eventId) { const resolvedSrc = ref(null) const loading = ref(false) async function resolve() { if (!imageUrl) { resolvedSrc.value = null return } // 1. Memory cache (instant) if (memoryCache.has(imageUrl)) { resolvedSrc.value = memoryCache.get(imageUrl) return } // 2. IndexedDB cache const cached = await getCachedImage(imageUrl) if (cached) { resolvedSrc.value = cached return } // 3. Fetch, create thumbnail, cache loading.value = true try { const blobUrl = await fetchAndCache(imageUrl, eventId) resolvedSrc.value = blobUrl } catch (e) { // Fallback: use original URL directly (works when online) console.warn('Image cache failed, using direct URL:', e) resolvedSrc.value = imageUrl } finally { loading.value = false } } resolve() return { resolvedSrc, loading } } /** * Resolve full-res image for EventPanel (no thumbnail, just cache check). * Returns the original URL — browser Cache-Control handles caching. * When offline, falls back to cached thumbnail. */ export async function resolveFullRes(imageUrl) { if (!imageUrl) return null // If online, return original URL (browser caches via HTTP headers) if (navigator.onLine) return imageUrl // Offline: try cached thumbnail as fallback const cached = await getCachedImage(imageUrl) return cached || imageUrl } /** * Clear all cached images for a specific event. */ export async function clearEventImages(eventId) { try { const entries = await db.imageCache.where('eventId').equals(eventId).toArray() for (const entry of entries) { if (memoryCache.has(entry.url)) { URL.revokeObjectURL(memoryCache.get(entry.url)) memoryCache.delete(entry.url) } } await db.imageCache.where('eventId').equals(eventId).delete() } catch (e) { console.warn('Clear event images failed:', e) } }