diff --git a/lib/main.dart b/lib/main.dart index e4521af..066f309 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -29,7 +29,7 @@ void main() async { WidgetsFlutterBinding.ensureInitialized(); // Logger startet sofort – zeichnet ALLES auf - MeloLogger().init('2.52.3'); + MeloLogger().init('2.54'); // ── Notifications initialisieren (Issue #9) ── const androidInit = AndroidInitializationSettings('@mipmap/ic_launcher'); diff --git a/lib/screens/now_playing_screen.dart b/lib/screens/now_playing_screen.dart index 393f2bb..b2583aa 100644 --- a/lib/screens/now_playing_screen.dart +++ b/lib/screens/now_playing_screen.dart @@ -569,7 +569,8 @@ class _NowPlayingScreenState extends State ), _fortschritt(), _controls(), - const SizedBox(height: 18), + _untereLeiste(), + const SizedBox(height: 10), ], ), ), @@ -601,16 +602,16 @@ class _NowPlayingScreenState extends State // Crossfade) — nie vorher (altes Bild unter neuem Key = unsichtbarer // Fade + harter Cut beim Nachladen). key: ValueKey('cover-$pfad'), - imageFilter: ImageFilter.blur(sigmaX: 45, sigmaY: 45), + imageFilter: ImageFilter.blur(sigmaX: 22, sigmaY: 22), child: Image.file(File(pfad), fit: BoxFit.cover), ); } + /// Kopfzeile im Spotify-Stil: „JETZT LÄUFT" links neben dem Schließen-Pfeil, + /// rechts Like-Button (Favoriten) und Queue-Zugriff. Widget _kopfzeile() { - final sleepAktiv = _player.sleepTimerAktiv; - final rest = _player.sleepRestzeit; return Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), + padding: const EdgeInsets.fromLTRB(4, 2, 8, 0), child: Row( children: [ IconButton( @@ -619,31 +620,27 @@ class _NowPlayingScreenState extends State onPressed: () => Navigator.of(context).pop(), tooltip: 'Schließen', ), + const Text( + 'JETZT LÄUFT', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w700, + letterSpacing: 2.2, + color: MeloTheme.textSekundaer, + ), + ), const Spacer(), - // Lyrics-Toggle (🎤): wechselt Cover ↔ Lyrics + // Like-Button (Favoriten) IconButton( icon: Icon( - _zeigeLyrics ? Icons.album : Icons.mic_none, - color: _zeigeLyrics ? MeloTheme.rot : Colors.white, + _istFavorit ? Icons.favorite : Icons.favorite_border, + size: 24, + color: _istFavorit ? MeloTheme.rot : Colors.white, ), - onPressed: () => setState(() => _zeigeLyrics = !_zeigeLyrics), - tooltip: _zeigeLyrics ? 'Cover anzeigen' : 'Lyrics anzeigen', - ), - // Sleep-Timer - if (sleepAktiv && rest != null) - Padding( - padding: const EdgeInsets.only(right: 4), - child: Text( - _formatRestzeit(rest), - style: const TextStyle(fontSize: 12, color: MeloTheme.rot), - ), - ), - IconButton( - icon: Icon(Icons.bedtime, - size: 22, - color: sleepAktiv ? MeloTheme.rot : Colors.white), - onPressed: _zeigeSleepTimer, - tooltip: 'Schlaf-Timer', + onPressed: _toggleFavorit, + tooltip: _istFavorit + ? 'Aus Favoriten entfernen' + : 'Zu Favoriten hinzufügen', ), // Queue-Zugriff IconButton( @@ -656,19 +653,20 @@ class _NowPlayingScreenState extends State ); } - /// Cover-Ansicht: großes Cover (Radius 20 + Glow), Titel, Künstler, Like. + /// Cover-Ansicht: großes Cover (Radius 24 + Glow + Karten-Schatten), + /// darunter Titel und Künstler in klarer Typografie (Spotify-Stil). Widget _coverAnsicht(Song? song) { final hatCover = _hatCover && _coverPfad != null; final coverGroesse = - (MediaQuery.of(context).size.width * 0.72).clamp(220.0, 340.0); + (MediaQuery.of(context).size.width * 0.78).clamp(240.0, 360.0); return SingleChildScrollView( key: const ValueKey('cover'), - padding: const EdgeInsets.symmetric(horizontal: 24), + padding: const EdgeInsets.symmetric(horizontal: 28), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - const SizedBox(height: 16), + const SizedBox(height: 12), // Cover mit Glow — AnimatedBuilder um den Container: Ohne Listener/ // Builder würde der BoxShadow nur bei fremden setStates neu gebaut // (Pulsation zufällig sichtbar, friert bei Pause/Pufferung ein). @@ -678,73 +676,61 @@ class _NowPlayingScreenState extends State width: coverGroesse, height: coverGroesse, decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), + borderRadius: BorderRadius.circular(24), boxShadow: [ + // Glow in der Akzentfarbe des Users, pulsierend bei Wiedergabe BoxShadow( - // Glow in der Akzentfarbe des Users, pulsierend bei Wiedergabe - color: MeloTheme.akzent.withValues(alpha: 0.45), - blurRadius: 50 + 20 * _glowController.value, // 50–70 - spreadRadius: 6 + 6 * _glowController.value, // 6–12 + color: MeloTheme.akzent.withValues(alpha: 0.4), + blurRadius: 45 + 18 * _glowController.value, // 45–63 + spreadRadius: 5 + 5 * _glowController.value, // 5–10 + ), + // Karten-Schatten für Tiefe (Cover als „Karte") + BoxShadow( + color: Colors.black.withValues(alpha: 0.55), + blurRadius: 28, + offset: const Offset(0, 14), ), ], ), child: ClipRRect( - borderRadius: BorderRadius.circular(20), + borderRadius: BorderRadius.circular(24), child: hatCover ? Image.file(File(_coverPfad!), fit: BoxFit.cover) : Container( color: MeloTheme.rotHell, child: const Icon(Icons.music_note, - size: 90, color: MeloTheme.rot), + size: 100, color: MeloTheme.rot), ), ), ), ), const SizedBox(height: 30), - // Titel + Künstler + Like - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - song?.titel ?? 'Kein Song', - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - fontSize: 22, - fontWeight: FontWeight.w700, - color: Colors.white, - ), - ), - const SizedBox(height: 4), - Text( - song?.kuenstler ?? '', - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - fontSize: 15, - color: MeloTheme.textSekundaer, - ), - ), - ], - ), + // Titel + Künstler linksbündig (Like-Button ist jetzt im Header) + Align( + alignment: Alignment.centerLeft, + child: Text( + song?.titel ?? 'Kein Song', + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.w800, + color: Colors.white, ), - // Like-Button (Favoriten) - IconButton( - icon: Icon( - _istFavorit ? Icons.favorite : Icons.favorite_border, - size: 28, - color: _istFavorit ? MeloTheme.rot : Colors.white, - ), - onPressed: _toggleFavorit, - tooltip: _istFavorit - ? 'Aus Favoriten entfernen' - : 'Zu Favoriten hinzufügen', + ), + ), + const SizedBox(height: 6), + Align( + alignment: Alignment.centerLeft, + child: Text( + song?.kuenstler ?? '', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 16, + color: MeloTheme.textSekundaer, ), - ], + ), ), const SizedBox(height: 8), ], @@ -838,11 +824,14 @@ class _NowPlayingScreenState extends State children: [ SliderTheme( data: SliderTheme.of(context).copyWith( - trackHeight: 3, + // Dickerer Slider (Spotify-Stil), weiße Blende + trackHeight: 6, activeTrackColor: MeloTheme.rot, inactiveTrackColor: MeloTheme.dunkel2, - thumbColor: MeloTheme.rot, + thumbColor: Colors.white, + thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 7), overlayColor: MeloTheme.rot.withValues(alpha: 0.2), + overlayShape: const RoundSliderOverlayShape(overlayRadius: 14), ), child: Slider( value: posMs, @@ -875,24 +864,22 @@ class _NowPlayingScreenState extends State ); } + /// Controls im Spotify-Stil: Shuffle, Prev, Play/Pause (groß), Next, Repeat. Widget _controls() { return Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ _shuffleBtn(), - _repeatBtn(), - _ctrlBtn(Icons.skip_previous, 34, _player.vorheriges), + _ctrlBtn(Icons.skip_previous, 36, _player.vorheriges), _playBtn(), // Next: Long-Press öffnet zusätzlich das Play-Next-Menü GestureDetector( onLongPress: _zeigePlayNextMenue, - child: _ctrlBtn(Icons.skip_next, 34, _player.naechstes), + child: _ctrlBtn(Icons.skip_next, 36, _player.naechstes), ), - // Play-Next-Menü - _ctrlBtn(Icons.playlist_play, 30, _zeigePlayNextMenue, - farbe: MeloTheme.textSekundaer), + _repeatBtn(), ], ), ); @@ -948,12 +935,12 @@ class _NowPlayingScreenState extends State customBorder: const CircleBorder(), onTap: _player.playPause, child: Container( - width: 64, - height: 64, + width: 74, + height: 74, alignment: Alignment.center, child: Icon( _spielt ? Icons.pause : Icons.play_arrow, - size: 36, + size: 42, color: Colors.white, ), ), @@ -961,6 +948,96 @@ class _NowPlayingScreenState extends State ); } + /// Untere Leiste (Spotify-Stil): Schlaf-Timer links, Lyrics-Toggle und + /// Geschwindigkeits-Anzeige rechts. + Widget _untereLeiste() { + final sleepAktiv = _player.sleepTimerAktiv; + final rest = _player.sleepRestzeit; + final geschwindigkeit = _player.geschwindigkeit; + final speedAktiv = geschwindigkeit != 1.0; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 2), + child: Row( + children: [ + // Schlaf-Timer (Tipp = Auswahl-Sheet) + GestureDetector( + onTap: _zeigeSleepTimer, + behavior: HitTestBehavior.opaque, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.bedtime, + size: 20, + color: sleepAktiv ? MeloTheme.rot : MeloTheme.textSekundaer, + ), + if (sleepAktiv && rest != null) ...[ + const SizedBox(width: 6), + Text( + _formatRestzeit(rest).replaceAll('⏱ ', ''), + style: const TextStyle(fontSize: 12, color: MeloTheme.rot), + ), + ], + ], + ), + ), + const Spacer(), + // Lyrics-Toggle (🎤): wechselt Cover ↔ Lyrics + IconButton( + icon: Icon( + _zeigeLyrics ? Icons.album : Icons.mic_none, + size: 22, + color: _zeigeLyrics ? MeloTheme.rot : Colors.white70, + ), + onPressed: () => setState(() => _zeigeLyrics = !_zeigeLyrics), + tooltip: _zeigeLyrics ? 'Cover anzeigen' : 'Lyrics anzeigen', + ), + const SizedBox(width: 4), + // Geschwindigkeit (Tipp = nächste Stufe, wird persistiert) + GestureDetector( + onTap: _naechsteGeschwindigkeit, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + decoration: BoxDecoration( + color: speedAktiv ? MeloTheme.rot : MeloTheme.dunkel2, + borderRadius: BorderRadius.circular(12), + ), + child: Text( + '${geschwindigkeit.toStringAsFixed(2)}x', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: speedAktiv ? Colors.white : MeloTheme.textSekundaer, + ), + ), + ), + ), + ], + ), + ); + } + + /// Gängige Geschwindigkeitsstufen — Tipp im Fullscreen wechselt durch. + static const List _geschwindigkeiten = [ + 0.5, 0.75, 1.0, 1.25, 1.5, 2.0, + ]; + + Future _naechsteGeschwindigkeit() async { + final aktuell = _player.geschwindigkeit; + final idx = _geschwindigkeiten.indexOf(aktuell); + final neu = idx >= 0 + ? _geschwindigkeiten[(idx + 1) % _geschwindigkeiten.length] + : 1.0; + // Wie in den Einstellungen persistieren, damit die Wahl erhalten bleibt + try { + final p = await SharedPreferences.getInstance(); + await p.setDouble('playback_speed', neu); + } catch (e) { + debugPrint('Geschwindigkeit persistieren fehlgeschlagen: $e'); + } + await _player.setGeschwindigkeit(neu); + } + String _formatZeit(Duration d) { final min = d.inMinutes; final sek = d.inSeconds % 60;