Files
SpotiFLAC-Mobile/lib/widgets/mornye_player_background.dart

141 lines
5.3 KiB
Dart

import 'package:flutter/material.dart';
import 'package:spotiflac_android/theme/cover_palette.dart';
/// Opaque cover-derived surface, with full-bleed artwork on the player page
/// and a muted two-tone gradient behind lyrics and the queue.
class MornyePlayerBackground extends StatelessWidget {
const MornyePlayerBackground({
super.key,
required this.artUri,
this.artwork,
this.squareArtwork = true,
this.artworkAspectRatio,
});
final Uri? artUri;
final Widget? artwork;
final bool squareArtwork;
final double? artworkAspectRatio;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final source = artUri?.scheme == 'file'
? artUri!.toFilePath()
: artUri?.toString();
return Theme(
// Missing or unreadable covers use neutral gray, not the app's accent.
data: theme.copyWith(
colorScheme: theme.colorScheme.copyWith(
brightness: Brightness.dark,
primary: const Color(0xff808080),
),
),
child: CoverPaletteBuilder(
imageSource: source,
builder: (context, _) {
final dominant = HSLColor.fromColor(
source == null
? const Color(0xff808080)
: CoverPalette.sourceColor(source, Brightness.dark) ??
const Color(0xff808080),
);
final muted = dominant.withSaturation(
dominant.saturation.clamp(0.0, 0.34),
);
// Carry the cover's muted colour through the controls instead of
// fading bright artwork into a nearly black, flat surface.
final backdrop = dominant.withSaturation(
dominant.saturation.clamp(0.0, 0.12),
);
final motion = MediaQuery.disableAnimationsOf(context)
? Duration.zero
: const Duration(milliseconds: 380);
return AnimatedContainer(
duration: motion,
curve: Curves.easeInOutCubic,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: artwork == null
? Alignment.topLeft
: Alignment.topCenter,
end: artwork == null
? Alignment.bottomRight
: Alignment.bottomCenter,
stops: artwork == null ? null : const [0, 0.5, 1],
colors: artwork == null
? [
muted
.withLightness(0.28 + dominant.lightness * 0.24)
.toColor(),
muted
.withLightness(0.14 + dominant.lightness * 0.20)
.toColor(),
]
: [
backdrop.withLightness(0.62).toColor(),
backdrop.withLightness(0.54).toColor(),
backdrop.withLightness(0.34).toColor(),
],
),
),
child: Stack(
fit: StackFit.expand,
children: [
Positioned(
left: 0,
right: 0,
top: 0,
height: artworkAspectRatio != null && artworkAspectRatio! > 0
? (MediaQuery.sizeOf(context).width / artworkAspectRatio!)
.clamp(
0.0,
MediaQuery.sizeOf(context).height * 0.75,
)
: squareArtwork
? MediaQuery.sizeOf(context).width
: MediaQuery.sizeOf(context).height * 0.66,
child: ShaderMask(
blendMode: BlendMode.dstIn,
shaderCallback: (bounds) => const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
stops: [0, 0.58, 1],
colors: [Colors.white, Colors.white, Colors.transparent],
).createShader(bounds),
child: HeroMode(
// The compact header owns the route hero while the
// full cover is fading out behind the lyrics.
enabled: artwork != null,
child: AnimatedSwitcher(
duration: motion,
switchInCurve: Curves.easeInOutCubic,
switchOutCurve: Curves.easeInOutCubic,
layoutBuilder: (current, previous) => Stack(
fit: StackFit.expand,
children: [
for (final child in previous)
HeroMode(enabled: false, child: child),
?current,
],
),
child: artwork == null
? null
: KeyedSubtree(
key: const ValueKey('full-player-artwork'),
child: artwork!,
),
),
),
),
),
],
),
);
},
),
);
}
}