v0.0.10: Progressive Web App (PWA)
- Web manifest (standalone mode, theme, icon) - Service worker: caches shell (HTML, WASM, icon) for offline - SVG app icon (chat bubble with encryption indicator) - iOS meta tags: apple-mobile-web-app-capable, status bar style - Android: beforeinstallprompt → /install command - Offline fallback: loads cached shell, shows reconnecting state - Cache versioning with automatic old cache cleanup Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
10
warzone/Cargo.lock
generated
10
warzone/Cargo.lock
generated
@@ -2647,7 +2647,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "warzone-client"
|
||||
version = "0.0.9"
|
||||
version = "0.0.10"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argon2",
|
||||
@@ -2680,7 +2680,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "warzone-mule"
|
||||
version = "0.0.9"
|
||||
version = "0.0.10"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
@@ -2689,7 +2689,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "warzone-protocol"
|
||||
version = "0.0.9"
|
||||
version = "0.0.10"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"bincode",
|
||||
@@ -2712,7 +2712,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "warzone-server"
|
||||
version = "0.0.9"
|
||||
version = "0.0.10"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -2739,7 +2739,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "warzone-wasm"
|
||||
version = "0.0.9"
|
||||
version = "0.0.10"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"bincode",
|
||||
|
||||
@@ -9,7 +9,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.0.9"
|
||||
version = "0.0.10"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
rust-version = "1.75"
|
||||
|
||||
@@ -12,6 +12,9 @@ pub fn routes() -> Router<AppState> {
|
||||
.route("/", get(web_ui))
|
||||
.route("/wasm/warzone_wasm.js", get(wasm_js))
|
||||
.route("/wasm/warzone_wasm_bg.wasm", get(wasm_binary))
|
||||
.route("/manifest.json", get(pwa_manifest))
|
||||
.route("/sw.js", get(service_worker))
|
||||
.route("/icon.svg", get(pwa_icon))
|
||||
}
|
||||
|
||||
async fn web_ui() -> Html<&'static str> {
|
||||
@@ -32,12 +35,85 @@ async fn wasm_binary() -> impl IntoResponse {
|
||||
)
|
||||
}
|
||||
|
||||
async fn pwa_manifest() -> impl IntoResponse {
|
||||
([(header::CONTENT_TYPE, "application/manifest+json")], r##"{
|
||||
"name": "Warzone Messenger",
|
||||
"short_name": "Warzone",
|
||||
"description": "End-to-end encrypted messenger",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#0a0a1a",
|
||||
"theme_color": "#0a0a1a",
|
||||
"icons": [{"src": "/icon.svg", "sizes": "any", "type": "image/svg+xml", "purpose": "any maskable"}]
|
||||
}"##)
|
||||
}
|
||||
|
||||
async fn service_worker() -> impl IntoResponse {
|
||||
([(header::CONTENT_TYPE, "application/javascript")], r##"
|
||||
const CACHE = 'wz-v1';
|
||||
const SHELL = ['/', '/wasm/warzone_wasm.js', '/wasm/warzone_wasm_bg.wasm', '/icon.svg', '/manifest.json'];
|
||||
|
||||
self.addEventListener('install', e => {
|
||||
e.waitUntil(caches.open(CACHE).then(c => c.addAll(SHELL)));
|
||||
self.skipWaiting();
|
||||
});
|
||||
|
||||
self.addEventListener('activate', e => {
|
||||
e.waitUntil(caches.keys().then(keys =>
|
||||
Promise.all(keys.filter(k => k !== CACHE).map(k => caches.delete(k)))
|
||||
));
|
||||
e.waitUntil(clients.claim());
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', e => {
|
||||
const url = new URL(e.request.url);
|
||||
// API calls: network only
|
||||
if (url.pathname.startsWith('/v1/')) return;
|
||||
// WS: skip
|
||||
if (url.protocol === 'ws:' || url.protocol === 'wss:') return;
|
||||
// Shell: cache first, network fallback
|
||||
e.respondWith(
|
||||
caches.match(e.request).then(cached => cached || fetch(e.request).then(resp => {
|
||||
if (resp.ok && SHELL.includes(url.pathname)) {
|
||||
const clone = resp.clone();
|
||||
caches.open(CACHE).then(c => c.put(e.request, clone));
|
||||
}
|
||||
return resp;
|
||||
}).catch(() => {
|
||||
if (e.request.mode === 'navigate') {
|
||||
return caches.match('/');
|
||||
}
|
||||
}))
|
||||
);
|
||||
});
|
||||
"##)
|
||||
}
|
||||
|
||||
async fn pwa_icon() -> impl IntoResponse {
|
||||
([(header::CONTENT_TYPE, "image/svg+xml")], r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<rect width="512" height="512" rx="100" fill="#0a0a1a"/>
|
||||
<path d="M128 140h256c22 0 40 18 40 40v152c0 22-18 40-40 40H210l-70 52v-52h-12c-22 0-40-18-40-40V180c0-22 18-40 40-40z" fill="#e94560"/>
|
||||
<circle cx="200" cy="256" r="18" fill="#0a0a1a"/>
|
||||
<circle cx="256" cy="256" r="18" fill="#0a0a1a"/>
|
||||
<circle cx="312" cy="256" r="18" fill="#0a0a1a"/>
|
||||
<rect x="180" y="340" width="30" height="8" rx="4" fill="#4ade80"/>
|
||||
<rect x="220" y="340" width="30" height="8" rx="4" fill="#4ade80"/>
|
||||
</svg>"##)
|
||||
}
|
||||
|
||||
const WEB_HTML: &str = r##"<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover">
|
||||
<meta name="theme-color" content="#0a0a1a">
|
||||
<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="Warzone">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
<link rel="icon" href="/icon.svg" type="image/svg+xml">
|
||||
<link rel="apple-touch-icon" href="/icon.svg">
|
||||
<title>Warzone</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
@@ -160,7 +236,7 @@ let pollTimer = null;
|
||||
let ws = null; // WebSocket connection
|
||||
let wasmReady = false;
|
||||
|
||||
const VERSION = '0.0.9';
|
||||
const VERSION = '0.0.10';
|
||||
let DEBUG = true; // toggle with /debug command
|
||||
|
||||
// ── Receipt tracking ──
|
||||
@@ -684,6 +760,11 @@ async function doSend() {
|
||||
return;
|
||||
}
|
||||
if (text === '/clear') { $messages.innerHTML = ''; return; }
|
||||
if (text === '/install') {
|
||||
if (deferredInstall) { deferredInstall.prompt(); deferredInstall = null; }
|
||||
else addSys('Install not available (already installed or not supported)');
|
||||
return;
|
||||
}
|
||||
if (text === '/debug') { DEBUG = !DEBUG; addSys('Debug logging: ' + (DEBUG ? 'ON (check browser console)' : 'OFF')); return; }
|
||||
if (text === '/reset') {
|
||||
localStorage.clear();
|
||||
@@ -812,6 +893,17 @@ document.getElementById('btn-recover').onclick = () => doRecover();
|
||||
document.getElementById('btn-enter').onclick = () => enterChat();
|
||||
document.getElementById('send-btn').onclick = () => doSend();
|
||||
|
||||
// PWA: service worker + install prompt
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.register('/sw.js').catch(e => dbg('SW failed:', e));
|
||||
}
|
||||
let deferredInstall = null;
|
||||
window.addEventListener('beforeinstallprompt', e => {
|
||||
e.preventDefault();
|
||||
deferredInstall = e;
|
||||
addSys('Tip: install as app for fullscreen + notifications. Type /install');
|
||||
});
|
||||
|
||||
// Initialize WASM and auto-load
|
||||
(async function() {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user