Commit Graph
3 Commits
Author SHA1 Message Date
hansdezwartandClaude Opus 5 3a7eb8c612 Save the match every second instead of waiting to be told
A force-quit deep into a turn came back at the clock's last-turn-switch value,
losing the whole interrupted turn. The tell was the delay counter returning at
a full 12: the newest saved state was the one tap() writes, and tap() sets
turnAt and calls saveGame() in the same breath, so it carries held ~ 0.
Restoring it reproduces the moment of the turn switch exactly.

The two triggers meant to save at quit time — visibilitychange and pagehide —
do not fire in Android's WebView. MainActivity's onPause() forwards to
web.onPause() under a comment claiming that is what makes visibilitychange
fire. It isn't: document visibility follows the WebView's view and window
visibility, not that call. The forwarding is still right for stopping timers
and audio, but the saved match should never have depended on it.

This is not new. It has almost certainly been broken since 1.0, and the release
check could not catch it, because for the first 12 seconds of a turn the delay
absorbs everything and render() never touches st.reserve. A clock that has lost
its place is indistinguishable from one that is correctly sitting still, so
every force-quit test that quit soon after a turn change passed. RELEASING.md
now says to let the turn run 40 seconds first, which is the only version of
that check worth running.

So the page stops waiting to be told. A timer writes the match once a second
while a clock is running — a timer rather than a hook in loop(), because
requestAnimationFrame stops when the page is hidden, which is exactly when this
matters. blur joins the listeners too: a different signal, and one that does
fire when the task switcher opens. At most a second can now be lost, which is
well inside the delay nobody is charged for anyway.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 15:07:18 +02:00
hansdezwartandClaude Opus 5 862b029953 Keep the navigation bar; hide only the status bar
hideSystemBars() asked for systemBars(), which is the union of the two, so
the way out of the app was an invisible swipe you had to remember.
statusBars() leaves the navigation bar where it is: still fullscreen at the
top, but leaving is now an affordance you can see. On three-button
navigation it also owns the bottom strip outright, so those taps never
reach the panel at all — the lift-not-press rule still carries the gesture
case, where the pill is only drawn over the app.

The pre-Android-11 branch loses HIDE_NAVIGATION, LAYOUT_HIDE_NAVIGATION and
IMMERSIVE_STICKY along with it; immersive only governs a navigation bar
that is hidden, and this one isn't.

The layout needed nothing: .panel--bottom .inner already pads by
max(14px, env(safe-area-inset-bottom)), so the readout lifts clear of the
bar by itself. Confirmed on device.

Not tagged. This rides along with whatever else goes into 1.1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 22:07:24 +02:00
hansdezwartandClaude Opus 5 9f2081a0c9 Add the Android app: a WebView around public_html
A single Activity holding one WebView, in Java rather than Kotlin — for
~120 lines of setup Kotlin would only add its Gradle plugin, its stdlib
in the APK and a third version to keep aligned with Gradle and AGP. One
dependency: androidx.webkit 1.16.0.

The assets are served from https://appassets.androidplatform.net through
WebViewAssetLoader rather than file://. That is the part that matters: an
https origin is a secure context, and localStorage — which holds every
setting and the match in progress — is blocked on file://, so a saved
game would silently vanish. Confirmed on device: force-quit mid-match and
everything comes back.

public_html stays the only copy of the web app. A task syncs it into the
assets at build time, so the browser version and the app version cannot
drift; the APK's index.html hashes identical to the working tree. sw.js
is left out on purpose — it is cache-first, and a service-worker cache
outlives an app upgrade, so it would serve a stale index.html with no way
to clear it. The APK is the cache now.

Three web APIs don't exist in a WebView, so the Activity supplies them:
FLAG_KEEP_SCREEN_ON for navigator.wakeLock, native immersive mode for
requestFullscreen(), and LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES so the
panels' env(safe-area-inset-*) has something to report. textZoom is
pinned to 100: the layout is built on clamp() and vw, and a large system
font would scale the text inside it.

The manifest declares no permissions whatsoever.

Wiring note: AGP 9 refuses a Provider in sourceSets.assets.srcDir, since
it can't tell generated from static directories. The sync is a task with
a DirectoryProperty output, attached via androidComponents.onVariants,
which is the supported path and carries the task dependency.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 19:10:38 +02:00