tsacdop-podcast-app-android/lib/episodes/episodedetail.dart

793 lines
27 KiB
Dart

import 'dart:io';
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:tsacdop/home/audioplayer.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:intl/intl.dart';
import 'package:tuple/tuple.dart';
import 'package:audio_service/audio_service.dart';
import 'package:tsacdop/class/audiostate.dart';
import 'package:tsacdop/class/episodebrief.dart';
import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
import 'episodedownload.dart';
class EpisodeDetail extends StatefulWidget {
final EpisodeBrief episodeItem;
final String heroTag;
EpisodeDetail({this.episodeItem, this.heroTag, Key key}) : super(key: key);
@override
_EpisodeDetailState createState() => _EpisodeDetailState();
}
class _EpisodeDetailState extends State<EpisodeDetail> {
final textstyle = TextStyle(fontSize: 15.0, color: Colors.black);
double downloadProgress;
bool _loaddes;
bool _showMenu;
String path;
Future getSDescription(String url) async {
var dbHelper = DBHelper();
widget.episodeItem.description = (await dbHelper.getDescription(url))
.replaceAll(RegExp(r'\s?<p>(<br>)?</p>\s?'), '');
if (mounted)
setState(() {
_loaddes = true;
});
}
ScrollController _controller;
_scrollListener() {
if (_controller.offset > _controller.position.maxScrollExtent * 0.8) {
setState(() {
_showMenu = true;
});
} else
setState(() {
_showMenu = false;
});
}
_launchUrl(String url) async {
if (await canLaunch(url)) {
await launch(url);
} else {
throw 'Could not launch $url';
}
}
@override
void initState() {
super.initState();
_loaddes = false;
_showMenu = false;
getSDescription(widget.episodeItem.enclosureUrl);
_controller = ScrollController();
_controller.addListener(_scrollListener);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle(
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
systemNavigationBarColor: Theme.of(context).primaryColor,
systemNavigationBarIconBrightness:
Theme.of(context).accentColorBrightness,
// statusBarColor: Theme.of(context).primaryColor,
),
child: Scaffold(
backgroundColor: Theme.of(context).primaryColor,
appBar: AppBar(
title: Text(widget.episodeItem.feedTitle),
centerTitle: true,
),
body: Stack(
children: <Widget>[
Container(
color: Theme.of(context).primaryColor,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Container(
padding: EdgeInsets.symmetric(horizontal: 20.0),
alignment: Alignment.topLeft,
child: Text(
widget.episodeItem.title,
style: Theme.of(context).textTheme.headline5,
),
),
Container(
alignment: Alignment.centerLeft,
padding: EdgeInsets.symmetric(horizontal: 20.0),
height: 30.0,
child: Text(
'Published ' +
DateFormat.yMMMd().format(
DateTime.fromMillisecondsSinceEpoch(
widget.episodeItem.pubDate)),
style: TextStyle(
color: Theme.of(context).accentColor)),
),
Container(
padding: EdgeInsets.symmetric(horizontal: 20.0),
height: 50.0,
child: Row(
children: <Widget>[
(widget.episodeItem.explicit == 1)
? Container(
decoration: BoxDecoration(
color: Colors.red[800],
shape: BoxShape.circle),
height: 25.0,
width: 25.0,
margin: EdgeInsets.only(right: 10.0),
alignment: Alignment.center,
child: Text('E',
style:
TextStyle(color: Colors.white)))
: Center(),
widget.episodeItem.duration != 0
? Container(
decoration: BoxDecoration(
color: Colors.cyan[300],
borderRadius: BorderRadius.all(
Radius.circular(15.0))),
height: 25.0,
margin: EdgeInsets.only(right: 10.0),
padding: EdgeInsets.symmetric(
horizontal: 10.0),
alignment: Alignment.center,
child: Text(
(widget.episodeItem.duration)
.toString() +
'mins',
style: textstyle),
)
: Center(),
widget.episodeItem.enclosureLength != null
? Container(
decoration: BoxDecoration(
color: Colors.lightBlue[300],
borderRadius: BorderRadius.all(
Radius.circular(15.0))),
height: 25.0,
margin: EdgeInsets.only(right: 10.0),
padding: EdgeInsets.symmetric(
horizontal: 10.0),
alignment: Alignment.center,
child: Text(
((widget.episodeItem
.enclosureLength) ~/
1000000)
.toString() +
'MB',
style: textstyle),
)
: Center(),
],
),
),
],
),
),
Expanded(
child: Container(
padding: EdgeInsets.only(top: 5.0),
child: SingleChildScrollView(
scrollDirection: Axis.vertical,
//physics: const AlwaysScrollableScrollPhysics(),
controller: _controller,
child: _loaddes
? (widget.episodeItem.description.contains('<'))
? Html(
padding:
EdgeInsets.symmetric(horizontal: 20.0),
defaultTextStyle: TextStyle(height: 1.8),
data: widget.episodeItem.description,
linkStyle: TextStyle(
color: Theme.of(context).accentColor,
decoration: TextDecoration.underline,
textBaseline: TextBaseline.ideographic),
onLinkTap: (url) {
_launchUrl(url);
},
useRichText: true,
)
: Container(
padding:
EdgeInsets.symmetric(horizontal: 20.0),
alignment: Alignment.topLeft,
child: Text(
widget.episodeItem.description,
style: TextStyle(
height: 1.8,
),
))
: Center(),
),
),
),
Selector<AudioPlayerNotifier, bool>(
selector: (_, audio) => audio.playerRunning,
builder: (_, data, __) {
return Padding(
padding: EdgeInsets.only(bottom: data ? 60.0 : 0),
);
}),
],
),
),
Selector<AudioPlayerNotifier, bool>(
selector: (_, audio) => audio.playerRunning,
builder: (_, data, __) {
return Container(
alignment: Alignment.bottomCenter,
padding: EdgeInsets.only(bottom: data ? 60.0 : 0),
child: AnimatedContainer(
duration: Duration(milliseconds: 400),
height: !_showMenu ? 50 : 0,
child: SingleChildScrollView(
scrollDirection: Axis.vertical,
child: MenuBar(
episodeItem: widget.episodeItem,
heroTag: widget.heroTag,
),
),
),
);
}),
Container(child: PlayerWidget()),
],
),
),
);
}
}
class MenuBar extends StatefulWidget {
final EpisodeBrief episodeItem;
final String heroTag;
MenuBar({this.episodeItem, this.heroTag, Key key}) : super(key: key);
@override
_MenuBarState createState() => _MenuBarState();
}
class _MenuBarState extends State<MenuBar> {
bool _liked;
int _like;
Future<int> saveLiked(String url) async {
var dbHelper = DBHelper();
int result = await dbHelper.setLiked(url);
if (result == 1 && mounted) setState(() => _liked = true);
return result;
}
Future<int> setUnliked(String url) async {
var dbHelper = DBHelper();
int result = await dbHelper.setUniked(url);
if (result == 1 && mounted)
setState(() {
_liked = false;
_like = 0;
});
return result;
}
@override
void initState() {
super.initState();
_liked = false;
_like = widget.episodeItem.liked;
}
Widget _buttonOnMenu(Widget widget, VoidCallback onTap) => Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
child: Container(
height: 50.0,
padding: EdgeInsets.symmetric(horizontal: 15.0),
child: widget),
),
);
@override
Widget build(BuildContext context) {
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
return Container(
height: 50.0,
decoration: BoxDecoration(
color: Theme.of(context).scaffoldBackgroundColor,
border: Border.all(
color: Theme.of(context).brightness == Brightness.light
? Colors.grey[200]
: Theme.of(context).primaryColor,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Hero(
tag: widget.episodeItem.enclosureUrl + widget.heroTag,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 10.0),
child: Container(
height: 30.0,
width: 30.0,
color: Theme.of(context).scaffoldBackgroundColor,
child: CircleAvatar(
backgroundImage:
FileImage(File("${widget.episodeItem.imagePath}"))),
),
),
),
(_like == 0 && !_liked)
? _buttonOnMenu(
Icon(
Icons.favorite_border,
color: Colors.grey[700],
),
() => saveLiked(widget.episodeItem.enclosureUrl))
: (_like == 1 && !_liked)
? _buttonOnMenu(
Icon(
Icons.favorite,
color: Colors.red,
),
() => setUnliked(widget.episodeItem.enclosureUrl))
: Stack(
alignment: Alignment.center,
children: <Widget>[
LoveOpen(),
_buttonOnMenu(
Icon(
Icons.favorite,
color: Colors.red,
),
() => setUnliked(widget.episodeItem.enclosureUrl)),
],
),
DownloadButton(episodeBrief: widget.episodeItem),
Selector<AudioPlayerNotifier, List<String>>(
selector: (_, audio) =>
audio.queue.playlist.map((e) => e.enclosureUrl).toList(),
builder: (_, data, __) {
return data.contains(widget.episodeItem.enclosureUrl)
? _buttonOnMenu(
Icon(Icons.playlist_add_check,
color: Theme.of(context).accentColor),
() {})
: _buttonOnMenu(
Icon(Icons.playlist_add, color: Colors.grey[700]), () {
Fluttertoast.showToast(
msg: 'Added to playlist',
gravity: ToastGravity.BOTTOM,
);
audio.addToPlaylist(widget.episodeItem);
});
},
),
Spacer(),
// Text(audio.audioState.toString()),
Selector<AudioPlayerNotifier,
Tuple2<EpisodeBrief, BasicPlaybackState>>(
selector: (_, audio) => Tuple2(audio.episode, audio.audioState),
builder: (_, data, __) {
return (widget.episodeItem.title != data.item1?.title)
? Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
audio.episodeLoad(widget.episodeItem);
},
child: Container(
alignment: Alignment.center,
height: 50.0,
padding: EdgeInsets.symmetric(horizontal: 20.0),
child: Row(
children: <Widget>[
Text('Play Now',
style: TextStyle(
color: Theme.of(context).accentColor,
fontSize: 15,
fontWeight: FontWeight.bold,
)),
Icon(
Icons.play_arrow,
color: Theme.of(context).accentColor,
),
],
),
),
),
)
: (widget.episodeItem.title == data.item1?.title &&
data.item2 == BasicPlaybackState.playing)
? Container(
padding: EdgeInsets.only(right: 30),
child: SizedBox(
width: 20, height: 15, child: WaveLoader()))
: Container(
padding: EdgeInsets.only(right: 30),
child: SizedBox(
width: 20,
height: 15,
child: LineLoader(),
),
);
},
),
],
),
);
}
}
class LinePainter extends CustomPainter {
double _fraction;
Paint _paint;
Color _maincolor;
LinePainter(this._fraction, this._maincolor) {
_paint = Paint()
..color = _maincolor
..strokeWidth = 2.0
..strokeCap = StrokeCap.round;
}
@override
void paint(Canvas canvas, Size size) {
canvas.drawLine(Offset(0, size.height / 2.0),
Offset(size.width * _fraction, size.height / 2.0), _paint);
}
@override
bool shouldRepaint(LinePainter oldDelegate) {
return oldDelegate._fraction != _fraction;
}
}
class LineLoader extends StatefulWidget {
@override
_LineLoaderState createState() => _LineLoaderState();
}
class _LineLoaderState extends State<LineLoader>
with SingleTickerProviderStateMixin {
double _fraction = 0.0;
Animation animation;
AnimationController controller;
@override
void initState() {
super.initState();
controller =
AnimationController(vsync: this, duration: Duration(milliseconds: 500));
animation = Tween(begin: 0.0, end: 1.0).animate(controller)
..addListener(() {
if (mounted)
setState(() {
_fraction = animation.value;
});
});
controller.forward();
controller.addStatusListener((status) {
if (status == AnimationStatus.completed) {
controller.reset();
} else if (status == AnimationStatus.dismissed) {
controller.forward();
}
});
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: LinePainter(_fraction, Theme.of(context).accentColor));
}
}
class WavePainter extends CustomPainter {
double _fraction;
double _value;
Color _color;
WavePainter(this._fraction, this._color);
@override
void paint(Canvas canvas, Size size) {
if (_fraction < 0.5) {
_value = _fraction;
} else {
_value = 1 - _fraction;
}
Path _path = Path();
Paint _paint = Paint()
..color = _color
..strokeWidth = 2.0
..strokeCap = StrokeCap.round
..style = PaintingStyle.stroke;
_path.moveTo(0, size.height / 2);
_path.lineTo(0, size.height / 2 + size.height * _value * 0.2);
_path.moveTo(0, size.height / 2);
_path.lineTo(0, size.height / 2 - size.height * _value * 0.2);
_path.moveTo(size.width / 4, size.height / 2);
_path.lineTo(size.width / 4, size.height / 2 + size.height * _value * 0.8);
_path.moveTo(size.width / 4, size.height / 2);
_path.lineTo(size.width / 4, size.height / 2 - size.height * _value * 0.8);
_path.moveTo(size.width / 2, size.height / 2);
_path.lineTo(size.width / 2, size.height / 2 + size.height * _value * 0.5);
_path.moveTo(size.width / 2, size.height / 2);
_path.lineTo(size.width / 2, size.height / 2 - size.height * _value * 0.5);
_path.moveTo(size.width * 3 / 4, size.height / 2);
_path.lineTo(
size.width * 3 / 4, size.height / 2 + size.height * _value * 0.6);
_path.moveTo(size.width * 3 / 4, size.height / 2);
_path.lineTo(
size.width * 3 / 4, size.height / 2 - size.height * _value * 0.6);
_path.moveTo(size.width, size.height / 2);
_path.lineTo(size.width, size.height / 2 + size.height * _value * 0.2);
_path.moveTo(size.width, size.height / 2);
_path.lineTo(size.width, size.height / 2 - size.height * _value * 0.2);
canvas.drawPath(_path, _paint);
}
@override
bool shouldRepaint(WavePainter oldDelegate) {
return oldDelegate._fraction != _fraction;
}
}
class WaveLoader extends StatefulWidget {
@override
_WaveLoaderState createState() => _WaveLoaderState();
}
class _WaveLoaderState extends State<WaveLoader>
with SingleTickerProviderStateMixin {
double _fraction = 0.0;
Animation animation;
AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this, duration: Duration(milliseconds: 1000));
animation = Tween(begin: 0.0, end: 1.0).animate(_controller)
..addListener(() {
if (mounted)
setState(() {
_fraction = animation.value;
});
});
_controller.forward();
_controller.addStatusListener((status) {
if (status == AnimationStatus.completed) {
_controller.reset();
} else if (status == AnimationStatus.dismissed) {
_controller.forward();
}
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: WavePainter(_fraction, Theme.of(context).accentColor));
}
}
class ImageRotate extends StatefulWidget {
final String title;
final String path;
ImageRotate({this.title, this.path, Key key}) : super(key: key);
@override
_ImageRotateState createState() => _ImageRotateState();
}
class _ImageRotateState extends State<ImageRotate>
with SingleTickerProviderStateMixin {
Animation _animation;
AnimationController _controller;
double _value;
@override
void initState() {
super.initState();
_value = 0;
_controller = AnimationController(
vsync: this,
duration: Duration(milliseconds: 2000),
);
_animation = Tween(begin: 0.0, end: 1.0).animate(_controller)
..addListener(() {
if (mounted)
setState(() {
_value = _animation.value;
});
});
_controller.forward();
_controller.addStatusListener((status) {
if (status == AnimationStatus.completed) {
_controller.reset();
} else if (status == AnimationStatus.dismissed) {
_controller.forward();
}
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Transform.rotate(
angle: 2 * math.pi * _value,
child: Container(
padding: EdgeInsets.all(10.0),
child: ClipRRect(
borderRadius: BorderRadius.all(Radius.circular(15.0)),
child: Container(
height: 30.0,
width: 30.0,
color: Colors.white,
child: Image.file(File("${widget.path}")),
),
),
),
);
}
}
class LovePainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
Path _path = Path();
Paint _paint = Paint()
..color = Colors.red
..strokeWidth = 2.0
..strokeCap = StrokeCap.round;
_path.moveTo(size.width / 2, size.height / 6);
_path.quadraticBezierTo(size.width / 4, 0, size.width / 8, size.height / 6);
_path.quadraticBezierTo(
0, size.height / 3, size.width / 8, size.height * 0.55);
_path.quadraticBezierTo(
size.width / 4, size.height * 0.8, size.width / 2, size.height);
_path.quadraticBezierTo(size.width * 0.75, size.height * 0.8,
size.width * 7 / 8, size.height * 0.55);
_path.quadraticBezierTo(
size.width, size.height / 3, size.width * 7 / 8, size.height / 6);
_path.quadraticBezierTo(
size.width * 3 / 4, 0, size.width / 2, size.height / 6);
canvas.drawPath(_path, _paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
class LoveOpen extends StatefulWidget {
@override
_LoveOpenState createState() => _LoveOpenState();
}
class _LoveOpenState extends State<LoveOpen>
with SingleTickerProviderStateMixin {
Animation _animationA;
AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: Duration(milliseconds: 300),
);
_animationA = Tween(begin: 0.0, end: 1.0).animate(_controller)
..addListener(() {
if (mounted) setState(() {});
});
_controller.forward();
_controller.addStatusListener((status) {
if (status == AnimationStatus.completed) {
_controller.reset();
}
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
Widget _littleHeart(double scale, double value, double angle) => Container(
alignment: Alignment.centerLeft,
padding: EdgeInsets.only(left: value),
child: ScaleTransition(
scale: _animationA,
alignment: Alignment.center,
child: Transform.rotate(
angle: angle,
child: SizedBox(
height: 5 * scale,
width: 6 * scale,
child: CustomPaint(
painter: LovePainter(),
),
),
),
),
);
@override
Widget build(BuildContext context) {
return Container(
width: 50,
height: 50,
alignment: Alignment.center,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Row(
children: <Widget>[
_littleHeart(0.5, 10, -math.pi / 6),
_littleHeart(1.2, 3, 0),
],
),
Row(
children: <Widget>[
_littleHeart(0.8, 6, math.pi * 1.5),
_littleHeart(0.9, 24, math.pi / 2),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
_littleHeart(1, 8, -math.pi * 0.7),
_littleHeart(0.8, 8, math.pi),
_littleHeart(0.6, 3, -math.pi * 1.2)
],
),
],
),
);
}
}