import 'package:audio_service/audio_service.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../shared/theme.dart'; import 'audio_handler.dart'; import 'player_expansion_controller.dart'; import 'wischen.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.ruhig(context, 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 StatefulWidget { const _Leiste({required this.handler, required this.item}); final MeloAudioHandler handler; final MediaItem item; @override State<_Leiste> createState() => _LeisteState(); } class _LeisteState extends State<_Leiste> { /// Für die horizontale Titel-Wechsel-Geste (unverändert). double _horizontalVersatz = 0; bool _ziehtHorizontalGerade = false; /// Rohe, aufsummierte vertikale Zugstrecke seit Gestenbeginn — exakt wie /// bisher `_versatz.dy`, nur ohne die Optik-Verantwortung (die liegt jetzt /// im PlayerExpansionController). Wird für [PlayerExpansionController. /// dragEnd] gebraucht, das dieselbe rohe Strecke wie wischen.dart erwartet; /// aus `progress` ließe sich das wegen des Clampings nicht mehr exakt /// zurückrechnen. double _vertikaleStrecke = 0; void _oeffne(BuildContext context) => context.read().open(context); @override Widget build(BuildContext context) { final texte = Theme.of(context).textTheme; final handler = widget.handler; final item = widget.item; 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: [ GestureDetector( onVerticalDragStart: (_) => _vertikaleStrecke = 0, onVerticalDragUpdate: (details) { _vertikaleStrecke += details.delta.dy; context.read().dragBy(details.delta.dy); }, onVerticalDragEnd: (details) => context .read() .dragEnd(context, _vertikaleStrecke, details.velocity.pixelsPerSecond.dy), onHorizontalDragUpdate: (details) => setState(() { _ziehtHorizontalGerade = true; _horizontalVersatz += details.delta.dx; }), onHorizontalDragEnd: (details) { final wechsel = titelWechselMitStrecke( _horizontalVersatz, details.velocity.pixelsPerSecond.dx); setState(() { _ziehtHorizontalGerade = false; _horizontalVersatz = 0; }); switch (wechsel) { case Wechsel.naechster: handler.skipToNext(); case Wechsel.vorheriger: handler.skipToPrevious(); case null: break; } }, child: InkWell( onTap: () => _oeffne(context), child: AnimatedContainer( duration: _ziehtHorizontalGerade ? Duration.zero : MeloMotion.ruhig(context, MeloMotion.normal), curve: MeloMotion.curve, transform: Matrix4.translationValues(_horizontalVersatz, 0, 0), height: MiniPlayer.hoehe, child: Row( children: [ const SizedBox(width: MeloSpace.md), // Das Cover rendert WanderndesCover in HomeShell in // einer eigenen Ebene über allem (deckt exakt diese // Position bei progress == 0 ab) — hier nur Platz // freihalten, damit Titel/Bedienung nicht springen. const SizedBox(width: 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), ); }, ); }, ); } }