import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart' show CupertinoIcons; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/providers/music_player_provider.dart'; import 'package:spotiflac_android/providers/player_motion_artwork_provider.dart'; import 'package:spotiflac_android/providers/player_artwork_video_provider.dart'; import 'package:spotiflac_android/providers/runtime_profile_provider.dart'; import 'package:spotiflac_android/theme/mornye_theme.dart'; import 'package:spotiflac_android/screens/now_playing_screen.dart'; import 'package:spotiflac_android/utils/string_utils.dart'; import 'package:spotiflac_android/widgets/audio_quality_badges.dart'; import 'package:spotiflac_android/widgets/player_artwork.dart'; import 'package:spotiflac_android/widgets/settings_group.dart'; import 'package:spotiflac_android/widgets/mornye_chrome.dart'; class MiniPlayer extends ConsumerStatefulWidget { const MiniPlayer({super.key, this.compact = false, this.bottomPadding = 8}); final bool compact; final double bottomPadding; @override 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 isPlaying = ref.watch(playbackPlayingProvider); final isLoading = ref.watch(playbackLoadingProvider); if (mediaItem.id == _dismissedItemId && !isPlaying) { return const SizedBox.shrink(); } final controller = ref.read(musicPlayerControllerProvider); final colorScheme = Theme.of(context).colorScheme; final mornye = context.isMornye; final compact = mornye && widget.compact; if (mornye && !MediaQuery.disableAnimationsOf(context)) { final artwork = ref .watch( playerMotionArtworkProvider(( album: mediaItem.album ?? '', artist: mediaItem.artist ?? '', )), ) .value; if (artwork != null) { ref.watch(playerArtworkVideoProvider(artwork.source)); } } final player = 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: mornye ? null : Border( top: BorderSide( color: colorScheme.outlineVariant.withValues(alpha: 0.5), ), ), ), child: Material( color: mornye ? Colors.transparent : settingsGroupColor(context).withValues(alpha: 0.72), child: InkWell( onTap: () { Navigator.of( context, rootNavigator: true, ).push(NowPlayingRoute()); }, child: Column( mainAxisSize: MainAxisSize.min, children: [ if (!mornye) _MiniPlayerProgress( duration: mediaItem.duration ?? Duration.zero, backgroundColor: colorScheme.surfaceContainerHighest, ), Padding( padding: EdgeInsets.symmetric( horizontal: compact ? 8 : 12, vertical: mornye ? 0 : 8, ), child: Row( children: [ Hero( tag: kNowPlayingArtworkHeroTag, child: ClipRRect( borderRadius: BorderRadius.circular(6), child: SizedBox( width: mornye ? 38 : 44, height: mornye ? 38 : 44, child: PlayerArtwork( artUri: mediaItem.artUri?.toString(), colorScheme: colorScheme, cacheWidth: 132, iconSize: 22, ), ), ), ), SizedBox(width: compact ? 8 : 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ ExplicitTrackTitle( title: mediaItem.title, explicit: parseExplicitFlag( mediaItem.extras?['explicit'], ) == true, 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: mornye ? Color.lerp( colorScheme.onSurfaceVariant, colorScheme.onSurface, colorScheme.brightness == Brightness.dark ? 0.7 : 0.6, ) : colorScheme.onSurfaceVariant, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ), ), IconButton( color: mornye ? colorScheme.onSurface : null, tooltip: isPlaying ? context.l10n.actionPause : context.l10n.tooltipPlay, icon: isLoading ? const SizedBox.square( dimension: 20, child: CircularProgressIndicator( strokeWidth: 2.5, ), ) : Icon( mornye ? (isPlaying ? CupertinoIcons.pause_fill : CupertinoIcons.play_fill) : (isPlaying ? Icons.pause : Icons.play_arrow), ), onPressed: isLoading ? null : () => controller.togglePlayPause(isPlaying), ), if (!compact) IconButton( color: mornye ? colorScheme.onSurface : null, tooltip: context.l10n.nowPlayingNextTrack, icon: Icon( mornye ? CupertinoIcons.forward_fill : Icons.skip_next, ), onPressed: controller.next, ), ], ), ), ], ), ), ), ), ); return mornye ? Padding( padding: EdgeInsets.only(bottom: widget.bottomPadding), child: MornyeGlass.navigation( strongTint: true, tintOpacity: MornyeTheme.chromeOpacity(context), blurEnabled: !ref.watch(lowEndDeviceProvider) || ref.watch(backdropBlurEnabledProvider), child: player, ), ) : player; } } class _MiniPlayerProgress extends ConsumerWidget { final Duration duration; final Color backgroundColor; const _MiniPlayerProgress({ required this.duration, required this.backgroundColor, }); @override Widget build(BuildContext context, WidgetRef ref) { final durationMs = duration.inMilliseconds; final positionMs = ref.watch(playbackPositionProvider).inMilliseconds; final progress = durationMs > 0 ? (positionMs / durationMs).clamp(0.0, 1.0) : 0.0; return LinearProgressIndicator( value: progress, minHeight: 2, backgroundColor: backgroundColor, ); } }