Time
@@ -364,9 +402,13 @@
/* ============ persistence (degrades to memory if blocked) ============ */
var store = {
get: function(k, d){ try{ var v = localStorage.getItem(k); return v === null ? d : v; }catch(e){ return d; } },
- set: function(k, v){ try{ localStorage.setItem(k, v); }catch(e){} }
+ set: function(k, v){ try{ localStorage.setItem(k, v); }catch(e){} },
+ num: function(k, d){ var n = parseInt(store.get(k, ""), 10); return isFinite(n) ? n : d; }
};
+ // ask the browser not to evict us when the device is low on space
+ try{ if(navigator.storage && navigator.storage.persist) navigator.storage.persist(); }catch(e){}
+
var THEMES = [
{ id:"sage", accent:"#7E9A79", ink:"#FFFFFF" },
{ id:"brass", accent:"#C3A153", ink:"#FFFFFF" },
@@ -376,8 +418,8 @@
];
var cfg = {
- base: parseInt(store.get("bg.base", "180000"), 10),
- delay: parseInt(store.get("bg.delay", "15000"), 10),
+ base: store.num("bg.base", 180000),
+ delay: store.num("bg.delay", 15000),
theme: store.get("bg.theme", "sage"),
sound: store.get("bg.sound", "1") === "1"
};
@@ -408,6 +450,7 @@
});
var btnPlay = document.getElementById("playpause");
var btnSound = document.getElementById("sound");
+ var btnReset = document.getElementById("reset");
var sheet = document.getElementById("sheet");
var scrim = document.getElementById("scrim");
@@ -511,6 +554,7 @@
function flag(side){
st.reserve[side] = 0;
st.phase = FLAG;
+ paintPlay();
render();
sndFlag();
buzz([90, 70, 90]);
@@ -525,6 +569,7 @@
st.turnAt = performance.now();
st.ticked = false;
st.phase = RUN;
+ paintPlay(); // the bar has to show what the clock is doing
sndSwap(); buzz(14);
return;
}
@@ -539,10 +584,17 @@
sndSwap(); buzz(14);
}
+ function pauseNow(){
+ if(st.phase !== RUN) return;
+ st.held = elapsed();
+ st.phase = PAUSE;
+ paintPlay();
+ }
+
function togglePause(){
if(st.phase === RUN){
- st.held = elapsed();
- st.phase = PAUSE;
+ pauseNow();
+ return;
}else if(st.phase === PAUSE){
st.turnAt = performance.now() - st.held;
st.phase = RUN;
@@ -550,14 +602,22 @@
paintPlay();
}
+ /* SVG elements aren't HTMLElements, so they have no .hidden property —
+ setting it only makes a JS expando. The attribute has to be set by hand. */
+ function showIcon(btn, name, on){
+ var svg = btn.querySelector('[data-icon="' + name + '"]');
+ if(on) svg.removeAttribute("hidden"); else svg.setAttribute("hidden", "");
+ }
+
function paintPlay(){
var showPlay = st.phase !== RUN;
- btnPlay.querySelector('[data-icon="pause"]').hidden = showPlay;
- btnPlay.querySelector('[data-icon="play"]').hidden = !showPlay;
+ showIcon(btnPlay, "pause", !showPlay);
+ showIcon(btnPlay, "play", showPlay);
btnPlay.setAttribute("aria-label", showPlay ? "Start clock" : "Pause clock");
}
function reset(){
+ disarm();
st.phase = IDLE; st.active = -1;
st.reserve = [cfg.base, cfg.base];
st.moves = [0, 0];
@@ -566,6 +626,22 @@
render();
}
+ /* reset needs two taps within 2s — one stray tap can't wipe a live game */
+ var armT = null;
+ function disarm(){
+ if(armT){ clearTimeout(armT); armT = null; }
+ btnReset.classList.remove("armed");
+ btnReset.setAttribute("aria-label", "Reset clock");
+ }
+ function armReset(){
+ if(armT){ disarm(); reset(); buzz(20); return; } // second tap inside the window
+ btnReset.classList.remove("armed");
+ void btnReset.offsetWidth; // restart the drain animation
+ btnReset.classList.add("armed");
+ btnReset.setAttribute("aria-label", "Tap again to reset");
+ armT = setTimeout(disarm, 2000);
+ }
+
/* ============ theme ============ */
function applyTheme(){
var t = THEMES.filter(function(x){ return x.id === cfg.theme; })[0] || THEMES[0];
@@ -599,10 +675,30 @@
document.getElementById("v-delay").textContent = Math.round(cfg.delay / 1000);
document.getElementById("t-sound").setAttribute("aria-checked", cfg.sound ? "true" : "false");
btnSound.setAttribute("aria-pressed", cfg.sound ? "true" : "false");
- btnSound.querySelector('[data-icon="on"]').hidden = !cfg.sound;
- btnSound.querySelector('[data-icon="off"]').hidden = cfg.sound;
+ showIcon(btnSound, "on", cfg.sound);
+ showIcon(btnSound, "off", !cfg.sound);
+ paintPresets();
}
+ /* highlight is derived from cfg, so it clears when you step away from a
+ preset and comes back by itself when you step onto one */
+ function paintPresets(){
+ [].forEach.call(document.querySelectorAll(".preset"), function(b){
+ var on = (+b.dataset.base === cfg.base && +b.dataset.presetDelay === cfg.delay);
+ b.setAttribute("aria-pressed", on ? "true" : "false");
+ });
+ }
+
+ [].forEach.call(document.querySelectorAll(".preset"), function(b){
+ b.addEventListener("click", function(){
+ cfg.base = parseInt(b.dataset.base, 10);
+ cfg.delay = parseInt(b.dataset.presetDelay, 10);
+ store.set("bg.base", String(cfg.base));
+ store.set("bg.delay", String(cfg.delay));
+ paintSettings(); reset();
+ });
+ });
+
[].forEach.call(document.querySelectorAll("[data-time-adj]"), function(b){
b.addEventListener("click", function(){
var next = cfg.base + parseInt(b.dataset.timeAdj, 10) * 1000;
@@ -633,11 +729,16 @@
sheet.classList.toggle("on", on);
scrim.classList.toggle("on", on);
}
- document.getElementById("settings").addEventListener("click", function(){ paintSettings(); openSheet(true); });
+ document.getElementById("settings").addEventListener("click", function(){
+ pauseNow(); // nobody's clock runs while the sheet is open
+ disarm();
+ paintSettings();
+ openSheet(true);
+ });
document.getElementById("done").addEventListener("click", function(){ openSheet(false); });
scrim.addEventListener("click", function(){ openSheet(false); });
- document.getElementById("reset").addEventListener("click", reset);
+ btnReset.addEventListener("click", armReset);
btnPlay.addEventListener("click", togglePause);
/* ============ panel taps ============ */
diff --git a/public_html/sw.js b/public_html/sw.js
index d427f59..07b0ebe 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-v1";
+const CACHE = "bgclock-v2";
const FILES = [
"./",
"./index.html",