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:
+1
-1
@@ -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');
|
||||||
|
|||||||
@@ -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, // 50–70
|
color: MeloTheme.akzent.withValues(alpha: 0.4),
|
||||||
spreadRadius: 6 + 6 * _glowController.value, // 6–12
|
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(
|
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;
|
||||||
|
|||||||
Reference in New Issue
Block a user