Change import ompl and refresh all to work in isolate

Add speed setting when playing
Add real dark mode
This commit is contained in:
stonegate 2020-04-18 12:48:02 +08:00
parent 3f989ac7b6
commit 1177d18b1a
31 changed files with 1602 additions and 876 deletions

View File

@ -11,20 +11,21 @@ Enjoy podcasts with Tsacdop.
Tsacdop is a podcast player developed with flutter, a clean, simply beautiful and friendly app, only support Android right now.
Credit to flutter team and all involved plugins, especially [webfeed](https://github.com/witochandra/webfeed) and [Just_Audio](https://pub.dev/packages/just_audio).
The podcasts search engine is powered by [ListenNotes](https://listennotes.com).
<a href='https://play.google.com/store/apps/details?id=com.stonegate.tsacdop&pcampaignid=pcampaignidMKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' src='https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png'/></a>
## Features
* Podcasts group manage
* Subscriptoin group management
* Playlist support
* Sleep timer
* OMPL file export and import
* Syncing in background
* Listen and subscribe history data
* Dark theme / Accent color
* Donwload for offline playing
* Auto syncing in background
* Listen and subscribe history record
* Dark mode/ Accent color personalization
* Download for offline playing
More to come...

View File

@ -1,2 +1,13 @@
arguments=
auto.sync=false
build.scans.enabled=false
connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER)
connection.project.dir=
eclipse.preferences.version=1
gradle.user.home=
java.home=C\:/Program Files/Java/jdk1.8.0_171
jvm.arguments=
offline.mode=false
override.workspace.settings=true
show.console.view=true
show.executions.view=true

View File

@ -0,0 +1,37 @@
package io.flutter.plugins;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.embedding.engine.plugins.shim.ShimPluginRegistry;
import io.flutter.plugin.common.PluginRegistry;
import io.flutter.plugins.pathprovider.PathProviderPlugin;
import com.tekartik.sqflite.SqflitePlugin;
import io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin;
/**
* Generated file. Do not edit. This file is generated by the Flutter tool based
* on the plugins that support the Android platform.
*/
@Keep
public final class IsolatePluginRegistrant {
public static void registerWith(PluginRegistry registry) {
if (alreadyRegisteredWith(registry)) {
return;
}
PathProviderPlugin.registerWith(registry.registrarFor("io.flutter.plugins.pathprovider.PathProviderPlugin"));
SqflitePlugin.registerWith(registry.registrarFor("com.tekartik.sqflite.SqflitePlugin"));
SharedPreferencesPlugin.registerWith(registry.registrarFor("io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin"));
}
private static boolean alreadyRegisteredWith(PluginRegistry registry) {
final String key = IsolatePluginRegistrant.class.getCanonicalName();
if (registry.hasPlugin(key)) {
return true;
}
registry.registrarFor(key);
return false;
}
}

View File

@ -5,16 +5,18 @@ import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.view.FlutterNativeView
import io.flutter.plugins.GeneratedPluginRegistrant
import io.flutter.plugins.IsolatePluginRegistrant
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
import io.flutter.embedding.engine.dart.DartExecutor
import io.flutter.embedding.engine.dart.DartExecutor.DartCallback
import com.rmawatson.flutterisolate.FlutterIsolatePlugin
class MainActivity: FlutterActivity() {
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
GeneratedPluginRegistrant.registerWith(flutterEngine);
FlutterIsolatePlugin.setCustomIsolateRegistrant(IsolatePluginRegistrant::class.java);
MethodChannel(flutterEngine.dartExecutor, "android_app_retain").apply {
setMethodCallHandler { method, result ->
if (method.method == "sendToBackground") {

View File

@ -133,6 +133,7 @@ class AudioPlayerNotifier extends ChangeNotifier {
bool _autoPlay = true;
DateTime _current;
int _currentPosition;
double _currentSpeed = 1;
BehaviorSubject<List<MediaItem>> queueSubject;
BasicPlaybackState get audioState => _audioState;
@ -152,6 +153,7 @@ class AudioPlayerNotifier extends ChangeNotifier {
bool get autoPlay => _autoPlay;
int get timeLeft => _timeLeft;
double get switchValue => _switchValue;
double get currentSpeed => _currentSpeed;
set setSwitchValue(double value) {
_switchValue = value;
@ -173,7 +175,7 @@ class AudioPlayerNotifier extends ChangeNotifier {
super.addListener(listener);
_queueUpdate = false;
await AudioService.connect();
bool running = AudioService.running;
bool running = AudioService.running;
if (running) {}
}
@ -205,8 +207,6 @@ class AudioPlayerNotifier extends ChangeNotifier {
await _queue.savePlaylist();
} else {
await _queue.getPlaylist();
// _queue.playlist
// .removeWhere((item) => item.enclosureUrl == episode.enclosureUrl);
await _queue.delFromPlaylist(episode);
await _queue.addToPlayListAt(episodeNew, 0);
_backgroundAudioDuration = 0;
@ -257,7 +257,7 @@ class AudioPlayerNotifier extends ChangeNotifier {
}
notifyListeners();
});
queueSubject = BehaviorSubject<List<MediaItem>>();
queueSubject.addStream(
AudioService.queueStream.distinct().where((event) => event != null));
@ -304,14 +304,16 @@ class AudioPlayerNotifier extends ChangeNotifier {
notifyListeners();
});
double s = _currentSpeed ?? 1.0;
int getPosition = 0;
Timer.periodic(Duration(milliseconds: 500), (timer) {
if (_noSlide) {
if (_audioState == BasicPlaybackState.playing) {
if (_backgroundAudioPosition < _backgroundAudioDuration - 500)
_backgroundAudioPosition = _currentPosition +
DateTime.now().difference(_current).inMilliseconds;
else
_backgroundAudioPosition = _backgroundAudioDuration;
getPosition = _currentPosition +
(DateTime.now().difference(_current).inMilliseconds * s).toInt();
_backgroundAudioPosition = getPosition < _backgroundAudioDuration
? getPosition
: _backgroundAudioDuration;
} else
_backgroundAudioPosition = _currentPosition;
@ -435,6 +437,12 @@ class AudioPlayerNotifier extends ChangeNotifier {
}
}
setSpeed(double speed) async {
await AudioService.customAction('setSpeed', speed);
_currentSpeed = speed;
notifyListeners();
}
//Set sleep timer
sleepTimer(int mins) {
if (_sleepTimerMode == SleepTimerMode.timer) {
@ -710,7 +718,7 @@ class AudioPlayerTask extends BackgroundAudioTask {
}
@override
void onCustomAction(funtion, argument) {
void onCustomAction(funtion, argument) async {
switch (funtion) {
case 'stopAtEnd':
_stopAtEnd = true;
@ -718,6 +726,8 @@ class AudioPlayerTask extends BackgroundAudioTask {
case 'cancelStopAtEnd':
_stopAtEnd = false;
break;
case 'setSpeed':
await _audioPlayer.setSpeed(argument);
}
}

View File

@ -16,8 +16,15 @@ class FiresideData {
DBHelper dbHelper = DBHelper();
String parseLink(String link) {
if (link == "http://www.shengfm.cn/")
return "https://guiguzaozhidao.fireside.fm/";
else
return link;
}
Future fatchData() async {
Response response = await Dio().get(link);
Response response = await Dio().get(parseLink(link));
if (response.statusCode == 200) {
var doc = parse(response.data);
RegExp reg = RegExp(r'https(.+)jpg');
@ -54,11 +61,14 @@ class FiresideData {
Future getData() async {
List<String> data = await dbHelper.getFiresideData(id);
_background = data[0];
_hosts = json
.decode(data[1])['hosts']
.cast<Map<String, Object>>()
.map<PodcastHost>(PodcastHost.fromJson)
.toList();
if (data[1] != '') {
_hosts = json
.decode(data[1])['hosts']
.cast<Map<String, Object>>()
.map<PodcastHost>(PodcastHost.fromJson)
.toList();
} else
_hosts = null;
}
}

View File

@ -107,7 +107,7 @@ class GroupList extends ChangeNotifier {
await group.getPodcasts();
});
_orderChanged.clear();
// notifyListeners();
// notifyListeners();
}
}
@ -129,11 +129,12 @@ class GroupList extends ChangeNotifier {
notifyListeners();
});
}
//update podcasts of each group
Future updateGroups() async{
//update podcasts of each group
Future updateGroups() async {
await Future.forEach(_groups, (group) async {
await group.getPodcasts();
});
await group.getPodcasts();
});
notifyListeners();
}
@ -180,11 +181,11 @@ class GroupList extends ChangeNotifier {
notifyListeners();
}
Future updatePodcast(PodcastLocal podcastLocal) async {
List<int> counts = await dbHelper.getPodcastCounts(podcastLocal.id);
Future updatePodcast(String id) async {
List<int> counts = await dbHelper.getPodcastCounts(id);
_groups.forEach((group) {
if (group.podcastList.contains(podcastLocal.id)) {
group.podcasts.firstWhere((podcast) => podcast.id == podcastLocal.id)
if (group.podcastList.contains(id)) {
group.podcasts.firstWhere((podcast) => podcast.id == id)
..upateCount = counts[0]
..episodeCount = counts[1];
notifyListeners();
@ -192,6 +193,13 @@ class GroupList extends ChangeNotifier {
});
}
Future subscribeNewPodcast(String id) async {
_groups[0].podcastList.insert(0, id);
_saveGroup();
await _groups[0].getPodcasts();
notifyListeners();
}
List<PodcastGroup> getPodcastGroup(String id) {
List<PodcastGroup> result = [];
_groups.forEach((group) {

View File

@ -0,0 +1,88 @@
import 'dart:isolate';
import 'package:flutter/material.dart';
import 'package:flutter_isolate/flutter_isolate.dart';
import 'package:tsacdop/local_storage/key_value_storage.dart';
import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
import 'podcastlocal.dart';
enum RefreshState { none, fetch, error }
class RefreshItem {
String title;
RefreshState refreshState;
RefreshItem(this.title, this.refreshState);
}
class RefreshWorker extends ChangeNotifier {
FlutterIsolate refreshIsolate;
ReceivePort receivePort;
SendPort refreshSendPort;
RefreshItem _currentRefreshItem = RefreshItem('', RefreshState.none);
bool _complete = false;
RefreshItem get currentRefreshItem => _currentRefreshItem;
bool get complete => _complete;
bool _created = false;
Future<void> _createIsolate() async {
receivePort = ReceivePort();
refreshIsolate = await FlutterIsolate.spawn(
refreshIsolateEntryPoint, receivePort.sendPort);
}
void _listen() {
receivePort.distinct().listen((message) {
if (message is List) {
_currentRefreshItem =
RefreshItem(message[0], RefreshState.values[message[1]]);
notifyListeners();
} else if (message is String && message == "done") {
_currentRefreshItem = RefreshItem('', RefreshState.none);
_complete = true;
notifyListeners();
refreshIsolate?.kill();
refreshIsolate = null;
_created = false;
}
});
}
Future<void> start() async {
if (!_created) {
_complete = false;
_createIsolate();
_listen();
_created = true;
}
}
void dispose() {
refreshIsolate?.kill();
refreshIsolate = null;
super.dispose();
}
}
Future<void> refreshIsolateEntryPoint(SendPort sendPort) async {
var dbHelper = DBHelper();
List<PodcastLocal> podcastList = await dbHelper.getPodcastLocalAll();
int i = 0;
await Future.forEach(podcastList, (podcastLocal) async {
sendPort.send([podcastLocal.title, 1]);
try {
i += await dbHelper.updatePodcastRss(podcastLocal);
print('Refresh ' + podcastLocal.title);
} catch (e) {
sendPort.send([podcastLocal.title, 2]);
await Future.delayed(Duration(seconds: 1));
}
});
KeyValueStorage refreshstorage = KeyValueStorage('refreshdate');
await refreshstorage.saveInt(DateTime.now().millisecondsSinceEpoch);
KeyValueStorage refreshcountstorage = KeyValueStorage('refreshcount');
await refreshcountstorage.saveInt(i);
sendPort.send("done");
}

View File

@ -28,11 +28,13 @@ class SettingState extends ChangeNotifier {
KeyValueStorage downloadUsingDataStorage =
KeyValueStorage('downloadUsingData');
KeyValueStorage introStorage = KeyValueStorage('intro');
KeyValueStorage realDarkStorage = KeyValueStorage('realDark');
Future initData() async {
await _getTheme();
await _getAccentSetColor();
await _getShowIntro();
await _getRealDark();
}
ThemeMode _theme;
@ -98,12 +100,21 @@ class SettingState extends ChangeNotifier {
bool _showIntro;
bool get showIntro => _showIntro;
bool _realDark;
bool get realDark => _realDark;
set setRealDark(bool boo) {
_realDark = boo;
_setRealDark();
notifyListeners();
}
@override
void addListener(VoidCallback listener) {
super.addListener(listener);
_getTheme();
_getAccentSetColor();
_getAutoUpdate();
_getRealDark();
_getDownloadUsingData();
_getUpdateInterval().then((value) {
if (_initUpdateTag == 0) setWorkManager(24);
@ -165,7 +176,17 @@ class SettingState extends ChangeNotifier {
int i = await introStorage.getInt();
_showIntro = i == 0 ? true : false;
}
Future saveShowIntro() async{
Future saveShowIntro() async {
await introStorage.saveInt(1);
}
Future _getRealDark() async {
int i = await realDarkStorage.getInt();
_realDark = i == 0 ? false : true;
}
Future _setRealDark() async {
await realDarkStorage.saveInt(_realDark ? 1 : 0);
}
}

View File

@ -0,0 +1,197 @@
import 'dart:io';
import 'dart:isolate';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
import 'package:image/image.dart' as img;
import 'package:color_thief_flutter/color_thief_flutter.dart';
import 'package:uuid/uuid.dart';
import 'package:dio/dio.dart';
import 'package:flutter_isolate/flutter_isolate.dart';
import '../webfeed/webfeed.dart';
import '../local_storage/sqflite_localpodcast.dart';
import 'fireside_data.dart';
import 'podcastlocal.dart';
enum SubscribeState { none, start, subscribe, fetch, stop, exist, error }
class SubscribeItem {
String url;
String title;
SubscribeState subscribeState;
String id;
SubscribeItem(this.url, this.title,
{this.subscribeState = SubscribeState.none, this.id = ''});
}
class SubscribeWorker extends ChangeNotifier {
FlutterIsolate subIsolate;
ReceivePort receivePort;
SendPort subSendPort;
SubscribeItem _subscribeItem;
SubscribeItem _currentSubscribeItem = SubscribeItem('', '');
bool _created = false;
setSubscribeItem(SubscribeItem item) async{
_subscribeItem = item;
await _start();
}
_setCurrentSubscribeItem(SubscribeItem item) {
_currentSubscribeItem = item;
notifyListeners();
}
SubscribeItem get currentSubscribeItem => _currentSubscribeItem;
Future<void> _createIsolate() async {
receivePort = ReceivePort();
subIsolate =
await FlutterIsolate.spawn(subIsolateEntryPoint, receivePort.sendPort);
}
void listen() {
receivePort.distinct().listen((message) {
if (message is SendPort) {
subSendPort = message;
subSendPort.send([_subscribeItem.url, _subscribeItem.title]);
} else if (message is List) {
_setCurrentSubscribeItem(SubscribeItem(message[1], message[0],
subscribeState: SubscribeState.values[message[2]],
id: message.length == 4 ? message[3] : ''));
print(message[2]);
} else if (message is String && message == "done") {
subIsolate?.kill();
subIsolate = null;
_created = false;
}
});
}
Future _start() async {
if (_created == false) {
await _createIsolate();
_created = true;
listen();
} else
subSendPort.send([_subscribeItem.url, _subscribeItem.title]);
}
void dispose() {
subIsolate?.kill();
subIsolate = null;
super.dispose();
}
}
Future<void> subIsolateEntryPoint(SendPort sendPort) async {
List<SubscribeItem> items = [];
bool _running = false;
ReceivePort subReceivePort = ReceivePort();
sendPort.send(subReceivePort.sendPort);
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;
}
Future<void> _subscribe(SubscribeItem item) async {
var dbHelper = DBHelper();
String rss = item.url;
sendPort.send([item.title, item.url, 1]);
BaseOptions options = new BaseOptions(
connectTimeout: 20000,
receiveTimeout: 20000,
);
print(rss);
Response response = await Dio(options).get(rss);
if (response.statusCode == 200) {
var p = RssFeed.parse(response.data);
var dir = await getApplicationDocumentsDirectory();
String realUrl =
response.redirects.isEmpty ? rss : response.realUri.toString();
print(realUrl);
bool checkUrl = await dbHelper.checkPodcast(realUrl);
if (checkUrl) {
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);
String uuid = Uuid().v4();
File("${dir.path}/$uuid.png")
..writeAsBytesSync(img.encodePng(thumbnail));
String imagePath = "${dir.path}/$uuid.png";
String primaryColor = await _getColor(File("${dir.path}/$uuid.png"));
String author = p.itunes.author ?? p.author ?? '';
String provider = p.generator ?? '';
String link = p.link ?? '';
PodcastLocal podcastLocal = PodcastLocal(p.title, p.itunes.image.href,
realUrl, primaryColor, author, uuid, imagePath, provider, link,
description: p.description);
// await groupList.subscribe(podcastLocal);
await dbHelper.savePodcastLocal(podcastLocal);
sendPort.send([item.title, item.url, 2, uuid]);
if (provider.contains('fireside')) {
FiresideData data = FiresideData(uuid, link);
try {
await data.fatchData();
} catch (e) {
print(e);
}
}
int count = await dbHelper.savePodcastRss(p, uuid);
sendPort.send([item.title, item.url, 3, uuid]);
await Future.delayed(Duration(seconds: 5));
sendPort.send([item.title, item.url, 4]);
items.removeWhere((element) => element.url == item.url);
if (items.length > 0) {
await _subscribe(items.first);
} else
sendPort.send("done");
} else {
sendPort.send([item.title, item.url, 5]);
await Future.delayed(Duration(seconds: 5));
sendPort.send([item.title, item.url, 4]);
items.removeWhere((element) => element.url == item.url);
if (items.length > 0) {
await _subscribe(items.first);
} else
sendPort.send("done");
}
} else {
sendPort.send([item.title, item.url, 6]);
await Future.delayed(Duration(seconds: 5));
sendPort.send([item.title, item.url, 4]);
items.removeWhere((element) => element.url == item.url);
if (items.length > 0) {
await _subscribe(items.first);
} else
sendPort.send("done");
}
}
subReceivePort.distinct().listen((message) {
if (message is List<String>) {
items.add(SubscribeItem(message[0], message[1]));
if (!_running) {
_subscribe(items.first);
_running = true;
}
}
});
}

View File

@ -11,6 +11,7 @@ import 'package:intl/intl.dart';
import 'package:tuple/tuple.dart';
import 'package:audio_service/audio_service.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:tsacdop/class/audiostate.dart';
import 'package:tsacdop/class/episodebrief.dart';
@ -165,7 +166,7 @@ class _EpisodeDetailState extends State<EpisodeDetail> {
child: Text(
(widget.episodeItem.duration ~/ 60)
.toString() +
'mins',
'min',
style: textstyle),
)
: Center(),
@ -207,7 +208,12 @@ class _EpisodeDetailState extends State<EpisodeDetail> {
? Html(
padding: EdgeInsets.only(
left: 20.0, right: 20, bottom: 10),
defaultTextStyle: TextStyle(height: 1.8),
defaultTextStyle:
GoogleFonts.libreBaskerville(
textStyle: TextStyle(
height: 1.8,
),
),
data: _description,
linkStyle: TextStyle(
color: Theme.of(context).accentColor,
@ -230,8 +236,10 @@ class _EpisodeDetailState extends State<EpisodeDetail> {
_launchUrl(link.url);
},
text: _description,
style: TextStyle(
height: 1.8,
style: GoogleFonts.libreBaskerville(
textStyle: TextStyle(
height: 1.8,
),
),
linkStyle: TextStyle(
color:

View File

@ -24,7 +24,6 @@ class _DownloadButtonState extends State<DownloadButton> {
bool _permissionReady;
bool _usingData;
StreamSubscription _connectivity;
EpisodeTask _task;
@override
void initState() {

View File

@ -72,7 +72,7 @@ class AboutApp extends StatelessWidget {
image: AssetImage('assets/logo.png'),
height: 80,
),
Text('Version: 0.1.8'),
Text('Version: 0.1.9'),
],
),
),

View File

@ -1,6 +1,7 @@
import 'dart:io';
import 'dart:convert';
import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:color_thief_flutter/color_thief_flutter.dart';
@ -10,17 +11,22 @@ import 'package:provider/provider.dart';
import 'package:path_provider/path_provider.dart';
import 'package:image/image.dart' as img;
import 'package:fluttertoast/fluttertoast.dart';
import 'package:tsacdop/class/audiostate.dart';
import 'package:tsacdop/class/download_state.dart';
import 'package:tsacdop/class/fireside_data.dart';
import 'package:tsacdop/class/refresh_podcast.dart';
import 'package:uuid/uuid.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/local_storage/sqflite_localpodcast.dart';
import 'package:tsacdop/home/appbar/popupmenu.dart';
import 'package:tsacdop/webfeed/webfeed.dart';
import 'package:tsacdop/.env.dart';
import '../../class/importompl.dart';
import '../../class/podcast_group.dart';
import '../../class/searchpodcast.dart';
import '../../class/podcastlocal.dart';
import '../../class/subscribe_podcast.dart';
import '../../local_storage/sqflite_localpodcast.dart';
import '../../home/appbar/popupmenu.dart';
import '../../util/context_extension.dart';
import '../../webfeed/webfeed.dart';
import '../../.env.dart';
import '../nested_home.dart';
class MyHomePage extends StatefulWidget {
@ -52,6 +58,7 @@ class _MyHomePageState extends State<MyHomePage> {
child: Scaffold(
key: _scaffoldKey,
appBar: AppBar(
//elevation: 1.0,
centerTitle: true,
leading: IconButton(
tooltip: 'Add',
@ -74,15 +81,16 @@ class _MyHomePageState extends State<MyHomePage> {
],
),
body: WillPopScope(
onWillPop: () async {
if (Platform.isAndroid) {
_androidAppRetain.invokeMethod('sendToBackground');
return false;
} else {
return true;
}
},
child: Home()),
onWillPop: () async {
if (Platform.isAndroid) {
_androidAppRetain.invokeMethod('sendToBackground');
return false;
} else {
return true;
}
},
child: Home(),
),
),
);
}
@ -105,6 +113,32 @@ class _MyHomePageDelegate extends SearchDelegate<int> {
return searchResult.results;
}
static Future getRss(String url) async {
try {
BaseOptions options = new BaseOptions(
connectTimeout: 10000,
receiveTimeout: 10000,
);
Response response = await Dio(options).get(url);
var p = RssFeed.parse(response.data);
return OnlinePodcast(
rss: url,
title: p.title,
publisher: p.author,
description: p.description,
image: p.itunes.image.href);
} catch (e) {
throw e;
}
}
RegExp rssExp = RegExp(r'^(https?):\/\/(.*)');
Widget invalidRss() => Container(
height: 50,
alignment: Alignment.center,
child: Text('Invalid rss link'),
);
@override
ThemeData appBarTheme(BuildContext context) => Theme.of(context);
@ -135,27 +169,46 @@ class _MyHomePageDelegate extends SearchDelegate<int> {
height: 20,
),
));
return FutureBuilder(
future: getList(query),
builder: (BuildContext context, AsyncSnapshot<List> snapshot) {
if (!snapshot.hasData && query != null)
return Container(
padding: EdgeInsets.only(top: 200),
alignment: Alignment.topCenter,
child: CircularProgressIndicator(),
);
List content = snapshot.data;
return ListView.builder(
scrollDirection: Axis.vertical,
itemCount: content.length,
itemBuilder: (BuildContext context, int index) {
else if (rssExp.stringMatch(query) != null)
return FutureBuilder(
future: getRss(rssExp.stringMatch(query)),
builder: (context, snapshot) {
if (snapshot.hasError)
return invalidRss();
else if (snapshot.hasData)
return SearchResult(
onlinePodcast: content[index],
onlinePodcast: snapshot.data,
);
},
);
},
);
else
return Container(
padding: EdgeInsets.only(top: 200),
alignment: Alignment.topCenter,
child: CircularProgressIndicator(),
);
},
);
else
return FutureBuilder(
future: getList(query),
builder: (BuildContext context, AsyncSnapshot<List> snapshot) {
if (!snapshot.hasData && query != null)
return Container(
padding: EdgeInsets.only(top: 200),
alignment: Alignment.topCenter,
child: CircularProgressIndicator(),
);
List content = snapshot.data;
return ListView.builder(
scrollDirection: Axis.vertical,
itemCount: content.length,
itemBuilder: (BuildContext context, int index) {
return SearchResult(
onlinePodcast: content[index],
);
},
);
},
);
}
@override
@ -189,27 +242,46 @@ class _MyHomePageDelegate extends SearchDelegate<int> {
),
),
);
return FutureBuilder(
future: getList(query),
builder: (BuildContext context, AsyncSnapshot<List> snapshot) {
if (!snapshot.hasData && query != null)
return Container(
padding: EdgeInsets.only(top: 200),
alignment: Alignment.topCenter,
child: CircularProgressIndicator(),
);
List content = snapshot.data;
return ListView.builder(
scrollDirection: Axis.vertical,
itemCount: content.length,
itemBuilder: (BuildContext context, int index) {
else if (rssExp.stringMatch(query) != null)
return FutureBuilder(
future: getRss(rssExp.stringMatch(query)),
builder: (context, snapshot) {
if (snapshot.hasError)
return invalidRss();
else if (snapshot.hasData)
return SearchResult(
onlinePodcast: content[index],
onlinePodcast: snapshot.data,
);
},
);
},
);
else
return Container(
padding: EdgeInsets.only(top: 200),
alignment: Alignment.topCenter,
child: CircularProgressIndicator(),
);
},
);
else
return FutureBuilder(
future: getList(query),
builder: (BuildContext context, AsyncSnapshot<List> snapshot) {
if (!snapshot.hasData && query != null)
return Container(
padding: EdgeInsets.only(top: 200),
alignment: Alignment.topCenter,
child: CircularProgressIndicator(),
);
List content = snapshot.data;
return ListView.builder(
scrollDirection: Axis.vertical,
itemCount: content.length,
itemBuilder: (BuildContext context, int index) {
return SearchResult(
onlinePodcast: content[index],
);
},
);
},
);
}
}
@ -220,21 +292,32 @@ class SearchResult extends StatefulWidget {
_SearchResultState createState() => _SearchResultState();
}
class _SearchResultState extends State<SearchResult> {
class _SearchResultState extends State<SearchResult>
with SingleTickerProviderStateMixin {
bool _issubscribe;
bool _adding;
bool _showDes;
AnimationController _controller;
Animation _animation;
double _value;
@override
void initState() {
super.initState();
_issubscribe = false;
_adding = false;
_showDes = false;
_value = 0;
_controller =
AnimationController(vsync: this, duration: Duration(milliseconds: 300));
_animation = Tween<double>(begin: 0.0, end: 1.0).animate(_controller)
..addListener(() {
setState(() {
_value = _animation.value;
});
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@ -248,91 +331,98 @@ class _SearchResultState extends State<SearchResult> {
@override
Widget build(BuildContext context) {
var importOmpl = Provider.of<ImportOmpl>(context, listen: false);
var groupList = Provider.of<GroupList>(context, listen: false);
savePodcast(String rss) async {
if (mounted) setState(() => _adding = true);
// var importOmpl = Provider.of<ImportOmpl>(context, listen: false);
// var groupList = Provider.of<GroupList>(context, listen: false);
var subscribeWorker = Provider.of<SubscribeWorker>(context, listen: false);
importOmpl.importState = ImportState.import;
try {
BaseOptions options = new BaseOptions(
connectTimeout: 30000,
receiveTimeout: 30000,
);
Response response = await Dio(options).get(rss);
var dbHelper = DBHelper();
String _realUrl = response.realUri.toString();
bool _checkUrl = await dbHelper.checkPodcast(_realUrl);
if (_checkUrl) {
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);
String _uuid = Uuid().v4();
File("${dir.path}/$_uuid.png")
..writeAsBytesSync(img.encodePng(thumbnail));
String _imagePath = "${dir.path}/$_uuid.png";
String _primaryColor = await getColor(File("${dir.path}/$_uuid.png"));
String _author = _p.itunes.author ?? _p.author ?? '';
String _provider = _p.generator ?? '';
String _link = _p.link ?? '';
PodcastLocal podcastLocal = PodcastLocal(
_p.title,
_p.itunes.image.href,
_realUrl,
_primaryColor,
_author,
_uuid,
_imagePath,
_provider,
_link,
description: _p.description);
await groupList.subscribe(podcastLocal);
if (_provider.contains('fireside')) {
FiresideData data = FiresideData(_uuid, _link);
await data.fatchData();
}
importOmpl.importState = ImportState.parse;
await dbHelper.savePodcastRss(_p, _uuid);
groupList.updatePodcast(podcastLocal);
importOmpl.importState = ImportState.complete;
} else {
importOmpl.importState = ImportState.error;
Fluttertoast.showToast(
msg: 'Podcast Subscribed Already',
gravity: ToastGravity.TOP,
);
await Future.delayed(Duration(seconds: 10));
importOmpl.importState = ImportState.stop;
}
} catch (e) {
importOmpl.importState = ImportState.error;
Fluttertoast.showToast(
msg: 'Network error, Subscribe failed',
gravity: ToastGravity.TOP,
);
await Future.delayed(Duration(seconds: 5));
importOmpl.importState = ImportState.stop;
}
savePodcast(OnlinePodcast podcast) async {
SubscribeItem item = SubscribeItem(podcast.rss, podcast.title);
subscribeWorker.setSubscribeItem(item);
}
// savePodcast(String rss) async {
// if (mounted) setState(() => _adding = true);
// importOmpl.importState = ImportState.import;
// try {
// BaseOptions options = new BaseOptions(
// connectTimeout: 30000,
// receiveTimeout: 30000,
// );
// Response response = await Dio(options).get(rss);
// var dbHelper = DBHelper();
// String _realUrl = response.realUri.toString();
// bool _checkUrl = await dbHelper.checkPodcast(_realUrl);
// if (_checkUrl) {
// 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);
// String _uuid = Uuid().v4();
// File("${dir.path}/$_uuid.png")
// ..writeAsBytesSync(img.encodePng(thumbnail));
// String _imagePath = "${dir.path}/$_uuid.png";
// String _primaryColor = await getColor(File("${dir.path}/$_uuid.png"));
// String _author = _p.itunes.author ?? _p.author ?? '';
// String _provider = _p.generator ?? '';
// String _link = _p.link ?? '';
// PodcastLocal podcastLocal = PodcastLocal(
// _p.title,
// _p.itunes.image.href,
// _realUrl,
// _primaryColor,
// _author,
// _uuid,
// _imagePath,
// _provider,
// _link,
// description: _p.description);
// await groupList.subscribe(podcastLocal);
// if (_provider.contains('fireside')) {
// FiresideData data = FiresideData(_uuid, _link);
// await data.fatchData();
// }
// importOmpl.importState = ImportState.parse;
// await dbHelper.savePodcastRss(_p, _uuid);
// groupList.updatePodcast(podcastLocal.id);
// importOmpl.importState = ImportState.complete;
// } else {
// importOmpl.importState = ImportState.error;
// Fluttertoast.showToast(
// msg: 'Podcast subscribed already',
// gravity: ToastGravity.TOP,
// );
// await Future.delayed(Duration(seconds: 10));
// importOmpl.importState = ImportState.stop;
// }
// } catch (e) {
// importOmpl.importState = ImportState.error;
// Fluttertoast.showToast(
// msg: 'Network error, Subscribe failed',
// gravity: ToastGravity.TOP,
// );
// await Future.delayed(Duration(seconds: 5));
// importOmpl.importState = ImportState.stop;
// }
// }
return Container(
decoration: BoxDecoration(
border: Border(
@ -348,6 +438,10 @@ class _SearchResultState extends State<SearchResult> {
onTap: () {
setState(() {
_showDes = !_showDes;
if (_value == 0)
_controller.forward();
else
_controller.reverse();
});
},
leading: ClipRRect(
@ -361,51 +455,33 @@ class _SearchResultState extends State<SearchResult> {
),
),
title: Text(widget.onlinePodcast.title),
subtitle: Text(widget.onlinePodcast.publisher),
subtitle: Text(widget.onlinePodcast.publisher ?? ''),
trailing: Row(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Icon(_showDes
? Icons.keyboard_arrow_up
: Icons.keyboard_arrow_down),
Transform.rotate(
angle: math.pi * _value,
child: Icon(Icons.keyboard_arrow_down),
),
Padding(padding: EdgeInsets.only(right: 10.0)),
Container(
width: 100,
height: 35,
child: !_issubscribe
? !_adding
? OutlineButton(
highlightedBorderColor:
Theme.of(context).accentColor,
splashColor: Theme.of(context)
.accentColor
.withOpacity(0.8),
child: Text('Subscribe',
style: TextStyle(
color: Theme.of(context).accentColor)),
onPressed: () {
importOmpl.rssTitle =
widget.onlinePodcast.title;
savePodcast(widget.onlinePodcast.rss);
})
: OutlineButton(
highlightedBorderColor:
Theme.of(context).accentColor,
splashColor: Theme.of(context)
.accentColor
.withOpacity(0.8),
child: SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation(
Theme.of(context).accentColor),
)),
onPressed: () {},
)
? OutlineButton(
highlightedBorderColor: Theme.of(context).accentColor,
splashColor:
Theme.of(context).accentColor.withOpacity(0.8),
child: Text('Subscribe',
style: TextStyle(
color: Theme.of(context).accentColor)),
onPressed: () {
savePodcast(widget.onlinePodcast);
setState(() => _issubscribe = true);
})
: OutlineButton(
color: context.accentColor.withOpacity(0.8),
highlightedBorderColor: Colors.grey[500],
disabledTextColor: Colors.grey[500],
child: Text('Subscribe'),

View File

@ -1,75 +1,72 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:tsacdop/class/importompl.dart';
import '../../class/podcast_group.dart';
import '../../class/subscribe_podcast.dart';
import '../../class/refresh_podcast.dart';
import '../../util/context_extension.dart';
class Import extends StatelessWidget {
Widget importColumn(String text, BuildContext context) {
return Container(
color: context.primaryColorDark,
child: 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.centerLeft,
child: Text(text),
),
]),
);
}
@override
Widget build(BuildContext context) {
return Consumer<ImportOmpl>(
builder: (_, importOmpl, __) => Container(
color: Theme.of(context).primaryColorDark,
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.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,
padding: EdgeInsets.symmetric(horizontal: 20.0),
alignment: Alignment.centerLeft,
child:
Text('Connetting: ' + (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('Fetch data: ' + (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()),
GroupList groupList = Provider.of<GroupList>(context, listen: false);
return Column(
children: <Widget>[
Consumer<SubscribeWorker>(
builder: (_, subscribeWorker, __) {
SubscribeItem item = subscribeWorker.currentSubscribeItem;
switch (item.subscribeState) {
case SubscribeState.start:
return importColumn("Subscribe: ${item.title}", context);
case SubscribeState.subscribe:
groupList.subscribeNewPodcast(item.id);
return importColumn("Fetch data ${item.title}", context);
case SubscribeState.fetch:
groupList.updatePodcast(item.id);
return importColumn("Subscribe success ${item.title}", context);
case SubscribeState.exist:
return importColumn(
"Subscribe failed, podcast existed ${item.title}", context);
case SubscribeState.error:
return importColumn(
"Subscribe failed, network error ${item.title}", context);
default:
return Center();
}
},
),
Consumer<RefreshWorker>(
builder: (context, refreshWorker, child) {
RefreshItem item = refreshWorker.currentRefreshItem;
if (refreshWorker.complete) groupList.updateGroups();
switch (item.refreshState) {
case RefreshState.fetch:
return importColumn("Fetch data ${item.title}", context);
case RefreshState.error:
return importColumn("Update error ${item.title}", context);
default:
return Center();
}
},
)
],
);
}
}

View File

@ -5,6 +5,8 @@ import 'package:flutter/material.dart';
import 'package:dio/dio.dart';
import 'package:provider/provider.dart';
import 'package:tsacdop/class/fireside_data.dart';
import 'package:tsacdop/class/refresh_podcast.dart';
import 'package:tsacdop/class/subscribe_podcast.dart';
import 'package:tsacdop/local_storage/key_value_storage.dart';
import 'package:xml/xml.dart' as xml;
import 'package:file_picker/file_picker.dart';
@ -84,108 +86,109 @@ class _PopupMenuState extends State<PopupMenu> {
@override
Widget build(BuildContext context) {
ImportOmpl importOmpl = Provider.of<ImportOmpl>(context, listen: false);
GroupList groupList = Provider.of<GroupList>(context, listen: false);
_refreshAll() async {
var dbHelper = DBHelper();
List<PodcastLocal> podcastList = await dbHelper.getPodcastLocalAll();
int i = 0;
await Future.forEach(podcastList, (podcastLocal) async {
importOmpl.rssTitle = podcastLocal.title;
importOmpl.importState = ImportState.parse;
i += await dbHelper.updatePodcastRss(podcastLocal);
print('Refresh ' + podcastLocal.title);
});
KeyValueStorage refreshstorage = KeyValueStorage('refreshdate');
await refreshstorage.saveInt(DateTime.now().millisecondsSinceEpoch);
KeyValueStorage refreshcountstorage = KeyValueStorage('refreshcount');
await refreshcountstorage.saveInt(i);
importOmpl.importState = ImportState.complete;
groupList.updateGroups();
}
saveOmpl(String rss) async {
var dbHelper = DBHelper();
importOmpl.importState = ImportState.import;
BaseOptions options = new BaseOptions(
connectTimeout: 20000,
receiveTimeout: 20000,
);
Response response = await Dio(options).get(rss);
if (response.statusCode == 200) {
var _p = RssFeed.parse(response.data);
var dir = await getApplicationDocumentsDirectory();
String _realUrl =
response.redirects.isEmpty ? rss : response.realUri.toString();
print(_realUrl);
bool _checkUrl = await dbHelper.checkPodcast(_realUrl);
if (_checkUrl) {
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);
String _uuid = Uuid().v4();
File("${dir.path}/$_uuid.png")
..writeAsBytesSync(img.encodePng(thumbnail));
String _imagePath = "${dir.path}/$_uuid.png";
String _primaryColor =
await _getColor(File("${dir.path}/$_uuid.png"));
String _author = _p.itunes.author ?? _p.author ?? '';
String _provider = _p.generator ?? '';
String _link = _p.link ?? '';
PodcastLocal podcastLocal = PodcastLocal(
_p.title,
_p.itunes.image.href,
_realUrl,
_primaryColor,
_author,
_uuid,
_imagePath,
_provider,
_link,
description: _p.description);
await groupList.subscribe(podcastLocal);
if (_provider.contains('fireside')) {
FiresideData data = FiresideData(_uuid, _link);
await data.fatchData();
}
importOmpl.importState = ImportState.parse;
await dbHelper.savePodcastRss(_p, _uuid);
groupList.updatePodcast(podcastLocal);
importOmpl.importState = ImportState.complete;
} else {
importOmpl.importState = ImportState.error;
Fluttertoast.showToast(
msg: 'Podcast Subscribed Already',
gravity: ToastGravity.TOP,
);
await Future.delayed(Duration(seconds: 5));
importOmpl.importState = ImportState.stop;
}
} else {
importOmpl.importState = ImportState.error;
Fluttertoast.showToast(
msg: 'Network error, Subscribe failed',
gravity: ToastGravity.TOP,
);
await Future.delayed(Duration(seconds: 5));
importOmpl.importState = ImportState.stop;
}
}
// ImportOmpl importOmpl = Provider.of<ImportOmpl>(context, listen: false);
// GroupList groupList = Provider.of<GroupList>(context, listen: false);
var refreshWorker = Provider.of<RefreshWorker>(context, listen: false);
var subscribeWorker = Provider.of<SubscribeWorker>(context, listen: false);
// _refreshAll() async {
// var dbHelper = DBHelper();
// List<PodcastLocal> podcastList = await dbHelper.getPodcastLocalAll();
// int i = 0;
// await Future.forEach(podcastList, (podcastLocal) async {
// importOmpl.rssTitle = podcastLocal.title;
// importOmpl.importState = ImportState.parse;
// i += await dbHelper.updatePodcastRss(podcastLocal);
// print('Refresh ' + podcastLocal.title);
// });
// KeyValueStorage refreshstorage = KeyValueStorage('refreshdate');
// await refreshstorage.saveInt(DateTime.now().millisecondsSinceEpoch);
// KeyValueStorage refreshcountstorage = KeyValueStorage('refreshcount');
// await refreshcountstorage.saveInt(i);
// importOmpl.importState = ImportState.complete;
// groupList.updateGroups();
// }
//
// saveOmpl(String rss) async {
// var dbHelper = DBHelper();
// importOmpl.importState = ImportState.import;
// BaseOptions options = new BaseOptions(
// connectTimeout: 20000,
// receiveTimeout: 20000,
// );
// Response response = await Dio(options).get(rss);
// if (response.statusCode == 200) {
// var _p = RssFeed.parse(response.data);
//
// var dir = await getApplicationDocumentsDirectory();
//
// String _realUrl =
// response.redirects.isEmpty ? rss : response.realUri.toString();
//
// print(_realUrl);
// bool _checkUrl = await dbHelper.checkPodcast(_realUrl);
//
// if (_checkUrl) {
// 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);
// String _uuid = Uuid().v4();
// File("${dir.path}/$_uuid.png")
// ..writeAsBytesSync(img.encodePng(thumbnail));
//
// String _imagePath = "${dir.path}/$_uuid.png";
// String _primaryColor =
// await _getColor(File("${dir.path}/$_uuid.png"));
// String _author = _p.itunes.author ?? _p.author ?? '';
// String _provider = _p.generator ?? '';
// String _link = _p.link ?? '';
// PodcastLocal podcastLocal = PodcastLocal(
// _p.title,
// _p.itunes.image.href,
// _realUrl,
// _primaryColor,
// _author,
// _uuid,
// _imagePath,
// _provider,
// _link,
// description: _p.description);
//
// await groupList.subscribe(podcastLocal);
//
// if (_provider.contains('fireside')) {
// FiresideData data = FiresideData(_uuid, _link);
// await data.fatchData();
// }
//
// importOmpl.importState = ImportState.parse;
//
// await dbHelper.savePodcastRss(_p, _uuid);
// groupList.updatePodcast(podcastLocal.id);
// importOmpl.importState = ImportState.complete;
// } else {
// importOmpl.importState = ImportState.error;
//
// Fluttertoast.showToast(
// msg: 'Podcast Subscribed Already',
// gravity: ToastGravity.TOP,
// );
// await Future.delayed(Duration(seconds: 5));
// importOmpl.importState = ImportState.stop;
// }
// } else {
// importOmpl.importState = ImportState.error;
//
// Fluttertoast.showToast(
// msg: 'Network error, Subscribe failed',
// gravity: ToastGravity.TOP,
// );
// await Future.delayed(Duration(seconds: 5));
// importOmpl.importState = ImportState.stop;
// }
// }
//
void _saveOmpl(String path) async {
File file = File(path);
try {
@ -198,18 +201,18 @@ class _PopupMenuState extends State<PopupMenu> {
.toList();
if (total.length == 0) {
Fluttertoast.showToast(
msg: 'File Not Valid',
msg: 'File not valid',
gravity: ToastGravity.BOTTOM,
);
} else {
for (int i = 0; i < total.length; i++) {
if (total[i].xmlUrl != null) {
importOmpl.rssTitle = total[i].text;
try {
await saveOmpl(total[i].xmlUrl);
} catch (e) {
print(e.toString());
}
// importOmpl.rssTitle = total[i].text;
//await saveOmpl(total[i].xmlUrl);
SubscribeItem item =
SubscribeItem(total[i].xmlUrl, total[i].text);
await subscribeWorker.setSubscribeItem(item);
await Future.delayed(Duration(milliseconds: 500));
print(total[i].text);
}
}
@ -221,8 +224,8 @@ class _PopupMenuState extends State<PopupMenu> {
msg: 'File error, Subscribe failed',
gravity: ToastGravity.TOP,
);
await Future.delayed(Duration(seconds: 5));
importOmpl.importState = ImportState.stop;
//await Future.delayed(Duration(seconds: 5));
// importOmpl.importState = ImportState.stop;
}
}
@ -233,7 +236,11 @@ class _PopupMenuState extends State<PopupMenu> {
return;
}
print('File Path' + filePath);
importOmpl.importState = ImportState.start;
//importOmpl.importState = ImportState.start;
Fluttertoast.showToast(
msg: 'Read file successfully',
gravity: ToastGravity.TOP,
);
_saveOmpl(filePath);
} on PlatformException catch (e) {
print(e.toString());
@ -338,7 +345,8 @@ class _PopupMenuState extends State<PopupMenu> {
} else if (value == 2) {
_getFilePath();
} else if (value == 1) {
_refreshAll();
//_refreshAll();
refreshWorker.start();
} else if (value == 3) {
// setting.theme != 2 ? setting.setTheme(2) : setting.setTheme(1);
} else if (value == 4) {

View File

@ -134,294 +134,6 @@ class PlayerWidget extends StatefulWidget {
class _PlayerWidgetState extends State<PlayerWidget> {
final GlobalKey<AnimatedListState> miniPlaylistKey = GlobalKey();
Widget _controlPanel(BuildContext context) {
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
return Container(
color: Theme.of(context).primaryColor,
height: 300,
padding: EdgeInsets.symmetric(horizontal: 10.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Consumer<AudioPlayerNotifier>(
builder: (_, data, __) {
Color _c = (Theme.of(context).brightness == Brightness.light)
? data.episode.primaryColor.colorizedark()
: data.episode.primaryColor.colorizeLight();
return Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(
padding: EdgeInsets.only(top: 10, left: 10, right: 10),
child: SliderTheme(
data: SliderTheme.of(context).copyWith(
activeTrackColor:
Theme.of(context).brightness == Brightness.dark
? Colors.black38
: Colors.grey[400],
inactiveTrackColor:
Theme.of(context).primaryColorDark,
trackHeight: 20.0,
trackShape: MyRectangularTrackShape(),
thumbColor: Theme.of(context).accentColor,
thumbShape: MyRoundSliderThumpShape(
enabledThumbRadius: 10.0,
disabledThumbRadius: 10.0,
thumbCenterColor: _c),
overlayColor:
Theme.of(context).accentColor.withAlpha(32),
overlayShape:
RoundSliderOverlayShape(overlayRadius: 4.0),
),
child: Slider(
value: data.seekSliderValue,
onChanged: (double val) {
audio.sliderSeek(val);
}),
),
),
Container(
height: 20.0,
padding: EdgeInsets.symmetric(horizontal: 30.0),
child: Row(
children: <Widget>[
Text(
_stringForSeconds(
data.backgroundAudioPosition / 1000) ??
'',
style: TextStyle(fontSize: 10),
),
Expanded(
child: Container(
alignment: Alignment.center,
child: data.remoteErrorMessage != null
? Text(data.remoteErrorMessage,
style: const TextStyle(
color: const Color(0xFFFF0000)))
: Text(
data.audioState ==
BasicPlaybackState
.buffering ||
data.audioState ==
BasicPlaybackState
.connecting ||
data.audioState ==
BasicPlaybackState.none
? 'Buffring...'
: '',
style: TextStyle(
color: Theme.of(context).accentColor),
),
),
),
Text(
_stringForSeconds(
data.backgroundAudioDuration / 1000) ??
'',
style: TextStyle(fontSize: 10),
),
],
),
),
],
);
},
),
Container(
height: 100,
child: Selector<AudioPlayerNotifier, BasicPlaybackState>(
selector: (_, audio) => audio.audioState,
builder: (_, backplay, __) {
return Material(
color: Colors.transparent,
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
padding: EdgeInsets.symmetric(horizontal: 30.0),
onPressed: backplay == BasicPlaybackState.playing
? () => audio.forwardAudio(-10)
: null,
iconSize: 32.0,
icon: Icon(Icons.replay_10),
color: Colors.grey[500]),
Container(
margin: EdgeInsets.symmetric(horizontal: 30),
height: 60,
width: 60,
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
shape: BoxShape.circle,
border: Border.all(
color: Theme.of(context).brightness ==
Brightness.dark
? Colors.black12
: Colors.white10,
width: 1),
boxShadow: Theme.of(context).brightness ==
Brightness.dark
? _customShadowNight
: _customShadow),
child: backplay == BasicPlaybackState.playing
? Material(
color: Colors.transparent,
child: InkWell(
borderRadius:
BorderRadius.all(Radius.circular(30)),
onTap:
backplay == BasicPlaybackState.playing
? () {
audio.pauseAduio();
}
: null,
child: SizedBox(
height: 60,
width: 60,
child: Icon(
Icons.pause,
size: 40,
),
),
),
)
: Material(
color: Colors.transparent,
child: InkWell(
borderRadius:
BorderRadius.all(Radius.circular(30)),
onTap:
backplay == BasicPlaybackState.playing
? null
: () {
audio.resumeAudio();
},
child: SizedBox(
height: 60,
width: 60,
child: Icon(
Icons.play_arrow,
size: 40,
color: Theme.of(context).accentColor,
),
),
),
),
),
IconButton(
padding: EdgeInsets.symmetric(horizontal: 30.0),
onPressed: backplay == BasicPlaybackState.playing
? () => audio.forwardAudio(30)
: null,
iconSize: 32.0,
icon: Icon(Icons.forward_30),
color: Colors.grey[500]),
],
),
);
},
),
),
Container(
height: 70.0,
padding: EdgeInsets.all(20),
alignment: Alignment.center,
child: Selector<AudioPlayerNotifier, String>(
selector: (_, audio) => audio.episode.title,
builder: (_, title, __) {
return Container(
child: LayoutBuilder(
builder: (context, size) {
var span = TextSpan(
text: title,
style: TextStyle(
fontWeight: FontWeight.bold, fontSize: 20));
var tp = TextPainter(
text: span,
maxLines: 1,
textDirection: TextDirection.ltr);
tp.layout(maxWidth: size.maxWidth);
if (tp.didExceedMaxLines) {
return Marquee(
text: title,
style: TextStyle(
fontWeight: FontWeight.bold, fontSize: 18),
scrollAxis: Axis.horizontal,
crossAxisAlignment: CrossAxisAlignment.start,
blankSpace: 30.0,
velocity: 50.0,
pauseAfterRound: Duration(seconds: 1),
startPadding: 30.0,
accelerationDuration: Duration(seconds: 1),
accelerationCurve: Curves.linear,
decelerationDuration: Duration(milliseconds: 500),
decelerationCurve: Curves.easeOut,
);
} else {
return Text(
title,
style: TextStyle(
fontWeight: FontWeight.bold, fontSize: 20),
);
}
},
),
);
},
),
),
Spacer(),
Selector<AudioPlayerNotifier, Tuple3<EpisodeBrief, bool, bool>>(
selector: (_, audio) => Tuple3(
audio.episode, audio.stopOnComplete, audio.startSleepTimer),
builder: (_, data, __) {
return Container(
padding: EdgeInsets.all(5.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(
height: 30.0,
width: 30.0,
child: CircleAvatar(
backgroundImage:
FileImage(File("${data.item1.imagePath}")),
),
),
Container(
padding: EdgeInsets.symmetric(horizontal: 5.0),
width: 150,
child: Text(
data.item1.feedTitle,
maxLines: 1,
overflow: TextOverflow.fade,
),
),
Spacer(),
LastPosition(),
IconButton(
padding: EdgeInsets.zero,
onPressed: () => Navigator.push(
context,
SlideUptRoute(
page: EpisodeDetail(
episodeItem: data.item1,
heroTag: 'playpanel')),
),
icon: Icon(Icons.info),
),
],
),
);
},
),
]),
);
}
Widget _playlist(BuildContext context) {
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
return Container(
@ -446,7 +158,7 @@ class _PlayerWidgetState extends State<PlayerWidget> {
// color: context.primaryColorDark,
alignment: Alignment.centerLeft,
child: Text(
'Queue',
'Playlist',
style: TextStyle(
color: Theme.of(context).accentColor,
fontWeight: FontWeight.bold,
@ -684,7 +396,7 @@ class _PlayerWidgetState extends State<PlayerWidget> {
TabBarView(
children: <Widget>[
SleepMode(),
_controlPanel(context),
ControlPanel(),
_playlist(context),
],
),
@ -1432,3 +1144,434 @@ class SleepModeState extends State<SleepMode>
);
}
}
class ControlPanel extends StatefulWidget {
ControlPanel({Key key}) : super(key: key);
@override
_ControlPanelState createState() => _ControlPanelState();
}
class _ControlPanelState extends State<ControlPanel>
with SingleTickerProviderStateMixin {
final _speedToSelect = [0.5, 0.6, 0.8, 1.0, 1.2, 1.5, 2.0];
double _speedSelected;
double _setSpeed;
AnimationController _controller;
Animation<double> _animation;
List<BoxShadow> customShadow(double scale) => [
BoxShadow(
blurRadius: 26 * (1 - scale),
offset: Offset(-6, -6) * (1 - scale),
color: Colors.white),
BoxShadow(
blurRadius: 8 * (1 - scale),
offset: Offset(2, 2) * (1 - scale),
color: Colors.grey[600].withOpacity(0.4))
];
List<BoxShadow> customShadowNight(double scale) => [
BoxShadow(
blurRadius: 6 * (1 - scale),
offset: Offset(-1, -1) * (1 - scale),
color: Colors.grey[100].withOpacity(0.3)),
BoxShadow(
blurRadius: 8 * (1 - scale),
offset: Offset(2, 2) * (1 - scale),
color: Colors.black)
];
@override
void initState() {
_speedSelected = 0;
_setSpeed = 0;
_controller =
AnimationController(vsync: this, duration: Duration(milliseconds: 400));
_animation = Tween<double>(begin: 0.0, end: 1.0).animate(_controller)
..addListener(() {
setState(() => _setSpeed = _animation.value);
});
super.initState();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
return Container(
color: Theme.of(context).primaryColor,
height: 300,
padding: EdgeInsets.symmetric(horizontal: 10.0),
child: Stack(
children: <Widget>[
Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Consumer<AudioPlayerNotifier>(
builder: (_, data, __) {
Color _c =
(Theme.of(context).brightness == Brightness.light)
? data.episode.primaryColor.colorizedark()
: data.episode.primaryColor.colorizeLight();
return Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(
padding:
EdgeInsets.only(top: 10, left: 10, right: 10),
child: SliderTheme(
data: SliderTheme.of(context).copyWith(
activeTrackColor: Theme.of(context).brightness ==
Brightness.dark
? Colors.black38
: Colors.grey[400],
inactiveTrackColor:
Theme.of(context).primaryColorDark,
trackHeight: 20.0,
trackShape: MyRectangularTrackShape(),
thumbColor: Theme.of(context).accentColor,
thumbShape: MyRoundSliderThumpShape(
enabledThumbRadius: 10.0,
disabledThumbRadius: 10.0,
thumbCenterColor: _c),
overlayColor:
Theme.of(context).accentColor.withAlpha(32),
overlayShape:
RoundSliderOverlayShape(overlayRadius: 4.0),
),
child: Slider(
value: data.seekSliderValue,
onChanged: (double val) {
audio.sliderSeek(val);
}),
),
),
Container(
height: 20.0,
padding: EdgeInsets.symmetric(horizontal: 30.0),
child: Row(
children: <Widget>[
Text(
_stringForSeconds(
data.backgroundAudioPosition / 1000) ??
'',
style: TextStyle(fontSize: 10),
),
Expanded(
child: Container(
alignment: Alignment.center,
child: data.remoteErrorMessage != null
? Text(data.remoteErrorMessage,
style: const TextStyle(
color: const Color(0xFFFF0000)))
: Text(
data.audioState ==
BasicPlaybackState
.buffering ||
data.audioState ==
BasicPlaybackState
.connecting ||
data.audioState ==
BasicPlaybackState.none
? 'Buffring...'
: '',
style: TextStyle(
color: Theme.of(context)
.accentColor),
),
),
),
Text(
_stringForSeconds(
data.backgroundAudioDuration / 1000) ??
'',
style: TextStyle(fontSize: 10),
),
],
),
),
],
);
},
),
Container(
height: 100,
child: Selector<AudioPlayerNotifier, BasicPlaybackState>(
selector: (_, audio) => audio.audioState,
builder: (_, backplay, __) {
return Material(
color: Colors.transparent,
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
padding: EdgeInsets.symmetric(horizontal: 30.0),
onPressed:
backplay == BasicPlaybackState.playing
? () => audio.forwardAudio(-10)
: null,
iconSize: 32.0,
icon: Icon(Icons.replay_10),
color: Colors.grey[500]),
Container(
margin: EdgeInsets.symmetric(horizontal: 30),
height: 60,
width: 60,
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
shape: BoxShape.circle,
border: Border.all(
color: Theme.of(context).brightness ==
Brightness.dark
? Colors.black12
: Colors.white10,
width: 1),
boxShadow: Theme.of(context).brightness ==
Brightness.dark
? _customShadowNight
: _customShadow),
child: backplay == BasicPlaybackState.playing
? Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.all(
Radius.circular(30)),
onTap: backplay ==
BasicPlaybackState.playing
? () {
audio.pauseAduio();
}
: null,
child: SizedBox(
height: 60,
width: 60,
child: Icon(
Icons.pause,
size: 40,
),
),
),
)
: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.all(
Radius.circular(30)),
onTap: backplay ==
BasicPlaybackState.playing
? null
: () {
audio.resumeAudio();
},
child: SizedBox(
height: 60,
width: 60,
child: Icon(
Icons.play_arrow,
size: 40,
color:
Theme.of(context).accentColor,
),
),
),
),
),
IconButton(
padding: EdgeInsets.symmetric(horizontal: 30.0),
onPressed:
backplay == BasicPlaybackState.playing
? () => audio.forwardAudio(30)
: null,
iconSize: 32.0,
icon: Icon(Icons.forward_30),
color: Colors.grey[500]),
],
),
);
},
),
),
Container(
height: 70.0,
padding:
EdgeInsets.only(left: 60, right: 60, bottom: 10, top: 10),
alignment: Alignment.center,
child: Selector<AudioPlayerNotifier, String>(
selector: (_, audio) => audio.episode.title,
builder: (_, title, __) {
return Container(
child: LayoutBuilder(
builder: (context, size) {
var span = TextSpan(
text: title,
style: TextStyle(
fontWeight: FontWeight.bold, fontSize: 20));
var tp = TextPainter(
text: span,
maxLines: 1,
textDirection: TextDirection.ltr);
tp.layout(maxWidth: size.maxWidth);
if (tp.didExceedMaxLines) {
return Marquee(
text: title,
style: TextStyle(
fontWeight: FontWeight.bold, fontSize: 18),
scrollAxis: Axis.horizontal,
crossAxisAlignment: CrossAxisAlignment.start,
blankSpace: 30.0,
velocity: 50.0,
pauseAfterRound: Duration(seconds: 1),
startPadding: 30.0,
accelerationDuration: Duration(seconds: 1),
accelerationCurve: Curves.linear,
decelerationDuration:
Duration(milliseconds: 500),
decelerationCurve: Curves.easeOut,
);
} else {
return Text(
title,
style: TextStyle(
fontWeight: FontWeight.bold, fontSize: 20),
);
}
},
),
);
},
),
),
Spacer(),
Selector<AudioPlayerNotifier, Tuple3<EpisodeBrief, bool, bool>>(
selector: (_, audio) => Tuple3(audio.episode,
audio.stopOnComplete, audio.startSleepTimer),
builder: (_, data, __) {
return Container(
padding:
EdgeInsets.only(left: 5.0, right: 5.0, bottom: 5.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
InkWell(
onTap: () => Navigator.push(
context,
SlideUptRoute(
page: EpisodeDetail(
episodeItem: data.item1,
heroTag: 'playpanel'))),
child: Container(
height: 30.0,
width: 30.0,
child: CircleAvatar(
backgroundImage:
FileImage(File("${data.item1.imagePath}")),
),
),
),
Container(
padding: EdgeInsets.symmetric(horizontal: 5.0),
width: 150,
child: Text(
data.item1.feedTitle,
maxLines: 1,
overflow: TextOverflow.fade,
),
),
Spacer(),
LastPosition(),
IconButton(
padding: EdgeInsets.zero,
onPressed: () {
if (_setSpeed == 0)
_controller.forward();
else
_controller.reverse();
},
icon: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Transform.rotate(
angle: math.pi * _setSpeed,
child: Text('X')),
Selector<AudioPlayerNotifier, double>(
selector: (_, audio) =>
audio.currentSpeed ?? 1.0,
builder: (context, value, child) =>
Text(value.toString()),
),
],
),
),
],
),
);
},
),
]),
Positioned(
right: 0,
bottom: 50.0,
child: _setSpeed == 0
? Center()
: SingleChildScrollView(
padding: EdgeInsets.all(10.0),
scrollDirection: Axis.horizontal,
child: Row(
children: _speedToSelect
.map<Widget>((e) => InkWell(
onTap: () {
if (_setSpeed == 1) {
setState(() => _speedSelected = e);
audio.setSpeed(e);
}
},
child: Container(
height: 30,
width: 30,
margin: EdgeInsets.symmetric(horizontal: 5),
decoration:
e == _speedSelected && _setSpeed > 0
? BoxDecoration(
color: context.accentColor,
shape: BoxShape.circle,
boxShadow: context.brightness ==
Brightness.light
? customShadow(1.0)
: customShadowNight(1.0),
)
: BoxDecoration(
color: context.primaryColor,
shape: BoxShape.circle,
boxShadow: context.brightness ==
Brightness.light
? customShadow(1 - _setSpeed)
: customShadowNight(
1 - _setSpeed)),
alignment: Alignment.center,
child: _setSpeed > 0
? Text(e.toString(),
style: TextStyle(
fontWeight: FontWeight.bold,
color: e == _speedSelected
? Colors.white
: null))
: Center(),
),
))
.toList(),
),
),
),
],
),
);
}
}

View File

@ -9,17 +9,17 @@ import 'package:tsacdop/class/audiostate.dart';
import 'package:tsacdop/util/custompaint.dart';
import 'package:tuple/tuple.dart';
import 'package:line_icons/line_icons.dart';
import 'package:tsacdop/class/episodebrief.dart';
import 'package:tsacdop/class/importompl.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/episodes/episodedetail.dart';
import 'package:tsacdop/podcasts/podcastdetail.dart';
import 'package:tsacdop/podcasts/podcastmanage.dart';
import 'package:tsacdop/util/pageroute.dart';
import 'package:tsacdop/util/colorize.dart';
import 'package:tsacdop/util/context_extension.dart';
import '../class/episodebrief.dart';
import '../class/podcast_group.dart';
import '../class/podcastlocal.dart';
import '../local_storage/sqflite_localpodcast.dart';
import '../episodes/episodedetail.dart';
import '../podcasts/podcastdetail.dart';
import '../podcasts/podcastmanage.dart';
import '../util/pageroute.dart';
import '../util/colorize.dart';
import '../util/context_extension.dart';
class ScrollPodcasts extends StatefulWidget {
@override
@ -284,31 +284,29 @@ class _ScrollPodcastsState extends State<ScrollPodcasts> {
],
),
),
Consumer<ImportOmpl>(
builder: (_, ompl, __) => Container(
height: (_width - 20) / 3 + 40,
margin: EdgeInsets.only(left: 10, right: 10),
decoration: BoxDecoration(
color: Theme.of(context).scaffoldBackgroundColor,
),
child: TabBarView(
children: groups[_groupIndex]
.podcasts
.map<Widget>((PodcastLocal podcastLocal) {
return Container(
decoration: BoxDecoration(
color: Theme.of(context).brightness ==
Brightness.light
? Theme.of(context).primaryColor
: Colors.black12),
margin: EdgeInsets.symmetric(horizontal: 5.0),
key: ObjectKey(podcastLocal.title),
child: PodcastPreview(
podcastLocal: podcastLocal,
),
);
}).toList(),
),
Container(
height: (_width - 20) / 3 + 40,
margin: EdgeInsets.only(left: 10, right: 10),
decoration: BoxDecoration(
color: Theme.of(context).scaffoldBackgroundColor,
),
child: TabBarView(
children: groups[_groupIndex]
.podcasts
.map<Widget>((PodcastLocal podcastLocal) {
return Container(
decoration: BoxDecoration(
color: Theme.of(context).brightness ==
Brightness.light
? Theme.of(context).primaryColor
: Colors.black12),
margin: EdgeInsets.symmetric(horizontal: 5.0),
key: ObjectKey(podcastLocal.title),
child: PodcastPreview(
podcastLocal: podcastLocal,
),
);
}).toList(),
),
),
],
@ -557,12 +555,16 @@ class ShowEpisode extends StatelessWidget {
),
),
Spacer(),
Selector<AudioPlayerNotifier, EpisodeBrief>(
selector: (_, audio) => audio.episode,
Selector<AudioPlayerNotifier,
Tuple2<EpisodeBrief, bool>>(
selector: (_, audio) => Tuple2(
audio.episode, audio.playerRunning),
builder: (_, data, __) {
return (episodes[index]
.enclosureUrl ==
data?.enclosureUrl)
.enclosureUrl ==
data.item1
?.enclosureUrl &&
data.item2)
? Container(
height: 20,
width: 20,
@ -624,11 +626,10 @@ class ShowEpisode extends StatelessWidget {
alignment: Alignment.center,
child: Text(
_stringForSeconds(
episodes[index]
.duration
.toDouble())
.toString() +
'|',
episodes[index]
.duration
.toDouble())
.toString(),
style: TextStyle(
fontSize: _width / 35,
// color: _c,
@ -637,6 +638,20 @@ class ShowEpisode extends StatelessWidget {
),
)
: Center(),
episodes[index].duration == 0 ||
episodes[index].enclosureLength ==
null ||
episodes[index].enclosureLength ==
0
? Center()
: Text(
'|',
style: TextStyle(
fontSize: _width / 35,
// color: _c,
// fontStyle: FontStyle.italic,
),
),
episodes[index].enclosureLength != null &&
episodes[index].enclosureLength !=
0

View File

@ -162,7 +162,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
PlaylistButton(),
],
),
Container(height: 2, color: Theme.of(context).primaryColorDark),
Container(height: 2, color: context.primaryColor),
],
),
);

View File

@ -405,7 +405,7 @@ class _DismissibleContainerState extends State<DismissibleContainer> {
widget.episode.duration != 0
? _episodeTag(
(widget.episode.duration ~/ 60).toString() +
'mins',
'min',
Colors.cyan[300])
: Center(),
widget.episode.enclosureLength != null

View File

@ -28,12 +28,10 @@ class _SlideIntroState extends State<SlideIntro> {
color: Colors.grey[600].withOpacity(0.4))
];
PageController _controller;
int _index;
double _position;
@override
void initState() {
super.initState();
_index = 0;
_position = 0;
_controller = PageController()
..addListener(() {
@ -78,7 +76,8 @@ class _SlideIntroState extends State<SlideIntro> {
color: Colors.grey[100].withOpacity(0.5),
width: MediaQuery.of(context).size.width,
// alignment: Alignment.center,
padding: EdgeInsets.only(left: 40, right: 20, bottom: 30, top: 20),
padding:
EdgeInsets.only(left: 40, right: 20, bottom: 30, top: 20),
child: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
@ -182,7 +181,7 @@ class _SlideIntroState extends State<SlideIntro> {
height: 40,
width: 80,
decoration: BoxDecoration(
border: Border.all(width:1, color: Colors.white),
border: Border.all(width: 1, color: Colors.white),
borderRadius: BorderRadius.all(Radius.circular(20)),
color: Colors.white,
boxShadow: _customShadow,
@ -194,7 +193,7 @@ class _SlideIntroState extends State<SlideIntro> {
borderRadius:
BorderRadius.all(Radius.circular(20)),
onTap: () => _controller.animateToPage(
_index + 1,
_position.toInt() + 1,
duration: Duration(milliseconds: 200),
curve: Curves.bounceIn),
child: SizedBox(

View File

@ -119,7 +119,6 @@ class DBHelper {
}
Future savePodcastLocal(PodcastLocal podcastLocal) async {
print('podcast saved in sqllite');
int _milliseconds = DateTime.now().millisecondsSinceEpoch;
var dbClient = await database;
await dbClient.transaction((txn) async {
@ -163,8 +162,11 @@ class DBHelper {
var dbClient = await database;
List<Map> list = await dbClient.rawQuery(
'SELECT background_image, hosts FROM PodcastLocal WHERE id = ?', [id]);
List<String> data = [list.first['background_image'], list.first['hosts']];
return data;
if (list.length > 0) {
List<String> data = [list.first['background_image'], list.first['hosts']];
return data;
}
return ['', ''];
}
Future delPodcastLocal(String id) async {
@ -301,7 +303,7 @@ class DBHelper {
RegExp yyyy = RegExp(r'[1-2][0-9]{3}');
RegExp hhmm = RegExp(r'[0-2][0-9]\:[0-5][0-9]');
RegExp ddmmm = RegExp(r'[0-3][0-9]\s[A-Z][a-z]{2}');
RegExp mmDd = RegExp(r'([0-1]|\s)[0-9]\-[0-3][0-9]');
RegExp mmDd = RegExp(r'([1-2][0-9]{3}\-[0-1]|\s)[0-9]\-[0-3][0-9]');
// RegExp timezone
RegExp z = RegExp(r'(\+|\-)[0-1][0-9]00');
String timezone = z.stringMatch(pubDate);
@ -331,8 +333,10 @@ class DBHelper {
.parse(month + year + time);
} else if (year != null && time != null && month == null) {
String month = mmDd.stringMatch(pubDate);
date = DateFormat('mm-dd yyyy HH:mm', 'en_US')
.parse(month + ' ' + year + ' ' + time);
date = DateFormat('yyyy-MM-dd HH:mm', 'en_US')
.parse(month + ' ' + time);
print(month);
print(date.toString());
} else {
date = DateTime.now().toUtc();
}
@ -426,65 +430,78 @@ class DBHelper {
}
Future<int> updatePodcastRss(PodcastLocal podcastLocal) async {
Response response = await Dio().get(podcastLocal.rssUrl);
var feed = RssFeed.parse(response.data);
String url, description;
feed.items.removeWhere((item) => item == null);
int result = feed.items.length;
BaseOptions options = new BaseOptions(
connectTimeout: 20000,
receiveTimeout: 20000,
);
Response response = await Dio(options).get(podcastLocal.rssUrl);
if (response.statusCode == 200) {
var feed = RssFeed.parse(response.data);
String url, description;
feed.items.removeWhere((item) => item == null);
int result = feed.items.length;
var dbClient = await database;
int count = Sqflite.firstIntValue(await dbClient.rawQuery(
'SELECT COUNT(*) FROM Episodes WHERE feed_id = ?', [podcastLocal.id]));
var dbClient = await database;
int count = Sqflite.firstIntValue(await dbClient.rawQuery(
'SELECT COUNT(*) FROM Episodes WHERE feed_id = ?',
[podcastLocal.id]));
await dbClient.rawUpdate(
"UPDATE Episodes SET is_new = 0 WHERE feed_id = ?", [podcastLocal.id]);
await dbClient.rawUpdate(
"UPDATE Episodes SET is_new = 0 WHERE feed_id = ?",
[podcastLocal.id]);
for (int i = 0; i < result; i++) {
print(feed.items[i].title);
description = _getDescription(feed.items[i].content.value ?? '',
feed.items[i].description ?? '', feed.items[i].itunes.summary ?? '');
for (int i = 0; i < result; i++) {
print(feed.items[i].title);
description = _getDescription(
feed.items[i].content.value ?? '',
feed.items[i].description ?? '',
feed.items[i].itunes.summary ?? '');
if (feed.items[i].enclosure?.url != null) {
_isXimalaya(feed.items[i].enclosure.url)
? url = feed.items[i].enclosure.url.split('=').last
: url = feed.items[i].enclosure.url;
}
if (feed.items[i].enclosure?.url != null) {
_isXimalaya(feed.items[i].enclosure.url)
? url = feed.items[i].enclosure.url.split('=').last
: url = feed.items[i].enclosure.url;
}
final title = feed.items[i].itunes.title ?? feed.items[i].title;
final length = feed.items[i]?.enclosure?.length ?? 0;
final pubDate = feed.items[i].pubDate;
final date = _parsePubDate(pubDate);
final milliseconds = date.millisecondsSinceEpoch;
final duration = feed.items[i].itunes.duration?.inSeconds ?? 0;
final explicit = _getExplicit(feed.items[i].itunes.explicit);
final title = feed.items[i].itunes.title ?? feed.items[i].title;
final length = feed.items[i]?.enclosure?.length ?? 0;
final pubDate = feed.items[i].pubDate;
final date = _parsePubDate(pubDate);
final milliseconds = date.millisecondsSinceEpoch;
final duration = feed.items[i].itunes.duration?.inSeconds ?? 0;
final explicit = _getExplicit(feed.items[i].itunes.explicit);
if (url != null) {
await dbClient.transaction((txn) async {
await txn.rawInsert(
"""INSERT OR IGNORE INTO Episodes(title, enclosure_url, enclosure_length, pubDate,
if (url != null) {
await dbClient.transaction((txn) async {
await txn.rawInsert(
"""INSERT OR IGNORE INTO Episodes(title, enclosure_url, enclosure_length, pubDate,
description, feed_id, milliseconds, duration, explicit, media_id, is_new) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1)""",
[
title,
url,
length,
pubDate,
description,
podcastLocal.id,
milliseconds,
duration,
explicit,
url,
]);
});
[
title,
url,
length,
pubDate,
description,
podcastLocal.id,
milliseconds,
duration,
explicit,
url,
]);
});
}
}
}
int countUpdate = Sqflite.firstIntValue(await dbClient.rawQuery(
'SELECT COUNT(*) FROM Episodes WHERE feed_id = ?', [podcastLocal.id]));
int countUpdate = Sqflite.firstIntValue(await dbClient.rawQuery(
'SELECT COUNT(*) FROM Episodes WHERE feed_id = ?',
[podcastLocal.id]));
await dbClient.rawUpdate(
"""UPDATE PodcastLocal SET update_count = ?, episode_count = ? WHERE id = ?""",
[countUpdate - count, countUpdate, podcastLocal.id]);
return countUpdate - count;
await dbClient.rawUpdate(
"""UPDATE PodcastLocal SET update_count = ?, episode_count = ? WHERE id = ?""",
[countUpdate - count, countUpdate, podcastLocal.id]);
return countUpdate - count;
} else {
throw ("network error");
}
}
Future<List<EpisodeBrief>> getRssItem(String id, int i, bool reverse) async {

View File

@ -3,13 +3,13 @@ import 'package:provider/provider.dart';
import 'package:flutter/services.dart';
import 'package:flutter_downloader/flutter_downloader.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';
import 'package:tsacdop/class/settingstate.dart';
import 'package:tsacdop/class/download_state.dart';
import 'class/podcast_group.dart';
import 'home/appbar/addpodcast.dart';
import 'class/audiostate.dart';
import 'class/settingstate.dart';
import 'class/download_state.dart';
import 'class/refresh_podcast.dart';
import 'class/subscribe_podcast.dart';
import 'intro_slider/app_intro.dart';
final SettingState themeSetting = SettingState();
@ -23,9 +23,11 @@ Future main() async {
ChangeNotifierProvider(create: (_) => themeSetting),
ChangeNotifierProvider(create: (_) => AudioPlayerNotifier()),
ChangeNotifierProvider(create: (_) => GroupList()),
ChangeNotifierProvider(create: (_) => ImportOmpl()),
ChangeNotifierProvider(create: (_) => DownloadState(),
)
ChangeNotifierProvider(create: (_) => SubscribeWorker()),
ChangeNotifierProvider(create: (_) => RefreshWorker()),
ChangeNotifierProvider(
create: (_) => DownloadState(),
),
],
child: MyApp(),
),
@ -72,7 +74,10 @@ class MyApp extends StatelessWidget {
darkTheme: ThemeData.dark().copyWith(
accentColor: setting.accentSetColor,
primaryColorDark: Colors.grey[800],
// scaffoldBackgroundColor: Colors.black87,
scaffoldBackgroundColor: setting.realDark ? Colors.black87 : null,
primaryColor: setting.realDark ? Colors.black : null,
popupMenuTheme: PopupMenuThemeData()
.copyWith(color: setting.realDark ? Colors.black87 : null),
appBarTheme: AppBarTheme(elevation: 0),
),
home: setting.showIntro ? SlideIntro(goto: Goto.home) : MyHomePage(),

View File

@ -38,19 +38,26 @@ class _PodcastDetailState extends State<PodcastDetail> {
List<PodcastHost> hosts;
Future _updateRssItem(PodcastLocal podcastLocal) async {
var dbHelper = DBHelper();
final result = await dbHelper.updatePodcastRss(podcastLocal);
if (result == 0) {
try {
final result = await dbHelper.updatePodcastRss(podcastLocal);
if (result == 0) {
Fluttertoast.showToast(
msg: 'No Update',
gravity: ToastGravity.TOP,
);
} else {
Fluttertoast.showToast(
msg: 'Updated $result Episodes',
gravity: ToastGravity.TOP,
);
Provider.of<GroupList>(context, listen: false)
.updatePodcast(podcastLocal.id);
}
} catch (e) {
Fluttertoast.showToast(
msg: 'No Update',
msg: 'Update failed, network error',
gravity: ToastGravity.TOP,
);
} else {
Fluttertoast.showToast(
msg: 'Updated $result Episodes',
gravity: ToastGravity.TOP,
);
Provider.of<GroupList>(context, listen: false)
.updatePodcast(podcastLocal);
}
if (mounted) setState(() {});
}
@ -449,11 +456,13 @@ class _PodcastDetailState extends State<PodcastDetail> {
itemBuilder: (context) => [
PopupMenuItem(
value: 0,
child: Text('Newest first'),
child:
Text('Newest first'),
),
PopupMenuItem(
value: 1,
child: Text('Oldest first'),
child:
Text('Oldest first'),
)
],
onSelected: (value) {
@ -661,4 +670,3 @@ class _AboutPodcastState extends State<AboutPodcast> {
);
}
}

View File

@ -301,11 +301,6 @@ class _PodcastCardState extends State<PodcastCard> {
Brightness.light
? Color.fromRGBO(113, 113, 113, 1)
: Color.fromRGBO(15, 15, 15, 1),
// statusBarColor:
// Theme.of(context).brightness ==
// Brightness.light
// ? Color.fromRGBO(113, 113, 113, 1)
// : Color.fromRGBO(5, 5, 5, 1),
),
child: AlertDialog(
elevation: 1,

View File

@ -59,6 +59,28 @@ class Libries extends StatelessWidget {
},
).toList(),
),
Container(
height: 30.0,
padding: EdgeInsets.symmetric(horizontal: 80),
alignment: Alignment.centerLeft,
child: Text('Fonts',
style: Theme.of(context)
.textTheme
.bodyText1
.copyWith(color: Theme.of(context).accentColor)),
),
Column(
children: fonts.map<Widget>(
(e) {
return ListTile(
contentPadding: EdgeInsets.symmetric(horizontal: 80),
onTap: () => _launchUrl(e.link),
title: Text(e.name),
subtitle: Text(e.license),
);
},
).toList(),
),
Container(
height: 30.0,
padding: EdgeInsets.symmetric(horizontal: 80),

View File

@ -1,50 +1,72 @@
const String apacheLicense = "Apache License 2.0";
const String apacheLicense = "Apache License 2.0";
const String mit = "MIT License";
const String bsd ="BSD 3-Clause";
const String bsd = "BSD 3-Clause";
const String gpl = "GPL 3.0";
const String font = "Open Font License";
class Libries {
String name; String license; String link;
String name;
String license;
String link;
Libries(this.name, this.license, this.link);
}
List<Libries> google = [
Libries('Android X', apacheLicense, 'https://source.android.com/setup/start/licenses'),
Libries('Flutter', bsd, 'https://github.com/flutter/flutter/blob/master/LICENSE')
Libries('Android X', apacheLicense,
'https://source.android.com/setup/start/licenses'),
Libries(
'Flutter', bsd, 'https://github.com/flutter/flutter/blob/master/LICENSE')
];
List<Libries> fonts = [
Libries('Libre Baskerville', font,
"https://fonts.google.com/specimen/Libre+Baskerville"),
Libries('Teko', font, "https://fonts.google.com/specimen/Teko")
];
List<Libries> plugins = [
Libries('webfeed', mit, 'https://pub.dev/packages/webfeed'),
Libries('json_annotation',bsd, 'https://pub.dev/packages/json_annotation'),
Libries('json_annotation', bsd, 'https://pub.dev/packages/json_annotation'),
Libries('sqflite', mit, 'https://pub.dev/packages/sqflite'),
Libries('flutter_html', mit, 'https://pub.dev/packages/flutter_html'),
Libries('path_provider', bsd, 'https://pub.dev/packages/path_provider'),
Libries('color_thief_flutter', mit, 'https://pub.dev/packages/color_thief_flutter'),
Libries('color_thief_flutter', mit,
'https://pub.dev/packages/color_thief_flutter'),
Libries('provider', mit, 'https://pub.dev/packages/provider'),
Libries('google_fonts', apacheLicense, 'https://pub.dev/packages/google_fonts'),
Libries(
'google_fonts', apacheLicense, 'https://pub.dev/packages/google_fonts'),
Libries('dio', mit, 'https://pub.dev/packages/dio'),
Libries('file_picker', mit, 'https://pub.dev/packages/file_picker'),
Libries('xml', mit, 'https://pub.dev/packages/xml'),
Libries('marquee', mit, 'https://pub.dev/packages/marquee'),
Libries('flutter_downloader', bsd, 'https://pub.dev/packages/flutter_downloader'),
Libries('permission_handler', mit, 'https://pub.dev/packages/permission_handler'),
Libries(
'flutter_downloader', bsd, 'https://pub.dev/packages/flutter_downloader'),
Libries(
'permission_handler', mit, 'https://pub.dev/packages/permission_handler'),
Libries('fluttertoast', mit, 'https://pub.dev/packages/fluttertoast'),
Libries('intl', bsd, 'https://pub.dev/packages/intl'),
Libries('url_launcher', bsd, 'https://pub.dev/packages/url_launcher'),
Libries('image', apacheLicense, 'https://pub.dev/packages/image'),
Libries('shared_preferences', bsd, 'https://pub.dev/packages/shared_preferences'),
Libries(
'shared_preferences', bsd, 'https://pub.dev/packages/shared_preferences'),
Libries('uuid', mit, 'https://pub.dev/packages/uuid'),
Libries('tuple', bsd, 'https://pub.dev/packages/tuple'),
Libries('cached_network_image', mit, 'https://pub.dev/packages/cached_network_image'),
Libries('cached_network_image', mit,
'https://pub.dev/packages/cached_network_image'),
Libries('workmanager', mit, 'https://pub.dev/packages/workmanager'),
Libries('flutter_colorpicker', mit, 'https://pub.dev/packages/flutter_colorpicker'),
Libries('flutter_colorpicker', mit,
'https://pub.dev/packages/flutter_colorpicker'),
Libries('app_settings', mit, 'https://pub.dev/packages/app_settings'),
Libries('fl_chart', bsd, 'https://pub.dev/packages/fl_chart'),
Libries('audio_service', mit, 'https://pub.dev/packages/audio_service'),
Libries('just_audio', apacheLicense, 'https://pub.dev/packages/just_audio'),
Libries('line_icons', gpl, 'https://pub.dev/packages/line_icons'),
Libries('flutter_file_dialog', bsd, 'https://pub.dev/packages/flutter_file_dialog'),
Libries('flutter_file_dialog', bsd,
'https://pub.dev/packages/flutter_file_dialog'),
Libries('flutter_linkify', mit, 'https://pub.dev/packages/flutter_linkify'),
Libries('extended_nested_scroll_view', mit, 'https://pub.dev/packages/extended_nested_scroll_view'),
Libries('extended_nested_scroll_view', mit,
'https://pub.dev/packages/extended_nested_scroll_view'),
Libries('connectivity', bsd, 'https://pub.dev/packages/connectivity'),
Libries('Rxdart', apacheLicense, 'https://pub.dev/packages/rxdart')
];
Libries('Rxdart', apacheLicense, 'https://pub.dev/packages/rxdart'),
Libries('flutter_isolate', mit, 'https://pub.dev/packages/flutter_isolate'),
];

View File

@ -33,7 +33,7 @@ class Settings extends StatelessWidget {
_exportOmpl() async {
var dbHelper = DBHelper();
List<PodcastLocal> podcastList = await dbHelper.getPodcastLocalAll();
var ompl = omplBuilder(podcastList);
var ompl = omplBuilder(podcastList.reversed.toList());
var tempdir = await getTemporaryDirectory();
var file = File(join(tempdir.path, 'tsacdop_ompl.xml'));
print(file.path);
@ -226,7 +226,8 @@ class Settings extends StatelessWidget {
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SlideIntro(goto: Goto.settings))),
builder: (context) =>
SlideIntro(goto: Goto.settings))),
contentPadding:
EdgeInsets.symmetric(horizontal: 25.0),
leading: Icon(LineIcons.columns_solid),

View File

@ -62,16 +62,11 @@ class ThemeSetting extends StatelessWidget {
AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle(
statusBarIconBrightness: Brightness.light,
systemNavigationBarColor:
Theme.of(context).brightness ==
Brightness.light
? Color.fromRGBO(113, 113, 113, 1)
: Color.fromRGBO(15, 15, 15, 1),
// statusBarColor:
// Theme.of(context).brightness ==
// Brightness.light
// ? Color.fromRGBO(113, 113, 113, 1)
// : Color.fromRGBO(5, 5, 5, 1),
systemNavigationBarColor:
Theme.of(context).brightness ==
Brightness.light
? Color.fromRGBO(113, 113, 113, 1)
: Color.fromRGBO(15, 15, 15, 1),
),
child: AlertDialog(
titlePadding: EdgeInsets.only(
@ -124,6 +119,22 @@ class ThemeSetting extends StatelessWidget {
title: Text('Theme'),
subtitle: Text('System default'),
),
ListTile(
contentPadding:
EdgeInsets.only(left: 80.0, right: 20, bottom: 10),
// leading: Icon(Icons.colorize),
title: Text('Real Dark'),
subtitle: Text(
'Turn on if you think the night is not dark enough'),
trailing: Selector<SettingState, bool>(
selector: (_, setting) => setting.realDark,
builder: (_, data, __) => Switch(
value: data,
onChanged: (boo) async {
settings.setRealDark = boo;
}),
),
),
Divider(height: 2),
ListTile(
onTap: () => showGeneralDialog(
@ -139,16 +150,11 @@ class ThemeSetting extends StatelessWidget {
AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle(
statusBarIconBrightness: Brightness.light,
systemNavigationBarColor:
Theme.of(context).brightness ==
Brightness.light
? Color.fromRGBO(113, 113, 113, 1)
: Color.fromRGBO(15, 15, 15, 1),
// statusBarColor:
// Theme.of(context).brightness ==
// Brightness.light
// ? Color.fromRGBO(113, 113, 113, 1)
// : Color.fromRGBO(5, 5, 5, 1),
systemNavigationBarColor:
Theme.of(context).brightness ==
Brightness.light
? Color.fromRGBO(113, 113, 113, 1)
: Color.fromRGBO(15, 15, 15, 1),
),
child: AlertDialog(
elevation: 1,

View File

@ -217,13 +217,16 @@ class EpisodeGrid extends StatelessWidget {
),
Spacer(),
Selector<AudioPlayerNotifier,
EpisodeBrief>(
selector: (_, audio) =>
Tuple2<EpisodeBrief, bool>>(
selector: (_, audio) => Tuple2(
audio.episode,
audio.playerRunning),
builder: (_, data, __) {
return (episodes[index]
.enclosureUrl ==
data?.enclosureUrl)
.enclosureUrl ==
data.item1
?.enclosureUrl &&
data.item2)
? Container(
height: 20,
width: 20,
@ -357,15 +360,31 @@ class EpisodeGrid extends StatelessWidget {
alignment: Alignment.center,
child: Text(
_stringForSeconds(
episodes[index]
.duration
.toDouble()) +
'|',
episodes[index]
.duration
.toDouble()),
style: TextStyle(
fontSize: _width / 35),
),
)
: Center(),
episodes[index].duration == 0 ||
episodes[index]
.enclosureLength ==
null ||
episodes[index]
.enclosureLength ==
0 ||
layout == Layout.three
? Center()
: Text(
'|',
style: TextStyle(
fontSize: _width / 35,
// color: _c,
// fontStyle: FontStyle.italic,
),
),
layout == Layout.two &&
episodes[index]
.enclosureLength !=

View File

@ -11,7 +11,7 @@ description: An easy-use podacasts player.
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 0.1.8
version: 0.1.9
environment:
sdk: ">=2.6.0 <3.0.0"
@ -22,7 +22,7 @@ dependencies:
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^0.1.2
cupertino_icons: ^0.1.3
dev_dependencies:
flutter_test:
@ -30,12 +30,12 @@ dev_dependencies:
json_annotation: ^3.0.1
sqflite: ^1.3.0
flutter_html: ^0.11.1
path_provider: ^1.6.1
path_provider: ^1.6.5
color_thief_flutter: ^1.0.2
provider: ^4.0.5
google_fonts: ^0.4.0
google_fonts: ^0.5.0+1
dio: ^3.0.9
file_picker: ^1.6.2
file_picker: ^1.6.3+1
xml: ^3.5.0
marquee: ^1.3.1
flutter_downloader: ^1.4.3
@ -47,12 +47,12 @@ dev_dependencies:
shared_preferences: ^0.5.6+1
uuid: ^2.0.4
tuple: ^1.0.3
cached_network_image: ^2.0.0
cached_network_image: ^2.1.0+1
workmanager: ^0.2.2
flutter_colorpicker: ^0.3.2
app_settings: ^3.0.1
fl_chart: ^0.9.0
audio_service: ^0.7.1
audio_service: ^0.7.2
just_audio:
git:
url: https://github.com/stonega/just_audio.git
@ -64,7 +64,8 @@ dev_dependencies:
extended_nested_scroll_view: ^0.4.0
connectivity: ^0.4.8+2
flare_flutter: ^2.0.1
rxdart: ^0.23.1
rxdart: ^0.24.0
flutter_isolate: ^1.0.0+11
# For information on the generic Dart part of this file, see the