feat(worldclock): add an option to display time with seconds

One switch in Settings > Clock adds seconds to the home readout and
every city row, in the same 12/24-hour and locale form as before, and
gives the analog face its second hand. Off by default.

The tab now ticks on the wall-clock boundary through a new
Ticker.alignedTicks: once a minute with seconds off, once a second only
with them on, replacing the free-running 200 ms tick. The setting is in
backups as world_clock_show_seconds; older files import with it off.

Fixes #23
This commit is contained in:
makiolaj committed 2026-10-21 18:48:15 +02:00
1 parent 3f431a7f32
commit 785adf715b
30 files changed
+455 -65

No files matched your search

+35 -12
View File
@@ -595,7 +595,7 @@ Four slices:
| Slice | Keys | Owner |
|---|---|---|
| Appearance (M1) | `theme_mode`, `dynamic_color` | `AppearancePrefs` in `core-prefs`, the family's shared key names |
| Clock defaults (M2) | `default_snooze_minutes`, `default_snooze_limit`, `default_vibrate`, `default_volume_ramp_seconds`, `default_alarm_ringtone_uri`, `default_timer_ringtone_uri`, `default_dismiss_challenge`, `home_zone_id` | `ClockPrefs`, surfaced as `SettingsPrefs.defaults: Flow<ClockDefaults>` |
| Clock defaults (M2) | `default_snooze_minutes`, `default_snooze_limit`, `default_vibrate`, `default_volume_ramp_seconds`, `default_alarm_ringtone_uri`, `default_timer_ringtone_uri`, `default_dismiss_challenge`, `home_zone_id`, `world_clock_show_seconds` | `ClockPrefs`, surfaced as `SettingsPrefs.defaults: Flow<ClockDefaults>` |
| Stopwatch run record (M2) | `stopwatch_state`, `stopwatch_started_elapsed_millis`, `stopwatch_accumulated_millis`, `stopwatch_last_lap_cumulative_millis` (M7 is the first caller of the last, for the in-progress lap's split) | `StopwatchPrefs`, behind `StopwatchStateStore` |
| System facts (M3) | `last_boot_id` | `SystemPrefs`, behind `BootStateStore`, the persisted half of the reboot gate |
| Timers (M6) | `timer_presets`, `timer_ring_sounding_since_millis` | `TimerPrefs`: the presets surfaced as `SettingsPrefs.timerPresets`, the ring session's anchor behind `TimerRingStateStore` |
@@ -1423,6 +1423,12 @@ immediately and then every period (one second for the pill, since the pill
formats to whole seconds), so a fresh subscriber is never blank. `RealTicker` is
nothing but `delay`, which makes it testable under `runTest`'s virtual clock.
`alignedTicks(unit)` is the same seam for a readout of whole wall-clock units:
after the immediate emission it waits `WallBoundaries.untilNext(now, unit)`,
re-measured against `WallClock` after every wake, so it fires on the second or
minute boundary itself and a long `delay` or a set clock does not accumulate.
The world clock is its one caller (§16, "The cadence").
### The dismiss challenge, and why it cannot strand anyone
`ChallengeGate` is a pure state machine whose single boolean `open` means
@@ -2409,8 +2415,8 @@ ramp to 1 at 07:00, 1 until 17:00, a linear ramp back to 0 at 19:00. It needs no
latitude, it has obvious tests, and the app never claims it is sunrise.
The rest is ordinary `Canvas` work over `AnalogFace`'s pure angles: twelve hour
marks with the four quarters longer, three hands, a centre cap, all in scheme
tokens. The morph is built once and the path re-derived into a reused
marks with the four quarters longer, the hour and minute hands (and a second
hand when seconds are on, below), a centre cap, all in scheme tokens. The morph is built once and the path re-derived into a reused
`Path` per frame through `toPath(Morph, Float, Path)`, which is not
`@Composable` and is therefore callable from a `DrawScope`. M8 left the digital
time out from under the face on the grounds that an analog face that also
@@ -2426,15 +2432,32 @@ refused for the live pill (§12).
### The cadence
`WorldClockDefaults.Tick = 200 ms`. `RealTicker` is a plain `delay` loop with no
alignment to the wall second, so a one-second cadence would leave the second
hand up to a full second behind the device clock, which is visible and not
something a clock app ships. The cost is bounded by making the state **stable**:
`HomeFaceState.time` is truncated to the second, `WorldClockRowState.time` to
the minute, and the flow is `distinctUntilChanged`. The upstream emits five
times a second, a new state is produced about once a second, and the rows' own
values change once a minute. The period is a `Ticker` parameter, never a `delay`
at a call site (§12).
**Display time with seconds** (Settings > Clock, `world_clock_show_seconds`,
off by default, #23) decides how often the tab wakes. Off, the face has an hour
and a minute hand, the readout under it and every row show hours and minutes,
and the tab ticks **once a minute**. On, the readout and the rows add seconds
in the same 12/24-hour and locale form (`HH:mm:ss`, or the locale's medium
time), the face gains its second hand, and the tab ticks **once a second**.
The second hand belongs to the switch rather than being always there: a hand
that steps every second is exactly what makes the tab wake every second, and
the point of the switch is that nobody pays for that without asking.
Both cadences are `Ticker.alignedTicks` (§12), `WorldClockDefaults.MinuteTick`
or `SecondTick`, so a tick lands on the wall-clock boundary itself rather than
up to a period late; a free-running one-second `delay` could leave the second
hand a full second behind the device clock. This replaces M8's 200 ms
free-running tick, which got within a fifth of a second only by waking five
times as often as anything changed. `HomeFaceState.time` and
`WorldClockRowState.time` are truncated to the same unit the tab ticks in, and
the flow stays `distinctUntilChanged`, which now only absorbs the odd early
wake. The setting is read with the stored list and the home zone, so flipping
it switches the cadence at once through the same `flatMapLatest`.
`TIME_SET` and `TIMEZONE_CHANGED` still self-heal within one tick, which with
seconds off is now up to a minute. That is only reachable by changing the
clock in system settings and returning within the five-second
`WhileSubscribed` grace; any longer and the flow restarts and reads the new
time at once.
### Twenty-four cities, and a reorder a screen reader can reach