diff --git a/app.js b/app.js index a5e0634..114a9b3 100644 --- a/app.js +++ b/app.js @@ -2,7 +2,7 @@ 'use strict'; // ==================== CONFIG ==================== - var BUILD = '10'; // bump every deploy — shown on Debug screen to confirm version + var BUILD = '11'; // bump every deploy — shown on Debug screen to confirm version var STORAGE_KEY = 'stepview_v1'; var RING_CIRC = 603; // 2*pi*r, r=96 var KCAL_PER_KG_PER_KM = 0.9; // gross walking estimate @@ -313,6 +313,7 @@ case 'weight-dec': state.settings.weightKg = clampWeight(state.settings.weightKg - 1); afterSetting(); break; case 'height-inc': state.settings.heightCm = clampHeight(state.settings.heightCm + 1); afterSetting(); break; case 'height-dec': state.settings.heightCm = clampHeight(state.settings.heightCm - 1); afterSetting(); break; + case 'update-refresh': if (waitingWorker) waitingWorker.postMessage('skipWaiting'); break; } } function afterSetting() { renderSettings(); render(); save(); } @@ -437,10 +438,40 @@ }; } + // ==================== SERVICE WORKER ==================== + var waitingWorker = null; + function setupSW() { + if (!('serviceWorker' in navigator)) return; + navigator.serviceWorker.register('sw.js').then(function (reg) { + if (reg.waiting) { + waitingWorker = reg.waiting; + showUpdateBanner(); + } + reg.addEventListener('updatefound', function () { + var newWorker = reg.installing; + newWorker.addEventListener('statechange', function () { + if (newWorker.state === 'installed' && navigator.serviceWorker.controller) { + waitingWorker = newWorker; + showUpdateBanner(); + } + }); + }); + }); + navigator.serviceWorker.addEventListener('controllerchange', function () { + window.location.reload(); + }); + } + + function showUpdateBanner() { + var banner = $('update-banner'); + if (banner) banner.classList.remove('hidden'); + } + // ==================== INIT ==================== function init() { document.querySelectorAll('.screen').forEach(function (s) { if (s.id) screens[s.id] = s; }); load(); + setupSW(); setupEvents(); setupMotion(); // always-on accelerometer capture (independent of the UI) render(); diff --git a/index.html b/index.html index 84c693c..e64602d 100644 --- a/index.html +++ b/index.html @@ -7,7 +7,8 @@ StepView — Step, Distance & Calorie Counter - + +
@@ -132,6 +133,9 @@

Debug

- + + diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..472a737 --- /dev/null +++ b/manifest.json @@ -0,0 +1,16 @@ +{ + "name": "StepView", + "short_name": "StepView", + "description": "Step, distance, and calorie counter for Meta Ray-Ban Display glasses.", + "start_url": "./index.html", + "display": "standalone", + "background_color": "#000000", + "theme_color": "#000000", + "icons": [ + { + "src": "favicon.svg", + "sizes": "any", + "type": "image/svg+xml" + } + ] +} diff --git a/styles.css b/styles.css index f3f2519..c2cddaf 100644 --- a/styles.css +++ b/styles.css @@ -234,4 +234,25 @@ html, body { } .toast.show { transform: translateX(-50%) translateY(0); opacity: 1; } +/* --- Update Banner --- */ +.update-banner { + position: absolute; + bottom: 22px; + left: 50%; + transform: translateX(-50%); + background: var(--surface); + border: 2px solid var(--green); + border-radius: 16px; + padding: 14px 24px; + font-size: 19px; + font-weight: 700; + color: #fff; + z-index: 100; + cursor: pointer; + box-shadow: 0 4px 24px rgba(0, 0, 0, 0.5); + white-space: nowrap; +} +.update-banner span { color: var(--green); margin-left: 8px; text-decoration: underline; } +.update-banner:focus { outline: none; border-color: var(--focus); box-shadow: 0 0 28px var(--focus-glow); } + .hidden { display: none !important; } diff --git a/sw.js b/sw.js new file mode 100644 index 0000000..4b1a4c0 --- /dev/null +++ b/sw.js @@ -0,0 +1,55 @@ +var CACHE_NAME = 'stepview-v11'; +var ASSETS = [ + './', + 'index.html', + 'app.js?v=11', + 'styles.css?v=11', + 'favicon.svg', + 'manifest.json' +]; + +self.addEventListener('install', function (event) { + event.waitUntil( + caches.open(CACHE_NAME).then(function (cache) { + return cache.addAll(ASSETS); + }) + ); +}); + +self.addEventListener('activate', function (event) { + event.waitUntil( + caches.keys().then(function (keys) { + return Promise.all( + keys.map(function (key) { + if (key !== CACHE_NAME) { + return caches.delete(key); + } + }) + ); + }) + ); +}); + +self.addEventListener('fetch', function (event) { + event.respondWith( + caches.match(event.request).then(function (response) { + if (response) return response; + return fetch(event.request).then(function (networkResponse) { + if (!networkResponse || networkResponse.status !== 200 || networkResponse.type !== 'basic') { + return networkResponse; + } + var responseToCache = networkResponse.clone(); + caches.open(CACHE_NAME).then(function (cache) { + cache.put(event.request, responseToCache); + }); + return networkResponse; + }); + }) + ); +}); + +self.addEventListener('message', function (event) { + if (event.data === 'skipWaiting') { + self.skipWaiting(); + } +});