Two test failures from the comparator rewrite:
1. "different unparseable strings → newer" was useful — branch-name
builds (e.g. PackageInfo reports 'main' while server reports
'dev') should still surface the banner so the operator sees that
something is misaligned. Restore that fallback: if both sides
parse to all-zero components (no numeric structure on either),
compare as strings.
2. "honors prerelease ordering" tested pub_semver behavior we no
longer use. Replaced with tests that cover what the new
comparator actually does: zero-padding for length mismatch,
leading-zero normalization, 4-part date+build comparisons, and
month-rollover correctness without leading zeros.
Update banner showing on identical versions:
- pubspec.yaml was stuck at the placeholder 0.1.0+1, so
PackageInfo.version returned "0.1.0" while the server reported the
actual release tag (e.g. "2026.05.10.1"). Comparison correctly said
"newer" → banner always showed.
- Bump pubspec to 2026.05.11.0+1 so the local default matches the
release cadence even before CI overrides it.
- Update flutter.yml release step to pass --build-name="${TAG#v}" so
every tagged APK reports the tag as its PackageInfo.version. Future
releases stop drifting from pubspec.
- Rewrite isVersionNewer to do component-wise int comparison with
zero-padding: pub_semver.Version.parse rejects 4-part date versions
like "2026.05.10.1", at which point the old code fell back to
string inequality and treated "2026.05.10" as newer than itself
vs "2026.05.10.0". Drop the pub_semver import (no longer used).
Lock-screen play/pause not responding:
- PlaybackState only listed MediaAction.seek in systemActions, which
on Android 13+ means tapping the lock-screen play/pause button
doesn't route back to the AudioHandler. Add play, pause,
skipToNext, skipToPrevious to the set.
- Add androidCompactActionIndices: [0, 1, 2] so the compact
notification view explicitly maps the three buttons.
Album art being smaller than the lock-screen frame is upstream of
this commit — the cover-cache writes whatever pixel dimensions the
server returns. If the server's /api/albums/<id>/cover returns small
thumbnails for these albums, the lock screen renders them at that
size. Worth a separate look at the server cover-emit path.
Player bar: drop the volume slider — phone hardware controls volume.
Right cluster collapses to a single row of shuffle/repeat/queue.
Playlist card: GestureDetector defers to children by default, so taps
over the cover image area didn't always register. Add HitTestBehavior.opaque
so the whole card surface is tappable.
Update banner: was 3-4× the height of its text — IconButton's default
48dp tap target plus generous vertical padding dominated. Tighten
padding (8→4), shrink dismiss button to 32×32 with iconSize 16, slim
the action button to 28dp height with shrinkWrap tap target.
Home screen: ClampingScrollPhysics + 140dp bottom padding (already
applied in working tree) so scroll stops just above the mini-player.
The previous fix-forward intended this change but the Edit didn't
make it into the commit (only the Go side landed). Re-applying the
StateProvider → NotifierProvider conversion so flutter analyze stops
failing on the undefined-function for StateProvider.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the operator-facing half of #397. Soft banner mounts at the
top of the shell when the server has a newer APK bundled at
/api/client/version; tap Install → APK downloads to cache → Android
PackageInstaller intent fires → user taps Install in the system
dialog → app restarts on the new version.
### Dart side
- lib/update/update_info.dart — wire shape for /api/client/version
- lib/update/client_update_provider.dart — Riverpod AsyncNotifier
polling on app start + every 24h. Compares semver via pub_semver
with non-semver string-equality fallback. Server 404 = "no update
channel" = silent. Companion shouldShowUpdateBannerProvider gates
on a per-version dismissed-set (in-memory; resets on restart).
- lib/update/installer.dart — UpdateInstaller wraps dio.download +
the MethodChannel call to MainActivity.kt.
- lib/update/update_banner.dart — Material banner with idle /
downloading (with progress) / error stages. Mounted in
_ShellWithPlayerBar above the route content; SizedBox.shrink()
when no update available so non-shell routes (login, server-url)
see no banner anyway.
- isVersionNewer() extracted as a pure function and tested across
semver, prerelease ordering, leading-v normalization, and
non-semver fallback (date-tag style).
### Android side
- AndroidManifest.xml — REQUEST_INSTALL_PACKAGES permission;
FileProvider with authorities ${applicationId}.fileprovider.
- res/xml/file_paths.xml — exposes the cache directory so the
downloaded APK can be served via content:// URI (file:// is
blocked since Android 7).
- MainActivity.kt — MethodChannel handler builds the FileProvider
URI and fires Intent.ACTION_VIEW with FLAG_GRANT_READ_URI_PERMISSION
so the system installer can read across the process boundary.
Phase 3 (CI sequencing — bake APK + version file into /app/client/
during release.yml's tag flow) is the remaining piece. Without it
the server returns 404 from /api/client/version and the banner
silently does nothing — graceful degradation, but no actual updates.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>