Cover-Farbe, Teilen aus YouTube, Android Auto, geschärfte Typografie

P3-Liste aus dem UX-Review: Player nimmt Cover-Farbe auf, "Teilen" aus
YouTube trägt die Adresse in den Downloader ein, Android Auto blättert
durch die Bibliothek, Laufweite der Typografie geschärft, Überschriften
vereinheitlicht. 533 Tests grün, flutter analyze ohne Befund.
This commit is contained in:
Hermes (Server)
2026-08-25 09:09:27 +02:00
parent 490e73ce41
commit 60c57577a3
22 changed files with 1040 additions and 40 deletions
+82 -1
View File
@@ -1,3 +1,5 @@
import 'dart:async';
import 'package:audio_service/audio_service.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
@@ -9,6 +11,7 @@ import '../shared/favorite_button.dart';
import '../shared/server_favorite_button.dart';
import '../shared/theme.dart';
import 'audio_handler.dart';
import 'cover_farbe.dart';
import 'lrc.dart';
import 'queue_screen.dart';
@@ -27,7 +30,12 @@ class NowPlayingScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final handler = context.read<MeloAudioHandler>();
return Scaffold(
return _CoverGrund(
handler: handler,
child: Scaffold(
// Der Grund kommt aus dem Coverbild — deshalb hier durchsichtig.
backgroundColor: Colors.transparent,
extendBodyBehindAppBar: true,
appBar: AppBar(
backgroundColor: Colors.transparent,
actions: [
@@ -127,6 +135,79 @@ class NowPlayingScreen extends StatelessWidget {
},
),
),
),
);
}
}
/// Färbt den Grund des Players nach dem Coverbild ein.
///
/// Der Player sah für jeden Titel gleich aus — schwarz. Ein Verlauf aus der
/// prägenden Farbe des Covers gibt jedem Album sein eigenes Gesicht, ohne
/// dass die Schrift darunter leidet: die Farbe wird vorher so weit
/// abgedunkelt, dass Weiß darauf lesbar bleibt (siehe [lesbarAuf]).
class _CoverGrund extends StatefulWidget {
const _CoverGrund({required this.handler, required this.child});
final MeloAudioHandler handler;
final Widget child;
@override
State<_CoverGrund> createState() => _CoverGrundState();
}
class _CoverGrundState extends State<_CoverGrund> {
Color _farbe = MeloTheme.black;
Uri? _zuletzt;
StreamSubscription<MediaItem?>? _abo;
@override
void initState() {
super.initState();
_abo = widget.handler.mediaItem.listen(_uebernimm);
}
@override
void dispose() {
_abo?.cancel();
super.dispose();
}
Future<void> _uebernimm(MediaItem? item) async {
final art = item?.artUri;
// Nur bei einem echten Bildwechsel neu rechnen: `mediaItem` meldet auch
// nachgetragene Dauern, und jedes Mal ein Bild zu dekodieren wäre Unfug.
if (art == _zuletzt) return;
_zuletzt = art;
if (art == null) {
if (mounted) setState(() => _farbe = MeloTheme.black);
return;
}
final bild = await coverBildQuelle(art);
if (bild == null) {
if (mounted) setState(() => _farbe = MeloTheme.black);
return;
}
final farbe = lesbarAuf(await farbeAusBild(bild));
if (mounted && art == _zuletzt) setState(() => _farbe = farbe);
}
@override
Widget build(BuildContext context) {
return AnimatedContainer(
duration: MeloMotion.ruhig(context, MeloMotion.normal),
curve: MeloMotion.curve,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
// Nach unten in Schwarz auslaufen: dort sitzen Fortschritt und
// Bedienung, und die stehen auf dem gewohnten Grund am ruhigsten.
colors: [_farbe, MeloTheme.black],
stops: const [0.0, 0.75],
),
),
child: widget.child,
);
}
}