Files
SpotiFLAC-Mobile/lib/widgets/mornye_player_queue.dart

239 lines
9.4 KiB
Dart

import 'package:audio_service/audio_service.dart';
import 'package:flutter/cupertino.dart' show CupertinoIcons;
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotiflac_android/l10n/l10n.dart';
import 'package:spotiflac_android/providers/music_player_provider.dart';
import 'package:spotiflac_android/providers/settings_provider.dart';
import 'package:spotiflac_android/widgets/player_artwork.dart';
import 'package:spotiflac_android/widgets/player_queue_dismissible.dart';
/// Upcoming tracks share the player's artwork backdrop and transport controls.
class MornyePlayerQueue extends ConsumerWidget {
const MornyePlayerQueue({super.key, required this.colorScheme});
final ColorScheme colorScheme;
@override
Widget build(BuildContext context, WidgetRef ref) {
final queue = ref.watch(playQueueProvider).value ?? const <MediaItem>[];
final currentId = ref.watch(
currentMediaItemProvider.select((item) => item.value?.id),
);
final playback = ref.watch(
playbackStateProvider.select(
(state) => (
index: state.value?.queueIndex,
shuffle: state.value?.shuffleMode == AudioServiceShuffleMode.all,
repeat: state.value?.repeatMode ?? AudioServiceRepeatMode.none,
),
),
);
final reportedIndex = playback.index;
final currentIndex =
reportedIndex != null &&
reportedIndex >= 0 &&
reportedIndex < queue.length &&
queue[reportedIndex].id == currentId
? reportedIndex
: queue.indexWhere((item) => item.id == currentId);
final start = currentIndex + 1;
final count = queue.length - start;
final controller = ref.read(musicPlayerControllerProvider);
final autoMix = ref.watch(settingsProvider.select((s) => s.autoMix));
final autoplay = ref.watch(settingsProvider.select((s) => s.autoplay));
final type = Theme.of(context).textTheme;
final repeatLabel = switch (playback.repeat) {
AudioServiceRepeatMode.one => context.l10n.nowPlayingRepeatOne,
AudioServiceRepeatMode.none => context.l10n.nowPlayingRepeatOff,
_ => context.l10n.nowPlayingRepeatAll,
};
Widget modeButton({
required IconData icon,
required String label,
required bool selected,
required VoidCallback onPressed,
}) => Expanded(
child: Semantics(
selected: selected,
child: Tooltip(
message: label,
child: FilledButton(
style: FilledButton.styleFrom(
backgroundColor: Colors.white.withValues(
alpha: selected ? 0.3 : 0.12,
),
foregroundColor: Colors.white,
minimumSize: const Size(0, 44),
padding: const EdgeInsets.symmetric(horizontal: 8),
shape: const StadiumBorder(),
),
onPressed: onPressed,
child: Icon(icon, semanticLabel: label, size: 24),
),
),
),
);
return CustomScrollView(
key: const PageStorageKey('mornye-player-queue'),
slivers: [
SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 28),
sliver: SliverToBoxAdapter(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
modeButton(
icon: CupertinoIcons.arrow_merge,
label: autoMix
? context.l10n.autoMixOn
: context.l10n.autoMixOff,
selected: autoMix,
onPressed: () => ref
.read(settingsProvider.notifier)
.setAutoMix(!autoMix),
),
const SizedBox(width: 12),
modeButton(
icon: CupertinoIcons.shuffle,
label: playback.shuffle
? context.l10n.nowPlayingShuffleOn
: context.l10n.nowPlayingPlayInOrder,
selected: playback.shuffle,
onPressed: () => controller.setShuffle(!playback.shuffle),
),
const SizedBox(width: 12),
modeButton(
icon: playback.repeat == AudioServiceRepeatMode.one
? CupertinoIcons.repeat_1
: CupertinoIcons.repeat,
label: repeatLabel,
selected: playback.repeat != AudioServiceRepeatMode.none,
onPressed: () =>
controller.setRepeatMode(switch (playback.repeat) {
AudioServiceRepeatMode.none =>
AudioServiceRepeatMode.all,
AudioServiceRepeatMode.all =>
AudioServiceRepeatMode.one,
_ => AudioServiceRepeatMode.none,
}),
),
const SizedBox(width: 12),
modeButton(
icon: CupertinoIcons.infinite,
label: autoplay
? context.l10n.autoplayOn
: context.l10n.autoplayOff,
selected: autoplay,
onPressed: () => ref
.read(settingsProvider.notifier)
.setAutoplay(!autoplay),
),
],
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
context.l10n.mornyeContinuePlaying,
style: type.titleLarge?.copyWith(color: Colors.white),
),
if (autoplay &&
playback.repeat == AudioServiceRepeatMode.none)
Text(
context.l10n.autoplayFromLibrary,
style: type.bodySmall?.copyWith(
color: Colors.white70,
),
),
],
),
),
],
),
),
),
if (count == 0)
SliverPadding(
padding: const EdgeInsets.all(28),
sliver: SliverToBoxAdapter(
child: Text(
context.l10n.nowPlayingQueueEmpty,
style: type.bodyMedium?.copyWith(color: Colors.white70),
),
),
)
else
SliverPadding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
sliver: SliverReorderableList(
itemCount: count,
onReorderItem: (oldIndex, newIndex) =>
controller.moveQueueItem(start + oldIndex, start + newIndex),
proxyDecorator: (child, index, animation) => Material(
color: colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(12),
child: child,
),
itemBuilder: (context, index) {
final item = queue[start + index];
return PlayerQueueDismissible(
key: ObjectKey(item),
onRemove: () => controller.removeQueuedItem(item),
child: ListTile(
contentPadding: const EdgeInsets.only(left: 12),
minVerticalPadding: 6,
leading: ClipRRect(
borderRadius: BorderRadius.circular(5),
child: SizedBox.square(
dimension: 40,
child: PlayerArtwork(
artUri: item.artUri?.toString(),
colorScheme: colorScheme,
cacheWidth: 120,
iconSize: 22,
),
),
),
title: Text(
item.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: type.bodyLarge?.copyWith(color: Colors.white),
),
subtitle: Text(
item.artist ?? '',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: type.bodySmall?.copyWith(color: Colors.white70),
),
trailing: ReorderableDragStartListener(
index: index,
child: const ColoredBox(
color: Colors.transparent,
child: SizedBox.square(
dimension: 44,
child: Icon(
CupertinoIcons.line_horizontal_3,
color: Colors.white38,
),
),
),
),
onTap: () => controller.jumpTo(start + index),
),
);
},
),
),
],
);
}
}