mirror of
https://github.com/dergigi/boris.git
synced 2026-01-29 11:44:58 +01:00
feat: add Service Worker for robust offline image caching
- Implement Service Worker to intercept and cache image requests - Service Worker persists across hard reloads unlike Cache API alone - Simplify useImageCache hook to work with Service Worker - Images now work offline even after hard reload - Service Worker handles transparent cache-first serving for images
This commit is contained in:
@@ -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<string | undefined>(imageUrl)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [displayUrl, setDisplayUrl] = useState<string | undefined>(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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
26
src/main.tsx
26
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(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
|
||||
Reference in New Issue
Block a user