Change audio plugin to just_audio
Add storage setting Add history setting
|
@ -13,9 +13,7 @@ jobs:
|
|||
- run:
|
||||
name: Run Flutter doctor
|
||||
command: flutter doctor
|
||||
- run:
|
||||
name: flutter pub get
|
||||
command: flutter pub get
|
||||
|
||||
-run:
|
||||
name: flutter run
|
||||
command: flutter run
|
||||
|
|
14
README.md
|
@ -1,19 +1,21 @@
|
|||
# Tsacdop
|
||||
[![CircleCI](https://circleci.com/gh/stonega/tsacdop.svg?style=svg)](https://circleci.com/gh/stonega/workflows/tsacdop/)
|
||||
## About
|
||||
![logo](https://raw.githubusercontent.com/stonega/tsacdop/master/android/app/src/main/res/mipmap-hdpi/ic_launcher.png)
|
||||
|
||||
![tsacdop](https://raw.githubusercontent.com/stonega/tsacdop/master/android/app/src/main/res/mipmap-hdpi/text.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"/>
|
||||
<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.
|
||||
|
||||
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.
|
||||
|
||||
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).
|
||||
|
||||
## 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
|
||||
|
||||
Tsacdop is licensed under the [MIT](https://github.com/stonega/tsacdop/blob/master/LICENSE) license.
|
||||
|
|
|
@ -67,6 +67,7 @@ android {
|
|||
buildTypes {
|
||||
release {
|
||||
signingConfig signingConfigs.release
|
||||
shrinkResources false
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,15 +6,16 @@
|
|||
FlutterApplication and put your custom class here. -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<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">
|
||||
<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>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</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>
|
||||
<service android:name="com.google.flutter.plugins.audiofileplayer.AudiofileplayerService">
|
||||
<service android:name="com.ryanheise.audioservice.AudioService">
|
||||
<intent-filter>
|
||||
<action android:name="android.media.browse.MediaBrowserService" />
|
||||
</intent-filter>
|
||||
|
|
After Width: | Height: | Size: 212 B |
After Width: | Height: | Size: 217 B |
After Width: | Height: | Size: 140 B |
After Width: | Height: | Size: 272 B |
After Width: | Height: | Size: 251 B |
After Width: | Height: | Size: 257 B |
After Width: | Height: | Size: 102 B |
After Width: | Height: | Size: 184 B |
After Width: | Height: | Size: 163 B |
After Width: | Height: | Size: 108 B |
After Width: | Height: | Size: 159 B |
After Width: | Height: | Size: 156 B |
After Width: | Height: | Size: 166 B |
After Width: | Height: | Size: 92 B |
After Width: | Height: | Size: 264 B |
After Width: | Height: | Size: 259 B |
After Width: | Height: | Size: 162 B |
After Width: | Height: | Size: 288 B |
After Width: | Height: | Size: 260 B |
After Width: | Height: | Size: 265 B |
After Width: | Height: | Size: 114 B |
After Width: | Height: | Size: 341 B |
After Width: | Height: | Size: 302 B |
After Width: | Height: | Size: 202 B |
After Width: | Height: | Size: 550 B |
After Width: | Height: | Size: 460 B |
After Width: | Height: | Size: 450 B |
After Width: | Height: | Size: 196 B |
After Width: | Height: | Size: 435 B |
After Width: | Height: | Size: 411 B |
After Width: | Height: | Size: 244 B |
After Width: | Height: | Size: 488 B |
After Width: | Height: | Size: 509 B |
After Width: | Height: | Size: 522 B |
After Width: | Height: | Size: 244 B |
After Width: | Height: | Size: 4.4 KiB |
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 5.3 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 14 KiB |
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<resources>
|
||||
<color name = "blackGrey">
|
||||
#121212
|
||||
</color>
|
||||
</resources>
|
|
@ -1,7 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<network-security-config>
|
||||
<domain-config cleartextTrafficPermitted="true">
|
||||
<domain includeSubdomains="true">lizhi.fm</domain>
|
||||
<domain includeSubdomains="true">xmcdn.com</domain>
|
||||
</domain-config>
|
||||
<base-config cleartextTrafficPermitted="true">
|
||||
<trust-anchors>
|
||||
<certificates src="system" />
|
||||
</trust-anchors>
|
||||
</base-config>
|
||||
</network-security-config>
|
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 14 KiB |
|
@ -1,17 +1,46 @@
|
|||
import 'dart:typed_data';
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
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: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/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 {
|
||||
DBHelper dbHelper = DBHelper();
|
||||
|
@ -19,7 +48,9 @@ class PlayHistory {
|
|||
String url;
|
||||
double seconds;
|
||||
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 get episode => _episode;
|
||||
|
||||
|
@ -31,32 +62,36 @@ class PlayHistory {
|
|||
class Playlist {
|
||||
String name;
|
||||
DBHelper dbHelper = DBHelper();
|
||||
List<String> urls;
|
||||
// list of urls
|
||||
//List<String> _urls;
|
||||
//list of episodes
|
||||
List<EpisodeBrief> _playlist;
|
||||
//list of miediaitem
|
||||
|
||||
List<EpisodeBrief> get playlist => _playlist;
|
||||
KeyValueStorage storage = KeyValueStorage('playlist');
|
||||
Playlist(this.name, {List<String> urls}) : urls = urls ?? [];
|
||||
|
||||
getPlaylist() async {
|
||||
List<String> _urls = await storage.getStringList();
|
||||
if (_urls.length == 0) {
|
||||
List<String> urls = await storage.getStringList();
|
||||
print(urls);
|
||||
if (urls.length == 0) {
|
||||
_playlist = [];
|
||||
} else {
|
||||
_playlist = [];
|
||||
await Future.forEach(_urls, (url) async {
|
||||
await Future.forEach(urls, (url) async {
|
||||
EpisodeBrief episode = await dbHelper.getRssItemWithUrl(url);
|
||||
print(episode.title);
|
||||
_playlist.add(episode);
|
||||
});
|
||||
}
|
||||
print(_playlist.length);
|
||||
print('Playlist: ' + _playlist.length.toString());
|
||||
}
|
||||
|
||||
savePlaylist() async {
|
||||
urls = [];
|
||||
List<String> urls = [];
|
||||
urls.addAll(_playlist.map((e) => e.enclosureUrl));
|
||||
print(urls);
|
||||
await storage.saveStringlist(urls);
|
||||
await storage.saveStringList(urls);
|
||||
}
|
||||
|
||||
addToPlayList(EpisodeBrief episodeBrief) async {
|
||||
|
@ -64,6 +99,11 @@ class Playlist {
|
|||
await savePlaylist();
|
||||
}
|
||||
|
||||
addToPlayListAt(EpisodeBrief episodeBrief, int index) async {
|
||||
_playlist.insert(index, episodeBrief);
|
||||
await savePlaylist();
|
||||
}
|
||||
|
||||
delFromPlaylist(EpisodeBrief episodeBrief) async {
|
||||
_playlist
|
||||
.removeWhere((item) => item.enclosureUrl == episodeBrief.enclosureUrl);
|
||||
|
@ -71,39 +111,32 @@ class Playlist {
|
|||
}
|
||||
}
|
||||
|
||||
class AudioPlayer 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';
|
||||
|
||||
class AudioPlayerNotifier extends ChangeNotifier {
|
||||
DBHelper dbHelper = DBHelper();
|
||||
KeyValueStorage storage = KeyValueStorage('audioposition');
|
||||
EpisodeBrief _episode;
|
||||
Playlist _queue = Playlist('now');
|
||||
Playlist _queue = Playlist();
|
||||
BasicPlaybackState _audioState = BasicPlaybackState.none;
|
||||
bool _playerRunning = false;
|
||||
Audio _backgroundAudio;
|
||||
bool _backgroundAudioPlaying = false;
|
||||
double _backgroundAudioDurationSeconds = 0;
|
||||
double _backgroundAudioPositionSeconds = 0;
|
||||
bool _remoteAudioLoading = false;
|
||||
bool _noSlide = true;
|
||||
int _backgroundAudioDuration = 0;
|
||||
int _backgroundAudioPosition = 0;
|
||||
String _remoteErrorMessage;
|
||||
|
||||
double _seekSliderValue = 0.0;
|
||||
int _lastPostion;
|
||||
bool _skip = false;
|
||||
int _lastPostion = 0;
|
||||
bool _stopOnComplete = false;
|
||||
Timer _stopTimer;
|
||||
//Show stopwatch after user setting timer.
|
||||
bool _showStopWatch = false;
|
||||
|
||||
|
||||
final Logger _logger = Logger('audiofileplayer');
|
||||
bool _showStopWatch = false;
|
||||
bool _autoPlay = true;
|
||||
DateTime _current;
|
||||
int _currentPosition;
|
||||
|
||||
bool get backgroundAudioPlaying => _backgroundAudioPlaying;
|
||||
bool get remoteAudioLoading => _remoteAudioLoading;
|
||||
double get backgroundAudioDuration => _backgroundAudioDurationSeconds;
|
||||
double get backgroundAudioPosition => _backgroundAudioPositionSeconds;
|
||||
BasicPlaybackState get audioState => _audioState;
|
||||
|
||||
int get backgroundAudioDuration => _backgroundAudioDuration;
|
||||
int get backgroundAudioPosition => _backgroundAudioPosition;
|
||||
double get seekSliderValue => _seekSliderValue;
|
||||
String get remoteErrorMessage => _remoteErrorMessage;
|
||||
bool get playerRunning => _playerRunning;
|
||||
|
@ -112,385 +145,470 @@ class AudioPlayer extends ChangeNotifier {
|
|||
EpisodeBrief get episode => _episode;
|
||||
bool get stopOnComplete => _stopOnComplete;
|
||||
bool get showStopWatch => _showStopWatch;
|
||||
|
||||
bool get autoPlay => _autoPlay;
|
||||
|
||||
set setStopOnComplete(bool boo) {
|
||||
_stopOnComplete = boo;
|
||||
}
|
||||
|
||||
set autoPlaySwitch(bool boo) {
|
||||
_autoPlay = boo;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
void addListener(VoidCallback listener) async {
|
||||
super.addListener(listener);
|
||||
await AudioService.connect();
|
||||
}
|
||||
|
||||
loadPlaylist() async {
|
||||
await _queue.getPlaylist();
|
||||
_lastPostion = await storage.getInt();
|
||||
}
|
||||
|
||||
episodeLoad(EpisodeBrief episode) async {
|
||||
if (_playerRunning && _episode != null) {
|
||||
if (_playerRunning) {
|
||||
PlayHistory history = PlayHistory(_episode.title, _episode.enclosureUrl,
|
||||
backgroundAudioDuration, seekSliderValue);
|
||||
backgroundAudioPosition / 1000, seekSliderValue);
|
||||
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;
|
||||
await _queue.getPlaylist();
|
||||
_queue.playlist
|
||||
.removeWhere((item) => item.enclosureUrl == _episode.enclosureUrl);
|
||||
_queue.playlist.insert(0, _episode);
|
||||
await _queue.savePlaylist();
|
||||
await _play(_episode);
|
||||
}
|
||||
|
||||
_startAudioService(int position) async {
|
||||
if (!AudioService.connected) {
|
||||
await AudioService.connect();
|
||||
}
|
||||
await AudioService.start(
|
||||
backgroundTaskEntrypoint: _audioPlayerTaskEntrypoint,
|
||||
androidNotificationChannelName: 'Tsacdop',
|
||||
notificationColor: 0xFF2196f3,
|
||||
androidNotificationIcon: 'mipmap/ic_launcher',
|
||||
enableQueue: true,
|
||||
androidStopOnRemoveTask: 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 {
|
||||
_backgroundAudioPlaying = false;
|
||||
await _queue.getPlaylist();
|
||||
_backgroundAudioDuration = 0;
|
||||
_backgroundAudioPosition = 0;
|
||||
_seekSliderValue = 0;
|
||||
_episode = _queue.playlist.first;
|
||||
_skip = true;
|
||||
await _play(_episode);
|
||||
_playerRunning = true;
|
||||
notifyListeners();
|
||||
_startAudioService(_lastPostion ?? 0);
|
||||
}
|
||||
|
||||
playNext() async {
|
||||
storage.saveInt(0);
|
||||
_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();
|
||||
}
|
||||
AudioService.skipToNext();
|
||||
}
|
||||
|
||||
addToPlaylist(EpisodeBrief episode) async {
|
||||
_queue.addToPlayList(episode);
|
||||
await _queue.getPlaylist();
|
||||
if (_playerRunning) {
|
||||
await AudioService.addQueueItem(episode.toMediaItem());
|
||||
}
|
||||
print('add to playlist when not rnnning');
|
||||
await _queue.addToPlayList(episode);
|
||||
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 {
|
||||
_queue.delFromPlaylist(episode);
|
||||
await _queue.getPlaylist();
|
||||
if (_playerRunning) {
|
||||
await AudioService.removeQueueItem(episode.toMediaItem());
|
||||
}
|
||||
await _queue.delFromPlaylist(episode);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
pauseAduio() async {
|
||||
_pauseBackgroundAudio();
|
||||
notifyListeners();
|
||||
PlayHistory history = PlayHistory(_episode.title, _episode.enclosureUrl,
|
||||
backgroundAudioPosition, seekSliderValue);
|
||||
await dbHelper.saveHistory(history);
|
||||
AudioService.pause();
|
||||
}
|
||||
|
||||
resumeAudio() {
|
||||
_resumeBackgroundAudio();
|
||||
notifyListeners();
|
||||
resumeAudio() async {
|
||||
AudioService.play();
|
||||
}
|
||||
|
||||
forwardAudio(double s) {
|
||||
_forwardBackgroundAudio(s);
|
||||
notifyListeners();
|
||||
forwardAudio(int s) {
|
||||
int pos = _backgroundAudioPosition + s * 1000;
|
||||
AudioService.seekTo(pos);
|
||||
}
|
||||
|
||||
sliderSeek(double val) {
|
||||
sliderSeek(double val) async {
|
||||
print(val.toString());
|
||||
_noSlide = false;
|
||||
_seekSliderValue = val;
|
||||
notifyListeners();
|
||||
final double positionSeconds = val * _backgroundAudioDurationSeconds;
|
||||
_backgroundAudio.seek(positionSeconds);
|
||||
AudioSystem.instance.setPlaybackState(true, positionSeconds);
|
||||
_currentPosition = (val * _backgroundAudioDuration).toInt();
|
||||
await AudioService.seekTo(_currentPosition);
|
||||
_noSlide = true;
|
||||
}
|
||||
//Set sleep time
|
||||
|
||||
//Set sleep time
|
||||
sleepTimer(int mins) {
|
||||
_showStopWatch = true;
|
||||
notifyListeners();
|
||||
_stopTimer = Timer(Duration(minutes: mins),(){
|
||||
_stopTimer = Timer(Duration(minutes: mins), () {
|
||||
_stopOnComplete = false;
|
||||
_backgroundAudioPlaying = false;
|
||||
_remoteAudioLoading = false;
|
||||
_playerRunning = false;
|
||||
_showStopWatch = false;
|
||||
_disposeAudio();
|
||||
AudioService.stop();
|
||||
notifyListeners();
|
||||
});
|
||||
}
|
||||
|
||||
//Cancel sleep timer
|
||||
cancelTimer(){
|
||||
cancelTimer() {
|
||||
_stopTimer.cancel();
|
||||
_showStopWatch = false;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
_disposeAudio() {
|
||||
pauseAduio();
|
||||
AudioSystem.instance?.stopBackgroundDisplay();
|
||||
AudioSystem.instance?.removeMediaEventListener(_mediaEventListener);
|
||||
_backgroundAudio?.dispose();
|
||||
@override
|
||||
void dispose() async {
|
||||
await AudioService.stop();
|
||||
await AudioService.disconnect();
|
||||
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
|
||||
dispose() {
|
||||
_disposeAudio();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
_play(EpisodeBrief episodeBrief) async {
|
||||
AudioSystem.instance.addMediaEventListener(_mediaEventListener);
|
||||
String url = _queue.playlist.first.enclosureUrl;
|
||||
_getFile(url).then((result) {
|
||||
result == 'NotDownload'
|
||||
? _initbackgroundAudioPlayer(url)
|
||||
: _initbackgroundAudioPlayerLocal(result);
|
||||
Future<void> onStart() async {
|
||||
print('start background task');
|
||||
var playerStateSubscription = _audioPlayer.playbackStateStream
|
||||
.where((state) => state == AudioPlaybackState.completed)
|
||||
.listen((state) {
|
||||
_handlePlaybackCompleted();
|
||||
});
|
||||
}
|
||||
|
||||
Future<String> _getFile(String url) async {
|
||||
final task = await FlutterDownloader.loadTasksWithRawQuery(
|
||||
query: "SELECT * FROM task WHERE url = '$url' AND status = 3");
|
||||
if (task.length != 0) {
|
||||
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();
|
||||
var eventSubscription = _audioPlayer.playbackEventStream.listen((event) {
|
||||
BasicPlaybackState state;
|
||||
if (event.buffering) {
|
||||
state = BasicPlaybackState.buffering;
|
||||
} else {
|
||||
state = _stateToBasicState(event.state);
|
||||
}
|
||||
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 {
|
||||
final Uint8List imageBytes =
|
||||
File('${_episode.imagePath}').readAsBytesSync();
|
||||
AudioSystem.instance.setMetadata(AudioMetadata(
|
||||
title: episode.title,
|
||||
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);
|
||||
void playPause() {
|
||||
if (AudioServiceBackground.state.basicState == BasicPlaybackState.playing)
|
||||
onPause();
|
||||
else
|
||||
onPlay();
|
||||
}
|
||||
|
||||
Future<void> _resumeBackgroundAudio() async {
|
||||
_backgroundAudio.resume();
|
||||
|
||||
_backgroundAudioPlaying = true;
|
||||
|
||||
final Uint8List imageBytes =
|
||||
File('${_episode.imagePath}').readAsBytesSync();
|
||||
AudioSystem.instance.setMetadata(AudioMetadata(
|
||||
title: _episode.title,
|
||||
artist: _episode.feedTitle,
|
||||
album: _episode.feedTitle,
|
||||
genre: "Podcast",
|
||||
durationSeconds: _backgroundAudioDurationSeconds,
|
||||
artBytes: imageBytes));
|
||||
|
||||
AudioSystem.instance
|
||||
.setPlaybackState(true, _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);
|
||||
@override
|
||||
Future<void> onSkipToNext() async {
|
||||
if (_playing == null) {
|
||||
// First time, we want to start playing
|
||||
_playing = true;
|
||||
} else {
|
||||
// Stop current item
|
||||
await _audioPlayer.stop();
|
||||
_queue.removeAt(0);
|
||||
}
|
||||
AudioServiceBackground.setQueue(_queue);
|
||||
AudioServiceBackground.setMediaItem(mediaItem);
|
||||
_skipState = BasicPlaybackState.skippingToNext;
|
||||
await _audioPlayer.setUrl(mediaItem.id);
|
||||
print(mediaItem.id);
|
||||
Duration duration = await _audioPlayer.durationFuture;
|
||||
AudioServiceBackground.setMediaItem(
|
||||
mediaItem.copyWith(duration: duration.inMilliseconds));
|
||||
_skipState = null;
|
||||
// Resume playback if we were playing
|
||||
if (_playing) {
|
||||
onPlay();
|
||||
} else {
|
||||
_setState(state: BasicPlaybackState.paused);
|
||||
}
|
||||
}
|
||||
|
||||
void _pauseBackgroundAudio() {
|
||||
_backgroundAudio?.pause();
|
||||
_backgroundAudioPlaying = false;
|
||||
AudioSystem.instance
|
||||
.setPlaybackState(false, _backgroundAudioPositionSeconds);
|
||||
AudioSystem.instance.setAndroidNotificationButtons(<dynamic>[
|
||||
AndroidMediaButtonType.play,
|
||||
_forwardButton,
|
||||
AndroidMediaButtonType.stop,
|
||||
], androidCompactIndices: <int>[
|
||||
0,
|
||||
1,
|
||||
]);
|
||||
|
||||
AudioSystem.instance.setSupportedMediaActions(<MediaActionType>{
|
||||
MediaActionType.playPause,
|
||||
MediaActionType.play,
|
||||
MediaActionType.next,
|
||||
MediaActionType.previous,
|
||||
});
|
||||
@override
|
||||
void onPlay() async {
|
||||
if (_skipState == null) {
|
||||
if (_playing == null) {
|
||||
_playing = true;
|
||||
AudioServiceBackground.setQueue(_queue);
|
||||
await _audioPlayer.setUrl(mediaItem.id);
|
||||
Duration duration = await _audioPlayer.durationFuture;
|
||||
AudioServiceBackground.setMediaItem(
|
||||
mediaItem.copyWith(duration: duration.inMilliseconds));
|
||||
}
|
||||
_playing = true;
|
||||
_audioPlayer.play();
|
||||
}
|
||||
}
|
||||
|
||||
void _stopBackgroundAudio() {
|
||||
_backgroundAudio.pause();
|
||||
_backgroundAudio.dispose();
|
||||
_backgroundAudioPlaying = false;
|
||||
AudioSystem.instance.stopBackgroundDisplay();
|
||||
@override
|
||||
void onPause() {
|
||||
if (_skipState == null) {
|
||||
if (_playing == null) {}
|
||||
_playing = false;
|
||||
_audioPlayer.pause();
|
||||
}
|
||||
}
|
||||
|
||||
void _forwardBackgroundAudio(double seconds) {
|
||||
final double forwardposition = _backgroundAudioPositionSeconds + seconds;
|
||||
_backgroundAudio.seek(forwardposition);
|
||||
AudioSystem.instance
|
||||
.setPlaybackState(true, _backgroundAudioPositionSeconds);
|
||||
@override
|
||||
void onSeekTo(int position) {
|
||||
_audioPlayer.seek(Duration(milliseconds: position));
|
||||
}
|
||||
|
||||
final _pauseButton = AndroidCustomMediaButton(
|
||||
'pausenow', pausenowButtonId, 'ic_stat_pause_circle_filled');
|
||||
final _replay10Button = AndroidCustomMediaButton(
|
||||
'replay10', replay10ButtonId, 'ic_stat_replay_10');
|
||||
final _forwardButton = AndroidCustomMediaButton(
|
||||
'forward', forwardButtonId, 'ic_stat_forward_30');
|
||||
final _playnowButton = AndroidCustomMediaButton(
|
||||
'playnow', likeButtonId, 'ic_stat_play_circle_filled');
|
||||
@override
|
||||
void onClick(MediaButton button) {
|
||||
playPause();
|
||||
}
|
||||
|
||||
@override
|
||||
void onStop() async {
|
||||
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];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:intl/intl.dart';
|
||||
import 'package:audio_service/audio_service.dart';
|
||||
|
||||
class EpisodeBrief {
|
||||
final String title;
|
||||
|
@ -13,6 +14,7 @@ class EpisodeBrief {
|
|||
final int duration;
|
||||
final int explicit;
|
||||
final String imagePath;
|
||||
final String mediaId;
|
||||
EpisodeBrief(
|
||||
this.title,
|
||||
this.enclosureUrl,
|
||||
|
@ -21,21 +23,31 @@ class EpisodeBrief {
|
|||
this.feedTitle,
|
||||
this.primaryColor,
|
||||
this.liked,
|
||||
this.downloaded,
|
||||
this.downloaded,
|
||||
this.duration,
|
||||
this.explicit,
|
||||
this.imagePath
|
||||
);
|
||||
this.imagePath,
|
||||
this.mediaId);
|
||||
|
||||
String dateToString(){
|
||||
DateTime date = DateTime.fromMillisecondsSinceEpoch(pubDate);
|
||||
String dateToString() {
|
||||
DateTime date = DateTime.fromMillisecondsSinceEpoch(pubDate, isUtc: true);
|
||||
var diffrence = DateTime.now().difference(date);
|
||||
if(diffrence.inHours < 24) {
|
||||
if (diffrence.inHours < 24) {
|
||||
return '${diffrence.inHours} hours ago';
|
||||
} else if (diffrence.inDays < 7){
|
||||
return '${diffrence.inDays} days ago';}
|
||||
else {
|
||||
return DateFormat.yMMMd().format( DateTime.fromMillisecondsSinceEpoch(pubDate));
|
||||
}
|
||||
} else if (diffrence.inDays < 7) {
|
||||
return '${diffrence.inDays} days ago';
|
||||
} else {
|
||||
return DateFormat.yMMMd()
|
||||
.format(DateTime.fromMillisecondsSinceEpoch(pubDate));
|
||||
}
|
||||
}
|
||||
|
||||
MediaItem toMediaItem() {
|
||||
return MediaItem(
|
||||
id: mediaId,
|
||||
title: title,
|
||||
artist: feedTitle,
|
||||
album: feedTitle,
|
||||
artUri: 'file://$imagePath');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -55,8 +55,12 @@ class PodcastGroup {
|
|||
}
|
||||
|
||||
List<PodcastLocal> _podcasts;
|
||||
|
||||
List<PodcastLocal> _orderedPodcasts;
|
||||
List<PodcastLocal> get ordereddPodcasts => _orderedPodcasts;
|
||||
List<PodcastLocal> get podcasts => _podcasts;
|
||||
set setOrderedPodcasts(List<PodcastLocal> list) {
|
||||
_orderedPodcasts = list;
|
||||
}
|
||||
|
||||
GroupEntity toEntity() {
|
||||
return GroupEntity(name, id, color, podcastList);
|
||||
|
@ -82,9 +86,20 @@ class GroupList extends ChangeNotifier {
|
|||
GroupList({List<PodcastGroup> groups}) : _groups = groups ?? [];
|
||||
|
||||
bool _isLoading = false;
|
||||
|
||||
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
|
||||
void addListener(VoidCallback listener) {
|
||||
super.addListener(listener);
|
||||
|
@ -105,14 +120,15 @@ class GroupList extends ChangeNotifier {
|
|||
}
|
||||
|
||||
Future addGroup(PodcastGroup podcastGroup) async {
|
||||
_isLoading = true;
|
||||
_groups.add(podcastGroup);
|
||||
_saveGroup();
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future delGroup(PodcastGroup podcastGroup) async {
|
||||
_isLoading = true;
|
||||
notifyListeners();
|
||||
podcastGroup.podcastList.forEach((podcast) {
|
||||
if (!_groups.first.podcastList.contains(podcast)) {
|
||||
_groups[0].podcastList.insert(0, podcast);
|
||||
|
@ -121,11 +137,11 @@ class GroupList extends ChangeNotifier {
|
|||
_saveGroup();
|
||||
_groups.remove(podcastGroup);
|
||||
await _groups[0].getPodcasts();
|
||||
_isLoading = false;
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
updateGroup(PodcastGroup podcastGroup) async{
|
||||
updateGroup(PodcastGroup podcastGroup) async {
|
||||
var oldGroup = _groups.firstWhere((it) => it.id == podcastGroup.id);
|
||||
var index = _groups.indexOf(oldGroup);
|
||||
_groups.replaceRange(index, index + 1, [podcastGroup]);
|
||||
|
@ -161,7 +177,11 @@ class GroupList extends ChangeNotifier {
|
|||
_isLoading = true;
|
||||
notifyListeners();
|
||||
getPodcastGroup(id).forEach((group) {
|
||||
group.podcastList.remove(id);
|
||||
if (list.contains(group)) {
|
||||
list.remove(group);
|
||||
} else {
|
||||
group.podcastList.remove(id);
|
||||
}
|
||||
});
|
||||
list.forEach((s) {
|
||||
s.podcastList.insert(0, id);
|
||||
|
@ -190,8 +210,8 @@ class GroupList extends ChangeNotifier {
|
|||
notifyListeners();
|
||||
}
|
||||
|
||||
saveOrder(PodcastGroup group, List<PodcastLocal> podcasts) async {
|
||||
group.podcastList = podcasts.map((e) => e.id).toList();
|
||||
saveOrder(PodcastGroup group) async {
|
||||
group.podcastList = group.ordereddPodcasts.map((e) => e.id).toList();
|
||||
_saveGroup();
|
||||
await group.getPodcasts();
|
||||
notifyListeners();
|
||||
|
|
|
@ -6,16 +6,17 @@ import 'package:tsacdop/local_storage/key_value_storage.dart';
|
|||
class SettingState extends ChangeNotifier {
|
||||
KeyValueStorage themestorage = KeyValueStorage('themes');
|
||||
KeyValueStorage accentstorage = KeyValueStorage('accents');
|
||||
bool _isLoading;
|
||||
bool get isLoagding => _isLoading;
|
||||
KeyValueStorage autoupdatestorage = KeyValueStorage('autoupdate');
|
||||
|
||||
Future initData() async {
|
||||
await _getTheme();
|
||||
await _getAccentSetColor();
|
||||
await _getAutoUpdate();
|
||||
}
|
||||
|
||||
ThemeMode _theme;
|
||||
ThemeMode get theme => _theme;
|
||||
|
||||
set setTheme(ThemeMode mode) {
|
||||
_theme = mode;
|
||||
_saveTheme();
|
||||
|
@ -31,11 +32,20 @@ class SettingState extends ChangeNotifier {
|
|||
notifyListeners();
|
||||
}
|
||||
|
||||
bool _autoUpdate;
|
||||
bool get autoUpdate => _autoUpdate;
|
||||
set autoUpdate(bool boo) {
|
||||
_autoUpdate = boo;
|
||||
_saveAutoUpdate();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
void addListener(VoidCallback listener) {
|
||||
super.addListener(listener);
|
||||
_getTheme();
|
||||
_getAccentSetColor();
|
||||
_getAutoUpdate();
|
||||
}
|
||||
|
||||
_getTheme() async {
|
||||
|
@ -62,6 +72,14 @@ class SettingState extends ChangeNotifier {
|
|||
_saveAccentSetColor() async {
|
||||
await accentstorage
|
||||
.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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -10,6 +10,7 @@ import 'package:url_launcher/url_launcher.dart';
|
|||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:tsacdop/class/audiostate.dart';
|
||||
import 'package:tsacdop/class/episodebrief.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,
|
||||
builder: (_, data, __) {
|
||||
return Container(
|
||||
|
@ -290,7 +291,7 @@ class _MenuBarState extends State<MenuBar> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var audio = Provider.of<AudioPlayer>(context, listen: false);
|
||||
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
|
||||
return Container(
|
||||
height: 50.0,
|
||||
decoration: BoxDecoration(
|
||||
|
@ -346,7 +347,7 @@ class _MenuBarState extends State<MenuBar> {
|
|||
],
|
||||
),
|
||||
DownloadButton(episodeBrief: widget.episodeItem),
|
||||
Selector<AudioPlayer, List<String>>(
|
||||
Selector<AudioPlayerNotifier, List<String>>(
|
||||
selector: (_, audio) =>
|
||||
audio.queue.playlist.map((e) => e.enclosureUrl).toList(),
|
||||
builder: (_, data, __) {
|
||||
|
@ -367,9 +368,9 @@ class _MenuBarState extends State<MenuBar> {
|
|||
),
|
||||
Spacer(),
|
||||
// Text(audio.audioState.toString()),
|
||||
Selector<AudioPlayer, Tuple2<EpisodeBrief, bool>>(
|
||||
Selector<AudioPlayerNotifier, Tuple2<EpisodeBrief, BasicPlaybackState>>(
|
||||
selector: (_, audio) =>
|
||||
Tuple2(audio.episode, audio.backgroundAudioPlaying),
|
||||
Tuple2(audio.episode, audio.audioState),
|
||||
builder: (_, data, __) {
|
||||
return (widget.episodeItem.title != data.item1?.title)
|
||||
? Material(
|
||||
|
@ -400,7 +401,7 @@ class _MenuBarState extends State<MenuBar> {
|
|||
),
|
||||
)
|
||||
: (widget.episodeItem.title == data.item1?.title &&
|
||||
data.item2 == true)
|
||||
data.item2 == BasicPlaybackState.playing)
|
||||
? Container(
|
||||
padding: EdgeInsets.only(right: 30),
|
||||
child: SizedBox(
|
||||
|
@ -424,9 +425,10 @@ class _MenuBarState extends State<MenuBar> {
|
|||
class LinePainter extends CustomPainter {
|
||||
double _fraction;
|
||||
Paint _paint;
|
||||
LinePainter(this._fraction) {
|
||||
Color _maincolor;
|
||||
LinePainter(this._fraction, this._maincolor) {
|
||||
_paint = Paint()
|
||||
..color = Colors.blue
|
||||
..color = _maincolor
|
||||
..strokeWidth = 2.0
|
||||
..strokeCap = StrokeCap.round;
|
||||
}
|
||||
|
@ -483,14 +485,15 @@ class _LineLoaderState extends State<LineLoader>
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CustomPaint(painter: LinePainter(_fraction));
|
||||
return CustomPaint(painter: LinePainter(_fraction, Theme.of(context).accentColor));
|
||||
}
|
||||
}
|
||||
|
||||
class WavePainter extends CustomPainter {
|
||||
double _fraction;
|
||||
double _value;
|
||||
WavePainter(this._fraction);
|
||||
Color _color;
|
||||
WavePainter(this._fraction, this._color);
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
if (_fraction < 0.5) {
|
||||
|
@ -500,7 +503,7 @@ class WavePainter extends CustomPainter {
|
|||
}
|
||||
Path _path = Path();
|
||||
Paint _paint = Paint()
|
||||
..color = Colors.blue
|
||||
..color = _color
|
||||
..strokeWidth = 2.0
|
||||
..strokeCap = StrokeCap.round
|
||||
..style = PaintingStyle.stroke;
|
||||
|
@ -575,7 +578,7 @@ class _WaveLoaderState extends State<WaveLoader>
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CustomPaint(painter: WavePainter(_fraction));
|
||||
return CustomPaint(painter: WavePainter(_fraction, Theme.of(context).accentColor));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,14 +1,17 @@
|
|||
import 'dart:isolate';
|
||||
import 'dart:ui';
|
||||
import 'dart:io';
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:async';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:flutter_downloader/flutter_downloader.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:tsacdop/class/episodebrief.dart';
|
||||
import 'package:tsacdop/class/audiostate.dart';
|
||||
import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
|
||||
|
||||
class DownloadButton extends StatefulWidget {
|
||||
|
@ -74,6 +77,7 @@ class _DownloadButtonState extends State<DownloadButton> {
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
void _unbindBackgroundIsolate() {
|
||||
IsolateNameServer.removePortNameMapping('downloader_send_port');
|
||||
}
|
||||
|
@ -96,6 +100,7 @@ class _DownloadButtonState extends State<DownloadButton> {
|
|||
openFileFromNotification: false,
|
||||
);
|
||||
var dbHelper = DBHelper();
|
||||
|
||||
await dbHelper.saveDownloaded(task.link, task.taskId);
|
||||
Fluttertoast.showToast(
|
||||
msg: 'Downloading',
|
||||
|
@ -103,6 +108,7 @@ class _DownloadButtonState extends State<DownloadButton> {
|
|||
);
|
||||
}
|
||||
|
||||
|
||||
void _deleteDownload(_TaskInfo task) async {
|
||||
await FlutterDownloader.remove(
|
||||
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 {
|
||||
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);
|
||||
final saveDir = Directory(_localPath);
|
||||
bool hasExisted = await saveDir.exists();
|
||||
|
@ -173,6 +189,8 @@ class _DownloadButtonState extends State<DownloadButton> {
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
Future<bool> _checkPermmison() async {
|
||||
PermissionStatus permission = await PermissionHandler()
|
||||
.checkPermissionStatus(PermissionGroup.storage);
|
||||
|
@ -284,6 +302,7 @@ class _DownloadButtonState extends State<DownloadButton> {
|
|||
),
|
||||
);
|
||||
} else if (task.status == DownloadTaskStatus.complete) {
|
||||
_saveMediaId(task);
|
||||
return _buttonOnMenu(
|
||||
Icon(
|
||||
Icons.done_all,
|
||||
|
@ -307,4 +326,4 @@ class _TaskInfo {
|
|||
DownloadTaskStatus status = DownloadTaskStatus.undefined;
|
||||
|
||||
_TaskInfo({this.name, this.link});
|
||||
}
|
||||
}
|
|
@ -1,8 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.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 {
|
||||
_launchUrl(String url) async {
|
||||
if (await canLaunch(url)) {
|
||||
|
@ -71,7 +70,7 @@ class AboutApp extends StatelessWidget {
|
|||
image: AssetImage('assets/logo.png'),
|
||||
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),
|
||||
height: 50,
|
||||
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,
|
||||
),
|
||||
),
|
||||
|
@ -111,17 +110,17 @@ class AboutApp extends StatelessWidget {
|
|||
_listItem(
|
||||
context,
|
||||
'GitHub',
|
||||
FontAwesomeIcons.githubSquare,
|
||||
'https://github.com/stonaga/tsacdop'),
|
||||
LineIcons.github,
|
||||
'https://github.com/stonaga/'),
|
||||
_listItem(
|
||||
context,
|
||||
'Twitter',
|
||||
FontAwesomeIcons.twitterSquare,
|
||||
LineIcons.twitter,
|
||||
'https://twitter.com'),
|
||||
_listItem(
|
||||
context,
|
||||
'Gmail',
|
||||
FontAwesomeIcons.envelopeSquare,
|
||||
'Stone Gate',
|
||||
LineIcons.hat_cowboy_solid,
|
||||
'mailto:<xijieyin@gmail.com>?subject=Tsacdop Feedback'),
|
||||
],
|
||||
),
|
||||
|
|
|
@ -36,6 +36,7 @@ class _MyHomePageState extends State<MyHomePage> {
|
|||
return AnnotatedRegion<SystemUiOverlayStyle>(
|
||||
value: SystemUiOverlayStyle(
|
||||
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
|
||||
systemNavigationBarIconBrightness: Theme.of(context).accentColorBrightness,
|
||||
systemNavigationBarColor: Theme.of(context).primaryColor,
|
||||
statusBarColor: Theme.of(context).primaryColor,
|
||||
),
|
||||
|
|
|
@ -13,6 +13,7 @@ import 'package:color_thief_flutter/color_thief_flutter.dart';
|
|||
import 'package:image/image.dart' as img;
|
||||
import 'package:uuid/uuid.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:line_icons/line_icons.dart';
|
||||
|
||||
import 'package:tsacdop/class/podcast_group.dart';
|
||||
import 'package:tsacdop/settings/settting.dart';
|
||||
|
@ -142,7 +143,7 @@ class PopupMenu extends StatelessWidget {
|
|||
|
||||
void _saveOmpl(String path) async {
|
||||
File file = File(path);
|
||||
String opml = file.readAsStringSync();
|
||||
try{String opml = file.readAsStringSync();
|
||||
|
||||
var content = xml.parse(opml);
|
||||
var total = content
|
||||
|
@ -167,6 +168,15 @@ class PopupMenu extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
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),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Icon(Icons.refresh),
|
||||
Icon(LineIcons.cloud_download_alt_solid),
|
||||
Padding(padding: EdgeInsets.symmetric(horizontal: 5.0),),
|
||||
Text('Refresh All'),
|
||||
],
|
||||
|
@ -208,7 +218,7 @@ class PopupMenu extends StatelessWidget {
|
|||
padding: EdgeInsets.only(left: 10),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Icon(Icons.attachment),
|
||||
Icon(LineIcons.paperclip_solid),
|
||||
Padding(padding: EdgeInsets.symmetric(horizontal: 5.0),),
|
||||
Text('Import OMPL'),
|
||||
],
|
||||
|
@ -226,7 +236,7 @@ class PopupMenu extends StatelessWidget {
|
|||
padding: EdgeInsets.only(left: 10),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Icon(Icons.swap_calls),
|
||||
Icon(LineIcons.cog_solid),
|
||||
Padding(padding: EdgeInsets.symmetric(horizontal: 5.0),),
|
||||
Text('Settings'),
|
||||
],
|
||||
|
@ -239,7 +249,7 @@ class PopupMenu extends StatelessWidget {
|
|||
padding: EdgeInsets.only(left: 10),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Icon(Icons.info_outline),
|
||||
Icon(LineIcons.info_circle_solid),
|
||||
Padding(padding: EdgeInsets.symmetric(horizontal: 5.0),),
|
||||
Text('About'),
|
||||
],
|
||||
|
|
|
@ -7,6 +7,7 @@ import 'package:provider/provider.dart';
|
|||
import 'package:marquee/marquee.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
import 'package:audio_service/audio_service.dart';
|
||||
|
||||
import 'package:tsacdop/class/episodebrief.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/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 {
|
||||
@override
|
||||
_PlayerWidgetState createState() => _PlayerWidgetState();
|
||||
|
@ -50,17 +142,17 @@ class _PlayerWidgetState extends State<PlayerWidget> {
|
|||
_timeLeft = _minSelected;
|
||||
_timer = Timer.periodic(Duration(minutes: 1), (timer) {
|
||||
setState(() {
|
||||
if(_timeLeft < 1){
|
||||
if (_timeLeft < 1) {
|
||||
_timer.cancel();
|
||||
} else{
|
||||
} else {
|
||||
_timeLeft = _timeLeft - 1;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Widget _sleepTimer(BuildContext context) {
|
||||
var audio = Provider.of<AudioPlayer>(context);
|
||||
var audio = Provider.of<AudioPlayerNotifier>(context);
|
||||
return Container(
|
||||
height: 50,
|
||||
margin: EdgeInsets.all(10.0),
|
||||
|
@ -141,7 +233,7 @@ class _PlayerWidgetState extends State<PlayerWidget> {
|
|||
}
|
||||
|
||||
Widget _expandedPanel(BuildContext context) {
|
||||
var audio = Provider.of<AudioPlayer>(context, listen: false);
|
||||
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
|
||||
return Stack(
|
||||
children: <Widget>[
|
||||
Container(
|
||||
|
@ -156,7 +248,7 @@ class _PlayerWidgetState extends State<PlayerWidget> {
|
|||
height: 80.0,
|
||||
padding: EdgeInsets.all(20),
|
||||
alignment: Alignment.center,
|
||||
child: Selector<AudioPlayer, String>(
|
||||
child: Selector<AudioPlayerNotifier, String>(
|
||||
selector: (_, audio) => audio.episode.title,
|
||||
builder: (_, title, __) {
|
||||
return Container(
|
||||
|
@ -201,8 +293,12 @@ class _PlayerWidgetState extends State<PlayerWidget> {
|
|||
},
|
||||
),
|
||||
),
|
||||
Consumer<AudioPlayer>(
|
||||
Consumer<AudioPlayerNotifier>(
|
||||
builder: (_, data, __) {
|
||||
Color _c =
|
||||
(Theme.of(context).brightness == Brightness.light)
|
||||
? data.episode.primaryColor.colorizedark()
|
||||
: data.episode.primaryColor.colorizeLight();
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
|
@ -215,10 +311,12 @@ class _PlayerWidgetState extends State<PlayerWidget> {
|
|||
.accentColor
|
||||
.withOpacity(0.5),
|
||||
inactiveTrackColor: Colors.grey[300],
|
||||
trackHeight: 3.0,
|
||||
trackHeight: 2.0,
|
||||
thumbColor: Theme.of(context).accentColor,
|
||||
thumbShape: RoundSliderThumbShape(
|
||||
enabledThumbRadius: 6.0),
|
||||
thumbShape: MyRoundSliderThumpShape(
|
||||
enabledThumbRadius: 5.0,
|
||||
disabledThumbRadius: 5.0,
|
||||
thumbCenterColor: _c),
|
||||
overlayColor:
|
||||
Theme.of(context).accentColor.withAlpha(32),
|
||||
overlayShape:
|
||||
|
@ -238,7 +336,7 @@ class _PlayerWidgetState extends State<PlayerWidget> {
|
|||
children: <Widget>[
|
||||
Text(
|
||||
_stringForSeconds(
|
||||
data.backgroundAudioPosition) ??
|
||||
data.backgroundAudioPosition / 1000) ??
|
||||
'',
|
||||
style: TextStyle(fontSize: 10),
|
||||
),
|
||||
|
@ -250,7 +348,12 @@ class _PlayerWidgetState extends State<PlayerWidget> {
|
|||
style: const TextStyle(
|
||||
color: const Color(0xFFFF0000)))
|
||||
: Text(
|
||||
data.remoteAudioLoading
|
||||
data.audioState ==
|
||||
BasicPlaybackState
|
||||
.buffering ||
|
||||
data.audioState ==
|
||||
BasicPlaybackState
|
||||
.connecting
|
||||
? 'Buffring...'
|
||||
: '',
|
||||
style: TextStyle(
|
||||
|
@ -261,7 +364,7 @@ class _PlayerWidgetState extends State<PlayerWidget> {
|
|||
),
|
||||
Text(
|
||||
_stringForSeconds(
|
||||
data.backgroundAudioDuration) ??
|
||||
data.backgroundAudioDuration / 1000) ??
|
||||
'',
|
||||
style: TextStyle(fontSize: 10),
|
||||
),
|
||||
|
@ -274,8 +377,8 @@ class _PlayerWidgetState extends State<PlayerWidget> {
|
|||
),
|
||||
Container(
|
||||
height: 100,
|
||||
child: Selector<AudioPlayer, bool>(
|
||||
selector: (_, audio) => audio.backgroundAudioPlaying,
|
||||
child: Selector<AudioPlayerNotifier, BasicPlaybackState>(
|
||||
selector: (_, audio) => audio.audioState,
|
||||
builder: (_, backplay, __) {
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
|
@ -285,22 +388,24 @@ class _PlayerWidgetState extends State<PlayerWidget> {
|
|||
children: [
|
||||
IconButton(
|
||||
padding: EdgeInsets.symmetric(horizontal: 30.0),
|
||||
onPressed: backplay
|
||||
? () => audio.forwardAudio(-10)
|
||||
: null,
|
||||
onPressed:
|
||||
backplay == BasicPlaybackState.playing
|
||||
? () => audio.forwardAudio(-10)
|
||||
: null,
|
||||
iconSize: 32.0,
|
||||
icon: Icon(Icons.replay_10),
|
||||
color:
|
||||
Theme.of(context).tabBarTheme.labelColor),
|
||||
backplay
|
||||
backplay == BasicPlaybackState.playing
|
||||
? IconButton(
|
||||
padding:
|
||||
EdgeInsets.symmetric(horizontal: 30.0),
|
||||
onPressed: backplay
|
||||
? () {
|
||||
audio.pauseAduio();
|
||||
}
|
||||
: null,
|
||||
onPressed:
|
||||
backplay == BasicPlaybackState.playing
|
||||
? () {
|
||||
audio.pauseAduio();
|
||||
}
|
||||
: null,
|
||||
iconSize: 40.0,
|
||||
icon: Icon(Icons.pause_circle_filled),
|
||||
color: Theme.of(context)
|
||||
|
@ -309,11 +414,12 @@ class _PlayerWidgetState extends State<PlayerWidget> {
|
|||
: IconButton(
|
||||
padding:
|
||||
EdgeInsets.symmetric(horizontal: 30.0),
|
||||
onPressed: backplay
|
||||
? null
|
||||
: () {
|
||||
audio.resumeAudio();
|
||||
},
|
||||
onPressed:
|
||||
backplay == BasicPlaybackState.playing
|
||||
? null
|
||||
: () {
|
||||
audio.resumeAudio();
|
||||
},
|
||||
iconSize: 40.0,
|
||||
icon: Icon(Icons.play_circle_filled),
|
||||
color: Theme.of(context)
|
||||
|
@ -321,9 +427,10 @@ class _PlayerWidgetState extends State<PlayerWidget> {
|
|||
.labelColor),
|
||||
IconButton(
|
||||
padding: EdgeInsets.symmetric(horizontal: 30.0),
|
||||
onPressed: backplay
|
||||
? () => audio.forwardAudio(30)
|
||||
: null,
|
||||
onPressed:
|
||||
backplay == BasicPlaybackState.playing
|
||||
? () => audio.forwardAudio(30)
|
||||
: null,
|
||||
iconSize: 32.0,
|
||||
icon: Icon(Icons.forward_30),
|
||||
color:
|
||||
|
@ -344,7 +451,7 @@ class _PlayerWidgetState extends State<PlayerWidget> {
|
|||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
borderRadius: BorderRadius.all(Radius.circular(10.0)),
|
||||
),
|
||||
child: Selector<AudioPlayer,
|
||||
child: Selector<AudioPlayerNotifier,
|
||||
Tuple3<EpisodeBrief, bool, bool>>(
|
||||
selector: (_, audio) => Tuple3(audio.episode,
|
||||
audio.stopOnComplete, audio.showStopWatch),
|
||||
|
@ -394,6 +501,7 @@ class _PlayerWidgetState extends State<PlayerWidget> {
|
|||
],
|
||||
onSelected: (value) {
|
||||
if (value == 1) {
|
||||
audio.sleepTimer(_minSelected);
|
||||
audio.setStopOnComplete = true;
|
||||
} else if (value == 2) {
|
||||
setState(() => _showTimer = true);
|
||||
|
@ -414,8 +522,7 @@ class _PlayerWidgetState extends State<PlayerWidget> {
|
|||
color:
|
||||
Theme.of(context).accentColor,
|
||||
),
|
||||
child: Text(
|
||||
_timeLeft.toString(),
|
||||
child: Text(_timeLeft.toString(),
|
||||
style: TextStyle(
|
||||
color: Colors.white)),
|
||||
),
|
||||
|
@ -475,7 +582,7 @@ class _PlayerWidgetState extends State<PlayerWidget> {
|
|||
// margin: EdgeInsets.all(20),
|
||||
//padding: EdgeInsets.only(bottom: 10.0),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.all(Radius.circular(10.0)),
|
||||
// borderRadius: BorderRadius.all(Radius.circular(10.0)),
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
),
|
||||
child: Column(
|
||||
|
@ -511,7 +618,7 @@ class _PlayerWidgetState extends State<PlayerWidget> {
|
|||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Selector<AudioPlayer, List<EpisodeBrief>>(
|
||||
child: Selector<AudioPlayerNotifier, List<EpisodeBrief>>(
|
||||
selector: (_, audio) => audio.queue.playlist,
|
||||
builder: (_, playlist, __) {
|
||||
return ListView.builder(
|
||||
|
@ -623,7 +730,7 @@ class _PlayerWidgetState extends State<PlayerWidget> {
|
|||
}
|
||||
|
||||
Widget _miniPanel(double width, BuildContext context) {
|
||||
var audio = Provider.of<AudioPlayer>(context, listen: false);
|
||||
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).primaryColor,
|
||||
|
@ -631,7 +738,7 @@ class _PlayerWidgetState extends State<PlayerWidget> {
|
|||
height: 60,
|
||||
child:
|
||||
Column(mainAxisAlignment: MainAxisAlignment.start, children: <Widget>[
|
||||
Selector<AudioPlayer, Tuple2<String, double>>(
|
||||
Selector<AudioPlayerNotifier, Tuple2<String, double>>(
|
||||
selector: (_, audio) =>
|
||||
Tuple2(audio.episode?.primaryColor, audio.seekSliderValue),
|
||||
builder: (_, data, __) {
|
||||
|
@ -657,58 +764,33 @@ class _PlayerWidgetState extends State<PlayerWidget> {
|
|||
children: <Widget>[
|
||||
Expanded(
|
||||
flex: 4,
|
||||
child: Selector<AudioPlayer, String>(
|
||||
child: Selector<AudioPlayerNotifier, String>(
|
||||
selector: (_, audio) => audio.episode.title,
|
||||
builder: (_, title, __) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, size) {
|
||||
var span = TextSpan(
|
||||
text: title,
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
);
|
||||
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),
|
||||
);
|
||||
}
|
||||
},
|
||||
return Text(
|
||||
title,
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.clip,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Selector<AudioPlayer, Tuple2<bool, double>>(
|
||||
child: Selector<AudioPlayerNotifier,
|
||||
Tuple2<BasicPlaybackState, double>>(
|
||||
selector: (_, audio) => Tuple2(
|
||||
audio.remoteAudioLoading,
|
||||
audio.audioState,
|
||||
(audio.backgroundAudioDuration -
|
||||
audio.backgroundAudioPosition)),
|
||||
audio.backgroundAudioPosition) /
|
||||
1000),
|
||||
builder: (_, data, __) {
|
||||
return Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 10),
|
||||
alignment: Alignment.center,
|
||||
child: data.item1
|
||||
child: data.item1 == BasicPlaybackState.buffering ||
|
||||
data.item1 == BasicPlaybackState.connecting
|
||||
? Text(
|
||||
'Buffring...',
|
||||
style: TextStyle(
|
||||
|
@ -730,30 +812,32 @@ class _PlayerWidgetState extends State<PlayerWidget> {
|
|||
),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Selector<AudioPlayer, bool>(
|
||||
selector: (_, audio) => audio.backgroundAudioPlaying,
|
||||
child: Selector<AudioPlayerNotifier, BasicPlaybackState>(
|
||||
selector: (_, audio) => audio.audioState,
|
||||
builder: (_, audioplay, __) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
audioplay
|
||||
audioplay == BasicPlaybackState.playing
|
||||
? InkWell(
|
||||
onTap: audioplay
|
||||
? () {
|
||||
audio.pauseAduio();
|
||||
}
|
||||
: null,
|
||||
onTap:
|
||||
audioplay == BasicPlaybackState.playing
|
||||
? () {
|
||||
audio.pauseAduio();
|
||||
}
|
||||
: null,
|
||||
child: ImageRotate(
|
||||
title: audio.episode.title,
|
||||
path: audio.episode.imagePath),
|
||||
)
|
||||
: InkWell(
|
||||
onTap: audioplay
|
||||
? null
|
||||
: () {
|
||||
audio.resumeAudio();
|
||||
},
|
||||
onTap:
|
||||
audioplay == BasicPlaybackState.playing
|
||||
? null
|
||||
: () {
|
||||
audio.resumeAudio();
|
||||
},
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: <Widget>[
|
||||
|
@ -781,11 +865,10 @@ class _PlayerWidgetState extends State<PlayerWidget> {
|
|||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: audioplay
|
||||
? () => audio.forwardAudio(30)
|
||||
: null,
|
||||
onPressed:
|
||||
() => audio.playNext(),
|
||||
iconSize: 25.0,
|
||||
icon: Icon(Icons.forward_30),
|
||||
icon: Icon(Icons.skip_next),
|
||||
color:
|
||||
Theme.of(context).tabBarTheme.labelColor),
|
||||
],
|
||||
|
@ -803,7 +886,7 @@ class _PlayerWidgetState extends State<PlayerWidget> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
double _width = MediaQuery.of(context).size.width;
|
||||
return Selector<AudioPlayer, bool>(
|
||||
return Selector<AudioPlayerNotifier, bool>(
|
||||
selector: (_, audio) => audio.playerRunning,
|
||||
builder: (_, playerrunning, __) {
|
||||
return !playerrunning
|
||||
|
|
|
@ -31,7 +31,7 @@ class _HomeState extends State<Home> {
|
|||
}
|
||||
|
||||
_getPlaylist() async {
|
||||
await Provider.of<AudioPlayer>(context, listen: false).loadPlaylist();
|
||||
await Provider.of<AudioPlayerNotifier>(context, listen: false).loadPlaylist();
|
||||
setState(() {
|
||||
_loadPlay = true;
|
||||
});
|
||||
|
@ -39,7 +39,7 @@ class _HomeState extends State<Home> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var audio = Provider.of<AudioPlayer>(context, listen: false);
|
||||
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
|
||||
return Stack(children: <Widget>[
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
|
@ -58,7 +58,7 @@ class _HomeState extends State<Home> {
|
|||
bottom: 50,
|
||||
right: _loadPlay ? 5 : -25,
|
||||
child: Container(
|
||||
child: Selector<AudioPlayer, Tuple3<bool, Playlist, int>>(
|
||||
child: Selector<AudioPlayerNotifier, Tuple3<bool, Playlist, int>>(
|
||||
selector: (_, audio) =>
|
||||
Tuple3(audio.playerRunning, audio.queue, audio.lastPositin),
|
||||
builder: (_, data, __) => !_loadPlay
|
||||
|
@ -90,7 +90,7 @@ class _HomeState extends State<Home> {
|
|||
offset: Offset(1, 1)),
|
||||
]),
|
||||
height: 40,
|
||||
child: Text(_stringForSeconds(data.item3) + '...',
|
||||
child: Text(_stringForSeconds(data.item3~/1000) + '...',
|
||||
style: TextStyle(color: Colors.white)),
|
||||
),
|
||||
CircleAvatar(
|
||||
|
|
|
@ -5,7 +5,9 @@ import 'package:flutter/cupertino.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.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/importompl.dart';
|
||||
import 'package:tsacdop/class/podcast_group.dart';
|
||||
|
@ -89,7 +91,9 @@ class _ScrollPodcastsState extends State<ScrollPodcasts> {
|
|||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyText1
|
||||
.copyWith(color: Colors.red[300]),
|
||||
.copyWith(
|
||||
color: Theme.of(context)
|
||||
.accentColor),
|
||||
)),
|
||||
Spacer(),
|
||||
Container(
|
||||
|
@ -184,7 +188,9 @@ class _ScrollPodcastsState extends State<ScrollPodcasts> {
|
|||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyText1
|
||||
.copyWith(color: Colors.red[300]),
|
||||
.copyWith(
|
||||
color: Theme.of(context)
|
||||
.accentColor),
|
||||
)),
|
||||
Spacer(),
|
||||
Container(
|
||||
|
@ -374,43 +380,78 @@ class ShowEpisode extends StatelessWidget {
|
|||
final List<EpisodeBrief> podcast;
|
||||
final PodcastLocal podcastLocal;
|
||||
ShowEpisode({Key key, this.podcast, this.podcastLocal}) : super(key: key);
|
||||
|
||||
Offset offset;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
double _width = MediaQuery.of(context).size.width;
|
||||
_showPopupMenu(Offset offset) async {
|
||||
print(offset.dx);
|
||||
_showPopupMenu(Offset offset, EpisodeBrief episode, BuildContext context,
|
||||
bool isPlaying, bool isInPlaylist) async {
|
||||
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
|
||||
double left = offset.dx;
|
||||
double top = offset.dy;
|
||||
await showMenu(
|
||||
await showMenu<int>(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(10))),
|
||||
context: context,
|
||||
position: RelativeRect.fromLTRB(left, top, _width - left, 0),
|
||||
items: [
|
||||
items: <PopupMenuEntry<int>>[
|
||||
PopupMenuItem(
|
||||
value: 0,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: <Widget>[
|
||||
Icon(Icons.play_circle_outline),
|
||||
Padding(padding: EdgeInsets.symmetric(horizontal: 2),),
|
||||
Text('Play')
|
||||
Icon(
|
||||
LineIcons.play_circle_solid,
|
||||
color: Theme.of(context).accentColor,
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 2),
|
||||
),
|
||||
!isPlaying ? Text('Play') : Text('Playing'),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(child: Row(
|
||||
children: <Widget>[
|
||||
Icon(Icons.favorite_border),
|
||||
Padding(padding: EdgeInsets.symmetric(horizontal: 2),),
|
||||
Text('Like')
|
||||
],
|
||||
)),
|
||||
PopupMenuItem(
|
||||
value: 1,
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
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(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
// physics: const AlwaysScrollableScrollPhysics(),
|
||||
physics: ClampingScrollPhysics(),
|
||||
primary: false,
|
||||
slivers: <Widget>[
|
||||
SliverPadding(
|
||||
|
@ -427,88 +468,110 @@ class ShowEpisode extends StatelessWidget {
|
|||
Color _c = (Theme.of(context).brightness == Brightness.light)
|
||||
? podcastLocal.primaryColor.colorizedark()
|
||||
: podcastLocal.primaryColor.colorizeLight();
|
||||
return GestureDetector(
|
||||
onLongPressStart: (details) => _showPopupMenu(Offset(
|
||||
details.globalPosition.dx, details.globalPosition.dy)),
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
ScaleRoute(
|
||||
page: EpisodeDetail(
|
||||
episodeItem: podcast[index],
|
||||
heroTag: 'scroll',
|
||||
//unique hero tag
|
||||
)),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
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,
|
||||
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,
|
||||
padding: EdgeInsets.all(10.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
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: () {
|
||||
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>[
|
||||
Hero(
|
||||
tag: podcast[index].enclosureUrl + 'scroll',
|
||||
Expanded(
|
||||
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(
|
||||
height: _width / 18,
|
||||
width: _width / 18,
|
||||
child: CircleAvatar(
|
||||
backgroundImage: FileImage(
|
||||
File("${podcastLocal.imagePath}")),
|
||||
padding: EdgeInsets.only(top: 2.0),
|
||||
alignment: Alignment.topLeft,
|
||||
child: Text(
|
||||
podcast[index].title,
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
@ -2,7 +2,7 @@ import 'dart:ui';
|
|||
|
||||
import 'package:flutter/material.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/util/episodegrid.dart';
|
||||
|
||||
|
@ -49,8 +49,8 @@ class _MainTabState extends State<MainTab> with TickerProviderStateMixin {
|
|||
],
|
||||
onSelected: (value) {
|
||||
if (value == 0) {
|
||||
Navigator.push(
|
||||
context, MaterialPageRoute(builder: (context) => PlayedHistory()));
|
||||
Navigator.push(context,
|
||||
MaterialPageRoute(builder: (context) => PlayedHistory()));
|
||||
}
|
||||
},
|
||||
);
|
||||
|
@ -81,6 +81,7 @@ class _MainTabState extends State<MainTab> with TickerProviderStateMixin {
|
|||
height: 50,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: TabBar(
|
||||
indicatorSize: TabBarIndicatorSize.tab,
|
||||
isScrollable: true,
|
||||
labelPadding: EdgeInsets.all(10.0),
|
||||
controller: _controller,
|
||||
|
@ -134,26 +135,73 @@ class RecentUpdate extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _RecentUpdateState extends State<RecentUpdate> {
|
||||
Future<List<EpisodeBrief>> _getRssItem() async {
|
||||
Future<List<EpisodeBrief>> _getRssItem(int top) async {
|
||||
var dbHelper = DBHelper();
|
||||
List<EpisodeBrief> episodes = await dbHelper.getRecentRssItem();
|
||||
List<EpisodeBrief> episodes = await dbHelper.getRecentRssItem(top);
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder<List<EpisodeBrief>>(
|
||||
future: _getRssItem(),
|
||||
future: _getRssItem(_top),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) print(snapshot.error);
|
||||
return (snapshot.hasData)
|
||||
? EpisodeGrid(
|
||||
podcast: snapshot.data,
|
||||
showDownload: false,
|
||||
showFavorite: false,
|
||||
showNumber: false,
|
||||
heroTag: 'recent',
|
||||
)
|
||||
? CustomScrollView(
|
||||
controller: _controller,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
primary: false,
|
||||
slivers: <Widget>[
|
||||
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());
|
||||
},
|
||||
);
|
||||
|
@ -179,12 +227,18 @@ class _MyFavoriteState extends State<MyFavorite> {
|
|||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) print(snapshot.error);
|
||||
return (snapshot.hasData)
|
||||
? EpisodeGrid(
|
||||
podcast: snapshot.data,
|
||||
showDownload: false,
|
||||
showFavorite: false,
|
||||
showNumber: false,
|
||||
heroTag: 'favorite',
|
||||
? CustomScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
primary: false,
|
||||
slivers: <Widget>[
|
||||
EpisodeGrid(
|
||||
podcast: snapshot.data,
|
||||
showDownload: false,
|
||||
showFavorite: false,
|
||||
showNumber: false,
|
||||
heroTag: 'favorite',
|
||||
)
|
||||
],
|
||||
)
|
||||
: Center(child: CircularProgressIndicator());
|
||||
},
|
||||
|
@ -211,12 +265,19 @@ class _MyDownloadState extends State<MyDownload> {
|
|||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) print(snapshot.error);
|
||||
return (snapshot.hasData)
|
||||
? EpisodeGrid(
|
||||
podcast: snapshot.data,
|
||||
showDownload: true,
|
||||
showFavorite: false,
|
||||
showNumber: false,
|
||||
heroTag: 'download',
|
||||
? CustomScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
primary: false,
|
||||
slivers: <Widget>[
|
||||
EpisodeGrid(
|
||||
podcast: snapshot.data,
|
||||
showDownload: true,
|
||||
showFavorite: false,
|
||||
showNumber: false,
|
||||
heroTag: 'download',
|
||||
)
|
||||
],
|
||||
|
||||
)
|
||||
: Center(child: CircularProgressIndicator());
|
||||
},
|
||||
|
|
|
@ -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(),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -43,7 +43,7 @@ class KeyValueStorage {
|
|||
return prefs.getInt(key);
|
||||
}
|
||||
|
||||
Future<bool> saveStringlist(List<String> playList) async{
|
||||
Future<bool> saveStringList(List<String> playList) async{
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
return prefs.setStringList(key, playList);
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import 'package:tsacdop/class/podcastlocal.dart';
|
|||
import 'package:tsacdop/class/audiostate.dart';
|
||||
import 'package:tsacdop/class/episodebrief.dart';
|
||||
import 'package:tsacdop/webfeed/webfeed.dart';
|
||||
import 'package:tsacdop/class/sub_history.dart';
|
||||
|
||||
class DBHelper {
|
||||
static Database _db;
|
||||
|
@ -35,10 +36,13 @@ class DBHelper {
|
|||
enclosure_url TEXT UNIQUE, enclosure_length INTEGER, pubDate TEXT,
|
||||
description TEXT, feed_id TEXT, feed_link TEXT, milliseconds INTEGER,
|
||||
duration INTEGER DEFAULT 0, explicit INTEGER DEFAULT 0, liked INTEGER DEFAULT 0,
|
||||
downloaded TEXT DEFAULT 'ND', download_date INTEGER DEFAULT 0)""");
|
||||
downloaded TEXT DEFAULT 'ND', download_date INTEGER DEFAULT 0, media_id TEXT)""");
|
||||
await db.execute(
|
||||
"""CREATE TABLE PlayHistory(id INTEGER PRIMARY KEY, title TEXT, enclosure_url TEXT UNIQUE,
|
||||
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 {
|
||||
|
@ -97,7 +101,7 @@ class DBHelper {
|
|||
int _milliseconds = DateTime.now().millisecondsSinceEpoch;
|
||||
var dbClient = await database;
|
||||
await dbClient.transaction((txn) async {
|
||||
return await txn.rawInsert(
|
||||
await txn.rawInsert(
|
||||
"""INSERT OR IGNORE INTO PodcastLocal (id, title, imageUrl, rssUrl,
|
||||
primaryColor, author, description, add_date, imagePath, provider, link) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
||||
[
|
||||
|
@ -114,6 +118,16 @@ class DBHelper {
|
|||
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 {
|
||||
|
@ -146,6 +160,10 @@ class DBHelper {
|
|||
print('Removed all download tasks');
|
||||
}
|
||||
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 {
|
||||
|
@ -174,16 +192,46 @@ class DBHelper {
|
|||
""");
|
||||
List<PlayHistory> playHistory = [];
|
||||
list.forEach((record) {
|
||||
playHistory.add(PlayHistory(
|
||||
record['title'],
|
||||
record['enclosure_url'],
|
||||
record['seconds'],
|
||||
record['seek_value'],
|
||||
));
|
||||
playHistory.add(PlayHistory(record['title'], record['enclosure_url'],
|
||||
record['seconds'], record['seek_value'],
|
||||
playdate: DateTime.fromMillisecondsSinceEpoch(record['add_date'])));
|
||||
});
|
||||
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 {
|
||||
var dbClient = await database;
|
||||
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 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 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 {
|
||||
date = DateFormat('EEE, dd MMM yyyy HH:mm:ss Z', 'en_US').parse(pubDate);
|
||||
} catch (e) {
|
||||
|
@ -209,7 +268,7 @@ class DBHelper {
|
|||
try {
|
||||
date = DateFormat('EEE, dd MMM yyyy HH:mm Z', 'en_US').parse(pubDate);
|
||||
} 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 time = hhmm.stringMatch(pubDate);
|
||||
String month = ddmmm.stringMatch(pubDate);
|
||||
|
@ -221,12 +280,12 @@ class DBHelper {
|
|||
date = DateFormat('mm-dd yyyy HH:mm', 'en_US')
|
||||
.parse(month + ' ' + year + ' ' + time);
|
||||
} else {
|
||||
date = DateTime.now();
|
||||
date = DateTime.now().toUtc();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return date;
|
||||
return date.add(Duration(hours: timezoneInt));
|
||||
}
|
||||
|
||||
int getExplicit(bool b) {
|
||||
|
@ -276,7 +335,7 @@ class DBHelper {
|
|||
await dbClient.transaction((txn) {
|
||||
return txn.rawInsert(
|
||||
"""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,
|
||||
_url,
|
||||
|
@ -287,6 +346,7 @@ class DBHelper {
|
|||
_milliseconds,
|
||||
_duration,
|
||||
_explicit,
|
||||
_url
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
@ -333,7 +393,7 @@ class DBHelper {
|
|||
await dbClient.transaction((txn) {
|
||||
return txn.rawInsert(
|
||||
"""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,
|
||||
_url,
|
||||
|
@ -344,6 +404,7 @@ class DBHelper {
|
|||
_milliseconds,
|
||||
_duration,
|
||||
_explicit,
|
||||
_url
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
@ -358,7 +419,7 @@ class DBHelper {
|
|||
List<Map> list = await dbClient
|
||||
.rawQuery("""SELECT E.title, E.enclosure_url, E.enclosure_length,
|
||||
E.milliseconds, P.imagePath, P.title as feedTitle, E.duration, E.explicit, E.liked,
|
||||
E.downloaded, P.primaryColor
|
||||
E.downloaded, P.primaryColor , E.media_id
|
||||
FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
|
||||
WHERE P.id = ? ORDER BY E.milliseconds DESC""", [id]);
|
||||
for (int x = 0; x < list.length; x++) {
|
||||
|
@ -373,7 +434,8 @@ class DBHelper {
|
|||
list[x]['downloaded'],
|
||||
list[x]['duration'],
|
||||
list[x]['explicit'],
|
||||
list[x]['imagePath']));
|
||||
list[x]['imagePath'],
|
||||
list[x]['media_id']));
|
||||
}
|
||||
return episodes;
|
||||
}
|
||||
|
@ -384,7 +446,7 @@ class DBHelper {
|
|||
List<Map> list = await dbClient
|
||||
.rawQuery("""SELECT E.title, E.enclosure_url, E.enclosure_length,
|
||||
E.milliseconds, P.imagePath, P.title as feed_title, E.duration, E.explicit, E.liked,
|
||||
E.downloaded, P.primaryColor
|
||||
E.downloaded, P.primaryColor, E.media_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]);
|
||||
for (int x = 0; x < list.length; x++) {
|
||||
|
@ -399,7 +461,8 @@ class DBHelper {
|
|||
list[x]['downloaded'],
|
||||
list[x]['duration'],
|
||||
list[x]['explicit'],
|
||||
list[x]['imagePath']));
|
||||
list[x]['imagePath'],
|
||||
list[x]['media_id']));
|
||||
}
|
||||
return episodes;
|
||||
}
|
||||
|
@ -410,7 +473,7 @@ class DBHelper {
|
|||
List<Map> list = await dbClient.rawQuery(
|
||||
"""SELECT E.title, E.enclosure_url, E.enclosure_length,
|
||||
E.milliseconds, P.imagePath, P.title as feed_title, E.duration, E.explicit, E.liked,
|
||||
E.downloaded, P.primaryColor
|
||||
E.downloaded, P.primaryColor, E.media_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""",
|
||||
[url]);
|
||||
|
@ -427,19 +490,20 @@ class DBHelper {
|
|||
list.first['downloaded'],
|
||||
list.first['duration'],
|
||||
list.first['explicit'],
|
||||
list.first['imagePath']);
|
||||
list.first['imagePath'],
|
||||
list.first['media_id']);
|
||||
return episode;
|
||||
}
|
||||
|
||||
Future<List<EpisodeBrief>> getRecentRssItem() async {
|
||||
Future<List<EpisodeBrief>> getRecentRssItem(int top) async {
|
||||
var dbClient = await database;
|
||||
List<EpisodeBrief> episodes = List();
|
||||
List<Map> list = await dbClient
|
||||
.rawQuery("""SELECT E.title, E.enclosure_url, E.enclosure_length,
|
||||
E.milliseconds, P.title as feed_title, E.duration, E.explicit, E.liked,
|
||||
E.downloaded, P.imagePath, P.primaryColor
|
||||
E.downloaded, P.imagePath, P.primaryColor, E.media_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++) {
|
||||
episodes.add(EpisodeBrief(
|
||||
list[x]['title'],
|
||||
|
@ -452,7 +516,8 @@ class DBHelper {
|
|||
list[x]['doanloaded'],
|
||||
list[x]['duration'],
|
||||
list[x]['explicit'],
|
||||
list[x]['imagePath']));
|
||||
list[x]['imagePath'],
|
||||
list[x]['media_id']));
|
||||
}
|
||||
return episodes;
|
||||
}
|
||||
|
@ -463,7 +528,7 @@ class DBHelper {
|
|||
List<Map> list = await dbClient.rawQuery(
|
||||
"""SELECT E.title, E.enclosure_url, E.enclosure_length, E.milliseconds, P.imagePath,
|
||||
P.title as feed_title, E.duration, E.explicit, E.liked, E.downloaded,
|
||||
P.primaryColor 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""");
|
||||
for (int x = 0; x < list.length; x++) {
|
||||
episodes.add(EpisodeBrief(
|
||||
|
@ -477,7 +542,8 @@ class DBHelper {
|
|||
list[x]['downloaded'],
|
||||
list[x]['duration'],
|
||||
list[x]['explicit'],
|
||||
list[x]['imagePath']));
|
||||
list[x]['imagePath'],
|
||||
list[x]['media_id']));
|
||||
}
|
||||
return episodes;
|
||||
}
|
||||
|
@ -507,10 +573,21 @@ class DBHelper {
|
|||
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 {
|
||||
var dbClient = await database;
|
||||
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);
|
||||
return count;
|
||||
}
|
||||
|
@ -521,7 +598,7 @@ class DBHelper {
|
|||
List<Map> list = await dbClient.rawQuery(
|
||||
"""SELECT E.title, E.enclosure_url, E.enclosure_length, E.milliseconds, P.imagePath,
|
||||
P.title as feed_title, E.duration, E.explicit, E.liked, E.downloaded,
|
||||
P.primaryColor 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""");
|
||||
for (int x = 0; x < list.length; x++) {
|
||||
episodes.add(EpisodeBrief(
|
||||
|
@ -535,7 +612,8 @@ class DBHelper {
|
|||
list[x]['downloaded'],
|
||||
list[x]['duration'],
|
||||
list[x]['explicit'],
|
||||
list[x]['imagePath']));
|
||||
list[x]['imagePath'],
|
||||
list[x]['media_id']));
|
||||
}
|
||||
return episodes;
|
||||
}
|
||||
|
@ -562,7 +640,7 @@ class DBHelper {
|
|||
List<Map> list = await dbClient.rawQuery(
|
||||
"""SELECT E.title, E.enclosure_url, E.enclosure_length, E.milliseconds, P.imagePath,
|
||||
P.title as feed_title, E.duration, E.explicit, E.liked, E.downloaded,
|
||||
P.primaryColor 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]);
|
||||
episode = EpisodeBrief(
|
||||
list.first['title'],
|
||||
|
@ -575,7 +653,33 @@ class DBHelper {
|
|||
list.first['downloaded'],
|
||||
list.first['duration'],
|
||||
list.first['explicit'],
|
||||
list.first['imagePath']);
|
||||
list.first['imagePath'],
|
||||
list.first['media_id']);
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -10,57 +10,61 @@ import 'package:tsacdop/home/appbar/addpodcast.dart';
|
|||
import 'package:tsacdop/class/audiostate.dart';
|
||||
import 'package:tsacdop/class/importompl.dart';
|
||||
import 'package:tsacdop/class/settingstate.dart';
|
||||
import '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();
|
||||
|
||||
Future main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await themeSetting.initData();
|
||||
await FlutterDownloader.initialize();
|
||||
runApp(
|
||||
MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider(create: (_) => themeSetting),
|
||||
ChangeNotifierProvider(create: (_) => AudioPlayer()),
|
||||
ChangeNotifierProvider(create: (_) => AudioPlayerNotifier()),
|
||||
ChangeNotifierProvider(create: (_) => GroupList()),
|
||||
ChangeNotifierProvider(create: (_) => ImportOmpl()),
|
||||
],
|
||||
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(
|
||||
[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 {
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<SettingState>(
|
||||
builder: (_, setting, __) {
|
||||
if (setting.autoUpdate) setWorkManager();
|
||||
return MaterialApp(
|
||||
themeMode: setting.theme,
|
||||
debugShowCheckedModeBanner: false,
|
||||
|
@ -78,7 +82,6 @@ class MyApp extends StatelessWidget {
|
|||
elevation: 0,
|
||||
),
|
||||
textTheme: TextTheme(
|
||||
headline1: TextStyle(fontSize: 72.0, fontWeight: FontWeight.bold),
|
||||
bodyText2:
|
||||
TextStyle(fontSize: 15.0, fontWeight: FontWeight.normal),
|
||||
),
|
||||
|
@ -89,6 +92,7 @@ class MyApp extends StatelessWidget {
|
|||
),
|
||||
darkTheme: ThemeData.dark().copyWith(
|
||||
accentColor: setting.accentSetColor,
|
||||
appBarTheme: AppBarTheme(elevation: 0),
|
||||
),
|
||||
home: MyHomePage(),
|
||||
);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -7,16 +7,13 @@ import 'package:flutter/services.dart';
|
|||
import 'package:html/parser.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:tsacdop/class/podcastlocal.dart';
|
||||
import 'package:tsacdop/class/episodebrief.dart';
|
||||
import 'package:tsacdop/episodes/episodedetail.dart';
|
||||
import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
|
||||
import 'package:tsacdop/util/episodegrid.dart';
|
||||
import 'package:tsacdop/util/pageroute.dart';
|
||||
import 'package:tsacdop/home/audioplayer.dart';
|
||||
import 'package:tsacdop/class/fireside_data.dart';
|
||||
import 'package:tsacdop/util/colorize.dart';
|
||||
|
@ -104,7 +101,9 @@ class _PodcastDetailState extends State<PodcastDetail> {
|
|||
fit: BoxFit.cover)),
|
||||
alignment: Alignment.centerRight,
|
||||
child: Container(
|
||||
color: Theme.of(context).scaffoldBackgroundColor.withOpacity(0.5),
|
||||
color: Theme.of(context)
|
||||
.scaffoldBackgroundColor
|
||||
.withOpacity(0.5),
|
||||
padding: EdgeInsets.symmetric(vertical: 5.0),
|
||||
width: MediaQuery.of(context).size.width,
|
||||
alignment: Alignment.centerRight,
|
||||
|
@ -122,7 +121,8 @@ class _PodcastDetailState extends State<PodcastDetail> {
|
|||
children: <Widget>[
|
||||
CircleAvatar(
|
||||
backgroundColor: Colors.grey[400],
|
||||
backgroundImage: CachedNetworkImageProvider(
|
||||
backgroundImage:
|
||||
CachedNetworkImageProvider(
|
||||
host.image,
|
||||
)),
|
||||
Padding(
|
||||
|
@ -162,7 +162,7 @@ class _PodcastDetailState extends State<PodcastDetail> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
double _width = MediaQuery.of(context).size.width;
|
||||
|
||||
Color _color = widget.podcastLocal.primaryColor.colorizedark();
|
||||
return AnnotatedRegion<SystemUiOverlayStyle>(
|
||||
value: SystemUiOverlayStyle(
|
||||
|
@ -283,7 +283,7 @@ class _PodcastDetailState extends State<PodcastDetail> {
|
|||
'Hosted on ' +
|
||||
widget.podcastLocal
|
||||
.provider,
|
||||
maxLines: 1,
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
color: Colors.white),
|
||||
)
|
||||
|
@ -308,6 +308,8 @@ class _PodcastDetailState extends State<PodcastDetail> {
|
|||
),
|
||||
title: top < 70
|
||||
? Text(widget.podcastLocal.title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(color: Colors.white))
|
||||
: Center(),
|
||||
);
|
||||
|
@ -322,184 +324,15 @@ class _PodcastDetailState extends State<PodcastDetail> {
|
|||
),
|
||||
),
|
||||
SliverPadding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 15.0),
|
||||
sliver: SliverGrid(
|
||||
gridDelegate:
|
||||
SliverGridDelegateWithFixedCrossAxisCount(
|
||||
childAspectRatio: 1.0,
|
||||
crossAxisCount: 3,
|
||||
mainAxisSpacing: 6.0,
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10.0),
|
||||
sliver: EpisodeGrid(
|
||||
podcast: snapshot.data,
|
||||
showDownload: false,
|
||||
showFavorite: true,
|
||||
showNumber: true,
|
||||
heroTag: 'podcast',
|
||||
)),
|
||||
],
|
||||
)
|
||||
: Center(child: CircularProgressIndicator());
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
import 'dart:io';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter_colorpicker/flutter_colorpicker.dart';
|
||||
|
||||
import 'package:tsacdop/class/podcast_group.dart';
|
||||
import 'package:tsacdop/class/podcastlocal.dart';
|
||||
|
@ -21,87 +19,7 @@ class PodcastGroupList extends StatefulWidget {
|
|||
_PodcastGroupListState createState() => _PodcastGroupListState();
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _PodcastGroupListState extends State<PodcastGroupList> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var groupList = Provider.of<GroupList>(context, listen: false);
|
||||
|
@ -109,372 +27,38 @@ class _PodcastGroupListState extends State<PodcastGroupList>
|
|||
? Container(
|
||||
color: Theme.of(context).primaryColor,
|
||||
)
|
||||
: Stack(
|
||||
children: <Widget>[
|
||||
Container(
|
||||
color: Theme.of(context).primaryColor,
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
ReorderableListView(
|
||||
onReorder: (int oldIndex, int newIndex) {
|
||||
setState(() {
|
||||
if (newIndex > oldIndex) {
|
||||
newIndex -= 1;
|
||||
}
|
||||
final PodcastLocal podcast =
|
||||
widget.group.podcasts.removeAt(oldIndex);
|
||||
widget.group.podcasts.insert(newIndex, podcast);
|
||||
_controller.forward();
|
||||
});
|
||||
},
|
||||
children: widget.group.podcasts
|
||||
.map<Widget>((PodcastLocal podcastLocal) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).primaryColor),
|
||||
key: ObjectKey(podcastLocal.title),
|
||||
child: PodcastCard(
|
||||
podcastLocal: podcastLocal,
|
||||
group: widget.group,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 30,
|
||||
right: 30,
|
||||
child: _saveButton(context),
|
||||
),
|
||||
],
|
||||
: Container(
|
||||
color: Theme.of(context).primaryColor,
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
ReorderableListView(
|
||||
onReorder: (int oldIndex, int newIndex) {
|
||||
setState(() {
|
||||
if (newIndex > oldIndex) {
|
||||
newIndex -= 1;
|
||||
}
|
||||
final PodcastLocal podcast =
|
||||
widget.group.podcasts.removeAt(oldIndex);
|
||||
widget.group.podcasts.insert(newIndex, podcast);
|
||||
});
|
||||
widget.group.setOrderedPodcasts = widget.group.podcasts;
|
||||
groupList.addToOrderChanged(widget.group.name);
|
||||
},
|
||||
children: widget.group.podcasts
|
||||
.map<Widget>((PodcastLocal podcastLocal) {
|
||||
return Container(
|
||||
decoration:
|
||||
BoxDecoration(color: Theme.of(context).primaryColor),
|
||||
key: ObjectKey(podcastLocal.title),
|
||||
child: PodcastCard(
|
||||
podcastLocal: podcastLocal,
|
||||
group: widget.group,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
_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)),
|
||||
)
|
||||
],
|
||||
title: Text('Create new group'),
|
||||
title: Text('Edit group name'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
TextField(
|
||||
decoration: InputDecoration(
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 10),
|
||||
hintText: 'New Group',
|
||||
hintText: widget.group.name,
|
||||
hintStyle: TextStyle(fontSize: 18),
|
||||
filled: true,
|
||||
focusedBorder: UnderlineInputBorder(
|
||||
|
|
|
@ -1,23 +1,135 @@
|
|||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.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/podcasts/podcastgroup.dart';
|
||||
import 'package:tsacdop/podcasts/podcastlist.dart';
|
||||
import 'package:tsacdop/util/pageroute.dart';
|
||||
import 'custom_tabview.dart';
|
||||
|
||||
class PodcastManage extends StatefulWidget {
|
||||
@override
|
||||
_PodcastManageState createState() => _PodcastManageState();
|
||||
}
|
||||
|
||||
class _PodcastManageState extends State<PodcastManage> {
|
||||
Decoration getIndicator() {
|
||||
return const UnderlineTabIndicator(
|
||||
borderSide: BorderSide(color: Colors.red, width: 0),
|
||||
insets: EdgeInsets.only(
|
||||
top: 10.0,
|
||||
));
|
||||
class _PodcastManageState extends State<PodcastManage>
|
||||
with TickerProviderStateMixin {
|
||||
bool _showSetting;
|
||||
double _menuValue;
|
||||
AnimationController _controller;
|
||||
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) {
|
||||
|
@ -53,59 +165,300 @@ class _PodcastManageState extends State<PodcastManage> {
|
|||
List<PodcastGroup> _groups = groupList.groups;
|
||||
return _isLoading
|
||||
? Center()
|
||||
: DefaultTabController(
|
||||
length: _groups.length,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
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(
|
||||
: Stack(
|
||||
children: <Widget>[
|
||||
CustomTabView(
|
||||
itemCount: _groups.length,
|
||||
tabBuilder: (context, index) => Tab(
|
||||
child: Container(
|
||||
child: TabBarView(
|
||||
children: _groups.map<Widget>((group) {
|
||||
return Container(
|
||||
key: ObjectKey(group),
|
||||
child: PodcastGroupList(group: group));
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
));
|
||||
height: 30.0,
|
||||
padding: EdgeInsets.symmetric(horizontal: 10.0),
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: (_scroll - index).abs() > 1
|
||||
? Colors.grey[300]
|
||||
: Colors.grey[300]
|
||||
.withOpacity((_scroll - index).abs()),
|
||||
borderRadius:
|
||||
BorderRadius.all(Radius.circular(15)),
|
||||
),
|
||||
child: Text(
|
||||
_groups[index].name,
|
||||
)),
|
||||
),
|
||||
pageBuilder: (context, index) => Container(
|
||||
key: ObjectKey(_groups[index].name),
|
||||
child: PodcastGroupList(group: _groups[index])),
|
||||
onPositionChange: (value) =>
|
||||
setState(() => _index = value),
|
||||
onScroll: (value) => setState(() => _scroll = value),
|
||||
),
|
||||
_showSetting
|
||||
? Positioned.fill(
|
||||
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(),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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'),
|
||||
];
|
|
@ -1,10 +1,30 @@
|
|||
import 'package:flutter/material.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/storage.dart';
|
||||
import 'package:tsacdop/settings/history.dart';
|
||||
import 'libries.dart';
|
||||
|
||||
class Settings extends StatelessWidget {
|
||||
_launchUrl(String url) async {
|
||||
if (await canLaunch(url)) {
|
||||
await launch(url);
|
||||
} else {
|
||||
throw 'Could not launch $url';
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
|
||||
var settings = Provider.of<SettingState>(context, listen: false);
|
||||
return AnnotatedRegion<SystemUiOverlayStyle>(
|
||||
value: SystemUiOverlayStyle(
|
||||
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
|
||||
|
@ -18,121 +38,167 @@ class Settings extends StatelessWidget {
|
|||
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('Prefrence',
|
||||
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) => ThemeSetting())),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 25.0),
|
||||
leading: Icon(Icons.colorize),
|
||||
title: Text('Appearance'),
|
||||
subtitle: Text('Colors and themes'),
|
||||
),
|
||||
Divider(height: 2),
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 25.0),
|
||||
leading: Icon(Icons.network_check),
|
||||
title: Text('Network'),
|
||||
subtitle: Text('Download network setting'),
|
||||
),
|
||||
Divider(height: 2),
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 25.0),
|
||||
leading: Icon(Icons.storage),
|
||||
title: Text('Cache'),
|
||||
subtitle: Text('Manage and clear cache'),
|
||||
),
|
||||
Divider(height: 2),
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 25.0),
|
||||
leading: Icon(Icons.update),
|
||||
title: Text('Update'),
|
||||
subtitle: Text('Update in background'),
|
||||
),
|
||||
Divider(height: 2),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.all(10.0),
|
||||
),
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
height: 30.0,
|
||||
padding: EdgeInsets.symmetric(horizontal: 80),
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text('Info',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyText1
|
||||
.copyWith(color: Theme.of(context).accentColor)),
|
||||
),
|
||||
ListView(
|
||||
shrinkWrap: true,
|
||||
scrollDirection: Axis.vertical,
|
||||
children: <Widget>[
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 25.0),
|
||||
leading: Icon(Icons.colorize),
|
||||
title: Text('Changelog'),
|
||||
subtitle: Text('List of chagnes'),
|
||||
),
|
||||
Divider(height: 2),
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 25.0),
|
||||
leading: Icon(Icons.network_check),
|
||||
title: Text('Credit'),
|
||||
subtitle: Text('Open source libraried in application'),
|
||||
),
|
||||
Divider(height: 2),
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 25.0),
|
||||
leading: Icon(Icons.storage),
|
||||
title: Text('Cache'),
|
||||
subtitle: Text('Manage and clear cache'),
|
||||
),
|
||||
Divider(height: 2),
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 25.0),
|
||||
leading: Icon(Icons.update),
|
||||
title: Text('Update'),
|
||||
subtitle: Text('Update in background'),
|
||||
),
|
||||
Divider(height: 2),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
body: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
scrollDirection: Axis.vertical,
|
||||
child: 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('Prefrence',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyText1
|
||||
.copyWith(color: Theme.of(context).accentColor)),
|
||||
),
|
||||
ListView(
|
||||
physics: ClampingScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
scrollDirection: Axis.vertical,
|
||||
children: <Widget>[
|
||||
ListTile(
|
||||
onTap: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ThemeSetting())),
|
||||
contentPadding:
|
||||
EdgeInsets.symmetric(horizontal: 25.0),
|
||||
leading: Icon(LineIcons.adjust_solid),
|
||||
title: Text('Appearance'),
|
||||
subtitle: Text('Colors and themes'),
|
||||
),
|
||||
Divider(height: 2),
|
||||
ListTile(
|
||||
contentPadding:
|
||||
EdgeInsets.symmetric(horizontal: 25.0),
|
||||
leading: Icon(LineIcons.play_circle),
|
||||
title: Text('AutoPlay'),
|
||||
subtitle: Text('Autoplay next episode in playlist'),
|
||||
trailing: Selector<AudioPlayerNotifier, bool>(
|
||||
selector: (_, audio) => audio.autoPlay,
|
||||
builder: (_, data, __) => Switch(
|
||||
value: data,
|
||||
onChanged: (boo) => audio.autoPlaySwitch = boo),
|
||||
),
|
||||
),
|
||||
Divider(height: 2),
|
||||
ListTile(
|
||||
contentPadding:
|
||||
EdgeInsets.symmetric(horizontal: 25.0),
|
||||
leading: Icon(LineIcons.cloud_download_alt_solid),
|
||||
title: Text('AutoUpdate'),
|
||||
subtitle: Text('Auto update feed every day'),
|
||||
trailing: Selector<SettingState, bool>(
|
||||
selector: (_, settings) => settings.autoUpdate,
|
||||
builder: (_, data, __) => Switch(
|
||||
value: data,
|
||||
onChanged: (boo) async {
|
||||
settings.autoUpdate = boo;
|
||||
if (!boo) await Workmanager.cancelAll();
|
||||
}),
|
||||
),
|
||||
),
|
||||
Divider(height: 2),
|
||||
ListTile(
|
||||
onTap: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => StorageSetting())),
|
||||
contentPadding:
|
||||
EdgeInsets.symmetric(horizontal: 25.0),
|
||||
leading: Icon(LineIcons.save),
|
||||
title: Text('Storage'),
|
||||
subtitle: Text('Manage cache and download storage'),
|
||||
),
|
||||
Divider(height: 2),
|
||||
ListTile(
|
||||
onTap: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => PlayedHistory())),
|
||||
contentPadding:
|
||||
EdgeInsets.symmetric(horizontal: 25.0),
|
||||
leading: Icon(Icons.update),
|
||||
title: Text('History'),
|
||||
subtitle: Text('Listen data'),
|
||||
),
|
||||
Divider(height: 2),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.all(10.0),
|
||||
),
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
height: 30.0,
|
||||
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),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -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),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -7,7 +7,7 @@ import 'package:tsacdop/class/settingstate.dart';
|
|||
class ThemeSetting extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var settings = Provider.of<SettingState>(context);
|
||||
var settings = Provider.of<SettingState>(context, listen: false);
|
||||
return AnnotatedRegion<SystemUiOverlayStyle>(
|
||||
value: SystemUiOverlayStyle(
|
||||
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
|
||||
|
|
|
@ -22,7 +22,8 @@ extension Colorize on String {
|
|||
_c =
|
||||
Color.fromRGBO((255 - color[0]), 255 - color[1], 255 - color[2], 1.0);
|
||||
} 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;
|
||||
}
|
||||
|
|
|
@ -5,6 +5,11 @@ import 'dart:ui';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_downloader/flutter_downloader.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/episodes/episodedetail.dart';
|
||||
import 'package:tsacdop/util/pageroute.dart';
|
||||
|
@ -24,32 +29,119 @@ class EpisodeGrid extends StatelessWidget {
|
|||
this.showNumber,
|
||||
this.heroTag})
|
||||
: super(key: key);
|
||||
|
||||
Offset offset;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
double _width = MediaQuery.of(context).size.width;
|
||||
return CustomScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
primary: false,
|
||||
slivers: <Widget>[
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.all(5.0),
|
||||
sliver: SliverGrid(
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
childAspectRatio: 1.0,
|
||||
crossAxisCount: 3,
|
||||
mainAxisSpacing: 6.0,
|
||||
crossAxisSpacing: 6.0,
|
||||
|
||||
_showPopupMenu(Offset offset, EpisodeBrief episode, BuildContext context,
|
||||
bool isPlaying, bool isInPlaylist) async {
|
||||
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
|
||||
double left = offset.dx;
|
||||
double top = offset.dy;
|
||||
await showMenu<int>(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(10))),
|
||||
context: context,
|
||||
position: RelativeRect.fromLTRB(left, top, _width - left, 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) {
|
||||
Color _c =
|
||||
(Theme.of(context).brightness == Brightness.light)
|
||||
? podcast[index].primaryColor.colorizedark()
|
||||
: podcast[index].primaryColor.colorizeLight();
|
||||
return Material(
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 1,
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
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,
|
||||
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: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
|
@ -61,25 +153,17 @@ class EpisodeGrid extends StatelessWidget {
|
|||
);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
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),
|
||||
borderRadius: BorderRadius.all(Radius.circular(5.0)),
|
||||
border: Border.all(
|
||||
color:
|
||||
Theme.of(context).brightness == Brightness.light
|
||||
? Theme.of(context).primaryColor
|
||||
: Theme.of(context).scaffoldBackgroundColor,
|
||||
width: 1.0,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
|
@ -94,6 +178,7 @@ class EpisodeGrid extends StatelessWidget {
|
|||
height: _width / 16,
|
||||
width: _width / 16,
|
||||
child: CircleAvatar(
|
||||
backgroundColor: _c.withOpacity(0.5),
|
||||
backgroundImage: FileImage(
|
||||
File("${podcast[index].imagePath}")),
|
||||
),
|
||||
|
@ -140,7 +225,6 @@ class EpisodeGrid extends StatelessWidget {
|
|||
alignment: Alignment.bottomLeft,
|
||||
child: Text(
|
||||
podcast[index].dateToString(),
|
||||
//podcast[index].pubDate.substring(4, 16),
|
||||
style: TextStyle(
|
||||
fontSize: _width / 35,
|
||||
color: _c,
|
||||
|
@ -175,13 +259,13 @@ class EpisodeGrid extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
childCount: podcast.length,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
childCount: podcast.length,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
After Width: | Height: | Size: 122 KiB |
After Width: | Height: | Size: 141 KiB |
After Width: | Height: | Size: 175 KiB |
After Width: | Height: | Size: 216 KiB |
18
pubspec.yaml
|
@ -11,7 +11,7 @@ description: An easy-use podacasts player.
|
|||
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
||||
# Read more about iOS versioning at
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
version: 0.1.1
|
||||
version: 0.1.2
|
||||
|
||||
environment:
|
||||
sdk: ">=2.6.0 <3.0.0"
|
||||
|
@ -28,7 +28,7 @@ dev_dependencies:
|
|||
flutter_test:
|
||||
sdk: flutter
|
||||
json_annotation: ^3.0.1
|
||||
sqflite: ^1.2.1
|
||||
sqflite: ^1.2.2+1
|
||||
flutter_html: ^0.11.1
|
||||
path_provider: ^1.6.1
|
||||
color_thief_flutter: ^1.0.1
|
||||
|
@ -38,7 +38,6 @@ dev_dependencies:
|
|||
file_picker: ^1.4.3+2
|
||||
xml: ^3.5.0
|
||||
marquee: ^1.3.1
|
||||
audiofileplayer: ^1.1.1
|
||||
flutter_downloader: ^1.4.1
|
||||
permission_handler: ^4.3.0
|
||||
fluttertoast: ^3.1.3
|
||||
|
@ -49,11 +48,16 @@ dev_dependencies:
|
|||
uuid: ^2.0.4
|
||||
tuple: ^1.0.3
|
||||
cached_network_image: ^2.0.0
|
||||
workmanager: ^0.2.0
|
||||
font_awesome_flutter: ^8.7.0
|
||||
workmanager: ^0.2.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
|
||||
|
|