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;