diff --git a/android/app/src/main/res/drawable/ic_favorite.xml b/android/app/src/main/res/drawable/ic_favorite.xml
new file mode 100644
index 00000000..e3ad0ea0
--- /dev/null
+++ b/android/app/src/main/res/drawable/ic_favorite.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/android/app/src/main/res/drawable/ic_favorite_on.xml b/android/app/src/main/res/drawable/ic_favorite_on.xml
new file mode 100644
index 00000000..54c45ad2
--- /dev/null
+++ b/android/app/src/main/res/drawable/ic_favorite_on.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/lib/services/audio_services/mobile_audio_service.dart b/lib/services/audio_services/mobile_audio_service.dart
index 491b1622..35c000ae 100644
--- a/lib/services/audio_services/mobile_audio_service.dart
+++ b/lib/services/audio_services/mobile_audio_service.dart
@@ -7,6 +7,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/audio_player/state.dart';
+import 'package:spotube/provider/metadata_plugin/library/tracks.dart';
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
import 'package:spotube/services/audio_player/audio_player.dart';
import 'package:spotube/services/metadata/metadata.dart';
@@ -49,6 +50,9 @@ class MobileAudioService extends BaseAudioHandler {
/// Name of the custom "shuffle" media control action.
static const String _shuffleActionName = 'shuffle';
+ /// Name of the custom "favorite" media control action.
+ static const String _favoriteActionName = 'favorite';
+
// ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member
AudioPlayerState get playlist => audioPlayerNotifier.state;
@@ -143,6 +147,22 @@ class MobileAudioService extends BaseAudioHandler {
playbackState.add(await _transformEvent());
return null;
}
+ if (name == _favoriteActionName) {
+ final track = playlist.activeTrack;
+ if (track != null) {
+ final isLiked =
+ await ref.read(metadataPluginIsSavedTrackProvider(track.id).future);
+ final notifier = ref.read(metadataPluginSavedTracksProvider.notifier);
+ if (isLiked) {
+ await notifier.removeFavorite([track]);
+ } else {
+ await notifier.addFavorite([track]);
+ }
+ // Refresh playback state so the control's icon reflects the new state.
+ playbackState.add(await _transformEvent());
+ }
+ return null;
+ }
return super.customAction(name, extras);
}
@@ -534,12 +554,26 @@ class MobileAudioService extends BaseAudioHandler {
Future _transformEvent() async {
try {
+ final activeTrackId = playlist.activeTrack?.id;
+ final favoriteAsync = activeTrackId != null
+ ? ref.read(metadataPluginIsSavedTrackProvider(activeTrackId))
+ : null;
+ final isFavorite = favoriteAsync?.valueOrNull ?? false;
+
return PlaybackState(
controls: [
MediaControl.skipToPrevious,
audioPlayer.isPlaying ? MediaControl.pause : MediaControl.play,
MediaControl.skipToNext,
- MediaControl.stop,
+ // Toggles the current track's Liked status instead of the less
+ // useful "stop". audio_service doesn't expose a standard favorite
+ // action, so this is a tappable custom control, same as shuffle.
+ MediaControl.custom(
+ androidIcon:
+ isFavorite ? 'drawable/ic_favorite_on' : 'drawable/ic_favorite',
+ label: 'Favorite',
+ name: _favoriteActionName,
+ ),
// Explicit shuffle toggle. audio_service always advertises the
// standard ACTION_SET_SHUFFLE_MODE, but Android Auto doesn't reliably
// render a toggle from it, so we expose a tappable custom control.