A lot of bug fixed
This commit is contained in:
parent
62100085b0
commit
a1d004aa43
|
@ -2,7 +2,7 @@
|
|||
[![CircleCI](https://circleci.com/gh/stonega/tsacdop.svg?style=svg)](https://circleci.com/gh/stonega/workflows/tsacdop/)
|
||||
## About
|
||||
<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>
|
||||
<img src="https://raw.githubusercontent.com/stonega/tsacdop/master/android/app/src/main/res/mipmap-xhdpi/text.png" art = "Tsacdop"/>
|
||||
</p>
|
||||
|
@ -18,7 +18,7 @@ The podcasts search engine is powered by [ListenNotes](https://listennotes.com).
|
|||
|
||||
## 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
|
||||
|
||||
|
|
|
@ -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
|
|
@ -59,12 +59,10 @@ class PlayHistory {
|
|||
}
|
||||
}
|
||||
|
||||
class Playlist {
|
||||
class Playlist extends ChangeNotifier {
|
||||
String name;
|
||||
DBHelper dbHelper = DBHelper();
|
||||
// list of urls
|
||||
//List<String> _urls;
|
||||
//list of episodes
|
||||
|
||||
List<EpisodeBrief> _playlist;
|
||||
//list of miediaitem
|
||||
|
||||
|
@ -73,14 +71,13 @@ class Playlist {
|
|||
|
||||
getPlaylist() async {
|
||||
List<String> urls = await storage.getStringList();
|
||||
print(urls);
|
||||
if (urls.length == 0) {
|
||||
_playlist = [];
|
||||
} else {
|
||||
_playlist = [];
|
||||
await Future.forEach(urls, (url) async {
|
||||
EpisodeBrief episode = await dbHelper.getRssItemWithUrl(url);
|
||||
if(episode != null) _playlist.add(episode);
|
||||
if (episode != null) _playlist.add(episode);
|
||||
});
|
||||
}
|
||||
print('Playlist: ' + _playlist.length.toString());
|
||||
|
@ -89,7 +86,6 @@ class Playlist {
|
|||
savePlaylist() async {
|
||||
List<String> urls = [];
|
||||
urls.addAll(_playlist.map((e) => e.enclosureUrl));
|
||||
print(urls);
|
||||
await storage.saveStringList(urls);
|
||||
}
|
||||
|
||||
|
@ -103,10 +99,12 @@ class Playlist {
|
|||
await savePlaylist();
|
||||
}
|
||||
|
||||
delFromPlaylist(EpisodeBrief episodeBrief) async {
|
||||
Future<int> delFromPlaylist(EpisodeBrief episodeBrief) async {
|
||||
int index = _playlist.indexOf(episodeBrief);
|
||||
_playlist
|
||||
.removeWhere((item) => item.enclosureUrl == episodeBrief.enclosureUrl);
|
||||
await savePlaylist();
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -115,6 +113,7 @@ class AudioPlayerNotifier extends ChangeNotifier {
|
|||
KeyValueStorage storage = KeyValueStorage('audioposition');
|
||||
EpisodeBrief _episode;
|
||||
Playlist _queue = Playlist();
|
||||
bool _queueUpdate = false;
|
||||
BasicPlaybackState _audioState = BasicPlaybackState.none;
|
||||
bool _playerRunning = false;
|
||||
bool _noSlide = true;
|
||||
|
@ -127,7 +126,6 @@ class AudioPlayerNotifier extends ChangeNotifier {
|
|||
bool _stopOnComplete = false;
|
||||
Timer _stopTimer;
|
||||
int _timeLeft = 0;
|
||||
//Show stopwatch after user setting timer.
|
||||
bool _showStopWatch = false;
|
||||
double _switchValue = 0;
|
||||
bool _autoPlay = true;
|
||||
|
@ -143,6 +141,7 @@ class AudioPlayerNotifier extends ChangeNotifier {
|
|||
bool get playerRunning => _playerRunning;
|
||||
int get lastPositin => _lastPostion;
|
||||
Playlist get queue => _queue;
|
||||
bool get queueUpdate => _queueUpdate;
|
||||
EpisodeBrief get episode => _episode;
|
||||
bool get stopOnComplete => _stopOnComplete;
|
||||
bool get showStopWatch => _showStopWatch;
|
||||
|
@ -155,7 +154,7 @@ class AudioPlayerNotifier extends ChangeNotifier {
|
|||
notifyListeners();
|
||||
}
|
||||
|
||||
set autoPlaySwitch(bool boo) {
|
||||
set autoPlaySwitch(bool boo) {
|
||||
_autoPlay = boo;
|
||||
notifyListeners();
|
||||
}
|
||||
|
@ -163,38 +162,50 @@ class AudioPlayerNotifier extends ChangeNotifier {
|
|||
@override
|
||||
void addListener(VoidCallback listener) async {
|
||||
super.addListener(listener);
|
||||
_queueUpdate = false;
|
||||
await AudioService.connect();
|
||||
if(await AudioService.running){
|
||||
AudioService.stop();
|
||||
bool running = await AudioService.running;
|
||||
if (running) {
|
||||
await AudioService.pause();
|
||||
}
|
||||
}
|
||||
|
||||
loadPlaylist() async {
|
||||
await _queue.getPlaylist();
|
||||
_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 {
|
||||
final EpisodeBrief episodeNew =
|
||||
await dbHelper.getRssItemWithUrl(episode.enclosureUrl);
|
||||
if (_playerRunning) {
|
||||
PlayHistory history = PlayHistory(_episode.title, _episode.enclosureUrl,
|
||||
backgroundAudioPosition / 1000, seekSliderValue);
|
||||
await dbHelper.saveHistory(history);
|
||||
AudioService.addQueueItemAt(episode.toMediaItem(), 0);
|
||||
AudioService.addQueueItemAt(episodeNew.toMediaItem(), 0);
|
||||
_queue.playlist
|
||||
.removeWhere((item) => item.enclosureUrl == episode.enclosureUrl);
|
||||
_queue.playlist.insert(0, episode);
|
||||
_queue.playlist.insert(0, episodeNew);
|
||||
notifyListeners();
|
||||
await _queue.savePlaylist();
|
||||
} else {
|
||||
await _queue.getPlaylist();
|
||||
_queue.playlist
|
||||
.removeWhere((item) => item.enclosureUrl == episode.enclosureUrl);
|
||||
_queue.playlist.insert(0, episode);
|
||||
_queue.playlist.insert(0, episodeNew);
|
||||
_queue.savePlaylist();
|
||||
_backgroundAudioDuration = 0;
|
||||
_backgroundAudioPosition = 0;
|
||||
_seekSliderValue = 0;
|
||||
_episode = episode;
|
||||
_episode = episodeNew;
|
||||
_playerRunning = true;
|
||||
notifyListeners();
|
||||
await _queue.savePlaylist();
|
||||
|
@ -207,13 +218,13 @@ class AudioPlayerNotifier extends ChangeNotifier {
|
|||
await AudioService.connect();
|
||||
}
|
||||
await AudioService.start(
|
||||
backgroundTaskEntrypoint: _audioPlayerTaskEntrypoint,
|
||||
androidNotificationChannelName: 'Tsacdop',
|
||||
notificationColor: 0xFF2196f3,
|
||||
androidNotificationIcon: 'drawable/ic_notification',
|
||||
enableQueue: true,
|
||||
androidStopOnRemoveTask: true,
|
||||
);
|
||||
backgroundTaskEntrypoint: _audioPlayerTaskEntrypoint,
|
||||
androidNotificationChannelName: 'Tsacdop',
|
||||
notificationColor: 0xFF4d91be,
|
||||
androidNotificationIcon: 'drawable/ic_notification',
|
||||
enableQueue: true,
|
||||
androidStopOnRemoveTask: true,
|
||||
androidStopForegroundOnPause: true);
|
||||
_playerRunning = true;
|
||||
if (_autoPlay) {
|
||||
await Future.forEach(_queue.playlist, (episode) async {
|
||||
|
@ -242,45 +253,58 @@ class AudioPlayerNotifier extends ChangeNotifier {
|
|||
print(_episode.title);
|
||||
_queue.delFromPlaylist(_episode);
|
||||
}
|
||||
if (_audioState == BasicPlaybackState.paused ||
|
||||
_audioState == BasicPlaybackState.skippingToNext &&
|
||||
_episode != null) {
|
||||
if (_audioState == BasicPlaybackState.skippingToNext &&
|
||||
_episode != null &&
|
||||
_backgroundAudioPosition > 0) {
|
||||
PlayHistory history = PlayHistory(_episode.title, _episode.enclosureUrl,
|
||||
backgroundAudioPosition / 1000, seekSliderValue);
|
||||
_backgroundAudioPosition / 1000, _seekSliderValue);
|
||||
await dbHelper.saveHistory(history);
|
||||
}
|
||||
if (_audioState == BasicPlaybackState.stopped) {
|
||||
_playerRunning = false;
|
||||
if (_audioState == BasicPlaybackState.stopped) _playerRunning = false;
|
||||
|
||||
if (_audioState == BasicPlaybackState.error) {
|
||||
_remoteErrorMessage = 'Network Error';
|
||||
}
|
||||
if (_audioState != BasicPlaybackState.error &&
|
||||
_audioState != BasicPlaybackState.paused) {
|
||||
_remoteErrorMessage = null;
|
||||
}
|
||||
|
||||
_currentPosition = event?.currentPosition ?? 0;
|
||||
notifyListeners();
|
||||
});
|
||||
|
||||
Timer.periodic(Duration(milliseconds: 500), (timer) {
|
||||
if (_noSlide) {
|
||||
_audioState == BasicPlaybackState.playing
|
||||
? (_backgroundAudioPosition < _backgroundAudioDuration - 500)
|
||||
? _backgroundAudioPosition = _currentPosition +
|
||||
DateTime.now().difference(_current).inMilliseconds
|
||||
: _backgroundAudioPosition = _backgroundAudioDuration
|
||||
: _backgroundAudioPosition = _currentPosition;
|
||||
if (_audioState == BasicPlaybackState.playing) {
|
||||
if (_backgroundAudioPosition < _backgroundAudioDuration - 500)
|
||||
_backgroundAudioPosition = _currentPosition +
|
||||
DateTime.now().difference(_current).inMilliseconds;
|
||||
else
|
||||
_backgroundAudioPosition = _backgroundAudioDuration;
|
||||
} else
|
||||
_backgroundAudioPosition = _currentPosition;
|
||||
|
||||
if (_backgroundAudioDuration != null &&
|
||||
_backgroundAudioDuration != 0 &&
|
||||
_backgroundAudioPosition != null) {
|
||||
_seekSliderValue =
|
||||
_backgroundAudioPosition / _backgroundAudioDuration ?? 0;
|
||||
}
|
||||
} else
|
||||
_seekSliderValue = 0;
|
||||
|
||||
if (_backgroundAudioPosition > 0) {
|
||||
_lastPostion = _backgroundAudioPosition;
|
||||
storage.saveInt(_lastPostion);
|
||||
}
|
||||
|
||||
if ((_queue.playlist.length == 1 || !_autoPlay) &&
|
||||
_seekSliderValue == 1 &&
|
||||
_seekSliderValue > 0.9 &&
|
||||
_episode != null) {
|
||||
_queue.delFromPlaylist(_episode);
|
||||
_lastPostion = 0;
|
||||
storage.saveInt(_lastPostion);
|
||||
PlayHistory history = PlayHistory(
|
||||
final PlayHistory history = PlayHistory(
|
||||
_episode.title,
|
||||
_episode.enclosureUrl,
|
||||
backgroundAudioPosition / 1000,
|
||||
|
@ -325,6 +349,7 @@ class AudioPlayerNotifier extends ChangeNotifier {
|
|||
}
|
||||
print('add to playlist when not rnnning');
|
||||
await _queue.addToPlayListAt(episode, index);
|
||||
_queueUpdate = !_queueUpdate;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
|
@ -332,17 +357,20 @@ class AudioPlayerNotifier extends ChangeNotifier {
|
|||
int index = _queue.playlist
|
||||
.indexWhere((item) => item.enclosureUrl == episode.enclosureUrl);
|
||||
if (index > 0) {
|
||||
EpisodeBrief episodeNew =
|
||||
await dbHelper.getRssItemWithUrl(episode.enclosureUrl);
|
||||
await delFromPlaylist(episode);
|
||||
await addToPlaylistAt(episode, index);
|
||||
await addToPlaylistAt(episodeNew, index);
|
||||
}
|
||||
}
|
||||
|
||||
delFromPlaylist(EpisodeBrief episode) async {
|
||||
Future<int> delFromPlaylist(EpisodeBrief episode) async {
|
||||
if (_playerRunning) {
|
||||
await AudioService.removeQueueItem(episode.toMediaItem());
|
||||
}
|
||||
await _queue.delFromPlaylist(episode);
|
||||
int index = await _queue.delFromPlaylist(episode);
|
||||
notifyListeners();
|
||||
return index;
|
||||
}
|
||||
|
||||
moveToTop(EpisodeBrief episode) async {
|
||||
|
@ -354,6 +382,7 @@ class AudioPlayerNotifier extends ChangeNotifier {
|
|||
_lastPostion = 0;
|
||||
storage.saveInt(_lastPostion);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
pauseAduio() async {
|
||||
|
@ -361,22 +390,32 @@ class AudioPlayerNotifier extends ChangeNotifier {
|
|||
}
|
||||
|
||||
resumeAudio() async {
|
||||
AudioService.play();
|
||||
if (_audioState != BasicPlaybackState.connecting &&
|
||||
_audioState != BasicPlaybackState.none) AudioService.play();
|
||||
}
|
||||
|
||||
forwardAudio(int s) {
|
||||
int pos = _backgroundAudioPosition + s * 1000;
|
||||
AudioService.seekTo(pos);
|
||||
}
|
||||
|
||||
seekTo(int position) async{
|
||||
if (_audioState != BasicPlaybackState.connecting &&
|
||||
_audioState != BasicPlaybackState.none)
|
||||
await AudioService.seekTo(position);
|
||||
}
|
||||
|
||||
sliderSeek(double val) async {
|
||||
print(val.toString());
|
||||
_noSlide = false;
|
||||
_seekSliderValue = val;
|
||||
notifyListeners();
|
||||
_currentPosition = (val * _backgroundAudioDuration).toInt();
|
||||
await AudioService.seekTo(_currentPosition);
|
||||
_noSlide = true;
|
||||
if (_audioState != BasicPlaybackState.connecting &&
|
||||
_audioState != BasicPlaybackState.none) {
|
||||
_noSlide = false;
|
||||
_seekSliderValue = val;
|
||||
notifyListeners();
|
||||
_currentPosition = (val * _backgroundAudioDuration).toInt();
|
||||
await AudioService.seekTo(_currentPosition);
|
||||
_noSlide = true;
|
||||
}
|
||||
}
|
||||
|
||||
//Set sleep time
|
||||
|
@ -459,6 +498,10 @@ class AudioPlayerTask extends BackgroundAudioTask {
|
|||
_handlePlaybackCompleted();
|
||||
});
|
||||
var eventSubscription = _audioPlayer.playbackEventStream.listen((event) {
|
||||
print('buffer position' + event.bufferedPosition.toString());
|
||||
if (event.playbackError != null) {
|
||||
_setState(state: BasicPlaybackState.error);
|
||||
}
|
||||
BasicPlaybackState state;
|
||||
if (event.buffering) {
|
||||
state = BasicPlaybackState.buffering;
|
||||
|
@ -514,7 +557,7 @@ class AudioPlayerTask extends BackgroundAudioTask {
|
|||
_skipState = BasicPlaybackState.skippingToNext;
|
||||
await _audioPlayer.setUrl(mediaItem.id);
|
||||
print(mediaItem.id);
|
||||
Duration duration = await _audioPlayer.durationFuture ?? 0;
|
||||
Duration duration = await _audioPlayer.durationFuture ?? Duration.zero;
|
||||
AudioServiceBackground.setMediaItem(
|
||||
mediaItem.copyWith(duration: duration.inMilliseconds));
|
||||
_skipState = null;
|
||||
|
@ -590,7 +633,7 @@ class AudioPlayerTask extends BackgroundAudioTask {
|
|||
AudioServiceBackground.setQueue(_queue);
|
||||
AudioServiceBackground.setMediaItem(mediaItem);
|
||||
await _audioPlayer.setUrl(mediaItem.id);
|
||||
Duration duration = await _audioPlayer.durationFuture;
|
||||
Duration duration = await _audioPlayer.durationFuture ?? Duration.zero;
|
||||
AudioServiceBackground.setMediaItem(
|
||||
mediaItem.copyWith(duration: duration.inMilliseconds));
|
||||
onPlay();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -15,6 +15,7 @@ class EpisodeBrief {
|
|||
final int explicit;
|
||||
final String imagePath;
|
||||
final String mediaId;
|
||||
final int isNew;
|
||||
EpisodeBrief(
|
||||
this.title,
|
||||
this.enclosureUrl,
|
||||
|
@ -27,7 +28,8 @@ class EpisodeBrief {
|
|||
this.duration,
|
||||
this.explicit,
|
||||
this.imagePath,
|
||||
this.mediaId);
|
||||
this.mediaId,
|
||||
this.isNew);
|
||||
|
||||
String dateToString() {
|
||||
DateTime date = DateTime.fromMillisecondsSinceEpoch(pubDate, isUtc: true);
|
||||
|
|
|
@ -21,7 +21,6 @@ class GroupEntity {
|
|||
|
||||
static GroupEntity fromJson(Map<String, Object> json) {
|
||||
List<String> list = List.from(json['podcastList']);
|
||||
print(json['[podcastList']);
|
||||
return GroupEntity(json['name'] as String, json['id'] as String,
|
||||
json['color'] as String, list);
|
||||
}
|
||||
|
@ -58,6 +57,7 @@ class PodcastGroup {
|
|||
List<PodcastLocal> _orderedPodcasts;
|
||||
List<PodcastLocal> get ordereddPodcasts => _orderedPodcasts;
|
||||
List<PodcastLocal> get podcasts => _podcasts;
|
||||
|
||||
set setOrderedPodcasts(List<PodcastLocal> list) {
|
||||
_orderedPodcasts = list;
|
||||
}
|
||||
|
@ -88,18 +88,29 @@ class GroupList extends ChangeNotifier {
|
|||
bool _isLoading = false;
|
||||
bool get isLoading => _isLoading;
|
||||
|
||||
List<String> _orderChanged = [];
|
||||
List<String> get orderChanged => _orderChanged;
|
||||
void addToOrderChanged(String name) {
|
||||
_orderChanged.add(name);
|
||||
List<PodcastGroup> _orderChanged = [];
|
||||
List<PodcastGroup> get orderChanged => _orderChanged;
|
||||
|
||||
void addToOrderChanged(PodcastGroup group) {
|
||||
_orderChanged.add(group);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void drlFromOrderChanged(String name) {
|
||||
_orderChanged.remove(name);
|
||||
_orderChanged.removeWhere((group) => group.name == name);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
clearOrderChanged() async {
|
||||
if (_orderChanged.length > 0) {
|
||||
await Future.forEach(_orderChanged, (PodcastGroup group) async {
|
||||
await group.getPodcasts();
|
||||
});
|
||||
_orderChanged.clear();
|
||||
// notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void addListener(VoidCallback listener) {
|
||||
super.addListener(listener);
|
||||
|
@ -162,6 +173,18 @@ class GroupList extends ChangeNotifier {
|
|||
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> result = [];
|
||||
_groups.forEach((group) {
|
||||
|
|
|
@ -11,8 +11,8 @@ class PodcastLocal {
|
|||
final String link;
|
||||
|
||||
final String description;
|
||||
final int upateCount;
|
||||
final int episodeCount;
|
||||
int upateCount;
|
||||
int episodeCount;
|
||||
PodcastLocal(
|
||||
this.title,
|
||||
this.imageUrl,
|
||||
|
|
|
@ -10,24 +10,23 @@ void callbackDispatcher() {
|
|||
Workmanager.executeTask((task, inputData) async {
|
||||
var dbHelper = DBHelper();
|
||||
List<PodcastLocal> podcastList = await dbHelper.getPodcastLocalAll();
|
||||
int i = 0;
|
||||
await Future.forEach(podcastList, (podcastLocal) async {
|
||||
i += await dbHelper.updatePodcastRss(podcastLocal);
|
||||
await dbHelper.updatePodcastRss(podcastLocal);
|
||||
print('Refresh ' + podcastLocal.title);
|
||||
});
|
||||
KeyValueStorage refreshstorage = KeyValueStorage('refreshdate');
|
||||
await refreshstorage.saveInt(DateTime.now().millisecondsSinceEpoch);
|
||||
KeyValueStorage refreshcountstorage = KeyValueStorage('refreshcount');
|
||||
await refreshcountstorage.saveInt(i);
|
||||
return Future.value(true);
|
||||
});
|
||||
}
|
||||
|
||||
class SettingState extends ChangeNotifier {
|
||||
KeyValueStorage themestorage = KeyValueStorage('themes');
|
||||
KeyValueStorage accentstorage = KeyValueStorage('accents');
|
||||
KeyValueStorage autoupdatestorage = KeyValueStorage('autoupdate');
|
||||
KeyValueStorage intervalstorage = KeyValueStorage('updateInterval');
|
||||
KeyValueStorage themeStorage = KeyValueStorage('themes');
|
||||
KeyValueStorage accentStorage = KeyValueStorage('accents');
|
||||
KeyValueStorage autoupdateStorage = KeyValueStorage('autoupdate');
|
||||
KeyValueStorage intervalStorage = KeyValueStorage('updateInterval');
|
||||
KeyValueStorage downloadUsingDataStorage =
|
||||
KeyValueStorage('downloadUsingData');
|
||||
|
||||
Future initData() async {
|
||||
await _getTheme();
|
||||
|
@ -49,7 +48,7 @@ class SettingState extends ChangeNotifier {
|
|||
_saveUpdateInterval();
|
||||
Workmanager.initialize(
|
||||
callbackDispatcher,
|
||||
isInDebugMode: true,
|
||||
isInDebugMode: false,
|
||||
);
|
||||
Workmanager.registerPeriodicTask("1", "update_podcasts",
|
||||
frequency: Duration(hours: hour),
|
||||
|
@ -60,8 +59,8 @@ class SettingState extends ChangeNotifier {
|
|||
print('work manager init done + ');
|
||||
}
|
||||
|
||||
void cancelWork() {
|
||||
Workmanager.cancelAll();
|
||||
Future cancelWork() async{
|
||||
await Workmanager.cancelAll();
|
||||
print('work job cancelled');
|
||||
}
|
||||
|
||||
|
@ -86,58 +85,74 @@ class SettingState extends ChangeNotifier {
|
|||
notifyListeners();
|
||||
}
|
||||
|
||||
bool _downloadUsingData;
|
||||
bool get downloadUsingData => _downloadUsingData;
|
||||
set downloadUsingData(bool boo) {
|
||||
_downloadUsingData = boo;
|
||||
_saveDownloadUsingData();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
void addListener(VoidCallback listener) {
|
||||
super.addListener(listener);
|
||||
_getTheme();
|
||||
_getAccentSetColor();
|
||||
_getAutoUpdate();
|
||||
_getDownloadUsingData();
|
||||
_getUpdateInterval().then((value) {
|
||||
if (_initUpdateTag == 0) setWorkManager(24);
|
||||
});
|
||||
}
|
||||
|
||||
Future _getTheme() async {
|
||||
int mode = await themestorage.getInt();
|
||||
int mode = await themeStorage.getInt();
|
||||
_theme = ThemeMode.values[mode];
|
||||
}
|
||||
|
||||
Future _saveTheme() async {
|
||||
await themestorage.saveInt(_theme.index);
|
||||
await themeStorage.saveInt(_theme.index);
|
||||
}
|
||||
|
||||
Future _getAccentSetColor() async {
|
||||
String colorString = await accentstorage.getString();
|
||||
print(colorString);
|
||||
String colorString = await accentStorage.getString();
|
||||
if (colorString.isNotEmpty) {
|
||||
int color = int.parse('FF' + colorString.toUpperCase(), radix: 16);
|
||||
_accentSetColor = Color(color).withOpacity(1.0);
|
||||
print(_accentSetColor.toString());
|
||||
} else {
|
||||
_accentSetColor = Colors.blue[400];
|
||||
}
|
||||
}
|
||||
|
||||
Future _saveAccentSetColor() async {
|
||||
await accentstorage
|
||||
await accentStorage
|
||||
.saveString(_accentSetColor.toString().substring(10, 16));
|
||||
}
|
||||
|
||||
Future _getAutoUpdate() async {
|
||||
int i = await autoupdatestorage.getInt();
|
||||
int i = await autoupdateStorage.getInt();
|
||||
_autoUpdate = i == 0 ? true : false;
|
||||
}
|
||||
|
||||
Future _saveAutoUpdate() async {
|
||||
await autoupdatestorage.saveInt(_autoUpdate ? 0 : 1);
|
||||
await autoupdateStorage.saveInt(_autoUpdate ? 0 : 1);
|
||||
}
|
||||
|
||||
Future _getUpdateInterval() async {
|
||||
_initUpdateTag = await intervalstorage.getInt();
|
||||
_updateInterval = _initUpdateTag;
|
||||
_initUpdateTag = await intervalStorage.getInt();
|
||||
_updateInterval = _initUpdateTag;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter_html/flutter_html.dart';
|
||||
import 'package:tsacdop/class/download_state.dart';
|
||||
import 'package:tsacdop/home/audioplayer.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
|
@ -382,7 +383,7 @@ class _MenuBarState extends State<MenuBar> {
|
|||
() => setUnliked(widget.episodeItem.enclosureUrl)),
|
||||
],
|
||||
),
|
||||
DownloadButton(episodeBrief: widget.episodeItem),
|
||||
DownloadButton(episode: widget.episodeItem),
|
||||
Selector<AudioPlayerNotifier, List<String>>(
|
||||
selector: (_, audio) =>
|
||||
audio.queue.playlist.map((e) => e.enclosureUrl).toList(),
|
||||
|
|
|
@ -1,193 +1,95 @@
|
|||
import 'dart:isolate';
|
||||
import 'dart:ui';
|
||||
import 'dart:io';
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.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:permission_handler/permission_handler.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/audiostate.dart';
|
||||
import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
|
||||
import 'package:tsacdop/class/settingstate.dart';
|
||||
|
||||
class DownloadButton extends StatefulWidget {
|
||||
final EpisodeBrief episodeBrief;
|
||||
DownloadButton({this.episodeBrief, Key key}) : super(key: key);
|
||||
final EpisodeBrief episode;
|
||||
DownloadButton({this.episode, Key key}) : super(key: key);
|
||||
@override
|
||||
_DownloadButtonState createState() => _DownloadButtonState();
|
||||
}
|
||||
|
||||
class _DownloadButtonState extends State<DownloadButton> {
|
||||
_TaskInfo _task;
|
||||
bool _isLoading;
|
||||
bool _permissionReady;
|
||||
String _localPath;
|
||||
ReceivePort _port = ReceivePort();
|
||||
|
||||
Future<String> _getPath() async {
|
||||
final dir = await getExternalStorageDirectory();
|
||||
return dir.path;
|
||||
}
|
||||
bool _usingData;
|
||||
StreamSubscription _connectivity;
|
||||
EpisodeTask _task;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_bindBackgroundIsolate();
|
||||
|
||||
FlutterDownloader.registerCallback(downloadCallback);
|
||||
|
||||
_isLoading = true;
|
||||
_permissionReady = false;
|
||||
|
||||
_prepare();
|
||||
_connectivity = Connectivity().onConnectivityChanged.listen((result) {
|
||||
_usingData = result == ConnectivityResult.mobile;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_unbindBackgroundIsolate();
|
||||
_connectivity.cancel();
|
||||
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) {
|
||||
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 {
|
||||
void _requestDownload(EpisodeBrief episode, bool downloadUsingData) async {
|
||||
_permissionReady = await _checkPermmison();
|
||||
if (_permissionReady)
|
||||
task.taskId = await FlutterDownloader.enqueue(
|
||||
url: task.link,
|
||||
savedDir: _localPath,
|
||||
showNotification: true,
|
||||
openFileFromNotification: false,
|
||||
);
|
||||
var dbHelper = DBHelper();
|
||||
|
||||
await dbHelper.saveDownloaded(task.link, task.taskId);
|
||||
Fluttertoast.showToast(
|
||||
msg: 'Downloading',
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
);
|
||||
bool _dataConfirm = true;
|
||||
if (_permissionReady) {
|
||||
if (downloadUsingData && _usingData) {
|
||||
_dataConfirm = await _useDataConfirem();
|
||||
}
|
||||
if (_dataConfirm) {
|
||||
Provider.of<DownloadState>(context, listen: false).startTask(episode);
|
||||
Fluttertoast.showToast(
|
||||
msg: 'Downloading',
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _deleteDownload(_TaskInfo task) async {
|
||||
await FlutterDownloader.remove(
|
||||
taskId: task.taskId, shouldDeleteContent: true);
|
||||
var dbHelper = DBHelper();
|
||||
await dbHelper.delDownloaded(task.link);
|
||||
await _prepare();
|
||||
setState(() {});
|
||||
void _deleteDownload(EpisodeBrief episode) async {
|
||||
Provider.of<DownloadState>(context, listen: false).delTask(episode);
|
||||
Fluttertoast.showToast(
|
||||
msg: 'Download removed',
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
);
|
||||
}
|
||||
|
||||
void _pauseDownload(_TaskInfo task) async {
|
||||
await FlutterDownloader.pause(taskId: task.taskId);
|
||||
void _pauseDownload(EpisodeBrief episode) async {
|
||||
Provider.of<DownloadState>(context, listen: false).pauseTask(episode);
|
||||
Fluttertoast.showToast(
|
||||
msg: 'Download paused',
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
);
|
||||
}
|
||||
|
||||
void _resumeDownload(_TaskInfo task) async {
|
||||
String newTaskId = await FlutterDownloader.resume(taskId: task.taskId);
|
||||
task.taskId = newTaskId;
|
||||
var dbHelper = DBHelper();
|
||||
await dbHelper.saveDownloaded(task.taskId, task.link);
|
||||
void _resumeDownload(EpisodeBrief episode) async {
|
||||
Provider.of<DownloadState>(context, listen: false).resumeTask(episode);
|
||||
Fluttertoast.showToast(
|
||||
msg: 'Download resumed',
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
);
|
||||
}
|
||||
|
||||
void _retryDownload(_TaskInfo task) async {
|
||||
String newTaskId = await FlutterDownloader.retry(taskId: task.taskId);
|
||||
task.taskId = newTaskId;
|
||||
var dbHelper = DBHelper();
|
||||
await dbHelper.saveDownloaded(task.taskId, task.link);
|
||||
void _retryDownload(EpisodeBrief episode) async {
|
||||
Provider.of<DownloadState>(context, listen: false).retryTask(episode);
|
||||
Fluttertoast.showToast(
|
||||
msg: 'Download again',
|
||||
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 {
|
||||
PermissionStatus permission = await PermissionHandler()
|
||||
.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(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
|
@ -218,112 +174,111 @@ class _DownloadButtonState extends State<DownloadButton> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _isLoading
|
||||
? Center()
|
||||
: Row(
|
||||
children: <Widget>[
|
||||
_downloadButton(_task),
|
||||
AnimatedContainer(
|
||||
duration: Duration(seconds: 1),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).accentColor,
|
||||
borderRadius: BorderRadius.all(Radius.circular(15.0))),
|
||||
height: 20.0,
|
||||
width:
|
||||
(_task.status == DownloadTaskStatus.running) ? 50.0 : 0,
|
||||
alignment: Alignment.center,
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Text('${_task.progress}%',
|
||||
style: TextStyle(color: Colors.white)),
|
||||
)),
|
||||
],
|
||||
);
|
||||
return Consumer<DownloadState>(builder: (_, downloader, __) {
|
||||
EpisodeTask _task = Provider.of<DownloadState>(context, listen: false)
|
||||
.episodeToTask(widget.episode);
|
||||
return Row(
|
||||
children: <Widget>[
|
||||
_downloadButton(_task, context),
|
||||
AnimatedContainer(
|
||||
duration: Duration(seconds: 1),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).accentColor,
|
||||
borderRadius: BorderRadius.all(Radius.circular(15.0))),
|
||||
height: 20.0,
|
||||
width: (_task.status == DownloadTaskStatus.running) ? 50.0 : 0,
|
||||
alignment: Alignment.center,
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Text('${_task.progress}%',
|
||||
style: TextStyle(color: Colors.white)),
|
||||
)),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _downloadButton(_TaskInfo task) {
|
||||
if (task.status == DownloadTaskStatus.undefined) {
|
||||
return _buttonOnMenu(
|
||||
Icon(
|
||||
Icons.arrow_downward,
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
() => _requestDownload(task));
|
||||
} else if (task.status == DownloadTaskStatus.running) {
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
if (task.progress > 0) _pauseDownload(task);
|
||||
},
|
||||
child: Container(
|
||||
height: 50.0,
|
||||
alignment: Alignment.center,
|
||||
padding: EdgeInsets.symmetric(horizontal: 18.0),
|
||||
child: SizedBox(
|
||||
height: 18,
|
||||
width: 18,
|
||||
child: CircularProgressIndicator(
|
||||
backgroundColor: Colors.grey[500],
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
Theme.of(context).accentColor),
|
||||
value: task.progress / 100,
|
||||
Widget _downloadButton(EpisodeTask task, BuildContext context) {
|
||||
switch (task.status.value) {
|
||||
case 0:
|
||||
return Selector<SettingState, bool>(
|
||||
selector: (_, settings) => settings.downloadUsingData,
|
||||
builder: (_, data, __) => _buttonOnMenu(
|
||||
Icon(
|
||||
Icons.arrow_downward,
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
() => _requestDownload(task.episode, data)),
|
||||
);
|
||||
break;
|
||||
case 2:
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
if (task.progress > 0) _pauseDownload(task.episode);
|
||||
},
|
||||
child: Container(
|
||||
height: 50.0,
|
||||
alignment: Alignment.center,
|
||||
padding: EdgeInsets.symmetric(horizontal: 18.0),
|
||||
child: SizedBox(
|
||||
height: 18,
|
||||
width: 18,
|
||||
child: CircularProgressIndicator(
|
||||
backgroundColor: Colors.grey[500],
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
Theme.of(context).accentColor),
|
||||
value: task.progress / 100,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else if (task.status == DownloadTaskStatus.paused) {
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
_resumeDownload(task);
|
||||
},
|
||||
child: Container(
|
||||
height: 50.0,
|
||||
alignment: Alignment.center,
|
||||
padding: EdgeInsets.symmetric(horizontal: 18),
|
||||
child: SizedBox(
|
||||
height: 18,
|
||||
width: 18,
|
||||
child: CircularProgressIndicator(
|
||||
backgroundColor: Colors.grey[500],
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.red),
|
||||
value: task.progress / 100,
|
||||
);
|
||||
break;
|
||||
case 6:
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
_resumeDownload(task.episode);
|
||||
},
|
||||
child: Container(
|
||||
height: 50.0,
|
||||
alignment: Alignment.center,
|
||||
padding: EdgeInsets.symmetric(horizontal: 18),
|
||||
child: SizedBox(
|
||||
height: 18,
|
||||
width: 18,
|
||||
child: CircularProgressIndicator(
|
||||
backgroundColor: Colors.grey[500],
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.red),
|
||||
value: task.progress / 100,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else if (task.status == DownloadTaskStatus.complete) {
|
||||
if(!widget.episodeBrief.mediaId.contains('file://'))
|
||||
{_saveMediaId(task).then((episode) =>
|
||||
Provider.of<AudioPlayerNotifier>(context, listen: false)
|
||||
.updateMediaItem(episode));}
|
||||
return _buttonOnMenu(
|
||||
Icon(
|
||||
Icons.done_all,
|
||||
color: Theme.of(context).accentColor,
|
||||
),
|
||||
() => _deleteDownload(task));
|
||||
} else if (task.status == DownloadTaskStatus.failed) {
|
||||
return _buttonOnMenu(
|
||||
Icon(Icons.refresh, color: Colors.red), () => _retryDownload(task));
|
||||
);
|
||||
break;
|
||||
case 3:
|
||||
Provider.of<AudioPlayerNotifier>(context, listen: false)
|
||||
.updateMediaItem(task.episode);
|
||||
return _buttonOnMenu(
|
||||
Icon(
|
||||
Icons.done_all,
|
||||
color: Theme.of(context).accentColor,
|
||||
), () {
|
||||
_deleteDownload(task.episode);
|
||||
});
|
||||
break;
|
||||
case 4:
|
||||
return _buttonOnMenu(Icon(Icons.refresh, color: Colors.red),
|
||||
() => _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});
|
||||
}
|
||||
|
|
|
@ -80,7 +80,7 @@ class AboutApp extends StatelessWidget {
|
|||
padding: EdgeInsets.symmetric(horizontal: 50),
|
||||
height: 50,
|
||||
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,
|
||||
),
|
||||
),
|
||||
|
@ -110,12 +110,12 @@ class AboutApp extends StatelessWidget {
|
|||
_listItem(context, 'GitHub', LineIcons.github,
|
||||
'https://github.com/stonaga/'),
|
||||
_listItem(context, 'Twitter', LineIcons.twitter,
|
||||
'https://twitter.com'),
|
||||
'https://twitter.com/shimenmen'),
|
||||
_listItem(
|
||||
context,
|
||||
'Stone Gate',
|
||||
LineIcons.hat_cowboy_solid,
|
||||
'mailto:<xijieyin@gmail.com>?subject=Tsacdop Feedback'),
|
||||
LineIcons.medium,
|
||||
'https://medium.com/@stonegate'),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
@ -91,7 +91,6 @@ class _MyHomePageState extends State<MyHomePage> {
|
|||
class _MyHomePageDelegate extends SearchDelegate<int> {
|
||||
static Future<List> getList(String searchText) async {
|
||||
String apiKey = environment['apiKey'];
|
||||
print(apiKey);
|
||||
String url =
|
||||
"https://listennotes.p.mashape.com/api/v1/search?only_in=title%2Cdescription&q=" +
|
||||
searchText +
|
||||
|
@ -312,7 +311,7 @@ class _SearchResultState extends State<SearchResult> {
|
|||
importOmpl.importState = ImportState.parse;
|
||||
|
||||
await dbHelper.savePodcastRss(_p, _uuid);
|
||||
|
||||
groupList.updatePodcast(podcastLocal);
|
||||
importOmpl.importState = ImportState.complete;
|
||||
} else {
|
||||
importOmpl.importState = ImportState.error;
|
||||
|
|
|
@ -173,7 +173,7 @@ class _PopupMenuState extends State<PopupMenu> {
|
|||
importOmpl.importState = ImportState.parse;
|
||||
|
||||
await dbHelper.savePodcastRss(_p, _uuid);
|
||||
|
||||
groupList.updatePodcast(podcastLocal);
|
||||
importOmpl.importState = ImportState.complete;
|
||||
} else {
|
||||
importOmpl.importState = ImportState.error;
|
||||
|
|
|
@ -5,8 +5,11 @@ import 'dart:math' as math;
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.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:audio_service/audio_service.dart';
|
||||
import 'package:line_icons/line_icons.dart';
|
||||
|
||||
import 'package:tsacdop/class/episodebrief.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/colorize.dart';
|
||||
import 'package:tsacdop/util/day_night_switch.dart';
|
||||
import 'package:tsacdop/util/context_extension.dart';
|
||||
|
||||
class MyRectangularTrackShape extends RectangularSliderTrackShape {
|
||||
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];
|
||||
int _minSelected;
|
||||
|
||||
final GlobalKey<AnimatedListState> _miniPlaylistKey = GlobalKey();
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_minSelected = 30;
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(Widget oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
Widget _sleppMode(BuildContext context) {
|
||||
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
|
||||
return Selector<AudioPlayerNotifier, Tuple3<bool, int, double>>(
|
||||
|
@ -571,9 +570,14 @@ class _PlayerWidgetState extends State<PlayerWidget> {
|
|||
Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 5.0),
|
||||
width: 200,
|
||||
child: Text(data.item1.feedTitle, maxLines: 1, overflow: TextOverflow.fade,),
|
||||
child: Text(
|
||||
data.item1.feedTitle,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.fade,
|
||||
),
|
||||
),
|
||||
Spacer(),
|
||||
LastPosition(),
|
||||
IconButton(
|
||||
onPressed: () => Navigator.push(
|
||||
context,
|
||||
|
@ -596,138 +600,252 @@ class _PlayerWidgetState extends State<PlayerWidget> {
|
|||
Widget _playlist(BuildContext context) {
|
||||
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
|
||||
return Container(
|
||||
alignment: Alignment.bottomLeft,
|
||||
child: AnimatedContainer(
|
||||
duration: Duration(milliseconds: 400),
|
||||
height: 300,
|
||||
width: MediaQuery.of(context).size.width,
|
||||
alignment: Alignment.center,
|
||||
// margin: EdgeInsets.all(20),
|
||||
//padding: EdgeInsets.only(bottom: 10.0),
|
||||
decoration: BoxDecoration(
|
||||
// borderRadius: BorderRadius.all(Radius.circular(10.0)),
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
child: Selector<AudioPlayerNotifier, List<EpisodeBrief>>(
|
||||
selector: (_, audio) => audio.queue.playlist,
|
||||
builder: (_, playlist, __) {
|
||||
return ListView.builder(
|
||||
shrinkWrap: true,
|
||||
scrollDirection: Axis.vertical,
|
||||
itemCount: playlist.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
print(playlist.length);
|
||||
if (index == 0) {
|
||||
return Container(
|
||||
height: 60,
|
||||
padding: EdgeInsets.symmetric(horizontal: 20.0),
|
||||
alignment: Alignment.topLeft,
|
||||
height: 300,
|
||||
width: MediaQuery.of(context).size.width,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20.0),
|
||||
height: 60.0,
|
||||
// color: context.primaryColorDark,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20.0),
|
||||
height: 20.0,
|
||||
// color: context.primaryColorDark,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
'Playlist',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).accentColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16),
|
||||
),
|
||||
),
|
||||
Spacer(),
|
||||
Container(
|
||||
height: 60,
|
||||
alignment: Alignment.center,
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'Playlist',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).accentColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16),
|
||||
),
|
||||
Spacer(),
|
||||
Container(
|
||||
alignment: Alignment.center,
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
height: 40,
|
||||
width: 80,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).primaryColor,
|
||||
borderRadius:
|
||||
BorderRadius.all(Radius.circular(20)),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).brightness ==
|
||||
Brightness.dark
|
||||
? Colors.black12
|
||||
: Colors.white10,
|
||||
width: 1),
|
||||
boxShadow: Theme.of(context).brightness ==
|
||||
Brightness.dark
|
||||
? _customShadowNight
|
||||
: _customShadow),
|
||||
child: Material(
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
height: 30,
|
||||
width: 60,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).primaryColor,
|
||||
borderRadius: BorderRadius.all(Radius.circular(15)),
|
||||
border: Border.all(
|
||||
color:
|
||||
Theme.of(context).brightness == Brightness.dark
|
||||
? Colors.black12
|
||||
: Colors.white10,
|
||||
width: 1),
|
||||
boxShadow:
|
||||
Theme.of(context).brightness == Brightness.dark
|
||||
? _customShadowNight
|
||||
: _customShadow),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.all(Radius.circular(15)),
|
||||
onTap: () {
|
||||
audio.playNext();
|
||||
_miniPlaylistKey.currentState.removeItem(
|
||||
0, (context, animation) => Container());
|
||||
_miniPlaylistKey.currentState.removeItem(
|
||||
1, (context, animation) => Container());
|
||||
_miniPlaylistKey.currentState.insertItem(0);
|
||||
},
|
||||
child: SizedBox(
|
||||
height: 30,
|
||||
width: 60,
|
||||
child: Icon(
|
||||
Icons.skip_next,
|
||||
size: 30,
|
||||
),
|
||||
),
|
||||
),
|
||||
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>[
|
||||
Container(
|
||||
// child: Text('p'),
|
||||
height: 10.0,
|
||||
width: 10.0,
|
||||
height: 8.0,
|
||||
width: 8.0,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.transparent,
|
||||
|
@ -775,8 +893,8 @@ class _PlayerWidgetState extends State<PlayerWidget> {
|
|||
width: 2.0)),
|
||||
),
|
||||
Container(
|
||||
height: 10.0,
|
||||
width: 10.0,
|
||||
height: 8.0,
|
||||
width: 8.0,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.transparent,
|
||||
|
@ -785,8 +903,8 @@ class _PlayerWidgetState extends State<PlayerWidget> {
|
|||
width: 2.0)),
|
||||
),
|
||||
Container(
|
||||
height: 10.0,
|
||||
width: 10.0,
|
||||
height: 8.0,
|
||||
width: 8.0,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
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 {
|
||||
final String title;
|
||||
final String path;
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -347,7 +347,7 @@ class _PodcastPreviewState extends State<PodcastPreview> {
|
|||
}
|
||||
return (snapshot.hasData)
|
||||
? ShowEpisode(
|
||||
podcast: snapshot.data,
|
||||
episodes: snapshot.data,
|
||||
podcastLocal: widget.podcastLocal,
|
||||
)
|
||||
: Center(child: CircularProgressIndicator());
|
||||
|
@ -400,9 +400,9 @@ class _PodcastPreviewState extends State<PodcastPreview> {
|
|||
}
|
||||
|
||||
class ShowEpisode extends StatelessWidget {
|
||||
final List<EpisodeBrief> podcast;
|
||||
final List<EpisodeBrief> episodes;
|
||||
final PodcastLocal podcastLocal;
|
||||
ShowEpisode({Key key, this.podcast, this.podcastLocal}) : super(key: key);
|
||||
ShowEpisode({Key key, this.episodes, this.podcastLocal}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -513,16 +513,16 @@ class ShowEpisode extends StatelessWidget {
|
|||
details.globalPosition.dy),
|
||||
onLongPress: () => _showPopupMenu(
|
||||
offset,
|
||||
podcast[index],
|
||||
episodes[index],
|
||||
context,
|
||||
data.item1 == podcast[index],
|
||||
data.item2.contains(podcast[index].enclosureUrl)),
|
||||
data.item1 == episodes[index],
|
||||
data.item2.contains(episodes[index].enclosureUrl)),
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
ScaleRoute(
|
||||
page: EpisodeDetail(
|
||||
episodeItem: podcast[index],
|
||||
episodeItem: episodes[index],
|
||||
heroTag: 'scroll',
|
||||
//unique hero tag
|
||||
)),
|
||||
|
@ -548,7 +548,7 @@ class ShowEpisode extends StatelessWidget {
|
|||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Hero(
|
||||
tag: podcast[index].enclosureUrl +
|
||||
tag: episodes[index].enclosureUrl +
|
||||
'scroll',
|
||||
child: Container(
|
||||
height: _width / 18,
|
||||
|
@ -560,7 +560,7 @@ class ShowEpisode extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
Spacer(),
|
||||
index < podcastLocal.upateCount
|
||||
episodes[index].isNew == 1
|
||||
? Text(
|
||||
'New',
|
||||
style: TextStyle(
|
||||
|
@ -577,7 +577,7 @@ class ShowEpisode extends StatelessWidget {
|
|||
padding: EdgeInsets.only(top: 2.0),
|
||||
alignment: Alignment.topLeft,
|
||||
child: Text(
|
||||
podcast[index].title,
|
||||
episodes[index].title,
|
||||
style: TextStyle(
|
||||
fontSize: _width / 32,
|
||||
),
|
||||
|
@ -591,7 +591,7 @@ class ShowEpisode extends StatelessWidget {
|
|||
child: Container(
|
||||
alignment: Alignment.bottomLeft,
|
||||
child: Text(
|
||||
podcast[index].dateToString(),
|
||||
episodes[index].dateToString(),
|
||||
//podcast[index].pubDate.substring(4, 16),
|
||||
style: TextStyle(
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart' hide NestedScrollView;
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tsacdop/class/download_state.dart';
|
||||
import 'package:tsacdop/home/playlist.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/episodebrief.dart';
|
||||
import 'package:tsacdop/local_storage/key_value_storage.dart';
|
||||
import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
|
||||
import 'package:tsacdop/util/episodegrid.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/audioplayer.dart';
|
||||
import 'home_groups.dart';
|
||||
import 'download_list.dart';
|
||||
|
||||
class Home extends StatefulWidget {
|
||||
@override
|
||||
|
@ -58,6 +61,10 @@ class _HomeState extends State<Home> with SingleTickerProviderStateMixin {
|
|||
Import(),
|
||||
Expanded(
|
||||
child: NestedScrollView(
|
||||
innerScrollPositionKeyBuilder: () {
|
||||
return Key('tab' + _controller.index.toString());
|
||||
},
|
||||
pinnedHeaderSliverHeightBuilder: () => 50,
|
||||
headerSliverBuilder:
|
||||
(BuildContext context, bool innerBoxScrolled) {
|
||||
return <Widget>[
|
||||
|
@ -100,9 +107,12 @@ class _HomeState extends State<Home> with SingleTickerProviderStateMixin {
|
|||
body: TabBarView(
|
||||
controller: _controller,
|
||||
children: <Widget>[
|
||||
_RecentUpdate(),
|
||||
_MyFavorite(),
|
||||
_MyDownload(),
|
||||
NestedScrollViewInnerScrollPositionKeyWidget(
|
||||
Key('tab0'), _RecentUpdate()),
|
||||
NestedScrollViewInnerScrollPositionKeyWidget(
|
||||
Key('tab1'), _MyFavorite()),
|
||||
NestedScrollViewInnerScrollPositionKeyWidget(
|
||||
Key('tab2'), _MyDownload()),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -317,13 +327,11 @@ class _RecentUpdate extends StatefulWidget {
|
|||
_RecentUpdateState createState() => _RecentUpdateState();
|
||||
}
|
||||
|
||||
class _RecentUpdateState extends State<_RecentUpdate> {
|
||||
int _updateCount = 0;
|
||||
class _RecentUpdateState extends State<_RecentUpdate>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
Future<List<EpisodeBrief>> _getRssItem(int top) async {
|
||||
var dbHelper = DBHelper();
|
||||
List<EpisodeBrief> episodes = await dbHelper.getRecentRssItem(top);
|
||||
KeyValueStorage refreshcountstorage = KeyValueStorage('refreshcount');
|
||||
_updateCount = await refreshcountstorage.getInt();
|
||||
return episodes;
|
||||
}
|
||||
|
||||
|
@ -337,18 +345,18 @@ class _RecentUpdateState extends State<_RecentUpdate> {
|
|||
});
|
||||
}
|
||||
|
||||
int _top;
|
||||
int _top = 99;
|
||||
bool _loadMore;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadMore = false;
|
||||
_top = 33;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return FutureBuilder<List<EpisodeBrief>>(
|
||||
future: _getRssItem(_top),
|
||||
builder: (context, snapshot) {
|
||||
|
@ -367,7 +375,6 @@ class _RecentUpdateState extends State<_RecentUpdate> {
|
|||
slivers: <Widget>[
|
||||
EpisodeGrid(
|
||||
episodes: snapshot.data,
|
||||
updateCount: _updateCount,
|
||||
),
|
||||
SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
|
@ -386,6 +393,9 @@ class _RecentUpdateState extends State<_RecentUpdate> {
|
|||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
}
|
||||
|
||||
class _MyFavorite extends StatefulWidget {
|
||||
|
@ -393,7 +403,8 @@ class _MyFavorite extends StatefulWidget {
|
|||
_MyFavoriteState createState() => _MyFavoriteState();
|
||||
}
|
||||
|
||||
class _MyFavoriteState extends State<_MyFavorite> {
|
||||
class _MyFavoriteState extends State<_MyFavorite>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
Future<List<EpisodeBrief>> _getLikedRssItem(_top) async {
|
||||
var dbHelper = DBHelper();
|
||||
List<EpisodeBrief> episodes = await dbHelper.getLikedRssItem(_top);
|
||||
|
@ -410,18 +421,18 @@ class _MyFavoriteState extends State<_MyFavorite> {
|
|||
});
|
||||
}
|
||||
|
||||
int _top;
|
||||
int _top = 99;
|
||||
bool _loadMore;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadMore = false;
|
||||
_top = 33;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return FutureBuilder<List<EpisodeBrief>>(
|
||||
future: _getLikedRssItem(_top),
|
||||
builder: (context, snapshot) {
|
||||
|
@ -436,7 +447,6 @@ class _MyFavoriteState extends State<_MyFavorite> {
|
|||
},
|
||||
child: CustomScrollView(
|
||||
key: PageStorageKey<String>('favorite'),
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
slivers: <Widget>[
|
||||
EpisodeGrid(
|
||||
episodes: snapshot.data,
|
||||
|
@ -459,6 +469,9 @@ class _MyFavoriteState extends State<_MyFavorite> {
|
|||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
}
|
||||
|
||||
class _MyDownload extends StatefulWidget {
|
||||
|
@ -466,32 +479,45 @@ class _MyDownload extends StatefulWidget {
|
|||
_MyDownloadState createState() => _MyDownloadState();
|
||||
}
|
||||
|
||||
class _MyDownloadState extends State<_MyDownload> {
|
||||
Future<List<EpisodeBrief>> _getDownloadedRssItem() async {
|
||||
var dbHelper = DBHelper();
|
||||
List<EpisodeBrief> episodes = await dbHelper.getDownloadedRssItem();
|
||||
return episodes;
|
||||
class _MyDownloadState extends State<_MyDownload>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
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());
|
||||
},
|
||||
);
|
||||
}
|
||||
bool get wantKeepAlive => true;
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import 'package:line_icons/line_icons.dart';
|
|||
import 'package:tsacdop/class/audiostate.dart';
|
||||
import 'package:tsacdop/class/episodebrief.dart';
|
||||
import 'package:tsacdop/util/colorize.dart';
|
||||
import 'package:tsacdop/util/context_extension.dart';
|
||||
|
||||
class PlaylistPage extends StatefulWidget {
|
||||
@override
|
||||
|
@ -19,21 +20,8 @@ class PlaylistPage extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _PlaylistPageState extends State<PlaylistPage> {
|
||||
final GlobalKey<AnimatedListState> _playlistKey = GlobalKey();
|
||||
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 sum = 0;
|
||||
if (episodes.length == 0) {
|
||||
|
@ -87,13 +75,16 @@ class _PlaylistPageState extends State<PlaylistPage> {
|
|||
),
|
||||
body: SafeArea(
|
||||
child:
|
||||
Selector<AudioPlayerNotifier, Tuple2<List<EpisodeBrief>, bool>>(
|
||||
Selector<AudioPlayerNotifier, Tuple3<Playlist, bool, bool>>(
|
||||
selector: (_, audio) =>
|
||||
Tuple2(audio.queue.playlist, audio.playerRunning),
|
||||
Tuple3(audio.queue, audio.playerRunning, audio.queueUpdate),
|
||||
builder: (_, data, __) {
|
||||
print('update');
|
||||
final List<EpisodeBrief> episodes = data.item1.playlist;
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
height: _topHeight,
|
||||
|
@ -115,7 +106,7 @@ class _PlaylistPageState extends State<PlaylistPage> {
|
|||
),
|
||||
children: <TextSpan>[
|
||||
TextSpan(
|
||||
text: data.item1.length.toString(),
|
||||
text: episodes.length.toString(),
|
||||
style: GoogleFonts.cairo(
|
||||
textStyle: TextStyle(
|
||||
color: Theme.of(context).accentColor,
|
||||
|
@ -124,7 +115,7 @@ class _PlaylistPageState extends State<PlaylistPage> {
|
|||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: data.item1.length < 2
|
||||
text: episodes.length < 2
|
||||
? ' episode '
|
||||
: ' episodes ',
|
||||
style: TextStyle(
|
||||
|
@ -133,7 +124,7 @@ class _PlaylistPageState extends State<PlaylistPage> {
|
|||
)),
|
||||
TextSpan(
|
||||
text:
|
||||
_sumPlaylistLength(data.item1).toString(),
|
||||
_sumPlaylistLength(episodes).toString(),
|
||||
style: GoogleFonts.cairo(
|
||||
textStyle: TextStyle(
|
||||
color: Theme.of(context).accentColor,
|
||||
|
@ -179,171 +170,24 @@ class _PlaylistPageState extends State<PlaylistPage> {
|
|||
height: 3,
|
||||
),
|
||||
Expanded(
|
||||
child: AnimatedList(
|
||||
controller: _controller,
|
||||
key: _playlistKey,
|
||||
shrinkWrap: true,
|
||||
child: ReorderableListView(
|
||||
onReorder: (int oldIndex, int newIndex) {
|
||||
if (newIndex > oldIndex) {
|
||||
newIndex -= 1;
|
||||
}
|
||||
final EpisodeBrief episodeRemove =
|
||||
episodes[oldIndex];
|
||||
audio.delFromPlaylist(episodeRemove);
|
||||
audio.addToPlaylistAt(episodeRemove, newIndex);
|
||||
setState(() {});
|
||||
},
|
||||
scrollDirection: Axis.vertical,
|
||||
initialItemCount: data.item1.length,
|
||||
itemBuilder: (context, index, animation) {
|
||||
Color _c = (Theme.of(context).brightness ==
|
||||
Brightness.light)
|
||||
? data.item1[index].primaryColor.colorizedark()
|
||||
: data.item1[index].primaryColor.colorizeLight();
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
children: episodes
|
||||
.map<Widget>((episode) => DismissibleContainer(
|
||||
episode: episode,
|
||||
key: ValueKey(episode.enclosureUrl),
|
||||
))
|
||||
.toList()),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
@ -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,
|
||||
// ),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,9 +28,10 @@ class DBHelper {
|
|||
void _onCreate(Database db, int version) async {
|
||||
await db
|
||||
.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,
|
||||
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
|
||||
.execute("""CREATE TABLE Episodes(id INTEGER PRIMARY KEY,title TEXT,
|
||||
enclosure_url TEXT UNIQUE, enclosure_length INTEGER, pubDate TEXT,
|
||||
|
@ -52,10 +53,9 @@ class DBHelper {
|
|||
await Future.forEach(podcasts, (s) async {
|
||||
List<Map> list;
|
||||
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]);
|
||||
int count = Sqflite.firstIntValue(await dbClient.rawQuery(
|
||||
'SELECT COUNT(*) FROM Episodes WHERE feed_id = ?', [s]));
|
||||
podcastLocal.add(PodcastLocal(
|
||||
list.first['title'],
|
||||
list.first['imageUrl'],
|
||||
|
@ -67,7 +67,7 @@ class DBHelper {
|
|||
list.first['provider'],
|
||||
list.first['link'],
|
||||
upateCount: list.first['update_count'],
|
||||
episodeCount: count));
|
||||
episodeCount: list.first['episode_count']));
|
||||
});
|
||||
return podcastLocal;
|
||||
}
|
||||
|
@ -94,6 +94,14 @@ class DBHelper {
|
|||
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 {
|
||||
var dbClient = await database;
|
||||
List<Map> list = await dbClient
|
||||
|
@ -122,8 +130,6 @@ class DBHelper {
|
|||
podcastLocal.provider,
|
||||
podcastLocal.link
|
||||
]);
|
||||
});
|
||||
await dbClient.transaction((txn) async {
|
||||
await txn.rawInsert(
|
||||
"""REPLACE INTO SubscribeHistory(id, title, rss_url, add_date) VALUES (?, ?, ?, ?)""",
|
||||
[
|
||||
|
@ -240,12 +246,17 @@ class DBHelper {
|
|||
return (sum ~/ 60).toDouble();
|
||||
}
|
||||
|
||||
Future<int> getPosition(EpisodeBrief episodeBrief) async {
|
||||
Future<PlayHistory> getPosition(EpisodeBrief episodeBrief) async {
|
||||
var dbClient = await database;
|
||||
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]);
|
||||
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) {
|
||||
|
@ -335,13 +346,6 @@ class DBHelper {
|
|||
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].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) {
|
||||
_isXimalaya(feed.items[i].enclosure.url)
|
||||
? 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;
|
||||
}
|
||||
|
||||
|
@ -391,57 +401,56 @@ class DBHelper {
|
|||
int count = Sqflite.firstIntValue(await dbClient.rawQuery(
|
||||
'SELECT COUNT(*) FROM Episodes WHERE feed_id = ?', [podcastLocal.id]));
|
||||
|
||||
print(count);
|
||||
await dbClient.rawUpdate(
|
||||
"""UPDATE PodcastLocal SET update_count = ? WHERE 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 ?? '');
|
||||
"UPDATE Episodes SET is_new = 0 WHERE feed_id = ?", [podcastLocal.id]);
|
||||
|
||||
if (feed.items[i].enclosure?.url != null) {
|
||||
_isXimalaya(feed.items[i].enclosure.url)
|
||||
? url = feed.items[i].enclosure.url.split('=').last
|
||||
: url = feed.items[i].enclosure.url;
|
||||
}
|
||||
for (int i = 0; i < result; i++) {
|
||||
print(feed.items[i].title);
|
||||
description = _getDescription(feed.items[i].content.value ?? '',
|
||||
feed.items[i].description ?? '', feed.items[i].itunes.summary ?? '');
|
||||
|
||||
final title = feed.items[i].itunes.title ?? feed.items[i].title;
|
||||
final length = feed.items[i]?.enclosure?.length ?? 0;
|
||||
final pubDate = feed.items[i].pubDate;
|
||||
final date = _parsePubDate(pubDate);
|
||||
final milliseconds = date.millisecondsSinceEpoch;
|
||||
final duration = feed.items[i].itunes.duration?.inMinutes ?? 0;
|
||||
final explicit = _getExplicit(feed.items[i].itunes.explicit);
|
||||
|
||||
if (url != null) {
|
||||
await dbClient.transaction((txn) {
|
||||
return txn.rawInsert(
|
||||
"""INSERT OR IGNORE INTO Episodes(title, enclosure_url, enclosure_length, pubDate,
|
||||
description, feed_id, milliseconds, duration, explicit, media_id) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
||||
[
|
||||
title,
|
||||
url,
|
||||
length,
|
||||
pubDate,
|
||||
description,
|
||||
podcastLocal.id,
|
||||
milliseconds,
|
||||
duration,
|
||||
explicit,
|
||||
url
|
||||
]);
|
||||
});
|
||||
}
|
||||
if (feed.items[i].enclosure?.url != null) {
|
||||
_isXimalaya(feed.items[i].enclosure.url)
|
||||
? url = feed.items[i].enclosure.url.split('=').last
|
||||
: url = feed.items[i].enclosure.url;
|
||||
}
|
||||
|
||||
final title = feed.items[i].itunes.title ?? feed.items[i].title;
|
||||
final length = feed.items[i]?.enclosure?.length ?? 0;
|
||||
final pubDate = feed.items[i].pubDate;
|
||||
final date = _parsePubDate(pubDate);
|
||||
final milliseconds = date.millisecondsSinceEpoch;
|
||||
final duration = feed.items[i].itunes.duration?.inMinutes ?? 0;
|
||||
final explicit = _getExplicit(feed.items[i].itunes.explicit);
|
||||
|
||||
if (url != null) {
|
||||
await dbClient.transaction((txn) async {
|
||||
int id = await txn.rawInsert(
|
||||
"""INSERT OR IGNORE INTO Episodes(title, enclosure_url, enclosure_length, pubDate,
|
||||
description, feed_id, milliseconds, duration, explicit, media_id, is_new) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1)""",
|
||||
[
|
||||
title,
|
||||
url,
|
||||
length,
|
||||
pubDate,
|
||||
description,
|
||||
podcastLocal.id,
|
||||
milliseconds,
|
||||
duration,
|
||||
explicit,
|
||||
url,
|
||||
]);
|
||||
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 {
|
||||
|
@ -450,7 +459,7 @@ class DBHelper {
|
|||
List<Map> list = await dbClient
|
||||
.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.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
|
||||
WHERE P.id = ? ORDER BY E.milliseconds DESC LIMIT ?""", [id, i]);
|
||||
for (int x = 0; x < list.length; x++) {
|
||||
|
@ -466,7 +475,8 @@ class DBHelper {
|
|||
list[x]['duration'],
|
||||
list[x]['explicit'],
|
||||
list[x]['imagePath'],
|
||||
list[x]['media_id']));
|
||||
list[x]['media_id'],
|
||||
list[x]['is_new']));
|
||||
}
|
||||
return episodes;
|
||||
}
|
||||
|
@ -477,7 +487,7 @@ class DBHelper {
|
|||
List<Map> list = await dbClient
|
||||
.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.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
|
||||
where E.feed_id = ? ORDER BY E.milliseconds DESC LIMIT 3""", [id]);
|
||||
for (int x = 0; x < list.length; x++) {
|
||||
|
@ -493,7 +503,8 @@ class DBHelper {
|
|||
list[x]['duration'],
|
||||
list[x]['explicit'],
|
||||
list[x]['imagePath'],
|
||||
list[x]['media_id']));
|
||||
list[x]['media_id'],
|
||||
list[x]['is_new']));
|
||||
}
|
||||
return episodes;
|
||||
}
|
||||
|
@ -504,7 +515,7 @@ class DBHelper {
|
|||
List<Map> list = await dbClient.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.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
|
||||
where E.enclosure_url = ? ORDER BY E.milliseconds DESC LIMIT 3""",
|
||||
[url]);
|
||||
|
@ -522,7 +533,8 @@ class DBHelper {
|
|||
list.first['duration'],
|
||||
list.first['explicit'],
|
||||
list.first['imagePath'],
|
||||
list.first['media_id']);
|
||||
list.first['media_id'],
|
||||
list.first['is_new']);
|
||||
return episode;
|
||||
}
|
||||
|
||||
|
@ -532,7 +544,7 @@ class DBHelper {
|
|||
List<Map> list = await dbClient
|
||||
.rawQuery("""SELECT E.title, E.enclosure_url, E.enclosure_length,
|
||||
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
|
||||
ORDER BY E.milliseconds DESC LIMIT ? """, [top]);
|
||||
for (int x = 0; x < list.length; x++) {
|
||||
|
@ -548,7 +560,8 @@ class DBHelper {
|
|||
list[x]['duration'],
|
||||
list[x]['explicit'],
|
||||
list[x]['imagePath'],
|
||||
list[x]['media_id']));
|
||||
list[x]['media_id'],
|
||||
list[x]['is_new']));
|
||||
}
|
||||
return episodes;
|
||||
}
|
||||
|
@ -559,8 +572,8 @@ class DBHelper {
|
|||
List<Map> list = await dbClient.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.downloaded,
|
||||
P.primaryColor, E.media_id FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
|
||||
WHERE E.liked = 1 ORDER BY E.milliseconds DESC LIMIT ?""",[i]);
|
||||
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]);
|
||||
for (int x = 0; x < list.length; x++) {
|
||||
episodes.add(EpisodeBrief(
|
||||
list[x]['title'],
|
||||
|
@ -574,7 +587,8 @@ class DBHelper {
|
|||
list[x]['duration'],
|
||||
list[x]['explicit'],
|
||||
list[x]['imagePath'],
|
||||
list[x]['media_id']));
|
||||
list[x]['media_id'],
|
||||
list[x]['is_new']));
|
||||
}
|
||||
return episodes;
|
||||
}
|
||||
|
@ -597,19 +611,19 @@ class DBHelper {
|
|||
|
||||
Future<int> saveDownloaded(String url, String id) async {
|
||||
var dbClient = await database;
|
||||
int _milliseconds = DateTime.now().millisecondsSinceEpoch;
|
||||
int milliseconds = DateTime.now().millisecondsSinceEpoch;
|
||||
int count = await dbClient.rawUpdate(
|
||||
"UPDATE Episodes SET downloaded = ?, download_date = ? WHERE enclosure_url = ?",
|
||||
[id, _milliseconds, url]);
|
||||
[id, milliseconds, url]);
|
||||
return count;
|
||||
}
|
||||
|
||||
Future<int> saveMediaId(String url, String path) async {
|
||||
Future<int> saveMediaId(String url, String path, String id) async {
|
||||
var dbClient = await database;
|
||||
int _milliseconds = DateTime.now().millisecondsSinceEpoch;
|
||||
int milliseconds = DateTime.now().millisecondsSinceEpoch;
|
||||
int count = await dbClient.rawUpdate(
|
||||
"UPDATE Episodes SET media_id = ?, download_date = ? WHERE enclosure_url = ?",
|
||||
[path, _milliseconds, url]);
|
||||
"UPDATE Episodes SET media_id = ?, download_date = ?, downloaded = ? WHERE enclosure_url = ?",
|
||||
[path, milliseconds, id, url]);
|
||||
return count;
|
||||
}
|
||||
|
||||
|
@ -628,8 +642,8 @@ class DBHelper {
|
|||
List<Map> list = await dbClient.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.downloaded,
|
||||
P.primaryColor, E.media_id 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""");
|
||||
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""");
|
||||
for (int x = 0; x < list.length; x++) {
|
||||
episodes.add(EpisodeBrief(
|
||||
list[x]['title'],
|
||||
|
@ -643,7 +657,8 @@ class DBHelper {
|
|||
list[x]['duration'],
|
||||
list[x]['explicit'],
|
||||
list[x]['imagePath'],
|
||||
list[x]['media_id']));
|
||||
list[x]['media_id'],
|
||||
list[x]['is_new']));
|
||||
}
|
||||
return episodes;
|
||||
}
|
||||
|
@ -670,7 +685,7 @@ class DBHelper {
|
|||
List<Map> list = await dbClient.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.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]);
|
||||
if (list.length == 0) {
|
||||
return null;
|
||||
|
@ -687,7 +702,8 @@ class DBHelper {
|
|||
list.first['duration'],
|
||||
list.first['explicit'],
|
||||
list.first['imagePath'],
|
||||
list.first['media_id']);
|
||||
list.first['media_id'],
|
||||
list.first['is_new']);
|
||||
return episode;
|
||||
}
|
||||
}
|
||||
|
@ -698,7 +714,7 @@ class DBHelper {
|
|||
List<Map> list = await dbClient.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.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]);
|
||||
episode = EpisodeBrief(
|
||||
list.first['title'],
|
||||
|
@ -712,7 +728,8 @@ class DBHelper {
|
|||
list.first['duration'],
|
||||
list.first['explicit'],
|
||||
list.first['imagePath'],
|
||||
list.first['media_id']);
|
||||
list.first['media_id'],
|
||||
list.first['is_new']);
|
||||
return episode;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,12 +8,13 @@ import 'package:tsacdop/home/appbar/addpodcast.dart';
|
|||
import 'package:tsacdop/class/audiostate.dart';
|
||||
import 'package:tsacdop/class/importompl.dart';
|
||||
import 'package:tsacdop/class/settingstate.dart';
|
||||
import 'package:tsacdop/class/download_state.dart';
|
||||
|
||||
final SettingState themeSetting = SettingState();
|
||||
Future main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await themeSetting.initData();
|
||||
await FlutterDownloader.initialize();
|
||||
|
||||
runApp(
|
||||
MultiProvider(
|
||||
providers: [
|
||||
|
@ -21,13 +22,16 @@ Future main() async {
|
|||
ChangeNotifierProvider(create: (_) => AudioPlayerNotifier()),
|
||||
ChangeNotifierProvider(create: (_) => GroupList()),
|
||||
ChangeNotifierProvider(create: (_) => ImportOmpl()),
|
||||
ChangeNotifierProvider(create: (_) => DownloadState(),
|
||||
)
|
||||
],
|
||||
child: MyApp(),
|
||||
),
|
||||
);
|
||||
|
||||
SystemUiOverlayStyle systemUiOverlayStyle =
|
||||
SystemUiOverlayStyle(statusBarColor: Colors.transparent, systemNavigationBarColor: Colors.transparent);
|
||||
await FlutterDownloader.initialize();
|
||||
SystemUiOverlayStyle systemUiOverlayStyle = SystemUiOverlayStyle(
|
||||
statusBarColor: Colors.transparent,
|
||||
systemNavigationBarColor: Colors.transparent);
|
||||
SystemChrome.setSystemUIOverlayStyle(systemUiOverlayStyle);
|
||||
|
||||
await SystemChrome.setPreferredOrientations(
|
||||
|
@ -67,7 +71,7 @@ class MyApp extends StatelessWidget {
|
|||
darkTheme: ThemeData.dark().copyWith(
|
||||
accentColor: setting.accentSetColor,
|
||||
primaryColorDark: Colors.grey[800],
|
||||
// scaffoldBackgroundColor: Colors.black87,
|
||||
// scaffoldBackgroundColor: Colors.black87,
|
||||
appBarTheme: AppBarTheme(elevation: 0),
|
||||
),
|
||||
home: MyHomePage(),
|
||||
|
|
|
@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter/services.dart';
|
||||
import 'package:html/parser.dart';
|
||||
import 'package:tsacdop/class/audiostate.dart';
|
||||
import 'package:tsacdop/class/podcast_group.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:flutter_linkify/flutter_linkify.dart';
|
||||
|
@ -35,15 +36,18 @@ class _PodcastDetailState extends State<PodcastDetail> {
|
|||
Future _updateRssItem(PodcastLocal podcastLocal) async {
|
||||
var dbHelper = DBHelper();
|
||||
final result = await dbHelper.updatePodcastRss(podcastLocal);
|
||||
result == 0
|
||||
? Fluttertoast.showToast(
|
||||
if(result == 0)
|
||||
{ Fluttertoast.showToast(
|
||||
msg: 'No Update',
|
||||
gravity: ToastGravity.TOP,
|
||||
)
|
||||
: Fluttertoast.showToast(
|
||||
);}
|
||||
else{
|
||||
Fluttertoast.showToast(
|
||||
msg: 'Updated $result Episodes',
|
||||
gravity: ToastGravity.TOP,
|
||||
);
|
||||
Provider.of<GroupList>(context, listen: false).updatePodcast(podcastLocal);
|
||||
}
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
|
@ -400,8 +404,6 @@ class _PodcastDetailState extends State<PodcastDetail> {
|
|||
episodes: snapshot.data,
|
||||
showFavorite: true,
|
||||
showNumber: true,
|
||||
updateCount:
|
||||
widget.podcastLocal.upateCount,
|
||||
episodeCount:
|
||||
widget.podcastLocal.episodeCount,
|
||||
),
|
||||
|
|
|
@ -40,7 +40,7 @@ class _PodcastGroupListState extends State<PodcastGroupList> {
|
|||
widget.group.podcasts.insert(newIndex, podcast);
|
||||
});
|
||||
widget.group.setOrderedPodcasts = widget.group.podcasts;
|
||||
groupList.addToOrderChanged(widget.group.name);
|
||||
groupList.addToOrderChanged(widget.group);
|
||||
},
|
||||
children: widget.group.podcasts
|
||||
.map<Widget>((PodcastLocal podcastLocal) {
|
||||
|
@ -319,7 +319,7 @@ class _PodcastCardState extends State<PodcastCard> {
|
|||
bottom: 20),
|
||||
title: Text('Remove confirm'),
|
||||
content: Text(
|
||||
'Are you sure you want to unsubscribe?'),
|
||||
'Are you sure you want to unsubscribe?'),
|
||||
actions: <Widget>[
|
||||
FlatButton(
|
||||
onPressed: () =>
|
||||
|
|
|
@ -72,7 +72,7 @@ class _PodcastManageState extends State<PodcastManage>
|
|||
Widget _saveButton(BuildContext context) {
|
||||
return Consumer<GroupList>(
|
||||
builder: (_, groupList, __) {
|
||||
if (groupList.orderChanged.contains(groupList.groups[_index].name)) {
|
||||
if (groupList.orderChanged.contains(groupList.groups[_index])) {
|
||||
_controller.forward();
|
||||
} else if (_fraction > 0) {
|
||||
_controller.reverse();
|
||||
|
@ -92,8 +92,8 @@ class _PodcastManageState extends State<PodcastManage>
|
|||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey[700],
|
||||
blurRadius: 5,
|
||||
color: Colors.grey[700].withOpacity(0.5),
|
||||
blurRadius: 1,
|
||||
offset: Offset(1, 1),
|
||||
),
|
||||
]),
|
||||
|
@ -161,298 +161,304 @@ class _PodcastManageState extends State<PodcastManage>
|
|||
OrderMenu(),
|
||||
],
|
||||
),
|
||||
body: Consumer<GroupList>(builder: (_, groupList, __) {
|
||||
bool _isLoading = groupList.isLoading;
|
||||
List<PodcastGroup> _groups = groupList.groups;
|
||||
return _isLoading
|
||||
? Center()
|
||||
: Stack(
|
||||
children: <Widget>[
|
||||
CustomTabView(
|
||||
itemCount: _groups.length,
|
||||
tabBuilder: (context, index) => Tab(
|
||||
child: Container(
|
||||
height: 30.0,
|
||||
padding: EdgeInsets.symmetric(horizontal: 10.0),
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: (_scroll - index).abs() > 1
|
||||
? Colors.grey[300]
|
||||
: Colors.grey[300]
|
||||
.withOpacity((_scroll - index).abs()),
|
||||
borderRadius:
|
||||
BorderRadius.all(Radius.circular(15)),
|
||||
),
|
||||
child: Text(
|
||||
_groups[index].name,
|
||||
)),
|
||||
),
|
||||
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),
|
||||
body: WillPopScope(
|
||||
onWillPop: () async {
|
||||
await Provider.of<GroupList>(context, listen: false).clearOrderChanged();
|
||||
return true;
|
||||
},
|
||||
child: Consumer<GroupList>(builder: (_, groupList, __) {
|
||||
bool _isLoading = groupList.isLoading;
|
||||
List<PodcastGroup> _groups = groupList.groups;
|
||||
return _isLoading
|
||||
? Center()
|
||||
: Stack(
|
||||
children: <Widget>[
|
||||
CustomTabView(
|
||||
itemCount: _groups.length,
|
||||
tabBuilder: (context, index) => Tab(
|
||||
child: Container(
|
||||
height: 30.0,
|
||||
padding: EdgeInsets.symmetric(horizontal: 10.0),
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: (_scroll - index).abs() > 1
|
||||
? Colors.grey[300]
|
||||
: Colors.grey[300]
|
||||
.withOpacity((_scroll - index).abs()),
|
||||
borderRadius:
|
||||
BorderRadius.all(Radius.circular(15)),
|
||||
),
|
||||
),
|
||||
)
|
||||
: Center(),
|
||||
Positioned(
|
||||
right: 30,
|
||||
bottom: 30,
|
||||
child: _saveButton(context),
|
||||
),
|
||||
_showSetting
|
||||
? Positioned(
|
||||
right: 30 * _menuValue,
|
||||
bottom: 100,
|
||||
child: Container(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: <Widget>[
|
||||
Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
_menuController.reverse();
|
||||
setState(() => _showSetting = false);
|
||||
_index == 0
|
||||
? Fluttertoast.showToast(
|
||||
msg:
|
||||
'Home group is not supported',
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
)
|
||||
: showGeneralDialog(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
barrierLabel:
|
||||
MaterialLocalizations.of(
|
||||
context)
|
||||
.modalBarrierDismissLabel,
|
||||
barrierColor: Colors.black54,
|
||||
transitionDuration:
|
||||
const Duration(
|
||||
milliseconds: 300),
|
||||
pageBuilder: (BuildContext
|
||||
context,
|
||||
Animation animaiton,
|
||||
Animation
|
||||
secondaryAnimation) =>
|
||||
RenameGroup(
|
||||
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)),
|
||||
],
|
||||
child: Text(
|
||||
_groups[index].name,
|
||||
)),
|
||||
),
|
||||
pageBuilder: (context, index) => Container(
|
||||
key: ValueKey(_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),
|
||||
),
|
||||
),
|
||||
)
|
||||
: Center(),
|
||||
Positioned(
|
||||
right: 30,
|
||||
bottom: 30,
|
||||
child: _saveButton(context),
|
||||
),
|
||||
_showSetting
|
||||
? Positioned(
|
||||
right: 30 * _menuValue,
|
||||
bottom: 100,
|
||||
child: Container(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: <Widget>[
|
||||
Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
_menuController.reverse();
|
||||
setState(() => _showSetting = false);
|
||||
_index == 0
|
||||
? Fluttertoast.showToast(
|
||||
msg:
|
||||
'Home group is not supported',
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
)
|
||||
: showGeneralDialog(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
barrierLabel:
|
||||
MaterialLocalizations.of(
|
||||
context)
|
||||
.modalBarrierDismissLabel,
|
||||
barrierColor: Colors.black54,
|
||||
transitionDuration:
|
||||
const Duration(
|
||||
milliseconds: 300),
|
||||
pageBuilder: (BuildContext
|
||||
context,
|
||||
Animation animaiton,
|
||||
Animation
|
||||
secondaryAnimation) =>
|
||||
RenameGroup(
|
||||
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:
|
||||
EdgeInsets.symmetric(vertical: 10.0)),
|
||||
Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
_menuController.reverse();
|
||||
setState(() => _showSetting = false);
|
||||
_index == 0
|
||||
? Fluttertoast.showToast(
|
||||
msg:
|
||||
'Home group is not supported',
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
)
|
||||
: showGeneralDialog(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
barrierLabel:
|
||||
MaterialLocalizations.of(
|
||||
context)
|
||||
.modalBarrierDismissLabel,
|
||||
barrierColor: Colors.black54,
|
||||
transitionDuration:
|
||||
const Duration(
|
||||
milliseconds: 300),
|
||||
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),
|
||||
// statusBarColor: Theme.of(
|
||||
// context)
|
||||
// .brightness ==
|
||||
// Brightness.light
|
||||
// ? Color.fromRGBO(
|
||||
// 113,
|
||||
// 113,
|
||||
// 113,
|
||||
// 1)
|
||||
// : Color.fromRGBO(
|
||||
// 5, 5, 5, 1),
|
||||
),
|
||||
child: AlertDialog(
|
||||
elevation: 1,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius:
|
||||
BorderRadius.all(
|
||||
Radius.circular(
|
||||
10.0))),
|
||||
titlePadding:
|
||||
EdgeInsets.only(
|
||||
top: 20,
|
||||
left: 20,
|
||||
right: 200,
|
||||
bottom: 20),
|
||||
title: Text(
|
||||
'Delete confirm'),
|
||||
content: Text(
|
||||
'Are you sure you want to delete this group? Podcasts will be moved to Home group.'),
|
||||
actions: <Widget>[
|
||||
FlatButton(
|
||||
onPressed: () =>
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: 10.0)),
|
||||
Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
_menuController.reverse();
|
||||
setState(() => _showSetting = false);
|
||||
_index == 0
|
||||
? Fluttertoast.showToast(
|
||||
msg:
|
||||
'Home group is not supported',
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
)
|
||||
: showGeneralDialog(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
barrierLabel:
|
||||
MaterialLocalizations.of(
|
||||
context)
|
||||
.modalBarrierDismissLabel,
|
||||
barrierColor: Colors.black54,
|
||||
transitionDuration:
|
||||
const Duration(
|
||||
milliseconds: 300),
|
||||
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),
|
||||
// statusBarColor: Theme.of(
|
||||
// context)
|
||||
// .brightness ==
|
||||
// Brightness.light
|
||||
// ? Color.fromRGBO(
|
||||
// 113,
|
||||
// 113,
|
||||
// 113,
|
||||
// 1)
|
||||
// : Color.fromRGBO(
|
||||
// 5, 5, 5, 1),
|
||||
),
|
||||
child: AlertDialog(
|
||||
elevation: 1,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius
|
||||
.all(Radius
|
||||
.circular(
|
||||
10.0))),
|
||||
titlePadding:
|
||||
EdgeInsets.only(
|
||||
top: 20,
|
||||
left: 20,
|
||||
right: 200,
|
||||
bottom: 20),
|
||||
title: Text(
|
||||
'Delete confirm'),
|
||||
content: Text(
|
||||
'Are you sure you want to delete this group? Podcasts will be moved to Home group.'),
|
||||
actions: <Widget>[
|
||||
FlatButton(
|
||||
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(
|
||||
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(
|
||||
context)
|
||||
.pop();
|
||||
},
|
||||
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)),
|
||||
],
|
||||
.pop();
|
||||
},
|
||||
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(),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ import 'package:flutter_file_dialog/flutter_file_dialog.dart';
|
|||
|
||||
import 'package:tsacdop/class/audiostate.dart';
|
||||
import 'package:tsacdop/util/ompl_build.dart';
|
||||
import 'package:tsacdop/util/context_extension.dart';
|
||||
import 'theme.dart';
|
||||
import 'storage.dart';
|
||||
import 'history.dart';
|
||||
|
@ -56,7 +57,7 @@ class Settings extends StatelessWidget {
|
|||
appBar: AppBar(
|
||||
title: Text('Settings'),
|
||||
elevation: 0,
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
backgroundColor: context.primaryColor,
|
||||
),
|
||||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
|
@ -81,7 +82,7 @@ class Settings extends StatelessWidget {
|
|||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyText1
|
||||
.copyWith(color: Theme.of(context).accentColor)),
|
||||
.copyWith(color: context.accentColor)),
|
||||
),
|
||||
ListView(
|
||||
physics: ClampingScrollPhysics(),
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:app_settings/app_settings.dart';
|
||||
import 'package:tsacdop/settings/downloads_manage.dart';
|
||||
import 'package:tsacdop/class/settingstate.dart';
|
||||
|
||||
class StorageSetting extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var settings = Provider.of<SettingState>(context, listen: false);
|
||||
return AnnotatedRegion<SystemUiOverlayStyle>(
|
||||
value: SystemUiOverlayStyle(
|
||||
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
|
||||
|
@ -24,6 +27,54 @@ class StorageSetting extends StatelessWidget {
|
|||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
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(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
|
|
|
@ -71,11 +71,10 @@ class SyncingSetting extends StatelessWidget {
|
|||
value: data.item1,
|
||||
onChanged: (boo) async {
|
||||
settings.autoUpdate = boo;
|
||||
if (boo) {
|
||||
if (boo)
|
||||
settings.setWorkManager(data.item2);
|
||||
} else {
|
||||
else
|
||||
settings.cancelWork();
|
||||
}
|
||||
}),
|
||||
),
|
||||
Divider(height: 2),
|
||||
|
@ -92,7 +91,8 @@ class SyncingSetting extends StatelessWidget {
|
|||
elevation: 1,
|
||||
value: data.item2,
|
||||
onChanged: data.item1
|
||||
? (value) {
|
||||
? (value) async {
|
||||
await settings.cancelWork();
|
||||
settings.setWorkManager(value);
|
||||
}
|
||||
: null,
|
||||
|
|
|
@ -3,6 +3,7 @@ import 'package:flutter/services.dart';
|
|||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter_colorpicker/flutter_colorpicker.dart';
|
||||
import 'package:tsacdop/class/settingstate.dart';
|
||||
import 'package:tsacdop/util/context_extension.dart';
|
||||
|
||||
class ThemeSetting extends StatelessWidget {
|
||||
@override
|
||||
|
@ -61,11 +62,11 @@ class ThemeSetting extends StatelessWidget {
|
|||
AnnotatedRegion<SystemUiOverlayStyle>(
|
||||
value: SystemUiOverlayStyle(
|
||||
statusBarIconBrightness: Brightness.light,
|
||||
// systemNavigationBarColor:
|
||||
// Theme.of(context).brightness ==
|
||||
// Brightness.light
|
||||
// ? Color.fromRGBO(113, 113, 113, 1)
|
||||
// : Color.fromRGBO(15, 15, 15, 1),
|
||||
systemNavigationBarColor:
|
||||
Theme.of(context).brightness ==
|
||||
Brightness.light
|
||||
? Color.fromRGBO(113, 113, 113, 1)
|
||||
: Color.fromRGBO(15, 15, 15, 1),
|
||||
// statusBarColor:
|
||||
// Theme.of(context).brightness ==
|
||||
// Brightness.light
|
||||
|
@ -138,11 +139,11 @@ class ThemeSetting extends StatelessWidget {
|
|||
AnnotatedRegion<SystemUiOverlayStyle>(
|
||||
value: SystemUiOverlayStyle(
|
||||
statusBarIconBrightness: Brightness.light,
|
||||
// systemNavigationBarColor:
|
||||
// Theme.of(context).brightness ==
|
||||
// Brightness.light
|
||||
// ? Color.fromRGBO(113, 113, 113, 1)
|
||||
// : Color.fromRGBO(15, 15, 15, 1),
|
||||
systemNavigationBarColor:
|
||||
Theme.of(context).brightness ==
|
||||
Brightness.light
|
||||
? Color.fromRGBO(113, 113, 113, 1)
|
||||
: Color.fromRGBO(15, 15, 15, 1),
|
||||
// statusBarColor:
|
||||
// Theme.of(context).brightness ==
|
||||
// Brightness.light
|
||||
|
@ -155,7 +156,7 @@ class ThemeSetting extends StatelessWidget {
|
|||
top: 20,
|
||||
left: 40,
|
||||
right: 200,
|
||||
bottom: 20),
|
||||
bottom: 0),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(10.0))),
|
||||
|
@ -165,7 +166,7 @@ class ThemeSetting extends StatelessWidget {
|
|||
onColorChanged: (value) {
|
||||
settings.setAccentColor = value;
|
||||
},
|
||||
pickerColor: Colors.blue,
|
||||
pickerColor: context.accentColor,
|
||||
),
|
||||
),
|
||||
))),
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
import 'dart:async';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
@ -21,18 +22,15 @@ class EpisodeGrid extends StatelessWidget {
|
|||
final bool showFavorite;
|
||||
final bool showDownload;
|
||||
final bool showNumber;
|
||||
final int updateCount;
|
||||
final String heroTag;
|
||||
final int episodeCount;
|
||||
EpisodeGrid(
|
||||
{Key key,
|
||||
@required this.episodes,
|
||||
this.heroTag = '',
|
||||
this.showDownload = false,
|
||||
this.showFavorite = false,
|
||||
this.showNumber = false,
|
||||
this.updateCount = 0,
|
||||
this.episodeCount = 0})
|
||||
this.episodeCount = 0
|
||||
})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
|
@ -183,7 +181,7 @@ class EpisodeGrid extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
Spacer(),
|
||||
index < updateCount
|
||||
episodes[index].isNew == 1
|
||||
? Text('New',
|
||||
style: TextStyle(
|
||||
color: Colors.red,
|
||||
|
@ -197,7 +195,7 @@ class EpisodeGrid extends StatelessWidget {
|
|||
? Container(
|
||||
alignment: Alignment.topRight,
|
||||
child: Text(
|
||||
(episodeCount- index).toString(),
|
||||
(episodeCount - index).toString(),
|
||||
style: GoogleFonts.teko(
|
||||
textStyle: TextStyle(
|
||||
fontSize: _width / 24,
|
||||
|
@ -240,10 +238,6 @@ class EpisodeGrid extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
Spacer(),
|
||||
showDownload
|
||||
? DownloadIcon(
|
||||
episodeBrief: episodes[index])
|
||||
: Center(),
|
||||
Padding(
|
||||
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 {
|
||||
const OpenContainerWrapper({
|
||||
|
@ -469,3 +321,5 @@ class OpenContainerWrapper extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -53,13 +53,18 @@ dev_dependencies:
|
|||
app_settings: ^3.0.1
|
||||
fl_chart: ^0.8.7
|
||||
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
|
||||
line_icons:
|
||||
git:
|
||||
url: https://github.com/galonsos/line_icons.git
|
||||
flutter_file_dialog: ^0.0.5
|
||||
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
|
||||
# following page: https://www.dartlang.org/tools/pub/pubspec
|
||||
|
|
Loading…
Reference in New Issue