import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:just_audio/just_audio.dart'; import '../services/player_service.dart'; import '../models/song.dart'; import '../utils/farb_theme.dart'; import '../screens/now_playing_screen.dart'; class MiniPlayer extends StatefulWidget { const MiniPlayer({super.key}); @override State createState() => _MiniPlayerState(); } class _MiniPlayerState extends State { final PlayerService _player = PlayerService(); Duration _position = Duration.zero; Duration _dauer = Duration.zero; bool _spielt = false; StreamSubscription? _posSub; StreamSubscription? _stateSub; StreamSubscription? _songSub; @override void initState() { super.initState(); _posSub = _player.positionStream.listen((pos) { if (mounted) { setState(() => _position = pos); } }); _stateSub = _player.stateStream.listen((state) { if (mounted) { setState(() { _spielt = state.playing; _dauer = state.processingState == ProcessingState.ready ? (_player.dauer) : Duration.zero; }); } }); _songSub = _player.onSongWechsel.listen((_) { if (mounted) setState(() {}); }); _player.addListener(_onPlayerChanged); } void _onPlayerChanged() { if (mounted) setState(() {}); } void _oeffneNowPlaying() { Navigator.of(context).push( PageRouteBuilder( transitionDuration: const Duration(milliseconds: 350), reverseTransitionDuration: const Duration(milliseconds: 280), pageBuilder: (_, __, ___) => const NowPlayingScreen(), transitionsBuilder: (_, animation, __, child) { final offset = Tween( begin: const Offset(0, 1), end: Offset.zero, ).animate( CurvedAnimation( parent: animation, curve: Curves.easeOutCubic, reverseCurve: Curves.easeInCubic, ), ); return SlideTransition(position: offset, child: child); }, ), ); } @override void dispose() { _player.removeListener(_onPlayerChanged); _posSub?.cancel(); _stateSub?.cancel(); _songSub?.cancel(); super.dispose(); } @override Widget build(BuildContext context) { final song = _player.aktuellerSong; if (song == null) return const SizedBox.shrink(); final progress = _dauer.inSeconds > 0 ? _position.inSeconds / _dauer.inSeconds : 0.0; final hatCover = song.coverPfad != null && File(song.coverPfad!).existsSync(); final akzent = MeloTheme.akzent; return GestureDetector( onTap: _oeffneNowPlaying, child: Container( margin: const EdgeInsets.symmetric(horizontal: 16), decoration: BoxDecoration( color: MeloTheme.dunkel1, borderRadius: BorderRadius.circular(20), border: Border.all(color: MeloTheme.dunkel2), boxShadow: [ BoxShadow( color: akzent.withValues(alpha: 0.15), blurRadius: 20, offset: const Offset(0, 4), ), ], ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Padding( padding: const EdgeInsets.fromLTRB(12, 10, 12, 6), child: Row( children: [ // Cover mit Fallback auf Notensymbol ClipRRect( borderRadius: BorderRadius.circular(12), child: Container( width: 36, height: 36, decoration: BoxDecoration( gradient: LinearGradient( colors: [akzent, Color.lerp(akzent, Colors.black, 0.5)!], ), ), child: hatCover ? Image.file(File(song.coverPfad!), fit: BoxFit.cover) : const Center(child: Text('♪', style: TextStyle(fontSize: 16, color: Colors.white))), ), ), const SizedBox(width: 10), // Titel + Künstler Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(song.titel, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: Colors.white)), Text(song.kuenstler, style: const TextStyle(fontSize: 11, color: MeloTheme.textSekundaer)), ], ), ), // Shuffle _shuffleBtn(), // Repeat (3 Zustände) _repeatBtn(), // Steuerung _btn(Icons.skip_previous, _player.vorheriges), _playBtn(), _btn(Icons.skip_next, _player.naechstes), ], ), ), // Fortschritt if (_dauer.inSeconds > 0) Padding( padding: const EdgeInsets.fromLTRB(12, 0, 12, 8), child: ClipRRect( borderRadius: BorderRadius.circular(2), child: LinearProgressIndicator( value: progress, backgroundColor: MeloTheme.dunkel2, valueColor: AlwaysStoppedAnimation(akzent), minHeight: 2, ), ), ), ], ), ), ); } Widget _shuffleBtn() { final aktiv = _player.zufallsmodus; return _btn( Icons.shuffle, () => _player.setZufallsmodus(!aktiv), farbe: aktiv ? MeloTheme.akzent : Colors.white, ); } Widget _repeatBtn() { final modus = _player.wiederholmodus; final (icon, farbe) = switch (modus) { Wiederholmodus.aus => (Icons.repeat, Colors.white), Wiederholmodus.titel => (Icons.repeat_one, MeloTheme.akzent), Wiederholmodus.playlist => (Icons.repeat, MeloTheme.akzent), }; return _btn(icon, _naechsterWiederholmodus, farbe: farbe); } void _naechsterWiederholmodus() { final modus = _player.wiederholmodus; _player.setWiederholmodus(switch (modus) { Wiederholmodus.aus => Wiederholmodus.titel, Wiederholmodus.titel => Wiederholmodus.playlist, Wiederholmodus.playlist => Wiederholmodus.aus, }); } Widget _btn(IconData icon, VoidCallback onTap, {Color farbe = Colors.white}) { return Material( color: Colors.transparent, child: InkWell( borderRadius: BorderRadius.circular(50), onTap: onTap, child: Container( width: 32, height: 32, alignment: Alignment.center, child: Icon(icon, size: 18, color: farbe), ), ), ); } Widget _playBtn() { return Material( color: MeloTheme.akzent, borderRadius: BorderRadius.circular(50), child: InkWell( borderRadius: BorderRadius.circular(50), onTap: _player.playPause, child: Container( width: 36, height: 36, alignment: Alignment.center, child: Icon(_spielt ? Icons.pause : Icons.play_arrow, size: 20, color: Colors.white), ), ), ); } }