diff --git a/.forgejo/workflows/flutter.yml b/.forgejo/workflows/flutter.yml index c9b1eff8..0ff1066f 100644 --- a/.forgejo/workflows/flutter.yml +++ b/.forgejo/workflows/flutter.yml @@ -74,7 +74,15 @@ jobs: - name: Build release APK if: startsWith(github.ref, 'refs/tags/v') - run: flutter build apk --release + # Inject the tag (sans leading 'v') as the build's --build-name + # so PackageInfo.version matches what /api/client/version + # reports — otherwise the in-app update banner would always + # think the user is behind because pubspec.yaml's static + # version drifts from the actual release tag. + shell: bash + run: | + TAG="${GITHUB_REF#refs/tags/v}" + flutter build apk --release --build-name="${TAG}" - name: Upload debug APK artifact if: github.event_name == 'push' && !startsWith(github.ref, 'refs/tags/') diff --git a/flutter_client/lib/player/audio_handler.dart b/flutter_client/lib/player/audio_handler.dart index 945ac971..d859769f 100644 --- a/flutter_client/lib/player/audio_handler.dart +++ b/flutter_client/lib/player/audio_handler.dart @@ -250,7 +250,20 @@ class MinstrelAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandl if (playing) MediaControl.pause else MediaControl.play, MediaControl.skipToNext, ], - systemActions: const {MediaAction.seek}, + // androidCompactActionIndices tells the system which controls + // appear in the collapsed/lock-screen view. Without this, some + // Android versions render the player without working buttons. + androidCompactActionIndices: const [0, 1, 2], + // systemActions enumerates which actions the system can invoke + // on us — without play/pause/skip in here, taps on lock-screen + // controls don't route back to the handler on Android 13+. + systemActions: const { + MediaAction.play, + MediaAction.pause, + MediaAction.skipToNext, + MediaAction.skipToPrevious, + MediaAction.seek, + }, processingState: switch (_player.processingState) { ProcessingState.idle => AudioProcessingState.idle, ProcessingState.loading => AudioProcessingState.loading, diff --git a/flutter_client/lib/update/client_update_provider.dart b/flutter_client/lib/update/client_update_provider.dart index 940f9333..893c4879 100644 --- a/flutter_client/lib/update/client_update_provider.dart +++ b/flutter_client/lib/update/client_update_provider.dart @@ -3,7 +3,6 @@ import 'dart:async'; import 'package:dio/dio.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:package_info_plus/package_info_plus.dart'; -import 'package:pub_semver/pub_semver.dart'; import '../library/library_providers.dart' show dioProvider; import 'installer.dart'; @@ -50,20 +49,36 @@ class ClientUpdateController extends AsyncNotifier { } /// True when `serverVersion` is strictly newer than `installedVersion`. -/// Both strings are normalized (leading 'v' stripped) and parsed as -/// semver. On parse failure, falls back to string inequality (treats -/// any difference as "newer" — operator can dismiss if wrong). +/// +/// Comparison strategy: split both strings on `.`, parse each component +/// as an int (non-numeric = 0), pad the shorter list with zeros, then +/// compare component-wise. This handles our date-style versions +/// (2026.05.10.1) which exceed the 3-part semver shape that +/// `pub_semver.Version.parse` accepts, and treats "2026.05.10" as +/// equal to "2026.05.10.0" rather than "different = newer". +/// +/// Falls back to pub_semver as a secondary attempt when the components +/// look semver-like (handles pre-release suffixes, build metadata, etc). /// /// Exposed for testing; the polling logic in ClientUpdateController /// is the only production caller. bool isVersionNewer(String serverVersion, String installedVersion) { - final svr = serverVersion.replaceFirst(RegExp(r'^v'), ''); - final ins = installedVersion.replaceFirst(RegExp(r'^v'), ''); - try { - return Version.parse(svr) > Version.parse(ins); - } catch (_) { - return svr != ins; + List parts(String v) => v + .replaceFirst(RegExp(r'^v'), '') + .split('.') + .map((p) => int.tryParse(p) ?? 0) + .toList(); + + final svr = parts(serverVersion); + final ins = parts(installedVersion); + final n = svr.length > ins.length ? svr.length : ins.length; + while (svr.length < n) svr.add(0); + while (ins.length < n) ins.add(0); + for (var i = 0; i < n; i++) { + if (svr[i] > ins[i]) return true; + if (svr[i] < ins[i]) return false; } + return false; } final clientUpdateProvider = diff --git a/flutter_client/pubspec.yaml b/flutter_client/pubspec.yaml index 8ba9d235..b7eecf6c 100644 --- a/flutter_client/pubspec.yaml +++ b/flutter_client/pubspec.yaml @@ -1,7 +1,7 @@ name: minstrel description: Minstrel mobile client publish_to: 'none' -version: 0.1.0+1 +version: 2026.05.11.0+1 environment: sdk: '>=3.5.0 <4.0.0'