new file: lib/class/podcast_group.dart

modified:   lib/class/podcastlocal.dart
	modified:   lib/class/settingstate.dart
	modified:   lib/episodes/episodedetail.dart
	modified:   lib/episodes/episodedownload.dart
	modified:   lib/home/appbar/addpodcast.dart
	modified:   lib/home/appbar/popupmenu.dart
	modified:   lib/home/audio_player.dart
	modified:   lib/home/home.dart
	modified:   lib/home/homescroll.dart
	modified:   lib/home/hometab.dart
	new file:   lib/local_storage/key_value_storage.dart
	renamed:    lib/class/sqflite_localpodcast.dart -> lib/local_storage/sqflite_localpodcast.dart
	modified:   lib/main.dart
	modified:   lib/podcasts/podcastdetail.dart
	new file:   lib/podcasts/podcastgroup.dart
	modified:   lib/podcasts/podcastlist.dart
	modified:   lib/podcasts/podcastmanage.dart
	modified:   lib/util/episodegrid.dart
	modified:   lib/webfeed/domain/rss_item.dart
	modified:   lib/webfeed/domain/rss_itunes.dart
	modified:   pubspec.lock
	modified:   pubspec.yaml
Add podcast group support
Manage group with provider
This commit is contained in:
stonegate 2020-02-20 17:09:21 +08:00
parent d3efce463c
commit 16567a7199
23 changed files with 1275 additions and 524 deletions

View File

@ -0,0 +1,158 @@
import 'dart:collection';
import 'dart:core';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:tsacdop/local_storage/key_value_storage.dart';
import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
import 'package:uuid/uuid.dart';
import 'package:tsacdop/class/podcastlocal.dart';
class GroupEntity {
final String name;
final String id;
final String color;
final List<String> podcastList;
GroupEntity(this.name, this.id, this.color, this.podcastList);
Map<String, Object> toJson() {
return {'name': name, 'id': id, 'color': color, 'podcastList': podcastList};
}
static GroupEntity fromJson(Map<String, Object> json) {
List<String> list = List.from(json['podcastList']);
print(json['[podcastList']);
return
GroupEntity(
json['name'] as String,
json['id'] as String,
json['color'] as String,
list);
}
}
class PodcastGroup {
final String name;
final String id;
final String color;
final List<String> podcastList;
PodcastGroup(this.name,
{this.color = '#000000', String id, List<String> podcastList})
: id = id ?? Uuid().v4(),
podcastList = podcastList ?? [];
Future getPodcasts() async {
var dbHelper = DBHelper();
if (podcastList != []) {
_podcasts = await dbHelper.getPodcastLocal(podcastList, 0);
}
}
List<PodcastLocal> _podcasts;
List<PodcastLocal> get podcasts => _podcasts;
GroupEntity toEntity() {
return GroupEntity(name, id, color, podcastList);
}
static PodcastGroup fromEntity(GroupEntity entity) {
return PodcastGroup(
entity.name,
id: entity.id,
color: entity.color,
podcastList: entity.podcastList,
);
}
}
class GroupList extends ChangeNotifier {
List<PodcastGroup> _groups;
DBHelper dbHelper = DBHelper();
UnmodifiableListView<PodcastGroup> get groups =>
UnmodifiableListView(_groups);
KeyValueStorage storage = KeyValueStorage('groups');
GroupList({List<PodcastGroup> groups}) : _groups = groups ?? [];
bool _isLoading = false;
bool get isLoading => _isLoading;
@override
void addListener(VoidCallback listener) {
super.addListener(listener);
loadGroups();
}
Future loadGroups() async {
_isLoading = true;
notifyListeners();
storage.getGroups().then((loadgroups) async {
_groups.addAll(loadgroups.map((e) => PodcastGroup.fromEntity(e)));
await Future.forEach(_groups, (group) async {
await group.getPodcasts();
});
_isLoading = false;
notifyListeners();
});
}
Future addGroup(PodcastGroup podcastGroup) async {
_groups.add(podcastGroup);
_saveGroup();
notifyListeners();
}
Future delGroup(PodcastGroup podcastGroup) async {
_groups.remove(podcastGroup);
notifyListeners();
_saveGroup();
}
void updateGroup(PodcastGroup podcastGroup) {
var oldGroup = _groups.firstWhere((it) => it.id == podcastGroup.id);
var index = _groups.indexOf(oldGroup);
_groups.replaceRange(index, index + 1, [podcastGroup]);
notifyListeners();
_saveGroup();
}
void _saveGroup() {
storage.saveGroup(_groups.map((it) => it.toEntity()).toList());
}
Future subscribe(PodcastLocal podcastLocal) async {
_groups[0].podcastList.add(podcastLocal.id);
_saveGroup();
await dbHelper.savePodcastLocal(podcastLocal);
await _groups[0].getPodcasts();
notifyListeners();
}
List<PodcastGroup> getPodcastGroup(String id) {
List<PodcastGroup> result =[];
_groups.forEach((group) {
if (group.podcastList.contains(id)) {
result.add(group);
}
});
return result;
}
changeGroup(String id, List<String> l) {
_groups.forEach((group) {
if (group.podcastList.contains(id)) {
group.podcastList.remove(id);
}
});
l.forEach((s) {
_groups.forEach((group) {
if (group.name == s) group.podcastList.add(id);
});
});
notifyListeners();
_saveGroup();
}
}

View File

@ -1,4 +1,6 @@
import 'package:uuid/uuid.dart';
class PodcastLocal {
final String title;
final String imageUrl;
@ -6,5 +8,7 @@ class PodcastLocal {
final String author;
String description;
final String primaryColor;
PodcastLocal(this.title, this.imageUrl, this.rssUrl, this.primaryColor, this.author);
}
final String id;
PodcastLocal(this.title, this.imageUrl, this.rssUrl, this.primaryColor, this.author,{String id}) : id = id ?? Uuid().v4();
}

View File

