Full screen player

This commit is contained in:
LittleSheep 2024-08-27 20:49:48 +08:00
parent 2e17078fea
commit a2bc08bbd9
9 changed files with 262 additions and 23 deletions

View File

@ -59,6 +59,7 @@ class MyApp extends StatelessWidget {
void _initializeProviders(BuildContext context) async { void _initializeProviders(BuildContext context) async {
Get.lazyPut(() => SpotifyProvider()); Get.lazyPut(() => SpotifyProvider());
Get.put(AudioPlayerProvider());
Get.put(ActiveSourcedTrackProvider()); Get.put(ActiveSourcedTrackProvider());
Get.put(AudioPlayerStreamProvider()); Get.put(AudioPlayerStreamProvider());
@ -69,7 +70,6 @@ class MyApp extends StatelessWidget {
Get.put(ScrobblerProvider()); Get.put(ScrobblerProvider());
Get.put(UserPreferencesProvider()); Get.put(UserPreferencesProvider());
Get.put(AudioPlayerProvider());
Get.put(QueryingTrackInfoProvider()); Get.put(QueryingTrackInfoProvider());
Get.put(SourcedTrackProvider()); Get.put(SourcedTrackProvider());

View File

@ -128,11 +128,11 @@ class AudioPlayerProvider extends GetxController {
// Giving the initial track a boost so MediaKit won't skip // Giving the initial track a boost so MediaKit won't skip
// because of timeout // because of timeout
final intendedActiveTrack = medias.elementAt(initialIndex); // final intendedActiveTrack = medias.elementAt(initialIndex);
if (intendedActiveTrack.track is! LocalTrack) { // if (intendedActiveTrack.track is! LocalTrack) {
await Get.find<SourcedTrackProvider>() // await Get.find<SourcedTrackProvider>()
.fetch(RhythmMedia(intendedActiveTrack.track)); // .fetch(RhythmMedia(intendedActiveTrack.track));
} // }
if (medias.isEmpty) return; if (medias.isEmpty) return;

View File

@ -20,12 +20,15 @@ class AudioPlayerStreamProvider extends GetxController {
List<StreamSubscription>? _subscriptions; List<StreamSubscription>? _subscriptions;
@override AudioPlayerStreamProvider() {
void onInit() {
super.onInit();
AudioServices.create().then( AudioServices.create().then(
(value) => notificationService = value, (value) => notificationService = value,
); );
}
@override
void onInit() {
super.onInit();
_subscriptions = [ _subscriptions = [
subscribeToPlaylist(), subscribeToPlaylist(),

View File

@ -1,6 +1,5 @@
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:rhythm_box/screens/explore.dart'; import 'package:rhythm_box/screens/explore.dart';
import 'package:rhythm_box/screens/player/view.dart';
import 'package:rhythm_box/screens/playlist/view.dart'; import 'package:rhythm_box/screens/playlist/view.dart';
import 'package:rhythm_box/screens/settings.dart'; import 'package:rhythm_box/screens/settings.dart';
import 'package:rhythm_box/shells/nav_shell.dart'; import 'package:rhythm_box/shells/nav_shell.dart';
@ -28,9 +27,4 @@ final router = GoRouter(routes: [
), ),
], ],
), ),
GoRoute(
path: '/player',
name: 'player',
builder: (context, state) => const PlayerScreen(),
),
]); ]);

View File

@ -1,15 +1,235 @@
import 'dart:async';
import 'package:dismissible_page/dismissible_page.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:get/get.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:rhythm_box/providers/audio_player.dart';
import 'package:rhythm_box/services/artist.dart';
import 'package:rhythm_box/services/audio_player/audio_player.dart';
import 'package:rhythm_box/widgets/auto_cache_image.dart';
import 'package:rhythm_box/services/audio_services/image.dart';
import 'package:rhythm_box/widgets/tracks/querying_track_info.dart';
class PlayerScreen extends StatefulWidget { class PlayerScreen extends StatefulWidget {
const PlayerScreen({super.key}); final Duration durationCurrent, durationTotal;
const PlayerScreen({
super.key,
required this.durationCurrent,
required this.durationTotal,
});
@override @override
State<PlayerScreen> createState() => _PlayerScreenState(); State<PlayerScreen> createState() => _PlayerScreenState();
} }
class _PlayerScreenState extends State<PlayerScreen> { class _PlayerScreenState extends State<PlayerScreen> {
late final AudioPlayerProvider _playback = Get.find();
late final QueryingTrackInfoProvider _query = Get.find();
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;
Duration _durationCurrent = Duration.zero;
Duration _durationTotal = Duration.zero;
void _updateDurationCurrent(Duration dur) {
setState(() => _durationCurrent = dur);
}
void _updateDurationTotal(Duration dur) {
setState(() => _durationTotal = dur);
}
List<StreamSubscription>? _subscriptions;
Future<void> _togglePlayState() async {
if (!audioPlayer.isPlaying) {
await audioPlayer.resume();
} else {
await audioPlayer.pause();
}
setState(() {});
}
String _formatDuration(Duration duration) {
String negativeSign = duration.isNegative ? '-' : '';
String twoDigits(int n) => n.toString().padLeft(2, '0');
String twoDigitMinutes = twoDigits(duration.inMinutes.abs());
String twoDigitSeconds = twoDigits(duration.inSeconds.remainder(60).abs());
return '$negativeSign$twoDigitMinutes:$twoDigitSeconds';
}
double? _draggingValue;
@override
void initState() {
super.initState();
_durationCurrent = widget.durationCurrent;
_durationTotal = widget.durationTotal;
_subscriptions = [
audioPlayer.durationStream.listen(_updateDurationTotal),
audioPlayer.positionStream.listen(_updateDurationCurrent),
];
}
@override
void dispose() {
if (_subscriptions != null) {
for (final subscription in _subscriptions!) {
subscription.cancel();
}
}
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return const Placeholder(); final size = MediaQuery.of(context).size;
return DismissiblePage(
backgroundColor: Theme.of(context).colorScheme.surface,
onDismissed: () {
Navigator.of(context).pop();
},
direction: DismissiblePageDismissDirection.down,
child: Material(
color: Colors.transparent,
child: SafeArea(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Hero(
tag: const Key('current-active-track-album-art'),
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(16)),
child: AspectRatio(
aspectRatio: 1,
child: _albumArt != null
? AutoCacheImage(
_albumArt!,
width: size.width,
height: size.width,
)
: Container(
color: Theme.of(context)
.colorScheme
.surfaceContainerHigh,
width: 64,
height: 64,
child: const Center(child: Icon(Icons.image)),
),
),
).marginSymmetric(horizontal: 24),
),
const Gap(24),
Text(
_playback.state.value.activeTrack?.name ?? 'Not playing',
style: Theme.of(context).textTheme.titleLarge,
),
Text(
_playback.state.value.activeTrack?.artists?.asString() ??
'No author',
style: Theme.of(context).textTheme.bodyMedium,
overflow: TextOverflow.ellipsis,
),
const Gap(24),
Column(
children: [
SliderTheme(
data: SliderThemeData(
trackHeight: 2,
trackShape: _PlayerProgressTrackShape(),
thumbShape: const RoundSliderThumbShape(
enabledThumbRadius: 8,
),
overlayShape: SliderComponentShape.noOverlay,
),
child: Slider(
value: _draggingValue ??
_durationCurrent.inMilliseconds.toDouble(),
min: 0,
max: _durationTotal.inMilliseconds.toDouble(),
onChanged: (value) {
setState(() => _draggingValue = value);
},
onChangeEnd: (value) {
print('Seek to $value ms');
audioPlayer.seek(Duration(milliseconds: value.toInt()));
},
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
_formatDuration(_durationCurrent),
style: GoogleFonts.robotoMono(fontSize: 12),
),
Text(
_formatDuration(_durationTotal),
style: GoogleFonts.robotoMono(fontSize: 12),
),
],
).paddingSymmetric(horizontal: 8, vertical: 4),
],
).paddingSymmetric(horizontal: 24),
const Gap(24),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
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:
_isFetchingActiveTrack ? null : _togglePlayState,
),
),
],
)
],
),
).marginAll(24),
),
);
}
}
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);
} }
} }

