diff --git a/404.php b/404.php index 625ee92..b9928b5 100644 --- a/404.php +++ b/404.php @@ -72,6 +72,7 @@ http_response_code(404); MVLog - 404 + diff --git a/_footer.php b/_footer.php index 8d8ad31..544c4ca 100644 --- a/_footer.php +++ b/_footer.php @@ -5,7 +5,7 @@ if(!btn || !('serviceWorker' in navigator) || !('PushManager' in window) || !('Notification' in window)) return; function b64uToUint8Array(s){ const pad='='.repeat((4-s.length%4)%4); const b64=(s+pad).replace(/-/g,'+').replace(/_/g,'/'); const raw=atob(b64); return Uint8Array.from([...raw].map(c=>c.charCodeAt(0))); } async function state(reg){ const sub=await reg.pushManager.getSubscription(); btn.classList.toggle('active', !!sub); btn.title=sub?'Notifications on':'Notifications'; btn.hidden=false; return sub; } - navigator.serviceWorker.register('sw.js').then(async reg=>{ + navigator.serviceWorker.register('/sw.js').then(async reg=>{ const cfg=await fetch('push_config.php',{cache:'no-store'}).then(r=>r.json()).catch(()=>({enabled:false})); if(!cfg.enabled || !cfg.publicKey) return; await state(reg); diff --git a/_pwa_head.php b/_pwa_head.php new file mode 100644 index 0000000..2869518 --- /dev/null +++ b/_pwa_head.php @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/img/apple-touch-icon.png b/assets/img/apple-touch-icon.png new file mode 100644 index 0000000..7d6dbfc Binary files /dev/null and b/assets/img/apple-touch-icon.png differ diff --git a/assets/img/pwa-icon-192.png b/assets/img/pwa-icon-192.png new file mode 100644 index 0000000..cf5f0b0 Binary files /dev/null and b/assets/img/pwa-icon-192.png differ diff --git a/assets/img/pwa-icon-512.png b/assets/img/pwa-icon-512.png new file mode 100644 index 0000000..e6e556d Binary files /dev/null and b/assets/img/pwa-icon-512.png differ diff --git a/assets/img/pwa-maskable-512.png b/assets/img/pwa-maskable-512.png new file mode 100644 index 0000000..78ba040 Binary files /dev/null and b/assets/img/pwa-maskable-512.png differ diff --git a/contact.php.example b/contact.php.example index 23fb49e..64bd46b 100644 --- a/contact.php.example +++ b/contact.php.example @@ -54,6 +54,7 @@ $siteHeaderTitle = $headerTitles[array_rand($headerTitles)]; MVL - Contact + diff --git a/index.php b/index.php index 0d375f8..a9e3963 100644 --- a/index.php +++ b/index.php @@ -151,6 +151,7 @@ function render_blank_page(){ MVLog + @@ -388,6 +389,7 @@ $siteHeaderTitle = $headerTitles[array_rand($headerTitles)]; <?=h($pageTitle)?> + diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..f196851 --- /dev/null +++ b/manifest.json @@ -0,0 +1,40 @@ +{ + "name": "MVLog - Bubulescu.Org", + "short_name": "MVLog", + "description": "Motorcycle road stories, hockey passion, pizza nerdism and facts remembered by an unreliable narrator.", + "id": "/", + "start_url": "/", + "scope": "/", + "display": "standalone", + "display_override": [ + "standalone", + "minimal-ui" + ], + "background_color": "#111315", + "theme_color": "#1b1e22", + "categories": [ + "travel", + "photo", + "entertainment" + ], + "icons": [ + { + "src": "/assets/img/pwa-icon-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/assets/img/pwa-icon-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/assets/img/pwa-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/offline.html b/offline.html new file mode 100644 index 0000000..599a94b --- /dev/null +++ b/offline.html @@ -0,0 +1,24 @@ + + + + + +MVLog - Offline + + + + + + + + +
+
+ +

You’re offline

+

MVLog could not reach the network. Previously opened pages and assets may still be available.

+

Try home again

+
+
+ + diff --git a/sw.js b/sw.js index 7424e88..a2234f1 100644 --- a/sw.js +++ b/sw.js @@ -1,3 +1,119 @@ +const MVLOG_CACHE_PREFIX = 'mvlog-pwa-'; +const MVLOG_CACHE_VERSION = 'mvlog-pwa-v1'; +const APP_SHELL_CACHE = `${MVLOG_CACHE_VERSION}-app-shell`; +const RUNTIME_CACHE = `${MVLOG_CACHE_VERSION}-runtime`; + +const APP_SHELL_URLS = [ + '/', + '/offline.html', + '/style.css', + '/js/map_lightbox.js', + '/assets/fonts/BebasNeue-Regular.ttf', + '/assets/fonts/IBMPlexSans-SemiBold.ttf', + '/assets/fonts/NotoSans-Bold.ttf', + '/assets/img/background.jpg', + '/assets/img/moto_travel.png', + '/assets/img/pwa-icon-192.png', + '/assets/img/pwa-icon-512.png', + '/assets/img/pwa-maskable-512.png', + '/assets/img/apple-touch-icon.png', + '/manifest.json' +]; + +self.addEventListener('install', event => { + event.waitUntil((async () => { + const cache = await caches.open(APP_SHELL_CACHE); + await Promise.all(APP_SHELL_URLS.map(url => + cache.add(new Request(url, { cache: 'reload' })).catch(() => undefined) + )); + await self.skipWaiting(); + })()); +}); + +self.addEventListener('activate', event => { + event.waitUntil((async () => { + const names = await caches.keys(); + await Promise.all(names.map(name => { + if (name.startsWith(MVLOG_CACHE_PREFIX) && ![APP_SHELL_CACHE, RUNTIME_CACHE].includes(name)) { + return caches.delete(name); + } + return undefined; + })); + await clients.claim(); + })()); +}); + +function shouldBypassFetch(request, url) { + if (request.method !== 'GET') return true; + if (!['http:', 'https:'].includes(url.protocol)) return true; + if (url.origin !== self.location.origin) return false; + + const path = url.pathname; + if (path === '/admin.php' || path === '/login.php' || path === '/logout.php') return true; + if (path === '/push_config.php' || path === '/push_subscribe.php' || path === '/job_status.php') return true; + if (path.startsWith('/api/') || path.startsWith('/lib/') || path.startsWith('/cache/') || path.startsWith('/in-dir/')) return true; + if (path.startsWith('/out-dir/') && /\.(mp4|m4v|mov|webm)$/i.test(path)) return true; + if (/\.(mp4|m4v|mov|webm)$/i.test(path)) return true; + + return false; +} + +function isStaticAsset(url) { + if (url.origin !== self.location.origin) return false; + const path = url.pathname; + return path === '/style.css' + || path === '/manifest.json' + || path === '/offline.html' + || path.startsWith('/assets/') + || path.startsWith('/js/'); +} + +async function networkFirstNavigation(request) { + try { + const response = await fetch(request); + if (response && response.ok) { + const cache = await caches.open(RUNTIME_CACHE); + cache.put(request, response.clone()).catch(() => undefined); + } + return response; + } catch (error) { + const exact = await caches.match(request); + if (exact) return exact; + const offline = await caches.match('/offline.html'); + if (offline) return offline; + const home = await caches.match('/'); + if (home) return home; + return new Response('Offline', { status: 503, headers: { 'Content-Type': 'text/plain; charset=utf-8' } }); + } +} + +async function staleWhileRevalidate(request) { + const cached = await caches.match(request); + const cache = await caches.open(RUNTIME_CACHE); + const network = fetch(request).then(response => { + if (response && response.ok) { + cache.put(request, response.clone()).catch(() => undefined); + } + return response; + }).catch(() => cached); + return cached || network; +} + +self.addEventListener('fetch', event => { + const request = event.request; + const url = new URL(request.url); + if (shouldBypassFetch(request, url)) return; + + if (request.mode === 'navigate') { + event.respondWith(networkFirstNavigation(request)); + return; + } + + if (isStaticAsset(url)) { + event.respondWith(staleWhileRevalidate(request)); + } +}); + self.addEventListener('push', event => { let data = {}; try { data = event.data ? event.data.json() : {}; } catch (e) { data = { title: 'MVLog', body: event.data ? event.data.text() : 'New article @ MVLog!' }; }