Export ompli file

Storage management
Syncing setting
This commit is contained in:
stonegate 2020-03-20 03:58:30 +08:00
parent 505215bf0d
commit 57bf41114b
79 changed files with 4193 additions and 2316 deletions

View File

@ -16,7 +16,7 @@ jobs:
- run: echo $ENCODED_KEYSTORE | base64 -di > ${HOME}/keystore.jks - run: echo $ENCODED_KEYSTORE | base64 -di > ${HOME}/keystore.jks
- run: echo 'export KEYSTORE=${HOME}/keystore.jks' >> $BASH_ENV - run: echo 'export KEYSTORE=${HOME}/keystore.jks' >> $BASH_ENV
- run: dart tool/env.dart
- run: - run:
name: Build the Android version name: Build the Android version
command: flutter build apk --split-per-abi --no-shrink command: flutter build apk --split-per-abi --no-shrink

View File

@ -5,7 +5,7 @@
<img src="https://raw.githubusercontent.com/stonega/tsacdop/master/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png" art = "Logo"/> <img src="https://raw.githubusercontent.com/stonega/tsacdop/master/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png" art = "Logo"/>
</br> </br>
<img src="https://raw.githubusercontent.com/stonega/tsacdop/master/android/app/src/main/res/mipmap-xhdpi/text.png" art = "Tsacdop"/> <img src="https://raw.githubusercontent.com/stonega/tsacdop/master/android/app/src/main/res/mipmap-xhdpi/text.png" art = "Tsacdop"/>
t p</p> </p>
Enjoy podcasts with Tsacdop. Enjoy podcasts with Tsacdop.

View File

