diff --git a/composeJournalsApp/src/webMain/resources/icon-512.png b/composeJournalsApp/src/webMain/resources/icon-512.png new file mode 100644 index 00000000..640aca38 Binary files /dev/null and b/composeJournalsApp/src/webMain/resources/icon-512.png differ diff --git a/composeJournalsApp/src/webMain/resources/index.html b/composeJournalsApp/src/webMain/resources/index.html index f33e41ad..144b818b 100644 --- a/composeJournalsApp/src/webMain/resources/index.html +++ b/composeJournalsApp/src/webMain/resources/index.html @@ -6,6 +6,9 @@ spectacled Journals + + + @@ -17,5 +20,16 @@ + \ No newline at end of file diff --git a/composeJournalsApp/src/webMain/resources/manifest.webmanifest b/composeJournalsApp/src/webMain/resources/manifest.webmanifest new file mode 100644 index 00000000..d6dbf8e9 --- /dev/null +++ b/composeJournalsApp/src/webMain/resources/manifest.webmanifest @@ -0,0 +1,20 @@ +{ + "name": "spectacled Journals", + "short_name": "Journals", + "description": "Keep private, standards-based journal entries synced over CalDAV.", + "lang": "en", + "start_url": ".", + "scope": ".", + "display": "standalone", + "orientation": "any", + "background_color": "#ffffff", + "theme_color": "#006896", + "icons": [ + { + "src": "icon-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any" + } + ] +} diff --git a/composeJournalsApp/src/webMain/resources/service-worker.js b/composeJournalsApp/src/webMain/resources/service-worker.js new file mode 100644 index 00000000..8c864ce1 --- /dev/null +++ b/composeJournalsApp/src/webMain/resources/service-worker.js @@ -0,0 +1,63 @@ +// Spectacled offline service worker. +// +// Strategy: network-first for same-origin GET requests to static app-shell assets, falling back +// to the cache only when the network fails (i.e. offline). Network-first is deliberate: +// - It never serves a stale bundle or a stale sql.js worker while online, so app updates and +// the DAT-6 IndexedDB persistence mechanism always load the freshest files. The cache is +// purely an offline fallback. +// - Dynamic requests are never cached: non-GET methods (CalDAV PROPFIND/REPORT/PUT/POST), any +// cross-origin request (the CalDAV server / proxy), and same-origin requests that aren't a +// navigation or a known static asset extension all pass straight through, untouched. +// +// Bump CACHE_VERSION whenever this file changes to drop the previous cache on activation. + +const CACHE_VERSION = 'spectacled-shell-v1'; + +// Only these same-origin responses are treated as cacheable app-shell assets. +const SHELL_ASSET = /\.(?:js|mjs|css|wasm|html|ico|png|svg|webmanifest|json|woff2?)$/i; + +self.addEventListener('install', () => { + // Take over as soon as installed instead of waiting for existing tabs to close. + self.skipWaiting(); +}); + +self.addEventListener('activate', (event) => { + event.waitUntil((async () => { + const keys = await caches.keys(); + await Promise.all(keys.filter((k) => k !== CACHE_VERSION).map((k) => caches.delete(k))); + await self.clients.claim(); + })()); +}); + +self.addEventListener('fetch', (event) => { + const request = event.request; + const url = new URL(request.url); + + // Leave everything that isn't a same-origin GET for a static shell asset (or a navigation) to + // the browser's default handling — crucially, all CalDAV/proxy traffic. + if (request.method !== 'GET' || url.origin !== self.location.origin) return; + const isNavigation = request.mode === 'navigate'; + if (!isNavigation && !SHELL_ASSET.test(url.pathname)) return; + + event.respondWith((async () => { + const cache = await caches.open(CACHE_VERSION); + try { + const response = await fetch(request); + // Cache a copy of successful same-origin responses for offline use. + if (response && response.ok && response.type === 'basic') { + cache.put(request, response.clone()); + } + return response; + } catch (error) { + // Offline: serve the cached copy if we have one. + const cached = await cache.match(request); + if (cached) return cached; + // For a page load with no cached match, fall back to the cached app shell. + if (isNavigation) { + const shell = (await cache.match('index.html')) || (await cache.match('./')); + if (shell) return shell; + } + throw error; + } + })()); +}); diff --git a/composeNotesApp/src/webMain/resources/icon-512.png b/composeNotesApp/src/webMain/resources/icon-512.png new file mode 100644 index 00000000..bf1f4b0b Binary files /dev/null and b/composeNotesApp/src/webMain/resources/icon-512.png differ diff --git a/composeNotesApp/src/webMain/resources/index.html b/composeNotesApp/src/webMain/resources/index.html index dec446d7..24cb1b14 100644 --- a/composeNotesApp/src/webMain/resources/index.html +++ b/composeNotesApp/src/webMain/resources/index.html @@ -6,6 +6,9 @@ spectacled Notes + + + @@ -17,5 +20,16 @@ + \ No newline at end of file diff --git a/composeNotesApp/src/webMain/resources/manifest.webmanifest b/composeNotesApp/src/webMain/resources/manifest.webmanifest new file mode 100644 index 00000000..67b4b01e --- /dev/null +++ b/composeNotesApp/src/webMain/resources/manifest.webmanifest @@ -0,0 +1,20 @@ +{ + "name": "spectacled Notes", + "short_name": "Notes", + "description": "Keep private, standards-based notes synced over CalDAV.", + "lang": "en", + "start_url": ".", + "scope": ".", + "display": "standalone", + "orientation": "any", + "background_color": "#ffffff", + "theme_color": "#994c2c", + "icons": [ + { + "src": "icon-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any" + } + ] +} diff --git a/composeNotesApp/src/webMain/resources/service-worker.js b/composeNotesApp/src/webMain/resources/service-worker.js new file mode 100644 index 00000000..8c864ce1 --- /dev/null +++ b/composeNotesApp/src/webMain/resources/service-worker.js @@ -0,0 +1,63 @@ +// Spectacled offline service worker. +// +// Strategy: network-first for same-origin GET requests to static app-shell assets, falling back +// to the cache only when the network fails (i.e. offline). Network-first is deliberate: +// - It never serves a stale bundle or a stale sql.js worker while online, so app updates and +// the DAT-6 IndexedDB persistence mechanism always load the freshest files. The cache is +// purely an offline fallback. +// - Dynamic requests are never cached: non-GET methods (CalDAV PROPFIND/REPORT/PUT/POST), any +// cross-origin request (the CalDAV server / proxy), and same-origin requests that aren't a +// navigation or a known static asset extension all pass straight through, untouched. +// +// Bump CACHE_VERSION whenever this file changes to drop the previous cache on activation. + +const CACHE_VERSION = 'spectacled-shell-v1'; + +// Only these same-origin responses are treated as cacheable app-shell assets. +const SHELL_ASSET = /\.(?:js|mjs|css|wasm|html|ico|png|svg|webmanifest|json|woff2?)$/i; + +self.addEventListener('install', () => { + // Take over as soon as installed instead of waiting for existing tabs to close. + self.skipWaiting(); +}); + +self.addEventListener('activate', (event) => { + event.waitUntil((async () => { + const keys = await caches.keys(); + await Promise.all(keys.filter((k) => k !== CACHE_VERSION).map((k) => caches.delete(k))); + await self.clients.claim(); + })()); +}); + +self.addEventListener('fetch', (event) => { + const request = event.request; + const url = new URL(request.url); + + // Leave everything that isn't a same-origin GET for a static shell asset (or a navigation) to + // the browser's default handling — crucially, all CalDAV/proxy traffic. + if (request.method !== 'GET' || url.origin !== self.location.origin) return; + const isNavigation = request.mode === 'navigate'; + if (!isNavigation && !SHELL_ASSET.test(url.pathname)) return; + + event.respondWith((async () => { + const cache = await caches.open(CACHE_VERSION); + try { + const response = await fetch(request); + // Cache a copy of successful same-origin responses for offline use. + if (response && response.ok && response.type === 'basic') { + cache.put(request, response.clone()); + } + return response; + } catch (error) { + // Offline: serve the cached copy if we have one. + const cached = await cache.match(request); + if (cached) return cached; + // For a page load with no cached match, fall back to the cached app shell. + if (isNavigation) { + const shell = (await cache.match('index.html')) || (await cache.match('./')); + if (shell) return shell; + } + throw error; + } + })()); +}); diff --git a/composeTasksApp/src/webMain/resources/icon-512.png b/composeTasksApp/src/webMain/resources/icon-512.png new file mode 100644 index 00000000..78860496 Binary files /dev/null and b/composeTasksApp/src/webMain/resources/icon-512.png differ diff --git a/composeTasksApp/src/webMain/resources/index.html b/composeTasksApp/src/webMain/resources/index.html index 7c910c90..68c94617 100644 --- a/composeTasksApp/src/webMain/resources/index.html +++ b/composeTasksApp/src/webMain/resources/index.html @@ -6,6 +6,9 @@ spectacled Tasks + + + @@ -17,5 +20,16 @@ + \ No newline at end of file diff --git a/composeTasksApp/src/webMain/resources/manifest.webmanifest b/composeTasksApp/src/webMain/resources/manifest.webmanifest new file mode 100644 index 00000000..e4e9b408 --- /dev/null +++ b/composeTasksApp/src/webMain/resources/manifest.webmanifest @@ -0,0 +1,20 @@ +{ + "name": "spectacled Tasks", + "short_name": "Tasks", + "description": "Keep private, standards-based tasks synced over CalDAV.", + "lang": "en", + "start_url": ".", + "scope": ".", + "display": "standalone", + "orientation": "any", + "background_color": "#ffffff", + "theme_color": "#296f23", + "icons": [ + { + "src": "icon-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any" + } + ] +} diff --git a/composeTasksApp/src/webMain/resources/service-worker.js b/composeTasksApp/src/webMain/resources/service-worker.js new file mode 100644 index 00000000..8c864ce1 --- /dev/null +++ b/composeTasksApp/src/webMain/resources/service-worker.js @@ -0,0 +1,63 @@ +// Spectacled offline service worker. +// +// Strategy: network-first for same-origin GET requests to static app-shell assets, falling back +// to the cache only when the network fails (i.e. offline). Network-first is deliberate: +// - It never serves a stale bundle or a stale sql.js worker while online, so app updates and +// the DAT-6 IndexedDB persistence mechanism always load the freshest files. The cache is +// purely an offline fallback. +// - Dynamic requests are never cached: non-GET methods (CalDAV PROPFIND/REPORT/PUT/POST), any +// cross-origin request (the CalDAV server / proxy), and same-origin requests that aren't a +// navigation or a known static asset extension all pass straight through, untouched. +// +// Bump CACHE_VERSION whenever this file changes to drop the previous cache on activation. + +const CACHE_VERSION = 'spectacled-shell-v1'; + +// Only these same-origin responses are treated as cacheable app-shell assets. +const SHELL_ASSET = /\.(?:js|mjs|css|wasm|html|ico|png|svg|webmanifest|json|woff2?)$/i; + +self.addEventListener('install', () => { + // Take over as soon as installed instead of waiting for existing tabs to close. + self.skipWaiting(); +}); + +self.addEventListener('activate', (event) => { + event.waitUntil((async () => { + const keys = await caches.keys(); + await Promise.all(keys.filter((k) => k !== CACHE_VERSION).map((k) => caches.delete(k))); + await self.clients.claim(); + })()); +}); + +self.addEventListener('fetch', (event) => { + const request = event.request; + const url = new URL(request.url); + + // Leave everything that isn't a same-origin GET for a static shell asset (or a navigation) to + // the browser's default handling — crucially, all CalDAV/proxy traffic. + if (request.method !== 'GET' || url.origin !== self.location.origin) return; + const isNavigation = request.mode === 'navigate'; + if (!isNavigation && !SHELL_ASSET.test(url.pathname)) return; + + event.respondWith((async () => { + const cache = await caches.open(CACHE_VERSION); + try { + const response = await fetch(request); + // Cache a copy of successful same-origin responses for offline use. + if (response && response.ok && response.type === 'basic') { + cache.put(request, response.clone()); + } + return response; + } catch (error) { + // Offline: serve the cached copy if we have one. + const cached = await cache.match(request); + if (cached) return cached; + // For a page load with no cached match, fall back to the cached app shell. + if (isNavigation) { + const shell = (await cache.match('index.html')) || (await cache.match('./')); + if (shell) return shell; + } + throw error; + } + })()); +});