diff --git a/public_html/README.md b/public_html/README.md index 6822bab..dd5d0aa 100644 --- a/public_html/README.md +++ b/public_html/README.md @@ -15,12 +15,29 @@ GitHub Pages is free and takes about two minutes: 2. Upload all six files to the root: `index.html`, `manifest.webmanifest`, `sw.js`, `icon-192.png`, `icon-512.png`, `icon-maskable-512.png`. 3. **Settings → Pages → Source: Deploy from a branch → `main` / `(root)` → Save.** -4. Wait a minute, then open `https://.github.io/bgclock/` in Chrome on Android. -5. Chrome menu (⋮) → **Add to Home screen** → **Install**. +4. Wait a minute, then open `https://.github.io/bgclock/` on your phone. +5. **Android, Chrome:** menu (⋮) → **Add to Home screen** → **Install**. + **iOS, Safari:** Share (□↑) → **Add to Home Screen** → **Add**. It has to be + Safari — Chrome and Firefox on iOS can't install a web app. Open it from the home-screen icon and it runs fullscreen with no browser chrome, offline, with the screen kept awake while a game is on. +On iOS a few things behave differently, none of them fatal: + +- **Install from Safari, and play from the home-screen icon.** In a Safari tab + you lose a strip of the screen to the toolbar, and iOS clears the saved + settings of a site you haven't visited for a week. The installed app keeps + its own storage and doesn't get pruned that way. +- **There's no vibration** — iOS gives web pages no haptics at all, so the + turn-change buzz is silent there. The sounds still play. +- **The ringer switch doesn't mute the clock.** The page claims a playback + audio session so you can hear the delay tick with the phone silenced, which + in exchange pauses any music you had going. If you'd rather keep the music, + delete the `navigator.audioSession` line in `index.html`. +- **Screen wake needs iOS 16.4 or newer.** Below that the screen dims on its + usual schedule mid-game. + If you're using a different path than `/bgclock/`, edit the `"id"` field in `manifest.webmanifest` to match, or just delete that line. @@ -62,7 +79,8 @@ Brass is the colour in the chess.com app, if you want the exact original look. Changing time or delay resets the clock. Settings are kept in `localStorage`, so they survive closing the tab, force-quitting the installed app and rebooting the phone — the app also asks for persistent storage so they aren't evicted when the -device runs low on space. Only the settings are stored; a game in progress isn't. +device runs low on space (Chrome honours that request; Safari ignores it). Only +the settings are stored; a game in progress isn't. ## Sounds diff --git a/public_html/index.html b/public_html/index.html index 79811ca..24f62b8 100644 --- a/public_html/index.html +++ b/public_html/index.html @@ -7,6 +7,7 @@ + @@ -40,13 +41,16 @@ font-feature-settings:"tnum" 1; -webkit-tap-highlight-color:transparent; -webkit-user-select:none;user-select:none; + -webkit-touch-callout:none; /* no iOS long-press callout on a panel */ overscroll-behavior:none; touch-action:manipulation; } /* ---------- frame ---------- */ .app{ - height:100dvh;height:100vh; + height:100vh;height:100dvh; /* dvh must win — 100vh on iOS Safari is the + large viewport, so the bar and the bottom + panel end up under the toolbar */ display:grid; grid-template-rows:1fr auto 1fr; } @@ -193,7 +197,8 @@ padding:8px 22px max(22px,env(safe-area-inset-bottom)); transform:translateY(101%); transition:transform .26s cubic-bezier(.32,.72,0,1); - max-height:88dvh;overflow-y:auto; + max-height:88vh;max-height:88dvh;overflow-y:auto; + overscroll-behavior:contain; /* scrolling the sheet can't rubber-band the app */ box-shadow:0 -18px 50px rgba(0,0,0,.45); } .sheet.on{transform:translateY(0)} @@ -478,8 +483,18 @@ if(ac.state === "suspended") ac.resume(); return ac; } + // iOS silences Web Audio through the ringer switch unless the page claims a + // playback session. Trade-off: it also interrupts whatever else is playing, + // so it's only claimed the first time the clock actually makes a sound. + var claimedSession = false; + function claimAudioSession(){ + if(claimedSession) return; + claimedSession = true; + try{ if(navigator.audioSession) navigator.audioSession.type = "playback"; }catch(e){} + } function tone(freq, at, dur, peak, type, partial){ var c = audio(); if(!c) return; + claimAudioSession(); var t0 = c.currentTime + at; var o = c.createOscillator(), g = c.createGain(); o.type = type || "sine"; @@ -773,11 +788,22 @@ /* ============ keep the screen awake ============ */ var lock = null; function keepAwake(){ - if(!navigator.wakeLock) return; - navigator.wakeLock.request("screen").then(function(l){ lock = l; }).catch(function(){}); + if(!navigator.wakeLock || lock) return; + if(document.visibilityState !== "visible") return; // request() rejects when hidden + navigator.wakeLock.request("screen").then(function(l){ + lock = l; + // the OS drops the lock whenever the app goes to the background; without + // clearing it here the stale handle blocks every later re-request + l.addEventListener("release", function(){ lock = null; }); + }).catch(function(){}); } document.addEventListener("visibilitychange", function(){ - if(document.visibilityState === "visible" && !lock) keepAwake(); + if(document.visibilityState !== "visible") return; + keepAwake(); + // iOS interrupts the audio context on backgrounding; nudge it back so the + // flag still sounds even if nobody has tapped since returning + // (old webkitAudioContext.resume() returns undefined, so don't chain on it) + if(ac && ac.state === "suspended"){ try{ ac.resume(); }catch(e){} } }); keepAwake(); diff --git a/public_html/sw.js b/public_html/sw.js index 07b0ebe..692cda0 100644 --- a/public_html/sw.js +++ b/public_html/sw.js @@ -1,5 +1,5 @@ /* Cache-first: once installed the clock never touches the network again. */ -const CACHE = "bgclock-v2"; +const CACHE = "bgclock-v3"; const FILES = [ "./", "./index.html",