@ -45,7 +45,7 @@ android {
defaultConfig { defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.stonegate.tsacdop" applicationId "com.stonegate.tsacdop"
minSdkVersion 16 minSdkVersion 19
targetSdkVersion 28 targetSdkVersion 28
versionCode flutterVersionCode.toInteger() versionCode flutterVersionCode.toInteger()
versionName flutterVersionName versionName flutterVersionName

View File

@ -1,4 +1,4 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.stonegate.tsacdop"> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.stonegate.tsacdop" xmlns:tools="http://schemas.android.com/tools">
<!-- io.flutter.app.FlutterApplication is an android.app.Application that <!-- io.flutter.app.FlutterApplication is an android.app.Application that
calls FlutterMain.startInitialization(this); in its onCreate method. calls FlutterMain.startInitialization(this); in its onCreate method.
In most cases you can leave this as-is, but you if you want to provide In most cases you can leave this as-is, but you if you want to provide
@ -7,7 +7,7 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/> <uses-permission android:name="android.permission.WAKE_LOCK"/>
<application android:name="io.flutter.app.FlutterApplication" android:label="Tsacdop" android:icon="@mipmap/ic_launcher" android:networkSecurityConfig="@xml/network_security_config"> <application android:name="io.flutter.app.FlutterApplication" android:label="Tsacdop" android:icon="@mipmap/ic_launcher_icon" android:roundIcon="@mipmap/ic_launcher_round" android:networkSecurityConfig="@xml/network_security_config">
<activity android:name=".MainActivity" android:launchMode="singleTop" android:theme="@style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize"> <activity android:name=".MainActivity" android:launchMode="singleTop" android:theme="@style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 725 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 427 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 822 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -7,7 +7,7 @@
<bitmap <bitmap
android:gravity="center" android:gravity="center"
android:tileMode="disabled" android:tileMode="disabled"
android:src="@mipmap/ic_launcher" /> android:src="@mipmap/ic_splash" />
</item> </item>
<item android:bottom="100dp"> <item android:bottom="100dp">
<bitmap <bitmap

View File

@ -7,7 +7,7 @@
<bitmap <bitmap
android:gravity="center" android:gravity="center"
android:tileMode="disabled" android:tileMode="disabled"
android:src="@mipmap/ic_launcher" /> android:src="@mipmap/ic_splash" />
</item> </item>
<item android:bottom="100dp"> <item android:bottom="100dp">
<bitmap <bitmap

View File

@ -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>

View File

@ -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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@ -3,4 +3,7 @@
<color name = "blackGrey"> <color name = "blackGrey">
#121212 #121212
</color> </color>
<color name="ic_launcher_background">
#ffffff
</color>
</resources> </resources>

View File

@ -80,8 +80,7 @@ class Playlist {
_playlist = []; _playlist = [];
await Future.forEach(urls, (url) async { await Future.forEach(urls, (url) async {
EpisodeBrief episode = await dbHelper.getRssItemWithUrl(url); EpisodeBrief episode = await dbHelper.getRssItemWithUrl(url);
print(episode.title); if(episode != null) _playlist.add(episode);
_playlist.add(episode);
}); });
} }
print('Playlist: ' + _playlist.length.toString()); print('Playlist: ' + _playlist.length.toString());
@ -127,8 +126,10 @@ class AudioPlayerNotifier extends ChangeNotifier {
int _lastPostion = 0; int _lastPostion = 0;
bool _stopOnComplete = false; bool _stopOnComplete = false;
Timer _stopTimer; Timer _stopTimer;
int _timeLeft = 0;
//Show stopwatch after user setting timer. //Show stopwatch after user setting timer.
bool _showStopWatch = false; bool _showStopWatch = false;
double _switchValue = 0;
bool _autoPlay = true; bool _autoPlay = true;
DateTime _current; DateTime _current;
int _currentPosition; int _currentPosition;
@ -146,11 +147,18 @@ class AudioPlayerNotifier extends ChangeNotifier {
bool get stopOnComplete => _stopOnComplete; bool get stopOnComplete => _stopOnComplete;
bool get showStopWatch => _showStopWatch; bool get showStopWatch => _showStopWatch;
bool get autoPlay => _autoPlay; bool get autoPlay => _autoPlay;
int get timeLeft => _timeLeft;
double get switchValue => _switchValue;
set setStopOnComplete(bool boo) { set setSwitchValue(double value) {
_stopOnComplete = boo; _switchValue = value;
notifyListeners();
} }
// set setStopOnComplete(bool boo) {
// _stopOnComplete = boo;
//}
set autoPlaySwitch(bool boo) { set autoPlaySwitch(bool boo) {
_autoPlay = boo; _autoPlay = boo;
notifyListeners(); notifyListeners();
@ -203,12 +211,12 @@ class AudioPlayerNotifier extends ChangeNotifier {
backgroundTaskEntrypoint: _audioPlayerTaskEntrypoint, backgroundTaskEntrypoint: _audioPlayerTaskEntrypoint,
androidNotificationChannelName: 'Tsacdop', androidNotificationChannelName: 'Tsacdop',
notificationColor: 0xFF2196f3, notificationColor: 0xFF2196f3,
androidNotificationIcon: 'mipmap/ic_launcher', androidNotificationIcon: 'drawable/ic_notification',
enableQueue: true, enableQueue: true,
androidStopOnRemoveTask: true, androidStopOnRemoveTask: true,
); );
_playerRunning = true; _playerRunning = true;
if (autoPlay) { if (_autoPlay) {
await Future.forEach(_queue.playlist, (episode) async { await Future.forEach(_queue.playlist, (episode) async {
await AudioService.addQueueItem(episode.toMediaItem()); await AudioService.addQueueItem(episode.toMediaItem());
}); });
@ -217,8 +225,6 @@ class AudioPlayerNotifier extends ChangeNotifier {
} }
await AudioService.play(); await AudioService.play();
AudioService.currentMediaItemStream.listen((item) async { AudioService.currentMediaItemStream.listen((item) async {
print(position);
print(_backgroundAudioDuration);
if (item != null) { if (item != null) {
_episode = await dbHelper.getRssItemWithMediaId(item.id); _episode = await dbHelper.getRssItemWithMediaId(item.id);
_backgroundAudioDuration = item?.duration ?? 0; _backgroundAudioDuration = item?.duration ?? 0;
@ -226,7 +232,6 @@ class AudioPlayerNotifier extends ChangeNotifier {
AudioService.seekTo(position); AudioService.seekTo(position);
position = 0; position = 0;
} }
// _playerRunning = true;
} }
notifyListeners(); notifyListeners();
}); });
@ -235,6 +240,7 @@ class AudioPlayerNotifier extends ChangeNotifier {
_audioState = event?.basicState; _audioState = event?.basicState;
if (_audioState == BasicPlaybackState.skippingToNext && if (_audioState == BasicPlaybackState.skippingToNext &&
_episode != null) { _episode != null) {
print(_episode.title);
_queue.delFromPlaylist(_episode); _queue.delFromPlaylist(_episode);
} }
if (_audioState == BasicPlaybackState.paused || if (_audioState == BasicPlaybackState.paused ||
@ -253,7 +259,7 @@ class AudioPlayerNotifier extends ChangeNotifier {
Timer.periodic(Duration(milliseconds: 500), (timer) { Timer.periodic(Duration(milliseconds: 500), (timer) {
if (_noSlide) { if (_noSlide) {
_audioState == BasicPlaybackState.playing _audioState == BasicPlaybackState.playing
? (_backgroundAudioPosition < _backgroundAudioDuration) ? (_backgroundAudioPosition < _backgroundAudioDuration - 500)
? _backgroundAudioPosition = _currentPosition + ? _backgroundAudioPosition = _currentPosition +
DateTime.now().difference(_current).inMilliseconds DateTime.now().difference(_current).inMilliseconds
: _backgroundAudioPosition = _backgroundAudioDuration : _backgroundAudioPosition = _backgroundAudioDuration
@ -269,6 +275,19 @@ class AudioPlayerNotifier extends ChangeNotifier {
_lastPostion = _backgroundAudioPosition; _lastPostion = _backgroundAudioPosition;
storage.saveInt(_lastPostion); storage.saveInt(_lastPostion);
} }
if ((_queue.playlist.length == 1 || !_autoPlay) &&
_seekSliderValue == 1 &&
_episode != null) {
_queue.delFromPlaylist(_episode);
_lastPostion = 0;
storage.saveInt(_lastPostion);
PlayHistory history = PlayHistory(
_episode.title,
_episode.enclosureUrl,
backgroundAudioPosition / 1000,
seekSliderValue);
dbHelper.saveHistory(history);
}
notifyListeners(); notifyListeners();
} }
if (_audioState == BasicPlaybackState.stopped) { if (_audioState == BasicPlaybackState.stopped) {
@ -327,6 +346,17 @@ class AudioPlayerNotifier extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
moveToTop(EpisodeBrief episode) async {
await delFromPlaylist(episode);
if (_playerRunning) {
await addToPlaylistAt(episode, 1);
} else {
await addToPlaylistAt(episode, 0);
_lastPostion = 0;
storage.saveInt(_lastPostion);
}
}
pauseAduio() async { pauseAduio() async {
AudioService.pause(); AudioService.pause();
} }
@ -353,10 +383,22 @@ class AudioPlayerNotifier extends ChangeNotifier {
//Set sleep time //Set sleep time
sleepTimer(int mins) { sleepTimer(int mins) {
_showStopWatch = true; _showStopWatch = true;
_switchValue = 1;
notifyListeners(); notifyListeners();
_timeLeft = mins * 60;
Timer.periodic(Duration(seconds: 1), (timer) {
if (_timeLeft == 0) {
timer.cancel();
notifyListeners();
} else {
_timeLeft = _timeLeft - 1;
notifyListeners();
}
});
_stopTimer = Timer(Duration(minutes: mins), () { _stopTimer = Timer(Duration(minutes: mins), () {
_stopOnComplete = false; _stopOnComplete = false;
_showStopWatch = false; _showStopWatch = false;
_switchValue = 0;
AudioService.stop(); AudioService.stop();
notifyListeners(); notifyListeners();
}); });
@ -365,7 +407,9 @@ class AudioPlayerNotifier extends ChangeNotifier {
//Cancel sleep timer //Cancel sleep timer
cancelTimer() { cancelTimer() {
_stopTimer.cancel(); _stopTimer.cancel();
_timeLeft = 0;
_showStopWatch = false; _showStopWatch = false;
_switchValue = 0;
notifyListeners(); notifyListeners();
} }
@ -393,7 +437,7 @@ class AudioPlayerTask extends BackgroundAudioTask {
case AudioPlaybackState.none: case AudioPlaybackState.none:
return BasicPlaybackState.none; return BasicPlaybackState.none;
case AudioPlaybackState.stopped: case AudioPlaybackState.stopped:
return BasicPlaybackState.stopped; return _skipState ?? BasicPlaybackState.stopped;
case AudioPlaybackState.paused: case AudioPlaybackState.paused:
return BasicPlaybackState.paused; return BasicPlaybackState.paused;
case AudioPlaybackState.playing: case AudioPlaybackState.playing:
@ -438,6 +482,10 @@ class AudioPlayerTask extends BackgroundAudioTask {
if (hasNext) { if (hasNext) {
onSkipToNext(); onSkipToNext();
} else { } else {
_skipState = BasicPlaybackState.skippingToNext;
_audioPlayer.stop();
_queue.removeAt(0);
_skipState = null;
onStop(); onStop();
} }
} }
@ -459,20 +507,24 @@ class AudioPlayerTask extends BackgroundAudioTask {
await _audioPlayer.stop(); await _audioPlayer.stop();
_queue.removeAt(0); _queue.removeAt(0);
} }
AudioServiceBackground.setQueue(_queue); if (_queue.length == 0) {
AudioServiceBackground.setMediaItem(mediaItem); onStop();
_skipState = BasicPlaybackState.skippingToNext;
await _audioPlayer.setUrl(mediaItem.id);
print(mediaItem.id);
Duration duration = await _audioPlayer.durationFuture;
AudioServiceBackground.setMediaItem(
mediaItem.copyWith(duration: duration.inMilliseconds));
_skipState = null;
// Resume playback if we were playing
if (_playing) {
onPlay();
} else { } else {
_setState(state: BasicPlaybackState.paused); AudioServiceBackground.setQueue(_queue);
AudioServiceBackground.setMediaItem(mediaItem);
_skipState = BasicPlaybackState.skippingToNext;
await _audioPlayer.setUrl(mediaItem.id);
print(mediaItem.id);
Duration duration = await _audioPlayer.durationFuture;
AudioServiceBackground.setMediaItem(
mediaItem.copyWith(duration: duration.inMilliseconds));
_skipState = null;
// Resume playback if we were playing
if (_playing) {
onPlay();
} else {
_setState(state: BasicPlaybackState.paused);
}
} }
} }

View File

@ -32,8 +32,12 @@ class EpisodeBrief {
String dateToString() { String dateToString() {
DateTime date = DateTime.fromMillisecondsSinceEpoch(pubDate, isUtc: true); DateTime date = DateTime.fromMillisecondsSinceEpoch(pubDate, isUtc: true);
var diffrence = DateTime.now().difference(date); var diffrence = DateTime.now().difference(date);
if (diffrence.inHours < 24) { if (diffrence.inHours < 1) {
return '1 hour ago';
} else if (diffrence.inHours < 24) {
return '${diffrence.inHours} hours ago'; return '${diffrence.inHours} hours ago';
} else if (diffrence.inHours == 24) {
return '1 day ago';
} else if (diffrence.inDays < 7) { } else if (diffrence.inDays < 7) {
return '${diffrence.inDays} days ago'; return '${diffrence.inDays} days ago';
} else { } else {

View File

@ -3,12 +3,15 @@ class PodcastLocal {
final String imageUrl; final String imageUrl;
final String rssUrl; final String rssUrl;
final String author; final String author;
String description;
final String primaryColor; final String primaryColor;
final String id; final String id;
final String imagePath; final String imagePath;
final String provider; final String provider;
final String link; final String link;
final String description;
final int upateCount;
PodcastLocal( PodcastLocal(
this.title, this.title,
this.imageUrl, this.imageUrl,
@ -18,5 +21,10 @@ class PodcastLocal {
this.id, this.id,
this.imagePath, this.imagePath,
this.provider, this.provider,
this.link); this.link,
{
this.description ='',
this.upateCount = 0,
}
);
} }

View File

@ -1,17 +1,38 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:tsacdop/class/podcastlocal.dart';
import 'package:workmanager/workmanager.dart';
import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
import 'package:tsacdop/local_storage/key_value_storage.dart'; import 'package:tsacdop/local_storage/key_value_storage.dart';
void callbackDispatcher() {
Workmanager.executeTask((task, inputData) async {
var dbHelper = DBHelper();
print('Start task');
List<PodcastLocal> podcastList = await dbHelper.getPodcastLocalAll();
int i = 0;
await Future.forEach(podcastList, (podcastLocal) async {
i += await dbHelper.updatePodcastRss(podcastLocal);
print('Refresh ' + podcastLocal.title);
});
KeyValueStorage refreshstorage = KeyValueStorage('refreshdate');
await refreshstorage.saveInt(DateTime.now().millisecondsSinceEpoch);
KeyValueStorage refreshcountstorage = KeyValueStorage('refreshcount');
await refreshcountstorage.saveInt(i);
return Future.value(true);
});
}
class SettingState extends ChangeNotifier { class SettingState extends ChangeNotifier {
KeyValueStorage themestorage = KeyValueStorage('themes'); KeyValueStorage themestorage = KeyValueStorage('themes');
KeyValueStorage accentstorage = KeyValueStorage('accents'); KeyValueStorage accentstorage = KeyValueStorage('accents');
KeyValueStorage autoupdatestorage = KeyValueStorage('autoupdate'); KeyValueStorage autoupdatestorage = KeyValueStorage('autoupdate');
KeyValueStorage intervalstorage = KeyValueStorage('updateInterval');
Future initData() async { Future initData() async {
await _getTheme(); await _getTheme();
await _getAccentSetColor(); await _getAccentSetColor();
await _getAutoUpdate();
} }
ThemeMode _theme; ThemeMode _theme;
@ -23,6 +44,28 @@ class SettingState extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
void setWorkManager(int hour) {
_updateInterval = hour;
notifyListeners();
_saveUpdateInterval();
Workmanager.initialize(
callbackDispatcher,
isInDebugMode: true,
);
Workmanager.registerPeriodicTask("1", "update_podcasts",
frequency: Duration(hours: hour),
initialDelay: Duration(seconds: 10),
constraints: Constraints(
networkType: NetworkType.connected,
));
print('work manager init done + ');
}
void cancelWork() {
Workmanager.cancelAll();
print('work job cancelled');
}
Color _accentSetColor; Color _accentSetColor;
Color get accentSetColor => _accentSetColor; Color get accentSetColor => _accentSetColor;
@ -32,6 +75,10 @@ class SettingState extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
int _updateInterval;
int get updateInterval => _updateInterval;
int _initUpdateTag;
bool _autoUpdate; bool _autoUpdate;
bool get autoUpdate => _autoUpdate; bool get autoUpdate => _autoUpdate;
set autoUpdate(bool boo) { set autoUpdate(bool boo) {
@ -46,18 +93,21 @@ class SettingState extends ChangeNotifier {
_getTheme(); _getTheme();
_getAccentSetColor(); _getAccentSetColor();
_getAutoUpdate(); _getAutoUpdate();
_getUpdateInterval().then((value) {
if (_initUpdateTag == 0) setWorkManager(24);
});
} }
_getTheme() async { Future _getTheme() async {
int mode = await themestorage.getInt(); int mode = await themestorage.getInt();
_theme = ThemeMode.values[mode]; _theme = ThemeMode.values[mode];
} }
_saveTheme() async { Future _saveTheme() async {
await themestorage.saveInt(_theme.index); await themestorage.saveInt(_theme.index);
} }
_getAccentSetColor() async { Future _getAccentSetColor() async {
String colorString = await accentstorage.getString(); String colorString = await accentstorage.getString();
print(colorString); print(colorString);
if (colorString.isNotEmpty) { if (colorString.isNotEmpty) {
@ -69,17 +119,26 @@ class SettingState extends ChangeNotifier {
} }
} }
_saveAccentSetColor() async { Future _saveAccentSetColor() async {
await accentstorage await accentstorage
.saveString(_accentSetColor.toString().substring(10, 16)); .saveString(_accentSetColor.toString().substring(10, 16));
} }
_getAutoUpdate() async { Future _getAutoUpdate() async {
int i = await autoupdatestorage.getInt(); int i = await autoupdatestorage.getInt();
_autoUpdate = i == 0 ? false : true; _autoUpdate = i == 0 ? true : false;
} }
_saveAutoUpdate() async { Future _saveAutoUpdate() async {
await autoupdatestorage.saveInt(_autoUpdate ? 1 : 0); await autoupdatestorage.saveInt(_autoUpdate ? 0 : 1);
}
Future _getUpdateInterval() async {
_initUpdateTag = await intervalstorage.getInt();
_updateInterval = _initUpdateTag == 0 ? 24 : _initUpdateTag;
}
Future _saveUpdateInterval() async {
await intervalstorage.saveInt(_updateInterval);
} }
} }

View File

@ -33,7 +33,8 @@ class _EpisodeDetailState extends State<EpisodeDetail> {
String path; String path;
Future getSDescription(String url) async { Future getSDescription(String url) async {
var dbHelper = DBHelper(); var dbHelper = DBHelper();
widget.episodeItem.description = await dbHelper.getDescription(url); widget.episodeItem.description = (await dbHelper.getDescription(url))
.replaceAll(RegExp(r'\s?<p>(<br>)?</p>\s?'), '');
if (mounted) if (mounted)
setState(() { setState(() {
_loaddes = true; _loaddes = true;
@ -82,159 +83,179 @@ class _EpisodeDetailState extends State<EpisodeDetail> {
value: SystemUiOverlayStyle( value: SystemUiOverlayStyle(
statusBarIconBrightness: Theme.of(context).accentColorBrightness, statusBarIconBrightness: Theme.of(context).accentColorBrightness,
systemNavigationBarColor: Theme.of(context).primaryColor, systemNavigationBarColor: Theme.of(context).primaryColor,
statusBarColor: Theme.of(context).primaryColor, systemNavigationBarIconBrightness:
Theme.of(context).accentColorBrightness,
// statusBarColor: Theme.of(context).primaryColor,
), ),
child: SafeArea( child: Scaffold(
child: Scaffold( backgroundColor: Theme.of(context).primaryColor,
backgroundColor: Theme.of(context).primaryColor, appBar: AppBar(
appBar: AppBar( title: Text(widget.episodeItem.feedTitle),
title: Text(widget.episodeItem.feedTitle), centerTitle: true,
centerTitle: true, ),
), body: Stack(
body: Stack( children: <Widget>[
children: <Widget>[ Container(
Container( color: Theme.of(context).primaryColor,
color: Theme.of(context).primaryColor, child: Column(
padding: EdgeInsets.all(10.0), mainAxisAlignment: MainAxisAlignment.start,
child: Column( mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start, children: <Widget>[
mainAxisSize: MainAxisSize.min, Container(
children: <Widget>[ child: Column(
Container( mainAxisAlignment: MainAxisAlignment.start,
child: Column( crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start, children: <Widget>[
crossAxisAlignment: CrossAxisAlignment.start, Container(
children: <Widget>[ padding: EdgeInsets.symmetric(horizontal: 20.0),
Container( alignment: Alignment.topLeft,
padding: EdgeInsets.symmetric(horizontal: 12.0), child: Text(
alignment: Alignment.topLeft, widget.episodeItem.title,
child: Text( style: Theme.of(context).textTheme.headline5,
widget.episodeItem.title,
style: Theme.of(context).textTheme.headline5,
),
), ),
Container( ),
alignment: Alignment.centerLeft, Container(
padding: EdgeInsets.symmetric(horizontal: 12.0), alignment: Alignment.centerLeft,
height: 30.0, padding: EdgeInsets.symmetric(horizontal: 20.0),
child: Text( height: 30.0,
'Published ' + child: Text(
DateFormat.yMMMd().format( 'Published ' +
DateTime.fromMillisecondsSinceEpoch( DateFormat.yMMMd().format(
widget.episodeItem.pubDate)), DateTime.fromMillisecondsSinceEpoch(
style: TextStyle(color: Colors.blue[500])), widget.episodeItem.pubDate)),
), style: TextStyle(
Container( color: Theme.of(context).accentColor)),
padding: EdgeInsets.all(12.0), ),
height: 50.0, Container(
child: Row( padding: EdgeInsets.symmetric(horizontal: 20.0),
children: <Widget>[ height: 50.0,
(widget.episodeItem.explicit == 1) child: Row(
? Container( children: <Widget>[
decoration: BoxDecoration( (widget.episodeItem.explicit == 1)
color: Colors.red[800], ? Container(
shape: BoxShape.circle), decoration: BoxDecoration(
height: 25.0, color: Colors.red[800],
width: 25.0, shape: BoxShape.circle),
margin: EdgeInsets.only(right: 10.0), height: 25.0,
alignment: Alignment.center, width: 25.0,
child: Text('E', margin: EdgeInsets.only(right: 10.0),
style: alignment: Alignment.center,
TextStyle(color: Colors.white))) child: Text('E',
: Center(), style:
widget.episodeItem.duration != 0 TextStyle(color: Colors.white)))
? Container( : Center(),
decoration: BoxDecoration( widget.episodeItem.duration != 0
color: Colors.cyan[300], ? Container(
borderRadius: BorderRadius.all( decoration: BoxDecoration(
Radius.circular(15.0))), color: Colors.cyan[300],
height: 30.0, borderRadius: BorderRadius.all(
margin: EdgeInsets.only(right: 10.0), Radius.circular(15.0))),
padding: EdgeInsets.symmetric( height: 25.0,
horizontal: 10.0), margin: EdgeInsets.only(right: 10.0),
alignment: Alignment.center, padding: EdgeInsets.symmetric(
child: Text( horizontal: 10.0),
(widget.episodeItem.duration) alignment: Alignment.center,
.toString() + child: Text(
'mins', (widget.episodeItem.duration)
style: textstyle), .toString() +
) 'mins',
: Center(), style: textstyle),
widget.episodeItem.enclosureLength != null
? Container(
decoration: BoxDecoration(
color: Colors.lightBlue[300],
borderRadius: BorderRadius.all(
Radius.circular(15.0))),
height: 30.0,
margin: EdgeInsets.only(right: 10.0),
padding: EdgeInsets.symmetric(
horizontal: 10.0),
alignment: Alignment.center,
child: Text(
((widget.episodeItem
.enclosureLength) ~/
1000000)
.toString() +
'MB',
style: textstyle),
)
: Center(),
],
),
),
],
),
),
Expanded(
child: Container(
padding:
EdgeInsets.only(left: 12.0, right: 12.0, top: 5.0),
child: SingleChildScrollView(
controller: _controller,
child: _loaddes
? (widget.episodeItem.description.contains('<'))
? Html(
data: widget.episodeItem.description,
onLinkTap: (url) {
_launchUrl(url);
},
useRichText: true,
) )
: Container( : Center(),
alignment: Alignment.topLeft, widget.episodeItem.enclosureLength != null
child: ? Container(
Text(widget.episodeItem.description)) decoration: BoxDecoration(
: Center(), color: Colors.lightBlue[300],
borderRadius: BorderRadius.all(
Radius.circular(15.0))),
height: 25.0,
margin: EdgeInsets.only(right: 10.0),
padding: EdgeInsets.symmetric(
horizontal: 10.0),
alignment: Alignment.center,
child: Text(
((widget.episodeItem
.enclosureLength) ~/
1000000)
.toString() +
'MB',
style: textstyle),
)
: Center(),
],
),
),
],
),
),
Expanded(
child: Container(
padding: EdgeInsets.only(top: 5.0),
child: SingleChildScrollView(
scrollDirection: Axis.vertical,
//physics: const AlwaysScrollableScrollPhysics(),
controller: _controller,
child: _loaddes
? (widget.episodeItem.description.contains('<'))
? Html(
padding:
EdgeInsets.symmetric(horizontal: 20.0),
defaultTextStyle: TextStyle(height: 1.8),
data: widget.episodeItem.description,
linkStyle: TextStyle(
color: Theme.of(context).accentColor,
decoration: TextDecoration.underline,
textBaseline: TextBaseline.ideographic),
onLinkTap: (url) {
_launchUrl(url);
},
useRichText: true,
)
: Container(
padding:
EdgeInsets.symmetric(horizontal: 20.0),
alignment: Alignment.topLeft,
child: Text(
widget.episodeItem.description,
style: TextStyle(
height: 1.8,
),
))
: Center(),
),
),
),
Selector<AudioPlayerNotifier, bool>(
selector: (_, audio) => audio.playerRunning,
builder: (_, data, __) {
return Padding(
padding: EdgeInsets.only(bottom: data ? 60.0 : 0),
);
}),
],
),
),
Selector<AudioPlayerNotifier, bool>(
selector: (_, audio) => audio.playerRunning,
builder: (_, data, __) {
return Container(
alignment: Alignment.bottomCenter,
padding: EdgeInsets.only(bottom: data ? 60.0 : 0),
child: AnimatedContainer(
duration: Duration(milliseconds: 400),
height: !_showMenu ? 50 : 0,
child: SingleChildScrollView(
scrollDirection: Axis.vertical,
child: MenuBar(
episodeItem: widget.episodeItem,
heroTag: widget.heroTag,
), ),
), ),
), ),
], );
), }),
), Container(child: PlayerWidget()),
Selector<AudioPlayerNotifier, bool>( ],
selector: (_, audio) => audio.playerRunning,
builder: (_, data, __) {
return Container(
alignment: Alignment.bottomCenter,
padding:
EdgeInsets.only(bottom: data == true ? 60.0 : 10.0),
child: AnimatedContainer(
duration: Duration(milliseconds: 400),
height: !_showMenu ? 50 : 0,
child: SingleChildScrollView(
scrollDirection: Axis.vertical,
child: MenuBar(
episodeItem: widget.episodeItem,
heroTag: widget.heroTag,
),
),
),
);
}),
Container(child: PlayerWidget()),
],
),
), ),
), ),
); );
@ -368,9 +389,9 @@ class _MenuBarState extends State<MenuBar> {
), ),
Spacer(), Spacer(),
// Text(audio.audioState.toString()), // Text(audio.audioState.toString()),
Selector<AudioPlayerNotifier, Tuple2<EpisodeBrief, BasicPlaybackState>>( Selector<AudioPlayerNotifier,
selector: (_, audio) => Tuple2<EpisodeBrief, BasicPlaybackState>>(
Tuple2(audio.episode, audio.audioState), selector: (_, audio) => Tuple2(audio.episode, audio.audioState),
builder: (_, data, __) { builder: (_, data, __) {
return (widget.episodeItem.title != data.item1?.title) return (widget.episodeItem.title != data.item1?.title)
? Material( ? Material(
@ -485,7 +506,8 @@ class _LineLoaderState extends State<LineLoader>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return CustomPaint(painter: LinePainter(_fraction, Theme.of(context).accentColor)); return CustomPaint(
painter: LinePainter(_fraction, Theme.of(context).accentColor));
} }
} }
@ -578,7 +600,8 @@ class _WaveLoaderState extends State<WaveLoader>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return CustomPaint(painter: WavePainter(_fraction, Theme.of(context).accentColor)); return CustomPaint(
painter: WavePainter(_fraction, Theme.of(context).accentColor));
} }
} }

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:line_icons/line_icons.dart'; import 'package:line_icons/line_icons.dart';
class AboutApp extends StatelessWidget { class AboutApp extends StatelessWidget {
_launchUrl(String url) async { _launchUrl(String url) async {
if (await canLaunch(url)) { if (await canLaunch(url)) {
@ -44,15 +45,16 @@ class AboutApp extends StatelessWidget {
value: SystemUiOverlayStyle( value: SystemUiOverlayStyle(
statusBarIconBrightness: Theme.of(context).accentColorBrightness, statusBarIconBrightness: Theme.of(context).accentColorBrightness,
systemNavigationBarColor: Theme.of(context).primaryColor, systemNavigationBarColor: Theme.of(context).primaryColor,
statusBarColor: Theme.of(context).primaryColor, systemNavigationBarIconBrightness:
Theme.of(context).accentColorBrightness,
), ),
child: SafeArea( child: Scaffold(
child: Scaffold( backgroundColor: Theme.of(context).primaryColor,
backgroundColor: Theme.of(context).primaryColor, appBar: AppBar(
appBar: AppBar( title: Text('About'),
title: Text('About'), ),
), body: SafeArea(
body: Container( child: Container(
padding: EdgeInsets.all(20), padding: EdgeInsets.all(20),
alignment: Alignment.topLeft, alignment: Alignment.topLeft,
child: Column( child: Column(
@ -70,7 +72,7 @@ class AboutApp extends StatelessWidget {
image: AssetImage('assets/logo.png'), image: AssetImage('assets/logo.png'),
height: 80, height: 80,
), ),
Text('Version: 0.1.2'), Text('Version: 0.1.4'),
], ],
), ),
), ),
@ -78,7 +80,7 @@ class AboutApp extends StatelessWidget {
padding: EdgeInsets.symmetric(horizontal: 50), padding: EdgeInsets.symmetric(horizontal: 50),
height: 50, height: 50,
child: Text( child: Text(
'Tsacdop is a podcasts client developed influtter, a simple, beautiful, and easy-use player.', 'Tsacdop is a podcasts client developed in flutter, a simple, beautiful, and easy-use application.',
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
), ),
@ -86,13 +88,11 @@ class AboutApp extends StatelessWidget {
padding: EdgeInsets.all(5.0), padding: EdgeInsets.all(5.0),
), ),
Container( Container(
padding: EdgeInsets.only( padding: EdgeInsets.only(top: 20.0, bottom: 10.0),
top: 20.0,
bottom: 10.0
),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(10)), borderRadius: BorderRadius.all(Radius.circular(10)),
border: Border.all(color: Theme.of(context).accentColor, width: 1), border: Border.all(
color: Theme.of(context).accentColor, width: 1),
), ),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
@ -107,20 +107,14 @@ class AboutApp extends StatelessWidget {
TextStyle(color: Theme.of(context).accentColor), TextStyle(color: Theme.of(context).accentColor),
), ),
), ),
_listItem( _listItem(context, 'GitHub', LineIcons.github,
context,
'GitHub',
LineIcons.github,
'https://github.com/stonaga/'), 'https://github.com/stonaga/'),
_listItem( _listItem(context, 'Twitter', LineIcons.twitter,
context,
'Twitter',
LineIcons.twitter,
'https://twitter.com'), 'https://twitter.com'),
_listItem( _listItem(
context, context,
'Stone Gate', 'Stone Gate',
LineIcons.hat_cowboy_solid, LineIcons.hat_cowboy_solid,
'mailto:<xijieyin@gmail.com>?subject=Tsacdop Feedback'), 'mailto:<xijieyin@gmail.com>?subject=Tsacdop Feedback'),
], ],
), ),
@ -155,8 +149,8 @@ class AboutApp extends StatelessWidget {
), ),
], ],
), ),
)), ),
), )),
); );
} }
} }

View File

@ -21,6 +21,7 @@ import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
import 'package:tsacdop/home/home.dart'; import 'package:tsacdop/home/home.dart';
import 'package:tsacdop/home/appbar/popupmenu.dart'; import 'package:tsacdop/home/appbar/popupmenu.dart';
import 'package:tsacdop/webfeed/webfeed.dart'; import 'package:tsacdop/webfeed/webfeed.dart';
import 'package:tsacdop/.env.dart';
class MyHomePage extends StatefulWidget { class MyHomePage extends StatefulWidget {
@override @override
@ -30,42 +31,47 @@ class MyHomePage extends StatefulWidget {
class _MyHomePageState extends State<MyHomePage> { class _MyHomePageState extends State<MyHomePage> {
final _MyHomePageDelegate _delegate = _MyHomePageDelegate(); final _MyHomePageDelegate _delegate = _MyHomePageDelegate();
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>(); final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
@override
void initState() {
super.initState();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AnnotatedRegion<SystemUiOverlayStyle>( return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle( value: SystemUiOverlayStyle(
statusBarIconBrightness: Theme.of(context).accentColorBrightness, statusBarIconBrightness: Theme.of(context).accentColorBrightness,
systemNavigationBarIconBrightness: Theme.of(context).accentColorBrightness, systemNavigationBarIconBrightness:
Theme.of(context).accentColorBrightness,
systemNavigationBarColor: Theme.of(context).primaryColor, systemNavigationBarColor: Theme.of(context).primaryColor,
statusBarColor: Theme.of(context).primaryColor, // statusBarColor: Theme.of(context).primaryColor,
), ),
child: SafeArea( child: Scaffold(
child: Scaffold( key: _scaffoldKey,
key: _scaffoldKey, appBar: AppBar(
appBar: AppBar( centerTitle: true,
centerTitle: true, leading: IconButton(
leading: IconButton( tooltip: 'Add',
tooltip: 'Add', icon: const Icon(Icons.add_circle_outline),
icon: const Icon(Icons.add_circle_outline), onPressed: () async {
onPressed: () async { await showSearch<int>(
await showSearch<int>( context: context,
context: context, delegate: _delegate,
delegate: _delegate, );
); },
},
),
title: Image(
image: Theme.of(context).brightness == Brightness.light
? AssetImage('assets/text.png') : AssetImage('assets/text_light.png'),
height: 30,
),
actions: <Widget>[
PopupMenu(),
],
), ),
body: Home(), title: Image(
image: Theme.of(context).brightness == Brightness.light
? AssetImage('assets/text.png')
: AssetImage('assets/text_light.png'),
height: 30,
),
actions: <Widget>[
PopupMenu(),
],
), ),
body: Home(),
), ),
); );
} }
@ -73,13 +79,15 @@ class _MyHomePageState extends State<MyHomePage> {
class _MyHomePageDelegate extends SearchDelegate<int> { class _MyHomePageDelegate extends SearchDelegate<int> {
static Future<List> getList(String searchText) async { static Future<List> getList(String searchText) async {
String apiKey = environment['apiKey'];
print(apiKey);
String url = String url =
"https://listennotes.p.mashape.com/api/v1/search?only_in=title%2Cdescription&q=" + "https://listennotes.p.mashape.com/api/v1/search?only_in=title%2Cdescription&q=" +
searchText + searchText +
"&sort_by_date=0&type=podcast"; "&sort_by_date=0&type=podcast";
Response response = await Dio().get(url, Response response = await Dio().get(url,
options: Options(headers: { options: Options(headers: {
'X-Mashape-Key': "UtSwKG4afSmshZfglwsXylLKJZHgp1aZHi2jsnSYK5mZi0A32T", 'X-Mashape-Key': "$apiKey",
'Accept': "application/json" 'Accept': "application/json"
})); }));
Map searchResultMap = jsonDecode(response.toString()); Map searchResultMap = jsonDecode(response.toString());
@ -112,7 +120,8 @@ class _MyHomePageDelegate extends SearchDelegate<int> {
padding: EdgeInsets.only(top: 400), padding: EdgeInsets.only(top: 400),
child: Image( child: Image(
image: Theme.of(context).brightness == Brightness.light image: Theme.of(context).brightness == Brightness.light
? AssetImage('assets/listennotes.png') : AssetImage('assets/listennotes_light.png'), ? AssetImage('assets/listennotes.png')
: AssetImage('assets/listennotes_light.png'),
height: 20, height: 20,
), ),
)); ));
@ -236,7 +245,11 @@ class _SearchResultState extends State<SearchResult> {
importOmpl.importState = ImportState.import; importOmpl.importState = ImportState.import;
try { try {
Response response = await Dio().get(rss); BaseOptions options = new BaseOptions(
connectTimeout: 30000,
receiveTimeout: 30000,
);
Response response = await Dio(options).get(rss);
var dbHelper = DBHelper(); var dbHelper = DBHelper();
String _realUrl = response.realUri.toString(); String _realUrl = response.realUri.toString();
@ -276,8 +289,8 @@ class _SearchResultState extends State<SearchResult> {
_uuid, _uuid,
_imagePath, _imagePath,
_provider, _provider,
_link); _link,
podcastLocal.description = _p.description; description: _p.description);
await groupList.subscribe(podcastLocal); await groupList.subscribe(podcastLocal);
if (_provider.contains('fireside')) { if (_provider.contains('fireside')) {
@ -347,28 +360,48 @@ class _SearchResultState extends State<SearchResult> {
? Icons.keyboard_arrow_up ? Icons.keyboard_arrow_up
: Icons.keyboard_arrow_down), : Icons.keyboard_arrow_down),
Padding(padding: EdgeInsets.only(right: 10.0)), Padding(padding: EdgeInsets.only(right: 10.0)),
!_issubscribe Container(
? !_adding width: 100,
? OutlineButton( height: 35,
child: Text('Subscribe', child: !_issubscribe
style: TextStyle( ? !_adding
color: Theme.of(context).accentColor)), ? OutlineButton(
onPressed: () { highlightedBorderColor:
importOmpl.rssTitle = widget.onlinePodcast.title; Theme.of(context).accentColor,
savePodcast(widget.onlinePodcast.rss); splashColor: Theme.of(context)
}) .accentColor
: OutlineButton( .withOpacity(0.8),
child: SizedBox( child: Text('Subscribe',
height: 20, style: TextStyle(
width: 20, color: Theme.of(context).accentColor)),
child: CircularProgressIndicator( onPressed: () {
strokeWidth: 2, importOmpl.rssTitle =
valueColor: widget.onlinePodcast.title;
AlwaysStoppedAnimation(Colors.blue), savePodcast(widget.onlinePodcast.rss);
)), })
onPressed: () {}, : OutlineButton(
) highlightedBorderColor:
: OutlineButton(child: Text('Subscribe'), onPressed: () {}), Theme.of(context).accentColor,
splashColor: Theme.of(context)
.accentColor
.withOpacity(0.8),
child: SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation(
Theme.of(context).accentColor),
)),
onPressed: () {},
)
: OutlineButton(
highlightedBorderColor: Colors.grey[500],
disabledTextColor: Colors.grey[500],
child: Text('Subscribe'),
disabledBorderColor: Colors.grey[500],
onPressed: () {}),
),
], ],
), ),
), ),

View File

@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:tsacdop/class/fireside_data.dart'; import 'package:tsacdop/class/fireside_data.dart';
import 'package:tsacdop/local_storage/key_value_storage.dart';
import 'package:xml/xml.dart' as xml; import 'package:xml/xml.dart' as xml;
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -14,6 +15,7 @@ import 'package:image/image.dart' as img;
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
import 'package:fluttertoast/fluttertoast.dart'; import 'package:fluttertoast/fluttertoast.dart';
import 'package:line_icons/line_icons.dart'; import 'package:line_icons/line_icons.dart';
import 'package:intl/intl.dart';
import 'package:tsacdop/class/podcast_group.dart'; import 'package:tsacdop/class/podcast_group.dart';
import 'package:tsacdop/settings/settting.dart'; import 'package:tsacdop/settings/settting.dart';
@ -37,8 +39,13 @@ class OmplOutline {
} }
} }
class PopupMenu extends StatelessWidget { class PopupMenu extends StatefulWidget {
Future<String> getColor(File file) async { @override
_PopupMenuState createState() => _PopupMenuState();
}
class _PopupMenuState extends State<PopupMenu> {
Future<String> _getColor(File file) async {
final imageProvider = FileImage(file); final imageProvider = FileImage(file);
var colorImage = await getImageFromProvider(imageProvider); var colorImage = await getImageFromProvider(imageProvider);
var color = await getColorFromImage(colorImage); var color = await getColorFromImage(colorImage);
@ -46,35 +53,85 @@ class PopupMenu extends StatelessWidget {
return primaryColor; return primaryColor;
} }
bool _showDate;
String _refreshDate;
_getRefreshDate() async {
int refreshDate;
KeyValueStorage refreshstorage = KeyValueStorage('refreshdate');
int i = await refreshstorage.getInt();
if (i == 0) {
KeyValueStorage refreshstorage = KeyValueStorage('refreshdate');
await refreshstorage.saveInt(DateTime.now().millisecondsSinceEpoch);
refreshDate = DateTime.now().millisecondsSinceEpoch;
} else {
refreshDate = i;
}
DateTime date = DateTime.fromMillisecondsSinceEpoch(refreshDate);
var diffrence = DateTime.now().difference(date);
if (diffrence.inMinutes < 10) {
_refreshDate = 'Just now';
} else if (diffrence.inHours < 1) {
_refreshDate = '1 hour ago';
} else if (diffrence.inHours < 24) {
_refreshDate = '${diffrence.inHours} hours ago';
} else if (diffrence.inHours == 24) {
_refreshDate = '1 day ago';
} else if (diffrence.inDays < 7) {
_refreshDate = '${diffrence.inDays} days ago';
} else {
_refreshDate = DateFormat.yMMMd()
.format(DateTime.fromMillisecondsSinceEpoch(refreshDate));
}
setState(() {
_showDate = true;
});
}
@override
void initState() {
super.initState();
_showDate = false;
_getRefreshDate();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
ImportOmpl importOmpl = Provider.of<ImportOmpl>(context, listen: false); ImportOmpl importOmpl = Provider.of<ImportOmpl>(context, listen: false);
GroupList groupList = Provider.of<GroupList>(context, listen: false); GroupList groupList = Provider.of<GroupList>(context, listen: false);
_refreshAll() async { _refreshAll() async {
var dbHelper = DBHelper(); var dbHelper = DBHelper();
List<PodcastLocal> podcastList = await dbHelper.getPodcastLocalAll(); List<PodcastLocal> podcastList = await dbHelper.getPodcastLocalAll();
int i = 0;
await Future.forEach(podcastList, (podcastLocal) async { await Future.forEach(podcastList, (podcastLocal) async {
importOmpl.rssTitle = podcastLocal.title; importOmpl.rssTitle = podcastLocal.title;
importOmpl.importState = ImportState.parse; importOmpl.importState = ImportState.parse;
await dbHelper.updatePodcastRss(podcastLocal); i += await dbHelper.updatePodcastRss(podcastLocal);
print('Refresh ' + podcastLocal.title); print('Refresh ' + podcastLocal.title);
}); });
KeyValueStorage refreshstorage = KeyValueStorage('refreshdate');
await refreshstorage.saveInt(DateTime.now().millisecondsSinceEpoch);
KeyValueStorage refreshcountstorage = KeyValueStorage('refreshcount');
await refreshcountstorage.saveInt(i);
importOmpl.importState = ImportState.complete; importOmpl.importState = ImportState.complete;
} }
saveOmpl(String rss) async { saveOmpl(String rss) async {
var dbHelper = DBHelper(); var dbHelper = DBHelper();
importOmpl.importState = ImportState.import; importOmpl.importState = ImportState.import;
BaseOptions options = new BaseOptions(
Response response = await Dio().get(rss); connectTimeout: 20000,
receiveTimeout: 20000,
);
Response response = await Dio(options).get(rss);
if (response.statusCode == 200) { if (response.statusCode == 200) {
var _p = RssFeed.parse(response.data); var _p = RssFeed.parse(response.data);
var dir = await getApplicationDocumentsDirectory(); var dir = await getApplicationDocumentsDirectory();
String _realUrl = response.redirects.isEmpty ? rss : response.realUri.toString(); String _realUrl =
response.redirects.isEmpty ? rss : response.realUri.toString();
print(_realUrl); print(_realUrl);
bool _checkUrl = await dbHelper.checkPodcast(_realUrl); bool _checkUrl = await dbHelper.checkPodcast(_realUrl);
@ -87,9 +144,10 @@ class PopupMenu extends StatelessWidget {
String _uuid = Uuid().v4(); String _uuid = Uuid().v4();
File("${dir.path}/$_uuid.png") File("${dir.path}/$_uuid.png")
..writeAsBytesSync(img.encodePng(thumbnail)); ..writeAsBytesSync(img.encodePng(thumbnail));
String _imagePath = "${dir.path}/$_uuid.png"; String _imagePath = "${dir.path}/$_uuid.png";
String _primaryColor = await getColor(File("${dir.path}/$_uuid.png")); String _primaryColor =
await _getColor(File("${dir.path}/$_uuid.png"));
String _author = _p.itunes.author ?? _p.author ?? ''; String _author = _p.itunes.author ?? _p.author ?? '';
String _provider = _p.generator ?? ''; String _provider = _p.generator ?? '';
String _link = _p.link ?? ''; String _link = _p.link ?? '';
@ -102,14 +160,12 @@ class PopupMenu extends StatelessWidget {
_uuid, _uuid,
_imagePath, _imagePath,
_provider, _provider,
_link); _link,
description: _p.description);
podcastLocal.description = _p.description;
await groupList.subscribe(podcastLocal); await groupList.subscribe(podcastLocal);
if (_provider.contains('fireside')) if (_provider.contains('fireside')) {
{
FiresideData data = FiresideData(_uuid, _link); FiresideData data = FiresideData(_uuid, _link);
await data.fatchData(); await data.fatchData();
} }
@ -143,33 +199,34 @@ class PopupMenu extends StatelessWidget {
void _saveOmpl(String path) async { void _saveOmpl(String path) async {
File file = File(path); File file = File(path);
try{String opml = file.readAsStringSync(); try {
String opml = file.readAsStringSync();
var content = xml.parse(opml); var content = xml.parse(opml);
var total = content var total = content
.findAllElements('outline') .findAllElements('outline')
.map((ele) => OmplOutline.parse(ele)) .map((ele) => OmplOutline.parse(ele))
.toList(); .toList();
if (total.length == 0) { if (total.length == 0) {
Fluttertoast.showToast( Fluttertoast.showToast(
msg: 'File Not Valid', msg: 'File Not Valid',
gravity: ToastGravity.BOTTOM, gravity: ToastGravity.BOTTOM,
); );
} else { } else {
for (int i = 0; i < total.length; i++) { for (int i = 0; i < total.length; i++) {
if (total[i].xmlUrl != null) { if (total[i].xmlUrl != null) {
importOmpl.rssTitle = total[i].text; importOmpl.rssTitle = total[i].text;
try { try {
await saveOmpl(total[i].xmlUrl); await saveOmpl(total[i].xmlUrl);
} catch (e) { } catch (e) {
print(e.toString()); print(e.toString());
}
print(total[i].text);
} }
print(total[i].text);
} }
print('Import fisnished');
} }
print('Import fisnished'); } catch (e) {
}}
catch(e){
print(e); print(e);
Fluttertoast.showToast( Fluttertoast.showToast(
msg: 'File error, Subscribe failed', msg: 'File error, Subscribe failed',
@ -195,7 +252,8 @@ class PopupMenu extends StatelessWidget {
} }
return PopupMenuButton<int>( return PopupMenuButton<int>(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10))), shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(10))),
elevation: 1, elevation: 1,
tooltip: 'Menu', tooltip: 'Menu',
itemBuilder: (context) => [ itemBuilder: (context) => [
@ -204,40 +262,60 @@ class PopupMenu extends StatelessWidget {
child: Container( child: Container(
padding: EdgeInsets.only(left: 10), padding: EdgeInsets.only(left: 10),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
Icon(LineIcons.cloud_download_alt_solid), Icon(LineIcons.cloud_download_alt_solid),
Padding(padding: EdgeInsets.symmetric(horizontal: 5.0),), Padding(
Text('Refresh All'), padding: EdgeInsets.symmetric(horizontal: 5.0),
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
'Refresh All',
),
_showDate
? Text(
_refreshDate,
style: TextStyle(color: Colors.red, fontSize: 12),
)
: Center(),
],
),
], ],
), ),
), ),
), ),
PopupMenuItem( PopupMenuItem(
value: 2, value: 2,
child: Container( child: Container(
padding: EdgeInsets.only(left: 10), padding: EdgeInsets.only(left: 10),
child: Row( child: Row(
children: <Widget>[ children: <Widget>[
Icon(LineIcons.paperclip_solid), Icon(LineIcons.paperclip_solid),
Padding(padding: EdgeInsets.symmetric(horizontal: 5.0),), Padding(
Text('Import OMPL'), padding: EdgeInsets.symmetric(horizontal: 5.0),
], ),
), Text('Import OMPL'),
],
), ),
), ),
),
// PopupMenuItem(
// value: 3, // PopupMenuItem(
// child: setting.theme != 2 ? Text('Night Mode') : Text('Light Mode'), // value: 3,
// ), // child: setting.theme != 2 ? Text('Night Mode') : Text('Light Mode'),
PopupMenuItem( // ),
PopupMenuItem(
value: 4, value: 4,
child: Container( child: Container(
padding: EdgeInsets.only(left: 10), padding: EdgeInsets.only(left: 10),
child: Row( child: Row(
children: <Widget>[ children: <Widget>[
Icon(LineIcons.cog_solid), Icon(LineIcons.cog_solid),
Padding(padding: EdgeInsets.symmetric(horizontal: 5.0),), Padding(
padding: EdgeInsets.symmetric(horizontal: 5.0),
),
Text('Settings'), Text('Settings'),
], ],
), ),
@ -250,7 +328,9 @@ class PopupMenu extends StatelessWidget {
child: Row( child: Row(
children: <Widget>[ children: <Widget>[
Icon(LineIcons.info_circle_solid), Icon(LineIcons.info_circle_solid),
Padding(padding: EdgeInsets.symmetric(horizontal: 5.0),), Padding(
padding: EdgeInsets.symmetric(horizontal: 5.0),
),
Text('About'), Text('About'),
], ],
), ),
@ -266,11 +346,11 @@ class PopupMenu extends StatelessWidget {
} else if (value == 1) { } else if (value == 1) {
_refreshAll(); _refreshAll();
} else if (value == 3) { } else if (value == 3) {
// setting.theme != 2 ? setting.setTheme(2) : setting.setTheme(1); // setting.theme != 2 ? setting.setTheme(2) : setting.setTheme(1);
} else if (value == 4) { } else if (value == 4) {
Navigator.push( Navigator.push(
context, MaterialPageRoute(builder: (context) => Settings())); context, MaterialPageRoute(builder: (context) => Settings()));
} }
}, },
); );
} }

View File

@ -81,10 +81,10 @@ class _AudioPanelState extends State<AudioPanel>
color: Theme.of(context).primaryColor, color: Theme.of(context).primaryColor,
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
offset: Offset(0, -1), offset: Offset(0, -0.5),
blurRadius: 4, blurRadius: 1,
color: Theme.of(context).brightness == Brightness.light color: Theme.of(context).brightness == Brightness.light
? Colors.grey[400] ? Colors.grey[400].withOpacity(0.5)
: Colors.grey[800], : Colors.grey[800],
), ),
], ],

File diff suppressed because it is too large Load Diff

View File

@ -1,45 +1,15 @@
import 'dart:io';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:tuple/tuple.dart';
import 'hometab.dart'; import 'hometab.dart';
import 'package:tsacdop/home/appbar/importompl.dart'; import 'package:tsacdop/home/appbar/importompl.dart';
import 'package:tsacdop/home/audioplayer.dart'; import 'package:tsacdop/home/audioplayer.dart';
import 'package:tsacdop/class/audiostate.dart';
import 'homescroll.dart'; import 'homescroll.dart';
class Home extends StatefulWidget { class Home extends StatelessWidget {
@override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
bool _loadPlay;
static String _stringForSeconds(int seconds) {
if (seconds == null) return null;
return '${(seconds ~/ 60)}:${(seconds.truncate() % 60).toString().padLeft(2, '0')}';
}
@override
void initState() {
super.initState();
_loadPlay = false;
_getPlaylist();
}
_getPlaylist() async {
await Provider.of<AudioPlayerNotifier>(context, listen: false).loadPlaylist();
setState(() {
_loadPlay = true;
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
return Stack(children: <Widget>[ return Stack(children: <Widget>[
Column( Column(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
@ -52,69 +22,6 @@ class _HomeState extends State<Home> {
), ),
], ],
), ),
AnimatedPositioned(
duration: Duration(milliseconds: 2000),
curve: Curves.elasticOut,
bottom: 50,
right: _loadPlay ? 5 : -25,
child: Container(
child: Selector<AudioPlayerNotifier, Tuple3<bool, Playlist, int>>(
selector: (_, audio) =>
Tuple3(audio.playerRunning, audio.queue, audio.lastPositin),
builder: (_, data, __) => !_loadPlay
? Center()
: data.item1 || data.item2.playlist.length == 0
? Center()
: InkWell(
onTap: () => audio.playlistLoad(),
child: Stack(
alignment: Alignment.centerLeft,
children: <Widget>[
Container(
padding: EdgeInsets.only(left: 45, right: 10.0),
alignment: Alignment.centerRight,
decoration: BoxDecoration(
color: Theme.of(context).accentColor,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(20.0),
bottomLeft: Radius.circular(20.0),
bottomRight: Radius.circular(10.0),
topRight: Radius.circular(10.0)),
boxShadow: [
BoxShadow(
color: Theme.of(context).brightness ==
Brightness.light
? Colors.grey[400]
: Colors.grey[800],
blurRadius: 4,
offset: Offset(1, 1)),
]),
height: 40,
child: Text(_stringForSeconds(data.item3~/1000) + '...',
style: TextStyle(color: Colors.white)),
),
CircleAvatar(
radius: 20,
backgroundImage: FileImage(File(
"${data.item2.playlist.first.imagePath}")),
),
Container(
height: 40.0,
width: 40,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.black12),
child: Icon(
Icons.play_arrow,
color: Colors.white,
),
),
],
),
),
),
),
),
Container(child: PlayerWidget()), Container(child: PlayerWidget()),
]); ]);
} }

View File

@ -244,11 +244,30 @@ class _ScrollPodcastsState extends State<ScrollPodcasts> {
child: ClipRRect( child: ClipRRect(
borderRadius: BorderRadius.all( borderRadius: BorderRadius.all(
Radius.circular(25.0)), Radius.circular(25.0)),
child: LimitedBox( child: Stack(
maxHeight: 50, alignment: Alignment.bottomCenter,
maxWidth: 50, children: <Widget>[
child: Image.file( LimitedBox(
File("${podcastLocal.imagePath}")), maxHeight: 50,
maxWidth: 50,
child: Image.file(File(
"${podcastLocal.imagePath}")),
),
podcastLocal.upateCount > 0
? Container(
alignment: Alignment.center,
height: 10,
width: 40,
color: Colors.black54,
child: Text('New',
style: TextStyle(
color: Colors.red,
fontSize: 8,
fontStyle: FontStyle
.italic)),
)
: Center(),
],
), ),
), ),
); );
@ -380,10 +399,11 @@ class ShowEpisode extends StatelessWidget {
final List<EpisodeBrief> podcast; final List<EpisodeBrief> podcast;
final PodcastLocal podcastLocal; final PodcastLocal podcastLocal;
ShowEpisode({Key key, this.podcast, this.podcastLocal}) : super(key: key); ShowEpisode({Key key, this.podcast, this.podcastLocal}) : super(key: key);
Offset offset;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
double _width = MediaQuery.of(context).size.width; double _width = MediaQuery.of(context).size.width;
Offset offset;
_showPopupMenu(Offset offset, EpisodeBrief episode, BuildContext context, _showPopupMenu(Offset offset, EpisodeBrief episode, BuildContext context,
bool isPlaying, bool isInPlaylist) async { bool isPlaying, bool isInPlaylist) async {
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false); var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
@ -432,7 +452,7 @@ class ShowEpisode extends StatelessWidget {
if (value == 0) { if (value == 0) {
if (!isPlaying) audio.episodeLoad(episode); if (!isPlaying) audio.episodeLoad(episode);
} else if (value == 1) { } else if (value == 1) {
if (isInPlaylist) { if (!isInPlaylist) {
audio.addToPlaylist(episode); audio.addToPlaylist(episode);
Fluttertoast.showToast( Fluttertoast.showToast(
msg: 'Added to playlist', msg: 'Added to playlist',
@ -536,6 +556,14 @@ class ShowEpisode extends StatelessWidget {
), ),
), ),
Spacer(), Spacer(),
index < podcastLocal.upateCount
? Text(
'New',
style: TextStyle(
color: Colors.red,
fontStyle: FontStyle.italic),
)
: Center(),
], ],
), ),
), ),

View File

@ -1,10 +1,16 @@
import 'dart:io';
import 'dart:ui'; import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:tsacdop/local_storage/key_value_storage.dart';
import 'package:tuple/tuple.dart';
import 'package:tsacdop/class/audiostate.dart';
import 'package:tsacdop/class/episodebrief.dart'; import 'package:tsacdop/class/episodebrief.dart';
import 'package:tsacdop/settings/history.dart'; import 'package:tsacdop/home/playlist.dart';
import 'package:tsacdop/local_storage/sqflite_localpodcast.dart'; import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
import 'package:tsacdop/util/episodegrid.dart'; import 'package:tsacdop/util/episodegrid.dart';
import 'package:tsacdop/util/mypopupmenu.dart';
class MainTab extends StatefulWidget { class MainTab extends StatefulWidget {
@override @override
@ -13,6 +19,12 @@ class MainTab extends StatefulWidget {
class _MainTabState extends State<MainTab> with TickerProviderStateMixin { class _MainTabState extends State<MainTab> with TickerProviderStateMixin {
TabController _controller; TabController _controller;
bool _loadPlay;
static String _stringForSeconds(int seconds) {
if (seconds == null) return null;
return '${(seconds ~/ 60)}:${(seconds.truncate() % 60).toString().padLeft(2, '0')}';
}
Decoration getIndicator(BuildContext context) { Decoration getIndicator(BuildContext context) {
return UnderlineTabIndicator( return UnderlineTabIndicator(
borderSide: BorderSide(color: Theme.of(context).accentColor, width: 2), borderSide: BorderSide(color: Theme.of(context).accentColor, width: 2),
@ -23,25 +35,121 @@ class _MainTabState extends State<MainTab> with TickerProviderStateMixin {
)); ));
} }
Widget playHistory() { _getPlaylist() async {
return PopupMenuButton<int>( await Provider.of<AudioPlayerNotifier>(context, listen: false)
.loadPlaylist();
setState(() {
_loadPlay = true;
});
}
Widget playlist(BuildContext context) {
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
return MyPopupMenuButton<int>(
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(10))), borderRadius: BorderRadius.all(Radius.circular(10))),
elevation: 1, elevation: 1,
icon: Icon(Icons.history), icon: Icon(Icons.playlist_play),
tooltip: "Menu", tooltip: "Menu",
itemBuilder: (context) => [ itemBuilder: (context) => [
MyPopupMenuItem(
height: 50,
value: 1,
child: Container(
decoration: BoxDecoration(
// color: Theme.of(context).accentColor,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(10.0),
topRight: Radius.circular(10.0)),
),
child: Selector<AudioPlayerNotifier, Tuple3<bool, Playlist, int>>(
selector: (_, audio) =>
Tuple3(audio.playerRunning, audio.queue, audio.lastPositin),
builder: (_, data, __) => !_loadPlay
? Container(
height: 8.0,
)
: data.item1 || data.item2.playlist.length == 0
? Container(
height: 8.0,
)
: InkWell(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(10.0),
topRight: Radius.circular(10.0)),
onTap: () {
audio.playlistLoad();
Navigator.pop<int>(context);
},
child: Column(
children: <Widget>[
Padding(
padding: EdgeInsets.symmetric(vertical: 5),
),
Stack(
alignment: Alignment.center,
children: <Widget>[
CircleAvatar(
radius: 20,
backgroundImage: FileImage(File(
"${data.item2.playlist.first.imagePath}")),
),
Container(
height: 40.0,
width: 40.0,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.black12),
child: Icon(
Icons.play_arrow,
color: Colors.white,
),
),
],
),
Padding(
padding: EdgeInsets.symmetric(vertical: 2),
),
Container(
height: 70,
width: 140,
child: Column(
children: <Widget>[
Text(
_stringForSeconds(data.item3 ~/ 1000),
// style:
// TextStyle(color: Colors.white)
),
Text(
data.item2.playlist.first.title,
maxLines: 2,
textAlign: TextAlign.center,
overflow: TextOverflow.fade,
// style: TextStyle(color: Colors.white),
),
],
),
),
Divider(
height: 2,
),
],
),
),
),
),
),
PopupMenuItem( PopupMenuItem(
value: 0, value: 0,
child: Container( child: Container(
padding: EdgeInsets.only(left: 10), padding: EdgeInsets.only(left: 10),
child: Row( child: Row(
children: <Widget>[ children: <Widget>[
Icon(Icons.history), Icon(Icons.playlist_play),
Padding( Padding(
padding: EdgeInsets.symmetric(horizontal: 5.0), padding: EdgeInsets.symmetric(horizontal: 5.0),
), ),
Text('Play History'), Text('Playlist'),
], ],
), ),
), ),
@ -49,9 +157,9 @@ class _MainTabState extends State<MainTab> with TickerProviderStateMixin {
], ],
onSelected: (value) { onSelected: (value) {
if (value == 0) { if (value == 0) {
Navigator.push(context, Navigator.push(
MaterialPageRoute(builder: (context) => PlayedHistory())); context, MaterialPageRoute(builder: (context) => PlaylistPage()));
} } else if (value == 1) {}
}, },
); );
} }
@ -60,6 +168,8 @@ class _MainTabState extends State<MainTab> with TickerProviderStateMixin {
void initState() { void initState() {
super.initState(); super.initState();
_controller = TabController(length: 3, vsync: this); _controller = TabController(length: 3, vsync: this);
_loadPlay = false;
_getPlaylist();
} }
@override @override
@ -103,7 +213,7 @@ class _MainTabState extends State<MainTab> with TickerProviderStateMixin {
), ),
), ),
Spacer(), Spacer(),
playHistory(), playlist(context),
], ],
), ),
Expanded( Expanded(
@ -135,9 +245,12 @@ class RecentUpdate extends StatefulWidget {
} }
class _RecentUpdateState extends State<RecentUpdate> { class _RecentUpdateState extends State<RecentUpdate> {
int _updateCount = 0;
Future<List<EpisodeBrief>> _getRssItem(int top) async { Future<List<EpisodeBrief>> _getRssItem(int top) async {
var dbHelper = DBHelper(); var dbHelper = DBHelper();
List<EpisodeBrief> episodes = await dbHelper.getRecentRssItem(top); List<EpisodeBrief> episodes = await dbHelper.getRecentRssItem(top);
KeyValueStorage refreshcountstorage = KeyValueStorage('refreshcount');
_updateCount = await refreshcountstorage.getInt();
return episodes; return episodes;
} }
@ -189,6 +302,7 @@ class _RecentUpdateState extends State<RecentUpdate> {
showFavorite: false, showFavorite: false,
showNumber: false, showNumber: false,
heroTag: 'recent', heroTag: 'recent',
updateCount: _updateCount,
), ),
SliverList( SliverList(
delegate: SliverChildBuilderDelegate( delegate: SliverChildBuilderDelegate(
@ -277,7 +391,6 @@ class _MyDownloadState extends State<MyDownload> {
heroTag: 'download', heroTag: 'download',
) )
], ],
) )
: Center(child: CircularProgressIndicator()); : Center(child: CircularProgressIndicator());
}, },