View File

@ -1,11 +1,12 @@
import 'dart:async'; import 'dart:async';
import 'dart:math'; import 'dart:math';
import 'package:dismissible_page/dismissible_page.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:go_router/go_router.dart';
import 'package:rhythm_box/providers/audio_player.dart'; import 'package:rhythm_box/providers/audio_player.dart';
import 'package:rhythm_box/screens/player/view.dart';
import 'package:rhythm_box/services/audio_player/audio_player.dart'; import 'package:rhythm_box/services/audio_player/audio_player.dart';
import 'package:rhythm_box/services/audio_services/image.dart'; import 'package:rhythm_box/services/audio_services/image.dart';
import 'package:rhythm_box/widgets/auto_cache_image.dart'; import 'package:rhythm_box/widgets/auto_cache_image.dart';
@ -71,6 +72,12 @@ class _BottomPlayerState extends State<BottomPlayer>
_subscriptions = [ _subscriptions = [
audioPlayer.durationStream.listen(_updateDurationTotal), audioPlayer.durationStream.listen(_updateDurationTotal),
audioPlayer.positionStream.listen(_updateDurationCurrent), audioPlayer.positionStream.listen(_updateDurationCurrent),
_playback.state.listen((state) {
if (state.playlist.medias.isNotEmpty && !_isLifted) {
_animationController.animateTo(1);
_isLifted = true;
}
}),
_playback.isPlaying.listen((value) { _playback.isPlaying.listen((value) {
if (value && !_isLifted) { if (value && !_isLifted) {
_animationController.animateTo(1); _animationController.animateTo(1);
@ -104,6 +111,7 @@ class _BottomPlayerState extends State<BottomPlayer>
axisAlignment: -1, axisAlignment: -1,
child: Obx( child: Obx(
() => GestureDetector( () => GestureDetector(
behavior: HitTestBehavior.translucent,
child: Column( child: Column(
children: [ children: [
if (_durationCurrent != Duration.zero) if (_durationCurrent != Duration.zero)
@ -122,10 +130,10 @@ class _BottomPlayerState extends State<BottomPlayer>
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
ClipRRect( Hero(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: Hero(
tag: const Key('current-active-track-album-art'), tag: const Key('current-active-track-album-art'),
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: _albumArt != null child: _albumArt != null
? AutoCacheImage(_albumArt!, width: 64, height: 64) ? AutoCacheImage(_albumArt!, width: 64, height: 64)
: Container( : Container(
@ -172,7 +180,10 @@ class _BottomPlayerState extends State<BottomPlayer>
], ],
), ),
onTap: () { onTap: () {
GoRouter.of(context).pushNamed('player'); context.pushTransparentRoute(PlayerScreen(
durationCurrent: _durationCurrent,
durationTotal: _durationTotal,
));
}, },
), ),
), ),

View File

@ -65,6 +65,8 @@ class _PlaylistTrackListState extends State<PlaylistTrackList> {
title: Text(item?.name ?? 'Loading...'), title: Text(item?.name ?? 'Loading...'),
subtitle: Text( subtitle: Text(
item?.artists?.asString() ?? 'Please stand by...', item?.artists?.asString() ?? 'Please stand by...',
maxLines: 1,
overflow: TextOverflow.ellipsis,
), ),
onTap: () { onTap: () {
if (item == null) return; if (item == null) return;

View File

@ -326,6 +326,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.0" version: "2.0.0"
dismissible_page:
dependency: "direct main"
description:
name: dismissible_page
sha256: "5b2316f770fe83583f770df1f6505cb19102081c5971979806e77f2e507a9958"
url: "https://pub.dev"
source: hosted
version: "1.0.2"
drift: drift:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@ -81,6 +81,7 @@ dependencies:
git: git:
url: https://github.com/KRTirtho/scrobblenaut.git url: https://github.com/KRTirtho/scrobblenaut.git
ref: dart-3-support ref: dart-3-support
dismissible_page: ^1.0.2
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: