tsacdop-podcast-app-android/lib/home/audioplayer.dart

1513 lines
70 KiB
Dart
Raw Normal View History

2020-02-25 10:58:33 +01:00
import 'dart:io';
import 'dart:math' as math;
2020-07-26 12:20:42 +02:00
import 'package:audio_service/audio_service.dart';
2020-02-25 10:58:33 +01:00
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
2020-07-26 12:20:42 +02:00
import 'package:line_icons/line_icons.dart';
2020-02-25 10:58:33 +01:00
import 'package:marquee/marquee.dart';
2020-07-26 12:20:42 +02:00
import 'package:provider/provider.dart';
2020-02-25 10:58:33 +01:00
import 'package:tuple/tuple.dart';
2020-03-01 13:17:06 +01:00
2020-07-26 12:20:42 +02:00
import '../episodes/episode_detail.dart';
import '../local_storage/key_value_storage.dart';
import '../local_storage/sqflite_localpodcast.dart';
import '../state/audio_state.dart';
2020-05-06 18:50:32 +02:00
import '../type/episodebrief.dart';
import '../type/play_histroy.dart';
import '../util/audiopanel.dart';
2020-07-26 12:20:42 +02:00
import '../util/custom_slider.dart';
import '../util/custom_widget.dart';
import '../util/extension_helper.dart';
import '../util/pageroute.dart';
import 'playlist.dart';
2020-04-11 19:23:12 +02:00
final List<BoxShadow> _customShadow = [
BoxShadow(blurRadius: 26, offset: Offset(-6, -6), color: Colors.white),
BoxShadow(
blurRadius: 8,
offset: Offset(2, 2),
color: Colors.grey[600].withOpacity(0.4))
];
final List<BoxShadow> _customShadowNight = [
BoxShadow(
blurRadius: 6,
offset: Offset(-1, -1),
color: Colors.grey[100].withOpacity(0.3)),
BoxShadow(blurRadius: 8, offset: Offset(2, 2), color: Colors.black)
];
2020-07-30 19:18:56 +02:00
const List kMinsToSelect = [10, 15, 20, 25, 30, 45, 60, 70, 80, 90, 99];
const List kMinPlayerHeight = <double>[70.0, 75.0, 80.0];
const List kMaxPlayerHeight = <double>[300.0, 325.0, 350.0];
2020-06-27 20:27:39 +02:00
2020-07-29 14:06:49 +02:00
class PlayerWidget extends StatelessWidget {
PlayerWidget({this.playerKey, this.isPlayingPage = false});
2020-07-29 14:06:49 +02:00
final GlobalKey<AudioPanelState> playerKey;
final bool isPlayingPage;
2020-07-29 14:06:49 +02:00
Widget _miniPanel(BuildContext context) {
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
2020-07-04 16:42:56 +02:00
final s = context.s;
2020-02-25 10:58:33 +01:00
return Container(
2020-07-29 14:06:49 +02:00
color: context.primaryColor,
2020-02-25 10:58:33 +01:00
height: 60,
child:
Column(mainAxisAlignment: MainAxisAlignment.start, children: <Widget>[
Selector<AudioPlayerNotifier, Tuple2<String, double>>(
2020-02-25 10:58:33 +01:00
selector: (_, audio) =>
Tuple2(audio.episode?.primaryColor, audio.seekSliderValue),
builder: (_, data, __) {
2020-08-15 19:43:45 +02:00
final c = context.brightness == Brightness.light
2020-03-01 13:17:06 +01:00
? data.item1.colorizedark()
: data.item1.colorizeLight();
2020-02-25 10:58:33 +01:00
return SizedBox(
2020-07-29 14:06:49 +02:00
height: 2,
child: LinearProgressIndicator(
value: data.item2,
backgroundColor: context.primaryColor,
2020-07-30 19:18:56 +02:00
valueColor: AlwaysStoppedAnimation<Color>(c),
2020-07-29 14:06:49 +02:00
),
);
2020-02-25 10:58:33 +01:00
},
),
Expanded(
2020-07-29 14:06:49 +02:00
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
2020-02-25 10:58:33 +01:00
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Expanded(
flex: 4,
child: Selector<AudioPlayerNotifier, String>(
selector: (_, audio) => audio.episode?.title,
2020-02-25 10:58:33 +01:00
builder: (_, title, __) {
return Text(
title,
style: TextStyle(fontWeight: FontWeight.bold),
maxLines: 2,
overflow: TextOverflow.clip,
2020-02-25 10:58:33 +01:00
);
},
),
),
Expanded(
flex: 2,
child: Selector<AudioPlayerNotifier,
Tuple3<bool, double, String>>(
selector: (_, audio) => Tuple3(
audio.buffering,
2020-02-25 10:58:33 +01:00
(audio.backgroundAudioDuration -
audio.backgroundAudioPosition) /
1000,
audio.remoteErrorMessage),
2020-02-25 10:58:33 +01:00
builder: (_, data, __) {
2020-07-29 14:06:49 +02:00
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: data.item3 != null
? Text(data.item3,
2020-07-26 12:20:42 +02:00
style:
const TextStyle(color: Color(0xFFFF0000)))
: data.item1
? Text(
2020-07-04 16:42:56 +02:00
s.buffering,
2020-07-29 14:06:49 +02:00
style:
TextStyle(color: context.accentColor),
)
2020-06-02 16:05:49 +02:00
: Text(
2020-07-04 16:42:56 +02:00
s.timeLeft(
2020-07-28 06:35:57 +02:00
(data.item2).toInt().toTime ?? ''),
2020-06-02 16:05:49 +02:00
maxLines: 2,
2020-02-25 10:58:33 +01:00
),
);
},
),
),
Expanded(
flex: 2,
2020-08-15 19:43:45 +02:00
child: Selector<AudioPlayerNotifier,
Tuple3<bool, bool, EpisodeBrief>>(
2020-07-29 14:06:49 +02:00
selector: (_, audio) =>
2020-08-15 19:43:45 +02:00
Tuple3(audio.buffering, audio.playing, audio.episode),
2020-07-29 14:06:49 +02:00
builder: (_, data, __) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
data.item1
? Stack(
alignment: Alignment.center,
children: <Widget>[
Padding(
padding: const EdgeInsets.symmetric(
vertical: 10.0),
child: SizedBox(
2020-08-15 19:43:45 +02:00
height: 30.0,
width: 30.0,
child: CircleAvatar(
backgroundColor: data.item3
.backgroudColor(context),
backgroundImage:
data.item3.avatarImage,
),
),
),
2020-07-29 14:06:49 +02:00
Container(
height: 40.0,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.black),
),
])
: data.item2
? InkWell(
onTap: data.item2
? () => audio.pauseAduio()
: null,
2020-08-15 19:43:45 +02:00
child:
ImageRotate(episodeItem: data.item3),
2020-07-29 14:06:49 +02:00
)
: InkWell(
onTap: data.item2
? null
: () => audio.resumeAudio(),
child: Stack(
alignment: Alignment.center,
children: <Widget>[
Padding(
padding: EdgeInsets.symmetric(
vertical: 10.0),
child: SizedBox(
2020-08-15 19:43:45 +02:00
height: 30.0,
width: 30.0,
child: CircleAvatar(
backgroundColor: data.item3
.backgroudColor(context),
backgroundImage:
data.item3.avatarImage,
),
),
2020-07-29 14:06:49 +02:00
),
Container(
height: 40.0,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.black),
),
if (!data.item1)
Icon(
Icons.play_arrow,
color: Colors.white,
)
],
),
),
IconButton(
onPressed: () => audio.playNext(),
iconSize: 20.0,
icon: Icon(Icons.skip_next),
color: context.textColor)
],
);
},
),
2020-02-25 10:58:33 +01:00
),
],
),
),
),
]),
);
}
@override
Widget build(BuildContext context) {
2020-07-30 19:18:56 +02:00
return Selector<AudioPlayerNotifier, Tuple2<bool, PlayerHeight>>(
selector: (_, audio) => Tuple2(audio.playerRunning, audio?.playerHeight),
builder: (_, data, __) {
2020-07-31 10:50:48 +02:00
if (!data.item1) {
return Center();
} else {
var minHeight = kMinPlayerHeight[data.item2.index];
var maxHeight = kMaxPlayerHeight[data.item2.index];
return AudioPanel(
minHeight: minHeight,
maxHeight: maxHeight,
key: playerKey,
miniPanel: _miniPanel(context),
expandedPanel: ControlPanel(
2020-07-30 19:18:56 +02:00
maxHeight: maxHeight,
isPlayingPage: isPlayingPage,
2020-07-31 10:50:48 +02:00
onExpand: () {
playerKey.currentState.scrollToTop();
},
onClose: () {
playerKey.currentState.backToMini();
},
));
}
2020-02-25 10:58:33 +01:00
},
);
}
}
2020-07-30 19:18:56 +02:00
class LastPosition extends StatelessWidget {
2020-03-31 18:36:20 +02:00
LastPosition({Key key}) : super(key: key);
Future<PlayHistory> getPosition(EpisodeBrief episode) async {
var dbHelper = DBHelper();
return await dbHelper.getPosition(episode);
}
@override
Widget build(BuildContext context) {
2020-07-04 16:42:56 +02:00
final s = context.s;
2020-03-31 18:36:20 +02:00
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
return Selector<AudioPlayerNotifier, EpisodeBrief>(
selector: (_, audio) => audio.episode,
builder: (context, episode, child) {
2020-08-09 17:10:32 +02:00
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Selector<AudioPlayerNotifier, bool>(
selector: (_, audio) => audio.skipSilence,
builder: (_, data, __) => FlatButton(
child: Row(
children: [
Icon(Icons.flash_on, size: 18),
SizedBox(width: 5),
Text(s.skipSilence),
],
),
color: data ? context.accentColor : Colors.transparent,
padding: EdgeInsets.symmetric(horizontal: 10),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(100.0),
side: BorderSide(
color: data
? context.accentColor
: Theme.of(context)
.colorScheme
.onSurface
.withOpacity(0.12))),
textColor: data ? Colors.white : null,
onPressed: () =>
audio.setSkipSilence(skipSilence: !data))),
SizedBox(width: 10),
Selector<AudioPlayerNotifier, bool>(
selector: (_, audio) => audio.boostVolume,
builder: (_, data, __) => FlatButton(
child: Row(
children: [
Icon(Icons.volume_up, size: 18),
SizedBox(width: 5),
Text(s.boostVolume),
],
),
color: data ? context.accentColor : Colors.transparent,
padding: EdgeInsets.symmetric(horizontal: 10),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(100.0),
side: BorderSide(
color: data
? context.accentColor
: Theme.of(context)
.colorScheme
.onSurface
.withOpacity(0.12))),
textColor: data ? Colors.white : null,
onPressed: () =>
audio.setBoostVolume(boostVolume: !data))),
SizedBox(width: 10),
FutureBuilder<PlayHistory>(
future: getPosition(episode),
builder: (context, snapshot) {
return snapshot.hasData
? snapshot.data.seekValue > 0.90
? Container(
2020-07-30 19:18:56 +02:00
height: 20,
2020-08-09 17:10:32 +02:00
padding: EdgeInsets.symmetric(horizontal: 10),
child: SizedBox(
width: 20,
height: 20,
child: CustomPaint(
painter: ListenedAllPainter(
context.accentColor,
stroke: 2.0),
),
2020-07-30 19:18:56 +02:00
),
2020-08-09 17:10:32 +02:00
)
: snapshot.data.seconds < 10
? Center()
: OutlineButton(
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(100.0),
side: BorderSide(
color: context.accentColor)),
highlightedBorderColor: Colors.green[700],
onPressed: () => audio.seekTo(
(snapshot.data.seconds * 1000).toInt()),
child: Row(
children: [
SizedBox(
width: 20,
height: 20,
child: CustomPaint(
painter: ListenedPainter(
context.textColor,
stroke: 2.0),
),
2020-08-01 09:31:18 +02:00
),
2020-08-09 17:10:32 +02:00
SizedBox(width: 5),
Text(snapshot.data.seconds.toTime),
],
),
)
: Center();
}),
Selector<AudioPlayerNotifier, double>(
selector: (_, audio) => audio.switchValue,
builder: (_, data, __) => data == 1
? Container(
height: 20,
width: 40,
child: Transform.rotate(
angle: math.pi * 0.7,
child: Icon(Icons.brightness_2,
size: 18, color: context.accentColor)))
: Center(),
)
],
),
2020-07-30 19:18:56 +02:00
);
2020-03-31 18:36:20 +02:00
},
);
}
}
2020-07-28 06:35:57 +02:00
class PlaylistWidget extends StatefulWidget {
const PlaylistWidget({Key key}) : super(key: key);
@override
_PlaylistWidgetState createState() => _PlaylistWidgetState();
}
class _PlaylistWidgetState extends State<PlaylistWidget> {
final GlobalKey<AnimatedListState> miniPlaylistKey = GlobalKey();
@override
Widget build(BuildContext context) {
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
return Container(
alignment: Alignment.topLeft,
height: 300,
width: double.infinity,
decoration: BoxDecoration(
color: context.accentColor.withAlpha(70),
borderRadius: BorderRadius.circular(10),
),
child: Column(
children: <Widget>[
Expanded(
child:
Selector<AudioPlayerNotifier, Tuple2<List<EpisodeBrief>, bool>>(
selector: (_, audio) =>
Tuple2(audio.queue.playlist, audio.queueUpdate),
builder: (_, data, __) {
2020-08-06 09:26:49 +02:00
var episodesToPlay = data.item1.sublist(1);
return AnimatedList(
key: miniPlaylistKey,
shrinkWrap: true,
scrollDirection: Axis.vertical,
initialItemCount: episodesToPlay.length,
itemBuilder: (context, index, animation) => ScaleTransition(
alignment: Alignment.center,
scale: animation,
child: Column(
children: <Widget>[
Row(
children: <Widget>[
Expanded(
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
audio.episodeLoad(data.item1[index]);
miniPlaylistKey.currentState.removeItem(
index,
(context, animation) => Center());
},
child: Container(
height: 60,
padding:
EdgeInsets.symmetric(horizontal: 20),
alignment: Alignment.centerLeft,
child: Row(
mainAxisAlignment:
MainAxisAlignment.center,
crossAxisAlignment:
CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(
padding: EdgeInsets.all(10.0),
child: ClipRRect(
borderRadius: BorderRadius.all(
Radius.circular(15.0)),
child: Container(
height: 30.0,
width: 30.0,
child: Image.file(File(
"${episodesToPlay[index].imagePath}"))),
2020-07-28 06:35:57 +02:00
),
),
2020-08-06 09:26:49 +02:00
Expanded(
child: Align(
alignment: Alignment.centerLeft,
child: Text(
episodesToPlay[index].title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
2020-07-28 06:35:57 +02:00
),
),
),
2020-08-06 09:26:49 +02:00
],
),
),
),
),
),
Padding(
padding:
const EdgeInsets.symmetric(horizontal: 20.0),
child: Material(
borderRadius: BorderRadius.circular(100),
clipBehavior: Clip.hardEdge,
color: context.primaryColor,
child: InkWell(
borderRadius:
BorderRadius.all(Radius.circular(15.0)),
onTap: () async {
var episdoe =
episodesToPlay.removeAt(index);
episodesToPlay.insert(0, episdoe);
2020-08-06 09:26:49 +02:00
miniPlaylistKey.currentState.removeItem(
index,
(context, animation) {
return Center();
},
duration: Duration.zero,
2020-08-06 09:26:49 +02:00
);
miniPlaylistKey.currentState.insertItem(0,
duration: Duration(milliseconds: 100));
await Future.delayed(
Duration(milliseconds: 100));
2020-08-06 09:26:49 +02:00
await audio
.moveToTop(data.item1[index + 1]);
},
child: SizedBox(
height: 30.0,
width: 30.0,
child: Transform.rotate(
angle: math.pi,
child: Icon(
LineIcons.download_solid,
size: 20.0,
2020-07-28 06:35:57 +02:00
),
),
2020-08-06 09:26:49 +02:00
),
2020-07-29 14:06:49 +02:00
),
2020-08-06 09:26:49 +02:00
),
2020-07-29 14:06:49 +02:00
),
2020-08-06 09:26:49 +02:00
],
),
2020-08-15 19:43:45 +02:00
Divider(height: 1),
2020-08-06 09:26:49 +02:00
],
2020-07-29 14:06:49 +02:00
),
2020-07-28 06:35:57 +02:00
),
);
},
),
),
2020-07-29 14:06:49 +02:00
SizedBox(
2020-07-28 06:35:57 +02:00
height: 60.0,
2020-07-29 14:06:49 +02:00
child: Padding(
2020-07-30 11:28:29 +02:00
padding: const EdgeInsets.symmetric(horizontal: 20),
2020-07-29 14:06:49 +02:00
child: Row(
children: <Widget>[
2020-07-30 11:28:29 +02:00
Text(
context.s.homeMenuPlaylist,
style: TextStyle(
color: context.accentColor,
fontWeight: FontWeight.bold,
fontSize: 16),
2020-07-28 06:35:57 +02:00
),
2020-07-29 14:06:49 +02:00
Spacer(),
Material(
borderRadius: BorderRadius.circular(100),
color: context.primaryColor,
child: InkWell(
2020-07-28 06:35:57 +02:00
borderRadius: BorderRadius.all(Radius.circular(15)),
2020-07-29 14:06:49 +02:00
onTap: () {
audio.playNext();
miniPlaylistKey.currentState
.removeItem(0, (context, animation) => Container());
miniPlaylistKey.currentState.insertItem(0);
},
child: SizedBox(
height: 30,
width: 60,
child: Icon(
Icons.skip_next,
size: 30,
2020-07-28 06:35:57 +02:00
),
),
),
),
2020-07-29 14:06:49 +02:00
SizedBox(width: 20),
Material(
borderRadius: BorderRadius.circular(100),
color: context.primaryColor,
2020-07-28 06:35:57 +02:00
child: InkWell(
2020-07-29 14:06:49 +02:00
borderRadius: BorderRadius.circular(15.0),
2020-07-28 06:35:57 +02:00
onTap: () {
Navigator.push(
context,
2020-08-15 19:43:45 +02:00
SlideLeftRoute(
page: PlaylistPage(
initPage: InitPage.playlist,
)),
2020-07-28 06:35:57 +02:00
);
},
child: SizedBox(
height: 30.0,
width: 30.0,
child: Transform.rotate(
angle: math.pi,
child: Icon(
LineIcons.database_solid,
size: 20.0,
),
),
),
),
),
2020-07-29 14:06:49 +02:00
],
),
2020-07-28 06:35:57 +02:00
),
),
],
),
);
}
}
2020-04-11 19:23:12 +02:00
class SleepMode extends StatefulWidget {
SleepMode({Key key}) : super(key: key);
@override
SleepModeState createState() => SleepModeState();
}
class SleepModeState extends State<SleepMode>
with SingleTickerProviderStateMixin {
int _minSelected;
AnimationController _controller;
Animation<double> _animation;
2020-06-27 20:27:39 +02:00
Future _getDefaultTime() async {
2020-07-26 12:20:42 +02:00
var defaultSleepTimerStorage = KeyValueStorage(defaultSleepTimerKey);
var defaultTime = await defaultSleepTimerStorage.getInt(defaultValue: 30);
2020-06-27 20:27:39 +02:00
setState(() => _minSelected = defaultTime);
}
2020-04-11 19:23:12 +02:00
@override
void initState() {
super.initState();
_minSelected = 30;
2020-06-27 20:27:39 +02:00
_getDefaultTime();
2020-07-29 14:06:49 +02:00
2020-04-11 19:23:12 +02:00
_controller =
AnimationController(vsync: this, duration: Duration(milliseconds: 400));
_animation = Tween<double>(begin: 0.0, end: 1.0).animate(_controller)
..addListener(() {
if (mounted) {
setState(() {});
}
2020-04-11 19:23:12 +02:00
});
_controller.addStatusListener((status) {
if (status == AnimationStatus.completed) {
Provider.of<AudioPlayerNotifier>(context, listen: false)
2020-07-30 11:28:29 +02:00
..sleepTimer(_minSelected)
..setSwitchValue = 1;
2020-04-11 19:23:12 +02:00
}
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
List<BoxShadow> customShadow(double scale) => [
BoxShadow(
blurRadius: 26 * (1 - scale),
offset: Offset(-6, -6) * (1 - scale),
color: Colors.white),
BoxShadow(
blurRadius: 8 * (1 - scale),
offset: Offset(2, 2) * (1 - scale),
color: Colors.grey[600].withOpacity(0.4))
];
List<BoxShadow> customShadowNight(double scale) => [
BoxShadow(
blurRadius: 6 * (1 - scale),
offset: Offset(-1, -1) * (1 - scale),
color: Colors.grey[100].withOpacity(0.3)),
BoxShadow(
blurRadius: 8 * (1 - scale),
offset: Offset(2, 2) * (1 - scale),
color: Colors.black)
];
@override
Widget build(BuildContext context) {
2020-07-04 16:42:56 +02:00
final s = context.s;
2020-07-26 12:20:42 +02:00
final _colorTween =
2020-07-28 06:35:57 +02:00
ColorTween(begin: context.accentColor.withAlpha(60), end: Colors.black);
2020-04-11 19:23:12 +02:00
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
return Selector<AudioPlayerNotifier, Tuple3<int, double, SleepTimerMode>>(
selector: (_, audio) =>
2020-07-28 06:35:57 +02:00
Tuple3(audio?.timeLeft, audio?.switchValue, audio.sleepTimerMode),
2020-04-11 19:23:12 +02:00
builder: (_, data, __) {
2020-07-30 11:28:29 +02:00
var fraction =
data.item2 == 1 ? 1.0 : math.min(_animation.value * 2, 1.0);
var move =
data.item2 == 1 ? 1.0 : math.max(_animation.value * 2 - 1, 0.0);
2020-07-28 06:35:57 +02:00
return LayoutBuilder(builder: (context, constraints) {
2020-07-29 14:06:49 +02:00
var width = constraints.maxWidth;
2020-07-28 06:35:57 +02:00
return Container(
height: 300,
decoration: BoxDecoration(
color: _colorTween.transform(move),
borderRadius: BorderRadius.circular(10)),
child: Stack(
children: <Widget>[
Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
2020-07-29 14:06:49 +02:00
SizedBox(
height: 10,
2020-07-28 06:35:57 +02:00
),
2020-07-29 14:06:49 +02:00
Expanded(
2020-07-28 06:35:57 +02:00
child: Padding(
padding: EdgeInsets.symmetric(vertical: 20),
2020-07-29 14:06:49 +02:00
child: move == 1
? Center()
: Wrap(
direction: Axis.horizontal,
2020-07-30 19:18:56 +02:00
children: kMinsToSelect
2020-07-29 14:06:49 +02:00
.map((e) => InkWell(
onTap: () =>
setState(() => _minSelected = e),
child: Container(
margin: EdgeInsets.all(10.0),
decoration: BoxDecoration(
color: (e == _minSelected)
? context.accentColor
: context.primaryColor,
shape: BoxShape.circle,
),
alignment: Alignment.center,
height: 30,
width: 30,
child: Text(e.toString(),
style: TextStyle(
fontWeight: FontWeight.bold,
color: (e == _minSelected)
? Colors.white
: null)),
2020-07-28 06:35:57 +02:00
),
2020-07-29 14:06:49 +02:00
))
.toList(),
),
2020-04-11 19:23:12 +02:00
),
),
2020-07-28 06:35:57 +02:00
Stack(
children: <Widget>[
2020-07-29 14:06:49 +02:00
SizedBox(
2020-04-11 19:23:12 +02:00
height: 100,
2020-07-29 14:06:49 +02:00
width: width,
2020-07-28 06:35:57 +02:00
),
Positioned(
left: data.item3 == SleepTimerMode.timer
2020-07-29 14:06:49 +02:00
? -width * (move) / 4
: width * (move) / 4,
child: SizedBox(
2020-07-28 06:35:57 +02:00
height: 100,
2020-07-29 14:06:49 +02:00
width: width,
2020-07-28 06:35:57 +02:00
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Container(
alignment: Alignment.center,
height: 40,
width: 120,
decoration: BoxDecoration(
border:
Border.all(color: context.primaryColor),
color: _colorTween.transform(move),
2020-07-30 11:28:29 +02:00
borderRadius: BorderRadius.circular(20),
2020-07-28 06:35:57 +02:00
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
audio.setSleepTimerMode =
SleepTimerMode.endOfEpisode;
if (fraction == 0) {
_controller.forward();
} else if (fraction == 1) {
_controller.reverse();
audio.cancelTimer();
}
},
2020-07-29 14:06:49 +02:00
borderRadius: BorderRadius.circular(20),
2020-07-28 06:35:57 +02:00
child: SizedBox(
2020-07-29 14:06:49 +02:00
height: 40,
width: 120,
child: Center(
child: Text(
2020-07-28 06:35:57 +02:00
s.endOfEpisode,
style: TextStyle(
color: (move > 0
? Colors.white
: null)),
2020-07-29 14:06:49 +02:00
),
),
),
2020-07-28 06:35:57 +02:00
),
2020-04-11 19:23:12 +02:00
),
),
2020-07-28 06:35:57 +02:00
Container(
height: 100 * (1 - fraction),
2020-07-29 14:06:49 +02:00
width: 1,
2020-07-28 06:35:57 +02:00
color: context.primaryColorDark,
2020-04-11 19:23:12 +02:00
),
2020-07-28 06:35:57 +02:00
Container(
height: 40,
width: 120,
alignment: Alignment.center,
decoration: BoxDecoration(
border:
Border.all(color: context.primaryColor),
color: _colorTween.transform(move),
2020-07-29 14:06:49 +02:00
borderRadius: BorderRadius.circular(20),
2020-07-28 06:35:57 +02:00
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
audio.setSleepTimerMode =
SleepTimerMode.timer;
if (fraction == 0) {
_controller.forward();
} else if (fraction == 1) {
_controller.reverse();
audio.cancelTimer();
}
},
2020-07-29 14:06:49 +02:00
borderRadius: BorderRadius.circular(20),
2020-07-28 06:35:57 +02:00
child: SizedBox(
height: 40,
width: 120,
child: Center(
child: Text(
data.item2 == 1
? data.item1.toTime
: (_minSelected * 60).toTime,
style: TextStyle(
color: (move > 0
? Colors.white
: null)),
),
2020-04-11 19:23:12 +02:00
),
),
),
),
2020-07-28 06:35:57 +02:00
)
],
),
2020-04-11 19:23:12 +02:00
),
),
2020-07-28 06:35:57 +02:00
],
),
2020-07-29 14:06:49 +02:00
SizedBox(
height: 60.0,
child: Padding(
2020-07-30 11:28:29 +02:00
padding: EdgeInsets.symmetric(horizontal: 20.0),
2020-07-29 14:06:49 +02:00
child: Align(
alignment: Alignment.centerLeft,
child: Text(
context.s.sleepTimer,
style: TextStyle(
2020-09-29 19:25:44 +02:00
color: context.accentColor,
2020-07-29 14:06:49 +02:00
fontWeight: FontWeight.bold,
fontSize: 16),
),
),
),
)
2020-07-28 06:35:57 +02:00
],
),
if (move > 0)
2020-07-30 11:28:29 +02:00
Positioned(
bottom: 120,
left: width / 2 - 100,
width: 200,
child: Center(
child: Transform.translate(
offset: Offset(0, -50 * move),
2020-07-30 11:28:29 +02:00
child: Text(s.goodNight,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 20,
color: Colors.white.withOpacity(move))),
2020-07-30 11:28:29 +02:00
),
),
2020-04-11 19:23:12 +02:00
),
2020-07-29 14:06:49 +02:00
if (data.item2 == 1) CustomPaint(painter: StarSky()),
if (data.item2 == 1) MeteorLoader()
2020-07-28 06:35:57 +02:00
],
),
);
});
2020-04-11 19:23:12 +02:00
},
);
}
}
class ControlPanel extends StatefulWidget {
ControlPanel(
{this.onExpand,
this.onClose,
this.maxHeight,
this.isPlayingPage = false,
Key key})
2020-07-30 19:18:56 +02:00
: super(key: key);
final VoidCallback onExpand;
final VoidCallback onClose;
2020-07-30 19:18:56 +02:00
final double maxHeight;
final bool isPlayingPage;
@override
_ControlPanelState createState() => _ControlPanelState();
}
class _ControlPanelState extends State<ControlPanel>
2020-07-29 14:06:49 +02:00
with TickerProviderStateMixin {
double _setSpeed;
AnimationController _controller;
Animation<double> _animation;
2020-07-29 14:06:49 +02:00
TabController _tabController;
2020-07-30 11:28:29 +02:00
int _tabIndex = 0;
List<BoxShadow> customShadow(double scale) => [
BoxShadow(
blurRadius: 26 * (1 - scale),
offset: Offset(-6, -6) * (1 - scale),
color: Colors.white),
BoxShadow(
blurRadius: 8 * (1 - scale),
offset: Offset(2, 2) * (1 - scale),
color: Colors.grey[600].withOpacity(0.4))
];
List<BoxShadow> customShadowNight(double scale) => [
BoxShadow(
blurRadius: 6 * (1 - scale),
offset: Offset(-1, -1) * (1 - scale),
color: Colors.grey[100].withOpacity(0.3)),
BoxShadow(
blurRadius: 8 * (1 - scale),
offset: Offset(2, 2) * (1 - scale),
color: Colors.black)
];
2020-09-10 12:01:28 +02:00
Future<List<double>> _getSpeedList() async {
var storage = KeyValueStorage('speedListKey');
return await storage.getSpeedList();
}
@override
void initState() {
_setSpeed = 0;
2020-07-30 11:28:29 +02:00
_tabController = TabController(vsync: this, length: 2)
..addListener(() {
setState(() => _tabIndex = _tabController.index);
});
_controller =
AnimationController(vsync: this, duration: Duration(milliseconds: 400));
_animation = Tween<double>(begin: 0.0, end: 1.0).animate(_controller)
..addListener(() {
setState(() => _setSpeed = _animation.value);
});
super.initState();
}
@override
void dispose() {
_controller.dispose();
2020-07-29 14:06:49 +02:00
_tabController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
2020-07-28 06:35:57 +02:00
return LayoutBuilder(
builder: (context, constraints) {
2020-07-30 19:18:56 +02:00
var height = constraints.maxHeight;
2020-07-28 06:35:57 +02:00
return Container(
color: context.primaryColor,
height: 300,
2020-07-29 14:06:49 +02:00
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Consumer<AudioPlayerNotifier>(
builder: (_, data, __) {
return Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(
padding:
EdgeInsets.only(top: 20, left: 30, right: 30),
2020-07-29 14:06:49 +02:00
child: SliderTheme(
data: SliderTheme.of(context).copyWith(
2020-07-30 19:18:56 +02:00
activeTrackColor: height <= widget.maxHeight
2020-07-29 14:06:49 +02:00
? context.accentColor.withAlpha(70)
2020-07-30 11:28:29 +02:00
: Colors.transparent,
2020-07-30 19:18:56 +02:00
inactiveTrackColor: height > widget.maxHeight
2020-07-30 11:28:29 +02:00
? Colors.transparent
2020-07-29 14:06:49 +02:00
: context.primaryColorDark,
trackHeight: 8.0,
trackShape: MyRectangularTrackShape(),
thumbColor: context.accentColor,
thumbShape: RoundSliderThumbShape(
enabledThumbRadius: 6.0,
disabledThumbRadius: 6.0,
),
2020-07-30 11:28:29 +02:00
overlayColor: context.accentColor.withAlpha(32),
2020-07-29 14:06:49 +02:00
overlayShape:
RoundSliderOverlayShape(overlayRadius: 4.0),
2020-07-28 06:35:57 +02:00
),
2020-07-29 14:06:49 +02:00
child: Slider(
value: data.seekSliderValue,
onChanged: (val) {
audio.sliderSeek(val);
}),
),
),
Container(
height: 20.0,
padding: EdgeInsets.symmetric(horizontal: 30.0),
2020-07-30 19:18:56 +02:00
child: height > widget.maxHeight
2020-07-29 14:06:49 +02:00
? Center()
: Row(
children: <Widget>[
Text(
(data.backgroundAudioPosition ~/ 1000)
.toTime ??
'',
style: TextStyle(fontSize: 10),
),
2020-07-29 14:06:49 +02:00
Expanded(
child: Container(
alignment: Alignment.center,
child: data.remoteErrorMessage != null
? Text(data.remoteErrorMessage,
style: const TextStyle(
color: Color(0xFFFF0000)))
: Text(
data.audioState ==
AudioProcessingState
.buffering ||
data.audioState ==
AudioProcessingState
.connecting ||
data.audioState ==
AudioProcessingState
.none ||
data.audioState ==
AudioProcessingState
.skippingToNext
? context.s.buffering
: '',
style: TextStyle(
2020-07-30 11:28:29 +02:00
color: context.accentColor),
2020-07-29 14:06:49 +02:00
),
),
),
Text(
(data.backgroundAudioDuration ~/ 1000)
.toTime ??
'',
style: TextStyle(fontSize: 10),
),
],
),
),
],
);
},
),
SizedBox(
height: 100,
child: Selector<AudioPlayerNotifier, bool>(
selector: (_, audio) => audio.playing,
builder: (_, playing, __) {
return Material(
color: Colors.transparent,
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
FlatButton(
color: Colors.transparent,
padding: EdgeInsets.only(right: 10, left: 10),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(100.0),
side: BorderSide(color: Colors.transparent)),
onPressed: playing ? () => audio.rewind() : null,
child: Row(
children: [
Icon(Icons.fast_rewind,
size: 32, color: Colors.grey[500]),
SizedBox(width: 5),
Selector<AudioPlayerNotifier, int>(
selector: (_, audio) =>
audio.rewindSeconds,
builder: (_, seconds, __) => Padding(
padding:
const EdgeInsets.only(top: 5.0),
child: Text('$seconds s',
style: GoogleFonts.teko(
textBaseline:
TextBaseline.ideographic,
textStyle: TextStyle(
color: Colors.grey[500],
fontSize: 25),
)),
2020-07-29 14:06:49 +02:00
)),
],
),
),
2020-07-29 14:06:49 +02:00
Container(
margin: EdgeInsets.symmetric(horizontal: 30),
height: 60,
width: 60,
decoration: BoxDecoration(
color: context.primaryColor,
shape: BoxShape.circle,
border: Border.all(
color:
context.brightness == Brightness.dark
2020-07-28 06:35:57 +02:00
? Colors.black12
: Colors.white10,
2020-07-29 14:06:49 +02:00
width: 1),
boxShadow:
context.brightness == Brightness.dark
? _customShadowNight
: _customShadow),
child: playing
? Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.all(
Radius.circular(30)),
onTap: playing
? () {
audio.pauseAduio();
}
: null,
child: SizedBox(
height: 60,
width: 60,
child: Icon(
Icons.pause,
size: 40,
2020-07-28 06:35:57 +02:00
),
2020-07-29 14:06:49 +02:00
),
),
)
: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.all(
Radius.circular(30)),
onTap: playing
? null
: () {
audio.resumeAudio();
},
child: SizedBox(
height: 60,
width: 60,
child: Icon(
Icons.play_arrow,
size: 40,
color: context.accentColor,
2020-07-28 06:35:57 +02:00
),
),
2020-07-29 14:06:49 +02:00
),
),
),
FlatButton(
padding: EdgeInsets.only(left: 10.0, right: 10),
2020-07-29 14:06:49 +02:00
onPressed:
playing ? () => audio.fastForward() : null,
color: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(100.0),
side: BorderSide(color: Colors.transparent)),
child: Row(
children: [
Selector<AudioPlayerNotifier, int>(
selector: (_, audio) =>
audio.fastForwardSeconds,
builder: (_, seconds, __) => Padding(
padding:
const EdgeInsets.only(top: 5.0),
child: Text('$seconds s',
style: GoogleFonts.teko(
textStyle: TextStyle(
color: Colors.grey[500],
fontSize: 25),
)),
)),
SizedBox(width: 10),
Icon(Icons.fast_forward,
size: 32.0, color: Colors.grey[500]),
],
),
2020-07-29 14:06:49 +02:00
)
],
),
);
},
),
),
2020-08-01 09:31:18 +02:00
SizedBox(
2020-07-30 19:18:56 +02:00
height: 80.0,
child: Column(
2020-08-01 09:31:18 +02:00
crossAxisAlignment: CrossAxisAlignment.center,
2020-07-30 19:18:56 +02:00
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
child: Selector<AudioPlayerNotifier, String>(
selector: (_, audio) => audio.episode.title,
builder: (_, title, __) {
return Container(
2020-08-01 09:31:18 +02:00
padding: EdgeInsets.only(left: 60, right: 60),
2020-07-30 19:18:56 +02:00
child: LayoutBuilder(
builder: (context, size) {
var span = TextSpan(
text: title,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 20));
var tp = TextPainter(
text: span,
maxLines: 1,
textDirection: TextDirection.ltr);
tp.layout(maxWidth: size.maxWidth);
if (tp.didExceedMaxLines) {
return Marquee(
text: title,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18),
scrollAxis: Axis.horizontal,
crossAxisAlignment:
CrossAxisAlignment.start,
blankSpace: 30.0,
velocity: 50.0,
pauseAfterRound: Duration.zero,
startPadding: 30.0,
accelerationDuration:
Duration(milliseconds: 100),
accelerationCurve: Curves.linear,
decelerationDuration:
Duration(milliseconds: 100),
decelerationCurve: Curves.linear,
);
} else {
return Text(
title,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 20),
);
}
},
),
);
2020-07-29 14:06:49 +02:00
},
),
2020-07-30 19:18:56 +02:00
),
if (height <= widget.maxHeight) LastPosition()
],
2020-07-29 14:06:49 +02:00
),
),
2020-07-30 19:18:56 +02:00
if (height > widget.maxHeight)
2020-07-29 14:06:49 +02:00
SizedBox(
2020-07-30 19:18:56 +02:00
height: height - widget.maxHeight,
2020-07-29 14:06:49 +02:00
child: SingleChildScrollView(
physics: NeverScrollableScrollPhysics(),
child: SizedBox(
height: 300,
2020-07-30 11:28:29 +02:00
child: ScrollConfiguration(
behavior: NoGrowBehavior(),
child: TabBarView(
controller: _tabController,
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20.0),
child: PlaylistWidget(),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20.0),
child: SleepMode(),
)
]),
))),
2020-07-29 14:06:49 +02:00
),
Expanded(
child: Stack(
alignment: Alignment.center,
children: [
2020-07-30 19:18:56 +02:00
if (height <= widget.maxHeight)
2020-07-29 14:06:49 +02:00
Selector<AudioPlayerNotifier,
2020-09-10 12:01:28 +02:00
Tuple4<EpisodeBrief, bool, bool, double>>(
selector: (_, audio) => Tuple4(
audio.episode,
audio.stopOnComplete,
audio.startSleepTimer,
audio.currentSpeed),
2020-07-29 14:06:49 +02:00
builder: (_, data, __) {
2020-09-10 12:01:28 +02:00
final currentSpeed = data.item4 ?? 1.0;
2020-07-29 14:06:49 +02:00
return Container(
padding:
const EdgeInsets.symmetric(horizontal: 20.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
if (_setSpeed == 0)
Expanded(
child: InkWell(
onTap: () async {
widget.onClose();
if (!widget.isPlayingPage) {
Navigator.push(
context,
FadeRoute(
page: EpisodeDetail(
episodeItem: data.item1,
heroTag: 'playpanel')));
}
},
2020-07-29 14:06:49 +02:00
child: Row(
children: [
SizedBox(
height: 30.0,
width: 30.0,
child: CircleAvatar(
2020-08-15 19:43:45 +02:00
backgroundImage:
data.item1.avatarImage,
2020-07-29 14:06:49 +02:00
),
),
SizedBox(width: 5),
SizedBox(
width: 100,
child: Text(
data.item1.feedTitle,
maxLines: 1,
2020-07-29 14:06:49 +02:00
overflow: TextOverflow.fade,
),
),
],
),
),
2020-07-28 06:35:57 +02:00
),
2020-07-29 14:06:49 +02:00
if (_setSpeed > 0)
Expanded(
child: SingleChildScrollView(
padding: EdgeInsets.all(10.0),
scrollDirection: Axis.horizontal,
2020-09-10 12:01:28 +02:00
child: FutureBuilder<List<double>>(
future: _getSpeedList(),
initialData: [],
builder: (context, snapshot) => Row(
children: snapshot.data
.map<Widget>((e) => InkWell(
onTap: () {
if (_setSpeed == 1) {
audio.setSpeed(e);
}
},
child: Container(
height: 30,
width: 30,
margin: EdgeInsets
.symmetric(
horizontal: 5),
decoration: e ==
currentSpeed &&
_setSpeed > 0
? BoxDecoration(
color: context
.accentColor,
shape: BoxShape
.circle,
boxShadow: context
.brightness ==
Brightness
.light
? customShadow(
1.0)
: customShadowNight(
1.0),
)
: BoxDecoration(
color: context
.primaryColor,
shape: BoxShape
.circle,
boxShadow: context
.brightness ==
Brightness
.light
? customShadow(1 -
_setSpeed)
: customShadowNight(1 -
_setSpeed)),
alignment:
Alignment.center,
child: _setSpeed > 0
? Text(e.toString(),
style: TextStyle(
fontWeight:
FontWeight
.bold,
color: e ==
currentSpeed
? Colors
.white
: null))
: Center(),
),
))
.toList(),
),
2020-07-29 14:06:49 +02:00
),
2020-07-28 06:35:57 +02:00
),
2020-07-29 14:06:49 +02:00
),
IconButton(
padding: EdgeInsets.zero,
onPressed: () {
if (_setSpeed == 0) {
_controller.forward();
} else {
_controller.reverse();
2020-07-28 06:35:57 +02:00
}
},
2020-07-29 14:06:49 +02:00
icon: Row(
mainAxisAlignment:
MainAxisAlignment.spaceEvenly,
children: <Widget>[
Transform.rotate(
angle: math.pi * _setSpeed,
child: Text('X')),
2020-09-10 12:01:28 +02:00
Text(currentSpeed.toStringAsFixed(1)),
2020-07-29 14:06:49 +02:00
],
2020-07-28 06:35:57 +02:00
),
2020-07-29 14:06:49 +02:00
),
],
),
);
},
2020-07-28 06:35:57 +02:00
),
2020-07-30 11:28:29 +02:00
if (_setSpeed == 0)
Positioned(
2020-07-30 19:18:56 +02:00
bottom: widget.maxHeight == kMaxPlayerHeight[2]
? 35.0
: widget.maxHeight == kMaxPlayerHeight[1]
? 25.0
: 15.0,
2020-07-30 11:28:29 +02:00
child: InkWell(
child: SizedBox(
height: 50,
width: 100,
child: Align(
alignment: Alignment.bottomCenter,
child: CustomPaint(
size: Size(100, 5),
painter: TabIndicator(
index: _tabIndex,
indicatorSize: 20,
2020-07-30 19:18:56 +02:00
fraction:
(height - widget.maxHeight) / 300,
2020-07-30 11:28:29 +02:00
accentColor: context.accentColor,
color: context.textColor)),
2020-07-29 14:06:49 +02:00
),
2020-07-30 11:28:29 +02:00
),
onTap: widget.onExpand),
2020-07-30 11:28:29 +02:00
),
2020-07-30 19:18:56 +02:00
if (_setSpeed == 0 && height > widget.maxHeight)
2020-07-30 11:28:29 +02:00
Transform.translate(
2020-07-30 19:18:56 +02:00
offset:
Offset(0, 5) * (height - widget.maxHeight) / 300,
2020-07-30 11:28:29 +02:00
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: context.width / 2 - 80),
child: TabBar(
controller: _tabController,
indicatorSize: TabBarIndicatorSize.label,
labelColor: context.accentColor,
unselectedLabelColor: context.textColor,
indicator: BoxDecoration(),
tabs: [
Container(
height: 20,
width: 20,
child: Icon(Icons.playlist_play)),
Container(
height: 20,
width: 20,
child: Transform.rotate(
angle: math.pi * 0.7,
child: Icon(Icons.brightness_2,
size: 18))),
],
),
),
),
2020-07-29 14:06:49 +02:00
],
),
),
]),
2020-07-28 06:35:57 +02:00
);
},
);
}
}