@ -1,19 +1,13 @@
import 'package:flutter/foundation.dart';
enum Setting {start, stop}
//two types podcast update, backhome nedd to back to default grooup.
enum Update {backhome, justupdate}
class SettingState extends ChangeNotifier{
Setting _subscribeupdate;
Setting get subscribeupdate => _subscribeupdate;
set subscribeUpdate(Setting s){
Update _subscribeupdate;
Update get subscribeupdate => _subscribeupdate;
set subscribeUpdate(Update s){
_subscribeupdate = s;
notifyListeners();
}
Setting _themeupdate;
Setting get themeUpdate => _themeupdate;
set themeUpdate(Setting s){
_themeupdate = s;
notifyListeners();
}
}

View File

@ -1,22 +1,23 @@
import 'dart:io';
import 'dart:math' as math;
import 'package:flutter/material.dart';
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 'package:path_provider/path_provider.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:tsacdop/class/audiostate.dart';
import 'package:tsacdop/class/episodebrief.dart';
import 'package:tsacdop/class/sqflite_localpodcast.dart';
import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
import 'episodedownload.dart';
enum DownloadState { stop, load, donwload, complete, error }
class EpisodeDetail extends StatefulWidget {
final EpisodeBrief episodeItem;
final String heroTag;
EpisodeDetail({this.episodeItem, this.heroTag, Key key}) : super(key: key);
final String path;
EpisodeDetail({this.episodeItem, this.heroTag, this.path, Key key})
: super(key: key);
@override
_EpisodeDetailState createState() => _EpisodeDetailState();
@ -26,7 +27,10 @@ class _EpisodeDetailState extends State<EpisodeDetail> {
final textstyle = TextStyle(fontSize: 15.0, color: Colors.black);
double downloadProgress;
bool _loaddes;
String path;
Future getSDescription(String title) async {
var dir = await getApplicationDocumentsDirectory();
path = dir.path;
var dbHelper = DBHelper();
widget.episodeItem.description = await dbHelper.getDescription(title);
if (mounted)
@ -62,7 +66,7 @@ class _EpisodeDetailState extends State<EpisodeDetail> {
),
body: Container(
color: Colors.grey[100],
padding: EdgeInsets.all(12.0),
padding: EdgeInsets.all(10.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
@ -77,7 +81,7 @@ class _EpisodeDetailState extends State<EpisodeDetail> {
alignment: Alignment.topLeft,
child: Text(
widget.episodeItem.title,
style: Theme.of(context).textTheme.title,
style: Theme.of(context).textTheme.headline5,
),
),
Container(
@ -144,22 +148,26 @@ class _EpisodeDetailState extends State<EpisodeDetail> {
child: Container(
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,
)
child: _loaddes
? (widget.episodeItem.description.contains('<'))
? Html(
data: widget.episodeItem.description,
onLinkTap: (url) {
_launchUrl(url);
},
useRichText: true,
)
: Container(
alignment: Alignment.topLeft,
child: Text(widget.episodeItem.description))
: Center(),
),
),
),
MenuBar(
episodeItem: widget.episodeItem,
heroTag: widget.heroTag,
),
episodeItem: widget.episodeItem,
heroTag: widget.heroTag,
path: widget.path),
],
),
),
@ -168,9 +176,11 @@ class _EpisodeDetailState extends State<EpisodeDetail> {
}
class MenuBar extends StatefulWidget {
final String path;
final EpisodeBrief episodeItem;
final String heroTag;
MenuBar({this.episodeItem, this.heroTag, Key key}) : super(key: key);
MenuBar({this.episodeItem, this.heroTag, this.path, Key key})
: super(key: key);
@override
_MenuBarState createState() => _MenuBarState();
}
@ -204,7 +214,7 @@ class _MenuBarState extends State<MenuBar> {
_like = widget.episodeItem.liked;
}
Widget _buttonOnMenu(Widget widget, Function() onTap) => Material(
Widget _buttonOnMenu(Widget widget, VoidCallback onTap) => Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
@ -232,7 +242,8 @@ class _MenuBarState extends State<MenuBar> {
(widget.episodeItem.title == urlChange.title &&
urlChange.audioState == AudioState.play)
? ImageRotate(
url: widget.episodeItem.imageUrl,
title: widget.episodeItem.feedTitle,
path: widget.path,
)
: Hero(
tag: widget.episodeItem.enclosureUrl + widget.heroTag,
@ -244,9 +255,8 @@ class _MenuBarState extends State<MenuBar> {
height: 30.0,
width: 30.0,
color: Colors.white,
child: CachedNetworkImage(
imageUrl: widget.episodeItem.imageUrl,
),
child: Image.file(File(
"${widget.path}/${widget.episodeItem.feedTitle}.png")),
),
),
),
@ -298,10 +308,18 @@ class _MenuBarState extends State<MenuBar> {
alignment: Alignment.center,
height: 50.0,
padding: EdgeInsets.symmetric(horizontal: 20.0),
child:Row(
child: Row(
children: <Widget>[
Text('Play Now', style: TextStyle(color: Colors.blue, fontSize: 15, fontWeight: FontWeight.bold,)),
Icon(Icons.play_arrow, color: Colors.blue,),
Text('Play Now',
style: TextStyle(
color: Colors.blue,
fontSize: 15,
fontWeight: FontWeight.bold,
)),
Icon(
Icons.play_arrow,
color: Colors.blue,
),
],
),
),
@ -487,8 +505,9 @@ class _WaveLoaderState extends State<WaveLoader>
}
class ImageRotate extends StatefulWidget {
final String url;
ImageRotate({this.url, Key key}) : super(key: key);
final String title;
final String path;
ImageRotate({this.title, this.path, Key key}) : super(key: key);
@override
_ImageRotateState createState() => _ImageRotateState();
}
@ -541,9 +560,7 @@ class _ImageRotateState extends State<ImageRotate>
height: 30.0,
width: 30.0,
color: Colors.white,
child: CachedNetworkImage(
imageUrl: widget.url,
),
child: Image.file(File("${widget.path}/${widget.title}.png")),
),
),
),
@ -590,35 +607,26 @@ class LoveOpen extends StatefulWidget {
class _LoveOpenState extends State<LoveOpen>
with SingleTickerProviderStateMixin {
Animation _animationA;
Animation _animationB;
AnimationController _controller;
var rect = RelativeRect.fromLTRB(100, 100, 100, 100);
var rectend = RelativeRect.fromLTRB(0, 0, 0, 0);
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: Duration(milliseconds: 500),
duration: Duration(milliseconds: 100),
);
_animationA = Tween(begin: 0.0, end: 1.0).animate(_controller)
..addListener(() {
if (mounted) setState(() {});
});
_animationB =
RelativeRectTween(begin: rect, end: rectend).animate(_controller)
..addListener(() {
if (mounted) setState(() {});
});
_controller.forward();
_controller.addStatusListener((status) {
if (status == AnimationStatus.completed) {
_controller.reset();
}
}
});
}
@ -635,8 +643,8 @@ class _LoveOpenState extends State<LoveOpen>
scale: _animationA,
alignment: Alignment.center,
child: Transform.rotate(
angle: angle,
child: SizedBox(
angle: angle,
child: SizedBox(
height: 5 * scale,
width: 6 * scale,
child: CustomPaint(
@ -658,22 +666,22 @@ class _LoveOpenState extends State<LoveOpen>
children: <Widget>[
Row(
children: <Widget>[
_littleHeart(1.3, 10, -math.pi/6),
_littleHeart(1.5, 3, 0),
_littleHeart(0.5, 10, -math.pi / 6),
_littleHeart(1.2, 3, 0),
],
),
Row(
children: <Widget>[
_littleHeart(0.8, 6, math.pi*1.5),
_littleHeart(1.2, 24, math.pi/2),
_littleHeart(0.8, 6, math.pi * 1.5),
_littleHeart(0.9, 24, math.pi / 2),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
_littleHeart(1, 8,-math.pi*0.7),
_littleHeart(1.3, 8, math.pi),
_littleHeart(1.1, 3, -math.pi*1.2)
_littleHeart(1, 8, -math.pi * 0.7),
_littleHeart(0.8, 8, math.pi),
_littleHeart(0.6, 3, -math.pi * 1.2)
],
),
],

View File

@ -8,8 +8,9 @@ import 'package:path_provider/path_provider.dart';
import 'package:flutter_downloader/flutter_downloader.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:tsacdop/class/downloadstate.dart';
import 'package:tsacdop/class/episodebrief.dart';
import 'package:tsacdop/class/sqflite_localpodcast.dart';
import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
class DownloadButton extends StatefulWidget {
final EpisodeBrief episodeBrief;
@ -150,9 +151,9 @@ class _DownloadButtonState extends State<DownloadButton> {
final tasks = await FlutterDownloader.loadTasks();
_task = _TaskInfo(
name: widget.episodeBrief.title,
link: widget.episodeBrief.enclosureUrl);
name: widget.episodeBrief.title,
link: widget.episodeBrief.enclosureUrl,
);
tasks?.forEach((task) {
if (_task.link == task.url) {
_task.taskId = task.taskId;
@ -203,13 +204,28 @@ class _DownloadButtonState extends State<DownloadButton> {
@override
Widget build(BuildContext context) {
return _downloadButton(_task);
return _isLoading
? Center()
: Row(
children: <Widget>[
_downloadButton(_task),
AnimatedContainer(
duration: Duration(seconds: 1),
decoration: BoxDecoration(
color: Colors.cyan[300],
borderRadius: BorderRadius.all(Radius.circular(15.0))),
height: 20.0,
width:
(_task.status == DownloadTaskStatus.running) ? 50.0 : 0,
alignment: Alignment.center,
child: Text('${_task.progress}%',
style: TextStyle(color: Colors.white))),
],
);
}
Widget _downloadButton(_TaskInfo task) {
if (_isLoading)
return Center();
else if (task.status == DownloadTaskStatus.undefined) {
if (task.status == DownloadTaskStatus.undefined) {
return _buttonOnMenu(
Icon(
Icons.arrow_downward,
@ -217,33 +233,28 @@ class _DownloadButtonState extends State<DownloadButton> {
),
() => _requestDownload(task));
} else if (task.status == DownloadTaskStatus.running) {
return Row(
children: <Widget>[
Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
_pauseDownload(task);
},
child: Container(
height: 50.0,
alignment: Alignment.center,
padding: EdgeInsets.symmetric(horizontal: 18.0),
child: SizedBox(
height: 18,
width: 18,
child: CircularProgressIndicator(
backgroundColor: Colors.grey[500],
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.blue),
value: task.progress / 100,
),
),
return Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
(task.progress > 0) ? _pauseDownload(task) : null;
},
child: Container(
height: 50.0,
alignment: Alignment.center,
padding: EdgeInsets.symmetric(horizontal: 18.0),
child: SizedBox(
height: 18,
width: 18,
child: CircularProgressIndicator(
backgroundColor: Colors.grey[500],
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.cyan[300]),
value: task.progress / 100,
),
),
),
Text('${task.progress}%', style: TextStyle(color: Colors.blue,),),
],
),
);
} else if (task.status == DownloadTaskStatus.paused) {
return Material(

View File

@ -11,9 +11,11 @@ import 'package:image/image.dart' as img;
import 'package:fluttertoast/fluttertoast.dart';
import 'package:tsacdop/class/importompl.dart';
import 'package:tsacdop/class/podcast_group.dart';
import 'package:tsacdop/class/searchpodcast.dart';
import 'package:tsacdop/class/podcastlocal.dart';
import 'package:tsacdop/class/sqflite_localpodcast.dart';
import 'package:tsacdop/class/settingstate.dart';
import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
import 'package:tsacdop/home/home.dart';
import 'package:tsacdop/home/appbar/popupmenu.dart';
import 'package:tsacdop/webfeed/webfeed.dart';
@ -211,7 +213,8 @@ class _SearchResultState extends State<SearchResult> {
@override
Widget build(BuildContext context) {
ImportOmpl importOmpl = Provider.of<ImportOmpl>(context);
var groupList = Provider.of<GroupList>(context);
var _settingState = Provider.of<SettingState>(context);
savePodcast(String rss) async {
print(rss);
if (mounted) setState(() => _adding = true);
@ -240,17 +243,19 @@ class _SearchResultState extends State<SearchResult> {
PodcastLocal podcastLocal = PodcastLocal(
_p.title, _p.itunes.image.href, rss, _primaryColor, _p.author);
podcastLocal.description = _p.description;
var dbHelper = DBHelper();
await dbHelper.savePodcastLocal(podcastLocal);
groupList.subscribe(podcastLocal);
importOmpl.importState = ImportState.parse;
var dbHelper = DBHelper();
await dbHelper.savePodcastRss(_p);
importOmpl.importState = ImportState.complete;
importOmpl.importState = ImportState.stop;
_settingState.subscribeUpdate = Update.backhome;
print('fatch data');
} catch (e) {
importOmpl.importState = ImportState.error;
Fluttertoast.showToast(
msg: 'Network error, Subscribe failed',
gravity: ToastGravity.BOTTOM,

View File

@ -4,6 +4,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:dio/dio.dart';
import 'package:provider/provider.dart';
import 'package:tsacdop/class/settingstate.dart';
import 'package:xml/xml.dart' as xml;
import 'package:file_picker/file_picker.dart';
import 'package:flutter/services.dart';
@ -13,7 +14,7 @@ import 'package:image/image.dart' as img;
import 'about.dart';
import 'package:tsacdop/class/podcastlocal.dart';
import 'package:tsacdop/class/sqflite_localpodcast.dart';
import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
import 'package:tsacdop/class/importompl.dart';
import 'package:tsacdop/webfeed/webfeed.dart';
@ -43,10 +44,11 @@ class PopupMenu extends StatelessWidget {
@override
Widget build(BuildContext context) {
ImportOmpl importOmpl = Provider.of<ImportOmpl>(context);
var _settingState = Provider.of<SettingState>(context);
_refreshAll() async {
var dbHelper = DBHelper();
List<PodcastLocal> podcastList = await dbHelper.getPodcastLocal();
List<PodcastLocal> podcastList =
await dbHelper.getPodcastLocalAll();
await Future.forEach(podcastList, (podcastLocal) async {
importOmpl.rssTitle = podcastLocal.title;
importOmpl.importState = ImportState.parse;
@ -83,12 +85,16 @@ class PopupMenu extends StatelessWidget {
_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(_p);
importOmpl.importState = ImportState.complete;
_settingState.subscribeUpdate = Update.backhome;
} catch (e) {
importOmpl.importState = ImportState.error;
}
@ -110,8 +116,6 @@ class PopupMenu extends StatelessWidget {
print(total[i].text);
}
}
importOmpl.importState = ImportState.complete;
importOmpl.importState = ImportState.stop;
print('Import fisnished');
} catch (e) {
importOmpl.importState = ImportState.error;

View File

@ -15,7 +15,7 @@ import 'package:tsacdop/class/audiostate.dart';
import 'package:tsacdop/class/episodebrief.dart';
import 'package:tsacdop/episodes/episodedetail.dart';
import 'package:tsacdop/home/audiopanel.dart';
import 'package:tsacdop/class/sqflite_localpodcast.dart';
import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
import 'package:tsacdop/util/pageroute.dart';
final Logger _logger = Logger('audiofileplayer');
@ -84,7 +84,9 @@ class _PlayerWidgetState extends State<PlayerWidget> {
_remoteAudioLoading = true;
Provider.of<Urlchange>(context, listen: false).audioState = AudioState.load;
if (_backgroundAudioPlaying == true) _backgroundAudio?.pause();
if (_backgroundAudioPlaying == true)
{ _backgroundAudio?.pause();
AudioSystem.instance.stopBackgroundDisplay();}
_backgroundAudio?.dispose();
_backgroundAudio = Audio.loadFromRemoteUrl(url,
onDuration: (double durationSeconds) {
@ -135,7 +137,9 @@ class _PlayerWidgetState extends State<PlayerWidget> {
_remoteAudioLoading = true;
ByteData audio = getAudio(path);
Provider.of<Urlchange>(context, listen: false).audioState = AudioState.load;
if (_backgroundAudioPlaying == true) _backgroundAudio?.pause();
if (_backgroundAudioPlaying == true)
{_backgroundAudio?.pause();
AudioSystem.instance.stopBackgroundDisplay();}
_backgroundAudio?.dispose();
_backgroundAudio = Audio.loadFromByteData(audio,
onDuration: (double durationSeconds) {

View File

@ -1,13 +1,10 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:tsacdop/podcasts/podcastlist.dart';
import 'hometab.dart';
import 'package:tsacdop/home/appbar/importompl.dart';
import 'package:tsacdop/home/audio_player.dart';
import 'homescroll.dart';
import 'package:tsacdop/util/pageroute.dart';
import 'package:tsacdop/podcasts/podcastmanage.dart';
class Home extends StatelessWidget {
@ -19,28 +16,6 @@ class Home extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Import(),
Container(
height: 30,
padding: EdgeInsets.symmetric(horizontal: 15),
alignment: Alignment.bottomRight,
child: GestureDetector(
onTap: () {
Navigator.push(
context,
SlideLeftRoute(page: PodcastManage()),
);
},
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(),

View File

@ -6,15 +6,17 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:path_provider/path_provider.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:tsacdop/class/episodebrief.dart';
import 'package:tsacdop/class/podcast_group.dart';
import 'package:tsacdop/class/podcastlocal.dart';
import 'package:tsacdop/class/importompl.dart';
import 'package:tsacdop/class/sqflite_localpodcast.dart';
import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
import 'package:tsacdop/episodes/episodedetail.dart';
import 'package:tsacdop/podcasts/podcastdetail.dart';
import 'package:tsacdop/podcasts/podcastlist.dart';
import 'package:tsacdop/podcasts/podcastmanage.dart';
import 'package:tsacdop/util/pageroute.dart';
import 'package:tsacdop/class/settingstate.dart';
@ -25,101 +27,184 @@ class ScrollPodcasts extends StatefulWidget {
class _ScrollPodcastsState extends State<ScrollPodcasts> {
var dir;
bool _loading;
List<PodcastLocal> podcastList;
getPodcastLocal() async {
var dbHelper = DBHelper();
podcastList = await dbHelper.getPodcastLocal();
dir = await getApplicationDocumentsDirectory();
setState(() {
_loading = true;
});
}
int _groupIndex;
bool _loaded;
ImportState importState;
Setting subscribeUpdate;
Update subscribeUpdate;
@override
void didChangeDependencies() {
super.didChangeDependencies();
final importState = Provider.of<ImportOmpl>(context).importState;
final subscribeUpdate = Provider.of<SettingState>(context).subscribeupdate;
if (importState == ImportState.complete ||
subscribeUpdate == Setting.start) {
subscribeUpdate = Provider.of<SettingState>(context).subscribeupdate;
if (subscribeUpdate == Update.backhome) {
setState(() {
getPodcastLocal();
_groupIndex = 0;
});
} else if (subscribeUpdate == Update.justupdate) {
setState(() {});
}
}
@override
void initState() {
super.initState();
_loading = false;
getPodcastLocal();
_loaded = false;
_groupIndex = 0;
getApplicationDocumentsDirectory().then((value) {
dir = value.path;
setState(() => _loaded = true);
});
}
@override
Widget build(BuildContext context) {
double _width = MediaQuery.of(context).size.width;
return !_loading
? Container(
height: (_width - 20) / 3 + 110,
)
: DefaultTabController(
length: podcastList.length,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(
height: 70,
alignment: Alignment.centerLeft,
child: TabBar(
labelPadding: EdgeInsets.only(
top: 5.0, bottom: 10.0, left: 6.0, right: 6.0),
indicator:
CircleTabIndicator(color: Colors.blue, radius: 3),
isScrollable: true,
tabs: podcastList.map<Tab>((PodcastLocal podcastLocal) {
return Tab(
child: ClipRRect(
borderRadius: BorderRadius.all(Radius.circular(25.0)),
child: LimitedBox(
maxHeight: 50,
maxWidth: 50,
child: Image.file(
File("${dir.path}/${podcastLocal.title}.png")),
return Consumer<GroupList>(builder: (_, groupList, __) {
var groups = groupList.groups;
bool isLoading = groupList.isLoading;
return isLoading
? Container(
height: (_width - 20) / 3 + 110,
child: SizedBox(
height: 20.0,
width: 20.0,
child: CircularProgressIndicator()),
)
: DefaultTabController(
length: groups[_groupIndex].podcastList.length,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
GestureDetector(
onVerticalDragEnd: (event) {
if (event.primaryVelocity > 200) {
if (groups.length == 1) {
Fluttertoast.showToast(
msg: 'Add some groups',
gravity: ToastGravity.BOTTOM,
);
} else {
if (mounted)
setState(() {
(_groupIndex != 0)
? _groupIndex--
: _groupIndex = groups.length - 1;
});
}
} else if (event.primaryVelocity < -200) {
if (groups.length == 1) {
Fluttertoast.showToast(
msg: 'Add some groups',
gravity: ToastGravity.BOTTOM,
);
} else {
setState(() {
(_groupIndex < groups.length - 1)
? _groupIndex++
: _groupIndex = 0;
});
}
}
},
child: Column(
children: <Widget>[
Container(
child: Row(
children: <Widget>[
Container(
padding:
EdgeInsets.symmetric(horizontal: 15.0),
child: Text(
groups[_groupIndex].name,
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.red[300]),
)),
Spacer(),
Container(
height: 30,
padding: EdgeInsets.symmetric(horizontal: 15),
alignment: Alignment.bottomRight,
child: InkWell(
onTap: () {
Navigator.push(
context,
SlideLeftRoute(page: PodcastManage()),
);
},
child: Container(
height: 30,
padding: EdgeInsets.all(5.0),
child: Text('See All',
style: TextStyle(
color: Colors.red[300],
fontWeight: FontWeight.bold,
)),
),
),
),
],
),
),
);
}).toList(),
),
),
Container(
height: (_width - 20) / 3 + 40,
margin: EdgeInsets.only(left: 10, right: 10),
decoration: BoxDecoration(
color: Colors.white,
),
child: TabBarView(
children:
podcastList.map<Widget>((PodcastLocal podcastLocal) {
return Container(
decoration: BoxDecoration(color: Colors.grey[100]),
margin: EdgeInsets.symmetric(horizontal: 5.0),
key: ObjectKey(podcastLocal.title),
child: PodcastPreview(
podcastLocal: podcastLocal,
Container(
color: Colors.white10,
height: 70,
width: _width,
alignment: Alignment.centerLeft,
child: TabBar(
labelPadding: EdgeInsets.only(
top: 5.0, bottom: 10.0, left: 6.0, right: 6.0),
indicator: CircleTabIndicator(
color: Colors.blue, radius: 3),
isScrollable: true,
tabs: groups[_groupIndex].podcasts
.map<Tab>((PodcastLocal podcastLocal) {
return Tab(
child: ClipRRect(
borderRadius:
BorderRadius.all(Radius.circular(25.0)),
child: LimitedBox(
maxHeight: 50,
maxWidth: 50,
child: !_loaded
? CircularProgressIndicator()
: Image.file(File(
"$dir/${podcastLocal.title}.png")),
),
),
);
}).toList(),
),
),
);
}).toList(),
],
),
),
),
],
),
);
Container(
height: (_width - 20) / 3 + 40,
margin: EdgeInsets.only(left: 10, right: 10),
decoration: BoxDecoration(
color: Colors.white,
),
child: TabBarView(
children: groups[_groupIndex].podcasts
.map<Widget>((PodcastLocal podcastLocal) {
return Container(
decoration: BoxDecoration(color: Colors.grey[100]),
margin: EdgeInsets.symmetric(horizontal: 5.0),
key: ObjectKey(podcastLocal.title),
child: PodcastPreview(
podcastLocal: podcastLocal,
),
);
}).toList(),
),
),
],
),
);
});
}
}
@ -250,6 +335,7 @@ class ShowEpisode extends StatelessWidget {
episodeItem: podcast[index],
heroTag: 'scroll',
//unique hero tag
path: path,
)),
);
},

View File

@ -2,7 +2,7 @@ import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:tsacdop/class/episodebrief.dart';
import 'package:tsacdop/class/sqflite_localpodcast.dart';
import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
import 'package:tsacdop/util/episodegrid.dart';
class MainTab extends StatefulWidget {

View File

@ -0,0 +1,34 @@
import 'dart:async';
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:tsacdop/class/podcast_group.dart';
class KeyValueStorage {
final String key;
KeyValueStorage(this.key);
Future<List<GroupEntity>> getGroups() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
if (prefs.getString(key) == null) {
PodcastGroup home = PodcastGroup('Home');
await prefs.setString(
key,
json.encode({
'groups': [home.toEntity().toJson()]
}));}
print(prefs.getString(key));
return json
.decode(prefs.getString(key))['groups']
.cast<Map<String, Object>>()
.map<GroupEntity>(GroupEntity.fromJson)
.toList(growable: false);
}
Future<bool> saveGroup(List<GroupEntity> groupList) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
return prefs.setString(
key,
json.encode(
{'groups': groupList.map((group) => group.toJson()).toList()}));
}
}

View File

@ -5,8 +5,8 @@ import 'package:path/path.dart';
import 'package:intl/intl.dart';
import 'package:flutter_downloader/flutter_downloader.dart';
import 'package:path_provider/path_provider.dart';
import 'podcastlocal.dart';
import 'episodebrief.dart';
import 'package:tsacdop/class/podcastlocal.dart';
import 'package:tsacdop/class/episodebrief.dart';
import 'package:tsacdop/webfeed/webfeed.dart';
class DBHelper {
@ -25,72 +25,72 @@ class DBHelper {
}
void _onCreate(Database db, int version) async {
await db.execute(
"""CREATE TABLE PodcastLocal(id INTEGER PRIMARY KEY,title TEXT,
await db
.execute("""CREATE TABLE PodcastLocal(id TEXT PRIMARY KEY,title TEXT,
imageUrl TEXT,rssUrl TEXT UNIQUE,primaryColor TEXT,author TEXT,
description TEXT, add_date INTEGER, order_id INTEGER default 0)""");
description TEXT, add_date INTEGER, order_id INTEGER DEFAULT 0)""");
await db
.execute("""CREATE TABLE Episodes(id INTEGER PRIMARY KEY,title TEXT,
enclosure_url TEXT UNIQUE, enclosure_length INTEGER, pubDate TEXT,
description TEXT, feed_title TEXT, feed_link TEXT, milliseconds INTEGER,
duration INTEGER DEFAULT 0, explicit INTEGER DEFAULT 0, liked INTEGER DEFAULT 0,
downloaded TEXT DEFAULT 'ND', download_date INTEGER DEFAULT 0)""");
await db.execute(
"""CREATE TABLE Setting(id INTEGER PRIMARY KEY, setting TEXT, setting_value INTEGER DEFAULT 0)""");
await db
.execute("""INSERT INTO Setting (setting) VALUES('podcasts_order') """);
}
Future<List<PodcastLocal>> getPodcastLocal() async {
Future<List<PodcastLocal>> getPodcastLocal(
List<String> podcasts, int podcastsOrder) async {
var dbClient = await database;
//query podcasts order setting
List<Map> setting = await dbClient.rawQuery("SELECT setting_value FROM Setting WHERE setting = 'podcasts_order'");
int podcastsOrder = setting.first['setting_value'];
List<Map> list;
if (podcastsOrder == 0)
{ list = await dbClient.rawQuery(
List<PodcastLocal> podcastLocal = List();
await Future.forEach(podcasts, (s) async {
List<Map> list;
if (podcastsOrder == 0) {
list = await dbClient.rawQuery(
'SELECT id, title, imageUrl, rssUrl, primaryColor, author FROM PodcastLocal WHERE id = ? ORDER BY add_date DESC',
[s]);
} else if (podcastsOrder == 1) {
list = await dbClient.rawQuery(
'SELECT id, title, imageUrl, rssUrl, primaryColor, author FROM PodcastLocal WHERE id = ? ORDER BY add_date',
[s]);
} else if (podcastsOrder == 2) {
list = await dbClient.rawQuery(
'SELECT id, title, imageUrl, rssUrl, primaryColor, author FROM PodcastLocal WHERE id = ? ORDER BY order_id , add_date DESC',
[s]);
print('Get podcasts list Ordered by 2');
}
podcastLocal.add(PodcastLocal(
list.first['title'],
list.first['imageUrl'],
list.first['rssUrl'],
list.first['primaryColor'],
list.first['author'],
id: list.first['id']));
});
return podcastLocal;
}
Future<List<PodcastLocal>> getPodcastLocalAll() async {
var dbClient = await database;
List<Map> list = await dbClient.rawQuery(
'SELECT title, imageUrl, rssUrl, primaryColor, author FROM PodcastLocal ORDER BY add_date DESC');
print('Get podcasts list Ordered by 0');}
else if (podcastsOrder == 1)
{ list = await dbClient.rawQuery(
'SELECT title, imageUrl, rssUrl, primaryColor, author FROM PodcastLocal ORDER BY add_date');}
else if (podcastsOrder ==2)
{ list = await dbClient.rawQuery(
'SELECT title, imageUrl, rssUrl, primaryColor, author FROM PodcastLocal ORDER BY order_id');
print('Get podcasts list Ordered by 2');}
List<PodcastLocal> podcastLocal = List();
for (int i = 0; i < list.length; i++) {
podcastLocal.add(PodcastLocal(
list[i]['title'],
list[i]['imageUrl'],
list[i]['rssUrl'],
list[i]['primaryColor'],
list[i]['author'],
));
podcastLocal.add(PodcastLocal(list[i]['title'], list[i]['imageUrl'],
list[i]['rssUrl'], list[i]['primaryColor'], list[i]['author'],
id: list[i]['id']));
}
return podcastLocal;
}
//save podcast order adter user save
//save podcast order adter user save
saveOrder(List<PodcastLocal> podcastList) async {
var dbClient = await database;
for (int i = 0; i < podcastList.length; i++){
for (int i = 0; i < podcastList.length; i++) {
await dbClient.rawUpdate(
"UPDATE OR IGNORE PodcastLocal SET order_id = ? WHERE title = ?",
[i, podcastList[i].title]);
print(podcastList[i].title);
}
await dbClient.rawUpdate(
"UPDATE OR IGNORE Setting SET setting_value = 2 WHERE setting = 'podcasts_order' ");
print('Changed order');
}
updateOrderSetting(int value) async{
var dbClient = await database;
await dbClient.rawUpdate(
"UPDATE OR IGNORE Setting SET setting_value = ? WHERE setting = 'podcasts_order'",[value]);
}
Future savePodcastLocal(PodcastLocal podcastLocal) async {
@ -99,9 +99,10 @@ class DBHelper {
var dbClient = await database;
await dbClient.transaction((txn) async {
return await txn.rawInsert(
"""INSERT OR IGNORE INTO PodcastLocal (title, imageUrl, rssUrl,
primaryColor, author, description, add_date) VALUES(?, ?, ?, ?, ?, ?, ?)""",
"""INSERT OR IGNORE INTO PodcastLocal (id, title, imageUrl, rssUrl,
primaryColor, author, description, add_date) VALUES(?, ?, ?, ?, ?, ?, ?, ?)""",
[
podcastLocal.id,
podcastLocal.title,
podcastLocal.imageUrl,
podcastLocal.rssUrl,
@ -188,22 +189,22 @@ class DBHelper {
} else {
for (int i = 0; i < (_result - _count); i++) {
print(_p.items[i].title);
_p.items[i].itunes.title != null
? _title = _p.items[i].itunes.title
: _title = _p.items[i].title;
_p.items[i].itunes.summary != null
_title = _p.items[i].itunes.title ?? _p.items[i].title;
_p.items[i].itunes.summary.contains('<')
? _description = _p.items[i].itunes.summary
: _description = _p.items[i].description;
isXimalaya(_p.items[i].enclosure.url)
? _url = _p.items[i].enclosure.url.split('=').last
: _url = _p.items[i].enclosure.url;
final _length = _p.items[i].enclosure.length;
final _pubDate = _p.items[i].pubDate;
final _date = _parsePubDate(_pubDate);
final _milliseconds = _date.millisecondsSinceEpoch;
(_p.items[i].itunes.duration != null)
? _duration = _p.items[i].itunes.duration.inMinutes
: _duration = 0;
_duration = _p.items[i].itunes.duration?.inMinutes ?? 0;
final _explicit = getExplicit(_p.items[i].itunes.explicit);
if (_p.items[i].enclosure.url != null) {
await dbClient.transaction((txn) {
@ -384,7 +385,6 @@ class DBHelper {
int count = await dbClient.rawUpdate(
"UPDATE Episodes SET downloaded = ?, download_date = ? WHERE enclosure_url = ?",
[id, _milliseconds, url]);
print('Downloaded ' + url);
return count;
}

View File

@ -1,8 +1,12 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_statusbarcolor/flutter_statusbarcolor.dart';
import 'package:provider/provider.dart';
import 'package:flutter/services.dart';
import 'package:flutter_downloader/flutter_downloader.dart';
import 'package:path_provider/path_provider.dart';
import 'package:tsacdop/class/podcast_group.dart';
import 'package:tsacdop/home/appbar/addpodcast.dart';
import 'package:tsacdop/class/audiostate.dart';
import 'package:tsacdop/class/importompl.dart';
@ -15,6 +19,7 @@ void main() async {
ChangeNotifierProvider(create: (context) => Urlchange()),
ChangeNotifierProvider(create: (context) => ImportOmpl()),
ChangeNotifierProvider(create: (context) => SettingState()),
ChangeNotifierProvider(create: (context) => GroupList()),
],
child: MyApp(),
),
@ -23,22 +28,13 @@ void main() async {
await FlutterDownloader.initialize();
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
await FlutterStatusbarcolor.setStatusBarColor(Colors.grey[100]);
if (useWhiteForeground(Colors.grey[100])) {
FlutterStatusbarcolor.setStatusBarWhiteForeground(true);
} else {
FlutterStatusbarcolor.setStatusBarWhiteForeground(false);
}
await FlutterStatusbarcolor.setNavigationBarColor(Colors.grey[100]);
if (useWhiteForeground(Colors.grey[100])) {
FlutterStatusbarcolor.setNavigationBarWhiteForeground(true);
} else {
FlutterStatusbarcolor.setNavigationBarWhiteForeground(false);
}
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'TsacDop',

View File

@ -5,7 +5,7 @@ import 'dart:async';
import 'package:tsacdop/class/podcastlocal.dart';
import 'package:tsacdop/class/episodebrief.dart';
import 'package:tsacdop/class/sqflite_localpodcast.dart';
import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
import 'package:tsacdop/util/episodegrid.dart';
import 'package:tsacdop/webfeed/webfeed.dart';

View File

@ -0,0 +1,398 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:path_provider/path_provider.dart';
import 'package:provider/provider.dart';
import 'package:tsacdop/class/podcast_group.dart';
import 'package:tsacdop/class/podcastlocal.dart';
import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
import 'package:tsacdop/class/settingstate.dart';
class PodcastGroupList extends StatefulWidget {
final PodcastGroup group;
PodcastGroupList({this.group, Key key}) : super(key: key);
@override
_PodcastGroupListState createState() => _PodcastGroupListState();
}
class _PodcastGroupListState extends State<PodcastGroupList> {
bool _loading;
bool _loadSave;
String dir;
@override
void initState() {
super.initState();
_loading = false;
_loadSave = false;
getApplicationDocumentsDirectory().then((value) {
dir = value.path;
setState(() {
_loading = true;
});
});
}
Widget _saveButton(BuildContext context) {
var _settingState = Provider.of<SettingState>(context);
_saveOrder(List<PodcastLocal> podcastList) async {
var dbHelper = DBHelper();
await dbHelper.saveOrder(podcastList);
}
var podcastList = widget.group.podcasts;
return Container(
child: InkWell(
child: AnimatedContainer(
duration: Duration(milliseconds: 800),
width: _loadSave ? 70 : 0,
height: 40,
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.all(Radius.circular(5.0))),
alignment: Alignment.center,
child: Text(
'Save',
style: TextStyle(color: Colors.white),
maxLines: 1,
)),
onTap: () async {
await _saveOrder(podcastList);
Fluttertoast.showToast(
msg: 'Setting Saved',
gravity: ToastGravity.BOTTOM,
);
_settingState.subscribeUpdate = Update.justupdate;
setState(() {
_loadSave = false;
});
},
),
);
}
@override
Widget build(BuildContext context) {
return widget.group.podcastList.length == 0
? Center()
: Container(
color: Colors.grey[100],
child: Stack(
children: <Widget>[
ReorderableListView(
onReorder: (int oldIndex, int newIndex) {
setState(() {
if (newIndex > oldIndex) {
newIndex -= 1;
}
final PodcastLocal podcast =
widget.group.podcasts.removeAt(oldIndex);
widget.group.podcasts.insert(newIndex, podcast);
_loadSave = true;
});
},
children:
widget.group.podcasts.map<Widget>((PodcastLocal podcastLocal) {
return Container(
decoration: BoxDecoration(color: Colors.grey[100]),
key: ObjectKey(podcastLocal.title),
child: !_loading
? CircularProgressIndicator()
: PodcastCard(
path: dir,
podcastLocal: podcastLocal,
group: widget.group.name,
),
);
}).toList(),
),
AnimatedPositioned(
duration: Duration(seconds: 1),
bottom: 30,
right: _loadSave ? 50 : 0,
child: Center(),
//_saveButton(context),
),
],
),
);
}
}
class PodcastCard extends StatefulWidget {
final PodcastLocal podcastLocal;
final String path;
final String group;
PodcastCard({this.podcastLocal, this.path, this.group, Key key})
: super(key: key);
@override
_PodcastCardState createState() => _PodcastCardState();
}
class _PodcastCardState extends State<PodcastCard> {
bool _loadMenu;
bool _remove;
bool _addGroup;
bool _loadGroup;
List<String> _selectedGroups;
List<String> _belongGroups;
Color _c;
_unSubscribe(String title) async {
var dbHelper = DBHelper();
await dbHelper.delPodcastLocal(title);
}
@override
void initState() {
super.initState();
_loadMenu = false;
_remove = false;
_addGroup = false;
_loadGroup = false;
}
Widget _buttonOnMenu(Widget widget, VoidCallback onTap) => Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
child: Container(
height: 50.0,
padding: EdgeInsets.symmetric(horizontal: 15.0),
child: widget),
),
);
@override
Widget build(BuildContext context) {
var color = json.decode(widget.podcastLocal.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], 1.0);
double _width = MediaQuery.of(context).size.width;
var _settingState = Provider.of<SettingState>(context);
var _groupList = Provider.of<GroupList>(context);
_selectedGroups = _groupList.groups.map((e) => e.name).toList();
_belongGroups = _groupList
.getPodcastGroup(widget.podcastLocal.id)
.map((e) => e.name)
.toList();
return _remove
? Center()
: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
InkWell(
onTap: () => setState(() => _loadMenu = !_loadMenu),
child: Container(
padding: EdgeInsets.symmetric(horizontal: 12),
height: 100,
child: Row(children: <Widget>[
Container(
child: Icon(
Icons.unfold_more,
color: _c,
),
),
Container(
child: ClipRRect(
borderRadius: BorderRadius.all(Radius.circular(30)),
child: Container(
height: 60,
width: 60,
child: Image.file(File(
"${widget.path}/${widget.podcastLocal.title}.png")),
),
),
),
Container(
width: _width / 2,
padding: EdgeInsets.symmetric(horizontal: 10),
alignment: Alignment.centerLeft,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(
alignment: Alignment.centerLeft,
child: Text(
widget.podcastLocal.title,
maxLines: 2,
overflow: TextOverflow.fade,
style: TextStyle(
fontWeight: FontWeight.bold, fontSize: 15),
),
),
!_loadGroup
? Center()
: Row(
children: _belongGroups.map((group) {
return Container(
padding: EdgeInsets.only(right: 5.0),
child: Text(group));
}).toList(),
),
],
)),
Spacer(),
Icon(_loadMenu
? Icons.keyboard_arrow_up
: Icons.keyboard_arrow_down),
Padding(
padding: EdgeInsets.symmetric(horizontal: 5.0),
),
OutlineButton(
child: Text('Remove'),
onPressed: () {
showDialog(
context: context,
child: AlertDialog(
title: Text('Remove confirm'),
content: Text(
'${widget.podcastLocal.title} will be removed from device.'),
actions: <Widget>[
FlatButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('CANCEL'),
),
FlatButton(
onPressed: () {
_unSubscribe(widget.podcastLocal.title);
_settingState.subscribeUpdate =
Update.justupdate;
Navigator.of(context).pop();
},
child: Text(
'CONFIRM',
style: TextStyle(color: Colors.red),
),
)
],
));
},
),
]),
),
),
!_loadMenu
? Center()
: Container(
child: Container(
decoration: BoxDecoration(
color: Colors.grey[100],
border: Border(
bottom: BorderSide(color: Colors.grey[300]),
top: BorderSide(color: Colors.grey[300]))),
height: 50,
child: _addGroup
? Row(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Expanded(
flex: 4,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Consumer<GroupList>(
builder: (_, groupList, __) => Row(
children: groupList.groups
.map<Widget>(
(PodcastGroup group) {
return Container(
padding: EdgeInsets.only(left: 5.0),
child: FilterChip(
key: ValueKey<String>(group.name),
label: Text(group.name),
selected: _belongGroups
.contains(group.name) &&
_selectedGroups
.contains(group.name),
onSelected: (bool value) {
setState(() {
if (!value) {
_selectedGroups
.remove(group.name);
} else {
_selectedGroups
.add(group.name);
}
});
},
),
);
}).toList()),
),
),
),
Expanded(
flex: 1,
child: Row(
children: <Widget>[
IconButton(
icon: Icon(Icons.clear),
onPressed: () => setState(() {
_addGroup = false;
}),
),
IconButton(
onPressed: () async {
print(_selectedGroups);
if (_selectedGroups.length > 0) {
setState(() {
_addGroup = false;
});
await _groupList.changeGroup(
widget.podcastLocal.id,
_selectedGroups,
);
_settingState.subscribeUpdate =
Update.justupdate;
Fluttertoast.showToast(
msg: 'Setting Saved',
gravity: ToastGravity.BOTTOM,
);
if (!_selectedGroups
.contains(widget.group)) {
print(widget.group);
setState(() {
_remove = true;
});
}
} else
Fluttertoast.showToast(
msg:
'At least select one group',
gravity: ToastGravity.BOTTOM,
);
},
icon: Icon(Icons.done),
),
],
),
)
],
)
: Row(
mainAxisAlignment:
MainAxisAlignment.spaceEvenly,
children: <Widget>[
_buttonOnMenu(Icon(Icons.fullscreen), () {}),
_buttonOnMenu(Icon(Icons.add), () {
setState(() {
_addGroup = true;
});
}),
_buttonOnMenu(
Icon(Icons.notifications), () {})
],
),
),
),
],
);
}
}

View File

@ -6,9 +6,11 @@ import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:path_provider/path_provider.dart';
import 'package:provider/provider.dart';
import 'package:tsacdop/class/podcastlocal.dart';
import 'package:tsacdop/class/sqflite_localpodcast.dart';
import 'package:tsacdop/class/settingstate.dart';
import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
import 'package:tsacdop/podcasts/podcastdetail.dart';
import 'package:tsacdop/util/pageroute.dart';
@ -48,12 +50,14 @@ class _AboutPodcastState extends State<AboutPodcast> {
@override
Widget build(BuildContext context) {
var _settingState = Provider.of<SettingState>(context);
return AlertDialog(
actions: <Widget>[
FlatButton(
padding: EdgeInsets.all(10.0),
onPressed: () {
_unSubscribe(widget.podcastLocal.title);
_settingState.subscribeUpdate = Update.justupdate;
Navigator.of(context).pop();
},
color: Colors.grey[200],
@ -88,95 +92,104 @@ class PodcastList extends StatefulWidget {
class _PodcastListState extends State<PodcastList> {
var dir;
Future<List<PodcastLocal>> getPodcastLocal() async {
dir = await getApplicationDocumentsDirectory();
var dbHelper = DBHelper();
var podcastList = await dbHelper.getPodcastLocal();
var podcastList = await dbHelper.getPodcastLocalAll();
return podcastList;
}
@override
Widget build(BuildContext context) {
double _width = MediaQuery.of(context).size.width;
return Container(
color: Colors.grey[100],
child: FutureBuilder<List<PodcastLocal>>(
future: getPodcastLocal(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return CustomScrollView(
primary: false,
slivers: <Widget>[
SliverPadding(
padding: const EdgeInsets.all(10.0),
sliver: SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
childAspectRatio: 0.8,
crossAxisCount: 3,
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return InkWell(
onTap: () {
Navigator.push(
context,
ScaleRoute(
page: PodcastDetail(
podcastLocal: snapshot.data[index],
)),
);
},
onLongPress: () {
showDialog(
context: context,
builder: (BuildContext context) => AboutPodcast(
podcastLocal: snapshot.data[index]),
).then((_) => setState(() {}));
},
child: Container(
alignment: Alignment.center,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Container(
height: 10.0,
),
ClipRRect(
borderRadius:
BorderRadius.all(Radius.circular(_width/8)),
child: Container(
height: _width/4,
width: _width/4,
child: Image.file(File(
"${dir.path}/${snapshot.data[index].title}.png")),
return Scaffold(
appBar: AppBar(
title: Text('Podcasts'),
centerTitle: true,
backgroundColor: Colors.grey[100],
elevation: 0,
),
body: Container(
color: Colors.grey[100],
child: FutureBuilder<List<PodcastLocal>>(
future: getPodcastLocal(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return CustomScrollView(
primary: false,
slivers: <Widget>[
SliverPadding(
padding: const EdgeInsets.all(10.0),
sliver: SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
childAspectRatio: 0.8,
crossAxisCount: 3,
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return InkWell(
onTap: () {
Navigator.push(
context,
ScaleRoute(
page: PodcastDetail(
podcastLocal: snapshot.data[index],
)),
);
},
onLongPress: () {
showDialog(
context: context,
builder: (BuildContext context) => AboutPodcast(
podcastLocal: snapshot.data[index]),
).then((_) => setState(() {}));
},
child: Container(
alignment: Alignment.center,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Container(
height: 10.0,
),
),
Container(
padding: EdgeInsets.all(4.0),
child: Text(
snapshot.data[index].title,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16.0,
color: Colors.black.withOpacity(0.5),
ClipRRect(
borderRadius: BorderRadius.all(
Radius.circular(_width / 8)),
child: Container(
height: _width / 4,
width: _width / 4,
child: Image.file(File(
"${dir.path}/${snapshot.data[index].title}.png")),
),
maxLines: 2,
),
),
],
Container(
padding: EdgeInsets.all(4.0),
child: Text(
snapshot.data[index].title,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16.0,
color: Colors.black.withOpacity(0.5),
),
maxLines: 2,
),
),
],
),
),
),
);
},
childCount: snapshot.data.length,
);
},
childCount: snapshot.data.length,
),
),
),
),
],
);
}
return Text('NoData');
},
],
);
}
return Text('NoData');
},
),
),
);
}

View File

@ -1,13 +1,9 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:path_provider/path_provider.dart';
import 'package:provider/provider.dart';
import 'package:tsacdop/class/podcastlocal.dart';
import 'package:tsacdop/class/sqflite_localpodcast.dart';
import 'package:tsacdop/class/settingstate.dart';
import 'package:tsacdop/class/podcast_group.dart';
import 'package:tsacdop/podcasts/podcastgroup.dart';
import 'package:tsacdop/podcasts/podcastlist.dart';
import 'package:tsacdop/util/pageroute.dart';
class PodcastManage extends StatefulWidget {
@override
@ -15,152 +11,208 @@ class PodcastManage extends StatefulWidget {
}
class _PodcastManageState extends State<PodcastManage> {
var dir;
bool _loading;
bool _loadSave;
Color _c;
double _width;
List<PodcastLocal> podcastList;
getPodcastLocal() async {
dir = await getApplicationDocumentsDirectory();
var dbHelper = DBHelper();
podcastList = await dbHelper.getPodcastLocal();
setState(() {
_loading = true;
});
}
_unSubscribe(String title) async {
var dbHelper = DBHelper();
await dbHelper.delPodcastLocal(title);
print('Unsubscribe');
Decoration getIndicator() {
return const UnderlineTabIndicator(
borderSide: BorderSide(color: Colors.red, width: 2),
insets: EdgeInsets.only(
top: 10.0,
));
}
@override
void initState() {
super.initState();
_loading = false;
_loadSave = false;
getPodcastLocal();
}
void _onReorder(int oldIndex, int newIndex) {
setState(() {
if (newIndex > oldIndex) {
newIndex -= 1;
}
final PodcastLocal podcast = podcastList.removeAt(oldIndex);
podcastList.insert(newIndex, podcast);
_loadSave = true;
});
}
_saveOrder(List<PodcastLocal> podcastList) async {
var dbHelper = DBHelper();
await dbHelper.saveOrder(podcastList);
}
Widget _podcastCard(BuildContext context, PodcastLocal podcastLocal) {
var _settingState = Provider.of<SettingState>(context);
return Container(
padding: EdgeInsets.symmetric(horizontal: 12),
height: 100,
child: Row(children: <Widget>[
Container(
child: Icon(
Icons.unfold_more,
color: _c,
),
),
Container(
child: ClipRRect(
borderRadius: BorderRadius.all(Radius.circular(30)),
child: Container(
height: 60,
width: 60,
child: Image.file(File("${dir.path}/${podcastLocal.title}.png")),
),
),
),
Container(
width: _width / 2,
padding: EdgeInsets.symmetric(horizontal: 10),
child: Text(
podcastLocal.title,
maxLines: 2,
overflow: TextOverflow.fade,
)),
Spacer(),
OutlineButton(
child: Text('Unsubscribe'),
onPressed: () {
_unSubscribe(podcastLocal.title);
_settingState.subscribeUpdate = Setting.start;
setState(() {
getPodcastLocal();
});
},
),
]),
);
}
@override
Widget build(BuildContext context) {
_width = MediaQuery.of(context).size.width;
var _settingState = Provider.of<SettingState>(context);
return Scaffold(
appBar: AppBar(
title: Text('Podcasts'),
backgroundColor: Colors.grey[100],
elevation: 0,
centerTitle: true,
backgroundColor: Colors.grey[100],
title: Text('Groups'),
actions: <Widget>[
!_loadSave
? Center()
: InkWell(
child: Container(
padding: EdgeInsets.all(20.0),
alignment: Alignment.center,
child: Text('Save')),
onTap: () async{
await _saveOrder(podcastList);
Fluttertoast.showToast(
msg: 'Saved',
gravity: ToastGravity.BOTTOM,
);
_settingState.subscribeUpdate = Setting.start;
setState(() {
_loadSave = false;
});
},
),
IconButton(
icon: Icon(Icons.menu),
onPressed: () {},
),
OrderMenu(),
],
),
body: Container(
color: Colors.grey[100],
child: !_loading
? CircularProgressIndicator()
: ReorderableListView(
onReorder: _onReorder,
children: podcastList.map<Widget>((PodcastLocal podcastLocal) {
var color = json.decode(podcastLocal.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], 1.0);
return Container(
decoration: BoxDecoration(color: Colors.grey[100]),
margin: EdgeInsets.symmetric(horizontal: 5.0),
key: ObjectKey(podcastLocal.title),
child: _podcastCard(context, podcastLocal),
);
}).toList(),
body: Consumer<GroupList>(builder: (_, groupList, __) {
bool _isLoading = groupList.isLoading;
List<PodcastGroup> _groups = groupList.groups;
return _isLoading
? Center()
: DefaultTabController(
length: _groups.length,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Row(
children: <Widget>[
Expanded(
child: Container(
height: 50,
padding: EdgeInsets.symmetric(horizontal: 10.0),
alignment: Alignment.centerLeft,
child: TabBar(
labelPadding: EdgeInsets.only(
top: 5.0,
bottom: 6.0,
left: 10.0,
right: 10.0),
indicator: getIndicator(),
isScrollable: true,
tabs: _groups.map<Tab>((group) {
return Tab(
child: Text(group.name),
);
}).toList(),
),
),
),
Container(
child: FlatButton(
onPressed: () => showDialog(
context: context,
builder: (BuildContext context) =>
AddGroup()),
child: Icon(Icons.add)),
),
],
),
Expanded(
child: Container(
child: TabBarView(
children: _groups.map<Widget>((group) {
return Container(
key: ObjectKey(group),
child: PodcastGroupList(group: group));
}).toList(),),
),
)
],
));
}),
);
}
}
class OrderMenu extends StatelessWidget {
@override
Widget build(BuildContext context) {
return PopupMenuButton(
elevation: 3,
tooltip: 'Menu',
itemBuilder: (context) => [
PopupMenuItem(
value: 1,
child: Text('All Podcasts'),
),
PopupMenuItem(
value: 2,
child: Text('New group'),
)
],
onSelected: (value) {
if (value == 1) {
Navigator.push(context, ScaleRoute(page: PodcastList()));
}
if (value == 2) {
showDialog(
context: context, builder: (BuildContext context) => AddGroup());
}
},
);
}
}
class AddGroup extends StatefulWidget {
@override
_AddGroupState createState() => _AddGroupState();
}
class _AddGroupState extends State<AddGroup> {
TextEditingController _controller;
String _newGroup;
int _error;
@override
void initState() {
super.initState();
_error = 0;
_controller = TextEditingController();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
var groupList = Provider.of<GroupList>(context);
List list = groupList.groups.map((e) => e.name).toList();
return AlertDialog(
elevation: 1,
contentPadding: EdgeInsets.symmetric(horizontal: 20),
titlePadding: EdgeInsets.only(top: 20, left: 20, right: 20, bottom: 20),
actionsPadding: EdgeInsets.all(0),
actions: <Widget>[
FlatButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(
'CANCEL',
style: TextStyle(color: Colors.grey[700]),
),
),
FlatButton(
onPressed: () async {
if (list.contains(_newGroup)) {
setState(() => _error = 1);
} else {
groupList.addGroup(PodcastGroup(_newGroup));
Navigator.of(context).pop();
}
},
child: Text('DONE'),
)
],
title: Text('Create new group'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
TextField(
decoration: InputDecoration(
contentPadding: EdgeInsets.symmetric(horizontal: 10),
hintText: 'New Group',
hintStyle: TextStyle(fontSize: 18),
filled: true,
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: Colors.white, width: 2.0),
),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(color: Colors.blue, width: 2.0),
),
),
cursorRadius: Radius.circular(2),
autofocus: true,
maxLines: 1,
controller: _controller,
onChanged: (value) {
_newGroup = value;
},
),
Container(
alignment: Alignment.centerLeft,
child: (_error == 1)
? Text(
'Group existed',
style: TextStyle(color: Colors.red[400]),
)
: Center(),
),
],
),
);
}

View File

@ -25,7 +25,6 @@ class EpisodeGrid extends StatelessWidget {
this.showNumber,
this.heroTag})
: super(key: key);
double _width;
Future<String> _getPath() async {
var dir = await getApplicationDocumentsDirectory();
return dir.path;
@ -33,7 +32,7 @@ class EpisodeGrid extends StatelessWidget {
@override
Widget build(BuildContext context) {
_width = MediaQuery.of(context).size.width;
double _width = MediaQuery.of(context).size.width;
return FutureBuilder(
future: _getPath(),
builder: (context, snapshot) {
@ -67,7 +66,8 @@ class EpisodeGrid extends StatelessWidget {
ScaleRoute(
page: EpisodeDetail(
episodeItem: podcast[index],
heroTag: heroTag)),
heroTag: heroTag,
path: snapshot.data,)),
);
},
child: Container(

View File

@ -46,14 +46,14 @@ class RssItem {
factory RssItem.parse(XmlElement element) {
return RssItem(
title: findElementOrNull(element, "title")?.text,
description: findElementOrNull(element, "description")?.text,
link: findElementOrNull(element, "link")?.text,
description: findElementOrNull(element, "description")?.text?.trim() ?? 'No shownote provided for this episode',
link: findElementOrNull(element, "link")?.text?.trim(),
categories: element.findElements("category").map((element) {
return RssCategory.parse(element);
}).toList(),
guid: findElementOrNull(element, "guid")?.text,
pubDate: findElementOrNull(element, "pubDate")?.text,
author: findElementOrNull(element, "author")?.text,
//guid: findElementOrNull(element, "guid")?.text,
pubDate: findElementOrNull(element, "pubDate")?.text?.trim(),
author: findElementOrNull(element, "author")?.text?.trim(),
// comments: findElementOrNull(element, "comments")?.text,
// source: RssSource.parse(findElementOrNull(element, "source")),
// content: RssContent.parse(findElementOrNull(element, "content:encoded")),

View File

@ -45,7 +45,7 @@ class RssItunes {
}
return RssItunes(
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"),
title: findElementOrNull(element, "itunes:title")?.text?.trim(),
// subtitle: findElementOrNull(element, "itunes:subtitle")?.text?.trim(),

View File

@ -36,13 +36,6 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.5"
cached_network_image:
dependency: "direct dev"
description:
name: cached_network_image
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.0.0"
charcode:
dependency: transitive
description:
@ -111,13 +104,6 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_cache_manager:
dependency: transitive
description:
name: flutter_cache_manager
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.3"
flutter_downloader:
dependency: "direct dev"
description:
@ -240,13 +226,6 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.0.4"
network_image_to_byte:
dependency: "direct dev"
description:
name: network_image_to_byte
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.0.1"
path:
dependency: transitive
description:
@ -317,6 +296,34 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.0.5"
shared_preferences:
dependency: "direct dev"
description:
name: shared_preferences
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.5.6+1"
shared_preferences_macos:
dependency: transitive
description:
name: shared_preferences_macos
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.0.1+5"
shared_preferences_platform_interface:
dependency: transitive
description:
name: shared_preferences_platform_interface
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.1"
shared_preferences_web:
dependency: transitive
description:
name: shared_preferences_web
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.1.2+3"
sky_engine:
dependency: transitive
description: flutter
@ -414,7 +421,7 @@ packages:
source: hosted
version: "0.1.1"
uuid:
dependency: transitive
dependency: "direct dev"
description:
name: uuid
url: "https://pub.flutter-io.cn"
@ -443,4 +450,4 @@ packages:
version: "3.5.0"
sdks:
dart: ">=2.6.0 <3.0.0"
flutter: ">=1.12.13 <2.0.0"
flutter: ">=1.12.13+hotfix.4 <2.0.0"

View File

@ -29,16 +29,14 @@ dev_dependencies:
sdk: flutter
flutter_statusbarcolor: ^0.2.3
json_annotation: any
cached_network_image: any
sqflite: any
flutter_html: any
flutter_html: ^0.11.1
webfeed: any
path_provider: any
color_thief_flutter: ^1.0.1
provider: ^4.0.1
google_fonts: ^0.3.2
dio: ^3.0.8
network_image_to_byte: ^0.0.1
file_picker: ^1.2.0
xml: ^3.5.0
marquee: ^1.3.1
@ -49,6 +47,10 @@ dev_dependencies:
intl: ^0.16.1
url_launcher: ^5.4.1
image: ^2.1.4
shared_preferences: ^0.5.6+1
uuid: ^2.0.4
# For information on the generic Dart part of this file, see the