Files
SpotiFLAC-Mobile/lib/widgets/material_player_backdrop.dart
T
zarzet 05a5613332 feat(player): add configurable full-screen cover art
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.
2026-10-17 22:27:06 +07:00

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),
],
),
),
),
),
),
],
);
}
}