PWA basic implemented (not for admin.php)
This commit is contained in:
parent
d7da479741
commit
acc77ac17f
12 changed files with 193 additions and 1 deletions
1
404.php
1
404.php
|
|
@ -72,6 +72,7 @@ http_response_code(404);
|
|||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>MVLog - 404</title>
|
||||
<?php include __DIR__ . '/_pwa_head.php'; ?>
|
||||
|
||||
<!-- Basic Open Graph -->
|
||||
<meta property="og:type" content="website">
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
8
_pwa_head.php
Normal file
8
_pwa_head.php
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<link rel="manifest" href="/manifest.json">
|
||||
<meta name="theme-color" content="#1b1e22">
|
||||
<meta name="application-name" content="MVLog">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<meta name="apple-mobile-web-app-title" content="MVLog">
|
||||
<link rel="apple-touch-icon" href="/assets/img/apple-touch-icon.png">
|
||||
BIN
assets/img/apple-touch-icon.png
Normal file
BIN
assets/img/apple-touch-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 73 KiB |
BIN
assets/img/pwa-icon-192.png
Normal file
BIN
assets/img/pwa-icon-192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 45 KiB |
BIN
assets/img/pwa-icon-512.png
Normal file
BIN
assets/img/pwa-icon-512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 273 KiB |
BIN
assets/img/pwa-maskable-512.png
Normal file
BIN
assets/img/pwa-maskable-512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 522 KiB |
|
|
@ -54,6 +54,7 @@ $siteHeaderTitle = $headerTitles[array_rand($headerTitles)];
|
|||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>MVL - Contact</title>
|
||||
<?php include __DIR__ . '/_pwa_head.php'; ?>
|
||||
<meta property="og:title" content="MVLog: journal of an unreliable narrator.">
|
||||
<meta property="og:description" content="Motorcycle road stories, hockey passion, pizza nerdism and facts remembered by an unreliable narrator.">
|
||||
<meta property="og:image" content="https://bubulescu.org/assets/img/moto_travel.png">
|
||||
|
|
|
|||
|
|
@ -151,6 +151,7 @@ function render_blank_page(){
|
|||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>MVLog</title>
|
||||
<?php include __DIR__ . '/_pwa_head.php'; ?>
|
||||
|
||||
<link rel="icon" type="image/png" href="assets/img/moto_travel.png">
|
||||
<link rel="stylesheet" href="style.css?v=20260531u">
|
||||
|
|
@ -388,6 +389,7 @@ $siteHeaderTitle = $headerTitles[array_rand($headerTitles)];
|
|||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title><?=h($pageTitle)?></title>
|
||||
<?php include __DIR__ . '/_pwa_head.php'; ?>
|
||||
|
||||
<!-- Basic Open Graph -->
|
||||
<meta property="og:type" content="<?=h($ogType)?>">
|
||||
|
|
|
|||
40
manifest.json
Normal file
40
manifest.json
Normal file
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
24
offline.html
Normal file
24
offline.html
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>MVLog - Offline</title>
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
<meta name="theme-color" content="#1b1e22">
|
||||
<link rel="apple-touch-icon" href="/assets/img/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" href="/assets/img/moto_travel.png">
|
||||
<link rel="stylesheet" href="/style.css">
|
||||
<style>main.offline-page{min-height:calc(100vh - 8rem);display:grid;place-items:center;padding:7rem 1rem 3rem;text-align:center}.offline-card{max-width:34rem;background:rgba(27,30,34,.88);border:1px solid rgba(59,167,160,.35);border-radius:1rem;padding:2rem;box-shadow:0 2px 16px #0008}.offline-card img{width:6rem;height:6rem;object-fit:contain}.offline-card h1{margin:.8rem 0 .5rem}.offline-card p{color:#d4d7dd}</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="offline-page">
|
||||
<section class="offline-card">
|
||||
<img src="/assets/img/moto_travel.png" alt="">
|
||||
<h1>You’re offline</h1>
|
||||
<p>MVLog could not reach the network. Previously opened pages and assets may still be available.</p>
|
||||
<p><a class="button" href="/">Try home again</a></p>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
116
sw.js
116
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!' }; }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue