mirror of
https://github.com/dergigi/boris.git
synced 2025-12-19 15:44:20 +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:
56
public/sw.js
Normal file
56
public/sw.js
Normal file
@@ -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
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
@@ -1,94 +1,46 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { cacheImage, getCachedImage, loadCachedImage } from '../services/imageCacheService'
|
import { cacheImage, getCachedImage } from '../services/imageCacheService'
|
||||||
import { UserSettings } from '../services/settingsService'
|
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 imageUrl - The URL of the image to cache
|
||||||
* @param settings - User settings to determine if caching is enabled
|
* @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(
|
export function useImageCache(
|
||||||
imageUrl: string | undefined,
|
imageUrl: string | undefined,
|
||||||
settings: UserSettings | undefined
|
settings: UserSettings | undefined
|
||||||
): string | undefined {
|
): string | undefined {
|
||||||
const [cachedUrl, setCachedUrl] = useState<string | undefined>(imageUrl)
|
const [displayUrl, setDisplayUrl] = useState<string | undefined>(imageUrl)
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!imageUrl) {
|
if (!imageUrl) {
|
||||||
setCachedUrl(undefined)
|
setDisplayUrl(undefined)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// If caching is disabled, just use the original URL
|
// Always show the original URL - Service Worker will serve from cache if available
|
||||||
const enableCache = settings?.enableImageCache ?? true // Default to enabled
|
setDisplayUrl(imageUrl)
|
||||||
if (!enableCache) {
|
|
||||||
setCachedUrl(imageUrl)
|
// If caching is disabled, don't pre-cache
|
||||||
|
const enableCache = settings?.enableImageCache ?? true
|
||||||
|
if (!enableCache || !navigator.onLine) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store imageUrl in local variable for closure
|
// Pre-cache the image for offline availability
|
||||||
const urlToCache = imageUrl
|
// Service Worker will handle serving it, but we ensure it's cached
|
||||||
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
|
const maxSize = settings?.imageCacheSizeMB ?? 210
|
||||||
|
cacheImage(imageUrl, maxSize).catch(err => {
|
||||||
cacheImage(urlToCache, maxSize)
|
console.warn('Failed to pre-cache image:', err)
|
||||||
.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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [imageUrl, settings?.enableImageCache, settings?.imageCacheSizeMB])
|
}, [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 App from './App.tsx'
|
||||||
import './index.css'
|
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(
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App />
|
<App />
|
||||||
|
|||||||
Reference in New Issue
Block a user