193
lib/home/playlist.dart Normal file
View 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,
),
],
),
),
);
});
},
),
),
),
);
}
}

View File

@ -30,13 +30,14 @@ class DBHelper {
.execute("""CREATE TABLE PodcastLocal(id TEXT PRIMARY KEY,title TEXT, .execute("""CREATE TABLE PodcastLocal(id TEXT PRIMARY KEY,title TEXT,
imageUrl TEXT,rssUrl TEXT UNIQUE,primaryColor TEXT,author TEXT, imageUrl TEXT,rssUrl TEXT UNIQUE,primaryColor TEXT,author TEXT,
description TEXT, add_date INTEGER, imagePath TEXT, provider TEXT, link TEXT, description TEXT, add_date INTEGER, imagePath TEXT, provider TEXT, link TEXT,
background_image TEXT DEFAULT '',hosts TEXT DEFAULT '')"""); background_image TEXT DEFAULT '',hosts TEXT DEFAULT '',update_count INTEGER DEFAULT 0)""");
await db await db
.execute("""CREATE TABLE Episodes(id INTEGER PRIMARY KEY,title TEXT, .execute("""CREATE TABLE Episodes(id INTEGER PRIMARY KEY,title TEXT,
enclosure_url TEXT UNIQUE, enclosure_length INTEGER, pubDate TEXT, enclosure_url TEXT UNIQUE, enclosure_length INTEGER, pubDate TEXT,
description TEXT, feed_id TEXT, feed_link TEXT, milliseconds INTEGER, description TEXT, feed_id TEXT, feed_link TEXT, milliseconds INTEGER,
duration INTEGER DEFAULT 0, explicit INTEGER DEFAULT 0, liked INTEGER DEFAULT 0, duration INTEGER DEFAULT 0, explicit INTEGER DEFAULT 0, liked INTEGER DEFAULT 0,
downloaded TEXT DEFAULT 'ND', download_date INTEGER DEFAULT 0, media_id TEXT)"""); downloaded TEXT DEFAULT 'ND', download_date INTEGER DEFAULT 0, media_id TEXT,
is_new INTEGER DEFAULT 0)""");
await db.execute( await db.execute(
"""CREATE TABLE PlayHistory(id INTEGER PRIMARY KEY, title TEXT, enclosure_url TEXT UNIQUE, """CREATE TABLE PlayHistory(id INTEGER PRIMARY KEY, title TEXT, enclosure_url TEXT UNIQUE,
seconds REAL, seek_value REAL, add_date INTEGER)"""); seconds REAL, seek_value REAL, add_date INTEGER)""");
@ -51,7 +52,7 @@ class DBHelper {
await Future.forEach(podcasts, (s) async { await Future.forEach(podcasts, (s) async {
List<Map> list; List<Map> list;
list = await dbClient.rawQuery( list = await dbClient.rawQuery(
'SELECT id, title, imageUrl, rssUrl, primaryColor, author, imagePath , provider, link FROM PodcastLocal WHERE id = ?', 'SELECT id, title, imageUrl, rssUrl, primaryColor, author, imagePath , provider, link ,update_count FROM PodcastLocal WHERE id = ?',
[s]); [s]);
podcastLocal.add(PodcastLocal( podcastLocal.add(PodcastLocal(
list.first['title'], list.first['title'],
@ -62,7 +63,8 @@ class DBHelper {
list.first['id'], list.first['id'],
list.first['imagePath'], list.first['imagePath'],
list.first['provider'], list.first['provider'],
list.first['link'])); list.first['link'],
upateCount: list.first['update_count']));
}); });
return podcastLocal; return podcastLocal;
} }
@ -199,16 +201,19 @@ class DBHelper {
return playHistory; return playHistory;
} }
Future<List<SubHistory>> getSubHistory() async{ Future<List<SubHistory>> getSubHistory() async {
var dbClient = await database; var dbClient = await database;
List<Map> list = await dbClient.rawQuery( List<Map> list = await dbClient.rawQuery(
"""SELECT title, rss_url, add_date, remove_date, status FROM SubscribeHistory """SELECT title, rss_url, add_date, remove_date, status FROM SubscribeHistory
ORDER BY add_date DESC""" ORDER BY add_date DESC""");
); return list
return list.map((record) => SubHistory( .map((record) => SubHistory(
record['status']==0 ? true : false, DateTime.fromMillisecondsSinceEpoch(record['remove_date']), record['status'] == 0 ? true : false,
DateTime.fromMillisecondsSinceEpoch(record['add_date']), record['rss_url'], record['title'] DateTime.fromMillisecondsSinceEpoch(record['remove_date']),
)).toList(); DateTime.fromMillisecondsSinceEpoch(record['add_date']),
record['rss_url'],
record['title']))
.toList();
} }
Future<double> listenMins(int day) async { Future<double> listenMins(int day) async {
@ -252,12 +257,12 @@ class DBHelper {
RegExp z = RegExp(r'(\+|\-)[0-1][0-9]00'); RegExp z = RegExp(r'(\+|\-)[0-1][0-9]00');
String timezone = z.stringMatch(pubDate); String timezone = z.stringMatch(pubDate);
int timezoneInt = 0; int timezoneInt = 0;
if(timezone!=null){ if (timezone != null) {
if(timezone.substring(0, 1) == '-'){ if (timezone.substring(0, 1) == '-') {
timezoneInt = int.parse(timezone.substring(1,2)); timezoneInt = int.parse(timezone.substring(1, 2));
} else { } else {
timezoneInt = -int.parse(timezone.substring(1,2)); timezoneInt = -int.parse(timezone.substring(1, 2));
} }
} }
try { try {
date = DateFormat('EEE, dd MMM yyyy HH:mm:ss Z', 'en_US').parse(pubDate); date = DateFormat('EEE, dd MMM yyyy HH:mm:ss Z', 'en_US').parse(pubDate);
@ -288,7 +293,7 @@ class DBHelper {
return date.add(Duration(hours: timezoneInt)); return date.add(Duration(hours: timezoneInt));
} }
int getExplicit(bool b) { int _getExplicit(bool b) {
int result; int result;
if (b == true) { if (b == true) {
result = 1; result = 1;
@ -299,117 +304,147 @@ class DBHelper {
} }
} }
bool isXimalaya(String input) { bool _isXimalaya(String input) {
RegExp ximalaya = RegExp(r"ximalaya.com"); RegExp ximalaya = RegExp(r"ximalaya.com");
return ximalaya.hasMatch(input); return ximalaya.hasMatch(input);
} }
Future<int> savePodcastRss(RssFeed _p, String id) async { String _getDescription(String content, String description, String summary) {
int _result = _p.items.length; if (content.length >= description.length) {
var dbClient = await database; if (content.length >= summary.length) {
String _description, _url; return content;
for (int i = 0; i < _result; i++) {
print(_p.items[i].title);
if (_p.items[i].itunes.summary != null) {
_p.items[i].itunes.summary.contains('<')
? _description = _p.items[i].itunes.summary
: _description = _p.items[i].description;
} else { } else {
_description = _p.items[i].description; return summary;
}
} else if (description.length >= summary.length) {
return description;
} else {
return summary;
}
}
Future<int> savePodcastRss(RssFeed feed, String id) async {
feed.items.removeWhere((item) => item == null);
int result = feed.items.length;
var dbClient = await database;
String description, url;
for (int i = 0; i < result; i++) {
print(feed.items[i].title);
description = _getDescription(feed.items[i].content.value ?? '',
feed.items[i].description ?? '', feed.items[i].itunes.summary ?? '');
// if (feed.items[i].itunes.summary != null) {
// feed.items[i].itunes.summary.contains('<')
// ? description = feed.items[i].itunes.summary
// : description = feed.items[i].description;
// } else {
// description = feed.items[i].description;
// }
if (feed.items[i].enclosure != null) {
_isXimalaya(feed.items[i].enclosure.url)
? url = feed.items[i].enclosure.url.split('=').last
: url = feed.items[i].enclosure.url;
} }
isXimalaya(_p.items[i].enclosure.url) final title = feed.items[i].itunes.title ?? feed.items[i].title;
? _url = _p.items[i].enclosure.url.split('=').last final length = feed.items[i]?.enclosure?.length;
: _url = _p.items[i].enclosure.url; final pubDate = feed.items[i].pubDate;
print(pubDate);
final date = _parsePubDate(pubDate);
final milliseconds = date.millisecondsSinceEpoch;
final duration = feed.items[i].itunes.duration?.inMinutes ?? 0;
final explicit = _getExplicit(feed.items[i].itunes.explicit);
final _title = _p.items[i].itunes.title ?? _p.items[i].title; if (url != null) {
final _length = _p.items[i].enclosure.length;
final _pubDate = _p.items[i].pubDate;
print(_pubDate);
final _date = _parsePubDate(_pubDate);
final _milliseconds = _date.millisecondsSinceEpoch;
final _duration = _p.items[i].itunes.duration?.inMinutes ?? 0;
final _explicit = getExplicit(_p.items[i].itunes.explicit);
if (_url != null) {
await dbClient.transaction((txn) { await dbClient.transaction((txn) {
return txn.rawInsert( return txn.rawInsert(
"""INSERT OR IGNORE INTO Episodes(title, enclosure_url, enclosure_length, pubDate, """INSERT OR IGNORE INTO Episodes(title, enclosure_url, enclosure_length, pubDate,
description, feed_id, milliseconds, duration, explicit, media_id) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", description, feed_id, milliseconds, duration, explicit, media_id) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
[ [
_title, title,
_url, url,
_length, length,
_pubDate, pubDate,
_description, description,
id, id,
_milliseconds, milliseconds,
_duration, duration,
_explicit, explicit,
_url url
]); ]);
}); });
} }
} }
return _result; return result;
} }
Future<int> updatePodcastRss(PodcastLocal podcastLocal) async { Future<int> updatePodcastRss(PodcastLocal podcastLocal) async {
Response response = await Dio().get(podcastLocal.rssUrl); Response response = await Dio().get(podcastLocal.rssUrl);
var _p = RssFeed.parse(response.data); var feed = RssFeed.parse(response.data);
String _url, _description; String url, description;
int _result = _p.items.length; feed.items.removeWhere((item) => item == null);
int result = feed.items.length;
var dbClient = await database; var dbClient = await database;
int _count = Sqflite.firstIntValue(await dbClient.rawQuery( int count = Sqflite.firstIntValue(await dbClient.rawQuery(
'SELECT COUNT(*) FROM Episodes WHERE feed_id = ?', [podcastLocal.id])); 'SELECT COUNT(*) FROM Episodes WHERE feed_id = ?', [podcastLocal.id]));
print(_count);
if (_count == _result) { print(count);
_result = 0; await dbClient.rawUpdate(
return _result; """UPDATE PodcastLocal SET update_count = ? WHERE id = ?""",
[(result - count), podcastLocal.id]);
if (count == result) {
result = 0;
return result;
} else { } else {
for (int i = 0; i < (_result - _count); i++) { for (int i = 0; i < (result - count); i++) {
print(_p.items[i].title); print(feed.items[i].title);
if (_p.items[i].itunes.summary != null) { // if (feed.items[i].itunes.summary != null) {
_p.items[i].itunes.summary.contains('<') // feed.items[i].itunes.summary.contains('<')
? _description = _p.items[i].itunes.summary // ? description = feed.items[i].itunes.summary
: _description = _p.items[i].description; // : description = feed.items[i].description;
} else { // } else {
_description = _p.items[i].description; // description = feed.items[i].description;
// }
description = _getDescription(
feed.items[i].content.value ?? '',
feed.items[i].description ?? '',
feed.items[i].itunes.summary ?? '');
if (feed.items[i].enclosure?.url != null) {
_isXimalaya(feed.items[i].enclosure.url)
? url = feed.items[i].enclosure.url.split('=').last
: url = feed.items[i].enclosure.url;
} }
isXimalaya(_p.items[i].enclosure.url) final title = feed.items[i].itunes.title ?? feed.items[i].title;
? _url = _p.items[i].enclosure.url.split('=').last final length = feed.items[i]?.enclosure?.length;
: _url = _p.items[i].enclosure.url; final pubDate = feed.items[i].pubDate;
final date = _parsePubDate(pubDate);
final milliseconds = date.millisecondsSinceEpoch;
final duration = feed.items[i].itunes.duration?.inMinutes ?? 0;
final explicit = _getExplicit(feed.items[i].itunes.explicit);
final _title = _p.items[i].itunes.title ?? _p.items[i].title; if (url != null) {
final _length = _p.items[i].enclosure.length;
final _pubDate = _p.items[i].pubDate;
final _date = _parsePubDate(_pubDate);
final _milliseconds = _date.millisecondsSinceEpoch;
final _duration = _p.items[i].itunes.duration?.inMinutes ?? 0;
final _explicit = getExplicit(_p.items[i].itunes.explicit);
if (_url != null) {
await dbClient.transaction((txn) { await dbClient.transaction((txn) {
return txn.rawInsert( return txn.rawInsert(
"""INSERT OR IGNORE INTO Episodes(title, enclosure_url, enclosure_length, pubDate, """INSERT OR IGNORE INTO Episodes(title, enclosure_url, enclosure_length, pubDate,
description, feed_id, milliseconds, duration, explicit, media_id) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", description, feed_id, milliseconds, duration, explicit, media_id) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
[ [
_title, title,
_url, url,
_length, length,
_pubDate, pubDate,
_description, description,
podcastLocal.id, podcastLocal.id,
_milliseconds, milliseconds,
_duration, duration,
_explicit, explicit,
_url url
]); ]);
}); });
} }
} }
return _result - _count; return result - count;
} }
} }
@ -573,7 +608,6 @@ class DBHelper {
return count; return count;
} }
Future<int> saveMediaId(String url, String path) async { Future<int> saveMediaId(String url, String path) async {
var dbClient = await database; var dbClient = await database;
int _milliseconds = DateTime.now().millisecondsSinceEpoch; int _milliseconds = DateTime.now().millisecondsSinceEpoch;
@ -583,11 +617,11 @@ class DBHelper {
return count; return count;
} }
Future<int> delDownloaded(String url) async { Future<int> delDownloaded(String url) async {
var dbClient = await database; var dbClient = await database;
int count = await dbClient.rawUpdate( int count = await dbClient.rawUpdate(
"UPDATE Episodes SET downloaded = 'ND', media_id = ? WHERE enclosure_url = ?", [url, url]); "UPDATE Episodes SET downloaded = 'ND', media_id = ? WHERE enclosure_url = ?",
[url, url]);
print('Deleted ' + url); print('Deleted ' + url);
return count; return count;
} }
@ -642,23 +676,27 @@ class DBHelper {
P.title as feed_title, E.duration, E.explicit, E.liked, E.downloaded, P.title as feed_title, E.duration, E.explicit, E.liked, E.downloaded,
P.primaryColor, E.media_id FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id P.primaryColor, E.media_id FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
WHERE E.enclosure_url = ?""", [url]); WHERE E.enclosure_url = ?""", [url]);
episode = EpisodeBrief( if (list.length == 0) {
list.first['title'], return null;
list.first['enclosure_url'], } else {
list.first['enclosure_length'], episode = EpisodeBrief(
list.first['milliseconds'], list.first['title'],
list.first['feed_title'], list.first['enclosure_url'],
list.first['primaryColor'], list.first['enclosure_length'],
list.first['liked'], list.first['milliseconds'],
list.first['downloaded'], list.first['feed_title'],
list.first['duration'], list.first['primaryColor'],
list.first['explicit'], list.first['liked'],
list.first['imagePath'], list.first['downloaded'],
list.first['media_id']); list.first['duration'],
return episode; list.first['explicit'],
list.first['imagePath'],
list.first['media_id']);
return episode;
}
} }
Future<EpisodeBrief> getRssItemWithMediaId(String id) async { Future<EpisodeBrief> getRssItemWithMediaId(String id) async {
var dbClient = await database; var dbClient = await database;
EpisodeBrief episode; EpisodeBrief episode;
List<Map> list = await dbClient.rawQuery( List<Map> list = await dbClient.rawQuery(
@ -681,5 +719,4 @@ class DBHelper {
list.first['media_id']); list.first['media_id']);
return episode; return episode;
} }
} }

View File

@ -41,30 +41,20 @@ Future main() async {
child: MyApp(), child: MyApp(),
), ),
); );
SystemUiOverlayStyle systemUiOverlayStyle =
SystemUiOverlayStyle(statusBarColor: Colors.transparent, systemNavigationBarColor: Colors.transparent);
SystemChrome.setSystemUIOverlayStyle(systemUiOverlayStyle);
await SystemChrome.setPreferredOrientations( await SystemChrome.setPreferredOrientations(
[DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]); [DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]);
} }
class MyApp extends StatelessWidget { class MyApp extends StatelessWidget {
void setWorkManager() {
Workmanager.initialize(
callbackDispatcher,
isInDebugMode: true,
);
Workmanager.registerPeriodicTask("1", "update_podcasts",
frequency: Duration(hours: 12),
initialDelay: Duration(seconds: 5),
constraints: Constraints(
networkType: NetworkType.connected,
requiresBatteryNotLow: true,
));
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Consumer<SettingState>( return Consumer<SettingState>(
builder: (_, setting, __) { builder: (_, setting, __) {
if (setting.autoUpdate) setWorkManager();
return MaterialApp( return MaterialApp(
themeMode: setting.theme, themeMode: setting.theme,
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
@ -92,6 +82,7 @@ class MyApp extends StatelessWidget {
), ),
darkTheme: ThemeData.dark().copyWith( darkTheme: ThemeData.dark().copyWith(
accentColor: setting.accentSetColor, accentColor: setting.accentSetColor,
// scaffoldBackgroundColor: Colors.black87,
appBarTheme: AppBarTheme(elevation: 0), appBarTheme: AppBarTheme(elevation: 0),
), ),
home: MyHomePage(), home: MyHomePage(),

View File

@ -101,9 +101,7 @@ class _PodcastDetailState extends State<PodcastDetail> {
fit: BoxFit.cover)), fit: BoxFit.cover)),
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: Container( child: Container(
color: Theme.of(context) color: Colors.black26,
.scaffoldBackgroundColor
.withOpacity(0.5),
padding: EdgeInsets.symmetric(vertical: 5.0), padding: EdgeInsets.symmetric(vertical: 5.0),
width: MediaQuery.of(context).size.width, width: MediaQuery.of(context).size.width,
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
@ -162,186 +160,206 @@ class _PodcastDetailState extends State<PodcastDetail> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Color _color = widget.podcastLocal.primaryColor.colorizedark(); Color _color = widget.podcastLocal.primaryColor.colorizedark();
return AnnotatedRegion<SystemUiOverlayStyle>( return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle( value: SystemUiOverlayStyle(
statusBarIconBrightness: Brightness.light, statusBarIconBrightness: Brightness.dark,
systemNavigationBarColor: Theme.of(context).primaryColor, systemNavigationBarColor: Theme.of(context).primaryColor,
statusBarColor: _color, systemNavigationBarIconBrightness:
Theme.of(context).accentColorBrightness,
//statusBarColor: _color,
), ),
child: SafeArea( child: Scaffold(
child: Scaffold( body: SafeArea(
body: RefreshIndicator( top: false,
key: _refreshIndicatorKey, child: RefreshIndicator(
color: Theme.of(context).accentColor, key: _refreshIndicatorKey,
onRefresh: () => _updateRssItem(widget.podcastLocal), color: Theme.of(context).accentColor,
child: Stack( onRefresh: () => _updateRssItem(widget.podcastLocal),
children: <Widget>[ child: Stack(
FutureBuilder<List<EpisodeBrief>>( children: <Widget>[
future: _getRssItem(widget.podcastLocal), FutureBuilder<List<EpisodeBrief>>(
builder: (context, snapshot) { future: _getRssItem(widget.podcastLocal),
if (snapshot.hasError) print(snapshot.error); builder: (context, snapshot) {
return (snapshot.hasData) if (snapshot.hasError) print(snapshot.error);
? CustomScrollView( return (snapshot.hasData)
physics: const AlwaysScrollableScrollPhysics(), ? CustomScrollView(
primary: true, physics: const AlwaysScrollableScrollPhysics(),
slivers: <Widget>[ primary: true,
SliverAppBar( slivers: <Widget>[
actions: <Widget>[ SliverAppBar(
PopupMenuButton<String>( brightness: Brightness.dark,
shape: RoundedRectangleBorder( actions: <Widget>[
borderRadius: BorderRadius.all( PopupMenuButton<String>(
Radius.circular(10))), shape: RoundedRectangleBorder(
elevation: 2, borderRadius: BorderRadius.all(
tooltip: 'Menu', Radius.circular(10))),
itemBuilder: (context) => [ elevation: 2,
widget.podcastLocal.link != null tooltip: 'Menu',
? PopupMenuItem( itemBuilder: (context) => [
value: widget.podcastLocal.link, widget.podcastLocal.link != null
child: Container( ? PopupMenuItem(
padding: value: widget.podcastLocal.link,
EdgeInsets.only(left: 10), child: Container(
child: Row( padding:
children: <Widget>[ EdgeInsets.only(left: 10),
Icon(Icons.link, child: Row(
color: Theme.of(context) children: <Widget>[
.tabBarTheme Icon(Icons.link,
.labelColor), color: Theme.of(context)
Padding( .tabBarTheme
padding: .labelColor),
EdgeInsets.symmetric( Padding(
horizontal: 5.0), padding:
), EdgeInsets.symmetric(
Text('Visit Site'), horizontal: 5.0),
], ),
Text('Visit Site'),
],
),
), ),
), )
) : Center(),
: Center(), PopupMenuItem(
PopupMenuItem( value: widget.podcastLocal.rssUrl,
value: widget.podcastLocal.rssUrl, child: Container(
child: Container( padding: EdgeInsets.only(left: 10),
padding: EdgeInsets.only(left: 10), child: Row(
child: Row( children: <Widget>[
children: <Widget>[ Icon(
Icon( Icons.rss_feed,
Icons.rss_feed, color: Theme.of(context)
color: Theme.of(context) .tabBarTheme
.tabBarTheme .labelColor,
.labelColor, ),
), Padding(
Padding( padding: EdgeInsets.symmetric(
padding: EdgeInsets.symmetric( horizontal: 5.0),
horizontal: 5.0), ),
), Text('View Rss Feed'),
Text('View Rss Feed'), ],
], ),
), ),
), ),
),
],
onSelected: (url) {
_launchUrl(url);
},
)
],
elevation: 0,
iconTheme: IconThemeData(color: Colors.white),
expandedHeight: 170,
backgroundColor: _color,
floating: true,
pinned: true,
flexibleSpace: LayoutBuilder(builder:
(BuildContext context,
BoxConstraints constraints) {
top = constraints.biggest.height;
return FlexibleSpaceBar(
background: Stack(
children: <Widget>[
Container(
margin: EdgeInsets.only(top: 120),
padding: EdgeInsets.only(
left: 80, right: 120),
color: Colors.white10,
alignment: Alignment.centerLeft,
child: Column(
mainAxisAlignment:
MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment:
CrossAxisAlignment.start,
children: <Widget>[
Text(
widget.podcastLocal.author ??
'',
style: TextStyle(
color: Colors.white)),
widget.podcastLocal.provider
.isNotEmpty
? Text(
'Hosted on ' +
widget.podcastLocal
.provider,
maxLines: 1,
style: TextStyle(
color: Colors.white),
)
: Center(),
],
),
),
Container(
alignment: Alignment.centerRight,
padding: EdgeInsets.only(right: 10),
child: SizedBox(
height: 120,
child: Image.file(File(
"${widget.podcastLocal.imagePath}")),
),
),
Container(
alignment: Alignment.center,
child: podcastInfo(context),
),
], ],
), onSelected: (url) {
title: top < 70 _launchUrl(url);
? Text(widget.podcastLocal.title, },
maxLines: 1, )
overflow: TextOverflow.ellipsis, ],
style: TextStyle(color: Colors.white)) elevation: 0,
: Center(), iconTheme: IconThemeData(
); color: Colors.white,
}), ),
), expandedHeight:
SliverList( 150 + MediaQuery.of(context).padding.top,
delegate: SliverChildBuilderDelegate( backgroundColor: _color,
(BuildContext context, int index) { floating: true,
return hostsList(context, hosts); pinned: true,
}, flexibleSpace: LayoutBuilder(builder:
childCount: 1, (BuildContext context,
BoxConstraints constraints) {
top = constraints.biggest.height;
return FlexibleSpaceBar(
background: Stack(
children: <Widget>[
Container(
margin: EdgeInsets.only(
top: 120 +
MediaQuery.of(context)
.padding
.top),
padding: EdgeInsets.only(
left: 80, right: 120),
color: Colors.white10,
alignment: Alignment.centerLeft,
child: Column(
mainAxisAlignment:
MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment:
CrossAxisAlignment.start,
children: <Widget>[
Text(
widget.podcastLocal.author ??
'',
maxLines: 1,
overflow:
TextOverflow.ellipsis,
style: TextStyle(
color: Colors.white)),
widget.podcastLocal.provider
.isNotEmpty
? Text(
'Hosted on ' +
widget.podcastLocal
.provider,
maxLines: 1,
style: TextStyle(
color: Colors.white),
)
: Center(),
],
),
),
Container(
alignment: Alignment.centerRight,
padding: EdgeInsets.only(right: 10),
child: SizedBox(
height: 120,
child: Image.file(File(
"${widget.podcastLocal.imagePath}")),
),
),
Container(
alignment: Alignment.center,
child: podcastInfo(context),
),
],
),
title: top <
70 +
MediaQuery.of(context)
.padding
.top
? Text(widget.podcastLocal.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style:
TextStyle(color: Colors.white))
: Center(),
);
}),
), ),
), SliverList(
SliverPadding( delegate: SliverChildBuilderDelegate(
padding: const EdgeInsets.symmetric( (BuildContext context, int index) {
horizontal: 10.0), return hostsList(context, hosts);
sliver: EpisodeGrid( },
podcast: snapshot.data, childCount: 1,
showDownload: false, ),
showFavorite: true, ),
showNumber: true, SliverPadding(
heroTag: 'podcast', padding: const EdgeInsets.symmetric(
)), horizontal: 10.0),
], sliver: EpisodeGrid(
) podcast: snapshot.data,
: Center(child: CircularProgressIndicator()); showDownload: false,
}, showFavorite: true,
), showNumber: true,
Container(child: PlayerWidget()), heroTag: 'podcast',
], updateCount: widget.podcastLocal.upateCount,
)),
],
)
: Center(child: CircularProgressIndicator());
},
),
Container(child: PlayerWidget()),
],
),
), ),
)), ),
), ),
); );
} }

View File

@ -29,35 +29,31 @@ class _PodcastGroupListState extends State<PodcastGroupList> {
) )
: Container( : Container(
color: Theme.of(context).primaryColor, color: Theme.of(context).primaryColor,
child: Stack( child: ReorderableListView(
children: <Widget>[ onReorder: (int oldIndex, int newIndex) {
ReorderableListView( setState(() {
onReorder: (int oldIndex, int newIndex) { if (newIndex > oldIndex) {
setState(() { newIndex -= 1;
if (newIndex > oldIndex) { }
newIndex -= 1; final PodcastLocal podcast =
} widget.group.podcasts.removeAt(oldIndex);
final PodcastLocal podcast = widget.group.podcasts.insert(newIndex, podcast);
widget.group.podcasts.removeAt(oldIndex); });
widget.group.podcasts.insert(newIndex, podcast); widget.group.setOrderedPodcasts = widget.group.podcasts;
}); groupList.addToOrderChanged(widget.group.name);
widget.group.setOrderedPodcasts = widget.group.podcasts; },
groupList.addToOrderChanged(widget.group.name); children: widget.group.podcasts
}, .map<Widget>((PodcastLocal podcastLocal) {
children: widget.group.podcasts return Container(
.map<Widget>((PodcastLocal podcastLocal) { decoration:
return Container( BoxDecoration(color: Theme.of(context).primaryColor),
decoration: key: ObjectKey(podcastLocal.title),
BoxDecoration(color: Theme.of(context).primaryColor), child: PodcastCard(
key: ObjectKey(podcastLocal.title), podcastLocal: podcastLocal,
child: PodcastCard( group: widget.group,
podcastLocal: podcastLocal, ),
group: widget.group, );
), }).toList(),
);
}).toList(),
),
],
), ),
); );
} }
@ -393,89 +389,88 @@ class _RenameGroupState extends State<RenameGroup> {
var groupList = Provider.of<GroupList>(context, listen: false); var groupList = Provider.of<GroupList>(context, listen: false);
List list = groupList.groups.map((e) => e.name).toList(); List list = groupList.groups.map((e) => e.name).toList();
return AnnotatedRegion<SystemUiOverlayStyle>( return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle( value: SystemUiOverlayStyle(
statusBarIconBrightness: Brightness.light, statusBarIconBrightness: Brightness.light,
systemNavigationBarColor: systemNavigationBarColor:
Theme.of(context).brightness == Brightness.light Theme.of(context).brightness == Brightness.light
? Color.fromRGBO(113, 113, 113, 1) ? Color.fromRGBO(113, 113, 113, 1)
: Color.fromRGBO(5, 5, 5, 1), : Color.fromRGBO(5, 5, 5, 1),
statusBarColor: Theme.of(context).brightness == Brightness.light // statusBarColor: Theme.of(context).brightness == Brightness.light
? Color.fromRGBO(113, 113, 113, 1) // ? Color.fromRGBO(113, 113, 113, 1)
: Color.fromRGBO(15, 15, 15, 1), // : Color.fromRGBO(15, 15, 15, 1),
), ),
child: SafeArea( child: AlertDialog(
child: AlertDialog( shape: RoundedRectangleBorder(
shape: RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(10))),
borderRadius: BorderRadius.all(Radius.circular(10))), elevation: 1,
elevation: 1, contentPadding: EdgeInsets.symmetric(horizontal: 20),
contentPadding: EdgeInsets.symmetric(horizontal: 20), titlePadding:
titlePadding: EdgeInsets.only(top: 20, left: 20, right: 200, bottom: 20),
EdgeInsets.only(top: 20, left: 20, right: 200, bottom: 20), actionsPadding: EdgeInsets.all(0),
actionsPadding: EdgeInsets.all(0), actions: <Widget>[
actions: <Widget>[ FlatButton(
FlatButton( onPressed: () => Navigator.of(context).pop(),
onPressed: () => Navigator.of(context).pop(), child: Text(
child: Text( 'CANCEL',
'CANCEL', style: TextStyle(color: Colors.grey[600]),
style: TextStyle(color: Colors.grey[600]),
),
),
FlatButton(
onPressed: () async {
if (list.contains(_newName)) {
setState(() => _error = 1);
} else {
PodcastGroup newGroup = PodcastGroup(_newName,
color: widget.group.color,
id: widget.group.id,
podcastList: widget.group.podcastList);
groupList.updateGroup(newGroup);
Navigator.of(context).pop();
}
},
child: Text('DONE',
style: TextStyle(color: Theme.of(context).accentColor)),
)
],
title: Text('Edit group name'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
TextField(
decoration: InputDecoration(
contentPadding: EdgeInsets.symmetric(horizontal: 10),
hintText: widget.group.name,
hintStyle: TextStyle(fontSize: 18),
filled: true,
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).accentColor, width: 2.0),
),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).accentColor, width: 2.0),
),
),
cursorRadius: Radius.circular(2),
autofocus: true,
maxLines: 1,
controller: _controller,
onChanged: (value) {
_newName = value;
},
),
Container(
alignment: Alignment.centerLeft,
child: (_error == 1)
? Text(
'Group existed',
style: TextStyle(color: Colors.red[400]),
)
: Center(),
),
],
), ),
), ),
)); FlatButton(
onPressed: () async {
if (list.contains(_newName)) {
setState(() => _error = 1);
} else {
PodcastGroup newGroup = PodcastGroup(_newName,
color: widget.group.color,
id: widget.group.id,
podcastList: widget.group.podcastList);
groupList.updateGroup(newGroup);
Navigator.of(context).pop();
}
},
child: Text('DONE',
style: TextStyle(color: Theme.of(context).accentColor)),
)
],
title: Text('Edit group name'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
TextField(
decoration: InputDecoration(
contentPadding: EdgeInsets.symmetric(horizontal: 10),
hintText: widget.group.name,
hintStyle: TextStyle(fontSize: 18),
filled: true,
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).accentColor, width: 2.0),
),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).accentColor, width: 2.0),
),
),
cursorRadius: Radius.circular(2),
autofocus: true,
maxLines: 1,
controller: _controller,
onChanged: (value) {
_newName = value;
},
),
Container(
alignment: Alignment.centerLeft,
child: (_error == 1)
? Text(
'Group existed',
style: TextStyle(color: Colors.red[400]),
)
: Center(),
),
],
),
),
);
} }
} }

