From 9f0402a7a1c61f482ec65576e5278fee54c46d86 Mon Sep 17 00:00:00 2001 From: Lars <129963861+Raasu258@users.noreply.github.com> Date: Tue, 1 Sep 2026 00:24:06 +0200 Subject: [PATCH] feat(android-auto): add favorite button to notification UI Adds a tappable 'favorite' custom media control to the Android Auto notification, replacing the less useful stop control (skip/pause/play/ skip already cover playback stop). audio_service has no built-in favorite action, so this follows the same custom-control pattern already used for shuffle: a custom action name is wired up in customAction(), and _transformEvent() swaps the control's icon (ic_favorite / ic_favorite_on) based on whether the active track is currently saved, via the metadata plugin's saved- tracks provider. --- .../app/src/main/res/drawable/ic_favorite.xml | 9 +++++ .../src/main/res/drawable/ic_favorite_on.xml | 9 +++++ .../audio_services/mobile_audio_service.dart | 36 ++++++++++++++++++- 3 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 android/app/src/main/res/drawable/ic_favorite.xml create mode 100644 android/app/src/main/res/drawable/ic_favorite_on.xml 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.