Export ompli file
Storage management Syncing setting
@ -16,7 +16,7 @@ jobs:
|
||||
|
||||
- run: echo $ENCODED_KEYSTORE | base64 -di > ${HOME}/keystore.jks
|
||||
- run: echo 'export KEYSTORE=${HOME}/keystore.jks' >> $BASH_ENV
|
||||
|
||||
- run: dart tool/env.dart
|
||||
- run:
|
||||
name: Build the Android version
|
||||
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"/>
|
||||
</br>
|
||||
<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.
|
||||
|
||||
|
@ -45,7 +45,7 @@ android {
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId "com.stonegate.tsacdop"
|
||||
minSdkVersion 16
|
||||
minSdkVersion 19
|
||||
targetSdkVersion 28
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
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
|
||||
calls FlutterMain.startInitialization(this); in its onCreate method.
|
||||
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.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||
<application android:name="io.flutter.app.FlutterApplication" android:label="Tsacdop" android:icon="@mipmap/ic_launcher" android:networkSecurityConfig="@xml/network_security_config">
|
||||
<application android:name="io.flutter.app.FlutterApplication" android:label="Tsacdop" android:icon="@mipmap/ic_launcher_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">
|
||||
<intent-filter>
|
||||
<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
|
||||
android:gravity="center"
|
||||
android:tileMode="disabled"
|
||||
android:src="@mipmap/ic_launcher" />
|
||||
android:src="@mipmap/ic_splash" />
|
||||
</item>
|
||||
<item android:bottom="100dp">
|
||||
<bitmap
|
||||
|
@ -7,7 +7,7 @@
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:tileMode="disabled"
|
||||
android:src="@mipmap/ic_launcher" />
|
||||
android:src="@mipmap/ic_splash" />
|
||||
</item>
|
||||
<item android:bottom="100dp">
|
||||
<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">
|
||||
#121212
|
||||
</color>
|
||||
<color name="ic_launcher_background">
|
||||
#ffffff
|
||||
</color>
|
||||
</resources>
|
@ -80,8 +80,7 @@ class Playlist {
|
||||
_playlist = [];
|
||||
await Future.forEach(urls, (url) async {
|
||||
EpisodeBrief episode = await dbHelper.getRssItemWithUrl(url);
|
||||
print(episode.title);
|
||||
_playlist.add(episode);
|
||||
if(episode != null) _playlist.add(episode);
|
||||
});
|
||||
}
|
||||
print('Playlist: ' + _playlist.length.toString());
|
||||
@ -127,8 +126,10 @@ class AudioPlayerNotifier extends ChangeNotifier {
|
||||
int _lastPostion = 0;
|
||||
bool _stopOnComplete = false;
|
||||
Timer _stopTimer;
|
||||
int _timeLeft = 0;
|
||||
//Show stopwatch after user setting timer.
|
||||
bool _showStopWatch = false;
|
||||
double _switchValue = 0;
|
||||
bool _autoPlay = true;
|
||||
DateTime _current;
|
||||
int _currentPosition;
|
||||
@ -146,11 +147,18 @@ class AudioPlayerNotifier extends ChangeNotifier {
|
||||
bool get stopOnComplete => _stopOnComplete;
|
||||
bool get showStopWatch => _showStopWatch;
|
||||
bool get autoPlay => _autoPlay;
|
||||
int get timeLeft => _timeLeft;
|
||||
double get switchValue => _switchValue;
|
||||
|
||||
set setStopOnComplete(bool boo) {
|
||||
_stopOnComplete = boo;
|
||||
set setSwitchValue(double value) {
|
||||
_switchValue = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// set setStopOnComplete(bool boo) {
|
||||
// _stopOnComplete = boo;
|
||||
//}
|
||||
|
||||
set autoPlaySwitch(bool boo) {
|
||||
_autoPlay = boo;
|
||||
notifyListeners();
|
||||
@ -203,12 +211,12 @@ class AudioPlayerNotifier extends ChangeNotifier {
|
||||
backgroundTaskEntrypoint: _audioPlayerTaskEntrypoint,
|
||||
androidNotificationChannelName: 'Tsacdop',
|
||||
notificationColor: 0xFF2196f3,
|
||||
androidNotificationIcon: 'mipmap/ic_launcher',
|
||||
androidNotificationIcon: 'drawable/ic_notification',
|
||||
enableQueue: true,
|
||||
androidStopOnRemoveTask: true,
|
||||
);
|
||||
_playerRunning = true;
|
||||
if (autoPlay) {
|
||||
if (_autoPlay) {
|
||||
await Future.forEach(_queue.playlist, (episode) async {
|
||||
await AudioService.addQueueItem(episode.toMediaItem());
|
||||
});
|
||||
@ -217,8 +225,6 @@ class AudioPlayerNotifier extends ChangeNotifier {
|
||||
}
|
||||
await AudioService.play();
|
||||
AudioService.currentMediaItemStream.listen((item) async {
|
||||
print(position);
|
||||
print(_backgroundAudioDuration);
|
||||
if (item != null) {
|
||||
_episode = await dbHelper.getRssItemWithMediaId(item.id);
|
||||
_backgroundAudioDuration = item?.duration ?? 0;
|
||||
@ -226,7 +232,6 @@ class AudioPlayerNotifier extends ChangeNotifier {
|
||||
AudioService.seekTo(position);
|
||||
position = 0;
|
||||
}
|
||||
// _playerRunning = true;
|
||||
}
|
||||
notifyListeners();
|
||||
});
|
||||
@ -235,6 +240,7 @@ class AudioPlayerNotifier extends ChangeNotifier {
|
||||
_audioState = event?.basicState;
|
||||
if (_audioState == BasicPlaybackState.skippingToNext &&
|
||||
_episode != null) {
|
||||
print(_episode.title);
|
||||
_queue.delFromPlaylist(_episode);
|
||||
}
|
||||
if (_audioState == BasicPlaybackState.paused ||
|
||||
@ -253,7 +259,7 @@ class AudioPlayerNotifier extends ChangeNotifier {
|
||||
Timer.periodic(Duration(milliseconds: 500), (timer) {
|
||||
if (_noSlide) {
|
||||
_audioState == BasicPlaybackState.playing
|
||||
? (_backgroundAudioPosition < _backgroundAudioDuration)
|
||||
? (_backgroundAudioPosition < _backgroundAudioDuration - 500)
|
||||
? _backgroundAudioPosition = _currentPosition +
|
||||
DateTime.now().difference(_current).inMilliseconds
|
||||
: _backgroundAudioPosition = _backgroundAudioDuration
|
||||
@ -269,6 +275,19 @@ class AudioPlayerNotifier extends ChangeNotifier {
|
||||
_lastPostion = _backgroundAudioPosition;
|
||||
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();
|
||||
}
|
||||
if (_audioState == BasicPlaybackState.stopped) {
|
||||
@ -327,6 +346,17 @@ class AudioPlayerNotifier extends ChangeNotifier {
|
||||
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 {
|
||||
AudioService.pause();
|
||||
}
|
||||
@ -353,10 +383,22 @@ class AudioPlayerNotifier extends ChangeNotifier {
|
||||
//Set sleep time
|
||||
sleepTimer(int mins) {
|
||||
_showStopWatch = true;
|
||||
_switchValue = 1;
|
||||
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), () {
|
||||
_stopOnComplete = false;
|
||||
_showStopWatch = false;
|
||||
_switchValue = 0;
|
||||
AudioService.stop();
|
||||
notifyListeners();
|
||||
});
|
||||
@ -365,7 +407,9 @@ class AudioPlayerNotifier extends ChangeNotifier {
|
||||
//Cancel sleep timer
|
||||
cancelTimer() {
|
||||
_stopTimer.cancel();
|
||||
_timeLeft = 0;
|
||||
_showStopWatch = false;
|
||||
_switchValue = 0;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@ -393,7 +437,7 @@ class AudioPlayerTask extends BackgroundAudioTask {
|
||||
case AudioPlaybackState.none:
|
||||
return BasicPlaybackState.none;
|
||||
case AudioPlaybackState.stopped:
|
||||
return BasicPlaybackState.stopped;
|
||||
return _skipState ?? BasicPlaybackState.stopped;
|
||||
case AudioPlaybackState.paused:
|
||||
return BasicPlaybackState.paused;
|
||||
case AudioPlaybackState.playing:
|
||||
@ -438,6 +482,10 @@ class AudioPlayerTask extends BackgroundAudioTask {
|
||||
if (hasNext) {
|
||||
onSkipToNext();
|
||||
} else {
|
||||
_skipState = BasicPlaybackState.skippingToNext;
|
||||
_audioPlayer.stop();
|
||||
_queue.removeAt(0);
|
||||
_skipState = null;
|
||||
onStop();
|
||||
}
|
||||
}
|
||||
@ -459,20 +507,24 @@ class AudioPlayerTask extends BackgroundAudioTask {
|
||||
await _audioPlayer.stop();
|
||||
_queue.removeAt(0);
|
||||
}
|
||||
AudioServiceBackground.setQueue(_queue);
|
||||
AudioServiceBackground.setMediaItem(mediaItem);
|
||||
_skipState = BasicPlaybackState.skippingToNext;
|
||||
await _audioPlayer.setUrl(mediaItem.id);
|
||||
print(mediaItem.id);
|
||||
Duration duration = await _audioPlayer.durationFuture;
|
||||
AudioServiceBackground.setMediaItem(
|
||||
mediaItem.copyWith(duration: duration.inMilliseconds));
|
||||
_skipState = null;
|
||||
// Resume playback if we were playing
|
||||
if (_playing) {
|
||||
onPlay();
|
||||
if (_queue.length == 0) {
|
||||
onStop();
|
||||
} 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() {
|
||||
DateTime date = DateTime.fromMillisecondsSinceEpoch(pubDate, isUtc: true);
|
||||
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';
|
||||
} else if (diffrence.inHours == 24) {
|
||||
return '1 day ago';
|
||||
} else if (diffrence.inDays < 7) {
|
||||
return '${diffrence.inDays} days ago';
|
||||
} else {
|
||||
|
@ -3,12 +3,15 @@ class PodcastLocal {
|
||||
final String imageUrl;
|
||||
final String rssUrl;
|
||||
final String author;
|
||||
String description;
|
||||
|
||||
final String primaryColor;
|
||||
final String id;
|
||||
final String imagePath;
|
||||
final String provider;
|
||||
final String link;
|
||||
|
||||
final String description;
|
||||
final int upateCount;
|
||||
PodcastLocal(
|
||||
this.title,
|
||||
this.imageUrl,
|
||||
@ -18,5 +21,10 @@ class PodcastLocal {
|
||||
this.id,
|
||||
this.imagePath,
|
||||
this.provider,
|
||||
this.link);
|
||||
this.link,
|
||||
{
|
||||
this.description ='',
|
||||
this.upateCount = 0,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -1,17 +1,38 @@
|
||||
import 'package:flutter/foundation.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';
|
||||
|
||||
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 {
|
||||
KeyValueStorage themestorage = KeyValueStorage('themes');
|
||||
KeyValueStorage accentstorage = KeyValueStorage('accents');
|
||||
KeyValueStorage autoupdatestorage = KeyValueStorage('autoupdate');
|
||||
KeyValueStorage intervalstorage = KeyValueStorage('updateInterval');
|
||||
|
||||
Future initData() async {
|
||||
await _getTheme();
|
||||
await _getAccentSetColor();
|
||||
await _getAutoUpdate();
|
||||
}
|
||||
|
||||
ThemeMode _theme;
|
||||
@ -23,6 +44,28 @@ class SettingState extends ChangeNotifier {
|
||||
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 get accentSetColor => _accentSetColor;
|
||||
|
||||
@ -32,6 +75,10 @@ class SettingState extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
int _updateInterval;
|
||||
int get updateInterval => _updateInterval;
|
||||
|
||||
int _initUpdateTag;
|
||||
bool _autoUpdate;
|
||||
bool get autoUpdate => _autoUpdate;
|
||||
set autoUpdate(bool boo) {
|
||||
@ -46,18 +93,21 @@ class SettingState extends ChangeNotifier {
|
||||
_getTheme();
|
||||
_getAccentSetColor();
|
||||
_getAutoUpdate();
|
||||
_getUpdateInterval().then((value) {
|
||||
if (_initUpdateTag == 0) setWorkManager(24);
|
||||
});
|
||||
}
|
||||
|
||||
_getTheme() async {
|
||||
Future _getTheme() async {
|
||||
int mode = await themestorage.getInt();
|
||||
_theme = ThemeMode.values[mode];
|
||||
}
|
||||
|
||||
_saveTheme() async {
|
||||
Future _saveTheme() async {
|
||||
await themestorage.saveInt(_theme.index);
|
||||
}
|
||||
|
||||
_getAccentSetColor() async {
|
||||
Future _getAccentSetColor() async {
|
||||
String colorString = await accentstorage.getString();
|
||||
print(colorString);
|
||||
if (colorString.isNotEmpty) {
|
||||
@ -69,17 +119,26 @@ class SettingState extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
_saveAccentSetColor() async {
|
||||
Future _saveAccentSetColor() async {
|
||||
await accentstorage
|
||||
.saveString(_accentSetColor.toString().substring(10, 16));
|
||||
}
|
||||
|
||||
_getAutoUpdate() async {
|
||||
Future _getAutoUpdate() async {
|
||||
int i = await autoupdatestorage.getInt();
|
||||
_autoUpdate = i == 0 ? false : true;
|
||||
_autoUpdate = i == 0 ? true : false;
|
||||
}
|
||||
|
||||
_saveAutoUpdate() async {
|
||||
await autoupdatestorage.saveInt(_autoUpdate ? 1 : 0);
|
||||
Future _saveAutoUpdate() async {
|
||||
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;
|
||||
Future getSDescription(String url) async {
|
||||
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)
|
||||
setState(() {
|
||||
_loaddes = true;
|
||||
@ -82,159 +83,179 @@ class _EpisodeDetailState extends State<EpisodeDetail> {
|
||||
value: SystemUiOverlayStyle(
|
||||
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
|
||||
systemNavigationBarColor: Theme.of(context).primaryColor,
|
||||
statusBarColor: Theme.of(context).primaryColor,
|
||||
systemNavigationBarIconBrightness:
|
||||
Theme.of(context).accentColorBrightness,
|
||||
// statusBarColor: Theme.of(context).primaryColor,
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Scaffold(
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
appBar: AppBar(
|
||||
title: Text(widget.episodeItem.feedTitle),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: Stack(
|
||||
children: <Widget>[
|
||||
Container(
|
||||
color: Theme.of(context).primaryColor,
|
||||
padding: EdgeInsets.all(10.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.0),
|
||||
alignment: Alignment.topLeft,
|
||||
child: Text(
|
||||
widget.episodeItem.title,
|
||||
style: Theme.of(context).textTheme.headline5,
|
||||
),
|
||||
child: Scaffold(
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
appBar: AppBar(
|
||||
title: Text(widget.episodeItem.feedTitle),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: Stack(
|
||||
children: <Widget>[
|
||||
Container(
|
||||
color: Theme.of(context).primaryColor,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20.0),
|
||||
alignment: Alignment.topLeft,
|
||||
child: Text(
|
||||
widget.episodeItem.title,
|
||||
style: Theme.of(context).textTheme.headline5,
|
||||
),
|
||||
Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.0),
|
||||
height: 30.0,
|
||||
child: Text(
|
||||
'Published ' +
|
||||
DateFormat.yMMMd().format(
|
||||
DateTime.fromMillisecondsSinceEpoch(
|
||||
widget.episodeItem.pubDate)),
|
||||
style: TextStyle(color: Colors.blue[500])),
|
||||
),
|
||||
Container(
|
||||
padding: EdgeInsets.all(12.0),
|
||||
height: 50.0,
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
(widget.episodeItem.explicit == 1)
|
||||
? Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red[800],
|
||||
shape: BoxShape.circle),
|
||||
height: 25.0,
|
||||
width: 25.0,
|
||||
margin: EdgeInsets.only(right: 10.0),
|
||||
alignment: Alignment.center,
|
||||
child: Text('E',
|
||||
style:
|
||||
TextStyle(color: Colors.white)))
|
||||
: Center(),
|
||||
widget.episodeItem.duration != 0
|
||||
? Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.cyan[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.duration)
|
||||
.toString() +
|
||||
'mins',
|
||||
style: textstyle),
|
||||
)
|
||||
: Center(),
|
||||
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(
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: EdgeInsets.symmetric(horizontal: 20.0),
|
||||
height: 30.0,
|
||||
child: Text(
|
||||
'Published ' +
|
||||
DateFormat.yMMMd().format(
|
||||
DateTime.fromMillisecondsSinceEpoch(
|
||||
widget.episodeItem.pubDate)),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).accentColor)),
|
||||
),
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20.0),
|
||||
height: 50.0,
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
(widget.episodeItem.explicit == 1)
|
||||
? Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red[800],
|
||||
shape: BoxShape.circle),
|
||||
height: 25.0,
|
||||
width: 25.0,
|
||||
margin: EdgeInsets.only(right: 10.0),
|
||||
alignment: Alignment.center,
|
||||
child: Text('E',
|
||||
style:
|
||||
TextStyle(color: Colors.white)))
|
||||
: Center(),
|
||||
widget.episodeItem.duration != 0
|
||||
? Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.cyan[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.duration)
|
||||
.toString() +
|
||||
'mins',
|
||||
style: textstyle),
|
||||
)
|
||||
: Container(
|
||||
alignment: Alignment.topLeft,
|
||||
child:
|
||||
Text(widget.episodeItem.description))
|
||||
: Center(),
|
||||
: Center(),
|
||||
widget.episodeItem.enclosureLength != null
|
||||
? Container(
|
||||
decoration: BoxDecoration(
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
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()),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
Container(child: PlayerWidget()),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -368,9 +389,9 @@ class _MenuBarState extends State<MenuBar> {
|
||||
),
|
||||
Spacer(),
|
||||
// Text(audio.audioState.toString()),
|
||||
Selector<AudioPlayerNotifier, Tuple2<EpisodeBrief, BasicPlaybackState>>(
|
||||
selector: (_, audio) =>
|
||||
Tuple2(audio.episode, audio.audioState),
|
||||
Selector<AudioPlayerNotifier,
|
||||
Tuple2<EpisodeBrief, BasicPlaybackState>>(
|
||||
selector: (_, audio) => Tuple2(audio.episode, audio.audioState),
|
||||
builder: (_, data, __) {
|
||||
return (widget.episodeItem.title != data.item1?.title)
|
||||
? Material(
|
||||
@ -485,7 +506,8 @@ class _LineLoaderState extends State<LineLoader>
|
||||
|
||||
@override
|
||||
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
|
||||
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:url_launcher/url_launcher.dart';
|
||||
import 'package:line_icons/line_icons.dart';
|
||||
|
||||
class AboutApp extends StatelessWidget {
|
||||
_launchUrl(String url) async {
|
||||
if (await canLaunch(url)) {
|
||||
@ -44,15 +45,16 @@ class AboutApp extends StatelessWidget {
|
||||
value: SystemUiOverlayStyle(
|
||||
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
|
||||
systemNavigationBarColor: Theme.of(context).primaryColor,
|
||||
statusBarColor: Theme.of(context).primaryColor,
|
||||
systemNavigationBarIconBrightness:
|
||||
Theme.of(context).accentColorBrightness,
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Scaffold(
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
appBar: AppBar(
|
||||
title: Text('About'),
|
||||
),
|
||||
body: Container(
|
||||
child: Scaffold(
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
appBar: AppBar(
|
||||
title: Text('About'),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(20),
|
||||
alignment: Alignment.topLeft,
|
||||
child: Column(
|
||||
@ -70,7 +72,7 @@ class AboutApp extends StatelessWidget {
|
||||
image: AssetImage('assets/logo.png'),
|
||||
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),
|
||||
height: 50,
|
||||
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,
|
||||
),
|
||||
),
|
||||
@ -86,13 +88,11 @@ class AboutApp extends StatelessWidget {
|
||||
padding: EdgeInsets.all(5.0),
|
||||
),
|
||||
Container(
|
||||
padding: EdgeInsets.only(
|
||||
top: 20.0,
|
||||
bottom: 10.0
|
||||
),
|
||||
padding: EdgeInsets.only(top: 20.0, bottom: 10.0),
|
||||
decoration: BoxDecoration(
|
||||
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(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
@ -107,20 +107,14 @@ class AboutApp extends StatelessWidget {
|
||||
TextStyle(color: Theme.of(context).accentColor),
|
||||
),
|
||||
),
|
||||
_listItem(
|
||||
context,
|
||||
'GitHub',
|
||||
LineIcons.github,
|
||||
_listItem(context, 'GitHub', LineIcons.github,
|
||||
'https://github.com/stonaga/'),
|
||||
_listItem(
|
||||
context,
|
||||
'Twitter',
|
||||
LineIcons.twitter,
|
||||
_listItem(context, 'Twitter', LineIcons.twitter,
|
||||
'https://twitter.com'),
|
||||
_listItem(
|
||||
context,
|
||||
'Stone Gate',
|
||||
LineIcons.hat_cowboy_solid,
|
||||
LineIcons.hat_cowboy_solid,
|
||||
'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/appbar/popupmenu.dart';
|
||||
import 'package:tsacdop/webfeed/webfeed.dart';
|
||||
import 'package:tsacdop/.env.dart';
|
||||
|
||||
class MyHomePage extends StatefulWidget {
|
||||
@override
|
||||
@ -30,42 +31,47 @@ class MyHomePage extends StatefulWidget {
|
||||
class _MyHomePageState extends State<MyHomePage> {
|
||||
final _MyHomePageDelegate _delegate = _MyHomePageDelegate();
|
||||
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnnotatedRegion<SystemUiOverlayStyle>(
|
||||
value: SystemUiOverlayStyle(
|
||||
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
|
||||
systemNavigationBarIconBrightness: Theme.of(context).accentColorBrightness,
|
||||
systemNavigationBarIconBrightness:
|
||||
Theme.of(context).accentColorBrightness,
|
||||
systemNavigationBarColor: Theme.of(context).primaryColor,
|
||||
statusBarColor: Theme.of(context).primaryColor,
|
||||
// statusBarColor: Theme.of(context).primaryColor,
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Scaffold(
|
||||
key: _scaffoldKey,
|
||||
appBar: AppBar(
|
||||
centerTitle: true,
|
||||
leading: IconButton(
|
||||
tooltip: 'Add',
|
||||
icon: const Icon(Icons.add_circle_outline),
|
||||
onPressed: () async {
|
||||
await showSearch<int>(
|
||||
context: context,
|
||||
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(),
|
||||
],
|
||||
child: Scaffold(
|
||||
key: _scaffoldKey,
|
||||
appBar: AppBar(
|
||||
centerTitle: true,
|
||||
leading: IconButton(
|
||||
tooltip: 'Add',
|
||||
icon: const Icon(Icons.add_circle_outline),
|
||||
onPressed: () async {
|
||||
await showSearch<int>(
|
||||
context: context,
|
||||
delegate: _delegate,
|
||||
);
|
||||
},
|
||||
),
|
||||
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> {
|
||||
static Future<List> getList(String searchText) async {
|
||||
String apiKey = environment['apiKey'];
|
||||
print(apiKey);
|
||||
String url =
|
||||
"https://listennotes.p.mashape.com/api/v1/search?only_in=title%2Cdescription&q=" +
|
||||
searchText +
|
||||
"&sort_by_date=0&type=podcast";
|
||||
Response response = await Dio().get(url,
|
||||
options: Options(headers: {
|
||||
'X-Mashape-Key': "UtSwKG4afSmshZfglwsXylLKJZHgp1aZHi2jsnSYK5mZi0A32T",
|
||||
'X-Mashape-Key': "$apiKey",
|
||||
'Accept': "application/json"
|
||||
}));
|
||||
Map searchResultMap = jsonDecode(response.toString());
|
||||
@ -112,7 +120,8 @@ class _MyHomePageDelegate extends SearchDelegate<int> {
|
||||
padding: EdgeInsets.only(top: 400),
|
||||
child: Image(
|
||||
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,
|
||||
),
|
||||
));
|
||||
@ -236,7 +245,11 @@ class _SearchResultState extends State<SearchResult> {
|
||||
|
||||
importOmpl.importState = ImportState.import;
|
||||
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();
|
||||
String _realUrl = response.realUri.toString();
|
||||
|
||||
@ -276,8 +289,8 @@ class _SearchResultState extends State<SearchResult> {
|
||||
_uuid,
|
||||
_imagePath,
|
||||
_provider,
|
||||
_link);
|
||||
podcastLocal.description = _p.description;
|
||||
_link,
|
||||
description: _p.description);
|
||||
await groupList.subscribe(podcastLocal);
|
||||
|
||||
if (_provider.contains('fireside')) {
|
||||
@ -347,28 +360,48 @@ class _SearchResultState extends State<SearchResult> {
|
||||
? Icons.keyboard_arrow_up
|
||||
: Icons.keyboard_arrow_down),
|
||||
Padding(padding: EdgeInsets.only(right: 10.0)),
|
||||
!_issubscribe
|
||||
? !_adding
|
||||
? OutlineButton(
|
||||
child: Text('Subscribe',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).accentColor)),
|
||||
onPressed: () {
|
||||
importOmpl.rssTitle = widget.onlinePodcast.title;
|
||||
savePodcast(widget.onlinePodcast.rss);
|
||||
})
|
||||
: OutlineButton(
|
||||
child: SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor:
|
||||
AlwaysStoppedAnimation(Colors.blue),
|
||||
)),
|
||||
onPressed: () {},
|
||||
)
|
||||
: OutlineButton(child: Text('Subscribe'), onPressed: () {}),
|
||||
Container(
|
||||
width: 100,
|
||||
height: 35,
|
||||
child: !_issubscribe
|
||||
? !_adding
|
||||
? OutlineButton(
|
||||
highlightedBorderColor:
|
||||
Theme.of(context).accentColor,
|
||||
splashColor: Theme.of(context)
|
||||
.accentColor
|
||||
.withOpacity(0.8),
|
||||
child: Text('Subscribe',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).accentColor)),
|
||||
onPressed: () {
|
||||
importOmpl.rssTitle =
|
||||
widget.onlinePodcast.title;
|
||||
savePodcast(widget.onlinePodcast.rss);
|
||||
})
|
||||
: OutlineButton(
|
||||
highlightedBorderColor:
|
||||
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:provider/provider.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:file_picker/file_picker.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@ -14,6 +15,7 @@ import 'package:image/image.dart' as img;
|
||||
import 'package:uuid/uuid.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:line_icons/line_icons.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import 'package:tsacdop/class/podcast_group.dart';
|
||||
import 'package:tsacdop/settings/settting.dart';
|
||||
@ -37,8 +39,13 @@ class OmplOutline {
|
||||
}
|
||||
}
|
||||
|
||||
class PopupMenu extends StatelessWidget {
|
||||
Future<String> getColor(File file) async {
|
||||
class PopupMenu extends StatefulWidget {
|
||||
@override
|
||||
_PopupMenuState createState() => _PopupMenuState();
|
||||
}
|
||||
|
||||
class _PopupMenuState extends State<PopupMenu> {
|
||||
Future<String> _getColor(File file) async {
|
||||
final imageProvider = FileImage(file);
|
||||
var colorImage = await getImageFromProvider(imageProvider);
|
||||
var color = await getColorFromImage(colorImage);
|
||||
@ -46,35 +53,85 @@ class PopupMenu extends StatelessWidget {
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
ImportOmpl importOmpl = Provider.of<ImportOmpl>(context, listen: false);
|
||||
GroupList groupList = Provider.of<GroupList>(context, listen: false);
|
||||
|
||||
|
||||
_refreshAll() async {
|
||||
var dbHelper = DBHelper();
|
||||
List<PodcastLocal> podcastList = await dbHelper.getPodcastLocalAll();
|
||||
int i = 0;
|
||||
await Future.forEach(podcastList, (podcastLocal) async {
|
||||
importOmpl.rssTitle = podcastLocal.title;
|
||||
importOmpl.importState = ImportState.parse;
|
||||
await dbHelper.updatePodcastRss(podcastLocal);
|
||||
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);
|
||||
importOmpl.importState = ImportState.complete;
|
||||
}
|
||||
|
||||
saveOmpl(String rss) async {
|
||||
var dbHelper = DBHelper();
|
||||
importOmpl.importState = ImportState.import;
|
||||
|
||||
Response response = await Dio().get(rss);
|
||||
BaseOptions options = new BaseOptions(
|
||||
connectTimeout: 20000,
|
||||
receiveTimeout: 20000,
|
||||
);
|
||||
Response response = await Dio(options).get(rss);
|
||||
if (response.statusCode == 200) {
|
||||
var _p = RssFeed.parse(response.data);
|
||||
|
||||
|
||||
var dir = await getApplicationDocumentsDirectory();
|
||||
|
||||
String _realUrl = response.redirects.isEmpty ? rss : response.realUri.toString();
|
||||
|
||||
|
||||
String _realUrl =
|
||||
response.redirects.isEmpty ? rss : response.realUri.toString();
|
||||
|
||||
print(_realUrl);
|
||||
bool _checkUrl = await dbHelper.checkPodcast(_realUrl);
|
||||
|
||||
@ -87,9 +144,10 @@ class PopupMenu extends StatelessWidget {
|
||||
String _uuid = Uuid().v4();
|
||||
File("${dir.path}/$_uuid.png")
|
||||
..writeAsBytesSync(img.encodePng(thumbnail));
|
||||
|
||||
|
||||
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 _provider = _p.generator ?? '';
|
||||
String _link = _p.link ?? '';
|
||||
@ -102,14 +160,12 @@ class PopupMenu extends StatelessWidget {
|
||||
_uuid,
|
||||
_imagePath,
|
||||
_provider,
|
||||
_link);
|
||||
|
||||
podcastLocal.description = _p.description;
|
||||
_link,
|
||||
description: _p.description);
|
||||
|
||||
await groupList.subscribe(podcastLocal);
|
||||
|
||||
if (_provider.contains('fireside'))
|
||||
{
|
||||
if (_provider.contains('fireside')) {
|
||||
FiresideData data = FiresideData(_uuid, _link);
|
||||
await data.fatchData();
|
||||
}
|
||||
@ -143,33 +199,34 @@ class PopupMenu extends StatelessWidget {
|
||||
|
||||
void _saveOmpl(String path) async {
|
||||
File file = File(path);
|
||||
try{String opml = file.readAsStringSync();
|
||||
try {
|
||||
String opml = file.readAsStringSync();
|
||||
|
||||
var content = xml.parse(opml);
|
||||
var total = content
|
||||
.findAllElements('outline')
|
||||
.map((ele) => OmplOutline.parse(ele))
|
||||
.toList();
|
||||
if (total.length == 0) {
|
||||
Fluttertoast.showToast(
|
||||
msg: 'File Not Valid',
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
);
|
||||
} else {
|
||||
for (int i = 0; i < total.length; i++) {
|
||||
if (total[i].xmlUrl != null) {
|
||||
importOmpl.rssTitle = total[i].text;
|
||||
try {
|
||||
await saveOmpl(total[i].xmlUrl);
|
||||
} catch (e) {
|
||||
print(e.toString());
|
||||
var content = xml.parse(opml);
|
||||
var total = content
|
||||
.findAllElements('outline')
|
||||
.map((ele) => OmplOutline.parse(ele))
|
||||
.toList();
|
||||
if (total.length == 0) {
|
||||
Fluttertoast.showToast(
|
||||
msg: 'File Not Valid',
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
);
|
||||
} else {
|
||||
for (int i = 0; i < total.length; i++) {
|
||||
if (total[i].xmlUrl != null) {
|
||||
importOmpl.rssTitle = total[i].text;
|
||||
try {
|
||||
await saveOmpl(total[i].xmlUrl);
|
||||
} catch (e) {
|
||||
print(e.toString());
|
||||
}
|
||||
print(total[i].text);
|
||||
}
|
||||
print(total[i].text);
|
||||
}
|
||||
print('Import fisnished');
|
||||
}
|
||||
print('Import fisnished');
|
||||
}}
|
||||
catch(e){
|
||||
} catch (e) {
|
||||
print(e);
|
||||
Fluttertoast.showToast(
|
||||
msg: 'File error, Subscribe failed',
|
||||
@ -195,7 +252,8 @@ class PopupMenu extends StatelessWidget {
|
||||
}
|
||||
|
||||
return PopupMenuButton<int>(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10))),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(10))),
|
||||
elevation: 1,
|
||||
tooltip: 'Menu',
|
||||
itemBuilder: (context) => [
|
||||
@ -204,40 +262,60 @@ class PopupMenu extends StatelessWidget {
|
||||
child: Container(
|
||||
padding: EdgeInsets.only(left: 10),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Icon(LineIcons.cloud_download_alt_solid),
|
||||
Padding(padding: EdgeInsets.symmetric(horizontal: 5.0),),
|
||||
Text('Refresh All'),
|
||||
Padding(
|
||||
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(
|
||||
value: 2,
|
||||
child: Container(
|
||||
padding: EdgeInsets.only(left: 10),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Icon(LineIcons.paperclip_solid),
|
||||
Padding(padding: EdgeInsets.symmetric(horizontal: 5.0),),
|
||||
Text('Import OMPL'),
|
||||
],
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 2,
|
||||
child: Container(
|
||||
padding: EdgeInsets.only(left: 10),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Icon(LineIcons.paperclip_solid),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 5.0),
|
||||
),
|
||||
Text('Import OMPL'),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// PopupMenuItem(
|
||||
// value: 3,
|
||||
// child: setting.theme != 2 ? Text('Night Mode') : Text('Light Mode'),
|
||||
// ),
|
||||
PopupMenuItem(
|
||||
),
|
||||
|
||||
// PopupMenuItem(
|
||||
// value: 3,
|
||||
// child: setting.theme != 2 ? Text('Night Mode') : Text('Light Mode'),
|
||||
// ),
|
||||
PopupMenuItem(
|
||||
value: 4,
|
||||
child: Container(
|
||||
padding: EdgeInsets.only(left: 10),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Icon(LineIcons.cog_solid),
|
||||
Padding(padding: EdgeInsets.symmetric(horizontal: 5.0),),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 5.0),
|
||||
),
|
||||
Text('Settings'),
|
||||
],
|
||||
),
|
||||
@ -250,7 +328,9 @@ class PopupMenu extends StatelessWidget {
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Icon(LineIcons.info_circle_solid),
|
||||
Padding(padding: EdgeInsets.symmetric(horizontal: 5.0),),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 5.0),
|
||||
),
|
||||
Text('About'),
|
||||
],
|
||||
),
|
||||
@ -266,11 +346,11 @@ class PopupMenu extends StatelessWidget {
|
||||
} else if (value == 1) {
|
||||
_refreshAll();
|
||||
} else if (value == 3) {
|
||||
// setting.theme != 2 ? setting.setTheme(2) : setting.setTheme(1);
|
||||
} else if (value == 4) {
|
||||
Navigator.push(
|
||||
// setting.theme != 2 ? setting.setTheme(2) : setting.setTheme(1);
|
||||
} else if (value == 4) {
|
||||
Navigator.push(
|
||||
context, MaterialPageRoute(builder: (context) => Settings()));
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -81,10 +81,10 @@ class _AudioPanelState extends State<AudioPanel>
|
||||
color: Theme.of(context).primaryColor,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
offset: Offset(0, -1),
|
||||
blurRadius: 4,
|
||||
offset: Offset(0, -0.5),
|
||||
blurRadius: 1,
|
||||
color: Theme.of(context).brightness == Brightness.light
|
||||
? Colors.grey[400]
|
||||
? Colors.grey[400].withOpacity(0.5)
|
||||
: Colors.grey[800],
|
||||
),
|
||||
],
|
||||
|
@ -1,45 +1,15 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
import 'hometab.dart';
|
||||
import 'package:tsacdop/home/appbar/importompl.dart';
|
||||
import 'package:tsacdop/home/audioplayer.dart';
|
||||
import 'package:tsacdop/class/audiostate.dart';
|
||||
import 'homescroll.dart';
|
||||
|
||||
class Home extends StatefulWidget {
|
||||
@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;
|
||||
});
|
||||
}
|
||||
class Home extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
|
||||
return Stack(children: <Widget>[
|
||||
Column(
|
||||
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()),
|
||||
]);
|
||||
}
|
||||
|
@ -244,11 +244,30 @@ class _ScrollPodcastsState extends State<ScrollPodcasts> {
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(25.0)),
|
||||
child: LimitedBox(
|
||||
maxHeight: 50,
|
||||
maxWidth: 50,
|
||||
child: Image.file(
|
||||
File("${podcastLocal.imagePath}")),
|
||||
child: Stack(
|
||||
alignment: Alignment.bottomCenter,
|
||||
children: <Widget>[
|
||||
LimitedBox(
|
||||
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 PodcastLocal podcastLocal;
|
||||
ShowEpisode({Key key, this.podcast, this.podcastLocal}) : super(key: key);
|
||||
Offset offset;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
double _width = MediaQuery.of(context).size.width;
|
||||
Offset offset;
|
||||
_showPopupMenu(Offset offset, EpisodeBrief episode, BuildContext context,
|
||||
bool isPlaying, bool isInPlaylist) async {
|
||||
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
|
||||
@ -432,7 +452,7 @@ class ShowEpisode extends StatelessWidget {
|
||||
if (value == 0) {
|
||||
if (!isPlaying) audio.episodeLoad(episode);
|
||||
} else if (value == 1) {
|
||||
if (isInPlaylist) {
|
||||
if (!isInPlaylist) {
|
||||
audio.addToPlaylist(episode);
|
||||
Fluttertoast.showToast(
|
||||
msg: 'Added to playlist',
|
||||
@ -536,6 +556,14 @@ class ShowEpisode extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
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 '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/settings/history.dart';
|
||||
import 'package:tsacdop/home/playlist.dart';
|
||||
import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
|
||||
import 'package:tsacdop/util/episodegrid.dart';
|
||||
import 'package:tsacdop/util/mypopupmenu.dart';
|
||||
|
||||
class MainTab extends StatefulWidget {
|
||||
@override
|
||||
@ -13,6 +19,12 @@ class MainTab extends StatefulWidget {
|
||||
|
||||
class _MainTabState extends State<MainTab> with TickerProviderStateMixin {
|
||||
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) {
|
||||
return UnderlineTabIndicator(
|
||||
borderSide: BorderSide(color: Theme.of(context).accentColor, width: 2),
|
||||
@ -23,25 +35,121 @@ class _MainTabState extends State<MainTab> with TickerProviderStateMixin {
|
||||
));
|
||||
}
|
||||
|
||||
Widget playHistory() {
|
||||
return PopupMenuButton<int>(
|
||||
_getPlaylist() async {
|
||||
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(
|
||||
borderRadius: BorderRadius.all(Radius.circular(10))),
|
||||
elevation: 1,
|
||||
icon: Icon(Icons.history),
|
||||
icon: Icon(Icons.playlist_play),
|
||||
tooltip: "Menu",
|
||||
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(
|
||||
value: 0,
|
||||
child: Container(
|
||||
padding: EdgeInsets.only(left: 10),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Icon(Icons.history),
|
||||
Icon(Icons.playlist_play),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 5.0),
|
||||
),
|
||||
Text('Play History'),
|
||||
Text('Playlist'),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -49,9 +157,9 @@ class _MainTabState extends State<MainTab> with TickerProviderStateMixin {
|
||||
],
|
||||
onSelected: (value) {
|
||||
if (value == 0) {
|
||||
Navigator.push(context,
|
||||
MaterialPageRoute(builder: (context) => PlayedHistory()));
|
||||
}
|
||||
Navigator.push(
|
||||
context, MaterialPageRoute(builder: (context) => PlaylistPage()));
|
||||
} else if (value == 1) {}
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -60,6 +168,8 @@ class _MainTabState extends State<MainTab> with TickerProviderStateMixin {
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = TabController(length: 3, vsync: this);
|
||||
_loadPlay = false;
|
||||
_getPlaylist();
|
||||
}
|
||||
|
||||
@override
|
||||
@ -103,7 +213,7 @@ class _MainTabState extends State<MainTab> with TickerProviderStateMixin {
|
||||
),
|
||||
),
|
||||
Spacer(),
|
||||
playHistory(),
|
||||
playlist(context),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
@ -135,9 +245,12 @@ class RecentUpdate extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _RecentUpdateState extends State<RecentUpdate> {
|
||||
int _updateCount = 0;
|
||||
Future<List<EpisodeBrief>> _getRssItem(int top) async {
|
||||
var dbHelper = DBHelper();
|
||||
List<EpisodeBrief> episodes = await dbHelper.getRecentRssItem(top);
|
||||
KeyValueStorage refreshcountstorage = KeyValueStorage('refreshcount');
|
||||
_updateCount = await refreshcountstorage.getInt();
|
||||
return episodes;
|
||||
}
|
||||
|
||||
@ -189,6 +302,7 @@ class _RecentUpdateState extends State<RecentUpdate> {
|
||||
showFavorite: false,
|
||||
showNumber: false,
|
||||
heroTag: 'recent',
|
||||
updateCount: _updateCount,
|
||||
),
|
||||
SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
@ -277,7 +391,6 @@ class _MyDownloadState extends State<MyDownload> {
|
||||
heroTag: 'download',
|
||||
)
|
||||
],
|
||||
|
||||
)
|
||||
: 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,
|
||||
imageUrl TEXT,rssUrl TEXT UNIQUE,primaryColor TEXT,author TEXT,
|
||||
description TEXT, add_date INTEGER, imagePath TEXT, provider TEXT, link TEXT,
|
||||
background_image TEXT DEFAULT '',hosts TEXT DEFAULT '')""");
|
||||
background_image TEXT DEFAULT '',hosts TEXT DEFAULT '',update_count INTEGER DEFAULT 0)""");
|
||||
await db
|
||||
.execute("""CREATE TABLE Episodes(id INTEGER PRIMARY KEY,title TEXT,
|
||||
enclosure_url TEXT UNIQUE, enclosure_length INTEGER, pubDate TEXT,
|
||||
description TEXT, feed_id TEXT, feed_link TEXT, milliseconds INTEGER,
|
||||
duration INTEGER DEFAULT 0, explicit INTEGER DEFAULT 0, liked INTEGER DEFAULT 0,
|
||||
downloaded TEXT DEFAULT 'ND', download_date INTEGER DEFAULT 0, media_id TEXT)""");
|
||||
downloaded TEXT DEFAULT 'ND', download_date INTEGER DEFAULT 0, media_id TEXT,
|
||||
is_new INTEGER DEFAULT 0)""");
|
||||
await db.execute(
|
||||
"""CREATE TABLE PlayHistory(id INTEGER PRIMARY KEY, title TEXT, enclosure_url TEXT UNIQUE,
|
||||
seconds REAL, seek_value REAL, add_date INTEGER)""");
|
||||
@ -51,7 +52,7 @@ class DBHelper {
|
||||
await Future.forEach(podcasts, (s) async {
|
||||
List<Map> list;
|
||||
list = await dbClient.rawQuery(
|
||||
'SELECT id, title, imageUrl, rssUrl, primaryColor, author, imagePath , provider, link FROM PodcastLocal WHERE id = ?',
|
||||
'SELECT id, title, imageUrl, rssUrl, primaryColor, author, imagePath , provider, link ,update_count FROM PodcastLocal WHERE id = ?',
|
||||
[s]);
|
||||
podcastLocal.add(PodcastLocal(
|
||||
list.first['title'],
|
||||
@ -62,7 +63,8 @@ class DBHelper {
|
||||
list.first['id'],
|
||||
list.first['imagePath'],
|
||||
list.first['provider'],
|
||||
list.first['link']));
|
||||
list.first['link'],
|
||||
upateCount: list.first['update_count']));
|
||||
});
|
||||
return podcastLocal;
|
||||
}
|
||||
@ -199,16 +201,19 @@ class DBHelper {
|
||||
return playHistory;
|
||||
}
|
||||
|
||||
Future<List<SubHistory>> getSubHistory() async{
|
||||
Future<List<SubHistory>> getSubHistory() async {
|
||||
var dbClient = await database;
|
||||
List<Map> list = await dbClient.rawQuery(
|
||||
"""SELECT title, rss_url, add_date, remove_date, status FROM SubscribeHistory
|
||||
ORDER BY add_date DESC"""
|
||||
);
|
||||
return list.map((record) => SubHistory(
|
||||
record['status']==0 ? true : false, DateTime.fromMillisecondsSinceEpoch(record['remove_date']),
|
||||
DateTime.fromMillisecondsSinceEpoch(record['add_date']), record['rss_url'], record['title']
|
||||
)).toList();
|
||||
"""SELECT title, rss_url, add_date, remove_date, status FROM SubscribeHistory
|
||||
ORDER BY add_date DESC""");
|
||||
return list
|
||||
.map((record) => SubHistory(
|
||||
record['status'] == 0 ? true : false,
|
||||
DateTime.fromMillisecondsSinceEpoch(record['remove_date']),
|
||||
DateTime.fromMillisecondsSinceEpoch(record['add_date']),
|
||||
record['rss_url'],
|
||||
record['title']))
|
||||
.toList();
|
||||
}
|
||||
|
||||
Future<double> listenMins(int day) async {
|
||||
@ -252,12 +257,12 @@ class DBHelper {
|
||||
RegExp z = RegExp(r'(\+|\-)[0-1][0-9]00');
|
||||
String timezone = z.stringMatch(pubDate);
|
||||
int timezoneInt = 0;
|
||||
if(timezone!=null){
|
||||
if(timezone.substring(0, 1) == '-'){
|
||||
timezoneInt = int.parse(timezone.substring(1,2));
|
||||
} else {
|
||||
timezoneInt = -int.parse(timezone.substring(1,2));
|
||||
}
|
||||
if (timezone != null) {
|
||||
if (timezone.substring(0, 1) == '-') {
|
||||
timezoneInt = int.parse(timezone.substring(1, 2));
|
||||
} else {
|
||||
timezoneInt = -int.parse(timezone.substring(1, 2));
|
||||
}
|
||||
}
|
||||
try {
|
||||
date = DateFormat('EEE, dd MMM yyyy HH:mm:ss Z', 'en_US').parse(pubDate);
|
||||
@ -288,7 +293,7 @@ class DBHelper {
|
||||
return date.add(Duration(hours: timezoneInt));
|
||||
}
|
||||
|
||||
int getExplicit(bool b) {
|
||||
int _getExplicit(bool b) {
|
||||
int result;
|
||||
if (b == true) {
|
||||
result = 1;
|
||||
@ -299,117 +304,147 @@ class DBHelper {
|
||||
}
|
||||
}
|
||||
|
||||
bool isXimalaya(String input) {
|
||||
bool _isXimalaya(String input) {
|
||||
RegExp ximalaya = RegExp(r"ximalaya.com");
|
||||
return ximalaya.hasMatch(input);
|
||||
}
|
||||
|
||||
Future<int> savePodcastRss(RssFeed _p, String id) async {
|
||||
int _result = _p.items.length;
|
||||
var dbClient = await database;
|
||||
String _description, _url;
|
||||
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;
|
||||
String _getDescription(String content, String description, String summary) {
|
||||
if (content.length >= description.length) {
|
||||
if (content.length >= summary.length) {
|
||||
return content;
|
||||
} 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)
|
||||
? _url = _p.items[i].enclosure.url.split('=').last
|
||||
: _url = _p.items[i].enclosure.url;
|
||||
final title = feed.items[i].itunes.title ?? feed.items[i].title;
|
||||
final length = feed.items[i]?.enclosure?.length;
|
||||
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;
|
||||
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) {
|
||||
if (url != null) {
|
||||
await dbClient.transaction((txn) {
|
||||
return txn.rawInsert(
|
||||
"""INSERT OR IGNORE INTO Episodes(title, enclosure_url, enclosure_length, pubDate,
|
||||
description, feed_id, milliseconds, duration, explicit, media_id) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
||||
[
|
||||
_title,
|
||||
_url,
|
||||
_length,
|
||||
_pubDate,
|
||||
_description,
|
||||
title,
|
||||
url,
|
||||
length,
|
||||
pubDate,
|
||||
description,
|
||||
id,
|
||||
_milliseconds,
|
||||
_duration,
|
||||
_explicit,
|
||||
_url
|
||||
milliseconds,
|
||||
duration,
|
||||
explicit,
|
||||
url
|
||||
]);
|
||||
});
|
||||
}
|
||||
}
|
||||
return _result;
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<int> updatePodcastRss(PodcastLocal podcastLocal) async {
|
||||
Response response = await Dio().get(podcastLocal.rssUrl);
|
||||
var _p = RssFeed.parse(response.data);
|
||||
String _url, _description;
|
||||
int _result = _p.items.length;
|
||||
var feed = RssFeed.parse(response.data);
|
||||
String url, description;
|
||||
feed.items.removeWhere((item) => item == null);
|
||||
int result = feed.items.length;
|
||||
|
||||
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]));
|
||||
print(_count);
|
||||
if (_count == _result) {
|
||||
_result = 0;
|
||||
return _result;
|
||||
|
||||
print(count);
|
||||
await dbClient.rawUpdate(
|
||||
"""UPDATE PodcastLocal SET update_count = ? WHERE id = ?""",
|
||||
[(result - count), podcastLocal.id]);
|
||||
if (count == result) {
|
||||
result = 0;
|
||||
return result;
|
||||
} else {
|
||||
for (int i = 0; i < (_result - _count); i++) {
|
||||
print(_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 {
|
||||
_description = _p.items[i].description;
|
||||
for (int i = 0; i < (result - count); i++) {
|
||||
print(feed.items[i].title);
|
||||
// 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;
|
||||
// }
|
||||
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)
|
||||
? _url = _p.items[i].enclosure.url.split('=').last
|
||||
: _url = _p.items[i].enclosure.url;
|
||||
final title = feed.items[i].itunes.title ?? feed.items[i].title;
|
||||
final length = feed.items[i]?.enclosure?.length;
|
||||
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;
|
||||
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) {
|
||||
if (url != null) {
|
||||
await dbClient.transaction((txn) {
|
||||
return txn.rawInsert(
|
||||
"""INSERT OR IGNORE INTO Episodes(title, enclosure_url, enclosure_length, pubDate,
|
||||
description, feed_id, milliseconds, duration, explicit, media_id) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
||||
[
|
||||
_title,
|
||||
_url,
|
||||
_length,
|
||||
_pubDate,
|
||||
_description,
|
||||
title,
|
||||
url,
|
||||
length,
|
||||
pubDate,
|
||||
description,
|
||||
podcastLocal.id,
|
||||
_milliseconds,
|
||||
_duration,
|
||||
_explicit,
|
||||
_url
|
||||
milliseconds,
|
||||
duration,
|
||||
explicit,
|
||||
url
|
||||
]);
|
||||
});
|
||||
}
|
||||
}
|
||||
return _result - _count;
|
||||
return result - count;
|
||||
}
|
||||
}
|
||||
|
||||
@ -573,7 +608,6 @@ class DBHelper {
|
||||
return count;
|
||||
}
|
||||
|
||||
|
||||
Future<int> saveMediaId(String url, String path) async {
|
||||
var dbClient = await database;
|
||||
int _milliseconds = DateTime.now().millisecondsSinceEpoch;
|
||||
@ -583,11 +617,11 @@ class DBHelper {
|
||||
return count;
|
||||
}
|
||||
|
||||
|
||||
Future<int> delDownloaded(String url) async {
|
||||
var dbClient = await database;
|
||||
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);
|
||||
return count;
|
||||
}
|
||||
@ -642,23 +676,27 @@ class DBHelper {
|
||||
P.title as feed_title, E.duration, E.explicit, E.liked, E.downloaded,
|
||||
P.primaryColor, E.media_id FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
|
||||
WHERE E.enclosure_url = ?""", [url]);
|
||||
episode = EpisodeBrief(
|
||||
list.first['title'],
|
||||
list.first['enclosure_url'],
|
||||
list.first['enclosure_length'],
|
||||
list.first['milliseconds'],
|
||||
list.first['feed_title'],
|
||||
list.first['primaryColor'],
|
||||
list.first['liked'],
|
||||
list.first['downloaded'],
|
||||
list.first['duration'],
|
||||
list.first['explicit'],
|
||||
list.first['imagePath'],
|
||||
list.first['media_id']);
|
||||
return episode;
|
||||
if (list.length == 0) {
|
||||
return null;
|
||||
} else {
|
||||
episode = EpisodeBrief(
|
||||
list.first['title'],
|
||||
list.first['enclosure_url'],
|
||||
list.first['enclosure_length'],
|
||||
list.first['milliseconds'],
|
||||
list.first['feed_title'],
|
||||
list.first['primaryColor'],
|
||||
list.first['liked'],
|
||||
list.first['downloaded'],
|
||||
list.first['duration'],
|
||||
list.first['explicit'],
|
||||
list.first['imagePath'],
|
||||
list.first['media_id']);
|
||||
return episode;
|
||||
}
|
||||
}
|
||||
|
||||
Future<EpisodeBrief> getRssItemWithMediaId(String id) async {
|
||||
Future<EpisodeBrief> getRssItemWithMediaId(String id) async {
|
||||
var dbClient = await database;
|
||||
EpisodeBrief episode;
|
||||
List<Map> list = await dbClient.rawQuery(
|
||||
@ -681,5 +719,4 @@ class DBHelper {
|
||||
list.first['media_id']);
|
||||
return episode;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -41,30 +41,20 @@ Future main() async {
|
||||
child: MyApp(),
|
||||
),
|
||||
);
|
||||
|
||||
SystemUiOverlayStyle systemUiOverlayStyle =
|
||||
SystemUiOverlayStyle(statusBarColor: Colors.transparent, systemNavigationBarColor: Colors.transparent);
|
||||
SystemChrome.setSystemUIOverlayStyle(systemUiOverlayStyle);
|
||||
|
||||
await SystemChrome.setPreferredOrientations(
|
||||
[DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]);
|
||||
}
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
void setWorkManager() {
|
||||
Workmanager.initialize(
|
||||
callbackDispatcher,
|
||||
isInDebugMode: true,
|
||||
);
|
||||
|
||||
Workmanager.registerPeriodicTask("1", "update_podcasts",
|
||||
frequency: Duration(hours: 12),
|
||||
initialDelay: Duration(seconds: 5),
|
||||
constraints: Constraints(
|
||||
networkType: NetworkType.connected,
|
||||
requiresBatteryNotLow: true,
|
||||
));
|
||||
}
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<SettingState>(
|
||||
builder: (_, setting, __) {
|
||||
if (setting.autoUpdate) setWorkManager();
|
||||
return MaterialApp(
|
||||
themeMode: setting.theme,
|
||||
debugShowCheckedModeBanner: false,
|
||||
@ -92,6 +82,7 @@ class MyApp extends StatelessWidget {
|
||||
),
|
||||
darkTheme: ThemeData.dark().copyWith(
|
||||
accentColor: setting.accentSetColor,
|
||||
// scaffoldBackgroundColor: Colors.black87,
|
||||
appBarTheme: AppBarTheme(elevation: 0),
|
||||
),
|
||||
home: MyHomePage(),
|
||||
|
@ -101,9 +101,7 @@ class _PodcastDetailState extends State<PodcastDetail> {
|
||||
fit: BoxFit.cover)),
|
||||
alignment: Alignment.centerRight,
|
||||
child: Container(
|
||||
color: Theme.of(context)
|
||||
.scaffoldBackgroundColor
|
||||
.withOpacity(0.5),
|
||||
color: Colors.black26,
|
||||
padding: EdgeInsets.symmetric(vertical: 5.0),
|
||||
width: MediaQuery.of(context).size.width,
|
||||
alignment: Alignment.centerRight,
|
||||
@ -162,186 +160,206 @@ class _PodcastDetailState extends State<PodcastDetail> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
Color _color = widget.podcastLocal.primaryColor.colorizedark();
|
||||
return AnnotatedRegion<SystemUiOverlayStyle>(
|
||||
value: SystemUiOverlayStyle(
|
||||
statusBarIconBrightness: Brightness.light,
|
||||
statusBarIconBrightness: Brightness.dark,
|
||||
systemNavigationBarColor: Theme.of(context).primaryColor,
|
||||
statusBarColor: _color,
|
||||
systemNavigationBarIconBrightness:
|
||||
Theme.of(context).accentColorBrightness,
|
||||
//statusBarColor: _color,
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Scaffold(
|
||||
body: RefreshIndicator(
|
||||
key: _refreshIndicatorKey,
|
||||
color: Theme.of(context).accentColor,
|
||||
onRefresh: () => _updateRssItem(widget.podcastLocal),
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
FutureBuilder<List<EpisodeBrief>>(
|
||||
future: _getRssItem(widget.podcastLocal),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) print(snapshot.error);
|
||||
return (snapshot.hasData)
|
||||
? CustomScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
primary: true,
|
||||
slivers: <Widget>[
|
||||
SliverAppBar(
|
||||
actions: <Widget>[
|
||||
PopupMenuButton<String>(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(10))),
|
||||
elevation: 2,
|
||||
tooltip: 'Menu',
|
||||
itemBuilder: (context) => [
|
||||
widget.podcastLocal.link != null
|
||||
? PopupMenuItem(
|
||||
value: widget.podcastLocal.link,
|
||||
child: Container(
|
||||
padding:
|
||||
EdgeInsets.only(left: 10),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Icon(Icons.link,
|
||||
color: Theme.of(context)
|
||||
.tabBarTheme
|
||||
.labelColor),
|
||||
Padding(
|
||||
padding:
|
||||
EdgeInsets.symmetric(
|
||||
horizontal: 5.0),
|
||||
),
|
||||
Text('Visit Site'),
|
||||
],
|
||||
child: Scaffold(
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
child: RefreshIndicator(
|
||||
key: _refreshIndicatorKey,
|
||||
color: Theme.of(context).accentColor,
|
||||
onRefresh: () => _updateRssItem(widget.podcastLocal),
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
FutureBuilder<List<EpisodeBrief>>(
|
||||
future: _getRssItem(widget.podcastLocal),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) print(snapshot.error);
|
||||
return (snapshot.hasData)
|
||||
? CustomScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
primary: true,
|
||||
slivers: <Widget>[
|
||||
SliverAppBar(
|
||||
brightness: Brightness.dark,
|
||||
actions: <Widget>[
|
||||
PopupMenuButton<String>(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(10))),
|
||||
elevation: 2,
|
||||
tooltip: 'Menu',
|
||||
itemBuilder: (context) => [
|
||||
widget.podcastLocal.link != null
|
||||
? PopupMenuItem(
|
||||
value: widget.podcastLocal.link,
|
||||
child: Container(
|
||||
padding:
|
||||
EdgeInsets.only(left: 10),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Icon(Icons.link,
|
||||
color: Theme.of(context)
|
||||
.tabBarTheme
|
||||
.labelColor),
|
||||
Padding(
|
||||
padding:
|
||||
EdgeInsets.symmetric(
|
||||
horizontal: 5.0),
|
||||
),
|
||||
Text('Visit Site'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: Center(),
|
||||
PopupMenuItem(
|
||||
value: widget.podcastLocal.rssUrl,
|
||||
child: Container(
|
||||
padding: EdgeInsets.only(left: 10),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
Icons.rss_feed,
|
||||
color: Theme.of(context)
|
||||
.tabBarTheme
|
||||
.labelColor,
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 5.0),
|
||||
),
|
||||
Text('View Rss Feed'),
|
||||
],
|
||||
)
|
||||
: Center(),
|
||||
PopupMenuItem(
|
||||
value: widget.podcastLocal.rssUrl,
|
||||
child: Container(
|
||||
padding: EdgeInsets.only(left: 10),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
Icons.rss_feed,
|
||||
color: Theme.of(context)
|
||||
.tabBarTheme
|
||||
.labelColor,
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 5.0),
|
||||
),
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
title: top < 70
|
||||
? Text(widget.podcastLocal.title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(color: Colors.white))
|
||||
: Center(),
|
||||
);
|
||||
}),
|
||||
),
|
||||
SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(BuildContext context, int index) {
|
||||
return hostsList(context, hosts);
|
||||
},
|
||||
childCount: 1,
|
||||
onSelected: (url) {
|
||||
_launchUrl(url);
|
||||
},
|
||||
)
|
||||
],
|
||||
elevation: 0,
|
||||
iconTheme: IconThemeData(
|
||||
color: Colors.white,
|
||||
),
|
||||
expandedHeight:
|
||||
150 + MediaQuery.of(context).padding.top,
|
||||
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 +
|
||||
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(),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10.0),
|
||||
sliver: EpisodeGrid(
|
||||
podcast: snapshot.data,
|
||||
showDownload: false,
|
||||
showFavorite: true,
|
||||
showNumber: true,
|
||||
heroTag: 'podcast',
|
||||
)),
|
||||
],
|
||||
)
|
||||
: Center(child: CircularProgressIndicator());
|
||||
},
|
||||
),
|
||||
Container(child: PlayerWidget()),
|
||||
],
|
||||
SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(BuildContext context, int index) {
|
||||
return hostsList(context, hosts);
|
||||
},
|
||||
childCount: 1,
|
||||
),
|
||||
),
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10.0),
|
||||
sliver: EpisodeGrid(
|
||||
podcast: snapshot.data,
|
||||
showDownload: false,
|
||||
showFavorite: true,
|
||||
showNumber: true,
|
||||
heroTag: 'podcast',
|
||||
updateCount: widget.podcastLocal.upateCount,
|
||||
)),
|
||||
],
|
||||
)
|
||||
: Center(child: CircularProgressIndicator());
|
||||
},
|
||||
),
|
||||
Container(child: PlayerWidget()),
|
||||
],
|
||||
),
|
||||
),
|
||||
)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -29,35 +29,31 @@ class _PodcastGroupListState extends State<PodcastGroupList> {
|
||||
)
|
||||
: Container(
|
||||
color: Theme.of(context).primaryColor,
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
ReorderableListView(
|
||||
onReorder: (int oldIndex, int newIndex) {
|
||||
setState(() {
|
||||
if (newIndex > oldIndex) {
|
||||
newIndex -= 1;
|
||||
}
|
||||
final PodcastLocal podcast =
|
||||
widget.group.podcasts.removeAt(oldIndex);
|
||||
widget.group.podcasts.insert(newIndex, podcast);
|
||||
});
|
||||
widget.group.setOrderedPodcasts = widget.group.podcasts;
|
||||
groupList.addToOrderChanged(widget.group.name);
|
||||
},
|
||||
children: widget.group.podcasts
|
||||
.map<Widget>((PodcastLocal podcastLocal) {
|
||||
return Container(
|
||||
decoration:
|
||||
BoxDecoration(color: Theme.of(context).primaryColor),
|
||||
key: ObjectKey(podcastLocal.title),
|
||||
child: PodcastCard(
|
||||
podcastLocal: podcastLocal,
|
||||
group: widget.group,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
child: ReorderableListView(
|
||||
onReorder: (int oldIndex, int newIndex) {
|
||||
setState(() {
|
||||
if (newIndex > oldIndex) {
|
||||
newIndex -= 1;
|
||||
}
|
||||
final PodcastLocal podcast =
|
||||
widget.group.podcasts.removeAt(oldIndex);
|
||||
widget.group.podcasts.insert(newIndex, podcast);
|
||||
});
|
||||
widget.group.setOrderedPodcasts = widget.group.podcasts;
|
||||
groupList.addToOrderChanged(widget.group.name);
|
||||
},
|
||||
children: widget.group.podcasts
|
||||
.map<Widget>((PodcastLocal podcastLocal) {
|
||||
return Container(
|
||||
decoration:
|
||||
BoxDecoration(color: Theme.of(context).primaryColor),
|
||||
key: ObjectKey(podcastLocal.title),
|
||||
child: PodcastCard(
|
||||
podcastLocal: podcastLocal,
|
||||
group: widget.group,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -393,89 +389,88 @@ class _RenameGroupState extends State<RenameGroup> {
|
||||
var groupList = Provider.of<GroupList>(context, listen: false);
|
||||
List list = groupList.groups.map((e) => e.name).toList();
|
||||
return AnnotatedRegion<SystemUiOverlayStyle>(
|
||||
value: SystemUiOverlayStyle(
|
||||
statusBarIconBrightness: Brightness.light,
|
||||
systemNavigationBarColor:
|
||||
Theme.of(context).brightness == Brightness.light
|
||||
? Color.fromRGBO(113, 113, 113, 1)
|
||||
: Color.fromRGBO(5, 5, 5, 1),
|
||||
statusBarColor: Theme.of(context).brightness == Brightness.light
|
||||
? Color.fromRGBO(113, 113, 113, 1)
|
||||
: Color.fromRGBO(15, 15, 15, 1),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: AlertDialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(10))),
|
||||
elevation: 1,
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 20),
|
||||
titlePadding:
|
||||
EdgeInsets.only(top: 20, left: 20, right: 200, bottom: 20),
|
||||
actionsPadding: EdgeInsets.all(0),
|
||||
actions: <Widget>[
|
||||
FlatButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(
|
||||
'CANCEL',
|
||||
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(),
|
||||
),
|
||||
],
|
||||
value: SystemUiOverlayStyle(
|
||||
statusBarIconBrightness: Brightness.light,
|
||||
systemNavigationBarColor:
|
||||
Theme.of(context).brightness == Brightness.light
|
||||
? Color.fromRGBO(113, 113, 113, 1)
|
||||
: Color.fromRGBO(5, 5, 5, 1),
|
||||
// statusBarColor: Theme.of(context).brightness == Brightness.light
|
||||
// ? Color.fromRGBO(113, 113, 113, 1)
|
||||
// : Color.fromRGBO(15, 15, 15, 1),
|
||||
),
|
||||
child: AlertDialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(10))),
|
||||
elevation: 1,
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 20),
|
||||
titlePadding:
|
||||
EdgeInsets.only(top: 20, left: 20, right: 200, bottom: 20),
|
||||
actionsPadding: EdgeInsets.all(0),
|
||||
actions: <Widget>[
|
||||
FlatButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(
|
||||
'CANCEL',
|
||||
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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -99,15 +99,16 @@ class _PodcastListState extends State<PodcastList> {
|
||||
value: SystemUiOverlayStyle(
|
||||
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
|
||||
systemNavigationBarColor: Theme.of(context).primaryColor,
|
||||
statusBarColor: Theme.of(context).primaryColor,
|
||||
systemNavigationBarIconBrightness:
|
||||
Theme.of(context).accentColorBrightness,
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Podcasts'),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: Container(
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Podcasts'),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Container(
|
||||
color: Theme.of(context).primaryColor,
|
||||
child: FutureBuilder<List<PodcastLocal>>(
|
||||
future: getPodcastLocal(),
|
||||
@ -161,18 +162,16 @@ class _PodcastListState extends State<PodcastList> {
|
||||
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: AboutPodcast(
|
||||
podcastLocal:
|
||||
snapshot.data[index]),
|
||||
// statusBarColor: Theme.of(context)
|
||||
// .brightness ==
|
||||
// Brightness.light
|
||||
// ? Color.fromRGBO(
|
||||
// 113, 113, 113, 1)
|
||||
// : Color.fromRGBO(5, 5, 5, 1),
|
||||
),
|
||||
child: AboutPodcast(
|
||||
podcastLocal:
|
||||
snapshot.data[index]),
|
||||
));
|
||||
},
|
||||
child: Container(
|
||||
|
@ -137,330 +137,322 @@ class _PodcastManageState extends State<PodcastManage>
|
||||
value: SystemUiOverlayStyle(
|
||||
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
|
||||
systemNavigationBarColor: Theme.of(context).primaryColor,
|
||||
statusBarColor: Theme.of(context).primaryColor,
|
||||
systemNavigationBarIconBrightness:
|
||||
Theme.of(context).accentColorBrightness,
|
||||
// statusBarColor: Theme.of(context).primaryColor,
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
centerTitle: true,
|
||||
title: Text('Groups'),
|
||||
actions: <Widget>[
|
||||
IconButton(
|
||||
onPressed: () => 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) =>
|
||||
AddGroup()),
|
||||
icon: Icon(Icons.add)),
|
||||
OrderMenu(),
|
||||
],
|
||||
),
|
||||
body: Consumer<GroupList>(builder: (_, groupList, __) {
|
||||
bool _isLoading = groupList.isLoading;
|
||||
List<PodcastGroup> _groups = groupList.groups;
|
||||
return _isLoading
|
||||
? Center()
|
||||
: Stack(
|
||||
children: <Widget>[
|
||||
CustomTabView(
|
||||
itemCount: _groups.length,
|
||||
tabBuilder: (context, index) => Tab(
|
||||
child: Container(
|
||||
height: 30.0,
|
||||
padding: EdgeInsets.symmetric(horizontal: 10.0),
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: (_scroll - index).abs() > 1
|
||||
? Colors.grey[300]
|
||||
: Colors.grey[300]
|
||||
.withOpacity((_scroll - index).abs()),
|
||||
borderRadius:
|
||||
BorderRadius.all(Radius.circular(15)),
|
||||
),
|
||||
child: Text(
|
||||
_groups[index].name,
|
||||
)),
|
||||
),
|
||||
pageBuilder: (context, index) => Container(
|
||||
key: ObjectKey(_groups[index].name),
|
||||
child: PodcastGroupList(group: _groups[index])),
|
||||
onPositionChange: (value) =>
|
||||
setState(() => _index = value),
|
||||
onScroll: (value) => setState(() => _scroll = value),
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
centerTitle: true,
|
||||
title: Text('Groups'),
|
||||
actions: <Widget>[
|
||||
IconButton(
|
||||
onPressed: () => 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) =>
|
||||
AddGroup()),
|
||||
icon: Icon(Icons.add)),
|
||||
OrderMenu(),
|
||||
],
|
||||
),
|
||||
body: Consumer<GroupList>(builder: (_, groupList, __) {
|
||||
bool _isLoading = groupList.isLoading;
|
||||
List<PodcastGroup> _groups = groupList.groups;
|
||||
return _isLoading
|
||||
? Center()
|
||||
: Stack(
|
||||
children: <Widget>[
|
||||
CustomTabView(
|
||||
itemCount: _groups.length,
|
||||
tabBuilder: (context, index) => Tab(
|
||||
child: Container(
|
||||
height: 30.0,
|
||||
padding: EdgeInsets.symmetric(horizontal: 10.0),
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: (_scroll - index).abs() > 1
|
||||
? Colors.grey[300]
|
||||
: Colors.grey[300]
|
||||
.withOpacity((_scroll - index).abs()),
|
||||
borderRadius:
|
||||
BorderRadius.all(Radius.circular(15)),
|
||||
),
|
||||
child: Text(
|
||||
_groups[index].name,
|
||||
)),
|
||||
),
|
||||
_showSetting
|
||||
? Positioned.fill(
|
||||
child: GestureDetector(
|
||||
onTap: () async {
|
||||
await _menuController.reverse();
|
||||
setState(() => _showSetting = false);
|
||||
},
|
||||
child: Container(
|
||||
color: Theme.of(context)
|
||||
.scaffoldBackgroundColor
|
||||
.withOpacity(0.5 * _menuController.value),
|
||||
),
|
||||
),
|
||||
)
|
||||
: Center(),
|
||||
Positioned(
|
||||
right: 30,
|
||||
bottom: 30,
|
||||
child: _saveButton(context),
|
||||
),
|
||||
_showSetting
|
||||
? Positioned(
|
||||
right: 30 * _menuValue,
|
||||
bottom: 100,
|
||||
pageBuilder: (context, index) => Container(
|
||||
key: ObjectKey(_groups[index].name),
|
||||
child: PodcastGroupList(group: _groups[index])),
|
||||
onPositionChange: (value) =>
|
||||
setState(() => _index = value),
|
||||
onScroll: (value) => setState(() => _scroll = value),
|
||||
),
|
||||
_showSetting
|
||||
? Positioned.fill(
|
||||
top: 50,
|
||||
child: GestureDetector(
|
||||
onTap: () async {
|
||||
await _menuController.reverse();
|
||||
setState(() => _showSetting = false);
|
||||
},
|
||||
child: Container(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: <Widget>[
|
||||
Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
_menuController.reverse();
|
||||
setState(() => _showSetting = false);
|
||||
_index == 0
|
||||
? Fluttertoast.showToast(
|
||||
msg:
|
||||
'Home group is not supported',
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
)
|
||||
: showGeneralDialog(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
barrierLabel:
|
||||
MaterialLocalizations.of(
|
||||
context)
|
||||
.modalBarrierDismissLabel,
|
||||
barrierColor: Colors.black54,
|
||||
transitionDuration:
|
||||
const Duration(
|
||||
milliseconds: 300),
|
||||
pageBuilder: (BuildContext
|
||||
context,
|
||||
Animation animaiton,
|
||||
Animation
|
||||
secondaryAnimation) =>
|
||||
RenameGroup(
|
||||
group: _groups[_index],
|
||||
));
|
||||
},
|
||||
child: Container(
|
||||
height: 30.0,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[700],
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(10.0))),
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 10),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
Icons.text_fields,
|
||||
color: Colors.white,
|
||||
size: 15.0,
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 5.0),
|
||||
),
|
||||
Text('Edit Name',
|
||||
style: TextStyle(
|
||||
color: Colors.white)),
|
||||
],
|
||||
),
|
||||
color: Theme.of(context)
|
||||
.scaffoldBackgroundColor
|
||||
.withOpacity(0.5 * _menuController.value),
|
||||
),
|
||||
),
|
||||
)
|
||||
: Center(),
|
||||
Positioned(
|
||||
right: 30,
|
||||
bottom: 30,
|
||||
child: _saveButton(context),
|
||||
),
|
||||
_showSetting
|
||||
? Positioned(
|
||||
right: 30 * _menuValue,
|
||||
bottom: 100,
|
||||
child: Container(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: <Widget>[
|
||||
Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
_menuController.reverse();
|
||||
setState(() => _showSetting = false);
|
||||
_index == 0
|
||||
? Fluttertoast.showToast(
|
||||
msg:
|
||||
'Home group is not supported',
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
)
|
||||
: showGeneralDialog(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
barrierLabel:
|
||||
MaterialLocalizations.of(
|
||||
context)
|
||||
.modalBarrierDismissLabel,
|
||||
barrierColor: Colors.black54,
|
||||
transitionDuration:
|
||||
const Duration(
|
||||
milliseconds: 300),
|
||||
pageBuilder: (BuildContext
|
||||
context,
|
||||
Animation animaiton,
|
||||
Animation
|
||||
secondaryAnimation) =>
|
||||
RenameGroup(
|
||||
group: _groups[_index],
|
||||
));
|
||||
},
|
||||
child: Container(
|
||||
height: 30.0,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[700],
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(10.0))),
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 10),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
Icons.text_fields,
|
||||
color: Colors.white,
|
||||
size: 15.0,
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 5.0),
|
||||
),
|
||||
Text('Edit Name',
|
||||
style: TextStyle(
|
||||
color: Colors.white)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: 10.0)),
|
||||
Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
_menuController.reverse();
|
||||
setState(() => _showSetting = false);
|
||||
_index == 0
|
||||
? Fluttertoast.showToast(
|
||||
msg:
|
||||
'Home group is not supported',
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
)
|
||||
: showGeneralDialog(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
barrierLabel:
|
||||
MaterialLocalizations.of(
|
||||
context)
|
||||
.modalBarrierDismissLabel,
|
||||
barrierColor: Colors.black54,
|
||||
transitionDuration:
|
||||
const Duration(
|
||||
milliseconds: 300),
|
||||
pageBuilder: (BuildContext
|
||||
context,
|
||||
Animation animaiton,
|
||||
Animation
|
||||
secondaryAnimation) =>
|
||||
AnnotatedRegion<
|
||||
SystemUiOverlayStyle>(
|
||||
value:
|
||||
SystemUiOverlayStyle(
|
||||
statusBarIconBrightness:
|
||||
Brightness.light,
|
||||
systemNavigationBarColor:
|
||||
Theme.of(context)
|
||||
.brightness ==
|
||||
Brightness
|
||||
.light
|
||||
? Color
|
||||
.fromRGBO(
|
||||
113,
|
||||
113,
|
||||
113,
|
||||
1)
|
||||
: Color
|
||||
.fromRGBO(
|
||||
15,
|
||||
15,
|
||||
15,
|
||||
1),
|
||||
statusBarColor:
|
||||
Theme.of(context)
|
||||
.brightness ==
|
||||
Brightness
|
||||
.light
|
||||
? Color
|
||||
.fromRGBO(
|
||||
113,
|
||||
113,
|
||||
113,
|
||||
1)
|
||||
: Color
|
||||
.fromRGBO(
|
||||
5,
|
||||
5,
|
||||
5,
|
||||
1),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: AlertDialog(
|
||||
elevation: 1,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius
|
||||
.all(Radius
|
||||
.circular(
|
||||
10.0))),
|
||||
titlePadding:
|
||||
EdgeInsets.only(
|
||||
top: 20,
|
||||
left: 20,
|
||||
right: 200,
|
||||
bottom: 20),
|
||||
title: Text(
|
||||
'Delete confirm'),
|
||||
content: Text(
|
||||
'Are you sure you want to delete this group? Podcasts will be moved to Home group.'),
|
||||
actions: <Widget>[
|
||||
FlatButton(
|
||||
onPressed: () =>
|
||||
Navigator.of(
|
||||
context)
|
||||
.pop(),
|
||||
child: Text(
|
||||
'CANCEL',
|
||||
style: TextStyle(
|
||||
color: Colors
|
||||
.grey[
|
||||
600]),
|
||||
),
|
||||
),
|
||||
FlatButton(
|
||||
onPressed: () {
|
||||
if (_index ==
|
||||
groupList
|
||||
.groups
|
||||
.length -
|
||||
1) {
|
||||
setState(
|
||||
() {
|
||||
_index =
|
||||
_index -
|
||||
1;
|
||||
_scroll =
|
||||
0;
|
||||
});
|
||||
groupList.delGroup(_groups[
|
||||
),
|
||||
Padding(
|
||||
padding:
|
||||
EdgeInsets.symmetric(vertical: 10.0)),
|
||||
Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
_menuController.reverse();
|
||||
setState(() => _showSetting = false);
|
||||
_index == 0
|
||||
? Fluttertoast.showToast(
|
||||
msg:
|
||||
'Home group is not supported',
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
)
|
||||
: showGeneralDialog(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
barrierLabel:
|
||||
MaterialLocalizations.of(
|
||||
context)
|
||||
.modalBarrierDismissLabel,
|
||||
barrierColor: Colors.black54,
|
||||
transitionDuration:
|
||||
const Duration(
|
||||
milliseconds: 300),
|
||||
pageBuilder: (BuildContext
|
||||
context,
|
||||
Animation animaiton,
|
||||
Animation
|
||||
secondaryAnimation) =>
|
||||
AnnotatedRegion<
|
||||
SystemUiOverlayStyle>(
|
||||
value:
|
||||
SystemUiOverlayStyle(
|
||||
statusBarIconBrightness:
|
||||
Brightness.light,
|
||||
systemNavigationBarColor:
|
||||
Theme.of(context)
|
||||
.brightness ==
|
||||
Brightness
|
||||
.light
|
||||
? Color
|
||||
.fromRGBO(
|
||||
113,
|
||||
113,
|
||||
113,
|
||||
1)
|
||||
: Color
|
||||
.fromRGBO(
|
||||
15,
|
||||
15,
|
||||
15,
|
||||
1),
|
||||
// statusBarColor: Theme.of(
|
||||
// context)
|
||||
// .brightness ==
|
||||
// Brightness.light
|
||||
// ? Color.fromRGBO(
|
||||
// 113,
|
||||
// 113,
|
||||
// 113,
|
||||
// 1)
|
||||
// : Color.fromRGBO(
|
||||
// 5, 5, 5, 1),
|
||||
),
|
||||
child: AlertDialog(
|
||||
elevation: 1,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius:
|
||||
BorderRadius.all(
|
||||
Radius.circular(
|
||||
10.0))),
|
||||
titlePadding:
|
||||
EdgeInsets.only(
|
||||
top: 20,
|
||||
left: 20,
|
||||
right: 200,
|
||||
bottom: 20),
|
||||
title: Text(
|
||||
'Delete confirm'),
|
||||
content: Text(
|
||||
'Are you sure you want to delete this group? Podcasts will be moved to Home group.'),
|
||||
actions: <Widget>[
|
||||
FlatButton(
|
||||
onPressed: () =>
|
||||
Navigator.of(
|
||||
context)
|
||||
.pop(),
|
||||
child: Text(
|
||||
'CANCEL',
|
||||
style: TextStyle(
|
||||
color: Colors
|
||||
.grey[
|
||||
600]),
|
||||
),
|
||||
),
|
||||
FlatButton(
|
||||
onPressed: () {
|
||||
if (_index ==
|
||||
groupList
|
||||
.groups
|
||||
.length -
|
||||
1) {
|
||||
setState(() {
|
||||
_index =
|
||||
_index -
|
||||
1;
|
||||
_scroll = 0;
|
||||
});
|
||||
groupList.delGroup(
|
||||
_groups[
|
||||
_index +
|
||||
1]);
|
||||
} else {
|
||||
groupList.delGroup(
|
||||
_groups[
|
||||
_index]);
|
||||
}
|
||||
Navigator.of(
|
||||
context)
|
||||
.pop();
|
||||
},
|
||||
child: Text(
|
||||
'CONFIRM',
|
||||
style: TextStyle(
|
||||
color: Colors
|
||||
.red),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
));
|
||||
},
|
||||
child: Container(
|
||||
height: 30,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[700],
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(10.0))),
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 10),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
Icons.delete_outline,
|
||||
color: Colors.red,
|
||||
size: 15.0,
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 5.0),
|
||||
),
|
||||
Text('Delete',
|
||||
style: TextStyle(
|
||||
color: Colors.red)),
|
||||
],
|
||||
),
|
||||
} else {
|
||||
groupList.delGroup(
|
||||
_groups[
|
||||
_index]);
|
||||
}
|
||||
Navigator.of(
|
||||
context)
|
||||
.pop();
|
||||
},
|
||||
child: Text(
|
||||
'CONFIRM',
|
||||
style: TextStyle(
|
||||
color: Colors
|
||||
.red),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
));
|
||||
},
|
||||
child: Container(
|
||||
height: 30,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[700],
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(10.0))),
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 10),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
Icons.delete_outline,
|
||||
color: Colors.red,
|
||||
size: 15.0,
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 5.0),
|
||||
),
|
||||
Text('Delete',
|
||||
style: TextStyle(
|
||||
color: Colors.red)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: Center(),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
: Center(),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -525,85 +517,84 @@ class _AddGroupState extends State<AddGroup> {
|
||||
var groupList = Provider.of<GroupList>(context);
|
||||
List list = groupList.groups.map((e) => e.name).toList();
|
||||
return AnnotatedRegion<SystemUiOverlayStyle>(
|
||||
value: SystemUiOverlayStyle(
|
||||
statusBarIconBrightness: Brightness.light,
|
||||
systemNavigationBarColor:
|
||||
Theme.of(context).brightness == Brightness.light
|
||||
? Color.fromRGBO(113, 113, 113, 1)
|
||||
: Color.fromRGBO(5, 5, 5, 1),
|
||||
statusBarColor: Theme.of(context).brightness == Brightness.light
|
||||
? Color.fromRGBO(113, 113, 113, 1)
|
||||
: Color.fromRGBO(15, 15, 15, 1),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: AlertDialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(10))),
|
||||
elevation: 1,
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 20),
|
||||
titlePadding:
|
||||
EdgeInsets.only(top: 20, left: 20, right: 200, bottom: 20),
|
||||
actionsPadding: EdgeInsets.all(0),
|
||||
actions: <Widget>[
|
||||
FlatButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(
|
||||
'CANCEL',
|
||||
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(),
|
||||
),
|
||||
],
|
||||
value: SystemUiOverlayStyle(
|
||||
statusBarIconBrightness: Brightness.light,
|
||||
systemNavigationBarColor:
|
||||
Theme.of(context).brightness == Brightness.light
|
||||
? Color.fromRGBO(113, 113, 113, 1)
|
||||
: Color.fromRGBO(5, 5, 5, 1),
|
||||
// statusBarColor: Theme.of(context).brightness == Brightness.light
|
||||
// ? Color.fromRGBO(113, 113, 113, 1)
|
||||
// : Color.fromRGBO(15, 15, 15, 1),
|
||||
),
|
||||
child: AlertDialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(10))),
|
||||
elevation: 1,
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 20),
|
||||
titlePadding:
|
||||
EdgeInsets.only(top: 20, left: 20, right: 200, bottom: 20),
|
||||
actionsPadding: EdgeInsets.all(0),
|
||||
actions: <Widget>[
|
||||
FlatButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(
|
||||
'CANCEL',
|
||||
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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -99,17 +99,19 @@ class _DownloadsManageState extends State<DownloadsManage> {
|
||||
Widget build(BuildContext context) {
|
||||
return AnnotatedRegion<SystemUiOverlayStyle>(
|
||||
value: SystemUiOverlayStyle(
|
||||
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
|
||||
systemNavigationBarColor: Theme.of(context).primaryColor,
|
||||
statusBarColor: Theme.of(context).primaryColor),
|
||||
child: SafeArea(
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Downloads'),
|
||||
elevation: 0,
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
),
|
||||
body: Stack(
|
||||
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
|
||||
systemNavigationBarColor: Theme.of(context).primaryColor,
|
||||
systemNavigationBarIconBrightness:
|
||||
Theme.of(context).accentColorBrightness,
|
||||
),
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Downloads'),
|
||||
elevation: 0,
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
@ -138,7 +140,7 @@ class _DownloadsManageState extends State<DownloadsManage> {
|
||||
fontSize: 40,
|
||||
fontWeight: FontWeight.bold)),
|
||||
TextSpan(
|
||||
text: ' episodes ',
|
||||
text: _fileNum < 2 ? ' episode' : ' episodes ',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).accentColor,
|
||||
fontSize: 20,
|
||||
|
@ -65,74 +65,79 @@ class _PlayedHistoryState extends State<PlayedHistory>
|
||||
value: SystemUiOverlayStyle(
|
||||
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
|
||||
systemNavigationBarColor: Theme.of(context).primaryColor,
|
||||
statusBarColor: Theme.of(context).primaryColor,
|
||||
systemNavigationBarIconBrightness:
|
||||
Theme.of(context).accentColorBrightness,
|
||||
//statusBarColor: Theme.of(context).primaryColor,
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Scaffold(
|
||||
body: NestedScrollView(
|
||||
headerSliverBuilder:
|
||||
(BuildContext context, bool innerBoxScrolled) {
|
||||
return <Widget>[
|
||||
SliverAppBar(
|
||||
elevation: 0,
|
||||
expandedHeight: 260,
|
||||
floating: false,
|
||||
pinned: true,
|
||||
flexibleSpace: LayoutBuilder(
|
||||
builder:
|
||||
(BuildContext context, BoxConstraints constraints) {
|
||||
top = constraints.biggest.height;
|
||||
return FlexibleSpaceBar(
|
||||
title: top < 70
|
||||
? Text(
|
||||
'History',
|
||||
)
|
||||
: Center(),
|
||||
background: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: 50, left: 50, right: 50, bottom: 30),
|
||||
child: FutureBuilder<List<FlSpot>>(
|
||||
future: getData(),
|
||||
builder: (context, snapshot) {
|
||||
return snapshot.hasData
|
||||
? HistoryChart(snapshot.data)
|
||||
: Center();
|
||||
}),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
SliverPersistentHeader(
|
||||
delegate: _SliverAppBarDelegate(
|
||||
TabBar(
|
||||
controller: _controller,
|
||||
tabs: <Widget>[
|
||||
Tab(
|
||||
child: Text('Listen'),
|
||||
),
|
||||
Tab(
|
||||
child: Text('Subscribe'),
|
||||
)
|
||||
],
|
||||
child: Scaffold(
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
body: SafeArea(
|
||||
child: NestedScrollView(
|
||||
headerSliverBuilder: (BuildContext context, bool innerBoxScrolled) {
|
||||
return <Widget>[
|
||||
SliverAppBar(
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
elevation: 0,
|
||||
expandedHeight: 260,
|
||||
floating: false,
|
||||
pinned: true,
|
||||
flexibleSpace: LayoutBuilder(
|
||||
builder:
|
||||
(BuildContext context, BoxConstraints constraints) {
|
||||
top = constraints.biggest.height;
|
||||
return FlexibleSpaceBar(
|
||||
title: top < 70 + MediaQuery.of(context).padding.top
|
||||
? Text(
|
||||
'History',
|
||||
)
|
||||
: Center(),
|
||||
background: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: 50, left: 50, right: 50, bottom: 30),
|
||||
child: FutureBuilder<List<FlSpot>>(
|
||||
future: getData(),
|
||||
builder: (context, snapshot) {
|
||||
return snapshot.hasData
|
||||
? HistoryChart(snapshot.data)
|
||||
: Center();
|
||||
}),
|
||||
),
|
||||
Theme.of(context).primaryColor),
|
||||
pinned: true,
|
||||
);
|
||||
},
|
||||
),
|
||||
];
|
||||
},
|
||||
body: TabBarView(controller: _controller, children: <Widget>[
|
||||
FutureBuilder<List<PlayHistory>>(
|
||||
future: getPlayHistory(),
|
||||
builder: (context, snapshot) {
|
||||
double _width = MediaQuery.of(context).size.width;
|
||||
return snapshot.hasData
|
||||
? ListView.builder(
|
||||
shrinkWrap: true,
|
||||
scrollDirection: Axis.vertical,
|
||||
itemCount: snapshot.data.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return Column(
|
||||
),
|
||||
SliverPersistentHeader(
|
||||
delegate: _SliverAppBarDelegate(
|
||||
TabBar(
|
||||
controller: _controller,
|
||||
tabs: <Widget>[
|
||||
Tab(
|
||||
child: Text('Listen'),
|
||||
),
|
||||
Tab(
|
||||
child: Text('Subscribe'),
|
||||
)
|
||||
],
|
||||
),
|
||||
Theme.of(context).primaryColor),
|
||||
pinned: true,
|
||||
),
|
||||
];
|
||||
},
|
||||
body: TabBarView(controller: _controller, children: <Widget>[
|
||||
FutureBuilder<List<PlayHistory>>(
|
||||
future: getPlayHistory(),
|
||||
builder: (context, snapshot) {
|
||||
double _width = MediaQuery.of(context).size.width;
|
||||
return snapshot.hasData
|
||||
? ListView.builder(
|
||||
shrinkWrap: true,
|
||||
scrollDirection: Axis.vertical,
|
||||
itemCount: snapshot.data.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return Container(
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
ListTile(
|
||||
title: Column(
|
||||
@ -160,12 +165,17 @@ class _PlayedHistoryState extends State<PlayedHistory>
|
||||
width: _width,
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Icon(Icons.timelapse, color: Colors.grey[400],),
|
||||
Icon(
|
||||
Icons.timelapse,
|
||||
color: Colors.grey[400],
|
||||
),
|
||||
Container(
|
||||
height: 2,
|
||||
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 *
|
||||
snapshot.data[index]
|
||||
.seekValue <
|
||||
@ -201,24 +211,27 @@ class _PlayedHistoryState extends State<PlayedHistory>
|
||||
),
|
||||
// Divider(height: 2),
|
||||
],
|
||||
);
|
||||
})
|
||||
: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
},
|
||||
),
|
||||
FutureBuilder<List<SubHistory>>(
|
||||
future: getSubHistory(),
|
||||
builder: (context, snapshot) {
|
||||
return snapshot.hasData
|
||||
? ListView.builder(
|
||||
shrinkWrap: true,
|
||||
scrollDirection: Axis.vertical,
|
||||
itemCount: snapshot.data.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
bool _status = snapshot.data[index].status;
|
||||
return Column(
|
||||
),
|
||||
);
|
||||
})
|
||||
: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
},
|
||||
),
|
||||
FutureBuilder<List<SubHistory>>(
|
||||
future: getSubHistory(),
|
||||
builder: (context, snapshot) {
|
||||
return snapshot.hasData
|
||||
? ListView.builder(
|
||||
shrinkWrap: true,
|
||||
scrollDirection: Axis.vertical,
|
||||
itemCount: snapshot.data.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
bool _status = snapshot.data[index].status;
|
||||
return Container(
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
ListTile(
|
||||
enabled: _status,
|
||||
@ -274,14 +287,16 @@ class _PlayedHistoryState extends State<PlayedHistory>
|
||||
height: 2,
|
||||
)
|
||||
],
|
||||
);
|
||||
})
|
||||
: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
},
|
||||
),
|
||||
])),
|
||||
),
|
||||
);
|
||||
})
|
||||
: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
},
|
||||
),
|
||||
]),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -318,72 +333,74 @@ class HistoryChart extends StatelessWidget {
|
||||
HistoryChart(this.stats);
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LineChart(
|
||||
LineChartData(
|
||||
backgroundColor: Colors.transparent,
|
||||
gridData: FlGridData(
|
||||
show: true,
|
||||
drawHorizontalLine: true,
|
||||
getDrawingHorizontalLine: (value) {
|
||||
return value % 60 == 0
|
||||
? FlLine(
|
||||
color: Theme.of(context).brightness == Brightness.light
|
||||
? Colors.grey[400]
|
||||
: Colors.grey[700],
|
||||
strokeWidth: 1,
|
||||
)
|
||||
: FlLine(color: Colors.transparent);
|
||||
},
|
||||
),
|
||||
titlesData: FlTitlesData(
|
||||
show: true,
|
||||
bottomTitles: SideTitles(
|
||||
textStyle: TextStyle(
|
||||
color: const Color(0xff67727d),
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
),
|
||||
showTitles: true,
|
||||
reservedSize: 10,
|
||||
getTitles: (value) {
|
||||
return DateFormat.E().format(
|
||||
DateTime.now().subtract(Duration(days: (7 - value.toInt()))));
|
||||
return SafeArea(
|
||||
child: LineChart(
|
||||
LineChartData(
|
||||
backgroundColor: Colors.transparent,
|
||||
gridData: FlGridData(
|
||||
show: true,
|
||||
drawHorizontalLine: true,
|
||||
getDrawingHorizontalLine: (value) {
|
||||
return value % 60 == 0
|
||||
? FlLine(
|
||||
color: Theme.of(context).brightness == Brightness.light
|
||||
? Colors.grey[400]
|
||||
: Colors.grey[700],
|
||||
strokeWidth: 1,
|
||||
)
|
||||
: FlLine(color: Colors.transparent);
|
||||
},
|
||||
margin: 5,
|
||||
),
|
||||
leftTitles: SideTitles(
|
||||
showTitles: true,
|
||||
textStyle: TextStyle(
|
||||
color: const Color(0xff67727d),
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
titlesData: FlTitlesData(
|
||||
show: true,
|
||||
bottomTitles: SideTitles(
|
||||
textStyle: TextStyle(
|
||||
color: const Color(0xff67727d),
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
),
|
||||
showTitles: true,
|
||||
reservedSize: 10,
|
||||
getTitles: (value) {
|
||||
return DateFormat.E().format(DateTime.now()
|
||||
.subtract(Duration(days: (7 - value.toInt()))));
|
||||
},
|
||||
margin: 5,
|
||||
),
|
||||
leftTitles: SideTitles(
|
||||
showTitles: true,
|
||||
textStyle: TextStyle(
|
||||
color: const Color(0xff67727d),
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
),
|
||||
getTitles: (value) {
|
||||
return value % 60 == 0 && value > 0 ? '${value ~/ 60}h' : '';
|
||||
},
|
||||
reservedSize: 20,
|
||||
margin: 5,
|
||||
),
|
||||
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';
|
||||
|
||||
class Libries extends StatelessWidget {
|
||||
_launchUrl(String url) async {
|
||||
_launchUrl(String url) async {
|
||||
if (await canLaunch(url)) {
|
||||
await launch(url);
|
||||
} else {
|
||||
throw 'Could not launch $url';
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnnotatedRegion<SystemUiOverlayStyle>(
|
||||
value: SystemUiOverlayStyle(
|
||||
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
|
||||
systemNavigationBarColor: Theme.of(context).primaryColor,
|
||||
statusBarColor: Theme.of(context).primaryColor),
|
||||
child: SafeArea(
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Libraies'),
|
||||
elevation: 0,
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
|
||||
systemNavigationBarColor: Theme.of(context).primaryColor,
|
||||
systemNavigationBarIconBrightness:
|
||||
Theme.of(context).accentColorBrightness,
|
||||
),
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Libraies'),
|
||||
elevation: 0,
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
),
|
||||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.vertical,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
|
@ -41,4 +41,5 @@ List<Libries> plugins = [
|
||||
Libries('audio_service', mit, 'https://pub.dev/packages/audio_service'),
|
||||
Libries('just_audio', apacheLicense, 'https://pub.dev/packages/just_audio'),
|
||||
Libries('line_icons', gpl, 'https://pub.dev/packages/line_icons'),
|
||||
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/services.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:tsacdop/class/podcastlocal.dart';
|
||||
import 'package:tsacdop/local_storage/sqflite_localpodcast.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/settingstate.dart';
|
||||
import 'package:tsacdop/settings/theme.dart';
|
||||
import 'package:tsacdop/settings/storage.dart';
|
||||
import 'package:tsacdop/settings/history.dart';
|
||||
import 'package:tsacdop/util/ompl_build.dart';
|
||||
import 'theme.dart';
|
||||
import 'storage.dart';
|
||||
import 'history.dart';
|
||||
import 'syncing.dart';
|
||||
import 'libries.dart';
|
||||
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
|
||||
var settings = Provider.of<SettingState>(context, listen: false);
|
||||
return AnnotatedRegion<SystemUiOverlayStyle>(
|
||||
value: SystemUiOverlayStyle(
|
||||
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
|
||||
systemNavigationBarColor: Theme.of(context).primaryColor,
|
||||
statusBarColor: Theme.of(context).primaryColor,
|
||||
systemNavigationBarIconBrightness:
|
||||
Theme.of(context).accentColorBrightness,
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Settings'),
|
||||
elevation: 0,
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Settings'),
|
||||
elevation: 0,
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
),
|
||||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
scrollDirection: Axis.vertical,
|
||||
child: Column(
|
||||
@ -72,16 +93,14 @@ class Settings extends StatelessWidget {
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ThemeSetting())),
|
||||
contentPadding:
|
||||
EdgeInsets.symmetric(horizontal: 25.0),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 25.0),
|
||||
leading: Icon(LineIcons.adjust_solid),
|
||||
title: Text('Appearance'),
|
||||
subtitle: Text('Colors and themes'),
|
||||
),
|
||||
Divider(height: 2),
|
||||
ListTile(
|
||||
contentPadding:
|
||||
EdgeInsets.symmetric(horizontal: 25.0),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 25.0),
|
||||
leading: Icon(LineIcons.play_circle),
|
||||
title: Text('AutoPlay'),
|
||||
subtitle: Text('Autoplay next episode in playlist'),
|
||||
@ -94,20 +113,14 @@ class Settings extends StatelessWidget {
|
||||
),
|
||||
Divider(height: 2),
|
||||
ListTile(
|
||||
contentPadding:
|
||||
EdgeInsets.symmetric(horizontal: 25.0),
|
||||
onTap: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => SyncingSetting())),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 25.0),
|
||||
leading: Icon(LineIcons.cloud_download_alt_solid),
|
||||
title: Text('AutoUpdate'),
|
||||
subtitle: Text('Auto update feed every day'),
|
||||
trailing: Selector<SettingState, bool>(
|
||||
selector: (_, settings) => settings.autoUpdate,
|
||||
builder: (_, data, __) => Switch(
|
||||
value: data,
|
||||
onChanged: (boo) async {
|
||||
settings.autoUpdate = boo;
|
||||
if (!boo) await Workmanager.cancelAll();
|
||||
}),
|
||||
),
|
||||
title: Text('Syncing'),
|
||||
subtitle: Text('Refresh podcasts in the background'),
|
||||
),
|
||||
Divider(height: 2),
|
||||
ListTile(
|
||||
@ -115,8 +128,7 @@ class Settings extends StatelessWidget {
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => StorageSetting())),
|
||||
contentPadding:
|
||||
EdgeInsets.symmetric(horizontal: 25.0),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 25.0),
|
||||
leading: Icon(LineIcons.save),
|
||||
title: Text('Storage'),
|
||||
subtitle: Text('Manage cache and download storage'),
|
||||
@ -127,13 +139,22 @@ class Settings extends StatelessWidget {
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => PlayedHistory())),
|
||||
contentPadding:
|
||||
EdgeInsets.symmetric(horizontal: 25.0),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 25.0),
|
||||
leading: Icon(Icons.update),
|
||||
title: Text('History'),
|
||||
subtitle: Text('Listen data'),
|
||||
),
|
||||
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(
|
||||
onTap: () => _launchUrl(
|
||||
'https://github.com/stonega/tsacdop/releases'),
|
||||
contentPadding:
|
||||
EdgeInsets.symmetric(horizontal: 25.0),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 25.0),
|
||||
leading: Icon(LineIcons.map_signs_solid),
|
||||
title: Text('Changelog'),
|
||||
subtitle: Text('List of chagnes'),
|
||||
),
|
||||
Divider(height: 2),
|
||||
ListTile(
|
||||
onTap: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => Libries())),
|
||||
contentPadding:
|
||||
EdgeInsets.symmetric(horizontal: 25.0),
|
||||
onTap: () => Navigator.push(context,
|
||||
MaterialPageRoute(builder: (context) => Libries())),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 25.0),
|
||||
leading: Icon(LineIcons.book_open_solid),
|
||||
title: Text('Libraries'),
|
||||
subtitle:
|
||||
Text('Open source libraried in application'),
|
||||
subtitle: Text('Open source libraries in application'),
|
||||
),
|
||||
Divider(height: 2),
|
||||
ListTile(
|
||||
onTap: () => _launchUrl(
|
||||
'mailto:<xijieyin@gmail.com>?subject=Tsacdop Feedback'),
|
||||
contentPadding:
|
||||
EdgeInsets.symmetric(horizontal: 25.0),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 25.0),
|
||||
leading: Icon(LineIcons.bug_solid),
|
||||
title: Text('Feedback'),
|
||||
subtitle: Text('Bugs and feature requests'),
|
||||
|
@ -8,17 +8,19 @@ class StorageSetting extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return AnnotatedRegion<SystemUiOverlayStyle>(
|
||||
value: SystemUiOverlayStyle(
|
||||
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
|
||||
systemNavigationBarColor: Theme.of(context).primaryColor,
|
||||
statusBarColor: Theme.of(context).primaryColor),
|
||||
child: SafeArea(
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Storage'),
|
||||
elevation: 0,
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
),
|
||||
body: Column(
|
||||
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
|
||||
systemNavigationBarColor: Theme.of(context).primaryColor,
|
||||
systemNavigationBarIconBrightness:
|
||||
Theme.of(context).accentColorBrightness,
|
||||
),
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Storage'),
|
||||
elevation: 0,
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
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);
|
||||
return AnnotatedRegion<SystemUiOverlayStyle>(
|
||||
value: SystemUiOverlayStyle(
|
||||
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
|
||||
systemNavigationBarColor: Theme.of(context).primaryColor,
|
||||
statusBarColor: Theme.of(context).primaryColor),
|
||||
child: SafeArea(
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Appearance'),
|
||||
elevation: 0,
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
),
|
||||
body: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: EdgeInsets.all(10.0),
|
||||
),
|
||||
Container(
|
||||
height: 30.0,
|
||||
padding: EdgeInsets.symmetric(horizontal: 80),
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text('Interface',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyText1
|
||||
.copyWith(color: Theme.of(context).accentColor)),
|
||||
),
|
||||
ListView(
|
||||
shrinkWrap: true,
|
||||
scrollDirection: Axis.vertical,
|
||||
children: <Widget>[
|
||||
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(
|
||||
titlePadding: EdgeInsets.only(
|
||||
top: 20,
|
||||
left: 40,
|
||||
right: 200,
|
||||
),
|
||||
elevation: 1,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(10.0))),
|
||||
title: Text('Theme'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
RadioListTile(
|
||||
title: Container(
|
||||
padding: EdgeInsets.only(
|
||||
right: 80),
|
||||
child:
|
||||
Text('System default')),
|
||||
value: ThemeMode.system,
|
||||
groupValue: settings.theme,
|
||||
onChanged: (value) {
|
||||
settings.setTheme = value;
|
||||
Navigator.of(context).pop();
|
||||
}),
|
||||
RadioListTile(
|
||||
title: Text('Dark mode'),
|
||||
value: ThemeMode.dark,
|
||||
groupValue: settings.theme,
|
||||
onChanged: (value) {
|
||||
settings.setTheme = value;
|
||||
Navigator.of(context).pop();
|
||||
}),
|
||||
RadioListTile(
|
||||
title: Text('Light mode'),
|
||||
value: ThemeMode.light,
|
||||
groupValue: settings.theme,
|
||||
onChanged: (value) {
|
||||
settings.setTheme = value;
|
||||
Navigator.of(context).pop();
|
||||
}),
|
||||
],
|
||||
),
|
||||
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
|
||||
systemNavigationBarColor: Theme.of(context).primaryColor,
|
||||
systemNavigationBarIconBrightness:
|
||||
Theme.of(context).accentColorBrightness,
|
||||
),
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Appearance'),
|
||||
elevation: 0,
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
),
|
||||
body: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: EdgeInsets.all(10.0),
|
||||
),
|
||||
Container(
|
||||
height: 30.0,
|
||||
padding: EdgeInsets.symmetric(horizontal: 80),
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text('Interface',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyText1
|
||||
.copyWith(color: Theme.of(context).accentColor)),
|
||||
),
|
||||
ListView(
|
||||
shrinkWrap: true,
|
||||
scrollDirection: Axis.vertical,
|
||||
children: <Widget>[
|
||||
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(
|
||||
titlePadding: EdgeInsets.only(
|
||||
top: 20,
|
||||
left: 40,
|
||||
right: 200,
|
||||
),
|
||||
elevation: 1,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(10.0))),
|
||||
title: Text('Theme'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
RadioListTile(
|
||||
title: Container(
|
||||
padding:
|
||||
EdgeInsets.only(right: 80),
|
||||
child: Text('System default')),
|
||||
value: ThemeMode.system,
|
||||
groupValue: settings.theme,
|
||||
onChanged: (value) {
|
||||
settings.setTheme = value;
|
||||
Navigator.of(context).pop();
|
||||
}),
|
||||
RadioListTile(
|
||||
title: Text('Dark mode'),
|
||||
value: ThemeMode.dark,
|
||||
groupValue: settings.theme,
|
||||
onChanged: (value) {
|
||||
settings.setTheme = value;
|
||||
Navigator.of(context).pop();
|
||||
}),
|
||||
RadioListTile(
|
||||
title: Text('Light mode'),
|
||||
value: ThemeMode.light,
|
||||
groupValue: settings.theme,
|
||||
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(
|
||||
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),
|
||||
title: Text('Accent color'),
|
||||
subtitle: Text('Include the overlay color'),
|
||||
),
|
||||
Divider(height: 2),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
))),
|
||||
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 showNumber;
|
||||
final String heroTag;
|
||||
final int updateCount;
|
||||
EpisodeGrid(
|
||||
{Key key,
|
||||
this.podcast,
|
||||
this.showDownload,
|
||||
this.showFavorite,
|
||||
this.showNumber,
|
||||
this.heroTag})
|
||||
this.heroTag,
|
||||
this.updateCount = 0})
|
||||
: super(key: key);
|
||||
Offset offset;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
double _width = MediaQuery.of(context).size.width;
|
||||
|
||||
Offset _offset;
|
||||
_showPopupMenu(Offset offset, EpisodeBrief episode, BuildContext context,
|
||||
bool isPlaying, bool isInPlaylist) async {
|
||||
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
|
||||
@ -83,7 +85,7 @@ class EpisodeGrid extends StatelessWidget {
|
||||
if (value == 0) {
|
||||
if (!isPlaying) audio.episodeLoad(episode);
|
||||
} else if (value == 1) {
|
||||
if (isInPlaylist) {
|
||||
if (!isInPlaylist) {
|
||||
audio.addToPlaylist(episode);
|
||||
Fluttertoast.showToast(
|
||||
msg: 'Added to playlist',
|
||||
@ -134,10 +136,10 @@ class EpisodeGrid extends StatelessWidget {
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.all(Radius.circular(5.0)),
|
||||
onTapDown: (details) => offset = Offset(
|
||||
onTapDown: (details) => _offset = Offset(
|
||||
details.globalPosition.dx, details.globalPosition.dy),
|
||||
onLongPress: () => _showPopupMenu(
|
||||
offset,
|
||||
_offset,
|
||||
podcast[index],
|
||||
context,
|
||||
data.item1 == podcast[index],
|
||||
@ -185,6 +187,7 @@ class EpisodeGrid extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
Spacer(),
|
||||
index < updateCount ? Text('New', style: TextStyle(color: Colors.red, fontStyle: FontStyle.italic)) : Center(),
|
||||
showNumber
|
||||
? Container(
|
||||
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) {
|
||||
if (element == null) {
|
||||
return null;
|
||||
return RssContent('', ['']);
|
||||
}
|
||||
final content = element.text;
|
||||
final content = element.text.trim();
|
||||
final images = <String>[];
|
||||
_imagesRegExp.allMatches(content).forEach((match) {
|
||||
images.add(match.group(1));
|
||||
|
@ -44,20 +44,24 @@ class RssItem {
|
||||
});
|
||||
|
||||
factory RssItem.parse(XmlElement element) {
|
||||
if (RssEnclosure.parse(findElementOrNull(element, "enclosure")) == null) {
|
||||
return null;
|
||||
}
|
||||
return RssItem(
|
||||
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(),
|
||||
categories: element.findElements("category").map((element) {
|
||||
return RssCategory.parse(element);
|
||||
}).toList(),
|
||||
//guid: findElementOrNull(element, "guid")?.text,
|
||||
//guid: findElementOrNull(element, "guid")?.text,
|
||||
pubDate: findElementOrNull(element, "pubDate")?.text?.trim(),
|
||||
author: findElementOrNull(element, "author")?.text?.trim(),
|
||||
// comments: findElementOrNull(element, "comments")?.text,
|
||||
// source: RssSource.parse(findElementOrNull(element, "source")),
|
||||
// content: RssContent.parse(findElementOrNull(element, "content:encoded")),
|
||||
// media: Media.parse(element),
|
||||
// comments: findElementOrNull(element, "comments")?.text,
|
||||
// source: RssSource.parse(findElementOrNull(element, "source")),
|
||||
content: RssContent.parse(findElementOrNull(element, "content:encoded")),
|
||||
// media: Media.parse(element),
|
||||
enclosure: RssEnclosure.parse(findElementOrNull(element, "enclosure")),
|
||||
//dc: DublinCore.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.
|
||||
# Read more about iOS versioning at
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
version: 0.1.2
|
||||
version: 0.1.4
|
||||
|
||||
environment:
|
||||
sdk: ">=2.6.0 <3.0.0"
|
||||
@ -58,6 +58,7 @@ dev_dependencies:
|
||||
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
|
||||
|
@ -5,7 +5,6 @@
|
||||
// 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.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.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)};');
|
||||
}
|