Show a timer in the day bar while a walk is on
The bar already carries the asleep/awake counter; a walk in progress had nothing, so "how long have we been out" meant going to the Walks panel to look. It gets a second pill now, counting from the walk's start, and tapping it ends the walk — the same bargain the sleep pill offers for the sleep boundary. Two timers no longer fit beside the day controls on a narrow phone, so the bar wraps. That needed the children grouping first: with seven loose ones the break could land anywhere, and stranding "Today" alone on a second line is worse than not wrapping at all. The timers are one group and the day controls another, so the wrap falls between them. Two things follow from the bar changing height. The tab bar sticks to that height, and the ResizeObserver added when the pill first appearing had the same effect already covers it — nothing new needed. And both pills go to standby together while the big card is on screen: standby is visibility, not display, so the bar keeps its wrapped height while you scroll and the tab bar beneath it does not shuffle. The card shows the walk while there is one. It has room for a single timer, and you are necessarily awake on a walk, so "awake for 3h" is the less useful of the two readings; the bar keeps both. That also meant moving the card out of the early return for "no sleep logged yet" — a walk can be the first thing ever recorded, and the card was staying blank through it.
This commit is contained in:
+80
-15
@@ -985,6 +985,15 @@
|
||||
// counter without re-deriving from the event log.
|
||||
let bigClockState = null; // "asleep" | "awake" | null
|
||||
let bigClockSince = 0;
|
||||
let walkSince = 0; // start of the walk in progress, 0 when none is
|
||||
|
||||
// The walk currently in progress, if any. pairWindows already marks an
|
||||
// unmatched walk-start as ongoing, so this is just the last such window —
|
||||
// there can only be one, since a start closes the previous pair.
|
||||
function currentWalkStart(events) {
|
||||
const open = walkWindows(events).find(w => w.ongoing);
|
||||
return open ? open.start : 0;
|
||||
}
|
||||
|
||||
// The counter renders twice: the big card at the top of the page and the
|
||||
// compact pill in the frozen day bar. The pill only *shows* once the big
|
||||
@@ -992,17 +1001,21 @@
|
||||
// visible the pill is invisible but keeps its slot so the bar never shifts.
|
||||
function updateBarClockMode() {
|
||||
const pill = document.getElementById("bar-clock");
|
||||
if (!bigClockState || pill.hidden) return;
|
||||
const walkPill = document.getElementById("bar-walk");
|
||||
const card = document.getElementById("big-clock");
|
||||
const bar = document.querySelector(".day-bar");
|
||||
// The card lives on the Today tab, so on any other tab it isn't on screen
|
||||
// at all and the pill is the only timer there is. Asked explicitly rather
|
||||
// than left to the rect: a hidden element measures as zeroes, which would
|
||||
// give the right answer here by coincidence rather than by rule.
|
||||
// at all and the pills are the only timers there are. Asked explicitly
|
||||
// rather than left to the rect: a hidden element measures as zeroes, which
|
||||
// would give the right answer here by coincidence rather than by rule.
|
||||
const cardOnThisTab = card.offsetParent !== null;
|
||||
const cardVisible = cardOnThisTab &&
|
||||
card.getBoundingClientRect().bottom > bar.getBoundingClientRect().bottom;
|
||||
// Both stand by together. standby keeps the slot rather than removing it,
|
||||
// so the bar holds its height — including a wrapped second row — and the
|
||||
// tab bar stuck to that height doesn't shift about as you scroll.
|
||||
pill.classList.toggle("standby", cardVisible);
|
||||
walkPill.classList.toggle("standby", cardVisible);
|
||||
}
|
||||
|
||||
function renderBigClock(events) {
|
||||
@@ -1016,36 +1029,81 @@
|
||||
const { state, since: ts } = currentSleepState(events);
|
||||
bigClockState = state;
|
||||
bigClockSince = ts;
|
||||
|
||||
// The walk pill is independent of the sleep one: it comes and goes with the
|
||||
// walk, and the sleep pill is unaffected by either.
|
||||
walkSince = currentWalkStart(events);
|
||||
renderWalkPill();
|
||||
|
||||
if (!state) {
|
||||
card.hidden = true;
|
||||
pill.hidden = true;
|
||||
// A walk can be running before any sleep has ever been logged, and the
|
||||
// card is the only place its timer shows at the top of the day.
|
||||
renderBigClockCard();
|
||||
updateBarClockMode();
|
||||
return;
|
||||
}
|
||||
card.hidden = false;
|
||||
pill.hidden = false;
|
||||
card.classList.toggle("asleep", state === "asleep");
|
||||
card.classList.toggle("awake", state === "awake");
|
||||
pill.classList.toggle("asleep", state === "asleep");
|
||||
pill.classList.toggle("awake", state === "awake");
|
||||
label.textContent = state === "asleep" ? "Asleep for" : "Awake for";
|
||||
const counter = formatCounter(Date.now() - ts);
|
||||
time.textContent = counter;
|
||||
ptime.textContent = counter;
|
||||
since.textContent = `since ${formatTime(ts)}`;
|
||||
icon.textContent = state === "asleep" ? "😴" : "☀️";
|
||||
const flip = state === "asleep" ? "Sleep end" : "Sleep start";
|
||||
pill.title = `${state === "asleep" ? "Asleep" : "Awake"} since ${formatTime(ts)} — tap to log ${flip.toLowerCase()}`;
|
||||
pill.setAttribute("aria-label", `${state === "asleep" ? "Asleep" : "Awake"} since ${formatTime(ts)}. Log ${flip.toLowerCase()}.`);
|
||||
renderBigClockCard();
|
||||
updateBarClockMode();
|
||||
}
|
||||
|
||||
function tickBigClock() {
|
||||
if (!bigClockState) return;
|
||||
const counter = formatCounter(Date.now() - bigClockSince);
|
||||
// The card shows one thing at a time, and a walk in progress is the more
|
||||
// immediate of the two — you are necessarily awake on a walk, so "awake for
|
||||
// 3h" is the less useful reading. The bar keeps both.
|
||||
function renderBigClockCard() {
|
||||
const card = document.getElementById("big-clock");
|
||||
const label = document.getElementById("bc-label");
|
||||
const time = document.getElementById("bc-time");
|
||||
const ptime = document.getElementById("bar-clock-time");
|
||||
if (time) time.textContent = counter;
|
||||
if (ptime) ptime.textContent = counter;
|
||||
const since = document.getElementById("bc-since");
|
||||
const walking = walkSince > 0;
|
||||
if (!walking && !bigClockState) { card.hidden = true; return; }
|
||||
card.hidden = false;
|
||||
card.classList.toggle("walking", walking);
|
||||
card.classList.toggle("asleep", !walking && bigClockState === "asleep");
|
||||
card.classList.toggle("awake", !walking && bigClockState === "awake");
|
||||
const ts = walking ? walkSince : bigClockSince;
|
||||
label.textContent = walking
|
||||
? "Walking for"
|
||||
: (bigClockState === "asleep" ? "Asleep for" : "Awake for");
|
||||
time.textContent = formatCounter(Date.now() - ts);
|
||||
since.textContent = `since ${formatTime(ts)}`;
|
||||
}
|
||||
|
||||
function renderWalkPill() {
|
||||
const pill = document.getElementById("bar-walk");
|
||||
const wtime = document.getElementById("bar-walk-time");
|
||||
if (!walkSince) { pill.hidden = true; return; }
|
||||
pill.hidden = false;
|
||||
wtime.textContent = formatCounter(Date.now() - walkSince);
|
||||
pill.title = `Walking since ${formatTime(walkSince)} — tap to log walk end`;
|
||||
pill.setAttribute("aria-label", `Walking since ${formatTime(walkSince)}. Log walk end.`);
|
||||
}
|
||||
|
||||
function tickBigClock() {
|
||||
if (bigClockState) {
|
||||
const ptime = document.getElementById("bar-clock-time");
|
||||
if (ptime) ptime.textContent = formatCounter(Date.now() - bigClockSince);
|
||||
}
|
||||
if (walkSince) {
|
||||
const wtime = document.getElementById("bar-walk-time");
|
||||
if (wtime) wtime.textContent = formatCounter(Date.now() - walkSince);
|
||||
}
|
||||
// The card follows whichever of the two it is currently showing.
|
||||
if (bigClockState || walkSince) {
|
||||
const time = document.getElementById("bc-time");
|
||||
if (time) time.textContent = formatCounter(Date.now() - (walkSince || bigClockSince));
|
||||
}
|
||||
}
|
||||
|
||||
function renderWindowList(listId, emptyId, windows, ongoingLabel, extraClass) {
|
||||
@@ -4851,6 +4909,13 @@
|
||||
quickLog(state === "asleep" ? "sleep-end" : "sleep-start");
|
||||
});
|
||||
|
||||
document.getElementById("bar-walk").addEventListener("click", () => {
|
||||
// Re-read rather than trusting walkSince: the pill is only shown while a
|
||||
// walk is open, but another device may have ended it since the last render.
|
||||
if (!currentWalkStart(live())) return;
|
||||
quickLog("walk-end");
|
||||
});
|
||||
|
||||
document.querySelectorAll(".chart-days-picker button").forEach(b => {
|
||||
b.addEventListener("click", () => setChartDays(Number(b.dataset.days)));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user