Change audio plugin to just_audio

Add storage setting
Add history setting
This commit is contained in:
stonegate 2020-03-14 11:14:24 +08:00
parent f195d62b07
commit cb5b17bba1
84 changed files with 3033 additions and 1618 deletions

View File

@ -13,9 +13,7 @@ jobs:
- run: - run:
name: Run Flutter doctor name: Run Flutter doctor
command: flutter doctor command: flutter doctor
- run:
name: flutter pub get
command: flutter pub get
-run: -run:
name: flutter run name: flutter run
command: flutter run command: flutter run

View File

@ -1,19 +1,21 @@
# Tsacdop # Tsacdop
[![CircleCI](https://circleci.com/gh/stonega/tsacdop.svg?style=svg)](https://circleci.com/gh/stonega/workflows/tsacdop/) [![CircleCI](https://circleci.com/gh/stonega/tsacdop.svg?style=svg)](https://circleci.com/gh/stonega/workflows/tsacdop/)
## About ## About
![logo](https://raw.githubusercontent.com/stonega/tsacdop/master/android/app/src/main/res/mipmap-hdpi/ic_launcher.png) <p align="center">
<img src="https://raw.githubusercontent.com/stonega/tsacdop/master/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png" art = "Logo"/>
![tsacdop](https://raw.githubusercontent.com/stonega/tsacdop/master/android/app/src/main/res/mipmap-hdpi/text.png) <img src="https://raw.githubusercontent.com/stonega/tsacdop/master/android/app/src/main/res/mipmap-xxhdpi/text.png" art = "Tsacdop"/>
</p>
Enjoy podcasts with Tsacdop. Enjoy podcasts with Tsacdop.
Tsacdop is a podcasts player developed with flutter, only for Android right now. Tsacdop is a podcast player developed with flutter, only for Android right now.
The development is still on early stage. The development is still on early stage.
Credit to flutter team and involved plugin developers, especially [webfeed](https://github.com/witochandra/webfeed) and [audiofileplayer](https://github.com/google/flutter.plugins/tree/master/packages/audiofileplayer/). Credit to flutter team and involved plugin developers, especially [webfeed](https://github.com/witochandra/webfeed) and [Just_Audio](https://pub.dev/packages/just_audio).
The podcasts search engine is powered by [ListenNotes](https://listennotes.com). The podcasts search engine is powered by [ListenNotes](https://listennotes.com).
## Preview
![homepage](https://raw.githubusercontent.com/stonega/tsacdop/master/preview/Screenshot_homepage.png) ![podcast](https://raw.githubusercontent.com/stonega/tsacdop/master/preview/Screenshot_podcast.png) ![episode](https://raw.githubusercontent.com/stonega/tsacdop/master/preview/Screenshot_episode.png) ![data](https://raw.githubusercontent.com/stonega/tsacdop/master/preview/Screenshot_data.png)
## License ## License
Tsacdop is licensed under the [MIT](https://github.com/stonega/tsacdop/blob/master/LICENSE) license. Tsacdop is licensed under the [MIT](https://github.com/stonega/tsacdop/blob/master/LICENSE) license.

View File

@ -67,6 +67,7 @@ android {
buildTypes { buildTypes {
release { release {
signingConfig signingConfigs.release signingConfig signingConfigs.release
shrinkResources false
} }
} }

View File

@ -6,15 +6,16 @@
FlutterApplication and put your custom class here. --> FlutterApplication and put your custom class here. -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<application android:name="io.flutter.app.FlutterApplication" android:label="Tsacdop" android:icon="@mipmap/ic_launcher" android:networkSecurityConfig="@xml/network_security_config"> <application android:name="io.flutter.app.FlutterApplication" android:label="Tsacdop" android:icon="@mipmap/ic_launcher" android:networkSecurityConfig="@xml/network_security_config">
<activity android:name=".MainActivity" android:launchMode="singleTop" android:theme="@style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize"> <activity android:name=".MainActivity" android:launchMode="singleTop" android:theme="@style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
<meta-data android:name="io.flutter.embedding.android.SplashScreenUntilFirstFrame" android:value="true" /> <!-- <meta-data android:name="io.flutter.embedding.android.SplashScreenUntilFirstFrame" android:value="true" /> -->
</activity> </activity>
<service android:name="com.google.flutter.plugins.audiofileplayer.AudiofileplayerService"> <service android:name="com.ryanheise.audioservice.AudioService">
<intent-filter> <intent-filter>
<action android:name="android.media.browse.MediaBrowserService" /> <action android:name="android.media.browse.MediaBrowserService" />
</intent-filter> </intent-filter>

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 272 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 550 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 460 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 435 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 411 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 488 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 522 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<color name = "blackGrey">
#121212
</color>
</resources>

View File

@ -1,7 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<network-security-config> <network-security-config>
<domain-config cleartextTrafficPermitted="true"> <base-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">lizhi.fm</domain> <trust-anchors>
<domain includeSubdomains="true">xmcdn.com</domain> <certificates src="system" />
</domain-config> </trust-anchors>
</base-config>
</network-security-config> </network-security-config>

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
assets/text_light.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -1,17 +1,46 @@
import 'dart:typed_data';
import 'dart:async'; import 'dart:async';
import 'dart:io';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:audio_service/audio_service.dart';
import 'package:just_audio/just_audio.dart';
import 'package:tsacdop/class/episodebrief.dart'; import 'package:tsacdop/class/episodebrief.dart';
import 'package:audiofileplayer/audiofileplayer.dart';
import 'package:flutter_downloader/flutter_downloader.dart';
import 'package:logging/logging.dart';
import 'package:audiofileplayer/audio_system.dart';
import 'package:tsacdop/local_storage/key_value_storage.dart'; import 'package:tsacdop/local_storage/key_value_storage.dart';
import 'package:tsacdop/local_storage/sqflite_localpodcast.dart'; import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
//enum AudioState { load, play, pause, complete, error, stop } MediaControl playControl = MediaControl(
androidIcon: 'drawable/ic_stat_play_circle_filled',
label: 'Play',
action: MediaAction.play,
);
MediaControl pauseControl = MediaControl(
androidIcon: 'drawable/ic_stat_pause_circle_filled',
label: 'Pause',
action: MediaAction.pause,
);
MediaControl skipToNextControl = MediaControl(
androidIcon: 'drawable/baseline_skip_next_white_24',
label: 'Next',
action: MediaAction.skipToNext,
);
MediaControl skipToPreviousControl = MediaControl(
androidIcon: 'drawable/ic_action_skip_previous',
label: 'Previous',
action: MediaAction.skipToPrevious,
);
MediaControl stopControl = MediaControl(
androidIcon: 'drawable/baseline_close_white_24',
label: 'Stop',
action: MediaAction.stop,
);
MediaControl forward30 = MediaControl(
androidIcon: 'drawable/ic_stat_forward_30',
label: 'forward30',
action: MediaAction.fastForward,
);
void _audioPlayerTaskEntrypoint() async {
AudioServiceBackground.run(() => AudioPlayerTask());
}
class PlayHistory { class PlayHistory {
DBHelper dbHelper = DBHelper(); DBHelper dbHelper = DBHelper();
@ -19,7 +48,9 @@ class PlayHistory {
String url; String url;
double seconds; double seconds;
double seekValue; double seekValue;
PlayHistory(this.title, this.url, this.seconds, this.seekValue); DateTime playdate;
PlayHistory(this.title, this.url, this.seconds, this.seekValue,
{this.playdate});
EpisodeBrief _episode; EpisodeBrief _episode;
EpisodeBrief get episode => _episode; EpisodeBrief get episode => _episode;
@ -31,32 +62,36 @@ class PlayHistory {
class Playlist { class Playlist {
String name; String name;
DBHelper dbHelper = DBHelper(); DBHelper dbHelper = DBHelper();
List<String> urls; // list of urls
//List<String> _urls;
//list of episodes
List<EpisodeBrief> _playlist; List<EpisodeBrief> _playlist;
//list of miediaitem
List<EpisodeBrief> get playlist => _playlist; List<EpisodeBrief> get playlist => _playlist;
KeyValueStorage storage = KeyValueStorage('playlist'); KeyValueStorage storage = KeyValueStorage('playlist');
Playlist(this.name, {List<String> urls}) : urls = urls ?? [];
getPlaylist() async { getPlaylist() async {
List<String> _urls = await storage.getStringList(); List<String> urls = await storage.getStringList();
if (_urls.length == 0) { print(urls);
if (urls.length == 0) {
_playlist = []; _playlist = [];
} else { } else {
_playlist = []; _playlist = [];
await Future.forEach(_urls, (url) async { await Future.forEach(urls, (url) async {
EpisodeBrief episode = await dbHelper.getRssItemWithUrl(url); EpisodeBrief episode = await dbHelper.getRssItemWithUrl(url);
print(episode.title); print(episode.title);
_playlist.add(episode); _playlist.add(episode);
}); });
} }
print(_playlist.length); print('Playlist: ' + _playlist.length.toString());
} }
savePlaylist() async { savePlaylist() async {
urls = []; List<String> urls = [];
urls.addAll(_playlist.map((e) => e.enclosureUrl)); urls.addAll(_playlist.map((e) => e.enclosureUrl));
print(urls); print(urls);
await storage.saveStringlist(urls); await storage.saveStringList(urls);
} }
addToPlayList(EpisodeBrief episodeBrief) async { addToPlayList(EpisodeBrief episodeBrief) async {
@ -64,6 +99,11 @@ class Playlist {
await savePlaylist(); await savePlaylist();
} }
addToPlayListAt(EpisodeBrief episodeBrief, int index) async {
_playlist.insert(index, episodeBrief);
await savePlaylist();
}
delFromPlaylist(EpisodeBrief episodeBrief) async { delFromPlaylist(EpisodeBrief episodeBrief) async {
_playlist _playlist
.removeWhere((item) => item.enclosureUrl == episodeBrief.enclosureUrl); .removeWhere((item) => item.enclosureUrl == episodeBrief.enclosureUrl);
@ -71,39 +111,32 @@ class Playlist {
} }
} }
class AudioPlayer extends ChangeNotifier { class AudioPlayerNotifier extends ChangeNotifier {
static const String replay10ButtonId = 'replay10ButtonId';
static const String newReleasesButtonId = 'newReleasesButtonId';
static const String likeButtonId = 'likeButtonId';
static const String pausenowButtonId = 'pausenowButtonId';
static const String forwardButtonId = 'forwardButtonId';
DBHelper dbHelper = DBHelper(); DBHelper dbHelper = DBHelper();
KeyValueStorage storage = KeyValueStorage('audioposition'); KeyValueStorage storage = KeyValueStorage('audioposition');
EpisodeBrief _episode; EpisodeBrief _episode;
Playlist _queue = Playlist('now'); Playlist _queue = Playlist();
BasicPlaybackState _audioState = BasicPlaybackState.none;
bool _playerRunning = false; bool _playerRunning = false;
Audio _backgroundAudio; bool _noSlide = true;
bool _backgroundAudioPlaying = false; int _backgroundAudioDuration = 0;
double _backgroundAudioDurationSeconds = 0; int _backgroundAudioPosition = 0;
double _backgroundAudioPositionSeconds = 0;
bool _remoteAudioLoading = false;
String _remoteErrorMessage; String _remoteErrorMessage;
double _seekSliderValue = 0.0; double _seekSliderValue = 0.0;
int _lastPostion; int _lastPostion = 0;
bool _skip = false;
bool _stopOnComplete = false; bool _stopOnComplete = false;
Timer _stopTimer; Timer _stopTimer;
//Show stopwatch after user setting timer. //Show stopwatch after user setting timer.
bool _showStopWatch = false; bool _showStopWatch = false;
bool _autoPlay = true;
DateTime _current;
final Logger _logger = Logger('audiofileplayer'); int _currentPosition;
bool get backgroundAudioPlaying => _backgroundAudioPlaying; BasicPlaybackState get audioState => _audioState;
bool get remoteAudioLoading => _remoteAudioLoading;
double get backgroundAudioDuration => _backgroundAudioDurationSeconds; int get backgroundAudioDuration => _backgroundAudioDuration;
double get backgroundAudioPosition => _backgroundAudioPositionSeconds; int get backgroundAudioPosition => _backgroundAudioPosition;
double get seekSliderValue => _seekSliderValue; double get seekSliderValue => _seekSliderValue;
String get remoteErrorMessage => _remoteErrorMessage; String get remoteErrorMessage => _remoteErrorMessage;
bool get playerRunning => _playerRunning; bool get playerRunning => _playerRunning;
@ -112,385 +145,470 @@ class AudioPlayer extends ChangeNotifier {
EpisodeBrief get episode => _episode; EpisodeBrief get episode => _episode;
bool get stopOnComplete => _stopOnComplete; bool get stopOnComplete => _stopOnComplete;
bool get showStopWatch => _showStopWatch; bool get showStopWatch => _showStopWatch;
bool get autoPlay => _autoPlay;
set setStopOnComplete(bool boo) { set setStopOnComplete(bool boo) {
_stopOnComplete = boo; _stopOnComplete = boo;
} }
set autoPlaySwitch(bool boo) {
_autoPlay = boo;
notifyListeners();
}
@override
void addListener(VoidCallback listener) async {
super.addListener(listener);
await AudioService.connect();
}
loadPlaylist() async { loadPlaylist() async {
await _queue.getPlaylist(); await _queue.getPlaylist();
_lastPostion = await storage.getInt(); _lastPostion = await storage.getInt();
} }
episodeLoad(EpisodeBrief episode) async { episodeLoad(EpisodeBrief episode) async {
if (_playerRunning && _episode != null) { if (_playerRunning) {
PlayHistory history = PlayHistory(_episode.title, _episode.enclosureUrl, PlayHistory history = PlayHistory(_episode.title, _episode.enclosureUrl,
backgroundAudioDuration, seekSliderValue); backgroundAudioPosition / 1000, seekSliderValue);
await dbHelper.saveHistory(history); await dbHelper.saveHistory(history);
AudioService.addQueueItemAt(episode.toMediaItem(), 0);
_queue.playlist
.removeWhere((item) => item.enclosureUrl == episode.enclosureUrl);
_queue.playlist.insert(0, episode);
notifyListeners();
await _queue.savePlaylist();
} else {
await _queue.getPlaylist();
_queue.playlist
.removeWhere((item) => item.enclosureUrl == episode.enclosureUrl);
_queue.playlist.insert(0, episode);
_queue.savePlaylist();
_backgroundAudioDuration = 0;
_backgroundAudioPosition = 0;
_seekSliderValue = 0;
_episode = episode;
_playerRunning = true;
notifyListeners();
await _queue.savePlaylist();
_startAudioService(0);
} }
AudioSystem.instance.addMediaEventListener(_mediaEventListener); }
_backgroundAudioPlaying = false;
_episode = episode; _startAudioService(int position) async {
await _queue.getPlaylist(); if (!AudioService.connected) {
_queue.playlist await AudioService.connect();
.removeWhere((item) => item.enclosureUrl == _episode.enclosureUrl); }
_queue.playlist.insert(0, _episode); await AudioService.start(
await _queue.savePlaylist(); backgroundTaskEntrypoint: _audioPlayerTaskEntrypoint,
await _play(_episode); androidNotificationChannelName: 'Tsacdop',
notificationColor: 0xFF2196f3,
androidNotificationIcon: 'mipmap/ic_launcher',
enableQueue: true,
androidStopOnRemoveTask: true,
);
_playerRunning = true; _playerRunning = true;
notifyListeners(); if (autoPlay) {
await Future.forEach(_queue.playlist, (episode) async {
await AudioService.addQueueItem(episode.toMediaItem());
});
} else {
await AudioService.addQueueItem(_queue.playlist.first.toMediaItem());
}
await AudioService.play();
AudioService.currentMediaItemStream.listen((item) async {
print(position);
print(_backgroundAudioDuration);
if (item != null) {
_episode = await dbHelper.getRssItemWithMediaId(item.id);
_backgroundAudioDuration = item?.duration ?? 0;
if (position > 0 && _backgroundAudioDuration > 0) {
AudioService.seekTo(position);
position = 0;
}
// _playerRunning = true;
}
notifyListeners();
});
AudioService.playbackStateStream.listen((event) async {
_current = DateTime.now();
_audioState = event?.basicState;
if (_audioState == BasicPlaybackState.skippingToNext &&
_episode != null) {
_queue.delFromPlaylist(_episode);
}
if (_audioState == BasicPlaybackState.paused ||
_audioState == BasicPlaybackState.skippingToNext &&
_episode != null) {
PlayHistory history = PlayHistory(_episode.title, _episode.enclosureUrl,
backgroundAudioPosition / 1000, seekSliderValue);
await dbHelper.saveHistory(history);
}
if (_audioState == BasicPlaybackState.stopped) {
_playerRunning = false;
}
_currentPosition = event?.currentPosition ?? 0;
notifyListeners();
});
Timer.periodic(Duration(milliseconds: 500), (timer) {
if (_noSlide) {
_audioState == BasicPlaybackState.playing
? (_backgroundAudioPosition < _backgroundAudioDuration)
? _backgroundAudioPosition = _currentPosition +
DateTime.now().difference(_current).inMilliseconds
: _backgroundAudioPosition = _backgroundAudioDuration
: _backgroundAudioPosition = _currentPosition;
if (_backgroundAudioDuration != null &&
_backgroundAudioDuration != 0 &&
_backgroundAudioPosition != null) {
_seekSliderValue =
_backgroundAudioPosition / _backgroundAudioDuration ?? 0;
}
if (_backgroundAudioPosition > 0) {
_lastPostion = _backgroundAudioPosition;
storage.saveInt(_lastPostion);
}
notifyListeners();
}
if (_audioState == BasicPlaybackState.stopped) {
timer.cancel();
}
});
} }
playlistLoad() async { playlistLoad() async {
_backgroundAudioPlaying = false;
await _queue.getPlaylist(); await _queue.getPlaylist();
_backgroundAudioDuration = 0;
_backgroundAudioPosition = 0;
_seekSliderValue = 0;
_episode = _queue.playlist.first; _episode = _queue.playlist.first;
_skip = true;
await _play(_episode);
_playerRunning = true; _playerRunning = true;
notifyListeners(); notifyListeners();
_startAudioService(_lastPostion ?? 0);
} }
playNext() async { playNext() async {
storage.saveInt(0); AudioService.skipToNext();
_lastPostion = 0;
PlayHistory history = PlayHistory(_episode.title, _episode.enclosureUrl,
backgroundAudioDuration, seekSliderValue);
await dbHelper.saveHistory(history);
await _queue.delFromPlaylist(_episode);
if (_queue.playlist.length > 0 && !_stopOnComplete) {
playlistLoad();
} else {
_stopOnComplete = false;
_backgroundAudioPlaying = false;
_remoteAudioLoading = false;
_playerRunning = false;
_disposeAudio();
notifyListeners();
}
} }
addToPlaylist(EpisodeBrief episode) async { addToPlaylist(EpisodeBrief episode) async {
_queue.addToPlayList(episode); if (_playerRunning) {
await _queue.getPlaylist(); await AudioService.addQueueItem(episode.toMediaItem());
}
print('add to playlist when not rnnning');
await _queue.addToPlayList(episode);
notifyListeners(); notifyListeners();
} }
addToPlaylistAt(EpisodeBrief episode, int index) async {
if (_playerRunning) {
await AudioService.addQueueItemAt(episode.toMediaItem(), index);
}
print('add to playlist when not rnnning');
await _queue.addToPlayListAt(episode, index);
notifyListeners();
}
updateMediaItem(EpisodeBrief episode) async {
int index = _queue.playlist
.indexWhere((item) => item.enclosureUrl == episode.enclosureUrl);
if (index > 0) {
await delFromPlaylist(episode);
await addToPlaylistAt(episode, index);
}
}
delFromPlaylist(EpisodeBrief episode) async { delFromPlaylist(EpisodeBrief episode) async {
_queue.delFromPlaylist(episode); if (_playerRunning) {
await _queue.getPlaylist(); await AudioService.removeQueueItem(episode.toMediaItem());
}
await _queue.delFromPlaylist(episode);
notifyListeners(); notifyListeners();
} }
pauseAduio() async { pauseAduio() async {
_pauseBackgroundAudio(); AudioService.pause();
notifyListeners();
PlayHistory history = PlayHistory(_episode.title, _episode.enclosureUrl,
backgroundAudioPosition, seekSliderValue);
await dbHelper.saveHistory(history);
} }
resumeAudio() { resumeAudio() async {
_resumeBackgroundAudio(); AudioService.play();
notifyListeners();
} }
forwardAudio(double s) { forwardAudio(int s) {
_forwardBackgroundAudio(s); int pos = _backgroundAudioPosition + s * 1000;
notifyListeners(); AudioService.seekTo(pos);
} }
sliderSeek(double val) { sliderSeek(double val) async {
print(val.toString());
_noSlide = false;
_seekSliderValue = val; _seekSliderValue = val;
notifyListeners(); notifyListeners();
final double positionSeconds = val * _backgroundAudioDurationSeconds; _currentPosition = (val * _backgroundAudioDuration).toInt();
_backgroundAudio.seek(positionSeconds); await AudioService.seekTo(_currentPosition);
AudioSystem.instance.setPlaybackState(true, positionSeconds); _noSlide = true;
} }
//Set sleep time
//Set sleep time
sleepTimer(int mins) { sleepTimer(int mins) {
_showStopWatch = true; _showStopWatch = true;
notifyListeners(); notifyListeners();
_stopTimer = Timer(Duration(minutes: mins),(){ _stopTimer = Timer(Duration(minutes: mins), () {
_stopOnComplete = false; _stopOnComplete = false;
_backgroundAudioPlaying = false;
_remoteAudioLoading = false;
_playerRunning = false;
_showStopWatch = false; _showStopWatch = false;
_disposeAudio(); AudioService.stop();
notifyListeners(); notifyListeners();
}); });
} }
//Cancel sleep timer //Cancel sleep timer
cancelTimer(){ cancelTimer() {
_stopTimer.cancel(); _stopTimer.cancel();
_showStopWatch = false; _showStopWatch = false;
notifyListeners(); notifyListeners();
} }
_disposeAudio() { @override
pauseAduio(); void dispose() async {
AudioSystem.instance?.stopBackgroundDisplay(); await AudioService.stop();
AudioSystem.instance?.removeMediaEventListener(_mediaEventListener); await AudioService.disconnect();
_backgroundAudio?.dispose(); super.dispose();
}
}
class AudioPlayerTask extends BackgroundAudioTask {
List<MediaItem> _queue = [];
AudioPlayer _audioPlayer = AudioPlayer();
Completer _completer = Completer();
BasicPlaybackState _skipState;
bool _playing;
bool get hasNext => _queue.length > 0;
MediaItem get mediaItem => _queue.first;
BasicPlaybackState _stateToBasicState(AudioPlaybackState state) {
switch (state) {
case AudioPlaybackState.none:
return BasicPlaybackState.none;
case AudioPlaybackState.stopped:
return BasicPlaybackState.stopped;
case AudioPlaybackState.paused:
return BasicPlaybackState.paused;
case AudioPlaybackState.playing:
return BasicPlaybackState.playing;
case AudioPlaybackState.connecting:
return _skipState ?? BasicPlaybackState.connecting;
case AudioPlaybackState.completed:
return BasicPlaybackState.stopped;
default:
throw Exception("Illegal state");
}
} }
@override @override
dispose() { Future<void> onStart() async {
_disposeAudio(); print('start background task');
super.dispose(); var playerStateSubscription = _audioPlayer.playbackStateStream
} .where((state) => state == AudioPlaybackState.completed)
.listen((state) {
_play(EpisodeBrief episodeBrief) async { _handlePlaybackCompleted();
AudioSystem.instance.addMediaEventListener(_mediaEventListener);
String url = _queue.playlist.first.enclosureUrl;
_getFile(url).then((result) {
result == 'NotDownload'
? _initbackgroundAudioPlayer(url)
: _initbackgroundAudioPlayerLocal(result);
}); });
} var eventSubscription = _audioPlayer.playbackEventStream.listen((event) {
BasicPlaybackState state;
Future<String> _getFile(String url) async { if (event.buffering) {
final task = await FlutterDownloader.loadTasksWithRawQuery( state = BasicPlaybackState.buffering;
query: "SELECT * FROM task WHERE url = '$url' AND status = 3"); } else {
if (task.length != 0) { state = _stateToBasicState(event.state);
String _filePath = task.first.savedDir + '/' + task.first.filename;
return _filePath;
}
return 'NotDownload';
}
ByteData _getAudio(String path) {
File audioFile = File(path);
Uint8List audio = audioFile.readAsBytesSync();
return ByteData.view(audio.buffer);
}
onDuration(double durationSeconds) {
_backgroundAudioDurationSeconds = durationSeconds;
_remoteAudioLoading = false;
_backgroundAudioPlaying = true;
if (_skip) {
_forwardBackgroundAudio(_lastPostion.toDouble());
_backgroundAudioPositionSeconds = _lastPostion.toDouble();
}
_skip = false;
_setNotification(true);
notifyListeners();
}
onPosition(double positionSeconds) {
if (_backgroundAudioPositionSeconds < _backgroundAudioDurationSeconds) {
_seekSliderValue =
_backgroundAudioPositionSeconds / _backgroundAudioDurationSeconds;
_backgroundAudioPositionSeconds = positionSeconds;
notifyListeners();
} else {
_seekSliderValue = 1;
_backgroundAudioPositionSeconds = _backgroundAudioDurationSeconds;
notifyListeners();
}
_lastPostion = positionSeconds.toInt();
storage.saveInt(_lastPostion);
}
onError(String message) {
_remoteErrorMessage = message;
_backgroundAudio.dispose();
_backgroundAudio = null;
_backgroundAudioPlaying = false;
_remoteAudioLoading = false;
}
void _initbackgroundAudioPlayerLocal(String path) {
_remoteErrorMessage = null;
_remoteAudioLoading = true;
ByteData audio = _getAudio(path);
_backgroundAudio?.pause();
_backgroundAudioPositionSeconds = 0;
_setNotification(false);
_backgroundAudio = Audio.loadFromByteData(audio,
onDuration: (double durationSeconds) => onDuration(durationSeconds),
onPosition: (double positionSeconds) => onPosition(positionSeconds),
onError: (String message) => onError(message),
onComplete: () => playNext(),
looping: false,
playInBackground: true)
..play();
}
void _initbackgroundAudioPlayer(String url) {
_remoteErrorMessage = null;
_remoteAudioLoading = true;
notifyListeners();
_backgroundAudio?.pause();
_backgroundAudioPositionSeconds = 0;
_setNotification(false);
_backgroundAudio = Audio.loadFromRemoteUrl(url,
onDuration: (double durationSeconds) => onDuration(durationSeconds),
onPosition: (double positionSeconds) => onPosition(positionSeconds),
onError: (String message) => onError(message),
onComplete: () => playNext(),
looping: false,
playInBackground: true)
..resume();
}
void _mediaEventListener(MediaEvent mediaEvent) {
_logger.info('App received media event of type: ${mediaEvent.type}');
final MediaActionType type = mediaEvent.type;
if (type == MediaActionType.play) {
_resumeBackgroundAudio();
} else if (type == MediaActionType.pause) {
_pauseBackgroundAudio();
} else if (type == MediaActionType.playPause) {
_backgroundAudioPlaying
? _pauseBackgroundAudio()
: _resumeBackgroundAudio();
} else if (type == MediaActionType.stop) {
_stopBackgroundAudio();
} else if (type == MediaActionType.seekTo) {
_backgroundAudio.seek(mediaEvent.seekToPositionSeconds);
AudioSystem.instance
.setPlaybackState(true, mediaEvent.seekToPositionSeconds);
} else if (type == MediaActionType.skipForward) {
final double skipIntervalSeconds = mediaEvent.skipIntervalSeconds;
_forwardBackgroundAudio(skipIntervalSeconds);
_logger.info(
'Skip-forward event had skipIntervalSeconds $skipIntervalSeconds.');
} else if (type == MediaActionType.skipBackward) {
final double skipIntervalSeconds = mediaEvent.skipIntervalSeconds;
_forwardBackgroundAudio(skipIntervalSeconds);
_logger.info(
'Skip-backward event had skipIntervalSeconds $skipIntervalSeconds.');
} else if (type == MediaActionType.custom) {
if (mediaEvent.customEventId == replay10ButtonId) {
_forwardBackgroundAudio(-10.0);
} else if (mediaEvent.customEventId == likeButtonId) {
_resumeBackgroundAudio();
} else if (mediaEvent.customEventId == forwardButtonId) {
_forwardBackgroundAudio(30.0);
} else if (mediaEvent.customEventId == pausenowButtonId) {
_pauseBackgroundAudio();
} }
if (state != BasicPlaybackState.stopped) {
_setState(
state: state,
position: event.position.inMilliseconds,
);
}
});
await _completer.future;
playerStateSubscription.cancel();
eventSubscription.cancel();
}
void _handlePlaybackCompleted() {
if (hasNext) {
onSkipToNext();
} else {
onStop();
} }
} }
Future<void> _setNotification(bool boo) async { void playPause() {
final Uint8List imageBytes = if (AudioServiceBackground.state.basicState == BasicPlaybackState.playing)
File('${_episode.imagePath}').readAsBytesSync(); onPause();
AudioSystem.instance.setMetadata(AudioMetadata( else
title: episode.title, onPlay();
artist: episode.feedTitle,
album: episode.feedTitle,
genre: "Podcast",
durationSeconds: _backgroundAudioDurationSeconds,
artBytes: imageBytes));
AudioSystem.instance.setPlaybackState(boo, _backgroundAudioPositionSeconds);
AudioSystem.instance.setAndroidNotificationButtons(<dynamic>[
AndroidMediaButtonType.pause,
_forwardButton,
AndroidMediaButtonType.stop,
], androidCompactIndices: <int>[
0,
1
]);
AudioSystem.instance.setSupportedMediaActions(<MediaActionType>{
MediaActionType.playPause,
MediaActionType.pause,
MediaActionType.next,
MediaActionType.previous,
MediaActionType.skipForward,
MediaActionType.skipBackward,
MediaActionType.seekTo,
MediaActionType.custom,
}, skipIntervalSeconds: 30);
} }
Future<void> _resumeBackgroundAudio() async { @override
_backgroundAudio.resume(); Future<void> onSkipToNext() async {
if (_playing == null) {
_backgroundAudioPlaying = true; // First time, we want to start playing
_playing = true;
final Uint8List imageBytes = } else {
File('${_episode.imagePath}').readAsBytesSync(); // Stop current item
AudioSystem.instance.setMetadata(AudioMetadata( await _audioPlayer.stop();
title: _episode.title, _queue.removeAt(0);
artist: _episode.feedTitle, }
album: _episode.feedTitle, AudioServiceBackground.setQueue(_queue);
genre: "Podcast", AudioServiceBackground.setMediaItem(mediaItem);
durationSeconds: _backgroundAudioDurationSeconds, _skipState = BasicPlaybackState.skippingToNext;
artBytes: imageBytes)); await _audioPlayer.setUrl(mediaItem.id);
print(mediaItem.id);
AudioSystem.instance Duration duration = await _audioPlayer.durationFuture;
.setPlaybackState(true, _backgroundAudioPositionSeconds); AudioServiceBackground.setMediaItem(
mediaItem.copyWith(duration: duration.inMilliseconds));
AudioSystem.instance.setAndroidNotificationButtons(<dynamic>[ _skipState = null;
AndroidMediaButtonType.pause, // Resume playback if we were playing
_forwardButton, if (_playing) {
AndroidMediaButtonType.stop, onPlay();
], androidCompactIndices: <int>[ } else {
0, _setState(state: BasicPlaybackState.paused);
1 }
]);
AudioSystem.instance.setSupportedMediaActions(<MediaActionType>{
MediaActionType.playPause,
MediaActionType.pause,
MediaActionType.next,
MediaActionType.previous,
MediaActionType.skipForward,
MediaActionType.skipBackward,
MediaActionType.seekTo,
MediaActionType.custom,
}, skipIntervalSeconds: 30);
} }
void _pauseBackgroundAudio() { @override
_backgroundAudio?.pause(); void onPlay() async {
_backgroundAudioPlaying = false; if (_skipState == null) {
AudioSystem.instance if (_playing == null) {
.setPlaybackState(false, _backgroundAudioPositionSeconds); _playing = true;
AudioSystem.instance.setAndroidNotificationButtons(<dynamic>[ AudioServiceBackground.setQueue(_queue);
AndroidMediaButtonType.play, await _audioPlayer.setUrl(mediaItem.id);
_forwardButton, Duration duration = await _audioPlayer.durationFuture;
AndroidMediaButtonType.stop, AudioServiceBackground.setMediaItem(
], androidCompactIndices: <int>[ mediaItem.copyWith(duration: duration.inMilliseconds));
0, }
1, _playing = true;
]); _audioPlayer.play();
}
AudioSystem.instance.setSupportedMediaActions(<MediaActionType>{
MediaActionType.playPause,
MediaActionType.play,
MediaActionType.next,
MediaActionType.previous,
});
} }
void _stopBackgroundAudio() { @override
_backgroundAudio.pause(); void onPause() {
_backgroundAudio.dispose(); if (_skipState == null) {
_backgroundAudioPlaying = false; if (_playing == null) {}
AudioSystem.instance.stopBackgroundDisplay(); _playing = false;
_audioPlayer.pause();
}
} }
void _forwardBackgroundAudio(double seconds) { @override
final double forwardposition = _backgroundAudioPositionSeconds + seconds; void onSeekTo(int position) {
_backgroundAudio.seek(forwardposition); _audioPlayer.seek(Duration(milliseconds: position));
AudioSystem.instance
.setPlaybackState(true, _backgroundAudioPositionSeconds);
} }
final _pauseButton = AndroidCustomMediaButton( @override
'pausenow', pausenowButtonId, 'ic_stat_pause_circle_filled'); void onClick(MediaButton button) {
final _replay10Button = AndroidCustomMediaButton( playPause();
'replay10', replay10ButtonId, 'ic_stat_replay_10'); }
final _forwardButton = AndroidCustomMediaButton(
'forward', forwardButtonId, 'ic_stat_forward_30'); @override
final _playnowButton = AndroidCustomMediaButton( void onStop() async {
'playnow', likeButtonId, 'ic_stat_play_circle_filled'); await _audioPlayer.stop();
_setState(state: BasicPlaybackState.stopped);
_completer.complete();
}
@override
void onAddQueueItem(MediaItem mediaItem) async {
_queue.add(mediaItem);
AudioServiceBackground.setQueue(_queue);
}
@override
void onRemoveQueueItem(MediaItem mediaItem) async {
_queue.removeWhere((item) => item.id == mediaItem.id);
await AudioServiceBackground.setQueue(_queue);
}
@override
void onAddQueueItemAt(MediaItem mediaItem, int index) async {
if (index == 0) {
await _audioPlayer.stop();
_queue.removeWhere((item) => item.id == mediaItem.id);
_queue.insert(0, mediaItem);
AudioServiceBackground.setQueue(_queue);
AudioServiceBackground.setMediaItem(mediaItem);
await _audioPlayer.setUrl(mediaItem.id);
Duration duration = await _audioPlayer.durationFuture;
AudioServiceBackground.setMediaItem(
mediaItem.copyWith(duration: duration.inMilliseconds));
onPlay();
} else {
_queue.insert(index, mediaItem);
AudioServiceBackground.setQueue(_queue);
}
}
@override
void onFastForward() {
_audioPlayer.seek(Duration(
milliseconds: AudioServiceBackground.state.position + 30 * 1000));
}
@override
void onAudioFocusLost() {
if (_skipState == null) {
if (_playing == null) {}
_playing = false;
_audioPlayer.pause();
}
}
@override
void onAudioBecomingNoisy() {
if (_skipState == null) {
if (_playing == null) {}
_playing = false;
_audioPlayer.pause();
}
}
@override
void onAudioFocusGained() {
if (_skipState == null) {
if (_playing == null) {}
_playing = true;
_audioPlayer.play();
}
}
@override
void onCustomAction(funtion, argument) {
switch (funtion) {
case 'addQueue':
break;
case 'updateMedia':
break;
}
}
void _setState({@required BasicPlaybackState state, int position}) {
if (position == null) {
position = _audioPlayer.playbackEvent.position.inMilliseconds;
}
AudioServiceBackground.setState(
controls: getControls(state),
systemActions: [MediaAction.seekTo],
basicState: state,
position: position,
);
}
List<MediaControl> getControls(BasicPlaybackState state) {
if (_playing) {
return [pauseControl, forward30, skipToNextControl, stopControl];
} else {
return [playControl, forward30, skipToNextControl, stopControl];
}
}
} }

View File

@ -1,4 +1,5 @@
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:audio_service/audio_service.dart';
class EpisodeBrief { class EpisodeBrief {
final String title; final String title;
@ -13,6 +14,7 @@ class EpisodeBrief {
final int duration; final int duration;
final int explicit; final int explicit;
final String imagePath; final String imagePath;
final String mediaId;
EpisodeBrief( EpisodeBrief(
this.title, this.title,
this.enclosureUrl, this.enclosureUrl,
@ -21,21 +23,31 @@ class EpisodeBrief {
this.feedTitle, this.feedTitle,
this.primaryColor, this.primaryColor,
this.liked, this.liked,
this.downloaded, this.downloaded,
this.duration, this.duration,
this.explicit, this.explicit,
this.imagePath this.imagePath,
); this.mediaId);
String dateToString(){ String dateToString() {
DateTime date = DateTime.fromMillisecondsSinceEpoch(pubDate); DateTime date = DateTime.fromMillisecondsSinceEpoch(pubDate, isUtc: true);
var diffrence = DateTime.now().difference(date); var diffrence = DateTime.now().difference(date);
if(diffrence.inHours < 24) { if (diffrence.inHours < 24) {
return '${diffrence.inHours} hours ago'; return '${diffrence.inHours} hours ago';
} else if (diffrence.inDays < 7){ } else if (diffrence.inDays < 7) {
return '${diffrence.inDays} days ago';} return '${diffrence.inDays} days ago';
else { } else {
return DateFormat.yMMMd().format( DateTime.fromMillisecondsSinceEpoch(pubDate)); return DateFormat.yMMMd()
} .format(DateTime.fromMillisecondsSinceEpoch(pubDate));
} }
}
MediaItem toMediaItem() {
return MediaItem(
id: mediaId,
title: title,
artist: feedTitle,
album: feedTitle,
artUri: 'file://$imagePath');
}
} }

View File

@ -55,8 +55,12 @@ class PodcastGroup {
} }
List<PodcastLocal> _podcasts; List<PodcastLocal> _podcasts;
List<PodcastLocal> _orderedPodcasts;
List<PodcastLocal> get ordereddPodcasts => _orderedPodcasts;
List<PodcastLocal> get podcasts => _podcasts; List<PodcastLocal> get podcasts => _podcasts;
set setOrderedPodcasts(List<PodcastLocal> list) {
_orderedPodcasts = list;
}
GroupEntity toEntity() { GroupEntity toEntity() {
return GroupEntity(name, id, color, podcastList); return GroupEntity(name, id, color, podcastList);
@ -82,9 +86,20 @@ class GroupList extends ChangeNotifier {
GroupList({List<PodcastGroup> groups}) : _groups = groups ?? []; GroupList({List<PodcastGroup> groups}) : _groups = groups ?? [];
bool _isLoading = false; bool _isLoading = false;
bool get isLoading => _isLoading; bool get isLoading => _isLoading;
List<String> _orderChanged = [];
List<String> get orderChanged => _orderChanged;
void addToOrderChanged(String name) {
_orderChanged.add(name);
notifyListeners();
}
void drlFromOrderChanged(String name) {
_orderChanged.remove(name);
notifyListeners();
}
@override @override
void addListener(VoidCallback listener) { void addListener(VoidCallback listener) {
super.addListener(listener); super.addListener(listener);
@ -105,14 +120,15 @@ class GroupList extends ChangeNotifier {
} }
Future addGroup(PodcastGroup podcastGroup) async { Future addGroup(PodcastGroup podcastGroup) async {
_isLoading = true;
_groups.add(podcastGroup); _groups.add(podcastGroup);
_saveGroup(); _saveGroup();
_isLoading = false;
notifyListeners(); notifyListeners();
} }
Future delGroup(PodcastGroup podcastGroup) async { Future delGroup(PodcastGroup podcastGroup) async {
_isLoading = true; _isLoading = true;
notifyListeners();
podcastGroup.podcastList.forEach((podcast) { podcastGroup.podcastList.forEach((podcast) {
if (!_groups.first.podcastList.contains(podcast)) { if (!_groups.first.podcastList.contains(podcast)) {
_groups[0].podcastList.insert(0, podcast); _groups[0].podcastList.insert(0, podcast);
@ -121,11 +137,11 @@ class GroupList extends ChangeNotifier {
_saveGroup(); _saveGroup();
_groups.remove(podcastGroup); _groups.remove(podcastGroup);
await _groups[0].getPodcasts(); await _groups[0].getPodcasts();
_isLoading = false; _isLoading = false;
notifyListeners(); notifyListeners();
} }
updateGroup(PodcastGroup podcastGroup) async{ updateGroup(PodcastGroup podcastGroup) async {
var oldGroup = _groups.firstWhere((it) => it.id == podcastGroup.id); var oldGroup = _groups.firstWhere((it) => it.id == podcastGroup.id);
var index = _groups.indexOf(oldGroup); var index = _groups.indexOf(oldGroup);
_groups.replaceRange(index, index + 1, [podcastGroup]); _groups.replaceRange(index, index + 1, [podcastGroup]);
@ -161,7 +177,11 @@ class GroupList extends ChangeNotifier {
_isLoading = true; _isLoading = true;
notifyListeners(); notifyListeners();
getPodcastGroup(id).forEach((group) { getPodcastGroup(id).forEach((group) {
group.podcastList.remove(id); if (list.contains(group)) {
list.remove(group);
} else {
group.podcastList.remove(id);
}
}); });
list.forEach((s) { list.forEach((s) {
s.podcastList.insert(0, id); s.podcastList.insert(0, id);
@ -190,8 +210,8 @@ class GroupList extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
saveOrder(PodcastGroup group, List<PodcastLocal> podcasts) async { saveOrder(PodcastGroup group) async {
group.podcastList = podcasts.map((e) => e.id).toList(); group.podcastList = group.ordereddPodcasts.map((e) => e.id).toList();
_saveGroup(); _saveGroup();
await group.getPodcasts(); await group.getPodcasts();
notifyListeners(); notifyListeners();

View File

@ -6,16 +6,17 @@ import 'package:tsacdop/local_storage/key_value_storage.dart';
class SettingState extends ChangeNotifier { class SettingState extends ChangeNotifier {
KeyValueStorage themestorage = KeyValueStorage('themes'); KeyValueStorage themestorage = KeyValueStorage('themes');
KeyValueStorage accentstorage = KeyValueStorage('accents'); KeyValueStorage accentstorage = KeyValueStorage('accents');
bool _isLoading; KeyValueStorage autoupdatestorage = KeyValueStorage('autoupdate');
bool get isLoagding => _isLoading;
Future initData() async { Future initData() async {
await _getTheme(); await _getTheme();
await _getAccentSetColor(); await _getAccentSetColor();
await _getAutoUpdate();
} }
ThemeMode _theme; ThemeMode _theme;
ThemeMode get theme => _theme; ThemeMode get theme => _theme;
set setTheme(ThemeMode mode) { set setTheme(ThemeMode mode) {
_theme = mode; _theme = mode;
_saveTheme(); _saveTheme();
@ -31,11 +32,20 @@ class SettingState extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
bool _autoUpdate;
bool get autoUpdate => _autoUpdate;
set autoUpdate(bool boo) {
_autoUpdate = boo;
_saveAutoUpdate();
notifyListeners();
}
@override @override
void addListener(VoidCallback listener) { void addListener(VoidCallback listener) {
super.addListener(listener); super.addListener(listener);
_getTheme(); _getTheme();
_getAccentSetColor(); _getAccentSetColor();
_getAutoUpdate();
} }
_getTheme() async { _getTheme() async {
@ -62,6 +72,14 @@ class SettingState extends ChangeNotifier {
_saveAccentSetColor() async { _saveAccentSetColor() async {
await accentstorage await accentstorage
.saveString(_accentSetColor.toString().substring(10, 16)); .saveString(_accentSetColor.toString().substring(10, 16));
print(_accentSetColor.toString()); }
_getAutoUpdate() async {
int i = await autoupdatestorage.getInt();
_autoUpdate = i == 0 ? false : true;
}
_saveAutoUpdate() async {
await autoupdatestorage.saveInt(_autoUpdate ? 1 : 0);
} }
} }

View File

@ -0,0 +1,8 @@
class SubHistory {
DateTime subDate;
DateTime delDate;
bool status;
String title;
String rssUrl;
SubHistory(this.status, this.delDate, this.subDate, this.rssUrl, this.title);
}

View File

@ -10,6 +10,7 @@ import 'package:url_launcher/url_launcher.dart';
import 'package:fluttertoast/fluttertoast.dart'; import 'package:fluttertoast/fluttertoast.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
import 'package:audio_service/audio_service.dart';
import 'package:tsacdop/class/audiostate.dart'; import 'package:tsacdop/class/audiostate.dart';
import 'package:tsacdop/class/episodebrief.dart'; import 'package:tsacdop/class/episodebrief.dart';
import 'package:tsacdop/local_storage/sqflite_localpodcast.dart'; import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
@ -211,7 +212,7 @@ class _EpisodeDetailState extends State<EpisodeDetail> {
], ],
), ),
), ),
Selector<AudioPlayer, bool>( Selector<AudioPlayerNotifier, bool>(
selector: (_, audio) => audio.playerRunning, selector: (_, audio) => audio.playerRunning,
builder: (_, data, __) { builder: (_, data, __) {
return Container( return Container(
@ -290,7 +291,7 @@ class _MenuBarState extends State<MenuBar> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var audio = Provider.of<AudioPlayer>(context, listen: false); var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
return Container( return Container(
height: 50.0, height: 50.0,
decoration: BoxDecoration( decoration: BoxDecoration(
@ -346,7 +347,7 @@ class _MenuBarState extends State<MenuBar> {
], ],
), ),
DownloadButton(episodeBrief: widget.episodeItem), DownloadButton(episodeBrief: widget.episodeItem),
Selector<AudioPlayer, List<String>>( Selector<AudioPlayerNotifier, List<String>>(
selector: (_, audio) => selector: (_, audio) =>
audio.queue.playlist.map((e) => e.enclosureUrl).toList(), audio.queue.playlist.map((e) => e.enclosureUrl).toList(),
builder: (_, data, __) { builder: (_, data, __) {
@ -367,9 +368,9 @@ class _MenuBarState extends State<MenuBar> {
), ),
Spacer(), Spacer(),
// Text(audio.audioState.toString()), // Text(audio.audioState.toString()),
Selector<AudioPlayer, Tuple2<EpisodeBrief, bool>>( Selector<AudioPlayerNotifier, Tuple2<EpisodeBrief, BasicPlaybackState>>(
selector: (_, audio) => selector: (_, audio) =>
Tuple2(audio.episode, audio.backgroundAudioPlaying), Tuple2(audio.episode, audio.audioState),
builder: (_, data, __) { builder: (_, data, __) {
return (widget.episodeItem.title != data.item1?.title) return (widget.episodeItem.title != data.item1?.title)
? Material( ? Material(
@ -400,7 +401,7 @@ class _MenuBarState extends State<MenuBar> {
), ),
) )
: (widget.episodeItem.title == data.item1?.title && : (widget.episodeItem.title == data.item1?.title &&
data.item2 == true) data.item2 == BasicPlaybackState.playing)
? Container( ? Container(
padding: EdgeInsets.only(right: 30), padding: EdgeInsets.only(right: 30),
child: SizedBox( child: SizedBox(
@ -424,9 +425,10 @@ class _MenuBarState extends State<MenuBar> {
class LinePainter extends CustomPainter { class LinePainter extends CustomPainter {
double _fraction; double _fraction;
Paint _paint; Paint _paint;
LinePainter(this._fraction) { Color _maincolor;
LinePainter(this._fraction, this._maincolor) {
_paint = Paint() _paint = Paint()
..color = Colors.blue ..color = _maincolor
..strokeWidth = 2.0 ..strokeWidth = 2.0
..strokeCap = StrokeCap.round; ..strokeCap = StrokeCap.round;
} }
@ -483,14 +485,15 @@ class _LineLoaderState extends State<LineLoader>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return CustomPaint(painter: LinePainter(_fraction)); return CustomPaint(painter: LinePainter(_fraction, Theme.of(context).accentColor));
} }
} }
class WavePainter extends CustomPainter { class WavePainter extends CustomPainter {
double _fraction; double _fraction;
double _value; double _value;
WavePainter(this._fraction); Color _color;
WavePainter(this._fraction, this._color);
@override @override
void paint(Canvas canvas, Size size) { void paint(Canvas canvas, Size size) {
if (_fraction < 0.5) { if (_fraction < 0.5) {
@ -500,7 +503,7 @@ class WavePainter extends CustomPainter {
} }
Path _path = Path(); Path _path = Path();
Paint _paint = Paint() Paint _paint = Paint()
..color = Colors.blue ..color = _color
..strokeWidth = 2.0 ..strokeWidth = 2.0
..strokeCap = StrokeCap.round ..strokeCap = StrokeCap.round
..style = PaintingStyle.stroke; ..style = PaintingStyle.stroke;
@ -575,7 +578,7 @@ class _WaveLoaderState extends State<WaveLoader>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return CustomPaint(painter: WavePainter(_fraction)); return CustomPaint(painter: WavePainter(_fraction, Theme.of(context).accentColor));
} }
} }

View File

@ -1,14 +1,17 @@
import 'dart:isolate'; import 'dart:isolate';
import 'dart:ui'; import 'dart:ui';
import 'dart:io'; import 'dart:io';
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'dart:async'; import 'package:provider/provider.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as path;
import 'package:flutter_downloader/flutter_downloader.dart'; import 'package:flutter_downloader/flutter_downloader.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:fluttertoast/fluttertoast.dart'; import 'package:fluttertoast/fluttertoast.dart';
import 'package:tsacdop/class/episodebrief.dart'; import 'package:tsacdop/class/episodebrief.dart';
import 'package:tsacdop/class/audiostate.dart';
import 'package:tsacdop/local_storage/sqflite_localpodcast.dart'; import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
class DownloadButton extends StatefulWidget { class DownloadButton extends StatefulWidget {
@ -74,6 +77,7 @@ class _DownloadButtonState extends State<DownloadButton> {
}); });
} }
void _unbindBackgroundIsolate() { void _unbindBackgroundIsolate() {
IsolateNameServer.removePortNameMapping('downloader_send_port'); IsolateNameServer.removePortNameMapping('downloader_send_port');
} }
@ -96,6 +100,7 @@ class _DownloadButtonState extends State<DownloadButton> {
openFileFromNotification: false, openFileFromNotification: false,
); );
var dbHelper = DBHelper(); var dbHelper = DBHelper();
await dbHelper.saveDownloaded(task.link, task.taskId); await dbHelper.saveDownloaded(task.link, task.taskId);
Fluttertoast.showToast( Fluttertoast.showToast(
msg: 'Downloading', msg: 'Downloading',
@ -103,6 +108,7 @@ class _DownloadButtonState extends State<DownloadButton> {
); );
} }
void _deleteDownload(_TaskInfo task) async { void _deleteDownload(_TaskInfo task) async {
await FlutterDownloader.remove( await FlutterDownloader.remove(
taskId: task.taskId, shouldDeleteContent: true); taskId: task.taskId, shouldDeleteContent: true);
@ -146,6 +152,16 @@ class _DownloadButtonState extends State<DownloadButton> {
); );
} }
_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);
await Provider.of<AudioPlayerNotifier>(context, listen: false).updateMediaItem(episode);
}
Future<Null> _prepare() async { Future<Null> _prepare() async {
final tasks = await FlutterDownloader.loadTasks(); final tasks = await FlutterDownloader.loadTasks();
@ -161,7 +177,7 @@ class _DownloadButtonState extends State<DownloadButton> {
} }
}); });
_localPath = (await _getPath()) + '/' + widget.episodeBrief.feedTitle; _localPath = path.join((await _getPath()) ,widget.episodeBrief.feedTitle);
print(_localPath); print(_localPath);
final saveDir = Directory(_localPath); final saveDir = Directory(_localPath);
bool hasExisted = await saveDir.exists(); bool hasExisted = await saveDir.exists();
@ -173,6 +189,8 @@ class _DownloadButtonState extends State<DownloadButton> {
}); });
} }
Future<bool> _checkPermmison() async { Future<bool> _checkPermmison() async {
PermissionStatus permission = await PermissionHandler() PermissionStatus permission = await PermissionHandler()
.checkPermissionStatus(PermissionGroup.storage); .checkPermissionStatus(PermissionGroup.storage);
@ -284,6 +302,7 @@ class _DownloadButtonState extends State<DownloadButton> {
), ),
); );
} else if (task.status == DownloadTaskStatus.complete) { } else if (task.status == DownloadTaskStatus.complete) {
_saveMediaId(task);
return _buttonOnMenu( return _buttonOnMenu(
Icon( Icon(
Icons.done_all, Icons.done_all,
@ -307,4 +326,4 @@ class _TaskInfo {
DownloadTaskStatus status = DownloadTaskStatus.undefined; DownloadTaskStatus status = DownloadTaskStatus.undefined;
_TaskInfo({this.name, this.link}); _TaskInfo({this.name, this.link});
} }

View File

@ -1,8 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:line_icons/line_icons.dart';
class AboutApp extends StatelessWidget { class AboutApp extends StatelessWidget {
_launchUrl(String url) async { _launchUrl(String url) async {
if (await canLaunch(url)) { if (await canLaunch(url)) {
@ -71,7 +70,7 @@ class AboutApp extends StatelessWidget {
image: AssetImage('assets/logo.png'), image: AssetImage('assets/logo.png'),
height: 80, height: 80,
), ),
Text('Version: 0.1.1'), Text('Version: 0.1.2'),
], ],
), ),
), ),
@ -79,7 +78,7 @@ class AboutApp extends StatelessWidget {
padding: EdgeInsets.symmetric(horizontal: 50), padding: EdgeInsets.symmetric(horizontal: 50),
height: 50, height: 50,
child: Text( child: Text(
'Tsacdop is a podcast client developed with flutter, a simple, beautiful, and easy-use player.', 'Tsacdop is a podcasts client developed influtter, a simple, beautiful, and easy-use player.',
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
), ),
@ -111,17 +110,17 @@ class AboutApp extends StatelessWidget {
_listItem( _listItem(
context, context,
'GitHub', 'GitHub',
FontAwesomeIcons.githubSquare, LineIcons.github,
'https://github.com/stonaga/tsacdop'), 'https://github.com/stonaga/'),
_listItem( _listItem(
context, context,
'Twitter', 'Twitter',
FontAwesomeIcons.twitterSquare, LineIcons.twitter,
'https://twitter.com'), 'https://twitter.com'),
_listItem( _listItem(
context, context,
'Gmail', 'Stone Gate',
FontAwesomeIcons.envelopeSquare, LineIcons.hat_cowboy_solid,
'mailto:<xijieyin@gmail.com>?subject=Tsacdop Feedback'), 'mailto:<xijieyin@gmail.com>?subject=Tsacdop Feedback'),
], ],
), ),

View File

@ -36,6 +36,7 @@ class _MyHomePageState extends State<MyHomePage> {
return AnnotatedRegion<SystemUiOverlayStyle>( return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle( value: SystemUiOverlayStyle(
statusBarIconBrightness: Theme.of(context).accentColorBrightness, statusBarIconBrightness: Theme.of(context).accentColorBrightness,
systemNavigationBarIconBrightness: Theme.of(context).accentColorBrightness,
systemNavigationBarColor: Theme.of(context).primaryColor, systemNavigationBarColor: Theme.of(context).primaryColor,
statusBarColor: Theme.of(context).primaryColor, statusBarColor: Theme.of(context).primaryColor,
), ),

View File

@ -13,6 +13,7 @@ import 'package:color_thief_flutter/color_thief_flutter.dart';
import 'package:image/image.dart' as img; import 'package:image/image.dart' as img;
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
import 'package:fluttertoast/fluttertoast.dart'; import 'package:fluttertoast/fluttertoast.dart';
import 'package:line_icons/line_icons.dart';
import 'package:tsacdop/class/podcast_group.dart'; import 'package:tsacdop/class/podcast_group.dart';
import 'package:tsacdop/settings/settting.dart'; import 'package:tsacdop/settings/settting.dart';
@ -142,7 +143,7 @@ class PopupMenu extends StatelessWidget {
void _saveOmpl(String path) async { void _saveOmpl(String path) async {
File file = File(path); File file = File(path);
String opml = file.readAsStringSync(); try{String opml = file.readAsStringSync();
var content = xml.parse(opml); var content = xml.parse(opml);
var total = content var total = content
@ -167,6 +168,15 @@ class PopupMenu extends StatelessWidget {
} }
} }
print('Import fisnished'); print('Import fisnished');
}}
catch(e){
print(e);
Fluttertoast.showToast(
msg: 'File error, Subscribe failed',
gravity: ToastGravity.TOP,
);
await Future.delayed(Duration(seconds: 5));
importOmpl.importState = ImportState.stop;
} }
} }
@ -195,7 +205,7 @@ class PopupMenu extends StatelessWidget {
padding: EdgeInsets.only(left: 10), padding: EdgeInsets.only(left: 10),
child: Row( child: Row(
children: <Widget>[ children: <Widget>[
Icon(Icons.refresh), Icon(LineIcons.cloud_download_alt_solid),
Padding(padding: EdgeInsets.symmetric(horizontal: 5.0),), Padding(padding: EdgeInsets.symmetric(horizontal: 5.0),),
Text('Refresh All'), Text('Refresh All'),
], ],
@ -208,7 +218,7 @@ class PopupMenu extends StatelessWidget {
padding: EdgeInsets.only(left: 10), padding: EdgeInsets.only(left: 10),
child: Row( child: Row(
children: <Widget>[ children: <Widget>[
Icon(Icons.attachment), Icon(LineIcons.paperclip_solid),
Padding(padding: EdgeInsets.symmetric(horizontal: 5.0),), Padding(padding: EdgeInsets.symmetric(horizontal: 5.0),),
Text('Import OMPL'), Text('Import OMPL'),
], ],
@ -226,7 +236,7 @@ class PopupMenu extends StatelessWidget {
padding: EdgeInsets.only(left: 10), padding: EdgeInsets.only(left: 10),
child: Row( child: Row(
children: <Widget>[ children: <Widget>[
Icon(Icons.swap_calls), Icon(LineIcons.cog_solid),
Padding(padding: EdgeInsets.symmetric(horizontal: 5.0),), Padding(padding: EdgeInsets.symmetric(horizontal: 5.0),),
Text('Settings'), Text('Settings'),
], ],
@ -239,7 +249,7 @@ class PopupMenu extends StatelessWidget {
padding: EdgeInsets.only(left: 10), padding: EdgeInsets.only(left: 10),
child: Row( child: Row(
children: <Widget>[ children: <Widget>[
Icon(Icons.info_outline), Icon(LineIcons.info_circle_solid),
Padding(padding: EdgeInsets.symmetric(horizontal: 5.0),), Padding(padding: EdgeInsets.symmetric(horizontal: 5.0),),
Text('About'), Text('About'),
], ],

View File

@ -7,6 +7,7 @@ import 'package:provider/provider.dart';
import 'package:marquee/marquee.dart'; import 'package:marquee/marquee.dart';
import 'package:fluttertoast/fluttertoast.dart'; import 'package:fluttertoast/fluttertoast.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
import 'package:audio_service/audio_service.dart';
import 'package:tsacdop/class/episodebrief.dart'; import 'package:tsacdop/class/episodebrief.dart';
import 'package:tsacdop/class/audiostate.dart'; import 'package:tsacdop/class/audiostate.dart';
@ -15,6 +16,97 @@ import 'package:tsacdop/home/audiopanel.dart';
import 'package:tsacdop/util/pageroute.dart'; import 'package:tsacdop/util/pageroute.dart';
import 'package:tsacdop/util/colorize.dart'; import 'package:tsacdop/util/colorize.dart';
class MyRoundSliderThumpShape extends SliderComponentShape {
const MyRoundSliderThumpShape({
this.enabledThumbRadius = 10.0,
this.disabledThumbRadius,
this.thumbCenterColor,
});
final Color thumbCenterColor;
/// The preferred radius of the round thumb shape when the slider is enabled.
///
/// If it is not provided, then the material default of 10 is used.
final double enabledThumbRadius;
/// The preferred radius of the round thumb shape when the slider is disabled.
///
/// If no disabledRadius is provided, then it is equal to the
/// [enabledThumbRadius]
final double disabledThumbRadius;
double get _disabledThumbRadius => disabledThumbRadius ?? enabledThumbRadius;
@override
Size getPreferredSize(bool isEnabled, bool isDiscrete) {
return Size.fromRadius(
isEnabled == true ? enabledThumbRadius : _disabledThumbRadius);
}
@override
void paint(
PaintingContext context,
Offset center, {
Animation<double> activationAnimation,
@required Animation<double> enableAnimation,
bool isDiscrete,
TextPainter labelPainter,
RenderBox parentBox,
@required SliderThemeData sliderTheme,
TextDirection textDirection,
double value,
}) {
assert(context != null);
assert(center != null);
assert(enableAnimation != null);
assert(sliderTheme != null);
assert(sliderTheme.disabledThumbColor != null);
assert(sliderTheme.thumbColor != null);
final Canvas canvas = context.canvas;
final Tween<double> radiusTween = Tween<double>(
begin: _disabledThumbRadius,
end: enabledThumbRadius,
);
final ColorTween colorTween = ColorTween(
begin: sliderTheme.disabledThumbColor,
end: sliderTheme.thumbColor,
);
canvas.drawCircle(
center,
radiusTween.evaluate(enableAnimation),
Paint()
..color = thumbCenterColor
..style = PaintingStyle.stroke
..strokeWidth = 2,
);
canvas.drawLine(
Offset(center.dx - 6, center.dy),
Offset(center.dx + 6, center.dy),
Paint()
..color = Colors.grey[300]
..style = PaintingStyle.fill
..strokeWidth = 2,
);
canvas.drawCircle(
center,
radiusTween.evaluate(enableAnimation) - 2,
Paint()
..color = colorTween.evaluate(enableAnimation)
..style = PaintingStyle.fill
..strokeWidth = 2,
);
canvas.drawLine(
Offset(center.dx - 5, center.dy - 2),
Offset(center.dx + 5, center.dy + 2),
Paint()
..color = Colors.transparent
..style = PaintingStyle.fill
..strokeWidth = 2,
);
}
}
class PlayerWidget extends StatefulWidget { class PlayerWidget extends StatefulWidget {
@override @override
_PlayerWidgetState createState() => _PlayerWidgetState(); _PlayerWidgetState createState() => _PlayerWidgetState();
@ -50,17 +142,17 @@ class _PlayerWidgetState extends State<PlayerWidget> {
_timeLeft = _minSelected; _timeLeft = _minSelected;
_timer = Timer.periodic(Duration(minutes: 1), (timer) { _timer = Timer.periodic(Duration(minutes: 1), (timer) {
setState(() { setState(() {
if(_timeLeft < 1){ if (_timeLeft < 1) {
_timer.cancel(); _timer.cancel();
} else{ } else {
_timeLeft = _timeLeft - 1; _timeLeft = _timeLeft - 1;
} }
}); });
}); });
} }
Widget _sleepTimer(BuildContext context) { Widget _sleepTimer(BuildContext context) {
var audio = Provider.of<AudioPlayer>(context); var audio = Provider.of<AudioPlayerNotifier>(context);
return Container( return Container(
height: 50, height: 50,
margin: EdgeInsets.all(10.0), margin: EdgeInsets.all(10.0),
@ -141,7 +233,7 @@ class _PlayerWidgetState extends State<PlayerWidget> {
} }
Widget _expandedPanel(BuildContext context) { Widget _expandedPanel(BuildContext context) {
var audio = Provider.of<AudioPlayer>(context, listen: false); var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
return Stack( return Stack(
children: <Widget>[ children: <Widget>[
Container( Container(
@ -156,7 +248,7 @@ class _PlayerWidgetState extends State<PlayerWidget> {
height: 80.0, height: 80.0,
padding: EdgeInsets.all(20), padding: EdgeInsets.all(20),
alignment: Alignment.center, alignment: Alignment.center,
child: Selector<AudioPlayer, String>( child: Selector<AudioPlayerNotifier, String>(
selector: (_, audio) => audio.episode.title, selector: (_, audio) => audio.episode.title,
builder: (_, title, __) { builder: (_, title, __) {
return Container( return Container(
@ -201,8 +293,12 @@ class _PlayerWidgetState extends State<PlayerWidget> {
}, },
), ),
), ),
Consumer<AudioPlayer>( Consumer<AudioPlayerNotifier>(
builder: (_, data, __) { builder: (_, data, __) {
Color _c =
(Theme.of(context).brightness == Brightness.light)
? data.episode.primaryColor.colorizedark()
: data.episode.primaryColor.colorizeLight();
return Column( return Column(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@ -215,10 +311,12 @@ class _PlayerWidgetState extends State<PlayerWidget> {
.accentColor .accentColor
.withOpacity(0.5), .withOpacity(0.5),
inactiveTrackColor: Colors.grey[300], inactiveTrackColor: Colors.grey[300],
trackHeight: 3.0, trackHeight: 2.0,
thumbColor: Theme.of(context).accentColor, thumbColor: Theme.of(context).accentColor,
thumbShape: RoundSliderThumbShape( thumbShape: MyRoundSliderThumpShape(
enabledThumbRadius: 6.0), enabledThumbRadius: 5.0,
disabledThumbRadius: 5.0,
thumbCenterColor: _c),
overlayColor: overlayColor:
Theme.of(context).accentColor.withAlpha(32), Theme.of(context).accentColor.withAlpha(32),
overlayShape: overlayShape:
@ -238,7 +336,7 @@ class _PlayerWidgetState extends State<PlayerWidget> {
children: <Widget>[ children: <Widget>[
Text( Text(
_stringForSeconds( _stringForSeconds(
data.backgroundAudioPosition) ?? data.backgroundAudioPosition / 1000) ??
'', '',
style: TextStyle(fontSize: 10), style: TextStyle(fontSize: 10),
), ),
@ -250,7 +348,12 @@ class _PlayerWidgetState extends State<PlayerWidget> {
style: const TextStyle( style: const TextStyle(
color: const Color(0xFFFF0000))) color: const Color(0xFFFF0000)))
: Text( : Text(
data.remoteAudioLoading data.audioState ==
BasicPlaybackState
.buffering ||
data.audioState ==
BasicPlaybackState
.connecting
? 'Buffring...' ? 'Buffring...'
: '', : '',
style: TextStyle( style: TextStyle(
@ -261,7 +364,7 @@ class _PlayerWidgetState extends State<PlayerWidget> {
), ),
Text( Text(
_stringForSeconds( _stringForSeconds(
data.backgroundAudioDuration) ?? data.backgroundAudioDuration / 1000) ??
'', '',
style: TextStyle(fontSize: 10), style: TextStyle(fontSize: 10),
), ),
@ -274,8 +377,8 @@ class _PlayerWidgetState extends State<PlayerWidget> {
), ),
Container( Container(
height: 100, height: 100,
child: Selector<AudioPlayer, bool>( child: Selector<AudioPlayerNotifier, BasicPlaybackState>(
selector: (_, audio) => audio.backgroundAudioPlaying, selector: (_, audio) => audio.audioState,
builder: (_, backplay, __) { builder: (_, backplay, __) {
return Material( return Material(
color: Colors.transparent, color: Colors.transparent,
@ -285,22 +388,24 @@ class _PlayerWidgetState extends State<PlayerWidget> {
children: [ children: [
IconButton( IconButton(
padding: EdgeInsets.symmetric(horizontal: 30.0), padding: EdgeInsets.symmetric(horizontal: 30.0),
onPressed: backplay onPressed:
? () => audio.forwardAudio(-10) backplay == BasicPlaybackState.playing
: null, ? () => audio.forwardAudio(-10)
: null,
iconSize: 32.0, iconSize: 32.0,
icon: Icon(Icons.replay_10), icon: Icon(Icons.replay_10),
color: color:
Theme.of(context).tabBarTheme.labelColor), Theme.of(context).tabBarTheme.labelColor),
backplay backplay == BasicPlaybackState.playing
? IconButton( ? IconButton(
padding: padding:
EdgeInsets.symmetric(horizontal: 30.0), EdgeInsets.symmetric(horizontal: 30.0),
onPressed: backplay onPressed:
? () { backplay == BasicPlaybackState.playing
audio.pauseAduio(); ? () {
} audio.pauseAduio();
: null, }
: null,
iconSize: 40.0, iconSize: 40.0,
icon: Icon(Icons.pause_circle_filled), icon: Icon(Icons.pause_circle_filled),
color: Theme.of(context) color: Theme.of(context)
@ -309,11 +414,12 @@ class _PlayerWidgetState extends State<PlayerWidget> {
: IconButton( : IconButton(
padding: padding:
EdgeInsets.symmetric(horizontal: 30.0), EdgeInsets.symmetric(horizontal: 30.0),
onPressed: backplay onPressed:
? null backplay == BasicPlaybackState.playing
: () { ? null
audio.resumeAudio(); : () {
}, audio.resumeAudio();
},
iconSize: 40.0, iconSize: 40.0,
icon: Icon(Icons.play_circle_filled), icon: Icon(Icons.play_circle_filled),
color: Theme.of(context) color: Theme.of(context)
@ -321,9 +427,10 @@ class _PlayerWidgetState extends State<PlayerWidget> {
.labelColor), .labelColor),
IconButton( IconButton(
padding: EdgeInsets.symmetric(horizontal: 30.0), padding: EdgeInsets.symmetric(horizontal: 30.0),
onPressed: backplay onPressed:
? () => audio.forwardAudio(30) backplay == BasicPlaybackState.playing
: null, ? () => audio.forwardAudio(30)
: null,
iconSize: 32.0, iconSize: 32.0,
icon: Icon(Icons.forward_30), icon: Icon(Icons.forward_30),
color: color:
@ -344,7 +451,7 @@ class _PlayerWidgetState extends State<PlayerWidget> {
color: Theme.of(context).scaffoldBackgroundColor, color: Theme.of(context).scaffoldBackgroundColor,
borderRadius: BorderRadius.all(Radius.circular(10.0)), borderRadius: BorderRadius.all(Radius.circular(10.0)),
), ),
child: Selector<AudioPlayer, child: Selector<AudioPlayerNotifier,
Tuple3<EpisodeBrief, bool, bool>>( Tuple3<EpisodeBrief, bool, bool>>(
selector: (_, audio) => Tuple3(audio.episode, selector: (_, audio) => Tuple3(audio.episode,
audio.stopOnComplete, audio.showStopWatch), audio.stopOnComplete, audio.showStopWatch),
@ -394,6 +501,7 @@ class _PlayerWidgetState extends State<PlayerWidget> {
], ],
onSelected: (value) { onSelected: (value) {
if (value == 1) { if (value == 1) {
audio.sleepTimer(_minSelected);
audio.setStopOnComplete = true; audio.setStopOnComplete = true;
} else if (value == 2) { } else if (value == 2) {
setState(() => _showTimer = true); setState(() => _showTimer = true);
@ -414,8 +522,7 @@ class _PlayerWidgetState extends State<PlayerWidget> {
color: color:
Theme.of(context).accentColor, Theme.of(context).accentColor,
), ),
child: Text( child: Text(_timeLeft.toString(),
_timeLeft.toString(),
style: TextStyle( style: TextStyle(
color: Colors.white)), color: Colors.white)),
), ),
@ -475,7 +582,7 @@ class _PlayerWidgetState extends State<PlayerWidget> {
// margin: EdgeInsets.all(20), // margin: EdgeInsets.all(20),
//padding: EdgeInsets.only(bottom: 10.0), //padding: EdgeInsets.only(bottom: 10.0),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(10.0)), // borderRadius: BorderRadius.all(Radius.circular(10.0)),
color: Theme.of(context).scaffoldBackgroundColor, color: Theme.of(context).scaffoldBackgroundColor,
), ),
child: Column( child: Column(
@ -511,7 +618,7 @@ class _PlayerWidgetState extends State<PlayerWidget> {
), ),
), ),
Expanded( Expanded(
child: Selector<AudioPlayer, List<EpisodeBrief>>( child: Selector<AudioPlayerNotifier, List<EpisodeBrief>>(
selector: (_, audio) => audio.queue.playlist, selector: (_, audio) => audio.queue.playlist,
builder: (_, playlist, __) { builder: (_, playlist, __) {
return ListView.builder( return ListView.builder(
@ -623,7 +730,7 @@ class _PlayerWidgetState extends State<PlayerWidget> {
} }
Widget _miniPanel(double width, BuildContext context) { Widget _miniPanel(double width, BuildContext context) {
var audio = Provider.of<AudioPlayer>(context, listen: false); var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
return Container( return Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).primaryColor, color: Theme.of(context).primaryColor,
@ -631,7 +738,7 @@ class _PlayerWidgetState extends State<PlayerWidget> {
height: 60, height: 60,
child: child:
Column(mainAxisAlignment: MainAxisAlignment.start, children: <Widget>[ Column(mainAxisAlignment: MainAxisAlignment.start, children: <Widget>[
Selector<AudioPlayer, Tuple2<String, double>>( Selector<AudioPlayerNotifier, Tuple2<String, double>>(
selector: (_, audio) => selector: (_, audio) =>
Tuple2(audio.episode?.primaryColor, audio.seekSliderValue), Tuple2(audio.episode?.primaryColor, audio.seekSliderValue),
builder: (_, data, __) { builder: (_, data, __) {
@ -657,58 +764,33 @@ class _PlayerWidgetState extends State<PlayerWidget> {
children: <Widget>[ children: <Widget>[
Expanded( Expanded(
flex: 4, flex: 4,
child: Selector<AudioPlayer, String>( child: Selector<AudioPlayerNotifier, String>(
selector: (_, audio) => audio.episode.title, selector: (_, audio) => audio.episode.title,
builder: (_, title, __) { builder: (_, title, __) {
return LayoutBuilder( return Text(
builder: (context, size) { title,
var span = TextSpan( style: TextStyle(fontWeight: FontWeight.bold),
text: title, maxLines: 2,
style: TextStyle(fontWeight: FontWeight.bold), overflow: TextOverflow.clip,
);
var tp = TextPainter(
text: span,
maxLines: 2,
textDirection: TextDirection.ltr);
tp.layout(maxWidth: size.maxWidth);
if (tp.didExceedMaxLines) {
return Marquee(
text: title,
style: TextStyle(fontWeight: FontWeight.bold),
scrollAxis: Axis.vertical,
crossAxisAlignment: CrossAxisAlignment.start,
blankSpace: 30.0,
velocity: 50.0,
pauseAfterRound: Duration(seconds: 1),
startPadding: 30.0,
accelerationDuration: Duration(seconds: 1),
accelerationCurve: Curves.linear,
decelerationDuration: Duration(milliseconds: 500),
decelerationCurve: Curves.easeOut,
);
} else {
return Text(
title,
style: TextStyle(fontWeight: FontWeight.bold),
);
}
},
); );
}, },
), ),
), ),
Expanded( Expanded(
flex: 2, flex: 2,
child: Selector<AudioPlayer, Tuple2<bool, double>>( child: Selector<AudioPlayerNotifier,
Tuple2<BasicPlaybackState, double>>(
selector: (_, audio) => Tuple2( selector: (_, audio) => Tuple2(
audio.remoteAudioLoading, audio.audioState,
(audio.backgroundAudioDuration - (audio.backgroundAudioDuration -
audio.backgroundAudioPosition)), audio.backgroundAudioPosition) /
1000),
builder: (_, data, __) { builder: (_, data, __) {
return Container( return Container(
padding: EdgeInsets.symmetric(horizontal: 10), padding: EdgeInsets.symmetric(horizontal: 10),
alignment: Alignment.center, alignment: Alignment.center,
child: data.item1 child: data.item1 == BasicPlaybackState.buffering ||
data.item1 == BasicPlaybackState.connecting
? Text( ? Text(
'Buffring...', 'Buffring...',
style: TextStyle( style: TextStyle(
@ -730,30 +812,32 @@ class _PlayerWidgetState extends State<PlayerWidget> {
), ),
Expanded( Expanded(
flex: 2, flex: 2,
child: Selector<AudioPlayer, bool>( child: Selector<AudioPlayerNotifier, BasicPlaybackState>(
selector: (_, audio) => audio.backgroundAudioPlaying, selector: (_, audio) => audio.audioState,
builder: (_, audioplay, __) { builder: (_, audioplay, __) {
return Row( return Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ children: [
audioplay audioplay == BasicPlaybackState.playing
? InkWell( ? InkWell(
onTap: audioplay onTap:
? () { audioplay == BasicPlaybackState.playing
audio.pauseAduio(); ? () {
} audio.pauseAduio();
: null, }
: null,
child: ImageRotate( child: ImageRotate(
title: audio.episode.title, title: audio.episode.title,
path: audio.episode.imagePath), path: audio.episode.imagePath),
) )
: InkWell( : InkWell(
onTap: audioplay onTap:
? null audioplay == BasicPlaybackState.playing
: () { ? null
audio.resumeAudio(); : () {
}, audio.resumeAudio();
},
child: Stack( child: Stack(
alignment: Alignment.center, alignment: Alignment.center,
children: <Widget>[ children: <Widget>[
@ -781,11 +865,10 @@ class _PlayerWidgetState extends State<PlayerWidget> {
), ),
), ),
IconButton( IconButton(
onPressed: audioplay onPressed:
? () => audio.forwardAudio(30) () => audio.playNext(),
: null,
iconSize: 25.0, iconSize: 25.0,
icon: Icon(Icons.forward_30), icon: Icon(Icons.skip_next),
color: color:
Theme.of(context).tabBarTheme.labelColor), Theme.of(context).tabBarTheme.labelColor),
], ],
@ -803,7 +886,7 @@ class _PlayerWidgetState extends State<PlayerWidget> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
double _width = MediaQuery.of(context).size.width; double _width = MediaQuery.of(context).size.width;
return Selector<AudioPlayer, bool>( return Selector<AudioPlayerNotifier, bool>(
selector: (_, audio) => audio.playerRunning, selector: (_, audio) => audio.playerRunning,
builder: (_, playerrunning, __) { builder: (_, playerrunning, __) {
return !playerrunning return !playerrunning

View File

@ -31,7 +31,7 @@ class _HomeState extends State<Home> {
} }
_getPlaylist() async { _getPlaylist() async {
await Provider.of<AudioPlayer>(context, listen: false).loadPlaylist(); await Provider.of<AudioPlayerNotifier>(context, listen: false).loadPlaylist();
setState(() { setState(() {
_loadPlay = true; _loadPlay = true;
}); });
@ -39,7 +39,7 @@ class _HomeState extends State<Home> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var audio = Provider.of<AudioPlayer>(context, listen: false); var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
return Stack(children: <Widget>[ return Stack(children: <Widget>[
Column( Column(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
@ -58,7 +58,7 @@ class _HomeState extends State<Home> {
bottom: 50, bottom: 50,
right: _loadPlay ? 5 : -25, right: _loadPlay ? 5 : -25,
child: Container( child: Container(
child: Selector<AudioPlayer, Tuple3<bool, Playlist, int>>( child: Selector<AudioPlayerNotifier, Tuple3<bool, Playlist, int>>(
selector: (_, audio) => selector: (_, audio) =>
Tuple3(audio.playerRunning, audio.queue, audio.lastPositin), Tuple3(audio.playerRunning, audio.queue, audio.lastPositin),
builder: (_, data, __) => !_loadPlay builder: (_, data, __) => !_loadPlay
@ -90,7 +90,7 @@ class _HomeState extends State<Home> {
offset: Offset(1, 1)), offset: Offset(1, 1)),
]), ]),
height: 40, height: 40,
child: Text(_stringForSeconds(data.item3) + '...', child: Text(_stringForSeconds(data.item3~/1000) + '...',
style: TextStyle(color: Colors.white)), style: TextStyle(color: Colors.white)),
), ),
CircleAvatar( CircleAvatar(

View File

@ -5,7 +5,9 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:fluttertoast/fluttertoast.dart'; import 'package:fluttertoast/fluttertoast.dart';
import 'package:tsacdop/class/audiostate.dart';
import 'package:tuple/tuple.dart';
import 'package:line_icons/line_icons.dart';
import 'package:tsacdop/class/episodebrief.dart'; import 'package:tsacdop/class/episodebrief.dart';
import 'package:tsacdop/class/importompl.dart'; import 'package:tsacdop/class/importompl.dart';
import 'package:tsacdop/class/podcast_group.dart'; import 'package:tsacdop/class/podcast_group.dart';
@ -89,7 +91,9 @@ class _ScrollPodcastsState extends State<ScrollPodcasts> {
style: Theme.of(context) style: Theme.of(context)
.textTheme .textTheme
.bodyText1 .bodyText1
.copyWith(color: Colors.red[300]), .copyWith(
color: Theme.of(context)
.accentColor),
)), )),
Spacer(), Spacer(),
Container( Container(
@ -184,7 +188,9 @@ class _ScrollPodcastsState extends State<ScrollPodcasts> {
style: Theme.of(context) style: Theme.of(context)
.textTheme .textTheme
.bodyText1 .bodyText1
.copyWith(color: Colors.red[300]), .copyWith(
color: Theme.of(context)
.accentColor),
)), )),
Spacer(), Spacer(),
Container( Container(
@ -374,43 +380,78 @@ class ShowEpisode extends StatelessWidget {
final List<EpisodeBrief> podcast; final List<EpisodeBrief> podcast;
final PodcastLocal podcastLocal; final PodcastLocal podcastLocal;
ShowEpisode({Key key, this.podcast, this.podcastLocal}) : super(key: key); ShowEpisode({Key key, this.podcast, this.podcastLocal}) : super(key: key);
Offset offset;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
double _width = MediaQuery.of(context).size.width; double _width = MediaQuery.of(context).size.width;
_showPopupMenu(Offset offset) async { _showPopupMenu(Offset offset, EpisodeBrief episode, BuildContext context,
print(offset.dx); bool isPlaying, bool isInPlaylist) async {
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
double left = offset.dx; double left = offset.dx;
double top = offset.dy; double top = offset.dy;
await showMenu( await showMenu<int>(
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(10))), borderRadius: BorderRadius.all(Radius.circular(10))),
context: context, context: context,
position: RelativeRect.fromLTRB(left, top, _width - left, 0), position: RelativeRect.fromLTRB(left, top, _width - left, 0),
items: [ items: <PopupMenuEntry<int>>[
PopupMenuItem( PopupMenuItem(
value: 0,
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
children: <Widget>[ children: <Widget>[
Icon(Icons.play_circle_outline), Icon(
Padding(padding: EdgeInsets.symmetric(horizontal: 2),), LineIcons.play_circle_solid,
Text('Play') color: Theme.of(context).accentColor,
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 2),
),
!isPlaying ? Text('Play') : Text('Playing'),
], ],
), ),
), ),
PopupMenuItem(child: Row( PopupMenuItem(
children: <Widget>[ value: 1,
Icon(Icons.favorite_border), child: Row(
Padding(padding: EdgeInsets.symmetric(horizontal: 2),), children: <Widget>[
Text('Like') Icon(
], LineIcons.clock_solid,
)), color: Colors.red,
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 2),
),
!isInPlaylist ? Text('Later') : Text('Remove')
],
)),
], ],
elevation: 8.0, elevation: 5.0,
); ).then((value) {
if (value == 0) {
if (!isPlaying) audio.episodeLoad(episode);
} else if (value == 1) {
if (isInPlaylist) {
audio.addToPlaylist(episode);
Fluttertoast.showToast(
msg: 'Added to playlist',
gravity: ToastGravity.BOTTOM,
);
} else {
audio.delFromPlaylist(episode);
Fluttertoast.showToast(
msg: 'Removed from playlist',
gravity: ToastGravity.BOTTOM,
);
}
}
});
} }
return CustomScrollView( return CustomScrollView(
physics: const AlwaysScrollableScrollPhysics(), // physics: const AlwaysScrollableScrollPhysics(),
physics: ClampingScrollPhysics(),
primary: false, primary: false,
slivers: <Widget>[ slivers: <Widget>[
SliverPadding( SliverPadding(
@ -427,88 +468,110 @@ class ShowEpisode extends StatelessWidget {
Color _c = (Theme.of(context).brightness == Brightness.light) Color _c = (Theme.of(context).brightness == Brightness.light)
? podcastLocal.primaryColor.colorizedark() ? podcastLocal.primaryColor.colorizedark()
: podcastLocal.primaryColor.colorizeLight(); : podcastLocal.primaryColor.colorizeLight();
return GestureDetector( return Selector<AudioPlayerNotifier,
onLongPressStart: (details) => _showPopupMenu(Offset( Tuple2<EpisodeBrief, List<String>>>(
details.globalPosition.dx, details.globalPosition.dy)), selector: (_, audio) => Tuple2(
onTap: () { audio?.episode,
Navigator.push( audio.queue.playlist.map((e) => e.enclosureUrl).toList(),
context, ),
ScaleRoute( builder: (_, data, __) => Container(
page: EpisodeDetail(
episodeItem: podcast[index],
heroTag: 'scroll',
//unique hero tag
)),
);
},
child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(5.0)), borderRadius: BorderRadius.all(Radius.circular(5.0)),
color: Theme.of(context).scaffoldBackgroundColor, color: Theme.of(context).scaffoldBackgroundColor,
border: Border.all(
color: Theme.of(context).brightness == Brightness.light
? Theme.of(context).primaryColor
: Theme.of(context).scaffoldBackgroundColor,
// color: Theme.of(context).primaryColor,
width: 3.0,
),
), ),
alignment: Alignment.center, alignment: Alignment.center,
padding: EdgeInsets.all(10.0), child: Material(
child: Column( color: Colors.transparent,
mainAxisAlignment: MainAxisAlignment.center, child: InkWell(
children: <Widget>[ borderRadius: BorderRadius.all(Radius.circular(5.0)),
Expanded( onTapDown: (details) => offset = Offset(
flex: 2, details.globalPosition.dx,
child: Row( details.globalPosition.dy),
mainAxisAlignment: MainAxisAlignment.start, onLongPress: () => _showPopupMenu(
offset,
podcast[index],
context,
data.item1 == podcast[index],
data.item2.contains(podcast[index].enclosureUrl)),
onTap: () {
Navigator.push(
context,
ScaleRoute(
page: EpisodeDetail(
episodeItem: podcast[index],
heroTag: 'scroll',
//unique hero tag
)),
);
},
child: Container(
// decoration: BoxDecoration(
// border: Border.all(
// color: Theme.of(context).brightness ==
// Brightness.light
// ? Theme.of(context).primaryColor
// : Theme.of(context).scaffoldBackgroundColor,
// width: 0.0,
// ),
// ),
padding: EdgeInsets.all(10.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[ children: <Widget>[
Hero( Expanded(
tag: podcast[index].enclosureUrl + 'scroll', flex: 2,
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Hero(
tag: podcast[index].enclosureUrl +
'scroll',
child: Container(
height: _width / 18,
width: _width / 18,
child: CircleAvatar(
backgroundImage: FileImage(File(
"${podcastLocal.imagePath}")),
),
),
),
Spacer(),
],
),
),
Expanded(
flex: 5,
child: Container( child: Container(
height: _width / 18, padding: EdgeInsets.only(top: 2.0),
width: _width / 18, alignment: Alignment.topLeft,
child: CircleAvatar( child: Text(
backgroundImage: FileImage( podcast[index].title,
File("${podcastLocal.imagePath}")), style: TextStyle(
fontSize: _width / 32,
),
maxLines: 4,
overflow: TextOverflow.fade,
), ),
), ),
), ),
Spacer(), Expanded(
flex: 1,
child: Container(
alignment: Alignment.bottomLeft,
child: Text(
podcast[index].dateToString(),
//podcast[index].pubDate.substring(4, 16),
style: TextStyle(
fontSize: _width / 35,
color: _c,
fontStyle: FontStyle.italic,
),
),
)),
], ],
), ),
), ),
Expanded( ),
flex: 5,
child: Container(
padding: EdgeInsets.only(top: 2.0),
alignment: Alignment.topLeft,
child: Text(
podcast[index].title,
style: TextStyle(
fontSize: _width / 32,
),
maxLines: 4,
overflow: TextOverflow.fade,
),
),
),
Expanded(
flex: 1,
child: Container(
alignment: Alignment.bottomLeft,
child: Text(
podcast[index].dateToString(),
//podcast[index].pubDate.substring(4, 16),
style: TextStyle(
fontSize: _width / 35,
color: _c,
fontStyle: FontStyle.italic,
),
),
),
),
],
), ),
), ),
); );

View File

@ -2,7 +2,7 @@ import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:tsacdop/class/episodebrief.dart'; import 'package:tsacdop/class/episodebrief.dart';
import 'package:tsacdop/home/paly_history.dart'; import 'package:tsacdop/settings/history.dart';
import 'package:tsacdop/local_storage/sqflite_localpodcast.dart'; import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
import 'package:tsacdop/util/episodegrid.dart'; import 'package:tsacdop/util/episodegrid.dart';
@ -49,8 +49,8 @@ class _MainTabState extends State<MainTab> with TickerProviderStateMixin {
], ],
onSelected: (value) { onSelected: (value) {
if (value == 0) { if (value == 0) {
Navigator.push( Navigator.push(context,
context, MaterialPageRoute(builder: (context) => PlayedHistory())); MaterialPageRoute(builder: (context) => PlayedHistory()));
} }
}, },
); );
@ -81,6 +81,7 @@ class _MainTabState extends State<MainTab> with TickerProviderStateMixin {
height: 50, height: 50,
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: TabBar( child: TabBar(
indicatorSize: TabBarIndicatorSize.tab,
isScrollable: true, isScrollable: true,
labelPadding: EdgeInsets.all(10.0), labelPadding: EdgeInsets.all(10.0),
controller: _controller, controller: _controller,
@ -134,26 +135,73 @@ class RecentUpdate extends StatefulWidget {
} }
class _RecentUpdateState extends State<RecentUpdate> { class _RecentUpdateState extends State<RecentUpdate> {
Future<List<EpisodeBrief>> _getRssItem() async { Future<List<EpisodeBrief>> _getRssItem(int top) async {
var dbHelper = DBHelper(); var dbHelper = DBHelper();
List<EpisodeBrief> episodes = await dbHelper.getRecentRssItem(); List<EpisodeBrief> episodes = await dbHelper.getRecentRssItem(top);
return episodes; return episodes;
} }
ScrollController _controller;
int _top;
bool _loadMore;
_scrollListener() async {
if (_controller.offset == _controller.position.maxScrollExtent) {
if (mounted) setState(() => _loadMore = true);
await Future.delayed(Duration(seconds: 3));
if (mounted)
setState(() {
_top = _top + 33;
_loadMore = false;
});
}
}
@override
void initState() {
super.initState();
_loadMore = false;
_top = 33;
_controller = ScrollController();
_controller.addListener(_scrollListener);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return FutureBuilder<List<EpisodeBrief>>( return FutureBuilder<List<EpisodeBrief>>(
future: _getRssItem(), future: _getRssItem(_top),
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.hasError) print(snapshot.error); if (snapshot.hasError) print(snapshot.error);
return (snapshot.hasData) return (snapshot.hasData)
? EpisodeGrid( ? CustomScrollView(
podcast: snapshot.data, controller: _controller,
showDownload: false, physics: const AlwaysScrollableScrollPhysics(),
showFavorite: false, primary: false,
showNumber: false, slivers: <Widget>[
heroTag: 'recent', EpisodeGrid(
) podcast: snapshot.data,
showDownload: false,
showFavorite: false,
showNumber: false,
heroTag: 'recent',
),
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return _loadMore
? Container(
height: 2, child: LinearProgressIndicator())
: Center();
},
childCount: 1,
),
),
])
: Center(child: CircularProgressIndicator()); : Center(child: CircularProgressIndicator());
}, },
); );
@ -179,12 +227,18 @@ class _MyFavoriteState extends State<MyFavorite> {
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.hasError) print(snapshot.error); if (snapshot.hasError) print(snapshot.error);
return (snapshot.hasData) return (snapshot.hasData)
? EpisodeGrid( ? CustomScrollView(
podcast: snapshot.data, physics: const AlwaysScrollableScrollPhysics(),
showDownload: false, primary: false,
showFavorite: false, slivers: <Widget>[
showNumber: false, EpisodeGrid(
heroTag: 'favorite', podcast: snapshot.data,
showDownload: false,
showFavorite: false,
showNumber: false,
heroTag: 'favorite',
)
],
) )
: Center(child: CircularProgressIndicator()); : Center(child: CircularProgressIndicator());
}, },
@ -211,12 +265,19 @@ class _MyDownloadState extends State<MyDownload> {
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.hasError) print(snapshot.error); if (snapshot.hasError) print(snapshot.error);
return (snapshot.hasData) return (snapshot.hasData)
? EpisodeGrid( ? CustomScrollView(
podcast: snapshot.data, physics: const AlwaysScrollableScrollPhysics(),
showDownload: true, primary: false,
showFavorite: false, slivers: <Widget>[
showNumber: false, EpisodeGrid(
heroTag: 'download', podcast: snapshot.data,
showDownload: true,
showFavorite: false,
showNumber: false,
heroTag: 'download',
)
],
) )
: Center(child: CircularProgressIndicator()); : Center(child: CircularProgressIndicator());
}, },

View File

@ -1,73 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
import 'package:tsacdop/class/audiostate.dart';
class PlayedHistory extends StatefulWidget{
@override
_PlayedHistoryState createState() => _PlayedHistoryState();
}
class _PlayedHistoryState extends State<PlayedHistory> {
Future<List<PlayHistory>> gerPlayHistory() async{
DBHelper dbHelper = DBHelper();
List<PlayHistory> playHistory;
playHistory = await dbHelper.getPlayHistory();
await Future.forEach(playHistory, (playHistory) async{
await playHistory.getEpisode();
});
return playHistory;
}
static String _stringForSeconds(double seconds) {
if (seconds == null) return null;
return '${(seconds ~/ 60)}:${(seconds.truncate() % 60).toString().padLeft(2, '0')}';
}
@override
Widget build(BuildContext context) {
return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle(
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
systemNavigationBarColor: Theme.of(context).primaryColor,
statusBarColor: Theme.of(context).primaryColor,
),
child: SafeArea(
child: Scaffold(
appBar: AppBar(
title: Text('History'),
centerTitle: true,
elevation: 0,
backgroundColor: Theme.of(context).primaryColor,
),
body: FutureBuilder<List<PlayHistory>>(
future: gerPlayHistory(),
builder: (context, snapshot) {
return
snapshot.hasData ?
ListView.builder(
shrinkWrap: true,
scrollDirection: Axis.vertical,
itemCount: snapshot.data.length,
itemBuilder: (BuildContext context, int index){
return Column(
children: <Widget>[
ListTile(
title: Text(snapshot.data[index].title),
subtitle: Text(_stringForSeconds(snapshot.data[index].seconds)),
),
Divider(height: 2),
],
);
}
)
: Center(
child: CircularProgressIndicator(),
);
},
),
),
),
);
}
}

View File

@ -43,7 +43,7 @@ class KeyValueStorage {
return prefs.getInt(key); return prefs.getInt(key);
} }
Future<bool> saveStringlist(List<String> playList) async{ Future<bool> saveStringList(List<String> playList) async{
SharedPreferences prefs = await SharedPreferences.getInstance(); SharedPreferences prefs = await SharedPreferences.getInstance();
return prefs.setStringList(key, playList); return prefs.setStringList(key, playList);
} }

View File

@ -8,6 +8,7 @@ import 'package:tsacdop/class/podcastlocal.dart';
import 'package:tsacdop/class/audiostate.dart'; import 'package:tsacdop/class/audiostate.dart';
import 'package:tsacdop/class/episodebrief.dart'; import 'package:tsacdop/class/episodebrief.dart';
import 'package:tsacdop/webfeed/webfeed.dart'; import 'package:tsacdop/webfeed/webfeed.dart';
import 'package:tsacdop/class/sub_history.dart';
class DBHelper { class DBHelper {
static Database _db; static Database _db;
@ -35,10 +36,13 @@ class DBHelper {
enclosure_url TEXT UNIQUE, enclosure_length INTEGER, pubDate TEXT, enclosure_url TEXT UNIQUE, enclosure_length INTEGER, pubDate TEXT,
description TEXT, feed_id TEXT, feed_link TEXT, milliseconds INTEGER, description TEXT, feed_id TEXT, feed_link TEXT, milliseconds INTEGER,
duration INTEGER DEFAULT 0, explicit INTEGER DEFAULT 0, liked INTEGER DEFAULT 0, duration INTEGER DEFAULT 0, explicit INTEGER DEFAULT 0, liked INTEGER DEFAULT 0,
downloaded TEXT DEFAULT 'ND', download_date INTEGER DEFAULT 0)"""); downloaded TEXT DEFAULT 'ND', download_date INTEGER DEFAULT 0, media_id TEXT)""");
await db.execute( await db.execute(
"""CREATE TABLE PlayHistory(id INTEGER PRIMARY KEY, title TEXT, enclosure_url TEXT UNIQUE, """CREATE TABLE PlayHistory(id INTEGER PRIMARY KEY, title TEXT, enclosure_url TEXT UNIQUE,
seconds REAL, seek_value REAL, add_date INTEGER)"""); seconds REAL, seek_value REAL, add_date INTEGER)""");
await db.execute(
"""CREATE TABLE SubscribeHistory(id TEXT PRIMARY KEY, title TEXT, rss_url TEXT UNIQUE,
add_date INTEGER, remove_date INTEGER DEFAULT 0, status INTEGER DEFAULT 0)""");
} }
Future<List<PodcastLocal>> getPodcastLocal(List<String> podcasts) async { Future<List<PodcastLocal>> getPodcastLocal(List<String> podcasts) async {
@ -97,7 +101,7 @@ class DBHelper {
int _milliseconds = DateTime.now().millisecondsSinceEpoch; int _milliseconds = DateTime.now().millisecondsSinceEpoch;
var dbClient = await database; var dbClient = await database;
await dbClient.transaction((txn) async { await dbClient.transaction((txn) async {
return await txn.rawInsert( await txn.rawInsert(
"""INSERT OR IGNORE INTO PodcastLocal (id, title, imageUrl, rssUrl, """INSERT OR IGNORE INTO PodcastLocal (id, title, imageUrl, rssUrl,
primaryColor, author, description, add_date, imagePath, provider, link) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", primaryColor, author, description, add_date, imagePath, provider, link) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
[ [
@ -114,6 +118,16 @@ class DBHelper {
podcastLocal.link podcastLocal.link
]); ]);
}); });
await dbClient.transaction((txn) async {
await txn.rawInsert(
"""REPLACE INTO SubscribeHistory(id, title, rss_url, add_date) VALUES (?, ?, ?, ?)""",
[
podcastLocal.id,
podcastLocal.title,
podcastLocal.rssUrl,
_milliseconds
]);
});
} }
Future<int> saveFiresideData(List<String> list) async { Future<int> saveFiresideData(List<String> list) async {
@ -146,6 +160,10 @@ class DBHelper {
print('Removed all download tasks'); print('Removed all download tasks');
} }
await dbClient.rawDelete('DELETE FROM Episodes WHERE feed_id=?', [id]); await dbClient.rawDelete('DELETE FROM Episodes WHERE feed_id=?', [id]);
int _milliseconds = DateTime.now().millisecondsSinceEpoch;
await dbClient.rawUpdate(
"""UPDATE SubscribeHistory SET remove_date = ? , status = ? WHERE id = ?""",
[_milliseconds, 1, id]);
} }
Future<int> saveHistory(PlayHistory history) async { Future<int> saveHistory(PlayHistory history) async {
@ -174,16 +192,46 @@ class DBHelper {
"""); """);
List<PlayHistory> playHistory = []; List<PlayHistory> playHistory = [];
list.forEach((record) { list.forEach((record) {
playHistory.add(PlayHistory( playHistory.add(PlayHistory(record['title'], record['enclosure_url'],
record['title'], record['seconds'], record['seek_value'],
record['enclosure_url'], playdate: DateTime.fromMillisecondsSinceEpoch(record['add_date'])));
record['seconds'],
record['seek_value'],
));
}); });
return playHistory; return playHistory;
} }
Future<List<SubHistory>> getSubHistory() async{
var dbClient = await database;
List<Map> list = await dbClient.rawQuery(
"""SELECT title, rss_url, add_date, remove_date, status FROM SubscribeHistory
ORDER BY add_date DESC"""
);
return list.map((record) => SubHistory(
record['status']==0 ? true : false, DateTime.fromMillisecondsSinceEpoch(record['remove_date']),
DateTime.fromMillisecondsSinceEpoch(record['add_date']), record['rss_url'], record['title']
)).toList();
}
Future<double> listenMins(int day) async {
var dbClient = await database;
var now = DateTime.now();
var start = DateTime(now.year, now.month, now.day)
.subtract(Duration(days: day))
.millisecondsSinceEpoch;
var end = DateTime(now.year, now.month, now.day)
.subtract(Duration(days: (day - 1)))
.millisecondsSinceEpoch;
List<Map> list = await dbClient.rawQuery(
"SELECT seconds FROM PlayHistory WHERE add_date > ? AND add_date < ?",
[start, end]);
double sum = 0;
if (list.length == 0) {
sum = 0;
} else {
list.forEach((record) => sum += record['seconds']);
}
return (sum ~/ 60).toDouble();
}
Future<int> getPosition(EpisodeBrief episodeBrief) async { Future<int> getPosition(EpisodeBrief episodeBrief) async {
var dbClient = await database; var dbClient = await database;
List<Map> list = await dbClient.rawQuery( List<Map> list = await dbClient.rawQuery(
@ -200,6 +248,17 @@ class DBHelper {
RegExp hhmm = RegExp(r'[0-2][0-9]\:[0-5][0-9]'); RegExp hhmm = RegExp(r'[0-2][0-9]\:[0-5][0-9]');
RegExp ddmmm = RegExp(r'[0-3][0-9]\s[A-Z][a-z]{2}'); RegExp ddmmm = RegExp(r'[0-3][0-9]\s[A-Z][a-z]{2}');
RegExp mmDd = RegExp(r'([0-1]|\s)[0-9]\-[0-3][0-9]'); RegExp mmDd = RegExp(r'([0-1]|\s)[0-9]\-[0-3][0-9]');
// RegExp timezone
RegExp z = RegExp(r'(\+|\-)[0-1][0-9]00');
String timezone = z.stringMatch(pubDate);
int timezoneInt = 0;
if(timezone!=null){
if(timezone.substring(0, 1) == '-'){
timezoneInt = int.parse(timezone.substring(1,2));
} else {
timezoneInt = -int.parse(timezone.substring(1,2));
}
}
try { try {
date = DateFormat('EEE, dd MMM yyyy HH:mm:ss Z', 'en_US').parse(pubDate); date = DateFormat('EEE, dd MMM yyyy HH:mm:ss Z', 'en_US').parse(pubDate);
} catch (e) { } catch (e) {
@ -209,7 +268,7 @@ class DBHelper {
try { try {
date = DateFormat('EEE, dd MMM yyyy HH:mm Z', 'en_US').parse(pubDate); date = DateFormat('EEE, dd MMM yyyy HH:mm Z', 'en_US').parse(pubDate);
} catch (e) { } catch (e) {
//parse date using regex, bug in parse maonth/day //parse date using regex, still have issue in parse maonth/day
String year = yyyy.stringMatch(pubDate); String year = yyyy.stringMatch(pubDate);
String time = hhmm.stringMatch(pubDate); String time = hhmm.stringMatch(pubDate);
String month = ddmmm.stringMatch(pubDate); String month = ddmmm.stringMatch(pubDate);
@ -221,12 +280,12 @@ class DBHelper {
date = DateFormat('mm-dd yyyy HH:mm', 'en_US') date = DateFormat('mm-dd yyyy HH:mm', 'en_US')
.parse(month + ' ' + year + ' ' + time); .parse(month + ' ' + year + ' ' + time);
} else { } else {
date = DateTime.now(); date = DateTime.now().toUtc();
} }
} }
} }
} }
return date; return date.add(Duration(hours: timezoneInt));
} }
int getExplicit(bool b) { int getExplicit(bool b) {
@ -276,7 +335,7 @@ class DBHelper {
await dbClient.transaction((txn) { await dbClient.transaction((txn) {
return txn.rawInsert( return txn.rawInsert(
"""INSERT OR IGNORE INTO Episodes(title, enclosure_url, enclosure_length, pubDate, """INSERT OR IGNORE INTO Episodes(title, enclosure_url, enclosure_length, pubDate,
description, feed_id, milliseconds, duration, explicit) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)""", description, feed_id, milliseconds, duration, explicit, media_id) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
[ [
_title, _title,
_url, _url,
@ -287,6 +346,7 @@ class DBHelper {
_milliseconds, _milliseconds,
_duration, _duration,
_explicit, _explicit,
_url
]); ]);
}); });
} }
@ -333,7 +393,7 @@ class DBHelper {
await dbClient.transaction((txn) { await dbClient.transaction((txn) {
return txn.rawInsert( return txn.rawInsert(
"""INSERT OR IGNORE INTO Episodes(title, enclosure_url, enclosure_length, pubDate, """INSERT OR IGNORE INTO Episodes(title, enclosure_url, enclosure_length, pubDate,
description, feed_id, milliseconds, duration, explicit) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)""", description, feed_id, milliseconds, duration, explicit, media_id) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
[ [
_title, _title,
_url, _url,
@ -344,6 +404,7 @@ class DBHelper {
_milliseconds, _milliseconds,
_duration, _duration,
_explicit, _explicit,
_url
]); ]);
}); });
} }
@ -358,7 +419,7 @@ class DBHelper {
List<Map> list = await dbClient List<Map> list = await dbClient
.rawQuery("""SELECT E.title, E.enclosure_url, E.enclosure_length, .rawQuery("""SELECT E.title, E.enclosure_url, E.enclosure_length,
E.milliseconds, P.imagePath, P.title as feedTitle, E.duration, E.explicit, E.liked, E.milliseconds, P.imagePath, P.title as feedTitle, E.duration, E.explicit, E.liked,
E.downloaded, P.primaryColor E.downloaded, P.primaryColor , E.media_id
FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
WHERE P.id = ? ORDER BY E.milliseconds DESC""", [id]); WHERE P.id = ? ORDER BY E.milliseconds DESC""", [id]);
for (int x = 0; x < list.length; x++) { for (int x = 0; x < list.length; x++) {
@ -373,7 +434,8 @@ class DBHelper {
list[x]['downloaded'], list[x]['downloaded'],
list[x]['duration'], list[x]['duration'],
list[x]['explicit'], list[x]['explicit'],
list[x]['imagePath'])); list[x]['imagePath'],
list[x]['media_id']));
} }
return episodes; return episodes;
} }
@ -384,7 +446,7 @@ class DBHelper {
List<Map> list = await dbClient List<Map> list = await dbClient
.rawQuery("""SELECT E.title, E.enclosure_url, E.enclosure_length, .rawQuery("""SELECT E.title, E.enclosure_url, E.enclosure_length,
E.milliseconds, P.imagePath, P.title as feed_title, E.duration, E.explicit, E.liked, E.milliseconds, P.imagePath, P.title as feed_title, E.duration, E.explicit, E.liked,
E.downloaded, P.primaryColor E.downloaded, P.primaryColor, E.media_id
FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
where E.feed_id = ? ORDER BY E.milliseconds DESC LIMIT 3""", [id]); where E.feed_id = ? ORDER BY E.milliseconds DESC LIMIT 3""", [id]);
for (int x = 0; x < list.length; x++) { for (int x = 0; x < list.length; x++) {
@ -399,7 +461,8 @@ class DBHelper {
list[x]['downloaded'], list[x]['downloaded'],
list[x]['duration'], list[x]['duration'],
list[x]['explicit'], list[x]['explicit'],
list[x]['imagePath'])); list[x]['imagePath'],
list[x]['media_id']));
} }
return episodes; return episodes;
} }
@ -410,7 +473,7 @@ class DBHelper {
List<Map> list = await dbClient.rawQuery( List<Map> list = await dbClient.rawQuery(
"""SELECT E.title, E.enclosure_url, E.enclosure_length, """SELECT E.title, E.enclosure_url, E.enclosure_length,
E.milliseconds, P.imagePath, P.title as feed_title, E.duration, E.explicit, E.liked, E.milliseconds, P.imagePath, P.title as feed_title, E.duration, E.explicit, E.liked,
E.downloaded, P.primaryColor E.downloaded, P.primaryColor, E.media_id
FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
where E.enclosure_url = ? ORDER BY E.milliseconds DESC LIMIT 3""", where E.enclosure_url = ? ORDER BY E.milliseconds DESC LIMIT 3""",
[url]); [url]);
@ -427,19 +490,20 @@ class DBHelper {
list.first['downloaded'], list.first['downloaded'],
list.first['duration'], list.first['duration'],
list.first['explicit'], list.first['explicit'],
list.first['imagePath']); list.first['imagePath'],
list.first['media_id']);
return episode; return episode;
} }
Future<List<EpisodeBrief>> getRecentRssItem() async { Future<List<EpisodeBrief>> getRecentRssItem(int top) async {
var dbClient = await database; var dbClient = await database;
List<EpisodeBrief> episodes = List(); List<EpisodeBrief> episodes = List();
List<Map> list = await dbClient List<Map> list = await dbClient
.rawQuery("""SELECT E.title, E.enclosure_url, E.enclosure_length, .rawQuery("""SELECT E.title, E.enclosure_url, E.enclosure_length,
E.milliseconds, P.title as feed_title, E.duration, E.explicit, E.liked, E.milliseconds, P.title as feed_title, E.duration, E.explicit, E.liked,
E.downloaded, P.imagePath, P.primaryColor E.downloaded, P.imagePath, P.primaryColor, E.media_id
FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
ORDER BY E.milliseconds DESC LIMIT 99"""); ORDER BY E.milliseconds DESC LIMIT ? """, [top]);
for (int x = 0; x < list.length; x++) { for (int x = 0; x < list.length; x++) {
episodes.add(EpisodeBrief( episodes.add(EpisodeBrief(
list[x]['title'], list[x]['title'],
@ -452,7 +516,8 @@ class DBHelper {
list[x]['doanloaded'], list[x]['doanloaded'],
list[x]['duration'], list[x]['duration'],
list[x]['explicit'], list[x]['explicit'],
list[x]['imagePath'])); list[x]['imagePath'],
list[x]['media_id']));
} }
return episodes; return episodes;
} }
@ -463,7 +528,7 @@ class DBHelper {
List<Map> list = await dbClient.rawQuery( List<Map> list = await dbClient.rawQuery(
"""SELECT E.title, E.enclosure_url, E.enclosure_length, E.milliseconds, P.imagePath, """SELECT E.title, E.enclosure_url, E.enclosure_length, E.milliseconds, P.imagePath,
P.title as feed_title, E.duration, E.explicit, E.liked, E.downloaded, P.title as feed_title, E.duration, E.explicit, E.liked, E.downloaded,
P.primaryColor FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id 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 99"""); WHERE E.liked = 1 ORDER BY E.milliseconds DESC LIMIT 99""");
for (int x = 0; x < list.length; x++) { for (int x = 0; x < list.length; x++) {
episodes.add(EpisodeBrief( episodes.add(EpisodeBrief(
@ -477,7 +542,8 @@ class DBHelper {
list[x]['downloaded'], list[x]['downloaded'],
list[x]['duration'], list[x]['duration'],
list[x]['explicit'], list[x]['explicit'],
list[x]['imagePath'])); list[x]['imagePath'],
list[x]['media_id']));
} }
return episodes; return episodes;
} }
@ -507,10 +573,21 @@ class DBHelper {
return count; return count;
} }
Future<int> saveMediaId(String url, String path) async {
var dbClient = await database;
int _milliseconds = DateTime.now().millisecondsSinceEpoch;
int count = await dbClient.rawUpdate(
"UPDATE Episodes SET media_id = ?, download_date = ? WHERE enclosure_url = ?",
[path, _milliseconds, url]);
return count;
}
Future<int> delDownloaded(String url) async { Future<int> delDownloaded(String url) async {
var dbClient = await database; var dbClient = await database;
int count = await dbClient.rawUpdate( int count = await dbClient.rawUpdate(
"UPDATE Episodes SET downloaded = 'ND' WHERE enclosure_url = ?", [url]); "UPDATE Episodes SET downloaded = 'ND', media_id = ? WHERE enclosure_url = ?", [url, url]);
print('Deleted ' + url); print('Deleted ' + url);
return count; return count;
} }
@ -521,7 +598,7 @@ class DBHelper {
List<Map> list = await dbClient.rawQuery( List<Map> list = await dbClient.rawQuery(
"""SELECT E.title, E.enclosure_url, E.enclosure_length, E.milliseconds, P.imagePath, """SELECT E.title, E.enclosure_url, E.enclosure_length, E.milliseconds, P.imagePath,
P.title as feed_title, E.duration, E.explicit, E.liked, E.downloaded, P.title as feed_title, E.duration, E.explicit, E.liked, E.downloaded,
P.primaryColor FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id 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"""); WHERE E.downloaded != 'ND' ORDER BY E.download_date DESC LIMIT 99""");
for (int x = 0; x < list.length; x++) { for (int x = 0; x < list.length; x++) {
episodes.add(EpisodeBrief( episodes.add(EpisodeBrief(
@ -535,7 +612,8 @@ class DBHelper {
list[x]['downloaded'], list[x]['downloaded'],
list[x]['duration'], list[x]['duration'],
list[x]['explicit'], list[x]['explicit'],
list[x]['imagePath'])); list[x]['imagePath'],
list[x]['media_id']));
} }
return episodes; return episodes;
} }
@ -562,7 +640,7 @@ class DBHelper {
List<Map> list = await dbClient.rawQuery( List<Map> list = await dbClient.rawQuery(
"""SELECT E.title, E.enclosure_url, E.enclosure_length, E.milliseconds, P.imagePath, """SELECT E.title, E.enclosure_url, E.enclosure_length, E.milliseconds, P.imagePath,
P.title as feed_title, E.duration, E.explicit, E.liked, E.downloaded, P.title as feed_title, E.duration, E.explicit, E.liked, E.downloaded,
P.primaryColor FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id P.primaryColor, E.media_id FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
WHERE E.enclosure_url = ?""", [url]); WHERE E.enclosure_url = ?""", [url]);
episode = EpisodeBrief( episode = EpisodeBrief(
list.first['title'], list.first['title'],
@ -575,7 +653,33 @@ class DBHelper {
list.first['downloaded'], list.first['downloaded'],
list.first['duration'], list.first['duration'],
list.first['explicit'], list.first['explicit'],
list.first['imagePath']); list.first['imagePath'],
list.first['media_id']);
return episode; return episode;
} }
Future<EpisodeBrief> getRssItemWithMediaId(String id) async {
var dbClient = await database;
EpisodeBrief episode;
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.media_id = ?""", [id]);
episode = EpisodeBrief(
list.first['title'],
list.first['enclosure_url'],
list.first['enclosure_length'],
list.first['milliseconds'],
list.first['feed_title'],
list.first['primaryColor'],
list.first['liked'],
list.first['downloaded'],
list.first['duration'],
list.first['explicit'],
list.first['imagePath'],
list.first['media_id']);
return episode;
}
} }

View File

@ -10,57 +10,61 @@ import 'package:tsacdop/home/appbar/addpodcast.dart';
import 'package:tsacdop/class/audiostate.dart'; import 'package:tsacdop/class/audiostate.dart';
import 'package:tsacdop/class/importompl.dart'; import 'package:tsacdop/class/importompl.dart';
import 'package:tsacdop/class/settingstate.dart'; import 'package:tsacdop/class/settingstate.dart';
import 'local_storage/sqflite_localpodcast.dart'; import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
void callbackDispatcher() {
Workmanager.executeTask((task, inputData) async {
var dbHelper = DBHelper();
print('Start task');
List<PodcastLocal> podcastList = await dbHelper.getPodcastLocalAll();
await Future.forEach(podcastList, (podcastLocal) async {
await dbHelper.updatePodcastRss(podcastLocal);
print('Refresh ' + podcastLocal.title);
});
return Future.value(true);
});
}
final SettingState themeSetting = SettingState(); final SettingState themeSetting = SettingState();
Future main() async { Future main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
await themeSetting.initData(); await themeSetting.initData();
await FlutterDownloader.initialize();
runApp( runApp(
MultiProvider( MultiProvider(
providers: [ providers: [
ChangeNotifierProvider(create: (_) => themeSetting), ChangeNotifierProvider(create: (_) => themeSetting),
ChangeNotifierProvider(create: (_) => AudioPlayer()), ChangeNotifierProvider(create: (_) => AudioPlayerNotifier()),
ChangeNotifierProvider(create: (_) => GroupList()), ChangeNotifierProvider(create: (_) => GroupList()),
ChangeNotifierProvider(create: (_) => ImportOmpl()), ChangeNotifierProvider(create: (_) => ImportOmpl()),
], ],
child: MyApp(), child: MyApp(),
), ),
); );
Workmanager.initialize(
callbackDispatcher,
isInDebugMode: true,
);
Workmanager.registerPeriodicTask("2", "update_podcasts",
frequency: Duration(minutes: 1),
initialDelay: Duration(seconds: 5),
constraints: Constraints(
networkType: NetworkType.connected,
));
await FlutterDownloader.initialize();
await SystemChrome.setPreferredOrientations( await SystemChrome.setPreferredOrientations(
[DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]); [DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]);
} }
void callbackDispatcher() {
Workmanager.executeTask((task, inputData) async {
var dbHelper = DBHelper();
List<PodcastLocal> podcastList = await dbHelper.getPodcastLocalAll();
await Future.forEach(podcastList, (podcastLocal) async {
await dbHelper.updatePodcastRss(podcastLocal);
print('Refresh ' + podcastLocal.title);
});
return true;
});
}
class MyApp extends StatelessWidget { class MyApp extends StatelessWidget {
void setWorkManager() {
Workmanager.initialize(
callbackDispatcher,
isInDebugMode: true,
);
Workmanager.registerPeriodicTask("1", "update_podcasts",
frequency: Duration(hours: 12),
initialDelay: Duration(seconds: 5),
constraints: Constraints(
networkType: NetworkType.connected,
requiresBatteryNotLow: true,
));
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Consumer<SettingState>( return Consumer<SettingState>(
builder: (_, setting, __) { builder: (_, setting, __) {
if (setting.autoUpdate) setWorkManager();
return MaterialApp( return MaterialApp(
themeMode: setting.theme, themeMode: setting.theme,
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
@ -78,7 +82,6 @@ class MyApp extends StatelessWidget {
elevation: 0, elevation: 0,
), ),
textTheme: TextTheme( textTheme: TextTheme(
headline1: TextStyle(fontSize: 72.0, fontWeight: FontWeight.bold),
bodyText2: bodyText2:
TextStyle(fontSize: 15.0, fontWeight: FontWeight.normal), TextStyle(fontSize: 15.0, fontWeight: FontWeight.normal),
), ),
@ -89,6 +92,7 @@ class MyApp extends StatelessWidget {
), ),
darkTheme: ThemeData.dark().copyWith( darkTheme: ThemeData.dark().copyWith(
accentColor: setting.accentSetColor, accentColor: setting.accentSetColor,
appBarTheme: AppBarTheme(elevation: 0),
), ),
home: MyHomePage(), home: MyHomePage(),
); );

View File

@ -0,0 +1,146 @@
import 'package:flutter/material.dart';
class CustomTabView extends StatefulWidget {
final int itemCount;
final IndexedWidgetBuilder tabBuilder;
final IndexedWidgetBuilder pageBuilder;
final ValueChanged<int> onPositionChange;
final ValueChanged<double> onScroll;
final int initPosition;
CustomTabView({
@required this.itemCount,
@required this.tabBuilder,
@required this.pageBuilder,
this.onPositionChange,
this.onScroll,
this.initPosition,
});
@override
_CustomTabsState createState() => _CustomTabsState();
}
class _CustomTabsState extends State<CustomTabView>
with TickerProviderStateMixin {
TabController controller;
int _currentCount;
int _currentPosition;
@override
void initState() {
_currentPosition = widget.initPosition ?? 0;
controller = TabController(
length: widget.itemCount,
vsync: this,
initialIndex: _currentPosition,
);
controller.addListener(onPositionChange);
controller.animation.addListener(onScroll);
_currentCount = widget.itemCount;
super.initState();
}
@override
void didUpdateWidget(CustomTabView oldWidget) {
if (_currentCount != widget.itemCount) {
controller.animation.removeListener(onScroll);
controller.removeListener(onPositionChange);
controller.dispose();
if (widget.initPosition != null) {
_currentPosition = widget.initPosition;
}
if (_currentPosition > widget.itemCount - 1) {
_currentPosition = widget.itemCount - 1;
_currentPosition = _currentPosition < 0 ? 0 : _currentPosition;
if (widget.onPositionChange is ValueChanged<int>) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
widget.onPositionChange(_currentPosition);
}
});
}
}
_currentCount = widget.itemCount;
setState(() {
controller = TabController(
length: widget.itemCount,
vsync: this,
initialIndex: _currentPosition,
);
controller.addListener(onPositionChange);
controller.animation.addListener(onScroll);
});
} else if (widget.initPosition != null) {
controller.animateTo(widget.initPosition);
}
super.didUpdateWidget(oldWidget);
}
@override
void dispose() {
controller.animation.removeListener(onScroll);
controller.removeListener(onPositionChange);
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Container(
alignment: Alignment.centerLeft,
height: 50.0,
padding: EdgeInsets.all(10.0),
child: TabBar(
indicatorSize: TabBarIndicatorSize.label,
labelPadding: EdgeInsets.symmetric(horizontal: 5.0),
indicatorPadding: EdgeInsets.symmetric(horizontal: 5.0),
isScrollable: true,
controller: controller,
labelColor: Colors.white,
unselectedLabelColor: Colors.grey[700],
indicator: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(15)),
color: Theme.of(context).accentColor,
),
tabs: List.generate(
widget.itemCount,
(index) => widget.tabBuilder(context, index),
),
),
),
Expanded(
child: TabBarView(
controller: controller,
children: List.generate(
widget.itemCount,
(index) => widget.pageBuilder(context, index),
),
),
),
],
);
}
onPositionChange() {
if (!controller.indexIsChanging) {
_currentPosition = controller.index;
if (widget.onPositionChange is ValueChanged<int>) {
widget.onPositionChange(_currentPosition);
}
}
}
onScroll() {
if (widget.onScroll is ValueChanged<double>) {
widget.onScroll(controller.animation.value);
}
}
}

View File

@ -7,16 +7,13 @@ import 'package:flutter/services.dart';
import 'package:html/parser.dart'; import 'package:html/parser.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:fluttertoast/fluttertoast.dart'; import 'package:fluttertoast/fluttertoast.dart';
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:tsacdop/class/podcastlocal.dart'; import 'package:tsacdop/class/podcastlocal.dart';
import 'package:tsacdop/class/episodebrief.dart'; import 'package:tsacdop/class/episodebrief.dart';
import 'package:tsacdop/episodes/episodedetail.dart';
import 'package:tsacdop/local_storage/sqflite_localpodcast.dart'; import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
import 'package:tsacdop/util/episodegrid.dart'; import 'package:tsacdop/util/episodegrid.dart';
import 'package:tsacdop/util/pageroute.dart';
import 'package:tsacdop/home/audioplayer.dart'; import 'package:tsacdop/home/audioplayer.dart';
import 'package:tsacdop/class/fireside_data.dart'; import 'package:tsacdop/class/fireside_data.dart';
import 'package:tsacdop/util/colorize.dart'; import 'package:tsacdop/util/colorize.dart';
@ -104,7 +101,9 @@ class _PodcastDetailState extends State<PodcastDetail> {
fit: BoxFit.cover)), fit: BoxFit.cover)),
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: Container( child: Container(
color: Theme.of(context).scaffoldBackgroundColor.withOpacity(0.5), color: Theme.of(context)
.scaffoldBackgroundColor
.withOpacity(0.5),
padding: EdgeInsets.symmetric(vertical: 5.0), padding: EdgeInsets.symmetric(vertical: 5.0),
width: MediaQuery.of(context).size.width, width: MediaQuery.of(context).size.width,
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
@ -122,7 +121,8 @@ class _PodcastDetailState extends State<PodcastDetail> {
children: <Widget>[ children: <Widget>[
CircleAvatar( CircleAvatar(
backgroundColor: Colors.grey[400], backgroundColor: Colors.grey[400],
backgroundImage: CachedNetworkImageProvider( backgroundImage:
CachedNetworkImageProvider(
host.image, host.image,
)), )),
Padding( Padding(
@ -162,7 +162,7 @@ class _PodcastDetailState extends State<PodcastDetail> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
double _width = MediaQuery.of(context).size.width;
Color _color = widget.podcastLocal.primaryColor.colorizedark(); Color _color = widget.podcastLocal.primaryColor.colorizedark();
return AnnotatedRegion<SystemUiOverlayStyle>( return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle( value: SystemUiOverlayStyle(
@ -283,7 +283,7 @@ class _PodcastDetailState extends State<PodcastDetail> {
'Hosted on ' + 'Hosted on ' +
widget.podcastLocal widget.podcastLocal
.provider, .provider,
maxLines: 1, maxLines: 1,
style: TextStyle( style: TextStyle(
color: Colors.white), color: Colors.white),
) )
@ -308,6 +308,8 @@ class _PodcastDetailState extends State<PodcastDetail> {
), ),
title: top < 70 title: top < 70
? Text(widget.podcastLocal.title, ? Text(widget.podcastLocal.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(color: Colors.white)) style: TextStyle(color: Colors.white))
: Center(), : Center(),
); );
@ -322,184 +324,15 @@ class _PodcastDetailState extends State<PodcastDetail> {
), ),
), ),
SliverPadding( SliverPadding(
padding: padding: const EdgeInsets.symmetric(
const EdgeInsets.symmetric(horizontal: 15.0), horizontal: 10.0),
sliver: SliverGrid( sliver: EpisodeGrid(
gridDelegate: podcast: snapshot.data,
SliverGridDelegateWithFixedCrossAxisCount( showDownload: false,
childAspectRatio: 1.0, showFavorite: true,
crossAxisCount: 3, showNumber: true,
mainAxisSpacing: 6.0, heroTag: 'podcast',
crossAxisSpacing: 6.0, )),
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
EpisodeBrief episodeBrief =
snapshot.data[index];
Color _c = (Theme.of(context).brightness ==
Brightness.light)
? widget.podcastLocal.primaryColor
.colorizedark()
: widget.podcastLocal.primaryColor
.colorizeLight();
return Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
Navigator.push(
context,
ScaleRoute(
page: EpisodeDetail(
episodeItem: episodeBrief,
heroTag: 'podcast',
)),
);
},
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.all(
Radius.circular(5.0)),
color: Theme.of(context)
.scaffoldBackgroundColor,
border: Border.all(
color: Theme.of(context)
.brightness ==
Brightness.light
? Theme.of(context)
.primaryColor
: Theme.of(context)
.scaffoldBackgroundColor,
width: 3.0,
),
boxShadow: [
BoxShadow(
color: Theme.of(context)
.primaryColor,
blurRadius: 0.5,
spreadRadius: 0.5,
),
]),
alignment: Alignment.center,
padding: EdgeInsets.all(8.0),
child: Column(
mainAxisAlignment:
MainAxisAlignment.center,
children: <Widget>[
Expanded(
flex: 2,
child: Row(
mainAxisAlignment:
MainAxisAlignment.start,
children: <Widget>[
Hero(
tag: episodeBrief
.enclosureUrl +
'podcast',
child: Container(
height: _width / 16,
width: _width / 16,
child: CircleAvatar(
backgroundImage:
FileImage(File(
"${episodeBrief.imagePath}")),
),
),
),
Spacer(),
Container(
alignment:
Alignment.topRight,
child: Text(
(snapshot.data.length -
index)
.toString(),
style: GoogleFonts.teko(
textStyle: TextStyle(
fontSize:
_width / 24,
color: _c,
),
),
),
)
],
),
),
Expanded(
flex: 5,
child: Container(
alignment: Alignment.topLeft,
padding:
EdgeInsets.only(top: 2.0),
child: Text(
episodeBrief.title,
style: TextStyle(
fontSize: _width / 32,
),
maxLines: 4,
overflow: TextOverflow.fade,
),
),
),
Expanded(
flex: 1,
child: Row(
children: <Widget>[
Align(
alignment:
Alignment.bottomLeft,
child: Text(
episodeBrief
.dateToString(),
//podcast[index].pubDate.substring(4, 16),
style: TextStyle(
fontSize:
_width / 35,
color: _c,
fontStyle: FontStyle
.italic),
),
),
Spacer(),
DownloadIcon(
episodeBrief:
episodeBrief),
Padding(
padding:
EdgeInsets.all(1),
),
Container(
alignment:
Alignment.bottomRight,
child: (episodeBrief
.liked ==
0)
? Center()
: IconTheme(
data:
IconThemeData(
size: 15),
child: Icon(
Icons.favorite,
color:
Colors.red,
),
),
),
],
),
),
],
),
),
),
);
},
childCount: snapshot.data.length,
),
),
),
], ],
) )
: Center(child: CircularProgressIndicator()); : Center(child: CircularProgressIndicator());

View File

@ -1,12 +1,10 @@
import 'dart:io'; import 'dart:io';
import 'dart:math' as math;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:fluttertoast/fluttertoast.dart'; import 'package:fluttertoast/fluttertoast.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:flutter_colorpicker/flutter_colorpicker.dart';
import 'package:tsacdop/class/podcast_group.dart'; import 'package:tsacdop/class/podcast_group.dart';
import 'package:tsacdop/class/podcastlocal.dart'; import 'package:tsacdop/class/podcastlocal.dart';
@ -21,87 +19,7 @@ class PodcastGroupList extends StatefulWidget {
_PodcastGroupListState createState() => _PodcastGroupListState(); _PodcastGroupListState createState() => _PodcastGroupListState();
} }
class _PodcastGroupListState extends State<PodcastGroupList> class _PodcastGroupListState extends State<PodcastGroupList> {
with SingleTickerProviderStateMixin {
bool _showSetting;
AnimationController _controller;
Animation _animation;
double _fraction;
@override
void initState() {
super.initState();
_showSetting = false;
_fraction = 0;
_controller = AnimationController(
duration: const Duration(milliseconds: 500), vsync: this);
_animation = Tween(begin: 0.0, end: 1.0).animate(_controller)
..addListener(() {
if (mounted)
setState(() {
_fraction = _animation.value;
});
});
_controller.addStatusListener((status) {
if (status == AnimationStatus.completed) {
_controller.stop();
} else if (status == AnimationStatus.dismissed) {
_controller.stop();
}
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
Widget _saveButton(BuildContext context) {
var podcastList = widget.group.podcasts;
var _groupList = Provider.of<GroupList>(context, listen: false);
return Transform(
alignment: FractionalOffset(0.5, 0.5),
transform: Matrix4.rotationY(math.pi * _fraction),
child: Container(
child: InkWell(
child: Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: _fraction > 0.5 ? Colors.red : widget.group.getColor(),
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.grey[700],
blurRadius: 5,
offset: Offset(1, 1),
),
]),
alignment: Alignment.center,
child: Icon(
_fraction > 0.5 ? Icons.save : Icons.settings,
color: Colors.white,
)),
onTap: () async {
if (_fraction == 0) {
setState(() {
_showSetting = true;
});
} else {
await _groupList.saveOrder(widget.group, podcastList);
Fluttertoast.showToast(
msg: 'Setting Saved',
gravity: ToastGravity.BOTTOM,
);
_controller.reverse();
}
},
),
),
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var groupList = Provider.of<GroupList>(context, listen: false); var groupList = Provider.of<GroupList>(context, listen: false);
@ -109,372 +27,38 @@ class _PodcastGroupListState extends State<PodcastGroupList>
? Container( ? Container(
color: Theme.of(context).primaryColor, color: Theme.of(context).primaryColor,
) )
: Stack( : Container(
children: <Widget>[ color: Theme.of(context).primaryColor,
Container( child: Stack(
color: Theme.of(context).primaryColor, children: <Widget>[
child: Stack( ReorderableListView(
children: <Widget>[ onReorder: (int oldIndex, int newIndex) {
ReorderableListView( setState(() {
onReorder: (int oldIndex, int newIndex) { if (newIndex > oldIndex) {
setState(() { newIndex -= 1;
if (newIndex > oldIndex) { }
newIndex -= 1; final PodcastLocal podcast =
} widget.group.podcasts.removeAt(oldIndex);
final PodcastLocal podcast = widget.group.podcasts.insert(newIndex, podcast);
widget.group.podcasts.removeAt(oldIndex); });
widget.group.podcasts.insert(newIndex, podcast); widget.group.setOrderedPodcasts = widget.group.podcasts;
_controller.forward(); groupList.addToOrderChanged(widget.group.name);
}); },
}, children: widget.group.podcasts
children: widget.group.podcasts .map<Widget>((PodcastLocal podcastLocal) {
.map<Widget>((PodcastLocal podcastLocal) { return Container(
return Container( decoration:
decoration: BoxDecoration( BoxDecoration(color: Theme.of(context).primaryColor),
color: Theme.of(context).primaryColor), key: ObjectKey(podcastLocal.title),
key: ObjectKey(podcastLocal.title), child: PodcastCard(
child: PodcastCard( podcastLocal: podcastLocal,
podcastLocal: podcastLocal, group: widget.group,
group: widget.group, ),
), );
); }).toList(),
}).toList(),
),
Positioned(
bottom: 30,
right: 30,
child: _saveButton(context),
),
],
), ),
), ],
_showSetting ),
? Positioned.fill(
child: GestureDetector(
onTap: () => setState(() => _showSetting = false),
child: Container(
color: Theme.of(context)
.scaffoldBackgroundColor
.withOpacity(0.5),
),
),
)
: Center(),
_showSetting
? Container(
alignment: Alignment.bottomCenter,
child: Container(
height: 150.0,
alignment: Alignment.center,
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
boxShadow: [
BoxShadow(
offset: Offset(0, -1),
blurRadius: 4,
color: Theme.of(context).brightness ==
Brightness.light
? Colors.grey[400]
: Colors.grey[800],
),
],
),
child: SingleChildScrollView(
scrollDirection: Axis.vertical,
child: Container(
height: 150,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
// mainAxisSize: MainAxisSize.min,
children: <Widget>[
Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
setState(() => _showSetting = false);
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),
statusBarColor: Theme.of(
context)
.brightness ==
Brightness.light
? Color.fromRGBO(
113, 113, 113, 1)
: Color.fromRGBO(
5, 5, 5, 1),
),
child: SafeArea(
child: AlertDialog(
elevation: 1,
titlePadding:
EdgeInsets.only(
top: 20,
left: 40,
right: 200,
bottom: 20),
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.all(
Radius.circular(
10.0))),
title:
Text('Choose a color'),
content:
SingleChildScrollView(
child: MaterialPicker(
onColorChanged:
(value) {
PodcastGroup newGroup =
PodcastGroup(
widget
.group.name,
color: value
.toString()
.substring(
10,
16),
id: widget
.group.id,
podcastList:
widget
.group
.podcastList);
groupList.updateGroup(
newGroup);
Navigator.of(context)
.pop();
},
pickerColor:
Colors.blue,
),
),
))));
},
child: Container(
height: 50.0,
padding:
EdgeInsets.symmetric(horizontal: 20),
child: Row(
children: <Widget>[
Icon(Icons.colorize),
Padding(
padding: EdgeInsets.symmetric(
horizontal: 5.0),
),
Text('Change Color'),
],
),
),
),
),
Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
setState(() => _showSetting = false);
widget.group.name == 'Home'
? 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: 200),
pageBuilder: (BuildContext
context,
Animation animaiton,
Animation
secondaryAnimation) =>
RenameGroup(
group: widget.group,
));
},
child: Container(
height: 50.0,
padding:
EdgeInsets.symmetric(horizontal: 20),
child: Row(
children: <Widget>[
Icon(Icons.text_fields),
Padding(
padding: EdgeInsets.symmetric(
horizontal: 5.0),
),
Text('Rename'),
],
),
),
),
),
Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
setState(() => _showSetting = false);
widget.group.name == 'Home'
? 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: 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),
statusBarColor: Theme.of(
context)
.brightness ==
Brightness.light
? Color.fromRGBO(
113, 113, 113, 1)
: Color.fromRGBO(
5, 5, 5, 1),
),
child: SafeArea(
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: () {
groupList.delGroup(
widget.group);
Navigator.of(
context)
.pop();
},
child: Text(
'CONFIRM',
style: TextStyle(
color: Colors
.red),
),
)
],
),
),
));
},
child: Container(
height: 50,
padding:
EdgeInsets.symmetric(horizontal: 20),
child: Row(
children: <Widget>[
Icon(Icons.delete_outline),
Padding(
padding: EdgeInsets.symmetric(
horizontal: 5.0),
),
Text('Delete'),
],
),
),
),
),
],
),
),
),
),
)
: Center(),
],
); );
} }
} }
@ -853,14 +437,14 @@ class _RenameGroupState extends State<RenameGroup> {
style: TextStyle(color: Theme.of(context).accentColor)), style: TextStyle(color: Theme.of(context).accentColor)),
) )
], ],
title: Text('Create new group'), title: Text('Edit group name'),
content: Column( content: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: <Widget>[
TextField( TextField(
decoration: InputDecoration( decoration: InputDecoration(
contentPadding: EdgeInsets.symmetric(horizontal: 10), contentPadding: EdgeInsets.symmetric(horizontal: 10),
hintText: 'New Group', hintText: widget.group.name,
hintStyle: TextStyle(fontSize: 18), hintStyle: TextStyle(fontSize: 18),
filled: true, filled: true,
focusedBorder: UnderlineInputBorder( focusedBorder: UnderlineInputBorder(

View File

@ -1,23 +1,135 @@
import 'dart:math' as math;
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:line_icons/line_icons.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:tsacdop/class/podcast_group.dart'; import 'package:tsacdop/class/podcast_group.dart';
import 'package:tsacdop/podcasts/podcastgroup.dart'; import 'package:tsacdop/podcasts/podcastgroup.dart';
import 'package:tsacdop/podcasts/podcastlist.dart'; import 'package:tsacdop/podcasts/podcastlist.dart';
import 'package:tsacdop/util/pageroute.dart'; import 'package:tsacdop/util/pageroute.dart';
import 'custom_tabview.dart';
class PodcastManage extends StatefulWidget { class PodcastManage extends StatefulWidget {
@override @override
_PodcastManageState createState() => _PodcastManageState(); _PodcastManageState createState() => _PodcastManageState();
} }
class _PodcastManageState extends State<PodcastManage> { class _PodcastManageState extends State<PodcastManage>
Decoration getIndicator() { with TickerProviderStateMixin {
return const UnderlineTabIndicator( bool _showSetting;
borderSide: BorderSide(color: Colors.red, width: 0), double _menuValue;
insets: EdgeInsets.only( AnimationController _controller;
top: 10.0, AnimationController _menuController;
)); Animation _animation;
Animation _menuAnimation;
double _fraction;
int _index;
double _scroll;
@override
void initState() {
super.initState();
_showSetting = false;
_fraction = 0;
_menuValue = 0;
_scroll = 0;
_index = 0;
_menuController = AnimationController(
duration: const Duration(milliseconds: 300), vsync: this);
_controller = AnimationController(
duration: const Duration(milliseconds: 500), vsync: this);
_animation = Tween(begin: 0.0, end: 1.0).animate(_controller)
..addListener(() {
if (mounted)
setState(() {
_fraction = _animation.value;
});
});
_menuAnimation = Tween(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _menuController, curve: Curves.easeInOutBack))
..addListener(() {
if (mounted) setState(() => _menuValue = _menuAnimation.value);
});
_controller.addStatusListener((status) {
if (status == AnimationStatus.completed) {
_controller.stop();
} else if (status == AnimationStatus.dismissed) {
_controller.stop();
}
});
}
@override
void dispose() {
_controller.dispose();
_menuController.dispose();
super.dispose();
}
Widget _saveButton(BuildContext context) {
return Consumer<GroupList>(
builder: (_, groupList, __) {
if (groupList.orderChanged.contains(groupList.groups[_index].name)) {
_controller.forward();
} else if (_fraction > 0) {
_controller.reverse();
}
return Transform(
alignment: FractionalOffset(0.5, 0.5),
transform: Matrix4.rotationY(math.pi * _fraction),
child: Container(
child: InkWell(
child: Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: _fraction > 0.5
? Colors.red
: Theme.of(context).accentColor,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.grey[700],
blurRadius: 5,
offset: Offset(1, 1),
),
]),
alignment: Alignment.center,
child: _fraction > 0.5
? Icon(LineIcons.save_solid, color: Colors.white)
: AnimatedIcon(
color: Colors.white,
icon: AnimatedIcons.menu_close,
progress: _menuController,
),
// color: Colors.white,
),
onTap: () async {
if (_fraction == 0) {
!_showSetting
? _menuController.forward()
: await _menuController.reverse();
setState(() {
_showSetting = !_showSetting;
});
} else {
groupList.saveOrder(groupList.groups[_index]);
groupList.drlFromOrderChanged(groupList.groups[_index].name);
Fluttertoast.showToast(
msg: 'Setting Saved',
gravity: ToastGravity.BOTTOM,
);
_controller.reverse();
}
},
),
),
);
},
);
} }
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -53,59 +165,300 @@ class _PodcastManageState extends State<PodcastManage> {
List<PodcastGroup> _groups = groupList.groups; List<PodcastGroup> _groups = groupList.groups;
return _isLoading return _isLoading
? Center() ? Center()
: DefaultTabController( : Stack(
length: _groups.length, children: <Widget>[
child: Column( CustomTabView(
mainAxisAlignment: MainAxisAlignment.start, itemCount: _groups.length,
mainAxisSize: MainAxisSize.min, tabBuilder: (context, index) => Tab(
children: <Widget>[
Container(
height: 50,
padding: EdgeInsets.symmetric(horizontal: 10.0),
alignment: Alignment.centerLeft,
child: TabBar(
labelColor: Colors.white,
unselectedLabelColor: Colors.black,
labelPadding: EdgeInsets.all(5.0),
indicator: getIndicator(),
isScrollable: true,
tabs: _groups.map<Tab>((group) {
return Tab(
child: Container(
height: 30.0,
padding:
EdgeInsets.symmetric(horizontal: 10.0),
alignment: Alignment.center,
decoration: BoxDecoration(
color: group.getColor(),
// Theme.of(context).brightness ==
// Brightness.light
// ? Theme.of(context).primaryColorDark
// : Colors.grey[800],
borderRadius:
BorderRadius.all(Radius.circular(15)),
),
child: Text(
group.name,
)),
);
}).toList(),
),
),
Expanded(
child: Container( child: Container(
child: TabBarView( height: 30.0,
children: _groups.map<Widget>((group) { padding: EdgeInsets.symmetric(horizontal: 10.0),
return Container( alignment: Alignment.center,
key: ObjectKey(group), decoration: BoxDecoration(
child: PodcastGroupList(group: group)); color: (_scroll - index).abs() > 1
}).toList(), ? 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(
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: SafeArea(
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(
'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(),
],
);
}), }),
), ),
), ),

View File

@ -0,0 +1,287 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_downloader/flutter_downloader.dart';
import 'package:path_provider/path_provider.dart';
import 'package:line_icons/line_icons.dart';
import 'package:tsacdop/class/episodebrief.dart';
import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
class DownloadsManage extends StatefulWidget {
@override
_DownloadsManageState createState() => _DownloadsManageState();
}
class _DownloadsManageState extends State<DownloadsManage> {
//Downloaded size
int _size;
//Downloaded files
int _fileNum;
bool _loadEpisodes;
bool _clearing;
List<EpisodeBrief> _selectedList;
List<EpisodeBrief> _episodes = [];
_getDownloadedRssItem() async {
_episodes = [];
final tasks = await FlutterDownloader.loadTasksWithRawQuery(
query: "SELECT * FROM task WHERE status = 3");
var dbHelper = DBHelper();
await Future.forEach(tasks, (task) async {
EpisodeBrief episode = await dbHelper.getRssItemWithUrl(task.url);
_episodes.add(episode);
});
setState(() {
_loadEpisodes = true;
});
}
_getStorageSize() async {
_size = 0;
_fileNum = 0;
var dir = await getExternalStorageDirectory();
dir.list().forEach((d) {
var fileDir = Directory(d.path);
fileDir.list().forEach((file) async {
await File(file.path).stat().then((value) {
_size += value.size;
_fileNum += 1;
setState(() {});
});
});
});
}
_delSelectedEpisodes() async {
setState(() => _clearing = true);
await Future.forEach(_selectedList, (EpisodeBrief episode) async {
print(episode.downloaded);
await FlutterDownloader.remove(
taskId: episode.downloaded, shouldDeleteContent: true);
var dbHelper = DBHelper();
await dbHelper.delDownloaded(episode.enclosureUrl);
setState(() =>
_episodes.removeWhere((e) => e.enclosureUrl == episode.enclosureUrl));
});
await Future.delayed(Duration(seconds: 1));
setState(() {
_clearing = false;
});
await Future.delayed(Duration(seconds: 1));
setState(() => _selectedList = []);
_getStorageSize();
}
int sumSelected() {
int sum = 0;
if (_selectedList.length == 0) {
return sum;
} else {
_selectedList.forEach((episode) {
sum += episode.enclosureLength;
});
return sum;
}
}
@override
void initState() {
super.initState();
_clearing = false;
_loadEpisodes = false;
_selectedList = [];
_getStorageSize();
_getDownloadedRssItem();
}
@override
Widget build(BuildContext context) {
return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle(
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
systemNavigationBarColor: Theme.of(context).primaryColor,
statusBarColor: Theme.of(context).primaryColor),
child: SafeArea(
child: Scaffold(
appBar: AppBar(
title: Text('Downloads'),
elevation: 0,
backgroundColor: Theme.of(context).primaryColor,
),
body: Stack(
children: <Widget>[
Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Padding(
padding: EdgeInsets.all(10.0),
),
Container(
height: 100.0,
padding: EdgeInsets.only(bottom: 40, left: 60),
alignment: Alignment.centerLeft,
child: RichText(
text: TextSpan(
text: 'Total ',
style: TextStyle(
color: Theme.of(context).accentColor,
fontSize: 20,
),
children: <TextSpan>[
TextSpan(
text: _fileNum.toString(),
style: TextStyle(
color: Theme.of(context).accentColor,
fontSize: 40,
fontWeight: FontWeight.bold)),
TextSpan(
text: ' episodes ',
style: TextStyle(
color: Theme.of(context).accentColor,
fontSize: 20,
)),
TextSpan(
text: (_size ~/ 1000000).toString(),
style: TextStyle(
color: Theme.of(context).accentColor,
fontSize: 60,
fontWeight: FontWeight.bold)),
TextSpan(
text: ' Mb',
style: TextStyle(
color: Theme.of(context).accentColor,
fontSize: 20,
)),
],
),
),
),
_loadEpisodes
? Expanded(
child: ListView.builder(
itemCount: _episodes.length,
shrinkWrap: true,
scrollDirection: Axis.vertical,
itemBuilder: (context, index) {
return Column(
children: <Widget>[
ListTile(
onTap: () {
if (_selectedList
.contains(_episodes[index])) {
setState(() => _selectedList
.removeWhere((episode) =>
episode.enclosureUrl ==
_episodes[index]
.enclosureUrl));
} else {
setState(() => _selectedList
.add(_episodes[index]));
}
},
leading: CircleAvatar(
backgroundImage: FileImage(File(
"${_episodes[index].imagePath}")),
),
title: Text(
_episodes[index].title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
subtitle: _episodes[index]
.enclosureLength !=
0
? Text(((_episodes[index]
.enclosureLength) ~/
1000000)
.toString() +
' Mb')
: Center(),
trailing: Checkbox(
value: _selectedList
.contains(_episodes[index]),
onChanged: (bool boo) {
print(boo);
if (boo) {
setState(() => _selectedList
.add(_episodes[index]));
} else {
setState(() => _selectedList
.removeWhere((episode) =>
episode.enclosureUrl ==
_episodes[index]
.enclosureUrl));
}
},
),
),
Divider(
height: 2,
),
],
);
}),
)
: CircularProgressIndicator(),
],
),
AnimatedPositioned(
duration: Duration(milliseconds: 800),
curve: Curves.elasticInOut,
left: MediaQuery.of(context).size.width / 2 - 50,
bottom: _selectedList.length == 0 ? -100 : 30,
child: InkWell(
onTap: () => _delSelectedEpisodes(),
child: Stack(
alignment: _clearing
? Alignment.centerLeft
: Alignment.centerRight,
children: <Widget>[
Container(
alignment: Alignment.center,
width: 100,
height: 40,
decoration: BoxDecoration(
borderRadius:
BorderRadius.all(Radius.circular(20.0)),
color: Theme.of(context).accentColor,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Icon(
LineIcons.trash_alt_solid,
color: Colors.white,
),
Text((sumSelected() ~/ 1000000).toString() + 'Mb',
style: TextStyle(color: Colors.white)),
],
),
),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: AnimatedContainer(
duration: Duration(milliseconds: 500),
alignment: Alignment.center,
width: _clearing ? 100 : 0,
height: _clearing ? 40 : 0,
decoration: BoxDecoration(
borderRadius:
BorderRadius.all(Radius.circular(20.0)),
color: Colors.red.withOpacity(0.6),
),
),
),
],
)),
),
],
),
),
),
);
}
}

390
lib/settings/history.dart Normal file
View File

@ -0,0 +1,390 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:intl/intl.dart';
import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
import 'package:tsacdop/class/audiostate.dart';
import 'package:tsacdop/class/sub_history.dart';
class PlayedHistory extends StatefulWidget {
@override
_PlayedHistoryState createState() => _PlayedHistoryState();
}
class _PlayedHistoryState extends State<PlayedHistory>
with SingleTickerProviderStateMixin {
Future<List<PlayHistory>> getPlayHistory() async {
DBHelper dbHelper = DBHelper();
List<PlayHistory> playHistory;
playHistory = await dbHelper.getPlayHistory();
await Future.forEach(playHistory, (playHistory) async {
await playHistory.getEpisode();
});
return playHistory;
}
Future<List<SubHistory>> getSubHistory() async {
DBHelper dbHelper = DBHelper();
return await dbHelper.getSubHistory();
}
static String _stringForSeconds(double seconds) {
if (seconds == null) return null;
return '${(seconds ~/ 60)}:${(seconds.truncate() % 60).toString().padLeft(2, '0')}';
}
TabController _controller;
List<int> list = const [0, 1, 2, 3, 4, 5, 6];
Future<List<FlSpot>> getData() async {
var dbHelper = DBHelper();
List<FlSpot> stats = [];
await Future.forEach(list, (day) async {
double mins = await dbHelper.listenMins(7 - day);
stats.add(FlSpot(day.toDouble(), mins));
});
return stats;
}
@override
void initState() {
super.initState();
_controller = TabController(length: 2, vsync: this);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
double top = 0;
@override
Widget build(BuildContext context) {
return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle(
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
systemNavigationBarColor: Theme.of(context).primaryColor,
statusBarColor: Theme.of(context).primaryColor,
),
child: SafeArea(
child: Scaffold(
body: NestedScrollView(
headerSliverBuilder:
(BuildContext context, bool innerBoxScrolled) {
return <Widget>[
SliverAppBar(
elevation: 0,
expandedHeight: 260,
floating: false,
pinned: true,
flexibleSpace: LayoutBuilder(
builder:
(BuildContext context, BoxConstraints constraints) {
top = constraints.biggest.height;
return FlexibleSpaceBar(
title: top < 70
? Text(
'History',
)
: Center(),
background: Padding(
padding: EdgeInsets.only(
top: 50, left: 50, right: 50, bottom: 30),
child: FutureBuilder<List<FlSpot>>(
future: getData(),
builder: (context, snapshot) {
return snapshot.hasData
? HistoryChart(snapshot.data)
: Center();
}),
),
);
},
),
),
SliverPersistentHeader(
delegate: _SliverAppBarDelegate(
TabBar(
controller: _controller,
tabs: <Widget>[
Tab(
child: Text('Listen'),
),
Tab(
child: Text('Subscribe'),
)
],
),
Theme.of(context).primaryColor),
pinned: true,
),
];
},
body: TabBarView(controller: _controller, children: <Widget>[
FutureBuilder<List<PlayHistory>>(
future: getPlayHistory(),
builder: (context, snapshot) {
double _width = MediaQuery.of(context).size.width;
return snapshot.hasData
? ListView.builder(
shrinkWrap: true,
scrollDirection: Axis.vertical,
itemCount: snapshot.data.length,
itemBuilder: (BuildContext context, int index) {
return Column(
children: <Widget>[
ListTile(
title: Column(
mainAxisAlignment:
MainAxisAlignment.start,
crossAxisAlignment:
CrossAxisAlignment.start,
children: <Widget>[
Text(
DateFormat.yMd().add_jm().format(
snapshot.data[index].playdate),
style: TextStyle(
color: const Color(0xff67727d),
fontSize: 15,
fontStyle: FontStyle.italic),
),
Text(
snapshot.data[index].title,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
subtitle: Container(
width: _width,
child: Row(
children: <Widget>[
Icon(Icons.timelapse, color: Colors.grey[400],),
Container(
height: 2,
decoration: BoxDecoration(
border: Border(bottom: BorderSide(color: Colors.grey[400], width: 2.0))
),
width: _width *
snapshot.data[index]
.seekValue <
(_width - 120)
? _width *
snapshot
.data[index].seekValue
: _width - 120,
),
Padding(
padding: EdgeInsets.symmetric(
horizontal: 2),
),
Container(
width: 50,
alignment: Alignment.center,
decoration: BoxDecoration(
color: Theme.of(context)
.accentColor,
borderRadius: BorderRadius.all(
Radius.circular(10))),
padding: EdgeInsets.all(2),
child: Text(
_stringForSeconds(
snapshot.data[index].seconds),
style: TextStyle(
color: Colors.white),
),
),
],
),
),
),
// Divider(height: 2),
],
);
})
: Center(
child: CircularProgressIndicator(),
);
},
),
FutureBuilder<List<SubHistory>>(
future: getSubHistory(),
builder: (context, snapshot) {
return snapshot.hasData
? ListView.builder(
shrinkWrap: true,
scrollDirection: Axis.vertical,
itemCount: snapshot.data.length,
itemBuilder: (BuildContext context, int index) {
bool _status = snapshot.data[index].status;
return Column(
children: <Widget>[
ListTile(
enabled: _status,
title: Column(
mainAxisAlignment:
MainAxisAlignment.start,
crossAxisAlignment:
CrossAxisAlignment.start,
children: <Widget>[
Text(
DateFormat.yMd().add_jm().format(
snapshot.data[index].subDate),
style: TextStyle(
color: const Color(0xff67727d),
fontSize: 15,
fontStyle: FontStyle.italic),
),
Text(snapshot.data[index].title),
],
),
subtitle: Row(
children: <Widget>[
_status
? Text(DateTime.now()
.difference(snapshot
.data[index].subDate)
.inDays
.toString() +
' days')
: Text(snapshot.data[index].delDate
.difference(snapshot
.data[index].subDate)
.inDays
.toString() +
' days'),
Spacer(),
!_status
? Text(
'Removed at ' +
DateFormat.yMd()
.add_jm()
.format(snapshot
.data[index]
.delDate),
style: TextStyle(
color: Colors.red),
)
: Center(),
],
),
),
Divider(
height: 2,
)
],
);
})
: Center(
child: CircularProgressIndicator(),
);
},
),
])),
),
),
);
}
}
class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
_SliverAppBarDelegate(this._tabBar, this._color);
final Color _color;
final TabBar _tabBar;
@override
double get minExtent => _tabBar.preferredSize.height;
@override
double get maxExtent => _tabBar.preferredSize.height;
@override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return new Container(
color: _color,
child: _tabBar,
);
}
@override
bool shouldRebuild(_SliverAppBarDelegate oldDelegate) {
return true;
}
}
class HistoryChart extends StatelessWidget {
final List<FlSpot> stats;
HistoryChart(this.stats);
@override
Widget build(BuildContext context) {
return LineChart(
LineChartData(
backgroundColor: Colors.transparent,
gridData: FlGridData(
show: true,
drawHorizontalLine: true,
getDrawingHorizontalLine: (value) {
return value % 60 == 0
? FlLine(
color: Theme.of(context).brightness == Brightness.light
? Colors.grey[400]
: Colors.grey[700],
strokeWidth: 1,
)
: FlLine(color: Colors.transparent);
},
),
titlesData: FlTitlesData(
show: true,
bottomTitles: SideTitles(
textStyle: TextStyle(
color: const Color(0xff67727d),
fontWeight: FontWeight.bold,
fontSize: 12,
),
showTitles: true,
reservedSize: 10,
getTitles: (value) {
return DateFormat.E().format(
DateTime.now().subtract(Duration(days: (7 - value.toInt()))));
},
margin: 5,
),
leftTitles: SideTitles(
showTitles: true,
textStyle: TextStyle(
color: const Color(0xff67727d),
fontWeight: FontWeight.bold,
fontSize: 12,
),
getTitles: (value) {
return value % 60 == 0 && value > 0 ? '${value ~/ 60}h' : '';
},
reservedSize: 20,
margin: 5,
),
),
borderData: FlBorderData(
show: false,
border: Border(
left: BorderSide(color: Colors.red, width: 2),
)),
lineBarsData: [
LineChartBarData(
spots: this.stats,
isCurved: false,
colors: [Theme.of(context).accentColor],
barWidth: 3,
isStrokeCapRound: true,
dotData: FlDotData(
show: true,
dotSize: 5,
dotColor: Theme.of(context).accentColor,
),
),
],
),
);
}
}

90
lib/settings/libries.dart Normal file
View File

@ -0,0 +1,90 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:url_launcher/url_launcher.dart';
import 'licenses.dart';
class Libries extends StatelessWidget {
_launchUrl(String url) async {
if (await canLaunch(url)) {
await launch(url);
} else {
throw 'Could not launch $url';
}
}
@override
Widget build(BuildContext context) {
return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle(
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
systemNavigationBarColor: Theme.of(context).primaryColor,
statusBarColor: Theme.of(context).primaryColor),
child: SafeArea(
child: Scaffold(
appBar: AppBar(
title: Text('Libraies'),
elevation: 0,
backgroundColor: Theme.of(context).primaryColor,
),
body: SingleChildScrollView(
scrollDirection: Axis.vertical,
child: 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('Google',
style: Theme.of(context)
.textTheme
.bodyText1
.copyWith(color: Theme.of(context).accentColor)),
),
Column(
children: google.map<Widget>(
(e) {
return ListTile(
contentPadding: EdgeInsets.symmetric(horizontal: 80),
onTap: () => _launchUrl(e.link),
title: Text(e.name),
subtitle: Text(e.license),
);
},
).toList(),
),
Container(
height: 30.0,
padding: EdgeInsets.symmetric(horizontal: 80),
alignment: Alignment.centerLeft,
child: Text('Plugins',
style: Theme.of(context)
.textTheme
.bodyText1
.copyWith(color: Theme.of(context).accentColor)),
),
Container(
child: Column(
children: plugins.map<Widget>(
(e) {
return ListTile(
onTap: () => _launchUrl(e.link),
contentPadding: EdgeInsets.symmetric(horizontal: 80),
title: Text(e.name),
subtitle: Text(e.license),
);
},
).toList(),
),
),
],
),
),
),
),
);
}
}

View File

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

View File

@ -1,10 +1,30 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:workmanager/workmanager.dart';
import 'package:line_icons/line_icons.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:tsacdop/class/audiostate.dart';
import 'package:tsacdop/class/settingstate.dart';
import 'package:tsacdop/settings/theme.dart'; import 'package:tsacdop/settings/theme.dart';
import 'package:tsacdop/settings/storage.dart';
import 'package:tsacdop/settings/history.dart';
import 'libries.dart';
class Settings extends StatelessWidget { class Settings extends StatelessWidget {
_launchUrl(String url) async {
if (await canLaunch(url)) {
await launch(url);
} else {
throw 'Could not launch $url';
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
var settings = Provider.of<SettingState>(context, listen: false);
return AnnotatedRegion<SystemUiOverlayStyle>( return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle( value: SystemUiOverlayStyle(
statusBarIconBrightness: Theme.of(context).accentColorBrightness, statusBarIconBrightness: Theme.of(context).accentColorBrightness,
@ -18,121 +38,167 @@ class Settings extends StatelessWidget {
elevation: 0, elevation: 0,
backgroundColor: Theme.of(context).primaryColor, backgroundColor: Theme.of(context).primaryColor,
), ),
body: Column( body: SingleChildScrollView(
mainAxisAlignment: MainAxisAlignment.start, physics: const AlwaysScrollableScrollPhysics(),
mainAxisSize: MainAxisSize.min, scrollDirection: Axis.vertical,
children: <Widget>[ child: Column(
Column( mainAxisAlignment: MainAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start, mainAxisSize: MainAxisSize.min,
mainAxisSize: MainAxisSize.min, children: <Widget>[
children: <Widget>[ Column(
Padding( mainAxisAlignment: MainAxisAlignment.start,
padding: EdgeInsets.all(10.0), mainAxisSize: MainAxisSize.min,
), children: <Widget>[
Container( Padding(
height: 30.0, padding: EdgeInsets.all(10.0),
padding: EdgeInsets.symmetric(horizontal: 80), ),
alignment: Alignment.centerLeft, Container(
child: Text('Prefrence', height: 30.0,
style: Theme.of(context) padding: EdgeInsets.symmetric(horizontal: 80),
.textTheme alignment: Alignment.centerLeft,
.bodyText1 child: Text('Prefrence',
.copyWith(color: Theme.of(context).accentColor)), style: Theme.of(context)
), .textTheme
ListView( .bodyText1
shrinkWrap: true, .copyWith(color: Theme.of(context).accentColor)),
scrollDirection: Axis.vertical, ),
children: <Widget>[ ListView(
ListTile( physics: ClampingScrollPhysics(),
onTap: () => Navigator.push( shrinkWrap: true,
context, scrollDirection: Axis.vertical,
MaterialPageRoute( children: <Widget>[
builder: (context) => ThemeSetting())), ListTile(
contentPadding: EdgeInsets.symmetric(horizontal: 25.0), onTap: () => Navigator.push(
leading: Icon(Icons.colorize), context,
title: Text('Appearance'), MaterialPageRoute(
subtitle: Text('Colors and themes'), builder: (context) => ThemeSetting())),
), contentPadding:
Divider(height: 2), EdgeInsets.symmetric(horizontal: 25.0),
ListTile( leading: Icon(LineIcons.adjust_solid),
contentPadding: EdgeInsets.symmetric(horizontal: 25.0), title: Text('Appearance'),
leading: Icon(Icons.network_check), subtitle: Text('Colors and themes'),
title: Text('Network'), ),
subtitle: Text('Download network setting'), Divider(height: 2),
), ListTile(
Divider(height: 2), contentPadding:
ListTile( EdgeInsets.symmetric(horizontal: 25.0),
contentPadding: EdgeInsets.symmetric(horizontal: 25.0), leading: Icon(LineIcons.play_circle),
leading: Icon(Icons.storage), title: Text('AutoPlay'),
title: Text('Cache'), subtitle: Text('Autoplay next episode in playlist'),
subtitle: Text('Manage and clear cache'), trailing: Selector<AudioPlayerNotifier, bool>(
), selector: (_, audio) => audio.autoPlay,
Divider(height: 2), builder: (_, data, __) => Switch(
ListTile( value: data,
contentPadding: EdgeInsets.symmetric(horizontal: 25.0), onChanged: (boo) => audio.autoPlaySwitch = boo),
leading: Icon(Icons.update), ),
title: Text('Update'), ),
subtitle: Text('Update in background'), Divider(height: 2),
), ListTile(
Divider(height: 2), contentPadding:
], EdgeInsets.symmetric(horizontal: 25.0),
), leading: Icon(LineIcons.cloud_download_alt_solid),
], title: Text('AutoUpdate'),
), subtitle: Text('Auto update feed every day'),
Padding( trailing: Selector<SettingState, bool>(
padding: EdgeInsets.all(10.0), selector: (_, settings) => settings.autoUpdate,
), builder: (_, data, __) => Switch(
Column( value: data,
mainAxisAlignment: MainAxisAlignment.start, onChanged: (boo) async {
mainAxisSize: MainAxisSize.min, settings.autoUpdate = boo;
children: <Widget>[ if (!boo) await Workmanager.cancelAll();
Container( }),
height: 30.0, ),
padding: EdgeInsets.symmetric(horizontal: 80), ),
alignment: Alignment.centerLeft, Divider(height: 2),
child: Text('Info', ListTile(
style: Theme.of(context) onTap: () => Navigator.push(
.textTheme context,
.bodyText1 MaterialPageRoute(
.copyWith(color: Theme.of(context).accentColor)), builder: (context) => StorageSetting())),
), contentPadding:
ListView( EdgeInsets.symmetric(horizontal: 25.0),
shrinkWrap: true, leading: Icon(LineIcons.save),
scrollDirection: Axis.vertical, title: Text('Storage'),
children: <Widget>[ subtitle: Text('Manage cache and download storage'),
ListTile( ),
contentPadding: EdgeInsets.symmetric(horizontal: 25.0), Divider(height: 2),
leading: Icon(Icons.colorize), ListTile(
title: Text('Changelog'), onTap: () => Navigator.push(
subtitle: Text('List of chagnes'), context,
), MaterialPageRoute(
Divider(height: 2), builder: (context) => PlayedHistory())),
ListTile( contentPadding:
contentPadding: EdgeInsets.symmetric(horizontal: 25.0), EdgeInsets.symmetric(horizontal: 25.0),
leading: Icon(Icons.network_check), leading: Icon(Icons.update),
title: Text('Credit'), title: Text('History'),
subtitle: Text('Open source libraried in application'), subtitle: Text('Listen data'),
), ),
Divider(height: 2), Divider(height: 2),
ListTile( ],
contentPadding: EdgeInsets.symmetric(horizontal: 25.0), ),
leading: Icon(Icons.storage), ],
title: Text('Cache'), ),
subtitle: Text('Manage and clear cache'), Padding(
), padding: EdgeInsets.all(10.0),
Divider(height: 2), ),
ListTile( Column(
contentPadding: EdgeInsets.symmetric(horizontal: 25.0), mainAxisAlignment: MainAxisAlignment.start,
leading: Icon(Icons.update), mainAxisSize: MainAxisSize.min,
title: Text('Update'), children: <Widget>[
subtitle: Text('Update in background'), Container(
), height: 30.0,
Divider(height: 2), padding: EdgeInsets.symmetric(horizontal: 80),
], alignment: Alignment.centerLeft,
), child: Text('Info',
], style: Theme.of(context)
), .textTheme
], .bodyText1
.copyWith(color: Theme.of(context).accentColor)),
),
ListView(
physics: ClampingScrollPhysics(),
shrinkWrap: true,
scrollDirection: Axis.vertical,
children: <Widget>[
ListTile(
onTap: () => _launchUrl(
'https://github.com/stonega/tsacdop/releases'),
contentPadding:
EdgeInsets.symmetric(horizontal: 25.0),
leading: Icon(LineIcons.map_signs_solid),
title: Text('Changelog'),
subtitle: Text('List of chagnes'),
),
Divider(height: 2),
ListTile(
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => Libries())),
contentPadding:
EdgeInsets.symmetric(horizontal: 25.0),
leading: Icon(LineIcons.book_open_solid),
title: Text('Libraries'),
subtitle:
Text('Open source libraried in application'),
),
Divider(height: 2),
ListTile(
onTap: () => _launchUrl(
'mailto:<xijieyin@gmail.com>?subject=Tsacdop Feedback'),
contentPadding:
EdgeInsets.symmetric(horizontal: 25.0),
leading: Icon(LineIcons.bug_solid),
title: Text('Feedback'),
subtitle: Text('Bugs and feature requests'),
),
Divider(height: 2),
],
),
],
),
],
),
), ),
), ),
), ),

74
lib/settings/storage.dart Normal file
View File

@ -0,0 +1,74 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:app_settings/app_settings.dart';
import 'package:tsacdop/settings/downloads_manage.dart';
class StorageSetting extends StatelessWidget {
@override
Widget build(BuildContext context) {
return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle(
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
systemNavigationBarColor: Theme.of(context).primaryColor,
statusBarColor: Theme.of(context).primaryColor),
child: SafeArea(
child: Scaffold(
appBar: AppBar(
title: Text('Storage'),
elevation: 0,
backgroundColor: Theme.of(context).primaryColor,
),
body: Column(
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('Storage',
style: Theme.of(context)
.textTheme
.bodyText1
.copyWith(color: Theme.of(context).accentColor)),
),
ListView(
shrinkWrap: true,
scrollDirection: Axis.vertical,
children: <Widget>[
ListTile(
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DownloadsManage())),
contentPadding: EdgeInsets.symmetric(horizontal: 80.0),
title: Text('Downloads'),
subtitle: Text('Manage doanloaded audio files'),
),
Divider(height: 2),
ListTile(
onTap: () => AppSettings.openAppSettings(),
contentPadding: EdgeInsets.symmetric(horizontal: 80.0),
// leading: Icon(Icons.colorize),
title: Text('Cache'),
subtitle: Text('Audio cache'),
),
Divider(height: 2),
],
),
],
),
],
),
),
),
);
}
}

View File

@ -7,7 +7,7 @@ import 'package:tsacdop/class/settingstate.dart';
class ThemeSetting extends StatelessWidget { class ThemeSetting extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var settings = Provider.of<SettingState>(context); var settings = Provider.of<SettingState>(context, listen: false);
return AnnotatedRegion<SystemUiOverlayStyle>( return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle( value: SystemUiOverlayStyle(
statusBarIconBrightness: Theme.of(context).accentColorBrightness, statusBarIconBrightness: Theme.of(context).accentColorBrightness,

View File

@ -22,7 +22,8 @@ extension Colorize on String {
_c = _c =
Color.fromRGBO((255 - color[0]), 255 - color[1], 255 - color[2], 1.0); Color.fromRGBO((255 - color[0]), 255 - color[1], 255 - color[2], 1.0);
} else { } else {
_c = Color.fromRGBO(color[0], color[1], color[2], 1.0); _c = Color.fromRGBO(color[0] < 50 ? 100 : color[0],
color[1] < 50 ? 100 : color[1], color[2] < 50 ? 100 : color[2], 1.0);
} }
return _c; return _c;
} }

View File

@ -5,6 +5,11 @@ import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_downloader/flutter_downloader.dart'; import 'package:flutter_downloader/flutter_downloader.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';
import 'package:tuple/tuple.dart';
import 'package:line_icons/line_icons.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:tsacdop/class/audiostate.dart';
import 'package:tsacdop/class/episodebrief.dart'; import 'package:tsacdop/class/episodebrief.dart';
import 'package:tsacdop/episodes/episodedetail.dart'; import 'package:tsacdop/episodes/episodedetail.dart';
import 'package:tsacdop/util/pageroute.dart'; import 'package:tsacdop/util/pageroute.dart';
@ -24,32 +29,119 @@ class EpisodeGrid extends StatelessWidget {
this.showNumber, this.showNumber,
this.heroTag}) this.heroTag})
: super(key: key); : super(key: key);
Offset offset;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
double _width = MediaQuery.of(context).size.width; double _width = MediaQuery.of(context).size.width;
return CustomScrollView(
physics: const AlwaysScrollableScrollPhysics(), _showPopupMenu(Offset offset, EpisodeBrief episode, BuildContext context,
primary: false, bool isPlaying, bool isInPlaylist) async {
slivers: <Widget>[ var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
SliverPadding( double left = offset.dx;
padding: const EdgeInsets.all(5.0), double top = offset.dy;
sliver: SliverGrid( await showMenu<int>(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( shape: RoundedRectangleBorder(
childAspectRatio: 1.0, borderRadius: BorderRadius.all(Radius.circular(10))),
crossAxisCount: 3, context: context,
mainAxisSpacing: 6.0, position: RelativeRect.fromLTRB(left, top, _width - left, 0),
crossAxisSpacing: 6.0,
items: <PopupMenuEntry<int>>[
PopupMenuItem(
value: 0,
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Icon(
LineIcons.play_circle_solid,
color: Theme.of(context).accentColor,
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 2),
),
!isPlaying ? Text('Play') : Text('Playing'),
],
), ),
delegate: SliverChildBuilderDelegate( ),
(BuildContext context, int index) { PopupMenuItem(
Color _c = value: 1,
(Theme.of(context).brightness == Brightness.light) child: Row(
? podcast[index].primaryColor.colorizedark() children: <Widget>[
: podcast[index].primaryColor.colorizeLight(); Icon(
return Material( LineIcons.clock_solid,
color: Colors.red,
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 2),
),
!isInPlaylist ? Text('Later') : Text('Remove')
],
)),
],
elevation: 5.0,
).then((value) {
if (value == 0) {
if (!isPlaying) audio.episodeLoad(episode);
} else if (value == 1) {
if (isInPlaylist) {
audio.addToPlaylist(episode);
Fluttertoast.showToast(
msg: 'Added to playlist',
gravity: ToastGravity.BOTTOM,
);
} else {
audio.delFromPlaylist(episode);
Fluttertoast.showToast(
msg: 'Removed from playlist',
gravity: ToastGravity.BOTTOM,
);
}
}
});
}
return SliverPadding(
padding: const EdgeInsets.all(5.0),
sliver: SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
childAspectRatio: 1.0,
crossAxisCount: 3,
mainAxisSpacing: 6.0,
crossAxisSpacing: 6.0,
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
Color _c = (Theme.of(context).brightness == Brightness.light)
? podcast[index].primaryColor.colorizedark()
: podcast[index].primaryColor.colorizeLight();
return Selector<AudioPlayerNotifier,
Tuple2<EpisodeBrief, List<String>>>(
selector: (_, audio) => Tuple2(audio?.episode,
audio.queue.playlist.map((e) => e.enclosureUrl).toList()),
builder: (_, data, __) => Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(5.0)),
color: Theme.of(context).scaffoldBackgroundColor,
boxShadow: [
BoxShadow(
color: Theme.of(context).primaryColor,
blurRadius: 0.5,
spreadRadius: 0.5,
),
]),
alignment: Alignment.center,
child: Material(
color: Colors.transparent, color: Colors.transparent,
child: InkWell( child: InkWell(
borderRadius: BorderRadius.all(Radius.circular(5.0)),
onTapDown: (details) => offset = Offset(
details.globalPosition.dx, details.globalPosition.dy),
onLongPress: () => _showPopupMenu(
offset,
podcast[index],
context,
data.item1 == podcast[index],
data.item2.contains(podcast[index].enclosureUrl)),
onTap: () { onTap: () {
Navigator.push( Navigator.push(
context, context,
@ -61,25 +153,17 @@ class EpisodeGrid extends StatelessWidget {
); );
}, },
child: Container( child: Container(
padding: const EdgeInsets.all(8.0),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(5.0)), borderRadius: BorderRadius.all(Radius.circular(5.0)),
color: Theme.of(context).scaffoldBackgroundColor, border: Border.all(
border: Border.all( color:
color: Theme.of(context).brightness == Brightness.light
Theme.of(context).brightness == Brightness.light ? Theme.of(context).primaryColor
? Theme.of(context).primaryColor : Theme.of(context).scaffoldBackgroundColor,
: Theme.of(context).scaffoldBackgroundColor, width: 1.0,
width: 3.0, ),
), ),
boxShadow: [
BoxShadow(
color: Theme.of(context).primaryColor,
blurRadius: 0.5,
spreadRadius: 0.5,
),
]),
alignment: Alignment.center,
padding: EdgeInsets.all(8.0),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[ children: <Widget>[
@ -94,6 +178,7 @@ class EpisodeGrid extends StatelessWidget {
height: _width / 16, height: _width / 16,
width: _width / 16, width: _width / 16,
child: CircleAvatar( child: CircleAvatar(
backgroundColor: _c.withOpacity(0.5),
backgroundImage: FileImage( backgroundImage: FileImage(
File("${podcast[index].imagePath}")), File("${podcast[index].imagePath}")),
), ),
@ -140,7 +225,6 @@ class EpisodeGrid extends StatelessWidget {
alignment: Alignment.bottomLeft, alignment: Alignment.bottomLeft,
child: Text( child: Text(
podcast[index].dateToString(), podcast[index].dateToString(),
//podcast[index].pubDate.substring(4, 16),
style: TextStyle( style: TextStyle(
fontSize: _width / 35, fontSize: _width / 35,
color: _c, color: _c,
@ -175,13 +259,13 @@ class EpisodeGrid extends StatelessWidget {
), ),
), ),
), ),
); ),
}, ),
childCount: podcast.length, );
), },
), childCount: podcast.length,
), ),
], ),
); );
} }
} }

BIN
preview/Screenshot_data.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

View File

@ -11,7 +11,7 @@ description: An easy-use podacasts player.
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at # Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 0.1.1 version: 0.1.2
environment: environment:
sdk: ">=2.6.0 <3.0.0" sdk: ">=2.6.0 <3.0.0"
@ -28,7 +28,7 @@ dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
json_annotation: ^3.0.1 json_annotation: ^3.0.1
sqflite: ^1.2.1 sqflite: ^1.2.2+1
flutter_html: ^0.11.1 flutter_html: ^0.11.1
path_provider: ^1.6.1 path_provider: ^1.6.1
color_thief_flutter: ^1.0.1 color_thief_flutter: ^1.0.1
@ -38,7 +38,6 @@ dev_dependencies:
file_picker: ^1.4.3+2 file_picker: ^1.4.3+2
xml: ^3.5.0 xml: ^3.5.0
marquee: ^1.3.1 marquee: ^1.3.1
audiofileplayer: ^1.1.1
flutter_downloader: ^1.4.1 flutter_downloader: ^1.4.1
permission_handler: ^4.3.0 permission_handler: ^4.3.0
fluttertoast: ^3.1.3 fluttertoast: ^3.1.3
@ -49,11 +48,16 @@ dev_dependencies:
uuid: ^2.0.4 uuid: ^2.0.4
tuple: ^1.0.3 tuple: ^1.0.3
cached_network_image: ^2.0.0 cached_network_image: ^2.0.0
workmanager: ^0.2.0 workmanager: ^0.2.2
font_awesome_flutter: ^8.7.0
flutter_colorpicker: ^0.3.2 flutter_colorpicker: ^0.3.2
lazy_loading_list: ^1.0.0+1 app_settings: ^3.0.1
fl_chart: ^0.8.3
audio_service: ^0.6.2
just_audio: ^0.1.3
rxdart: ^0.23.1
line_icons:
git:
url: https://github.com/galonsos/line_icons.git
# For information on the generic Dart part of this file, see the # For information on the generic Dart part of this file, see the