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>
158 lines
6.4 KiB
Markdown
158 lines
6.4 KiB
Markdown
# 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`:
|
||
|
||
```kotlin
|
||
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
|
||
|
||
```bash
|
||
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
|
||
|
||
```bash
|
||
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:
|
||
|
||
```bash
|
||
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
|
||
|
||
```bash
|
||
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.
|