Progress 0 / 0 done

All Done!

Your Progress

StatsBoard.

Every stretch you complete is tracked here — across all sessions and routines.

Run Tracker

MyRuns.

Log your runs manually. Distance, time, elevation — and instant calorie estimates.

Appearance

YourStyle.

Pick a theme, choose a font, or build your own color palette. Changes save instantly.

// Accent dot / ring ctx.strokeStyle = '#b5f542'; ctx.lineWidth = size * 0.04; ctx.beginPath(); ctx.arc(size * 0.5, size * 0.5, size * 0.38, 0, Math.PI * 2); ctx.stroke(); // "S" letter ctx.fillStyle = '#b5f542'; ctx.font = `bold ${size * 0.42}px 'Bebas Neue', sans-serif`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText('S', size * 0.5, size * 0.52); return c.toDataURL('image/png'); } const icon192 = makeIcon(192); const icon512 = makeIcon(512); // Apply as apple-touch-icon immediately const atiEl = document.getElementById('apple-touch-icon'); if (atiEl) atiEl.href = icon192; // ── 2. Build & inject manifest as data URI ─────────────────── const manifest = { name: 'Stretch & Run', short_name: 'Stretch', description: 'Stretch routines, run tracker and stats — all offline.', start_url: '.', display: 'standalone', orientation: 'portrait', background_color: '#0d0f0e', theme_color: '#0d0f0e', categories: ['health', 'fitness', 'sports'], icons: [ { src: icon192, sizes: '192x192', type: 'image/png', purpose: 'any maskable' }, { src: icon512, sizes: '512x512', type: 'image/png', purpose: 'any maskable' }, ] }; const manifestBlob = new Blob( [JSON.stringify(manifest)], { type: 'application/manifest+json' } ); const manifestURL = URL.createObjectURL(manifestBlob); const manifestEl = document.getElementById('pwa-manifest'); if (manifestEl) manifestEl.href = manifestURL; // ── 3. Register inline Service Worker ─────────────────────── // The SW is defined as a string and injected via a Blob URL. // This means zero external files needed — it works from a local HTML file // opened via Android WebView or Chrome. if ('serviceWorker' in navigator) { const CACHE = 'stretch-v1'; const swCode = ` const CACHE = '${CACHE}'; // On install: cache the page itself (the SW's own URL acts as the app shell) self.addEventListener('install', e => { self.skipWaiting(); e.waitUntil( caches.open(CACHE).then(c => c.addAll(['./', self.location.href.replace('/sw-blob','')])) .catch(() => {}) // fail silently — offline caching is best-effort for a local file ); }); // On activate: claim all clients immediately self.addEventListener('activate', e => { e.waitUntil( caches.keys().then(keys => Promise.all(keys.filter(k => k !== CACHE).map(k => caches.delete(k))) ).then(() => self.clients.claim()) ); }); // On fetch: cache-first for same-origin, network-first for fonts/external self.addEventListener('fetch', e => { const url = new URL(e.request.url); // Google Fonts — network first, fall back to cache if (url.hostname.includes('fonts.g')) { e.respondWith( fetch(e.request) .then(res => { const c = res.clone(); caches.open(CACHE).then(ca => ca.put(e.request, c)); return res; }) .catch(() => caches.match(e.request)) ); return; } // Everything else — cache first e.respondWith( caches.match(e.request).then(cached => cached || fetch(e.request).then(res => { const c = res.clone(); caches.open(CACHE).then(ca => ca.put(e.request, c)); return res; }) ) ); }); `; const swBlob = new Blob([swCode], { type: 'application/javascript' }); const swURL = URL.createObjectURL(swBlob); navigator.serviceWorker.register(swURL, { scope: './' }) .then(reg => { console.log('[PWA] Service Worker registered', reg.scope); // Show install prompt handling window.addEventListener('beforeinstallprompt', e => { e.preventDefault(); window._deferredInstallPrompt = e; showInstallBanner(); }); }) .catch(err => console.warn('[PWA] SW registration failed (normal for file:// protocol):', err)); } // ── 4. Install banner ──────────────────────────────────────── // Shows a small "Add to Home Screen" banner when the browser // fires beforeinstallprompt (Chrome on Android). function showInstallBanner() { if (document.getElementById('install-banner')) return; const banner = document.createElement('div'); banner.id = 'install-banner'; banner.innerHTML = `
📲
Install Stretch & Run
Add to Home Screen for offline use
`; document.body.appendChild(banner); } window.installPWA = function() { const prompt = window._deferredInstallPrompt; if (!prompt) return; prompt.prompt(); prompt.userChoice.then(() => { window._deferredInstallPrompt = null; const b = document.getElementById('install-banner'); if (b) b.remove(); }); }; })(); // Boot design on load bootDesign(); renderRoutine('postrun');