import 'package:audio_service/audio_service.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../shared/cover.dart'; import '../shared/theme.dart'; import 'audio_handler.dart'; import 'now_playing_screen.dart'; /// Kompakte Wiedergabe-Leiste über der Bottom-Nav. /// Blendet sich aus, wenn nichts läuft. class MiniPlayer extends StatelessWidget { const MiniPlayer({super.key}); /// Höhe der Zeile ohne den Fortschrittsbalken. static const double hoehe = 72; @override Widget build(BuildContext context) { final handler = context.read(); return StreamBuilder( stream: handler.mediaItem, builder: (context, snapshot) { final item = snapshot.data; // Ein- und Ausblenden statt Aufpoppen: die Leiste kommt von unten // dazu, wenn etwas startet. return AnimatedSwitcher( duration: MeloMotion.normal, switchInCurve: MeloMotion.curve, transitionBuilder: (kind, animation) => SizeTransition( sizeFactor: animation, alignment: Alignment.topCenter, child: FadeTransition(opacity: animation, child: kind), ), child: item == null ? const SizedBox.shrink() : _Leiste(handler: handler, item: item), ); }, ); } } class _Leiste extends StatelessWidget { const _Leiste({required this.handler, required this.item}); final MeloAudioHandler handler; final MediaItem item; @override Widget build(BuildContext context) { final texte = Theme.of(context).textTheme; return DecoratedBox( decoration: const BoxDecoration( color: MeloTheme.surface, // Trennt die Leiste vom Inhalt darüber, ohne Schatten. border: Border(top: BorderSide(color: MeloTheme.border)), ), child: Material( color: Colors.transparent, child: Column( mainAxisSize: MainAxisSize.min, children: [ InkWell( onTap: () => Navigator.of(context).push(MaterialPageRoute( builder: (_) => const NowPlayingScreen(), )), child: SizedBox( height: MiniPlayer.hoehe, child: Row( children: [ const SizedBox(width: MeloSpace.md), CoverImage(artUri: item.artUri, size: 52), const SizedBox(width: MeloSpace.md), Expanded( child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( item.title, maxLines: 1, overflow: TextOverflow.ellipsis, style: texte.bodyLarge ?.copyWith(fontWeight: FontWeight.w600), ), Text( item.artist ?? 'Unbekannt', maxLines: 1, overflow: TextOverflow.ellipsis, style: texte.bodySmall, ), ], ), ), const SizedBox(width: MeloSpace.sm), _Bedienung(handler: handler), const SizedBox(width: MeloSpace.sm), ], ), ), ), _Fortschritt(handler: handler), ], ), ), ); } } class _Bedienung extends StatelessWidget { const _Bedienung({required this.handler}); final MeloAudioHandler handler; @override Widget build(BuildContext context) { return StreamBuilder( stream: handler.playbackState, builder: (context, snap) { final laeuft = snap.data?.playing ?? false; return Row( mainAxisSize: MainAxisSize.min, children: [ IconButton( tooltip: 'Vorheriger Titel', icon: const Icon(Icons.skip_previous), onPressed: handler.skipToPrevious, ), // Der wichtigste Knopf der Leiste: eingefärbt und größer, damit // er sich ohne Hinsehen treffen lässt. IconButton.filled( tooltip: laeuft ? 'Pause' : 'Abspielen', style: IconButton.styleFrom( backgroundColor: MeloTheme.red, foregroundColor: Colors.white, minimumSize: const Size(MeloTheme.minTouchTarget, MeloTheme.minTouchTarget), ), icon: Icon(laeuft ? Icons.pause : Icons.play_arrow, size: 26), onPressed: laeuft ? handler.pause : handler.play, ), IconButton( tooltip: 'Nächster Titel', icon: const Icon(Icons.skip_next), onPressed: handler.skipToNext, ), ], ); }, ); } } class _Fortschritt extends StatelessWidget { const _Fortschritt({required this.handler}); final MeloAudioHandler handler; @override Widget build(BuildContext context) { return StreamBuilder( stream: handler.durationStream, builder: (context, dSnap) { final dauer = dSnap.data ?? Duration.zero; return StreamBuilder( stream: handler.positionStream, builder: (context, pSnap) { final position = pSnap.data ?? Duration.zero; return LinearProgressIndicator( value: dauer.inMilliseconds > 0 ? (position.inMilliseconds / dauer.inMilliseconds) .clamp(0.0, 1.0) : 0, minHeight: 2, backgroundColor: MeloTheme.border, valueColor: const AlwaysStoppedAnimation(MeloTheme.red), ); }, ); }, ); } }