thats-me/frontend/_src/composables/useImageCache.js
2026-04-22 12:57:10 +02:00

175 lines
4.6 KiB
JavaScript

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)
}
}