Major additions: - Web-based setup wizard (setup.php, setup_wizard.php, setup-wizard.js) - Production Docker configuration (docker-compose.prod.yml, .env.production) - Database initialization SQL files (deploy/init_settings.sql) - Template builder system with drag-and-drop UI - Advanced features (OAuth, CDN, enhanced analytics, monetization) - Comprehensive documentation (deployment guides, quick start, feature docs) - Design system with accessibility and responsive layout - Deployment automation scripts (deploy.ps1, generate-secrets.ps1) Setup wizard allows customization of: - Platform name and branding - Domain configuration - Membership tiers and pricing - Admin credentials - Feature toggles Database includes 270+ tables for complete video streaming platform with advanced features for analytics, moderation, template building, and monetization. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
308 lines
8.4 KiB
JavaScript
308 lines
8.4 KiB
JavaScript
// EasyStream Service Worker - Enhanced PWA v2.0
|
|
const CACHE_VERSION = 'es-v2';
|
|
const STATIC_CACHE = `${CACHE_VERSION}-static`;
|
|
const IMAGE_CACHE = `${CACHE_VERSION}-images`;
|
|
const FONT_CACHE = `${CACHE_VERSION}-fonts`;
|
|
const VIDEO_CACHE = `${CACHE_VERSION}-video`;
|
|
|
|
// Assets to precache
|
|
const PRECACHE = [
|
|
'/index.js',
|
|
'/manifest.json',
|
|
'/f_scripts/shared/design-system.css',
|
|
'/f_scripts/shared/accessibility.css',
|
|
'/f_scripts/shared/responsive.css',
|
|
'/f_scripts/shared/themes.css'
|
|
];
|
|
|
|
// Cache size limits
|
|
const CACHE_LIMITS = {
|
|
images: 50,
|
|
fonts: 20,
|
|
video: 5
|
|
};
|
|
|
|
// Install event - precache critical assets
|
|
self.addEventListener('install', (event) => {
|
|
console.log('[SW] Installing service worker v2.0...');
|
|
event.waitUntil((async () => {
|
|
try {
|
|
const cache = await caches.open(STATIC_CACHE);
|
|
await cache.addAll(PRECACHE);
|
|
console.log('[SW] Precached static assets');
|
|
self.skipWaiting();
|
|
} catch (error) {
|
|
console.error('[SW] Install failed:', error);
|
|
}
|
|
})());
|
|
});
|
|
|
|
// Activate event - clean up old caches
|
|
self.addEventListener('activate', (event) => {
|
|
console.log('[SW] Activating service worker v2.0...');
|
|
event.waitUntil((async () => {
|
|
const keys = await caches.keys();
|
|
await Promise.all(
|
|
keys
|
|
.filter(k => !k.startsWith(CACHE_VERSION))
|
|
.map(k => {
|
|
console.log('[SW] Deleting old cache:', k);
|
|
return caches.delete(k);
|
|
})
|
|
);
|
|
self.clients.claim();
|
|
console.log('[SW] Activated and claimed clients');
|
|
})());
|
|
});
|
|
|
|
// Fetch event - implement caching strategies
|
|
self.addEventListener('fetch', (event) => {
|
|
const { request } = event;
|
|
const url = new URL(request.url);
|
|
|
|
// Skip caching for:
|
|
// - Uploads
|
|
// - HLS video segments
|
|
// - API calls that should always be fresh
|
|
// - POST/PUT/DELETE requests
|
|
if (
|
|
url.pathname.includes('/upload') ||
|
|
url.pathname.includes('/uploader') ||
|
|
url.pathname.includes('.m3u8') ||
|
|
url.pathname.includes('.ts') ||
|
|
url.pathname.includes('/api/') ||
|
|
request.method !== 'GET'
|
|
) {
|
|
return; // Network only
|
|
}
|
|
|
|
// NAVIGATION REQUESTS: Network-first with offline fallback
|
|
if (request.mode === 'navigate') {
|
|
event.respondWith(networkFirstStrategy(request, STATIC_CACHE));
|
|
return;
|
|
}
|
|
|
|
// IMAGES: Cache-first with network fallback
|
|
if (request.destination === 'image') {
|
|
event.respondWith(cacheFirstStrategy(request, IMAGE_CACHE, CACHE_LIMITS.images));
|
|
return;
|
|
}
|
|
|
|
// FONTS: Cache-first (fonts rarely change)
|
|
if (request.destination === 'font' || url.pathname.match(/\.(woff2?|ttf|eot)$/)) {
|
|
event.respondWith(cacheFirstStrategy(request, FONT_CACHE, CACHE_LIMITS.fonts));
|
|
return;
|
|
}
|
|
|
|
// CSS/JS: Stale-while-revalidate
|
|
if (request.destination === 'style' || request.destination === 'script') {
|
|
event.respondWith(staleWhileRevalidate(request, STATIC_CACHE));
|
|
return;
|
|
}
|
|
|
|
// VIDEO THUMBNAILS: Cache-first
|
|
if (url.pathname.match(/thumb|thumbnail|preview/i) && url.pathname.match(/\.(jpg|jpeg|png|webp)$/)) {
|
|
event.respondWith(cacheFirstStrategy(request, IMAGE_CACHE, CACHE_LIMITS.images));
|
|
return;
|
|
}
|
|
|
|
// DEFAULT: Network-first
|
|
event.respondWith(networkFirstStrategy(request, STATIC_CACHE));
|
|
});
|
|
|
|
// STRATEGY: Network-first with cache fallback
|
|
async function networkFirstStrategy(request, cacheName) {
|
|
try {
|
|
const networkResponse = await fetch(request);
|
|
if (networkResponse && networkResponse.ok) {
|
|
const cache = await caches.open(cacheName);
|
|
cache.put(request, networkResponse.clone());
|
|
}
|
|
return networkResponse;
|
|
} catch (error) {
|
|
const cachedResponse = await caches.match(request);
|
|
if (cachedResponse) {
|
|
return cachedResponse;
|
|
}
|
|
// Return offline page for navigation requests
|
|
if (request.mode === 'navigate') {
|
|
return new Response(getOfflineHTML(), {
|
|
headers: { 'Content-Type': 'text/html' }
|
|
});
|
|
}
|
|
return Response.error();
|
|
}
|
|
}
|
|
|
|
// STRATEGY: Cache-first with network fallback
|
|
async function cacheFirstStrategy(request, cacheName, limit) {
|
|
const cachedResponse = await caches.match(request);
|
|
if (cachedResponse) {
|
|
return cachedResponse;
|
|
}
|
|
|
|
try {
|
|
const networkResponse = await fetch(request);
|
|
if (networkResponse && networkResponse.ok) {
|
|
const cache = await caches.open(cacheName);
|
|
cache.put(request, networkResponse.clone());
|
|
// Limit cache size
|
|
if (limit) {
|
|
limitCacheSize(cacheName, limit);
|
|
}
|
|
}
|
|
return networkResponse;
|
|
} catch (error) {
|
|
return Response.error();
|
|
}
|
|
}
|
|
|
|
// STRATEGY: Stale-while-revalidate
|
|
async function staleWhileRevalidate(request, cacheName) {
|
|
const cachedResponse = await caches.match(request);
|
|
|
|
const fetchPromise = fetch(request).then((networkResponse) => {
|
|
if (networkResponse && networkResponse.ok) {
|
|
const cache = caches.open(cacheName);
|
|
cache.then(c => c.put(request, networkResponse.clone()));
|
|
}
|
|
return networkResponse;
|
|
}).catch(() => cachedResponse);
|
|
|
|
return cachedResponse || fetchPromise;
|
|
}
|
|
|
|
// Limit cache size to prevent unlimited growth
|
|
async function limitCacheSize(cacheName, maxItems) {
|
|
const cache = await caches.open(cacheName);
|
|
const keys = await cache.keys();
|
|
if (keys.length > maxItems) {
|
|
// Delete oldest entries (FIFO)
|
|
const deleteCount = keys.length - maxItems;
|
|
for (let i = 0; i < deleteCount; i++) {
|
|
await cache.delete(keys[i]);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Offline page HTML
|
|
function getOfflineHTML() {
|
|
return `
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Offline - EasyStream</title>
|
|
<style>
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
min-height: 100vh;
|
|
margin: 0;
|
|
background: #121212;
|
|
color: #f0f0f0;
|
|
text-align: center;
|
|
padding: 20px;
|
|
}
|
|
.offline-container {
|
|
max-width: 500px;
|
|
}
|
|
h1 {
|
|
font-size: 2rem;
|
|
margin-bottom: 1rem;
|
|
color: #06a2cb;
|
|
}
|
|
p {
|
|
font-size: 1.125rem;
|
|
line-height: 1.6;
|
|
margin-bottom: 2rem;
|
|
color: #d0d0d0;
|
|
}
|
|
.retry-btn {
|
|
display: inline-block;
|
|
padding: 12px 24px;
|
|
background: #06a2cb;
|
|
color: white;
|
|
text-decoration: none;
|
|
border-radius: 8px;
|
|
font-weight: 600;
|
|
transition: background 200ms;
|
|
}
|
|
.retry-btn:hover {
|
|
background: #92cefb;
|
|
}
|
|
.icon {
|
|
font-size: 4rem;
|
|
margin-bottom: 1rem;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="offline-container">
|
|
<div class="icon">📡</div>
|
|
<h1>You're Offline</h1>
|
|
<p>It looks like you've lost your internet connection. Please check your network and try again.</p>
|
|
<a href="javascript:location.reload()" class="retry-btn">Retry</a>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
`;
|
|
}
|
|
|
|
// Handle messages from the main thread
|
|
self.addEventListener('message', (event) => {
|
|
if (event.data && event.data.type === 'SKIP_WAITING') {
|
|
self.skipWaiting();
|
|
}
|
|
if (event.data && event.data.type === 'CLEAR_CACHE') {
|
|
event.waitUntil(
|
|
caches.keys().then(keys => Promise.all(keys.map(key => caches.delete(key))))
|
|
);
|
|
}
|
|
});
|
|
|
|
// Background sync for offline actions (if supported)
|
|
if ('sync' in self.registration) {
|
|
self.addEventListener('sync', (event) => {
|
|
if (event.tag === 'sync-watch-history') {
|
|
event.waitUntil(syncWatchHistory());
|
|
}
|
|
});
|
|
}
|
|
|
|
async function syncWatchHistory() {
|
|
// Placeholder for syncing watch history when back online
|
|
console.log('[SW] Syncing watch history...');
|
|
}
|
|
|
|
// Push notifications support
|
|
self.addEventListener('push', (event) => {
|
|
const options = {
|
|
body: event.data ? event.data.text() : 'New notification from EasyStream',
|
|
icon: '/android-icon-192x192.svg',
|
|
badge: '/android-icon-96x96.svg',
|
|
vibrate: [200, 100, 200],
|
|
data: {
|
|
dateOfArrival: Date.now(),
|
|
primaryKey: 1
|
|
}
|
|
};
|
|
|
|
event.waitUntil(
|
|
self.registration.showNotification('EasyStream', options)
|
|
);
|
|
});
|
|
|
|
// Notification click handler
|
|
self.addEventListener('notificationclick', (event) => {
|
|
event.notification.close();
|
|
event.waitUntil(
|
|
clients.openWindow('/')
|
|
);
|
|
});
|
|
|
|
console.log('[SW] Service Worker v2.0 loaded');
|