tsacdop-podcast-app-android/lib/podcasts/podcast_detail.dart

837 lines
40 KiB
Dart
Raw Normal View History

import 'dart:io';
import 'dart:async';
import 'package:connectivity/connectivity.dart';
2020-02-09 13:29:09 +01:00
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:html/parser.dart';
import 'package:tsacdop/state/download_state.dart';
2020-03-01 13:17:06 +01:00
import 'package:url_launcher/url_launcher.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:provider/provider.dart';
2020-04-11 19:23:12 +02:00
import 'package:line_icons/line_icons.dart';
import 'package:cached_network_image/cached_network_image.dart';
2020-05-06 18:50:32 +02:00
import '../type/podcastlocal.dart';
import '../type/episodebrief.dart';
import '../type/play_histroy.dart';
import '../local_storage/sqflite_localpodcast.dart';
import '../local_storage/key_value_storage.dart';
import '../util/episodegrid.dart';
import '../home/audioplayer.dart';
2020-05-06 18:50:32 +02:00
import '../type/fireside_data.dart';
import '../util/extension_helper.dart';
import '../util/custompaint.dart';
import '../util/general_dialog.dart';
2020-07-07 17:29:21 +02:00
import '../state/audio_state.dart';
2020-02-09 13:29:09 +01:00
class PodcastDetail extends StatefulWidget {
PodcastDetail({Key key, @required this.podcastLocal, this.hide = false})
: super(key: key);
2020-02-09 13:29:09 +01:00
final PodcastLocal podcastLocal;
final bool hide;
2020-02-09 13:29:09 +01:00
@override
_PodcastDetailState createState() => _PodcastDetailState();
}
class _PodcastDetailState extends State<PodcastDetail> {
final GlobalKey<RefreshIndicatorState> _refreshIndicatorKey =
GlobalKey<RefreshIndicatorState>();
/// Fireside background if hosted on fireside.
String _backgroundImage;
/// Fireside hosts if hosted on fireside.
List<PodcastHost> _hosts;
/// Episodes total count.
int _episodeCount;
/// Default layout.
Layout _layout;
2020-06-05 20:33:47 +02:00
bool _scroll;
2020-07-04 16:42:56 +02:00
Future _updateRssItem(BuildContext context, PodcastLocal podcastLocal) async {
2020-02-09 13:29:09 +01:00
var dbHelper = DBHelper();
final result = await dbHelper.updatePodcastRss(podcastLocal);
if (result == 0) {
Fluttertoast.showToast(
msg: 'No Update',
gravity: ToastGravity.TOP,
);
} else if (result > 0) {
Fluttertoast.showToast(
2020-07-04 16:42:56 +02:00
msg: context.s.updateEpisodesCount(result),
gravity: ToastGravity.TOP,
);
2020-06-11 17:13:10 +02:00
bool autoDownload = await dbHelper.getAutoDownload(podcastLocal.id);
if (autoDownload) {
2020-06-11 17:13:10 +02:00
var downloader = Provider.of<DownloadState>(context, listen: false);
var result = await Connectivity().checkConnectivity();
KeyValueStorage autoDownloadStorage =
KeyValueStorage(autoDownloadNetworkKey);
int autoDownloadNetwork = await autoDownloadStorage.getInt();
if (autoDownloadNetwork == 1) {
List<EpisodeBrief> episodes =
await dbHelper.getNewEpisodes(podcastLocal.id);
2020-06-11 17:13:10 +02:00
// For safety
if (episodes.length < 100)
2020-07-13 09:41:59 +02:00
for (var episode in episodes)
2020-06-11 17:13:10 +02:00
downloader.startTask(episode, showNotification: false);
} else if (result == ConnectivityResult.wifi) {
List<EpisodeBrief> episodes =
await dbHelper.getNewEpisodes(podcastLocal.id);
2020-06-11 17:13:10 +02:00
//For safety
if (episodes.length < 100)
2020-07-13 09:41:59 +02:00
for (var episode in episodes)
2020-06-11 17:13:10 +02:00
downloader.startTask(episode, showNotification: false);
}
}
} else {
2020-04-11 19:23:12 +02:00
Fluttertoast.showToast(
2020-07-04 16:42:56 +02:00
msg: context.s.updateFailed,
2020-04-11 19:23:12 +02:00
gravity: ToastGravity.TOP,
);
}
if (mounted) setState(() {});
2020-02-09 13:29:09 +01:00
}
Future<List<EpisodeBrief>> _getRssItem(
2020-04-11 19:23:12 +02:00
PodcastLocal podcastLocal, int i, bool reverse) async {
2020-02-09 13:29:09 +01:00
var dbHelper = DBHelper();
_episodeCount = await dbHelper.getPodcastCounts(podcastLocal.id);
KeyValueStorage storage = KeyValueStorage(podcastLayoutKey);
int index = await storage.getInt(defaultValue: 1);
if (_layout == null) _layout = Layout.values[index];
2020-04-11 19:23:12 +02:00
List<EpisodeBrief> episodes =
await dbHelper.getRssItem(podcastLocal.id, i, reverse);
if (podcastLocal.provider.contains('fireside')) {
FiresideData data = FiresideData(podcastLocal.id, podcastLocal.link);
await data.getData();
_backgroundImage = data.background;
_hosts = data.hosts;
}
2020-02-09 13:29:09 +01:00
return episodes;
}
2020-03-01 13:17:06 +01:00
_launchUrl(String url) async {
if (await canLaunch(url)) {
await launch(url);
} else {
Fluttertoast.showToast(
msg: '$url Invalid Link',
gravity: ToastGravity.TOP,
);
}
}
_markListened(String podcastId) async {
DBHelper dbHelper = DBHelper();
List<EpisodeBrief> episodes =
await dbHelper.getRssItem(podcastId, -1, true);
2020-07-13 09:41:59 +02:00
for (var episode in episodes) {
bool marked = await dbHelper.checkMarked(episode);
if (!marked) {
final PlayHistory history =
PlayHistory(episode.title, episode.enclosureUrl, 0, 1);
await dbHelper.saveHistory(history);
if (mounted) setState(() {});
}
2020-07-13 09:41:59 +02:00
}
}
Widget podcastInfo(BuildContext context) {
return Container(
height: 170,
2020-06-28 17:47:29 +02:00
padding: EdgeInsets.only(top: 40, left: 80, right: 130),
alignment: Alignment.topLeft,
child: Container(
2020-07-18 11:52:31 +02:00
padding: EdgeInsets.symmetric(vertical: 10),
child: Text(
widget.podcastLocal.title,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: Theme.of(context)
.textTheme
.headline5
.copyWith(color: Colors.white),
),
),
);
}
2020-03-01 13:17:06 +01:00
Widget hostsList(BuildContext context, List<PodcastHost> hosts) {
return Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
hosts != null
? Container(
decoration: BoxDecoration(
image: DecorationImage(
// colorFilter: ColorFilter.mode(_color, BlendMode.color),
image: CachedNetworkImageProvider(
_backgroundImage,
2020-03-01 13:17:06 +01:00
),
fit: BoxFit.cover)),
alignment: Alignment.centerRight,
child: Container(
color: Colors.black26,
2020-03-01 13:17:06 +01:00
padding: EdgeInsets.symmetric(vertical: 5.0),
width: MediaQuery.of(context).size.width,
alignment: Alignment.centerRight,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: hosts
.map((host) => Container(
padding: EdgeInsets.all(5.0),
width: 80.0,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
CircleAvatar(
backgroundColor: Colors.grey[400],
backgroundImage:
CachedNetworkImageProvider(
2020-03-01 13:17:06 +01:00
host.image,
)),
Padding(
padding: EdgeInsets.all(2),
),
Text(
host.name,
style: TextStyle(
backgroundColor:
Colors.black.withOpacity(0.5),
color: Colors.white,
),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.fade,
),
],
)))
.toList()
.cast<Widget>(),
),
),
))
: Center(),
Padding(padding: EdgeInsets.all(10.0)),
Container(
padding: EdgeInsets.only(left: 15.0, right: 15.0, bottom: 10.0),
alignment: Alignment.topLeft,
color: Theme.of(context).scaffoldBackgroundColor,
child: AboutPodcast(podcastLocal: widget.podcastLocal),
),
],
);
}
_confirmMarkListened(BuildContext context) => generalDialog(
context,
2020-07-04 16:42:56 +02:00
title: Text(context.s.markConfirm),
content: Text(context.s.markConfirmContent),
actions: <Widget>[
FlatButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text(
2020-07-04 16:42:56 +02:00
context.s.cancel,
style: TextStyle(color: Colors.grey[600]),
),
),
FlatButton(
onPressed: () async {
Navigator.of(context).pop();
await _markListened(widget.podcastLocal.id);
},
child: Text(
2020-07-04 16:42:56 +02:00
context.s.confirm,
style: TextStyle(color: context.accentColor),
),
)
],
);
double _topHeight = 0;
ScrollController _controller;
/// Episodes num load first time.
int _top;
/// Load more episodes when scroll to bottom.
bool _loadMore;
/// Change sort by.
2020-04-11 19:23:12 +02:00
bool _reverse;
@override
void initState() {
super.initState();
_loadMore = false;
_top = 99;
2020-04-11 19:23:12 +02:00
_reverse = false;
_controller = ScrollController();
2020-06-05 20:33:47 +02:00
_scroll = false;
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
2020-02-09 13:29:09 +01:00
@override
Widget build(BuildContext context) {
2020-03-01 13:17:06 +01:00
Color _color = widget.podcastLocal.primaryColor.colorizedark();
2020-07-02 14:58:55 +02:00
final s = context.s;
return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle(
statusBarIconBrightness: Brightness.dark,
systemNavigationBarColor: Theme.of(context).primaryColor,
systemNavigationBarIconBrightness:
Theme.of(context).accentColorBrightness,
//statusBarColor: _color,
),
child: Scaffold(
body: SafeArea(
top: false,
minimum: widget.hide ? EdgeInsets.only(bottom: 50) : EdgeInsets.zero,
child: RefreshIndicator(
key: _refreshIndicatorKey,
color: Theme.of(context).accentColor,
onRefresh: () async {
2020-07-04 16:42:56 +02:00
await _updateRssItem(context, widget.podcastLocal);
// audio.addNewEpisode(widget.podcastLocal.id);
},
child: Stack(
children: <Widget>[
Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Expanded(
child: FutureBuilder<List<EpisodeBrief>>(
2020-04-11 19:23:12 +02:00
future:
_getRssItem(widget.podcastLocal, _top, _reverse),
builder: (context, snapshot) {
if (snapshot.hasError) print(snapshot.error);
return (snapshot.hasData)
? CustomScrollView(
controller: _controller
..addListener(() async {
if (_controller.offset ==
_controller
.position.maxScrollExtent &&
snapshot.data.length == _top) {
if (mounted)
setState(() => _loadMore = true);
await Future.delayed(
Duration(seconds: 3));
if (mounted)
setState(() {
_top = _top + 33;
_loadMore = false;
});
}
if (_controller.offset > 0 &&
mounted &&
!_scroll)
2020-06-05 20:33:47 +02:00
setState(() {
_scroll = true;
});
}),
2020-04-11 19:23:12 +02:00
physics:
const AlwaysScrollableScrollPhysics(),
slivers: <Widget>[
SliverAppBar(
brightness: Brightness.dark,
actions: <Widget>[
PopupMenuButton<int>(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(10))),
elevation: 2,
2020-07-04 16:42:56 +02:00
tooltip: s.menu,
itemBuilder: (context) => [
widget.podcastLocal.link != null
? PopupMenuItem(
value: 0,
child: Container(
padding: EdgeInsets.only(
left: 10),
child: Row(
children: <Widget>[
Icon(Icons.link,
color: Theme.of(
context)
.tabBarTheme
.labelColor),
Padding(
padding: EdgeInsets
.symmetric(
horizontal:
5.0),
),
2020-07-02 14:58:55 +02:00
Text(s.menuVisitSite),
],
),
),
)
: Center(),
PopupMenuItem(
value: 1,
child: Container(
padding:
EdgeInsets.only(left: 10),
child: Row(
children: <Widget>[
Icon(
Icons.rss_feed,
color: Theme.of(context)
.tabBarTheme
.labelColor,
),
Padding(
padding:
EdgeInsets.symmetric(
horizontal: 5.0),
),
2020-07-02 14:58:55 +02:00
Text(s.menuViewRSS),
],
),
2020-03-01 13:17:06 +01:00
),
),
PopupMenuItem(
value: 2,
child: Container(
padding:
EdgeInsets.only(left: 10),
child: Row(
crossAxisAlignment:
CrossAxisAlignment.center,
children: <Widget>[
SizedBox(
width: 25,
height: 25,
child: CustomPaint(
painter:
ListenedAllPainter(
context
.textTheme
.bodyText1
.color,
stroke: 2)),
),
Padding(
padding:
EdgeInsets.symmetric(
horizontal: 5.0),
),
Text(
2020-07-02 14:58:55 +02:00
s.menuMarkAllListened,
),
],
),
),
),
],
onSelected: (int value) {
switch (value) {
case 0:
_launchUrl(
widget.podcastLocal.link);
break;
case 1:
_launchUrl(
widget.podcastLocal.rssUrl);
break;
case 2:
_confirmMarkListened(context);
break;
}
},
)
],
elevation: 0,
iconTheme: IconThemeData(
color: Colors.white,
),
expandedHeight: 150 +
MediaQuery.of(context).padding.top,
backgroundColor: _color,
floating: true,
pinned: true,
flexibleSpace: LayoutBuilder(builder:
(BuildContext context,
BoxConstraints constraints) {
_topHeight = constraints.biggest.height;
return FlexibleSpaceBar(
background: Stack(
children: <Widget>[
Container(
margin: EdgeInsets.only(
top: 120 +
MediaQuery.of(context)
.padding
.top),
padding: EdgeInsets.only(
left: 80, right: 120),
color: Colors.white10,
alignment: Alignment.centerLeft,
child: Column(
mainAxisAlignment:
MainAxisAlignment.start,
mainAxisSize:
MainAxisSize.min,
crossAxisAlignment:
CrossAxisAlignment.start,
children: <Widget>[
Text(
widget.podcastLocal
.author ??
'',
maxLines: 1,
overflow: TextOverflow
.ellipsis,
style: TextStyle(
color:
Colors.white)),
widget.podcastLocal.provider
.isNotEmpty
? Text(
2020-07-04 16:42:56 +02:00
s.hostedOn(widget
.podcastLocal
.provider),
maxLines: 1,
style: TextStyle(
color: Colors
.white),
)
: Center(),
],
),
),
Container(
alignment:
Alignment.centerRight,
padding:
EdgeInsets.only(right: 10),
child: SizedBox(
height: 120,
child: Image.file(File(
"${widget.podcastLocal.imagePath}")),
),
),
Container(
alignment: Alignment.center,
child: podcastInfo(context),
),
],
),
title: _topHeight <
70 +
MediaQuery.of(context)
.padding
.top
? Text(widget.podcastLocal.title,
maxLines: 1,
overflow:
TextOverflow.ellipsis,
style: TextStyle(
color: Colors.white))
: Center(),
);
}),
),
2020-04-11 19:23:12 +02:00
SliverToBoxAdapter(
child: hostsList(context, _hosts),
2020-04-11 19:23:12 +02:00
),
SliverToBoxAdapter(
child: Container(
height: 30,
child: Row(
children: <Widget>[
Material(
color: Colors.transparent,
child: PopupMenuButton<int>(
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(
10)),
2020-04-11 19:23:12 +02:00
elevation: 1,
2020-07-02 14:58:55 +02:00
tooltip: s.homeSubMenuSortBy,
2020-04-11 19:23:12 +02:00
child: Container(
height: 30,
padding:
EdgeInsets.symmetric(
horizontal: 15),
child: Row(
mainAxisSize:
MainAxisSize.min,
children: <Widget>[
2020-07-02 14:58:55 +02:00
Text(s
.homeSubMenuSortBy),
2020-04-11 19:23:12 +02:00
Padding(
padding: EdgeInsets
.symmetric(
horizontal:
5),
),
Icon(
_reverse
? LineIcons
.hourglass_start_solid
: LineIcons
.hourglass_end_solid,
size: 18,
)
],
)),
itemBuilder: (context) => [
PopupMenuItem(
value: 0,
child:
2020-07-04 16:42:56 +02:00
Text(s.newestFirst),
2020-04-11 19:23:12 +02:00
),
PopupMenuItem(
value: 1,
child:
2020-07-04 16:42:56 +02:00
Text(s.oldestFirst),
2020-04-11 19:23:12 +02:00
)
],
onSelected: (value) {
if (value == 0)
setState(() =>
_reverse = false);
else if (value == 1)
setState(() =>
_reverse = true);
},
),
),
Spacer(),
Material(
color: Colors.transparent,
child: IconButton(
padding: EdgeInsets.zero,
onPressed: () {
if (_layout == Layout.three)
setState(() {
_layout = Layout.one;
2020-04-11 19:23:12 +02:00
});
else if (_layout ==
Layout.two)
2020-04-11 19:23:12 +02:00
setState(() {
_layout = Layout.three;
});
else
setState(() {
_layout = Layout.two;
});
2020-04-11 19:23:12 +02:00
},
icon: _layout == Layout.three
? SizedBox(
height: 10,
width: 30,
child: CustomPaint(
painter: LayoutPainter(
0,
context
.textTheme
.bodyText1
.color),
),
)
: _layout == Layout.two
? SizedBox(
height: 10,
width: 30,
child:
CustomPaint(
painter: LayoutPainter(
1,
context
.textTheme
.bodyText1
.color),
),
)
: SizedBox(
height: 10,
width: 30,
child:
CustomPaint(
painter: LayoutPainter(
4,
context
.textTheme
.bodyText1
.color),
),
),
2020-04-11 19:23:12 +02:00
),
),
],
)),
),
EpisodeGrid(
episodes: snapshot.data,
showFavorite: true,
showNumber: true,
2020-04-11 19:23:12 +02:00
layout: _layout,
reverse: _reverse,
episodeCount: _episodeCount,
2020-06-05 20:33:47 +02:00
initNum: _scroll ? 0 : 12,
),
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return _loadMore
? Container(
height: 2,
child:
LinearProgressIndicator())
: Center();
},
childCount: 1,
),
),
],
)
: Center(child: CircularProgressIndicator());
},
),
),
Selector<AudioPlayerNotifier, bool>(
selector: (_, audio) => audio.playerRunning,
builder: (_, data, __) {
return Padding(
padding: EdgeInsets.only(bottom: data ? 60.0 : 0),
);
}),
],
),
Container(child: PlayerWidget()),
],
),
),
),
),
2020-02-09 13:29:09 +01:00
);
}
}
class AboutPodcast extends StatefulWidget {
final PodcastLocal podcastLocal;
AboutPodcast({this.podcastLocal, Key key}) : super(key: key);
@override
_AboutPodcastState createState() => _AboutPodcastState();
}
class _AboutPodcastState extends State<AboutPodcast> {
String _description;
bool _load;
bool _expand;
void getDescription(String id) async {
var dbHelper = DBHelper();
String description = await dbHelper.getFeedDescription(id);
if (description == null || description.isEmpty) {
_description = '';
} else {
var doc = parse(description);
_description = parse(doc.body.text).documentElement.text;
}
if (mounted) setState(() => _load = true);
}
_launchUrl(String url) async {
if (await canLaunch(url)) {
await launch(url);
} else {
throw 'Could not launch $url';
}
}
@override
void initState() {
super.initState();
_load = false;
_expand = false;
getDescription(widget.podcastLocal.id);
}
@override
Widget build(BuildContext context) {
return !_load
? Center()
: LayoutBuilder(
builder: (context, size) {
final span = TextSpan(text: _description);
final tp = TextPainter(
text: span, maxLines: 3, textDirection: TextDirection.ltr);
tp.layout(maxWidth: size.maxWidth);
if (tp.didExceedMaxLines) {
return GestureDetector(
onTap: () {
setState(() => _expand = !_expand);
},
child: !_expand
? Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Linkify(
onOpen: (link) {
_launchUrl(link.url);
},
text: _description,
linkStyle: TextStyle(
color: Theme.of(context).accentColor,
decoration: TextDecoration.underline,
textBaseline: TextBaseline.ideographic),
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
],
)
: Linkify(
onOpen: (link) {
_launchUrl(link.url);
},
text: _description,
linkStyle: TextStyle(
color: Theme.of(context).accentColor,
decoration: TextDecoration.underline,
textBaseline: TextBaseline.ideographic),
),
);
} else {
return Linkify(
text: _description,
onOpen: (link) {
_launchUrl(link.url);
},
linkStyle: TextStyle(
color: Theme.of(context).accentColor,
decoration: TextDecoration.underline,
textBaseline: TextBaseline.ideographic),
2020-03-01 13:17:06 +01:00
);
}
},
);
}
}