mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-10-01 05:39:34 +02:00
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:
1 parent
cec9d5cbce
commit
3d05383e00
4 files changed
+202
-57
No files matched your search
@@ -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(
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user