Files
bgclock/RELEASING.md
T
hansdezwartandClaude Opus 5 ec1a65c89c Reset by holding the button, not by tapping it twice
A test user found the reset button, tapped it once and gave up. That is what
the old feedback invited: a first tap brightened the icon and started a ring
draining over 2s, and a draining ring reads as "wait" or "loading", not "press
me again". The one thing that would have said otherwise — a word — is not
available here, because the bar is shared by two players sitting opposite each
other and every word in this app lives inside a panel that rotates to face one
of them.

"Tap this twice" has no wordless vocabulary. "Keep holding" has a very
well-worn one, so reset is now a 975ms hold: a dim track ring appears whole the
moment the finger lands, a bright arc fills over it from 12 o'clock, and
letting go early makes the arc retreat. The retreat is the instruction. A stray
hold is also far less likely than two stray taps inside 2s, so the live game
this was guarding is guarded better than before.

Three things that look incidental and are not:

- Hiding the ring is an opacity that waits out the retreat, not display:none,
  which cut the retreat off at the instant of release — invisible, and it was
  the whole point. The fade has a real duration because a 0s transition with a
  delay may be treated as no transition at all, taking the delay with it.
- width:auto on the ring: .btn svg sets a width for the bar icons, and
  inheriting it against the ring's new height draws an ellipse.
- The ring's transition is exempted from the blanket prefers-reduced-motion
  rule. Collapsing it would fill the ring the instant you touched the button
  and claim the reset was done 975ms before it was. It reports state.

Keyboard, switch and assistive activation cannot hold, and hold-only would have
left those users unable to reset at all, one release after an accessibility
pass. Clicks arriving with detail 0 — no pointer behind them — keep the old
two-presses-within-2s and the draining ring.

Success now sounds: the flag's falling triad, rising instead. Deliberately not
another rising fifth, which sndSwap owns and plays on every handover.

Also corrects a claim in RELEASING.md: the screenshots reproduce near-exactly,
not exactly. Two runs of identical code differ by a 4x13 pixel sliver where the
delay bar's fill edge lands mid-pixel, so a non-empty git diff after re-running
proves nothing on its own.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 14:51:06 +02:00

6.4 KiB
Raw Blame History

Releasing a new version

The clock lives in two places: the web app in public_html/, which you host yourself, and the Android app on F-Droid, which is built from a git tag.

Everything F-Droid needs comes from a tag in this repository. After the first release was accepted there is nothing to do in F-Droid's own repo — their bot watches the tags here and writes the build entry itself. Don't open a merge request against fdroiddata for a new version.

Worked example below bumps 1.0 to 1.1. Substitute your own numbers.

1. Make the change

Both versions share one copy of the app. Edit public_html/index.html and the Android build picks it up automatically — the Gradle build reads public_html/ directly rather than keeping a second copy, so the two can't drift.

If you changed anything visible, bump the service worker cache or installed web users will keep the old page:

# public_html/sw.js
const CACHE = "bgclock-v18";     # any new value; it only has to differ

2. Bump the version

In android/app/build.gradle.kts:

versionCode = 2          // an integer, +1 every release, never reused
versionName = "1.1"      // what people see

versionCode is what Android uses to decide one version is newer than another. It must go up every single release, even for a one-character fix.

3. Write the changelog

A new file named after the versionCode, not the version name:

fastlane/metadata/android/en-US/changelogs/2.txt

Maximum 500 characters. This is what shows up in F-Droid's "What's New".

4. Build it and put it on your phone

export ANDROID_HOME="$HOME/Android/Sdk"     # already in ~/.bashrc
cd android
./gradlew assembleDebug
adb install -r app/build/outputs/apk/debug/app-debug.apk
./gradlew --stop                            # the daemon holds ~500 MB otherwise

Play a real game before tagging. Worth checking specifically: start a match, make some moves, force-quit from the task switcher and reopen — everything should come back, paused. That is the check that proves the app's storage is working, and it fails silently rather than loudly.