View File

@ -99,15 +99,16 @@ class _PodcastListState extends State<PodcastList> {
value: SystemUiOverlayStyle( value: SystemUiOverlayStyle(
statusBarIconBrightness: Theme.of(context).accentColorBrightness, statusBarIconBrightness: Theme.of(context).accentColorBrightness,
systemNavigationBarColor: Theme.of(context).primaryColor, systemNavigationBarColor: Theme.of(context).primaryColor,
statusBarColor: Theme.of(context).primaryColor, systemNavigationBarIconBrightness:
Theme.of(context).accentColorBrightness,
), ),
child: SafeArea( child: Scaffold(
child: Scaffold( appBar: AppBar(
appBar: AppBar( title: Text('Podcasts'),
title: Text('Podcasts'), centerTitle: true,
centerTitle: true, ),
), body: SafeArea(
body: Container( child: Container(
color: Theme.of(context).primaryColor, color: Theme.of(context).primaryColor,
child: FutureBuilder<List<PodcastLocal>>( child: FutureBuilder<List<PodcastLocal>>(
future: getPodcastLocal(), future: getPodcastLocal(),
@ -161,18 +162,16 @@ class _PodcastListState extends State<PodcastList> {
113, 113, 113, 1) 113, 113, 113, 1)
: Color.fromRGBO( : Color.fromRGBO(
15, 15, 15, 1), 15, 15, 15, 1),
statusBarColor: Theme.of(context) // statusBarColor: Theme.of(context)
.brightness == // .brightness ==
Brightness.light // Brightness.light
? Color.fromRGBO( // ? Color.fromRGBO(
113, 113, 113, 1) // 113, 113, 113, 1)
: Color.fromRGBO(5, 5, 5, 1), // : Color.fromRGBO(5, 5, 5, 1),
),
child: SafeArea(
child: AboutPodcast(
podcastLocal:
snapshot.data[index]),
), ),
child: AboutPodcast(
podcastLocal:
snapshot.data[index]),
)); ));
}, },
child: Container( child: Container(

View File

@ -137,330 +137,322 @@ class _PodcastManageState extends State<PodcastManage>
value: SystemUiOverlayStyle( value: SystemUiOverlayStyle(
statusBarIconBrightness: Theme.of(context).accentColorBrightness, statusBarIconBrightness: Theme.of(context).accentColorBrightness,
systemNavigationBarColor: Theme.of(context).primaryColor, systemNavigationBarColor: Theme.of(context).primaryColor,
statusBarColor: Theme.of(context).primaryColor, systemNavigationBarIconBrightness:
Theme.of(context).accentColorBrightness,
// statusBarColor: Theme.of(context).primaryColor,
), ),
child: SafeArea( child: Scaffold(
child: Scaffold( appBar: AppBar(
appBar: AppBar( centerTitle: true,
centerTitle: true, title: Text('Groups'),
title: Text('Groups'), actions: <Widget>[
actions: <Widget>[ IconButton(
IconButton( onPressed: () => showGeneralDialog(
onPressed: () => showGeneralDialog( context: context,
context: context, barrierDismissible: true,
barrierDismissible: true, barrierLabel: MaterialLocalizations.of(context)
barrierLabel: MaterialLocalizations.of(context) .modalBarrierDismissLabel,
.modalBarrierDismissLabel, barrierColor: Colors.black54,
barrierColor: Colors.black54, transitionDuration: const Duration(milliseconds: 200),
transitionDuration: const Duration(milliseconds: 200), pageBuilder: (BuildContext context, Animation animaiton,
pageBuilder: (BuildContext context, Animation animaiton, Animation secondaryAnimation) =>
Animation secondaryAnimation) => AddGroup()),
AddGroup()), icon: Icon(Icons.add)),
icon: Icon(Icons.add)), OrderMenu(),
OrderMenu(), ],
], ),
), body: Consumer<GroupList>(builder: (_, groupList, __) {
body: Consumer<GroupList>(builder: (_, groupList, __) { bool _isLoading = groupList.isLoading;
bool _isLoading = groupList.isLoading; List<PodcastGroup> _groups = groupList.groups;
List<PodcastGroup> _groups = groupList.groups; return _isLoading
return _isLoading ? Center()
? Center() : Stack(
: Stack( children: <Widget>[
children: <Widget>[ CustomTabView(
CustomTabView( itemCount: _groups.length,
itemCount: _groups.length, tabBuilder: (context, index) => Tab(
tabBuilder: (context, index) => Tab( child: Container(
child: Container( height: 30.0,
height: 30.0, padding: EdgeInsets.symmetric(horizontal: 10.0),
padding: EdgeInsets.symmetric(horizontal: 10.0), alignment: Alignment.center,
alignment: Alignment.center, decoration: BoxDecoration(
decoration: BoxDecoration( color: (_scroll - index).abs() > 1
color: (_scroll - index).abs() > 1 ? Colors.grey[300]
? Colors.grey[300] : Colors.grey[300]
: Colors.grey[300] .withOpacity((_scroll - index).abs()),
.withOpacity((_scroll - index).abs()), borderRadius:
borderRadius: BorderRadius.all(Radius.circular(15)),
BorderRadius.all(Radius.circular(15)), ),
), child: Text(
child: Text( _groups[index].name,
_groups[index].name, )),
)),
),
pageBuilder: (context, index) => Container(
key: ObjectKey(_groups[index].name),
child: PodcastGroupList(group: _groups[index])),
onPositionChange: (value) =>
setState(() => _index = value),
onScroll: (value) => setState(() => _scroll = value),
), ),
_showSetting pageBuilder: (context, index) => Container(
? Positioned.fill( key: ObjectKey(_groups[index].name),
child: GestureDetector( child: PodcastGroupList(group: _groups[index])),
onTap: () async { onPositionChange: (value) =>
await _menuController.reverse(); setState(() => _index = value),
setState(() => _showSetting = false); onScroll: (value) => setState(() => _scroll = value),
}, ),
child: Container( _showSetting
color: Theme.of(context) ? Positioned.fill(
.scaffoldBackgroundColor top: 50,
.withOpacity(0.5 * _menuController.value), child: GestureDetector(
), onTap: () async {
), await _menuController.reverse();
) setState(() => _showSetting = false);
: Center(), },
Positioned(
right: 30,
bottom: 30,
child: _saveButton(context),
),
_showSetting
? Positioned(
right: 30 * _menuValue,
bottom: 100,
child: Container( child: Container(
alignment: Alignment.centerRight, color: Theme.of(context)
child: Column( .scaffoldBackgroundColor
mainAxisAlignment: MainAxisAlignment.start, .withOpacity(0.5 * _menuController.value),
crossAxisAlignment: CrossAxisAlignment.end, ),
children: <Widget>[ ),
Material( )
color: Colors.transparent, : Center(),
child: InkWell( Positioned(
onTap: () { right: 30,
_menuController.reverse(); bottom: 30,
setState(() => _showSetting = false); child: _saveButton(context),
_index == 0 ),
? Fluttertoast.showToast( _showSetting
msg: ? Positioned(
'Home group is not supported', right: 30 * _menuValue,
gravity: ToastGravity.BOTTOM, bottom: 100,
) child: Container(
: showGeneralDialog( alignment: Alignment.centerRight,
context: context, child: Column(
barrierDismissible: true, mainAxisAlignment: MainAxisAlignment.start,
barrierLabel: crossAxisAlignment: CrossAxisAlignment.end,
MaterialLocalizations.of( children: <Widget>[
context) Material(
.modalBarrierDismissLabel, color: Colors.transparent,
barrierColor: Colors.black54, child: InkWell(
transitionDuration: onTap: () {
const Duration( _menuController.reverse();
milliseconds: 300), setState(() => _showSetting = false);
pageBuilder: (BuildContext _index == 0
context, ? Fluttertoast.showToast(
Animation animaiton, msg:
Animation 'Home group is not supported',
secondaryAnimation) => gravity: ToastGravity.BOTTOM,
RenameGroup( )
group: _groups[_index], : showGeneralDialog(
)); context: context,
}, barrierDismissible: true,
child: Container( barrierLabel:
height: 30.0, MaterialLocalizations.of(
decoration: BoxDecoration( context)
color: Colors.grey[700], .modalBarrierDismissLabel,
borderRadius: BorderRadius.all( barrierColor: Colors.black54,
Radius.circular(10.0))), transitionDuration:
padding: EdgeInsets.symmetric( const Duration(
horizontal: 10), milliseconds: 300),
child: Row( pageBuilder: (BuildContext
children: <Widget>[ context,
Icon( Animation animaiton,
Icons.text_fields, Animation
color: Colors.white, secondaryAnimation) =>
size: 15.0, RenameGroup(
), group: _groups[_index],
Padding( ));
padding: EdgeInsets.symmetric( },
horizontal: 5.0), child: Container(
), height: 30.0,
Text('Edit Name', decoration: BoxDecoration(
style: TextStyle( color: Colors.grey[700],
color: Colors.white)), borderRadius: BorderRadius.all(
], Radius.circular(10.0))),
), padding: EdgeInsets.symmetric(
horizontal: 10),
child: Row(
children: <Widget>[
Icon(
Icons.text_fields,
color: Colors.white,
size: 15.0,
),
Padding(
padding: EdgeInsets.symmetric(
horizontal: 5.0),
),
Text('Edit Name',
style: TextStyle(
color: Colors.white)),
],
), ),
), ),
), ),
Padding( ),
padding: EdgeInsets.symmetric( Padding(
vertical: 10.0)), padding:
Material( EdgeInsets.symmetric(vertical: 10.0)),
color: Colors.transparent, Material(
child: InkWell( color: Colors.transparent,
onTap: () { child: InkWell(
_menuController.reverse(); onTap: () {
setState(() => _showSetting = false); _menuController.reverse();
_index == 0 setState(() => _showSetting = false);
? Fluttertoast.showToast( _index == 0
msg: ? Fluttertoast.showToast(
'Home group is not supported', msg:
gravity: ToastGravity.BOTTOM, 'Home group is not supported',
) gravity: ToastGravity.BOTTOM,
: showGeneralDialog( )
context: context, : showGeneralDialog(
barrierDismissible: true, context: context,
barrierLabel: barrierDismissible: true,
MaterialLocalizations.of( barrierLabel:
context) MaterialLocalizations.of(
.modalBarrierDismissLabel, context)
barrierColor: Colors.black54, .modalBarrierDismissLabel,
transitionDuration: barrierColor: Colors.black54,
const Duration( transitionDuration:
milliseconds: 300), const Duration(
pageBuilder: (BuildContext milliseconds: 300),
context, pageBuilder: (BuildContext
Animation animaiton, context,
Animation Animation animaiton,
secondaryAnimation) => Animation
AnnotatedRegion< secondaryAnimation) =>
SystemUiOverlayStyle>( AnnotatedRegion<
value: SystemUiOverlayStyle>(
SystemUiOverlayStyle( value:
statusBarIconBrightness: SystemUiOverlayStyle(
Brightness.light, statusBarIconBrightness:
systemNavigationBarColor: Brightness.light,
Theme.of(context) systemNavigationBarColor:
.brightness == Theme.of(context)
Brightness .brightness ==
.light Brightness
? Color .light
.fromRGBO( ? Color
113, .fromRGBO(
113, 113,
113, 113,
1) 113,
: Color 1)
.fromRGBO( : Color
15, .fromRGBO(
15, 15,
15, 15,
1), 15,
statusBarColor: 1),
Theme.of(context) // statusBarColor: Theme.of(
.brightness == // context)
Brightness // .brightness ==
.light // Brightness.light
? Color // ? Color.fromRGBO(
.fromRGBO( // 113,
113, // 113,
113, // 113,
113, // 1)
1) // : Color.fromRGBO(
: Color // 5, 5, 5, 1),
.fromRGBO( ),
5, child: AlertDialog(
5, elevation: 1,
5, shape: RoundedRectangleBorder(
1), borderRadius:
), BorderRadius.all(
child: SafeArea( Radius.circular(
child: AlertDialog( 10.0))),
elevation: 1, titlePadding:
shape: RoundedRectangleBorder( EdgeInsets.only(
borderRadius: BorderRadius top: 20,
.all(Radius left: 20,
.circular( right: 200,
10.0))), bottom: 20),
titlePadding: title: Text(
EdgeInsets.only( 'Delete confirm'),
top: 20, content: Text(
left: 20, 'Are you sure you want to delete this group? Podcasts will be moved to Home group.'),
right: 200, actions: <Widget>[
bottom: 20), FlatButton(
title: Text( onPressed: () =>
'Delete confirm'), Navigator.of(
content: Text( context)
'Are you sure you want to delete this group? Podcasts will be moved to Home group.'), .pop(),
actions: <Widget>[ child: Text(
FlatButton( 'CANCEL',
onPressed: () => style: TextStyle(
Navigator.of( color: Colors
context) .grey[
.pop(), 600]),
child: Text( ),
'CANCEL', ),
style: TextStyle( FlatButton(
color: Colors onPressed: () {
.grey[ if (_index ==
600]), groupList
), .groups
), .length -
FlatButton( 1) {
onPressed: () { setState(() {
if (_index == _index =
groupList _index -
.groups 1;
.length - _scroll = 0;
1) { });
setState( groupList.delGroup(
() { _groups[
_index =
_index -
1;
_scroll =
0;
});
groupList.delGroup(_groups[
_index + _index +
1]); 1]);
} else { } else {
groupList.delGroup( groupList.delGroup(
_groups[ _groups[
_index]); _index]);
} }
Navigator.of( Navigator.of(
context) context)
.pop(); .pop();
}, },
child: Text( child: Text(
'CONFIRM', 'CONFIRM',
style: TextStyle( style: TextStyle(
color: Colors color: Colors
.red), .red),
), ),
) )
], ],
), ),
), ));
)); },
}, child: Container(
child: Container( height: 30,
height: 30, decoration: BoxDecoration(
decoration: BoxDecoration( color: Colors.grey[700],
color: Colors.grey[700], borderRadius: BorderRadius.all(
borderRadius: BorderRadius.all( Radius.circular(10.0))),
Radius.circular(10.0))), padding: EdgeInsets.symmetric(
padding: EdgeInsets.symmetric( horizontal: 10),
horizontal: 10), child: Row(
child: Row( children: <Widget>[
children: <Widget>[ Icon(
Icon( Icons.delete_outline,
Icons.delete_outline, color: Colors.red,
color: Colors.red, size: 15.0,
size: 15.0, ),
), Padding(
Padding( padding: EdgeInsets.symmetric(
padding: EdgeInsets.symmetric( horizontal: 5.0),
horizontal: 5.0), ),
), Text('Delete',
Text('Delete', style: TextStyle(
style: TextStyle( color: Colors.red)),
color: Colors.red)), ],
],
),
), ),
), ),
), ),
], ),
), ],
), ),
) ),
: Center(), )
], : Center(),
); ],
}), );
), }),
), ),
); );
} }
@ -525,85 +517,84 @@ class _AddGroupState extends State<AddGroup> {
var groupList = Provider.of<GroupList>(context); var groupList = Provider.of<GroupList>(context);
List list = groupList.groups.map((e) => e.name).toList(); List list = groupList.groups.map((e) => e.name).toList();
return AnnotatedRegion<SystemUiOverlayStyle>( return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle( value: SystemUiOverlayStyle(
statusBarIconBrightness: Brightness.light, statusBarIconBrightness: Brightness.light,
systemNavigationBarColor: systemNavigationBarColor:
Theme.of(context).brightness == Brightness.light Theme.of(context).brightness == Brightness.light
? Color.fromRGBO(113, 113, 113, 1) ? Color.fromRGBO(113, 113, 113, 1)
: Color.fromRGBO(5, 5, 5, 1), : Color.fromRGBO(5, 5, 5, 1),
statusBarColor: Theme.of(context).brightness == Brightness.light // statusBarColor: Theme.of(context).brightness == Brightness.light
? Color.fromRGBO(113, 113, 113, 1) // ? Color.fromRGBO(113, 113, 113, 1)
: Color.fromRGBO(15, 15, 15, 1), // : Color.fromRGBO(15, 15, 15, 1),
), ),
child: SafeArea( child: AlertDialog(
child: AlertDialog( shape: RoundedRectangleBorder(
shape: RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(10))),
borderRadius: BorderRadius.all(Radius.circular(10))), elevation: 1,
elevation: 1, contentPadding: EdgeInsets.symmetric(horizontal: 20),
contentPadding: EdgeInsets.symmetric(horizontal: 20), titlePadding:
titlePadding: EdgeInsets.only(top: 20, left: 20, right: 200, bottom: 20),
EdgeInsets.only(top: 20, left: 20, right: 200, bottom: 20), actionsPadding: EdgeInsets.all(0),
actionsPadding: EdgeInsets.all(0), actions: <Widget>[
actions: <Widget>[ FlatButton(
FlatButton( onPressed: () => Navigator.of(context).pop(),
onPressed: () => Navigator.of(context).pop(), child: Text(
child: Text( 'CANCEL',
'CANCEL', style: TextStyle(color: Colors.grey[600]),
style: TextStyle(color: Colors.grey[600]),
),
),
FlatButton(
onPressed: () async {
if (list.contains(_newGroup)) {
setState(() => _error = 1);
} else {
groupList.addGroup(PodcastGroup(_newGroup));
Navigator.of(context).pop();
}
},
child: Text('DONE',
style: TextStyle(color: Theme.of(context).accentColor)),
)
],
title: Text('Create new group'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
TextField(
decoration: InputDecoration(
contentPadding: EdgeInsets.symmetric(horizontal: 10),
hintText: 'New Group',
hintStyle: TextStyle(fontSize: 18),
filled: true,
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).accentColor, width: 2.0),
),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).accentColor, width: 2.0),
),
),
cursorRadius: Radius.circular(2),
autofocus: true,
maxLines: 1,
controller: _controller,
onChanged: (value) {
_newGroup = value;
},
),
Container(
alignment: Alignment.centerLeft,
child: (_error == 1)
? Text(
'Group existed',
style: TextStyle(color: Colors.red[400]),
)
: Center(),
),
],
), ),
), ),
)); FlatButton(
onPressed: () async {
if (list.contains(_newGroup)) {
setState(() => _error = 1);
} else {
groupList.addGroup(PodcastGroup(_newGroup));
Navigator.of(context).pop();
}
},
child: Text('DONE',
style: TextStyle(color: Theme.of(context).accentColor)),
)
],
title: Text('Create new group'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
TextField(
decoration: InputDecoration(
contentPadding: EdgeInsets.symmetric(horizontal: 10),
hintText: 'New Group',
hintStyle: TextStyle(fontSize: 18),
filled: true,
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).accentColor, width: 2.0),
),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).accentColor, width: 2.0),
),
),
cursorRadius: Radius.circular(2),
autofocus: true,
maxLines: 1,
controller: _controller,
onChanged: (value) {
_newGroup = value;
},
),
Container(
alignment: Alignment.centerLeft,
child: (_error == 1)
? Text(
'Group existed',
style: TextStyle(color: Colors.red[400]),
)
: Center(),
),
],
),
),
);
} }
} }

