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();
|
State<NowPlayingScreen> createState() => _NowPlayingScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _NowPlayingScreenState extends State<NowPlayingScreen> {
|
class _NowPlayingScreenState extends State<NowPlayingScreen>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
final PlayerService _player = PlayerService();
|
final PlayerService _player = PlayerService();
|
||||||
final FavoritenService _favoriten = FavoritenService();
|
final FavoritenService _favoriten = FavoritenService();
|
||||||
final LyricsService _lyricsService = LyricsService();
|
final LyricsService _lyricsService = LyricsService();
|
||||||
@@ -51,9 +52,21 @@ class _NowPlayingScreenState extends State<NowPlayingScreen> {
|
|||||||
bool _istFavorit = false;
|
bool _istFavorit = false;
|
||||||
bool _toggleLaeuft = false; // In-Flight-Guard gegen Doppel-Tap-Race
|
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
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_glowController = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: const Duration(milliseconds: 1400),
|
||||||
|
);
|
||||||
|
MeloTheme.akzentNotifier.addListener(_onAkzentGeaendert);
|
||||||
_posSub = _player.positionStream.listen((pos) {
|
_posSub = _player.positionStream.listen((pos) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() => _position = pos);
|
setState(() => _position = pos);
|
||||||
@@ -67,6 +80,7 @@ class _NowPlayingScreenState extends State<NowPlayingScreen> {
|
|||||||
? _player.dauer
|
? _player.dauer
|
||||||
: Duration.zero;
|
: Duration.zero;
|
||||||
});
|
});
|
||||||
|
_syncGlowPulsation();
|
||||||
});
|
});
|
||||||
_songSub = _player.onSongWechsel.listen((song) {
|
_songSub = _player.onSongWechsel.listen((song) {
|
||||||
if (!mounted || song == null) return;
|
if (!mounted || song == null) return;
|
||||||
@@ -80,6 +94,7 @@ class _NowPlayingScreenState extends State<NowPlayingScreen> {
|
|||||||
});
|
});
|
||||||
_ladeLyricsFuer(song, _ladeGeneration);
|
_ladeLyricsFuer(song, _ladeGeneration);
|
||||||
_ladeFavoritStatus(song);
|
_ladeFavoritStatus(song);
|
||||||
|
_ladeCoverStatus(song);
|
||||||
});
|
});
|
||||||
_player.addListener(_onPlayerChanged);
|
_player.addListener(_onPlayerChanged);
|
||||||
// Initialen Zustand laden (falls schon ein Song läuft)
|
// Initialen Zustand laden (falls schon ein Song läuft)
|
||||||
@@ -88,18 +103,22 @@ class _NowPlayingScreenState extends State<NowPlayingScreen> {
|
|||||||
_ladeGeneration++;
|
_ladeGeneration++;
|
||||||
_ladeLyricsFuer(song, _ladeGeneration);
|
_ladeLyricsFuer(song, _ladeGeneration);
|
||||||
_ladeFavoritStatus(song);
|
_ladeFavoritStatus(song);
|
||||||
|
_ladeCoverStatus(song);
|
||||||
}
|
}
|
||||||
|
_syncGlowPulsation();
|
||||||
_syncSleepTick();
|
_syncSleepTick();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_player.removeListener(_onPlayerChanged);
|
_player.removeListener(_onPlayerChanged);
|
||||||
|
MeloTheme.akzentNotifier.removeListener(_onAkzentGeaendert);
|
||||||
_posSub?.cancel();
|
_posSub?.cancel();
|
||||||
_stateSub?.cancel();
|
_stateSub?.cancel();
|
||||||
_songSub?.cancel();
|
_songSub?.cancel();
|
||||||
_sleepTick?.cancel();
|
_sleepTick?.cancel();
|
||||||
_scrollController.dispose();
|
_scrollController.dispose();
|
||||||
|
_glowController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,9 +127,53 @@ class _NowPlayingScreenState extends State<NowPlayingScreen> {
|
|||||||
void _onPlayerChanged() {
|
void _onPlayerChanged() {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {});
|
setState(() {});
|
||||||
|
_syncGlowPulsation();
|
||||||
_syncSleepTick();
|
_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() {
|
void _syncSleepTick() {
|
||||||
final aktiv = _player.sleepTimerAktiv;
|
final aktiv = _player.sleepTimerAktiv;
|
||||||
if (aktiv && _sleepTick == null) {
|
if (aktiv && _sleepTick == null) {
|
||||||
@@ -435,7 +498,21 @@ class _NowPlayingScreenState extends State<NowPlayingScreen> {
|
|||||||
body: Stack(
|
body: Stack(
|
||||||
fit: StackFit.expand,
|
fit: StackFit.expand,
|
||||||
children: [
|
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
|
// Abdunklung für Lesbarkeit
|
||||||
const DecoratedBox(
|
const DecoratedBox(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -473,12 +550,13 @@ class _NowPlayingScreenState extends State<NowPlayingScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Blur-Hintergrund aus dem Cover (Bordmittel: ImageFiltered + dart:ui).
|
/// 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) {
|
Widget _hintergrundBlur(Song? song) {
|
||||||
final hatCover = song != null &&
|
final pfad = _coverPfad;
|
||||||
song.coverPfad != null &&
|
if (!_hatCover || pfad == null) {
|
||||||
File(song.coverPfad!).existsSync();
|
|
||||||
if (!hatCover) {
|
|
||||||
return const DecoratedBox(
|
return const DecoratedBox(
|
||||||
|
key: ValueKey('kein-cover'),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
gradient: LinearGradient(
|
gradient: LinearGradient(
|
||||||
begin: Alignment.topCenter,
|
begin: Alignment.topCenter,
|
||||||
@@ -489,8 +567,9 @@ class _NowPlayingScreenState extends State<NowPlayingScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return ImageFiltered(
|
return ImageFiltered(
|
||||||
|
key: ValueKey('cover-${song?.id ?? pfad}'),
|
||||||
imageFilter: ImageFilter.blur(sigmaX: 45, sigmaY: 45),
|
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.
|
/// Cover-Ansicht: großes Cover (Radius 20 + Glow), Titel, Künstler, Like.
|
||||||
Widget _coverAnsicht(Song? song) {
|
Widget _coverAnsicht(Song? song) {
|
||||||
final hatCover = song != null &&
|
final hatCover = _hatCover && _coverPfad != null;
|
||||||
song.coverPfad != null &&
|
|
||||||
File(song.coverPfad!).existsSync();
|
|
||||||
final coverGroesse =
|
final coverGroesse =
|
||||||
(MediaQuery.of(context).size.width * 0.72).clamp(220.0, 340.0);
|
(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),
|
borderRadius: BorderRadius.circular(20),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: MeloTheme.rot.withValues(alpha: 0.45),
|
// Glow in der Akzentfarbe des Users, pulsierend bei Wiedergabe
|
||||||
blurRadius: 60,
|
color: MeloTheme.akzent.withValues(alpha: 0.45),
|
||||||
spreadRadius: 6,
|
blurRadius: 50 + 20 * _glowController.value, // 50–70
|
||||||
|
spreadRadius: 6 + 6 * _glowController.value, // 6–12
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
child: hatCover
|
child: hatCover
|
||||||
? Image.file(File(song.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,
|
||||||
|
|||||||
Reference in New Issue
Block a user