5. Commit, tag, push

git add -A
git commit -m "…"
git tag -a v1.1 -m "Backgammon Clock 1.1"
git push
git push origin v1.1

Keep tags in the v1.1 shape for consistency, but nothing depends on it: UpdateCheckMode: Tags with no pattern matches every tag whatever it's called, and F-Droid's bot resolves whichever tag it finds to a commit hash before it writes the build entry.

Never move or delete a tag that has been pushed — F-Droid may already have built it. If a release is wrong, bump the version and release again. (The recipe pins a hash, so a moved tag can't retroactively change a version they already built; it would just leave your repo disagreeing with what's on people's phones.)

If you ever write a build entry by hand, its commit: must be a full 40-char commit hash — never a tag, never a branch. Tags are mutable, so a tag there means the thing F-Droid builds and signs can change under them, and they reject it on sight. This came up on the very first submission and is easy to get wrong, because fdroid lint doesn't check it under UpdateCheckMode: Tags — the pipeline goes green and a human catches it days later. Get the hash with:

git rev-parse v1.1^{commit}      # ^{commit}, or an annotated tag gives you the
                                 # tag object's hash instead of the commit's

This doesn't come up in a normal release: the bot writes the entry, and the bot writes hashes.

6. Wait

Their bot notices the new tag, adds a build entry, and the build server picks it up on its next cycle. Expect a day or two. Nothing to do.

Deploying the web version

Separate from all of the above, and manual: upload the six files in public_html/index.html, manifest.webmanifest, sw.js, icon-192.png, icon-512.png, icon-maskable-512.png — to the web host. README.md and RELEASING.md stay out of it; they live in the repo root for that reason.

If the screenshots need redoing

python3 tools/screenshots.py

That rewrites all five in fastlane/metadata/android/en-US/images/phoneScreenshots/ at 1170×2532. They are rendered from public_html/index.html with headless Chromium rather than taken on a phone, so anything that changes the look of the app is a reason to re-run it.

Near-exact, not exact. Two runs of identical code differ by a few bytes in shots 1 and 4: a 4×13 pixel sliver at the delay bar's fill edge, where the scaleX boundary lands mid-pixel and the rasteriser rounds it differently from one run to the next. Max channel difference 11, invisible. So a git diff after re-running is not evidence that anything changed — compare the pixels before believing it, and git checkout the files back if that sliver is all it is.

The script sets each scene by seeding localStorage before the app boots, so the app renders its own saved state rather than having the DOM poked from outside. Editing a scene means editing the SCENES table at the top.

Three traps, all of which fail quietly, and all of which the script already handles — they are written down here because they cost an afternoon each:

  • The capture lands whenever the virtual-time budget runs out, not when your script finishes — so freeze the render loop (requestAnimationFrame = () => 0) once the frame you want is on screen.
  • CSS transitions don't advance under --virtual-time-budget, so a panel caught mid-transition photographs in its old colour. Disable transitions in the screenshot build.
  • Snap-confined Chromium cannot write into hidden directories — not ~/.cache, not any dot-directory, not the private /tmp it is handed. It fails with a bare "Permission denied" and no hint as to why. Both the HTML it reads and the PNG it writes have to sit in a plainly-named directory.

Reference

  • App ID: nl.hansdezwart.bgclock — fixed forever; changing it makes a new app.
  • F-Droid listing text: fastlane/metadata/android/en-US/ (title ≤50 chars, summary ≤80, description ≤4000, changelog ≤500). Only these HTML tags work in the description: b big blockquote br cite em i li ol small strike strong sub sup tt u ul.
  • The original submission: https://gitlab.com/fdroid/fdroiddata/-/merge_requests/45478
  • F-Droid signs the APK with their key, not yours. Reproducible builds were declined at submission and can't be enabled later for this app ID.