diff --git a/PWA.md b/PWA.md new file mode 100644 index 0000000..e606805 --- /dev/null +++ b/PWA.md @@ -0,0 +1,10 @@ +# PWA updates + +When you change files that the service worker caches (`index.html`, `style.css`, `js/*`, icons, manifest, etc.): + +1. Open `sw.js` +2. Bump `CACHE_VERSION` (e.g. `bumpmesh-v1` → `bumpmesh-v2`) +3. If you added or renamed cached files, update the `PRECACHE` list in `sw.js` +4. Commit, push, and deploy + +Users pick up the update on their next visit. The new service worker installs, replaces the old cache, and serves fresh files. \ No newline at end of file diff --git a/README.md b/README.md index d27d50b..404325f 100644 --- a/README.md +++ b/README.md @@ -154,6 +154,8 @@ Open http://localhost:8000 in your browser and you're ready to go. > **Tip:** Any static server will work — the app has no server-side dependencies. +> **PWA changes:** Read [PWA.md](PWA.md) before you commit and push. + ## Dependencies Loaded via CDN ([jsDelivr](https://www.jsdelivr.com/)) — no build step or npm install needed: diff --git a/icons/icon-192.png b/icons/icon-192.png new file mode 100644 index 0000000..5a5947a Binary files /dev/null and b/icons/icon-192.png differ diff --git a/icons/icon-512.png b/icons/icon-512.png new file mode 100644 index 0000000..73421aa Binary files /dev/null and b/icons/icon-512.png differ diff --git a/index.html b/index.html index 6b1b661..8a72b93 100644 --- a/index.html +++ b/index.html @@ -24,15 +24,36 @@ + + + + + + + @@ -732,5 +753,12 @@

New in this release &mdas + diff --git a/js/main.js b/js/main.js index 58d7853..8be3004 100644 --- a/js/main.js +++ b/js/main.js @@ -900,8 +900,12 @@ let PRESETS = []; initViewer(canvas); -// Apply saved theme to 3D viewport on startup -setViewerTheme(document.documentElement.getAttribute('data-theme') === 'light'); +// Apply saved theme to 3D viewport + PWA chrome on startup +{ + const isLight = document.documentElement.getAttribute('data-theme') === 'light'; + setViewerTheme(isLight); + applyPwaThemeChrome(isLight); +} // Populate the language selector function populateLanguageSelector() { @@ -980,12 +984,26 @@ populateLanguageSelector(); } })(); +// Keep PWA browser chrome in sync with the *site* theme (not OS prefers-color-scheme). +function applyPwaThemeChrome(isLight) { + const themeColor = document.querySelector('meta[name="theme-color"]'); + const statusBar = document.querySelector('meta[name="apple-mobile-web-app-status-bar-style"]'); + if (themeColor) { + themeColor.setAttribute('content', isLight ? '#f0f0f5' : '#111114'); + } + // Apple: "default" = light status bar; "black-translucent" for dark UI + if (statusBar) { + statusBar.setAttribute('content', isLight ? 'default' : 'black-translucent'); + } +} + // Theme toggle document.getElementById('theme-toggle').addEventListener('click', () => { const isLight = document.documentElement.getAttribute('data-theme') !== 'light'; document.documentElement.setAttribute('data-theme', isLight ? 'light' : 'dark'); localStorage.setItem('stlt-theme', isLight ? 'light' : 'dark'); setViewerTheme(isLight); + applyPwaThemeChrome(isLight); }); wireEvents(); diff --git a/manifest.webmanifest b/manifest.webmanifest new file mode 100644 index 0000000..1bea4ca --- /dev/null +++ b/manifest.webmanifest @@ -0,0 +1,32 @@ +{ + "name": "BumpMesh by CNC Kitchen", + "short_name": "BumpMesh", + "description": "Add displacement textures to STL, OBJ, and 3MF models directly in your browser.", + "start_url": "./", + "scope": "./", + "display": "standalone", + "orientation": "any", + "background_color": "#111114", + "theme_color": "#111114", + "categories": ["utilities", "productivity"], + "icons": [ + { + "src": "icons/icon-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any" + }, + { + "src": "icons/icon-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any" + }, + { + "src": "icons/icon-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/sw.js b/sw.js new file mode 100644 index 0000000..a0e8c3d --- /dev/null +++ b/sw.js @@ -0,0 +1,114 @@ +const CACHE_VERSION = 'bumpmesh-v2'; + +const PRECACHE = [ + './', + './index.html', + './style.css', + './logo.png', + './manifest.webmanifest', + './icons/icon-192.png', + './icons/icon-512.png', + './js/main.js', + './js/viewer.js', + './js/stlLoader.js', + './js/smartResolution.js', + './js/presetTextures.js', + './js/previewMaterial.js', + './js/subdivision.js', + './js/regularize.js', + './js/exportPipeline.js', + './js/exporter.js', + './js/exclusion.js', + './js/meshValidation.js', + './js/i18n.js', + './js/meshIndex.js', + './js/mapping.js', + './js/displacement.js', + './js/decimation.js', + './js/meshRepair.js', + './js/textureAnalysis.js', + './js/threeCompat.js', + './js/exportWorker.js', + './js/i18n/en.js', + './js/i18n/de.js', + './js/i18n/es.js', + './js/i18n/fr.js', + './js/i18n/it.js', + './js/i18n/ja.js', + './js/i18n/ko.js', + './js/i18n/pt.js', + './js/i18n/tr.js', + './js/i18n/uk.js', +]; + +self.addEventListener('install', (event) => { + event.waitUntil( + caches.open(CACHE_VERSION) + .then((cache) => cache.addAll(PRECACHE)) + .then(() => self.skipWaiting()), + ); +}); + +self.addEventListener('activate', (event) => { + event.waitUntil( + caches.keys() + .then((keys) => Promise.all( + keys.filter((key) => key !== CACHE_VERSION).map((key) => caches.delete(key)), + )) + .then(() => self.clients.claim()), + ); +}); + +self.addEventListener('fetch', (event) => { + const { request } = event; + if (request.method !== 'GET') return; + + const url = new URL(request.url); + + if (url.origin !== self.location.origin) { + if (url.hostname === 'cdn.jsdelivr.net') { + event.respondWith(networkFirst(request)); + } + return; + } + + if (request.mode === 'navigate') { + event.respondWith(networkFirst(request, './index.html')); + return; + } + + event.respondWith(staleWhileRevalidate(request)); +}); + +async function networkFirst(request, fallbackUrl) { + try { + const response = await fetch(request); + if (response.ok) { + const cache = await caches.open(CACHE_VERSION); + cache.put(request, response.clone()); + } + return response; + } catch { + const cached = await caches.match(request); + if (cached) return cached; + if (fallbackUrl) { + const fallback = await caches.match(fallbackUrl); + if (fallback) return fallback; + } + throw new Error('Network unavailable'); + } +} + +async function staleWhileRevalidate(request) { + const cache = await caches.open(CACHE_VERSION); + const cached = await cache.match(request); + + const networkPromise = fetch(request) + .then((response) => { + if (response.ok) cache.put(request, response.clone()); + return response; + }) + .catch(() => null); + + return cached || networkPromise || fetch(request); +}