From 7573d360bfe65a939d50e08f04e6881c0ebaeeba Mon Sep 17 00:00:00 2001 From: zarzet Date: Tue, 14 Jul 2026 08:06:36 +0700 Subject: [PATCH] feat(player): swipe the mini player away to stop playback Horizontal Dismissible on the bar; stop() now also clears the current media item so the bar (and any listener of it) sees the session end. --- lib/services/music_player_service.dart | 2 + lib/widgets/mini_player.dart | 178 +++++++++++++++---------- 2 files changed, 106 insertions(+), 74 deletions(-) diff --git a/lib/services/music_player_service.dart b/lib/services/music_player_service.dart index 4ae29da2..8f467277 100644 --- a/lib/services/music_player_service.dart +++ b/lib/services/music_player_service.dart @@ -580,6 +580,8 @@ class MusicPlayerHandler extends BaseAudioHandler _userPaused = false; _recent.clear(); _playHistory.clear(); + // A stopped session has no current item; this also hides the mini player. + mediaItem.add(null); _broadcastState(playerState: PlayerState.stopped); await super.stop(); } diff --git a/lib/widgets/mini_player.dart b/lib/widgets/mini_player.dart index 7498fb66..0f16362e 100644 --- a/lib/widgets/mini_player.dart +++ b/lib/widgets/mini_player.dart @@ -5,16 +5,30 @@ import 'package:spotiflac_android/screens/now_playing_screen.dart'; import 'package:spotiflac_android/widgets/player_artwork.dart'; import 'package:spotiflac_android/widgets/settings_group.dart'; -class MiniPlayer extends ConsumerWidget { +class MiniPlayer extends ConsumerStatefulWidget { const MiniPlayer({super.key}); @override - Widget build(BuildContext context, WidgetRef ref) { + ConsumerState createState() => _MiniPlayerState(); +} + +class _MiniPlayerState extends ConsumerState { + // Hides the bar in the frames between a swipe-dismiss and the stopped + // service clearing the media item (a dismissed Dismissible must leave the + // tree immediately). Playing the same track again shows the bar normally. + String? _dismissedItemId; + + @override + Widget build(BuildContext context) { final mediaItem = ref.watch(currentMediaItemProvider).value; if (mediaItem == null) return const SizedBox.shrink(); final playback = ref.watch(playbackStateProvider).value; final isPlaying = playback?.playing ?? false; + if (mediaItem.id == _dismissedItemId && !isPlaying) { + return const SizedBox.shrink(); + } + final controller = ref.read(musicPlayerControllerProvider); final colorScheme = Theme.of(context).colorScheme; @@ -22,87 +36,103 @@ class MiniPlayer extends ConsumerWidget { final position = playback?.position.inMilliseconds ?? 0; final progress = duration > 0 ? (position / duration).clamp(0.0, 1.0) : 0.0; - return DecoratedBox( - position: DecorationPosition.foreground, - decoration: BoxDecoration( - border: Border( - top: BorderSide( - color: colorScheme.outlineVariant.withValues(alpha: 0.5), + return Dismissible( + key: ValueKey('mini-player-${mediaItem.id}'), + direction: DismissDirection.horizontal, + onDismissed: (_) { + setState(() => _dismissedItemId = mediaItem.id); + controller.stop(); + }, + child: DecoratedBox( + position: DecorationPosition.foreground, + decoration: BoxDecoration( + border: Border( + top: BorderSide( + color: colorScheme.outlineVariant.withValues(alpha: 0.5), + ), ), ), - ), - child: Material( - color: settingsGroupColor(context).withValues(alpha: 0.72), - child: InkWell( - onTap: () { - Navigator.of(context, rootNavigator: true).push(NowPlayingRoute()); - }, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - LinearProgressIndicator( - value: progress, - minHeight: 2, - backgroundColor: colorScheme.surfaceContainerHighest, - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - child: Row( - children: [ - Hero( - tag: kNowPlayingArtworkHeroTag, - child: ClipRRect( - borderRadius: BorderRadius.circular(6), - child: SizedBox( - width: 44, - height: 44, - child: PlayerArtwork( - artUri: mediaItem.artUri?.toString(), - colorScheme: colorScheme, - cacheWidth: 132, - iconSize: 22, + child: Material( + color: settingsGroupColor(context).withValues(alpha: 0.72), + child: InkWell( + onTap: () { + Navigator.of( + context, + rootNavigator: true, + ).push(NowPlayingRoute()); + }, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + LinearProgressIndicator( + value: progress, + minHeight: 2, + backgroundColor: colorScheme.surfaceContainerHighest, + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + child: Row( + children: [ + Hero( + tag: kNowPlayingArtworkHeroTag, + child: ClipRRect( + borderRadius: BorderRadius.circular(6), + child: SizedBox( + width: 44, + height: 44, + child: PlayerArtwork( + artUri: mediaItem.artUri?.toString(), + colorScheme: colorScheme, + cacheWidth: 132, + iconSize: 22, + ), + ), ), ), - ), - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - mediaItem.title, - style: Theme.of(context).textTheme.titleSmall - ?.copyWith(fontWeight: FontWeight.w600), - maxLines: 1, - overflow: TextOverflow.ellipsis, + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + mediaItem.title, + style: Theme.of(context).textTheme.titleSmall + ?.copyWith(fontWeight: FontWeight.w600), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + Text( + mediaItem.artist ?? '', + style: Theme.of(context).textTheme.bodySmall + ?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], ), - Text( - mediaItem.artist ?? '', - style: Theme.of(context).textTheme.bodySmall - ?.copyWith(color: colorScheme.onSurfaceVariant), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ], - ), + ), + IconButton( + icon: Icon(isPlaying ? Icons.pause : Icons.play_arrow), + onPressed: () => controller.togglePlayPause(isPlaying), + ), + IconButton( + icon: const Icon(Icons.skip_next), + onPressed: controller.next, + ), + ], ), - IconButton( - icon: Icon(isPlaying ? Icons.pause : Icons.play_arrow), - onPressed: () => controller.togglePlayPause(isPlaying), - ), - IconButton( - icon: const Icon(Icons.skip_next), - onPressed: controller.next, - ), - ], - ), + ), + ], ), - ], + ), ), ), - ), ); } }