freezer/lib/ui/player_screen.dart

888 lines
28 KiB
Dart
Raw Normal View History

2021-08-29 22:25:18 +00:00
import 'dart:math';
import 'package:cached_network_image/cached_network_image.dart';
2020-11-28 21:32:17 +00:00
import 'package:flutter/foundation.dart';
2020-06-23 19:23:12 +00:00
import 'package:flutter/material.dart';
import 'package:audio_service/audio_service.dart';
import 'package:flutter/services.dart';
2021-08-29 22:25:18 +00:00
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:freezer/api/cache.dart';
2020-06-23 19:23:12 +00:00
import 'package:freezer/api/deezer.dart';
2020-11-28 21:32:17 +00:00
import 'package:freezer/api/download.dart';
2020-06-23 19:23:12 +00:00
import 'package:freezer/api/player.dart';
import 'package:freezer/settings.dart';
import 'package:freezer/translations.i18n.dart';
import 'package:freezer/ui/elements.dart';
import 'package:freezer/ui/lyrics.dart';
2020-06-23 19:23:12 +00:00
import 'package:freezer/ui/menu.dart';
import 'package:freezer/ui/settings_screen.dart';
2020-06-23 19:23:12 +00:00
import 'package:freezer/ui/tiles.dart';
import 'package:just_audio/just_audio.dart';
2020-07-19 12:41:05 +00:00
import 'package:marquee/marquee.dart';
import 'package:palette_generator/palette_generator.dart';
2020-06-23 19:23:12 +00:00
import 'cached_image.dart';
import '../api/definitions.dart';
import 'player_bar.dart';
import 'dart:ui';
2021-02-09 20:14:14 +00:00
import 'dart:convert';
import 'dart:async';
2021-02-09 20:14:14 +00:00
//Changing item in queue view and pressing back causes the pageView to skip song
bool pageViewLock = false;
2020-11-28 21:32:17 +00:00
2021-03-16 19:35:50 +00:00
//So can be updated when going back from lyrics
Function updateColor;
2020-06-23 19:23:12 +00:00
class PlayerScreen extends StatefulWidget {
2021-08-29 22:25:18 +00:00
static const _blurStrength = 50.0;
2020-06-23 19:23:12 +00:00
@override
_PlayerScreenState createState() => _PlayerScreenState();
}
class _PlayerScreenState extends State<PlayerScreen> {
LinearGradient _bgGradient;
StreamSubscription _mediaItemSub;
2021-08-29 22:25:18 +00:00
StreamSubscription _playerStateSub;
2021-02-09 20:14:14 +00:00
ImageProvider _blurImage;
2021-08-29 22:25:18 +00:00
bool _wasConnected = true;
//Calculate background color
2020-11-28 21:32:17 +00:00
Future _updateColor() async {
2021-02-09 20:14:14 +00:00
if (!settings.colorGradientBackground && !settings.blurPlayerBackground)
return;
2021-08-29 22:25:18 +00:00
final imageProvider = CachedNetworkImageProvider(
AudioService.currentMediaItem.extras['thumb'] ??
AudioService.currentMediaItem.artUri);
2021-02-09 20:14:14 +00:00
//BG Image
if (settings.blurPlayerBackground)
2021-08-29 22:25:18 +00:00
setState(() => _blurImage = imageProvider);
2021-02-09 20:14:14 +00:00
2021-08-29 22:25:18 +00:00
if (settings.colorGradientBackground) {
//Run in isolate
PaletteGenerator palette =
await PaletteGenerator.fromImageProvider(imageProvider);
2020-11-28 21:32:17 +00:00
2021-02-09 20:14:14 +00:00
setState(() => _bgGradient = LinearGradient(
2021-04-05 20:27:54 +00:00
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
palette.dominantColor.color.withOpacity(0.7),
Color.fromARGB(0, 0, 0, 0)
],
stops: [
0.0,
0.6
]));
2021-02-09 20:14:14 +00:00
}
}
2021-08-29 22:25:18 +00:00
void _playbackStateChanged() {
if (AudioService.currentMediaItem == null) {
playerHelper.startService();
setState(() => _wasConnected = false);
} else if (!_wasConnected) setState(() => _wasConnected = true);
}
@override
void initState() {
2021-02-09 20:14:14 +00:00
Future.delayed(Duration(milliseconds: 600), _updateColor);
2021-08-29 22:25:18 +00:00
_playbackStateChanged();
_mediaItemSub = AudioService.currentMediaItemStream.listen((event) {
2021-08-29 22:25:18 +00:00
_playbackStateChanged();
2020-11-28 21:32:17 +00:00
_updateColor();
});
2021-08-29 22:25:18 +00:00
_playerStateSub =
AudioService.playbackStateStream.listen((_) => _playbackStateChanged());
2021-03-16 19:35:50 +00:00
updateColor = this._updateColor;
super.initState();
}
@override
void dispose() {
2021-08-29 22:25:18 +00:00
_mediaItemSub.cancel();
_playerStateSub.cancel();
super.dispose();
}
2020-06-23 19:23:12 +00:00
@override
Widget build(BuildContext context) {
2021-08-29 22:25:18 +00:00
final hasBackground =
settings.blurPlayerBackground || settings.colorGradientBackground;
final color = hasBackground
? Colors.transparent
: Theme.of(context).scaffoldBackgroundColor;
return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle(
statusBarColor: color,
statusBarBrightness: Brightness.light,
statusBarIconBrightness: Brightness.light,
systemNavigationBarIconBrightness: Brightness.light,
systemNavigationBarColor: color,
systemNavigationBarDividerColor: color,
),
child: Stack(
children: [
if (hasBackground)
Positioned.fill(
child: ImageFiltered(
imageFilter: ImageFilter.blur(
sigmaX: PlayerScreen._blurStrength,
sigmaY: PlayerScreen._blurStrength,
tileMode: TileMode.mirror),
child: DecoratedBox(
decoration: BoxDecoration(
gradient: _bgGradient,
image: _blurImage == null
? null
: DecorationImage(
image: _blurImage,
fit: BoxFit.cover,
colorFilter: ColorFilter.mode(
Colors.white.withOpacity(0.5),
BlendMode.dstATop))),
),
),
),
Scaffold(
backgroundColor: hasBackground ? Colors.transparent : null,
body: _wasConnected
? SafeArea(
child: OrientationBuilder(
builder: (context, orientation) =>
orientation == Orientation.landscape
? PlayerScreenHorizontal()
: PlayerScreenVertical(),
2021-02-09 20:14:14 +00:00
),
2021-08-29 22:25:18 +00:00
)
: Center(child: CircularProgressIndicator()),
),
],
),
);
}
}
//Landscape
class PlayerScreenHorizontal extends StatefulWidget {
@override
_PlayerScreenHorizontalState createState() => _PlayerScreenHorizontalState();
}
class _PlayerScreenHorizontalState extends State<PlayerScreenHorizontal> {
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
Padding(
padding: EdgeInsets.fromLTRB(16, 0, 16, 8),
child: Container(
2021-04-05 20:27:54 +00:00
width: ScreenUtil().setWidth(500),
child: Stack(
children: <Widget>[
BigAlbumArt(),
],
),
2021-04-05 20:27:54 +00:00
),
),
//Right side
SizedBox(
width: ScreenUtil().setWidth(500),
child: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Padding(
padding: EdgeInsets.fromLTRB(8, 16, 8, 0),
child: Container(
child: PlayerScreenTopRow(
2021-04-05 20:27:54 +00:00
textSize: ScreenUtil().setSp(24),
iconSize: ScreenUtil().setSp(36),
textWidth: ScreenUtil().setWidth(350),
short: true),
)),
Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
2020-07-19 12:41:05 +00:00
Container(
2021-04-05 20:27:54 +00:00
height: ScreenUtil().setSp(50),
child: AudioService
.currentMediaItem.displayTitle.length >=
22
? Marquee(
text: AudioService.currentMediaItem.displayTitle,
style: TextStyle(
fontSize: ScreenUtil().setSp(40),
fontWeight: FontWeight.bold),
blankSpace: 32.0,
startPadding: 10.0,
accelerationDuration: Duration(seconds: 1),
pauseAfterRound: Duration(seconds: 2),
)
: Text(
AudioService.currentMediaItem.displayTitle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: ScreenUtil().setSp(40),
fontWeight: FontWeight.bold),
)),
2021-08-29 22:25:18 +00:00
const SizedBox(height: 4.0),
Text(
2020-10-25 09:47:56 +00:00
AudioService.currentMediaItem.displaySubtitle ?? '',
maxLines: 1,
textAlign: TextAlign.center,
overflow: TextOverflow.clip,
style: TextStyle(
fontSize: ScreenUtil().setSp(32),
color: Theme.of(context).primaryColor,
),
),
],
),
Container(
padding: EdgeInsets.symmetric(horizontal: 16.0),
child: SeekBar(),
),
PlaybackControls(ScreenUtil().setSp(60)),
Padding(
padding: EdgeInsets.fromLTRB(8, 0, 8, 16),
child: Container(
padding: EdgeInsets.symmetric(horizontal: 2.0),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
IconButton(
icon: Icon(
Icons.subtitles,
size: ScreenUtil().setWidth(32),
semanticLabel: "Lyrics".i18n,
),
onPressed: () {
Navigator.of(context).push(MaterialPageRoute(
2021-04-05 20:27:54 +00:00
builder: (context) => LyricsScreen(
trackId:
AudioService.currentMediaItem.id)));
},
),
2020-11-28 21:32:17 +00:00
QualityInfoWidget(),
2020-11-01 19:23:24 +00:00
RepeatButton(ScreenUtil().setWidth(32)),
2020-11-28 21:32:17 +00:00
PlayerMenuButton()
],
),
2021-04-05 20:27:54 +00:00
))
],
),
)
],
);
}
}
2020-06-23 19:23:12 +00:00
//Portrait
class PlayerScreenVertical extends StatefulWidget {
@override
_PlayerScreenVerticalState createState() => _PlayerScreenVerticalState();
}
class _PlayerScreenVerticalState extends State<PlayerScreenVertical> {
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Padding(
2021-04-05 20:27:54 +00:00
padding: EdgeInsets.fromLTRB(30, 4, 16, 0),
child: PlayerScreenTopRow()),
Padding(
2021-08-29 22:25:18 +00:00
padding: EdgeInsets.symmetric(horizontal: 16.0),
child: SizedBox(
child: BigAlbumArt(),
width: min(MediaQuery.of(context).size.width,
MediaQuery.of(context).size.height) *
0.9,
height: min(MediaQuery.of(context).size.width,
MediaQuery.of(context).size.height) *
0.9,
),
),
2021-08-29 22:25:18 +00:00
const SizedBox(height: 4.0),
Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
2020-07-19 12:41:05 +00:00
Container(
2021-04-05 20:27:54 +00:00
height: ScreenUtil().setSp(80),
child: AudioService.currentMediaItem.displayTitle.length >= 26
? Marquee(
text: AudioService.currentMediaItem.displayTitle,
style: TextStyle(
fontSize: ScreenUtil().setSp(64),
fontWeight: FontWeight.bold),
blankSpace: 32.0,
startPadding: 10.0,
accelerationDuration: Duration(seconds: 1),
pauseAfterRound: Duration(seconds: 2),
)
: Text(
AudioService.currentMediaItem.displayTitle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: ScreenUtil().setSp(64),
fontWeight: FontWeight.bold),
)),
2021-08-29 22:25:18 +00:00
const SizedBox(height: 4),
Text(
2020-10-25 09:47:56 +00:00
AudioService.currentMediaItem.displaySubtitle ?? '',
maxLines: 1,
textAlign: TextAlign.center,
overflow: TextOverflow.clip,
style: TextStyle(
fontSize: ScreenUtil().setSp(52),
color: Theme.of(context).primaryColor,
),
),
],
),
SeekBar(),
PlaybackControls(ScreenUtil().setWidth(100)),
Padding(
padding: EdgeInsets.symmetric(vertical: 0, horizontal: 16.0),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
IconButton(
icon: Icon(
Icons.subtitles,
size: ScreenUtil().setWidth(46),
semanticLabel: "Lyrics".i18n,
),
2021-03-16 19:35:50 +00:00
onPressed: () async {
//Fix bottom buttons
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
2021-04-05 20:27:54 +00:00
systemNavigationBarColor:
settings.themeData.bottomAppBarColor,
statusBarColor: Colors.transparent));
2021-03-16 19:35:50 +00:00
await Navigator.of(context).push(MaterialPageRoute(
2021-04-05 20:27:54 +00:00
builder: (context) => LyricsScreen(
trackId: AudioService.currentMediaItem.id)));
2021-03-16 19:35:50 +00:00
updateColor();
},
),
IconButton(
icon: Icon(
Icons.file_download,
semanticLabel: "Download".i18n,
),
onPressed: () async {
Track t = Track.fromMediaItem(AudioService.currentMediaItem);
if (await downloadManager.addOfflineTrack(t,
private: false,
context: context,
isSingleton: true) !=
false)
Fluttertoast.showToast(
msg: 'Downloads added!'.i18n,
gravity: ToastGravity.BOTTOM,
toastLength: Toast.LENGTH_SHORT);
},
),
2020-11-28 21:32:17 +00:00
QualityInfoWidget(),
2020-11-01 19:23:24 +00:00
RepeatButton(ScreenUtil().setWidth(46)),
2020-11-28 21:32:17 +00:00
PlayerMenuButton()
],
),
)
],
2020-06-23 19:23:12 +00:00
);
}
}
2020-11-28 21:32:17 +00:00
class QualityInfoWidget extends StatefulWidget {
@override
_QualityInfoWidgetState createState() => _QualityInfoWidgetState();
}
class _QualityInfoWidgetState extends State<QualityInfoWidget> {
String value = '';
StreamSubscription streamSubscription;
//Load data from native
void _load() async {
if (AudioService.currentMediaItem == null) return;
2021-04-05 20:27:54 +00:00
Map data = await DownloadManager.platform.invokeMethod(
"getStreamInfo", {"id": AudioService.currentMediaItem.id});
2020-11-28 21:32:17 +00:00
//N/A
if (data == null) {
setState(() => value = '');
//If not show, try again later
if (AudioService.currentMediaItem.extras['show'] == null)
Future.delayed(Duration(milliseconds: 200), _load);
return;
}
//Update
StreamQualityInfo info = StreamQualityInfo.fromJson(data);
setState(() {
2021-04-05 20:27:54 +00:00
value =
'${info.format} ${info.bitrate(AudioService.currentMediaItem.duration)}kbps';
2020-11-28 21:32:17 +00:00
});
}
@override
void initState() {
_load();
if (streamSubscription == null)
2021-04-05 20:27:54 +00:00
streamSubscription =
AudioService.currentMediaItemStream.listen((event) async {
_load();
2020-11-28 21:32:17 +00:00
});
super.initState();
}
@override
void dispose() {
2021-04-05 20:27:54 +00:00
if (streamSubscription != null) streamSubscription.cancel();
2020-11-28 21:32:17 +00:00
super.dispose();
}
@override
Widget build(BuildContext context) {
return TextButton(
2020-11-28 21:32:17 +00:00
child: Text(value),
onPressed: () {
2021-04-05 20:27:54 +00:00
Navigator.of(context)
.push(MaterialPageRoute(builder: (context) => QualitySettings()));
2020-11-28 21:32:17 +00:00
},
);
}
}
class PlayerMenuButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return IconButton(
icon: Icon(
Icons.more_vert,
size: ScreenUtil().setWidth(46),
semanticLabel: "Options".i18n,
),
2020-11-28 21:32:17 +00:00
onPressed: () {
Track t = Track.fromMediaItem(AudioService.currentMediaItem);
2020-12-27 18:33:59 +00:00
MenuSheet m = MenuSheet(context, navigateCallback: () {
Navigator.of(context).pop();
});
2020-11-28 21:32:17 +00:00
if (AudioService.currentMediaItem.extras['show'] == null)
m.defaultTrackMenu(t, options: [m.sleepTimer(), m.wakelock()]);
2020-11-28 21:32:17 +00:00
else
m.defaultShowEpisodeMenu(
2021-04-05 20:27:54 +00:00
Show.fromJson(
jsonDecode(AudioService.currentMediaItem.extras['show'])),
ShowEpisode.fromMediaItem(AudioService.currentMediaItem),
options: [m.sleepTimer(), m.wakelock()]);
2020-11-28 21:32:17 +00:00
},
);
}
}
2020-11-01 19:23:24 +00:00
class RepeatButton extends StatefulWidget {
final double iconSize;
2021-04-05 20:27:54 +00:00
RepeatButton(this.iconSize, {Key key}) : super(key: key);
@override
2020-11-01 19:23:24 +00:00
_RepeatButtonState createState() => _RepeatButtonState();
}
2020-11-01 19:23:24 +00:00
class _RepeatButtonState extends State<RepeatButton> {
2021-08-29 22:25:18 +00:00
// ignore: missing_return
Icon get repeatIcon {
switch (playerHelper.repeatType) {
case LoopMode.off:
return Icon(
Icons.repeat,
size: widget.iconSize,
semanticLabel: "Repeat off".i18n,
);
case LoopMode.all:
return Icon(
Icons.repeat,
color: Theme.of(context).primaryColor,
size: widget.iconSize,
semanticLabel: "Repeat".i18n,
);
case LoopMode.one:
return Icon(
Icons.repeat_one,
color: Theme.of(context).primaryColor,
2021-07-02 16:28:59 +00:00
size: widget.iconSize,
semanticLabel: "Repeat one".i18n,
);
}
}
2020-11-01 19:23:24 +00:00
@override
Widget build(BuildContext context) {
return IconButton(
icon: repeatIcon,
onPressed: () async {
await playerHelper.changeRepeat();
setState(() {});
},
);
}
}
class PlaybackControls extends StatefulWidget {
final double iconSize;
2021-04-05 20:27:54 +00:00
PlaybackControls(this.iconSize, {Key key}) : super(key: key);
2020-11-01 19:23:24 +00:00
@override
_PlaybackControlsState createState() => _PlaybackControlsState();
}
class _PlaybackControlsState extends State<PlaybackControls> {
Icon get libraryIcon {
2021-04-05 20:27:54 +00:00
if (cache.checkTrackFavorite(
Track.fromMediaItem(AudioService.currentMediaItem))) {
return Icon(
Icons.favorite,
size: widget.iconSize * 0.64,
semanticLabel: "Unlove".i18n,
);
}
return Icon(
Icons.favorite_border,
size: widget.iconSize * 0.64,
semanticLabel: "Love".i18n,
);
}
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.max,
children: [
IconButton(
icon: Icon(
Icons.sentiment_very_dissatisfied,
size: ScreenUtil().setWidth(46),
semanticLabel: "Dislike".i18n,
),
2020-11-01 19:23:24 +00:00
onPressed: () async {
await deezerAPI.dislikeTrack(AudioService.currentMediaItem.id);
2021-04-05 20:27:54 +00:00
if (playerHelper.queueIndex <
(AudioService.queue ?? []).length - 1) {
2020-11-01 19:23:24 +00:00
AudioService.skipToNext();
}
2021-04-05 20:27:54 +00:00
}),
PrevNextButton(widget.iconSize, prev: true),
PlayPauseButton(widget.iconSize * 1.25),
PrevNextButton(widget.iconSize),
IconButton(
icon: libraryIcon,
onPressed: () async {
2021-04-05 20:27:54 +00:00
if (cache.libraryTracks == null) cache.libraryTracks = [];
2021-04-05 20:27:54 +00:00
if (cache.checkTrackFavorite(
Track.fromMediaItem(AudioService.currentMediaItem))) {
//Remove from library
2021-04-05 20:27:54 +00:00
setState(() => cache.libraryTracks
.remove(AudioService.currentMediaItem.id));
await deezerAPI
.removeFavorite(AudioService.currentMediaItem.id);
await cache.save();
} else {
//Add
2021-04-05 20:27:54 +00:00
setState(() =>
cache.libraryTracks.add(AudioService.currentMediaItem.id));
await deezerAPI
.addFavoriteTrack(AudioService.currentMediaItem.id);
await cache.save();
}
},
)
],
),
);
}
}
class BigAlbumArt extends StatefulWidget {
@override
_BigAlbumArtState createState() => _BigAlbumArtState();
}
class _BigAlbumArtState extends State<BigAlbumArt> {
PageController _pageController = PageController(
initialPage: playerHelper.queueIndex,
);
StreamSubscription _currentItemSub;
bool _animationLock = true;
@override
void initState() {
_currentItemSub = AudioService.currentMediaItemStream.listen((event) async {
_animationLock = true;
2021-04-05 20:27:54 +00:00
await _pageController.animateToPage(playerHelper.queueIndex,
duration: Duration(milliseconds: 300), curve: Curves.easeInOut);
_animationLock = false;
});
super.initState();
}
@override
void dispose() {
2021-04-05 20:27:54 +00:00
if (_currentItemSub != null) _currentItemSub.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onVerticalDragUpdate: (DragUpdateDetails details) {
if (details.delta.dy > 16) {
Navigator.of(context).pop();
}
},
child: PageView(
controller: _pageController,
onPageChanged: (int index) {
2021-02-09 20:14:14 +00:00
if (pageViewLock) {
pageViewLock = false;
return;
}
if (_animationLock) return;
AudioService.skipToQueueItem(AudioService.queue[index].id);
},
2021-08-29 22:25:18 +00:00
children: List.generate(
AudioService.queue.length,
(i) => ZoomableImage(
url: AudioService.queue[i].artUri,
)),
),
);
}
}
2020-06-23 19:23:12 +00:00
//Top row containing QueueSource, queue...
class PlayerScreenTopRow extends StatelessWidget {
final double textSize;
final double iconSize;
final double textWidth;
final bool short;
2021-04-05 20:27:54 +00:00
PlayerScreenTopRow(
{this.textSize, this.iconSize, this.textWidth, this.short});
2020-06-23 19:23:12 +00:00
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
2020-06-23 19:23:12 +00:00
children: <Widget>[
Container(
2021-04-05 20:27:54 +00:00
width: this.textWidth ?? ScreenUtil().setWidth(800),
child: Text(
2021-04-05 20:27:54 +00:00
(short ?? false)
? (playerHelper.queueSource.text ?? '')
: 'Playing from:'.i18n +
' ' +
(playerHelper.queueSource?.text ?? ''),
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.left,
2021-04-05 20:27:54 +00:00
style: TextStyle(fontSize: this.textSize ?? ScreenUtil().setSp(38)),
),
),
IconButton(
icon: Icon(
Icons.menu,
semanticLabel: "Queue".i18n,
),
2021-04-05 20:27:54 +00:00
iconSize: this.iconSize ?? ScreenUtil().setSp(52),
splashRadius: this.iconSize ?? ScreenUtil().setWidth(52),
2021-08-29 22:25:18 +00:00
onPressed: () => Navigator.of(context)
.push(MaterialPageRoute(builder: (context) => QueueScreen())),
2020-06-23 19:23:12 +00:00
),
],
);
}
}
class SeekBar extends StatefulWidget {
@override
_SeekBarState createState() => _SeekBarState();
}
class _SeekBarState extends State<SeekBar> {
double _pos;
double get position {
2021-08-29 22:25:18 +00:00
if (_pos != null) return _pos;
2020-06-23 19:23:12 +00:00
if (AudioService.playbackState == null) return 0.0;
2021-04-05 20:27:54 +00:00
double p =
AudioService.playbackState.currentPosition.inMilliseconds.toDouble() ??
0.0;
2020-06-23 19:23:12 +00:00
if (p > duration) return duration;
return p;
}
//Duration to mm:ss
String _timeString(double pos) {
Duration d = Duration(milliseconds: pos.toInt());
return "${d.inMinutes}:${d.inSeconds.remainder(60).toString().padLeft(2, '0')}";
}
double get duration {
if (AudioService.currentMediaItem == null) return 1.0;
return AudioService.currentMediaItem.duration.inMilliseconds.toDouble();
}
@override
Widget build(BuildContext context) {
return StreamBuilder(
stream: Stream.periodic(Duration(milliseconds: 250)),
builder: (BuildContext context, AsyncSnapshot snapshot) {
return Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Padding(
2021-08-29 22:25:18 +00:00
padding: EdgeInsets.symmetric(horizontal: 24.0),
2020-06-23 19:23:12 +00:00
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(
_timeString(position),
2021-04-05 20:27:54 +00:00
style: TextStyle(fontSize: ScreenUtil().setSp(35)),
2020-06-23 19:23:12 +00:00
),
Text(
_timeString(duration),
2021-04-05 20:27:54 +00:00
style: TextStyle(fontSize: ScreenUtil().setSp(35)),
2020-06-23 19:23:12 +00:00
)
],
),
),
2021-08-29 22:25:18 +00:00
Slider(
focusNode: FocusNode(
canRequestFocus: false,
skipTraversal:
true), // Don't focus on Slider - it doesn't work (and not needed)
value: position,
max: duration,
onChangeStart: (double d) {
setState(() {
_pos = d;
});
},
onChanged: (double d) {
setState(() {
_pos = d;
});
},
onChangeEnd: (double d) async {
await AudioService.seekTo(Duration(milliseconds: d.round()));
setState(() {
_pos = null;
});
},
),
2020-06-23 19:23:12 +00:00
],
);
},
);
}
}
class QueueScreen extends StatefulWidget {
@override
_QueueScreenState createState() => _QueueScreenState();
}
class _QueueScreenState extends State<QueueScreen> {
StreamSubscription _queueSub;
2021-08-29 22:25:18 +00:00
/// Basically a simple list that keeps itself synchronized with [AudioService.queue],
/// so that the [ReorderableListView] is updated instanly (as it should be)
List<MediaItem> _queueCache = [];
@override
void initState() {
2021-08-29 22:25:18 +00:00
_queueCache = AudioService.queue;
2021-04-05 20:27:54 +00:00
_queueSub = AudioService.queueStream.listen((event) {
2021-08-29 22:25:18 +00:00
_queueCache = AudioService.queue;
2021-04-05 20:27:54 +00:00
setState(() {});
});
super.initState();
}
@override
void dispose() {
2021-04-05 20:27:54 +00:00
if (_queueSub != null) _queueSub.cancel();
super.dispose();
}
2020-06-23 19:23:12 +00:00
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: FreezerAppBar(
'Queue'.i18n,
2020-06-23 19:23:12 +00:00
actions: <Widget>[
IconButton(
icon: Icon(
Icons.shuffle,
2021-07-02 16:28:59 +00:00
semanticLabel: "Shuffle".i18n,
),
2020-06-23 19:23:12 +00:00
onPressed: () async {
await playerHelper.toggleShuffle();
setState(() {});
2020-06-23 19:23:12 +00:00
},
)
],
),
2021-04-05 20:27:54 +00:00
body: ReorderableListView.builder(
onReorder: (int oldIndex, int newIndex) {
if (oldIndex == playerHelper.queueIndex) return;
2021-08-29 22:25:18 +00:00
setState(() => _queueCache.reorder(oldIndex, newIndex));
playerHelper.reorder(oldIndex, newIndex);
},
2021-08-29 22:25:18 +00:00
itemCount: _queueCache.length,
2021-04-05 20:27:54 +00:00
itemBuilder: (BuildContext context, int i) {
2021-08-29 22:25:18 +00:00
Track track = Track.fromMediaItem(AudioService.queue[i]);
2020-06-23 19:23:12 +00:00
return TrackTile(
2021-08-29 22:25:18 +00:00
track,
2021-04-05 20:27:54 +00:00
onTap: () {
2021-02-09 20:14:14 +00:00
pageViewLock = true;
2021-08-29 22:25:18 +00:00
AudioService.skipToQueueItem(track.id)
2021-04-05 20:27:54 +00:00
.then((value) => Navigator.of(context).pop());
2020-06-23 19:23:12 +00:00
},
2021-08-29 22:25:18 +00:00
key: Key(track.id),
trailing: IconButton(
icon: Icon(
Icons.close,
semanticLabel: "Close".i18n,
),
onPressed: () async {
2021-08-29 22:25:18 +00:00
await AudioService.removeQueueItem(track.toMediaItem());
setState(() {});
},
),
2020-06-23 19:23:12 +00:00
);
2021-04-05 20:27:54 +00:00
},
));
2020-06-23 19:23:12 +00:00
}
2021-04-05 20:27:54 +00:00
}