diff --git a/flutter_client/android/app/src/main/res/drawable/ic_stat_favorite.xml b/flutter_client/android/app/src/main/res/drawable/ic_stat_favorite.xml new file mode 100644 index 00000000..51cd65ac --- /dev/null +++ b/flutter_client/android/app/src/main/res/drawable/ic_stat_favorite.xml @@ -0,0 +1,10 @@ + + + diff --git a/flutter_client/android/app/src/main/res/drawable/ic_stat_favorite_border.xml b/flutter_client/android/app/src/main/res/drawable/ic_stat_favorite_border.xml new file mode 100644 index 00000000..390502df --- /dev/null +++ b/flutter_client/android/app/src/main/res/drawable/ic_stat_favorite_border.xml @@ -0,0 +1,10 @@ + + + diff --git a/flutter_client/lib/player/audio_handler.dart b/flutter_client/lib/player/audio_handler.dart index 8e60c65d..ce3a7df8 100644 --- a/flutter_client/lib/player/audio_handler.dart +++ b/flutter_client/lib/player/audio_handler.dart @@ -637,6 +637,12 @@ class MinstrelAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandl mediaItem.add(media.copyWith(rating: Rating.newHeartRating(liked))); } + /// Re-broadcasts PlaybackState so the notification favorite control's + /// icon/label reflects a like toggled from elsewhere (TrackRow, kebab, + /// another device via SSE). Sibling to refreshCurrentRating, which + /// updates the Wear/lock heart via MediaItem.rating. + void refreshFavoriteControl() => _broadcastState(null); + @override Future setShuffleMode(AudioServiceShuffleMode shuffleMode) async { await _player @@ -661,16 +667,56 @@ class MinstrelAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandl await _player.setVolume(v.clamp(0.0, 1.0)); } + /// Handles the notification favorite control (and any future custom + /// actions). Toggles the current track's like via the LikeBridge, + /// then re-broadcasts so the heart icon/label flips. Signature + /// matches AudioHandler.customAction (Future). + @override + Future customAction(String name, + [Map? extras]) async { + if (name == 'minstrel.favorite') { + final media = mediaItem.value; + final bridge = _likeBridge; + if (media == null || bridge == null) return null; + try { + await bridge.toggleTrackLike(media.id); + } catch (_) {} + _broadcastState(null); + return null; + } + return super.customAction(name, extras); + } + // _broadcastState accepts a nullable event because the shuffle/repeat // listeners don't have one — we just want to re-emit PlaybackState // with up-to-date shuffleMode/repeatMode fields. void _broadcastState(PlaybackEvent? event) { final playing = _player.playing; + // Favorite control: only when a track is playing and the LikeBridge + // is wired. Icon/label toggle by current like state; tapping it + // routes to customAction('minstrel.favorite'). Implemented as a + // MediaControl.custom (NOT setRating — that path is broken upstream, + // audio_service #376, and regressed the Pixel Watch). Kept out of + // androidCompactActionIndices so the compact/lock view is unchanged. + final favTrackId = mediaItem.value?.id; + final favBridge = _likeBridge; + final showFav = favTrackId != null && favBridge != null; + final favLiked = favTrackId != null && + favBridge != null && + favBridge.isTrackLiked(favTrackId); playbackState.add(PlaybackState( controls: [ MediaControl.skipToPrevious, if (playing) MediaControl.pause else MediaControl.play, MediaControl.skipToNext, + if (showFav) + MediaControl.custom( + androidIcon: favLiked + ? 'drawable/ic_stat_favorite' + : 'drawable/ic_stat_favorite_border', + label: favLiked ? 'Unfavorite' : 'Favorite', + name: 'minstrel.favorite', + ), ], // androidCompactActionIndices tells the system which controls // appear in the collapsed/lock-screen view. Without this, some diff --git a/flutter_client/lib/player/player_provider.dart b/flutter_client/lib/player/player_provider.dart index 91a3e77b..8a8be4db 100644 --- a/flutter_client/lib/player/player_provider.dart +++ b/flutter_client/lib/player/player_provider.dart @@ -62,7 +62,9 @@ class PlayerActions { // kebab menu, or another logged-in device propagating via SSE. _ref.listen>(likedIdsProvider, (_, next) { if (next.value == null) return; - _ref.read(audioHandlerProvider).refreshCurrentRating(); + _ref.read(audioHandlerProvider) + ..refreshCurrentRating() + ..refreshFavoriteControl(); }); } final Ref _ref;