View File

@ -99,17 +99,19 @@ class _DownloadsManageState extends State<DownloadsManage> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AnnotatedRegion<SystemUiOverlayStyle>( return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle( value: SystemUiOverlayStyle(
statusBarIconBrightness: Theme.of(context).accentColorBrightness, statusBarIconBrightness: Theme.of(context).accentColorBrightness,
systemNavigationBarColor: Theme.of(context).primaryColor, systemNavigationBarColor: Theme.of(context).primaryColor,
statusBarColor: Theme.of(context).primaryColor), systemNavigationBarIconBrightness:
child: SafeArea( Theme.of(context).accentColorBrightness,
child: Scaffold( ),
appBar: AppBar( child: Scaffold(
title: Text('Downloads'), appBar: AppBar(
elevation: 0, title: Text('Downloads'),
backgroundColor: Theme.of(context).primaryColor, elevation: 0,
), backgroundColor: Theme.of(context).primaryColor,
body: Stack( ),
body: SafeArea(
child: Stack(
children: <Widget>[ children: <Widget>[
Column( Column(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
@ -138,7 +140,7 @@ class _DownloadsManageState extends State<DownloadsManage> {
fontSize: 40, fontSize: 40,
fontWeight: FontWeight.bold)), fontWeight: FontWeight.bold)),
TextSpan( TextSpan(
text: ' episodes ', text: _fileNum < 2 ? ' episode' : ' episodes ',
style: TextStyle( style: TextStyle(
color: Theme.of(context).accentColor, color: Theme.of(context).accentColor,
fontSize: 20, fontSize: 20,

View File

@ -65,74 +65,79 @@ class _PlayedHistoryState extends State<PlayedHistory>
value: SystemUiOverlayStyle( value: SystemUiOverlayStyle(
statusBarIconBrightness: Theme.of(context).accentColorBrightness, statusBarIconBrightness: Theme.of(context).accentColorBrightness,
systemNavigationBarColor: Theme.of(context).primaryColor, systemNavigationBarColor: Theme.of(context).primaryColor,
statusBarColor: Theme.of(context).primaryColor, systemNavigationBarIconBrightness:
Theme.of(context).accentColorBrightness,
//statusBarColor: Theme.of(context).primaryColor,
), ),
child: SafeArea( child: Scaffold(
child: Scaffold( backgroundColor: Theme.of(context).primaryColor,
body: NestedScrollView( body: SafeArea(
headerSliverBuilder: child: NestedScrollView(
(BuildContext context, bool innerBoxScrolled) { headerSliverBuilder: (BuildContext context, bool innerBoxScrolled) {
return <Widget>[ return <Widget>[
SliverAppBar( SliverAppBar(
elevation: 0, backgroundColor: Theme.of(context).primaryColor,
expandedHeight: 260, elevation: 0,
floating: false, expandedHeight: 260,
pinned: true, floating: false,
flexibleSpace: LayoutBuilder( pinned: true,
builder: flexibleSpace: LayoutBuilder(
(BuildContext context, BoxConstraints constraints) { builder:
top = constraints.biggest.height; (BuildContext context, BoxConstraints constraints) {
return FlexibleSpaceBar( top = constraints.biggest.height;
title: top < 70 return FlexibleSpaceBar(
? Text( title: top < 70 + MediaQuery.of(context).padding.top
'History', ? Text(
) 'History',
: Center(), )
background: Padding( : Center(),
padding: EdgeInsets.only( background: Padding(
top: 50, left: 50, right: 50, bottom: 30), padding: EdgeInsets.only(
child: FutureBuilder<List<FlSpot>>( top: 50, left: 50, right: 50, bottom: 30),
future: getData(), child: FutureBuilder<List<FlSpot>>(
builder: (context, snapshot) { future: getData(),
return snapshot.hasData builder: (context, snapshot) {
? HistoryChart(snapshot.data) return snapshot.hasData
: Center(); ? HistoryChart(snapshot.data)
}), : Center();
), }),
);
},
),
),
SliverPersistentHeader(
delegate: _SliverAppBarDelegate(
TabBar(
controller: _controller,
tabs: <Widget>[
Tab(
child: Text('Listen'),
),
Tab(
child: Text('Subscribe'),
)
],
), ),
Theme.of(context).primaryColor), );
pinned: true, },
), ),
]; ),
}, SliverPersistentHeader(
body: TabBarView(controller: _controller, children: <Widget>[ delegate: _SliverAppBarDelegate(
FutureBuilder<List<PlayHistory>>( TabBar(
future: getPlayHistory(), controller: _controller,
builder: (context, snapshot) { tabs: <Widget>[
double _width = MediaQuery.of(context).size.width; Tab(
return snapshot.hasData child: Text('Listen'),
? ListView.builder( ),
shrinkWrap: true, Tab(
scrollDirection: Axis.vertical, child: Text('Subscribe'),
itemCount: snapshot.data.length, )
itemBuilder: (BuildContext context, int index) { ],
return Column( ),
Theme.of(context).primaryColor),
pinned: true,
),
];
},
body: TabBarView(controller: _controller, children: <Widget>[
FutureBuilder<List<PlayHistory>>(
future: getPlayHistory(),
builder: (context, snapshot) {
double _width = MediaQuery.of(context).size.width;
return snapshot.hasData
? ListView.builder(
shrinkWrap: true,
scrollDirection: Axis.vertical,
itemCount: snapshot.data.length,
itemBuilder: (BuildContext context, int index) {
return Container(
color: Theme.of(context).scaffoldBackgroundColor,
child: Column(
children: <Widget>[ children: <Widget>[
ListTile( ListTile(
title: Column( title: Column(
@ -160,12 +165,17 @@ class _PlayedHistoryState extends State<PlayedHistory>
width: _width, width: _width,
child: Row( child: Row(
children: <Widget>[ children: <Widget>[
Icon(Icons.timelapse, color: Colors.grey[400],), Icon(
Icons.timelapse,
color: Colors.grey[400],
),
Container( Container(
height: 2, height: 2,
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border(bottom: BorderSide(color: Colors.grey[400], width: 2.0)) border: Border(
), bottom: BorderSide(
color: Colors.grey[400],
width: 2.0))),
width: _width * width: _width *
snapshot.data[index] snapshot.data[index]
.seekValue < .seekValue <
@ -201,24 +211,27 @@ class _PlayedHistoryState extends State<PlayedHistory>
), ),
// Divider(height: 2), // Divider(height: 2),
], ],
); ),
}) );
: Center( })
child: CircularProgressIndicator(), : Center(
); child: CircularProgressIndicator(),
}, );
), },
FutureBuilder<List<SubHistory>>( ),
future: getSubHistory(), FutureBuilder<List<SubHistory>>(
builder: (context, snapshot) { future: getSubHistory(),
return snapshot.hasData builder: (context, snapshot) {
? ListView.builder( return snapshot.hasData
shrinkWrap: true, ? ListView.builder(
scrollDirection: Axis.vertical, shrinkWrap: true,
itemCount: snapshot.data.length, scrollDirection: Axis.vertical,
itemBuilder: (BuildContext context, int index) { itemCount: snapshot.data.length,
bool _status = snapshot.data[index].status; itemBuilder: (BuildContext context, int index) {
return Column( bool _status = snapshot.data[index].status;
return Container(
color: Theme.of(context).scaffoldBackgroundColor,
child: Column(
children: <Widget>[ children: <Widget>[
ListTile( ListTile(
enabled: _status, enabled: _status,
@ -274,14 +287,16 @@ class _PlayedHistoryState extends State<PlayedHistory>
height: 2, height: 2,
) )
], ],
); ),
}) );
: Center( })
child: CircularProgressIndicator(), : Center(
); child: CircularProgressIndicator(),
}, );
), },
])), ),
]),
),
), ),
), ),
); );
@ -318,72 +333,74 @@ class HistoryChart extends StatelessWidget {
HistoryChart(this.stats); HistoryChart(this.stats);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return LineChart( return SafeArea(
LineChartData( child: LineChart(
backgroundColor: Colors.transparent, LineChartData(
gridData: FlGridData( backgroundColor: Colors.transparent,
show: true, gridData: FlGridData(
drawHorizontalLine: true, show: true,
getDrawingHorizontalLine: (value) { drawHorizontalLine: true,
return value % 60 == 0 getDrawingHorizontalLine: (value) {
? FlLine( return value % 60 == 0
color: Theme.of(context).brightness == Brightness.light ? FlLine(
? Colors.grey[400] color: Theme.of(context).brightness == Brightness.light
: Colors.grey[700], ? Colors.grey[400]
strokeWidth: 1, : Colors.grey[700],
) strokeWidth: 1,
: FlLine(color: Colors.transparent); )
}, : FlLine(color: Colors.transparent);
),
titlesData: FlTitlesData(
show: true,
bottomTitles: SideTitles(
textStyle: TextStyle(
color: const Color(0xff67727d),
fontWeight: FontWeight.bold,
fontSize: 12,
),
showTitles: true,
reservedSize: 10,
getTitles: (value) {
return DateFormat.E().format(
DateTime.now().subtract(Duration(days: (7 - value.toInt()))));
}, },
margin: 5,
), ),
leftTitles: SideTitles( titlesData: FlTitlesData(
showTitles: true, show: true,
textStyle: TextStyle( bottomTitles: SideTitles(
color: const Color(0xff67727d), textStyle: TextStyle(
fontWeight: FontWeight.bold, color: const Color(0xff67727d),
fontSize: 12, fontWeight: FontWeight.bold,
fontSize: 12,
),
showTitles: true,
reservedSize: 10,
getTitles: (value) {
return DateFormat.E().format(DateTime.now()
.subtract(Duration(days: (7 - value.toInt()))));
},
margin: 5,
),
leftTitles: SideTitles(
showTitles: true,
textStyle: TextStyle(
color: const Color(0xff67727d),
fontWeight: FontWeight.bold,
fontSize: 12,
),
getTitles: (value) {
return value % 60 == 0 && value > 0 ? '${value ~/ 60}h' : '';
},
reservedSize: 20,
margin: 5,
), ),
getTitles: (value) {
return value % 60 == 0 && value > 0 ? '${value ~/ 60}h' : '';
},
reservedSize: 20,
margin: 5,
), ),
borderData: FlBorderData(
show: false,
border: Border(
left: BorderSide(color: Colors.red, width: 2),
)),
lineBarsData: [
LineChartBarData(
spots: this.stats,
isCurved: false,
colors: [Theme.of(context).accentColor],
barWidth: 3,
isStrokeCapRound: true,
dotData: FlDotData(
show: true,
dotSize: 5,
dotColor: Theme.of(context).accentColor,
),
),
],
), ),
borderData: FlBorderData(
show: false,
border: Border(
left: BorderSide(color: Colors.red, width: 2),
)),
lineBarsData: [
LineChartBarData(
spots: this.stats,
isCurved: false,
colors: [Theme.of(context).accentColor],
barWidth: 3,
isStrokeCapRound: true,
dotData: FlDotData(
show: true,
dotSize: 5,
dotColor: Theme.of(context).accentColor,
),
),
],
), ),
); );
} }

View File

@ -4,28 +4,31 @@ import 'package:url_launcher/url_launcher.dart';
import 'licenses.dart'; import 'licenses.dart';
class Libries extends StatelessWidget { class Libries extends StatelessWidget {
_launchUrl(String url) async { _launchUrl(String url) async {
if (await canLaunch(url)) { if (await canLaunch(url)) {
await launch(url); await launch(url);
} else { } else {
throw 'Could not launch $url'; throw 'Could not launch $url';
} }
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AnnotatedRegion<SystemUiOverlayStyle>( return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle( value: SystemUiOverlayStyle(
statusBarIconBrightness: Theme.of(context).accentColorBrightness, statusBarIconBrightness: Theme.of(context).accentColorBrightness,
systemNavigationBarColor: Theme.of(context).primaryColor, systemNavigationBarColor: Theme.of(context).primaryColor,
statusBarColor: Theme.of(context).primaryColor), systemNavigationBarIconBrightness:
child: SafeArea( Theme.of(context).accentColorBrightness,
child: Scaffold( ),
appBar: AppBar( child: Scaffold(
title: Text('Libraies'), appBar: AppBar(
elevation: 0, title: Text('Libraies'),
backgroundColor: Theme.of(context).primaryColor, elevation: 0,
), backgroundColor: Theme.of(context).primaryColor,
body: SingleChildScrollView( ),
body: SafeArea(
child: SingleChildScrollView(
scrollDirection: Axis.vertical, scrollDirection: Axis.vertical,
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,

View File

@ -41,4 +41,5 @@ List<Libries> plugins = [
Libries('audio_service', mit, 'https://pub.dev/packages/audio_service'), Libries('audio_service', mit, 'https://pub.dev/packages/audio_service'),
Libries('just_audio', apacheLicense, 'https://pub.dev/packages/just_audio'), Libries('just_audio', apacheLicense, 'https://pub.dev/packages/just_audio'),
Libries('line_icons', gpl, 'https://pub.dev/packages/line_icons'), Libries('line_icons', gpl, 'https://pub.dev/packages/line_icons'),
Libries('flutter_file_dialog', bsd, 'https://pub.dev/packages/flutter_file_dialog')
]; ];

View File

@ -1,15 +1,22 @@
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:workmanager/workmanager.dart'; import 'package:path/path.dart';
import 'package:line_icons/line_icons.dart'; import 'package:line_icons/line_icons.dart';
import 'package:tsacdop/class/podcastlocal.dart';
import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:path_provider/path_provider.dart';
import 'package:flutter_file_dialog/flutter_file_dialog.dart';
import 'package:tsacdop/class/audiostate.dart'; import 'package:tsacdop/class/audiostate.dart';
import 'package:tsacdop/class/settingstate.dart'; import 'package:tsacdop/util/ompl_build.dart';
import 'package:tsacdop/settings/theme.dart'; import 'theme.dart';
import 'package:tsacdop/settings/storage.dart'; import 'storage.dart';
import 'package:tsacdop/settings/history.dart'; import 'history.dart';
import 'syncing.dart';
import 'libries.dart'; import 'libries.dart';
class Settings extends StatelessWidget { class Settings extends StatelessWidget {
@ -21,24 +28,38 @@ class Settings extends StatelessWidget {
} }
} }
_exportOmpl() async {
var dbHelper = DBHelper();
List<PodcastLocal> podcastList = await dbHelper.getPodcastLocalAll();
var ompl = omplBuilder(podcastList);
var tempdir = await getTemporaryDirectory();
var file = File(join(tempdir.path, 'tsacdop_ompl.xml'));
print(file.path);
await file.writeAsString(ompl.toString());
final params = SaveFileDialogParams(sourceFilePath: file.path);
final filePath = await FlutterFileDialog.saveFile(params: params);
print(filePath);
print(ompl.toString());
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false); var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
var settings = Provider.of<SettingState>(context, listen: false);
return AnnotatedRegion<SystemUiOverlayStyle>( return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle( value: SystemUiOverlayStyle(
statusBarIconBrightness: Theme.of(context).accentColorBrightness, statusBarIconBrightness: Theme.of(context).accentColorBrightness,
systemNavigationBarColor: Theme.of(context).primaryColor, systemNavigationBarColor: Theme.of(context).primaryColor,
statusBarColor: Theme.of(context).primaryColor, systemNavigationBarIconBrightness:
Theme.of(context).accentColorBrightness,
), ),
child: SafeArea( child: Scaffold(
child: Scaffold( appBar: AppBar(
appBar: AppBar( title: Text('Settings'),
title: Text('Settings'), elevation: 0,
elevation: 0, backgroundColor: Theme.of(context).primaryColor,
backgroundColor: Theme.of(context).primaryColor, ),
), body: SafeArea(
body: SingleChildScrollView( child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
scrollDirection: Axis.vertical, scrollDirection: Axis.vertical,
child: Column( child: Column(
@ -72,16 +93,14 @@ class Settings extends StatelessWidget {
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => ThemeSetting())), builder: (context) => ThemeSetting())),
contentPadding: contentPadding: EdgeInsets.symmetric(horizontal: 25.0),
EdgeInsets.symmetric(horizontal: 25.0),
leading: Icon(LineIcons.adjust_solid), leading: Icon(LineIcons.adjust_solid),
title: Text('Appearance'), title: Text('Appearance'),
subtitle: Text('Colors and themes'), subtitle: Text('Colors and themes'),
), ),
Divider(height: 2), Divider(height: 2),
ListTile( ListTile(
contentPadding: contentPadding: EdgeInsets.symmetric(horizontal: 25.0),
EdgeInsets.symmetric(horizontal: 25.0),
leading: Icon(LineIcons.play_circle), leading: Icon(LineIcons.play_circle),
title: Text('AutoPlay'), title: Text('AutoPlay'),
subtitle: Text('Autoplay next episode in playlist'), subtitle: Text('Autoplay next episode in playlist'),
@ -94,20 +113,14 @@ class Settings extends StatelessWidget {
), ),
Divider(height: 2), Divider(height: 2),
ListTile( ListTile(
contentPadding: onTap: () => Navigator.push(
EdgeInsets.symmetric(horizontal: 25.0), context,
MaterialPageRoute(
builder: (context) => SyncingSetting())),
contentPadding: EdgeInsets.symmetric(horizontal: 25.0),
leading: Icon(LineIcons.cloud_download_alt_solid), leading: Icon(LineIcons.cloud_download_alt_solid),
title: Text('AutoUpdate'), title: Text('Syncing'),
subtitle: Text('Auto update feed every day'), subtitle: Text('Refresh podcasts in the background'),
trailing: Selector<SettingState, bool>(
selector: (_, settings) => settings.autoUpdate,
builder: (_, data, __) => Switch(
value: data,
onChanged: (boo) async {
settings.autoUpdate = boo;
if (!boo) await Workmanager.cancelAll();
}),
),
), ),
Divider(height: 2), Divider(height: 2),
ListTile( ListTile(
@ -115,8 +128,7 @@ class Settings extends StatelessWidget {
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => StorageSetting())), builder: (context) => StorageSetting())),
contentPadding: contentPadding: EdgeInsets.symmetric(horizontal: 25.0),
EdgeInsets.symmetric(horizontal: 25.0),
leading: Icon(LineIcons.save), leading: Icon(LineIcons.save),
title: Text('Storage'), title: Text('Storage'),
subtitle: Text('Manage cache and download storage'), subtitle: Text('Manage cache and download storage'),
@ -127,13 +139,22 @@ class Settings extends StatelessWidget {
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => PlayedHistory())), builder: (context) => PlayedHistory())),
contentPadding: contentPadding: EdgeInsets.symmetric(horizontal: 25.0),
EdgeInsets.symmetric(horizontal: 25.0),
leading: Icon(Icons.update), leading: Icon(Icons.update),
title: Text('History'), title: Text('History'),
subtitle: Text('Listen data'), subtitle: Text('Listen data'),
), ),
Divider(height: 2), Divider(height: 2),
ListTile(
onTap: () {
_exportOmpl();
},
contentPadding: EdgeInsets.symmetric(horizontal: 25.0),
leading: Icon(LineIcons.file_code_solid),
title: Text('Export'),
subtitle: Text('Export ompl file'),
),
Divider(height: 2),
], ],
), ),
], ],
@ -163,31 +184,25 @@ class Settings extends StatelessWidget {
ListTile( ListTile(
onTap: () => _launchUrl( onTap: () => _launchUrl(
'https://github.com/stonega/tsacdop/releases'), 'https://github.com/stonega/tsacdop/releases'),
contentPadding: contentPadding: EdgeInsets.symmetric(horizontal: 25.0),
EdgeInsets.symmetric(horizontal: 25.0),
leading: Icon(LineIcons.map_signs_solid), leading: Icon(LineIcons.map_signs_solid),
title: Text('Changelog'), title: Text('Changelog'),
subtitle: Text('List of chagnes'), subtitle: Text('List of chagnes'),
), ),
Divider(height: 2), Divider(height: 2),
ListTile( ListTile(
onTap: () => Navigator.push( onTap: () => Navigator.push(context,
context, MaterialPageRoute(builder: (context) => Libries())),
MaterialPageRoute( contentPadding: EdgeInsets.symmetric(horizontal: 25.0),
builder: (context) => Libries())),
contentPadding:
EdgeInsets.symmetric(horizontal: 25.0),
leading: Icon(LineIcons.book_open_solid), leading: Icon(LineIcons.book_open_solid),
title: Text('Libraries'), title: Text('Libraries'),
subtitle: subtitle: Text('Open source libraries in application'),
Text('Open source libraried in application'),
), ),
Divider(height: 2), Divider(height: 2),
ListTile( ListTile(
onTap: () => _launchUrl( onTap: () => _launchUrl(
'mailto:<xijieyin@gmail.com>?subject=Tsacdop Feedback'), 'mailto:<xijieyin@gmail.com>?subject=Tsacdop Feedback'),
contentPadding: contentPadding: EdgeInsets.symmetric(horizontal: 25.0),
EdgeInsets.symmetric(horizontal: 25.0),
leading: Icon(LineIcons.bug_solid), leading: Icon(LineIcons.bug_solid),
title: Text('Feedback'), title: Text('Feedback'),
subtitle: Text('Bugs and feature requests'), subtitle: Text('Bugs and feature requests'),

View File

@ -8,17 +8,19 @@ class StorageSetting extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AnnotatedRegion<SystemUiOverlayStyle>( return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle( value: SystemUiOverlayStyle(
statusBarIconBrightness: Theme.of(context).accentColorBrightness, statusBarIconBrightness: Theme.of(context).accentColorBrightness,
systemNavigationBarColor: Theme.of(context).primaryColor, systemNavigationBarColor: Theme.of(context).primaryColor,
statusBarColor: Theme.of(context).primaryColor), systemNavigationBarIconBrightness:
child: SafeArea( Theme.of(context).accentColorBrightness,
child: Scaffold( ),
appBar: AppBar( child: Scaffold(
title: Text('Storage'), appBar: AppBar(
elevation: 0, title: Text('Storage'),
backgroundColor: Theme.of(context).primaryColor, elevation: 0,
), backgroundColor: Theme.of(context).primaryColor,
body: Column( ),
body: SafeArea(
child: Column(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: <Widget>[

119
lib/settings/syncing.dart Normal file
View 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),
],
),
],
),
),
],
),
),
),
);
}
}

View File

@ -10,180 +10,174 @@ class ThemeSetting extends StatelessWidget {
var settings = Provider.of<SettingState>(context, listen: false); var settings = Provider.of<SettingState>(context, listen: false);
return AnnotatedRegion<SystemUiOverlayStyle>( return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle( value: SystemUiOverlayStyle(
statusBarIconBrightness: Theme.of(context).accentColorBrightness, statusBarIconBrightness: Theme.of(context).accentColorBrightness,
systemNavigationBarColor: Theme.of(context).primaryColor, systemNavigationBarColor: Theme.of(context).primaryColor,
statusBarColor: Theme.of(context).primaryColor), systemNavigationBarIconBrightness:
child: SafeArea( Theme.of(context).accentColorBrightness,
child: Scaffold( ),
appBar: AppBar( child: Scaffold(
title: Text('Appearance'), appBar: AppBar(
elevation: 0, title: Text('Appearance'),
backgroundColor: Theme.of(context).primaryColor, elevation: 0,
), backgroundColor: Theme.of(context).primaryColor,
body: Column( ),
mainAxisAlignment: MainAxisAlignment.start, body: Column(
mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[ mainAxisSize: MainAxisSize.min,
Column( children: <Widget>[
mainAxisAlignment: MainAxisAlignment.start, Column(
mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[ mainAxisSize: MainAxisSize.min,
Padding( children: <Widget>[
padding: EdgeInsets.all(10.0), Padding(
), padding: EdgeInsets.all(10.0),
Container( ),
height: 30.0, Container(
padding: EdgeInsets.symmetric(horizontal: 80), height: 30.0,
alignment: Alignment.centerLeft, padding: EdgeInsets.symmetric(horizontal: 80),
child: Text('Interface', alignment: Alignment.centerLeft,
style: Theme.of(context) child: Text('Interface',
.textTheme style: Theme.of(context)
.bodyText1 .textTheme
.copyWith(color: Theme.of(context).accentColor)), .bodyText1
), .copyWith(color: Theme.of(context).accentColor)),
ListView( ),
shrinkWrap: true, ListView(
scrollDirection: Axis.vertical, shrinkWrap: true,
children: <Widget>[ scrollDirection: Axis.vertical,
ListTile( children: <Widget>[
onTap: () => showGeneralDialog( ListTile(
context: context, onTap: () => showGeneralDialog(
barrierDismissible: true, context: context,
barrierLabel: MaterialLocalizations.of(context) barrierDismissible: true,
.modalBarrierDismissLabel, barrierLabel: MaterialLocalizations.of(context)
barrierColor: Colors.black54, .modalBarrierDismissLabel,
transitionDuration: barrierColor: Colors.black54,
const Duration(milliseconds: 200), transitionDuration: const Duration(milliseconds: 200),
pageBuilder: (BuildContext context, pageBuilder: (BuildContext context,
Animation animaiton, Animation animaiton,
Animation secondaryAnimation) => Animation secondaryAnimation) =>
AnnotatedRegion<SystemUiOverlayStyle>( AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle( value: SystemUiOverlayStyle(
statusBarIconBrightness: Brightness.light, statusBarIconBrightness: Brightness.light,
systemNavigationBarColor: // systemNavigationBarColor:
Theme.of(context).brightness == // Theme.of(context).brightness ==
Brightness.light // Brightness.light
? Color.fromRGBO(113, 113, 113, 1) // ? Color.fromRGBO(113, 113, 113, 1)
: Color.fromRGBO(15, 15, 15, 1), // : Color.fromRGBO(15, 15, 15, 1),
statusBarColor: // statusBarColor:
Theme.of(context).brightness == // Theme.of(context).brightness ==
Brightness.light // Brightness.light
? Color.fromRGBO(113, 113, 113, 1) // ? Color.fromRGBO(113, 113, 113, 1)
: Color.fromRGBO(5, 5, 5, 1), // : Color.fromRGBO(5, 5, 5, 1),
), ),
child: SafeArea( child: AlertDialog(
child: AlertDialog( titlePadding: EdgeInsets.only(
titlePadding: EdgeInsets.only( top: 20,
top: 20, left: 40,
left: 40, right: 200,
right: 200, ),
), elevation: 1,
elevation: 1, shape: RoundedRectangleBorder(
shape: RoundedRectangleBorder( borderRadius: BorderRadius.all(
borderRadius: BorderRadius.all( Radius.circular(10.0))),
Radius.circular(10.0))), title: Text('Theme'),
title: Text('Theme'), content: Column(
content: Column( mainAxisSize: MainAxisSize.min,
mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.start,
mainAxisAlignment: children: <Widget>[
MainAxisAlignment.start, RadioListTile(
children: <Widget>[ title: Container(
RadioListTile( padding:
title: Container( EdgeInsets.only(right: 80),
padding: EdgeInsets.only( child: Text('System default')),
right: 80), value: ThemeMode.system,
child: groupValue: settings.theme,
Text('System default')), onChanged: (value) {
value: ThemeMode.system, settings.setTheme = value;
groupValue: settings.theme, Navigator.of(context).pop();
onChanged: (value) { }),
settings.setTheme = value; RadioListTile(
Navigator.of(context).pop(); title: Text('Dark mode'),
}), value: ThemeMode.dark,
RadioListTile( groupValue: settings.theme,
title: Text('Dark mode'), onChanged: (value) {
value: ThemeMode.dark, settings.setTheme = value;
groupValue: settings.theme, Navigator.of(context).pop();
onChanged: (value) { }),
settings.setTheme = value; RadioListTile(
Navigator.of(context).pop(); title: Text('Light mode'),
}), value: ThemeMode.light,
RadioListTile( groupValue: settings.theme,
title: Text('Light mode'), onChanged: (value) {
value: ThemeMode.light, settings.setTheme = value;
groupValue: settings.theme, Navigator.of(context).pop();
onChanged: (value) { }),
settings.setTheme = value; ],
Navigator.of(context).pop(); ),
}), ),
], )),
), contentPadding: EdgeInsets.symmetric(horizontal: 80.0),
// leading: Icon(Icons.colorize),
title: Text('Theme'),
subtitle: Text('System default'),
),
Divider(height: 2),
ListTile(
onTap: () => showGeneralDialog(
context: context,
barrierDismissible: true,
barrierLabel: MaterialLocalizations.of(context)
.modalBarrierDismissLabel,
barrierColor: Colors.black54,
transitionDuration: const Duration(milliseconds: 200),
pageBuilder: (BuildContext context,
Animation animaiton,
Animation secondaryAnimation) =>
AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle(
statusBarIconBrightness: Brightness.light,
// systemNavigationBarColor:
// Theme.of(context).brightness ==
// Brightness.light
// ? Color.fromRGBO(113, 113, 113, 1)
// : Color.fromRGBO(15, 15, 15, 1),
// statusBarColor:
// Theme.of(context).brightness ==
// Brightness.light
// ? Color.fromRGBO(113, 113, 113, 1)
// : Color.fromRGBO(5, 5, 5, 1),
),
child: AlertDialog(
elevation: 1,
titlePadding: EdgeInsets.only(
top: 20,
left: 40,
right: 200,
bottom: 20),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(10.0))),
title: Text('Choose a color'),
content: SingleChildScrollView(
child: MaterialPicker(
onColorChanged: (value) {
settings.setAccentColor = value;
},
pickerColor: Colors.blue,
), ),
))),
contentPadding: EdgeInsets.symmetric(horizontal: 80.0),
// leading: Icon(Icons.colorize),
title: Text('Theme'),
subtitle: Text('System default'),
),
Divider(height: 2),
ListTile(
onTap: () => showGeneralDialog(
context: context,
barrierDismissible: true,
barrierLabel: MaterialLocalizations.of(context)
.modalBarrierDismissLabel,
barrierColor: Colors.black54,
transitionDuration:
const Duration(milliseconds: 200),
pageBuilder: (BuildContext context,
Animation animaiton,
Animation secondaryAnimation) =>
AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle(
statusBarIconBrightness: Brightness.light,
systemNavigationBarColor:
Theme.of(context).brightness ==
Brightness.light
? Color.fromRGBO(113, 113, 113, 1)
: Color.fromRGBO(15, 15, 15, 1),
statusBarColor:
Theme.of(context).brightness ==
Brightness.light
? Color.fromRGBO(113, 113, 113, 1)
: Color.fromRGBO(5, 5, 5, 1),
), ),
child: SafeArea( ))),
child: AlertDialog( contentPadding: EdgeInsets.symmetric(horizontal: 80.0),
elevation: 1, title: Text('Accent color'),
titlePadding: EdgeInsets.only( subtitle: Text('Include the overlay color'),
top: 20, ),
left: 40, Divider(height: 2),
right: 200, ],
bottom: 20), ),
shape: RoundedRectangleBorder( ],
borderRadius: BorderRadius.all( ),
Radius.circular(10.0))), ],
title: Text('Choose a color'),
content: SingleChildScrollView(
child: MaterialPicker(
onColorChanged: (value) {
settings.setAccentColor = value;
},
pickerColor: Colors.blue,
),
),
)))),
contentPadding: EdgeInsets.symmetric(horizontal: 80.0),
title: Text('Accent color'),
subtitle: Text('Include the overlay color'),
),
Divider(height: 2),
],
),
],
),
],
),
), ),
), ),
); );

View 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,
);
}
}

View File

@ -21,19 +21,21 @@ class EpisodeGrid extends StatelessWidget {
final bool showDownload; final bool showDownload;
final bool showNumber; final bool showNumber;
final String heroTag; final String heroTag;
final int updateCount;
EpisodeGrid( EpisodeGrid(
{Key key, {Key key,
this.podcast, this.podcast,
this.showDownload, this.showDownload,
this.showFavorite, this.showFavorite,
this.showNumber, this.showNumber,
this.heroTag}) this.heroTag,
this.updateCount = 0})
: super(key: key); : super(key: key);
Offset offset;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
double _width = MediaQuery.of(context).size.width; double _width = MediaQuery.of(context).size.width;
Offset _offset;
_showPopupMenu(Offset offset, EpisodeBrief episode, BuildContext context, _showPopupMenu(Offset offset, EpisodeBrief episode, BuildContext context,
bool isPlaying, bool isInPlaylist) async { bool isPlaying, bool isInPlaylist) async {
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false); var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
@ -83,7 +85,7 @@ class EpisodeGrid extends StatelessWidget {
if (value == 0) { if (value == 0) {
if (!isPlaying) audio.episodeLoad(episode); if (!isPlaying) audio.episodeLoad(episode);
} else if (value == 1) { } else if (value == 1) {
if (isInPlaylist) { if (!isInPlaylist) {
audio.addToPlaylist(episode); audio.addToPlaylist(episode);
Fluttertoast.showToast( Fluttertoast.showToast(
msg: 'Added to playlist', msg: 'Added to playlist',
@ -134,10 +136,10 @@ class EpisodeGrid extends StatelessWidget {
color: Colors.transparent, color: Colors.transparent,
child: InkWell( child: InkWell(
borderRadius: BorderRadius.all(Radius.circular(5.0)), borderRadius: BorderRadius.all(Radius.circular(5.0)),
onTapDown: (details) => offset = Offset( onTapDown: (details) => _offset = Offset(
details.globalPosition.dx, details.globalPosition.dy), details.globalPosition.dx, details.globalPosition.dy),
onLongPress: () => _showPopupMenu( onLongPress: () => _showPopupMenu(
offset, _offset,
podcast[index], podcast[index],
context, context,
data.item1 == podcast[index], data.item1 == podcast[index],
@ -185,6 +187,7 @@ class EpisodeGrid extends StatelessWidget {
), ),
), ),
Spacer(), Spacer(),
index < updateCount ? Text('New', style: TextStyle(color: Colors.red, fontStyle: FontStyle.italic)) : Center(),
showNumber showNumber
? Container( ? Container(
alignment: Alignment.topRight, alignment: Alignment.topRight,

567
lib/util/mypopupmenu.dart Normal file
View 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
View 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();
}

View File

@ -18,9 +18,9 @@ class RssContent {
factory RssContent.parse(XmlElement element) { factory RssContent.parse(XmlElement element) {
if (element == null) { if (element == null) {
return null; return RssContent('', ['']);
} }
final content = element.text; final content = element.text.trim();
final images = <String>[]; final images = <String>[];
_imagesRegExp.allMatches(content).forEach((match) { _imagesRegExp.allMatches(content).forEach((match) {
images.add(match.group(1)); images.add(match.group(1));

View File

@ -44,20 +44,24 @@ class RssItem {
}); });
factory RssItem.parse(XmlElement element) { factory RssItem.parse(XmlElement element) {
if (RssEnclosure.parse(findElementOrNull(element, "enclosure")) == null) {
return null;
}
return RssItem( return RssItem(
title: findElementOrNull(element, "title")?.text, title: findElementOrNull(element, "title")?.text,
description: findElementOrNull(element, "description")?.text?.trim() ?? 'No shownote provided for this episode', description: findElementOrNull(element, "description")?.text?.trim()
,
link: findElementOrNull(element, "link")?.text?.trim(), link: findElementOrNull(element, "link")?.text?.trim(),
categories: element.findElements("category").map((element) { categories: element.findElements("category").map((element) {
return RssCategory.parse(element); return RssCategory.parse(element);
}).toList(), }).toList(),
//guid: findElementOrNull(element, "guid")?.text, //guid: findElementOrNull(element, "guid")?.text,
pubDate: findElementOrNull(element, "pubDate")?.text?.trim(), pubDate: findElementOrNull(element, "pubDate")?.text?.trim(),
author: findElementOrNull(element, "author")?.text?.trim(), author: findElementOrNull(element, "author")?.text?.trim(),
// comments: findElementOrNull(element, "comments")?.text, // comments: findElementOrNull(element, "comments")?.text,
// source: RssSource.parse(findElementOrNull(element, "source")), // source: RssSource.parse(findElementOrNull(element, "source")),
// content: RssContent.parse(findElementOrNull(element, "content:encoded")), content: RssContent.parse(findElementOrNull(element, "content:encoded")),
// media: Media.parse(element), // media: Media.parse(element),
enclosure: RssEnclosure.parse(findElementOrNull(element, "enclosure")), enclosure: RssEnclosure.parse(findElementOrNull(element, "enclosure")),
//dc: DublinCore.parse(element), //dc: DublinCore.parse(element),
itunes: RssItemItunes.parse(element), itunes: RssItemItunes.parse(element),

View File

@ -11,7 +11,7 @@ description: An easy-use podacasts player.
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at # Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 0.1.2 version: 0.1.4
environment: environment:
sdk: ">=2.6.0 <3.0.0" sdk: ">=2.6.0 <3.0.0"
@ -58,6 +58,7 @@ dev_dependencies:
line_icons: line_icons:
git: git:
url: https://github.com/galonsos/line_icons.git url: https://github.com/galonsos/line_icons.git
flutter_file_dialog: ^0.0.5
# For information on the generic Dart part of this file, see the # For information on the generic Dart part of this file, see the

View File

@ -5,7 +5,6 @@
// gestures. You can also use WidgetTester to find child widgets in the widget // gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct. // tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:tsacdop/main.dart'; import 'package:tsacdop/main.dart';

11
tool/env.dart Normal file
View 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)};');
}