improve subscribe experience
This commit is contained in:
parent
56f258d44a
commit
4e01b3979f
|
@ -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
|
||||
|
||||
|
|
|
@ -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<MyHomePage> {
|
|||
|
||||
@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<int>(
|
||||
context: context,
|
||||
delegate: _delegate,
|
||||
);
|
||||
},
|
||||
),
|
||||
title: Text('🎙TsacDop', style: TextStyle(color: Colors.blue[600])),
|
||||
actions: <Widget>[
|
||||
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<int>(
|
||||
context: context,
|
||||
delegate: _delegate,
|
||||
);
|
||||
},
|
||||
),
|
||||
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) {
|
||||
return <Widget>[
|
||||
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<int> {
|
|||
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<SearchResult> {
|
||||
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<SearchResult> {
|
|||
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
|
||||
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(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: ListTile(
|
||||
|
@ -238,27 +264,27 @@ class _SearchResultState extends State<SearchResult> {
|
|||
),
|
||||
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),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -82,10 +82,11 @@ class DBHelper {
|
|||
List<Map> 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) {
|
||||
|
|
|
@ -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<EpisodeDetail> {
|
||||
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<EpisodeDetail> {
|
|||
_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<EpisodeDetail> {
|
|||
|
||||
@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<EpisodeDetail> {
|
|||
children: <Widget>[
|
||||
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>[
|
||||
(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<EpisodeDetail> {
|
|||
'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<EpisodeDetail> {
|
|||
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<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))));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -13,7 +13,6 @@ class Home extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _HomeState extends State<Home> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
|
@ -25,21 +24,25 @@ class _HomeState extends State<Home> {
|
|||
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(),
|
||||
),
|
||||
|
|
|
@ -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<ScrollPodcasts> {
|
||||
var dir;
|
||||
Future<List<PodcastLocal>> getPodcastLocal() async {
|
||||
var dbHelper = DBHelper();
|
||||
List<PodcastLocal> podcastList = await dbHelper.getPodcastLocal();
|
||||
dir = await getApplicationDocumentsDirectory();
|
||||
return podcastList;
|
||||
}
|
||||
|
||||
ImportState importState;
|
||||
didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
final importState = Provider.of<ImportOmpl>(context).importState;
|
||||
if (importState == ImportState.complete) {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder<List<PodcastLocal>>(
|
||||
|
@ -38,8 +56,8 @@ class _ScrollPodcastsState extends State<ScrollPodcasts> {
|
|||
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<ScrollPodcasts> {
|
|||
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<ScrollPodcasts> {
|
|||
),
|
||||
),
|
||||
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<ScrollPodcasts> {
|
|||
),
|
||||
);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -15,8 +15,8 @@ class _MainTabState extends State<MainTab> 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<MainTab> with TickerProviderStateMixin {
|
|||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
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: <Widget>[
|
||||
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),),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
@ -6,26 +6,72 @@ class Import extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<ImportOmpl>(
|
||||
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: <Widget>[
|
||||
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: <Widget>[
|
||||
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: <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()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
),
|
||||
|
|
|
@ -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<AboutPodcast> {
|
|||
children: <Widget>[
|
||||
!_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))
|
||||
|
|
|
@ -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<int> 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<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
|
||||
Widget build(BuildContext 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 {
|
||||
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);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import 'package:webfeed/util/helpers.dart';
|
||||
import '../../util/helpers.dart';
|
||||
import 'package:xml/xml.dart';
|
||||
|
||||
class DublinCore {
|
||||
|
|
|
@ -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),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue