RhythmBox/lib/screens/player/view.dart

434 lines
19 KiB
Dart
Raw Normal View History

2024-08-27 12:49:48 +00:00
import 'dart:async';
2024-08-27 16:04:45 +00:00
import 'dart:math';
2024-08-27 12:49:48 +00:00
import 'package:dismissible_page/dismissible_page.dart';
2024-08-27 06:48:31 +00:00
import 'package:flutter/material.dart';
2024-08-27 12:49:48 +00:00
import 'package:gap/gap.dart';
import 'package:get/get.dart';
import 'package:go_router/go_router.dart';
2024-08-27 12:49:48 +00:00
import 'package:google_fonts/google_fonts.dart';
2024-08-27 17:23:37 +00:00
import 'package:media_kit/media_kit.dart';
2024-08-27 12:49:48 +00:00
import 'package:rhythm_box/providers/audio_player.dart';
2024-08-30 05:23:57 +00:00
import 'package:rhythm_box/providers/auth.dart';
2024-08-27 17:23:37 +00:00
import 'package:rhythm_box/screens/player/queue.dart';
2024-08-28 16:33:59 +00:00
import 'package:rhythm_box/screens/player/siblings.dart';
2024-09-04 16:01:58 +00:00
import 'package:rhythm_box/screens/player/source_details.dart';
2024-08-27 12:49:48 +00:00
import 'package:rhythm_box/services/artist.dart';
import 'package:rhythm_box/services/audio_player/audio_player.dart';
2024-08-28 16:33:59 +00:00
import 'package:rhythm_box/services/duration.dart';
2024-08-27 12:49:48 +00:00
import 'package:rhythm_box/widgets/auto_cache_image.dart';
import 'package:rhythm_box/services/audio_services/image.dart';
2024-08-29 08:42:48 +00:00
import 'package:rhythm_box/widgets/lyrics/synced_lyrics.dart';
2024-08-29 11:10:54 +00:00
import 'package:rhythm_box/widgets/tracks/heart_button.dart';
2024-08-27 12:49:48 +00:00
import 'package:rhythm_box/widgets/tracks/querying_track_info.dart';
2024-08-27 06:48:31 +00:00
class PlayerScreen extends StatefulWidget {
const PlayerScreen({super.key});
2024-08-27 06:48:31 +00:00
@override
State<PlayerScreen> createState() => _PlayerScreenState();
}
class _PlayerScreenState extends State<PlayerScreen> {
2024-08-27 12:49:48 +00:00
late final AudioPlayerProvider _playback = Get.find();
late final QueryingTrackInfoProvider _query = Get.find();
2024-08-30 05:23:57 +00:00
late final AuthenticationProvider _auth = Get.find();
2024-09-02 12:25:19 +00:00
2024-08-27 12:49:48 +00:00
String? get _albumArt =>
(_playback.state.value.activeTrack?.album?.images).asUrlString(
index:
(_playback.state.value.activeTrack?.album?.images?.length ?? 1) - 1,
);
bool get _isPlaying => _playback.isPlaying.value;
bool get _isFetchingActiveTrack => _query.isQueryingTrackInfo.value;
2024-08-27 17:23:37 +00:00
PlaylistMode get _loopMode => _playback.state.value.loopMode;
2024-08-27 12:49:48 +00:00
Future<void> _togglePlayState() async {
if (!audioPlayer.isPlaying) {
await audioPlayer.resume();
} else {
await audioPlayer.pause();
}
setState(() {});
}
double? _draggingValue;
2024-08-28 17:45:33 +00:00
static const double maxAlbumSize = 360;
2024-08-27 06:48:31 +00:00
@override
Widget build(BuildContext context) {
2024-08-27 12:49:48 +00:00
final size = MediaQuery.of(context).size;
2024-08-28 17:45:33 +00:00
final albumSize = max(size.shortestSide, maxAlbumSize).toDouble();
final isLargeScreen = size.width >= 720;
2024-08-27 12:49:48 +00:00
return DismissiblePage(
backgroundColor: Theme.of(context).colorScheme.surface,
onDismissed: () {
Navigator.of(context).pop();
},
direction: DismissiblePageDismissDirection.down,
2024-09-04 15:28:59 +00:00
child: Scaffold(
backgroundColor: Colors.transparent,
body: SafeArea(
child: Center(
child: Row(
children: [
Expanded(
child: ListView(
shrinkWrap: true,
padding: const EdgeInsets.symmetric(vertical: 24),
children: [
Obx(
2024-09-06 10:10:12 +00:00
() => Center(
child: LimitedBox(
maxHeight: maxAlbumSize,
maxWidth: maxAlbumSize,
child: Hero(
tag: const Key('current-active-track-album-art'),
child: AspectRatio(
aspectRatio: 1,
child: ClipRRect(
borderRadius: const BorderRadius.all(
Radius.circular(16),
),
child: _albumArt != null
? AutoCacheImage(
_albumArt!,
width: albumSize,
height: albumSize,
fit: BoxFit.cover,
)
: Container(
color: Theme.of(context)
.colorScheme
.surfaceContainerHigh,
width: 64,
height: 64,
child: const Center(
child: Icon(Icons.image),
),
2024-09-04 15:28:59 +00:00
),
2024-09-06 10:10:12 +00:00
),
).marginSymmetric(horizontal: 24),
),
2024-09-04 15:28:59 +00:00
),
2024-08-29 08:42:48 +00:00
),
2024-08-27 12:49:48 +00:00
),
2024-09-04 15:28:59 +00:00
const Gap(24),
Obx(
() => Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_playback.state.value.activeTrack?.name ??
'Not playing',
style:
Theme.of(context).textTheme.titleLarge,
textAlign: TextAlign.left,
),
Text(
_playback.state.value.activeTrack?.artists
?.asString() ??
'No author',
style:
Theme.of(context).textTheme.bodyMedium,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.left,
),
],
),
),
if (_playback.state.value.activeTrack != null &&
_auth.auth.value != null)
TrackHeartButton(
key: ValueKey(
_playback.state.value.activeTrack!.id!,
),
trackId: _playback.state.value.activeTrack!.id!,
),
],
).paddingSymmetric(horizontal: 32),
),
const Gap(24),
Obx(
() => Column(
children: [
SliderTheme(
data: SliderThemeData(
trackHeight: 2,
trackShape: _PlayerProgressTrackShape(),
thumbShape: const RoundSliderThumbShape(
enabledThumbRadius: 8,
),
overlayShape: SliderComponentShape.noOverlay,
),
child: Slider(
secondaryTrackValue: _playback
.durationBuffered.value.inMilliseconds
.abs()
.toDouble(),
value: _draggingValue?.abs() ??
_playback
.durationCurrent.value.inMilliseconds
.toDouble()
.abs(),
min: 0,
max: max(
_playback.durationCurrent.value.inMilliseconds
.abs(),
_playback.durationTotal.value.inMilliseconds
.abs(),
).toDouble(),
onChanged: (value) {
setState(() => _draggingValue = value);
},
onChangeEnd: (value) {
audioPlayer.seek(
Duration(milliseconds: value.toInt()),
);
setState(() => _draggingValue = null);
},
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
2024-08-29 11:10:54 +00:00
children: [
Text(
2024-09-04 15:28:59 +00:00
_playback.durationCurrent.value
.toHumanReadableString(),
style: GoogleFonts.robotoMono(fontSize: 12),
2024-08-29 11:10:54 +00:00
),
Text(
2024-09-04 15:28:59 +00:00
_playback.durationTotal.value
.toHumanReadableString(),
style: GoogleFonts.robotoMono(fontSize: 12),
2024-08-29 11:10:54 +00:00
),
],
2024-09-04 15:28:59 +00:00
).paddingSymmetric(horizontal: 8, vertical: 4),
],
).paddingSymmetric(horizontal: 24),
),
const Gap(24),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
StreamBuilder<bool>(
stream: audioPlayer.shuffledStream,
builder: (context, snapshot) {
final shuffled = snapshot.data ?? false;
return Obx(
() => IconButton(
icon: Icon(
shuffled
? Icons.shuffle_on_outlined
: Icons.shuffle,
),
onPressed: _isFetchingActiveTrack
? null
: () {
if (shuffled) {
audioPlayer.setShuffle(false);
} else {
audioPlayer.setShuffle(true);
}
},
),
);
},
2024-08-29 11:10:54 +00:00
),
2024-09-04 15:28:59 +00:00
Obx(
() => IconButton(
icon: const Icon(Icons.skip_previous),
onPressed: _isFetchingActiveTrack
? null
: audioPlayer.skipToPrevious,
2024-08-29 11:10:54 +00:00
),
2024-09-04 15:28:59 +00:00
),
const Gap(8),
Obx(
() => SizedBox(
width: 56,
height: 56,
child: IconButton.filled(
icon: _isFetchingActiveTrack
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2.5,
),
)
: Icon(
!_isPlaying
? Icons.play_arrow
: Icons.pause,
size: 28,
),
onPressed: _togglePlayState,
2024-08-28 17:45:33 +00:00
),
),
2024-09-04 15:28:59 +00:00
),
const Gap(8),
Obx(
() => IconButton(
icon: const Icon(Icons.skip_next),
onPressed: _isFetchingActiveTrack
? null
: audioPlayer.skipToNext,
2024-08-28 17:45:33 +00:00
),
),
2024-09-04 15:28:59 +00:00
Obx(
() => IconButton(
icon: Icon(
_loopMode == PlaylistMode.none
? Icons.repeat
: _loopMode == PlaylistMode.loop
? Icons.repeat_on_outlined
: Icons.repeat_one_on_outlined,
2024-08-28 17:45:33 +00:00
),
2024-09-04 15:28:59 +00:00
onPressed: _isFetchingActiveTrack
? null
: () async {
await audioPlayer.setLoopMode(
switch (_loopMode) {
PlaylistMode.loop =>
PlaylistMode.single,
PlaylistMode.single =>
PlaylistMode.none,
PlaylistMode.none =>
PlaylistMode.loop,
},
);
},
2024-08-29 08:42:48 +00:00
),
2024-08-28 17:45:33 +00:00
),
2024-09-04 15:28:59 +00:00
],
),
const Gap(20),
2024-09-06 08:22:39 +00:00
Center(
child: SizedBox(
height: 40,
child: ListView(
scrollDirection: Axis.horizontal,
shrinkWrap: true,
children: [
TextButton.icon(
icon: const Icon(Icons.queue_music),
label: const Text(
'Queue',
maxLines: 1,
overflow: TextOverflow.fade,
),
onPressed: () {
showModalBottomSheet(
useRootNavigator: true,
isScrollControlled: true,
context: context,
builder: (context) =>
const PlayerQueuePopup(),
).then((_) {
if (mounted) {
setState(() {});
}
});
},
2024-09-04 16:01:58 +00:00
),
2024-09-06 08:22:39 +00:00
if (!isLargeScreen) const Gap(4),
if (!isLargeScreen)
TextButton.icon(
icon: const Icon(Icons.lyrics),
label: const Text(
'Lyrics',
maxLines: 1,
overflow: TextOverflow.fade,
),
onPressed: () {
GoRouter.of(context)
.pushNamed('playerLyrics');
},
),
const Gap(4),
2024-09-04 16:01:58 +00:00
TextButton.icon(
2024-09-06 08:22:39 +00:00
icon: const Icon(Icons.merge),
2024-09-04 16:01:58 +00:00
label: const Text(
2024-09-06 08:22:39 +00:00
'Sources',
2024-09-04 16:01:58 +00:00
maxLines: 1,
overflow: TextOverflow.fade,
),
2024-09-04 15:28:59 +00:00
onPressed: () {
2024-09-06 08:22:39 +00:00
showModalBottomSheet(
useRootNavigator: true,
isScrollControlled: true,
context: context,
builder: (context) =>
const SiblingTracksPopup(),
).then((_) {
if (mounted) {
setState(() {});
}
});
2024-09-04 15:28:59 +00:00
},
),
2024-09-06 08:22:39 +00:00
const Gap(4),
TextButton.icon(
label: const Text('Info'),
icon: const Icon(Icons.info),
onPressed: () {
showModalBottomSheet(
useRootNavigator: true,
context: context,
builder: (context) =>
const SourceDetailsPopup(),
).then((_) {
if (mounted) {
setState(() {});
}
});
},
2024-09-04 16:01:58 +00:00
),
2024-09-06 08:22:39 +00:00
],
),
2024-09-04 16:01:58 +00:00
),
2024-09-04 15:28:59 +00:00
),
],
),
2024-08-28 17:45:33 +00:00
),
2024-09-04 15:28:59 +00:00
if (isLargeScreen) const Gap(24),
if (isLargeScreen)
const Expanded(
child: SyncedLyrics(defaultTextZoom: 67),
)
],
),
2024-08-27 12:49:48 +00:00
),
2024-08-30 04:56:28 +00:00
).marginSymmetric(horizontal: 24),
2024-08-27 12:49:48 +00:00
),
);
}
}
class _PlayerProgressTrackShape extends RoundedRectSliderTrackShape {
@override
Rect getPreferredRect({
required RenderBox parentBox,
Offset offset = Offset.zero,
required SliderThemeData sliderTheme,
bool isEnabled = false,
bool isDiscrete = false,
}) {
final trackHeight = sliderTheme.trackHeight;
final trackLeft = offset.dx;
final trackTop = offset.dy + (parentBox.size.height - trackHeight!) / 2;
final trackWidth = parentBox.size.width;
return Rect.fromLTWH(trackLeft, trackTop, trackWidth, trackHeight);
2024-08-27 06:48:31 +00:00
}
}