715 lines
22 KiB
Dart
715 lines
22 KiB
Dart
import 'dart:ui';
|
|
import 'dart:async';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:audio_service/audio_service.dart';
|
|
import 'package:flutter_screenutil/screenutil.dart';
|
|
import 'package:freezer/api/deezer.dart';
|
|
import 'package:freezer/api/player.dart';
|
|
import 'package:freezer/ui/menu.dart';
|
|
import 'package:freezer/ui/settings_screen.dart';
|
|
import 'package:freezer/ui/tiles.dart';
|
|
import 'package:async/async.dart';
|
|
|
|
import 'cached_image.dart';
|
|
import '../api/definitions.dart';
|
|
import 'player_bar.dart';
|
|
|
|
class PlayerScreen extends StatefulWidget {
|
|
@override
|
|
_PlayerScreenState createState() => _PlayerScreenState();
|
|
}
|
|
|
|
class _PlayerScreenState extends State<PlayerScreen> {
|
|
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
//Responsive
|
|
ScreenUtil.init(context, allowFontScaling: true);
|
|
|
|
return Scaffold(
|
|
body: SafeArea(
|
|
child: StreamBuilder(
|
|
stream: StreamZip([AudioService.playbackStateStream, AudioService.currentMediaItemStream]),
|
|
builder: (BuildContext context, AsyncSnapshot snapshot) {
|
|
|
|
//When disconnected
|
|
if (AudioService.currentMediaItem == null) {
|
|
playerHelper.startService();
|
|
return Center(child: CircularProgressIndicator(),);
|
|
}
|
|
|
|
return OrientationBuilder(
|
|
builder: (context, orientation) {
|
|
//Landscape
|
|
if (orientation == Orientation.landscape) {
|
|
return PlayerScreenHorizontal();
|
|
}
|
|
//Portrait
|
|
return PlayerScreenVertical();
|
|
},
|
|
);
|
|
|
|
},
|
|
),
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
//Landscape
|
|
class PlayerScreenHorizontal extends StatefulWidget {
|
|
@override
|
|
_PlayerScreenHorizontalState createState() => _PlayerScreenHorizontalState();
|
|
}
|
|
|
|
class _PlayerScreenHorizontalState extends State<PlayerScreenHorizontal> {
|
|
|
|
double iconSize = ScreenUtil().setWidth(64);
|
|
bool _lyrics = false;
|
|
|
|
@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(
|
|
width: ScreenUtil().setWidth(500),
|
|
child: Stack(
|
|
children: <Widget>[
|
|
CachedImage(
|
|
url: AudioService.currentMediaItem.artUri,
|
|
),
|
|
if (_lyrics) LyricsWidget(
|
|
artUri: AudioService.currentMediaItem.artUri,
|
|
trackId: AudioService.currentMediaItem.id,
|
|
lyrics: Track.fromMediaItem(AudioService.currentMediaItem).lyrics,
|
|
height: ScreenUtil().setWidth(500),
|
|
),
|
|
],
|
|
),
|
|
)
|
|
),
|
|
//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(
|
|
textSize: ScreenUtil().setSp(26),
|
|
iconSize: ScreenUtil().setSp(32),
|
|
textWidth: ScreenUtil().setWidth(256),
|
|
short: true
|
|
),
|
|
)
|
|
),
|
|
Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: <Widget>[
|
|
Text(
|
|
AudioService.currentMediaItem.displayTitle,
|
|
maxLines: 1,
|
|
textAlign: TextAlign.center,
|
|
overflow: TextOverflow.clip,
|
|
style: TextStyle(
|
|
fontSize: ScreenUtil().setSp(40),
|
|
fontWeight: FontWeight.bold
|
|
),
|
|
),
|
|
Container(height: 4,),
|
|
Text(
|
|
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(),
|
|
),
|
|
Container(
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
|
mainAxisSize: MainAxisSize.max,
|
|
children: <Widget>[
|
|
PrevNextButton(iconSize, prev: true,),
|
|
PlayPauseButton(iconSize),
|
|
PrevNextButton(iconSize)
|
|
],
|
|
),
|
|
),
|
|
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)),
|
|
onPressed: () {
|
|
setState(() => _lyrics = !_lyrics);
|
|
},
|
|
),
|
|
FlatButton(
|
|
onPressed: () => Navigator.push(
|
|
context,
|
|
MaterialPageRoute(builder: (context) => QualitySettings())
|
|
),
|
|
child: Text(
|
|
AudioService.currentMediaItem.extras['qualityString'],
|
|
style: TextStyle(fontSize: ScreenUtil().setSp(24)),
|
|
),
|
|
),
|
|
IconButton(
|
|
icon: Icon(Icons.more_vert, size: ScreenUtil().setWidth(32)),
|
|
onPressed: () {
|
|
Track t = Track.fromMediaItem(AudioService.currentMediaItem);
|
|
MenuSheet m = MenuSheet(context);
|
|
m.defaultTrackMenu(t);
|
|
},
|
|
)
|
|
],
|
|
),
|
|
)
|
|
)
|
|
],
|
|
),
|
|
)
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
//Portrait
|
|
class PlayerScreenVertical extends StatefulWidget {
|
|
@override
|
|
_PlayerScreenVerticalState createState() => _PlayerScreenVerticalState();
|
|
}
|
|
|
|
class _PlayerScreenVerticalState extends State<PlayerScreenVertical> {
|
|
double iconSize = ScreenUtil().setWidth(100);
|
|
bool _lyrics = false;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Column(
|
|
mainAxisSize: MainAxisSize.max,
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: <Widget>[
|
|
Padding(
|
|
padding: EdgeInsets.fromLTRB(28, 10, 28, 0),
|
|
child: PlayerScreenTopRow()
|
|
),
|
|
Padding(
|
|
padding: EdgeInsets.fromLTRB(16, 0, 16, 0),
|
|
child: Container(
|
|
height: ScreenUtil().setHeight(1050),
|
|
child: Stack(
|
|
children: <Widget>[
|
|
CachedImage(
|
|
url: AudioService.currentMediaItem.artUri,
|
|
),
|
|
if (_lyrics) LyricsWidget(
|
|
artUri: AudioService.currentMediaItem.artUri,
|
|
trackId: AudioService.currentMediaItem.id,
|
|
lyrics: Track.fromMediaItem(AudioService.currentMediaItem).lyrics,
|
|
height: ScreenUtil().setHeight(1050),
|
|
),
|
|
],
|
|
),
|
|
)
|
|
),
|
|
Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: <Widget>[
|
|
Text(
|
|
AudioService.currentMediaItem.displayTitle,
|
|
maxLines: 1,
|
|
textAlign: TextAlign.center,
|
|
overflow: TextOverflow.clip,
|
|
style: TextStyle(
|
|
fontSize: ScreenUtil().setSp(64),
|
|
fontWeight: FontWeight.bold
|
|
),
|
|
),
|
|
Container(height: 4,),
|
|
Text(
|
|
AudioService.currentMediaItem.displaySubtitle,
|
|
maxLines: 1,
|
|
textAlign: TextAlign.center,
|
|
overflow: TextOverflow.clip,
|
|
style: TextStyle(
|
|
fontSize: ScreenUtil().setSp(52),
|
|
color: Theme.of(context).primaryColor,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
SeekBar(),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
|
mainAxisSize: MainAxisSize.max,
|
|
children: <Widget>[
|
|
PrevNextButton(iconSize, prev: true,),
|
|
PlayPauseButton(iconSize),
|
|
PrevNextButton(iconSize)
|
|
],
|
|
),
|
|
//Container(height: 8.0,),
|
|
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)),
|
|
onPressed: () {
|
|
setState(() => _lyrics = !_lyrics);
|
|
},
|
|
),
|
|
FlatButton(
|
|
onPressed: () => Navigator.push(
|
|
context,
|
|
MaterialPageRoute(builder: (context) => QualitySettings())
|
|
),
|
|
child: Text(
|
|
AudioService.currentMediaItem.extras['qualityString'],
|
|
style: TextStyle(
|
|
fontSize: ScreenUtil().setSp(32),
|
|
),
|
|
),
|
|
),
|
|
IconButton(
|
|
icon: Icon(Icons.more_vert, size: ScreenUtil().setWidth(46)),
|
|
onPressed: () {
|
|
Track t = Track.fromMediaItem(AudioService.currentMediaItem);
|
|
MenuSheet m = MenuSheet(context);
|
|
m.defaultTrackMenu(t);
|
|
},
|
|
)
|
|
],
|
|
),
|
|
)
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
class LyricsWidget extends StatefulWidget {
|
|
|
|
final Lyrics lyrics;
|
|
final String trackId;
|
|
final String artUri;
|
|
final double height;
|
|
LyricsWidget({this.artUri, this.lyrics, this.trackId, this.height, Key key}): super(key: key);
|
|
|
|
@override
|
|
_LyricsWidgetState createState() => _LyricsWidgetState();
|
|
}
|
|
|
|
class _LyricsWidgetState extends State<LyricsWidget> {
|
|
|
|
bool _loading = true;
|
|
Lyrics _l;
|
|
Color _textColor = Colors.black;
|
|
ScrollController _scrollController = ScrollController();
|
|
Timer _timer;
|
|
int _currentIndex;
|
|
double _boxHeight;
|
|
String _trackId;
|
|
|
|
Future _load() async {
|
|
_trackId = widget.trackId;
|
|
|
|
//Get text color by album art (black or white)
|
|
if (widget.artUri != null) {
|
|
bool bw = await imagesDatabase.isDark(widget.artUri);
|
|
if (bw != null) setState(() => _textColor = bw?Colors.white:Colors.black);
|
|
}
|
|
|
|
if (widget.lyrics.lyrics == null || widget.lyrics.lyrics.length == 0) {
|
|
//Load from api
|
|
try {
|
|
_l = await deezerAPI.lyrics(_trackId);
|
|
setState(() => _loading = false);
|
|
} catch (e) {
|
|
//Error Lyrics
|
|
setState(() => _l = Lyrics().error);
|
|
}
|
|
} else {
|
|
//Use provided lyrics
|
|
_l = widget.lyrics;
|
|
setState(() => _loading = false);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void initState() {
|
|
this._boxHeight = widget.height??400.0;
|
|
_load();
|
|
|
|
Timer.periodic(Duration(milliseconds: 500), (timer) {
|
|
_timer = timer;
|
|
if (_loading) return;
|
|
//Update index of current lyric
|
|
setState(() {
|
|
_currentIndex = _l.lyrics.lastIndexWhere((l) => l.offset <= AudioService.playbackState.currentPosition);
|
|
});
|
|
//Scroll to current lyric
|
|
if (_currentIndex <= 0) return;
|
|
_scrollController.animateTo(
|
|
(_boxHeight * _currentIndex),
|
|
duration: Duration(milliseconds: 250),
|
|
curve: Curves.ease
|
|
);
|
|
|
|
});
|
|
super.initState();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
if (_timer != null) _timer.cancel();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
void didUpdateWidget(LyricsWidget oldWidget) {
|
|
if (this._trackId != widget.trackId) {
|
|
setState(() {
|
|
_loading = true;
|
|
this._trackId = widget.trackId;
|
|
});
|
|
_load();
|
|
}
|
|
super.didUpdateWidget(oldWidget);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
height: _boxHeight,
|
|
width: _boxHeight,
|
|
child: BackdropFilter(
|
|
filter: ImageFilter.blur(
|
|
sigmaX: 7.0,
|
|
sigmaY: 7.0
|
|
),
|
|
child: Container(
|
|
child: _loading?
|
|
Center(child: CircularProgressIndicator(),) :
|
|
SingleChildScrollView(
|
|
controller: _scrollController,
|
|
child: Column(
|
|
children: List.generate(_l.lyrics.length, (i) {
|
|
return Container(
|
|
height: _boxHeight,
|
|
child: Center(
|
|
child: Stack(
|
|
children: <Widget>[
|
|
Text(
|
|
_l.lyrics[i].text,
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(
|
|
fontSize: 36.0,
|
|
fontWeight: (_currentIndex == i)?FontWeight.bold:FontWeight.normal,
|
|
foreground: Paint()
|
|
..strokeWidth = 6
|
|
..style = PaintingStyle.stroke
|
|
..color = (_textColor==Colors.black)?Colors.white:Colors.black,
|
|
),
|
|
),
|
|
Text(
|
|
_l.lyrics[i].text,
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(
|
|
color: _textColor,
|
|
fontSize: 36.0,
|
|
fontWeight: (_currentIndex == i)?FontWeight.bold:FontWeight.normal
|
|
),
|
|
),
|
|
],
|
|
)
|
|
)
|
|
);
|
|
}),
|
|
),
|
|
)
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
//Top row containing QueueSource, queue...
|
|
class PlayerScreenTopRow extends StatelessWidget {
|
|
|
|
double textSize;
|
|
double iconSize;
|
|
double textWidth;
|
|
bool short;
|
|
PlayerScreenTopRow({this.textSize, this.iconSize, this.textWidth, this.short});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Row(
|
|
mainAxisSize: MainAxisSize.max,
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: <Widget>[
|
|
Row(
|
|
children: <Widget>[
|
|
Padding(
|
|
padding: EdgeInsets.fromLTRB(0, 0, 8, 0),
|
|
child: InkWell(
|
|
child: Container(
|
|
padding: EdgeInsets.all(8.0),
|
|
child: Icon(Icons.keyboard_arrow_down, size: this.iconSize??ScreenUtil().setWidth(46)),
|
|
),
|
|
onTap: () {
|
|
Navigator.of(context).pop();
|
|
},
|
|
),
|
|
),
|
|
Container(
|
|
width: this.textWidth??ScreenUtil().setWidth(600),
|
|
child: Text(
|
|
(short??false)?playerHelper.queueSource.text:'Playing from: ' + playerHelper.queueSource.text,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
textAlign: TextAlign.left,
|
|
style: TextStyle(fontSize: this.textSize??ScreenUtil().setSp(34)),
|
|
),
|
|
)
|
|
],
|
|
),
|
|
Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: <Widget>[
|
|
RepeatButton(size: this.iconSize),
|
|
Container(width: 16.0,),
|
|
InkWell(
|
|
child: Container(
|
|
padding: EdgeInsets.all(8.0),
|
|
child: Icon(Icons.menu, size: this.iconSize??ScreenUtil().setWidth(46)),
|
|
),
|
|
onTap: (){
|
|
Navigator.of(context).push(MaterialPageRoute(
|
|
builder: (context) => QueueScreen()
|
|
));
|
|
},
|
|
),
|
|
],
|
|
)
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
class RepeatButton extends StatefulWidget {
|
|
|
|
double size;
|
|
RepeatButton({this.size, Key key}): super(key: key);
|
|
|
|
@override
|
|
_RepeatButtonState createState() => _RepeatButtonState();
|
|
}
|
|
|
|
class _RepeatButtonState extends State<RepeatButton> {
|
|
|
|
double _size = ScreenUtil().setWidth(46);
|
|
|
|
Icon get icon {
|
|
switch (playerHelper.repeatType) {
|
|
case RepeatType.NONE:
|
|
return Icon(Icons.repeat, size: widget.size??_size);
|
|
case RepeatType.LIST:
|
|
return Icon(
|
|
Icons.repeat,
|
|
color: Theme.of(context).primaryColor,
|
|
size: widget.size??_size
|
|
);
|
|
case RepeatType.TRACK:
|
|
return Icon(
|
|
Icons.repeat_one,
|
|
color: Theme.of(context).primaryColor,
|
|
size: widget.size??_size
|
|
);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return InkWell(
|
|
onTap: () async {
|
|
await playerHelper.changeRepeat();
|
|
setState(() {});
|
|
},
|
|
child: Container(
|
|
padding: EdgeInsets.all(8.0),
|
|
child: icon,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
class SeekBar extends StatefulWidget {
|
|
@override
|
|
_SeekBarState createState() => _SeekBarState();
|
|
}
|
|
|
|
class _SeekBarState extends State<SeekBar> {
|
|
|
|
bool _seeking = false;
|
|
double _pos;
|
|
|
|
double get position {
|
|
if (_seeking) return _pos;
|
|
if (AudioService.playbackState == null) return 0.0;
|
|
double p = AudioService.playbackState.currentPosition.inMilliseconds.toDouble()??0.0;
|
|
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(
|
|
padding: EdgeInsets.symmetric(vertical: 0.0, horizontal: 24.0),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: <Widget>[
|
|
Text(
|
|
_timeString(position),
|
|
style: TextStyle(
|
|
fontSize: ScreenUtil().setSp(35)
|
|
),
|
|
),
|
|
Text(
|
|
_timeString(duration),
|
|
style: TextStyle(
|
|
fontSize: ScreenUtil().setSp(35)
|
|
),
|
|
)
|
|
],
|
|
),
|
|
),
|
|
Container(
|
|
height: 32.0,
|
|
child: Slider(
|
|
value: position,
|
|
max: duration,
|
|
onChangeStart: (double d) {
|
|
setState(() {
|
|
_seeking = true;
|
|
_pos = d;
|
|
});
|
|
},
|
|
onChanged: (double d) {
|
|
setState(() {
|
|
_pos = d;
|
|
});
|
|
},
|
|
onChangeEnd: (double d) async {
|
|
await AudioService.seekTo(Duration(milliseconds: d.round()));
|
|
setState(() {
|
|
_pos = d;
|
|
_seeking = false;
|
|
});
|
|
},
|
|
),
|
|
)
|
|
],
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
class QueueScreen extends StatefulWidget {
|
|
@override
|
|
_QueueScreenState createState() => _QueueScreenState();
|
|
}
|
|
|
|
class _QueueScreenState extends State<QueueScreen> {
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: Text('Queue'),
|
|
actions: <Widget>[
|
|
IconButton(
|
|
icon: Icon(Icons.shuffle),
|
|
onPressed: () async {
|
|
await AudioService.customAction('shuffleQueue');
|
|
setState(() => {});
|
|
},
|
|
)
|
|
],
|
|
),
|
|
body: ListView.builder(
|
|
itemCount: AudioService.queue.length,
|
|
itemBuilder: (context, i) {
|
|
Track t = Track.fromMediaItem(AudioService.queue[i]);
|
|
return TrackTile(
|
|
t,
|
|
onTap: () async {
|
|
await AudioService.playFromMediaId(t.id);
|
|
Navigator.of(context).pop();
|
|
},
|
|
onHold: () {
|
|
MenuSheet m = MenuSheet(context);
|
|
m.defaultTrackMenu(t);
|
|
},
|
|
);
|
|
},
|
|
)
|
|
);
|
|
}
|
|
} |