1
0
mirror of https://github.com/stonega/tsacdop synced 2025-02-16 11:31:45 +01:00

A lot of bug fixed

This commit is contained in:
stonegate 2020-04-01 00:36:20 +08:00
parent 62100085b0
commit a1d004aa43
31 changed files with 1787 additions and 1273 deletions

View File

@ -2,7 +2,7 @@
[![CircleCI](https://circleci.com/gh/stonega/tsacdop.svg?style=svg)](https://circleci.com/gh/stonega/workflows/tsacdop/) [![CircleCI](https://circleci.com/gh/stonega/tsacdop.svg?style=svg)](https://circleci.com/gh/stonega/workflows/tsacdop/)
## About ## About
<p align="center"> <p align="center">
<img src="https://raw.githubusercontent.com/stonega/tsacdop/master/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png" art = "Logo"/> <img src="https://raw.githubusercontent.com/stonega/tsacdop/master/android/app/src/main/res/mipmap-xhdpi/ic_notification.png" art = "Logo"/>
</br> </br>
<img src="https://raw.githubusercontent.com/stonega/tsacdop/master/android/app/src/main/res/mipmap-xhdpi/text.png" art = "Tsacdop"/> <img src="https://raw.githubusercontent.com/stonega/tsacdop/master/android/app/src/main/res/mipmap-xhdpi/text.png" art = "Tsacdop"/>
</p> </p>
@ -18,7 +18,7 @@ The podcasts search engine is powered by [ListenNotes](https://listennotes.com).
## License ## License
Tsacdop is licensed under the [MIT](https://github.com/stonega/tsacdop/blob/master/LICENSE) license. Tsacdop is licensed under the [GPL V3.0](https://github.com/stonega/tsacdop/blob/master/LICENSE) license.
## Getting Started ## Getting Started

View File

@ -0,0 +1,4 @@
eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8
org.eclipse.jdt.core.compiler.compliance=1.8
org.eclipse.jdt.core.compiler.source=1.8

View File

@ -59,12 +59,10 @@ class PlayHistory {
} }
} }
class Playlist { class Playlist extends ChangeNotifier {
String name; String name;
DBHelper dbHelper = DBHelper(); DBHelper dbHelper = DBHelper();
// list of urls
//List<String> _urls;
//list of episodes
List<EpisodeBrief> _playlist; List<EpisodeBrief> _playlist;
//list of miediaitem //list of miediaitem
@ -73,14 +71,13 @@ class Playlist {
getPlaylist() async { getPlaylist() async {
List<String> urls = await storage.getStringList(); List<String> urls = await storage.getStringList();
print(urls);
if (urls.length == 0) { if (urls.length == 0) {
_playlist = []; _playlist = [];
} else { } else {
_playlist = []; _playlist = [];
await Future.forEach(urls, (url) async { await Future.forEach(urls, (url) async {
EpisodeBrief episode = await dbHelper.getRssItemWithUrl(url); EpisodeBrief episode = await dbHelper.getRssItemWithUrl(url);
if(episode != null) _playlist.add(episode); if (episode != null) _playlist.add(episode);
}); });
} }
print('Playlist: ' + _playlist.length.toString()); print('Playlist: ' + _playlist.length.toString());
@ -89,7 +86,6 @@ class Playlist {
savePlaylist() async { savePlaylist() async {
List<String> urls = []; List<String> urls = [];
urls.addAll(_playlist.map((e) => e.enclosureUrl)); urls.addAll(_playlist.map((e) => e.enclosureUrl));
print(urls);
await storage.saveStringList(urls); await storage.saveStringList(urls);
} }
@ -103,10 +99,12 @@ class Playlist {
await savePlaylist(); await savePlaylist();
} }
delFromPlaylist(EpisodeBrief episodeBrief) async { Future<int> delFromPlaylist(EpisodeBrief episodeBrief) async {
int index = _playlist.indexOf(episodeBrief);
_playlist _playlist
.removeWhere((item) => item.enclosureUrl == episodeBrief.enclosureUrl); .removeWhere((item) => item.enclosureUrl == episodeBrief.enclosureUrl);
await savePlaylist(); await savePlaylist();
return index;
} }
} }
@ -115,6 +113,7 @@ class AudioPlayerNotifier extends ChangeNotifier {
KeyValueStorage storage = KeyValueStorage('audioposition'); KeyValueStorage storage = KeyValueStorage('audioposition');
EpisodeBrief _episode; EpisodeBrief _episode;
Playlist _queue = Playlist(); Playlist _queue = Playlist();
bool _queueUpdate = false;
BasicPlaybackState _audioState = BasicPlaybackState.none; BasicPlaybackState _audioState = BasicPlaybackState.none;
bool _playerRunning = false; bool _playerRunning = false;
bool _noSlide = true; bool _noSlide = true;
@ -127,7 +126,6 @@ class AudioPlayerNotifier extends ChangeNotifier {
bool _stopOnComplete = false; bool _stopOnComplete = false;
Timer _stopTimer; Timer _stopTimer;
int _timeLeft = 0; int _timeLeft = 0;
//Show stopwatch after user setting timer.
bool _showStopWatch = false; bool _showStopWatch = false;
double _switchValue = 0; double _switchValue = 0;
bool _autoPlay = true; bool _autoPlay = true;
@ -143,6 +141,7 @@ class AudioPlayerNotifier extends ChangeNotifier {
bool get playerRunning => _playerRunning; bool get playerRunning => _playerRunning;
int get lastPositin => _lastPostion; int get lastPositin => _lastPostion;
Playlist get queue => _queue; Playlist get queue => _queue;
bool get queueUpdate => _queueUpdate;
EpisodeBrief get episode => _episode; EpisodeBrief get episode => _episode;
bool get stopOnComplete => _stopOnComplete; bool get stopOnComplete => _stopOnComplete;
bool get showStopWatch => _showStopWatch; bool get showStopWatch => _showStopWatch;
@ -155,7 +154,7 @@ class AudioPlayerNotifier extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
set autoPlaySwitch(bool boo) { set autoPlaySwitch(bool boo) {
_autoPlay = boo; _autoPlay = boo;
notifyListeners(); notifyListeners();
} }
@ -163,38 +162,50 @@ class AudioPlayerNotifier extends ChangeNotifier {
@override @override
void addListener(VoidCallback listener) async { void addListener(VoidCallback listener) async {
super.addListener(listener); super.addListener(listener);
_queueUpdate = false;
await AudioService.connect(); await AudioService.connect();
if(await AudioService.running){ bool running = await AudioService.running;
AudioService.stop(); if (running) {
await AudioService.pause();
} }
} }
loadPlaylist() async { loadPlaylist() async {
await _queue.getPlaylist(); await _queue.getPlaylist();
_lastPostion = await storage.getInt(); _lastPostion = await storage.getInt();
if (_lastPostion > 0 && _queue.playlist.length > 0) {
final EpisodeBrief episode = _queue.playlist.first;
final int duration = episode.enclosureLength * 60;
final double seekValue = duration != 0 ? _lastPostion / duration : 1;
final PlayHistory history = PlayHistory(
episode.title, episode.enclosureUrl, _lastPostion / 1000, seekValue);
await dbHelper.saveHistory(history);
}
} }
episodeLoad(EpisodeBrief episode) async { episodeLoad(EpisodeBrief episode) async {
final EpisodeBrief episodeNew =
await dbHelper.getRssItemWithUrl(episode.enclosureUrl);
if (_playerRunning) { if (_playerRunning) {
PlayHistory history = PlayHistory(_episode.title, _episode.enclosureUrl, PlayHistory history = PlayHistory(_episode.title, _episode.enclosureUrl,
backgroundAudioPosition / 1000, seekSliderValue); backgroundAudioPosition / 1000, seekSliderValue);
await dbHelper.saveHistory(history); await dbHelper.saveHistory(history);
AudioService.addQueueItemAt(episode.toMediaItem(), 0); AudioService.addQueueItemAt(episodeNew.toMediaItem(), 0);
_queue.playlist _queue.playlist
.removeWhere((item) => item.enclosureUrl == episode.enclosureUrl); .removeWhere((item) => item.enclosureUrl == episode.enclosureUrl);
_queue.playlist.insert(0, episode); _queue.playlist.insert(0, episodeNew);
notifyListeners(); notifyListeners();
await _queue.savePlaylist(); await _queue.savePlaylist();
} else { } else {
await _queue.getPlaylist(); await _queue.getPlaylist();
_queue.playlist _queue.playlist
.removeWhere((item) => item.enclosureUrl == episode.enclosureUrl); .removeWhere((item) => item.enclosureUrl == episode.enclosureUrl);
_queue.playlist.insert(0, episode); _queue.playlist.insert(0, episodeNew);
_queue.savePlaylist(); _queue.savePlaylist();
_backgroundAudioDuration = 0; _backgroundAudioDuration = 0;
_backgroundAudioPosition = 0; _backgroundAudioPosition = 0;
_seekSliderValue = 0; _seekSliderValue = 0;
_episode = episode; _episode = episodeNew;
_playerRunning = true; _playerRunning = true;
notifyListeners(); notifyListeners();
await _queue.savePlaylist(); await _queue.savePlaylist();
@ -207,13 +218,13 @@ class AudioPlayerNotifier extends ChangeNotifier {
await AudioService.connect(); await AudioService.connect();
} }
await AudioService.start( await AudioService.start(
backgroundTaskEntrypoint: _audioPlayerTaskEntrypoint, backgroundTaskEntrypoint: _audioPlayerTaskEntrypoint,
androidNotificationChannelName: 'Tsacdop', androidNotificationChannelName: 'Tsacdop',
notificationColor: 0xFF2196f3, notificationColor: 0xFF4d91be,
androidNotificationIcon: 'drawable/ic_notification', androidNotificationIcon: 'drawable/ic_notification',
enableQueue: true, enableQueue: true,
androidStopOnRemoveTask: true, androidStopOnRemoveTask: true,
); androidStopForegroundOnPause: true);
_playerRunning = true; _playerRunning = true;
if (_autoPlay) { if (_autoPlay) {
await Future.forEach(_queue.playlist, (episode) async { await Future.forEach(_queue.playlist, (episode) async {
@ -242,45 +253,58 @@ class AudioPlayerNotifier extends ChangeNotifier {
print(_episode.title); print(_episode.title);
_queue.delFromPlaylist(_episode); _queue.delFromPlaylist(_episode);
} }
if (_audioState == BasicPlaybackState.paused || if (_audioState == BasicPlaybackState.skippingToNext &&
_audioState == BasicPlaybackState.skippingToNext && _episode != null &&
_episode != null) { _backgroundAudioPosition > 0) {
PlayHistory history = PlayHistory(_episode.title, _episode.enclosureUrl, PlayHistory history = PlayHistory(_episode.title, _episode.enclosureUrl,
backgroundAudioPosition / 1000, seekSliderValue); _backgroundAudioPosition / 1000, _seekSliderValue);
await dbHelper.saveHistory(history); await dbHelper.saveHistory(history);
} }
if (_audioState == BasicPlaybackState.stopped) { if (_audioState == BasicPlaybackState.stopped) _playerRunning = false;
_playerRunning = false;
if (_audioState == BasicPlaybackState.error) {
_remoteErrorMessage = 'Network Error';
} }
if (_audioState != BasicPlaybackState.error &&
_audioState != BasicPlaybackState.paused) {
_remoteErrorMessage = null;
}
_currentPosition = event?.currentPosition ?? 0; _currentPosition = event?.currentPosition ?? 0;
notifyListeners(); notifyListeners();
}); });
Timer.periodic(Duration(milliseconds: 500), (timer) { Timer.periodic(Duration(milliseconds: 500), (timer) {
if (_noSlide) { if (_noSlide) {
_audioState == BasicPlaybackState.playing if (_audioState == BasicPlaybackState.playing) {
? (_backgroundAudioPosition < _backgroundAudioDuration - 500) if (_backgroundAudioPosition < _backgroundAudioDuration - 500)
? _backgroundAudioPosition = _currentPosition + _backgroundAudioPosition = _currentPosition +
DateTime.now().difference(_current).inMilliseconds DateTime.now().difference(_current).inMilliseconds;
: _backgroundAudioPosition = _backgroundAudioDuration else
: _backgroundAudioPosition = _currentPosition; _backgroundAudioPosition = _backgroundAudioDuration;
} else
_backgroundAudioPosition = _currentPosition;
if (_backgroundAudioDuration != null && if (_backgroundAudioDuration != null &&
_backgroundAudioDuration != 0 && _backgroundAudioDuration != 0 &&
_backgroundAudioPosition != null) { _backgroundAudioPosition != null) {
_seekSliderValue = _seekSliderValue =
_backgroundAudioPosition / _backgroundAudioDuration ?? 0; _backgroundAudioPosition / _backgroundAudioDuration ?? 0;
} } else
_seekSliderValue = 0;
if (_backgroundAudioPosition > 0) { if (_backgroundAudioPosition > 0) {
_lastPostion = _backgroundAudioPosition; _lastPostion = _backgroundAudioPosition;
storage.saveInt(_lastPostion); storage.saveInt(_lastPostion);
} }
if ((_queue.playlist.length == 1 || !_autoPlay) && if ((_queue.playlist.length == 1 || !_autoPlay) &&
_seekSliderValue == 1 && _seekSliderValue > 0.9 &&
_episode != null) { _episode != null) {
_queue.delFromPlaylist(_episode); _queue.delFromPlaylist(_episode);
_lastPostion = 0; _lastPostion = 0;
storage.saveInt(_lastPostion); storage.saveInt(_lastPostion);
PlayHistory history = PlayHistory( final PlayHistory history = PlayHistory(
_episode.title, _episode.title,
_episode.enclosureUrl, _episode.enclosureUrl,
backgroundAudioPosition / 1000, backgroundAudioPosition / 1000,
@ -325,6 +349,7 @@ class AudioPlayerNotifier extends ChangeNotifier {
} }
print('add to playlist when not rnnning'); print('add to playlist when not rnnning');
await _queue.addToPlayListAt(episode, index); await _queue.addToPlayListAt(episode, index);
_queueUpdate = !_queueUpdate;
notifyListeners(); notifyListeners();
} }
@ -332,17 +357,20 @@ class AudioPlayerNotifier extends ChangeNotifier {
int index = _queue.playlist int index = _queue.playlist
.indexWhere((item) => item.enclosureUrl == episode.enclosureUrl); .indexWhere((item) => item.enclosureUrl == episode.enclosureUrl);
if (index > 0) { if (index > 0) {
EpisodeBrief episodeNew =
await dbHelper.getRssItemWithUrl(episode.enclosureUrl);
await delFromPlaylist(episode); await delFromPlaylist(episode);
await addToPlaylistAt(episode, index); await addToPlaylistAt(episodeNew, index);
} }
} }
delFromPlaylist(EpisodeBrief episode) async { Future<int> delFromPlaylist(EpisodeBrief episode) async {
if (_playerRunning) { if (_playerRunning) {
await AudioService.removeQueueItem(episode.toMediaItem()); await AudioService.removeQueueItem(episode.toMediaItem());
} }
await _queue.delFromPlaylist(episode); int index = await _queue.delFromPlaylist(episode);
notifyListeners(); notifyListeners();
return index;
} }
moveToTop(EpisodeBrief episode) async { moveToTop(EpisodeBrief episode) async {
@ -354,6 +382,7 @@ class AudioPlayerNotifier extends ChangeNotifier {
_lastPostion = 0; _lastPostion = 0;
storage.saveInt(_lastPostion); storage.saveInt(_lastPostion);
} }
notifyListeners();
} }
pauseAduio() async { pauseAduio() async {
@ -361,22 +390,32 @@ class AudioPlayerNotifier extends ChangeNotifier {
} }
resumeAudio() async { resumeAudio() async {
AudioService.play(); if (_audioState != BasicPlaybackState.connecting &&
_audioState != BasicPlaybackState.none) AudioService.play();
} }
forwardAudio(int s) { forwardAudio(int s) {
int pos = _backgroundAudioPosition + s * 1000; int pos = _backgroundAudioPosition + s * 1000;
AudioService.seekTo(pos); AudioService.seekTo(pos);
} }
seekTo(int position) async{
if (_audioState != BasicPlaybackState.connecting &&
_audioState != BasicPlaybackState.none)
await AudioService.seekTo(position);
}
sliderSeek(double val) async { sliderSeek(double val) async {
print(val.toString()); print(val.toString());
_noSlide = false; if (_audioState != BasicPlaybackState.connecting &&
_seekSliderValue = val; _audioState != BasicPlaybackState.none) {
notifyListeners(); _noSlide = false;
_currentPosition = (val * _backgroundAudioDuration).toInt(); _seekSliderValue = val;
await AudioService.seekTo(_currentPosition); notifyListeners();
_noSlide = true; _currentPosition = (val * _backgroundAudioDuration).toInt();
await AudioService.seekTo(_currentPosition);
_noSlide = true;
}
} }
//Set sleep time //Set sleep time
@ -459,6 +498,10 @@ class AudioPlayerTask extends BackgroundAudioTask {
_handlePlaybackCompleted(); _handlePlaybackCompleted();
}); });
var eventSubscription = _audioPlayer.playbackEventStream.listen((event) { var eventSubscription = _audioPlayer.playbackEventStream.listen((event) {
print('buffer position' + event.bufferedPosition.toString());
if (event.playbackError != null) {
_setState(state: BasicPlaybackState.error);
}
BasicPlaybackState state; BasicPlaybackState state;
if (event.buffering) { if (event.buffering) {
state = BasicPlaybackState.buffering; state = BasicPlaybackState.buffering;
@ -514,7 +557,7 @@ class AudioPlayerTask extends BackgroundAudioTask {
_skipState = BasicPlaybackState.skippingToNext; _skipState = BasicPlaybackState.skippingToNext;
await _audioPlayer.setUrl(mediaItem.id); await _audioPlayer.setUrl(mediaItem.id);
print(mediaItem.id); print(mediaItem.id);
Duration duration = await _audioPlayer.durationFuture ?? 0; Duration duration = await _audioPlayer.durationFuture ?? Duration.zero;
AudioServiceBackground.setMediaItem( AudioServiceBackground.setMediaItem(
mediaItem.copyWith(duration: duration.inMilliseconds)); mediaItem.copyWith(duration: duration.inMilliseconds));
_skipState = null; _skipState = null;
@ -590,7 +633,7 @@ class AudioPlayerTask extends BackgroundAudioTask {
AudioServiceBackground.setQueue(_queue); AudioServiceBackground.setQueue(_queue);
AudioServiceBackground.setMediaItem(mediaItem); AudioServiceBackground.setMediaItem(mediaItem);
await _audioPlayer.setUrl(mediaItem.id); await _audioPlayer.setUrl(mediaItem.id);
Duration duration = await _audioPlayer.durationFuture; Duration duration = await _audioPlayer.durationFuture ?? Duration.zero;
AudioServiceBackground.setMediaItem( AudioServiceBackground.setMediaItem(
mediaItem.copyWith(duration: duration.inMilliseconds)); mediaItem.copyWith(duration: duration.inMilliseconds));
onPlay(); onPlay();

View File

@ -0,0 +1,191 @@
import 'dart:io';
import 'dart:isolate';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_downloader/flutter_downloader.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as path;
import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
import 'episodebrief.dart';
class EpisodeTask {
final String taskId;
int progress;
DownloadTaskStatus status;
final EpisodeBrief episode;
EpisodeTask(
this.episode,
this.taskId, {
this.progress = 0,
this.status = DownloadTaskStatus.undefined,
});
}
void downloadCallback(String id, DownloadTaskStatus status, int progress) {
print('Homepage callback task in $id status ($status) $progress');
final SendPort send =
IsolateNameServer.lookupPortByName('downloader_send_port');
send.send([id, status, progress]);
}
class DownloadState extends ChangeNotifier {
DBHelper dbHelper = DBHelper();
List<EpisodeTask> _episodeTasks = [];
List<EpisodeTask> get episodeTasks => _episodeTasks;
@override
void addListener(VoidCallback listener) async {
_loadTasks();
_bindBackgroundIsolate();
FlutterDownloader.registerCallback(downloadCallback);
super.addListener(listener);
}
_loadTasks() async {
_episodeTasks = [];
DBHelper dbHelper = DBHelper();
var tasks = await FlutterDownloader.loadTasks();
if (tasks.length != 0)
await Future.forEach(tasks, (DownloadTask task) async {
EpisodeBrief episode = await dbHelper.getRssItemWithUrl(task.url);
_episodeTasks.add(EpisodeTask(episode, task.taskId,
progress: task.progress, status: task.status));
});
print(_episodeTasks.length);
notifyListeners();
}
void _bindBackgroundIsolate() {
ReceivePort _port = ReceivePort();
bool isSuccess = IsolateNameServer.registerPortWithName(
_port.sendPort, 'downloader_send_port');
if (!isSuccess) {
_unbindBackgroundIsolate();
_bindBackgroundIsolate();
return;
}
_port.listen((dynamic data) {
String id = data[0];
DownloadTaskStatus status = data[1];
int progress = data[2];
_episodeTasks.forEach((episodeTask) {
if (episodeTask.taskId == id) {
episodeTask.status = status;
episodeTask.progress = progress;
if (status == DownloadTaskStatus.complete) {
_saveMediaId(episodeTask).then((value) {
notifyListeners();
});
} else
notifyListeners();
}
});
});
}
Future _saveMediaId(EpisodeTask episodeTask) async {
episodeTask.status = DownloadTaskStatus.complete;
final completeTask = await FlutterDownloader.loadTasksWithRawQuery(
query:
"SELECT * FROM task WHERE task_id = '${episodeTask.taskId}'");
String filePath = 'file://' +
path.join(completeTask.first.savedDir, completeTask.first.filename);
print(filePath);
dbHelper.saveMediaId(
episodeTask.episode.enclosureUrl, filePath, episodeTask.taskId);
}
void _unbindBackgroundIsolate() {
IsolateNameServer.removePortNameMapping('downloader_send_port');
}
EpisodeTask episodeToTask(EpisodeBrief episode) {
return _episodeTasks
.firstWhere((task) => task.episode.enclosureUrl == episode.enclosureUrl,
orElse: () {
return EpisodeTask(
episode,
'',
);
});
}
@override
void dispose() {
_unbindBackgroundIsolate();
super.dispose();
}
Future startTask(EpisodeBrief episode) async {
final dir = await getExternalStorageDirectory();
String localPath = path.join(dir.path, episode.feedTitle);
final saveDir = Directory(localPath);
bool hasExisted = await saveDir.exists();
if (!hasExisted) {
saveDir.create();
}
String taskId = await FlutterDownloader.enqueue(
url: episode.enclosureUrl,
savedDir: localPath,
showNotification: true,
openFileFromNotification: false,
);
_episodeTasks.add(EpisodeTask(episode, taskId));
notifyListeners();
}
Future pauseTask(EpisodeBrief episode) async {
EpisodeTask task = episodeToTask(episode);
await FlutterDownloader.pause(taskId: task.taskId);
}
Future resumeTask(EpisodeBrief episode) async {
EpisodeTask task = episodeToTask(episode);
String newTaskId = await FlutterDownloader.resume(taskId: task.taskId);
int index = _episodeTasks.indexOf(task);
_removeTask(episode);
FlutterDownloader.remove(taskId: task.taskId);
var dbHelper = DBHelper();
_episodeTasks.insert(index, EpisodeTask(episode, newTaskId));
await dbHelper.saveDownloaded(newTaskId, episode.enclosureUrl);
}
Future retryTask(EpisodeBrief episode) async {
EpisodeTask task = episodeToTask(episode);
String newTaskId = await FlutterDownloader.retry(taskId: task.taskId);
await FlutterDownloader.remove(taskId: task.taskId);
int index = _episodeTasks.indexOf(task);
_removeTask(episode);
var dbHelper = DBHelper();
_episodeTasks.insert(index, EpisodeTask(episode, newTaskId));
await dbHelper.saveDownloaded(newTaskId, episode.enclosureUrl);
}
Future removeTask(EpisodeBrief episode) async {
EpisodeTask task = episodeToTask(episode);
await FlutterDownloader.remove(
taskId: task.taskId, shouldDeleteContent: false);
}
Future delTask(EpisodeBrief episode) async {
EpisodeTask task = episodeToTask(episode);
await FlutterDownloader.remove(
taskId: task.taskId, shouldDeleteContent: true);
await dbHelper.delDownloaded(episode.enclosureUrl);
_episodeTasks.forEach((episodeTask) {
if (episodeTask.taskId == task.taskId)
episodeTask.status = DownloadTaskStatus.undefined;
notifyListeners();
});
_removeTask(episode);
}
_removeTask(EpisodeBrief episode) {
_episodeTasks.removeWhere(
(element) => element.episode.enclosureUrl == episode.enclosureUrl);
}
}

View File

@ -1,18 +0,0 @@
import 'package:flutter/foundation.dart';
enum DownloadState { stop, load, donwload, complete, error }
class EpisodeDownload with ChangeNotifier {
String _title;
String get title => _title;
set title(String t) {
_title = t;
notifyListeners();
}
DownloadState _downloadState = DownloadState.stop;
DownloadState get downloadState => _downloadState;
set downloadState(DownloadState state){
_downloadState = state;
notifyListeners();
}
}

View File

@ -15,6 +15,7 @@ class EpisodeBrief {
final int explicit; final int explicit;
final String imagePath; final String imagePath;
final String mediaId; final String mediaId;
final int isNew;
EpisodeBrief( EpisodeBrief(
this.title, this.title,
this.enclosureUrl, this.enclosureUrl,
@ -27,7 +28,8 @@ class EpisodeBrief {
this.duration, this.duration,
this.explicit, this.explicit,
this.imagePath, this.imagePath,
this.mediaId); this.mediaId,
this.isNew);
String dateToString() { String dateToString() {
DateTime date = DateTime.fromMillisecondsSinceEpoch(pubDate, isUtc: true); DateTime date = DateTime.fromMillisecondsSinceEpoch(pubDate, isUtc: true);

View File

@ -21,7 +21,6 @@ class GroupEntity {
static GroupEntity fromJson(Map<String, Object> json) { static GroupEntity fromJson(Map<String, Object> json) {
List<String> list = List.from(json['podcastList']); List<String> list = List.from(json['podcastList']);
print(json['[podcastList']);
return GroupEntity(json['name'] as String, json['id'] as String, return GroupEntity(json['name'] as String, json['id'] as String,
json['color'] as String, list); json['color'] as String, list);
} }
@ -58,6 +57,7 @@ class PodcastGroup {
List<PodcastLocal> _orderedPodcasts; List<PodcastLocal> _orderedPodcasts;
List<PodcastLocal> get ordereddPodcasts => _orderedPodcasts; List<PodcastLocal> get ordereddPodcasts => _orderedPodcasts;
List<PodcastLocal> get podcasts => _podcasts; List<PodcastLocal> get podcasts => _podcasts;
set setOrderedPodcasts(List<PodcastLocal> list) { set setOrderedPodcasts(List<PodcastLocal> list) {
_orderedPodcasts = list; _orderedPodcasts = list;
} }
@ -88,18 +88,29 @@ class GroupList extends ChangeNotifier {
bool _isLoading = false; bool _isLoading = false;
bool get isLoading => _isLoading; bool get isLoading => _isLoading;
List<String> _orderChanged = []; List<PodcastGroup> _orderChanged = [];
List<String> get orderChanged => _orderChanged; List<PodcastGroup> get orderChanged => _orderChanged;
void addToOrderChanged(String name) {
_orderChanged.add(name); void addToOrderChanged(PodcastGroup group) {
_orderChanged.add(group);
notifyListeners(); notifyListeners();
} }
void drlFromOrderChanged(String name) { void drlFromOrderChanged(String name) {
_orderChanged.remove(name); _orderChanged.removeWhere((group) => group.name == name);
notifyListeners(); notifyListeners();
} }
clearOrderChanged() async {
if (_orderChanged.length > 0) {
await Future.forEach(_orderChanged, (PodcastGroup group) async {
await group.getPodcasts();
});
_orderChanged.clear();
// notifyListeners();
}
}
@override @override
void addListener(VoidCallback listener) { void addListener(VoidCallback listener) {
super.addListener(listener); super.addListener(listener);
@ -162,6 +173,18 @@ class GroupList extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
Future updatePodcast(PodcastLocal podcastLocal) async {
List<int> counts = await dbHelper.getPodcastCounts(podcastLocal.id);
_groups.forEach((group) {
if (group.podcastList.contains(podcastLocal.id)) {
group.podcasts.firstWhere((podcast) => podcast.id == podcastLocal.id)
..upateCount = counts[0]
..episodeCount = counts[1];
notifyListeners();
}
});
}
List<PodcastGroup> getPodcastGroup(String id) { List<PodcastGroup> getPodcastGroup(String id) {
List<PodcastGroup> result = []; List<PodcastGroup> result = [];
_groups.forEach((group) { _groups.forEach((group) {

View File

@ -11,8 +11,8 @@ class PodcastLocal {
final String link; final String link;
final String description; final String description;
final int upateCount; int upateCount;
final int episodeCount; int episodeCount;
PodcastLocal( PodcastLocal(
this.title, this.title,
this.imageUrl, this.imageUrl,

View File

@ -10,24 +10,23 @@ void callbackDispatcher() {
Workmanager.executeTask((task, inputData) async { Workmanager.executeTask((task, inputData) async {
var dbHelper = DBHelper(); var dbHelper = DBHelper();
List<PodcastLocal> podcastList = await dbHelper.getPodcastLocalAll(); List<PodcastLocal> podcastList = await dbHelper.getPodcastLocalAll();
int i = 0;
await Future.forEach(podcastList, (podcastLocal) async { await Future.forEach(podcastList, (podcastLocal) async {
i += await dbHelper.updatePodcastRss(podcastLocal); await dbHelper.updatePodcastRss(podcastLocal);
print('Refresh ' + podcastLocal.title); print('Refresh ' + podcastLocal.title);
}); });
KeyValueStorage refreshstorage = KeyValueStorage('refreshdate'); KeyValueStorage refreshstorage = KeyValueStorage('refreshdate');
await refreshstorage.saveInt(DateTime.now().millisecondsSinceEpoch); await refreshstorage.saveInt(DateTime.now().millisecondsSinceEpoch);
KeyValueStorage refreshcountstorage = KeyValueStorage('refreshcount');
await refreshcountstorage.saveInt(i);
return Future.value(true); return Future.value(true);
}); });
} }
class SettingState extends ChangeNotifier { class SettingState extends ChangeNotifier {
KeyValueStorage themestorage = KeyValueStorage('themes'); KeyValueStorage themeStorage = KeyValueStorage('themes');
KeyValueStorage accentstorage = KeyValueStorage('accents'); KeyValueStorage accentStorage = KeyValueStorage('accents');
KeyValueStorage autoupdatestorage = KeyValueStorage('autoupdate'); KeyValueStorage autoupdateStorage = KeyValueStorage('autoupdate');
KeyValueStorage intervalstorage = KeyValueStorage('updateInterval'); KeyValueStorage intervalStorage = KeyValueStorage('updateInterval');
KeyValueStorage downloadUsingDataStorage =
KeyValueStorage('downloadUsingData');
Future initData() async { Future initData() async {
await _getTheme(); await _getTheme();
@ -49,7 +48,7 @@ class SettingState extends ChangeNotifier {
_saveUpdateInterval(); _saveUpdateInterval();
Workmanager.initialize( Workmanager.initialize(
callbackDispatcher, callbackDispatcher,
isInDebugMode: true, isInDebugMode: false,
); );
Workmanager.registerPeriodicTask("1", "update_podcasts", Workmanager.registerPeriodicTask("1", "update_podcasts",
frequency: Duration(hours: hour), frequency: Duration(hours: hour),
@ -60,8 +59,8 @@ class SettingState extends ChangeNotifier {
print('work manager init done + '); print('work manager init done + ');
} }
void cancelWork() { Future cancelWork() async{
Workmanager.cancelAll(); await Workmanager.cancelAll();
print('work job cancelled'); print('work job cancelled');
} }
@ -86,58 +85,74 @@ class SettingState extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
bool _downloadUsingData;
bool get downloadUsingData => _downloadUsingData;
set downloadUsingData(bool boo) {
_downloadUsingData = boo;
_saveDownloadUsingData();
notifyListeners();
}
@override @override
void addListener(VoidCallback listener) { void addListener(VoidCallback listener) {
super.addListener(listener); super.addListener(listener);
_getTheme(); _getTheme();
_getAccentSetColor(); _getAccentSetColor();
_getAutoUpdate(); _getAutoUpdate();
_getDownloadUsingData();
_getUpdateInterval().then((value) { _getUpdateInterval().then((value) {
if (_initUpdateTag == 0) setWorkManager(24); if (_initUpdateTag == 0) setWorkManager(24);
}); });
} }
Future _getTheme() async { Future _getTheme() async {
int mode = await themestorage.getInt(); int mode = await themeStorage.getInt();
_theme = ThemeMode.values[mode]; _theme = ThemeMode.values[mode];
} }
Future _saveTheme() async { Future _saveTheme() async {
await themestorage.saveInt(_theme.index); await themeStorage.saveInt(_theme.index);
} }
Future _getAccentSetColor() async { Future _getAccentSetColor() async {
String colorString = await accentstorage.getString(); String colorString = await accentStorage.getString();
print(colorString);
if (colorString.isNotEmpty) { if (colorString.isNotEmpty) {
int color = int.parse('FF' + colorString.toUpperCase(), radix: 16); int color = int.parse('FF' + colorString.toUpperCase(), radix: 16);
_accentSetColor = Color(color).withOpacity(1.0); _accentSetColor = Color(color).withOpacity(1.0);
print(_accentSetColor.toString());
} else { } else {
_accentSetColor = Colors.blue[400]; _accentSetColor = Colors.blue[400];
} }
} }
Future _saveAccentSetColor() async { Future _saveAccentSetColor() async {
await accentstorage await accentStorage
.saveString(_accentSetColor.toString().substring(10, 16)); .saveString(_accentSetColor.toString().substring(10, 16));
} }
Future _getAutoUpdate() async { Future _getAutoUpdate() async {
int i = await autoupdatestorage.getInt(); int i = await autoupdateStorage.getInt();
_autoUpdate = i == 0 ? true : false; _autoUpdate = i == 0 ? true : false;
} }
Future _saveAutoUpdate() async { Future _saveAutoUpdate() async {
await autoupdatestorage.saveInt(_autoUpdate ? 0 : 1); await autoupdateStorage.saveInt(_autoUpdate ? 0 : 1);
} }
Future _getUpdateInterval() async { Future _getUpdateInterval() async {
_initUpdateTag = await intervalstorage.getInt(); _initUpdateTag = await intervalStorage.getInt();
_updateInterval = _initUpdateTag; _updateInterval = _initUpdateTag;
} }
Future _saveUpdateInterval() async { Future _saveUpdateInterval() async {
await intervalstorage.saveInt(_updateInterval); await intervalStorage.saveInt(_updateInterval);
}
Future _getDownloadUsingData() async {
int i = await downloadUsingDataStorage.getInt();
_downloadUsingData = i == 0 ? true : false;
}
Future _saveDownloadUsingData() async {
await downloadUsingDataStorage.saveInt(_downloadUsingData ? 0 : 1);
} }
} }

View File

@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:flutter_html/flutter_html.dart'; import 'package:flutter_html/flutter_html.dart';
import 'package:tsacdop/class/download_state.dart';
import 'package:tsacdop/home/audioplayer.dart'; import 'package:tsacdop/home/audioplayer.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:fluttertoast/fluttertoast.dart'; import 'package:fluttertoast/fluttertoast.dart';
@ -382,7 +383,7 @@ class _MenuBarState extends State<MenuBar> {
() => setUnliked(widget.episodeItem.enclosureUrl)), () => setUnliked(widget.episodeItem.enclosureUrl)),
], ],
), ),
DownloadButton(episodeBrief: widget.episodeItem), DownloadButton(episode: widget.episodeItem),
Selector<AudioPlayerNotifier, List<String>>( Selector<AudioPlayerNotifier, List<String>>(
selector: (_, audio) => selector: (_, audio) =>
audio.queue.playlist.map((e) => e.enclosureUrl).toList(), audio.queue.playlist.map((e) => e.enclosureUrl).toList(),

View File

@ -1,193 +1,95 @@
import 'dart:isolate';
import 'dart:ui'; import 'dart:ui';
import 'dart:io';
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as path;
import 'package:flutter_downloader/flutter_downloader.dart'; import 'package:flutter_downloader/flutter_downloader.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:fluttertoast/fluttertoast.dart'; import 'package:fluttertoast/fluttertoast.dart';
import 'package:connectivity/connectivity.dart';
import 'package:tsacdop/class/download_state.dart';
import 'package:tsacdop/class/episodebrief.dart'; import 'package:tsacdop/class/episodebrief.dart';
import 'package:tsacdop/class/audiostate.dart'; import 'package:tsacdop/class/audiostate.dart';
import 'package:tsacdop/local_storage/sqflite_localpodcast.dart'; import 'package:tsacdop/class/settingstate.dart';
class DownloadButton extends StatefulWidget { class DownloadButton extends StatefulWidget {
final EpisodeBrief episodeBrief; final EpisodeBrief episode;
DownloadButton({this.episodeBrief, Key key}) : super(key: key); DownloadButton({this.episode, Key key}) : super(key: key);
@override @override
_DownloadButtonState createState() => _DownloadButtonState(); _DownloadButtonState createState() => _DownloadButtonState();
} }
class _DownloadButtonState extends State<DownloadButton> { class _DownloadButtonState extends State<DownloadButton> {
_TaskInfo _task;
bool _isLoading;
bool _permissionReady; bool _permissionReady;
String _localPath; bool _usingData;
ReceivePort _port = ReceivePort(); StreamSubscription _connectivity;
EpisodeTask _task;
Future<String> _getPath() async {
final dir = await getExternalStorageDirectory();
return dir.path;
}
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_bindBackgroundIsolate();
FlutterDownloader.registerCallback(downloadCallback);
_isLoading = true;
_permissionReady = false; _permissionReady = false;
_connectivity = Connectivity().onConnectivityChanged.listen((result) {
_prepare(); _usingData = result == ConnectivityResult.mobile;
});
} }
@override @override
void dispose() { void dispose() {
_unbindBackgroundIsolate(); _connectivity.cancel();
super.dispose(); super.dispose();
} }
void _bindBackgroundIsolate() { void _requestDownload(EpisodeBrief episode, bool downloadUsingData) async {
bool isSuccess = IsolateNameServer.registerPortWithName(
_port.sendPort, 'downloader_send_port');
if (!isSuccess) {
_unbindBackgroundIsolate();
_bindBackgroundIsolate();
return;
}
_port.listen((dynamic data) {
print('UI isolate callback: $data');
String id = data[0];
DownloadTaskStatus status = data[1];
int progress = data[2];
if (_task.taskId == id) {
print(_task.progress);
setState(() {
_task.status = status;
_task.progress = progress;
});
}
});
}
void _unbindBackgroundIsolate() {
IsolateNameServer.removePortNameMapping('downloader_send_port');
}
static void downloadCallback(
String id, DownloadTaskStatus status, int progress) {
print('Background callback task in $id status ($status) $progress');
final SendPort send =
IsolateNameServer.lookupPortByName('downloader_send_port');
send.send([id, status, progress]);
}
void _requestDownload(_TaskInfo task) async {
_permissionReady = await _checkPermmison(); _permissionReady = await _checkPermmison();
if (_permissionReady) bool _dataConfirm = true;
task.taskId = await FlutterDownloader.enqueue( if (_permissionReady) {
url: task.link, if (downloadUsingData && _usingData) {
savedDir: _localPath, _dataConfirm = await _useDataConfirem();
showNotification: true, }
openFileFromNotification: false, if (_dataConfirm) {
); Provider.of<DownloadState>(context, listen: false).startTask(episode);
var dbHelper = DBHelper(); Fluttertoast.showToast(
msg: 'Downloading',
await dbHelper.saveDownloaded(task.link, task.taskId); gravity: ToastGravity.BOTTOM,
Fluttertoast.showToast( );
msg: 'Downloading', }
gravity: ToastGravity.BOTTOM, }
);
} }
void _deleteDownload(_TaskInfo task) async { void _deleteDownload(EpisodeBrief episode) async {
await FlutterDownloader.remove( Provider.of<DownloadState>(context, listen: false).delTask(episode);
taskId: task.taskId, shouldDeleteContent: true);
var dbHelper = DBHelper();
await dbHelper.delDownloaded(task.link);
await _prepare();
setState(() {});
Fluttertoast.showToast( Fluttertoast.showToast(
msg: 'Download removed', msg: 'Download removed',
gravity: ToastGravity.BOTTOM, gravity: ToastGravity.BOTTOM,
); );
} }
void _pauseDownload(_TaskInfo task) async { void _pauseDownload(EpisodeBrief episode) async {
await FlutterDownloader.pause(taskId: task.taskId); Provider.of<DownloadState>(context, listen: false).pauseTask(episode);
Fluttertoast.showToast( Fluttertoast.showToast(
msg: 'Download paused', msg: 'Download paused',
gravity: ToastGravity.BOTTOM, gravity: ToastGravity.BOTTOM,
); );
} }
void _resumeDownload(_TaskInfo task) async { void _resumeDownload(EpisodeBrief episode) async {
String newTaskId = await FlutterDownloader.resume(taskId: task.taskId); Provider.of<DownloadState>(context, listen: false).resumeTask(episode);
task.taskId = newTaskId;
var dbHelper = DBHelper();
await dbHelper.saveDownloaded(task.taskId, task.link);
Fluttertoast.showToast( Fluttertoast.showToast(
msg: 'Download resumed', msg: 'Download resumed',
gravity: ToastGravity.BOTTOM, gravity: ToastGravity.BOTTOM,
); );
} }
void _retryDownload(_TaskInfo task) async { void _retryDownload(EpisodeBrief episode) async {
String newTaskId = await FlutterDownloader.retry(taskId: task.taskId); Provider.of<DownloadState>(context, listen: false).retryTask(episode);
task.taskId = newTaskId;
var dbHelper = DBHelper();
await dbHelper.saveDownloaded(task.taskId, task.link);
Fluttertoast.showToast( Fluttertoast.showToast(
msg: 'Download again', msg: 'Download again',
gravity: ToastGravity.BOTTOM, gravity: ToastGravity.BOTTOM,
); );
} }
Future<EpisodeBrief> _saveMediaId(_TaskInfo task) async {
final completeTask = await FlutterDownloader.loadTasksWithRawQuery(
query: "SELECT * FROM task WHERE url = '${task.link}'");
String filePath = 'file://' +
path.join(completeTask.first.savedDir, completeTask.first.filename);
var dbHelper = DBHelper();
await dbHelper.saveMediaId(task.link, filePath);
EpisodeBrief episode = await dbHelper.getRssItemWithUrl(task.link);
return episode;
}
Future<Null> _prepare() async {
final tasks = await FlutterDownloader.loadTasks();
_task = _TaskInfo(
name: widget.episodeBrief.title,
link: widget.episodeBrief.enclosureUrl,
);
tasks?.forEach((task) {
if (_task.link == task.url) {
_task.taskId = task.taskId;
_task.status = task.status;
_task.progress = task.progress;
}
});
_localPath = path.join((await _getPath()), widget.episodeBrief.feedTitle);
print(_localPath);
final saveDir = Directory(_localPath);
bool hasExisted = await saveDir.exists();
if (!hasExisted) {
saveDir.create();
}
setState(() {
_isLoading = false;
});
}
Future<bool> _checkPermmison() async { Future<bool> _checkPermmison() async {
PermissionStatus permission = await PermissionHandler() PermissionStatus permission = await PermissionHandler()
.checkPermissionStatus(PermissionGroup.storage); .checkPermissionStatus(PermissionGroup.storage);
@ -205,6 +107,60 @@ class _DownloadButtonState extends State<DownloadButton> {
} }
} }
Future<bool> _useDataConfirem() async {
bool ifUseData = false;
await showGeneralDialog(
context: context,
barrierDismissible: true,
barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
barrierColor: Colors.black54,
transitionDuration: const Duration(milliseconds: 200),
pageBuilder: (BuildContext context, Animation animaiton,
Animation secondaryAnimation) =>
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),
),
child: AlertDialog(
elevation: 1,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(10.0))),
titlePadding:
EdgeInsets.only(top: 20, left: 20, right: 100, bottom: 20),
title: Text('Cellular data warn'),
content:
Text('Are you sure you want to use cellular data to download?'),
actions: <Widget>[
FlatButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text(
'CANCEL',
style: TextStyle(color: Colors.grey[600]),
),
),
FlatButton(
onPressed: () {
ifUseData = true;
Navigator.of(context).pop();
},
child: Text(
'CONFIRM',
style: TextStyle(color: Colors.red),
),
)
],
),
),
);
return ifUseData;
}
Widget _buttonOnMenu(Widget widget, Function() onTap) => Material( Widget _buttonOnMenu(Widget widget, Function() onTap) => Material(
color: Colors.transparent, color: Colors.transparent,
child: InkWell( child: InkWell(
@ -218,112 +174,111 @@ class _DownloadButtonState extends State<DownloadButton> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return _isLoading return Consumer<DownloadState>(builder: (_, downloader, __) {
? Center() EpisodeTask _task = Provider.of<DownloadState>(context, listen: false)
: Row( .episodeToTask(widget.episode);
children: <Widget>[ return Row(
_downloadButton(_task), children: <Widget>[
AnimatedContainer( _downloadButton(_task, context),
duration: Duration(seconds: 1), AnimatedContainer(
decoration: BoxDecoration( duration: Duration(seconds: 1),
color: Theme.of(context).accentColor, decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(15.0))), color: Theme.of(context).accentColor,
height: 20.0, borderRadius: BorderRadius.all(Radius.circular(15.0))),
width: height: 20.0,
(_task.status == DownloadTaskStatus.running) ? 50.0 : 0, width: (_task.status == DownloadTaskStatus.running) ? 50.0 : 0,
alignment: Alignment.center, alignment: Alignment.center,
child: SingleChildScrollView( child: SingleChildScrollView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
child: Text('${_task.progress}%', child: Text('${_task.progress}%',
style: TextStyle(color: Colors.white)), style: TextStyle(color: Colors.white)),
)), )),
], ],
); );
});
} }
Widget _downloadButton(_TaskInfo task) { Widget _downloadButton(EpisodeTask task, BuildContext context) {
if (task.status == DownloadTaskStatus.undefined) { switch (task.status.value) {
return _buttonOnMenu( case 0:
Icon( return Selector<SettingState, bool>(
Icons.arrow_downward, selector: (_, settings) => settings.downloadUsingData,
color: Colors.grey[700], builder: (_, data, __) => _buttonOnMenu(
), Icon(
() => _requestDownload(task)); Icons.arrow_downward,
} else if (task.status == DownloadTaskStatus.running) { color: Colors.grey[700],
return Material( ),
color: Colors.transparent, () => _requestDownload(task.episode, data)),
child: InkWell( );
onTap: () { break;
if (task.progress > 0) _pauseDownload(task); case 2:
}, return Material(
child: Container( color: Colors.transparent,
height: 50.0, child: InkWell(
alignment: Alignment.center, onTap: () {
padding: EdgeInsets.symmetric(horizontal: 18.0), if (task.progress > 0) _pauseDownload(task.episode);
child: SizedBox( },
height: 18, child: Container(
width: 18, height: 50.0,
child: CircularProgressIndicator( alignment: Alignment.center,
backgroundColor: Colors.grey[500], padding: EdgeInsets.symmetric(horizontal: 18.0),
strokeWidth: 2, child: SizedBox(
valueColor: AlwaysStoppedAnimation<Color>( height: 18,
Theme.of(context).accentColor), width: 18,
value: task.progress / 100, child: CircularProgressIndicator(
backgroundColor: Colors.grey[500],
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
Theme.of(context).accentColor),
value: task.progress / 100,
),
), ),
), ),
), ),
), );
); break;
} else if (task.status == DownloadTaskStatus.paused) { case 6:
return Material( return Material(
color: Colors.transparent, color: Colors.transparent,
child: InkWell( child: InkWell(
onTap: () { onTap: () {
_resumeDownload(task); _resumeDownload(task.episode);
}, },
child: Container( child: Container(
height: 50.0, height: 50.0,
alignment: Alignment.center, alignment: Alignment.center,
padding: EdgeInsets.symmetric(horizontal: 18), padding: EdgeInsets.symmetric(horizontal: 18),
child: SizedBox( child: SizedBox(
height: 18, height: 18,
width: 18, width: 18,
child: CircularProgressIndicator( child: CircularProgressIndicator(
backgroundColor: Colors.grey[500], backgroundColor: Colors.grey[500],
strokeWidth: 2, strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.red), valueColor: AlwaysStoppedAnimation<Color>(Colors.red),
value: task.progress / 100, value: task.progress / 100,
),
), ),
), ),
), ),
), );
); break;
} else if (task.status == DownloadTaskStatus.complete) { case 3:
if(!widget.episodeBrief.mediaId.contains('file://')) Provider.of<AudioPlayerNotifier>(context, listen: false)
{_saveMediaId(task).then((episode) => .updateMediaItem(task.episode);
Provider.of<AudioPlayerNotifier>(context, listen: false) return _buttonOnMenu(
.updateMediaItem(episode));} Icon(
return _buttonOnMenu( Icons.done_all,
Icon( color: Theme.of(context).accentColor,
Icons.done_all, ), () {
color: Theme.of(context).accentColor, _deleteDownload(task.episode);
), });
() => _deleteDownload(task)); break;
} else if (task.status == DownloadTaskStatus.failed) { case 4:
return _buttonOnMenu( return _buttonOnMenu(Icon(Icons.refresh, color: Colors.red),
Icon(Icons.refresh, color: Colors.red), () => _retryDownload(task)); () => _retryDownload(task.episode));
break;
default:
return Center();
} }
return Center();
} }
} }
class _TaskInfo {
final String name;
final String link;
String taskId;
int progress = 0;
DownloadTaskStatus status = DownloadTaskStatus.undefined;
_TaskInfo({this.name, this.link});
}

View File

@ -80,7 +80,7 @@ class AboutApp extends StatelessWidget {
padding: EdgeInsets.symmetric(horizontal: 50), padding: EdgeInsets.symmetric(horizontal: 50),
height: 50, height: 50,
child: Text( child: Text(
'Tsacdop is a podcasts client developed in flutter, a simple, beautiful, and easy-use application.', 'Tsacdop is a podcast player developed in flutter, a simple, beautiful, and easy-use application.',
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
), ),
@ -110,12 +110,12 @@ class AboutApp extends StatelessWidget {
_listItem(context, 'GitHub', LineIcons.github, _listItem(context, 'GitHub', LineIcons.github,
'https://github.com/stonaga/'), 'https://github.com/stonaga/'),
_listItem(context, 'Twitter', LineIcons.twitter, _listItem(context, 'Twitter', LineIcons.twitter,
'https://twitter.com'), 'https://twitter.com/shimenmen'),
_listItem( _listItem(
context, context,
'Stone Gate', 'Stone Gate',
LineIcons.hat_cowboy_solid, LineIcons.medium,
'mailto:<xijieyin@gmail.com>?subject=Tsacdop Feedback'), 'https://medium.com/@stonegate'),
], ],
), ),
), ),

View File

@ -91,7 +91,6 @@ class _MyHomePageState extends State<MyHomePage> {
class _MyHomePageDelegate extends SearchDelegate<int> { class _MyHomePageDelegate extends SearchDelegate<int> {
static Future<List> getList(String searchText) async { static Future<List> getList(String searchText) async {
String apiKey = environment['apiKey']; String apiKey = environment['apiKey'];
print(apiKey);
String url = String url =
"https://listennotes.p.mashape.com/api/v1/search?only_in=title%2Cdescription&q=" + "https://listennotes.p.mashape.com/api/v1/search?only_in=title%2Cdescription&q=" +
searchText + searchText +
@ -312,7 +311,7 @@ class _SearchResultState extends State<SearchResult> {
importOmpl.importState = ImportState.parse; importOmpl.importState = ImportState.parse;
await dbHelper.savePodcastRss(_p, _uuid); await dbHelper.savePodcastRss(_p, _uuid);
groupList.updatePodcast(podcastLocal);
importOmpl.importState = ImportState.complete; importOmpl.importState = ImportState.complete;
} else { } else {
importOmpl.importState = ImportState.error; importOmpl.importState = ImportState.error;

View File

@ -173,7 +173,7 @@ class _PopupMenuState extends State<PopupMenu> {
importOmpl.importState = ImportState.parse; importOmpl.importState = ImportState.parse;
await dbHelper.savePodcastRss(_p, _uuid); await dbHelper.savePodcastRss(_p, _uuid);
groupList.updatePodcast(podcastLocal);
importOmpl.importState = ImportState.complete; importOmpl.importState = ImportState.complete;
} else { } else {
importOmpl.importState = ImportState.error; importOmpl.importState = ImportState.error;

View File

@ -5,8 +5,11 @@ import 'dart:math' as math;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:marquee/marquee.dart'; import 'package:marquee/marquee.dart';
import 'package:tsacdop/home/playlist.dart';
import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
import 'package:audio_service/audio_service.dart'; import 'package:audio_service/audio_service.dart';
import 'package:line_icons/line_icons.dart';
import 'package:tsacdop/class/episodebrief.dart'; import 'package:tsacdop/class/episodebrief.dart';
import 'package:tsacdop/class/audiostate.dart'; import 'package:tsacdop/class/audiostate.dart';
@ -15,6 +18,7 @@ import 'package:tsacdop/home/audiopanel.dart';
import 'package:tsacdop/util/pageroute.dart'; import 'package:tsacdop/util/pageroute.dart';
import 'package:tsacdop/util/colorize.dart'; import 'package:tsacdop/util/colorize.dart';
import 'package:tsacdop/util/day_night_switch.dart'; import 'package:tsacdop/util/day_night_switch.dart';
import 'package:tsacdop/util/context_extension.dart';
class MyRectangularTrackShape extends RectangularSliderTrackShape { class MyRectangularTrackShape extends RectangularSliderTrackShape {
Rect getPreferredRect({ Rect getPreferredRect({
@ -143,18 +147,13 @@ class _PlayerWidgetState extends State<PlayerWidget> {
List minsToSelect = [1, 5, 10, 15, 20, 25, 30, 45, 60, 70, 80, 90, 99]; List minsToSelect = [1, 5, 10, 15, 20, 25, 30, 45, 60, 70, 80, 90, 99];
int _minSelected; int _minSelected;
final GlobalKey<AnimatedListState> _miniPlaylistKey = GlobalKey();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_minSelected = 30; _minSelected = 30;
} }
@override
void didUpdateWidget(Widget oldWidget) {
super.didUpdateWidget(oldWidget);
}
Widget _sleppMode(BuildContext context) { Widget _sleppMode(BuildContext context) {
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false); var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
return Selector<AudioPlayerNotifier, Tuple3<bool, int, double>>( return Selector<AudioPlayerNotifier, Tuple3<bool, int, double>>(
@ -571,9 +570,14 @@ class _PlayerWidgetState extends State<PlayerWidget> {
Container( Container(
padding: EdgeInsets.symmetric(horizontal: 5.0), padding: EdgeInsets.symmetric(horizontal: 5.0),
width: 200, width: 200,
child: Text(data.item1.feedTitle, maxLines: 1, overflow: TextOverflow.fade,), child: Text(
data.item1.feedTitle,
maxLines: 1,
overflow: TextOverflow.fade,
),
), ),
Spacer(), Spacer(),
LastPosition(),
IconButton( IconButton(
onPressed: () => Navigator.push( onPressed: () => Navigator.push(
context, context,
@ -596,138 +600,252 @@ class _PlayerWidgetState extends State<PlayerWidget> {
Widget _playlist(BuildContext context) { Widget _playlist(BuildContext context) {
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false); var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
return Container( return Container(
alignment: Alignment.bottomLeft, alignment: Alignment.topLeft,
child: AnimatedContainer( height: 300,
duration: Duration(milliseconds: 400), width: MediaQuery.of(context).size.width,
height: 300, decoration: BoxDecoration(
width: MediaQuery.of(context).size.width, color: Theme.of(context).primaryColor,
alignment: Alignment.center, ),
// margin: EdgeInsets.all(20), child: Column(
//padding: EdgeInsets.only(bottom: 10.0), children: <Widget>[
decoration: BoxDecoration( Container(
// borderRadius: BorderRadius.all(Radius.circular(10.0)), padding: EdgeInsets.symmetric(horizontal: 20.0),
color: Theme.of(context).primaryColor, height: 60.0,
), // color: context.primaryColorDark,
child: Selector<AudioPlayerNotifier, List<EpisodeBrief>>( alignment: Alignment.centerLeft,
selector: (_, audio) => audio.queue.playlist, child: Row(
builder: (_, playlist, __) { children: <Widget>[
return ListView.builder( Container(
shrinkWrap: true, padding: EdgeInsets.symmetric(horizontal: 20.0),
scrollDirection: Axis.vertical, height: 20.0,
itemCount: playlist.length, // color: context.primaryColorDark,
itemBuilder: (BuildContext context, int index) { alignment: Alignment.centerLeft,
print(playlist.length); child: Text(
if (index == 0) { 'Playlist',
return Container( style: TextStyle(
height: 60, color: Theme.of(context).accentColor,
padding: EdgeInsets.symmetric(horizontal: 20.0), fontWeight: FontWeight.bold,
fontSize: 16),
),
),
Spacer(),
Container(
height: 60,
alignment: Alignment.center,
child: Container(
alignment: Alignment.center, alignment: Alignment.center,
child: Row( height: 30,
mainAxisAlignment: MainAxisAlignment.center, width: 60,
mainAxisSize: MainAxisSize.min, decoration: BoxDecoration(
children: <Widget>[ color: Theme.of(context).primaryColor,
Text( borderRadius: BorderRadius.all(Radius.circular(15)),
'Playlist', border: Border.all(
style: TextStyle( color:
color: Theme.of(context).accentColor, Theme.of(context).brightness == Brightness.dark
fontWeight: FontWeight.bold, ? Colors.black12
fontSize: 16), : Colors.white10,
), width: 1),
Spacer(), boxShadow:
Container( Theme.of(context).brightness == Brightness.dark
alignment: Alignment.center, ? _customShadowNight
child: Container( : _customShadow),
alignment: Alignment.center, child: Material(
height: 40, color: Colors.transparent,
width: 80, child: InkWell(
decoration: BoxDecoration( borderRadius: BorderRadius.all(Radius.circular(15)),
color: Theme.of(context).primaryColor, onTap: () {
borderRadius: audio.playNext();
BorderRadius.all(Radius.circular(20)), _miniPlaylistKey.currentState.removeItem(
border: Border.all( 0, (context, animation) => Container());
color: Theme.of(context).brightness == _miniPlaylistKey.currentState.removeItem(
Brightness.dark 1, (context, animation) => Container());
? Colors.black12 _miniPlaylistKey.currentState.insertItem(0);
: Colors.white10, },
width: 1), child: SizedBox(
boxShadow: Theme.of(context).brightness == height: 30,
Brightness.dark width: 60,
? _customShadowNight child: Icon(
: _customShadow), Icons.skip_next,
child: Material( size: 30,
color: Colors.transparent,
child: InkWell(
borderRadius:
BorderRadius.all(Radius.circular(20)),
onTap: () => audio.playNext(),
child: SizedBox(
height: 40,
width: 80,
child: Icon(
Icons.skip_next,
size: 30,
),
),
),
),
),
),
],
),
);
} else {
return Column(
children: <Widget>[
Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
audio.episodeLoad(playlist[index]);
},
child: Container(
height: 60,
padding: EdgeInsets.symmetric(horizontal: 10),
alignment: Alignment.centerLeft,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(
padding: EdgeInsets.all(10.0),
child: ClipRRect(
borderRadius:
BorderRadius.all(Radius.circular(15.0)),
child: Container(
height: 30.0,
width: 30.0,
child: Image.file(File(
"${playlist[index].imagePath}"))),
),
),
Expanded(
child: Container(
alignment: Alignment.centerLeft,
child: Text(
playlist[index].title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
),
],
),
), ),
), ),
), ),
Divider(height: 2), ),
], ),
); ),
} Container(
margin: EdgeInsets.only(left: 20),
width: 30.0,
height: 30.0,
decoration: BoxDecoration(
boxShadow: (Theme.of(context).brightness == Brightness.dark)
? _customShadowNight
: _customShadow,
color: Theme.of(context).primaryColor,
shape: BoxShape.circle,
),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.all(Radius.circular(15.0)),
onTap: () {
Navigator.push(
context,
SlideLeftRoute(page: PlaylistPage()),
);
},
child: SizedBox(
height: 30.0,
width: 30.0,
child: Transform.rotate(
angle: math.pi,
child: Icon(
LineIcons.database_solid,
size: 20.0,
),
),
),
),
),
),
],
),
),
Expanded(
child: Selector<AudioPlayerNotifier, List<EpisodeBrief>>(
selector: (_, audio) => audio.queue.playlist,
builder: (_, playlist, __) {
return AnimatedList(
key: _miniPlaylistKey,
shrinkWrap: true,
scrollDirection: Axis.vertical,
initialItemCount: playlist.length,
itemBuilder: (context, index, animation) => ScaleTransition(
alignment: Alignment.centerLeft,
scale: animation,
child: index == 0
? Center()
: Column(
children: <Widget>[
Row(
children: <Widget>[
Expanded(
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
audio.episodeLoad(playlist[index]);
_miniPlaylistKey.currentState
.removeItem(
index,
(context, animation) =>
Center());
_miniPlaylistKey.currentState
.insertItem(0);
},
child: Container(
height: 60,
padding: EdgeInsets.symmetric(
horizontal: 20),
alignment: Alignment.centerLeft,
child: Row(
mainAxisAlignment:
MainAxisAlignment.center,
crossAxisAlignment:
CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(
padding: EdgeInsets.all(10.0),
child: ClipRRect(
borderRadius:
BorderRadius.all(
Radius.circular(
15.0)),
child: Container(
height: 30.0,
width: 30.0,
child: Image.file(File(
"${playlist[index].imagePath}"))),
),
),
Expanded(
child: Container(
alignment:
Alignment.centerLeft,
child: Text(
playlist[index].title,
maxLines: 1,
overflow:
TextOverflow.ellipsis,
),
),
),
],
),
),
),
),
),
Container(
margin:
EdgeInsets.symmetric(horizontal: 20),
width: 30.0,
height: 30.0,
decoration: BoxDecoration(
boxShadow:
(Theme.of(context).brightness ==
Brightness.dark)
? _customShadowNight
: _customShadow,
color: Theme.of(context).primaryColor,
shape: BoxShape.circle,
),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.all(
Radius.circular(15.0)),
onTap: () async {
await audio
.moveToTop(playlist[index]);
_miniPlaylistKey.currentState
.removeItem(
index,
(context, animation) => Center(),
duration:
Duration(milliseconds: 500),
);
_miniPlaylistKey.currentState
.insertItem(1,
duration: Duration(
milliseconds: 200));
},
child: SizedBox(
height: 30.0,
width: 30.0,
child: Transform.rotate(
angle: math.pi,
child: Icon(
LineIcons.download_solid,
size: 20.0,
),
),
),
),
),
),
],
),
Divider(height: 2),
],
),
),
);
}, },
); ),
}, ),
), ],
), ),
); );
} }
@ -765,8 +883,8 @@ class _PlayerWidgetState extends State<PlayerWidget> {
tabs: <Widget>[ tabs: <Widget>[
Container( Container(
// child: Text('p'), // child: Text('p'),
height: 10.0, height: 8.0,
width: 10.0, width: 8.0,
decoration: BoxDecoration( decoration: BoxDecoration(
shape: BoxShape.circle, shape: BoxShape.circle,
color: Colors.transparent, color: Colors.transparent,
@ -775,8 +893,8 @@ class _PlayerWidgetState extends State<PlayerWidget> {
width: 2.0)), width: 2.0)),
), ),
Container( Container(
height: 10.0, height: 8.0,
width: 10.0, width: 8.0,
decoration: BoxDecoration( decoration: BoxDecoration(
shape: BoxShape.circle, shape: BoxShape.circle,
color: Colors.transparent, color: Colors.transparent,
@ -785,8 +903,8 @@ class _PlayerWidgetState extends State<PlayerWidget> {
width: 2.0)), width: 2.0)),
), ),
Container( Container(
height: 10.0, height: 8.0,
width: 10.0, width: 8.0,
decoration: BoxDecoration( decoration: BoxDecoration(
shape: BoxShape.circle, shape: BoxShape.circle,
color: Colors.transparent, color: Colors.transparent,
@ -971,6 +1089,83 @@ class _PlayerWidgetState extends State<PlayerWidget> {
} }
} }
class LastPosition extends StatefulWidget {
LastPosition({Key key}) : super(key: key);
@override
_LastPositionState createState() => _LastPositionState();
}
class _LastPositionState extends State<LastPosition> {
static String _stringForSeconds(double seconds) {
if (seconds == null) return null;
return '${(seconds ~/ 60)}:${(seconds.truncate() % 60).toString().padLeft(2, '0')}';
}
Future<PlayHistory> getPosition(EpisodeBrief episode) async {
var dbHelper = DBHelper();
return await dbHelper.getPosition(episode);
}
@override
Widget build(BuildContext context) {
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
return Selector<AudioPlayerNotifier, EpisodeBrief>(
selector: (_, audio) => audio.episode,
builder: (context, episode, child) {
return FutureBuilder<PlayHistory>(
future: getPosition(episode),
builder: (context, snapshot) {
if (snapshot.hasError) print(snapshot.error);
return snapshot.hasData
? snapshot.data.seekValue > 0.95
? Container(
height: 20.0,
alignment: Alignment.center,
decoration: BoxDecoration(
border: Border.all(
width: 1,
color: Theme.of(context)
.textTheme
.bodyText1
.color),
borderRadius:
BorderRadius.all(Radius.circular(10.0))),
child: Text('Played before'))
: snapshot.data.seconds < 10
? Center()
: Material(
color: Colors.transparent,
child: InkWell(
borderRadius:
BorderRadius.all(Radius.circular(10.0)),
onTap: () => audio.seekTo(
(snapshot.data.seconds * 1000).toInt()),
child: Container(
width: 120.0,
height: 20.0,
alignment: Alignment.center,
decoration: BoxDecoration(
border: Border.all(
width: 1,
color: Theme.of(context)
.textTheme
.bodyText1
.color),
borderRadius: BorderRadius.all(
Radius.circular(10.0))),
child: Text('Last time ' +
_stringForSeconds(snapshot.data.seconds)),
),
),
)
: Center();
});
},
);
}
}
class ImageRotate extends StatefulWidget { class ImageRotate extends StatefulWidget {
final String title; final String title;
final String path; final String path;

133
lib/home/download_list.dart Normal file
View File

@ -0,0 +1,133 @@
import 'dart:io';
import 'package:provider/provider.dart';
import 'package:flutter/material.dart';
import 'package:tsacdop/class/download_state.dart';
import 'package:tsacdop/episodes/episodedetail.dart';
import 'package:tsacdop/util/pageroute.dart';
class DownloadList extends StatefulWidget {
DownloadList({Key key}) : super(key: key);
@override
_DownloadListState createState() => _DownloadListState();
}
Widget _downloadButton(EpisodeTask task, BuildContext context) {
var downloader = Provider.of<DownloadState>(context, listen: false);
switch (task.status.value) {
case 2:
return IconButton(
icon: Icon(
Icons.pause_circle_filled,
),
onPressed: () => downloader.pauseTask(task.episode),
);
case 4:
return Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
IconButton(
icon: Icon(Icons.refresh),
onPressed: () => downloader.retryTask(task.episode),
),
IconButton(
icon: Icon(Icons.close),
onPressed: () => downloader.delTask(task.episode),
),
],
);
case 6:
return IconButton(
icon: Icon(Icons.play_circle_filled),
onPressed: () => downloader.resumeTask(task.episode),
);
break;
default:
return Center();
}
}
class _DownloadListState extends State<DownloadList> {
@override
Widget build(BuildContext context) {
return SliverPadding(
padding: EdgeInsets.all(5.0),
sliver: Consumer<DownloadState>(builder: (_, downloader, __) {
var tasks = downloader.episodeTasks
.where((task) => task.status.value != 3)
.toList();
return tasks.length > 0
? SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return ListTile(
onTap: () => Navigator.push(
context,
ScaleRoute(
page: EpisodeDetail(
episodeItem: tasks[index].episode,
)),
),
title: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Expanded(
flex: 5,
child: Container(
child: Text(
tasks[index].episode.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
),
Expanded(
flex: 1,
child: tasks[index].progress >= 0
? Container(
width: 40.0,
padding:
EdgeInsets.symmetric(horizontal: 2),
alignment: Alignment.center,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(
Radius.circular(6)),
color: Colors.red),
child: Text(
tasks[index].progress.toString() + '%',
style: TextStyle(color: Colors.white),
))
: Container(),
),
],
),
subtitle: SizedBox(
height: 2,
child: LinearProgressIndicator(
value: tasks[index].progress / 100,
),
),
leading: CircleAvatar(
backgroundImage: FileImage(
File("${tasks[index].episode.imagePath}")),
),
trailing: _downloadButton(tasks[index], context),
);
},
childCount: tasks.length,
),
)
: SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Center();
},
childCount: 1,
),
);
}),
);
}
}

View File

@ -347,7 +347,7 @@ class _PodcastPreviewState extends State<PodcastPreview> {
} }
return (snapshot.hasData) return (snapshot.hasData)
? ShowEpisode( ? ShowEpisode(
podcast: snapshot.data, episodes: snapshot.data,
podcastLocal: widget.podcastLocal, podcastLocal: widget.podcastLocal,
) )
: Center(child: CircularProgressIndicator()); : Center(child: CircularProgressIndicator());
@ -400,9 +400,9 @@ class _PodcastPreviewState extends State<PodcastPreview> {
} }
class ShowEpisode extends StatelessWidget { class ShowEpisode extends StatelessWidget {
final List<EpisodeBrief> podcast; final List<EpisodeBrief> episodes;
final PodcastLocal podcastLocal; final PodcastLocal podcastLocal;
ShowEpisode({Key key, this.podcast, this.podcastLocal}) : super(key: key); ShowEpisode({Key key, this.episodes, this.podcastLocal}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -513,16 +513,16 @@ class ShowEpisode extends StatelessWidget {
details.globalPosition.dy), details.globalPosition.dy),
onLongPress: () => _showPopupMenu( onLongPress: () => _showPopupMenu(
offset, offset,
podcast[index], episodes[index],
context, context,
data.item1 == podcast[index], data.item1 == episodes[index],
data.item2.contains(podcast[index].enclosureUrl)), data.item2.contains(episodes[index].enclosureUrl)),
onTap: () { onTap: () {
Navigator.push( Navigator.push(
context, context,
ScaleRoute( ScaleRoute(
page: EpisodeDetail( page: EpisodeDetail(
episodeItem: podcast[index], episodeItem: episodes[index],
heroTag: 'scroll', heroTag: 'scroll',
//unique hero tag //unique hero tag
)), )),
@ -548,7 +548,7 @@ class ShowEpisode extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[ children: <Widget>[
Hero( Hero(
tag: podcast[index].enclosureUrl + tag: episodes[index].enclosureUrl +
'scroll', 'scroll',
child: Container( child: Container(
height: _width / 18, height: _width / 18,
@ -560,7 +560,7 @@ class ShowEpisode extends StatelessWidget {
), ),
), ),
Spacer(), Spacer(),
index < podcastLocal.upateCount episodes[index].isNew == 1
? Text( ? Text(
'New', 'New',
style: TextStyle( style: TextStyle(
@ -577,7 +577,7 @@ class ShowEpisode extends StatelessWidget {
padding: EdgeInsets.only(top: 2.0), padding: EdgeInsets.only(top: 2.0),
alignment: Alignment.topLeft, alignment: Alignment.topLeft,
child: Text( child: Text(
podcast[index].title, episodes[index].title,
style: TextStyle( style: TextStyle(
fontSize: _width / 32, fontSize: _width / 32,
), ),
@ -591,7 +591,7 @@ class ShowEpisode extends StatelessWidget {
child: Container( child: Container(
alignment: Alignment.bottomLeft, alignment: Alignment.bottomLeft,
child: Text( child: Text(
podcast[index].dateToString(), episodes[index].dateToString(),
//podcast[index].pubDate.substring(4, 16), //podcast[index].pubDate.substring(4, 16),
style: TextStyle( style: TextStyle(
fontSize: _width / 35, fontSize: _width / 35,
@ -608,7 +608,7 @@ class ShowEpisode extends StatelessWidget {
), ),
); );
}, },
childCount: (podcast.length > 3) ? 3 : podcast.length, childCount: (episodes.length > 3) ? 3 : episodes.length,
), ),
), ),
), ),

View File

@ -1,13 +1,15 @@
import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart' hide NestedScrollView; import 'package:flutter/material.dart' hide NestedScrollView;
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:tsacdop/class/download_state.dart';
import 'package:tsacdop/home/playlist.dart'; import 'package:tsacdop/home/playlist.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart';
import 'package:tsacdop/class/audiostate.dart'; import 'package:tsacdop/class/audiostate.dart';
import 'package:tsacdop/class/episodebrief.dart'; import 'package:tsacdop/class/episodebrief.dart';
import 'package:tsacdop/local_storage/key_value_storage.dart';
import 'package:tsacdop/local_storage/sqflite_localpodcast.dart'; import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
import 'package:tsacdop/util/episodegrid.dart'; import 'package:tsacdop/util/episodegrid.dart';
import 'package:tsacdop/util/mypopupmenu.dart'; import 'package:tsacdop/util/mypopupmenu.dart';
@ -15,6 +17,7 @@ import 'package:tsacdop/util/mypopupmenu.dart';
import 'package:tsacdop/home/appbar/importompl.dart'; import 'package:tsacdop/home/appbar/importompl.dart';
import 'package:tsacdop/home/audioplayer.dart'; import 'package:tsacdop/home/audioplayer.dart';
import 'home_groups.dart'; import 'home_groups.dart';
import 'download_list.dart';
class Home extends StatefulWidget { class Home extends StatefulWidget {
@override @override
@ -58,6 +61,10 @@ class _HomeState extends State<Home> with SingleTickerProviderStateMixin {
Import(), Import(),
Expanded( Expanded(
child: NestedScrollView( child: NestedScrollView(
innerScrollPositionKeyBuilder: () {
return Key('tab' + _controller.index.toString());
},
pinnedHeaderSliverHeightBuilder: () => 50,
headerSliverBuilder: headerSliverBuilder:
(BuildContext context, bool innerBoxScrolled) { (BuildContext context, bool innerBoxScrolled) {
return <Widget>[ return <Widget>[
@ -100,9 +107,12 @@ class _HomeState extends State<Home> with SingleTickerProviderStateMixin {
body: TabBarView( body: TabBarView(
controller: _controller, controller: _controller,
children: <Widget>[ children: <Widget>[
_RecentUpdate(), NestedScrollViewInnerScrollPositionKeyWidget(
_MyFavorite(), Key('tab0'), _RecentUpdate()),
_MyDownload(), NestedScrollViewInnerScrollPositionKeyWidget(
Key('tab1'), _MyFavorite()),
NestedScrollViewInnerScrollPositionKeyWidget(
Key('tab2'), _MyDownload()),
], ],
), ),
), ),
@ -317,13 +327,11 @@ class _RecentUpdate extends StatefulWidget {
_RecentUpdateState createState() => _RecentUpdateState(); _RecentUpdateState createState() => _RecentUpdateState();
} }
class _RecentUpdateState extends State<_RecentUpdate> { class _RecentUpdateState extends State<_RecentUpdate>
int _updateCount = 0; with AutomaticKeepAliveClientMixin {
Future<List<EpisodeBrief>> _getRssItem(int top) async { Future<List<EpisodeBrief>> _getRssItem(int top) async {
var dbHelper = DBHelper(); var dbHelper = DBHelper();
List<EpisodeBrief> episodes = await dbHelper.getRecentRssItem(top); List<EpisodeBrief> episodes = await dbHelper.getRecentRssItem(top);
KeyValueStorage refreshcountstorage = KeyValueStorage('refreshcount');
_updateCount = await refreshcountstorage.getInt();
return episodes; return episodes;
} }
@ -337,18 +345,18 @@ class _RecentUpdateState extends State<_RecentUpdate> {
}); });
} }
int _top; int _top = 99;
bool _loadMore; bool _loadMore;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_loadMore = false; _loadMore = false;
_top = 33;
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context);
return FutureBuilder<List<EpisodeBrief>>( return FutureBuilder<List<EpisodeBrief>>(
future: _getRssItem(_top), future: _getRssItem(_top),
builder: (context, snapshot) { builder: (context, snapshot) {
@ -367,7 +375,6 @@ class _RecentUpdateState extends State<_RecentUpdate> {
slivers: <Widget>[ slivers: <Widget>[
EpisodeGrid( EpisodeGrid(
episodes: snapshot.data, episodes: snapshot.data,
updateCount: _updateCount,
), ),
SliverList( SliverList(
delegate: SliverChildBuilderDelegate( delegate: SliverChildBuilderDelegate(
@ -386,6 +393,9 @@ class _RecentUpdateState extends State<_RecentUpdate> {
}, },
); );
} }
@override
bool get wantKeepAlive => true;
} }
class _MyFavorite extends StatefulWidget { class _MyFavorite extends StatefulWidget {
@ -393,7 +403,8 @@ class _MyFavorite extends StatefulWidget {
_MyFavoriteState createState() => _MyFavoriteState(); _MyFavoriteState createState() => _MyFavoriteState();
} }
class _MyFavoriteState extends State<_MyFavorite> { class _MyFavoriteState extends State<_MyFavorite>
with AutomaticKeepAliveClientMixin {
Future<List<EpisodeBrief>> _getLikedRssItem(_top) async { Future<List<EpisodeBrief>> _getLikedRssItem(_top) async {
var dbHelper = DBHelper(); var dbHelper = DBHelper();
List<EpisodeBrief> episodes = await dbHelper.getLikedRssItem(_top); List<EpisodeBrief> episodes = await dbHelper.getLikedRssItem(_top);
@ -410,18 +421,18 @@ class _MyFavoriteState extends State<_MyFavorite> {
}); });
} }
int _top; int _top = 99;
bool _loadMore; bool _loadMore;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_loadMore = false; _loadMore = false;
_top = 33;
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context);
return FutureBuilder<List<EpisodeBrief>>( return FutureBuilder<List<EpisodeBrief>>(
future: _getLikedRssItem(_top), future: _getLikedRssItem(_top),
builder: (context, snapshot) { builder: (context, snapshot) {
@ -436,7 +447,6 @@ class _MyFavoriteState extends State<_MyFavorite> {
}, },
child: CustomScrollView( child: CustomScrollView(
key: PageStorageKey<String>('favorite'), key: PageStorageKey<String>('favorite'),
physics: const AlwaysScrollableScrollPhysics(),
slivers: <Widget>[ slivers: <Widget>[
EpisodeGrid( EpisodeGrid(
episodes: snapshot.data, episodes: snapshot.data,
@ -459,6 +469,9 @@ class _MyFavoriteState extends State<_MyFavorite> {
}, },
); );
} }
@override
bool get wantKeepAlive => true;
} }
class _MyDownload extends StatefulWidget { class _MyDownload extends StatefulWidget {
@ -466,32 +479,45 @@ class _MyDownload extends StatefulWidget {
_MyDownloadState createState() => _MyDownloadState(); _MyDownloadState createState() => _MyDownloadState();
} }
class _MyDownloadState extends State<_MyDownload> { class _MyDownloadState extends State<_MyDownload>
Future<List<EpisodeBrief>> _getDownloadedRssItem() async { with AutomaticKeepAliveClientMixin {
var dbHelper = DBHelper(); @override
List<EpisodeBrief> episodes = await dbHelper.getDownloadedRssItem(); Widget build(BuildContext context) {
return episodes; super.build(context);
return CustomScrollView(
key: PageStorageKey<String>('downloas_list'),
slivers: <Widget>[
DownloadList(),
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Container(
alignment: Alignment.centerLeft,
padding: EdgeInsets.all(15.0),
child: Text('Downloaded'),
);
},
childCount: 1,
),
),
Consumer<DownloadState>(
builder: (_, downloader, __) {
var episodes = downloader.episodeTasks
.where((task) => task.status.value == 3)
.toList()
.map((e) => e.episode)
.toList()
.reversed
.toList();
return EpisodeGrid(
episodes: episodes,
);
},
),
],
);
} }
@override @override
Widget build(BuildContext context) { bool get wantKeepAlive => true;
return FutureBuilder<List<EpisodeBrief>>(
future: _getDownloadedRssItem(),
builder: (context, snapshot) {
if (snapshot.hasError) print(snapshot.error);
return (snapshot.hasData)
? CustomScrollView(
physics: const AlwaysScrollableScrollPhysics(),
primary: false,
slivers: <Widget>[
EpisodeGrid(
episodes: snapshot.data,
showDownload: true,
)
],
)
: Center(child: CircularProgressIndicator());
},
);
}
} }

View File

@ -12,6 +12,7 @@ import 'package:line_icons/line_icons.dart';
import 'package:tsacdop/class/audiostate.dart'; import 'package:tsacdop/class/audiostate.dart';
import 'package:tsacdop/class/episodebrief.dart'; import 'package:tsacdop/class/episodebrief.dart';
import 'package:tsacdop/util/colorize.dart'; import 'package:tsacdop/util/colorize.dart';
import 'package:tsacdop/util/context_extension.dart';
class PlaylistPage extends StatefulWidget { class PlaylistPage extends StatefulWidget {
@override @override
@ -19,21 +20,8 @@ class PlaylistPage extends StatefulWidget {
} }
class _PlaylistPageState extends State<PlaylistPage> { class _PlaylistPageState extends State<PlaylistPage> {
final GlobalKey<AnimatedListState> _playlistKey = GlobalKey();
final textstyle = TextStyle(fontSize: 15.0, color: Colors.black); final textstyle = TextStyle(fontSize: 15.0, color: Colors.black);
Widget _episodeTag(String text, Color color) {
return Container(
decoration: BoxDecoration(
color: color, borderRadius: BorderRadius.all(Radius.circular(15.0))),
height: 23.0,
margin: EdgeInsets.only(right: 10.0),
padding: EdgeInsets.symmetric(horizontal: 8.0),
alignment: Alignment.center,
child: Text(text, style: TextStyle(fontSize: 14.0, color: Colors.black)),
);
}
int _sumPlaylistLength(List<EpisodeBrief> episodes) { int _sumPlaylistLength(List<EpisodeBrief> episodes) {
int sum = 0; int sum = 0;
if (episodes.length == 0) { if (episodes.length == 0) {
@ -87,13 +75,16 @@ class _PlaylistPageState extends State<PlaylistPage> {
), ),
body: SafeArea( body: SafeArea(
child: child:
Selector<AudioPlayerNotifier, Tuple2<List<EpisodeBrief>, bool>>( Selector<AudioPlayerNotifier, Tuple3<Playlist, bool, bool>>(
selector: (_, audio) => selector: (_, audio) =>
Tuple2(audio.queue.playlist, audio.playerRunning), Tuple3(audio.queue, audio.playerRunning, audio.queueUpdate),
builder: (_, data, __) { builder: (_, data, __) {
print('update');
final List<EpisodeBrief> episodes = data.item1.playlist;
return Column( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[ children: <Widget>[
Container( Container(
height: _topHeight, height: _topHeight,
@ -115,7 +106,7 @@ class _PlaylistPageState extends State<PlaylistPage> {
), ),
children: <TextSpan>[ children: <TextSpan>[
TextSpan( TextSpan(
text: data.item1.length.toString(), text: episodes.length.toString(),
style: GoogleFonts.cairo( style: GoogleFonts.cairo(
textStyle: TextStyle( textStyle: TextStyle(
color: Theme.of(context).accentColor, color: Theme.of(context).accentColor,
@ -124,7 +115,7 @@ class _PlaylistPageState extends State<PlaylistPage> {
), ),
), ),
TextSpan( TextSpan(
text: data.item1.length < 2 text: episodes.length < 2
? ' episode ' ? ' episode '
: ' episodes ', : ' episodes ',
style: TextStyle( style: TextStyle(
@ -133,7 +124,7 @@ class _PlaylistPageState extends State<PlaylistPage> {
)), )),
TextSpan( TextSpan(
text: text:
_sumPlaylistLength(data.item1).toString(), _sumPlaylistLength(episodes).toString(),
style: GoogleFonts.cairo( style: GoogleFonts.cairo(
textStyle: TextStyle( textStyle: TextStyle(
color: Theme.of(context).accentColor, color: Theme.of(context).accentColor,
@ -179,171 +170,24 @@ class _PlaylistPageState extends State<PlaylistPage> {
height: 3, height: 3,
), ),
Expanded( Expanded(
child: AnimatedList( child: ReorderableListView(
controller: _controller, onReorder: (int oldIndex, int newIndex) {
key: _playlistKey, if (newIndex > oldIndex) {
shrinkWrap: true, newIndex -= 1;
}
final EpisodeBrief episodeRemove =
episodes[oldIndex];
audio.delFromPlaylist(episodeRemove);
audio.addToPlaylistAt(episodeRemove, newIndex);
setState(() {});
},
scrollDirection: Axis.vertical, scrollDirection: Axis.vertical,
initialItemCount: data.item1.length, children: episodes
itemBuilder: (context, index, animation) { .map<Widget>((episode) => DismissibleContainer(
Color _c = (Theme.of(context).brightness == episode: episode,
Brightness.light) key: ValueKey(episode.enclosureUrl),
? data.item1[index].primaryColor.colorizedark() ))
: data.item1[index].primaryColor.colorizeLight(); .toList()),
return ScaleTransition(
alignment: Alignment.centerLeft,
scale: animation,
child: Dismissible(
background: Container(
padding: EdgeInsets.symmetric(horizontal: 20.0),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: <Widget>[
Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.red),
padding: EdgeInsets.all(5),
child: Icon(
LineIcons.trash_alt_solid,
color: Colors.white,
size: 15,
),
),
Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.red),
padding: EdgeInsets.all(5),
child: Icon(
LineIcons.trash_alt_solid,
color: Colors.white,
size: 15,
),
),
],
),
height: 50,
color: Theme.of(context).accentColor,
),
key: Key(data.item1[index].enclosureUrl),
onDismissed: (direction) async {
await audio.delFromPlaylist(data.item1[index]);
_playlistKey.currentState.removeItem(
index, (context, animation) => Center());
Fluttertoast.showToast(
msg: 'Removed From Playlist',
gravity: ToastGravity.BOTTOM,
);
},
child: Column(
children: <Widget>[
ListTile(
title: Container(
padding: EdgeInsets.only(
top: 10.0, bottom: 5.0),
child: Text(
data.item1[index].title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
leading: CircleAvatar(
backgroundColor: _c.withOpacity(0.5),
backgroundImage: FileImage(File(
"${data.item1[index].imagePath}")),
),
trailing: index == 0
? data.item2
? Padding(
padding: EdgeInsets.only(
right: 15, top: 20),
child: SizedBox(
width: 20,
height: 15,
child: WaveLoader()),
)
: IconButton(
icon: Icon(
Icons.play_arrow,
color: Theme.of(context)
.accentColor,
),
onPressed: () =>
audio.playlistLoad())
: Transform.rotate(
angle: math.pi,
child: IconButton(
tooltip: 'Move to Top',
icon: Icon(
LineIcons.download_solid),
onPressed: () async {
await audio.moveToTop(
data.item1[index]);
_playlistKey.currentState
.removeItem(
index,
(context,
animation) =>
Container());
data.item2
? _playlistKey
.currentState
.insertItem(1)
: _playlistKey
.currentState
.insertItem(0);
}),
),
subtitle: Container(
padding:
EdgeInsets.only(top: 5, bottom: 10),
child: Row(
children: <Widget>[
(data.item1[index].explicit == 1)
? Container(
decoration: BoxDecoration(
color: Colors.red[800],
shape: BoxShape.circle),
height: 20.0,
width: 20.0,
margin: EdgeInsets.only(
right: 10.0),
alignment: Alignment.center,
child: Text('E',
style: TextStyle(
color: Colors.white)))
: Center(),
data.item1[index].duration != 0
? _episodeTag(
(data.item1[index].duration)
.toString() +
'mins',
Colors.cyan[300])
: Center(),
data.item1[index].enclosureLength !=
null
? _episodeTag(
((data.item1[index]
.enclosureLength) ~/
1000000)
.toString() +
'MB',
Colors.lightBlue[300])
: Center(),
],
),
),
),
Divider(
height: 2,
),
],
),
),
);
}),
), ),
], ],
); );
@ -354,3 +198,152 @@ class _PlaylistPageState extends State<PlaylistPage> {
); );
} }
} }
class DismissibleContainer extends StatefulWidget {
final EpisodeBrief episode;
DismissibleContainer({this.episode, Key key}) : super(key: key);
@override
_DismissibleContainerState createState() => _DismissibleContainerState();
}
class _DismissibleContainerState extends State<DismissibleContainer> {
bool _delete;
Widget _episodeTag(String text, Color color) {
return Container(
decoration: BoxDecoration(
color: color, borderRadius: BorderRadius.all(Radius.circular(15.0))),
height: 23.0,
margin: EdgeInsets.only(right: 10.0),
padding: EdgeInsets.symmetric(horizontal: 8.0),
alignment: Alignment.center,
child: Text(text, style: TextStyle(fontSize: 14.0, color: Colors.black)),
);
}
@override
void initState() {
_delete = false;
super.initState();
}
@override
Widget build(BuildContext context) {
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
return AnimatedContainer(
duration: Duration(milliseconds: 300),
height: _delete ? 0 : 95.0,
child: _delete
? Container(
color: context.accentColor,
)
: Dismissible(
key: ValueKey(widget.episode.enclosureUrl + 't'),
background: Container(
padding: EdgeInsets.symmetric(horizontal: 20.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Container(
decoration: BoxDecoration(
shape: BoxShape.circle, color: Colors.red),
padding: EdgeInsets.all(5),
alignment: Alignment.center,
child: Icon(
LineIcons.trash_alt_solid,
color: Colors.white,
size: 15,
),
),
Container(
decoration: BoxDecoration(
shape: BoxShape.circle, color: Colors.red),
padding: EdgeInsets.all(5),
alignment: Alignment.center,
child: Icon(
LineIcons.trash_alt_solid,
color: Colors.white,
size: 15,
),
),
],
),
height: 50,
color: Theme.of(context).accentColor,
),
onDismissed: (direction) async {
setState(() {
_delete = true;
});
int index = await audio.delFromPlaylist(widget.episode);
final episodeRemove = widget.episode;
Fluttertoast.showToast(
msg: 'Removed From Playlist',
gravity: ToastGravity.BOTTOM,
);
Scaffold.of(context).showSnackBar(SnackBar(
content: Text('1 episode removed'),
action: SnackBarAction(
label: 'Undo',
onPressed: () {
audio.addToPlaylistAt(episodeRemove, index);
}),
));
},
child: Column(
children: <Widget>[
ListTile(
title: Container(
padding: EdgeInsets.only(top: 10.0, bottom: 5.0),
child: Text(
widget.episode.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
leading: CircleAvatar(
//backgroundColor: _c.withOpacity(0.5),
backgroundImage:
FileImage(File("${widget.episode.imagePath}")),
),
subtitle: Container(
padding: EdgeInsets.only(top: 5, bottom: 10),
child: Row(
children: <Widget>[
(widget.episode.explicit == 1)
? Container(
decoration: BoxDecoration(
color: Colors.red[800],
shape: BoxShape.circle),
height: 20.0,
width: 20.0,
margin: EdgeInsets.only(right: 10.0),
alignment: Alignment.center,
child: Text('E',
style: TextStyle(color: Colors.white)))
: Center(),
widget.episode.duration != 0
? _episodeTag(
(widget.episode.duration).toString() + 'mins',
Colors.cyan[300])
: Center(),
widget.episode.enclosureLength != null
? _episodeTag(
((widget.episode.enclosureLength) ~/ 1000000)
.toString() +
'MB',
Colors.lightBlue[300])
: Center(),
],
),
),
),
// Divider(
// height: 2,
// ),
],
),
),
);
}
}

View File

@ -28,9 +28,10 @@ class DBHelper {
void _onCreate(Database db, int version) async { void _onCreate(Database db, int version) async {
await db await db
.execute("""CREATE TABLE PodcastLocal(id TEXT PRIMARY KEY,title TEXT, .execute("""CREATE TABLE PodcastLocal(id TEXT PRIMARY KEY,title TEXT,
imageUrl TEXT,rssUrl TEXT UNIQUE,primaryColor TEXT,author TEXT, imageUrl TEXT,rssUrl TEXT UNIQUE, primaryColor TEXT, author TEXT,
description TEXT, add_date INTEGER, imagePath TEXT, provider TEXT, link TEXT, description TEXT, add_date INTEGER, imagePath TEXT, provider TEXT, link TEXT,
background_image TEXT DEFAULT '',hosts TEXT DEFAULT '',update_count INTEGER DEFAULT 0)"""); background_image TEXT DEFAULT '',hosts TEXT DEFAULT '',update_count INTEGER DEFAULT 0,
episode_count INTEGER DEFAULT 0)""");
await db await db
.execute("""CREATE TABLE Episodes(id INTEGER PRIMARY KEY,title TEXT, .execute("""CREATE TABLE Episodes(id INTEGER PRIMARY KEY,title TEXT,
enclosure_url TEXT UNIQUE, enclosure_length INTEGER, pubDate TEXT, enclosure_url TEXT UNIQUE, enclosure_length INTEGER, pubDate TEXT,
@ -52,10 +53,9 @@ class DBHelper {
await Future.forEach(podcasts, (s) async { await Future.forEach(podcasts, (s) async {
List<Map> list; List<Map> list;
list = await dbClient.rawQuery( list = await dbClient.rawQuery(
'SELECT id, title, imageUrl, rssUrl, primaryColor, author, imagePath , provider, link ,update_count FROM PodcastLocal WHERE id = ?', """SELECT id, title, imageUrl, rssUrl, primaryColor, author, imagePath , provider,
link ,update_count, episode_count FROM PodcastLocal WHERE id = ?""",
[s]); [s]);
int count = Sqflite.firstIntValue(await dbClient.rawQuery(
'SELECT COUNT(*) FROM Episodes WHERE feed_id = ?', [s]));
podcastLocal.add(PodcastLocal( podcastLocal.add(PodcastLocal(
list.first['title'], list.first['title'],
list.first['imageUrl'], list.first['imageUrl'],
@ -67,7 +67,7 @@ class DBHelper {
list.first['provider'], list.first['provider'],
list.first['link'], list.first['link'],
upateCount: list.first['update_count'], upateCount: list.first['update_count'],
episodeCount: count)); episodeCount: list.first['episode_count']));
}); });
return podcastLocal; return podcastLocal;
} }
@ -94,6 +94,14 @@ class DBHelper {
return podcastLocal; return podcastLocal;
} }
Future<List<int>> getPodcastCounts(String id) async {
var dbClient = await database;
List<Map> list = await dbClient.rawQuery(
'SELECT update_count, episode_count FROM PodcastLocal WHERE id = ?',
[id]);
return [list.first['update_count'], list.first['episode_count']];
}
Future<bool> checkPodcast(String url) async { Future<bool> checkPodcast(String url) async {
var dbClient = await database; var dbClient = await database;
List<Map> list = await dbClient List<Map> list = await dbClient
@ -122,8 +130,6 @@ class DBHelper {
podcastLocal.provider, podcastLocal.provider,
podcastLocal.link podcastLocal.link
]); ]);
});
await dbClient.transaction((txn) async {
await txn.rawInsert( await txn.rawInsert(
"""REPLACE INTO SubscribeHistory(id, title, rss_url, add_date) VALUES (?, ?, ?, ?)""", """REPLACE INTO SubscribeHistory(id, title, rss_url, add_date) VALUES (?, ?, ?, ?)""",
[ [
@ -240,12 +246,17 @@ class DBHelper {
return (sum ~/ 60).toDouble(); return (sum ~/ 60).toDouble();
} }
Future<int> getPosition(EpisodeBrief episodeBrief) async { Future<PlayHistory> getPosition(EpisodeBrief episodeBrief) async {
var dbClient = await database; var dbClient = await database;
List<Map> list = await dbClient.rawQuery( List<Map> list = await dbClient.rawQuery(
"SELECT seconds FROM PlayHistory Where enclosure_url = ?", "SELECT title, enclosure_url, seconds, seek_value, add_date FROM PlayHistory Where enclosure_url = ?",
[episodeBrief.enclosureUrl]); [episodeBrief.enclosureUrl]);
return list.length > 0 ? list.first['seconds'] : 0; return list.length > 0
? PlayHistory(list.first['title'], list.first['enclosure_url'],
list.first['seconds'], list.first['seek_value'],
playdate:
DateTime.fromMillisecondsSinceEpoch(list.first['add_date']))
: PlayHistory(episodeBrief.title, episodeBrief.enclosureUrl, 0, 0);
} }
DateTime _parsePubDate(String pubDate) { DateTime _parsePubDate(String pubDate) {
@ -335,13 +346,6 @@ class DBHelper {
print(feed.items[i].title); print(feed.items[i].title);
description = _getDescription(feed.items[i].content.value ?? '', description = _getDescription(feed.items[i].content.value ?? '',
feed.items[i].description ?? '', feed.items[i].itunes.summary ?? ''); feed.items[i].description ?? '', feed.items[i].itunes.summary ?? '');
// if (feed.items[i].itunes.summary != null) {
// feed.items[i].itunes.summary.contains('<')
// ? description = feed.items[i].itunes.summary
// : description = feed.items[i].description;
// } else {
// description = feed.items[i].description;
// }
if (feed.items[i].enclosure != null) { if (feed.items[i].enclosure != null) {
_isXimalaya(feed.items[i].enclosure.url) _isXimalaya(feed.items[i].enclosure.url)
? url = feed.items[i].enclosure.url.split('=').last ? url = feed.items[i].enclosure.url.split('=').last
@ -377,6 +381,12 @@ class DBHelper {
}); });
} }
} }
int countUpdate = Sqflite.firstIntValue(await dbClient
.rawQuery('SELECT COUNT(*) FROM Episodes WHERE feed_id = ?', [id]));
await dbClient.rawUpdate(
"""UPDATE PodcastLocal SET episode_count = ? WHERE id = ?""",
[countUpdate, id]);
return result; return result;
} }
@ -391,57 +401,56 @@ class DBHelper {
int count = Sqflite.firstIntValue(await dbClient.rawQuery( int count = Sqflite.firstIntValue(await dbClient.rawQuery(
'SELECT COUNT(*) FROM Episodes WHERE feed_id = ?', [podcastLocal.id])); 'SELECT COUNT(*) FROM Episodes WHERE feed_id = ?', [podcastLocal.id]));
print(count);
await dbClient.rawUpdate( await dbClient.rawUpdate(
"""UPDATE PodcastLocal SET update_count = ? WHERE id = ?""", "UPDATE Episodes SET is_new = 0 WHERE feed_id = ?", [podcastLocal.id]);
[(result - count), podcastLocal.id]);
if (count == result) {
result = 0;
return result;
} else {
for (int i = 0; i < (result - count); 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) { for (int i = 0; i < result; i++) {
_isXimalaya(feed.items[i].enclosure.url) print(feed.items[i].title);
? url = feed.items[i].enclosure.url.split('=').last description = _getDescription(feed.items[i].content.value ?? '',
: url = feed.items[i].enclosure.url; feed.items[i].description ?? '', feed.items[i].itunes.summary ?? '');
}
final title = feed.items[i].itunes.title ?? feed.items[i].title; if (feed.items[i].enclosure?.url != null) {
final length = feed.items[i]?.enclosure?.length ?? 0; _isXimalaya(feed.items[i].enclosure.url)
final pubDate = feed.items[i].pubDate; ? url = feed.items[i].enclosure.url.split('=').last
final date = _parsePubDate(pubDate); : url = feed.items[i].enclosure.url;
final milliseconds = date.millisecondsSinceEpoch; }
final duration = feed.items[i].itunes.duration?.inMinutes ?? 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;
if (url != null) { final pubDate = feed.items[i].pubDate;
await dbClient.transaction((txn) { final date = _parsePubDate(pubDate);
return txn.rawInsert( final milliseconds = date.millisecondsSinceEpoch;
"""INSERT OR IGNORE INTO Episodes(title, enclosure_url, enclosure_length, pubDate, final duration = feed.items[i].itunes.duration?.inMinutes ?? 0;
description, feed_id, milliseconds, duration, explicit, media_id) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", final explicit = _getExplicit(feed.items[i].itunes.explicit);
[
title, if (url != null) {
url, await dbClient.transaction((txn) async {
length, int id = await txn.rawInsert(
pubDate, """INSERT OR IGNORE INTO Episodes(title, enclosure_url, enclosure_length, pubDate,
description, description, feed_id, milliseconds, duration, explicit, media_id, is_new) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1)""",
podcastLocal.id, [
milliseconds, title,
duration, url,
explicit, length,
url pubDate,
]); description,
}); podcastLocal.id,
} milliseconds,
duration,
explicit,
url,
]);
print("$id");
});
} }
return (result - count) < 0 ? 0 : (result - count);
} }
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;
} }
Future<List<EpisodeBrief>> getRssItem(String id, int i) async { Future<List<EpisodeBrief>> getRssItem(String id, int i) async {
@ -450,7 +459,7 @@ class DBHelper {
List<Map> list = await dbClient List<Map> list = await dbClient
.rawQuery("""SELECT E.title, E.enclosure_url, E.enclosure_length, .rawQuery("""SELECT E.title, E.enclosure_url, E.enclosure_length,
E.milliseconds, P.imagePath, P.title as feedTitle, E.duration, E.explicit, E.liked, E.milliseconds, P.imagePath, P.title as feedTitle, E.duration, E.explicit, E.liked,
E.downloaded, P.primaryColor , E.media_id E.downloaded, P.primaryColor , E.media_id, E.is_new
FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
WHERE P.id = ? ORDER BY E.milliseconds DESC LIMIT ?""", [id, i]); WHERE P.id = ? ORDER BY E.milliseconds DESC LIMIT ?""", [id, i]);
for (int x = 0; x < list.length; x++) { for (int x = 0; x < list.length; x++) {
@ -466,7 +475,8 @@ class DBHelper {
list[x]['duration'], list[x]['duration'],
list[x]['explicit'], list[x]['explicit'],
list[x]['imagePath'], list[x]['imagePath'],
list[x]['media_id'])); list[x]['media_id'],
list[x]['is_new']));
} }
return episodes; return episodes;
} }
@ -477,7 +487,7 @@ class DBHelper {
List<Map> list = await dbClient List<Map> list = await dbClient
.rawQuery("""SELECT E.title, E.enclosure_url, E.enclosure_length, .rawQuery("""SELECT E.title, E.enclosure_url, E.enclosure_length,
E.milliseconds, P.imagePath, P.title as feed_title, E.duration, E.explicit, E.liked, E.milliseconds, P.imagePath, P.title as feed_title, E.duration, E.explicit, E.liked,
E.downloaded, P.primaryColor, E.media_id E.downloaded, P.primaryColor, E.media_id, E.is_new
FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
where E.feed_id = ? ORDER BY E.milliseconds DESC LIMIT 3""", [id]); where E.feed_id = ? ORDER BY E.milliseconds DESC LIMIT 3""", [id]);
for (int x = 0; x < list.length; x++) { for (int x = 0; x < list.length; x++) {
@ -493,7 +503,8 @@ class DBHelper {
list[x]['duration'], list[x]['duration'],
list[x]['explicit'], list[x]['explicit'],
list[x]['imagePath'], list[x]['imagePath'],
list[x]['media_id'])); list[x]['media_id'],
list[x]['is_new']));
} }
return episodes; return episodes;
} }
@ -504,7 +515,7 @@ class DBHelper {
List<Map> list = await dbClient.rawQuery( List<Map> list = await dbClient.rawQuery(
"""SELECT E.title, E.enclosure_url, E.enclosure_length, """SELECT E.title, E.enclosure_url, E.enclosure_length,
E.milliseconds, P.imagePath, P.title as feed_title, E.duration, E.explicit, E.liked, E.milliseconds, P.imagePath, P.title as feed_title, E.duration, E.explicit, E.liked,
E.downloaded, P.primaryColor, E.media_id E.downloaded, P.primaryColor, E.media_id, E.is_new
FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
where E.enclosure_url = ? ORDER BY E.milliseconds DESC LIMIT 3""", where E.enclosure_url = ? ORDER BY E.milliseconds DESC LIMIT 3""",
[url]); [url]);
@ -522,7 +533,8 @@ class DBHelper {
list.first['duration'], list.first['duration'],
list.first['explicit'], list.first['explicit'],
list.first['imagePath'], list.first['imagePath'],
list.first['media_id']); list.first['media_id'],
list.first['is_new']);
return episode; return episode;
} }
@ -532,7 +544,7 @@ class DBHelper {
List<Map> list = await dbClient List<Map> list = await dbClient
.rawQuery("""SELECT E.title, E.enclosure_url, E.enclosure_length, .rawQuery("""SELECT E.title, E.enclosure_url, E.enclosure_length,
E.milliseconds, P.title as feed_title, E.duration, E.explicit, E.liked, E.milliseconds, P.title as feed_title, E.duration, E.explicit, E.liked,
E.downloaded, P.imagePath, P.primaryColor, E.media_id E.downloaded, P.imagePath, P.primaryColor, E.media_id, E.is_new
FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
ORDER BY E.milliseconds DESC LIMIT ? """, [top]); ORDER BY E.milliseconds DESC LIMIT ? """, [top]);
for (int x = 0; x < list.length; x++) { for (int x = 0; x < list.length; x++) {
@ -548,7 +560,8 @@ class DBHelper {
list[x]['duration'], list[x]['duration'],
list[x]['explicit'], list[x]['explicit'],
list[x]['imagePath'], list[x]['imagePath'],
list[x]['media_id'])); list[x]['media_id'],
list[x]['is_new']));
} }
return episodes; return episodes;
} }
@ -559,8 +572,8 @@ class DBHelper {
List<Map> list = await dbClient.rawQuery( List<Map> list = await dbClient.rawQuery(
"""SELECT E.title, E.enclosure_url, E.enclosure_length, E.milliseconds, P.imagePath, """SELECT E.title, E.enclosure_url, E.enclosure_length, E.milliseconds, P.imagePath,
P.title as feed_title, E.duration, E.explicit, E.liked, E.downloaded, P.title as feed_title, E.duration, E.explicit, E.liked, E.downloaded,
P.primaryColor, E.media_id FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id P.primaryColor, E.media_id, E.is_new FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
WHERE E.liked = 1 ORDER BY E.milliseconds DESC LIMIT ?""",[i]); WHERE E.liked = 1 ORDER BY E.milliseconds DESC LIMIT ?""", [i]);
for (int x = 0; x < list.length; x++) { for (int x = 0; x < list.length; x++) {
episodes.add(EpisodeBrief( episodes.add(EpisodeBrief(
list[x]['title'], list[x]['title'],
@ -574,7 +587,8 @@ class DBHelper {
list[x]['duration'], list[x]['duration'],
list[x]['explicit'], list[x]['explicit'],
list[x]['imagePath'], list[x]['imagePath'],
list[x]['media_id'])); list[x]['media_id'],
list[x]['is_new']));
} }
return episodes; return episodes;
} }
@ -597,19 +611,19 @@ class DBHelper {
Future<int> saveDownloaded(String url, String id) async { Future<int> saveDownloaded(String url, String id) async {
var dbClient = await database; var dbClient = await database;
int _milliseconds = DateTime.now().millisecondsSinceEpoch; int milliseconds = DateTime.now().millisecondsSinceEpoch;
int count = await dbClient.rawUpdate( int count = await dbClient.rawUpdate(
"UPDATE Episodes SET downloaded = ?, download_date = ? WHERE enclosure_url = ?", "UPDATE Episodes SET downloaded = ?, download_date = ? WHERE enclosure_url = ?",
[id, _milliseconds, url]); [id, milliseconds, url]);
return count; return count;
} }
Future<int> saveMediaId(String url, String path) async { Future<int> saveMediaId(String url, String path, String id) async {
var dbClient = await database; var dbClient = await database;
int _milliseconds = DateTime.now().millisecondsSinceEpoch; int milliseconds = DateTime.now().millisecondsSinceEpoch;
int count = await dbClient.rawUpdate( int count = await dbClient.rawUpdate(
"UPDATE Episodes SET media_id = ?, download_date = ? WHERE enclosure_url = ?", "UPDATE Episodes SET media_id = ?, download_date = ?, downloaded = ? WHERE enclosure_url = ?",
[path, _milliseconds, url]); [path, milliseconds, id, url]);
return count; return count;
} }
@ -628,8 +642,8 @@ class DBHelper {
List<Map> list = await dbClient.rawQuery( List<Map> list = await dbClient.rawQuery(
"""SELECT E.title, E.enclosure_url, E.enclosure_length, E.milliseconds, P.imagePath, """SELECT E.title, E.enclosure_url, E.enclosure_length, E.milliseconds, P.imagePath,
P.title as feed_title, E.duration, E.explicit, E.liked, E.downloaded, P.title as feed_title, E.duration, E.explicit, E.liked, E.downloaded,
P.primaryColor, E.media_id FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id P.primaryColor, E.media_id, E.is_new FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
WHERE E.downloaded != 'ND' ORDER BY E.download_date DESC LIMIT 99"""); WHERE E.downloaded != 'ND' ORDER BY E.download_date DESC""");
for (int x = 0; x < list.length; x++) { for (int x = 0; x < list.length; x++) {
episodes.add(EpisodeBrief( episodes.add(EpisodeBrief(
list[x]['title'], list[x]['title'],
@ -643,7 +657,8 @@ class DBHelper {
list[x]['duration'], list[x]['duration'],
list[x]['explicit'], list[x]['explicit'],
list[x]['imagePath'], list[x]['imagePath'],
list[x]['media_id'])); list[x]['media_id'],
list[x]['is_new']));
} }
return episodes; return episodes;
} }
@ -670,7 +685,7 @@ class DBHelper {
List<Map> list = await dbClient.rawQuery( List<Map> list = await dbClient.rawQuery(
"""SELECT E.title, E.enclosure_url, E.enclosure_length, E.milliseconds, P.imagePath, """SELECT E.title, E.enclosure_url, E.enclosure_length, E.milliseconds, P.imagePath,
P.title as feed_title, E.duration, E.explicit, E.liked, E.downloaded, P.title as feed_title, E.duration, E.explicit, E.liked, E.downloaded,
P.primaryColor, E.media_id FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id P.primaryColor, E.media_id, E.is_new FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
WHERE E.enclosure_url = ?""", [url]); WHERE E.enclosure_url = ?""", [url]);
if (list.length == 0) { if (list.length == 0) {
return null; return null;
@ -687,7 +702,8 @@ class DBHelper {
list.first['duration'], list.first['duration'],
list.first['explicit'], list.first['explicit'],
list.first['imagePath'], list.first['imagePath'],
list.first['media_id']); list.first['media_id'],
list.first['is_new']);
return episode; return episode;
} }
} }
@ -698,7 +714,7 @@ class DBHelper {
List<Map> list = await dbClient.rawQuery( List<Map> list = await dbClient.rawQuery(
"""SELECT E.title, E.enclosure_url, E.enclosure_length, E.milliseconds, P.imagePath, """SELECT E.title, E.enclosure_url, E.enclosure_length, E.milliseconds, P.imagePath,
P.title as feed_title, E.duration, E.explicit, E.liked, E.downloaded, P.title as feed_title, E.duration, E.explicit, E.liked, E.downloaded,
P.primaryColor, E.media_id FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id P.primaryColor, E.media_id, E.is_new FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
WHERE E.media_id = ?""", [id]); WHERE E.media_id = ?""", [id]);
episode = EpisodeBrief( episode = EpisodeBrief(
list.first['title'], list.first['title'],
@ -712,7 +728,8 @@ class DBHelper {
list.first['duration'], list.first['duration'],
list.first['explicit'], list.first['explicit'],
list.first['imagePath'], list.first['imagePath'],
list.first['media_id']); list.first['media_id'],
list.first['is_new']);
return episode; return episode;
} }
} }

View File

@ -8,12 +8,13 @@ import 'package:tsacdop/home/appbar/addpodcast.dart';
import 'package:tsacdop/class/audiostate.dart'; import 'package:tsacdop/class/audiostate.dart';
import 'package:tsacdop/class/importompl.dart'; import 'package:tsacdop/class/importompl.dart';
import 'package:tsacdop/class/settingstate.dart'; import 'package:tsacdop/class/settingstate.dart';
import 'package:tsacdop/class/download_state.dart';
final SettingState themeSetting = SettingState(); final SettingState themeSetting = SettingState();
Future main() async { Future main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
await themeSetting.initData(); await themeSetting.initData();
await FlutterDownloader.initialize();
runApp( runApp(
MultiProvider( MultiProvider(
providers: [ providers: [
@ -21,13 +22,16 @@ Future main() async {
ChangeNotifierProvider(create: (_) => AudioPlayerNotifier()), ChangeNotifierProvider(create: (_) => AudioPlayerNotifier()),
ChangeNotifierProvider(create: (_) => GroupList()), ChangeNotifierProvider(create: (_) => GroupList()),
ChangeNotifierProvider(create: (_) => ImportOmpl()), ChangeNotifierProvider(create: (_) => ImportOmpl()),
ChangeNotifierProvider(create: (_) => DownloadState(),
)
], ],
child: MyApp(), child: MyApp(),
), ),
); );
await FlutterDownloader.initialize();
SystemUiOverlayStyle systemUiOverlayStyle = SystemUiOverlayStyle systemUiOverlayStyle = SystemUiOverlayStyle(
SystemUiOverlayStyle(statusBarColor: Colors.transparent, systemNavigationBarColor: Colors.transparent); statusBarColor: Colors.transparent,
systemNavigationBarColor: Colors.transparent);
SystemChrome.setSystemUIOverlayStyle(systemUiOverlayStyle); SystemChrome.setSystemUIOverlayStyle(systemUiOverlayStyle);
await SystemChrome.setPreferredOrientations( await SystemChrome.setPreferredOrientations(
@ -67,7 +71,7 @@ class MyApp extends StatelessWidget {
darkTheme: ThemeData.dark().copyWith( darkTheme: ThemeData.dark().copyWith(
accentColor: setting.accentSetColor, accentColor: setting.accentSetColor,
primaryColorDark: Colors.grey[800], primaryColorDark: Colors.grey[800],
// scaffoldBackgroundColor: Colors.black87, // scaffoldBackgroundColor: Colors.black87,
appBarTheme: AppBarTheme(elevation: 0), appBarTheme: AppBarTheme(elevation: 0),
), ),
home: MyHomePage(), home: MyHomePage(),

View File

@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:html/parser.dart'; import 'package:html/parser.dart';
import 'package:tsacdop/class/audiostate.dart'; import 'package:tsacdop/class/audiostate.dart';
import 'package:tsacdop/class/podcast_group.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:fluttertoast/fluttertoast.dart'; import 'package:fluttertoast/fluttertoast.dart';
import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:flutter_linkify/flutter_linkify.dart';
@ -35,15 +36,18 @@ class _PodcastDetailState extends State<PodcastDetail> {
Future _updateRssItem(PodcastLocal podcastLocal) async { Future _updateRssItem(PodcastLocal podcastLocal) async {
var dbHelper = DBHelper(); var dbHelper = DBHelper();
final result = await dbHelper.updatePodcastRss(podcastLocal); final result = await dbHelper.updatePodcastRss(podcastLocal);
result == 0 if(result == 0)
? Fluttertoast.showToast( { Fluttertoast.showToast(
msg: 'No Update', msg: 'No Update',
gravity: ToastGravity.TOP, gravity: ToastGravity.TOP,
) );}
: Fluttertoast.showToast( else{
Fluttertoast.showToast(
msg: 'Updated $result Episodes', msg: 'Updated $result Episodes',
gravity: ToastGravity.TOP, gravity: ToastGravity.TOP,
); );
Provider.of<GroupList>(context, listen: false).updatePodcast(podcastLocal);
}
if (mounted) setState(() {}); if (mounted) setState(() {});
} }
@ -400,8 +404,6 @@ class _PodcastDetailState extends State<PodcastDetail> {
episodes: snapshot.data, episodes: snapshot.data,
showFavorite: true, showFavorite: true,
showNumber: true, showNumber: true,
updateCount:
widget.podcastLocal.upateCount,
episodeCount: episodeCount:
widget.podcastLocal.episodeCount, widget.podcastLocal.episodeCount,
), ),

View File

@ -40,7 +40,7 @@ class _PodcastGroupListState extends State<PodcastGroupList> {
widget.group.podcasts.insert(newIndex, podcast); widget.group.podcasts.insert(newIndex, podcast);
}); });
widget.group.setOrderedPodcasts = widget.group.podcasts; widget.group.setOrderedPodcasts = widget.group.podcasts;
groupList.addToOrderChanged(widget.group.name); groupList.addToOrderChanged(widget.group);
}, },
children: widget.group.podcasts children: widget.group.podcasts
.map<Widget>((PodcastLocal podcastLocal) { .map<Widget>((PodcastLocal podcastLocal) {
@ -319,7 +319,7 @@ class _PodcastCardState extends State<PodcastCard> {
bottom: 20), bottom: 20),
title: Text('Remove confirm'), title: Text('Remove confirm'),
content: Text( content: Text(
'Are you sure you want to unsubscribe?'), 'Are you sure you want to unsubscribe?'),
actions: <Widget>[ actions: <Widget>[
FlatButton( FlatButton(
onPressed: () => onPressed: () =>

View File

@ -72,7 +72,7 @@ class _PodcastManageState extends State<PodcastManage>
Widget _saveButton(BuildContext context) { Widget _saveButton(BuildContext context) {
return Consumer<GroupList>( return Consumer<GroupList>(
builder: (_, groupList, __) { builder: (_, groupList, __) {
if (groupList.orderChanged.contains(groupList.groups[_index].name)) { if (groupList.orderChanged.contains(groupList.groups[_index])) {
_controller.forward(); _controller.forward();
} else if (_fraction > 0) { } else if (_fraction > 0) {
_controller.reverse(); _controller.reverse();
@ -92,8 +92,8 @@ class _PodcastManageState extends State<PodcastManage>
shape: BoxShape.circle, shape: BoxShape.circle,
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.grey[700], color: Colors.grey[700].withOpacity(0.5),
blurRadius: 5, blurRadius: 1,
offset: Offset(1, 1), offset: Offset(1, 1),
), ),
]), ]),
@ -161,298 +161,304 @@ class _PodcastManageState extends State<PodcastManage>
OrderMenu(), OrderMenu(),
], ],
), ),
body: Consumer<GroupList>(builder: (_, groupList, __) { body: WillPopScope(
bool _isLoading = groupList.isLoading; onWillPop: () async {
List<PodcastGroup> _groups = groupList.groups; await Provider.of<GroupList>(context, listen: false).clearOrderChanged();
return _isLoading return true;
? Center() },
: Stack( child: Consumer<GroupList>(builder: (_, groupList, __) {
children: <Widget>[ bool _isLoading = groupList.isLoading;
CustomTabView( List<PodcastGroup> _groups = groupList.groups;
itemCount: _groups.length, return _isLoading
tabBuilder: (context, index) => Tab( ? Center()
child: Container( : Stack(
height: 30.0, children: <Widget>[
padding: EdgeInsets.symmetric(horizontal: 10.0), CustomTabView(
alignment: Alignment.center, itemCount: _groups.length,
decoration: BoxDecoration( tabBuilder: (context, index) => Tab(
color: (_scroll - index).abs() > 1 child: Container(
? Colors.grey[300] height: 30.0,
: Colors.grey[300] padding: EdgeInsets.symmetric(horizontal: 10.0),
.withOpacity((_scroll - index).abs()), alignment: Alignment.center,
borderRadius: decoration: BoxDecoration(
BorderRadius.all(Radius.circular(15)), color: (_scroll - index).abs() > 1
), ? Colors.grey[300]
child: Text( : Colors.grey[300]
_groups[index].name, .withOpacity((_scroll - index).abs()),
)), borderRadius:
), BorderRadius.all(Radius.circular(15)),
pageBuilder: (context, index) => Container(
key: ObjectKey(_groups[index].name),
child: PodcastGroupList(group: _groups[index])),
onPositionChange: (value) =>
setState(() => _index = value),
onScroll: (value) => setState(() => _scroll = value),
),
_showSetting
? Positioned.fill(
top: 50,
child: GestureDetector(
onTap: () async {
await _menuController.reverse();
setState(() => _showSetting = false);
},
child: Container(
color: Theme.of(context)
.scaffoldBackgroundColor
.withOpacity(0.5 * _menuController.value),
), ),
), child: Text(
) _groups[index].name,
: Center(), )),
Positioned( ),
right: 30, pageBuilder: (context, index) => Container(
bottom: 30, key: ValueKey(_groups[index].name),
child: _saveButton(context), child: PodcastGroupList(group: _groups[index])),
), onPositionChange: (value) =>
_showSetting setState(() => _index = value),
? Positioned( onScroll: (value) => setState(() => _scroll = value),
right: 30 * _menuValue, ),
bottom: 100, _showSetting
child: Container( ? Positioned.fill(
alignment: Alignment.centerRight, top: 50,
child: Column( child: GestureDetector(
mainAxisAlignment: MainAxisAlignment.start, onTap: () async {
crossAxisAlignment: CrossAxisAlignment.end, await _menuController.reverse();
children: <Widget>[ setState(() => _showSetting = false);
Material( },
color: Colors.transparent, child: Container(
child: InkWell( color: Theme.of(context)
onTap: () { .scaffoldBackgroundColor
_menuController.reverse(); .withOpacity(0.5 * _menuController.value),
setState(() => _showSetting = false); ),
_index == 0 ),
? Fluttertoast.showToast( )
msg: : Center(),
'Home group is not supported', Positioned(
gravity: ToastGravity.BOTTOM, right: 30,
) bottom: 30,
: showGeneralDialog( child: _saveButton(context),
context: context, ),
barrierDismissible: true, _showSetting
barrierLabel: ? Positioned(
MaterialLocalizations.of( right: 30 * _menuValue,
context) bottom: 100,
.modalBarrierDismissLabel, child: Container(
barrierColor: Colors.black54, alignment: Alignment.centerRight,
transitionDuration: child: Column(
const Duration( mainAxisAlignment: MainAxisAlignment.start,
milliseconds: 300), crossAxisAlignment: CrossAxisAlignment.end,
pageBuilder: (BuildContext children: <Widget>[
context, Material(
Animation animaiton, color: Colors.transparent,
Animation child: InkWell(
secondaryAnimation) => onTap: () {
RenameGroup( _menuController.reverse();
group: _groups[_index], setState(() => _showSetting = false);
)); _index == 0
}, ? Fluttertoast.showToast(
child: Container( msg:
height: 30.0, 'Home group is not supported',
decoration: BoxDecoration( gravity: ToastGravity.BOTTOM,
color: Colors.grey[700], )
borderRadius: BorderRadius.all( : showGeneralDialog(
Radius.circular(10.0))), context: context,
padding: EdgeInsets.symmetric( barrierDismissible: true,
horizontal: 10), barrierLabel:
child: Row( MaterialLocalizations.of(
children: <Widget>[ context)
Icon( .modalBarrierDismissLabel,
Icons.text_fields, barrierColor: Colors.black54,
color: Colors.white, transitionDuration:
size: 15.0, const Duration(
), milliseconds: 300),
Padding( pageBuilder: (BuildContext
padding: EdgeInsets.symmetric( context,
horizontal: 5.0), Animation animaiton,
), Animation
Text('Edit Name', secondaryAnimation) =>
style: TextStyle( RenameGroup(
color: Colors.white)), group: _groups[_index],
], ));
},
child: Container(
height: 30.0,
decoration: BoxDecoration(
color: Colors.grey[700],
borderRadius: BorderRadius.all(
Radius.circular(10.0))),
padding: EdgeInsets.symmetric(
horizontal: 10),
child: Row(
children: <Widget>[
Icon(
Icons.text_fields,
color: Colors.white,
size: 15.0,
),
Padding(
padding: EdgeInsets.symmetric(
horizontal: 5.0),
),
Text('Edit Name',
style: TextStyle(
color: Colors.white)),
],
),
), ),
), ),
), ),
), Padding(
Padding( padding: EdgeInsets.symmetric(
padding: vertical: 10.0)),
EdgeInsets.symmetric(vertical: 10.0)), Material(
Material( color: Colors.transparent,
color: Colors.transparent, child: InkWell(
child: InkWell( onTap: () {
onTap: () { _menuController.reverse();
_menuController.reverse(); setState(() => _showSetting = false);
setState(() => _showSetting = false); _index == 0
_index == 0 ? Fluttertoast.showToast(
? Fluttertoast.showToast( msg:
msg: 'Home group is not supported',
'Home group is not supported', gravity: ToastGravity.BOTTOM,
gravity: ToastGravity.BOTTOM, )
) : showGeneralDialog(
: showGeneralDialog( context: context,
context: context, barrierDismissible: true,
barrierDismissible: true, barrierLabel:
barrierLabel: MaterialLocalizations.of(
MaterialLocalizations.of( context)
context) .modalBarrierDismissLabel,
.modalBarrierDismissLabel, barrierColor: Colors.black54,
barrierColor: Colors.black54, transitionDuration:
transitionDuration: const Duration(
const Duration( milliseconds: 300),
milliseconds: 300), pageBuilder: (BuildContext
pageBuilder: (BuildContext context,
context, Animation animaiton,
Animation animaiton, Animation
Animation secondaryAnimation) =>
secondaryAnimation) => AnnotatedRegion<
AnnotatedRegion< SystemUiOverlayStyle>(
SystemUiOverlayStyle>( value:
value: SystemUiOverlayStyle(
SystemUiOverlayStyle( statusBarIconBrightness:
statusBarIconBrightness: Brightness.light,
Brightness.light, systemNavigationBarColor:
systemNavigationBarColor: Theme.of(context)
Theme.of(context) .brightness ==
.brightness == Brightness
Brightness .light
.light ? Color
? Color .fromRGBO(
.fromRGBO( 113,
113, 113,
113, 113,
113, 1)
1) : Color
: Color .fromRGBO(
.fromRGBO( 15,
15, 15,
15, 15,
15, 1),
1), // statusBarColor: Theme.of(
// statusBarColor: Theme.of( // context)
// context) // .brightness ==
// .brightness == // Brightness.light
// Brightness.light // ? Color.fromRGBO(
// ? Color.fromRGBO( // 113,
// 113, // 113,
// 113, // 113,
// 113, // 1)
// 1) // : Color.fromRGBO(
// : Color.fromRGBO( // 5, 5, 5, 1),
// 5, 5, 5, 1), ),
), child: AlertDialog(
child: AlertDialog( elevation: 1,
elevation: 1, shape: RoundedRectangleBorder(
shape: RoundedRectangleBorder( borderRadius: BorderRadius
borderRadius: .all(Radius
BorderRadius.all( .circular(
Radius.circular( 10.0))),
10.0))), titlePadding:
titlePadding: EdgeInsets.only(
EdgeInsets.only( top: 20,
top: 20, left: 20,
left: 20, right: 200,
right: 200, bottom: 20),
bottom: 20), title: Text(
title: Text( 'Delete confirm'),
'Delete confirm'), content: Text(
content: Text( 'Are you sure you want to delete this group? Podcasts will be moved to Home group.'),
'Are you sure you want to delete this group? Podcasts will be moved to Home group.'), actions: <Widget>[
actions: <Widget>[ FlatButton(
FlatButton( onPressed: () =>
onPressed: () => Navigator.of(
context)
.pop(),
child: Text(
'CANCEL',
style: TextStyle(
color: Colors
.grey[
600]),
),
),
FlatButton(
onPressed: () {
if (_index ==
groupList
.groups
.length -
1) {
setState(() {
_index =
_index -
1;
_scroll = 0;
});
groupList.delGroup(
_groups[
_index +
1]);
} else {
groupList.delGroup(
_groups[
_index]);
}
Navigator.of( Navigator.of(
context) context)
.pop(), .pop();
child: Text( },
'CANCEL', child: Text(
style: TextStyle( 'CONFIRM',
color: Colors style: TextStyle(
.grey[ color: Colors
600]), .red),
), ),
), )
FlatButton( ],
onPressed: () { ),
if (_index == ));
groupList },
.groups child: Container(
.length - height: 30,
1) { decoration: BoxDecoration(
setState(() { color: Colors.grey[700],
_index = borderRadius: BorderRadius.all(
_index - Radius.circular(10.0))),
1; padding: EdgeInsets.symmetric(
_scroll = 0; horizontal: 10),
}); child: Row(
groupList.delGroup( children: <Widget>[
_groups[ Icon(
_index + Icons.delete_outline,
1]); color: Colors.red,
} else { size: 15.0,
groupList.delGroup( ),
_groups[ Padding(
_index]); padding: EdgeInsets.symmetric(
} horizontal: 5.0),
Navigator.of( ),
context) Text('Delete',
.pop(); style: TextStyle(
}, color: Colors.red)),
child: Text( ],
'CONFIRM', ),
style: TextStyle(
color: Colors
.red),
),
)
],
),
));
},
child: Container(
height: 30,
decoration: BoxDecoration(
color: Colors.grey[700],
borderRadius: BorderRadius.all(
Radius.circular(10.0))),
padding: EdgeInsets.symmetric(
horizontal: 10),
child: Row(
children: <Widget>[
Icon(
Icons.delete_outline,
color: Colors.red,
size: 15.0,
),
Padding(
padding: EdgeInsets.symmetric(
horizontal: 5.0),
),
Text('Delete',
style: TextStyle(
color: Colors.red)),
],
), ),
), ),
), ),
), ],
], ),
), ),
), )
) : Center(),
: Center(), ],
], );
); }),
}), ),
), ),
); );
} }

View File

@ -13,6 +13,7 @@ import 'package:flutter_file_dialog/flutter_file_dialog.dart';
import 'package:tsacdop/class/audiostate.dart'; import 'package:tsacdop/class/audiostate.dart';
import 'package:tsacdop/util/ompl_build.dart'; import 'package:tsacdop/util/ompl_build.dart';
import 'package:tsacdop/util/context_extension.dart';
import 'theme.dart'; import 'theme.dart';
import 'storage.dart'; import 'storage.dart';
import 'history.dart'; import 'history.dart';
@ -56,7 +57,7 @@ class Settings extends StatelessWidget {
appBar: AppBar( appBar: AppBar(
title: Text('Settings'), title: Text('Settings'),
elevation: 0, elevation: 0,
backgroundColor: Theme.of(context).primaryColor, backgroundColor: context.primaryColor,
), ),
body: SafeArea( body: SafeArea(
child: SingleChildScrollView( child: SingleChildScrollView(
@ -81,7 +82,7 @@ class Settings extends StatelessWidget {
style: Theme.of(context) style: Theme.of(context)
.textTheme .textTheme
.bodyText1 .bodyText1
.copyWith(color: Theme.of(context).accentColor)), .copyWith(color: context.accentColor)),
), ),
ListView( ListView(
physics: ClampingScrollPhysics(), physics: ClampingScrollPhysics(),

View File

@ -1,11 +1,14 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:app_settings/app_settings.dart'; import 'package:app_settings/app_settings.dart';
import 'package:tsacdop/settings/downloads_manage.dart'; import 'package:tsacdop/settings/downloads_manage.dart';
import 'package:tsacdop/class/settingstate.dart';
class StorageSetting extends StatelessWidget { class StorageSetting extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var settings = Provider.of<SettingState>(context, listen: false);
return AnnotatedRegion<SystemUiOverlayStyle>( return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle( value: SystemUiOverlayStyle(
statusBarIconBrightness: Theme.of(context).accentColorBrightness, statusBarIconBrightness: Theme.of(context).accentColorBrightness,
@ -24,6 +27,54 @@ class StorageSetting extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: <Widget>[
Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Padding(
padding: EdgeInsets.all(10.0),
),
Container(
height: 30.0,
padding: EdgeInsets.symmetric(horizontal: 80),
alignment: Alignment.centerLeft,
child: Text('Network',
style: Theme.of(context)
.textTheme
.bodyText1
.copyWith(color: Theme.of(context).accentColor)),
),
ListView(
physics: const BouncingScrollPhysics(),
shrinkWrap: true,
scrollDirection: Axis.vertical,
children: <Widget>[
ListTile(
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DownloadsManage())),
contentPadding:
EdgeInsets.only(left: 80.0, right: 25),
title: Text('Ask before using cellular data'),
subtitle: Text(
'Ask to confirem when using cellular data to download episodes.'),
trailing: Selector<SettingState, bool>(
selector: (_, settings) =>
settings.downloadUsingData,
builder: (_, data, __) {
return Switch(
value: data,
onChanged: (value) =>
settings.downloadUsingData = value,
);
},
),
),
Divider(height: 2),
],
),
]),
Column( Column(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,

View File

@ -71,11 +71,10 @@ class SyncingSetting extends StatelessWidget {
value: data.item1, value: data.item1,
onChanged: (boo) async { onChanged: (boo) async {
settings.autoUpdate = boo; settings.autoUpdate = boo;
if (boo) { if (boo)
settings.setWorkManager(data.item2); settings.setWorkManager(data.item2);
} else { else
settings.cancelWork(); settings.cancelWork();
}
}), }),
), ),
Divider(height: 2), Divider(height: 2),
@ -92,7 +91,8 @@ class SyncingSetting extends StatelessWidget {
elevation: 1, elevation: 1,
value: data.item2, value: data.item2,
onChanged: data.item1 onChanged: data.item1
? (value) { ? (value) async {
await settings.cancelWork();
settings.setWorkManager(value); settings.setWorkManager(value);
} }
: null, : null,

View File

@ -3,6 +3,7 @@ import 'package:flutter/services.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:flutter_colorpicker/flutter_colorpicker.dart'; import 'package:flutter_colorpicker/flutter_colorpicker.dart';
import 'package:tsacdop/class/settingstate.dart'; import 'package:tsacdop/class/settingstate.dart';
import 'package:tsacdop/util/context_extension.dart';
class ThemeSetting extends StatelessWidget { class ThemeSetting extends StatelessWidget {
@override @override
@ -61,11 +62,11 @@ class ThemeSetting extends StatelessWidget {
AnnotatedRegion<SystemUiOverlayStyle>( AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle( value: SystemUiOverlayStyle(
statusBarIconBrightness: Brightness.light, statusBarIconBrightness: Brightness.light,
// systemNavigationBarColor: systemNavigationBarColor:
// Theme.of(context).brightness == Theme.of(context).brightness ==
// Brightness.light Brightness.light
// ? Color.fromRGBO(113, 113, 113, 1) ? Color.fromRGBO(113, 113, 113, 1)
// : Color.fromRGBO(15, 15, 15, 1), : Color.fromRGBO(15, 15, 15, 1),
// statusBarColor: // statusBarColor:
// Theme.of(context).brightness == // Theme.of(context).brightness ==
// Brightness.light // Brightness.light
@ -138,11 +139,11 @@ class ThemeSetting extends StatelessWidget {
AnnotatedRegion<SystemUiOverlayStyle>( AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle( value: SystemUiOverlayStyle(
statusBarIconBrightness: Brightness.light, statusBarIconBrightness: Brightness.light,
// systemNavigationBarColor: systemNavigationBarColor:
// Theme.of(context).brightness == Theme.of(context).brightness ==
// Brightness.light Brightness.light
// ? Color.fromRGBO(113, 113, 113, 1) ? Color.fromRGBO(113, 113, 113, 1)
// : Color.fromRGBO(15, 15, 15, 1), : Color.fromRGBO(15, 15, 15, 1),
// statusBarColor: // statusBarColor:
// Theme.of(context).brightness == // Theme.of(context).brightness ==
// Brightness.light // Brightness.light
@ -155,7 +156,7 @@ class ThemeSetting extends StatelessWidget {
top: 20, top: 20,
left: 40, left: 40,
right: 200, right: 200,
bottom: 20), bottom: 0),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all( borderRadius: BorderRadius.all(
Radius.circular(10.0))), Radius.circular(10.0))),
@ -165,7 +166,7 @@ class ThemeSetting extends StatelessWidget {
onColorChanged: (value) { onColorChanged: (value) {
settings.setAccentColor = value; settings.setAccentColor = value;
}, },
pickerColor: Colors.blue, pickerColor: context.accentColor,
), ),
), ),
))), ))),

View File

@ -0,0 +1,11 @@
import 'package:flutter/material.dart';
extension ContextExtension on BuildContext{
Color get primaryColor => Theme.of(this).primaryColor;
Color get accentColor => Theme.of(this).accentColor;
Color get scaffoldBackgroundColor => Theme.of(this).scaffoldBackgroundColor;
Color get primaryColorDark => Theme.of(this).primaryColorDark;
Brightness get brightness => Theme.of(this).brightness;
double get width => MediaQuery.of(this).size.width;
double get height => MediaQuery.of(this).size.width;
}

View File

@ -1,5 +1,6 @@
import 'dart:io'; import 'dart:io';
import 'dart:isolate'; import 'dart:isolate';
import 'dart:async';
import 'dart:ui'; import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -21,18 +22,15 @@ class EpisodeGrid extends StatelessWidget {
final bool showFavorite; final bool showFavorite;
final bool showDownload; final bool showDownload;
final bool showNumber; final bool showNumber;
final int updateCount;
final String heroTag;
final int episodeCount; final int episodeCount;
EpisodeGrid( EpisodeGrid(
{Key key, {Key key,
@required this.episodes, @required this.episodes,
this.heroTag = '',
this.showDownload = false, this.showDownload = false,
this.showFavorite = false, this.showFavorite = false,
this.showNumber = false, this.showNumber = false,
this.updateCount = 0, this.episodeCount = 0
this.episodeCount = 0}) })
: super(key: key); : super(key: key);
@override @override
@ -183,7 +181,7 @@ class EpisodeGrid extends StatelessWidget {
), ),
), ),
Spacer(), Spacer(),
index < updateCount episodes[index].isNew == 1
? Text('New', ? Text('New',
style: TextStyle( style: TextStyle(
color: Colors.red, color: Colors.red,
@ -197,7 +195,7 @@ class EpisodeGrid extends StatelessWidget {
? Container( ? Container(
alignment: Alignment.topRight, alignment: Alignment.topRight,
child: Text( child: Text(
(episodeCount- index).toString(), (episodeCount - index).toString(),
style: GoogleFonts.teko( style: GoogleFonts.teko(
textStyle: TextStyle( textStyle: TextStyle(
fontSize: _width / 24, fontSize: _width / 24,
@ -240,10 +238,6 @@ class EpisodeGrid extends StatelessWidget {
), ),
), ),
Spacer(), Spacer(),
showDownload
? DownloadIcon(
episodeBrief: episodes[index])
: Center(),
Padding( Padding(
padding: EdgeInsets.all(1), padding: EdgeInsets.all(1),
), ),
@ -280,148 +274,6 @@ class EpisodeGrid extends StatelessWidget {
} }
} }
class DownloadIcon extends StatefulWidget {
final EpisodeBrief episodeBrief;
DownloadIcon({this.episodeBrief, Key key}) : super(key: key);
@override
_DownloadIconState createState() => _DownloadIconState();
}
class _DownloadIconState extends State<DownloadIcon> {
_TaskInfo _task;
bool _isLoading;
ReceivePort _port = ReceivePort();
@override
void initState() {
super.initState();
_bindBackgroundIsolate();
FlutterDownloader.registerCallback(downloadCallback);
_isLoading = true;
_prepare();
}
@override
void dispose() {
_unbindBackgroundIsolate();
super.dispose();
}
void _bindBackgroundIsolate() {
bool isSuccess = IsolateNameServer.registerPortWithName(
_port.sendPort, 'downloader_send_port');
if (!isSuccess) {
_unbindBackgroundIsolate();
_bindBackgroundIsolate();
return;
}
_port.listen((dynamic data) {
print('UI isolate callback: $data');
String id = data[0];
DownloadTaskStatus status = data[1];
int progress = data[2];
if (_task.taskId == id) {
setState(() {
_task.status = status;
_task.progress = progress;
});
}
});
}
void _unbindBackgroundIsolate() {
IsolateNameServer.removePortNameMapping('downloader_send_port');
}
static void downloadCallback(
String id, DownloadTaskStatus status, int progress) {
print('Background callback task in $id status ($status) $progress');
final SendPort send =
IsolateNameServer.lookupPortByName('downloader_send_port');
send.send([id, status, progress]);
}
Future<Null> _prepare() async {
final tasks = await FlutterDownloader.loadTasks();
_task = _TaskInfo(
name: widget.episodeBrief.title,
link: widget.episodeBrief.enclosureUrl);
tasks?.forEach((task) {
if (_task.link == task.url) {
_task.taskId = task.taskId;
_task.status = task.status;
_task.progress = task.progress;
}
});
setState(() {
_isLoading = false;
});
}
@override
Widget build(BuildContext context) {
return _downloadButton(_task);
}
Widget _downloadButton(_TaskInfo task) {
if (_isLoading)
return Center();
else if (task.status == DownloadTaskStatus.running) {
return SizedBox(
height: 12,
width: 12,
child: CircularProgressIndicator(
backgroundColor: Colors.grey[200],
strokeWidth: 1,
valueColor:
AlwaysStoppedAnimation<Color>(Theme.of(context).accentColor),
value: task.progress / 100,
),
);
} else if (task.status == DownloadTaskStatus.paused) {
return SizedBox(
height: 12,
width: 12,
child: CircularProgressIndicator(
backgroundColor: Colors.grey[200],
strokeWidth: 1,
valueColor: AlwaysStoppedAnimation<Color>(Colors.red),
value: task.progress / 100,
),
);
} else if (task.status == DownloadTaskStatus.complete) {
return IconTheme(
data: IconThemeData(size: 15),
child: Icon(
Icons.done_all,
color: Theme.of(context).accentColor,
),
);
} else if (task.status == DownloadTaskStatus.failed) {
return IconTheme(
data: IconThemeData(size: 15),
child: Icon(Icons.refresh, color: Colors.red),
);
}
return Center();
}
}
class _TaskInfo {
final String name;
final String link;
String taskId;
int progress = 0;
DownloadTaskStatus status = DownloadTaskStatus.undefined;
_TaskInfo({this.name, this.link});
}
class OpenContainerWrapper extends StatelessWidget { class OpenContainerWrapper extends StatelessWidget {
const OpenContainerWrapper({ const OpenContainerWrapper({
@ -469,3 +321,5 @@ class OpenContainerWrapper extends StatelessWidget {
); );
} }
} }

View File

@ -53,13 +53,18 @@ dev_dependencies:
app_settings: ^3.0.1 app_settings: ^3.0.1
fl_chart: ^0.8.7 fl_chart: ^0.8.7
audio_service: ^0.6.2 audio_service: ^0.6.2
just_audio: ^0.1.3 just_audio:
git:
url: https://github.com/stonega/just_audio.git
rxdart: ^0.23.1 rxdart: ^0.23.1
line_icons: line_icons:
git: git:
url: https://github.com/galonsos/line_icons.git url: https://github.com/galonsos/line_icons.git
flutter_file_dialog: ^0.0.5 flutter_file_dialog: ^0.0.5
flutter_linkify: ^3.1.0 flutter_linkify: ^3.1.0
extended_nested_scroll_view: ^0.4.0
connectivity: ^0.4.8+2
# For information on the generic Dart part of this file, see the # For information on the generic Dart part of this file, see the
# following page: https://www.dartlang.org/tools/pub/pubspec # following page: https://www.dartlang.org/tools/pub/pubspec