diff --git a/README.md b/README.md index db4cc98..2801cd5 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,11 @@ Enjoy podcasts with tsacdop! Tsacdop is a podcasts player developed with flutter. - +The development is still on early stage. Thanks for flutter team and all plugin developers, especially [webfeed](https://github.com/witochandra/webfeed) and [audiofileplayer](https://github.com/google/flutter.plugins/tree/master/packages/audiofileplayer/). -The podcasts engine is powered by [ListenNote](https://listennote.com). +The podcasts search engine is powered by [ListenNotes](https://listennotes.com). ## Getting Started diff --git a/lib/addpodcast.dart b/lib/addpodcast.dart index f2edbba..56c0cb5 100644 --- a/lib/addpodcast.dart +++ b/lib/addpodcast.dart @@ -1,8 +1,12 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:color_thief_flutter/color_thief_flutter.dart'; import 'class/importompl.dart'; import 'package:dio/dio.dart'; import 'package:provider/provider.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:image/image.dart' as img; import 'dart:convert'; import 'dart:async'; import 'class/searchpodcast.dart'; @@ -10,6 +14,7 @@ import 'class/podcastlocal.dart'; import 'class/sqflite_localpodcast.dart'; import 'home.dart'; import 'popupmenu.dart'; +import 'webfeed/webfeed.dart'; class MyHomePage extends StatefulWidget { @override @@ -22,31 +27,28 @@ class _MyHomePageState extends State { @override Widget build(BuildContext context) { - return ChangeNotifierProvider( - create: (context) => ImportOmpl(), - child: Scaffold( - key: _scaffoldKey, - appBar: AppBar( - elevation: 0, - centerTitle: true, - backgroundColor: Colors.grey[100], - leading: IconButton( - tooltip: 'Add', - icon: const Icon(Icons.add_circle_outline), - onPressed: () async { - await showSearch( - context: context, - delegate: _delegate, - ); - }, - ), - title: Text('🎙TsacDop', style: TextStyle(color: Colors.blue[600])), - actions: [ - PopupMenu(), - ], + return Scaffold( + key: _scaffoldKey, + appBar: AppBar( + elevation: 0, + centerTitle: true, + backgroundColor: Colors.grey[100], + leading: IconButton( + tooltip: 'Add', + icon: const Icon(Icons.add_circle_outline), + onPressed: () async { + await showSearch( + context: context, + delegate: _delegate, + ); + }, ), - body: Home(), + title: Text('🎙TsacDop', style: TextStyle(color: Colors.blue[600])), + actions: [ + PopupMenu(), + ], ), + body: Home(), ); } } @@ -119,13 +121,7 @@ class _MyHomePageDelegate extends SearchDelegate { List buildActions(BuildContext context) { return [ if (query.isEmpty) - IconButton( - tooltip: 'Voice Search', - icon: const Icon(Icons.mic), - onPressed: () { - query = 'TODO: implement voice input'; - }, - ) + Center() else IconButton( tooltip: 'Clear', @@ -144,9 +140,12 @@ class _MyHomePageDelegate extends SearchDelegate { height: 10, width: 10, margin: EdgeInsets.only(top: 400), - child: Image.asset( - 'assets/listennote.png', - fit: BoxFit.fill, + child: SizedBox( + height: 10, + child: Image.asset( + 'assets/listennote.png', + fit: BoxFit.fill, + ), ), ); return FutureBuilder( @@ -183,31 +182,6 @@ class SearchResult extends StatefulWidget { class _SearchResultState extends State { bool _issubscribe; bool _adding; - Future _subscribe(OnlinePodcast t) async { - if (mounted) - setState(() { - _adding = true; - }); - String _primaryColor; - await getColorFromUrl(t.image).then((color) { - print(color.toString()); - _primaryColor = color.toString(); - }); - var dbHelper = DBHelper(); - final PodcastLocal _pdt = - PodcastLocal(t.title, t.image, t.rss, _primaryColor, t.publisher); - _pdt.description = t.description; - print(t.title + t.rss); - await dbHelper.savePodcastLocal(_pdt); - final response = await Dio().get(t.rss); - int result = await dbHelper.savePodcastRss(response.data); - if (result == 0 && mounted) setState(() => _issubscribe = true); - } - - bool isXimalaya(String input) { - RegExp ximalaya = RegExp(r"ximalaya"); - return ximalaya.hasMatch(input); - } @override void initState() { @@ -221,8 +195,60 @@ class _SearchResultState extends State { super.dispose(); } + Future getColor(File file) async { + final imageProvider = FileImage(file); + var colorImage = await getImageFromProvider(imageProvider); + var color = await getColorFromImage(colorImage); + String primaryColor = color.toString(); + return primaryColor; + } + + @override Widget build(BuildContext context) { + final importOmpl = Provider.of(context); + savePodcast(String rss) async { + print(rss); + if (mounted) setState(() => _adding = true); + + importOmpl.importState = + ImportState.import; + + Response response = await Dio().get(rss); + if (mounted) setState(() => _issubscribe = true); + + var _p = RssFeed.parse(response.data); + + print(_p.title); + var dir = await getApplicationDocumentsDirectory(); + + Response> imageResponse = await Dio().get>( + _p.itunes.image.href, + options: Options(responseType: ResponseType.bytes)); + + img.Image image = img.decodeImage(imageResponse.data); + img.Image thumbnail = img.copyResize(image, width: 300); + File("${dir.path}/${_p.title}.png") + ..writeAsBytesSync(img.encodePng(thumbnail)); + + String _primaryColor = await getColor(File("${dir.path}/${_p.title}.png")); + PodcastLocal podcastLocal = PodcastLocal( + _p.title, _p.itunes.image.href, rss, _primaryColor, _p.author); + podcastLocal.description = _p.description; + var dbHelper = DBHelper(); + await dbHelper.savePodcastLocal(podcastLocal); + + importOmpl.importState = + ImportState.parse; + + await dbHelper.savePodcastRss(response.data); + + importOmpl.importState = + ImportState.complete; + importOmpl.importState = + ImportState.stop; + print('fatch data'); + } return Container( padding: EdgeInsets.symmetric(horizontal: 12.0), child: ListTile( @@ -238,27 +264,27 @@ class _SearchResultState extends State { ), title: Text(widget.onlinePodcast.title), subtitle: Text(widget.onlinePodcast.publisher), - trailing: isXimalaya(widget.onlinePodcast.rss) - ? OutlineButton(child: Text('Not Support'), onPressed: null) - : !_issubscribe - ? !_adding - ? OutlineButton( - child: Text('Subscribe', - style: TextStyle(color: Colors.blue)), - onPressed: () { - _subscribe(widget.onlinePodcast); - }) - : OutlineButton( - child: SizedBox( - height: 20, - width: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation(Colors.blue), - )), - onPressed: () {}, - ) - : OutlineButton(child: Text('Subscribe'), onPressed: null), + trailing: !_issubscribe + ? !_adding + ? OutlineButton( + child: + Text('Subscribe', style: TextStyle(color: Colors.blue)), + onPressed: () { + importOmpl.rssTitle = + widget.onlinePodcast.title; + savePodcast(widget.onlinePodcast.rss); + }) + : OutlineButton( + child: SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(Colors.blue), + )), + onPressed: () {}, + ) + : OutlineButton(child: Text('Subscribe'), onPressed: null), ), ); } diff --git a/lib/class/importompl.dart b/lib/class/importompl.dart index 35d31a8..9575f82 100644 --- a/lib/class/importompl.dart +++ b/lib/class/importompl.dart @@ -1,16 +1,19 @@ import 'package:flutter/foundation.dart'; -enum ImportState{start, import, complete, stop, error} +enum ImportState{start, import, parse, complete, stop, error} class ImportOmpl extends ChangeNotifier{ ImportState _importState = ImportState.stop; String _rssTitle; + String get rsstitle => _rssTitle; + set rssTitle(String title){ _rssTitle = title; - notifyListeners(); } + ImportState get importState => _importState; + set importState(ImportState state){ _importState = state; notifyListeners(); diff --git a/lib/class/sqflite_localpodcast.dart b/lib/class/sqflite_localpodcast.dart index fd522b9..65fc6fd 100644 --- a/lib/class/sqflite_localpodcast.dart +++ b/lib/class/sqflite_localpodcast.dart @@ -82,10 +82,11 @@ class DBHelper { List list = await dbClient.rawQuery( """SELECT downloaded FROM Episodes WHERE downloaded != 'ND' AND feed_title = ?""", [title]); - for(int i=0; i < list.length; i++){ - if(list[i] != null) - FlutterDownloader.remove(taskId: list[i]['downloaded'], shouldDeleteContent: true); - print('Removed all download task'); + for (int i = 0; i < list.length; i++) { + if (list[i] != null) + FlutterDownloader.remove( + taskId: list[i]['downloaded'], shouldDeleteContent: true); + print('Removed all download tasks'); } await dbClient .rawDelete('DELETE FROM Episodes WHERE feed_title=?', [title]); @@ -99,70 +100,21 @@ class DBHelper { return url; } - int stringToDate(String s) { - var months = { - 'Jan': 1, - 'Feb': 2, - 'Mar': 3, - 'Apr': 4, - 'May': 5, - 'Jun': 6, - 'Jul': 7, - 'Aug': 8, - 'Sep': 9, - 'Oct': 10, - 'Nov': 11, - 'Dec': 12 - }; - int y; - int m; - int d; - int h; - int min; - int sec; - int result; - try { - y = int.parse(s.substring(12, 16)); - } catch (e) { - y = 0; - } - - try { - m = months[s.substring(8, 11)]; - } catch (e) { - m = 0; - } - try { - d = int.parse(s.substring(5, 7)); - } catch (e) { - d = 0; - } - try { - h = int.parse(s.substring(17, 19)); - } catch (e) { - h = 0; - } - try { - min = int.parse(s.substring(20, 22)); - } catch (e) { - min = 0; - } - try { - sec = int.parse(s.substring(23, 25)); - } catch (e) { - sec = 0; - } - try { - result = DateTime(y, m, d, h, min, sec).millisecondsSinceEpoch; - } catch (e) { - result = 0; - } - return result; - } - - static _parsePubDate(String pubDate) { + DateTime _parsePubDate(String pubDate) { if (pubDate == null) return null; - return DateFormat('EEE, dd MMM yyyy HH:mm:ss Z', 'en_US').parse(pubDate); + DateTime date; + try { + date = DateFormat('EEE, dd MMM yyyy HH:mm:ss Z', 'en_US').parse(pubDate); + } catch (e) { + try{ + print('e'); + date = DateFormat('dd MMM yyyy HH:mm:ss Z', 'en_US').parse(pubDate);} + catch(e) { + print('e'); + date = DateTime(0); + } + } + return date; } int getExplicit(bool b) { @@ -185,6 +137,7 @@ class DBHelper { String _title; String _url; String _description; + int _duration; var _p = RssFeed.parse(rss); int _result = _p.items.length; var dbClient = await database; @@ -208,9 +161,11 @@ class DBHelper { : _url = _p.items[i].enclosure.url; final _length = _p.items[i].enclosure.length; final _pubDate = _p.items[i].pubDate; - final DateTime _date = _parsePubDate(_pubDate); + final _date = _parsePubDate(_pubDate); final _milliseconds = _date.millisecondsSinceEpoch; - final _duration = _p.items[i].itunes.duration.inMinutes; + (_p.items[i].itunes.duration != null ) + ? _duration = _p.items[i].itunes.duration.inMinutes + : _duration = 0; final _explicit = getExplicit(_p.items[i].itunes.explicit); if (_p.items[i].enclosure.url != null) { await dbClient.transaction((txn) { diff --git a/lib/episodedetail.dart b/lib/episodedetail.dart index e508b90..dd32837 100644 --- a/lib/episodedetail.dart +++ b/lib/episodedetail.dart @@ -6,6 +6,7 @@ import 'package:provider/provider.dart'; import 'package:flutter_html/flutter_html.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:url_launcher/url_launcher.dart'; + import 'class/audiostate.dart'; import 'class/episodebrief.dart'; import 'class/sqflite_localpodcast.dart'; @@ -25,7 +26,6 @@ class EpisodeDetail extends StatefulWidget { class _EpisodeDetailState extends State { final textstyle = TextStyle(fontSize: 15.0, color: Colors.black); double downloadProgress; - Color _c; bool _loaddes; Future getSDescription(String title) async { @@ -36,14 +36,15 @@ class _EpisodeDetailState extends State { _loaddes = true; }); } - + _launchUrl(String url) async { - if (await canLaunch(url)) { - await launch(url); - } else { - throw 'Could not launch $url'; + if (await canLaunch(url)) { + await launch(url); + } else { + throw 'Could not launch $url'; + } } -} + @override void initState() { super.initState(); @@ -53,11 +54,6 @@ class _EpisodeDetailState extends State { @override Widget build(BuildContext context) { - var color = json.decode(widget.episodeItem.primaryColor); - (color[0] > 200 && color[1] > 200 && color[2] > 200) - ? _c = Color.fromRGBO( - (255 - color[0]), 255 - color[1], 255 - color[2], 1.0) - : _c = Color.fromRGBO(color[0], color[1], color[2], 0.8); return Scaffold( backgroundColor: Colors.grey[100], @@ -81,29 +77,28 @@ class _EpisodeDetailState extends State { children: [ Container( padding: EdgeInsets.symmetric(horizontal: 12.0), - margin: EdgeInsets.only(bottom: 10.0), alignment: Alignment.topLeft, child: Text( widget.episodeItem.title, style: Theme.of(context).textTheme.title, ), ), + Container( + alignment: Alignment.centerLeft, + padding: EdgeInsets.symmetric(horizontal: 12.0), + height: 30.0, + child: Text( + 'Published ' + + widget.episodeItem.pubDate.substring(0, 16), + style: TextStyle(color: Colors.blue[500])), + ), Container( padding: EdgeInsets.all(12.0), - height: 50, + height: 50.0, child: Row( children: [ (widget.episodeItem.explicit == 1) - ? Container( - decoration: BoxDecoration( - color: Colors.red[800], - shape: BoxShape.circle), - height: 25.0, - margin: EdgeInsets.only(right: 10.0), - padding: EdgeInsets.symmetric(horizontal: 10.0), - alignment: Alignment.center, - child: Text('E', - style: TextStyle(color: Colors.white))) + ? ExplicitScale() : Center(), Container( decoration: BoxDecoration( @@ -133,19 +128,6 @@ class _EpisodeDetailState extends State { 'MB', style: textstyle), ), - Container( - decoration: BoxDecoration( - color: Colors.lightGreen[300], - borderRadius: - BorderRadius.all(Radius.circular(15.0))), - height: 30.0, - alignment: Alignment.center, - margin: EdgeInsets.only(right: 10.0), - padding: EdgeInsets.symmetric(horizontal: 10.0), - child: Text( - widget.episodeItem.pubDate.substring(0, 16), - style: textstyle), - ), ], ), ), @@ -157,12 +139,13 @@ class _EpisodeDetailState extends State { padding: EdgeInsets.only(left: 12.0, right: 12.0, top: 5.0), child: SingleChildScrollView( child: (widget.episodeItem.description != null && _loaddes) - ? Html(data: widget.episodeItem.description, - onLinkTap: (url){ - _launchUrl(url); - }, - useRichText: true, - ) + ? Html( + data: widget.episodeItem.description, + onLinkTap: (url) { + _launchUrl(url); + }, + useRichText: true, + ) : Center(), ), ), @@ -531,3 +514,53 @@ class _ImageRotateState extends State ); } } + +class ExplicitScale extends StatefulWidget { + @override + _ExplicitScaleState createState() => _ExplicitScaleState(); +} + + class _ExplicitScaleState extends State + with SingleTickerProviderStateMixin { + Animation _animation; + AnimationController _controller; + double _value; + + @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(() { + _value = _animation.value; + }); + }); + _controller.forward(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Transform.scale( + scale: _value, + child: Container( + decoration: + BoxDecoration(color: Colors.red[800], shape: BoxShape.circle), + height: 25.0, + width: 25.0, + margin: EdgeInsets.only(right: 10.0), + padding: EdgeInsets.symmetric(horizontal: 10.0), + alignment: Alignment.center, + child: Text('E', style: TextStyle(color: Colors.white)))); + } +} diff --git a/lib/episodegrid.dart b/lib/episodegrid.dart index 7792e27..9fcab9c 100644 --- a/lib/episodegrid.dart +++ b/lib/episodegrid.dart @@ -120,6 +120,7 @@ class EpisodeGrid extends StatelessWidget { Expanded( flex: 5, child: Container( + alignment: Alignment.topLeft, padding: EdgeInsets.only(top: 2.0), child: Text( podcast[index].title, diff --git a/lib/home.dart b/lib/home.dart index f03be81..5bf5c84 100644 --- a/lib/home.dart +++ b/lib/home.dart @@ -13,7 +13,6 @@ class Home extends StatefulWidget { } class _HomeState extends State { - @override Widget build(BuildContext context) { return Column( @@ -25,21 +24,25 @@ class _HomeState extends State { height: 30, padding: EdgeInsets.symmetric(horizontal: 15), alignment: Alignment.bottomRight, - - child: GestureDetector( - onTap: () { - Navigator.push( - context, - SlideLeftRoute(page: Podcast()), - ); - }, - child: Text('See All', - style: TextStyle( - color: Colors.red[300], fontWeight: FontWeight.bold, )), - - )), - Container( - child: ScrollPodcasts()), + child: GestureDetector( + onTap: () { + Navigator.push( + context, + SlideLeftRoute(page: Podcast()), + ); + }, + child: Container( + height: 30, + padding: EdgeInsets.all(5.0), + child: Text('See All', + style: TextStyle( + color: Colors.red[300], + fontWeight: FontWeight.bold, + )), + ), + ), + ), + Container(child: ScrollPodcasts()), Expanded( child: MainTab(), ), diff --git a/lib/homescroll.dart b/lib/homescroll.dart index 8309e2d..68a334d 100644 --- a/lib/homescroll.dart +++ b/lib/homescroll.dart @@ -1,11 +1,18 @@ import 'dart:convert'; +import 'dart:io'; +import 'dart:async'; + import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'dart:async'; +import 'package:provider/provider.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:path_provider/path_provider.dart'; + import 'class/episodebrief.dart'; import 'class/podcastlocal.dart'; +import 'class/importompl.dart'; import 'class/sqflite_localpodcast.dart'; -import 'package:cached_network_image/cached_network_image.dart'; + import 'episodedetail.dart'; import 'podcastdetail.dart'; import 'pageroute.dart'; @@ -16,12 +23,23 @@ class ScrollPodcasts extends StatefulWidget { } class _ScrollPodcastsState extends State { + var dir; Future> getPodcastLocal() async { var dbHelper = DBHelper(); List podcastList = await dbHelper.getPodcastLocal(); + dir = await getApplicationDocumentsDirectory(); return podcastList; } + ImportState importState; + didChangeDependencies() { + super.didChangeDependencies(); + final importState = Provider.of(context).importState; + if (importState == ImportState.complete) { + setState(() {}); + } + } + @override Widget build(BuildContext context) { return FutureBuilder>( @@ -38,8 +56,8 @@ class _ScrollPodcastsState extends State { height: 70, alignment: Alignment.centerLeft, child: TabBar( - labelPadding: - EdgeInsets.only(bottom: 15.0, left: 6.0, right: 6.0), + labelPadding: EdgeInsets.only( + top: 5.0, bottom: 10.0, left: 6.0, right: 6.0), indicator: CircleTabIndicator(color: Colors.blue, radius: 3), isScrollable: true, @@ -50,11 +68,8 @@ class _ScrollPodcastsState extends State { child: LimitedBox( maxHeight: 50, maxWidth: 50, - child: CachedNetworkImage( - imageUrl: podcastLocal.imageUrl, - placeholder: (context, url) => - CircularProgressIndicator(), - ), + child: Image.file( + File("${dir.path}/${podcastLocal.title}.png")), ), ), ); @@ -62,7 +77,7 @@ class _ScrollPodcastsState extends State { ), ), Container( - height: 200, + height: 195, margin: EdgeInsets.only(left: 10, right: 10), decoration: BoxDecoration( color: Colors.white, @@ -85,7 +100,9 @@ class _ScrollPodcastsState extends State { ), ); } - return Center(); + return Container( + height: 250.0, + ); }, ); } @@ -204,9 +221,9 @@ class ShowEpisode extends StatelessWidget { context, ScaleRoute( page: EpisodeDetail( - episodeItem: podcast[index], - heroTag: 'scroll', - )), + episodeItem: podcast[index], + heroTag: 'scroll', + )), ); }, child: Container( @@ -258,6 +275,7 @@ class ShowEpisode extends StatelessWidget { flex: 5, child: Container( padding: EdgeInsets.only(top: 2.0), + alignment: Alignment.topLeft, child: Text( podcast[index].title, style: TextStyle( @@ -269,7 +287,7 @@ class ShowEpisode extends StatelessWidget { ), Expanded( flex: 1, - child: Align( + child: Container( alignment: Alignment.bottomLeft, child: Text( podcast[index].pubDate.substring(4, 16), @@ -319,5 +337,3 @@ class _CirclePainter extends BoxPainter { canvas.drawCircle(circleOffset, radius, _paint); } } - - diff --git a/lib/hometab.dart b/lib/hometab.dart index 608a2cc..df953c3 100644 --- a/lib/hometab.dart +++ b/lib/hometab.dart @@ -15,8 +15,8 @@ class _MainTabState extends State with TickerProviderStateMixin { Decoration getIndicator() { return const UnderlineTabIndicator( borderSide: BorderSide(color: Colors.red, width: 2), - insets: EdgeInsets.only(left:20,top:10,) - );} + insets: EdgeInsets.only(left:10.0,right: 10.0, top:10.0,) + );} @override void initState() { super.initState(); @@ -36,20 +36,21 @@ class _MainTabState extends State with TickerProviderStateMixin { mainAxisSize: MainAxisSize.min, children: [ Container( + padding: EdgeInsets.symmetric(horizontal: 10.0), height: 50, alignment: Alignment.centerLeft, child: TabBar( isScrollable: true, labelPadding: - EdgeInsets.only(bottom:10.0,left: 20.0), + EdgeInsets.all(10.0), controller: _controller, labelColor: Colors.red, unselectedLabelColor: Colors.black, indicator: getIndicator(), tabs: [ Text('Recent Update',style: TextStyle(fontWeight: FontWeight.bold),), - Text('Favorite',style: TextStyle(fontWeight: FontWeight.bold),), - Text('Dowloads',style: TextStyle(fontWeight: FontWeight.bold),), + Text('Favorites',style: TextStyle(fontWeight: FontWeight.bold),), + Text('Downloads',style: TextStyle(fontWeight: FontWeight.bold),), ], ), ), diff --git a/lib/importompl.dart b/lib/importompl.dart index de0ab54..367ba75 100644 --- a/lib/importompl.dart +++ b/lib/importompl.dart @@ -6,26 +6,72 @@ class Import extends StatelessWidget { @override Widget build(BuildContext context) { return Consumer( - builder: (context, importOmpl, _) => Container( - child: importOmpl.importState == ImportState.start - ? Container( - height: 20.0, - alignment: Alignment.center, - child: Text('Start'), - ) - : importOmpl.importState == ImportState.import - ? Container( + builder: (context, importOmpl, _) => Container( + color: Colors.grey[300], + child: importOmpl.importState == ImportState.start + ? Column( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + height: 2.0, + child: LinearProgressIndicator()), + Container( + padding: EdgeInsets.symmetric(horizontal: 20.0), height: 20.0, - alignment: Alignment.center, - child: Text('Importing'+(importOmpl.rsstitle))) - : importOmpl.importState == ImportState.complete - ? Container( + alignment: Alignment.centerLeft, + child: Text('Read file successful'), + ), + ]) + : importOmpl.importState == ImportState.import + ? Column( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + height: 2.0, + child: LinearProgressIndicator()), + Container( height: 20.0, - alignment: Alignment.center, - child: Text('Complete'), - ) - : importOmpl.importState == ImportState.stop - ? Center() - : Center())); + padding: EdgeInsets.symmetric(horizontal: 20.0), + alignment: Alignment.centerLeft, + child: + Text('Importing: ' + (importOmpl.rsstitle))), + ], + ) + : importOmpl.importState == ImportState.parse + ? Column( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + height: 2.0, + child: LinearProgressIndicator()), + Container( + height: 20.0, + padding: EdgeInsets.symmetric(horizontal: 20.0), + alignment: Alignment.centerLeft, + child: Text('Fatch: ' + (importOmpl.rsstitle)), + ), + ], + ) + : importOmpl.importState == ImportState.error + ? Column( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + height: 2.0, + child: LinearProgressIndicator()), + Container( + height: 20.0, + padding: EdgeInsets.symmetric(horizontal: 20.0), + alignment: Alignment.centerLeft, + child: Text('Error: ' + (importOmpl.rsstitle)), + ), + ], + ) + : Center()), + ); } } diff --git a/lib/main.dart b/lib/main.dart index c04616c..08ca83f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,12 +4,14 @@ import 'package:provider/provider.dart'; import 'package:flutter_downloader/flutter_downloader.dart'; import 'addpodcast.dart'; import 'class/audiostate.dart'; +import 'class/importompl.dart'; void main() async { runApp( MultiProvider( providers: [ ChangeNotifierProvider(create: (context) => Urlchange()), + ChangeNotifierProvider(create: (context) => ImportOmpl()), ], child: MyApp(), ), diff --git a/lib/podcastlist.dart b/lib/podcastlist.dart index 9c0f416..323a85b 100644 --- a/lib/podcastlist.dart +++ b/lib/podcastlist.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'dart:async'; import 'package:flutter/foundation.dart'; +import 'package:flutter_html/flutter_html.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'class/podcastlocal.dart'; @@ -72,7 +73,7 @@ class _AboutPodcastState extends State { children: [ !_load ? Center() - : _description != null ? Text(_description) : Center(), + : _description != null ? Html(data: _description) : Center(), (widget.podcastLocal.author != null) ? Text(widget.podcastLocal.author, style: TextStyle(color: Colors.blue)) diff --git a/lib/popupmenu.dart b/lib/popupmenu.dart index c53a894..342ed78 100644 --- a/lib/popupmenu.dart +++ b/lib/popupmenu.dart @@ -6,6 +6,9 @@ import 'package:provider/provider.dart'; import 'package:xml/xml.dart' as xml; import 'package:file_picker/file_picker.dart'; import 'package:flutter/services.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:color_thief_flutter/color_thief_flutter.dart'; +import 'package:image/image.dart' as img; import 'about.dart'; import 'class/podcastlocal.dart'; import 'class/sqflite_localpodcast.dart'; @@ -28,43 +31,71 @@ class OmplOutline { class PopupMenu extends StatelessWidget { - Future saveOmpl(String rss) async { - var dbHelper = DBHelper(); - try { - Response response = await Dio().get(rss); - var _p = RssFeed.parse(response.data); - String _primaryColor = '[100,100,100]'; - PodcastLocal podcastLocal = PodcastLocal(_p.title, _p.itunes.image.href, - rss, _primaryColor, _p.author); - podcastLocal.description = _p.description; - int total = await dbHelper.savePodcastLocal(podcastLocal); - return total; - } catch (e) { - return 0; - } + Future getColor(File file) async { + final imageProvider = FileImage(file); + var colorImage = await getImageFromProvider(imageProvider); + var color = await getColorFromImage(colorImage); + String primaryColor = color.toString(); + return primaryColor; } @override Widget build(BuildContext context) { final importOmpl = Provider.of(context); + saveOmpl(String rss) async { + var dbHelper = DBHelper(); + try { + importOmpl.importState = ImportState.import; + Response response = await Dio().get(rss); + + var _p = RssFeed.parse(response.data); + var dir = await getApplicationDocumentsDirectory(); + + Response> imageResponse = await Dio().get>( + _p.itunes.image.href, + options: Options(responseType: ResponseType.bytes)); + img.Image image = img.decodeImage(imageResponse.data); + img.Image thumbnail = img.copyResize(image, width: 300); + File("${dir.path}/${_p.title}.png") + ..writeAsBytesSync(img.encodePng(thumbnail)); + + String _primaryColor = + await getColor(File("${dir.path}/${_p.title}.png")); + + PodcastLocal podcastLocal = PodcastLocal( + _p.title, _p.itunes.image.href, rss, _primaryColor, _p.author); + + podcastLocal.description = _p.description; + print('_p.description'); + await dbHelper.savePodcastLocal(podcastLocal); + + importOmpl.importState = ImportState.parse; + + await dbHelper.savePodcastRss(response.data); + } catch (e) { + print(e); + } + } + void _saveOmpl(String path) async { File file = File(path); String opml = file.readAsStringSync(); try { var content = xml.parse(opml); - importOmpl.importState = ImportState.import; var total = content .findAllElements('outline') .map((ele) => OmplOutline.parse(ele)) .toList(); for (int i = 0; i < total.length; i++) { - if (total[i].xmlUrl != null) - await saveOmpl(total[i].xmlUrl); - importOmpl.rssTitle = total[i].text; - print(total[i].text); + if (total[i].xmlUrl != null) { + importOmpl.rssTitle = total[i].text; + await saveOmpl(total[i].xmlUrl); + print(total[i].text); + } } importOmpl.importState = ImportState.complete; + importOmpl.importState = ImportState.stop; print('Import fisnished'); } catch (e) { print(e); diff --git a/lib/webfeed/domain/dublin_core/dublin_core.dart b/lib/webfeed/domain/dublin_core/dublin_core.dart index 564bb4c..16cf96c 100644 --- a/lib/webfeed/domain/dublin_core/dublin_core.dart +++ b/lib/webfeed/domain/dublin_core/dublin_core.dart @@ -1,4 +1,4 @@ -import 'package:webfeed/util/helpers.dart'; +import '../../util/helpers.dart'; import 'package:xml/xml.dart'; class DublinCore { diff --git a/lib/webfeed/domain/rss_item.dart b/lib/webfeed/domain/rss_item.dart index cf42669..16350ad 100644 --- a/lib/webfeed/domain/rss_item.dart +++ b/lib/webfeed/domain/rss_item.dart @@ -54,12 +54,12 @@ class RssItem { guid: findElementOrNull(element, "guid")?.text, pubDate: findElementOrNull(element, "pubDate")?.text, author: findElementOrNull(element, "author")?.text, - comments: findElementOrNull(element, "comments")?.text, - source: RssSource.parse(findElementOrNull(element, "source")), - content: RssContent.parse(findElementOrNull(element, "content:encoded")), - media: Media.parse(element), + // comments: findElementOrNull(element, "comments")?.text, + // source: RssSource.parse(findElementOrNull(element, "source")), + // content: RssContent.parse(findElementOrNull(element, "content:encoded")), + // media: Media.parse(element), enclosure: RssEnclosure.parse(findElementOrNull(element, "enclosure")), - dc: DublinCore.parse(element), + //dc: DublinCore.parse(element), itunes: RssItemItunes.parse(element), ); } diff --git a/lib/webfeed/domain/rss_item_itunes.dart b/lib/webfeed/domain/rss_item_itunes.dart index e4295e5..1f9b955 100644 --- a/lib/webfeed/domain/rss_item_itunes.dart +++ b/lib/webfeed/domain/rss_item_itunes.dart @@ -40,25 +40,26 @@ class RssItemItunes { if (element == null) { return null; } - var episodeStr = findElementOrNull(element, "itunes:episode")?.text?.trim(); - var seasonStr = findElementOrNull(element, "itunes:season")?.text?.trim(); - var durationStr = findElementOrNull(element, "itunes:duration")?.text?.trim(); + //var episodeStr = findElementOrNull(element, "itunes:episode")?.text?.trim(); + //var seasonStr = findElementOrNull(element, "itunes:season")?.text?.trim(); + var durationStr = + findElementOrNull(element, "itunes:duration")?.text?.trim(); return RssItemItunes( title: findElementOrNull(element, "itunes:title")?.text?.trim(), //episode: episodeStr == null ? null : int.parse(episodeStr), //season: seasonStr == null ? null : int.parse(seasonStr), duration: durationStr == null ? null : parseDuration(durationStr), - episodeType: newRssItunesEpisodeType(findElementOrNull(element, "itunes:episodeType")), + // episodeType: newRssItunesEpisodeType(findElementOrNull(element, "itunes:episodeType")), author: findElementOrNull(element, "itunes:author")?.text?.trim(), summary: findElementOrNull(element, "itunes:summary")?.text?.trim(), explicit: parseBoolLiteral(element, "itunes:explicit"), - subtitle: findElementOrNull(element, "itunes:subtitle")?.text?.trim(), - keywords: findElementOrNull(element, "itunes:keywords")?.text?.split(",")?.map((keyword) => keyword.trim())?.toList(), - image: RssItunesImage.parse(findElementOrNull(element, "itunes:image")), - category: RssItunesCategory.parse( - findElementOrNull(element, "itunes:category")), - block: parseBoolLiteral(element, "itunes:block"), + //subtitle: findElementOrNull(element, "itunes:subtitle")?.text?.trim(), + // keywords: findElementOrNull(element, "itunes:keywords")?.text?.split(",")?.map((keyword) => keyword.trim())?.toList(), + // image: RssItunesImage.parse(findElementOrNull(element, "itunes:image")), + // category: RssItunesCategory.parse( + // findElementOrNull(element, "itunes:category")), + // block: parseBoolLiteral(element, "itunes:block"), ); } } diff --git a/lib/webfeed/domain/rss_itunes.dart b/lib/webfeed/domain/rss_itunes.dart index 4a260c8..c5fa60a 100644 --- a/lib/webfeed/domain/rss_itunes.dart +++ b/lib/webfeed/domain/rss_itunes.dart @@ -48,22 +48,22 @@ class RssItunes { summary: findElementOrNull(element, "itunes:summary")?.text?.trim(), explicit: parseBoolLiteral(element, "itunes:explicit"), title: findElementOrNull(element, "itunes:title")?.text?.trim(), - subtitle: findElementOrNull(element, "itunes:subtitle")?.text?.trim(), - owner: RssItunesOwner.parse(findElementOrNull(element, "itunes:owner")), - keywords: findElementOrNull(element, "itunes:keywords") - ?.text - ?.split(",") - ?.map((keyword) => keyword.trim()) - ?.toList(), + // subtitle: findElementOrNull(element, "itunes:subtitle")?.text?.trim(), + //owner: RssItunesOwner.parse(findElementOrNull(element, "itunes:owner")), + // keywords: findElementOrNull(element, "itunes:keywords") + // ?.text + // ?.split(",") + // ?.map((keyword) => keyword.trim()) + // ?.toList(), image: RssItunesImage.parse(findElementOrNull(element, "itunes:image")), - categories: findAllDirectElementsOrNull(element, "itunes:category") - .map((ele) => RssItunesCategory.parse(ele)) - .toList(), - type: newRssItunesType(findElementOrNull(element, "itunes:type")), - newFeedUrl: - findElementOrNull(element, "itunes:new-feed-url")?.text?.trim(), - block: parseBoolLiteral(element, "itunes:block"), - complete: parseBoolLiteral(element, "itunes:complete"), + // categories: findAllDirectElementsOrNull(element, "itunes:category") + // .map((ele) => RssItunesCategory.parse(ele)) + // .toList(), + // type: newRssItunesType(findElementOrNull(element, "itunes:type")), + // newFeedUrl: + // findElementOrNull(element, "itunes:new-feed-url")?.text?.trim(), + // block: parseBoolLiteral(element, "itunes:block"), + // complete: parseBoolLiteral(element, "itunes:complete"), ); } } diff --git a/lib/webfeed/util/helpers.dart b/lib/webfeed/util/helpers.dart index 179926c..91ba26b 100644 --- a/lib/webfeed/util/helpers.dart +++ b/lib/webfeed/util/helpers.dart @@ -5,6 +5,7 @@ import 'package:xml/xml.dart'; XmlElement findElementOrNull(XmlElement element, String name, {String namespace}) { try { + return element.findAllElements(name, namespace: namespace).first; } on StateError { return null; diff --git a/pubspec.lock b/pubspec.lock index a370d47..e72bb2e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -185,7 +185,7 @@ packages: source: hosted version: "3.1.3" image: - dependency: transitive + dependency: "direct dev" description: name: image url: "https://pub.flutter-io.cn" diff --git a/pubspec.yaml b/pubspec.yaml index 1274640..600b534 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -48,6 +48,7 @@ dev_dependencies: fluttertoast: ^3.1.3 intl: ^0.16.1 url_launcher: ^5.4.1 + image: ^2.1.4 # For information on the generic Dart part of this file, see the