Export ompli file
Storage management Syncing setting
@ -16,7 +16,7 @@ jobs:
|
|||||||
|
|
||||||
- run: echo $ENCODED_KEYSTORE | base64 -di > ${HOME}/keystore.jks
|
- run: echo $ENCODED_KEYSTORE | base64 -di > ${HOME}/keystore.jks
|
||||||
- run: echo 'export KEYSTORE=${HOME}/keystore.jks' >> $BASH_ENV
|
- run: echo 'export KEYSTORE=${HOME}/keystore.jks' >> $BASH_ENV
|
||||||
|
- run: dart tool/env.dart
|
||||||
- run:
|
- run:
|
||||||
name: Build the Android version
|
name: Build the Android version
|
||||||
command: flutter build apk --split-per-abi --no-shrink
|
command: flutter build apk --split-per-abi --no-shrink
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
<img src="https://raw.githubusercontent.com/stonega/tsacdop/master/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png" art = "Logo"/>
|
<img src="https://raw.githubusercontent.com/stonega/tsacdop/master/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png" art = "Logo"/>
|
||||||
</br>
|
</br>
|
||||||
<img src="https://raw.githubusercontent.com/stonega/tsacdop/master/android/app/src/main/res/mipmap-xhdpi/text.png" art = "Tsacdop"/>
|
<img src="https://raw.githubusercontent.com/stonega/tsacdop/master/android/app/src/main/res/mipmap-xhdpi/text.png" art = "Tsacdop"/>
|
||||||
t p</p>
|
</p>
|
||||||
|
|
||||||
Enjoy podcasts with Tsacdop.
|
Enjoy podcasts with Tsacdop.
|
||||||
|
|
||||||
|
@ -45,7 +45,7 @@ android {
|
|||||||
defaultConfig {
|
defaultConfig {
|
||||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||||
applicationId "com.stonegate.tsacdop"
|
applicationId "com.stonegate.tsacdop"
|
||||||
minSdkVersion 16
|
minSdkVersion 19
|
||||||
targetSdkVersion 28
|
targetSdkVersion 28
|
||||||
versionCode flutterVersionCode.toInteger()
|
versionCode flutterVersionCode.toInteger()
|
||||||
versionName flutterVersionName
|
versionName flutterVersionName
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.stonegate.tsacdop">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.stonegate.tsacdop" xmlns:tools="http://schemas.android.com/tools">
|
||||||
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
|
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
|
||||||
calls FlutterMain.startInitialization(this); in its onCreate method.
|
calls FlutterMain.startInitialization(this); in its onCreate method.
|
||||||
In most cases you can leave this as-is, but you if you want to provide
|
In most cases you can leave this as-is, but you if you want to provide
|
||||||
@ -7,7 +7,7 @@
|
|||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||||
<application android:name="io.flutter.app.FlutterApplication" android:label="Tsacdop" android:icon="@mipmap/ic_launcher" android:networkSecurityConfig="@xml/network_security_config">
|
<application android:name="io.flutter.app.FlutterApplication" android:label="Tsacdop" android:icon="@mipmap/ic_launcher_icon" android:roundIcon="@mipmap/ic_launcher_round" android:networkSecurityConfig="@xml/network_security_config">
|
||||||
<activity android:name=".MainActivity" android:launchMode="singleTop" android:theme="@style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize">
|
<activity android:name=".MainActivity" android:launchMode="singleTop" android:theme="@style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
BIN
android/app/src/main/res/drawable-hdpi/ic_notification.png
Normal file
After Width: | Height: | Size: 725 B |
BIN
android/app/src/main/res/drawable-mdpi/ic_notification.png
Normal file
After Width: | Height: | Size: 427 B |
BIN
android/app/src/main/res/drawable-xhdpi/ic_notification.png
Normal file
After Width: | Height: | Size: 822 B |
BIN
android/app/src/main/res/drawable-xxhdpi/ic_notification.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
android/app/src/main/res/drawable-xxxhdpi/ic_notification.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
@ -7,7 +7,7 @@
|
|||||||
<bitmap
|
<bitmap
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:tileMode="disabled"
|
android:tileMode="disabled"
|
||||||
android:src="@mipmap/ic_launcher" />
|
android:src="@mipmap/ic_splash" />
|
||||||
</item>
|
</item>
|
||||||
<item android:bottom="100dp">
|
<item android:bottom="100dp">
|
||||||
<bitmap
|
<bitmap
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
<bitmap
|
<bitmap
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:tileMode="disabled"
|
android:tileMode="disabled"
|
||||||
android:src="@mipmap/ic_launcher" />
|
android:src="@mipmap/ic_splash" />
|
||||||
</item>
|
</item>
|
||||||
<item android:bottom="100dp">
|
<item android:bottom="100dp">
|
||||||
<bitmap
|
<bitmap
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 2.5 KiB |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 4.1 KiB |
BIN
android/app/src/main/res/mipmap-hdpi/ic_notification.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
android/app/src/main/res/mipmap-hdpi/ic_splash.png
Normal file
After Width: | Height: | Size: 6.5 KiB |
BIN
android/app/src/main/res/mipmap-ldpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
android/app/src/main/res/mipmap-ldpi/ic_notification.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
android/app/src/main/res/mipmap-ldpi/ic_splash.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 1.3 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_notification.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_splash.png
Normal file
After Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 2.6 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
Normal file
After Width: | Height: | Size: 3.6 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 4.7 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_notification.png
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_splash.png
Normal file
After Width: | Height: | Size: 9.0 KiB |
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 5.5 KiB |
After Width: | Height: | Size: 10 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 9.2 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_notification.png
Normal file
After Width: | Height: | Size: 7.2 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_splash.png
Normal file
After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 5.9 KiB |
After Width: | Height: | Size: 15 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_notification.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_splash.png
Normal file
After Width: | Height: | Size: 21 KiB |
@ -3,4 +3,7 @@
|
|||||||
<color name = "blackGrey">
|
<color name = "blackGrey">
|
||||||
#121212
|
#121212
|
||||||
</color>
|
</color>
|
||||||
|
<color name="ic_launcher_background">
|
||||||
|
#ffffff
|
||||||
|
</color>
|
||||||
</resources>
|
</resources>
|
@ -80,8 +80,7 @@ class Playlist {
|
|||||||
_playlist = [];
|
_playlist = [];
|
||||||
await Future.forEach(urls, (url) async {
|
await Future.forEach(urls, (url) async {
|
||||||
EpisodeBrief episode = await dbHelper.getRssItemWithUrl(url);
|
EpisodeBrief episode = await dbHelper.getRssItemWithUrl(url);
|
||||||
print(episode.title);
|
if(episode != null) _playlist.add(episode);
|
||||||
_playlist.add(episode);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
print('Playlist: ' + _playlist.length.toString());
|
print('Playlist: ' + _playlist.length.toString());
|
||||||
@ -127,8 +126,10 @@ class AudioPlayerNotifier extends ChangeNotifier {
|
|||||||
int _lastPostion = 0;
|
int _lastPostion = 0;
|
||||||
bool _stopOnComplete = false;
|
bool _stopOnComplete = false;
|
||||||
Timer _stopTimer;
|
Timer _stopTimer;
|
||||||
|
int _timeLeft = 0;
|
||||||
//Show stopwatch after user setting timer.
|
//Show stopwatch after user setting timer.
|
||||||
bool _showStopWatch = false;
|
bool _showStopWatch = false;
|
||||||
|
double _switchValue = 0;
|
||||||
bool _autoPlay = true;
|
bool _autoPlay = true;
|
||||||
DateTime _current;
|
DateTime _current;
|
||||||
int _currentPosition;
|
int _currentPosition;
|
||||||
@ -146,11 +147,18 @@ class AudioPlayerNotifier extends ChangeNotifier {
|
|||||||
bool get stopOnComplete => _stopOnComplete;
|
bool get stopOnComplete => _stopOnComplete;
|
||||||
bool get showStopWatch => _showStopWatch;
|
bool get showStopWatch => _showStopWatch;
|
||||||
bool get autoPlay => _autoPlay;
|
bool get autoPlay => _autoPlay;
|
||||||
|
int get timeLeft => _timeLeft;
|
||||||
|
double get switchValue => _switchValue;
|
||||||
|
|
||||||
set setStopOnComplete(bool boo) {
|
set setSwitchValue(double value) {
|
||||||
_stopOnComplete = boo;
|
_switchValue = value;
|
||||||
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// set setStopOnComplete(bool boo) {
|
||||||
|
// _stopOnComplete = boo;
|
||||||
|
//}
|
||||||
|
|
||||||
set autoPlaySwitch(bool boo) {
|
set autoPlaySwitch(bool boo) {
|
||||||
_autoPlay = boo;
|
_autoPlay = boo;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@ -203,12 +211,12 @@ class AudioPlayerNotifier extends ChangeNotifier {
|
|||||||
backgroundTaskEntrypoint: _audioPlayerTaskEntrypoint,
|
backgroundTaskEntrypoint: _audioPlayerTaskEntrypoint,
|
||||||
androidNotificationChannelName: 'Tsacdop',
|
androidNotificationChannelName: 'Tsacdop',
|
||||||
notificationColor: 0xFF2196f3,
|
notificationColor: 0xFF2196f3,
|
||||||
androidNotificationIcon: 'mipmap/ic_launcher',
|
androidNotificationIcon: 'drawable/ic_notification',
|
||||||
enableQueue: true,
|
enableQueue: true,
|
||||||
androidStopOnRemoveTask: true,
|
androidStopOnRemoveTask: true,
|
||||||
);
|
);
|
||||||
_playerRunning = true;
|
_playerRunning = true;
|
||||||
if (autoPlay) {
|
if (_autoPlay) {
|
||||||
await Future.forEach(_queue.playlist, (episode) async {
|
await Future.forEach(_queue.playlist, (episode) async {
|
||||||
await AudioService.addQueueItem(episode.toMediaItem());
|
await AudioService.addQueueItem(episode.toMediaItem());
|
||||||
});
|
});
|
||||||
@ -217,8 +225,6 @@ class AudioPlayerNotifier extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
await AudioService.play();
|
await AudioService.play();
|
||||||
AudioService.currentMediaItemStream.listen((item) async {
|
AudioService.currentMediaItemStream.listen((item) async {
|
||||||
print(position);
|
|
||||||
print(_backgroundAudioDuration);
|
|
||||||
if (item != null) {
|
if (item != null) {
|
||||||
_episode = await dbHelper.getRssItemWithMediaId(item.id);
|
_episode = await dbHelper.getRssItemWithMediaId(item.id);
|
||||||
_backgroundAudioDuration = item?.duration ?? 0;
|
_backgroundAudioDuration = item?.duration ?? 0;
|
||||||
@ -226,7 +232,6 @@ class AudioPlayerNotifier extends ChangeNotifier {
|
|||||||
AudioService.seekTo(position);
|
AudioService.seekTo(position);
|
||||||
position = 0;
|
position = 0;
|
||||||
}
|
}
|
||||||
// _playerRunning = true;
|
|
||||||
}
|
}
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
});
|
});
|
||||||
@ -235,6 +240,7 @@ class AudioPlayerNotifier extends ChangeNotifier {
|
|||||||
_audioState = event?.basicState;
|
_audioState = event?.basicState;
|
||||||
if (_audioState == BasicPlaybackState.skippingToNext &&
|
if (_audioState == BasicPlaybackState.skippingToNext &&
|
||||||
_episode != null) {
|
_episode != null) {
|
||||||
|
print(_episode.title);
|
||||||
_queue.delFromPlaylist(_episode);
|
_queue.delFromPlaylist(_episode);
|
||||||
}
|
}
|
||||||
if (_audioState == BasicPlaybackState.paused ||
|
if (_audioState == BasicPlaybackState.paused ||
|
||||||
@ -253,7 +259,7 @@ class AudioPlayerNotifier extends ChangeNotifier {
|
|||||||
Timer.periodic(Duration(milliseconds: 500), (timer) {
|
Timer.periodic(Duration(milliseconds: 500), (timer) {
|
||||||
if (_noSlide) {
|
if (_noSlide) {
|
||||||
_audioState == BasicPlaybackState.playing
|
_audioState == BasicPlaybackState.playing
|
||||||
? (_backgroundAudioPosition < _backgroundAudioDuration)
|
? (_backgroundAudioPosition < _backgroundAudioDuration - 500)
|
||||||
? _backgroundAudioPosition = _currentPosition +
|
? _backgroundAudioPosition = _currentPosition +
|
||||||
DateTime.now().difference(_current).inMilliseconds
|
DateTime.now().difference(_current).inMilliseconds
|
||||||
: _backgroundAudioPosition = _backgroundAudioDuration
|
: _backgroundAudioPosition = _backgroundAudioDuration
|
||||||
@ -269,6 +275,19 @@ class AudioPlayerNotifier extends ChangeNotifier {
|
|||||||
_lastPostion = _backgroundAudioPosition;
|
_lastPostion = _backgroundAudioPosition;
|
||||||
storage.saveInt(_lastPostion);
|
storage.saveInt(_lastPostion);
|
||||||
}
|
}
|
||||||
|
if ((_queue.playlist.length == 1 || !_autoPlay) &&
|
||||||
|
_seekSliderValue == 1 &&
|
||||||
|
_episode != null) {
|
||||||
|
_queue.delFromPlaylist(_episode);
|
||||||
|
_lastPostion = 0;
|
||||||
|
storage.saveInt(_lastPostion);
|
||||||
|
PlayHistory history = PlayHistory(
|
||||||
|
_episode.title,
|
||||||
|
_episode.enclosureUrl,
|
||||||
|
backgroundAudioPosition / 1000,
|
||||||
|
seekSliderValue);
|
||||||
|
dbHelper.saveHistory(history);
|
||||||
|
}
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
if (_audioState == BasicPlaybackState.stopped) {
|
if (_audioState == BasicPlaybackState.stopped) {
|
||||||
@ -327,6 +346,17 @@ class AudioPlayerNotifier extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
moveToTop(EpisodeBrief episode) async {
|
||||||
|
await delFromPlaylist(episode);
|
||||||
|
if (_playerRunning) {
|
||||||
|
await addToPlaylistAt(episode, 1);
|
||||||
|
} else {
|
||||||
|
await addToPlaylistAt(episode, 0);
|
||||||
|
_lastPostion = 0;
|
||||||
|
storage.saveInt(_lastPostion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pauseAduio() async {
|
pauseAduio() async {
|
||||||
AudioService.pause();
|
AudioService.pause();
|
||||||
}
|
}
|
||||||
@ -353,10 +383,22 @@ class AudioPlayerNotifier extends ChangeNotifier {
|
|||||||
//Set sleep time
|
//Set sleep time
|
||||||
sleepTimer(int mins) {
|
sleepTimer(int mins) {
|
||||||
_showStopWatch = true;
|
_showStopWatch = true;
|
||||||
|
_switchValue = 1;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
_timeLeft = mins * 60;
|
||||||
|
Timer.periodic(Duration(seconds: 1), (timer) {
|
||||||
|
if (_timeLeft == 0) {
|
||||||
|
timer.cancel();
|
||||||
|
notifyListeners();
|
||||||
|
} else {
|
||||||
|
_timeLeft = _timeLeft - 1;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
});
|
||||||
_stopTimer = Timer(Duration(minutes: mins), () {
|
_stopTimer = Timer(Duration(minutes: mins), () {
|
||||||
_stopOnComplete = false;
|
_stopOnComplete = false;
|
||||||
_showStopWatch = false;
|
_showStopWatch = false;
|
||||||
|
_switchValue = 0;
|
||||||
AudioService.stop();
|
AudioService.stop();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
});
|
});
|
||||||
@ -365,7 +407,9 @@ class AudioPlayerNotifier extends ChangeNotifier {
|
|||||||
//Cancel sleep timer
|
//Cancel sleep timer
|
||||||
cancelTimer() {
|
cancelTimer() {
|
||||||
_stopTimer.cancel();
|
_stopTimer.cancel();
|
||||||
|
_timeLeft = 0;
|
||||||
_showStopWatch = false;
|
_showStopWatch = false;
|
||||||
|
_switchValue = 0;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -393,7 +437,7 @@ class AudioPlayerTask extends BackgroundAudioTask {
|
|||||||
case AudioPlaybackState.none:
|
case AudioPlaybackState.none:
|
||||||
return BasicPlaybackState.none;
|
return BasicPlaybackState.none;
|
||||||
case AudioPlaybackState.stopped:
|
case AudioPlaybackState.stopped:
|
||||||
return BasicPlaybackState.stopped;
|
return _skipState ?? BasicPlaybackState.stopped;
|
||||||
case AudioPlaybackState.paused:
|
case AudioPlaybackState.paused:
|
||||||
return BasicPlaybackState.paused;
|
return BasicPlaybackState.paused;
|
||||||
case AudioPlaybackState.playing:
|
case AudioPlaybackState.playing:
|
||||||
@ -438,6 +482,10 @@ class AudioPlayerTask extends BackgroundAudioTask {
|
|||||||
if (hasNext) {
|
if (hasNext) {
|
||||||
onSkipToNext();
|
onSkipToNext();
|
||||||
} else {
|
} else {
|
||||||
|
_skipState = BasicPlaybackState.skippingToNext;
|
||||||
|
_audioPlayer.stop();
|
||||||
|
_queue.removeAt(0);
|
||||||
|
_skipState = null;
|
||||||
onStop();
|
onStop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -459,20 +507,24 @@ class AudioPlayerTask extends BackgroundAudioTask {
|
|||||||
await _audioPlayer.stop();
|
await _audioPlayer.stop();
|
||||||
_queue.removeAt(0);
|
_queue.removeAt(0);
|
||||||
}
|
}
|
||||||
AudioServiceBackground.setQueue(_queue);
|
if (_queue.length == 0) {
|
||||||
AudioServiceBackground.setMediaItem(mediaItem);
|
onStop();
|
||||||
_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 {
|
} else {
|
||||||
_setState(state: BasicPlaybackState.paused);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,8 +32,12 @@ class EpisodeBrief {
|
|||||||
String dateToString() {
|
String dateToString() {
|
||||||
DateTime date = DateTime.fromMillisecondsSinceEpoch(pubDate, isUtc: true);
|
DateTime date = DateTime.fromMillisecondsSinceEpoch(pubDate, isUtc: true);
|
||||||
var diffrence = DateTime.now().difference(date);
|
var diffrence = DateTime.now().difference(date);
|
||||||
if (diffrence.inHours < 24) {
|
if (diffrence.inHours < 1) {
|
||||||
|
return '1 hour ago';
|
||||||
|
} else if (diffrence.inHours < 24) {
|
||||||
return '${diffrence.inHours} hours ago';
|
return '${diffrence.inHours} hours ago';
|
||||||
|
} else if (diffrence.inHours == 24) {
|
||||||
|
return '1 day ago';
|
||||||
} else if (diffrence.inDays < 7) {
|
} else if (diffrence.inDays < 7) {
|
||||||
return '${diffrence.inDays} days ago';
|
return '${diffrence.inDays} days ago';
|
||||||
} else {
|
} else {
|
||||||
|
@ -3,12 +3,15 @@ class PodcastLocal {
|
|||||||
final String imageUrl;
|
final String imageUrl;
|
||||||
final String rssUrl;
|
final String rssUrl;
|
||||||
final String author;
|
final String author;
|
||||||
String description;
|
|
||||||
final String primaryColor;
|
final String primaryColor;
|
||||||
final String id;
|
final String id;
|
||||||
final String imagePath;
|
final String imagePath;
|
||||||
final String provider;
|
final String provider;
|
||||||
final String link;
|
final String link;
|
||||||
|
|
||||||
|
final String description;
|
||||||
|
final int upateCount;
|
||||||
PodcastLocal(
|
PodcastLocal(
|
||||||
this.title,
|
this.title,
|
||||||
this.imageUrl,
|
this.imageUrl,
|
||||||
@ -18,5 +21,10 @@ class PodcastLocal {
|
|||||||
this.id,
|
this.id,
|
||||||
this.imagePath,
|
this.imagePath,
|
||||||
this.provider,
|
this.provider,
|
||||||
this.link);
|
this.link,
|
||||||
|
{
|
||||||
|
this.description ='',
|
||||||
|
this.upateCount = 0,
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,38 @@
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:tsacdop/class/podcastlocal.dart';
|
||||||
|
import 'package:workmanager/workmanager.dart';
|
||||||
|
|
||||||
|
import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
|
||||||
import 'package:tsacdop/local_storage/key_value_storage.dart';
|
import 'package:tsacdop/local_storage/key_value_storage.dart';
|
||||||
|
|
||||||
|
void callbackDispatcher() {
|
||||||
|
Workmanager.executeTask((task, inputData) async {
|
||||||
|
var dbHelper = DBHelper();
|
||||||
|
print('Start task');
|
||||||
|
List<PodcastLocal> podcastList = await dbHelper.getPodcastLocalAll();
|
||||||
|
int i = 0;
|
||||||
|
await Future.forEach(podcastList, (podcastLocal) async {
|
||||||
|
i += await dbHelper.updatePodcastRss(podcastLocal);
|
||||||
|
print('Refresh ' + podcastLocal.title);
|
||||||
|
});
|
||||||
|
KeyValueStorage refreshstorage = KeyValueStorage('refreshdate');
|
||||||
|
await refreshstorage.saveInt(DateTime.now().millisecondsSinceEpoch);
|
||||||
|
KeyValueStorage refreshcountstorage = KeyValueStorage('refreshcount');
|
||||||
|
await refreshcountstorage.saveInt(i);
|
||||||
|
return Future.value(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
class SettingState extends ChangeNotifier {
|
class SettingState extends ChangeNotifier {
|
||||||
KeyValueStorage themestorage = KeyValueStorage('themes');
|
KeyValueStorage themestorage = KeyValueStorage('themes');
|
||||||
KeyValueStorage accentstorage = KeyValueStorage('accents');
|
KeyValueStorage accentstorage = KeyValueStorage('accents');
|
||||||
KeyValueStorage autoupdatestorage = KeyValueStorage('autoupdate');
|
KeyValueStorage autoupdatestorage = KeyValueStorage('autoupdate');
|
||||||
|
KeyValueStorage intervalstorage = KeyValueStorage('updateInterval');
|
||||||
|
|
||||||
Future initData() async {
|
Future initData() async {
|
||||||
await _getTheme();
|
await _getTheme();
|
||||||
await _getAccentSetColor();
|
await _getAccentSetColor();
|
||||||
await _getAutoUpdate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ThemeMode _theme;
|
ThemeMode _theme;
|
||||||
@ -23,6 +44,28 @@ class SettingState extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setWorkManager(int hour) {
|
||||||
|
_updateInterval = hour;
|
||||||
|
notifyListeners();
|
||||||
|
_saveUpdateInterval();
|
||||||
|
Workmanager.initialize(
|
||||||
|
callbackDispatcher,
|
||||||
|
isInDebugMode: true,
|
||||||
|
);
|
||||||
|
Workmanager.registerPeriodicTask("1", "update_podcasts",
|
||||||
|
frequency: Duration(hours: hour),
|
||||||
|
initialDelay: Duration(seconds: 10),
|
||||||
|
constraints: Constraints(
|
||||||
|
networkType: NetworkType.connected,
|
||||||
|
));
|
||||||
|
print('work manager init done + ');
|
||||||
|
}
|
||||||
|
|
||||||
|
void cancelWork() {
|
||||||
|
Workmanager.cancelAll();
|
||||||
|
print('work job cancelled');
|
||||||
|
}
|
||||||
|
|
||||||
Color _accentSetColor;
|
Color _accentSetColor;
|
||||||
Color get accentSetColor => _accentSetColor;
|
Color get accentSetColor => _accentSetColor;
|
||||||
|
|
||||||
@ -32,6 +75,10 @@ class SettingState extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int _updateInterval;
|
||||||
|
int get updateInterval => _updateInterval;
|
||||||
|
|
||||||
|
int _initUpdateTag;
|
||||||
bool _autoUpdate;
|
bool _autoUpdate;
|
||||||
bool get autoUpdate => _autoUpdate;
|
bool get autoUpdate => _autoUpdate;
|
||||||
set autoUpdate(bool boo) {
|
set autoUpdate(bool boo) {
|
||||||
@ -46,18 +93,21 @@ class SettingState extends ChangeNotifier {
|
|||||||
_getTheme();
|
_getTheme();
|
||||||
_getAccentSetColor();
|
_getAccentSetColor();
|
||||||
_getAutoUpdate();
|
_getAutoUpdate();
|
||||||
|
_getUpdateInterval().then((value) {
|
||||||
|
if (_initUpdateTag == 0) setWorkManager(24);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_getTheme() async {
|
Future _getTheme() async {
|
||||||
int mode = await themestorage.getInt();
|
int mode = await themestorage.getInt();
|
||||||
_theme = ThemeMode.values[mode];
|
_theme = ThemeMode.values[mode];
|
||||||
}
|
}
|
||||||
|
|
||||||
_saveTheme() async {
|
Future _saveTheme() async {
|
||||||
await themestorage.saveInt(_theme.index);
|
await themestorage.saveInt(_theme.index);
|
||||||
}
|
}
|
||||||
|
|
||||||
_getAccentSetColor() async {
|
Future _getAccentSetColor() async {
|
||||||
String colorString = await accentstorage.getString();
|
String colorString = await accentstorage.getString();
|
||||||
print(colorString);
|
print(colorString);
|
||||||
if (colorString.isNotEmpty) {
|
if (colorString.isNotEmpty) {
|
||||||
@ -69,17 +119,26 @@ class SettingState extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_saveAccentSetColor() async {
|
Future _saveAccentSetColor() async {
|
||||||
await accentstorage
|
await accentstorage
|
||||||
.saveString(_accentSetColor.toString().substring(10, 16));
|
.saveString(_accentSetColor.toString().substring(10, 16));
|
||||||
}
|
}
|
||||||
|
|
||||||
_getAutoUpdate() async {
|
Future _getAutoUpdate() async {
|
||||||
int i = await autoupdatestorage.getInt();
|
int i = await autoupdatestorage.getInt();
|
||||||
_autoUpdate = i == 0 ? false : true;
|
_autoUpdate = i == 0 ? true : false;
|
||||||
}
|
}
|
||||||
|
|
||||||
_saveAutoUpdate() async {
|
Future _saveAutoUpdate() async {
|
||||||
await autoupdatestorage.saveInt(_autoUpdate ? 1 : 0);
|
await autoupdatestorage.saveInt(_autoUpdate ? 0 : 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future _getUpdateInterval() async {
|
||||||
|
_initUpdateTag = await intervalstorage.getInt();
|
||||||
|
_updateInterval = _initUpdateTag == 0 ? 24 : _initUpdateTag;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future _saveUpdateInterval() async {
|
||||||
|
await intervalstorage.saveInt(_updateInterval);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,7 +33,8 @@ class _EpisodeDetailState extends State<EpisodeDetail> {
|
|||||||
String path;
|
String path;
|
||||||
Future getSDescription(String url) async {
|
Future getSDescription(String url) async {
|
||||||
var dbHelper = DBHelper();
|
var dbHelper = DBHelper();
|
||||||
widget.episodeItem.description = await dbHelper.getDescription(url);
|
widget.episodeItem.description = (await dbHelper.getDescription(url))
|
||||||
|
.replaceAll(RegExp(r'\s?<p>(<br>)?</p>\s?'), '');
|
||||||
if (mounted)
|
if (mounted)
|
||||||
setState(() {
|
setState(() {
|
||||||
_loaddes = true;
|
_loaddes = true;
|
||||||
@ -82,159 +83,179 @@ class _EpisodeDetailState extends State<EpisodeDetail> {
|
|||||||
value: SystemUiOverlayStyle(
|
value: SystemUiOverlayStyle(
|
||||||
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
|
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
|
||||||
systemNavigationBarColor: Theme.of(context).primaryColor,
|
systemNavigationBarColor: Theme.of(context).primaryColor,
|
||||||
statusBarColor: Theme.of(context).primaryColor,
|
systemNavigationBarIconBrightness:
|
||||||
|
Theme.of(context).accentColorBrightness,
|
||||||
|
// statusBarColor: Theme.of(context).primaryColor,
|
||||||
),
|
),
|
||||||
child: SafeArea(
|
child: Scaffold(
|
||||||
child: Scaffold(
|
backgroundColor: Theme.of(context).primaryColor,
|
||||||
backgroundColor: Theme.of(context).primaryColor,
|
appBar: AppBar(
|
||||||
appBar: AppBar(
|
title: Text(widget.episodeItem.feedTitle),
|
||||||
title: Text(widget.episodeItem.feedTitle),
|
centerTitle: true,
|
||||||
centerTitle: true,
|
),
|
||||||
),
|
body: Stack(
|
||||||
body: Stack(
|
children: <Widget>[
|
||||||
children: <Widget>[
|
Container(
|
||||||
Container(
|
color: Theme.of(context).primaryColor,
|
||||||
color: Theme.of(context).primaryColor,
|
child: Column(
|
||||||
padding: EdgeInsets.all(10.0),
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
child: Column(
|
mainAxisSize: MainAxisSize.min,
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
children: <Widget>[
|
||||||
mainAxisSize: MainAxisSize.min,
|
Container(
|
||||||
children: <Widget>[
|
child: Column(
|
||||||
Container(
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
child: Column(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
children: <Widget>[
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
Container(
|
||||||
children: <Widget>[
|
padding: EdgeInsets.symmetric(horizontal: 20.0),
|
||||||
Container(
|
alignment: Alignment.topLeft,
|
||||||
padding: EdgeInsets.symmetric(horizontal: 12.0),
|
child: Text(
|
||||||
alignment: Alignment.topLeft,
|
widget.episodeItem.title,
|
||||||
child: Text(
|
style: Theme.of(context).textTheme.headline5,
|
||||||
widget.episodeItem.title,
|
|
||||||
style: Theme.of(context).textTheme.headline5,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Container(
|
),
|
||||||
alignment: Alignment.centerLeft,
|
Container(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 12.0),
|
alignment: Alignment.centerLeft,
|
||||||
height: 30.0,
|
padding: EdgeInsets.symmetric(horizontal: 20.0),
|
||||||
child: Text(
|
height: 30.0,
|
||||||
'Published ' +
|
child: Text(
|
||||||
DateFormat.yMMMd().format(
|
'Published ' +
|
||||||
DateTime.fromMillisecondsSinceEpoch(
|
DateFormat.yMMMd().format(
|
||||||
widget.episodeItem.pubDate)),
|
DateTime.fromMillisecondsSinceEpoch(
|
||||||
style: TextStyle(color: Colors.blue[500])),
|
widget.episodeItem.pubDate)),
|
||||||
),
|
style: TextStyle(
|
||||||
Container(
|
color: Theme.of(context).accentColor)),
|
||||||
padding: EdgeInsets.all(12.0),
|
),
|
||||||
height: 50.0,
|
Container(
|
||||||
child: Row(
|
padding: EdgeInsets.symmetric(horizontal: 20.0),
|
||||||
children: <Widget>[
|
height: 50.0,
|
||||||
(widget.episodeItem.explicit == 1)
|
child: Row(
|
||||||
? Container(
|
children: <Widget>[
|
||||||
decoration: BoxDecoration(
|
(widget.episodeItem.explicit == 1)
|
||||||
color: Colors.red[800],
|
? Container(
|
||||||
shape: BoxShape.circle),
|
decoration: BoxDecoration(
|
||||||
height: 25.0,
|
color: Colors.red[800],
|
||||||
width: 25.0,
|
shape: BoxShape.circle),
|
||||||
margin: EdgeInsets.only(right: 10.0),
|
height: 25.0,
|
||||||
alignment: Alignment.center,
|
width: 25.0,
|
||||||
child: Text('E',
|
margin: EdgeInsets.only(right: 10.0),
|
||||||
style:
|
alignment: Alignment.center,
|
||||||
TextStyle(color: Colors.white)))
|
child: Text('E',
|
||||||
: Center(),
|
style:
|
||||||
widget.episodeItem.duration != 0
|
TextStyle(color: Colors.white)))
|
||||||
? Container(
|
: Center(),
|
||||||
decoration: BoxDecoration(
|
widget.episodeItem.duration != 0
|
||||||
color: Colors.cyan[300],
|
? Container(
|
||||||
borderRadius: BorderRadius.all(
|
decoration: BoxDecoration(
|
||||||
Radius.circular(15.0))),
|
color: Colors.cyan[300],
|
||||||
height: 30.0,
|
borderRadius: BorderRadius.all(
|
||||||
margin: EdgeInsets.only(right: 10.0),
|
Radius.circular(15.0))),
|
||||||
padding: EdgeInsets.symmetric(
|
height: 25.0,
|
||||||
horizontal: 10.0),
|
margin: EdgeInsets.only(right: 10.0),
|
||||||
alignment: Alignment.center,
|
padding: EdgeInsets.symmetric(
|
||||||
child: Text(
|
horizontal: 10.0),
|
||||||
(widget.episodeItem.duration)
|
alignment: Alignment.center,
|
||||||
.toString() +
|
child: Text(
|
||||||
'mins',
|
(widget.episodeItem.duration)
|
||||||
style: textstyle),
|
.toString() +
|
||||||
)
|
'mins',
|
||||||
: Center(),
|
style: textstyle),
|
||||||
widget.episodeItem.enclosureLength != null
|
|
||||||
? Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.lightBlue[300],
|
|
||||||
borderRadius: BorderRadius.all(
|
|
||||||
Radius.circular(15.0))),
|
|
||||||
height: 30.0,
|
|
||||||
margin: EdgeInsets.only(right: 10.0),
|
|
||||||
padding: EdgeInsets.symmetric(
|
|
||||||
horizontal: 10.0),
|
|
||||||
alignment: Alignment.center,
|
|
||||||
child: Text(
|
|
||||||
((widget.episodeItem
|
|
||||||
.enclosureLength) ~/
|
|
||||||
1000000)
|
|
||||||
.toString() +
|
|
||||||
'MB',
|
|
||||||
style: textstyle),
|
|
||||||
)
|
|
||||||
: Center(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: Container(
|
|
||||||
padding:
|
|
||||||
EdgeInsets.only(left: 12.0, right: 12.0, top: 5.0),
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
controller: _controller,
|
|
||||||
child: _loaddes
|
|
||||||
? (widget.episodeItem.description.contains('<'))
|
|
||||||
? Html(
|
|
||||||
data: widget.episodeItem.description,
|
|
||||||
onLinkTap: (url) {
|
|
||||||
_launchUrl(url);
|
|
||||||
},
|
|
||||||
useRichText: true,
|
|
||||||
)
|
)
|
||||||
: Container(
|
: Center(),
|
||||||
alignment: Alignment.topLeft,
|
widget.episodeItem.enclosureLength != null
|
||||||
child:
|
? Container(
|
||||||
Text(widget.episodeItem.description))
|
decoration: BoxDecoration(
|
||||||
: Center(),
|
color: Colors.lightBlue[300],
|
||||||
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(15.0))),
|
||||||
|
height: 25.0,
|
||||||
|
margin: EdgeInsets.only(right: 10.0),
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
horizontal: 10.0),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Text(
|
||||||
|
((widget.episodeItem
|
||||||
|
.enclosureLength) ~/
|
||||||
|
1000000)
|
||||||
|
.toString() +
|
||||||
|
'MB',
|
||||||
|
style: textstyle),
|
||||||
|
)
|
||||||
|
: Center(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Container(
|
||||||
|
padding: EdgeInsets.only(top: 5.0),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
scrollDirection: Axis.vertical,
|
||||||
|
//physics: const AlwaysScrollableScrollPhysics(),
|
||||||
|
controller: _controller,
|
||||||
|
child: _loaddes
|
||||||
|
? (widget.episodeItem.description.contains('<'))
|
||||||
|
? Html(
|
||||||
|
padding:
|
||||||
|
EdgeInsets.symmetric(horizontal: 20.0),
|
||||||
|
defaultTextStyle: TextStyle(height: 1.8),
|
||||||
|
data: widget.episodeItem.description,
|
||||||
|
linkStyle: TextStyle(
|
||||||
|
color: Theme.of(context).accentColor,
|
||||||
|
decoration: TextDecoration.underline,
|
||||||
|
textBaseline: TextBaseline.ideographic),
|
||||||
|
onLinkTap: (url) {
|
||||||
|
_launchUrl(url);
|
||||||
|
},
|
||||||
|
useRichText: true,
|
||||||
|
)
|
||||||
|
: Container(
|
||||||
|
padding:
|
||||||
|
EdgeInsets.symmetric(horizontal: 20.0),
|
||||||
|
alignment: Alignment.topLeft,
|
||||||
|
child: Text(
|
||||||
|
widget.episodeItem.description,
|
||||||
|
style: TextStyle(
|
||||||
|
height: 1.8,
|
||||||
|
),
|
||||||
|
))
|
||||||
|
: Center(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Selector<AudioPlayerNotifier, bool>(
|
||||||
|
selector: (_, audio) => audio.playerRunning,
|
||||||
|
builder: (_, data, __) {
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.only(bottom: data ? 60.0 : 0),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Selector<AudioPlayerNotifier, bool>(
|
||||||
|
selector: (_, audio) => audio.playerRunning,
|
||||||
|
builder: (_, data, __) {
|
||||||
|
return Container(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
padding: EdgeInsets.only(bottom: data ? 60.0 : 0),
|
||||||
|
child: AnimatedContainer(
|
||||||
|
duration: Duration(milliseconds: 400),
|
||||||
|
height: !_showMenu ? 50 : 0,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
scrollDirection: Axis.vertical,
|
||||||
|
child: MenuBar(
|
||||||
|
episodeItem: widget.episodeItem,
|
||||||
|
heroTag: widget.heroTag,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
);
|
||||||
),
|
}),
|
||||||
),
|
Container(child: PlayerWidget()),
|
||||||
Selector<AudioPlayerNotifier, bool>(
|
],
|
||||||
selector: (_, audio) => audio.playerRunning,
|
|
||||||
builder: (_, data, __) {
|
|
||||||
return Container(
|
|
||||||
alignment: Alignment.bottomCenter,
|
|
||||||
padding:
|
|
||||||
EdgeInsets.only(bottom: data == true ? 60.0 : 10.0),
|
|
||||||
child: AnimatedContainer(
|
|
||||||
duration: Duration(milliseconds: 400),
|
|
||||||
height: !_showMenu ? 50 : 0,
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
scrollDirection: Axis.vertical,
|
|
||||||
child: MenuBar(
|
|
||||||
episodeItem: widget.episodeItem,
|
|
||||||
heroTag: widget.heroTag,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
Container(child: PlayerWidget()),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -368,9 +389,9 @@ class _MenuBarState extends State<MenuBar> {
|
|||||||
),
|
),
|
||||||
Spacer(),
|
Spacer(),
|
||||||
// Text(audio.audioState.toString()),
|
// Text(audio.audioState.toString()),
|
||||||
Selector<AudioPlayerNotifier, Tuple2<EpisodeBrief, BasicPlaybackState>>(
|
Selector<AudioPlayerNotifier,
|
||||||
selector: (_, audio) =>
|
Tuple2<EpisodeBrief, BasicPlaybackState>>(
|
||||||
Tuple2(audio.episode, audio.audioState),
|
selector: (_, audio) => Tuple2(audio.episode, audio.audioState),
|
||||||
builder: (_, data, __) {
|
builder: (_, data, __) {
|
||||||
return (widget.episodeItem.title != data.item1?.title)
|
return (widget.episodeItem.title != data.item1?.title)
|
||||||
? Material(
|
? Material(
|
||||||
@ -485,7 +506,8 @@ class _LineLoaderState extends State<LineLoader>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return CustomPaint(painter: LinePainter(_fraction, Theme.of(context).accentColor));
|
return CustomPaint(
|
||||||
|
painter: LinePainter(_fraction, Theme.of(context).accentColor));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -578,7 +600,8 @@ class _WaveLoaderState extends State<WaveLoader>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return CustomPaint(painter: WavePainter(_fraction, Theme.of(context).accentColor));
|
return CustomPaint(
|
||||||
|
painter: WavePainter(_fraction, Theme.of(context).accentColor));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
import 'package:line_icons/line_icons.dart';
|
import 'package:line_icons/line_icons.dart';
|
||||||
|
|
||||||
class AboutApp extends StatelessWidget {
|
class AboutApp extends StatelessWidget {
|
||||||
_launchUrl(String url) async {
|
_launchUrl(String url) async {
|
||||||
if (await canLaunch(url)) {
|
if (await canLaunch(url)) {
|
||||||
@ -44,15 +45,16 @@ class AboutApp extends StatelessWidget {
|
|||||||
value: SystemUiOverlayStyle(
|
value: SystemUiOverlayStyle(
|
||||||
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
|
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
|
||||||
systemNavigationBarColor: Theme.of(context).primaryColor,
|
systemNavigationBarColor: Theme.of(context).primaryColor,
|
||||||
statusBarColor: Theme.of(context).primaryColor,
|
systemNavigationBarIconBrightness:
|
||||||
|
Theme.of(context).accentColorBrightness,
|
||||||
),
|
),
|
||||||
child: SafeArea(
|
child: Scaffold(
|
||||||
child: Scaffold(
|
backgroundColor: Theme.of(context).primaryColor,
|
||||||
backgroundColor: Theme.of(context).primaryColor,
|
appBar: AppBar(
|
||||||
appBar: AppBar(
|
title: Text('About'),
|
||||||
title: Text('About'),
|
),
|
||||||
),
|
body: SafeArea(
|
||||||
body: Container(
|
child: Container(
|
||||||
padding: EdgeInsets.all(20),
|
padding: EdgeInsets.all(20),
|
||||||
alignment: Alignment.topLeft,
|
alignment: Alignment.topLeft,
|
||||||
child: Column(
|
child: Column(
|
||||||
@ -70,7 +72,7 @@ class AboutApp extends StatelessWidget {
|
|||||||
image: AssetImage('assets/logo.png'),
|
image: AssetImage('assets/logo.png'),
|
||||||
height: 80,
|
height: 80,
|
||||||
),
|
),
|
||||||
Text('Version: 0.1.2'),
|
Text('Version: 0.1.4'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -78,7 +80,7 @@ class AboutApp extends StatelessWidget {
|
|||||||
padding: EdgeInsets.symmetric(horizontal: 50),
|
padding: EdgeInsets.symmetric(horizontal: 50),
|
||||||
height: 50,
|
height: 50,
|
||||||
child: Text(
|
child: Text(
|
||||||
'Tsacdop is a podcasts client developed influtter, a simple, beautiful, and easy-use player.',
|
'Tsacdop is a podcasts client developed in flutter, a simple, beautiful, and easy-use application.',
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -86,13 +88,11 @@ class AboutApp extends StatelessWidget {
|
|||||||
padding: EdgeInsets.all(5.0),
|
padding: EdgeInsets.all(5.0),
|
||||||
),
|
),
|
||||||
Container(
|
Container(
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(top: 20.0, bottom: 10.0),
|
||||||
top: 20.0,
|
|
||||||
bottom: 10.0
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.all(Radius.circular(10)),
|
borderRadius: BorderRadius.all(Radius.circular(10)),
|
||||||
border: Border.all(color: Theme.of(context).accentColor, width: 1),
|
border: Border.all(
|
||||||
|
color: Theme.of(context).accentColor, width: 1),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
@ -107,20 +107,14 @@ class AboutApp extends StatelessWidget {
|
|||||||
TextStyle(color: Theme.of(context).accentColor),
|
TextStyle(color: Theme.of(context).accentColor),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
_listItem(
|
_listItem(context, 'GitHub', LineIcons.github,
|
||||||
context,
|
|
||||||
'GitHub',
|
|
||||||
LineIcons.github,
|
|
||||||
'https://github.com/stonaga/'),
|
'https://github.com/stonaga/'),
|
||||||
_listItem(
|
_listItem(context, 'Twitter', LineIcons.twitter,
|
||||||
context,
|
|
||||||
'Twitter',
|
|
||||||
LineIcons.twitter,
|
|
||||||
'https://twitter.com'),
|
'https://twitter.com'),
|
||||||
_listItem(
|
_listItem(
|
||||||
context,
|
context,
|
||||||
'Stone Gate',
|
'Stone Gate',
|
||||||
LineIcons.hat_cowboy_solid,
|
LineIcons.hat_cowboy_solid,
|
||||||
'mailto:<xijieyin@gmail.com>?subject=Tsacdop Feedback'),
|
'mailto:<xijieyin@gmail.com>?subject=Tsacdop Feedback'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -155,8 +149,8 @@ class AboutApp extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)),
|
),
|
||||||
),
|
)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,7 @@ import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
|
|||||||
import 'package:tsacdop/home/home.dart';
|
import 'package:tsacdop/home/home.dart';
|
||||||
import 'package:tsacdop/home/appbar/popupmenu.dart';
|
import 'package:tsacdop/home/appbar/popupmenu.dart';
|
||||||
import 'package:tsacdop/webfeed/webfeed.dart';
|
import 'package:tsacdop/webfeed/webfeed.dart';
|
||||||
|
import 'package:tsacdop/.env.dart';
|
||||||
|
|
||||||
class MyHomePage extends StatefulWidget {
|
class MyHomePage extends StatefulWidget {
|
||||||
@override
|
@override
|
||||||
@ -30,42 +31,47 @@ class MyHomePage extends StatefulWidget {
|
|||||||
class _MyHomePageState extends State<MyHomePage> {
|
class _MyHomePageState extends State<MyHomePage> {
|
||||||
final _MyHomePageDelegate _delegate = _MyHomePageDelegate();
|
final _MyHomePageDelegate _delegate = _MyHomePageDelegate();
|
||||||
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
|
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AnnotatedRegion<SystemUiOverlayStyle>(
|
return AnnotatedRegion<SystemUiOverlayStyle>(
|
||||||
value: SystemUiOverlayStyle(
|
value: SystemUiOverlayStyle(
|
||||||
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
|
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
|
||||||
systemNavigationBarIconBrightness: Theme.of(context).accentColorBrightness,
|
systemNavigationBarIconBrightness:
|
||||||
|
Theme.of(context).accentColorBrightness,
|
||||||
systemNavigationBarColor: Theme.of(context).primaryColor,
|
systemNavigationBarColor: Theme.of(context).primaryColor,
|
||||||
statusBarColor: Theme.of(context).primaryColor,
|
// statusBarColor: Theme.of(context).primaryColor,
|
||||||
),
|
),
|
||||||
child: SafeArea(
|
child: Scaffold(
|
||||||
child: Scaffold(
|
key: _scaffoldKey,
|
||||||
key: _scaffoldKey,
|
appBar: AppBar(
|
||||||
appBar: AppBar(
|
centerTitle: true,
|
||||||
centerTitle: true,
|
leading: IconButton(
|
||||||
leading: IconButton(
|
tooltip: 'Add',
|
||||||
tooltip: 'Add',
|
icon: const Icon(Icons.add_circle_outline),
|
||||||
icon: const Icon(Icons.add_circle_outline),
|
onPressed: () async {
|
||||||
onPressed: () async {
|
await showSearch<int>(
|
||||||
await showSearch<int>(
|
context: context,
|
||||||
context: context,
|
delegate: _delegate,
|
||||||
delegate: _delegate,
|
);
|
||||||
);
|
},
|
||||||
},
|
|
||||||
),
|
|
||||||
title: Image(
|
|
||||||
image: Theme.of(context).brightness == Brightness.light
|
|
||||||
? AssetImage('assets/text.png') : AssetImage('assets/text_light.png'),
|
|
||||||
height: 30,
|
|
||||||
),
|
|
||||||
actions: <Widget>[
|
|
||||||
PopupMenu(),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
body: Home(),
|
title: Image(
|
||||||
|
image: Theme.of(context).brightness == Brightness.light
|
||||||
|
? AssetImage('assets/text.png')
|
||||||
|
: AssetImage('assets/text_light.png'),
|
||||||
|
height: 30,
|
||||||
|
),
|
||||||
|
actions: <Widget>[
|
||||||
|
PopupMenu(),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
|
body: Home(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -73,13 +79,15 @@ class _MyHomePageState extends State<MyHomePage> {
|
|||||||
|
|
||||||
class _MyHomePageDelegate extends SearchDelegate<int> {
|
class _MyHomePageDelegate extends SearchDelegate<int> {
|
||||||
static Future<List> getList(String searchText) async {
|
static Future<List> getList(String searchText) async {
|
||||||
|
String apiKey = environment['apiKey'];
|
||||||
|
print(apiKey);
|
||||||
String url =
|
String url =
|
||||||
"https://listennotes.p.mashape.com/api/v1/search?only_in=title%2Cdescription&q=" +
|
"https://listennotes.p.mashape.com/api/v1/search?only_in=title%2Cdescription&q=" +
|
||||||
searchText +
|
searchText +
|
||||||
"&sort_by_date=0&type=podcast";
|
"&sort_by_date=0&type=podcast";
|
||||||
Response response = await Dio().get(url,
|
Response response = await Dio().get(url,
|
||||||
options: Options(headers: {
|
options: Options(headers: {
|
||||||
'X-Mashape-Key': "UtSwKG4afSmshZfglwsXylLKJZHgp1aZHi2jsnSYK5mZi0A32T",
|
'X-Mashape-Key': "$apiKey",
|
||||||
'Accept': "application/json"
|
'Accept': "application/json"
|
||||||
}));
|
}));
|
||||||
Map searchResultMap = jsonDecode(response.toString());
|
Map searchResultMap = jsonDecode(response.toString());
|
||||||
@ -112,7 +120,8 @@ class _MyHomePageDelegate extends SearchDelegate<int> {
|
|||||||
padding: EdgeInsets.only(top: 400),
|
padding: EdgeInsets.only(top: 400),
|
||||||
child: Image(
|
child: Image(
|
||||||
image: Theme.of(context).brightness == Brightness.light
|
image: Theme.of(context).brightness == Brightness.light
|
||||||
? AssetImage('assets/listennotes.png') : AssetImage('assets/listennotes_light.png'),
|
? AssetImage('assets/listennotes.png')
|
||||||
|
: AssetImage('assets/listennotes_light.png'),
|
||||||
height: 20,
|
height: 20,
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
@ -236,7 +245,11 @@ class _SearchResultState extends State<SearchResult> {
|
|||||||
|
|
||||||
importOmpl.importState = ImportState.import;
|
importOmpl.importState = ImportState.import;
|
||||||
try {
|
try {
|
||||||
Response response = await Dio().get(rss);
|
BaseOptions options = new BaseOptions(
|
||||||
|
connectTimeout: 30000,
|
||||||
|
receiveTimeout: 30000,
|
||||||
|
);
|
||||||
|
Response response = await Dio(options).get(rss);
|
||||||
var dbHelper = DBHelper();
|
var dbHelper = DBHelper();
|
||||||
String _realUrl = response.realUri.toString();
|
String _realUrl = response.realUri.toString();
|
||||||
|
|
||||||
@ -276,8 +289,8 @@ class _SearchResultState extends State<SearchResult> {
|
|||||||
_uuid,
|
_uuid,
|
||||||
_imagePath,
|
_imagePath,
|
||||||
_provider,
|
_provider,
|
||||||
_link);
|
_link,
|
||||||
podcastLocal.description = _p.description;
|
description: _p.description);
|
||||||
await groupList.subscribe(podcastLocal);
|
await groupList.subscribe(podcastLocal);
|
||||||
|
|
||||||
if (_provider.contains('fireside')) {
|
if (_provider.contains('fireside')) {
|
||||||
@ -347,28 +360,48 @@ class _SearchResultState extends State<SearchResult> {
|
|||||||
? Icons.keyboard_arrow_up
|
? Icons.keyboard_arrow_up
|
||||||
: Icons.keyboard_arrow_down),
|
: Icons.keyboard_arrow_down),
|
||||||
Padding(padding: EdgeInsets.only(right: 10.0)),
|
Padding(padding: EdgeInsets.only(right: 10.0)),
|
||||||
!_issubscribe
|
Container(
|
||||||
? !_adding
|
width: 100,
|
||||||
? OutlineButton(
|
height: 35,
|
||||||
child: Text('Subscribe',
|
child: !_issubscribe
|
||||||
style: TextStyle(
|
? !_adding
|
||||||
color: Theme.of(context).accentColor)),
|
? OutlineButton(
|
||||||
onPressed: () {
|
highlightedBorderColor:
|
||||||
importOmpl.rssTitle = widget.onlinePodcast.title;
|
Theme.of(context).accentColor,
|
||||||
savePodcast(widget.onlinePodcast.rss);
|
splashColor: Theme.of(context)
|
||||||
})
|
.accentColor
|
||||||
: OutlineButton(
|
.withOpacity(0.8),
|
||||||
child: SizedBox(
|
child: Text('Subscribe',
|
||||||
height: 20,
|
style: TextStyle(
|
||||||
width: 20,
|
color: Theme.of(context).accentColor)),
|
||||||
child: CircularProgressIndicator(
|
onPressed: () {
|
||||||
strokeWidth: 2,
|
importOmpl.rssTitle =
|
||||||
valueColor:
|
widget.onlinePodcast.title;
|
||||||
AlwaysStoppedAnimation(Colors.blue),
|
savePodcast(widget.onlinePodcast.rss);
|
||||||
)),
|
})
|
||||||
onPressed: () {},
|
: OutlineButton(
|
||||||
)
|
highlightedBorderColor:
|
||||||
: OutlineButton(child: Text('Subscribe'), onPressed: () {}),
|
Theme.of(context).accentColor,
|
||||||
|
splashColor: Theme.of(context)
|
||||||
|
.accentColor
|
||||||
|
.withOpacity(0.8),
|
||||||
|
child: SizedBox(
|
||||||
|
height: 20,
|
||||||
|
width: 20,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
valueColor: AlwaysStoppedAnimation(
|
||||||
|
Theme.of(context).accentColor),
|
||||||
|
)),
|
||||||
|
onPressed: () {},
|
||||||
|
)
|
||||||
|
: OutlineButton(
|
||||||
|
highlightedBorderColor: Colors.grey[500],
|
||||||
|
disabledTextColor: Colors.grey[500],
|
||||||
|
child: Text('Subscribe'),
|
||||||
|
disabledBorderColor: Colors.grey[500],
|
||||||
|
onPressed: () {}),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:tsacdop/class/fireside_data.dart';
|
import 'package:tsacdop/class/fireside_data.dart';
|
||||||
|
import 'package:tsacdop/local_storage/key_value_storage.dart';
|
||||||
import 'package:xml/xml.dart' as xml;
|
import 'package:xml/xml.dart' as xml;
|
||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
@ -14,6 +15,7 @@ import 'package:image/image.dart' as img;
|
|||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:line_icons/line_icons.dart';
|
import 'package:line_icons/line_icons.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
import 'package:tsacdop/class/podcast_group.dart';
|
import 'package:tsacdop/class/podcast_group.dart';
|
||||||
import 'package:tsacdop/settings/settting.dart';
|
import 'package:tsacdop/settings/settting.dart';
|
||||||
@ -37,8 +39,13 @@ class OmplOutline {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class PopupMenu extends StatelessWidget {
|
class PopupMenu extends StatefulWidget {
|
||||||
Future<String> getColor(File file) async {
|
@override
|
||||||
|
_PopupMenuState createState() => _PopupMenuState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PopupMenuState extends State<PopupMenu> {
|
||||||
|
Future<String> _getColor(File file) async {
|
||||||
final imageProvider = FileImage(file);
|
final imageProvider = FileImage(file);
|
||||||
var colorImage = await getImageFromProvider(imageProvider);
|
var colorImage = await getImageFromProvider(imageProvider);
|
||||||
var color = await getColorFromImage(colorImage);
|
var color = await getColorFromImage(colorImage);
|
||||||
@ -46,35 +53,85 @@ class PopupMenu extends StatelessWidget {
|
|||||||
return primaryColor;
|
return primaryColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _showDate;
|
||||||
|
String _refreshDate;
|
||||||
|
_getRefreshDate() async {
|
||||||
|
int refreshDate;
|
||||||
|
KeyValueStorage refreshstorage = KeyValueStorage('refreshdate');
|
||||||
|
int i = await refreshstorage.getInt();
|
||||||
|
if (i == 0) {
|
||||||
|
KeyValueStorage refreshstorage = KeyValueStorage('refreshdate');
|
||||||
|
await refreshstorage.saveInt(DateTime.now().millisecondsSinceEpoch);
|
||||||
|
refreshDate = DateTime.now().millisecondsSinceEpoch;
|
||||||
|
} else {
|
||||||
|
refreshDate = i;
|
||||||
|
}
|
||||||
|
DateTime date = DateTime.fromMillisecondsSinceEpoch(refreshDate);
|
||||||
|
var diffrence = DateTime.now().difference(date);
|
||||||
|
if (diffrence.inMinutes < 10) {
|
||||||
|
_refreshDate = 'Just now';
|
||||||
|
} else if (diffrence.inHours < 1) {
|
||||||
|
_refreshDate = '1 hour ago';
|
||||||
|
} else if (diffrence.inHours < 24) {
|
||||||
|
_refreshDate = '${diffrence.inHours} hours ago';
|
||||||
|
} else if (diffrence.inHours == 24) {
|
||||||
|
_refreshDate = '1 day ago';
|
||||||
|
} else if (diffrence.inDays < 7) {
|
||||||
|
_refreshDate = '${diffrence.inDays} days ago';
|
||||||
|
} else {
|
||||||
|
_refreshDate = DateFormat.yMMMd()
|
||||||
|
.format(DateTime.fromMillisecondsSinceEpoch(refreshDate));
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_showDate = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_showDate = false;
|
||||||
|
_getRefreshDate();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
ImportOmpl importOmpl = Provider.of<ImportOmpl>(context, listen: false);
|
ImportOmpl importOmpl = Provider.of<ImportOmpl>(context, listen: false);
|
||||||
GroupList groupList = Provider.of<GroupList>(context, listen: false);
|
GroupList groupList = Provider.of<GroupList>(context, listen: false);
|
||||||
|
|
||||||
_refreshAll() async {
|
_refreshAll() async {
|
||||||
var dbHelper = DBHelper();
|
var dbHelper = DBHelper();
|
||||||
List<PodcastLocal> podcastList = await dbHelper.getPodcastLocalAll();
|
List<PodcastLocal> podcastList = await dbHelper.getPodcastLocalAll();
|
||||||
|
int i = 0;
|
||||||
await Future.forEach(podcastList, (podcastLocal) async {
|
await Future.forEach(podcastList, (podcastLocal) async {
|
||||||
importOmpl.rssTitle = podcastLocal.title;
|
importOmpl.rssTitle = podcastLocal.title;
|
||||||
importOmpl.importState = ImportState.parse;
|
importOmpl.importState = ImportState.parse;
|
||||||
await dbHelper.updatePodcastRss(podcastLocal);
|
i += await dbHelper.updatePodcastRss(podcastLocal);
|
||||||
print('Refresh ' + podcastLocal.title);
|
print('Refresh ' + podcastLocal.title);
|
||||||
});
|
});
|
||||||
|
KeyValueStorage refreshstorage = KeyValueStorage('refreshdate');
|
||||||
|
await refreshstorage.saveInt(DateTime.now().millisecondsSinceEpoch);
|
||||||
|
KeyValueStorage refreshcountstorage = KeyValueStorage('refreshcount');
|
||||||
|
await refreshcountstorage.saveInt(i);
|
||||||
importOmpl.importState = ImportState.complete;
|
importOmpl.importState = ImportState.complete;
|
||||||
}
|
}
|
||||||
|
|
||||||
saveOmpl(String rss) async {
|
saveOmpl(String rss) async {
|
||||||
var dbHelper = DBHelper();
|
var dbHelper = DBHelper();
|
||||||
importOmpl.importState = ImportState.import;
|
importOmpl.importState = ImportState.import;
|
||||||
|
BaseOptions options = new BaseOptions(
|
||||||
Response response = await Dio().get(rss);
|
connectTimeout: 20000,
|
||||||
|
receiveTimeout: 20000,
|
||||||
|
);
|
||||||
|
Response response = await Dio(options).get(rss);
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
var _p = RssFeed.parse(response.data);
|
var _p = RssFeed.parse(response.data);
|
||||||
|
|
||||||
var dir = await getApplicationDocumentsDirectory();
|
var dir = await getApplicationDocumentsDirectory();
|
||||||
|
|
||||||
String _realUrl = response.redirects.isEmpty ? rss : response.realUri.toString();
|
String _realUrl =
|
||||||
|
response.redirects.isEmpty ? rss : response.realUri.toString();
|
||||||
|
|
||||||
print(_realUrl);
|
print(_realUrl);
|
||||||
bool _checkUrl = await dbHelper.checkPodcast(_realUrl);
|
bool _checkUrl = await dbHelper.checkPodcast(_realUrl);
|
||||||
|
|
||||||
@ -87,9 +144,10 @@ class PopupMenu extends StatelessWidget {
|
|||||||
String _uuid = Uuid().v4();
|
String _uuid = Uuid().v4();
|
||||||
File("${dir.path}/$_uuid.png")
|
File("${dir.path}/$_uuid.png")
|
||||||
..writeAsBytesSync(img.encodePng(thumbnail));
|
..writeAsBytesSync(img.encodePng(thumbnail));
|
||||||
|
|
||||||
String _imagePath = "${dir.path}/$_uuid.png";
|
String _imagePath = "${dir.path}/$_uuid.png";
|
||||||
String _primaryColor = await getColor(File("${dir.path}/$_uuid.png"));
|
String _primaryColor =
|
||||||
|
await _getColor(File("${dir.path}/$_uuid.png"));
|
||||||
String _author = _p.itunes.author ?? _p.author ?? '';
|
String _author = _p.itunes.author ?? _p.author ?? '';
|
||||||
String _provider = _p.generator ?? '';
|
String _provider = _p.generator ?? '';
|
||||||
String _link = _p.link ?? '';
|
String _link = _p.link ?? '';
|
||||||
@ -102,14 +160,12 @@ class PopupMenu extends StatelessWidget {
|
|||||||
_uuid,
|
_uuid,
|
||||||
_imagePath,
|
_imagePath,
|
||||||
_provider,
|
_provider,
|
||||||
_link);
|
_link,
|
||||||
|
description: _p.description);
|
||||||
podcastLocal.description = _p.description;
|
|
||||||
|
|
||||||
await groupList.subscribe(podcastLocal);
|
await groupList.subscribe(podcastLocal);
|
||||||
|
|
||||||
if (_provider.contains('fireside'))
|
if (_provider.contains('fireside')) {
|
||||||
{
|
|
||||||
FiresideData data = FiresideData(_uuid, _link);
|
FiresideData data = FiresideData(_uuid, _link);
|
||||||
await data.fatchData();
|
await data.fatchData();
|
||||||
}
|
}
|
||||||
@ -143,33 +199,34 @@ class PopupMenu extends StatelessWidget {
|
|||||||
|
|
||||||
void _saveOmpl(String path) async {
|
void _saveOmpl(String path) async {
|
||||||
File file = File(path);
|
File file = File(path);
|
||||||
try{String opml = file.readAsStringSync();
|
try {
|
||||||
|
String opml = file.readAsStringSync();
|
||||||
|
|
||||||
var content = xml.parse(opml);
|
var content = xml.parse(opml);
|
||||||
var total = content
|
var total = content
|
||||||
.findAllElements('outline')
|
.findAllElements('outline')
|
||||||
.map((ele) => OmplOutline.parse(ele))
|
.map((ele) => OmplOutline.parse(ele))
|
||||||
.toList();
|
.toList();
|
||||||
if (total.length == 0) {
|
if (total.length == 0) {
|
||||||
Fluttertoast.showToast(
|
Fluttertoast.showToast(
|
||||||
msg: 'File Not Valid',
|
msg: 'File Not Valid',
|
||||||
gravity: ToastGravity.BOTTOM,
|
gravity: ToastGravity.BOTTOM,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
for (int i = 0; i < total.length; i++) {
|
for (int i = 0; i < total.length; i++) {
|
||||||
if (total[i].xmlUrl != null) {
|
if (total[i].xmlUrl != null) {
|
||||||
importOmpl.rssTitle = total[i].text;
|
importOmpl.rssTitle = total[i].text;
|
||||||
try {
|
try {
|
||||||
await saveOmpl(total[i].xmlUrl);
|
await saveOmpl(total[i].xmlUrl);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print(e.toString());
|
print(e.toString());
|
||||||
|
}
|
||||||
|
print(total[i].text);
|
||||||
}
|
}
|
||||||
print(total[i].text);
|
|
||||||
}
|
}
|
||||||
|
print('Import fisnished');
|
||||||
}
|
}
|
||||||
print('Import fisnished');
|
} catch (e) {
|
||||||
}}
|
|
||||||
catch(e){
|
|
||||||
print(e);
|
print(e);
|
||||||
Fluttertoast.showToast(
|
Fluttertoast.showToast(
|
||||||
msg: 'File error, Subscribe failed',
|
msg: 'File error, Subscribe failed',
|
||||||
@ -195,7 +252,8 @@ class PopupMenu extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return PopupMenuButton<int>(
|
return PopupMenuButton<int>(
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10))),
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(10))),
|
||||||
elevation: 1,
|
elevation: 1,
|
||||||
tooltip: 'Menu',
|
tooltip: 'Menu',
|
||||||
itemBuilder: (context) => [
|
itemBuilder: (context) => [
|
||||||
@ -204,40 +262,60 @@ class PopupMenu extends StatelessWidget {
|
|||||||
child: Container(
|
child: Container(
|
||||||
padding: EdgeInsets.only(left: 10),
|
padding: EdgeInsets.only(left: 10),
|
||||||
child: Row(
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Icon(LineIcons.cloud_download_alt_solid),
|
Icon(LineIcons.cloud_download_alt_solid),
|
||||||
Padding(padding: EdgeInsets.symmetric(horizontal: 5.0),),
|
Padding(
|
||||||
Text('Refresh All'),
|
padding: EdgeInsets.symmetric(horizontal: 5.0),
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
Text(
|
||||||
|
'Refresh All',
|
||||||
|
),
|
||||||
|
_showDate
|
||||||
|
? Text(
|
||||||
|
_refreshDate,
|
||||||
|
style: TextStyle(color: Colors.red, fontSize: 12),
|
||||||
|
)
|
||||||
|
: Center(),
|
||||||
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: 2,
|
value: 2,
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: EdgeInsets.only(left: 10),
|
padding: EdgeInsets.only(left: 10),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Icon(LineIcons.paperclip_solid),
|
Icon(LineIcons.paperclip_solid),
|
||||||
Padding(padding: EdgeInsets.symmetric(horizontal: 5.0),),
|
Padding(
|
||||||
Text('Import OMPL'),
|
padding: EdgeInsets.symmetric(horizontal: 5.0),
|
||||||
],
|
),
|
||||||
),
|
Text('Import OMPL'),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
// PopupMenuItem(
|
|
||||||
// value: 3,
|
// PopupMenuItem(
|
||||||
// child: setting.theme != 2 ? Text('Night Mode') : Text('Light Mode'),
|
// value: 3,
|
||||||
// ),
|
// child: setting.theme != 2 ? Text('Night Mode') : Text('Light Mode'),
|
||||||
PopupMenuItem(
|
// ),
|
||||||
|
PopupMenuItem(
|
||||||
value: 4,
|
value: 4,
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: EdgeInsets.only(left: 10),
|
padding: EdgeInsets.only(left: 10),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Icon(LineIcons.cog_solid),
|
Icon(LineIcons.cog_solid),
|
||||||
Padding(padding: EdgeInsets.symmetric(horizontal: 5.0),),
|
Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 5.0),
|
||||||
|
),
|
||||||
Text('Settings'),
|
Text('Settings'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -250,7 +328,9 @@ class PopupMenu extends StatelessWidget {
|
|||||||
child: Row(
|
child: Row(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Icon(LineIcons.info_circle_solid),
|
Icon(LineIcons.info_circle_solid),
|
||||||
Padding(padding: EdgeInsets.symmetric(horizontal: 5.0),),
|
Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 5.0),
|
||||||
|
),
|
||||||
Text('About'),
|
Text('About'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -266,11 +346,11 @@ class PopupMenu extends StatelessWidget {
|
|||||||
} else if (value == 1) {
|
} else if (value == 1) {
|
||||||
_refreshAll();
|
_refreshAll();
|
||||||
} else if (value == 3) {
|
} else if (value == 3) {
|
||||||
// setting.theme != 2 ? setting.setTheme(2) : setting.setTheme(1);
|
// setting.theme != 2 ? setting.setTheme(2) : setting.setTheme(1);
|
||||||
} else if (value == 4) {
|
} else if (value == 4) {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context, MaterialPageRoute(builder: (context) => Settings()));
|
context, MaterialPageRoute(builder: (context) => Settings()));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -81,10 +81,10 @@ class _AudioPanelState extends State<AudioPanel>
|
|||||||
color: Theme.of(context).primaryColor,
|
color: Theme.of(context).primaryColor,
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
offset: Offset(0, -1),
|
offset: Offset(0, -0.5),
|
||||||
blurRadius: 4,
|
blurRadius: 1,
|
||||||
color: Theme.of(context).brightness == Brightness.light
|
color: Theme.of(context).brightness == Brightness.light
|
||||||
? Colors.grey[400]
|
? Colors.grey[400].withOpacity(0.5)
|
||||||
: Colors.grey[800],
|
: Colors.grey[800],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -1,45 +1,15 @@
|
|||||||
import 'dart:io';
|
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
import 'package:tuple/tuple.dart';
|
|
||||||
|
|
||||||
import 'hometab.dart';
|
import 'hometab.dart';
|
||||||
import 'package:tsacdop/home/appbar/importompl.dart';
|
import 'package:tsacdop/home/appbar/importompl.dart';
|
||||||
import 'package:tsacdop/home/audioplayer.dart';
|
import 'package:tsacdop/home/audioplayer.dart';
|
||||||
import 'package:tsacdop/class/audiostate.dart';
|
|
||||||
import 'homescroll.dart';
|
import 'homescroll.dart';
|
||||||
|
|
||||||
class Home extends StatefulWidget {
|
class Home extends StatelessWidget {
|
||||||
@override
|
|
||||||
_HomeState createState() => _HomeState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _HomeState extends State<Home> {
|
|
||||||
bool _loadPlay;
|
|
||||||
|
|
||||||
static String _stringForSeconds(int seconds) {
|
|
||||||
if (seconds == null) return null;
|
|
||||||
return '${(seconds ~/ 60)}:${(seconds.truncate() % 60).toString().padLeft(2, '0')}';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_loadPlay = false;
|
|
||||||
_getPlaylist();
|
|
||||||
}
|
|
||||||
|
|
||||||
_getPlaylist() async {
|
|
||||||
await Provider.of<AudioPlayerNotifier>(context, listen: false).loadPlaylist();
|
|
||||||
setState(() {
|
|
||||||
_loadPlay = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
|
|
||||||
return Stack(children: <Widget>[
|
return Stack(children: <Widget>[
|
||||||
Column(
|
Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
@ -52,69 +22,6 @@ class _HomeState extends State<Home> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
AnimatedPositioned(
|
|
||||||
duration: Duration(milliseconds: 2000),
|
|
||||||
curve: Curves.elasticOut,
|
|
||||||
bottom: 50,
|
|
||||||
right: _loadPlay ? 5 : -25,
|
|
||||||
child: Container(
|
|
||||||
child: Selector<AudioPlayerNotifier, Tuple3<bool, Playlist, int>>(
|
|
||||||
selector: (_, audio) =>
|
|
||||||
Tuple3(audio.playerRunning, audio.queue, audio.lastPositin),
|
|
||||||
builder: (_, data, __) => !_loadPlay
|
|
||||||
? Center()
|
|
||||||
: data.item1 || data.item2.playlist.length == 0
|
|
||||||
? Center()
|
|
||||||
: InkWell(
|
|
||||||
onTap: () => audio.playlistLoad(),
|
|
||||||
child: Stack(
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
children: <Widget>[
|
|
||||||
Container(
|
|
||||||
padding: EdgeInsets.only(left: 45, right: 10.0),
|
|
||||||
alignment: Alignment.centerRight,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Theme.of(context).accentColor,
|
|
||||||
borderRadius: BorderRadius.only(
|
|
||||||
topLeft: Radius.circular(20.0),
|
|
||||||
bottomLeft: Radius.circular(20.0),
|
|
||||||
bottomRight: Radius.circular(10.0),
|
|
||||||
topRight: Radius.circular(10.0)),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Theme.of(context).brightness ==
|
|
||||||
Brightness.light
|
|
||||||
? Colors.grey[400]
|
|
||||||
: Colors.grey[800],
|
|
||||||
blurRadius: 4,
|
|
||||||
offset: Offset(1, 1)),
|
|
||||||
]),
|
|
||||||
height: 40,
|
|
||||||
child: Text(_stringForSeconds(data.item3~/1000) + '...',
|
|
||||||
style: TextStyle(color: Colors.white)),
|
|
||||||
),
|
|
||||||
CircleAvatar(
|
|
||||||
radius: 20,
|
|
||||||
backgroundImage: FileImage(File(
|
|
||||||
"${data.item2.playlist.first.imagePath}")),
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
height: 40.0,
|
|
||||||
width: 40,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
color: Colors.black12),
|
|
||||||
child: Icon(
|
|
||||||
Icons.play_arrow,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Container(child: PlayerWidget()),
|
Container(child: PlayerWidget()),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
@ -244,11 +244,30 @@ class _ScrollPodcastsState extends State<ScrollPodcasts> {
|
|||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.all(
|
borderRadius: BorderRadius.all(
|
||||||
Radius.circular(25.0)),
|
Radius.circular(25.0)),
|
||||||
child: LimitedBox(
|
child: Stack(
|
||||||
maxHeight: 50,
|
alignment: Alignment.bottomCenter,
|
||||||
maxWidth: 50,
|
children: <Widget>[
|
||||||
child: Image.file(
|
LimitedBox(
|
||||||
File("${podcastLocal.imagePath}")),
|
maxHeight: 50,
|
||||||
|
maxWidth: 50,
|
||||||
|
child: Image.file(File(
|
||||||
|
"${podcastLocal.imagePath}")),
|
||||||
|
),
|
||||||
|
podcastLocal.upateCount > 0
|
||||||
|
? Container(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
height: 10,
|
||||||
|
width: 40,
|
||||||
|
color: Colors.black54,
|
||||||
|
child: Text('New',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.red,
|
||||||
|
fontSize: 8,
|
||||||
|
fontStyle: FontStyle
|
||||||
|
.italic)),
|
||||||
|
)
|
||||||
|
: Center(),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -380,10 +399,11 @@ class ShowEpisode extends StatelessWidget {
|
|||||||
final List<EpisodeBrief> podcast;
|
final List<EpisodeBrief> podcast;
|
||||||
final PodcastLocal podcastLocal;
|
final PodcastLocal podcastLocal;
|
||||||
ShowEpisode({Key key, this.podcast, this.podcastLocal}) : super(key: key);
|
ShowEpisode({Key key, this.podcast, this.podcastLocal}) : super(key: key);
|
||||||
Offset offset;
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
double _width = MediaQuery.of(context).size.width;
|
double _width = MediaQuery.of(context).size.width;
|
||||||
|
Offset offset;
|
||||||
_showPopupMenu(Offset offset, EpisodeBrief episode, BuildContext context,
|
_showPopupMenu(Offset offset, EpisodeBrief episode, BuildContext context,
|
||||||
bool isPlaying, bool isInPlaylist) async {
|
bool isPlaying, bool isInPlaylist) async {
|
||||||
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
|
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
|
||||||
@ -432,7 +452,7 @@ class ShowEpisode extends StatelessWidget {
|
|||||||
if (value == 0) {
|
if (value == 0) {
|
||||||
if (!isPlaying) audio.episodeLoad(episode);
|
if (!isPlaying) audio.episodeLoad(episode);
|
||||||
} else if (value == 1) {
|
} else if (value == 1) {
|
||||||
if (isInPlaylist) {
|
if (!isInPlaylist) {
|
||||||
audio.addToPlaylist(episode);
|
audio.addToPlaylist(episode);
|
||||||
Fluttertoast.showToast(
|
Fluttertoast.showToast(
|
||||||
msg: 'Added to playlist',
|
msg: 'Added to playlist',
|
||||||
@ -536,6 +556,14 @@ class ShowEpisode extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Spacer(),
|
Spacer(),
|
||||||
|
index < podcastLocal.upateCount
|
||||||
|
? Text(
|
||||||
|
'New',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.red,
|
||||||
|
fontStyle: FontStyle.italic),
|
||||||
|
)
|
||||||
|
: Center(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -1,10 +1,16 @@
|
|||||||
|
import 'dart:io';
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:tsacdop/local_storage/key_value_storage.dart';
|
||||||
|
import 'package:tuple/tuple.dart';
|
||||||
|
import 'package:tsacdop/class/audiostate.dart';
|
||||||
import 'package:tsacdop/class/episodebrief.dart';
|
import 'package:tsacdop/class/episodebrief.dart';
|
||||||
import 'package:tsacdop/settings/history.dart';
|
import 'package:tsacdop/home/playlist.dart';
|
||||||
import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
|
import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
|
||||||
import 'package:tsacdop/util/episodegrid.dart';
|
import 'package:tsacdop/util/episodegrid.dart';
|
||||||
|
import 'package:tsacdop/util/mypopupmenu.dart';
|
||||||
|
|
||||||
class MainTab extends StatefulWidget {
|
class MainTab extends StatefulWidget {
|
||||||
@override
|
@override
|
||||||
@ -13,6 +19,12 @@ class MainTab extends StatefulWidget {
|
|||||||
|
|
||||||
class _MainTabState extends State<MainTab> with TickerProviderStateMixin {
|
class _MainTabState extends State<MainTab> with TickerProviderStateMixin {
|
||||||
TabController _controller;
|
TabController _controller;
|
||||||
|
bool _loadPlay;
|
||||||
|
static String _stringForSeconds(int seconds) {
|
||||||
|
if (seconds == null) return null;
|
||||||
|
return '${(seconds ~/ 60)}:${(seconds.truncate() % 60).toString().padLeft(2, '0')}';
|
||||||
|
}
|
||||||
|
|
||||||
Decoration getIndicator(BuildContext context) {
|
Decoration getIndicator(BuildContext context) {
|
||||||
return UnderlineTabIndicator(
|
return UnderlineTabIndicator(
|
||||||
borderSide: BorderSide(color: Theme.of(context).accentColor, width: 2),
|
borderSide: BorderSide(color: Theme.of(context).accentColor, width: 2),
|
||||||
@ -23,25 +35,121 @@ class _MainTabState extends State<MainTab> with TickerProviderStateMixin {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget playHistory() {
|
_getPlaylist() async {
|
||||||
return PopupMenuButton<int>(
|
await Provider.of<AudioPlayerNotifier>(context, listen: false)
|
||||||
|
.loadPlaylist();
|
||||||
|
setState(() {
|
||||||
|
_loadPlay = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget playlist(BuildContext context) {
|
||||||
|
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
|
||||||
|
return MyPopupMenuButton<int>(
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.all(Radius.circular(10))),
|
borderRadius: BorderRadius.all(Radius.circular(10))),
|
||||||
elevation: 1,
|
elevation: 1,
|
||||||
icon: Icon(Icons.history),
|
icon: Icon(Icons.playlist_play),
|
||||||
tooltip: "Menu",
|
tooltip: "Menu",
|
||||||
itemBuilder: (context) => [
|
itemBuilder: (context) => [
|
||||||
|
MyPopupMenuItem(
|
||||||
|
height: 50,
|
||||||
|
value: 1,
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
// color: Theme.of(context).accentColor,
|
||||||
|
borderRadius: BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(10.0),
|
||||||
|
topRight: Radius.circular(10.0)),
|
||||||
|
),
|
||||||
|
child: Selector<AudioPlayerNotifier, Tuple3<bool, Playlist, int>>(
|
||||||
|
selector: (_, audio) =>
|
||||||
|
Tuple3(audio.playerRunning, audio.queue, audio.lastPositin),
|
||||||
|
builder: (_, data, __) => !_loadPlay
|
||||||
|
? Container(
|
||||||
|
height: 8.0,
|
||||||
|
)
|
||||||
|
: data.item1 || data.item2.playlist.length == 0
|
||||||
|
? Container(
|
||||||
|
height: 8.0,
|
||||||
|
)
|
||||||
|
: InkWell(
|
||||||
|
borderRadius: BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(10.0),
|
||||||
|
topRight: Radius.circular(10.0)),
|
||||||
|
onTap: () {
|
||||||
|
audio.playlistLoad();
|
||||||
|
Navigator.pop<int>(context);
|
||||||
|
},
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 5),
|
||||||
|
),
|
||||||
|
Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
CircleAvatar(
|
||||||
|
radius: 20,
|
||||||
|
backgroundImage: FileImage(File(
|
||||||
|
"${data.item2.playlist.first.imagePath}")),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
height: 40.0,
|
||||||
|
width: 40.0,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: Colors.black12),
|
||||||
|
child: Icon(
|
||||||
|
Icons.play_arrow,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 2),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
height: 70,
|
||||||
|
width: 140,
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
Text(
|
||||||
|
_stringForSeconds(data.item3 ~/ 1000),
|
||||||
|
// style:
|
||||||
|
// TextStyle(color: Colors.white)
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
data.item2.playlist.first.title,
|
||||||
|
maxLines: 2,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
overflow: TextOverflow.fade,
|
||||||
|
// style: TextStyle(color: Colors.white),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Divider(
|
||||||
|
height: 2,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: 0,
|
value: 0,
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: EdgeInsets.only(left: 10),
|
padding: EdgeInsets.only(left: 10),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Icon(Icons.history),
|
Icon(Icons.playlist_play),
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 5.0),
|
padding: EdgeInsets.symmetric(horizontal: 5.0),
|
||||||
),
|
),
|
||||||
Text('Play History'),
|
Text('Playlist'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -49,9 +157,9 @@ class _MainTabState extends State<MainTab> with TickerProviderStateMixin {
|
|||||||
],
|
],
|
||||||
onSelected: (value) {
|
onSelected: (value) {
|
||||||
if (value == 0) {
|
if (value == 0) {
|
||||||
Navigator.push(context,
|
Navigator.push(
|
||||||
MaterialPageRoute(builder: (context) => PlayedHistory()));
|
context, MaterialPageRoute(builder: (context) => PlaylistPage()));
|
||||||
}
|
} else if (value == 1) {}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -60,6 +168,8 @@ class _MainTabState extends State<MainTab> with TickerProviderStateMixin {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_controller = TabController(length: 3, vsync: this);
|
_controller = TabController(length: 3, vsync: this);
|
||||||
|
_loadPlay = false;
|
||||||
|
_getPlaylist();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -103,7 +213,7 @@ class _MainTabState extends State<MainTab> with TickerProviderStateMixin {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Spacer(),
|
Spacer(),
|
||||||
playHistory(),
|
playlist(context),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
@ -135,9 +245,12 @@ class RecentUpdate extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _RecentUpdateState extends State<RecentUpdate> {
|
class _RecentUpdateState extends State<RecentUpdate> {
|
||||||
|
int _updateCount = 0;
|
||||||
Future<List<EpisodeBrief>> _getRssItem(int top) async {
|
Future<List<EpisodeBrief>> _getRssItem(int top) async {
|
||||||
var dbHelper = DBHelper();
|
var dbHelper = DBHelper();
|
||||||
List<EpisodeBrief> episodes = await dbHelper.getRecentRssItem(top);
|
List<EpisodeBrief> episodes = await dbHelper.getRecentRssItem(top);
|
||||||
|
KeyValueStorage refreshcountstorage = KeyValueStorage('refreshcount');
|
||||||
|
_updateCount = await refreshcountstorage.getInt();
|
||||||
return episodes;
|
return episodes;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -189,6 +302,7 @@ class _RecentUpdateState extends State<RecentUpdate> {
|
|||||||
showFavorite: false,
|
showFavorite: false,
|
||||||
showNumber: false,
|
showNumber: false,
|
||||||
heroTag: 'recent',
|
heroTag: 'recent',
|
||||||
|
updateCount: _updateCount,
|
||||||
),
|
),
|
||||||
SliverList(
|
SliverList(
|
||||||
delegate: SliverChildBuilderDelegate(
|
delegate: SliverChildBuilderDelegate(
|
||||||
@ -277,7 +391,6 @@ class _MyDownloadState extends State<MyDownload> {
|
|||||||
heroTag: 'download',
|
heroTag: 'download',
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
|
||||||
)
|
)
|
||||||
: Center(child: CircularProgressIndicator());
|
: Center(child: CircularProgressIndicator());
|
||||||
},
|
},
|
||||||
|
193
lib/home/playlist.dart
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
|
import 'package:tsacdop/episodes/episodedetail.dart';
|
||||||
|
import 'package:tuple/tuple.dart';
|
||||||
|
import 'package:line_icons/line_icons.dart';
|
||||||
|
import 'package:tsacdop/class/audiostate.dart';
|
||||||
|
import 'package:tsacdop/class/episodebrief.dart';
|
||||||
|
import 'package:tsacdop/util/colorize.dart';
|
||||||
|
|
||||||
|
class PlaylistPage extends StatefulWidget {
|
||||||
|
@override
|
||||||
|
_PlaylistPageState createState() => _PlaylistPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PlaylistPageState extends State<PlaylistPage> {
|
||||||
|
final GlobalKey<AnimatedListState> _playlistKey = GlobalKey();
|
||||||
|
final textstyle = TextStyle(fontSize: 15.0, color: Colors.black);
|
||||||
|
Widget episodeTag(String text, Color color) {
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color, borderRadius: BorderRadius.all(Radius.circular(15.0))),
|
||||||
|
height: 23.0,
|
||||||
|
margin: EdgeInsets.only(right: 10.0),
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 8.0),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Text(text, style: TextStyle(fontSize: 14.0, color: Colors.black)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
|
||||||
|
return AnnotatedRegion<SystemUiOverlayStyle>(
|
||||||
|
value: SystemUiOverlayStyle(
|
||||||
|
systemNavigationBarIconBrightness:
|
||||||
|
Theme.of(context).accentColorBrightness,
|
||||||
|
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
|
||||||
|
systemNavigationBarColor: Theme.of(context).primaryColor,
|
||||||
|
),
|
||||||
|
child: Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text('Playlist'),
|
||||||
|
elevation: 0,
|
||||||
|
backgroundColor: Theme.of(context).primaryColor,
|
||||||
|
),
|
||||||
|
body: SafeArea(
|
||||||
|
child:
|
||||||
|
Selector<AudioPlayerNotifier, Tuple2<List<EpisodeBrief>, bool>>(
|
||||||
|
selector: (_, audio) =>
|
||||||
|
Tuple2(audio.queue.playlist, audio.playerRunning),
|
||||||
|
builder: (_, data, __) {
|
||||||
|
return AnimatedList(
|
||||||
|
key: _playlistKey,
|
||||||
|
shrinkWrap: true,
|
||||||
|
scrollDirection: Axis.vertical,
|
||||||
|
initialItemCount: data.item1.length,
|
||||||
|
itemBuilder: (context, index, animation) {
|
||||||
|
Color _c =
|
||||||
|
(Theme.of(context).brightness == Brightness.light)
|
||||||
|
? data.item1[index].primaryColor.colorizedark()
|
||||||
|
: data.item1[index].primaryColor.colorizeLight();
|
||||||
|
return ScaleTransition(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
scale: animation,
|
||||||
|
child: Dismissible(
|
||||||
|
background: Container(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 20.0),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: <Widget>[
|
||||||
|
Icon(
|
||||||
|
Icons.delete,
|
||||||
|
color: Theme.of(context).accentColor,
|
||||||
|
),
|
||||||
|
Icon(
|
||||||
|
Icons.delete,
|
||||||
|
color: Theme.of(context).accentColor,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
height: 50,
|
||||||
|
color: Colors.grey[500],
|
||||||
|
),
|
||||||
|
key: Key(data.item1[index].enclosureUrl),
|
||||||
|
onDismissed: (direction) async {
|
||||||
|
await audio.delFromPlaylist(data.item1[index]);
|
||||||
|
_playlistKey.currentState.removeItem(
|
||||||
|
index, (context, animation) => Center());
|
||||||
|
Fluttertoast.showToast(
|
||||||
|
msg: 'Removed From Playlist',
|
||||||
|
gravity: ToastGravity.BOTTOM,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
ListTile(
|
||||||
|
title: Text(
|
||||||
|
data.item1[index].title,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
leading: CircleAvatar(
|
||||||
|
backgroundColor: _c.withOpacity(0.5),
|
||||||
|
backgroundImage: FileImage(
|
||||||
|
File("${data.item1[index].imagePath}")),
|
||||||
|
),
|
||||||
|
trailing: index == 0
|
||||||
|
? data.item2
|
||||||
|
? Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
right: 12.0),
|
||||||
|
child: SizedBox(
|
||||||
|
width: 20,
|
||||||
|
height: 15,
|
||||||
|
child: WaveLoader()),
|
||||||
|
)
|
||||||
|
: IconButton(
|
||||||
|
icon: Icon(Icons.play_arrow),
|
||||||
|
onPressed: () => audio.playlistLoad())
|
||||||
|
: IconButton(
|
||||||
|
tooltip: 'Move to Top',
|
||||||
|
icon:
|
||||||
|
Icon(LineIcons.arrow_circle_up_solid),
|
||||||
|
onPressed: () async {
|
||||||
|
await audio
|
||||||
|
.moveToTop(data.item1[index]);
|
||||||
|
_playlistKey.currentState.removeItem(
|
||||||
|
index,
|
||||||
|
(context, animation) =>
|
||||||
|
Container());
|
||||||
|
data.item2
|
||||||
|
? _playlistKey.currentState
|
||||||
|
.insertItem(1)
|
||||||
|
: _playlistKey.currentState
|
||||||
|
.insertItem(0);
|
||||||
|
}),
|
||||||
|
subtitle: Container(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 5),
|
||||||
|
child: Row(
|
||||||
|
children: <Widget>[
|
||||||
|
(data.item1[index].explicit == 1)
|
||||||
|
? Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.red[800],
|
||||||
|
shape: BoxShape.circle),
|
||||||
|
height: 20.0,
|
||||||
|
width: 20.0,
|
||||||
|
margin:
|
||||||
|
EdgeInsets.only(right: 10.0),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Text('E',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white)))
|
||||||
|
: Center(),
|
||||||
|
data.item1[index].duration != 0
|
||||||
|
? episodeTag(
|
||||||
|
(data.item1[index].duration)
|
||||||
|
.toString() +
|
||||||
|
'mins',
|
||||||
|
Colors.cyan[300])
|
||||||
|
: Center(),
|
||||||
|
data.item1[index].enclosureLength != null
|
||||||
|
? episodeTag(
|
||||||
|
((data.item1[index]
|
||||||
|
.enclosureLength) ~/
|
||||||
|
1000000)
|
||||||
|
.toString() +
|
||||||
|
'MB',
|
||||||
|
Colors.lightBlue[300])
|
||||||
|
: Center(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Divider(
|
||||||
|
height: 2,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -30,13 +30,14 @@ class DBHelper {
|
|||||||
.execute("""CREATE TABLE PodcastLocal(id TEXT PRIMARY KEY,title TEXT,
|
.execute("""CREATE TABLE PodcastLocal(id TEXT PRIMARY KEY,title TEXT,
|
||||||
imageUrl TEXT,rssUrl TEXT UNIQUE,primaryColor TEXT,author TEXT,
|
imageUrl TEXT,rssUrl TEXT UNIQUE,primaryColor TEXT,author TEXT,
|
||||||
description TEXT, add_date INTEGER, imagePath TEXT, provider TEXT, link TEXT,
|
description TEXT, add_date INTEGER, imagePath TEXT, provider TEXT, link TEXT,
|
||||||
background_image TEXT DEFAULT '',hosts TEXT DEFAULT '')""");
|
background_image TEXT DEFAULT '',hosts TEXT DEFAULT '',update_count INTEGER DEFAULT 0)""");
|
||||||
await db
|
await db
|
||||||
.execute("""CREATE TABLE Episodes(id INTEGER PRIMARY KEY,title TEXT,
|
.execute("""CREATE TABLE Episodes(id INTEGER PRIMARY KEY,title TEXT,
|
||||||
enclosure_url TEXT UNIQUE, enclosure_length INTEGER, pubDate TEXT,
|
enclosure_url TEXT UNIQUE, enclosure_length INTEGER, pubDate TEXT,
|
||||||
description TEXT, feed_id TEXT, feed_link TEXT, milliseconds INTEGER,
|
description TEXT, feed_id TEXT, feed_link TEXT, milliseconds INTEGER,
|
||||||
duration INTEGER DEFAULT 0, explicit INTEGER DEFAULT 0, liked INTEGER DEFAULT 0,
|
duration INTEGER DEFAULT 0, explicit INTEGER DEFAULT 0, liked INTEGER DEFAULT 0,
|
||||||
downloaded TEXT DEFAULT 'ND', download_date INTEGER DEFAULT 0, media_id TEXT)""");
|
downloaded TEXT DEFAULT 'ND', download_date INTEGER DEFAULT 0, media_id TEXT,
|
||||||
|
is_new INTEGER DEFAULT 0)""");
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"""CREATE TABLE PlayHistory(id INTEGER PRIMARY KEY, title TEXT, enclosure_url TEXT UNIQUE,
|
"""CREATE TABLE PlayHistory(id INTEGER PRIMARY KEY, title TEXT, enclosure_url TEXT UNIQUE,
|
||||||
seconds REAL, seek_value REAL, add_date INTEGER)""");
|
seconds REAL, seek_value REAL, add_date INTEGER)""");
|
||||||
@ -51,7 +52,7 @@ class DBHelper {
|
|||||||
await Future.forEach(podcasts, (s) async {
|
await Future.forEach(podcasts, (s) async {
|
||||||
List<Map> list;
|
List<Map> list;
|
||||||
list = await dbClient.rawQuery(
|
list = await dbClient.rawQuery(
|
||||||
'SELECT id, title, imageUrl, rssUrl, primaryColor, author, imagePath , provider, link FROM PodcastLocal WHERE id = ?',
|
'SELECT id, title, imageUrl, rssUrl, primaryColor, author, imagePath , provider, link ,update_count FROM PodcastLocal WHERE id = ?',
|
||||||
[s]);
|
[s]);
|
||||||
podcastLocal.add(PodcastLocal(
|
podcastLocal.add(PodcastLocal(
|
||||||
list.first['title'],
|
list.first['title'],
|
||||||
@ -62,7 +63,8 @@ class DBHelper {
|
|||||||
list.first['id'],
|
list.first['id'],
|
||||||
list.first['imagePath'],
|
list.first['imagePath'],
|
||||||
list.first['provider'],
|
list.first['provider'],
|
||||||
list.first['link']));
|
list.first['link'],
|
||||||
|
upateCount: list.first['update_count']));
|
||||||
});
|
});
|
||||||
return podcastLocal;
|
return podcastLocal;
|
||||||
}
|
}
|
||||||
@ -199,16 +201,19 @@ class DBHelper {
|
|||||||
return playHistory;
|
return playHistory;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<SubHistory>> getSubHistory() async{
|
Future<List<SubHistory>> getSubHistory() async {
|
||||||
var dbClient = await database;
|
var dbClient = await database;
|
||||||
List<Map> list = await dbClient.rawQuery(
|
List<Map> list = await dbClient.rawQuery(
|
||||||
"""SELECT title, rss_url, add_date, remove_date, status FROM SubscribeHistory
|
"""SELECT title, rss_url, add_date, remove_date, status FROM SubscribeHistory
|
||||||
ORDER BY add_date DESC"""
|
ORDER BY add_date DESC""");
|
||||||
);
|
return list
|
||||||
return list.map((record) => SubHistory(
|
.map((record) => SubHistory(
|
||||||
record['status']==0 ? true : false, DateTime.fromMillisecondsSinceEpoch(record['remove_date']),
|
record['status'] == 0 ? true : false,
|
||||||
DateTime.fromMillisecondsSinceEpoch(record['add_date']), record['rss_url'], record['title']
|
DateTime.fromMillisecondsSinceEpoch(record['remove_date']),
|
||||||
)).toList();
|
DateTime.fromMillisecondsSinceEpoch(record['add_date']),
|
||||||
|
record['rss_url'],
|
||||||
|
record['title']))
|
||||||
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<double> listenMins(int day) async {
|
Future<double> listenMins(int day) async {
|
||||||
@ -252,12 +257,12 @@ class DBHelper {
|
|||||||
RegExp z = RegExp(r'(\+|\-)[0-1][0-9]00');
|
RegExp z = RegExp(r'(\+|\-)[0-1][0-9]00');
|
||||||
String timezone = z.stringMatch(pubDate);
|
String timezone = z.stringMatch(pubDate);
|
||||||
int timezoneInt = 0;
|
int timezoneInt = 0;
|
||||||
if(timezone!=null){
|
if (timezone != null) {
|
||||||
if(timezone.substring(0, 1) == '-'){
|
if (timezone.substring(0, 1) == '-') {
|
||||||
timezoneInt = int.parse(timezone.substring(1,2));
|
timezoneInt = int.parse(timezone.substring(1, 2));
|
||||||
} else {
|
} else {
|
||||||
timezoneInt = -int.parse(timezone.substring(1,2));
|
timezoneInt = -int.parse(timezone.substring(1, 2));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
date = DateFormat('EEE, dd MMM yyyy HH:mm:ss Z', 'en_US').parse(pubDate);
|
date = DateFormat('EEE, dd MMM yyyy HH:mm:ss Z', 'en_US').parse(pubDate);
|
||||||
@ -288,7 +293,7 @@ class DBHelper {
|
|||||||
return date.add(Duration(hours: timezoneInt));
|
return date.add(Duration(hours: timezoneInt));
|
||||||
}
|
}
|
||||||
|
|
||||||
int getExplicit(bool b) {
|
int _getExplicit(bool b) {
|
||||||
int result;
|
int result;
|
||||||
if (b == true) {
|
if (b == true) {
|
||||||
result = 1;
|
result = 1;
|
||||||
@ -299,117 +304,147 @@ class DBHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isXimalaya(String input) {
|
bool _isXimalaya(String input) {
|
||||||
RegExp ximalaya = RegExp(r"ximalaya.com");
|
RegExp ximalaya = RegExp(r"ximalaya.com");
|
||||||
return ximalaya.hasMatch(input);
|
return ximalaya.hasMatch(input);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<int> savePodcastRss(RssFeed _p, String id) async {
|
String _getDescription(String content, String description, String summary) {
|
||||||
int _result = _p.items.length;
|
if (content.length >= description.length) {
|
||||||
var dbClient = await database;
|
if (content.length >= summary.length) {
|
||||||
String _description, _url;
|
return content;
|
||||||
for (int i = 0; i < _result; i++) {
|
|
||||||
print(_p.items[i].title);
|
|
||||||
if (_p.items[i].itunes.summary != null) {
|
|
||||||
_p.items[i].itunes.summary.contains('<')
|
|
||||||
? _description = _p.items[i].itunes.summary
|
|
||||||
: _description = _p.items[i].description;
|
|
||||||
} else {
|
} else {
|
||||||
_description = _p.items[i].description;
|
return summary;
|
||||||
|
}
|
||||||
|
} else if (description.length >= summary.length) {
|
||||||
|
return description;
|
||||||
|
} else {
|
||||||
|
return summary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> savePodcastRss(RssFeed feed, String id) async {
|
||||||
|
feed.items.removeWhere((item) => item == null);
|
||||||
|
int result = feed.items.length;
|
||||||
|
var dbClient = await database;
|
||||||
|
String description, url;
|
||||||
|
for (int i = 0; i < result; i++) {
|
||||||
|
print(feed.items[i].title);
|
||||||
|
description = _getDescription(feed.items[i].content.value ?? '',
|
||||||
|
feed.items[i].description ?? '', feed.items[i].itunes.summary ?? '');
|
||||||
|
// if (feed.items[i].itunes.summary != null) {
|
||||||
|
// feed.items[i].itunes.summary.contains('<')
|
||||||
|
// ? description = feed.items[i].itunes.summary
|
||||||
|
// : description = feed.items[i].description;
|
||||||
|
// } else {
|
||||||
|
// description = feed.items[i].description;
|
||||||
|
// }
|
||||||
|
if (feed.items[i].enclosure != null) {
|
||||||
|
_isXimalaya(feed.items[i].enclosure.url)
|
||||||
|
? url = feed.items[i].enclosure.url.split('=').last
|
||||||
|
: url = feed.items[i].enclosure.url;
|
||||||
}
|
}
|
||||||
|
|
||||||
isXimalaya(_p.items[i].enclosure.url)
|
final title = feed.items[i].itunes.title ?? feed.items[i].title;
|
||||||
? _url = _p.items[i].enclosure.url.split('=').last
|
final length = feed.items[i]?.enclosure?.length;
|
||||||
: _url = _p.items[i].enclosure.url;
|
final pubDate = feed.items[i].pubDate;
|
||||||
|
print(pubDate);
|
||||||
|
final date = _parsePubDate(pubDate);
|
||||||
|
final milliseconds = date.millisecondsSinceEpoch;
|
||||||
|
final duration = feed.items[i].itunes.duration?.inMinutes ?? 0;
|
||||||
|
final explicit = _getExplicit(feed.items[i].itunes.explicit);
|
||||||
|
|
||||||
final _title = _p.items[i].itunes.title ?? _p.items[i].title;
|
if (url != null) {
|
||||||
final _length = _p.items[i].enclosure.length;
|
|
||||||
final _pubDate = _p.items[i].pubDate;
|
|
||||||
print(_pubDate);
|
|
||||||
final _date = _parsePubDate(_pubDate);
|
|
||||||
final _milliseconds = _date.millisecondsSinceEpoch;
|
|
||||||
final _duration = _p.items[i].itunes.duration?.inMinutes ?? 0;
|
|
||||||
final _explicit = getExplicit(_p.items[i].itunes.explicit);
|
|
||||||
|
|
||||||
if (_url != null) {
|
|
||||||
await dbClient.transaction((txn) {
|
await dbClient.transaction((txn) {
|
||||||
return txn.rawInsert(
|
return txn.rawInsert(
|
||||||
"""INSERT OR IGNORE INTO Episodes(title, enclosure_url, enclosure_length, pubDate,
|
"""INSERT OR IGNORE INTO Episodes(title, enclosure_url, enclosure_length, pubDate,
|
||||||
description, feed_id, milliseconds, duration, explicit, media_id) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
description, feed_id, milliseconds, duration, explicit, media_id) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
||||||
[
|
[
|
||||||
_title,
|
title,
|
||||||
_url,
|
url,
|
||||||
_length,
|
length,
|
||||||
_pubDate,
|
pubDate,
|
||||||
_description,
|
description,
|
||||||
id,
|
id,
|
||||||
_milliseconds,
|
milliseconds,
|
||||||
_duration,
|
duration,
|
||||||
_explicit,
|
explicit,
|
||||||
_url
|
url
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return _result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<int> updatePodcastRss(PodcastLocal podcastLocal) async {
|
Future<int> updatePodcastRss(PodcastLocal podcastLocal) async {
|
||||||
Response response = await Dio().get(podcastLocal.rssUrl);
|
Response response = await Dio().get(podcastLocal.rssUrl);
|
||||||
var _p = RssFeed.parse(response.data);
|
var feed = RssFeed.parse(response.data);
|
||||||
String _url, _description;
|
String url, description;
|
||||||
int _result = _p.items.length;
|
feed.items.removeWhere((item) => item == null);
|
||||||
|
int result = feed.items.length;
|
||||||
|
|
||||||
var dbClient = await database;
|
var dbClient = await database;
|
||||||
int _count = Sqflite.firstIntValue(await dbClient.rawQuery(
|
int count = Sqflite.firstIntValue(await dbClient.rawQuery(
|
||||||
'SELECT COUNT(*) FROM Episodes WHERE feed_id = ?', [podcastLocal.id]));
|
'SELECT COUNT(*) FROM Episodes WHERE feed_id = ?', [podcastLocal.id]));
|
||||||
print(_count);
|
|
||||||
if (_count == _result) {
|
print(count);
|
||||||
_result = 0;
|
await dbClient.rawUpdate(
|
||||||
return _result;
|
"""UPDATE PodcastLocal SET update_count = ? WHERE id = ?""",
|
||||||
|
[(result - count), podcastLocal.id]);
|
||||||
|
if (count == result) {
|
||||||
|
result = 0;
|
||||||
|
return result;
|
||||||
} else {
|
} else {
|
||||||
for (int i = 0; i < (_result - _count); i++) {
|
for (int i = 0; i < (result - count); i++) {
|
||||||
print(_p.items[i].title);
|
print(feed.items[i].title);
|
||||||
if (_p.items[i].itunes.summary != null) {
|
// if (feed.items[i].itunes.summary != null) {
|
||||||
_p.items[i].itunes.summary.contains('<')
|
// feed.items[i].itunes.summary.contains('<')
|
||||||
? _description = _p.items[i].itunes.summary
|
// ? description = feed.items[i].itunes.summary
|
||||||
: _description = _p.items[i].description;
|
// : description = feed.items[i].description;
|
||||||
} else {
|
// } else {
|
||||||
_description = _p.items[i].description;
|
// description = feed.items[i].description;
|
||||||
|
// }
|
||||||
|
description = _getDescription(
|
||||||
|
feed.items[i].content.value ?? '',
|
||||||
|
feed.items[i].description ?? '',
|
||||||
|
feed.items[i].itunes.summary ?? '');
|
||||||
|
|
||||||
|
if (feed.items[i].enclosure?.url != null) {
|
||||||
|
_isXimalaya(feed.items[i].enclosure.url)
|
||||||
|
? url = feed.items[i].enclosure.url.split('=').last
|
||||||
|
: url = feed.items[i].enclosure.url;
|
||||||
}
|
}
|
||||||
|
|
||||||
isXimalaya(_p.items[i].enclosure.url)
|
final title = feed.items[i].itunes.title ?? feed.items[i].title;
|
||||||
? _url = _p.items[i].enclosure.url.split('=').last
|
final length = feed.items[i]?.enclosure?.length;
|
||||||
: _url = _p.items[i].enclosure.url;
|
final pubDate = feed.items[i].pubDate;
|
||||||
|
final date = _parsePubDate(pubDate);
|
||||||
|
final milliseconds = date.millisecondsSinceEpoch;
|
||||||
|
final duration = feed.items[i].itunes.duration?.inMinutes ?? 0;
|
||||||
|
final explicit = _getExplicit(feed.items[i].itunes.explicit);
|
||||||
|
|
||||||
final _title = _p.items[i].itunes.title ?? _p.items[i].title;
|
if (url != null) {
|
||||||
final _length = _p.items[i].enclosure.length;
|
|
||||||
final _pubDate = _p.items[i].pubDate;
|
|
||||||
final _date = _parsePubDate(_pubDate);
|
|
||||||
final _milliseconds = _date.millisecondsSinceEpoch;
|
|
||||||
final _duration = _p.items[i].itunes.duration?.inMinutes ?? 0;
|
|
||||||
final _explicit = getExplicit(_p.items[i].itunes.explicit);
|
|
||||||
|
|
||||||
if (_url != null) {
|
|
||||||
await dbClient.transaction((txn) {
|
await dbClient.transaction((txn) {
|
||||||
return txn.rawInsert(
|
return txn.rawInsert(
|
||||||
"""INSERT OR IGNORE INTO Episodes(title, enclosure_url, enclosure_length, pubDate,
|
"""INSERT OR IGNORE INTO Episodes(title, enclosure_url, enclosure_length, pubDate,
|
||||||
description, feed_id, milliseconds, duration, explicit, media_id) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
description, feed_id, milliseconds, duration, explicit, media_id) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
||||||
[
|
[
|
||||||
_title,
|
title,
|
||||||
_url,
|
url,
|
||||||
_length,
|
length,
|
||||||
_pubDate,
|
pubDate,
|
||||||
_description,
|
description,
|
||||||
podcastLocal.id,
|
podcastLocal.id,
|
||||||
_milliseconds,
|
milliseconds,
|
||||||
_duration,
|
duration,
|
||||||
_explicit,
|
explicit,
|
||||||
_url
|
url
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return _result - _count;
|
return result - count;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -573,7 +608,6 @@ class DBHelper {
|
|||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Future<int> saveMediaId(String url, String path) async {
|
Future<int> saveMediaId(String url, String path) async {
|
||||||
var dbClient = await database;
|
var dbClient = await database;
|
||||||
int _milliseconds = DateTime.now().millisecondsSinceEpoch;
|
int _milliseconds = DateTime.now().millisecondsSinceEpoch;
|
||||||
@ -583,11 +617,11 @@ class DBHelper {
|
|||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Future<int> delDownloaded(String url) async {
|
Future<int> delDownloaded(String url) async {
|
||||||
var dbClient = await database;
|
var dbClient = await database;
|
||||||
int count = await dbClient.rawUpdate(
|
int count = await dbClient.rawUpdate(
|
||||||
"UPDATE Episodes SET downloaded = 'ND', media_id = ? WHERE enclosure_url = ?", [url, url]);
|
"UPDATE Episodes SET downloaded = 'ND', media_id = ? WHERE enclosure_url = ?",
|
||||||
|
[url, url]);
|
||||||
print('Deleted ' + url);
|
print('Deleted ' + url);
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
@ -642,23 +676,27 @@ class DBHelper {
|
|||||||
P.title as feed_title, E.duration, E.explicit, E.liked, E.downloaded,
|
P.title as feed_title, E.duration, E.explicit, E.liked, E.downloaded,
|
||||||
P.primaryColor, E.media_id FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
|
P.primaryColor, E.media_id FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
|
||||||
WHERE E.enclosure_url = ?""", [url]);
|
WHERE E.enclosure_url = ?""", [url]);
|
||||||
episode = EpisodeBrief(
|
if (list.length == 0) {
|
||||||
list.first['title'],
|
return null;
|
||||||
list.first['enclosure_url'],
|
} else {
|
||||||
list.first['enclosure_length'],
|
episode = EpisodeBrief(
|
||||||
list.first['milliseconds'],
|
list.first['title'],
|
||||||
list.first['feed_title'],
|
list.first['enclosure_url'],
|
||||||
list.first['primaryColor'],
|
list.first['enclosure_length'],
|
||||||
list.first['liked'],
|
list.first['milliseconds'],
|
||||||
list.first['downloaded'],
|
list.first['feed_title'],
|
||||||
list.first['duration'],
|
list.first['primaryColor'],
|
||||||
list.first['explicit'],
|
list.first['liked'],
|
||||||
list.first['imagePath'],
|
list.first['downloaded'],
|
||||||
list.first['media_id']);
|
list.first['duration'],
|
||||||
return episode;
|
list.first['explicit'],
|
||||||
|
list.first['imagePath'],
|
||||||
|
list.first['media_id']);
|
||||||
|
return episode;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<EpisodeBrief> getRssItemWithMediaId(String id) async {
|
Future<EpisodeBrief> getRssItemWithMediaId(String id) async {
|
||||||
var dbClient = await database;
|
var dbClient = await database;
|
||||||
EpisodeBrief episode;
|
EpisodeBrief episode;
|
||||||
List<Map> list = await dbClient.rawQuery(
|
List<Map> list = await dbClient.rawQuery(
|
||||||
@ -681,5 +719,4 @@ class DBHelper {
|
|||||||
list.first['media_id']);
|
list.first['media_id']);
|
||||||
return episode;
|
return episode;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -41,30 +41,20 @@ Future main() async {
|
|||||||
child: MyApp(),
|
child: MyApp(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
SystemUiOverlayStyle systemUiOverlayStyle =
|
||||||
|
SystemUiOverlayStyle(statusBarColor: Colors.transparent, systemNavigationBarColor: Colors.transparent);
|
||||||
|
SystemChrome.setSystemUIOverlayStyle(systemUiOverlayStyle);
|
||||||
|
|
||||||
await SystemChrome.setPreferredOrientations(
|
await SystemChrome.setPreferredOrientations(
|
||||||
[DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]);
|
[DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]);
|
||||||
}
|
}
|
||||||
|
|
||||||
class MyApp extends StatelessWidget {
|
class MyApp extends StatelessWidget {
|
||||||
void setWorkManager() {
|
|
||||||
Workmanager.initialize(
|
|
||||||
callbackDispatcher,
|
|
||||||
isInDebugMode: true,
|
|
||||||
);
|
|
||||||
|
|
||||||
Workmanager.registerPeriodicTask("1", "update_podcasts",
|
|
||||||
frequency: Duration(hours: 12),
|
|
||||||
initialDelay: Duration(seconds: 5),
|
|
||||||
constraints: Constraints(
|
|
||||||
networkType: NetworkType.connected,
|
|
||||||
requiresBatteryNotLow: true,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Consumer<SettingState>(
|
return Consumer<SettingState>(
|
||||||
builder: (_, setting, __) {
|
builder: (_, setting, __) {
|
||||||
if (setting.autoUpdate) setWorkManager();
|
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
themeMode: setting.theme,
|
themeMode: setting.theme,
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
@ -92,6 +82,7 @@ class MyApp extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
darkTheme: ThemeData.dark().copyWith(
|
darkTheme: ThemeData.dark().copyWith(
|
||||||
accentColor: setting.accentSetColor,
|
accentColor: setting.accentSetColor,
|
||||||
|
// scaffoldBackgroundColor: Colors.black87,
|
||||||
appBarTheme: AppBarTheme(elevation: 0),
|
appBarTheme: AppBarTheme(elevation: 0),
|
||||||
),
|
),
|
||||||
home: MyHomePage(),
|
home: MyHomePage(),
|
||||||
|
@ -101,9 +101,7 @@ class _PodcastDetailState extends State<PodcastDetail> {
|
|||||||
fit: BoxFit.cover)),
|
fit: BoxFit.cover)),
|
||||||
alignment: Alignment.centerRight,
|
alignment: Alignment.centerRight,
|
||||||
child: Container(
|
child: Container(
|
||||||
color: Theme.of(context)
|
color: Colors.black26,
|
||||||
.scaffoldBackgroundColor
|
|
||||||
.withOpacity(0.5),
|
|
||||||
padding: EdgeInsets.symmetric(vertical: 5.0),
|
padding: EdgeInsets.symmetric(vertical: 5.0),
|
||||||
width: MediaQuery.of(context).size.width,
|
width: MediaQuery.of(context).size.width,
|
||||||
alignment: Alignment.centerRight,
|
alignment: Alignment.centerRight,
|
||||||
@ -162,186 +160,206 @@ class _PodcastDetailState extends State<PodcastDetail> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
||||||
Color _color = widget.podcastLocal.primaryColor.colorizedark();
|
Color _color = widget.podcastLocal.primaryColor.colorizedark();
|
||||||
return AnnotatedRegion<SystemUiOverlayStyle>(
|
return AnnotatedRegion<SystemUiOverlayStyle>(
|
||||||
value: SystemUiOverlayStyle(
|
value: SystemUiOverlayStyle(
|
||||||
statusBarIconBrightness: Brightness.light,
|
statusBarIconBrightness: Brightness.dark,
|
||||||
systemNavigationBarColor: Theme.of(context).primaryColor,
|
systemNavigationBarColor: Theme.of(context).primaryColor,
|
||||||
statusBarColor: _color,
|
systemNavigationBarIconBrightness:
|
||||||
|
Theme.of(context).accentColorBrightness,
|
||||||
|
//statusBarColor: _color,
|
||||||
),
|
),
|
||||||
child: SafeArea(
|
child: Scaffold(
|
||||||
child: Scaffold(
|
body: SafeArea(
|
||||||
body: RefreshIndicator(
|
top: false,
|
||||||
key: _refreshIndicatorKey,
|
child: RefreshIndicator(
|
||||||
color: Theme.of(context).accentColor,
|
key: _refreshIndicatorKey,
|
||||||
onRefresh: () => _updateRssItem(widget.podcastLocal),
|
color: Theme.of(context).accentColor,
|
||||||
child: Stack(
|
onRefresh: () => _updateRssItem(widget.podcastLocal),
|
||||||
children: <Widget>[
|
child: Stack(
|
||||||
FutureBuilder<List<EpisodeBrief>>(
|
children: <Widget>[
|
||||||
future: _getRssItem(widget.podcastLocal),
|
FutureBuilder<List<EpisodeBrief>>(
|
||||||
builder: (context, snapshot) {
|
future: _getRssItem(widget.podcastLocal),
|
||||||
if (snapshot.hasError) print(snapshot.error);
|
builder: (context, snapshot) {
|
||||||
return (snapshot.hasData)
|
if (snapshot.hasError) print(snapshot.error);
|
||||||
? CustomScrollView(
|
return (snapshot.hasData)
|
||||||
physics: const AlwaysScrollableScrollPhysics(),
|
? CustomScrollView(
|
||||||
primary: true,
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
slivers: <Widget>[
|
primary: true,
|
||||||
SliverAppBar(
|
slivers: <Widget>[
|
||||||
actions: <Widget>[
|
SliverAppBar(
|
||||||
PopupMenuButton<String>(
|
brightness: Brightness.dark,
|
||||||
shape: RoundedRectangleBorder(
|
actions: <Widget>[
|
||||||
borderRadius: BorderRadius.all(
|
PopupMenuButton<String>(
|
||||||
Radius.circular(10))),
|
shape: RoundedRectangleBorder(
|
||||||
elevation: 2,
|
borderRadius: BorderRadius.all(
|
||||||
tooltip: 'Menu',
|
Radius.circular(10))),
|
||||||
itemBuilder: (context) => [
|
elevation: 2,
|
||||||
widget.podcastLocal.link != null
|
tooltip: 'Menu',
|
||||||
? PopupMenuItem(
|
itemBuilder: (context) => [
|
||||||
value: widget.podcastLocal.link,
|
widget.podcastLocal.link != null
|
||||||
child: Container(
|
? PopupMenuItem(
|
||||||
padding:
|
value: widget.podcastLocal.link,
|
||||||
EdgeInsets.only(left: 10),
|
child: Container(
|
||||||
child: Row(
|
padding:
|
||||||
children: <Widget>[
|
EdgeInsets.only(left: 10),
|
||||||
Icon(Icons.link,
|
child: Row(
|
||||||
color: Theme.of(context)
|
children: <Widget>[
|
||||||
.tabBarTheme
|
Icon(Icons.link,
|
||||||
.labelColor),
|
color: Theme.of(context)
|
||||||
Padding(
|
.tabBarTheme
|
||||||
padding:
|
.labelColor),
|
||||||
EdgeInsets.symmetric(
|
Padding(
|
||||||
horizontal: 5.0),
|
padding:
|
||||||
),
|
EdgeInsets.symmetric(
|
||||||
Text('Visit Site'),
|
horizontal: 5.0),
|
||||||
],
|
),
|
||||||
|
Text('Visit Site'),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
)
|
||||||
)
|
: Center(),
|
||||||
: Center(),
|
PopupMenuItem(
|
||||||
PopupMenuItem(
|
value: widget.podcastLocal.rssUrl,
|
||||||
value: widget.podcastLocal.rssUrl,
|
child: Container(
|
||||||
child: Container(
|
padding: EdgeInsets.only(left: 10),
|
||||||
padding: EdgeInsets.only(left: 10),
|
child: Row(
|
||||||
child: Row(
|
children: <Widget>[
|
||||||
children: <Widget>[
|
Icon(
|
||||||
Icon(
|
Icons.rss_feed,
|
||||||
Icons.rss_feed,
|
color: Theme.of(context)
|
||||||
color: Theme.of(context)
|
.tabBarTheme
|
||||||
.tabBarTheme
|
.labelColor,
|
||||||
.labelColor,
|
),
|
||||||
),
|
Padding(
|
||||||
Padding(
|
padding: EdgeInsets.symmetric(
|
||||||
padding: EdgeInsets.symmetric(
|
horizontal: 5.0),
|
||||||
horizontal: 5.0),
|
),
|
||||||
),
|
Text('View Rss Feed'),
|
||||||
Text('View Rss Feed'),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
|
||||||
onSelected: (url) {
|
|
||||||
_launchUrl(url);
|
|
||||||
},
|
|
||||||
)
|
|
||||||
],
|
|
||||||
elevation: 0,
|
|
||||||
iconTheme: IconThemeData(color: Colors.white),
|
|
||||||
expandedHeight: 170,
|
|
||||||
backgroundColor: _color,
|
|
||||||
floating: true,
|
|
||||||
pinned: true,
|
|
||||||
flexibleSpace: LayoutBuilder(builder:
|
|
||||||
(BuildContext context,
|
|
||||||
BoxConstraints constraints) {
|
|
||||||
top = constraints.biggest.height;
|
|
||||||
return FlexibleSpaceBar(
|
|
||||||
background: Stack(
|
|
||||||
children: <Widget>[
|
|
||||||
Container(
|
|
||||||
margin: EdgeInsets.only(top: 120),
|
|
||||||
padding: EdgeInsets.only(
|
|
||||||
left: 80, right: 120),
|
|
||||||
color: Colors.white10,
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment:
|
|
||||||
MainAxisAlignment.start,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment:
|
|
||||||
CrossAxisAlignment.start,
|
|
||||||
children: <Widget>[
|
|
||||||
Text(
|
|
||||||
widget.podcastLocal.author ??
|
|
||||||
'',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.white)),
|
|
||||||
widget.podcastLocal.provider
|
|
||||||
.isNotEmpty
|
|
||||||
? Text(
|
|
||||||
'Hosted on ' +
|
|
||||||
widget.podcastLocal
|
|
||||||
.provider,
|
|
||||||
maxLines: 1,
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.white),
|
|
||||||
)
|
|
||||||
: Center(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
alignment: Alignment.centerRight,
|
|
||||||
padding: EdgeInsets.only(right: 10),
|
|
||||||
child: SizedBox(
|
|
||||||
height: 120,
|
|
||||||
child: Image.file(File(
|
|
||||||
"${widget.podcastLocal.imagePath}")),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
alignment: Alignment.center,
|
|
||||||
child: podcastInfo(context),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
onSelected: (url) {
|
||||||
title: top < 70
|
_launchUrl(url);
|
||||||
? Text(widget.podcastLocal.title,
|
},
|
||||||
maxLines: 1,
|
)
|
||||||
overflow: TextOverflow.ellipsis,
|
],
|
||||||
style: TextStyle(color: Colors.white))
|
elevation: 0,
|
||||||
: Center(),
|
iconTheme: IconThemeData(
|
||||||
);
|
color: Colors.white,
|
||||||
}),
|
),
|
||||||
),
|
expandedHeight:
|
||||||
SliverList(
|
150 + MediaQuery.of(context).padding.top,
|
||||||
delegate: SliverChildBuilderDelegate(
|
backgroundColor: _color,
|
||||||
(BuildContext context, int index) {
|
floating: true,
|
||||||
return hostsList(context, hosts);
|
pinned: true,
|
||||||
},
|
flexibleSpace: LayoutBuilder(builder:
|
||||||
childCount: 1,
|
(BuildContext context,
|
||||||
|
BoxConstraints constraints) {
|
||||||
|
top = constraints.biggest.height;
|
||||||
|
return FlexibleSpaceBar(
|
||||||
|
background: Stack(
|
||||||
|
children: <Widget>[
|
||||||
|
Container(
|
||||||
|
margin: EdgeInsets.only(
|
||||||
|
top: 120 +
|
||||||
|
MediaQuery.of(context)
|
||||||
|
.padding
|
||||||
|
.top),
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
left: 80, right: 120),
|
||||||
|
color: Colors.white10,
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment:
|
||||||
|
MainAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment:
|
||||||
|
CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
Text(
|
||||||
|
widget.podcastLocal.author ??
|
||||||
|
'',
|
||||||
|
maxLines: 1,
|
||||||
|
overflow:
|
||||||
|
TextOverflow.ellipsis,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white)),
|
||||||
|
widget.podcastLocal.provider
|
||||||
|
.isNotEmpty
|
||||||
|
? Text(
|
||||||
|
'Hosted on ' +
|
||||||
|
widget.podcastLocal
|
||||||
|
.provider,
|
||||||
|
maxLines: 1,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white),
|
||||||
|
)
|
||||||
|
: Center(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
padding: EdgeInsets.only(right: 10),
|
||||||
|
child: SizedBox(
|
||||||
|
height: 120,
|
||||||
|
child: Image.file(File(
|
||||||
|
"${widget.podcastLocal.imagePath}")),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: podcastInfo(context),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
title: top <
|
||||||
|
70 +
|
||||||
|
MediaQuery.of(context)
|
||||||
|
.padding
|
||||||
|
.top
|
||||||
|
? Text(widget.podcastLocal.title,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style:
|
||||||
|
TextStyle(color: Colors.white))
|
||||||
|
: Center(),
|
||||||
|
);
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
),
|
SliverList(
|
||||||
SliverPadding(
|
delegate: SliverChildBuilderDelegate(
|
||||||
padding: const EdgeInsets.symmetric(
|
(BuildContext context, int index) {
|
||||||
horizontal: 10.0),
|
return hostsList(context, hosts);
|
||||||
sliver: EpisodeGrid(
|
},
|
||||||
podcast: snapshot.data,
|
childCount: 1,
|
||||||
showDownload: false,
|
),
|
||||||
showFavorite: true,
|
),
|
||||||
showNumber: true,
|
SliverPadding(
|
||||||
heroTag: 'podcast',
|
padding: const EdgeInsets.symmetric(
|
||||||
)),
|
horizontal: 10.0),
|
||||||
],
|
sliver: EpisodeGrid(
|
||||||
)
|
podcast: snapshot.data,
|
||||||
: Center(child: CircularProgressIndicator());
|
showDownload: false,
|
||||||
},
|
showFavorite: true,
|
||||||
),
|
showNumber: true,
|
||||||
Container(child: PlayerWidget()),
|
heroTag: 'podcast',
|
||||||
],
|
updateCount: widget.podcastLocal.upateCount,
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: Center(child: CircularProgressIndicator());
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Container(child: PlayerWidget()),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -29,35 +29,31 @@ class _PodcastGroupListState extends State<PodcastGroupList> {
|
|||||||
)
|
)
|
||||||
: Container(
|
: Container(
|
||||||
color: Theme.of(context).primaryColor,
|
color: Theme.of(context).primaryColor,
|
||||||
child: Stack(
|
child: ReorderableListView(
|
||||||
children: <Widget>[
|
onReorder: (int oldIndex, int newIndex) {
|
||||||
ReorderableListView(
|
setState(() {
|
||||||
onReorder: (int oldIndex, int newIndex) {
|
if (newIndex > oldIndex) {
|
||||||
setState(() {
|
newIndex -= 1;
|
||||||
if (newIndex > oldIndex) {
|
}
|
||||||
newIndex -= 1;
|
final PodcastLocal podcast =
|
||||||
}
|
widget.group.podcasts.removeAt(oldIndex);
|
||||||
final PodcastLocal podcast =
|
widget.group.podcasts.insert(newIndex, podcast);
|
||||||
widget.group.podcasts.removeAt(oldIndex);
|
});
|
||||||
widget.group.podcasts.insert(newIndex, podcast);
|
widget.group.setOrderedPodcasts = widget.group.podcasts;
|
||||||
});
|
groupList.addToOrderChanged(widget.group.name);
|
||||||
widget.group.setOrderedPodcasts = widget.group.podcasts;
|
},
|
||||||
groupList.addToOrderChanged(widget.group.name);
|
children: widget.group.podcasts
|
||||||
},
|
.map<Widget>((PodcastLocal podcastLocal) {
|
||||||
children: widget.group.podcasts
|
return Container(
|
||||||
.map<Widget>((PodcastLocal podcastLocal) {
|
decoration:
|
||||||
return Container(
|
BoxDecoration(color: Theme.of(context).primaryColor),
|
||||||
decoration:
|
key: ObjectKey(podcastLocal.title),
|
||||||
BoxDecoration(color: Theme.of(context).primaryColor),
|
child: PodcastCard(
|
||||||
key: ObjectKey(podcastLocal.title),
|
podcastLocal: podcastLocal,
|
||||||
child: PodcastCard(
|
group: widget.group,
|
||||||
podcastLocal: podcastLocal,
|
),
|
||||||
group: widget.group,
|
);
|
||||||
),
|
}).toList(),
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -393,89 +389,88 @@ class _RenameGroupState extends State<RenameGroup> {
|
|||||||
var groupList = Provider.of<GroupList>(context, listen: false);
|
var groupList = Provider.of<GroupList>(context, listen: false);
|
||||||
List list = groupList.groups.map((e) => e.name).toList();
|
List list = groupList.groups.map((e) => e.name).toList();
|
||||||
return AnnotatedRegion<SystemUiOverlayStyle>(
|
return AnnotatedRegion<SystemUiOverlayStyle>(
|
||||||
value: SystemUiOverlayStyle(
|
value: SystemUiOverlayStyle(
|
||||||
statusBarIconBrightness: Brightness.light,
|
statusBarIconBrightness: Brightness.light,
|
||||||
systemNavigationBarColor:
|
systemNavigationBarColor:
|
||||||
Theme.of(context).brightness == Brightness.light
|
Theme.of(context).brightness == Brightness.light
|
||||||
? Color.fromRGBO(113, 113, 113, 1)
|
? Color.fromRGBO(113, 113, 113, 1)
|
||||||
: Color.fromRGBO(5, 5, 5, 1),
|
: Color.fromRGBO(5, 5, 5, 1),
|
||||||
statusBarColor: Theme.of(context).brightness == Brightness.light
|
// statusBarColor: Theme.of(context).brightness == Brightness.light
|
||||||
? Color.fromRGBO(113, 113, 113, 1)
|
// ? Color.fromRGBO(113, 113, 113, 1)
|
||||||
: Color.fromRGBO(15, 15, 15, 1),
|
// : Color.fromRGBO(15, 15, 15, 1),
|
||||||
),
|
),
|
||||||
child: SafeArea(
|
child: AlertDialog(
|
||||||
child: AlertDialog(
|
shape: RoundedRectangleBorder(
|
||||||
shape: RoundedRectangleBorder(
|
borderRadius: BorderRadius.all(Radius.circular(10))),
|
||||||
borderRadius: BorderRadius.all(Radius.circular(10))),
|
elevation: 1,
|
||||||
elevation: 1,
|
contentPadding: EdgeInsets.symmetric(horizontal: 20),
|
||||||
contentPadding: EdgeInsets.symmetric(horizontal: 20),
|
titlePadding:
|
||||||
titlePadding:
|
EdgeInsets.only(top: 20, left: 20, right: 200, bottom: 20),
|
||||||
EdgeInsets.only(top: 20, left: 20, right: 200, bottom: 20),
|
actionsPadding: EdgeInsets.all(0),
|
||||||
actionsPadding: EdgeInsets.all(0),
|
actions: <Widget>[
|
||||||
actions: <Widget>[
|
FlatButton(
|
||||||
FlatButton(
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
child: Text(
|
||||||
child: Text(
|
'CANCEL',
|
||||||
'CANCEL',
|
style: TextStyle(color: Colors.grey[600]),
|
||||||
style: TextStyle(color: Colors.grey[600]),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
FlatButton(
|
|
||||||
onPressed: () async {
|
|
||||||
if (list.contains(_newName)) {
|
|
||||||
setState(() => _error = 1);
|
|
||||||
} else {
|
|
||||||
PodcastGroup newGroup = PodcastGroup(_newName,
|
|
||||||
color: widget.group.color,
|
|
||||||
id: widget.group.id,
|
|
||||||
podcastList: widget.group.podcastList);
|
|
||||||
groupList.updateGroup(newGroup);
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: Text('DONE',
|
|
||||||
style: TextStyle(color: Theme.of(context).accentColor)),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
title: Text('Edit group name'),
|
|
||||||
content: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: <Widget>[
|
|
||||||
TextField(
|
|
||||||
decoration: InputDecoration(
|
|
||||||
contentPadding: EdgeInsets.symmetric(horizontal: 10),
|
|
||||||
hintText: widget.group.name,
|
|
||||||
hintStyle: TextStyle(fontSize: 18),
|
|
||||||
filled: true,
|
|
||||||
focusedBorder: UnderlineInputBorder(
|
|
||||||
borderSide: BorderSide(
|
|
||||||
color: Theme.of(context).accentColor, width: 2.0),
|
|
||||||
),
|
|
||||||
enabledBorder: UnderlineInputBorder(
|
|
||||||
borderSide: BorderSide(
|
|
||||||
color: Theme.of(context).accentColor, width: 2.0),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
cursorRadius: Radius.circular(2),
|
|
||||||
autofocus: true,
|
|
||||||
maxLines: 1,
|
|
||||||
controller: _controller,
|
|
||||||
onChanged: (value) {
|
|
||||||
_newName = value;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: (_error == 1)
|
|
||||||
? Text(
|
|
||||||
'Group existed',
|
|
||||||
style: TextStyle(color: Colors.red[400]),
|
|
||||||
)
|
|
||||||
: Center(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
));
|
FlatButton(
|
||||||
|
onPressed: () async {
|
||||||
|
if (list.contains(_newName)) {
|
||||||
|
setState(() => _error = 1);
|
||||||
|
} else {
|
||||||
|
PodcastGroup newGroup = PodcastGroup(_newName,
|
||||||
|
color: widget.group.color,
|
||||||
|
id: widget.group.id,
|
||||||
|
podcastList: widget.group.podcastList);
|
||||||
|
groupList.updateGroup(newGroup);
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Text('DONE',
|
||||||
|
style: TextStyle(color: Theme.of(context).accentColor)),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
title: Text('Edit group name'),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
TextField(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
contentPadding: EdgeInsets.symmetric(horizontal: 10),
|
||||||
|
hintText: widget.group.name,
|
||||||
|
hintStyle: TextStyle(fontSize: 18),
|
||||||
|
filled: true,
|
||||||
|
focusedBorder: UnderlineInputBorder(
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: Theme.of(context).accentColor, width: 2.0),
|
||||||
|
),
|
||||||
|
enabledBorder: UnderlineInputBorder(
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: Theme.of(context).accentColor, width: 2.0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
cursorRadius: Radius.circular(2),
|
||||||
|
autofocus: true,
|
||||||
|
maxLines: 1,
|
||||||
|
controller: _controller,
|
||||||
|
onChanged: (value) {
|
||||||
|
_newName = value;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: (_error == 1)
|
||||||
|
? Text(
|
||||||
|
'Group existed',
|
||||||
|
style: TextStyle(color: Colors.red[400]),
|
||||||
|
)
|
||||||
|
: Center(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -99,15 +99,16 @@ class _PodcastListState extends State<PodcastList> {
|
|||||||
value: SystemUiOverlayStyle(
|
value: SystemUiOverlayStyle(
|
||||||
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
|
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
|
||||||
systemNavigationBarColor: Theme.of(context).primaryColor,
|
systemNavigationBarColor: Theme.of(context).primaryColor,
|
||||||
statusBarColor: Theme.of(context).primaryColor,
|
systemNavigationBarIconBrightness:
|
||||||
|
Theme.of(context).accentColorBrightness,
|
||||||
),
|
),
|
||||||
child: SafeArea(
|
child: Scaffold(
|
||||||
child: Scaffold(
|
appBar: AppBar(
|
||||||
appBar: AppBar(
|
title: Text('Podcasts'),
|
||||||
title: Text('Podcasts'),
|
centerTitle: true,
|
||||||
centerTitle: true,
|
),
|
||||||
),
|
body: SafeArea(
|
||||||
body: Container(
|
child: Container(
|
||||||
color: Theme.of(context).primaryColor,
|
color: Theme.of(context).primaryColor,
|
||||||
child: FutureBuilder<List<PodcastLocal>>(
|
child: FutureBuilder<List<PodcastLocal>>(
|
||||||
future: getPodcastLocal(),
|
future: getPodcastLocal(),
|
||||||
@ -161,18 +162,16 @@ class _PodcastListState extends State<PodcastList> {
|
|||||||
113, 113, 113, 1)
|
113, 113, 113, 1)
|
||||||
: Color.fromRGBO(
|
: Color.fromRGBO(
|
||||||
15, 15, 15, 1),
|
15, 15, 15, 1),
|
||||||
statusBarColor: Theme.of(context)
|
// statusBarColor: Theme.of(context)
|
||||||
.brightness ==
|
// .brightness ==
|
||||||
Brightness.light
|
// Brightness.light
|
||||||
? Color.fromRGBO(
|
// ? Color.fromRGBO(
|
||||||
113, 113, 113, 1)
|
// 113, 113, 113, 1)
|
||||||
: Color.fromRGBO(5, 5, 5, 1),
|
// : Color.fromRGBO(5, 5, 5, 1),
|
||||||
),
|
|
||||||
child: SafeArea(
|
|
||||||
child: AboutPodcast(
|
|
||||||
podcastLocal:
|
|
||||||
snapshot.data[index]),
|
|
||||||
),
|
),
|
||||||
|
child: AboutPodcast(
|
||||||
|
podcastLocal:
|
||||||
|
snapshot.data[index]),
|
||||||
));
|
));
|
||||||
},
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
|
@ -137,330 +137,322 @@ class _PodcastManageState extends State<PodcastManage>
|
|||||||
value: SystemUiOverlayStyle(
|
value: SystemUiOverlayStyle(
|
||||||
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
|
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
|
||||||
systemNavigationBarColor: Theme.of(context).primaryColor,
|
systemNavigationBarColor: Theme.of(context).primaryColor,
|
||||||
statusBarColor: Theme.of(context).primaryColor,
|
systemNavigationBarIconBrightness:
|
||||||
|
Theme.of(context).accentColorBrightness,
|
||||||
|
// statusBarColor: Theme.of(context).primaryColor,
|
||||||
),
|
),
|
||||||
child: SafeArea(
|
child: Scaffold(
|
||||||
child: Scaffold(
|
appBar: AppBar(
|
||||||
appBar: AppBar(
|
centerTitle: true,
|
||||||
centerTitle: true,
|
title: Text('Groups'),
|
||||||
title: Text('Groups'),
|
actions: <Widget>[
|
||||||
actions: <Widget>[
|
IconButton(
|
||||||
IconButton(
|
onPressed: () => showGeneralDialog(
|
||||||
onPressed: () => showGeneralDialog(
|
context: context,
|
||||||
context: context,
|
barrierDismissible: true,
|
||||||
barrierDismissible: true,
|
barrierLabel: MaterialLocalizations.of(context)
|
||||||
barrierLabel: MaterialLocalizations.of(context)
|
.modalBarrierDismissLabel,
|
||||||
.modalBarrierDismissLabel,
|
barrierColor: Colors.black54,
|
||||||
barrierColor: Colors.black54,
|
transitionDuration: const Duration(milliseconds: 200),
|
||||||
transitionDuration: const Duration(milliseconds: 200),
|
pageBuilder: (BuildContext context, Animation animaiton,
|
||||||
pageBuilder: (BuildContext context, Animation animaiton,
|
Animation secondaryAnimation) =>
|
||||||
Animation secondaryAnimation) =>
|
AddGroup()),
|
||||||
AddGroup()),
|
icon: Icon(Icons.add)),
|
||||||
icon: Icon(Icons.add)),
|
OrderMenu(),
|
||||||
OrderMenu(),
|
],
|
||||||
],
|
),
|
||||||
),
|
body: Consumer<GroupList>(builder: (_, groupList, __) {
|
||||||
body: Consumer<GroupList>(builder: (_, groupList, __) {
|
bool _isLoading = groupList.isLoading;
|
||||||
bool _isLoading = groupList.isLoading;
|
List<PodcastGroup> _groups = groupList.groups;
|
||||||
List<PodcastGroup> _groups = groupList.groups;
|
return _isLoading
|
||||||
return _isLoading
|
? Center()
|
||||||
? Center()
|
: Stack(
|
||||||
: Stack(
|
children: <Widget>[
|
||||||
children: <Widget>[
|
CustomTabView(
|
||||||
CustomTabView(
|
itemCount: _groups.length,
|
||||||
itemCount: _groups.length,
|
tabBuilder: (context, index) => Tab(
|
||||||
tabBuilder: (context, index) => Tab(
|
child: Container(
|
||||||
child: Container(
|
height: 30.0,
|
||||||
height: 30.0,
|
padding: EdgeInsets.symmetric(horizontal: 10.0),
|
||||||
padding: EdgeInsets.symmetric(horizontal: 10.0),
|
alignment: Alignment.center,
|
||||||
alignment: Alignment.center,
|
decoration: BoxDecoration(
|
||||||
decoration: BoxDecoration(
|
color: (_scroll - index).abs() > 1
|
||||||
color: (_scroll - index).abs() > 1
|
? Colors.grey[300]
|
||||||
? Colors.grey[300]
|
: Colors.grey[300]
|
||||||
: Colors.grey[300]
|
.withOpacity((_scroll - index).abs()),
|
||||||
.withOpacity((_scroll - index).abs()),
|
borderRadius:
|
||||||
borderRadius:
|
BorderRadius.all(Radius.circular(15)),
|
||||||
BorderRadius.all(Radius.circular(15)),
|
),
|
||||||
),
|
child: Text(
|
||||||
child: Text(
|
_groups[index].name,
|
||||||
_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
|
pageBuilder: (context, index) => Container(
|
||||||
? Positioned.fill(
|
key: ObjectKey(_groups[index].name),
|
||||||
child: GestureDetector(
|
child: PodcastGroupList(group: _groups[index])),
|
||||||
onTap: () async {
|
onPositionChange: (value) =>
|
||||||
await _menuController.reverse();
|
setState(() => _index = value),
|
||||||
setState(() => _showSetting = false);
|
onScroll: (value) => setState(() => _scroll = value),
|
||||||
},
|
),
|
||||||
child: Container(
|
_showSetting
|
||||||
color: Theme.of(context)
|
? Positioned.fill(
|
||||||
.scaffoldBackgroundColor
|
top: 50,
|
||||||
.withOpacity(0.5 * _menuController.value),
|
child: GestureDetector(
|
||||||
),
|
onTap: () async {
|
||||||
),
|
await _menuController.reverse();
|
||||||
)
|
setState(() => _showSetting = false);
|
||||||
: Center(),
|
},
|
||||||
Positioned(
|
|
||||||
right: 30,
|
|
||||||
bottom: 30,
|
|
||||||
child: _saveButton(context),
|
|
||||||
),
|
|
||||||
_showSetting
|
|
||||||
? Positioned(
|
|
||||||
right: 30 * _menuValue,
|
|
||||||
bottom: 100,
|
|
||||||
child: Container(
|
child: Container(
|
||||||
alignment: Alignment.centerRight,
|
color: Theme.of(context)
|
||||||
child: Column(
|
.scaffoldBackgroundColor
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
.withOpacity(0.5 * _menuController.value),
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
),
|
||||||
children: <Widget>[
|
),
|
||||||
Material(
|
)
|
||||||
color: Colors.transparent,
|
: Center(),
|
||||||
child: InkWell(
|
Positioned(
|
||||||
onTap: () {
|
right: 30,
|
||||||
_menuController.reverse();
|
bottom: 30,
|
||||||
setState(() => _showSetting = false);
|
child: _saveButton(context),
|
||||||
_index == 0
|
),
|
||||||
? Fluttertoast.showToast(
|
_showSetting
|
||||||
msg:
|
? Positioned(
|
||||||
'Home group is not supported',
|
right: 30 * _menuValue,
|
||||||
gravity: ToastGravity.BOTTOM,
|
bottom: 100,
|
||||||
)
|
child: Container(
|
||||||
: showGeneralDialog(
|
alignment: Alignment.centerRight,
|
||||||
context: context,
|
child: Column(
|
||||||
barrierDismissible: true,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
barrierLabel:
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
MaterialLocalizations.of(
|
children: <Widget>[
|
||||||
context)
|
Material(
|
||||||
.modalBarrierDismissLabel,
|
color: Colors.transparent,
|
||||||
barrierColor: Colors.black54,
|
child: InkWell(
|
||||||
transitionDuration:
|
onTap: () {
|
||||||
const Duration(
|
_menuController.reverse();
|
||||||
milliseconds: 300),
|
setState(() => _showSetting = false);
|
||||||
pageBuilder: (BuildContext
|
_index == 0
|
||||||
context,
|
? Fluttertoast.showToast(
|
||||||
Animation animaiton,
|
msg:
|
||||||
Animation
|
'Home group is not supported',
|
||||||
secondaryAnimation) =>
|
gravity: ToastGravity.BOTTOM,
|
||||||
RenameGroup(
|
)
|
||||||
group: _groups[_index],
|
: showGeneralDialog(
|
||||||
));
|
context: context,
|
||||||
},
|
barrierDismissible: true,
|
||||||
child: Container(
|
barrierLabel:
|
||||||
height: 30.0,
|
MaterialLocalizations.of(
|
||||||
decoration: BoxDecoration(
|
context)
|
||||||
color: Colors.grey[700],
|
.modalBarrierDismissLabel,
|
||||||
borderRadius: BorderRadius.all(
|
barrierColor: Colors.black54,
|
||||||
Radius.circular(10.0))),
|
transitionDuration:
|
||||||
padding: EdgeInsets.symmetric(
|
const Duration(
|
||||||
horizontal: 10),
|
milliseconds: 300),
|
||||||
child: Row(
|
pageBuilder: (BuildContext
|
||||||
children: <Widget>[
|
context,
|
||||||
Icon(
|
Animation animaiton,
|
||||||
Icons.text_fields,
|
Animation
|
||||||
color: Colors.white,
|
secondaryAnimation) =>
|
||||||
size: 15.0,
|
RenameGroup(
|
||||||
),
|
group: _groups[_index],
|
||||||
Padding(
|
));
|
||||||
padding: EdgeInsets.symmetric(
|
},
|
||||||
horizontal: 5.0),
|
child: Container(
|
||||||
),
|
height: 30.0,
|
||||||
Text('Edit Name',
|
decoration: BoxDecoration(
|
||||||
style: TextStyle(
|
color: Colors.grey[700],
|
||||||
color: Colors.white)),
|
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(
|
Padding(
|
||||||
vertical: 10.0)),
|
padding:
|
||||||
Material(
|
EdgeInsets.symmetric(vertical: 10.0)),
|
||||||
color: Colors.transparent,
|
Material(
|
||||||
child: InkWell(
|
color: Colors.transparent,
|
||||||
onTap: () {
|
child: InkWell(
|
||||||
_menuController.reverse();
|
onTap: () {
|
||||||
setState(() => _showSetting = false);
|
_menuController.reverse();
|
||||||
_index == 0
|
setState(() => _showSetting = false);
|
||||||
? Fluttertoast.showToast(
|
_index == 0
|
||||||
msg:
|
? Fluttertoast.showToast(
|
||||||
'Home group is not supported',
|
msg:
|
||||||
gravity: ToastGravity.BOTTOM,
|
'Home group is not supported',
|
||||||
)
|
gravity: ToastGravity.BOTTOM,
|
||||||
: showGeneralDialog(
|
)
|
||||||
context: context,
|
: showGeneralDialog(
|
||||||
barrierDismissible: true,
|
context: context,
|
||||||
barrierLabel:
|
barrierDismissible: true,
|
||||||
MaterialLocalizations.of(
|
barrierLabel:
|
||||||
context)
|
MaterialLocalizations.of(
|
||||||
.modalBarrierDismissLabel,
|
context)
|
||||||
barrierColor: Colors.black54,
|
.modalBarrierDismissLabel,
|
||||||
transitionDuration:
|
barrierColor: Colors.black54,
|
||||||
const Duration(
|
transitionDuration:
|
||||||
milliseconds: 300),
|
const Duration(
|
||||||
pageBuilder: (BuildContext
|
milliseconds: 300),
|
||||||
context,
|
pageBuilder: (BuildContext
|
||||||
Animation animaiton,
|
context,
|
||||||
Animation
|
Animation animaiton,
|
||||||
secondaryAnimation) =>
|
Animation
|
||||||
AnnotatedRegion<
|
secondaryAnimation) =>
|
||||||
SystemUiOverlayStyle>(
|
AnnotatedRegion<
|
||||||
value:
|
SystemUiOverlayStyle>(
|
||||||
SystemUiOverlayStyle(
|
value:
|
||||||
statusBarIconBrightness:
|
SystemUiOverlayStyle(
|
||||||
Brightness.light,
|
statusBarIconBrightness:
|
||||||
systemNavigationBarColor:
|
Brightness.light,
|
||||||
Theme.of(context)
|
systemNavigationBarColor:
|
||||||
.brightness ==
|
Theme.of(context)
|
||||||
Brightness
|
.brightness ==
|
||||||
.light
|
Brightness
|
||||||
? Color
|
.light
|
||||||
.fromRGBO(
|
? Color
|
||||||
113,
|
.fromRGBO(
|
||||||
113,
|
113,
|
||||||
113,
|
113,
|
||||||
1)
|
113,
|
||||||
: Color
|
1)
|
||||||
.fromRGBO(
|
: Color
|
||||||
15,
|
.fromRGBO(
|
||||||
15,
|
15,
|
||||||
15,
|
15,
|
||||||
1),
|
15,
|
||||||
statusBarColor:
|
1),
|
||||||
Theme.of(context)
|
// statusBarColor: Theme.of(
|
||||||
.brightness ==
|
// context)
|
||||||
Brightness
|
// .brightness ==
|
||||||
.light
|
// Brightness.light
|
||||||
? Color
|
// ? Color.fromRGBO(
|
||||||
.fromRGBO(
|
// 113,
|
||||||
113,
|
// 113,
|
||||||
113,
|
// 113,
|
||||||
113,
|
// 1)
|
||||||
1)
|
// : Color.fromRGBO(
|
||||||
: Color
|
// 5, 5, 5, 1),
|
||||||
.fromRGBO(
|
),
|
||||||
5,
|
child: AlertDialog(
|
||||||
5,
|
elevation: 1,
|
||||||
5,
|
shape: RoundedRectangleBorder(
|
||||||
1),
|
borderRadius:
|
||||||
),
|
BorderRadius.all(
|
||||||
child: SafeArea(
|
Radius.circular(
|
||||||
child: AlertDialog(
|
10.0))),
|
||||||
elevation: 1,
|
titlePadding:
|
||||||
shape: RoundedRectangleBorder(
|
EdgeInsets.only(
|
||||||
borderRadius: BorderRadius
|
top: 20,
|
||||||
.all(Radius
|
left: 20,
|
||||||
.circular(
|
right: 200,
|
||||||
10.0))),
|
bottom: 20),
|
||||||
titlePadding:
|
title: Text(
|
||||||
EdgeInsets.only(
|
'Delete confirm'),
|
||||||
top: 20,
|
content: Text(
|
||||||
left: 20,
|
'Are you sure you want to delete this group? Podcasts will be moved to Home group.'),
|
||||||
right: 200,
|
actions: <Widget>[
|
||||||
bottom: 20),
|
FlatButton(
|
||||||
title: Text(
|
onPressed: () =>
|
||||||
'Delete confirm'),
|
Navigator.of(
|
||||||
content: Text(
|
context)
|
||||||
'Are you sure you want to delete this group? Podcasts will be moved to Home group.'),
|
.pop(),
|
||||||
actions: <Widget>[
|
child: Text(
|
||||||
FlatButton(
|
'CANCEL',
|
||||||
onPressed: () =>
|
style: TextStyle(
|
||||||
Navigator.of(
|
color: Colors
|
||||||
context)
|
.grey[
|
||||||
.pop(),
|
600]),
|
||||||
child: Text(
|
),
|
||||||
'CANCEL',
|
),
|
||||||
style: TextStyle(
|
FlatButton(
|
||||||
color: Colors
|
onPressed: () {
|
||||||
.grey[
|
if (_index ==
|
||||||
600]),
|
groupList
|
||||||
),
|
.groups
|
||||||
),
|
.length -
|
||||||
FlatButton(
|
1) {
|
||||||
onPressed: () {
|
setState(() {
|
||||||
if (_index ==
|
_index =
|
||||||
groupList
|
_index -
|
||||||
.groups
|
1;
|
||||||
.length -
|
_scroll = 0;
|
||||||
1) {
|
});
|
||||||
setState(
|
groupList.delGroup(
|
||||||
() {
|
_groups[
|
||||||
_index =
|
|
||||||
_index -
|
|
||||||
1;
|
|
||||||
_scroll =
|
|
||||||
0;
|
|
||||||
});
|
|
||||||
groupList.delGroup(_groups[
|
|
||||||
_index +
|
_index +
|
||||||
1]);
|
1]);
|
||||||
} else {
|
} else {
|
||||||
groupList.delGroup(
|
groupList.delGroup(
|
||||||
_groups[
|
_groups[
|
||||||
_index]);
|
_index]);
|
||||||
}
|
}
|
||||||
Navigator.of(
|
Navigator.of(
|
||||||
context)
|
context)
|
||||||
.pop();
|
.pop();
|
||||||
},
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
'CONFIRM',
|
'CONFIRM',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors
|
color: Colors
|
||||||
.red),
|
.red),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
));
|
||||||
));
|
},
|
||||||
},
|
child: Container(
|
||||||
child: Container(
|
height: 30,
|
||||||
height: 30,
|
decoration: BoxDecoration(
|
||||||
decoration: BoxDecoration(
|
color: Colors.grey[700],
|
||||||
color: Colors.grey[700],
|
borderRadius: BorderRadius.all(
|
||||||
borderRadius: BorderRadius.all(
|
Radius.circular(10.0))),
|
||||||
Radius.circular(10.0))),
|
padding: EdgeInsets.symmetric(
|
||||||
padding: EdgeInsets.symmetric(
|
horizontal: 10),
|
||||||
horizontal: 10),
|
child: Row(
|
||||||
child: Row(
|
children: <Widget>[
|
||||||
children: <Widget>[
|
Icon(
|
||||||
Icon(
|
Icons.delete_outline,
|
||||||
Icons.delete_outline,
|
color: Colors.red,
|
||||||
color: Colors.red,
|
size: 15.0,
|
||||||
size: 15.0,
|
),
|
||||||
),
|
Padding(
|
||||||
Padding(
|
padding: EdgeInsets.symmetric(
|
||||||
padding: EdgeInsets.symmetric(
|
horizontal: 5.0),
|
||||||
horizontal: 5.0),
|
),
|
||||||
),
|
Text('Delete',
|
||||||
Text('Delete',
|
style: TextStyle(
|
||||||
style: TextStyle(
|
color: Colors.red)),
|
||||||
color: Colors.red)),
|
],
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
: Center(),
|
)
|
||||||
],
|
: Center(),
|
||||||
);
|
],
|
||||||
}),
|
);
|
||||||
),
|
}),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -525,85 +517,84 @@ class _AddGroupState extends State<AddGroup> {
|
|||||||
var groupList = Provider.of<GroupList>(context);
|
var groupList = Provider.of<GroupList>(context);
|
||||||
List list = groupList.groups.map((e) => e.name).toList();
|
List list = groupList.groups.map((e) => e.name).toList();
|
||||||
return AnnotatedRegion<SystemUiOverlayStyle>(
|
return AnnotatedRegion<SystemUiOverlayStyle>(
|
||||||
value: SystemUiOverlayStyle(
|
value: SystemUiOverlayStyle(
|
||||||
statusBarIconBrightness: Brightness.light,
|
statusBarIconBrightness: Brightness.light,
|
||||||
systemNavigationBarColor:
|
systemNavigationBarColor:
|
||||||
Theme.of(context).brightness == Brightness.light
|
Theme.of(context).brightness == Brightness.light
|
||||||
? Color.fromRGBO(113, 113, 113, 1)
|
? Color.fromRGBO(113, 113, 113, 1)
|
||||||
: Color.fromRGBO(5, 5, 5, 1),
|
: Color.fromRGBO(5, 5, 5, 1),
|
||||||
statusBarColor: Theme.of(context).brightness == Brightness.light
|
// statusBarColor: Theme.of(context).brightness == Brightness.light
|
||||||
? Color.fromRGBO(113, 113, 113, 1)
|
// ? Color.fromRGBO(113, 113, 113, 1)
|
||||||
: Color.fromRGBO(15, 15, 15, 1),
|
// : Color.fromRGBO(15, 15, 15, 1),
|
||||||
),
|
),
|
||||||
child: SafeArea(
|
child: AlertDialog(
|
||||||
child: AlertDialog(
|
shape: RoundedRectangleBorder(
|
||||||
shape: RoundedRectangleBorder(
|
borderRadius: BorderRadius.all(Radius.circular(10))),
|
||||||
borderRadius: BorderRadius.all(Radius.circular(10))),
|
elevation: 1,
|
||||||
elevation: 1,
|
contentPadding: EdgeInsets.symmetric(horizontal: 20),
|
||||||
contentPadding: EdgeInsets.symmetric(horizontal: 20),
|
titlePadding:
|
||||||
titlePadding:
|
EdgeInsets.only(top: 20, left: 20, right: 200, bottom: 20),
|
||||||
EdgeInsets.only(top: 20, left: 20, right: 200, bottom: 20),
|
actionsPadding: EdgeInsets.all(0),
|
||||||
actionsPadding: EdgeInsets.all(0),
|
actions: <Widget>[
|
||||||
actions: <Widget>[
|
FlatButton(
|
||||||
FlatButton(
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
child: Text(
|
||||||
child: Text(
|
'CANCEL',
|
||||||
'CANCEL',
|
style: TextStyle(color: Colors.grey[600]),
|
||||||
style: TextStyle(color: Colors.grey[600]),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
FlatButton(
|
|
||||||
onPressed: () async {
|
|
||||||
if (list.contains(_newGroup)) {
|
|
||||||
setState(() => _error = 1);
|
|
||||||
} else {
|
|
||||||
groupList.addGroup(PodcastGroup(_newGroup));
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: Text('DONE',
|
|
||||||
style: TextStyle(color: Theme.of(context).accentColor)),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
title: Text('Create new group'),
|
|
||||||
content: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: <Widget>[
|
|
||||||
TextField(
|
|
||||||
decoration: InputDecoration(
|
|
||||||
contentPadding: EdgeInsets.symmetric(horizontal: 10),
|
|
||||||
hintText: 'New Group',
|
|
||||||
hintStyle: TextStyle(fontSize: 18),
|
|
||||||
filled: true,
|
|
||||||
focusedBorder: UnderlineInputBorder(
|
|
||||||
borderSide: BorderSide(
|
|
||||||
color: Theme.of(context).accentColor, width: 2.0),
|
|
||||||
),
|
|
||||||
enabledBorder: UnderlineInputBorder(
|
|
||||||
borderSide: BorderSide(
|
|
||||||
color: Theme.of(context).accentColor, width: 2.0),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
cursorRadius: Radius.circular(2),
|
|
||||||
autofocus: true,
|
|
||||||
maxLines: 1,
|
|
||||||
controller: _controller,
|
|
||||||
onChanged: (value) {
|
|
||||||
_newGroup = value;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: (_error == 1)
|
|
||||||
? Text(
|
|
||||||
'Group existed',
|
|
||||||
style: TextStyle(color: Colors.red[400]),
|
|
||||||
)
|
|
||||||
: Center(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
));
|
FlatButton(
|
||||||
|
onPressed: () async {
|
||||||
|
if (list.contains(_newGroup)) {
|
||||||
|
setState(() => _error = 1);
|
||||||
|
} else {
|
||||||
|
groupList.addGroup(PodcastGroup(_newGroup));
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Text('DONE',
|
||||||
|
style: TextStyle(color: Theme.of(context).accentColor)),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
title: Text('Create new group'),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
TextField(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
contentPadding: EdgeInsets.symmetric(horizontal: 10),
|
||||||
|
hintText: 'New Group',
|
||||||
|
hintStyle: TextStyle(fontSize: 18),
|
||||||
|
filled: true,
|
||||||
|
focusedBorder: UnderlineInputBorder(
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: Theme.of(context).accentColor, width: 2.0),
|
||||||
|
),
|
||||||
|
enabledBorder: UnderlineInputBorder(
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: Theme.of(context).accentColor, width: 2.0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
cursorRadius: Radius.circular(2),
|
||||||
|
autofocus: true,
|
||||||
|
maxLines: 1,
|
||||||
|
controller: _controller,
|
||||||
|
onChanged: (value) {
|
||||||
|
_newGroup = value;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: (_error == 1)
|
||||||
|
? Text(
|
||||||
|
'Group existed',
|
||||||
|
style: TextStyle(color: Colors.red[400]),
|
||||||
|
)
|
||||||
|
: Center(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -99,17 +99,19 @@ class _DownloadsManageState extends State<DownloadsManage> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AnnotatedRegion<SystemUiOverlayStyle>(
|
return AnnotatedRegion<SystemUiOverlayStyle>(
|
||||||
value: SystemUiOverlayStyle(
|
value: SystemUiOverlayStyle(
|
||||||
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
|
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
|
||||||
systemNavigationBarColor: Theme.of(context).primaryColor,
|
systemNavigationBarColor: Theme.of(context).primaryColor,
|
||||||
statusBarColor: Theme.of(context).primaryColor),
|
systemNavigationBarIconBrightness:
|
||||||
child: SafeArea(
|
Theme.of(context).accentColorBrightness,
|
||||||
child: Scaffold(
|
),
|
||||||
appBar: AppBar(
|
child: Scaffold(
|
||||||
title: Text('Downloads'),
|
appBar: AppBar(
|
||||||
elevation: 0,
|
title: Text('Downloads'),
|
||||||
backgroundColor: Theme.of(context).primaryColor,
|
elevation: 0,
|
||||||
),
|
backgroundColor: Theme.of(context).primaryColor,
|
||||||
body: Stack(
|
),
|
||||||
|
body: SafeArea(
|
||||||
|
child: Stack(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Column(
|
Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
@ -138,7 +140,7 @@ class _DownloadsManageState extends State<DownloadsManage> {
|
|||||||
fontSize: 40,
|
fontSize: 40,
|
||||||
fontWeight: FontWeight.bold)),
|
fontWeight: FontWeight.bold)),
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: ' episodes ',
|
text: _fileNum < 2 ? ' episode' : ' episodes ',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Theme.of(context).accentColor,
|
color: Theme.of(context).accentColor,
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
|
@ -65,74 +65,79 @@ class _PlayedHistoryState extends State<PlayedHistory>
|
|||||||
value: SystemUiOverlayStyle(
|
value: SystemUiOverlayStyle(
|
||||||
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
|
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
|
||||||
systemNavigationBarColor: Theme.of(context).primaryColor,
|
systemNavigationBarColor: Theme.of(context).primaryColor,
|
||||||
statusBarColor: Theme.of(context).primaryColor,
|
systemNavigationBarIconBrightness:
|
||||||
|
Theme.of(context).accentColorBrightness,
|
||||||
|
//statusBarColor: Theme.of(context).primaryColor,
|
||||||
),
|
),
|
||||||
child: SafeArea(
|
child: Scaffold(
|
||||||
child: Scaffold(
|
backgroundColor: Theme.of(context).primaryColor,
|
||||||
body: NestedScrollView(
|
body: SafeArea(
|
||||||
headerSliverBuilder:
|
child: NestedScrollView(
|
||||||
(BuildContext context, bool innerBoxScrolled) {
|
headerSliverBuilder: (BuildContext context, bool innerBoxScrolled) {
|
||||||
return <Widget>[
|
return <Widget>[
|
||||||
SliverAppBar(
|
SliverAppBar(
|
||||||
elevation: 0,
|
backgroundColor: Theme.of(context).primaryColor,
|
||||||
expandedHeight: 260,
|
elevation: 0,
|
||||||
floating: false,
|
expandedHeight: 260,
|
||||||
pinned: true,
|
floating: false,
|
||||||
flexibleSpace: LayoutBuilder(
|
pinned: true,
|
||||||
builder:
|
flexibleSpace: LayoutBuilder(
|
||||||
(BuildContext context, BoxConstraints constraints) {
|
builder:
|
||||||
top = constraints.biggest.height;
|
(BuildContext context, BoxConstraints constraints) {
|
||||||
return FlexibleSpaceBar(
|
top = constraints.biggest.height;
|
||||||
title: top < 70
|
return FlexibleSpaceBar(
|
||||||
? Text(
|
title: top < 70 + MediaQuery.of(context).padding.top
|
||||||
'History',
|
? Text(
|
||||||
)
|
'History',
|
||||||
: Center(),
|
)
|
||||||
background: Padding(
|
: Center(),
|
||||||
padding: EdgeInsets.only(
|
background: Padding(
|
||||||
top: 50, left: 50, right: 50, bottom: 30),
|
padding: EdgeInsets.only(
|
||||||
child: FutureBuilder<List<FlSpot>>(
|
top: 50, left: 50, right: 50, bottom: 30),
|
||||||
future: getData(),
|
child: FutureBuilder<List<FlSpot>>(
|
||||||
builder: (context, snapshot) {
|
future: getData(),
|
||||||
return snapshot.hasData
|
builder: (context, snapshot) {
|
||||||
? HistoryChart(snapshot.data)
|
return snapshot.hasData
|
||||||
: Center();
|
? 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,
|
},
|
||||||
),
|
),
|
||||||
];
|
),
|
||||||
},
|
SliverPersistentHeader(
|
||||||
body: TabBarView(controller: _controller, children: <Widget>[
|
delegate: _SliverAppBarDelegate(
|
||||||
FutureBuilder<List<PlayHistory>>(
|
TabBar(
|
||||||
future: getPlayHistory(),
|
controller: _controller,
|
||||||
builder: (context, snapshot) {
|
tabs: <Widget>[
|
||||||
double _width = MediaQuery.of(context).size.width;
|
Tab(
|
||||||
return snapshot.hasData
|
child: Text('Listen'),
|
||||||
? ListView.builder(
|
),
|
||||||
shrinkWrap: true,
|
Tab(
|
||||||
scrollDirection: Axis.vertical,
|
child: Text('Subscribe'),
|
||||||
itemCount: snapshot.data.length,
|
)
|
||||||
itemBuilder: (BuildContext context, int index) {
|
],
|
||||||
return Column(
|
),
|
||||||
|
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 Container(
|
||||||
|
color: Theme.of(context).scaffoldBackgroundColor,
|
||||||
|
child: Column(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Column(
|
title: Column(
|
||||||
@ -160,12 +165,17 @@ class _PlayedHistoryState extends State<PlayedHistory>
|
|||||||
width: _width,
|
width: _width,
|
||||||
child: Row(
|
child: Row(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Icon(Icons.timelapse, color: Colors.grey[400],),
|
Icon(
|
||||||
|
Icons.timelapse,
|
||||||
|
color: Colors.grey[400],
|
||||||
|
),
|
||||||
Container(
|
Container(
|
||||||
height: 2,
|
height: 2,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border(bottom: BorderSide(color: Colors.grey[400], width: 2.0))
|
border: Border(
|
||||||
),
|
bottom: BorderSide(
|
||||||
|
color: Colors.grey[400],
|
||||||
|
width: 2.0))),
|
||||||
width: _width *
|
width: _width *
|
||||||
snapshot.data[index]
|
snapshot.data[index]
|
||||||
.seekValue <
|
.seekValue <
|
||||||
@ -201,24 +211,27 @@ class _PlayedHistoryState extends State<PlayedHistory>
|
|||||||
),
|
),
|
||||||
// Divider(height: 2),
|
// Divider(height: 2),
|
||||||
],
|
],
|
||||||
);
|
),
|
||||||
})
|
);
|
||||||
: Center(
|
})
|
||||||
child: CircularProgressIndicator(),
|
: Center(
|
||||||
);
|
child: CircularProgressIndicator(),
|
||||||
},
|
);
|
||||||
),
|
},
|
||||||
FutureBuilder<List<SubHistory>>(
|
),
|
||||||
future: getSubHistory(),
|
FutureBuilder<List<SubHistory>>(
|
||||||
builder: (context, snapshot) {
|
future: getSubHistory(),
|
||||||
return snapshot.hasData
|
builder: (context, snapshot) {
|
||||||
? ListView.builder(
|
return snapshot.hasData
|
||||||
shrinkWrap: true,
|
? ListView.builder(
|
||||||
scrollDirection: Axis.vertical,
|
shrinkWrap: true,
|
||||||
itemCount: snapshot.data.length,
|
scrollDirection: Axis.vertical,
|
||||||
itemBuilder: (BuildContext context, int index) {
|
itemCount: snapshot.data.length,
|
||||||
bool _status = snapshot.data[index].status;
|
itemBuilder: (BuildContext context, int index) {
|
||||||
return Column(
|
bool _status = snapshot.data[index].status;
|
||||||
|
return Container(
|
||||||
|
color: Theme.of(context).scaffoldBackgroundColor,
|
||||||
|
child: Column(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
ListTile(
|
ListTile(
|
||||||
enabled: _status,
|
enabled: _status,
|
||||||
@ -274,14 +287,16 @@ class _PlayedHistoryState extends State<PlayedHistory>
|
|||||||
height: 2,
|
height: 2,
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
);
|
),
|
||||||
})
|
);
|
||||||
: Center(
|
})
|
||||||
child: CircularProgressIndicator(),
|
: Center(
|
||||||
);
|
child: CircularProgressIndicator(),
|
||||||
},
|
);
|
||||||
),
|
},
|
||||||
])),
|
),
|
||||||
|
]),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -318,72 +333,74 @@ class HistoryChart extends StatelessWidget {
|
|||||||
HistoryChart(this.stats);
|
HistoryChart(this.stats);
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return LineChart(
|
return SafeArea(
|
||||||
LineChartData(
|
child: LineChart(
|
||||||
backgroundColor: Colors.transparent,
|
LineChartData(
|
||||||
gridData: FlGridData(
|
backgroundColor: Colors.transparent,
|
||||||
show: true,
|
gridData: FlGridData(
|
||||||
drawHorizontalLine: true,
|
show: true,
|
||||||
getDrawingHorizontalLine: (value) {
|
drawHorizontalLine: true,
|
||||||
return value % 60 == 0
|
getDrawingHorizontalLine: (value) {
|
||||||
? FlLine(
|
return value % 60 == 0
|
||||||
color: Theme.of(context).brightness == Brightness.light
|
? FlLine(
|
||||||
? Colors.grey[400]
|
color: Theme.of(context).brightness == Brightness.light
|
||||||
: Colors.grey[700],
|
? Colors.grey[400]
|
||||||
strokeWidth: 1,
|
: Colors.grey[700],
|
||||||
)
|
strokeWidth: 1,
|
||||||
: FlLine(color: Colors.transparent);
|
)
|
||||||
},
|
: 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(
|
titlesData: FlTitlesData(
|
||||||
showTitles: true,
|
show: true,
|
||||||
textStyle: TextStyle(
|
bottomTitles: SideTitles(
|
||||||
color: const Color(0xff67727d),
|
textStyle: TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
color: const Color(0xff67727d),
|
||||||
fontSize: 12,
|
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,
|
||||||
),
|
),
|
||||||
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -4,28 +4,31 @@ import 'package:url_launcher/url_launcher.dart';
|
|||||||
import 'licenses.dart';
|
import 'licenses.dart';
|
||||||
|
|
||||||
class Libries extends StatelessWidget {
|
class Libries extends StatelessWidget {
|
||||||
_launchUrl(String url) async {
|
_launchUrl(String url) async {
|
||||||
if (await canLaunch(url)) {
|
if (await canLaunch(url)) {
|
||||||
await launch(url);
|
await launch(url);
|
||||||
} else {
|
} else {
|
||||||
throw 'Could not launch $url';
|
throw 'Could not launch $url';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AnnotatedRegion<SystemUiOverlayStyle>(
|
return AnnotatedRegion<SystemUiOverlayStyle>(
|
||||||
value: SystemUiOverlayStyle(
|
value: SystemUiOverlayStyle(
|
||||||
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
|
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
|
||||||
systemNavigationBarColor: Theme.of(context).primaryColor,
|
systemNavigationBarColor: Theme.of(context).primaryColor,
|
||||||
statusBarColor: Theme.of(context).primaryColor),
|
systemNavigationBarIconBrightness:
|
||||||
child: SafeArea(
|
Theme.of(context).accentColorBrightness,
|
||||||
child: Scaffold(
|
),
|
||||||
appBar: AppBar(
|
child: Scaffold(
|
||||||
title: Text('Libraies'),
|
appBar: AppBar(
|
||||||
elevation: 0,
|
title: Text('Libraies'),
|
||||||
backgroundColor: Theme.of(context).primaryColor,
|
elevation: 0,
|
||||||
),
|
backgroundColor: Theme.of(context).primaryColor,
|
||||||
body: SingleChildScrollView(
|
),
|
||||||
|
body: SafeArea(
|
||||||
|
child: SingleChildScrollView(
|
||||||
scrollDirection: Axis.vertical,
|
scrollDirection: Axis.vertical,
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
@ -41,4 +41,5 @@ List<Libries> plugins = [
|
|||||||
Libries('audio_service', mit, 'https://pub.dev/packages/audio_service'),
|
Libries('audio_service', mit, 'https://pub.dev/packages/audio_service'),
|
||||||
Libries('just_audio', apacheLicense, 'https://pub.dev/packages/just_audio'),
|
Libries('just_audio', apacheLicense, 'https://pub.dev/packages/just_audio'),
|
||||||
Libries('line_icons', gpl, 'https://pub.dev/packages/line_icons'),
|
Libries('line_icons', gpl, 'https://pub.dev/packages/line_icons'),
|
||||||
|
Libries('flutter_file_dialog', bsd, 'https://pub.dev/packages/flutter_file_dialog')
|
||||||
];
|
];
|
@ -1,15 +1,22 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:workmanager/workmanager.dart';
|
import 'package:path/path.dart';
|
||||||
import 'package:line_icons/line_icons.dart';
|
import 'package:line_icons/line_icons.dart';
|
||||||
|
import 'package:tsacdop/class/podcastlocal.dart';
|
||||||
|
import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:flutter_file_dialog/flutter_file_dialog.dart';
|
||||||
|
|
||||||
import 'package:tsacdop/class/audiostate.dart';
|
import 'package:tsacdop/class/audiostate.dart';
|
||||||
import 'package:tsacdop/class/settingstate.dart';
|
import 'package:tsacdop/util/ompl_build.dart';
|
||||||
import 'package:tsacdop/settings/theme.dart';
|
import 'theme.dart';
|
||||||
import 'package:tsacdop/settings/storage.dart';
|
import 'storage.dart';
|
||||||
import 'package:tsacdop/settings/history.dart';
|
import 'history.dart';
|
||||||
|
import 'syncing.dart';
|
||||||
import 'libries.dart';
|
import 'libries.dart';
|
||||||
|
|
||||||
class Settings extends StatelessWidget {
|
class Settings extends StatelessWidget {
|
||||||
@ -21,24 +28,38 @@ class Settings extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_exportOmpl() async {
|
||||||
|
var dbHelper = DBHelper();
|
||||||
|
List<PodcastLocal> podcastList = await dbHelper.getPodcastLocalAll();
|
||||||
|
var ompl = omplBuilder(podcastList);
|
||||||
|
var tempdir = await getTemporaryDirectory();
|
||||||
|
var file = File(join(tempdir.path, 'tsacdop_ompl.xml'));
|
||||||
|
print(file.path);
|
||||||
|
await file.writeAsString(ompl.toString());
|
||||||
|
final params = SaveFileDialogParams(sourceFilePath: file.path);
|
||||||
|
final filePath = await FlutterFileDialog.saveFile(params: params);
|
||||||
|
print(filePath);
|
||||||
|
print(ompl.toString());
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
|
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
|
||||||
var settings = Provider.of<SettingState>(context, listen: false);
|
|
||||||
return AnnotatedRegion<SystemUiOverlayStyle>(
|
return AnnotatedRegion<SystemUiOverlayStyle>(
|
||||||
value: SystemUiOverlayStyle(
|
value: SystemUiOverlayStyle(
|
||||||
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
|
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
|
||||||
systemNavigationBarColor: Theme.of(context).primaryColor,
|
systemNavigationBarColor: Theme.of(context).primaryColor,
|
||||||
statusBarColor: Theme.of(context).primaryColor,
|
systemNavigationBarIconBrightness:
|
||||||
|
Theme.of(context).accentColorBrightness,
|
||||||
),
|
),
|
||||||
child: SafeArea(
|
child: Scaffold(
|
||||||
child: Scaffold(
|
appBar: AppBar(
|
||||||
appBar: AppBar(
|
title: Text('Settings'),
|
||||||
title: Text('Settings'),
|
elevation: 0,
|
||||||
elevation: 0,
|
backgroundColor: Theme.of(context).primaryColor,
|
||||||
backgroundColor: Theme.of(context).primaryColor,
|
),
|
||||||
),
|
body: SafeArea(
|
||||||
body: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
physics: const AlwaysScrollableScrollPhysics(),
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
scrollDirection: Axis.vertical,
|
scrollDirection: Axis.vertical,
|
||||||
child: Column(
|
child: Column(
|
||||||
@ -72,16 +93,14 @@ class Settings extends StatelessWidget {
|
|||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => ThemeSetting())),
|
builder: (context) => ThemeSetting())),
|
||||||
contentPadding:
|
contentPadding: EdgeInsets.symmetric(horizontal: 25.0),
|
||||||
EdgeInsets.symmetric(horizontal: 25.0),
|
|
||||||
leading: Icon(LineIcons.adjust_solid),
|
leading: Icon(LineIcons.adjust_solid),
|
||||||
title: Text('Appearance'),
|
title: Text('Appearance'),
|
||||||
subtitle: Text('Colors and themes'),
|
subtitle: Text('Colors and themes'),
|
||||||
),
|
),
|
||||||
Divider(height: 2),
|
Divider(height: 2),
|
||||||
ListTile(
|
ListTile(
|
||||||
contentPadding:
|
contentPadding: EdgeInsets.symmetric(horizontal: 25.0),
|
||||||
EdgeInsets.symmetric(horizontal: 25.0),
|
|
||||||
leading: Icon(LineIcons.play_circle),
|
leading: Icon(LineIcons.play_circle),
|
||||||
title: Text('AutoPlay'),
|
title: Text('AutoPlay'),
|
||||||
subtitle: Text('Autoplay next episode in playlist'),
|
subtitle: Text('Autoplay next episode in playlist'),
|
||||||
@ -94,20 +113,14 @@ class Settings extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
Divider(height: 2),
|
Divider(height: 2),
|
||||||
ListTile(
|
ListTile(
|
||||||
contentPadding:
|
onTap: () => Navigator.push(
|
||||||
EdgeInsets.symmetric(horizontal: 25.0),
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => SyncingSetting())),
|
||||||
|
contentPadding: EdgeInsets.symmetric(horizontal: 25.0),
|
||||||
leading: Icon(LineIcons.cloud_download_alt_solid),
|
leading: Icon(LineIcons.cloud_download_alt_solid),
|
||||||
title: Text('AutoUpdate'),
|
title: Text('Syncing'),
|
||||||
subtitle: Text('Auto update feed every day'),
|
subtitle: Text('Refresh podcasts in the background'),
|
||||||
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),
|
Divider(height: 2),
|
||||||
ListTile(
|
ListTile(
|
||||||
@ -115,8 +128,7 @@ class Settings extends StatelessWidget {
|
|||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => StorageSetting())),
|
builder: (context) => StorageSetting())),
|
||||||
contentPadding:
|
contentPadding: EdgeInsets.symmetric(horizontal: 25.0),
|
||||||
EdgeInsets.symmetric(horizontal: 25.0),
|
|
||||||
leading: Icon(LineIcons.save),
|
leading: Icon(LineIcons.save),
|
||||||
title: Text('Storage'),
|
title: Text('Storage'),
|
||||||
subtitle: Text('Manage cache and download storage'),
|
subtitle: Text('Manage cache and download storage'),
|
||||||
@ -127,13 +139,22 @@ class Settings extends StatelessWidget {
|
|||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => PlayedHistory())),
|
builder: (context) => PlayedHistory())),
|
||||||
contentPadding:
|
contentPadding: EdgeInsets.symmetric(horizontal: 25.0),
|
||||||
EdgeInsets.symmetric(horizontal: 25.0),
|
|
||||||
leading: Icon(Icons.update),
|
leading: Icon(Icons.update),
|
||||||
title: Text('History'),
|
title: Text('History'),
|
||||||
subtitle: Text('Listen data'),
|
subtitle: Text('Listen data'),
|
||||||
),
|
),
|
||||||
Divider(height: 2),
|
Divider(height: 2),
|
||||||
|
ListTile(
|
||||||
|
onTap: () {
|
||||||
|
_exportOmpl();
|
||||||
|
},
|
||||||
|
contentPadding: EdgeInsets.symmetric(horizontal: 25.0),
|
||||||
|
leading: Icon(LineIcons.file_code_solid),
|
||||||
|
title: Text('Export'),
|
||||||
|
subtitle: Text('Export ompl file'),
|
||||||
|
),
|
||||||
|
Divider(height: 2),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -163,31 +184,25 @@ class Settings extends StatelessWidget {
|
|||||||
ListTile(
|
ListTile(
|
||||||
onTap: () => _launchUrl(
|
onTap: () => _launchUrl(
|
||||||
'https://github.com/stonega/tsacdop/releases'),
|
'https://github.com/stonega/tsacdop/releases'),
|
||||||
contentPadding:
|
contentPadding: EdgeInsets.symmetric(horizontal: 25.0),
|
||||||
EdgeInsets.symmetric(horizontal: 25.0),
|
|
||||||
leading: Icon(LineIcons.map_signs_solid),
|
leading: Icon(LineIcons.map_signs_solid),
|
||||||
title: Text('Changelog'),
|
title: Text('Changelog'),
|
||||||
subtitle: Text('List of chagnes'),
|
subtitle: Text('List of chagnes'),
|
||||||
),
|
),
|
||||||
Divider(height: 2),
|
Divider(height: 2),
|
||||||
ListTile(
|
ListTile(
|
||||||
onTap: () => Navigator.push(
|
onTap: () => Navigator.push(context,
|
||||||
context,
|
MaterialPageRoute(builder: (context) => Libries())),
|
||||||
MaterialPageRoute(
|
contentPadding: EdgeInsets.symmetric(horizontal: 25.0),
|
||||||
builder: (context) => Libries())),
|
|
||||||
contentPadding:
|
|
||||||
EdgeInsets.symmetric(horizontal: 25.0),
|
|
||||||
leading: Icon(LineIcons.book_open_solid),
|
leading: Icon(LineIcons.book_open_solid),
|
||||||
title: Text('Libraries'),
|
title: Text('Libraries'),
|
||||||
subtitle:
|
subtitle: Text('Open source libraries in application'),
|
||||||
Text('Open source libraried in application'),
|
|
||||||
),
|
),
|
||||||
Divider(height: 2),
|
Divider(height: 2),
|
||||||
ListTile(
|
ListTile(
|
||||||
onTap: () => _launchUrl(
|
onTap: () => _launchUrl(
|
||||||
'mailto:<xijieyin@gmail.com>?subject=Tsacdop Feedback'),
|
'mailto:<xijieyin@gmail.com>?subject=Tsacdop Feedback'),
|
||||||
contentPadding:
|
contentPadding: EdgeInsets.symmetric(horizontal: 25.0),
|
||||||
EdgeInsets.symmetric(horizontal: 25.0),
|
|
||||||
leading: Icon(LineIcons.bug_solid),
|
leading: Icon(LineIcons.bug_solid),
|
||||||
title: Text('Feedback'),
|
title: Text('Feedback'),
|
||||||
subtitle: Text('Bugs and feature requests'),
|
subtitle: Text('Bugs and feature requests'),
|
||||||
|
@ -8,17 +8,19 @@ class StorageSetting extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AnnotatedRegion<SystemUiOverlayStyle>(
|
return AnnotatedRegion<SystemUiOverlayStyle>(
|
||||||
value: SystemUiOverlayStyle(
|
value: SystemUiOverlayStyle(
|
||||||
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
|
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
|
||||||
systemNavigationBarColor: Theme.of(context).primaryColor,
|
systemNavigationBarColor: Theme.of(context).primaryColor,
|
||||||
statusBarColor: Theme.of(context).primaryColor),
|
systemNavigationBarIconBrightness:
|
||||||
child: SafeArea(
|
Theme.of(context).accentColorBrightness,
|
||||||
child: Scaffold(
|
),
|
||||||
appBar: AppBar(
|
child: Scaffold(
|
||||||
title: Text('Storage'),
|
appBar: AppBar(
|
||||||
elevation: 0,
|
title: Text('Storage'),
|
||||||
backgroundColor: Theme.of(context).primaryColor,
|
elevation: 0,
|
||||||
),
|
backgroundColor: Theme.of(context).primaryColor,
|
||||||
body: Column(
|
),
|
||||||
|
body: SafeArea(
|
||||||
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
|
119
lib/settings/syncing.dart
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:tuple/tuple.dart';
|
||||||
|
import 'package:tsacdop/class/settingstate.dart';
|
||||||
|
|
||||||
|
class SyncingSetting extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var settings = Provider.of<SettingState>(context, listen: false);
|
||||||
|
return AnnotatedRegion<SystemUiOverlayStyle>(
|
||||||
|
value: SystemUiOverlayStyle(
|
||||||
|
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
|
||||||
|
systemNavigationBarColor: Theme.of(context).primaryColor,
|
||||||
|
systemNavigationBarIconBrightness:
|
||||||
|
Theme.of(context).accentColorBrightness,
|
||||||
|
),
|
||||||
|
child: Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text('Syncing'),
|
||||||
|
elevation: 0,
|
||||||
|
backgroundColor: Theme.of(context).primaryColor,
|
||||||
|
),
|
||||||
|
body: SingleChildScrollView(
|
||||||
|
scrollDirection: Axis.vertical,
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
Selector<SettingState, Tuple2<bool, int>>(
|
||||||
|
selector: (_, settings) =>
|
||||||
|
Tuple2(settings.autoUpdate, settings.updateInterval),
|
||||||
|
builder: (_, data, __) => 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('Syncing',
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.bodyText1
|
||||||
|
.copyWith(color: Theme.of(context).accentColor)),
|
||||||
|
),
|
||||||
|
ListView(
|
||||||
|
shrinkWrap: true,
|
||||||
|
scrollDirection: Axis.vertical,
|
||||||
|
children: <Widget>[
|
||||||
|
ListTile(
|
||||||
|
onTap: () {
|
||||||
|
if (settings.autoUpdate) {
|
||||||
|
settings.autoUpdate = false;
|
||||||
|
settings.cancelWork();
|
||||||
|
} else {
|
||||||
|
settings.autoUpdate = true;
|
||||||
|
settings.setWorkManager(data.item2);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
contentPadding: EdgeInsets.only(
|
||||||
|
left: 80.0, right: 20, bottom: 20),
|
||||||
|
title: Text('Enable syncing'),
|
||||||
|
subtitle: Text(
|
||||||
|
'Refresh all podcasts in the background to get leatest episodes.'),
|
||||||
|
trailing: Switch(
|
||||||
|
value: data.item1,
|
||||||
|
onChanged: (boo) async {
|
||||||
|
settings.autoUpdate = boo;
|
||||||
|
if (boo) {
|
||||||
|
settings.setWorkManager(data.item2);
|
||||||
|
} else {
|
||||||
|
settings.cancelWork();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
Divider(height: 2),
|
||||||
|
ListTile(
|
||||||
|
contentPadding:
|
||||||
|
EdgeInsets.only(left: 80.0, right: 20),
|
||||||
|
title: Text('Update Interval'),
|
||||||
|
subtitle: Text('Default 24 hours'),
|
||||||
|
trailing: DropdownButton(
|
||||||
|
hint: data.item2 == 1
|
||||||
|
? Text(data.item2.toString() + ' hour')
|
||||||
|
: Text(data.item2.toString() + 'hours'),
|
||||||
|
underline: Center(),
|
||||||
|
elevation: 1,
|
||||||
|
value: data.item2,
|
||||||
|
onChanged: data.item1
|
||||||
|
? (value) {
|
||||||
|
settings.setWorkManager(value);
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
items: <int>[1, 2, 4, 8, 24, 48]
|
||||||
|
.map<DropdownMenuItem<int>>((e) {
|
||||||
|
return DropdownMenuItem<int>(
|
||||||
|
value: e,
|
||||||
|
child: e == 1
|
||||||
|
? Text(e.toString() + ' hour')
|
||||||
|
: Text(e.toString() + ' hours'));
|
||||||
|
}).toList()),
|
||||||
|
),
|
||||||
|
Divider(height: 2),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -10,180 +10,174 @@ class ThemeSetting extends StatelessWidget {
|
|||||||
var settings = Provider.of<SettingState>(context, listen: false);
|
var settings = Provider.of<SettingState>(context, listen: false);
|
||||||
return AnnotatedRegion<SystemUiOverlayStyle>(
|
return AnnotatedRegion<SystemUiOverlayStyle>(
|
||||||
value: SystemUiOverlayStyle(
|
value: SystemUiOverlayStyle(
|
||||||
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
|
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
|
||||||
systemNavigationBarColor: Theme.of(context).primaryColor,
|
systemNavigationBarColor: Theme.of(context).primaryColor,
|
||||||
statusBarColor: Theme.of(context).primaryColor),
|
systemNavigationBarIconBrightness:
|
||||||
child: SafeArea(
|
Theme.of(context).accentColorBrightness,
|
||||||
child: Scaffold(
|
),
|
||||||
appBar: AppBar(
|
child: Scaffold(
|
||||||
title: Text('Appearance'),
|
appBar: AppBar(
|
||||||
elevation: 0,
|
title: Text('Appearance'),
|
||||||
backgroundColor: Theme.of(context).primaryColor,
|
elevation: 0,
|
||||||
),
|
backgroundColor: Theme.of(context).primaryColor,
|
||||||
body: Column(
|
),
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
body: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
children: <Widget>[
|
mainAxisSize: MainAxisSize.min,
|
||||||
Column(
|
children: <Widget>[
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
children: <Widget>[
|
mainAxisSize: MainAxisSize.min,
|
||||||
Padding(
|
children: <Widget>[
|
||||||
padding: EdgeInsets.all(10.0),
|
Padding(
|
||||||
),
|
padding: EdgeInsets.all(10.0),
|
||||||
Container(
|
),
|
||||||
height: 30.0,
|
Container(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 80),
|
height: 30.0,
|
||||||
alignment: Alignment.centerLeft,
|
padding: EdgeInsets.symmetric(horizontal: 80),
|
||||||
child: Text('Interface',
|
alignment: Alignment.centerLeft,
|
||||||
style: Theme.of(context)
|
child: Text('Interface',
|
||||||
.textTheme
|
style: Theme.of(context)
|
||||||
.bodyText1
|
.textTheme
|
||||||
.copyWith(color: Theme.of(context).accentColor)),
|
.bodyText1
|
||||||
),
|
.copyWith(color: Theme.of(context).accentColor)),
|
||||||
ListView(
|
),
|
||||||
shrinkWrap: true,
|
ListView(
|
||||||
scrollDirection: Axis.vertical,
|
shrinkWrap: true,
|
||||||
children: <Widget>[
|
scrollDirection: Axis.vertical,
|
||||||
ListTile(
|
children: <Widget>[
|
||||||
onTap: () => showGeneralDialog(
|
ListTile(
|
||||||
context: context,
|
onTap: () => showGeneralDialog(
|
||||||
barrierDismissible: true,
|
context: context,
|
||||||
barrierLabel: MaterialLocalizations.of(context)
|
barrierDismissible: true,
|
||||||
.modalBarrierDismissLabel,
|
barrierLabel: MaterialLocalizations.of(context)
|
||||||
barrierColor: Colors.black54,
|
.modalBarrierDismissLabel,
|
||||||
transitionDuration:
|
barrierColor: Colors.black54,
|
||||||
const Duration(milliseconds: 200),
|
transitionDuration: const Duration(milliseconds: 200),
|
||||||
pageBuilder: (BuildContext context,
|
pageBuilder: (BuildContext context,
|
||||||
Animation animaiton,
|
Animation animaiton,
|
||||||
Animation secondaryAnimation) =>
|
Animation secondaryAnimation) =>
|
||||||
AnnotatedRegion<SystemUiOverlayStyle>(
|
AnnotatedRegion<SystemUiOverlayStyle>(
|
||||||
value: SystemUiOverlayStyle(
|
value: SystemUiOverlayStyle(
|
||||||
statusBarIconBrightness: Brightness.light,
|
statusBarIconBrightness: Brightness.light,
|
||||||
systemNavigationBarColor:
|
// systemNavigationBarColor:
|
||||||
Theme.of(context).brightness ==
|
// Theme.of(context).brightness ==
|
||||||
Brightness.light
|
// Brightness.light
|
||||||
? Color.fromRGBO(113, 113, 113, 1)
|
// ? Color.fromRGBO(113, 113, 113, 1)
|
||||||
: Color.fromRGBO(15, 15, 15, 1),
|
// : Color.fromRGBO(15, 15, 15, 1),
|
||||||
statusBarColor:
|
// statusBarColor:
|
||||||
Theme.of(context).brightness ==
|
// Theme.of(context).brightness ==
|
||||||
Brightness.light
|
// Brightness.light
|
||||||
? Color.fromRGBO(113, 113, 113, 1)
|
// ? Color.fromRGBO(113, 113, 113, 1)
|
||||||
: Color.fromRGBO(5, 5, 5, 1),
|
// : Color.fromRGBO(5, 5, 5, 1),
|
||||||
),
|
),
|
||||||
child: SafeArea(
|
child: AlertDialog(
|
||||||
child: AlertDialog(
|
titlePadding: EdgeInsets.only(
|
||||||
titlePadding: EdgeInsets.only(
|
top: 20,
|
||||||
top: 20,
|
left: 40,
|
||||||
left: 40,
|
right: 200,
|
||||||
right: 200,
|
),
|
||||||
),
|
elevation: 1,
|
||||||
elevation: 1,
|
shape: RoundedRectangleBorder(
|
||||||
shape: RoundedRectangleBorder(
|
borderRadius: BorderRadius.all(
|
||||||
borderRadius: BorderRadius.all(
|
Radius.circular(10.0))),
|
||||||
Radius.circular(10.0))),
|
title: Text('Theme'),
|
||||||
title: Text('Theme'),
|
content: Column(
|
||||||
content: Column(
|
mainAxisSize: MainAxisSize.min,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
mainAxisAlignment:
|
children: <Widget>[
|
||||||
MainAxisAlignment.start,
|
RadioListTile(
|
||||||
children: <Widget>[
|
title: Container(
|
||||||
RadioListTile(
|
padding:
|
||||||
title: Container(
|
EdgeInsets.only(right: 80),
|
||||||
padding: EdgeInsets.only(
|
child: Text('System default')),
|
||||||
right: 80),
|
value: ThemeMode.system,
|
||||||
child:
|
groupValue: settings.theme,
|
||||||
Text('System default')),
|
onChanged: (value) {
|
||||||
value: ThemeMode.system,
|
settings.setTheme = value;
|
||||||
groupValue: settings.theme,
|
Navigator.of(context).pop();
|
||||||
onChanged: (value) {
|
}),
|
||||||
settings.setTheme = value;
|
RadioListTile(
|
||||||
Navigator.of(context).pop();
|
title: Text('Dark mode'),
|
||||||
}),
|
value: ThemeMode.dark,
|
||||||
RadioListTile(
|
groupValue: settings.theme,
|
||||||
title: Text('Dark mode'),
|
onChanged: (value) {
|
||||||
value: ThemeMode.dark,
|
settings.setTheme = value;
|
||||||
groupValue: settings.theme,
|
Navigator.of(context).pop();
|
||||||
onChanged: (value) {
|
}),
|
||||||
settings.setTheme = value;
|
RadioListTile(
|
||||||
Navigator.of(context).pop();
|
title: Text('Light mode'),
|
||||||
}),
|
value: ThemeMode.light,
|
||||||
RadioListTile(
|
groupValue: settings.theme,
|
||||||
title: Text('Light mode'),
|
onChanged: (value) {
|
||||||
value: ThemeMode.light,
|
settings.setTheme = value;
|
||||||
groupValue: settings.theme,
|
Navigator.of(context).pop();
|
||||||
onChanged: (value) {
|
}),
|
||||||
settings.setTheme = value;
|
],
|
||||||
Navigator.of(context).pop();
|
),
|
||||||
}),
|
),
|
||||||
],
|
)),
|
||||||
),
|
contentPadding: EdgeInsets.symmetric(horizontal: 80.0),
|
||||||
|
// leading: Icon(Icons.colorize),
|
||||||
|
title: Text('Theme'),
|
||||||
|
subtitle: Text('System default'),
|
||||||
|
),
|
||||||
|
Divider(height: 2),
|
||||||
|
ListTile(
|
||||||
|
onTap: () => 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: 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) {
|
||||||
|
settings.setAccentColor = value;
|
||||||
|
},
|
||||||
|
pickerColor: Colors.blue,
|
||||||
),
|
),
|
||||||
))),
|
|
||||||
contentPadding: EdgeInsets.symmetric(horizontal: 80.0),
|
|
||||||
// leading: Icon(Icons.colorize),
|
|
||||||
title: Text('Theme'),
|
|
||||||
subtitle: Text('System default'),
|
|
||||||
),
|
|
||||||
Divider(height: 2),
|
|
||||||
ListTile(
|
|
||||||
onTap: () => 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(
|
contentPadding: EdgeInsets.symmetric(horizontal: 80.0),
|
||||||
elevation: 1,
|
title: Text('Accent color'),
|
||||||
titlePadding: EdgeInsets.only(
|
subtitle: Text('Include the overlay color'),
|
||||||
top: 20,
|
),
|
||||||
left: 40,
|
Divider(height: 2),
|
||||||
right: 200,
|
],
|
||||||
bottom: 20),
|
),
|
||||||
shape: RoundedRectangleBorder(
|
],
|
||||||
borderRadius: BorderRadius.all(
|
),
|
||||||
Radius.circular(10.0))),
|
],
|
||||||
title: Text('Choose a color'),
|
|
||||||
content: SingleChildScrollView(
|
|
||||||
child: MaterialPicker(
|
|
||||||
onColorChanged: (value) {
|
|
||||||
settings.setAccentColor = value;
|
|
||||||
},
|
|
||||||
pickerColor: Colors.blue,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)))),
|
|
||||||
contentPadding: EdgeInsets.symmetric(horizontal: 80.0),
|
|
||||||
title: Text('Accent color'),
|
|
||||||
subtitle: Text('Include the overlay color'),
|
|
||||||
),
|
|
||||||
Divider(height: 2),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
463
lib/util/day_night_switch.dart
Normal file
@ -0,0 +1,463 @@
|
|||||||
|
//Fork from https://github.com/divyanshub024/day_night_switch
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/gestures.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
|
|
||||||
|
const double _kTrackHeight = 80.0;
|
||||||
|
const double _kTrackWidth = 160.0;
|
||||||
|
const double _kTrackRadius = _kTrackHeight / 2.0;
|
||||||
|
const double _kThumbRadius = 36.0;
|
||||||
|
const double _kSwitchWidth =
|
||||||
|
_kTrackWidth - 2 * _kTrackRadius + 2 * kRadialReactionRadius;
|
||||||
|
const double _kSwitchHeight = 2 * kRadialReactionRadius + 8.0;
|
||||||
|
|
||||||
|
class DayNightSwitch extends StatefulWidget {
|
||||||
|
const DayNightSwitch({
|
||||||
|
@required this.value,
|
||||||
|
@required this.onChanged,
|
||||||
|
@required this.onDrag,
|
||||||
|
this.dragStartBehavior = DragStartBehavior.start,
|
||||||
|
this.height,
|
||||||
|
this.moonImage,
|
||||||
|
this.sunImage,
|
||||||
|
this.sunColor,
|
||||||
|
this.moonColor,
|
||||||
|
this.dayColor,
|
||||||
|
this.nightColor,
|
||||||
|
});
|
||||||
|
|
||||||
|
final bool value;
|
||||||
|
final ValueChanged<bool> onChanged;
|
||||||
|
final ValueChanged<double> onDrag;
|
||||||
|
final DragStartBehavior dragStartBehavior;
|
||||||
|
final double height;
|
||||||
|
final ImageProvider sunImage;
|
||||||
|
final ImageProvider moonImage;
|
||||||
|
final Color sunColor;
|
||||||
|
final Color moonColor;
|
||||||
|
final Color dayColor;
|
||||||
|
final Color nightColor;
|
||||||
|
|
||||||
|
@override
|
||||||
|
_DayNightSwitchState createState() => _DayNightSwitchState();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||||
|
super.debugFillProperties(properties);
|
||||||
|
properties.add(FlagProperty('value',
|
||||||
|
value: value, ifTrue: 'on', ifFalse: 'off', showName: true));
|
||||||
|
properties.add(ObjectFlagProperty<ValueChanged<bool>>(
|
||||||
|
'onChanged',
|
||||||
|
onChanged,
|
||||||
|
ifNull: 'disabled',
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DayNightSwitchState extends State<DayNightSwitch>
|
||||||
|
with TickerProviderStateMixin {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final Color moonColor = widget.moonColor ?? const Color(0xFFf5f3ce);
|
||||||
|
final Color nightColor = widget.nightColor ?? const Color(0xFF003366);
|
||||||
|
|
||||||
|
Color sunColor = widget.sunColor ?? const Color(0xFFFDB813);
|
||||||
|
Color dayColor = widget.dayColor ?? const Color(0xFF87CEEB);
|
||||||
|
|
||||||
|
return _SwitchRenderObjectWidget(
|
||||||
|
dragStartBehavior: widget.dragStartBehavior,
|
||||||
|
value: widget.value,
|
||||||
|
activeColor: moonColor,
|
||||||
|
inactiveColor: sunColor,
|
||||||
|
moonImage: widget.moonImage,
|
||||||
|
sunImage: widget.sunImage,
|
||||||
|
activeTrackColor: nightColor,
|
||||||
|
inactiveTrackColor: dayColor,
|
||||||
|
configuration: createLocalImageConfiguration(context),
|
||||||
|
onChanged: widget.onChanged,
|
||||||
|
onDrag: widget.onDrag,
|
||||||
|
additionalConstraints:
|
||||||
|
BoxConstraints.tight(Size(_kSwitchWidth, _kSwitchHeight)),
|
||||||
|
vsync: this,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SwitchRenderObjectWidget extends LeafRenderObjectWidget {
|
||||||
|
const _SwitchRenderObjectWidget({
|
||||||
|
Key key,
|
||||||
|
this.value,
|
||||||
|
this.activeColor,
|
||||||
|
this.inactiveColor,
|
||||||
|
this.moonImage,
|
||||||
|
this.sunImage,
|
||||||
|
this.activeTrackColor,
|
||||||
|
this.inactiveTrackColor,
|
||||||
|
this.configuration,
|
||||||
|
this.onChanged,
|
||||||
|
this.onDrag,
|
||||||
|
this.vsync,
|
||||||
|
this.additionalConstraints,
|
||||||
|
this.dragStartBehavior,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final bool value;
|
||||||
|
final Color activeColor;
|
||||||
|
final Color inactiveColor;
|
||||||
|
final ImageProvider moonImage;
|
||||||
|
final ImageProvider sunImage;
|
||||||
|
final Color activeTrackColor;
|
||||||
|
final Color inactiveTrackColor;
|
||||||
|
final ImageConfiguration configuration;
|
||||||
|
final ValueChanged<bool> onChanged;
|
||||||
|
final ValueChanged<double> onDrag;
|
||||||
|
final TickerProvider vsync;
|
||||||
|
final BoxConstraints additionalConstraints;
|
||||||
|
final DragStartBehavior dragStartBehavior;
|
||||||
|
|
||||||
|
@override
|
||||||
|
_RenderSwitch createRenderObject(BuildContext context) {
|
||||||
|
return _RenderSwitch(
|
||||||
|
dragStartBehavior: dragStartBehavior,
|
||||||
|
value: value,
|
||||||
|
activeColor: activeColor,
|
||||||
|
inactiveColor: inactiveColor,
|
||||||
|
moonImage: moonImage,
|
||||||
|
sunImage: sunImage,
|
||||||
|
activeTrackColor: activeTrackColor,
|
||||||
|
inactiveTrackColor: inactiveTrackColor,
|
||||||
|
configuration: configuration,
|
||||||
|
onChanged: onChanged,
|
||||||
|
onDrag: onDrag,
|
||||||
|
textDirection: Directionality.of(context),
|
||||||
|
additionalConstraints: additionalConstraints,
|
||||||
|
vSync: vsync,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void updateRenderObject(BuildContext context, _RenderSwitch renderObject) {
|
||||||
|
renderObject
|
||||||
|
..value = value
|
||||||
|
..activeColor = activeColor
|
||||||
|
..inactiveColor = inactiveColor
|
||||||
|
..activeThumbImage = moonImage
|
||||||
|
..inactiveThumbImage = sunImage
|
||||||
|
..activeTrackColor = activeTrackColor
|
||||||
|
..inactiveTrackColor = inactiveTrackColor
|
||||||
|
..configuration = configuration
|
||||||
|
..onChanged = onChanged
|
||||||
|
..onDrag = onDrag
|
||||||
|
..textDirection = Directionality.of(context)
|
||||||
|
..additionalConstraints = additionalConstraints
|
||||||
|
..dragStartBehavior = dragStartBehavior
|
||||||
|
..vsync = vsync;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RenderSwitch extends RenderToggleable {
|
||||||
|
ValueChanged<double> onDrag;
|
||||||
|
_RenderSwitch({
|
||||||
|
bool value,
|
||||||
|
Color activeColor,
|
||||||
|
Color inactiveColor,
|
||||||
|
ImageProvider moonImage,
|
||||||
|
ImageProvider sunImage,
|
||||||
|
Color activeTrackColor,
|
||||||
|
Color inactiveTrackColor,
|
||||||
|
ImageConfiguration configuration,
|
||||||
|
BoxConstraints additionalConstraints,
|
||||||
|
@required TextDirection textDirection,
|
||||||
|
ValueChanged<bool> onChanged,
|
||||||
|
this.onDrag,
|
||||||
|
@required TickerProvider vSync,
|
||||||
|
DragStartBehavior dragStartBehavior,
|
||||||
|
}) : assert(textDirection != null),
|
||||||
|
_activeThumbImage = moonImage,
|
||||||
|
_inactiveThumbImage = sunImage,
|
||||||
|
_activeTrackColor = activeTrackColor,
|
||||||
|
_inactiveTrackColor = inactiveTrackColor,
|
||||||
|
_configuration = configuration,
|
||||||
|
_textDirection = textDirection,
|
||||||
|
super(
|
||||||
|
value: value,
|
||||||
|
tristate: false,
|
||||||
|
activeColor: activeColor,
|
||||||
|
inactiveColor: inactiveColor,
|
||||||
|
onChanged: onChanged,
|
||||||
|
additionalConstraints: additionalConstraints,
|
||||||
|
vsync: vSync,
|
||||||
|
) {
|
||||||
|
_drag = HorizontalDragGestureRecognizer()
|
||||||
|
..onStart = _handleDragStart
|
||||||
|
..onUpdate = _handleDragUpdate
|
||||||
|
..onEnd = _handleDragEnd
|
||||||
|
..dragStartBehavior = dragStartBehavior;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImageProvider get activeThumbImage => _activeThumbImage;
|
||||||
|
ImageProvider _activeThumbImage;
|
||||||
|
set activeThumbImage(ImageProvider value) {
|
||||||
|
if (value == _activeThumbImage) return;
|
||||||
|
_activeThumbImage = value;
|
||||||
|
markNeedsPaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImageProvider get inactiveThumbImage => _inactiveThumbImage;
|
||||||
|
ImageProvider _inactiveThumbImage;
|
||||||
|
set inactiveThumbImage(ImageProvider value) {
|
||||||
|
if (value == _inactiveThumbImage) return;
|
||||||
|
_inactiveThumbImage = value;
|
||||||
|
markNeedsPaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
Color get activeTrackColor => _activeTrackColor;
|
||||||
|
Color _activeTrackColor;
|
||||||
|
set activeTrackColor(Color value) {
|
||||||
|
assert(value != null);
|
||||||
|
if (value == _activeTrackColor) return;
|
||||||
|
_activeTrackColor = value;
|
||||||
|
markNeedsPaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
Color get inactiveTrackColor => _inactiveTrackColor;
|
||||||
|
Color _inactiveTrackColor;
|
||||||
|
set inactiveTrackColor(Color value) {
|
||||||
|
assert(value != null);
|
||||||
|
if (value == _inactiveTrackColor) return;
|
||||||
|
_inactiveTrackColor = value;
|
||||||
|
markNeedsPaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImageConfiguration get configuration => _configuration;
|
||||||
|
ImageConfiguration _configuration;
|
||||||
|
set configuration(ImageConfiguration value) {
|
||||||
|
assert(value != null);
|
||||||
|
if (value == _configuration) return;
|
||||||
|
_configuration = value;
|
||||||
|
markNeedsPaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
TextDirection get textDirection => _textDirection;
|
||||||
|
TextDirection _textDirection;
|
||||||
|
set textDirection(TextDirection value) {
|
||||||
|
assert(value != null);
|
||||||
|
if (_textDirection == value) return;
|
||||||
|
_textDirection = value;
|
||||||
|
markNeedsPaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
DragStartBehavior get dragStartBehavior => _drag.dragStartBehavior;
|
||||||
|
set dragStartBehavior(DragStartBehavior value) {
|
||||||
|
assert(value != null);
|
||||||
|
if (_drag.dragStartBehavior == value) return;
|
||||||
|
_drag.dragStartBehavior = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void detach() {
|
||||||
|
_cachedThumbPainter?.dispose();
|
||||||
|
_cachedThumbPainter = null;
|
||||||
|
super.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
double get _trackInnerLength => size.width - 2.0 * kRadialReactionRadius;
|
||||||
|
|
||||||
|
HorizontalDragGestureRecognizer _drag;
|
||||||
|
|
||||||
|
void _handleDragStart(DragStartDetails details) {
|
||||||
|
if (isInteractive) reactionController.forward();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleDragUpdate(DragUpdateDetails details) {
|
||||||
|
if (isInteractive) {
|
||||||
|
position
|
||||||
|
..curve = null
|
||||||
|
..reverseCurve = null;
|
||||||
|
final double delta = details.primaryDelta / _trackInnerLength;
|
||||||
|
switch (textDirection) {
|
||||||
|
case TextDirection.rtl:
|
||||||
|
positionController.value -= delta;
|
||||||
|
break;
|
||||||
|
case TextDirection.ltr:
|
||||||
|
positionController.value += delta;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
positionController.addListener(() {onDrag(positionController.value);});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleDragEnd(DragEndDetails details) {
|
||||||
|
if (position.value >= 0.5)
|
||||||
|
positionController.forward();
|
||||||
|
else
|
||||||
|
positionController.reverse();
|
||||||
|
reactionController.reverse();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
void handleEvent(PointerEvent event, BoxHitTestEntry entry) {
|
||||||
|
assert(debugHandleEvent(event, entry));
|
||||||
|
if (event is PointerDownEvent && onChanged != null) _drag.addPointer(event);
|
||||||
|
super.handleEvent(event, entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
Color _cachedThumbColor;
|
||||||
|
ImageProvider _cachedThumbImage;
|
||||||
|
BoxPainter _cachedThumbPainter;
|
||||||
|
|
||||||
|
BoxDecoration _createDefaultThumbDecoration(
|
||||||
|
Color color, ImageProvider image) {
|
||||||
|
return BoxDecoration(
|
||||||
|
color: color,
|
||||||
|
image: image == null ? null : DecorationImage(image: image),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
boxShadow: kElevationToShadow[1],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isPainting = false;
|
||||||
|
|
||||||
|
void _handleDecorationChanged() {
|
||||||
|
// If the image decoration is available synchronously, we'll get called here
|
||||||
|
// during paint. There's no reason to mark ourselves as needing paint if we
|
||||||
|
// are already in the middle of painting. (In fact, doing so would trigger
|
||||||
|
// an assert).
|
||||||
|
if (!_isPainting) markNeedsPaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void describeSemanticsConfiguration(SemanticsConfiguration config) {
|
||||||
|
super.describeSemanticsConfiguration(config);
|
||||||
|
config.isToggled = value == true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(PaintingContext context, Offset offset) {
|
||||||
|
final Canvas canvas = context.canvas;
|
||||||
|
final bool isEnabled = onChanged != null;
|
||||||
|
final double currentValue = position.value;
|
||||||
|
|
||||||
|
double visualPosition;
|
||||||
|
switch (textDirection) {
|
||||||
|
case TextDirection.rtl:
|
||||||
|
visualPosition = 1.0 - currentValue;
|
||||||
|
break;
|
||||||
|
case TextDirection.ltr:
|
||||||
|
visualPosition = currentValue;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Color trackColor = isEnabled
|
||||||
|
? Color.lerp(inactiveTrackColor, activeTrackColor, currentValue)
|
||||||
|
: inactiveTrackColor;
|
||||||
|
|
||||||
|
final Color thumbColor = isEnabled
|
||||||
|
? Color.lerp(inactiveColor, activeColor, currentValue)
|
||||||
|
: inactiveColor;
|
||||||
|
|
||||||
|
final ImageProvider thumbImage = isEnabled
|
||||||
|
? (currentValue < 0.5 ? inactiveThumbImage : activeThumbImage)
|
||||||
|
: inactiveThumbImage;
|
||||||
|
|
||||||
|
// Paint the track
|
||||||
|
final Paint paint = Paint()..color = trackColor;
|
||||||
|
const double trackHorizontalPadding = kRadialReactionRadius - _kTrackRadius;
|
||||||
|
final Rect trackRect = Rect.fromLTWH(
|
||||||
|
offset.dx + trackHorizontalPadding,
|
||||||
|
offset.dy + (size.height - _kTrackHeight) / 2.0,
|
||||||
|
size.width - 2.0 * trackHorizontalPadding,
|
||||||
|
_kTrackHeight,
|
||||||
|
);
|
||||||
|
final RRect trackRRect = RRect.fromRectAndRadius(
|
||||||
|
trackRect, const Radius.circular(_kTrackRadius));
|
||||||
|
canvas.drawRRect(trackRRect, paint);
|
||||||
|
|
||||||
|
final Offset thumbPosition = Offset(
|
||||||
|
kRadialReactionRadius + visualPosition * _trackInnerLength,
|
||||||
|
size.height / 2.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
paintRadialReaction(canvas, offset, thumbPosition);
|
||||||
|
|
||||||
|
var linePaint = Paint()
|
||||||
|
..color = Colors.white
|
||||||
|
..strokeWidth = 4 + (6 * (1 - currentValue))
|
||||||
|
..strokeCap = StrokeCap.round
|
||||||
|
..style = PaintingStyle.stroke;
|
||||||
|
|
||||||
|
canvas.drawLine(
|
||||||
|
Offset(offset.dx + _kSwitchWidth * 0.1, offset.dy),
|
||||||
|
Offset(
|
||||||
|
offset.dx +
|
||||||
|
(_kSwitchWidth * 0.1) +
|
||||||
|
(_kSwitchWidth / 2 * (1 - currentValue)),
|
||||||
|
offset.dy),
|
||||||
|
linePaint,
|
||||||
|
);
|
||||||
|
|
||||||
|
canvas.drawLine(
|
||||||
|
Offset(offset.dx + _kSwitchWidth * 0.2, offset.dy + _kSwitchHeight),
|
||||||
|
Offset(
|
||||||
|
offset.dx +
|
||||||
|
(_kSwitchWidth * 0.2) +
|
||||||
|
(_kSwitchWidth / 2 * (1 - currentValue)),
|
||||||
|
offset.dy + _kSwitchHeight),
|
||||||
|
linePaint,
|
||||||
|
);
|
||||||
|
|
||||||
|
var starPaint = Paint()
|
||||||
|
..strokeWidth = 4 + (6 * (1 - currentValue))
|
||||||
|
..strokeCap = StrokeCap.round
|
||||||
|
..style = PaintingStyle.stroke
|
||||||
|
..color = Color.fromARGB((255 * currentValue).floor(), 255, 255, 255);
|
||||||
|
|
||||||
|
canvas.drawLine(
|
||||||
|
Offset(offset.dx, offset.dy + _kSwitchHeight * 0.7),
|
||||||
|
Offset(offset.dx, offset.dy + _kSwitchHeight * 0.7),
|
||||||
|
starPaint,
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
_isPainting = true;
|
||||||
|
BoxPainter thumbPainter;
|
||||||
|
if (_cachedThumbPainter == null ||
|
||||||
|
thumbColor != _cachedThumbColor ||
|
||||||
|
thumbImage != _cachedThumbImage) {
|
||||||
|
_cachedThumbColor = thumbColor;
|
||||||
|
_cachedThumbImage = thumbImage;
|
||||||
|
_cachedThumbPainter =
|
||||||
|
_createDefaultThumbDecoration(thumbColor, thumbImage)
|
||||||
|
.createBoxPainter(_handleDecorationChanged);
|
||||||
|
}
|
||||||
|
thumbPainter = _cachedThumbPainter;
|
||||||
|
|
||||||
|
// The thumb contracts slightly during the animation
|
||||||
|
final double inset = 1.0 - (currentValue - 0.5).abs() * 2.0;
|
||||||
|
final double radius = _kThumbRadius - inset;
|
||||||
|
thumbPainter.paint(
|
||||||
|
canvas,
|
||||||
|
thumbPosition + offset - Offset(radius, radius),
|
||||||
|
configuration.copyWith(size: Size.fromRadius(radius)),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
_isPainting = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.drawLine(
|
||||||
|
Offset(offset.dx + _kSwitchWidth * 0.3, offset.dy + _kSwitchHeight * 0.5),
|
||||||
|
Offset(
|
||||||
|
offset.dx +
|
||||||
|
(_kSwitchWidth * 0.3) +
|
||||||
|
(_kSwitchWidth / 2 * (1 - currentValue)),
|
||||||
|
offset.dy + _kSwitchHeight * 0.5),
|
||||||
|
linePaint,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -21,19 +21,21 @@ class EpisodeGrid extends StatelessWidget {
|
|||||||
final bool showDownload;
|
final bool showDownload;
|
||||||
final bool showNumber;
|
final bool showNumber;
|
||||||
final String heroTag;
|
final String heroTag;
|
||||||
|
final int updateCount;
|
||||||
EpisodeGrid(
|
EpisodeGrid(
|
||||||
{Key key,
|
{Key key,
|
||||||
this.podcast,
|
this.podcast,
|
||||||
this.showDownload,
|
this.showDownload,
|
||||||
this.showFavorite,
|
this.showFavorite,
|
||||||
this.showNumber,
|
this.showNumber,
|
||||||
this.heroTag})
|
this.heroTag,
|
||||||
|
this.updateCount = 0})
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
Offset offset;
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
double _width = MediaQuery.of(context).size.width;
|
double _width = MediaQuery.of(context).size.width;
|
||||||
|
Offset _offset;
|
||||||
_showPopupMenu(Offset offset, EpisodeBrief episode, BuildContext context,
|
_showPopupMenu(Offset offset, EpisodeBrief episode, BuildContext context,
|
||||||
bool isPlaying, bool isInPlaylist) async {
|
bool isPlaying, bool isInPlaylist) async {
|
||||||
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
|
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
|
||||||
@ -83,7 +85,7 @@ class EpisodeGrid extends StatelessWidget {
|
|||||||
if (value == 0) {
|
if (value == 0) {
|
||||||
if (!isPlaying) audio.episodeLoad(episode);
|
if (!isPlaying) audio.episodeLoad(episode);
|
||||||
} else if (value == 1) {
|
} else if (value == 1) {
|
||||||
if (isInPlaylist) {
|
if (!isInPlaylist) {
|
||||||
audio.addToPlaylist(episode);
|
audio.addToPlaylist(episode);
|
||||||
Fluttertoast.showToast(
|
Fluttertoast.showToast(
|
||||||
msg: 'Added to playlist',
|
msg: 'Added to playlist',
|
||||||
@ -134,10 +136,10 @@ class EpisodeGrid extends StatelessWidget {
|
|||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
borderRadius: BorderRadius.all(Radius.circular(5.0)),
|
borderRadius: BorderRadius.all(Radius.circular(5.0)),
|
||||||
onTapDown: (details) => offset = Offset(
|
onTapDown: (details) => _offset = Offset(
|
||||||
details.globalPosition.dx, details.globalPosition.dy),
|
details.globalPosition.dx, details.globalPosition.dy),
|
||||||
onLongPress: () => _showPopupMenu(
|
onLongPress: () => _showPopupMenu(
|
||||||
offset,
|
_offset,
|
||||||
podcast[index],
|
podcast[index],
|
||||||
context,
|
context,
|
||||||
data.item1 == podcast[index],
|
data.item1 == podcast[index],
|
||||||
@ -185,6 +187,7 @@ class EpisodeGrid extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Spacer(),
|
Spacer(),
|
||||||
|
index < updateCount ? Text('New', style: TextStyle(color: Colors.red, fontStyle: FontStyle.italic)) : Center(),
|
||||||
showNumber
|
showNumber
|
||||||
? Container(
|
? Container(
|
||||||
alignment: Alignment.topRight,
|
alignment: Alignment.topRight,
|
||||||
|
567
lib/util/mypopupmenu.dart
Normal file
@ -0,0 +1,567 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
const Duration _kMenuDuration = Duration(milliseconds: 300);
|
||||||
|
const double _kMenuCloseIntervalEnd = 2.0 / 3.0;
|
||||||
|
const double _kMenuMaxWidth = 5.0 * _kMenuWidthStep;
|
||||||
|
const double _kMenuMinWidth = 2.0 * _kMenuWidthStep;
|
||||||
|
const double _kMenuVerticalPadding = 8.0;
|
||||||
|
const double _kMenuWidthStep = 56.0;
|
||||||
|
const double _kMenuScreenPadding = 8.0;
|
||||||
|
|
||||||
|
class _MenuItem extends SingleChildRenderObjectWidget {
|
||||||
|
const _MenuItem({
|
||||||
|
Key key,
|
||||||
|
@required this.onLayout,
|
||||||
|
Widget child,
|
||||||
|
}) : assert(onLayout != null),
|
||||||
|
super(key: key, child: child);
|
||||||
|
|
||||||
|
final ValueChanged<Size> onLayout;
|
||||||
|
|
||||||
|
@override
|
||||||
|
RenderObject createRenderObject(BuildContext context) {
|
||||||
|
return _RenderMenuItem(onLayout);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void updateRenderObject(
|
||||||
|
BuildContext context, covariant _RenderMenuItem renderObject) {
|
||||||
|
renderObject.onLayout = onLayout;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RenderMenuItem extends RenderShiftedBox {
|
||||||
|
_RenderMenuItem(this.onLayout, [RenderBox child])
|
||||||
|
: assert(onLayout != null),
|
||||||
|
super(child);
|
||||||
|
|
||||||
|
ValueChanged<Size> onLayout;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void performLayout() {
|
||||||
|
if (child == null) {
|
||||||
|
size = Size.zero;
|
||||||
|
} else {
|
||||||
|
child.layout(constraints, parentUsesSize: true);
|
||||||
|
size = constraints.constrain(child.size);
|
||||||
|
}
|
||||||
|
final BoxParentData childParentData = child.parentData as BoxParentData;
|
||||||
|
childParentData.offset = Offset.zero;
|
||||||
|
onLayout(size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PopupMenu<T> extends StatelessWidget {
|
||||||
|
const _PopupMenu({
|
||||||
|
Key key,
|
||||||
|
this.route,
|
||||||
|
this.semanticLabel,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final _PopupMenuRoute<T> route;
|
||||||
|
final String semanticLabel;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final double unit = 1.0 /
|
||||||
|
(route.items.length +
|
||||||
|
1.5); // 1.0 for the width and 0.5 for the last item's fade.
|
||||||
|
final List<Widget> children = <Widget>[];
|
||||||
|
final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context);
|
||||||
|
|
||||||
|
for (int i = 0; i < route.items.length; i += 1) {
|
||||||
|
final double start = (i + 1) * unit;
|
||||||
|
final double end = (start + 1.5 * unit).clamp(0.0, 1.0) as double;
|
||||||
|
final CurvedAnimation opacity = CurvedAnimation(
|
||||||
|
parent: route.animation,
|
||||||
|
curve: Interval(start, end),
|
||||||
|
);
|
||||||
|
Widget item = route.items[i];
|
||||||
|
if (route.initialValue != null &&
|
||||||
|
route.items[i].represents(route.initialValue)) {
|
||||||
|
item = Container(
|
||||||
|
color: Theme.of(context).highlightColor,
|
||||||
|
child: item,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
children.add(
|
||||||
|
_MenuItem(
|
||||||
|
onLayout: (Size size) {
|
||||||
|
route.itemSizes[i] = size;
|
||||||
|
},
|
||||||
|
child: FadeTransition(
|
||||||
|
opacity: opacity,
|
||||||
|
child: item,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final CurveTween opacity =
|
||||||
|
CurveTween(curve: const Interval(0.0, 1.0 / 3.0));
|
||||||
|
final CurveTween width = CurveTween(curve: Interval(0.0, unit));
|
||||||
|
final CurveTween height =
|
||||||
|
CurveTween(curve: Interval(0.0, unit * route.items.length));
|
||||||
|
|
||||||
|
final Widget child = ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(
|
||||||
|
minWidth: _kMenuMinWidth,
|
||||||
|
maxWidth: _kMenuMaxWidth,
|
||||||
|
),
|
||||||
|
child: IntrinsicWidth(
|
||||||
|
stepWidth: _kMenuWidthStep,
|
||||||
|
child: Semantics(
|
||||||
|
scopesRoute: true,
|
||||||
|
namesRoute: true,
|
||||||
|
explicitChildNodes: true,
|
||||||
|
label: semanticLabel,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
bottom: _kMenuVerticalPadding
|
||||||
|
),
|
||||||
|
child: ListBody(children: children),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return AnimatedBuilder(
|
||||||
|
animation: route.animation,
|
||||||
|
builder: (BuildContext context, Widget child) {
|
||||||
|
return Opacity(
|
||||||
|
opacity: opacity.evaluate(route.animation),
|
||||||
|
child: Material(
|
||||||
|
shape: route.shape ?? popupMenuTheme.shape,
|
||||||
|
color: route.color ?? popupMenuTheme.color,
|
||||||
|
type: MaterialType.card,
|
||||||
|
elevation: route.elevation ?? popupMenuTheme.elevation ?? 8.0,
|
||||||
|
child: Align(
|
||||||
|
alignment: AlignmentDirectional.topEnd,
|
||||||
|
widthFactor: width.evaluate(route.animation),
|
||||||
|
heightFactor: height.evaluate(route.animation),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PopupMenuRouteLayout extends SingleChildLayoutDelegate {
|
||||||
|
_PopupMenuRouteLayout(this.position, this.itemSizes, this.selectedItemIndex,
|
||||||
|
this.textDirection);
|
||||||
|
|
||||||
|
final RelativeRect position;
|
||||||
|
|
||||||
|
List<Size> itemSizes;
|
||||||
|
|
||||||
|
final int selectedItemIndex;
|
||||||
|
|
||||||
|
final TextDirection textDirection;
|
||||||
|
|
||||||
|
@override
|
||||||
|
BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
|
||||||
|
return BoxConstraints.loose(
|
||||||
|
constraints.biggest -
|
||||||
|
const Offset(_kMenuScreenPadding * 2.0, _kMenuScreenPadding * 2.0)
|
||||||
|
as Size,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Offset getPositionForChild(Size size, Size childSize) {
|
||||||
|
double y = position.top;
|
||||||
|
if (selectedItemIndex != null && itemSizes != null) {
|
||||||
|
double selectedItemOffset =
|
||||||
|
_kMenuVerticalPadding;
|
||||||
|
for (int index = 0; index < selectedItemIndex; index += 1)
|
||||||
|
selectedItemOffset += itemSizes[index].height;
|
||||||
|
selectedItemOffset += itemSizes[selectedItemIndex].height / 2;
|
||||||
|
y = position.top +
|
||||||
|
(size.height - position.top - position.bottom) / 2.0 -
|
||||||
|
selectedItemOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
double x;
|
||||||
|
if (position.left > position.right) {
|
||||||
|
x = size.width - position.right - childSize.width;
|
||||||
|
} else if (position.left < position.right) {
|
||||||
|
// Menu button is closer to the left edge, so grow to the right, aligned to the left edge.
|
||||||
|
x = position.left;
|
||||||
|
} else {
|
||||||
|
// Menu button is equidistant from both edges, so grow in reading direction.
|
||||||
|
assert(textDirection != null);
|
||||||
|
switch (textDirection) {
|
||||||
|
case TextDirection.rtl:
|
||||||
|
x = size.width - position.right - childSize.width;
|
||||||
|
break;
|
||||||
|
case TextDirection.ltr:
|
||||||
|
x = position.left;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (x < _kMenuScreenPadding)
|
||||||
|
x = _kMenuScreenPadding;
|
||||||
|
else if (x + childSize.width > size.width - _kMenuScreenPadding)
|
||||||
|
x = size.width - childSize.width - _kMenuScreenPadding;
|
||||||
|
if (y < _kMenuScreenPadding)
|
||||||
|
y = _kMenuScreenPadding;
|
||||||
|
else if (y + childSize.height > size.height - _kMenuScreenPadding)
|
||||||
|
y = size.height - childSize.height - _kMenuScreenPadding;
|
||||||
|
return Offset(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRelayout(_PopupMenuRouteLayout oldDelegate) {
|
||||||
|
assert(itemSizes.length == oldDelegate.itemSizes.length);
|
||||||
|
|
||||||
|
return position != oldDelegate.position ||
|
||||||
|
selectedItemIndex != oldDelegate.selectedItemIndex ||
|
||||||
|
textDirection != oldDelegate.textDirection ||
|
||||||
|
!listEquals(itemSizes, oldDelegate.itemSizes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PopupMenuRoute<T> extends PopupRoute<T> {
|
||||||
|
_PopupMenuRoute({
|
||||||
|
this.position,
|
||||||
|
this.items,
|
||||||
|
this.initialValue,
|
||||||
|
this.elevation,
|
||||||
|
this.theme,
|
||||||
|
this.popupMenuTheme,
|
||||||
|
this.barrierLabel,
|
||||||
|
this.semanticLabel,
|
||||||
|
this.shape,
|
||||||
|
this.color,
|
||||||
|
this.showMenuContext,
|
||||||
|
this.captureInheritedThemes,
|
||||||
|
}) : itemSizes = List<Size>(items.length);
|
||||||
|
|
||||||
|
final RelativeRect position;
|
||||||
|
final List<PopupMenuEntry<T>> items;
|
||||||
|
final List<Size> itemSizes;
|
||||||
|
final T initialValue;
|
||||||
|
final double elevation;
|
||||||
|
final ThemeData theme;
|
||||||
|
final String semanticLabel;
|
||||||
|
final ShapeBorder shape;
|
||||||
|
final Color color;
|
||||||
|
final PopupMenuThemeData popupMenuTheme;
|
||||||
|
final BuildContext showMenuContext;
|
||||||
|
final bool captureInheritedThemes;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Animation<double> createAnimation() {
|
||||||
|
return CurvedAnimation(
|
||||||
|
parent: super.createAnimation(),
|
||||||
|
curve: Curves.linear,
|
||||||
|
reverseCurve: const Interval(0.0, _kMenuCloseIntervalEnd),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Duration get transitionDuration => _kMenuDuration;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get barrierDismissible => true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Color get barrierColor => null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String barrierLabel;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildPage(BuildContext context, Animation<double> animation,
|
||||||
|
Animation<double> secondaryAnimation) {
|
||||||
|
int selectedItemIndex;
|
||||||
|
if (initialValue != null) {
|
||||||
|
for (int index = 0;
|
||||||
|
selectedItemIndex == null && index < items.length;
|
||||||
|
index += 1) {
|
||||||
|
if (items[index].represents(initialValue)) selectedItemIndex = index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget menu = _PopupMenu<T>(route: this, semanticLabel: semanticLabel);
|
||||||
|
if (captureInheritedThemes) {
|
||||||
|
menu = InheritedTheme.captureAll(showMenuContext, menu);
|
||||||
|
} else {
|
||||||
|
if (theme != null) menu = Theme(data: theme, child: menu);
|
||||||
|
}
|
||||||
|
|
||||||
|
return MediaQuery.removePadding(
|
||||||
|
context: context,
|
||||||
|
removeTop: true,
|
||||||
|
removeBottom: true,
|
||||||
|
removeLeft: true,
|
||||||
|
removeRight: true,
|
||||||
|
child: Builder(
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return CustomSingleChildLayout(
|
||||||
|
delegate: _PopupMenuRouteLayout(
|
||||||
|
position,
|
||||||
|
itemSizes,
|
||||||
|
selectedItemIndex,
|
||||||
|
Directionality.of(context),
|
||||||
|
),
|
||||||
|
child: menu,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<T> _showMenu<T>({
|
||||||
|
@required BuildContext context,
|
||||||
|
@required RelativeRect position,
|
||||||
|
@required List<PopupMenuEntry<T>> items,
|
||||||
|
T initialValue,
|
||||||
|
double elevation,
|
||||||
|
String semanticLabel,
|
||||||
|
ShapeBorder shape,
|
||||||
|
Color color,
|
||||||
|
bool captureInheritedThemes = true,
|
||||||
|
bool useRootNavigator = false,
|
||||||
|
}) {
|
||||||
|
assert(context != null);
|
||||||
|
assert(position != null);
|
||||||
|
assert(useRootNavigator != null);
|
||||||
|
assert(items != null && items.isNotEmpty);
|
||||||
|
assert(captureInheritedThemes != null);
|
||||||
|
assert(debugCheckHasMaterialLocalizations(context));
|
||||||
|
|
||||||
|
String label = semanticLabel;
|
||||||
|
switch (Theme.of(context).platform) {
|
||||||
|
case TargetPlatform.iOS:
|
||||||
|
case TargetPlatform.macOS:
|
||||||
|
label = semanticLabel;
|
||||||
|
break;
|
||||||
|
case TargetPlatform.android:
|
||||||
|
case TargetPlatform.fuchsia:
|
||||||
|
label = semanticLabel ?? MaterialLocalizations.of(context)?.popupMenuLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Navigator.of(context, rootNavigator: useRootNavigator).push(_PopupMenuRoute<T>(
|
||||||
|
position: position,
|
||||||
|
items: items,
|
||||||
|
initialValue: initialValue,
|
||||||
|
elevation: elevation,
|
||||||
|
semanticLabel: label,
|
||||||
|
theme: Theme.of(context, shadowThemeOnly: true),
|
||||||
|
popupMenuTheme: PopupMenuTheme.of(context),
|
||||||
|
barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
|
||||||
|
shape: shape,
|
||||||
|
color: color,
|
||||||
|
showMenuContext: context,
|
||||||
|
captureInheritedThemes: captureInheritedThemes,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class MyPopupMenuButton<T> extends StatefulWidget {
|
||||||
|
/// Creates a button that shows a popup menu.
|
||||||
|
///
|
||||||
|
/// The [itemBuilder] argument must not be null.
|
||||||
|
const MyPopupMenuButton({
|
||||||
|
Key key,
|
||||||
|
@required this.itemBuilder,
|
||||||
|
this.initialValue,
|
||||||
|
this.onSelected,
|
||||||
|
this.onCanceled,
|
||||||
|
this.tooltip,
|
||||||
|
this.elevation,
|
||||||
|
this.padding = const EdgeInsets.all(8.0),
|
||||||
|
this.child,
|
||||||
|
this.icon,
|
||||||
|
this.offset = Offset.zero,
|
||||||
|
this.enabled = true,
|
||||||
|
this.shape,
|
||||||
|
this.color,
|
||||||
|
this.captureInheritedThemes = true,
|
||||||
|
}) : assert(itemBuilder != null),
|
||||||
|
assert(offset != null),
|
||||||
|
assert(enabled != null),
|
||||||
|
assert(captureInheritedThemes != null),
|
||||||
|
assert(!(child != null && icon != null),
|
||||||
|
'You can only pass [child] or [icon], not both.'),
|
||||||
|
super(key: key);
|
||||||
|
|
||||||
|
final PopupMenuItemBuilder<T> itemBuilder;
|
||||||
|
|
||||||
|
final T initialValue;
|
||||||
|
final PopupMenuItemSelected<T> onSelected;
|
||||||
|
|
||||||
|
final PopupMenuCanceled onCanceled;
|
||||||
|
|
||||||
|
final String tooltip;
|
||||||
|
|
||||||
|
final double elevation;
|
||||||
|
|
||||||
|
final EdgeInsetsGeometry padding;
|
||||||
|
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
final Widget icon;
|
||||||
|
|
||||||
|
final Offset offset;
|
||||||
|
final bool enabled;
|
||||||
|
final ShapeBorder shape;
|
||||||
|
|
||||||
|
final Color color;
|
||||||
|
|
||||||
|
final bool captureInheritedThemes;
|
||||||
|
|
||||||
|
@override
|
||||||
|
MyPopupMenuButtonState<T> createState() => MyPopupMenuButtonState<T>();
|
||||||
|
}
|
||||||
|
|
||||||
|
class MyPopupMenuButtonState<T> extends State<MyPopupMenuButton<T>> {
|
||||||
|
void showButtonMenu() {
|
||||||
|
final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context);
|
||||||
|
final RenderBox button = context.findRenderObject() as RenderBox;
|
||||||
|
final RenderBox overlay =
|
||||||
|
Overlay.of(context).context.findRenderObject() as RenderBox;
|
||||||
|
final RelativeRect position = RelativeRect.fromRect(
|
||||||
|
Rect.fromPoints(
|
||||||
|
button.localToGlobal(widget.offset, ancestor: overlay),
|
||||||
|
button.localToGlobal(button.size.bottomRight(Offset.zero),
|
||||||
|
ancestor: overlay),
|
||||||
|
),
|
||||||
|
Offset.zero & overlay.size,
|
||||||
|
);
|
||||||
|
final List<PopupMenuEntry<T>> items = widget.itemBuilder(context);
|
||||||
|
// Only show the menu if there is something to show
|
||||||
|
if (items.isNotEmpty) {
|
||||||
|
_showMenu<T>(
|
||||||
|
context: context,
|
||||||
|
elevation: widget.elevation ?? popupMenuTheme.elevation,
|
||||||
|
items: items,
|
||||||
|
initialValue: widget.initialValue,
|
||||||
|
position: position,
|
||||||
|
shape: widget.shape ?? popupMenuTheme.shape,
|
||||||
|
color: widget.color ?? popupMenuTheme.color,
|
||||||
|
captureInheritedThemes: widget.captureInheritedThemes,
|
||||||
|
).then<void>((T newValue) {
|
||||||
|
if (!mounted) return null;
|
||||||
|
if (newValue == null) {
|
||||||
|
if (widget.onCanceled != null) widget.onCanceled();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (widget.onSelected != null) widget.onSelected(newValue);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Icon _getIcon(TargetPlatform platform) {
|
||||||
|
assert(platform != null);
|
||||||
|
switch (platform) {
|
||||||
|
case TargetPlatform.android:
|
||||||
|
case TargetPlatform.fuchsia:
|
||||||
|
return const Icon(Icons.more_vert);
|
||||||
|
case TargetPlatform.iOS:
|
||||||
|
case TargetPlatform.macOS:
|
||||||
|
return const Icon(Icons.more_horiz);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
assert(debugCheckHasMaterialLocalizations(context));
|
||||||
|
|
||||||
|
if (widget.child != null)
|
||||||
|
return Tooltip(
|
||||||
|
message:
|
||||||
|
widget.tooltip ?? MaterialLocalizations.of(context).showMenuTooltip,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: widget.enabled ? showButtonMenu : null,
|
||||||
|
canRequestFocus: widget.enabled,
|
||||||
|
child: widget.child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return IconButton(
|
||||||
|
icon: widget.icon ?? _getIcon(Theme.of(context).platform),
|
||||||
|
padding: widget.padding,
|
||||||
|
tooltip:
|
||||||
|
widget.tooltip ?? MaterialLocalizations.of(context).showMenuTooltip,
|
||||||
|
onPressed: widget.enabled ? showButtonMenu : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MyPopupMenuItem<int> extends PopupMenuEntry<int> {
|
||||||
|
const MyPopupMenuItem({
|
||||||
|
Key key,
|
||||||
|
this.value,
|
||||||
|
this.enabled = true,
|
||||||
|
this.height = kMinInteractiveDimension,
|
||||||
|
this.textStyle,
|
||||||
|
@required this.child,
|
||||||
|
}) : assert(enabled != null),
|
||||||
|
assert(height != null),
|
||||||
|
super(key: key);
|
||||||
|
|
||||||
|
final int value;
|
||||||
|
|
||||||
|
final bool enabled;
|
||||||
|
|
||||||
|
@override
|
||||||
|
final double height;
|
||||||
|
final TextStyle textStyle;
|
||||||
|
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool represents(int value) => value == this.value;
|
||||||
|
|
||||||
|
@override
|
||||||
|
MyPopupMenuItemState<int, MyPopupMenuItem<int>> createState() =>
|
||||||
|
MyPopupMenuItemState<int, MyPopupMenuItem<int>>();
|
||||||
|
}
|
||||||
|
|
||||||
|
class MyPopupMenuItemState<int, W extends MyPopupMenuItem<int>>
|
||||||
|
extends State<W> {
|
||||||
|
@protected
|
||||||
|
Widget buildChild() => widget.child;
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void handleTap() {
|
||||||
|
Navigator.pop<int>(context, widget.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final ThemeData theme = Theme.of(context);
|
||||||
|
final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context);
|
||||||
|
TextStyle style = widget.textStyle ??
|
||||||
|
popupMenuTheme.textStyle ??
|
||||||
|
theme.textTheme.subtitle1;
|
||||||
|
|
||||||
|
Widget item = AnimatedDefaultTextStyle(
|
||||||
|
style: style,
|
||||||
|
duration: kThemeChangeDuration,
|
||||||
|
child: Container(
|
||||||
|
// alignment: AlignmentDirectional.centerStart,
|
||||||
|
// constraints: BoxConstraints(minHeight: widget.height),
|
||||||
|
padding: const EdgeInsets.all(0),
|
||||||
|
child: buildChild(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return item;
|
||||||
|
// return InkWell(
|
||||||
|
// onTap: widget.enabled ? handleTap : null,
|
||||||
|
// canRequestFocus: widget.enabled,
|
||||||
|
// child: item,
|
||||||
|
// );
|
||||||
|
}
|
||||||
|
}
|
28
lib/util/ompl_build.dart
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import 'package:tsacdop/class/podcastlocal.dart';
|
||||||
|
import 'package:xml/xml.dart' as xml;
|
||||||
|
|
||||||
|
omplBuilder(List<PodcastLocal> podcasts) {
|
||||||
|
var builder = xml.XmlBuilder();
|
||||||
|
builder.processing('xml', 'version="1.0"');
|
||||||
|
builder.element('ompl', nest: () {
|
||||||
|
builder.attribute('version', '1.0');
|
||||||
|
builder.element('head', nest: () {
|
||||||
|
builder.element('title', nest: 'Tsacdop Feeds');
|
||||||
|
});
|
||||||
|
builder.element('body', nest: () {
|
||||||
|
builder.element('outline', nest: () {
|
||||||
|
builder.attribute('text', 'feed');
|
||||||
|
podcasts.forEach((e) => builder.element(
|
||||||
|
'outline',
|
||||||
|
nest: () {
|
||||||
|
builder.attribute('type', 'rss');
|
||||||
|
builder.attribute('text', '${e.title}');
|
||||||
|
builder.attribute('xmlUrl', '${e.rssUrl}');
|
||||||
|
},
|
||||||
|
isSelfClosing: true,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return builder.build();
|
||||||
|
}
|
@ -18,9 +18,9 @@ class RssContent {
|
|||||||
|
|
||||||
factory RssContent.parse(XmlElement element) {
|
factory RssContent.parse(XmlElement element) {
|
||||||
if (element == null) {
|
if (element == null) {
|
||||||
return null;
|
return RssContent('', ['']);
|
||||||
}
|
}
|
||||||
final content = element.text;
|
final content = element.text.trim();
|
||||||
final images = <String>[];
|
final images = <String>[];
|
||||||
_imagesRegExp.allMatches(content).forEach((match) {
|
_imagesRegExp.allMatches(content).forEach((match) {
|
||||||
images.add(match.group(1));
|
images.add(match.group(1));
|
||||||
|
@ -44,20 +44,24 @@ class RssItem {
|
|||||||
});
|
});
|
||||||
|
|
||||||
factory RssItem.parse(XmlElement element) {
|
factory RssItem.parse(XmlElement element) {
|
||||||
|
if (RssEnclosure.parse(findElementOrNull(element, "enclosure")) == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return RssItem(
|
return RssItem(
|
||||||
title: findElementOrNull(element, "title")?.text,
|
title: findElementOrNull(element, "title")?.text,
|
||||||
description: findElementOrNull(element, "description")?.text?.trim() ?? 'No shownote provided for this episode',
|
description: findElementOrNull(element, "description")?.text?.trim()
|
||||||
|
,
|
||||||
link: findElementOrNull(element, "link")?.text?.trim(),
|
link: findElementOrNull(element, "link")?.text?.trim(),
|
||||||
categories: element.findElements("category").map((element) {
|
categories: element.findElements("category").map((element) {
|
||||||
return RssCategory.parse(element);
|
return RssCategory.parse(element);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
//guid: findElementOrNull(element, "guid")?.text,
|
//guid: findElementOrNull(element, "guid")?.text,
|
||||||
pubDate: findElementOrNull(element, "pubDate")?.text?.trim(),
|
pubDate: findElementOrNull(element, "pubDate")?.text?.trim(),
|
||||||
author: findElementOrNull(element, "author")?.text?.trim(),
|
author: findElementOrNull(element, "author")?.text?.trim(),
|
||||||
// comments: findElementOrNull(element, "comments")?.text,
|
// comments: findElementOrNull(element, "comments")?.text,
|
||||||
// source: RssSource.parse(findElementOrNull(element, "source")),
|
// source: RssSource.parse(findElementOrNull(element, "source")),
|
||||||
// content: RssContent.parse(findElementOrNull(element, "content:encoded")),
|
content: RssContent.parse(findElementOrNull(element, "content:encoded")),
|
||||||
// media: Media.parse(element),
|
// media: Media.parse(element),
|
||||||
enclosure: RssEnclosure.parse(findElementOrNull(element, "enclosure")),
|
enclosure: RssEnclosure.parse(findElementOrNull(element, "enclosure")),
|
||||||
//dc: DublinCore.parse(element),
|
//dc: DublinCore.parse(element),
|
||||||
itunes: RssItemItunes.parse(element),
|
itunes: RssItemItunes.parse(element),
|
||||||
|
@ -11,7 +11,7 @@ description: An easy-use podacasts player.
|
|||||||
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
||||||
# Read more about iOS versioning at
|
# Read more about iOS versioning at
|
||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
version: 0.1.2
|
version: 0.1.4
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.6.0 <3.0.0"
|
sdk: ">=2.6.0 <3.0.0"
|
||||||
@ -58,6 +58,7 @@ dev_dependencies:
|
|||||||
line_icons:
|
line_icons:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/galonsos/line_icons.git
|
url: https://github.com/galonsos/line_icons.git
|
||||||
|
flutter_file_dialog: ^0.0.5
|
||||||
|
|
||||||
|
|
||||||
# For information on the generic Dart part of this file, see the
|
# For information on the generic Dart part of this file, see the
|
||||||
|
@ -5,7 +5,6 @@
|
|||||||
// gestures. You can also use WidgetTester to find child widgets in the widget
|
// gestures. You can also use WidgetTester to find child widgets in the widget
|
||||||
// tree, read text, and verify that the values of widget properties are correct.
|
// tree, read text, and verify that the values of widget properties are correct.
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
import 'package:tsacdop/main.dart';
|
import 'package:tsacdop/main.dart';
|
||||||
|
11
tool/env.dart
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
Future<void> main() async {
|
||||||
|
final config = {
|
||||||
|
'apiKey': Platform.environment['API_KEY'],
|
||||||
|
};
|
||||||
|
|
||||||
|
final filename = 'lib/.env.dart';
|
||||||
|
File(filename).writeAsString('final environment = ${json.encode(config)};');
|
||||||
|
}
|