v2.54 — Now-Playing-Redesign im Spotify-Stil

## Now-Playing-Fullscreen
- Kopfzeile: „JETZT LÄUFT"-Header + Like-Button + Queue-Button (Like aus der Titel-Zeile in den Header verschoben)
- Großes Cover zentriert (78 % Breite, Radius 24) mit Glow-Pulsation + Karten-Schatten
- Titel (24, w800) + Künstler (16) linksbündig in klarer Typografie
- Fortschrittsbalken: dickerer Slider (6 px, weiße Blende), Position links / Dauer rechts
- Controls-Reihenfolge wie Spotify: Shuffle, Prev, Play/Pause (74 px), Next, Repeat
- Neue untere Leiste: Schlaf-Timer links, Lyrics-Toggle + Geschwindigkeits-Chip rechts
- Geschwindigkeits-Chip: Tipp wechselt 0.5x–2.0x, wird wie in den Einstellungen persistiert (playback_speed)
- Blur-Hintergrund dezenter (Sigma 45 → 22)
- Versionsstring in main.dart auf 2.54 aktualisiert (Logger)
This commit is contained in:
Dustin
2026-08-05 12:16:15 +02:00
parent 83bb86a1fa
commit 8b6f0925a6
2 changed files with 169 additions and 92 deletions
+1 -1
View File
@@ -29,7 +29,7 @@ void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
// Logger startet sofort zeichnet ALLES auf // Logger startet sofort zeichnet ALLES auf
MeloLogger().init('2.52.3'); MeloLogger().init('2.54');
// ── Notifications initialisieren (Issue #9) ── // ── Notifications initialisieren (Issue #9) ──
const androidInit = AndroidInitializationSettings('@mipmap/ic_launcher'); const androidInit = AndroidInitializationSettings('@mipmap/ic_launcher');
+156 -79
View File
@@ -569,7 +569,8 @@ class _NowPlayingScreenState extends State<NowPlayingScreen>
), ),
_fortschritt(), _fortschritt(),
_controls(), _controls(),
const SizedBox(height: 18), _untereLeiste(),
const SizedBox(height: 10),
], ],
), ),
), ),
@@ -601,16 +602,16 @@ class _NowPlayingScreenState extends State<NowPlayingScreen>
// Crossfade) — nie vorher (altes Bild unter neuem Key = unsichtbarer // Crossfade) — nie vorher (altes Bild unter neuem Key = unsichtbarer
// Fade + harter Cut beim Nachladen). // Fade + harter Cut beim Nachladen).
key: ValueKey('cover-$pfad'), 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), 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() { Widget _kopfzeile() {
final sleepAktiv = _player.sleepTimerAktiv;
final rest = _player.sleepRestzeit;
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8), padding: const EdgeInsets.fromLTRB(4, 2, 8, 0),
child: Row( child: Row(
children: [ children: [
IconButton( IconButton(
@@ -619,31 +620,27 @@ class _NowPlayingScreenState extends State<NowPlayingScreen>
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.of(context).pop(),
tooltip: 'Schließen', tooltip: 'Schließen',
), ),
const Text(
'JETZT LÄUFT',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w700,
letterSpacing: 2.2,
color: MeloTheme.textSekundaer,
),
),
const Spacer(), const Spacer(),
// Lyrics-Toggle (🎤): wechselt Cover ↔ Lyrics // Like-Button (Favoriten)
IconButton( IconButton(
icon: Icon( icon: Icon(
_zeigeLyrics ? Icons.album : Icons.mic_none, _istFavorit ? Icons.favorite : Icons.favorite_border,
color: _zeigeLyrics ? MeloTheme.rot : Colors.white, size: 24,
color: _istFavorit ? MeloTheme.rot : Colors.white,
), ),
onPressed: () => setState(() => _zeigeLyrics = !_zeigeLyrics), onPressed: _toggleFavorit,
tooltip: _zeigeLyrics ? 'Cover anzeigen' : 'Lyrics anzeigen', tooltip: _istFavorit
), ? 'Aus Favoriten entfernen'
// Sleep-Timer : 'Zu Favoriten hinzufügen',
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',
), ),
// Queue-Zugriff // Queue-Zugriff
IconButton( IconButton(
@@ -656,19 +653,20 @@ class _NowPlayingScreenState extends State<NowPlayingScreen>
); );
} }
/// 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) { Widget _coverAnsicht(Song? song) {
final hatCover = _hatCover && _coverPfad != null; final hatCover = _hatCover && _coverPfad != null;
final coverGroesse = 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( return SingleChildScrollView(
key: const ValueKey('cover'), key: const ValueKey('cover'),
padding: const EdgeInsets.symmetric(horizontal: 24), padding: const EdgeInsets.symmetric(horizontal: 28),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
const SizedBox(height: 16), const SizedBox(height: 12),
// Cover mit Glow — AnimatedBuilder um den Container: Ohne Listener/ // Cover mit Glow — AnimatedBuilder um den Container: Ohne Listener/
// Builder würde der BoxShadow nur bei fremden setStates neu gebaut // Builder würde der BoxShadow nur bei fremden setStates neu gebaut
// (Pulsation zufällig sichtbar, friert bei Pause/Pufferung ein). // (Pulsation zufällig sichtbar, friert bei Pause/Pufferung ein).
@@ -678,73 +676,61 @@ class _NowPlayingScreenState extends State<NowPlayingScreen>
width: coverGroesse, width: coverGroesse,
height: coverGroesse, height: coverGroesse,
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(24),
boxShadow: [ boxShadow: [
BoxShadow(
// Glow in der Akzentfarbe des Users, pulsierend bei Wiedergabe // Glow in der Akzentfarbe des Users, pulsierend bei Wiedergabe
color: MeloTheme.akzent.withValues(alpha: 0.45), BoxShadow(
blurRadius: 50 + 20 * _glowController.value, // 5070 color: MeloTheme.akzent.withValues(alpha: 0.4),
spreadRadius: 6 + 6 * _glowController.value, // 612 blurRadius: 45 + 18 * _glowController.value, // 4563
spreadRadius: 5 + 5 * _glowController.value, // 510
),
// 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( child: ClipRRect(
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(24),
child: hatCover child: hatCover
? Image.file(File(_coverPfad!), fit: BoxFit.cover) ? Image.file(File(_coverPfad!), fit: BoxFit.cover)
: Container( : Container(
color: MeloTheme.rotHell, color: MeloTheme.rotHell,
child: const Icon(Icons.music_note, child: const Icon(Icons.music_note,
size: 90, color: MeloTheme.rot), size: 100, color: MeloTheme.rot),
), ),
), ),
), ),
), ),
const SizedBox(height: 30), const SizedBox(height: 30),
// Titel + Künstler + Like // Titel + Künstler linksbündig (Like-Button ist jetzt im Header)
Row( Align(
crossAxisAlignment: CrossAxisAlignment.start, alignment: Alignment.centerLeft,
children: [ child: Text(
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
song?.titel ?? 'Kein Song', song?.titel ?? 'Kein Song',
maxLines: 2, maxLines: 2,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: const TextStyle( style: const TextStyle(
fontSize: 22, fontSize: 24,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w800,
color: Colors.white, color: Colors.white,
), ),
), ),
const SizedBox(height: 4), ),
Text( const SizedBox(height: 6),
Align(
alignment: Alignment.centerLeft,
child: Text(
song?.kuenstler ?? '', song?.kuenstler ?? '',
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: const TextStyle( style: const TextStyle(
fontSize: 15, fontSize: 16,
color: MeloTheme.textSekundaer, color: MeloTheme.textSekundaer,
), ),
), ),
],
),
),
// 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: 8), const SizedBox(height: 8),
], ],
@@ -838,11 +824,14 @@ class _NowPlayingScreenState extends State<NowPlayingScreen>
children: [ children: [
SliderTheme( SliderTheme(
data: SliderTheme.of(context).copyWith( data: SliderTheme.of(context).copyWith(
trackHeight: 3, // Dickerer Slider (Spotify-Stil), weiße Blende
trackHeight: 6,
activeTrackColor: MeloTheme.rot, activeTrackColor: MeloTheme.rot,
inactiveTrackColor: MeloTheme.dunkel2, inactiveTrackColor: MeloTheme.dunkel2,
thumbColor: MeloTheme.rot, thumbColor: Colors.white,
thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 7),
overlayColor: MeloTheme.rot.withValues(alpha: 0.2), overlayColor: MeloTheme.rot.withValues(alpha: 0.2),
overlayShape: const RoundSliderOverlayShape(overlayRadius: 14),
), ),
child: Slider( child: Slider(
value: posMs, value: posMs,
@@ -875,24 +864,22 @@ class _NowPlayingScreenState extends State<NowPlayingScreen>
); );
} }
/// Controls im Spotify-Stil: Shuffle, Prev, Play/Pause (groß), Next, Repeat.
Widget _controls() { Widget _controls() {
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ children: [
_shuffleBtn(), _shuffleBtn(),
_repeatBtn(), _ctrlBtn(Icons.skip_previous, 36, _player.vorheriges),
_ctrlBtn(Icons.skip_previous, 34, _player.vorheriges),
_playBtn(), _playBtn(),
// Next: Long-Press öffnet zusätzlich das Play-Next-Menü // Next: Long-Press öffnet zusätzlich das Play-Next-Menü
GestureDetector( GestureDetector(
onLongPress: _zeigePlayNextMenue, onLongPress: _zeigePlayNextMenue,
child: _ctrlBtn(Icons.skip_next, 34, _player.naechstes), child: _ctrlBtn(Icons.skip_next, 36, _player.naechstes),
), ),
// Play-Next-Menü _repeatBtn(),
_ctrlBtn(Icons.playlist_play, 30, _zeigePlayNextMenue,
farbe: MeloTheme.textSekundaer),
], ],
), ),
); );
@@ -948,12 +935,12 @@ class _NowPlayingScreenState extends State<NowPlayingScreen>
customBorder: const CircleBorder(), customBorder: const CircleBorder(),
onTap: _player.playPause, onTap: _player.playPause,
child: Container( child: Container(
width: 64, width: 74,
height: 64, height: 74,
alignment: Alignment.center, alignment: Alignment.center,
child: Icon( child: Icon(
_spielt ? Icons.pause : Icons.play_arrow, _spielt ? Icons.pause : Icons.play_arrow,
size: 36, size: 42,
color: Colors.white, color: Colors.white,
), ),
), ),
@@ -961,6 +948,96 @@ class _NowPlayingScreenState extends State<NowPlayingScreen>
); );
} }
/// 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<double> _geschwindigkeiten = [
0.5, 0.75, 1.0, 1.25, 1.5, 2.0,
];
Future<void> _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) { String _formatZeit(Duration d) {
final min = d.inMinutes; final min = d.inMinutes;
final sek = d.inSeconds % 60; final sek = d.inSeconds % 60;