improve subscribe experience

This commit is contained in:
stonegate 2020-02-11 21:01:57 +08:00
parent 56f258d44a
commit 4e01b3979f
20 changed files with 425 additions and 304 deletions

View File

@ -2,11 +2,11 @@
Enjoy podcasts with tsacdop! Enjoy podcasts with tsacdop!
Tsacdop is a podcasts player developed with flutter. 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/). 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 ## Getting Started

View File

@ -1,8 +1,12 @@
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:color_thief_flutter/color_thief_flutter.dart'; import 'package:color_thief_flutter/color_thief_flutter.dart';
import 'class/importompl.dart'; import 'class/importompl.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:provider/provider.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:convert';
import 'dart:async'; import 'dart:async';
import 'class/searchpodcast.dart'; import 'class/searchpodcast.dart';
@ -10,6 +14,7 @@ import 'class/podcastlocal.dart';
import 'class/sqflite_localpodcast.dart'; import 'class/sqflite_localpodcast.dart';
import 'home.dart'; import 'home.dart';
import 'popupmenu.dart'; import 'popupmenu.dart';
import 'webfeed/webfeed.dart';
class MyHomePage extends StatefulWidget { class MyHomePage extends StatefulWidget {
@override @override
@ -22,31 +27,28 @@ class _MyHomePageState extends State<MyHomePage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ChangeNotifierProvider( return Scaffold(
create: (context) => ImportOmpl(), key: _scaffoldKey,
child: Scaffold( appBar: AppBar(
key: _scaffoldKey, elevation: 0,
appBar: AppBar( centerTitle: true,
elevation: 0, backgroundColor: Colors.grey[100],
centerTitle: true, leading: IconButton(
backgroundColor: Colors.grey[100], tooltip: 'Add',
leading: IconButton( icon: const Icon(Icons.add_circle_outline),
tooltip: 'Add', onPressed: () async {
icon: const Icon(Icons.add_circle_outline), await showSearch<int>(
onPressed: () async { context: context,
await showSearch<int>( delegate: _delegate,
context: context, );
delegate: _delegate, },
);
},
),
title: Text('🎙TsacDop', style: TextStyle(color: Colors.blue[600])),
actions: <Widget>[
PopupMenu(),
],
), ),
body: Home(), title: Text('🎙TsacDop', style: TextStyle(color: Colors.blue[600])),
actions: <Widget>[
PopupMenu(),
],
), ),
body: Home(),
); );
} }
} }
@ -119,13 +121,7 @@ class _MyHomePageDelegate extends SearchDelegate<int> {
List<Widget> buildActions(BuildContext context) { List<Widget> buildActions(BuildContext context) {
return <Widget>[ return <Widget>[
if (query.isEmpty) if (query.isEmpty)
IconButton( Center()
tooltip: 'Voice Search',
icon: const Icon(Icons.mic),
onPressed: () {
query = 'TODO: implement voice input';
},
)
else else
IconButton( IconButton(
tooltip: 'Clear', tooltip: 'Clear',
@ -144,9 +140,12 @@ class _MyHomePageDelegate extends SearchDelegate<int> {
height: 10, height: 10,
width: 10, width: 10,
margin: EdgeInsets.only(top: 400), margin: EdgeInsets.only(top: 400),
child: Image.asset( child: SizedBox(
'assets/listennote.png', height: 10,
fit: BoxFit.fill, child: Image.asset(
'assets/listennote.png',
fit: BoxFit.fill,
),
), ),
); );
return FutureBuilder( return FutureBuilder(
@ -183,31 +182,6 @@ class SearchResult extends StatefulWidget {
class _SearchResultState extends State<SearchResult> { class _SearchResultState extends State<SearchResult> {
bool _issubscribe; bool _issubscribe;
bool _adding; 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 @override
void initState() { void initState() {
@ -221,8 +195,60 @@ class _SearchResultState extends State<SearchResult> {
super.dispose(); super.dispose();
} }
Future<String> 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final importOmpl = Provider.of<ImportOmpl>(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<List<int>> imageResponse = await Dio().get<List<int>>(
_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( return Container(
padding: EdgeInsets.symmetric(horizontal: 12.0), padding: EdgeInsets.symmetric(horizontal: 12.0),
child: ListTile( child: ListTile(
@ -238,27 +264,27 @@ class _SearchResultState extends State<SearchResult> {
), ),
title: Text(widget.onlinePodcast.title), title: Text(widget.onlinePodcast.title),
subtitle: Text(widget.onlinePodcast.publisher), subtitle: Text(widget.onlinePodcast.publisher),
trailing: isXimalaya(widget.onlinePodcast.rss) trailing: !_issubscribe
? OutlineButton(child: Text('Not Support'), onPressed: null) ? !_adding
: !_issubscribe ? OutlineButton(
? !_adding child:
? OutlineButton( Text('Subscribe', style: TextStyle(color: Colors.blue)),
child: Text('Subscribe', onPressed: () {
style: TextStyle(color: Colors.blue)), importOmpl.rssTitle =
onPressed: () { widget.onlinePodcast.title;
_subscribe(widget.onlinePodcast); savePodcast(widget.onlinePodcast.rss);
}) })
: OutlineButton( : OutlineButton(
child: SizedBox( child: SizedBox(
height: 20, height: 20,
width: 20, width: 20,
child: CircularProgressIndicator( child: CircularProgressIndicator(
strokeWidth: 2, strokeWidth: 2,
valueColor: AlwaysStoppedAnimation(Colors.blue), valueColor: AlwaysStoppedAnimation(Colors.blue),
)), )),
onPressed: () {}, onPressed: () {},
) )
: OutlineButton(child: Text('Subscribe'), onPressed: null), : OutlineButton(child: Text('Subscribe'), onPressed: null),
), ),
); );
} }

View File

@ -1,16 +1,19 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
enum ImportState{start, import, complete, stop, error} enum ImportState{start, import, parse, complete, stop, error}
class ImportOmpl extends ChangeNotifier{ class ImportOmpl extends ChangeNotifier{
ImportState _importState = ImportState.stop; ImportState _importState = ImportState.stop;
String _rssTitle; String _rssTitle;
String get rsstitle => _rssTitle; String get rsstitle => _rssTitle;
set rssTitle(String title){ set rssTitle(String title){
_rssTitle = title; _rssTitle = title;
notifyListeners();
} }
ImportState get importState => _importState; ImportState get importState => _importState;
set importState(ImportState state){ set importState(ImportState state){
_importState = state; _importState = state;
notifyListeners(); notifyListeners();

View File

@ -82,10 +82,11 @@ class DBHelper {
List<Map> list = await dbClient.rawQuery( List<Map> list = await dbClient.rawQuery(
"""SELECT downloaded FROM Episodes WHERE downloaded != 'ND' AND feed_title = ?""", """SELECT downloaded FROM Episodes WHERE downloaded != 'ND' AND feed_title = ?""",
[title]); [title]);
for(int i=0; i < list.length; i++){ for (int i = 0; i < list.length; i++) {
if(list[i] != null) if (list[i] != null)
FlutterDownloader.remove(taskId: list[i]['downloaded'], shouldDeleteContent: true); FlutterDownloader.remove(
print('Removed all download task'); taskId: list[i]['downloaded'], shouldDeleteContent: true);
print('Removed all download tasks');
} }
await dbClient await dbClient
.rawDelete('DELETE FROM Episodes WHERE feed_title=?', [title]); .rawDelete('DELETE FROM Episodes WHERE feed_title=?', [title]);
@ -99,70 +100,21 @@ class DBHelper {
return url; return url;
} }
int stringToDate(String s) { DateTime _parsePubDate(String pubDate) {
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) {
if (pubDate == null) return null; 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) { int getExplicit(bool b) {
@ -185,6 +137,7 @@ class DBHelper {
String _title; String _title;
String _url; String _url;
String _description; String _description;
int _duration;
var _p = RssFeed.parse(rss); var _p = RssFeed.parse(rss);
int _result = _p.items.length; int _result = _p.items.length;
var dbClient = await database; var dbClient = await database;
@ -208,9 +161,11 @@ class DBHelper {
: _url = _p.items[i].enclosure.url; : _url = _p.items[i].enclosure.url;
final _length = _p.items[i].enclosure.length; final _length = _p.items[i].enclosure.length;
final _pubDate = _p.items[i].pubDate; final _pubDate = _p.items[i].pubDate;
final DateTime _date = _parsePubDate(_pubDate); final _date = _parsePubDate(_pubDate);
final _milliseconds = _date.millisecondsSinceEpoch; 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); final _explicit = getExplicit(_p.items[i].itunes.explicit);
if (_p.items[i].enclosure.url != null) { if (_p.items[i].enclosure.url != null) {
await dbClient.transaction((txn) { await dbClient.transaction((txn) {

View File

@ -6,6 +6,7 @@ import 'package:provider/provider.dart';
import 'package:flutter_html/flutter_html.dart'; import 'package:flutter_html/flutter_html.dart';
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'class/audiostate.dart'; import 'class/audiostate.dart';
import 'class/episodebrief.dart'; import 'class/episodebrief.dart';
import 'class/sqflite_localpodcast.dart'; import 'class/sqflite_localpodcast.dart';
@ -25,7 +26,6 @@ class EpisodeDetail extends StatefulWidget {
class _EpisodeDetailState extends State<EpisodeDetail> { class _EpisodeDetailState extends State<EpisodeDetail> {
final textstyle = TextStyle(fontSize: 15.0, color: Colors.black); final textstyle = TextStyle(fontSize: 15.0, color: Colors.black);
double downloadProgress; double downloadProgress;
Color _c;
bool _loaddes; bool _loaddes;
Future getSDescription(String title) async { Future getSDescription(String title) async {
@ -36,14 +36,15 @@ class _EpisodeDetailState extends State<EpisodeDetail> {
_loaddes = true; _loaddes = true;
}); });
} }
_launchUrl(String url) async { _launchUrl(String url) async {
if (await canLaunch(url)) { if (await canLaunch(url)) {
await launch(url); await launch(url);
} else { } else {
throw 'Could not launch $url'; throw 'Could not launch $url';
}
} }
}
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -53,11 +54,6 @@ class _EpisodeDetailState extends State<EpisodeDetail> {
@override @override
Widget build(BuildContext context) { 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( return Scaffold(
backgroundColor: Colors.grey[100], backgroundColor: Colors.grey[100],
@ -81,29 +77,28 @@ class _EpisodeDetailState extends State<EpisodeDetail> {
children: <Widget>[ children: <Widget>[
Container( Container(
padding: EdgeInsets.symmetric(horizontal: 12.0), padding: EdgeInsets.symmetric(horizontal: 12.0),
margin: EdgeInsets.only(bottom: 10.0),
alignment: Alignment.topLeft, alignment: Alignment.topLeft,
child: Text( child: Text(
widget.episodeItem.title, widget.episodeItem.title,
style: Theme.of(context).textTheme.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( Container(
padding: EdgeInsets.all(12.0), padding: EdgeInsets.all(12.0),
height: 50, height: 50.0,
child: Row( child: Row(
children: <Widget>[ children: <Widget>[
(widget.episodeItem.explicit == 1) (widget.episodeItem.explicit == 1)
? Container( ? ExplicitScale()
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)))
: Center(), : Center(),
Container( Container(
decoration: BoxDecoration( decoration: BoxDecoration(
@ -133,19 +128,6 @@ class _EpisodeDetailState extends State<EpisodeDetail> {
'MB', 'MB',
style: textstyle), 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<EpisodeDetail> {
padding: EdgeInsets.only(left: 12.0, right: 12.0, top: 5.0), padding: EdgeInsets.only(left: 12.0, right: 12.0, top: 5.0),
child: SingleChildScrollView( child: SingleChildScrollView(
child: (widget.episodeItem.description != null && _loaddes) child: (widget.episodeItem.description != null && _loaddes)
? Html(data: widget.episodeItem.description, ? Html(
onLinkTap: (url){ data: widget.episodeItem.description,
_launchUrl(url); onLinkTap: (url) {
}, _launchUrl(url);
useRichText: true, },
) useRichText: true,
)
: Center(), : Center(),
), ),
), ),
@ -531,3 +514,53 @@ class _ImageRotateState extends State<ImageRotate>
); );
} }
} }
class ExplicitScale extends StatefulWidget {
@override
_ExplicitScaleState createState() => _ExplicitScaleState();
}
class _ExplicitScaleState extends State<ExplicitScale>
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))));
}
}

View File

@ -120,6 +120,7 @@ class EpisodeGrid extends StatelessWidget {
Expanded( Expanded(
flex: 5, flex: 5,
child: Container( child: Container(
alignment: Alignment.topLeft,
padding: EdgeInsets.only(top: 2.0), padding: EdgeInsets.only(top: 2.0),
child: Text( child: Text(
podcast[index].title, podcast[index].title,

View File

@ -13,7 +13,6 @@ class Home extends StatefulWidget {
} }
class _HomeState extends State<Home> { class _HomeState extends State<Home> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return Column(
@ -25,21 +24,25 @@ class _HomeState extends State<Home> {
height: 30, height: 30,
padding: EdgeInsets.symmetric(horizontal: 15), padding: EdgeInsets.symmetric(horizontal: 15),
alignment: Alignment.bottomRight, alignment: Alignment.bottomRight,
child: GestureDetector(
child: GestureDetector( onTap: () {
onTap: () { Navigator.push(
Navigator.push( context,
context, SlideLeftRoute(page: Podcast()),
SlideLeftRoute(page: Podcast()), );
); },
}, child: Container(
child: Text('See All', height: 30,
style: TextStyle( padding: EdgeInsets.all(5.0),
color: Colors.red[300], fontWeight: FontWeight.bold, )), child: Text('See All',
style: TextStyle(
)), color: Colors.red[300],
Container( fontWeight: FontWeight.bold,
child: ScrollPodcasts()), )),
),
),
),
Container(child: ScrollPodcasts()),
Expanded( Expanded(
child: MainTab(), child: MainTab(),
), ),

View File

@ -1,11 +1,18 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io';
import 'dart:async';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.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/episodebrief.dart';
import 'class/podcastlocal.dart'; import 'class/podcastlocal.dart';
import 'class/importompl.dart';
import 'class/sqflite_localpodcast.dart'; import 'class/sqflite_localpodcast.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'episodedetail.dart'; import 'episodedetail.dart';
import 'podcastdetail.dart'; import 'podcastdetail.dart';
import 'pageroute.dart'; import 'pageroute.dart';
@ -16,12 +23,23 @@ class ScrollPodcasts extends StatefulWidget {
} }
class _ScrollPodcastsState extends State<ScrollPodcasts> { class _ScrollPodcastsState extends State<ScrollPodcasts> {
var dir;
Future<List<PodcastLocal>> getPodcastLocal() async { Future<List<PodcastLocal>> getPodcastLocal() async {
var dbHelper = DBHelper(); var dbHelper = DBHelper();
List<PodcastLocal> podcastList = await dbHelper.getPodcastLocal(); List<PodcastLocal> podcastList = await dbHelper.getPodcastLocal();
dir = await getApplicationDocumentsDirectory();
return podcastList; return podcastList;
} }
ImportState importState;
didChangeDependencies() {
super.didChangeDependencies();
final importState = Provider.of<ImportOmpl>(context).importState;
if (importState == ImportState.complete) {
setState(() {});
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return FutureBuilder<List<PodcastLocal>>( return FutureBuilder<List<PodcastLocal>>(
@ -38,8 +56,8 @@ class _ScrollPodcastsState extends State<ScrollPodcasts> {
height: 70, height: 70,
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: TabBar( child: TabBar(
labelPadding: labelPadding: EdgeInsets.only(
EdgeInsets.only(bottom: 15.0, left: 6.0, right: 6.0), top: 5.0, bottom: 10.0, left: 6.0, right: 6.0),
indicator: indicator:
CircleTabIndicator(color: Colors.blue, radius: 3), CircleTabIndicator(color: Colors.blue, radius: 3),
isScrollable: true, isScrollable: true,
@ -50,11 +68,8 @@ class _ScrollPodcastsState extends State<ScrollPodcasts> {
child: LimitedBox( child: LimitedBox(
maxHeight: 50, maxHeight: 50,
maxWidth: 50, maxWidth: 50,
child: CachedNetworkImage( child: Image.file(
imageUrl: podcastLocal.imageUrl, File("${dir.path}/${podcastLocal.title}.png")),
placeholder: (context, url) =>
CircularProgressIndicator(),
),
), ),
), ),
); );
@ -62,7 +77,7 @@ class _ScrollPodcastsState extends State<ScrollPodcasts> {
), ),
), ),
Container( Container(
height: 200, height: 195,
margin: EdgeInsets.only(left: 10, right: 10), margin: EdgeInsets.only(left: 10, right: 10),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
@ -85,7 +100,9 @@ class _ScrollPodcastsState extends State<ScrollPodcasts> {
), ),
); );
} }
return Center(); return Container(
height: 250.0,
);
}, },
); );
} }
@ -204,9 +221,9 @@ class ShowEpisode extends StatelessWidget {
context, context,
ScaleRoute( ScaleRoute(
page: EpisodeDetail( page: EpisodeDetail(
episodeItem: podcast[index], episodeItem: podcast[index],
heroTag: 'scroll', heroTag: 'scroll',
)), )),
); );
}, },
child: Container( child: Container(
@ -258,6 +275,7 @@ class ShowEpisode extends StatelessWidget {
flex: 5, flex: 5,
child: Container( child: Container(
padding: EdgeInsets.only(top: 2.0), padding: EdgeInsets.only(top: 2.0),
alignment: Alignment.topLeft,
child: Text( child: Text(
podcast[index].title, podcast[index].title,
style: TextStyle( style: TextStyle(
@ -269,7 +287,7 @@ class ShowEpisode extends StatelessWidget {
), ),
Expanded( Expanded(
flex: 1, flex: 1,
child: Align( child: Container(
alignment: Alignment.bottomLeft, alignment: Alignment.bottomLeft,
child: Text( child: Text(
podcast[index].pubDate.substring(4, 16), podcast[index].pubDate.substring(4, 16),
@ -319,5 +337,3 @@ class _CirclePainter extends BoxPainter {
canvas.drawCircle(circleOffset, radius, _paint); canvas.drawCircle(circleOffset, radius, _paint);
} }
} }

View File

@ -15,8 +15,8 @@ class _MainTabState extends State<MainTab> with TickerProviderStateMixin {
Decoration getIndicator() { Decoration getIndicator() {
return const UnderlineTabIndicator( return const UnderlineTabIndicator(
borderSide: BorderSide(color: Colors.red, width: 2), 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 @override
void initState() { void initState() {
super.initState(); super.initState();
@ -36,20 +36,21 @@ class _MainTabState extends State<MainTab> with TickerProviderStateMixin {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: <Widget>[
Container( Container(
padding: EdgeInsets.symmetric(horizontal: 10.0),
height: 50, height: 50,
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: TabBar( child: TabBar(
isScrollable: true, isScrollable: true,
labelPadding: labelPadding:
EdgeInsets.only(bottom:10.0,left: 20.0), EdgeInsets.all(10.0),
controller: _controller, controller: _controller,
labelColor: Colors.red, labelColor: Colors.red,
unselectedLabelColor: Colors.black, unselectedLabelColor: Colors.black,
indicator: getIndicator(), indicator: getIndicator(),
tabs: <Widget>[ tabs: <Widget>[
Text('Recent Update',style: TextStyle(fontWeight: FontWeight.bold),), Text('Recent Update',style: TextStyle(fontWeight: FontWeight.bold),),
Text('Favorite',style: TextStyle(fontWeight: FontWeight.bold),), Text('Favorites',style: TextStyle(fontWeight: FontWeight.bold),),
Text('Dowloads',style: TextStyle(fontWeight: FontWeight.bold),), Text('Downloads',style: TextStyle(fontWeight: FontWeight.bold),),
], ],
), ),
), ),

View File

@ -6,26 +6,72 @@ class Import extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Consumer<ImportOmpl>( return Consumer<ImportOmpl>(
builder: (context, importOmpl, _) => Container( builder: (context, importOmpl, _) => Container(
child: importOmpl.importState == ImportState.start color: Colors.grey[300],
? Container( child: importOmpl.importState == ImportState.start
height: 20.0, ? Column(
alignment: Alignment.center, mainAxisAlignment: MainAxisAlignment.start,
child: Text('Start'), mainAxisSize: MainAxisSize.min,
) children: <Widget>[
: importOmpl.importState == ImportState.import SizedBox(
? Container( height: 2.0,
child: LinearProgressIndicator()),
Container(
padding: EdgeInsets.symmetric(horizontal: 20.0),
height: 20.0, height: 20.0,
alignment: Alignment.center, alignment: Alignment.centerLeft,
child: Text('Importing'+(importOmpl.rsstitle))) child: Text('Read file successful'),
: importOmpl.importState == ImportState.complete ),
? Container( ])
: importOmpl.importState == ImportState.import
? Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
SizedBox(
height: 2.0,
child: LinearProgressIndicator()),
Container(
height: 20.0, height: 20.0,
alignment: Alignment.center, padding: EdgeInsets.symmetric(horizontal: 20.0),
child: Text('Complete'), alignment: Alignment.centerLeft,
) child:
: importOmpl.importState == ImportState.stop Text('Importing: ' + (importOmpl.rsstitle))),
? Center() ],
: Center())); )
: importOmpl.importState == ImportState.parse
? Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
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: <Widget>[
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()),
);
} }
} }

View File

@ -4,12 +4,14 @@ import 'package:provider/provider.dart';
import 'package:flutter_downloader/flutter_downloader.dart'; import 'package:flutter_downloader/flutter_downloader.dart';
import 'addpodcast.dart'; import 'addpodcast.dart';
import 'class/audiostate.dart'; import 'class/audiostate.dart';
import 'class/importompl.dart';
void main() async { void main() async {
runApp( runApp(
MultiProvider( MultiProvider(
providers: [ providers: [
ChangeNotifierProvider(create: (context) => Urlchange()), ChangeNotifierProvider(create: (context) => Urlchange()),
ChangeNotifierProvider(create: (context) => ImportOmpl()),
], ],
child: MyApp(), child: MyApp(),
), ),

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'dart:async'; import 'dart:async';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'class/podcastlocal.dart'; import 'class/podcastlocal.dart';
@ -72,7 +73,7 @@ class _AboutPodcastState extends State<AboutPodcast> {
children: <Widget>[ children: <Widget>[
!_load !_load
? Center() ? Center()
: _description != null ? Text(_description) : Center(), : _description != null ? Html(data: _description) : Center(),
(widget.podcastLocal.author != null) (widget.podcastLocal.author != null)
? Text(widget.podcastLocal.author, ? Text(widget.podcastLocal.author,
style: TextStyle(color: Colors.blue)) style: TextStyle(color: Colors.blue))

View File

@ -6,6 +6,9 @@ import 'package:provider/provider.dart';
import 'package:xml/xml.dart' as xml; import 'package:xml/xml.dart' as xml;
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:flutter/services.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 'about.dart';
import 'class/podcastlocal.dart'; import 'class/podcastlocal.dart';
import 'class/sqflite_localpodcast.dart'; import 'class/sqflite_localpodcast.dart';
@ -28,43 +31,71 @@ class OmplOutline {
class PopupMenu extends StatelessWidget { class PopupMenu extends StatelessWidget {
Future<int> saveOmpl(String rss) async { Future<String> getColor(File file) async {
var dbHelper = DBHelper(); final imageProvider = FileImage(file);
try { var colorImage = await getImageFromProvider(imageProvider);
Response response = await Dio().get(rss); var color = await getColorFromImage(colorImage);
var _p = RssFeed.parse(response.data); String primaryColor = color.toString();
String _primaryColor = '[100,100,100]'; return primaryColor;
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;
}
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final importOmpl = Provider.of<ImportOmpl>(context); final importOmpl = Provider.of<ImportOmpl>(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<List<int>> imageResponse = await Dio().get<List<int>>(
_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 { void _saveOmpl(String path) async {
File file = File(path); File file = File(path);
String opml = file.readAsStringSync(); String opml = file.readAsStringSync();
try { try {
var content = xml.parse(opml); var content = xml.parse(opml);
importOmpl.importState = ImportState.import;
var total = content var total = content
.findAllElements('outline') .findAllElements('outline')
.map((ele) => OmplOutline.parse(ele)) .map((ele) => OmplOutline.parse(ele))
.toList(); .toList();
for (int i = 0; i < total.length; i++) { for (int i = 0; i < total.length; i++) {
if (total[i].xmlUrl != null) if (total[i].xmlUrl != null) {
await saveOmpl(total[i].xmlUrl); importOmpl.rssTitle = total[i].text;
importOmpl.rssTitle = total[i].text; await saveOmpl(total[i].xmlUrl);
print(total[i].text); print(total[i].text);
}
} }
importOmpl.importState = ImportState.complete; importOmpl.importState = ImportState.complete;
importOmpl.importState = ImportState.stop;
print('Import fisnished'); print('Import fisnished');
} catch (e) { } catch (e) {
print(e); print(e);

View File

@ -1,4 +1,4 @@
import 'package:webfeed/util/helpers.dart'; import '../../util/helpers.dart';
import 'package:xml/xml.dart'; import 'package:xml/xml.dart';
class DublinCore { class DublinCore {

View File

@ -54,12 +54,12 @@ class RssItem {
guid: findElementOrNull(element, "guid")?.text, guid: findElementOrNull(element, "guid")?.text,
pubDate: findElementOrNull(element, "pubDate")?.text, pubDate: findElementOrNull(element, "pubDate")?.text,
author: findElementOrNull(element, "author")?.text, author: findElementOrNull(element, "author")?.text,
comments: findElementOrNull(element, "comments")?.text, // comments: findElementOrNull(element, "comments")?.text,
source: RssSource.parse(findElementOrNull(element, "source")), // source: RssSource.parse(findElementOrNull(element, "source")),
content: RssContent.parse(findElementOrNull(element, "content:encoded")), // content: RssContent.parse(findElementOrNull(element, "content:encoded")),
media: Media.parse(element), // media: Media.parse(element),
enclosure: RssEnclosure.parse(findElementOrNull(element, "enclosure")), enclosure: RssEnclosure.parse(findElementOrNull(element, "enclosure")),
dc: DublinCore.parse(element), //dc: DublinCore.parse(element),
itunes: RssItemItunes.parse(element), itunes: RssItemItunes.parse(element),
); );
} }

View File

@ -40,25 +40,26 @@ class RssItemItunes {
if (element == null) { if (element == null) {
return null; return null;
} }
var episodeStr = findElementOrNull(element, "itunes:episode")?.text?.trim(); //var episodeStr = findElementOrNull(element, "itunes:episode")?.text?.trim();
var seasonStr = findElementOrNull(element, "itunes:season")?.text?.trim(); //var seasonStr = findElementOrNull(element, "itunes:season")?.text?.trim();
var durationStr = findElementOrNull(element, "itunes:duration")?.text?.trim(); var durationStr =
findElementOrNull(element, "itunes:duration")?.text?.trim();
return RssItemItunes( return RssItemItunes(
title: findElementOrNull(element, "itunes:title")?.text?.trim(), title: findElementOrNull(element, "itunes:title")?.text?.trim(),
//episode: episodeStr == null ? null : int.parse(episodeStr), //episode: episodeStr == null ? null : int.parse(episodeStr),
//season: seasonStr == null ? null : int.parse(seasonStr), //season: seasonStr == null ? null : int.parse(seasonStr),
duration: durationStr == null ? null : parseDuration(durationStr), 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(), author: findElementOrNull(element, "itunes:author")?.text?.trim(),
summary: findElementOrNull(element, "itunes:summary")?.text?.trim(), summary: findElementOrNull(element, "itunes:summary")?.text?.trim(),
explicit: parseBoolLiteral(element, "itunes:explicit"), explicit: parseBoolLiteral(element, "itunes:explicit"),
subtitle: findElementOrNull(element, "itunes:subtitle")?.text?.trim(), //subtitle: findElementOrNull(element, "itunes:subtitle")?.text?.trim(),
keywords: findElementOrNull(element, "itunes:keywords")?.text?.split(",")?.map((keyword) => keyword.trim())?.toList(), // keywords: findElementOrNull(element, "itunes:keywords")?.text?.split(",")?.map((keyword) => keyword.trim())?.toList(),
image: RssItunesImage.parse(findElementOrNull(element, "itunes:image")), // image: RssItunesImage.parse(findElementOrNull(element, "itunes:image")),
category: RssItunesCategory.parse( // category: RssItunesCategory.parse(
findElementOrNull(element, "itunes:category")), // findElementOrNull(element, "itunes:category")),
block: parseBoolLiteral(element, "itunes:block"), // block: parseBoolLiteral(element, "itunes:block"),
); );
} }
} }

View File

@ -48,22 +48,22 @@ class RssItunes {
summary: findElementOrNull(element, "itunes:summary")?.text?.trim(), summary: findElementOrNull(element, "itunes:summary")?.text?.trim(),
explicit: parseBoolLiteral(element, "itunes:explicit"), explicit: parseBoolLiteral(element, "itunes:explicit"),
title: findElementOrNull(element, "itunes:title")?.text?.trim(), title: findElementOrNull(element, "itunes:title")?.text?.trim(),
subtitle: findElementOrNull(element, "itunes:subtitle")?.text?.trim(), // subtitle: findElementOrNull(element, "itunes:subtitle")?.text?.trim(),
owner: RssItunesOwner.parse(findElementOrNull(element, "itunes:owner")), //owner: RssItunesOwner.parse(findElementOrNull(element, "itunes:owner")),
keywords: findElementOrNull(element, "itunes:keywords") // keywords: findElementOrNull(element, "itunes:keywords")
?.text // ?.text
?.split(",") // ?.split(",")
?.map((keyword) => keyword.trim()) // ?.map((keyword) => keyword.trim())
?.toList(), // ?.toList(),
image: RssItunesImage.parse(findElementOrNull(element, "itunes:image")), image: RssItunesImage.parse(findElementOrNull(element, "itunes:image")),
categories: findAllDirectElementsOrNull(element, "itunes:category") // categories: findAllDirectElementsOrNull(element, "itunes:category")
.map((ele) => RssItunesCategory.parse(ele)) // .map((ele) => RssItunesCategory.parse(ele))
.toList(), // .toList(),
type: newRssItunesType(findElementOrNull(element, "itunes:type")), // type: newRssItunesType(findElementOrNull(element, "itunes:type")),
newFeedUrl: // newFeedUrl:
findElementOrNull(element, "itunes:new-feed-url")?.text?.trim(), // findElementOrNull(element, "itunes:new-feed-url")?.text?.trim(),
block: parseBoolLiteral(element, "itunes:block"), // block: parseBoolLiteral(element, "itunes:block"),
complete: parseBoolLiteral(element, "itunes:complete"), // complete: parseBoolLiteral(element, "itunes:complete"),
); );
} }
} }

View File

@ -5,6 +5,7 @@ import 'package:xml/xml.dart';
XmlElement findElementOrNull(XmlElement element, String name, XmlElement findElementOrNull(XmlElement element, String name,
{String namespace}) { {String namespace}) {
try { try {
return element.findAllElements(name, namespace: namespace).first; return element.findAllElements(name, namespace: namespace).first;
} on StateError { } on StateError {
return null; return null;

View File

@ -185,7 +185,7 @@ packages:
source: hosted source: hosted
version: "3.1.3" version: "3.1.3"
image: image:
dependency: transitive dependency: "direct dev"
description: description:
name: image name: image
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"

View File

@ -48,6 +48,7 @@ dev_dependencies:
fluttertoast: ^3.1.3 fluttertoast: ^3.1.3
intl: ^0.16.1 intl: ^0.16.1
url_launcher: ^5.4.1 url_launcher: ^5.4.1
image: ^2.1.4
# For information on the generic Dart part of this file, see the # For information on the generic Dart part of this file, see the