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:
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user