diff --git a/public/sw.js b/public/sw.js new file mode 100644 index 00000000..0d7a781e --- /dev/null +++ b/public/sw.js @@ -0,0 +1,56 @@ +// Service Worker for Boris - handles offline image caching +const CACHE_NAME = 'boris-image-cache-v1' + +// Install event - activate immediately +self.addEventListener('install', (event) => { + console.log('[SW] Installing service worker...') + self.skipWaiting() +}) + +// Activate event - take control immediately +self.addEventListener('activate', (event) => { + console.log('[SW] Activating service worker...') + event.waitUntil(self.clients.claim()) +}) + +// Fetch event - intercept image requests +self.addEventListener('fetch', (event) => { + const url = new URL(event.request.url) + + // Only intercept image requests + const isImage = event.request.destination === 'image' || + /\.(jpg|jpeg|png|gif|webp|svg)$/i.test(url.pathname) + + if (!isImage) { + return // Let other requests pass through + } + + event.respondWith( + caches.open(CACHE_NAME).then(cache => { + return cache.match(event.request).then(cachedResponse => { + if (cachedResponse) { + console.log('[SW] Serving cached image:', url.pathname) + return cachedResponse + } + + // Not in cache, try to fetch + return fetch(event.request) + .then(response => { + // Only cache successful responses + if (response && response.status === 200) { + // Clone the response before caching + cache.put(event.request, response.clone()) + console.log('[SW] Cached new image:', url.pathname) + } + return response + }) + .catch(error => { + console.error('[SW] Fetch failed for:', url.pathname, error) + // Return a fallback or let it fail + throw error + }) + }) + }) + ) +}) + diff --git a/src/hooks/useImageCache.ts b/src/hooks/useImageCache.ts index 94e546c8..3fe94977 100644 --- a/src/hooks/useImageCache.ts +++ b/src/hooks/useImageCache.ts @@ -1,94 +1,46 @@ import { useState, useEffect } from 'react' -import { cacheImage, getCachedImage, loadCachedImage } from '../services/imageCacheService' +import { cacheImage, getCachedImage } from '../services/imageCacheService' import { UserSettings } from '../services/settingsService' /** - * Hook to cache and retrieve images using Cache API + * Hook to pre-cache images and return the URL for display + * With Service Worker active, images are automatically cached and served offline + * This hook ensures proactive caching for better offline experience * * @param imageUrl - The URL of the image to cache * @param settings - User settings to determine if caching is enabled - * @returns The cached blob URL or the original URL + * @returns The image URL (Service Worker handles caching transparently) */ export function useImageCache( imageUrl: string | undefined, settings: UserSettings | undefined ): string | undefined { - const [cachedUrl, setCachedUrl] = useState(imageUrl) - const [isLoading, setIsLoading] = useState(false) + const [displayUrl, setDisplayUrl] = useState(imageUrl) useEffect(() => { if (!imageUrl) { - setCachedUrl(undefined) + setDisplayUrl(undefined) return } - // If caching is disabled, just use the original URL - const enableCache = settings?.enableImageCache ?? true // Default to enabled - if (!enableCache) { - setCachedUrl(imageUrl) + // Always show the original URL - Service Worker will serve from cache if available + setDisplayUrl(imageUrl) + + // If caching is disabled, don't pre-cache + const enableCache = settings?.enableImageCache ?? true + if (!enableCache || !navigator.onLine) { return } - // Store imageUrl in local variable for closure - const urlToCache = imageUrl - const isOffline = !navigator.onLine - - // When online: show original URL first for immediate display - // When offline: don't show anything until we load from cache - if (!isOffline) { - setCachedUrl(urlToCache) - } - - // Try to load from cache asynchronously - loadCachedImage(urlToCache) - .then(blobUrl => { - if (blobUrl) { - console.log('📦 Using cached image:', urlToCache.substring(0, 50)) - setCachedUrl(blobUrl) - } else if (!isOffline) { - // Not cached and online - cache it now - if (!isLoading) { - setIsLoading(true) - const maxSize = settings?.imageCacheSizeMB ?? 210 - - cacheImage(urlToCache, maxSize) - .then(newBlobUrl => { - // Only update if we got a blob URL back - if (newBlobUrl && newBlobUrl.startsWith('blob:')) { - setCachedUrl(newBlobUrl) - } - }) - .catch(err => { - console.error('Failed to cache image:', err) - // Keep using original URL on error - }) - .finally(() => { - setIsLoading(false) - }) - } - } else { - // Offline and not cached - no image available - console.warn('⚠️ Image not available offline:', urlToCache.substring(0, 50)) - setCachedUrl(undefined) - } - }) - .catch(err => { - console.error('Failed to load cached image:', err) - // If online, fall back to original URL - if (!isOffline) { - setCachedUrl(urlToCache) - } - }) - - // Cleanup: revoke blob URLs when component unmounts or URL changes - return () => { - if (cachedUrl && cachedUrl.startsWith('blob:')) { - URL.revokeObjectURL(cachedUrl) - } - } + // Pre-cache the image for offline availability + // Service Worker will handle serving it, but we ensure it's cached + const maxSize = settings?.imageCacheSizeMB ?? 210 + cacheImage(imageUrl, maxSize).catch(err => { + console.warn('Failed to pre-cache image:', err) + }) }, [imageUrl, settings?.enableImageCache, settings?.imageCacheSizeMB]) - return cachedUrl + return displayUrl } /** diff --git a/src/main.tsx b/src/main.tsx index 9aa0f485..525b7d80 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -3,6 +3,32 @@ import ReactDOM from 'react-dom/client' import App from './App.tsx' import './index.css' +// Register Service Worker for offline image caching +if ('serviceWorker' in navigator) { + window.addEventListener('load', () => { + navigator.serviceWorker + .register('/sw.js') + .then(registration => { + console.log('✅ Service Worker registered:', registration.scope) + + // Update service worker when a new version is available + registration.addEventListener('updatefound', () => { + const newWorker = registration.installing + if (newWorker) { + newWorker.addEventListener('statechange', () => { + if (newWorker.state === 'activated') { + console.log('🔄 Service Worker updated, page may need reload') + } + }) + } + }) + }) + .catch(error => { + console.error('❌ Service Worker registration failed:', error) + }) + }) +} + ReactDOM.createRoot(document.getElementById('root')!).render(