Files
SpotiFLAC-Mobile/lib/widgets/mornye_player_background.dart
T
zarzet e61ff64adc feat(mornye): cache motion covers and refine player and search
Save album motion videos in app storage during downloads and prefer offline playback with an aspect-aware layout. Add cache management for saved videos.

Center active lyrics on opening, expand lyrics during manual scrolling, and adjust landscape header spacing. Group search rows and skeletons with dividers and remove redundant result icons.

Validation: Flutter analysis clean; 723 tests passed, 4 skipped; iOS simulator build installed.
2026-09-19 17:26:26 +07:00

118 lines
4.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, scheme) {
final dominant = HSLColor.fromColor(scheme.primary);
final muted = dominant.withSaturation(
dominant.saturation.clamp(0.0, 0.28),
);
final motion = MediaQuery.disableAnimationsOf(context)
? Duration.zero
: const Duration(milliseconds: 380);
return AnimatedContainer(
duration: motion,
curve: Curves.easeInOutCubic,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
muted.withLightness(artwork == null ? 0.28 : 0.10).toColor(),
muted.withLightness(artwork == null ? 0.15 : 0.10).toColor(),
],
),
),
child: Stack(
fit: StackFit.expand,
children: [
AnimatedPositioned(
duration: motion,
curve: Curves.easeInOutCubic,
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!,
),
),
),
),
),
],
),
);
},
),
);
}
}