v2.50.1 — Player-Glow an Akzent + Pulsation, Cover-Crossfade, existsSync-Fix
## Fullscreen-Player (now_playing_screen.dart) - Glow-Farbe folgt MeloTheme.akzentNotifier (Per-User-Akzent) statt fixem Rot; Listener rebuildet bei Akzentwechsel - Dynamische Glow-Pulsation: AnimationController (Bordmittel) pulsiert blurRadius 50–70 / spreadRadius 6–12 nur bei Wiedergabe, stillstehend bei Pause - Blur-Hintergrund crossfaded beim Songwechsel (AnimatedSwitcher 600ms, layoutBuilder mit StackFit.expand, ValueKey je Song) - existsSync aus dem Build-Pfad entfernt: async _ladeCoverStatus mit try/catch, Guard gegen stale Song-Wechsel
This commit is contained in:
@@ -21,7 +21,8 @@ class NowPlayingScreen extends StatefulWidget {
|
||||
State<NowPlayingScreen> createState() => _NowPlayingScreenState();
|
||||
}
|
||||
|
||||
class _NowPlayingScreenState extends State<NowPlayingScreen> {
|
||||
class _NowPlayingScreenState extends State<NowPlayingScreen>
|
||||
with SingleTickerProviderStateMixin {
|
||||
final PlayerService _player = PlayerService();
|
||||
final FavoritenService _favoriten = FavoritenService();
|
||||
final LyricsService _lyricsService = LyricsService();
|
||||
@@ -51,9 +52,21 @@ class _NowPlayingScreenState extends State<NowPlayingScreen> {
|
||||
bool _istFavorit = false;
|
||||
bool _toggleLaeuft = false; // In-Flight-Guard gegen Doppel-Tap-Race
|
||||
|
||||
// ─── Glow-Pulsation (synchron zum Play-Zustand) ───
|
||||
late final AnimationController _glowController;
|
||||
|
||||
// ─── Cover-Prüfung (async, kein existsSync im Build-Pfad) ───
|
||||
bool _hatCover = false;
|
||||
String? _coverPfad;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_glowController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 1400),
|
||||
);
|
||||
MeloTheme.akzentNotifier.addListener(_onAkzentGeaendert);
|
||||
_posSub = _player.positionStream.listen((pos) {
|
||||
if (!mounted) return;
|
||||
setState(() => _position = pos);
|
||||
@@ -67,6 +80,7 @@ class _NowPlayingScreenState extends State<NowPlayingScreen> {
|
||||
? _player.dauer
|
||||
: Duration.zero;
|
||||
});
|
||||
_syncGlowPulsation();
|
||||
});
|
||||
_songSub = _player.onSongWechsel.listen((song) {
|
||||
if (!mounted || song == null) return;
|
||||
@@ -80,6 +94,7 @@ class _NowPlayingScreenState extends State<NowPlayingScreen> {
|
||||
});
|
||||
_ladeLyricsFuer(song, _ladeGeneration);
|
||||
_ladeFavoritStatus(song);
|
||||
_ladeCoverStatus(song);
|
||||
});
|
||||
_player.addListener(_onPlayerChanged);
|
||||
// Initialen Zustand laden (falls schon ein Song läuft)
|
||||
@@ -88,18 +103,22 @@ class _NowPlayingScreenState extends State<NowPlayingScreen> {
|
||||
_ladeGeneration++;
|
||||
_ladeLyricsFuer(song, _ladeGeneration);
|
||||
_ladeFavoritStatus(song);
|
||||
_ladeCoverStatus(song);
|
||||
}
|
||||
_syncGlowPulsation();
|
||||
_syncSleepTick();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_player.removeListener(_onPlayerChanged);
|
||||
MeloTheme.akzentNotifier.removeListener(_onAkzentGeaendert);
|
||||
_posSub?.cancel();
|
||||
_stateSub?.cancel();
|
||||
_songSub?.cancel();
|
||||
_sleepTick?.cancel();
|
||||
_scrollController.dispose();
|
||||
_glowController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -108,9 +127,53 @@ class _NowPlayingScreenState extends State<NowPlayingScreen> {
|
||||
void _onPlayerChanged() {
|
||||
if (!mounted) return;
|
||||
setState(() {});
|
||||
_syncGlowPulsation();
|
||||
_syncSleepTick();
|
||||
}
|
||||
|
||||
void _onAkzentGeaendert() {
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
/// Glow-Pulsation: nur bei Wiedergabe animieren, bei Pause stillstehen.
|
||||
void _syncGlowPulsation() {
|
||||
if (_spielt) {
|
||||
if (!_glowController.isAnimating) {
|
||||
_glowController.repeat(reverse: true);
|
||||
}
|
||||
} else if (_glowController.isAnimating) {
|
||||
_glowController.stop();
|
||||
_glowController.value = 0; // Ruheposition (kleinster Glow)
|
||||
}
|
||||
}
|
||||
|
||||
/// Prüft asynchron, ob die Cover-Datei existiert (statt existsSync im
|
||||
/// Build-Pfad — vermeidet UI-Jank). try/catch gegen Dateisystem-Fehler.
|
||||
Future<void> _ladeCoverStatus(Song song) async {
|
||||
final pfad = song.coverPfad;
|
||||
if (pfad == null) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_hatCover = false;
|
||||
_coverPfad = null;
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
bool existiert;
|
||||
try {
|
||||
existiert = await File(pfad).exists();
|
||||
} catch (e) {
|
||||
MeloLogger().fehler('cover_pfad_check', e);
|
||||
existiert = false;
|
||||
}
|
||||
if (!mounted || _player.aktuellerSong?.coverPfad != pfad) return;
|
||||
setState(() {
|
||||
_hatCover = existiert;
|
||||
_coverPfad = pfad;
|
||||
});
|
||||
}
|
||||
|
||||
void _syncSleepTick() {
|
||||
final aktiv = _player.sleepTimerAktiv;
|
||||
if (aktiv && _sleepTick == null) {
|
||||
@@ -435,7 +498,21 @@ class _NowPlayingScreenState extends State<NowPlayingScreen> {
|
||||
body: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
_hintergrundBlur(song),
|
||||
// Blur-Hintergrund mit Crossfade beim Songwechsel (Muster wie
|
||||
// Cover↔Lyrics-Toggle, nur länger für weichen Übergang).
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 600),
|
||||
layoutBuilder: (currentChild, previousChildren) => Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
...previousChildren,
|
||||
if (currentChild != null) currentChild,
|
||||
],
|
||||
),
|
||||
transitionBuilder: (child, anim) =>
|
||||
FadeTransition(opacity: anim, child: child),
|
||||
child: _hintergrundBlur(song),
|
||||
),
|
||||
// Abdunklung für Lesbarkeit
|
||||
const DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
@@ -473,12 +550,13 @@ class _NowPlayingScreenState extends State<NowPlayingScreen> {
|
||||
}
|
||||
|
||||
/// Blur-Hintergrund aus dem Cover (Bordmittel: ImageFiltered + dart:ui).
|
||||
/// Existenz-Check kommt aus dem async geladenen [_hatCover]/[_coverPfad] —
|
||||
/// kein synchrones existsSync im Build-Pfad.
|
||||
Widget _hintergrundBlur(Song? song) {
|
||||
final hatCover = song != null &&
|
||||
song.coverPfad != null &&
|
||||
File(song.coverPfad!).existsSync();
|
||||
if (!hatCover) {
|
||||
final pfad = _coverPfad;
|
||||
if (!_hatCover || pfad == null) {
|
||||
return const DecoratedBox(
|
||||
key: ValueKey('kein-cover'),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
@@ -489,8 +567,9 @@ class _NowPlayingScreenState extends State<NowPlayingScreen> {
|
||||
);
|
||||
}
|
||||
return ImageFiltered(
|
||||
key: ValueKey('cover-${song?.id ?? pfad}'),
|
||||
imageFilter: ImageFilter.blur(sigmaX: 45, sigmaY: 45),
|
||||
child: Image.file(File(song.coverPfad!), fit: BoxFit.cover),
|
||||
child: Image.file(File(pfad), fit: BoxFit.cover),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -546,9 +625,7 @@ class _NowPlayingScreenState extends State<NowPlayingScreen> {
|
||||
|
||||
/// Cover-Ansicht: großes Cover (Radius 20 + Glow), Titel, Künstler, Like.
|
||||
Widget _coverAnsicht(Song? song) {
|
||||
final hatCover = song != null &&
|
||||
song.coverPfad != null &&
|
||||
File(song.coverPfad!).existsSync();
|
||||
final hatCover = _hatCover && _coverPfad != null;
|
||||
final coverGroesse =
|
||||
(MediaQuery.of(context).size.width * 0.72).clamp(220.0, 340.0);
|
||||
|
||||
@@ -567,16 +644,17 @@ class _NowPlayingScreenState extends State<NowPlayingScreen> {
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: MeloTheme.rot.withValues(alpha: 0.45),
|
||||
blurRadius: 60,
|
||||
spreadRadius: 6,
|
||||
// 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
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: hatCover
|
||||
? Image.file(File(song.coverPfad!), fit: BoxFit.cover)
|
||||
? Image.file(File(_coverPfad!), fit: BoxFit.cover)
|
||||
: Container(
|
||||
color: MeloTheme.rotHell,
|
||||
child: const Icon(Icons.music_note,
|
||||
|
||||
Reference in New Issue
Block a user