Compare commits

...

2 Commits

Author SHA1 Message Date
gituser-vibes
a8bd52718f
Merge pull request #1 from Raasu258/feat/add-favorite-button-android-auto-ui
feat(android-auto): add favorite button to notification UI
2026-09-01 08:17:44 -07:00
Lars
9f0402a7a1 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.
2026-09-01 00:28:20 +02:00
3 changed files with 53 additions and 1 deletions

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M16.5,3c-1.74,0 -3.41,0.81 -4.5,2.09C10.91,3.81 9.24,3 7.5,3 4.42,3 2,5.42 2,8.5c0,3.78 3.4,6.86 8.55,11.54L12,21.35l1.45,-1.32C18.6,15.36 22,12.28 22,8.5 22,5.42 19.58,3 16.5,3zM12.1,18.55l-0.1,0.1 -0.1,-0.1C7.14,14.24 4,11.39 4,8.5 4,6.5 5.5,5 7.5,5c1.54,0 3.04,0.99 3.57,2.36h1.87C13.46,5.99 14.96,5 16.5,5 18.5,5 20,6.5 20,8.5c0,2.89 -3.14,5.74 -7.9,10.05z" />
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M12,21.35l-1.45,-1.32C5.4,15.36 2,12.28 2,8.5 2,5.42 4.42,3 7.5,3c1.74,0 3.41,0.81 4.5,2.09C13.09,3.81 14.76,3 16.5,3 19.58,3 22,5.42 22,8.5c0,3.78 -3.4,6.86 -8.55,11.54L12,21.35z" />
</vector>

View File

@ -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<PlaybackState> _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.