feat(player): expand compact track action menus

Add favorite, playlist, sharing, and artist navigation actions. Use denser menu typography and prefer placement above the invoking control.
This commit is contained in:
zarzet committed 2026-09-25 01:22:03 +07:00
1 parent cec9d5cbce
commit 3d05383e00
4 files changed
+202 -57

No files matched your search

+43 -15
View File
@@ -17,6 +17,7 @@ Future<T?> showMornyeContextMenu<T>({
required BuildContext context,
required WidgetBuilder builder,
Rect? anchor,
bool preferAbove = false,
}) {
final navigator = Navigator.of(context, rootNavigator: true);
final themes = InheritedTheme.capture(from: context, to: navigator.context);
@@ -40,11 +41,13 @@ Future<T?> showMornyeContextMenu<T>({
child: CustomSingleChildLayout(
delegate: _MenuLayout(
anchor: target,
preferAbove: preferAbove,
padding: media.padding.copyWith(
bottom: math.max(media.padding.bottom, media.viewInsets.bottom),
),
),
child: ScaleTransition(
alignment: preferAbove ? Alignment.bottomRight : Alignment.center,
scale: animation.drive(Tween<double>(begin: 0.96, end: 1)),
child: themes.wrap(Builder(builder: builder)),
),
@@ -56,10 +59,15 @@ Future<T?> showMornyeContextMenu<T>({
}
class _MenuLayout extends SingleChildLayoutDelegate {
const _MenuLayout({required this.anchor, required this.padding});
const _MenuLayout({
required this.anchor,
required this.padding,
required this.preferAbove,
});
final Rect? anchor;
final EdgeInsets padding;
final bool preferAbove;
static const double _margin = 16;
@override
@@ -68,14 +76,13 @@ class _MenuLayout extends SingleChildLayoutDelegate {
320.0,
math.max(0.0, constraints.maxWidth - padding.horizontal - _margin * 2),
);
return BoxConstraints(
minWidth: width,
maxWidth: width,
maxHeight: math.max(
0,
constraints.maxHeight - padding.vertical - _margin * 2,
),
var height = math.max(
0.0,
constraints.maxHeight - padding.vertical - _margin * 2,
);
final above = (anchor?.top ?? 0) - padding.top - _margin - 8;
if (preferAbove && above >= 160) height = math.min(height, above);
return BoxConstraints(minWidth: width, maxWidth: width, maxHeight: height);
}
@override
@@ -93,17 +100,32 @@ class _MenuLayout extends SingleChildLayoutDelegate {
final x = anchor == null
? (size.width - childSize.width) / 2
: anchor!.right - childSize.width;
var y = anchor == null ? size.height * 0.18 : anchor!.bottom + 8;
if (anchor != null && y > bottom) {
y = anchor!.top - childSize.height - 8;
if (y < top) y = bottom;
var y = size.height * 0.18;
if (anchor != null) {
final above = anchor!.top - childSize.height - 8;
final below = anchor!.bottom + 8;
if (preferAbove) {
y = above >= top
? above
: below <= bottom
? below
: top;
} else {
y = below <= bottom
? below
: above >= top
? above
: bottom;
}
}
return Offset(x.clamp(left, right), y.clamp(top, bottom));
}
@override
bool shouldRelayout(_MenuLayout oldDelegate) =>
anchor != oldDelegate.anchor || padding != oldDelegate.padding;
anchor != oldDelegate.anchor ||
padding != oldDelegate.padding ||
preferAbove != oldDelegate.preferAbove;
}
class MornyeMenuAction {
@@ -131,11 +153,13 @@ class MornyeContextMenu extends StatelessWidget {
this.quickActions = const [],
required this.groups,
this.inheritSurface = false,
this.dense = false,
});
final List<MornyeMenuAction> quickActions;
final List<List<MornyeMenuAction>> groups;
final bool inheritSurface;
final bool dense;
@override
Widget build(BuildContext context) {
@@ -222,7 +246,11 @@ class MornyeContextMenu extends StatelessWidget {
textAlign: compact ? TextAlign.center : TextAlign.start,
style: theme.textTheme.bodyLarge?.copyWith(
color: color,
fontSize: compact ? 13 : 17,
fontSize: compact
? 13
: dense
? 15
: 17,
fontWeight: compact ? FontWeight.w600 : FontWeight.w400,
),
);
@@ -231,7 +259,7 @@ class MornyeContextMenu extends StatelessWidget {
child: CupertinoButton(
padding: compact
? const EdgeInsets.symmetric(horizontal: 4, vertical: 10)
: const EdgeInsets.symmetric(horizontal: 20, vertical: 13),
: EdgeInsets.symmetric(horizontal: 20, vertical: dense ? 10 : 13),
onPressed: action.onPressed,
child: compact
? Column(
+87 -36
View File
@@ -1,7 +1,10 @@
import 'package:audio_service/audio_service.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotiflac_android/l10n/l10n.dart';
import 'package:spotiflac_android/providers/library_collections_provider.dart';
import 'package:spotiflac_android/providers/music_player_provider.dart';
import 'package:spotiflac_android/theme/cover_palette.dart';
import 'package:spotiflac_android/theme/mornye_theme.dart';
import 'package:spotiflac_android/widgets/mornye_context_menu.dart';
@@ -16,16 +19,20 @@ class MornyePlayerNavigationMenu extends StatelessWidget {
Widget build(BuildContext context) {
final theme = MornyeTheme.build(Brightness.dark);
final art = mediaItem.artUri;
final source = art?.scheme == 'file' ? art!.toFilePath() : art?.toString();
return Theme(
data: theme.copyWith(
colorScheme: theme.colorScheme.copyWith(primary: Colors.grey),
),
child: CoverPaletteBuilder(
imageSource: art?.scheme == 'file'
? art!.toFilePath()
: art?.toString(),
builder: (context, palette) {
final dominant = HSLColor.fromColor(palette.primary);
imageSource: source,
builder: (context, _) {
final dominant = HSLColor.fromColor(
source == null
? Colors.grey
: CoverPalette.sourceColor(source, Brightness.dark) ??
Colors.grey,
);
final surface = dominant
.withSaturation(dominant.saturation.clamp(0.0, 0.28))
.withLightness(0.28)
@@ -38,6 +45,7 @@ class MornyePlayerNavigationMenu extends StatelessWidget {
),
child: MornyeContextMenu(
inheritSurface: true,
dense: true,
groups: [
[
if ((mediaItem.artist ?? '').trim().isNotEmpty)
@@ -65,7 +73,7 @@ class MornyePlayerNavigationMenu extends StatelessWidget {
}
/// A floating menu that inherits the player's dark appearance.
class MornyePlayerActionsSheet extends StatelessWidget {
class MornyePlayerActionsSheet extends ConsumerWidget {
const MornyePlayerActionsSheet({
super.key,
required this.mediaItem,
@@ -76,40 +84,81 @@ class MornyePlayerActionsSheet extends StatelessWidget {
final String? sleepTimerSubtitle;
@override
Widget build(BuildContext context) => MornyeContextMenu(
groups: [
[
if ((mediaItem.album ?? '').trim().isNotEmpty)
Widget build(BuildContext context, WidgetRef ref) {
final track = ref.watch(playerCollectionTrackProvider(mediaItem)).value;
final loved = ref.watch(
libraryCollectionsProvider.select(
(state) => track != null && state.isLoved(track),
),
);
return MornyeContextMenu(
dense: true,
quickActions: [
_action(
context,
'favorite',
loved ? context.l10n.mornyeFavorited : context.l10n.mornyeFavorite,
CupertinoIcons.star_fill,
selected: loved,
),
_action(
context,
'share',
context.l10n.trackMetadataShare,
CupertinoIcons.share_solid,
),
],
groups: [
[
_action(
context,
'album',
context.l10n.homeGoToAlbum,
CupertinoIcons.square_stack,
'playlist',
context.l10n.collectionAddToPlaylist,
CupertinoIcons.text_badge_plus,
),
_action(
context,
'details',
context.l10n.nowPlayingDetails,
CupertinoIcons.info,
),
],
[
if ((mediaItem.album ?? '').trim().isNotEmpty)
_action(
context,
'album',
context.l10n.homeGoToAlbum,
CupertinoIcons.square_stack,
subtitle: mediaItem.album,
),
if ((mediaItem.artist ?? '').trim().isNotEmpty)
_action(
context,
'artist',
context.l10n.mornyeGoToArtist,
CupertinoIcons.mic,
subtitle: mediaItem.artist,
),
_action(
context,
'details',
context.l10n.nowPlayingDetails,
CupertinoIcons.info,
),
],
[
_action(
context,
'sleepTimer',
context.l10n.nowPlayingSleepTimer,
CupertinoIcons.moon_zzz,
subtitle: sleepTimerSubtitle,
),
_action(
context,
'external',
context.l10n.nowPlayingOpenInExternalPlayer,
CupertinoIcons.arrow_up_right_square,
),
],
],
[
_action(
context,
'sleepTimer',
context.l10n.nowPlayingSleepTimer,
CupertinoIcons.moon_zzz,
subtitle: sleepTimerSubtitle,
),
_action(
context,
'external',
context.l10n.nowPlayingOpenInExternalPlayer,
CupertinoIcons.arrow_up_right_square,
),
],
],
);
);
}
MornyeMenuAction _action(
BuildContext context,
@@ -117,10 +166,12 @@ class MornyePlayerActionsSheet extends StatelessWidget {
String label,
IconData icon, {
String? subtitle,
bool selected = false,
}) => MornyeMenuAction(
icon: icon,
label: label,
subtitle: subtitle,
selected: selected,
onPressed: () => Navigator.of(context).pop(value),
);
}