✨ Full screen player
This commit is contained in:
parent
2e17078fea
commit
a2bc08bbd9
@ -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());
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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(),
|
||||||
|
@ -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(),
|
|
||||||
),
|
|
||||||
]);
|
]);
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
));
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -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;
|
||||||
|
@ -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:
|
||||||
|
@ -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:
|
||||||
|
Loading…
Reference in New Issue
Block a user