Files
SpotiFLAC-Mobile/lib/widgets/mornye_player_background.dart
T
zarzet 63e1a9b9c8 feat(ui): add Mornye glass theme and adaptive music player
Add a selectable Mornye theme with shared glass controls, dialogs, sheets, notifications, and collection headers while retaining Material components.

Update player artwork, lyrics transitions and highlighting, synchronized rolling time labels, volume controls, and compact navigation. Unify metadata, Library, search, and extension surfaces; preserve restored artwork and report extension load failures.

Validation: full Flutter analyzer clean; 713 tests passed and 4 skipped; pinned Dart formatting and diff checks passed; signed ARM64 and ARM32 release APKs built and verified.
2026-09-19 12:10:10 +07:00

108 lines
3.9 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,
});
final Uri? artUri;
final Widget? artwork;
final bool squareArtwork;
@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: [
Positioned(
left: 0,
right: 0,
top: 0,
height: 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!,
),
),
),
),
),
],
),
);
},
),
);
}
}