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 'export KEYSTORE=${HOME}/keystore.jks' >> $BASH_ENV
- run: dart tool/env.dart
- run:
name: Build the Android version
command: flutter build apk --split-per-abi --no-shrink

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"/>
</br>
<img src="https://raw.githubusercontent.com/stonega/tsacdop/master/android/app/src/main/res/mipmap-xhdpi/text.png" art = "Tsacdop"/>
t p</p>
</p>
Enjoy podcasts with Tsacdop.

View File

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

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

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
android:gravity="center"
android:tileMode="disabled"
android:src="@mipmap/ic_launcher" />
android:src="@mipmap/ic_splash" />
</item>
<item android:bottom="100dp">
<bitmap

View File

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

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">
#121212
</color>
<color name="ic_launcher_background">
#ffffff
</color>
</resources>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

567
lib/util/mypopupmenu.dart Normal file
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) {
if (element == null) {
return null;
return RssContent('', ['']);
}
final content = element.text;
final content = element.text.trim();
final images = <String>[];
_imagesRegExp.allMatches(content).forEach((match) {
images.add(match.group(1));

View File

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

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.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 0.1.2
version: 0.1.4
environment:
sdk: ">=2.6.0 <3.0.0"
@ -58,6 +58,7 @@ dev_dependencies:
line_icons:
git:
url: https://github.com/galonsos/line_icons.git
flutter_file_dialog: ^0.0.5
# For information on the generic Dart part of this file, see the

View File

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

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