mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-10-21 15:02:21 +02:00
Offer default and full-screen artwork in Material and Mornye, retaining landscape layouts and full-screen Mornye motion artwork. Keep the Material header readable over bright covers and normalize invalid saved styles without resetting other settings. Validation: 144 related Flutter tests, flutter analyze, generated settings, formatter and diff checks.
111 lines
3.8 KiB
Dart
111 lines
3.8 KiB
Dart
import 'dart:ui';
|
|
|
|
import 'package:audio_service/audio_service.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:spotiflac_android/routes/now_playing_route.dart';
|
|
import 'package:spotiflac_android/widgets/player_artwork.dart';
|
|
|
|
/// Full-screen cover style for the Material player: the cover fills the top
|
|
/// of the page edge to edge, then fades into a blurred copy of itself.
|
|
///
|
|
/// The blurred layer is tinted with the surface colour so titles, controls
|
|
/// and lyrics keep the same contrast as on the plain surface.
|
|
class MaterialPlayerBackdrop extends StatelessWidget {
|
|
const MaterialPlayerBackdrop({
|
|
super.key,
|
|
required this.mediaItem,
|
|
required this.colorScheme,
|
|
required this.showArtwork,
|
|
});
|
|
|
|
final MediaItem mediaItem;
|
|
final ColorScheme colorScheme;
|
|
|
|
/// False behind lyrics, where only the blurred surface remains.
|
|
final bool showArtwork;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final size = MediaQuery.sizeOf(context);
|
|
final artUri = mediaItem.artUri?.toString();
|
|
final appBarHeight = MediaQuery.paddingOf(context).top + kToolbarHeight;
|
|
final motion = MediaQuery.disableAnimationsOf(context)
|
|
? Duration.zero
|
|
: const Duration(milliseconds: 300);
|
|
return Stack(
|
|
fit: StackFit.expand,
|
|
children: [
|
|
ColoredBox(color: colorScheme.surface),
|
|
ImageFiltered(
|
|
imageFilter: ImageFilter.blur(sigmaX: 48, sigmaY: 48),
|
|
child: PlayerArtwork(
|
|
artUri: artUri,
|
|
colorScheme: colorScheme,
|
|
cacheWidth: 128,
|
|
),
|
|
),
|
|
ColoredBox(color: colorScheme.surface.withValues(alpha: 0.72)),
|
|
Positioned(
|
|
top: 0,
|
|
left: 0,
|
|
right: 0,
|
|
// Taller than wide on phones: the sides are cropped so the cover
|
|
// reaches down to the title instead of leaving an empty band.
|
|
height: size.width < size.height * 0.56
|
|
? size.height * 0.56
|
|
: size.width.clamp(0.0, size.height * 0.62),
|
|
child: AnimatedOpacity(
|
|
opacity: showArtwork ? 1 : 0,
|
|
duration: motion,
|
|
curve: Curves.easeInOutCubic,
|
|
child: ShaderMask(
|
|
blendMode: BlendMode.dstIn,
|
|
shaderCallback: (bounds) => const LinearGradient(
|
|
begin: Alignment.topCenter,
|
|
end: Alignment.bottomCenter,
|
|
stops: [0, 0.72, 1],
|
|
colors: [Colors.white, Colors.white, Colors.transparent],
|
|
).createShader(bounds),
|
|
child: Hero(
|
|
tag: kNowPlayingArtworkHeroTag,
|
|
child: PlayerArtwork(
|
|
artUri: artUri,
|
|
colorScheme: colorScheme,
|
|
cacheWidth: PlayerArtwork.transitionCacheWidth(context),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
// Keeps the status bar and app bar readable over bright covers.
|
|
Positioned(
|
|
top: 0,
|
|
left: 0,
|
|
right: 0,
|
|
height: appBarHeight + 32,
|
|
child: AnimatedOpacity(
|
|
opacity: showArtwork ? 1 : 0,
|
|
duration: motion,
|
|
child: DecoratedBox(
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
begin: Alignment.topCenter,
|
|
end: Alignment.bottomCenter,
|
|
// Keep the entire bar readable, then fade below it. Fading
|
|
// from the top left too little shade behind white icons.
|
|
stops: [0, appBarHeight / (appBarHeight + 32), 1],
|
|
colors: const [
|
|
Color(0x99000000),
|
|
Color(0x99000000),
|
|
Color(0x00000000),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|