This commit is contained in:
2026-03-29 20:52:57 -05:00
parent a97c3a5b57
commit cf155183f2
102 changed files with 55674 additions and 0 deletions

235
animex/sw.js Normal file
View File

@@ -0,0 +1,235 @@
/* sw.js
PWA offline service worker
- precaches core app shell + important pages
- runtime caches images/videos/pdf with limits
- navigation fallback to /offline.html
- supports skipWaiting and downloadOffline messages
*/
const CACHE_VERSION = '2025-11-20-8'; // bump when you change assets
const PRECACHE = `precache-${CACHE_VERSION}`;
const RUNTIME = `runtime-${CACHE_VERSION}`;
// Offline fallback page
const OFFLINE_URL = '/offline.html';
const PRECACHE_URLS = [
'/', // index
'/index.html',
'/Launch.html',
'/about.html',
'/in.html',
'/intro.html',
'/login.html',
'/manga.html',
'/library.html',
'/offline.html',
'/portal.html',
'/reader.html',
'/video_player.html',
'/pdf.html',
'/view.html',
'/search.html',
'/lists.html',
'/settings.html',
'/Resources/manifest.json',
'/Resources/styles.css',
'/Resources/manga.css',
'/Resources/series.css',
'/Resources/favicon.png',
'/Resources/Images/Launch.png',
'/Resources/Images/Launch_screen.png',
'/Resources/Images/logo-256.png',
'/Resources/Images/logo-512.png',
'/sw.js'
];
// Runtime cache limits
const MAX_IMAGE_ITEMS = 80;
const MAX_VIDEO_ITEMS = 15;
const MAX_PDF_ITEMS = 20;
/* Utility: trim cache to max items (LRU-ish by deleting oldest) */
async function trimCache(cacheName, maxItems) {
const cache = await caches.open(cacheName);
const keys = await cache.keys();
if (keys.length <= maxItems) return;
const removeCount = keys.length - maxItems;
for (let i = 0; i < removeCount; i++) {
await cache.delete(keys[i]);
}
}
/* Install: precache app shell */
self.addEventListener('install', event => {
self.skipWaiting(); // activate worker immediately (be careful with breaking changes)
event.waitUntil(
caches.open(PRECACHE)
.then(cache => cache.addAll(PRECACHE_URLS))
.catch(err => {
console.error('Precache failed:', err);
})
);
});
/* Activate: clean old caches */
self.addEventListener('activate', event => {
event.waitUntil((async () => {
const names = await caches.keys();
await Promise.all(
names.filter(name => name !== PRECACHE && name !== RUNTIME)
.map(name => caches.delete(name))
);
// Immediately take control of the pages
await self.clients.claim();
})());
});
/* Fetch handler */
self.addEventListener('fetch', event => {
const req = event.request;
const url = new URL(req.url);
// Only handle same-origin requests (adjust if assets are on a CDN)
const sameOrigin = url.origin === self.location.origin;
// 1) Navigation requests -> network-first, fallback to cache -> offline page
if (req.mode === 'navigate') {
event.respondWith(networkFirstFallbackToCache(req));
return;
}
// 2) API / JSON/XHR requests -> network-first (don't cache large dynamic responses)
if (sameOrigin && url.pathname.startsWith('/api')) {
event.respondWith(networkFirst(req));
return;
}
// 3) Images -> cache-first with size limit
if (req.destination === 'image' || /\.(?:png|jpg|jpeg|gif|webp|svg)$/i.test(url.pathname)) {
event.respondWith(cacheFirstWithRuntime(req, 'images-cache', MAX_IMAGE_ITEMS));
return;
}
// 4) Video files -> cache-first but avoid precaching; runtime cache with small limit
if (/\.(?:mp4|webm|m4v|mov)$/i.test(url.pathname)) {
event.respondWith(cacheFirstWithRuntime(req, 'videos-cache', MAX_VIDEO_ITEMS));
return;
}
// 5) PDFs and other documents -> cache-first with limit
if (/\.(?:pdf|epub|mobi)$/i.test(url.pathname) || req.destination === 'document') {
event.respondWith(cacheFirstWithRuntime(req, 'docs-cache', MAX_PDF_ITEMS));
return;
}
// 6) CSS/JS/font -> stale-while-revalidate strategy
if (/\.(?:js|css|woff2?|ttf|otf)$/i.test(url.pathname) || req.destination === 'script' || req.destination === 'style' || req.destination === 'font') {
event.respondWith(staleWhileRevalidate(req));
return;
}
// Default: try cache first then network
event.respondWith(
caches.match(req).then(cached => cached || fetch(req).catch(() => {
// if fetch failed and request is a navigation or HTML, show offline page
if (req.headers.get('accept') && req.headers.get('accept').includes('text/html')) {
return caches.match(OFFLINE_URL);
}
return new Response(null, { status: 503, statusText: 'Service Unavailable' });
}))
);
});
/* Strategies */
async function networkFirstFallbackToCache(request) {
try {
const response = await fetch(request);
// Put navigation responses in runtime cache for offline use
const cache = await caches.open(RUNTIME);
cache.put(request, response.clone()).catch(() => {});
return response;
} catch (err) {
// network failed -> try cache
const cached = await caches.match(request);
if (cached) return cached;
// finally fallback to offline page
return caches.match(OFFLINE_URL);
}
}
async function networkFirst(request) {
try {
const response = await fetch(request);
// Update runtime cache
const cache = await caches.open(RUNTIME);
cache.put(request, response.clone()).catch(() => {});
return response;
} catch (err) {
const cached = await caches.match(request);
if (cached) return cached;
throw err;
}
}
async function cacheFirstWithRuntime(request, cacheName, maxItems = 50) {
const cache = await caches.open(cacheName);
const cached = await cache.match(request);
if (cached) {
return cached;
}
try {
const response = await fetch(request);
// Only cache successful responses (200)
if (response && response.status === 200) {
cache.put(request, response.clone()).catch(() => {});
// trim if necessary
trimCache(cacheName, maxItems).catch(() => {});
}
return response;
} catch (err) {
// fallback to precache or offline
const fallback = await caches.match(request);
if (fallback) return fallback;
if (request.headers.get('accept') && request.headers.get('accept').includes('text/html')) {
return caches.match(OFFLINE_URL);
}
return new Response(null, { status: 503, statusText: 'Service Unavailable' });
}
}
async function staleWhileRevalidate(request) {
const cache = await caches.open(RUNTIME);
const cached = await cache.match(request);
const networkFetch = fetch(request).then(response => {
if (response && response.status === 200) {
cache.put(request, response.clone()).catch(() => {});
}
return response;
}).catch(() => null);
return cached || networkFetch;
}
/* Listen for messages from the page to trigger SW actions (skipWaiting, downloadOffline) */
self.addEventListener('message', event => {
const data = event.data;
if (!data) return;
if (data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
if (data.type === 'DOWNLOAD_OFFLINE') {
// Cache urls that are missing from PRECACHE
downloadOffline();
}
});
/* Pre-cache any resources that aren't yet cached */
async function downloadOffline() {
const cache = await caches.open(PRECACHE);
const cachedRequests = await cache.keys();
const cachedUrls = cachedRequests.map(r => new URL(r.url).pathname);
const toCache = PRECACHE_URLS.filter(url => !cachedUrls.includes(url));
return cache.addAll(toCache);
}