import 'dart:io'; import 'dart:async'; import 'package:connectivity/connectivity.dart'; 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'; import 'package:url_launcher/url_launcher.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:provider/provider.dart'; import 'package:line_icons/line_icons.dart'; import 'package:cached_network_image/cached_network_image.dart'; import '../type/podcastlocal.dart'; import '../type/episodebrief.dart'; import '../local_storage/sqflite_localpodcast.dart'; import '../local_storage/key_value_storage.dart'; import '../util/episodegrid.dart'; import '../home/audioplayer.dart'; import '../type/fireside_data.dart'; import '../util/colorize.dart'; import '../util/context_extension.dart'; import '../util/custompaint.dart'; import '../util/general_dialog.dart'; import '../state/audio_state.dart'; class PodcastDetail extends StatefulWidget { PodcastDetail({Key key, @required this.podcastLocal, this.hide = false}) : super(key: key); final PodcastLocal podcastLocal; final bool hide; @override _PodcastDetailState createState() => _PodcastDetailState(); } class _PodcastDetailState extends State { final GlobalKey _refreshIndicatorKey = GlobalKey(); /// Fireside background if hosted on fireside. String _backgroundImage; /// Fireside hosts if hosted on fireside. List _hosts; /// Episodes total count. int _episodeCount; /// Default layout. Layout _layout; bool _scroll; Future _updateRssItem(BuildContext context, PodcastLocal podcastLocal) async { 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( msg: context.s.updateEpisodesCount(result), gravity: ToastGravity.TOP, ); bool autoDownload = await dbHelper.getAutoDownload(podcastLocal.id); if (autoDownload) { var downloader = Provider.of(context, listen: false); var result = await Connectivity().checkConnectivity(); KeyValueStorage autoDownloadStorage = KeyValueStorage(autoDownloadNetworkKey); int autoDownloadNetwork = await autoDownloadStorage.getInt(); if (autoDownloadNetwork == 1) { List episodes = await dbHelper.getNewEpisodes(podcastLocal.id); // For safety if (episodes.length < 100) for (var episode in episodes) downloader.startTask(episode, showNotification: false); } else if (result == ConnectivityResult.wifi) { List episodes = await dbHelper.getNewEpisodes(podcastLocal.id); //For safety if (episodes.length < 100) for (var episode in episodes) downloader.startTask(episode, showNotification: false); } } } else { Fluttertoast.showToast( msg: context.s.updateFailed, gravity: ToastGravity.TOP, ); } if (mounted) setState(() {}); } Future> _getRssItem( PodcastLocal podcastLocal, int i, bool reverse) async { 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]; List 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; } return episodes; } _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 episodes = await dbHelper.getRssItem(podcastId, -1, true); 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(() {}); } } } Widget podcastInfo(BuildContext context) { return Container( height: 170, padding: EdgeInsets.only(top: 40, left: 80, right: 130), alignment: Alignment.topLeft, child: Container( 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), ), ), ); } Widget hostsList(BuildContext context, List hosts) { return Column( mainAxisAlignment: MainAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ hosts != null ? Container( decoration: BoxDecoration( image: DecorationImage( // colorFilter: ColorFilter.mode(_color, BlendMode.color), image: CachedNetworkImageProvider( _backgroundImage, ), fit: BoxFit.cover)), alignment: Alignment.centerRight, child: Container( color: Colors.black26, 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: [ CircleAvatar( backgroundColor: Colors.grey[400], backgroundImage: CachedNetworkImageProvider( 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(), ), ), )) : 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, title: Text(context.s.markConfirm), content: Text(context.s.markConfirmContent), actions: [ FlatButton( onPressed: () { Navigator.of(context).pop(); }, child: Text( context.s.cancel, style: TextStyle(color: Colors.grey[600]), ), ), FlatButton( onPressed: () async { Navigator.of(context).pop(); await _markListened(widget.podcastLocal.id); }, child: Text( 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. bool _reverse; @override void initState() { super.initState(); _loadMore = false; _top = 99; _reverse = false; _controller = ScrollController(); _scroll = false; } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { Color _color = widget.podcastLocal.primaryColor.colorizedark(); final s = context.s; return AnnotatedRegion( 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 { await _updateRssItem(context, widget.podcastLocal); // audio.addNewEpisode(widget.podcastLocal.id); }, child: Stack( children: [ Column( mainAxisAlignment: MainAxisAlignment.start, mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: FutureBuilder>( 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) setState(() { _scroll = true; }); }), physics: const AlwaysScrollableScrollPhysics(), //primary: true, slivers: [ SliverAppBar( brightness: Brightness.dark, actions: [ PopupMenuButton( shape: RoundedRectangleBorder( borderRadius: BorderRadius.all( Radius.circular(10))), elevation: 2, tooltip: s.menu, itemBuilder: (context) => [ widget.podcastLocal.link != null ? PopupMenuItem( value: 0, child: Container( padding: EdgeInsets.only( left: 10), child: Row( children: [ Icon(Icons.link, color: Theme.of( context) .tabBarTheme .labelColor), Padding( padding: EdgeInsets .symmetric( horizontal: 5.0), ), Text(s.menuVisitSite), ], ), ), ) : Center(), PopupMenuItem( value: 1, child: Container( padding: EdgeInsets.only(left: 10), child: Row( children: [ Icon( Icons.rss_feed, color: Theme.of(context) .tabBarTheme .labelColor, ), Padding( padding: EdgeInsets.symmetric( horizontal: 5.0), ), Text(s.menuViewRSS), ], ), ), ), PopupMenuItem( value: 2, child: Container( padding: EdgeInsets.only(left: 10), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ SizedBox( width: 25, height: 25, child: CustomPaint( painter: ListenedAllPainter( context .textTheme .bodyText1 .color, stroke: 2)), ), Padding( padding: EdgeInsets.symmetric( horizontal: 5.0), ), Text( 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: [ 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: [ Text( widget.podcastLocal .author ?? '', maxLines: 1, overflow: TextOverflow .ellipsis, style: TextStyle( color: Colors.white)), widget.podcastLocal.provider .isNotEmpty ? Text( 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(), ); }), ), SliverToBoxAdapter( child: hostsList(context, _hosts), ), SliverToBoxAdapter( child: Container( height: 30, child: Row( children: [ Material( color: Colors.transparent, child: PopupMenuButton( shape: RoundedRectangleBorder( borderRadius: BorderRadius.all( Radius.circular( 10))), elevation: 1, tooltip: s.homeSubMenuSortBy, child: Container( height: 30, padding: EdgeInsets.symmetric( horizontal: 15), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text(s .homeSubMenuSortBy), Padding( padding: EdgeInsets .symmetric( horizontal: 5), ), Icon( _reverse ? LineIcons .hourglass_start_solid : LineIcons .hourglass_end_solid, size: 18, ) ], )), itemBuilder: (context) => [ PopupMenuItem( value: 0, child: Text(s.newestFirst), ), PopupMenuItem( value: 1, child: Text(s.oldestFirst), ) ], 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; }); else if (_layout == Layout.two) setState(() { _layout = Layout.three; }); else setState(() { _layout = Layout.two; }); }, 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), ), ), ), ), ], )), ), EpisodeGrid( episodes: snapshot.data, showFavorite: true, showNumber: true, layout: _layout, reverse: _reverse, episodeCount: _episodeCount, 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( selector: (_, audio) => audio.playerRunning, builder: (_, data, __) { return Padding( padding: EdgeInsets.only(bottom: data ? 60.0 : 0), ); }), ], ), Container(child: PlayerWidget()), ], ), ), ), ), ); } } class AboutPodcast extends StatefulWidget { final PodcastLocal podcastLocal; AboutPodcast({this.podcastLocal, Key key}) : super(key: key); @override _AboutPodcastState createState() => _AboutPodcastState(); } class _AboutPodcastState extends State { 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: [ 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), ); } }, ); } }