modified: .circleci/config.yml

modified:   android/app/build.gradle
	modified:   android/app/src/main/AndroidManifest.xml
	new file:   android/app/src/main/res/drawable/launch_background_night.xml
	new file:   android/app/src/main/res/drawable/normal_background.xml
	new file:   android/app/src/main/res/values-night/styles.xml
	modified:   android/app/src/main/res/values/styles.xml
	new file:   assets/fireside.jpg
	new file:   assets/logo.png
	modified:   lib/class/audiostate.dart
	modified:   lib/class/fireside_data.dart
	modified:   lib/class/podcastlocal.dart
	modified:   lib/class/settingstate.dart
	modified:   lib/episodes/episodedetail.dart
	modified:   lib/episodes/episodedownload.dart
modified:   lib/home/appbar/about.dart
	modified:   lib/home/appbar/addpodcast.dart
	modified:   lib/home/appbar/importompl.dart
	modified:   lib/home/appbar/popupmenu.dart
	modified:   lib/home/audiopanel.dart
	modified:   lib/home/audioplayer.dart
	modified:   lib/home/homescroll.dart
	modified:   lib/home/hometab.dart
	new file:   lib/home/paly_history.dart
	modified:   lib/local_storage/key_value_storage.dart
	modified:   lib/local_storage/sqflite_localpodcast.dart
	modified:   lib/main.dart
	modified:   lib/podcasts/podcastdetail.dart
	modified:   lib/podcasts/podcastgroup.dart
	modified:   lib/podcasts/podcastlist.dart
	modified:   lib/podcasts/podcastmanage.dart
	new file:   lib/settings/settting.dart
	new file:   lib/settings/theme.dart
	new file:   lib/util/colorize.dart
	modified:   lib/util/episodegrid.dart
	modified:   pubspec.lock
	modified:   pubspec.yaml
This commit is contained in:
stonegate 2020-03-01 20:17:06 +08:00
parent 1021f2eea4
commit a6fc34e7bb
38 changed files with 2063 additions and 969 deletions

View File

@ -13,7 +13,13 @@ jobs:
- run:
name: Run Flutter doctor
command: flutter doctor
- run:
name: flutter pub get
command: flutter pub get
-run:
name: flutter run
command: flutter run
- run: echo $ENCODED_KEYSTORE | base64 -di > ${HOME}/keystore.jks
- run: echo 'export KEYSTORE=${HOME}/keystore.jks' >> $BASH_ENV

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"java.configuration.updateBuildConfiguration": "automatic"
}

View File

@ -54,10 +54,10 @@ android {
signingConfigs {
release {
storeFile file(System.getenv("KEYSTORE") ?: "keystore.jks")
storePassword System.getenv("KEYSTORE_PASSWORD")
keyAlias System.getenv("KEY_ALIAS")
keyPassword System.getenv("KEY_PASSWORD")
storeFile file(System.getenv("KEYSTORE") ?: "keystore.jks")
storePassword System.getenv("KEYSTORE_PASSWORD")
keyAlias System.getenv("KEY_ALIAS")
keyPassword System.getenv("KEY_PASSWORD")
// keyAlias keystoreProperties['keyAlias']
// keyPassword keystoreProperties['keyPassword']
// storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null

View File

@ -12,6 +12,7 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data android:name="io.flutter.embedding.android.SplashScreenUntilFirstFrame" android:value="true" />
</activity>
<service android:name="com.google.flutter.plugins.audiofileplayer.AudiofileplayerService">
<intent-filter>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/black" />
<!-- You can insert your own image assets here -->
<item>
<bitmap
android:gravity="center"
android:tileMode="disabled"
android:src="@mipmap/ic_launcher" />
</item>
<item android:bottom="100dp">
<bitmap
android:gravity="bottom"
android:src="@mipmap/text" />
</item>
</layer-list>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<item>
<bitmap
android:gravity="center"
android:tileMode="disabled"
android:src="@mipmap/ic_launcher" />
</item>
<item android:bottom="100dp">
<bitmap
android:gravity="bottom"
android:src="@mipmap/text" />
</item>
</layer-list>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">@drawable/launch_background_night</item>
</style>
</resources>

View File

@ -1,8 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
Flutter draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">@drawable/normal_background</item>
</style>
</resources>

BIN
assets/fireside.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -14,11 +14,18 @@ import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
enum AudioState { load, play, pause, complete, error, stop }
class PlayHistory {
DBHelper dbHelper = DBHelper();
String title;
String url;
double seconds;
double seekValue;
PlayHistory(this.title, this.url, this.seconds, this.seekValue);
EpisodeBrief _episode;
EpisodeBrief get episode => _episode;
getEpisode() async {
_episode = await dbHelper.getRssItemWithUrl(url);
}
}
class Playlist {
@ -30,13 +37,13 @@ class Playlist {
KeyValueStorage storage = KeyValueStorage('playlist');
Playlist(this.name, {List<String> urls}) : urls = urls ?? [];
getPlaylist() async{
getPlaylist() async {
List<String> _urls = await storage.getStringList();
if (_urls.length == 0) {
_playlist = [];
} else {
_playlist = [];
await Future.forEach(_urls, (url) async{
await Future.forEach(_urls, (url) async {
EpisodeBrief episode = await dbHelper.getRssItemWithUrl(url);
print(episode.title);
_playlist.add(episode);
@ -57,9 +64,10 @@ class Playlist {
await savePlaylist();
}
delFromPlaylist(EpisodeBrief episodeBrief) {
_playlist.remove(episodeBrief);
savePlaylist();
delFromPlaylist(EpisodeBrief episodeBrief) async {
_playlist
.removeWhere((item) => item.enclosureUrl == episodeBrief.enclosureUrl);
await savePlaylist();
}
}
@ -71,6 +79,7 @@ class AudioPlayer extends ChangeNotifier {
static const String forwardButtonId = 'forwardButtonId';
DBHelper dbHelper = DBHelper();
KeyValueStorage storage = KeyValueStorage('audioposition');
EpisodeBrief _episode;
Playlist _queue = Playlist('now');
bool _playerRunning = false;
@ -90,34 +99,43 @@ class AudioPlayer extends ChangeNotifier {
double get seekSliderValue => _seekSliderValue;
String get remoteErrorMessage => _remoteErrorMessage;
bool get playerRunning => _playerRunning;
int _lastPostion;
int get lastPositin => _lastPostion;
Playlist get queue => _queue;
AudioState _audioState = AudioState.stop;
AudioState get audioState => _audioState;
EpisodeBrief get episode => _episode;
@override
void addListener(VoidCallback listener) {
super.addListener(listener);
_queue.getPlaylist();
loadPlaylist() async {
await _queue.getPlaylist();
_lastPostion = await storage.getInt();
print(_lastPostion);
}
episodeLoad(EpisodeBrief episode) async {
if (_playerRunning && _episode != null) {
PlayHistory history = PlayHistory(_episode.title, _episode.enclosureUrl,
backgroundAudioDuration, seekSliderValue);
await dbHelper.saveHistory(history);
}
AudioSystem.instance.addMediaEventListener(_mediaEventListener);
_backgroundAudioPlaying = false;
_episode = episode;
await _queue.getPlaylist();
if (_queue.playlist.contains(_episode)) {
_queue.playlist.remove(_episode);
_queue.playlist.insert(0, _episode);
} else {
_queue.playlist.insert(0, _episode);
}
_queue.playlist
.removeWhere((item) => item.enclosureUrl == _episode.enclosureUrl);
_queue.playlist.insert(0, _episode);
await _queue.savePlaylist();
await _play(_episode);
_playerRunning = true;
_backgroundAudioPlaying = true;
notifyListeners();
}
playlistLoad() async {
_backgroundAudioPlaying = false;
await _queue.getPlaylist();
_episode = _queue.playlist.first;
await _play(_episode);
_playerRunning = true;
notifyListeners();
}
@ -127,10 +145,7 @@ class AudioPlayer extends ChangeNotifier {
await dbHelper.saveHistory(history);
await _queue.delFromPlaylist(_episode);
if (_queue.playlist.length > 0) {
_episode = _queue.playlist.first;
_play(_episode);
_backgroundAudioPlaying = true;
notifyListeners();
playlistLoad();
} else {
_backgroundAudioPlaying = false;
_remoteAudioLoading = false;
@ -145,19 +160,22 @@ class AudioPlayer extends ChangeNotifier {
notifyListeners();
}
delFromPlaylist(EpisodeBrief episode) async {
_queue.delFromPlaylist(episode);
await _queue.getPlaylist();
notifyListeners();
}
pauseAduio() async {
_pauseBackgroundAudio();
_audioState = AudioState.pause;
notifyListeners();
PlayHistory history = PlayHistory(_episode.title, _episode.enclosureUrl,
backgroundAudioDuration, seekSliderValue);
await dbHelper.saveHistory(history);
await _queue.delFromPlaylist(_episode);
}
resumeAudio() {
_resumeBackgroundAudio();
_audioState = AudioState.play;
notifyListeners();
}
@ -173,10 +191,13 @@ class AudioPlayer extends ChangeNotifier {
_backgroundAudio.seek(positionSeconds);
AudioSystem.instance.setPlaybackState(true, positionSeconds);
}
disopse() {
@override
dispose() {
pauseAduio();
AudioSystem.instance.removeMediaEventListener(_mediaEventListener);
_backgroundAudio?.dispose();
super.dispose();
}
_play(EpisodeBrief episodeBrief) async {
@ -229,6 +250,7 @@ class AudioPlayer extends ChangeNotifier {
_backgroundAudioPositionSeconds = _backgroundAudioDurationSeconds;
notifyListeners();
}
storage.saveInt(positionSeconds.toInt());
}, onError: (String message) {
_remoteErrorMessage = message;
_backgroundAudio.dispose();
@ -245,7 +267,7 @@ class AudioPlayer extends ChangeNotifier {
_remoteErrorMessage = null;
_remoteAudioLoading = true;
notifyListeners();
_backgroundAudio?.pause();
_backgroundAudio?.pause();
_backgroundAudioPositionSeconds = 0;
_setNotification(false);
_backgroundAudio =
@ -266,6 +288,7 @@ class AudioPlayer extends ChangeNotifier {
_backgroundAudioPositionSeconds = _backgroundAudioDurationSeconds;
notifyListeners();
}
storage.saveInt(positionSeconds.toInt());
}, onError: (String message) {
_remoteErrorMessage = message;
_backgroundAudio.dispose();
@ -390,7 +413,7 @@ class AudioPlayer extends ChangeNotifier {
}
void _pauseBackgroundAudio() {
_backgroundAudio.pause();
_backgroundAudio?.pause();
_backgroundAudioPlaying = false;
AudioSystem.instance
.setPlaybackState(false, _backgroundAudioPositionSeconds);
@ -421,7 +444,8 @@ class AudioPlayer extends ChangeNotifier {
void _forwardBackgroundAudio(double seconds) {
final double forwardposition = _backgroundAudioPositionSeconds + seconds;
_backgroundAudio.seek(forwardposition);
AudioSystem.instance.setPlaybackState(true, _backgroundAudioPositionSeconds);
AudioSystem.instance
.setPlaybackState(true, _backgroundAudioPositionSeconds);
}
final _pauseButton = AndroidCustomMediaButton(

View File

@ -33,7 +33,13 @@ class FiresideData {
String name = element.text.trim();
String image =
element.children.first.children.first.attributes.toString();
host = PodcastHost(name, reg.stringMatch(image));
print(reg.stringMatch(image));
host = PodcastHost(
name,
reg.stringMatch(image) ??
'http://xuanmei.us/assets/default/avatar_small-170afdc2be97fc6148b283083942d82c101d4c1061f6b28f87c8958b52664af9.jpg');
hosts.add(host);
});
List<String> data = [
@ -45,7 +51,7 @@ class FiresideData {
}
}
Future getData() async{
Future getData() async {
List<String> data = await dbHelper.getFiresideData(id);
_background = data[0];
_hosts = json

View File

@ -7,7 +7,6 @@ class PodcastLocal {
final String primaryColor;
final String id;
final String imagePath;
final String email;
final String provider;
final String link;
PodcastLocal(
@ -18,7 +17,6 @@ class PodcastLocal {
this.author,
this.id,
this.imagePath,
this.email,
this.provider,
this.link);
}

View File

@ -4,27 +4,64 @@ import 'package:flutter/material.dart';
import 'package:tsacdop/local_storage/key_value_storage.dart';
class SettingState extends ChangeNotifier {
KeyValueStorage storage = KeyValueStorage('themes');
int _theme;
KeyValueStorage themestorage = KeyValueStorage('themes');
KeyValueStorage accentstorage = KeyValueStorage('accents');
bool _isLoading;
bool get isLoagding => _isLoading;
int get theme => _theme;
setTheme(int theme) async{
_theme = theme;
Future initData() async {
await _getTheme();
await _getAccentSetColor();
}
ThemeMode _theme;
ThemeMode get theme => _theme;
set setTheme(ThemeMode mode) {
_theme = mode;
_saveTheme();
notifyListeners();
}
Color _accentSetColor;
Color get accentSetColor => _accentSetColor;
set setAccentColor(Color color) {
_accentSetColor = color;
_saveAccentSetColor();
notifyListeners();
await _saveTheme(theme);
}
@override
void addListener(VoidCallback listener) {
super.addListener(listener);
_getTheme();
_getAccentSetColor();
}
_getTheme() async {
_theme = await storage.getTheme();
int mode = await themestorage.getInt();
_theme = ThemeMode.values[mode];
}
_saveTheme(theme) async {
await storage.saveTheme(theme);
_saveTheme() async {
await themestorage.saveInt(_theme.index);
}
_getAccentSetColor() async {
String colorString = await accentstorage.getString();
print(colorString);
if (colorString.isNotEmpty) {
int color = int.parse('FF' + colorString.toUpperCase(), radix: 16);
_accentSetColor = Color(color).withOpacity(1.0);
print(_accentSetColor.toString());
} else {
_accentSetColor = Colors.blue[400];
}
}
_saveAccentSetColor() async {
await accentstorage
.saveString(_accentSetColor.toString().substring(10, 16));
print(_accentSetColor.toString());
}
}

View File

@ -28,6 +28,7 @@ class _EpisodeDetailState extends State<EpisodeDetail> {
final textstyle = TextStyle(fontSize: 15.0, color: Colors.black);
double downloadProgress;
bool _loaddes;
bool _showMenu;
String path;
Future getSDescription(String url) async {
var dbHelper = DBHelper();
@ -38,6 +39,18 @@ class _EpisodeDetailState extends State<EpisodeDetail> {
});
}
ScrollController _controller;
_scrollListener() {
if (_controller.offset > _controller.position.maxScrollExtent * 0.8) {
setState(() {
_showMenu = true;
});
} else
setState(() {
_showMenu = false;
});
}
_launchUrl(String url) async {
if (await canLaunch(url)) {
await launch(url);
@ -50,7 +63,16 @@ class _EpisodeDetailState extends State<EpisodeDetail> {
void initState() {
super.initState();
_loaddes = false;
_showMenu = false;
getSDescription(widget.episodeItem.enclosureUrl);
_controller = ScrollController();
_controller.addListener(_scrollListener);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
@ -119,38 +141,44 @@ class _EpisodeDetailState extends State<EpisodeDetail> {
style:
TextStyle(color: Colors.white)))
: Center(),
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),
),
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),
),
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(),
],
),
),
@ -162,6 +190,7 @@ class _EpisodeDetailState extends State<EpisodeDetail> {
padding:
EdgeInsets.only(left: 12.0, right: 12.0, top: 5.0),
child: SingleChildScrollView(
controller: _controller,
child: _loaddes
? (widget.episodeItem.description.contains('<'))
? Html(
@ -187,11 +216,18 @@ class _EpisodeDetailState extends State<EpisodeDetail> {
builder: (_, data, __) {
return Container(
alignment: Alignment.bottomCenter,
padding: EdgeInsets.only(
bottom: data == true ? 60.0 : 10.0),
child: MenuBar(
episodeItem: widget.episodeItem,
heroTag: widget.heroTag,
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,
),
),
),
);
}),
@ -264,7 +300,6 @@ class _MenuBarState extends State<MenuBar> {
? Colors.grey[200]
: Theme.of(context).primaryColor,
),
// borderRadius: BorderRadius.all(Radius.circular(10.0)),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
@ -274,14 +309,13 @@ class _MenuBarState extends State<MenuBar> {
tag: widget.episodeItem.enclosureUrl + widget.heroTag,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 10.0),
child: ClipRRect(
borderRadius: BorderRadius.all(Radius.circular(15.0)),
child: Container(
height: 30.0,
width: 30.0,
color: Theme.of(context).scaffoldBackgroundColor,
child: Image.file(File("${widget.episodeItem.imagePath}")),
),
child: Container(
height: 30.0,
width: 30.0,
color: Theme.of(context).scaffoldBackgroundColor,
child: CircleAvatar(
backgroundImage:
FileImage(File("${widget.episodeItem.imagePath}"))),
),
),
),
@ -313,13 +347,14 @@ class _MenuBarState extends State<MenuBar> {
),
DownloadButton(episodeBrief: widget.episodeItem),
Selector<AudioPlayer, List<String>>(
selector: (_, audio) => audio.queue.playlist.map((e)=>e.enclosureUrl).toList(),
selector: (_, audio) =>
audio.queue.playlist.map((e) => e.enclosureUrl).toList(),
builder: (_, data, __) {
print(data.length);
return data.contains(widget.episodeItem.enclosureUrl)
? _buttonOnMenu(
Icon(Icons.playlist_add_check, color: Theme.of(context).accentColor),
(){})
Icon(Icons.playlist_add_check,
color: Theme.of(context).accentColor),
() {})
: _buttonOnMenu(
Icon(Icons.playlist_add, color: Colors.grey[700]), () {
Fluttertoast.showToast(
@ -340,9 +375,6 @@ class _MenuBarState extends State<MenuBar> {
? Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.only(
topRight: Radius.circular(5.0),
bottomRight: Radius.circular(5.0)),
onTap: () {
audio.episodeLoad(widget.episodeItem);
},

View File

@ -211,14 +211,17 @@ class _DownloadButtonState extends State<DownloadButton> {
AnimatedContainer(
duration: Duration(seconds: 1),
decoration: BoxDecoration(
color: Colors.cyan[300],
color: Theme.of(context).accentColor,
borderRadius: BorderRadius.all(Radius.circular(15.0))),
height: 20.0,
width:
(_task.status == DownloadTaskStatus.running) ? 50.0 : 0,
alignment: Alignment.center,
child: Text('${_task.progress}%',
style: TextStyle(color: Colors.white))),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Text('${_task.progress}%',
style: TextStyle(color: Colors.white)),
)),
],
);
}
@ -236,7 +239,7 @@ class _DownloadButtonState extends State<DownloadButton> {
color: Colors.transparent,
child: InkWell(
onTap: () {
if(task.progress > 0) _pauseDownload(task);
if (task.progress > 0) _pauseDownload(task);
},
child: Container(
height: 50.0,
@ -248,7 +251,8 @@ class _DownloadButtonState extends State<DownloadButton> {
child: CircularProgressIndicator(
backgroundColor: Colors.grey[500],
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.cyan[300]),
valueColor: AlwaysStoppedAnimation<Color>(
Theme.of(context).accentColor),
value: task.progress / 100,
),
),
@ -283,7 +287,7 @@ class _DownloadButtonState extends State<DownloadButton> {
return _buttonOnMenu(
Icon(
Icons.done_all,
color: Colors.blue,
color: Theme.of(context).accentColor,
),
() => _deleteDownload(task));
} else if (task.status == DownloadTaskStatus.failed) {

View File

@ -1,35 +1,161 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
class AboutApp extends StatelessWidget {
TextSpan buildTextSpan() {
return TextSpan(children: [
TextSpan(text: 'Tsacdop\n', style: TextStyle(fontSize: 20)),
TextSpan(
text:
'Tsacdop is a podcast client developed by flutter, is a simple, easy-use player.\n'),
TextSpan(text: 'Github https://github.com/stonga/tsacdop .\n'),
]);
_launchUrl(String url) async {
if (await canLaunch(url)) {
await launch(url);
} else {
throw 'Could not launch $url';
}
}
Widget _listItem(
BuildContext context, String text, IconData icons, String url) =>
InkWell(
onTap: () => _launchUrl(url),
child: Container(
height: 50.0,
padding: EdgeInsets.symmetric(horizontal: 20.0),
alignment: Alignment.centerLeft,
decoration: BoxDecoration(
border: Border(
bottom: Divider.createBorderSide(context),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Icon(icons, color: Colors.grey[700]),
Padding(
padding: EdgeInsets.symmetric(horizontal: 10),
),
Text(text),
],
),
),
);
@override
Widget build(BuildContext context) {
return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle(
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
systemNavigationBarColor: Theme.of(context).primaryColor,
statusBarColor: Theme.of(context).primaryColor,
statusBarColor: Theme.of(context).primaryColor,
),
child: SafeArea(
child: Scaffold(
child: Scaffold(
backgroundColor: Theme.of(context).primaryColor,
appBar: AppBar(
title: Text('Tsacdop'),
centerTitle: true,
title: Text('About'),
),
body: Container(
padding: EdgeInsets.all(20),
alignment: Alignment.topLeft,
child: Text.rich(buildTextSpan()),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(
height: 200.0,
alignment: Alignment.center,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Image(
image: AssetImage('assets/logo.png'),
height: 80,
),
Text('Version: 0.1.1'),
],
),
),
Container(
padding: EdgeInsets.symmetric(horizontal: 50),
height: 50,
child: Text(
'Tsacdop is a podcast client developed with flutter, a simple, beautiful, and easy-use player.',
textAlign: TextAlign.center,
),
),
Padding(
padding: EdgeInsets.all(5.0),
),
Container(
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),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(
padding: EdgeInsets.symmetric(horizontal: 20.0),
alignment: Alignment.centerLeft,
child: Text(
'Developer',
style:
TextStyle(color: Theme.of(context).accentColor),
),
),
_listItem(
context,
'GitHub',
FontAwesomeIcons.githubSquare,
'https://github.com/stonaga/tsacdop'),
_listItem(
context,
'Twitter',
FontAwesomeIcons.twitterSquare,
'https://twitter.com'),
_listItem(
context,
'Gmail',
FontAwesomeIcons.envelopeSquare,
'mailto:<xijieyin@gmail.com>?subject=Tsacdop Feedback'),
],
),
),
Spacer(),
Container(
height: 50,
alignment: Alignment.center,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Image.asset(
'assets/text.png',
height: 25,
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 5),
),
Icon(
Icons.favorite,
color: Colors.blue,
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 5),
),
FlutterLogo(
size: 18,
),
],
),
),
],
),
)),
),
);

View File

@ -10,8 +10,10 @@ import 'package:provider/provider.dart';
import 'package:path_provider/path_provider.dart';
import 'package:image/image.dart' as img;
import 'package:fluttertoast/fluttertoast.dart';
import 'package:tsacdop/class/audiostate.dart';
import 'package:tsacdop/class/fireside_data.dart';
import 'package:uuid/uuid.dart';
import 'package:tuple/tuple.dart';
import 'package:tsacdop/class/importompl.dart';
import 'package:tsacdop/class/podcast_group.dart';
@ -30,9 +32,30 @@ class MyHomePage extends StatefulWidget {
class _MyHomePageState extends State<MyHomePage> {
final _MyHomePageDelegate _delegate = _MyHomePageDelegate();
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
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<AudioPlayer>(context, listen: false).loadPlaylist();
setState(() {
_loadPlay = true;
});
}
@override
Widget build(BuildContext context) {
var audio = Provider.of<AudioPlayer>(context, listen: false);
return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle(
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
@ -62,6 +85,37 @@ class _MyHomePageState extends State<MyHomePage> {
PopupMenu(),
],
),
floatingActionButton: Selector<AudioPlayer, 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()
: FloatingActionButton.extended(
tooltip: 'Play from playlist',
highlightElevation: 2,
hoverElevation: 2,
onPressed: () => audio.playlistLoad(),
elevation: 1,
backgroundColor: Theme.of(context).accentColor,
label: Text(_stringForSeconds(data.item3), style: TextStyle(color: Colors.white)),
icon: Stack(
alignment: Alignment.center,
children: <Widget>[
CircleAvatar(
radius: 15,
backgroundImage: FileImage(File(
"${data.item2.playlist.first.imagePath}")),
),
Container(
height: 30.0,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.black38),
),
],
)),
),
body: Home(),
),
),
@ -235,9 +289,10 @@ class _SearchResultState extends State<SearchResult> {
try {
Response response = await Dio().get(rss);
var dbHelper = DBHelper();
String _realUrl =
response.isRedirect ? response.realUri.toString() : rss;
String _realUrl = response.realUri.toString();
bool _checkUrl = await dbHelper.checkPodcast(_realUrl);
if (_checkUrl) {
if (mounted) setState(() => _issubscribe = true);
@ -261,7 +316,6 @@ class _SearchResultState extends State<SearchResult> {
String _imagePath = "${dir.path}/$_uuid.png";
String _primaryColor = await getColor(File("${dir.path}/$_uuid.png"));
String _author = _p.itunes.author ?? _p.author ?? '';
String _email = _p.itunes.owner?.email ?? '';
String _provider = _p.generator ?? '';
String _link = _p.link ?? '';
PodcastLocal podcastLocal = PodcastLocal(
@ -272,13 +326,12 @@ class _SearchResultState extends State<SearchResult> {
_author,
_uuid,
_imagePath,
_email,
_provider,
_link);
podcastLocal.description = _p.description;
await groupList.subscribe(podcastLocal);
if(_provider.contains('fireside'))
{
if (_provider.contains('fireside')) {
FiresideData data = FiresideData(_uuid, _link);
await data.fatchData();
}
@ -386,7 +439,10 @@ class _SearchResultState extends State<SearchResult> {
widget.onlinePodcast.description.trim(),
maxLines: 3,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodyText1.copyWith(color: Colors.white),
style: Theme.of(context)
.textTheme
.bodyText1
.copyWith(color: Colors.white),
),
)
: Center(),

View File

@ -6,7 +6,7 @@ class Import extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Consumer<ImportOmpl>(
builder: (context, importOmpl, _) => Container(
builder: (_, importOmpl, __) => Container(
color: Theme.of(context).primaryColorDark,
child: importOmpl.importState == ImportState.start
? Column(

View File

@ -4,8 +4,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:dio/dio.dart';
import 'package:provider/provider.dart';
import 'package:tsacdop/class/podcast_group.dart';
import 'package:tsacdop/class/settingstate.dart';
import 'package:tsacdop/class/fireside_data.dart';
import 'package:xml/xml.dart' as xml;
import 'package:file_picker/file_picker.dart';
import 'package:flutter/services.dart';
@ -15,6 +14,8 @@ import 'package:image/image.dart' as img;
import 'package:uuid/uuid.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:tsacdop/class/podcast_group.dart';
import 'package:tsacdop/settings/settting.dart';
import 'about.dart';
import 'package:tsacdop/class/podcastlocal.dart';
import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
@ -48,7 +49,6 @@ class PopupMenu extends StatelessWidget {
Widget build(BuildContext context) {
ImportOmpl importOmpl = Provider.of<ImportOmpl>(context, listen: false);
GroupList groupList = Provider.of<GroupList>(context, listen: false);
SettingState setting = Provider.of<SettingState>(context);
_refreshAll() async {
var dbHelper = DBHelper();
List<PodcastLocal> podcastList = await dbHelper.getPodcastLocalAll();
@ -68,28 +68,29 @@ class PopupMenu extends StatelessWidget {
Response response = await Dio().get(rss);
if (response.statusCode == 200) {
var _p = RssFeed.parse(response.data);
var dir = await getApplicationDocumentsDirectory();
Response<List<int>> imageResponse = await Dio().get<List<int>>(
_p.itunes.image.href,
options: Options(responseType: ResponseType.bytes));
img.Image image = img.decodeImage(imageResponse.data);
img.Image thumbnail = img.copyResize(image, width: 300);
String _uuid = Uuid().v4();
String _realUrl =
response.isRedirect ? response.realUri.toString() : rss;
String _realUrl = response.redirects.isEmpty ? rss : response.realUri.toString();
print(_realUrl);
bool _checkUrl = await dbHelper.checkPodcast(_realUrl);
if (_checkUrl) {
Response<List<int>> imageResponse = await Dio().get<List<int>>(
_p.itunes.image.href,
options: Options(responseType: ResponseType.bytes));
img.Image image = img.decodeImage(imageResponse.data);
img.Image thumbnail = img.copyResize(image, width: 300);
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 _author = _p.itunes.author ?? _p.author??'';
String _email = _p.itunes.owner.email?? '';
String _provider = _p.generator??'';
String _link = _p.link??'';
String _author = _p.itunes.author ?? _p.author ?? '';
String _provider = _p.generator ?? '';
String _link = _p.link ?? '';
PodcastLocal podcastLocal = PodcastLocal(
_p.title,
_p.itunes.image.href,
@ -98,13 +99,18 @@ class PopupMenu extends StatelessWidget {
_author,
_uuid,
_imagePath,
_email,
_provider,
_link);
podcastLocal.description = _p.description;
groupList.subscribe(podcastLocal);
await groupList.subscribe(podcastLocal);
if (_provider.contains('fireside'))
{
FiresideData data = FiresideData(_uuid, _link);
await data.fatchData();
}
importOmpl.importState = ImportState.parse;
@ -178,28 +184,70 @@ class PopupMenu extends StatelessWidget {
}
return PopupMenuButton<int>(
elevation: 3,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10))),
elevation: 1,
tooltip: 'Menu',
itemBuilder: (context) => [
PopupMenuItem(
value: 1,
child: Text('Refresh All'),
child: Container(
padding: EdgeInsets.only(left: 10),
child: Row(
children: <Widget>[
Icon(Icons.refresh),
Padding(padding: EdgeInsets.symmetric(horizontal: 5.0),),
Text('Refresh All'),
],
),
),
),
PopupMenuItem(
value: 2,
child: Text('Impoer OMPL'),
),
PopupMenuItem(
value: 3,
child: setting.theme != 2 ? Text('Night Mode') : Text('Light Mode'),
),
PopupMenuItem(
PopupMenuItem(
value: 2,
child: Container(
padding: EdgeInsets.only(left: 10),
child: Row(
children: <Widget>[
Icon(Icons.attachment),
Padding(padding: EdgeInsets.symmetric(horizontal: 5.0),),
Text('Import OMPL'),
],
),
),
),
// PopupMenuItem(
// value: 3,
// child: setting.theme != 2 ? Text('Night Mode') : Text('Light Mode'),
// ),
PopupMenuItem(
value: 4,
child: Text('About'),
child: Container(
padding: EdgeInsets.only(left: 10),
child: Row(
children: <Widget>[
Icon(Icons.swap_calls),
Padding(padding: EdgeInsets.symmetric(horizontal: 5.0),),
Text('Settings'),
],
),
),
),
PopupMenuItem(
value: 5,
child: Container(
padding: EdgeInsets.only(left: 10),
child: Row(
children: <Widget>[
Icon(Icons.info_outline),
Padding(padding: EdgeInsets.symmetric(horizontal: 5.0),),
Text('About'),
],
),
),
),
],
onSelected: (value) {
if (value == 4) {
if (value == 5) {
Navigator.push(
context, MaterialPageRoute(builder: (context) => AboutApp()));
} else if (value == 2) {
@ -207,8 +255,11 @@ class PopupMenu extends StatelessWidget {
} else if (value == 1) {
_refreshAll();
} else if (value == 3) {
setting.theme != 2 ? setting.setTheme(2) : setting.setTheme(1);
}
// setting.theme != 2 ? setting.setTheme(2) : setting.setTheme(1);
} else if (value == 4) {
Navigator.push(
context, MaterialPageRoute(builder: (context) => Settings()));
}
},
);
}

View File

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

View File

@ -1,16 +1,18 @@
import 'dart:convert';
import 'dart:io';
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:marquee/marquee.dart';
import 'package:tsacdop/class/episodebrief.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:tuple/tuple.dart';
import 'package:tsacdop/class/episodebrief.dart';
import 'package:tsacdop/class/audiostate.dart';
import 'package:tsacdop/episodes/episodedetail.dart';
import 'package:tsacdop/home/audiopanel.dart';
import 'package:tsacdop/util/pageroute.dart';
import 'package:tsacdop/util/colorize.dart';
class PlayerWidget extends StatefulWidget {
@override
@ -33,325 +35,402 @@ class _PlayerWidgetState extends State<PlayerWidget> {
Widget _expandedPanel(BuildContext context) {
var audio = Provider.of<AudioPlayer>(context, listen: false);
return !_showlist
? Container(
color: Theme.of(context).primaryColor,
height: 300,
padding: EdgeInsets.symmetric(horizontal: 10.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(
height: 80.0,
padding: EdgeInsets.all(20),
alignment: Alignment.center,
child: Selector<AudioPlayer, String>(
selector: (_, audio) => audio.episode.title,
builder: (_, title, __) {
return Container(
child: LayoutBuilder(
builder: (context, size) {
var span = TextSpan(text: title,style: TextStyle(
fontWeight: FontWeight.bold, fontSize: 20));
var tp = TextPainter(
text: span,
maxLines: 1,
textDirection: TextDirection.ltr);
tp.layout(maxWidth: size.maxWidth);
if (tp.didExceedMaxLines) {
return Marquee(
text: title,
style: TextStyle(
fontWeight: FontWeight.bold, fontSize: 18),
scrollAxis: Axis.horizontal,
crossAxisAlignment: CrossAxisAlignment.start,
blankSpace: 30.0,
velocity: 50.0,
pauseAfterRound: Duration(seconds: 1),
startPadding: 30.0,
accelerationDuration: Duration(seconds: 1),
accelerationCurve: Curves.linear,
decelerationDuration:
Duration(milliseconds: 500),
decelerationCurve: Curves.easeOut,
);
} else {
return Text(
title,
style: TextStyle(
fontWeight: FontWeight.bold, fontSize: 20),
);
}
},
),
);
},
),
return Stack(
children: <Widget>[
Container(
color: Theme.of(context).primaryColor,
height: 300,
padding: EdgeInsets.symmetric(horizontal: 10.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(
height: 80.0,
padding: EdgeInsets.all(20),
alignment: Alignment.center,
child: Selector<AudioPlayer, String>(
selector: (_, audio) => audio.episode.title,
builder: (_, title, __) {
return Container(
child: LayoutBuilder(
builder: (context, size) {
var span = TextSpan(
text: title,
style: TextStyle(
fontWeight: FontWeight.bold, fontSize: 20));
var tp = TextPainter(
text: span,
maxLines: 1,
textDirection: TextDirection.ltr);
tp.layout(maxWidth: size.maxWidth);
if (tp.didExceedMaxLines) {
return Marquee(
text: title,
style: TextStyle(
fontWeight: FontWeight.bold, fontSize: 18),
scrollAxis: Axis.horizontal,
crossAxisAlignment: CrossAxisAlignment.start,
blankSpace: 30.0,
velocity: 50.0,
pauseAfterRound: Duration(seconds: 1),
startPadding: 30.0,
accelerationDuration: Duration(seconds: 1),
accelerationCurve: Curves.linear,
decelerationDuration:
Duration(milliseconds: 500),
decelerationCurve: Curves.easeOut,
);
} else {
return Text(
title,
style: TextStyle(
fontWeight: FontWeight.bold, fontSize: 20),
);
}
},
),
);
},
),
Consumer<AudioPlayer>(
builder: (_, data, __) {
return Column(
),
Consumer<AudioPlayer>(
builder: (_, data, __) {
return Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(
padding: EdgeInsets.only(left: 30, right: 30),
child: SliderTheme(
data: SliderTheme.of(context).copyWith(
activeTrackColor: Colors.blue[100],
inactiveTrackColor: Colors.grey[300],
trackHeight: 3.0,
thumbColor: Colors.blue[400],
thumbShape: RoundSliderThumbShape(
enabledThumbRadius: 6.0),
overlayColor: Colors.blue.withAlpha(32),
overlayShape:
RoundSliderOverlayShape(overlayRadius: 14.0),
),
child: Slider(
value: data.seekSliderValue,
onChanged: (double val) {
audio.sliderSeek(val);
}),
),
),
Container(
height: 20.0,
padding: EdgeInsets.symmetric(horizontal: 50.0),
child: Row(
children: <Widget>[
Text(
_stringForSeconds(
data.backgroundAudioPosition) ??
'',
style: TextStyle(fontSize: 10),
),
Expanded(
child: Container(
alignment: Alignment.center,
child: data.remoteErrorMessage != null
? Text(data.remoteErrorMessage,
style: const TextStyle(
color: const Color(0xFFFF0000)))
: Text(
data.remoteAudioLoading
? 'Buffring...'
: '',
style: TextStyle(
color: Theme.of(context)
.accentColor),
),
),
),
Text(
_stringForSeconds(
data.backgroundAudioDuration) ??
'',
style: TextStyle(fontSize: 10),
),
],
),
),
],
);
},
),
Container(
height: 100,
child: Selector<AudioPlayer, bool>(
selector: (_, audio) => audio.backgroundAudioPlaying,
builder: (_, backplay, __) {
return Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
padding: EdgeInsets.symmetric(horizontal: 30.0),
onPressed: backplay
? () => audio.forwardAudio(-10)
: null,
iconSize: 32.0,
icon: Icon(Icons.replay_10),
color: Theme.of(context).tabBarTheme.labelColor),
backplay
? IconButton(
padding:
EdgeInsets.symmetric(horizontal: 30.0),
onPressed: backplay
? () {
audio.pauseAduio();
}
: null,
iconSize: 40.0,
icon: Icon(Icons.pause_circle_filled),
color:
Theme.of(context).tabBarTheme.labelColor)
: IconButton(
padding:
EdgeInsets.symmetric(horizontal: 30.0),
onPressed: backplay
? null
: () {
audio.resumeAudio();
},
iconSize: 40.0,
icon: Icon(Icons.play_circle_filled),
color:
Theme.of(context).tabBarTheme.labelColor),
IconButton(
padding: EdgeInsets.symmetric(horizontal: 30.0),
onPressed: backplay
? () => audio.forwardAudio(30)
: null,
iconSize: 32.0,
icon: Icon(Icons.forward_30),
color: Theme.of(context).tabBarTheme.labelColor),
],
);
},
),
),
Spacer(),
Container(
height: 50.0,
margin: EdgeInsets.all(10.0),
decoration: BoxDecoration(
color: Theme.of(context).scaffoldBackgroundColor,
borderRadius: BorderRadius.all(Radius.circular(10.0)),
),
child: Selector<AudioPlayer, EpisodeBrief>(
selector: (_, audio) => audio.episode,
builder: (_, episode, __) {
return Row(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(
padding: EdgeInsets.only(left: 30, right: 30),
child: SliderTheme(
data: SliderTheme.of(context).copyWith(
activeTrackColor: Colors.blue[100],
inactiveTrackColor: Colors.grey[300],
trackHeight: 3.0,
thumbColor: Colors.blue[400],
thumbShape: RoundSliderThumbShape(
enabledThumbRadius: 6.0),
overlayColor: Colors.blue.withAlpha(32),
overlayShape: RoundSliderOverlayShape(
overlayRadius: 14.0),
),
child: Slider(
value: data.seekSliderValue,
onChanged: (double val) {
audio.sliderSeek(val);
}),
),
Padding(
padding: EdgeInsets.all(5.0),
),
Container(
height: 20.0,
padding: EdgeInsets.symmetric(horizontal: 50.0),
child: Row(
children: <Widget>[
Text(
_stringForSeconds(
data.backgroundAudioPosition) ??
'',
style: TextStyle(fontSize: 10),
),
Expanded(
child: Container(
alignment: Alignment.center,
child: data.remoteErrorMessage != null
? Text(data.remoteErrorMessage,
style: const TextStyle(
color: const Color(0xFFFF0000)))
: Text(
data.remoteAudioLoading
? 'Buffring...'
: '',
style: TextStyle(
color: Theme.of(context)
.accentColor),
),
),
),
Text(
_stringForSeconds(
data.backgroundAudioDuration) ??
'',
style: TextStyle(fontSize: 10),
),
],
height: 30.0,
width: 30.0,
child: CircleAvatar(
backgroundImage:
FileImage(File("${episode.imagePath}")),
),
),
Spacer(),
Material(
color: Colors.transparent,
child: IconButton(
onPressed: () => Navigator.push(
context,
SlideUptRoute(
page: EpisodeDetail(
episodeItem: episode,
heroTag: 'playpanel')),
),
icon: Icon(Icons.info),
),
),
Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.only(
topRight: Radius.circular(10.0),
bottomRight: Radius.circular(10.0)),
child: Container(
height: 50.0,
width: 50.0,
child: Icon(Icons.keyboard_arrow_up)),
onTap: () => setState(() => _showlist = true)),
),
],
);
},
),
Container(
height: 100,
child: Selector<AudioPlayer, bool>(
selector: (_, audio) => audio.backgroundAudioPlaying,
builder: (_, backplay, __) {
return Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
padding: EdgeInsets.symmetric(horizontal: 30.0),
onPressed: backplay
? () => audio.forwardAudio(-10)
: null,
iconSize: 32.0,
icon: Icon(Icons.replay_10),
color:
Theme.of(context).tabBarTheme.labelColor),
backplay
? IconButton(
padding:
EdgeInsets.symmetric(horizontal: 30.0),
onPressed: backplay
? () {
audio.pauseAduio();
}
: null,
iconSize: 40.0,
icon: Icon(Icons.pause_circle_filled),
color: Theme.of(context)
.tabBarTheme
.labelColor)
: IconButton(
padding:
EdgeInsets.symmetric(horizontal: 30.0),
onPressed: backplay
? null
: () {
audio.resumeAudio();
},
iconSize: 40.0,
icon: Icon(Icons.play_circle_filled),
color: Theme.of(context)
.tabBarTheme
.labelColor),
IconButton(
padding: EdgeInsets.symmetric(horizontal: 30.0),
onPressed: backplay
? () => audio.forwardAudio(30)
: null,
iconSize: 32.0,
icon: Icon(Icons.forward_30),
color:
Theme.of(context).tabBarTheme.labelColor),
],
);
},
),
),
Spacer(),
Container(
height: 50.0,
margin: EdgeInsets.symmetric(vertical: 10.0),
padding: EdgeInsets.symmetric(horizontal: 10.0),
decoration: BoxDecoration(
color: Theme.of(context).scaffoldBackgroundColor,
borderRadius: BorderRadius.all(Radius.circular(10.0)),
),
child: Selector<AudioPlayer, EpisodeBrief>(
selector: (_, audio) => audio.episode,
builder: (_, episode, __) {
return Row(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(
padding: EdgeInsets.all(10.0),
child: ClipRRect(
borderRadius:
BorderRadius.all(Radius.circular(15.0)),
child: Container(
height: 30.0,
width: 30.0,
child: Image.file(
File("${episode.imagePath}"))),
),
),
Spacer(),
Material(
color: Colors.transparent,
child: InkWell(
onTap: () => Navigator.push(
context,
SlideUptRoute(
page: EpisodeDetail(
episodeItem: episode,
heroTag: 'playpanel')),
),
child: Icon(Icons.info),
),
),
IconButton(
icon: Icon(Icons.keyboard_arrow_up),
onPressed: () =>
setState(() => _showlist = true)),
],
);
},
),
),
]),
)
: Container(
height: 300,
),
]),
),
Container(
alignment: Alignment.bottomLeft,
child: AnimatedContainer(
duration: Duration(milliseconds: 400),
height: _showlist ? 300 : 0,
width: MediaQuery.of(context).size.width,
alignment: Alignment.center,
margin: EdgeInsets.all(20),
padding: EdgeInsets.all(10.0),
padding: EdgeInsets.only(bottom: 10.0),
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(10)),
borderRadius: BorderRadius.all(Radius.circular(10.0)),
color: Theme.of(context).scaffoldBackgroundColor,
),
child: Selector<AudioPlayer, List<EpisodeBrief>>(
selector: (_, audio) => audio.queue.playlist,
builder: (_, playlist, __) {
print(playlist.first.title);
double _width = MediaQuery.of(context).size.width;
return Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(
height: 30.0,
alignment: Alignment.centerRight,
child: IconButton(
icon: Icon(Icons.keyboard_arrow_down),
onPressed: () => setState(() => _showlist = false),
),
),
Expanded(
child: Container(
child: ListView.builder(
itemCount: playlist.length,
itemBuilder: (BuildContext context, int index) {
print(playlist.length);
return Container(
height: 50,
alignment: Alignment.centerLeft,
decoration: BoxDecoration(
border: Border(
bottom: Divider.createBorderSide(context),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(
padding: EdgeInsets.all(10.0),
child: ClipRRect(
borderRadius: BorderRadius.all(
Radius.circular(15.0)),
child: Container(
height: 30.0,
width: 30.0,
child: Image.file(File(
"${playlist[index].imagePath}"))),
),
),
Container(
alignment: Alignment.centerLeft,
width: _width - 200,
child: Text(
playlist[index].title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
Spacer(),
IconButton(
icon: Icon(Icons.play_arrow),
onPressed: null),
],
),
);
},
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
SingleChildScrollView(
scrollDirection: Axis.vertical,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 20.0),
alignment: Alignment.center,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(
'NEXT',
style: TextStyle(
color: Theme.of(context).accentColor,
fontWeight: FontWeight.bold),
),
),
Spacer(),
Container(
height: 40.0,
alignment: Alignment.centerRight,
child: IconButton(
icon: Icon(Icons.keyboard_arrow_down),
onPressed: () => setState(() => _showlist = false),
),
),
],
),
],
);
},
),
),
Expanded(
child: Selector<AudioPlayer, List<EpisodeBrief>>(
selector: (_, audio) => audio.queue.playlist,
builder: (_, playlist, __) {
return ListView.builder(
shrinkWrap: true,
scrollDirection: Axis.vertical,
itemCount: playlist.length,
itemBuilder: (BuildContext context, int index) {
print(playlist.length);
return index == 0
? Center()
: 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[400],
),
key: Key(playlist[index].enclosureUrl),
onDismissed: (direction) async {
await audio
.delFromPlaylist(playlist[index]);
Fluttertoast.showToast(
msg: 'Removed From Playlist',
gravity: ToastGravity.BOTTOM,
);
},
child: Column(
children: <Widget>[
Container(
height: 50,
padding: EdgeInsets.symmetric(
horizontal: 10),
alignment: Alignment.centerLeft,
decoration: BoxDecoration(
color: Theme.of(context)
.scaffoldBackgroundColor,
),
child: InkWell(
onTap: () {
audio.episodeLoad(playlist[index]);
setState(() => _showlist = false);
},
child: Row(
mainAxisAlignment:
MainAxisAlignment.start,
crossAxisAlignment:
CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(
padding: EdgeInsets.all(10.0),
child: ClipRRect(
borderRadius:
BorderRadius.all(
Radius.circular(
15.0)),
child: Container(
height: 30.0,
width: 30.0,
child: Image.file(File(
"${playlist[index].imagePath}"))),
),
),
Expanded(
child: Container(
alignment:
Alignment.centerLeft,
child: Text(
playlist[index].title,
maxLines: 1,
overflow:
TextOverflow.ellipsis,
),
),
),
],
),
),
),
Divider(height: 2),
],
),
);
},
);
},
),
),
],
),
);
),
),
],
);
}
Widget _miniPanel(double width, BuildContext context) {
@ -359,13 +438,6 @@ class _PlayerWidgetState extends State<PlayerWidget> {
return Container(
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
// boxShadow: [
// BoxShadow(
// offset: Offset(0, -1),
// blurRadius: 4,
// color: Colors.grey[400],
// ),
// ],
),
height: 60,
child:
@ -374,19 +446,9 @@ class _PlayerWidgetState extends State<PlayerWidget> {
selector: (_, audio) =>
Tuple2(audio.episode?.primaryColor, audio.seekSliderValue),
builder: (_, data, __) {
var color = json.decode(data.item1);
Color _c;
if (Theme.of(context).brightness == Brightness.light) {
_c = (color[0] > 200 && color[1] > 200 && color[2] > 200)
? Color.fromRGBO(
(255 - color[0]), 255 - color[1], 255 - color[2], 1.0)
: Color.fromRGBO(color[0], color[1], color[2], 1.0);
} else {
_c = (color[0] < 50 && color[1] < 50 && color[2] < 50)
? Color.fromRGBO(
(255 - color[0]), 255 - color[1], 255 - color[2], 1.0)
: Color.fromRGBO(color[0], color[1], color[2], 1.0);
}
Color _c = (Theme.of(context).brightness == Brightness.light)
? data.item1.colorizedark()
: data.item1.colorizeLight();
return SizedBox(
height: 2,
child: LinearProgressIndicator(
@ -411,7 +473,10 @@ class _PlayerWidgetState extends State<PlayerWidget> {
builder: (_, title, __) {
return LayoutBuilder(
builder: (context, size) {
var span = TextSpan(text: title, style: TextStyle(fontWeight: FontWeight.bold),);
var span = TextSpan(
text: title,
style: TextStyle(fontWeight: FontWeight.bold),
);
var tp = TextPainter(
text: span,
maxLines: 2,
@ -500,17 +565,30 @@ class _PlayerWidgetState extends State<PlayerWidget> {
: () {
audio.resumeAudio();
},
child: Container(
padding: EdgeInsets.all(10.0),
child: ClipRRect(
borderRadius: BorderRadius.all(
Radius.circular(15.0)),
child: Container(
height: 30.0,
width: 30.0,
child: Image.file(File(
"${audio.episode.imagePath}"))),
),
child: Stack(
alignment: Alignment.center,
children: <Widget>[
Container(
padding: EdgeInsets.all(10.0),
child: Container(
height: 30.0,
width: 30.0,
child: CircleAvatar(
backgroundImage: FileImage(File(
"${audio.episode.imagePath}")),
)),
),
Container(
height: 50.0,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.black),
),
Icon(
Icons.play_arrow,
color: Colors.white,
)
],
),
),
IconButton(

View File

@ -1,4 +1,3 @@
import 'dart:convert';
import 'dart:io';
import 'dart:async';
@ -8,14 +7,15 @@ import 'package:provider/provider.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:tsacdop/class/episodebrief.dart';
import 'package:tsacdop/class/importompl.dart';
import 'package:tsacdop/class/podcast_group.dart';
import 'package:tsacdop/class/podcastlocal.dart';
import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
import 'package:tsacdop/episodes/episodedetail.dart';
import 'package:tsacdop/podcasts/podcastdetail.dart';
import 'package:tsacdop/podcasts/podcastmanage.dart';
import 'package:tsacdop/util/pageroute.dart';
import 'package:tsacdop/util/colorize.dart';
class ScrollPodcasts extends StatefulWidget {
@override
@ -228,7 +228,8 @@ class _ScrollPodcastsState extends State<ScrollPodcasts> {
left: 6.0,
right: 6.0),
indicator: CircleTabIndicator(
color: Colors.blue, radius: 3),
color: Theme.of(context).accentColor,
radius: 3),
isScrollable: true,
tabs: groups[_groupIndex]
.podcasts
@ -251,29 +252,31 @@ class _ScrollPodcastsState extends State<ScrollPodcasts> {
],
),
),
Container(
height: (_width - 20) / 3 + 40,
margin: EdgeInsets.only(left: 10, right: 10),
decoration: BoxDecoration(
color: Theme.of(context).scaffoldBackgroundColor,
),
child: TabBarView(
children: groups[_groupIndex]
.podcasts
.map<Widget>((PodcastLocal podcastLocal) {
return Container(
decoration: BoxDecoration(
color: Theme.of(context).brightness ==
Brightness.light
? Theme.of(context).primaryColor
: Colors.black12),
margin: EdgeInsets.symmetric(horizontal: 5.0),
key: ObjectKey(podcastLocal.title),
child: PodcastPreview(
podcastLocal: podcastLocal,
),
);
}).toList(),
Consumer<ImportOmpl>(
builder: (_, ompl, __) => Container(
height: (_width - 20) / 3 + 40,
margin: EdgeInsets.only(left: 10, right: 10),
decoration: BoxDecoration(
color: Theme.of(context).scaffoldBackgroundColor,
),
child: TabBarView(
children: groups[_groupIndex]
.podcasts
.map<Widget>((PodcastLocal podcastLocal) {
return Container(
decoration: BoxDecoration(
color: Theme.of(context).brightness ==
Brightness.light
? Theme.of(context).primaryColor
: Colors.black12),
margin: EdgeInsets.symmetric(horizontal: 5.0),
key: ObjectKey(podcastLocal.title),
child: PodcastPreview(
podcastLocal: podcastLocal,
),
);
}).toList(),
),
),
),
],
@ -293,27 +296,15 @@ class PodcastPreview extends StatefulWidget {
class _PodcastPreviewState extends State<PodcastPreview> {
Future<List<EpisodeBrief>> _getRssItemTop(PodcastLocal podcastLocal) async {
var dbHelper = DBHelper();
Future<List<EpisodeBrief>> episodes =
dbHelper.getRssItemTop(podcastLocal.id);
List<EpisodeBrief> episodes = await dbHelper.getRssItemTop(podcastLocal.id);
return episodes;
}
Color _c;
@override
Widget build(BuildContext context) {
var color = json.decode(widget.podcastLocal.primaryColor);
if (Theme.of(context).brightness == Brightness.light) {
(color[0] > 200 && color[1] > 200 && color[2] > 200)
? _c = Color.fromRGBO(
(255 - color[0]), 255 - color[1], 255 - color[2], 1.0)
: _c = Color.fromRGBO(color[0], color[1], color[2], 1.0);
} else {
(color[0] < 50 && color[1] < 50 && color[2] < 50)
? _c = Color.fromRGBO(
(255 - color[0]), 255 - color[1], 255 - color[2], 1.0)
: _c = Color.fromRGBO(color[0], color[1], color[2], 1.0);
}
Color _c = (Theme.of(context).brightness == Brightness.light)
? widget.podcastLocal.primaryColor.colorizedark()
: widget.podcastLocal.primaryColor.colorizeLight();
return Column(
children: <Widget>[
Expanded(
@ -401,20 +392,10 @@ class ShowEpisode extends StatelessWidget {
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
Color _c;
var color = json.decode(podcast[index].primaryColor);
if (Theme.of(context).brightness == Brightness.light) {
(color[0] > 200 && color[1] > 200 && color[2] > 200)
? _c = Color.fromRGBO(
(255 - color[0]), 255 - color[1], 255 - color[2], 1.0)
: _c = Color.fromRGBO(color[0], color[1], color[2], 1.0);
} else {
(color[0] < 50 && color[1] < 50 && color[2] < 50)
? _c = Color.fromRGBO(
(255 - color[0]), 255 - color[1], 255 - color[2], 1.0)
: _c = Color.fromRGBO(color[0], color[1], color[2], 1.0);
}
Color _c =
(Theme.of(context).brightness == Brightness.light)
? podcastLocal.primaryColor.colorizedark()
: podcastLocal.primaryColor.colorizeLight();
return InkWell(
onTap: () {
Navigator.push(
@ -438,13 +419,6 @@ class ShowEpisode extends StatelessWidget {
// color: Theme.of(context).primaryColor,
width: 3.0,
),
// boxShadow: [
// BoxShadow(
// color: Theme.of(context).primaryColor,
// blurRadius: 1.0,
// spreadRadius: 0.5,
// ),
// ]
),
alignment: Alignment.center,
padding: EdgeInsets.all(10.0),
@ -459,15 +433,11 @@ class ShowEpisode extends StatelessWidget {
Hero(
tag: podcast[index].enclosureUrl + 'scroll',
child: Container(
child: ClipRRect(
borderRadius: BorderRadius.all(
Radius.circular(_width / 36)),
child: Container(
height: _width / 18,
width: _width / 18,
child: Image.file(
File("${podcastLocal.imagePath}")),
),
height: _width / 18,
width: _width / 18,
child: CircleAvatar(
backgroundImage: FileImage(
File("${podcastLocal.imagePath}")),
),
),
),

View File

@ -2,6 +2,7 @@ import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:tsacdop/class/episodebrief.dart';
import 'package:tsacdop/home/paly_history.dart';
import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
import 'package:tsacdop/util/episodegrid.dart';
@ -22,6 +23,39 @@ class _MainTabState extends State<MainTab> with TickerProviderStateMixin {
));
}
Widget playHistory() {
return PopupMenuButton<int>(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(10))),
elevation: 1,
icon: Icon(Icons.history),
tooltip: "Menu",
itemBuilder: (context) => [
PopupMenuItem(
value: 0,
child: Container(
padding: EdgeInsets.only(left: 10),
child: Row(
children: <Widget>[
Icon(Icons.history),
Padding(
padding: EdgeInsets.symmetric(horizontal: 5.0),
),
Text('Play History'),
],
),
),
),
],
onSelected: (value) {
if (value == 0) {
Navigator.push(
context, MaterialPageRoute(builder: (context) => PlayedHistory()));
}
},
);
}
@override
void initState() {
super.initState();
@ -40,30 +74,36 @@ class _MainTabState extends State<MainTab> with TickerProviderStateMixin {
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(
padding: EdgeInsets.symmetric(horizontal: 10.0),
height: 50,
alignment: Alignment.centerLeft,
child: TabBar(
isScrollable: true,
labelPadding: EdgeInsets.all(10.0),
controller: _controller,
indicator: getIndicator(context),
tabs: <Widget>[
Text(
'Recent Update',
style: TextStyle(fontWeight: FontWeight.bold),
Row(
children: <Widget>[
Container(
padding: EdgeInsets.symmetric(horizontal: 10.0),
height: 50,
alignment: Alignment.centerLeft,
child: TabBar(
isScrollable: true,
labelPadding: EdgeInsets.all(10.0),
controller: _controller,
indicator: getIndicator(context),
tabs: <Widget>[
Text(
'Recent Update',
style: TextStyle(fontWeight: FontWeight.bold),
),
Text(
'Favorites',
style: TextStyle(fontWeight: FontWeight.bold),
),
Text(
'Downloads',
style: TextStyle(fontWeight: FontWeight.bold),
),
],
),
Text(
'Favorites',
style: TextStyle(fontWeight: FontWeight.bold),
),
Text(
'Downloads',
style: TextStyle(fontWeight: FontWeight.bold),
),
],
),
),
Spacer(),
playHistory(),
],
),
Expanded(
child: Container(

View File

@ -0,0 +1,73 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
import 'package:tsacdop/class/audiostate.dart';
class PlayedHistory extends StatefulWidget{
@override
_PlayedHistoryState createState() => _PlayedHistoryState();
}
class _PlayedHistoryState extends State<PlayedHistory> {
Future<List<PlayHistory>> gerPlayHistory() async{
DBHelper dbHelper = DBHelper();
List<PlayHistory> playHistory;
playHistory = await dbHelper.getPlayHistory();
await Future.forEach(playHistory, (playHistory) async{
await playHistory.getEpisode();
});
return playHistory;
}
static String _stringForSeconds(double seconds) {
if (seconds == null) return null;
return '${(seconds ~/ 60)}:${(seconds.truncate() % 60).toString().padLeft(2, '0')}';
}
@override
Widget build(BuildContext context) {
return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle(
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
systemNavigationBarColor: Theme.of(context).primaryColor,
statusBarColor: Theme.of(context).primaryColor,
),
child: SafeArea(
child: Scaffold(
appBar: AppBar(
title: Text('History'),
centerTitle: true,
elevation: 0,
backgroundColor: Theme.of(context).primaryColor,
),
body: FutureBuilder<List<PlayHistory>>(
future: gerPlayHistory(),
builder: (context, snapshot) {
return
snapshot.hasData ?
ListView.builder(
shrinkWrap: true,
scrollDirection: Axis.vertical,
itemCount: snapshot.data.length,
itemBuilder: (BuildContext context, int index){
return Column(
children: <Widget>[
ListTile(
title: Text(snapshot.data[index].title),
subtitle: Text(_stringForSeconds(snapshot.data[index].seconds)),
),
Divider(height: 2),
],
);
}
)
: Center(
child: CircularProgressIndicator(),
);
},
),
),
),
);
}
}

View File

@ -32,12 +32,13 @@ class KeyValueStorage {
{'groups': groupList.map((group) => group.toJson()).toList()}));
}
Future<bool> saveTheme(int setting) async{
Future<bool> saveInt(int setting) async{
SharedPreferences prefs = await SharedPreferences.getInstance();
print(setting.toString());
return prefs.setInt(key, setting);
}
Future<int> getTheme() async{
Future<int> getInt() async{
SharedPreferences prefs = await SharedPreferences.getInstance();
if(prefs.getInt(key) == null) await prefs.setInt(key, 0);
return prefs.getInt(key);
@ -51,7 +52,18 @@ class KeyValueStorage {
Future<List<String>> getStringList() async{
SharedPreferences prefs = await SharedPreferences.getInstance();
if(prefs.getStringList(key) == null) {await prefs.setStringList(key, []);}
print(prefs.getStringList(key).toString());
return prefs.getStringList(key);
}
Future<bool> saveString(String string) async{
SharedPreferences prefs = await SharedPreferences.getInstance();
return prefs.setString(key, string);
}
Future<String> getString() async{
SharedPreferences prefs = await SharedPreferences.getInstance();
if(prefs.getString(key) == null) {await prefs.setString(key, '');}
return prefs.getString(key);
}
}

View File

@ -30,7 +30,7 @@ class DBHelper {
await db
.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, email TEXT, provider TEXT, link TEXT,
description TEXT, add_date INTEGER, imagePath TEXT, provider TEXT, link TEXT,
background_image TEXT DEFAULT '',hosts TEXT DEFAULT '')""");
await db
.execute("""CREATE TABLE Episodes(id INTEGER PRIMARY KEY,title TEXT,
@ -40,7 +40,7 @@ class DBHelper {
downloaded TEXT DEFAULT 'ND', download_date INTEGER DEFAULT 0)""");
await db.execute(
"""CREATE TABLE PlayHistory(id INTEGER PRIMARY KEY, title TEXT, enclosure_url TEXT UNIQUE,
seconds INTEGER, seek_value INTEGER, add_date INTEGER)""");
seconds REAL, seek_value REAL, add_date INTEGER)""");
}
Future<List<PodcastLocal>> getPodcastLocal(List<String> podcasts) async {
@ -49,7 +49,7 @@ class DBHelper {
await Future.forEach(podcasts, (s) async {
List<Map> list;
list = await dbClient.rawQuery(
'SELECT id, title, imageUrl, rssUrl, primaryColor, author, imagePath , email, provider, link FROM PodcastLocal WHERE id = ?',
'SELECT id, title, imageUrl, rssUrl, primaryColor, author, imagePath , provider, link FROM PodcastLocal WHERE id = ?',
[s]);
podcastLocal.add(PodcastLocal(
list.first['title'],
@ -59,7 +59,6 @@ class DBHelper {
list.first['author'],
list.first['id'],
list.first['imagePath'],
list.first['email'],
list.first['provider'],
list.first['link']));
});
@ -69,7 +68,7 @@ class DBHelper {
Future<List<PodcastLocal>> getPodcastLocalAll() async {
var dbClient = await database;
List<Map> list = await dbClient.rawQuery(
'SELECT id, title, imageUrl, rssUrl, primaryColor, author, imagePath, email, provider, link FROM PodcastLocal ORDER BY add_date DESC');
'SELECT id, title, imageUrl, rssUrl, primaryColor, author, imagePath, provider, link FROM PodcastLocal ORDER BY add_date DESC');
List<PodcastLocal> podcastLocal = List();
@ -82,7 +81,6 @@ class DBHelper {
list[i]['author'],
list[i]['id'],
list[i]['imagePath'],
list.first['email'],
list.first['provider'],
list.first['link']));
}
@ -103,7 +101,7 @@ class DBHelper {
await dbClient.transaction((txn) async {
return await txn.rawInsert(
"""INSERT OR IGNORE INTO PodcastLocal (id, title, imageUrl, rssUrl,
primaryColor, author, description, add_date, imagePath, email, provider, link) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
primaryColor, author, description, add_date, imagePath, provider, link) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
[
podcastLocal.id,
podcastLocal.title,
@ -114,28 +112,26 @@ class DBHelper {
podcastLocal.description,
_milliseconds,
podcastLocal.imagePath,
podcastLocal.email,
podcastLocal.provider,
podcastLocal.link
]);
});
}
Future<int> saveFiresideData(List<String> list)async{
Future<int> saveFiresideData(List<String> list) async {
var dbClient = await database;
int result = await dbClient.rawUpdate(
'UPDATE PodcastLocal SET background_image = ? , hosts = ? WHERE id = ?',[list[1],list[2],list[0]]
);
'UPDATE PodcastLocal SET background_image = ? , hosts = ? WHERE id = ?',
[list[1], list[2], list[0]]);
print('Fireside data save in sqllite');
return result;
}
Future<List<String>> getFiresideData(String id)async{
Future<List<String>> getFiresideData(String id) async {
var dbClient = await database;
List<Map> list = await dbClient.rawQuery(
'SELECT background_image, hosts FROM PodcastLocal WHERE id = ?', [id]
);
List<String> data = [list.first['background_image]'],list.first['hosts']];
'SELECT background_image, hosts FROM PodcastLocal WHERE id = ?', [id]);
List<String> data = [list.first['background_image'], list.first['hosts']];
return data;
}
@ -172,6 +168,32 @@ class DBHelper {
return result;
}
Future<List<PlayHistory>> getPlayHistory() async {
var dbClient = await database;
List<Map> list = await dbClient.rawQuery(
"""SELECT title, enclosure_url, seconds, seek_value, add_date FROM PlayHistory
ORDER BY add_date DESC
""");
List<PlayHistory> playHistory = [];
list.forEach((record) {
playHistory.add(PlayHistory(
record['title'],
record['enclosure_url'],
record['seconds'],
record['seek_value'],
));
});
return playHistory;
}
Future<int> getPosition(EpisodeBrief episodeBrief) async {
var dbClient = await database;
List<Map> list = await dbClient.rawQuery(
"SELECT seconds FROM PlayHistory Where enclosure_url = ?",
[episodeBrief.enclosureUrl]);
return list.length > 0 ? list.first['seconds'] : 0;
}
DateTime _parsePubDate(String pubDate) {
if (pubDate == null) return DateTime.now();
print(pubDate);

View File

@ -2,61 +2,100 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter/services.dart';
import 'package:flutter_downloader/flutter_downloader.dart';
import 'package:workmanager/workmanager.dart';
import 'package:tsacdop/class/podcastlocal.dart';
import 'package:tsacdop/class/podcast_group.dart';
import 'package:tsacdop/home/appbar/addpodcast.dart';
import 'package:tsacdop/class/audiostate.dart';
import 'package:tsacdop/class/importompl.dart';
import 'package:tsacdop/class/settingstate.dart';
import 'local_storage/sqflite_localpodcast.dart';
void main() async {
final SettingState themeSetting = SettingState();
Future main() async {
WidgetsFlutterBinding.ensureInitialized();
await themeSetting.initData();
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => AudioPlayer()),
ChangeNotifierProvider(create: (context) => ImportOmpl()),
ChangeNotifierProvider(create: (context) => SettingState()),
ChangeNotifierProvider(create: (context) => GroupList()),
ChangeNotifierProvider(create: (_) => themeSetting),
ChangeNotifierProvider(create: (_) => AudioPlayer()),
ChangeNotifierProvider(create: (_) => GroupList()),
ChangeNotifierProvider(create: (_) => ImportOmpl()),
],
child: MyApp(),
),
);
WidgetsFlutterBinding.ensureInitialized();
Workmanager.initialize(
callbackDispatcher,
isInDebugMode: true,
);
Workmanager.registerPeriodicTask("update", "simplePeriodicTask",
frequency: Duration(hours: 1),
initialDelay: Duration(seconds: 10),
constraints: Constraints(
networkType: NetworkType.connected,
requiresBatteryNotLow: true,
requiresCharging: false,
requiresDeviceIdle: true,
requiresStorageNotLow: true));
await FlutterDownloader.initialize();
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
await SystemChrome.setPreferredOrientations(
[DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]);
}
void callbackDispatcher() {
Workmanager.executeTask((task, inputData) async {
var dbHelper = DBHelper();
List<PodcastLocal> podcastList = await dbHelper.getPodcastLocalAll();
await Future.forEach(podcastList, (podcastLocal) async {
await dbHelper.updatePodcastRss(podcastLocal);
print('Refresh ' + podcastLocal.title);
});
return Future.value(true);
});
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
var theme = Provider.of<SettingState>(context).theme;
print(theme);
return MaterialApp(
themeMode: ThemeMode.system,
debugShowCheckedModeBanner: false,
title: 'TsacDop',
theme: ThemeData(
accentColorBrightness: Brightness.dark,
primaryColor: Colors.grey[100],
accentColor: Colors.blue[400],
primaryColorLight: Colors.white,
primaryColorDark: Colors.grey[300],
dialogBackgroundColor: Colors.white,
backgroundColor: Colors.grey[100],
appBarTheme: AppBarTheme(
color: Colors.grey[100],
elevation: 0,
),
textTheme: TextTheme(
headline1: TextStyle(fontSize: 72.0, fontWeight: FontWeight.bold),
bodyText2: TextStyle(fontSize: 15.0, fontWeight: FontWeight.normal),
),
tabBarTheme: TabBarTheme(
labelColor: Colors.black,
unselectedLabelColor: Colors.grey[400],
),
),
darkTheme: ThemeData.dark().copyWith(accentColor: Colors.blue[400],),
home: MyHomePage(),
return Consumer<SettingState>(
builder: (_, setting, __) {
return MaterialApp(
themeMode: setting.theme,
debugShowCheckedModeBanner: false,
title: 'Tsacdop',
theme: ThemeData(
accentColorBrightness: Brightness.dark,
primaryColor: Colors.grey[100],
accentColor: setting.accentSetColor,
primaryColorLight: Colors.white,
primaryColorDark: Colors.grey[300],
dialogBackgroundColor: Colors.white,
backgroundColor: Colors.grey[100],
appBarTheme: AppBarTheme(
color: Colors.grey[100],
elevation: 0,
),
textTheme: TextTheme(
headline1: TextStyle(fontSize: 72.0, fontWeight: FontWeight.bold),
bodyText2:
TextStyle(fontSize: 15.0, fontWeight: FontWeight.normal),
),
tabBarTheme: TabBarTheme(
labelColor: Colors.black,
unselectedLabelColor: Colors.grey[400],
),
),
darkTheme: ThemeData.dark().copyWith(
accentColor: setting.accentSetColor,
),
home: MyHomePage(),
);
},
);
}
}

View File

@ -1,4 +1,3 @@
import 'dart:convert';
import 'dart:io';
import 'dart:async';
@ -6,9 +5,11 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:html/parser.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:tsacdop/class/podcastlocal.dart';
import 'package:tsacdop/class/episodebrief.dart';
@ -18,6 +19,7 @@ import 'package:tsacdop/util/episodegrid.dart';
import 'package:tsacdop/util/pageroute.dart';
import 'package:tsacdop/home/audioplayer.dart';
import 'package:tsacdop/class/fireside_data.dart';
import 'package:tsacdop/util/colorize.dart';
class PodcastDetail extends StatefulWidget {
PodcastDetail({Key key, this.podcastLocal}) : super(key: key);
@ -29,7 +31,7 @@ class PodcastDetail extends StatefulWidget {
class _PodcastDetailState extends State<PodcastDetail> {
final GlobalKey<RefreshIndicatorState> _refreshIndicatorKey =
GlobalKey<RefreshIndicatorState>();
String background;
String backgroundImage;
List<PodcastHost> hosts;
Future _updateRssItem(PodcastLocal podcastLocal) async {
var dbHelper = DBHelper();
@ -52,12 +54,23 @@ class _PodcastDetailState extends State<PodcastDetail> {
if (podcastLocal.provider.contains('fireside')) {
FiresideData data = FiresideData(podcastLocal.id, podcastLocal.link);
await data.getData();
background = data.background;
backgroundImage = data.background;
hosts = data.hosts;
}
return episodes;
}
_launchUrl(String url) async {
if (await canLaunch(url)) {
await launch(url);
} else {
Fluttertoast.showToast(
msg: '$url Invalid Link',
gravity: ToastGravity.TOP,
);
}
}
Widget podcastInfo(BuildContext context) {
return Container(
height: 170,
@ -75,24 +88,87 @@ class _PodcastDetailState extends State<PodcastDetail> {
);
}
Widget hostsList(BuildContext context, List<PodcastHost> hosts) {
return Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
hosts != null
? Container(
decoration: BoxDecoration(
image: DecorationImage(
// colorFilter: ColorFilter.mode(_color, BlendMode.color),
image: CachedNetworkImageProvider(
backgroundImage,
),
fit: BoxFit.cover)),
alignment: Alignment.centerRight,
child: Container(
color: Theme.of(context).scaffoldBackgroundColor.withOpacity(0.5),
padding: EdgeInsets.symmetric(vertical: 5.0),
width: MediaQuery.of(context).size.width,
alignment: Alignment.centerRight,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: hosts
.map((host) => Container(
padding: EdgeInsets.all(5.0),
width: 80.0,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
CircleAvatar(
backgroundColor: Colors.grey[400],
backgroundImage: CachedNetworkImageProvider(
host.image,
)),
Padding(
padding: EdgeInsets.all(2),
),
Text(
host.name,
style: TextStyle(
backgroundColor:
Colors.black.withOpacity(0.5),
color: Colors.white,
),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.fade,
),
],
)))
.toList()
.cast<Widget>(),
),
),
))
: Center(),
Padding(padding: EdgeInsets.all(10.0)),
Container(
padding: EdgeInsets.only(left: 15.0, right: 15.0, bottom: 10.0),
alignment: Alignment.topLeft,
color: Theme.of(context).scaffoldBackgroundColor,
child: AboutPodcast(podcastLocal: widget.podcastLocal),
),
],
);
}
double top = 0;
@override
Widget build(BuildContext context) {
double _width = MediaQuery.of(context).size.width;
Color _color;
var color = json.decode(widget.podcastLocal.primaryColor);
(color[0] > 200 && color[1] > 200 && color[2] > 200)
? _color = Color.fromRGBO(
(255 - color[0]), 255 - color[1], 255 - color[2], 1.0)
: _color = Color.fromRGBO(color[0], color[1], color[2], 1.0);
Color _color = widget.podcastLocal.primaryColor.colorizedark();
return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle(
statusBarIconBrightness: Brightness.light,
systemNavigationBarColor: Theme.of(context).primaryColor,
statusBarColor: _color,
// statusBarColor: Theme.of(context).primaryColor,
),
child: SafeArea(
child: Scaffold(
@ -112,6 +188,64 @@ class _PodcastDetailState extends State<PodcastDetail> {
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'),
],
),
),
)
: 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,
@ -141,23 +275,19 @@ class _PodcastDetailState extends State<PodcastDetail> {
Text(
widget.podcastLocal.author ??
'',
style: Theme.of(context)
.textTheme
.bodyText1
.copyWith(
color:
Colors.grey[300])),
Text(
'Hosted on ' +
style: TextStyle(
color: Colors.white)),
widget.podcastLocal.provider
.isNotEmpty
? Text(
'Hosted on ' +
widget.podcastLocal
.provider ??
'',
style: Theme.of(context)
.textTheme
.bodyText1
.copyWith(
color:
Colors.grey[300]))
.provider,
maxLines: 1,
style: TextStyle(
color: Colors.white),
)
: Center(),
],
),
),
@ -186,61 +316,14 @@ class _PodcastDetailState extends State<PodcastDetail> {
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Padding(
padding: EdgeInsets.all(10.0),
),
Container(
alignment: Alignment.centerRight,
padding: EdgeInsets.symmetric(
horizontal: 10.0),
child: hosts != null
? Wrap(
children: hosts
.map((host) => Container(
padding:
EdgeInsets.all(5.0),
width: 60.0,
child: Column(
mainAxisAlignment:
MainAxisAlignment
.center,
mainAxisSize:
MainAxisSize.min,
children: <Widget>[
CachedNetworkImage(
imageUrl:
host.image),
Text(host.name),
],
)))
.toList()
.cast<Widget>(),
)
: Center(),
),
Container(
padding: EdgeInsets.only(
left: 15.0,
right: 15.0,
bottom: 10.0),
alignment: Alignment.topLeft,
color: Theme.of(context)
.scaffoldBackgroundColor,
child: AboutPodcast(
podcastLocal: widget.podcastLocal),
),
],
);
return hostsList(context, hosts);
},
childCount: 1,
),
),
SliverPadding(
padding: const EdgeInsets.symmetric(horizontal:15.0),
padding:
const EdgeInsets.symmetric(horizontal: 15.0),
sliver: SliverGrid(
gridDelegate:
SliverGridDelegateWithFixedCrossAxisCount(
@ -253,33 +336,13 @@ class _PodcastDetailState extends State<PodcastDetail> {
(BuildContext context, int index) {
EpisodeBrief episodeBrief =
snapshot.data[index];
Color _c;
var color = json.decode(
widget.podcastLocal.primaryColor);
if (Theme.of(context).brightness ==
Brightness.light) {
(color[0] > 200 &&
color[1] > 200 &&
color[2] > 200)
? _c = Color.fromRGBO(
(255 - color[0]),
255 - color[1],
255 - color[2],
1.0)
: _c = Color.fromRGBO(color[0],
color[1], color[2], 1.0);
} else {
(color[0] < 50 &&
color[1] < 50 &&
color[2] < 50)
? _c = Color.fromRGBO(
(255 - color[0]),
255 - color[1],
255 - color[2],
1.0)
: _c = Color.fromRGBO(color[0],
color[1], color[2], 1.0);
}
Color _c = (Theme.of(context).brightness ==
Brightness.light)
? widget.podcastLocal.primaryColor
.colorizedark()
: widget.podcastLocal.primaryColor
.colorizeLight();
return Material(
color: Colors.transparent,
child: InkWell(
@ -334,18 +397,12 @@ class _PodcastDetailState extends State<PodcastDetail> {
.enclosureUrl +
'podcast',
child: Container(
child: ClipRRect(
borderRadius:
BorderRadius.all(
Radius.circular(
_width /
32)),
child: Container(
height: _width / 16,
width: _width / 16,
child: Image.file(File(
"${episodeBrief.imagePath}")),
),
height: _width / 16,
width: _width / 16,
child: CircleAvatar(
backgroundImage:
FileImage(File(
"${episodeBrief.imagePath}")),
),
),
),
@ -527,7 +584,10 @@ class _AboutPodcastState extends State<AboutPodcast> {
: Text(_description),
);
} else {
return SelectableText(_description, toolbarOptions: ToolbarOptions(copy: true),);
return SelectableText(
_description,
toolbarOptions: ToolbarOptions(copy: true),
);
}
},
);

View File

@ -1,4 +1,3 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart';
@ -10,6 +9,7 @@ import 'package:tsacdop/class/podcast_group.dart';
import 'package:tsacdop/class/podcastlocal.dart';
import 'package:tsacdop/podcasts/podcastdetail.dart';
import 'package:tsacdop/util/pageroute.dart';
import 'package:tsacdop/util/colorize.dart';
class PodcastGroupList extends StatefulWidget {
final PodcastGroup group;
@ -126,7 +126,6 @@ class _PodcastCardState extends State<PodcastCard> {
bool _addGroup;
List<PodcastGroup> _selectedGroups;
List<PodcastGroup> _belongGroups;
Color _c;
@override
void initState() {
@ -149,18 +148,10 @@ class _PodcastCardState extends State<PodcastCard> {
@override
Widget build(BuildContext context) {
var color = json.decode(widget.podcastLocal.primaryColor);
if (Theme.of(context).brightness == Brightness.light) {
(color[0] > 200 && color[1] > 200 && color[2] > 200)
? _c = Color.fromRGBO(
(255 - color[0]), 255 - color[1], 255 - color[2], 1.0)
: _c = Color.fromRGBO(color[0], color[1], color[2], 1.0);
} else {
(color[0] < 50 && color[1] < 50 && color[2] < 50)
? _c = Color.fromRGBO(
(255 - color[0]), 255 - color[1], 255 - color[2], 1.0)
: _c = Color.fromRGBO(color[0], color[1], color[2], 1.0);
}
Color _c = (Theme.of(context).brightness == Brightness.light)
? widget.podcastLocal.primaryColor.colorizedark()
: widget.podcastLocal.primaryColor.colorizeLight();
double _width = MediaQuery.of(context).size.width;
var _groupList = Provider.of<GroupList>(context);
_belongGroups = _groupList.getPodcastGroup(widget.podcastLocal.id);
@ -340,25 +331,51 @@ class _PodcastCardState extends State<PodcastCard> {
}),
_buttonOnMenu(Icon(Icons.notifications), () {}),
_buttonOnMenu(Icon(Icons.remove_circle), () {
showDialog(
context: context,
child:
AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle(
systemNavigationBarColor:
Colors.black.withOpacity(0.5),
statusBarColor: Colors.red,
),
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: 2.0,
elevation: 1,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(10.0))),
titlePadding: EdgeInsets.only(
top: 20,
left: 20,
right: 200,
bottom: 20),
title: Text('Remove confirm'),
content: Text(
'${widget.podcastLocal.title} will be removed from device.'),
'Are you sure you want to unsubscribe?'),
actions: <Widget>[
FlatButton(
onPressed: () =>
Navigator.of(context).pop(),
child: Text('CANCEL'),
child: Text('CANCEL', style: TextStyle(color: Colors.grey[600]),),
),
FlatButton(
onPressed: () {
@ -374,7 +391,9 @@ class _PodcastCardState extends State<PodcastCard> {
)
],
),
));
),
),
);
}),
],
),

View File

@ -99,7 +99,7 @@ class _PodcastListState extends State<PodcastList> {
statusBarColor: Theme.of(context).primaryColor,
),
child: SafeArea(
child: Scaffold(
child: Scaffold(
appBar: AppBar(
title: Text('Podcasts'),
centerTitle: true,
@ -116,8 +116,7 @@ class _PodcastListState extends State<PodcastList> {
SliverPadding(
padding: const EdgeInsets.all(10.0),
sliver: SliverGrid(
gridDelegate:
SliverGridDelegateWithFixedCrossAxisCount(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
childAspectRatio: 0.8,
crossAxisCount: 3,
),
@ -164,7 +163,9 @@ class _PodcastListState extends State<PodcastList> {
child: Text(
snapshot.data[index].title,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyText1,
style: Theme.of(context)
.textTheme
.bodyText1,
maxLines: 2,
),
),

View File

@ -34,76 +34,85 @@ class _PodcastManageState extends State<PodcastManage> {
statusBarColor: Theme.of(context).primaryColor,
),
child: SafeArea(
child: Scaffold(
appBar: AppBar(
centerTitle: true,
title: Text('Groups'),
actions: <Widget>[
IconButton(
onPressed: () => showDialog(
context: context,
builder: (BuildContext context) => AddGroup()),
icon: Icon(Icons.add)),
OrderMenu(),
],
),
body: Consumer<GroupList>(builder: (_, groupList, __) {
bool _isLoading = groupList.isLoading;
List<PodcastGroup> _groups = groupList.groups;
return _isLoading
? Center()
: DefaultTabController(
length: _groups.length,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(
height: 50,
padding: EdgeInsets.symmetric(horizontal: 10.0),
alignment: Alignment.centerLeft,
child: TabBar(
// labelColor: Colors.black,
// unselectedLabelColor: Colors.grey[500],
labelPadding: EdgeInsets.all(5.0),
indicator: getIndicator(),
isScrollable: true,
tabs: _groups.map<Tab>((group) {
return Tab(
child: Container(
height: 30.0,
padding:
EdgeInsets.symmetric(horizontal: 10.0),
alignment: Alignment.center,
decoration: BoxDecoration(
color: Theme.of(context).brightness ==
Brightness.light
? Theme.of(context).primaryColorDark
: Colors.grey[800],
borderRadius:
BorderRadius.all(Radius.circular(15)),
),
child: Text(
group.name,
)),
);
}).toList(),
),
),
Expanded(
child: Container(
child: TabBarView(
children: _groups.map<Widget>((group) {
return Container(
key: ObjectKey(group),
child: PodcastGroupList(group: group));
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()
: DefaultTabController(
length: _groups.length,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(
height: 50,
padding: EdgeInsets.symmetric(horizontal: 10.0),
alignment: Alignment.centerLeft,
child: TabBar(
// labelColor: Colors.black,
// unselectedLabelColor: Colors.grey[500],
labelPadding: EdgeInsets.all(5.0),
indicator: getIndicator(),
isScrollable: true,
tabs: _groups.map<Tab>((group) {
return Tab(
child: Container(
height: 30.0,
padding: EdgeInsets.symmetric(
horizontal: 10.0),
alignment: Alignment.center,
decoration: BoxDecoration(
color: Theme.of(context).brightness ==
Brightness.light
? Theme.of(context).primaryColorDark
: Colors.grey[800],
borderRadius: BorderRadius.all(
Radius.circular(15)),
),
child: Text(
group.name,
)),
);
}).toList(),
),
),
)
],
));
}),
Expanded(
child: Container(
child: TabBarView(
children: _groups.map<Widget>((group) {
return Container(
key: ObjectKey(group),
child: PodcastGroupList(group: group));
}).toList(),
),
),
)
],
));
}),
),
),
),
);
@ -114,26 +123,28 @@ class OrderMenu extends StatelessWidget {
@override
Widget build(BuildContext context) {
return PopupMenuButton(
elevation: 3,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(10))),
elevation: 2,
tooltip: 'Menu',
itemBuilder: (context) => [
PopupMenuItem(
value: 1,
child: Text('All Podcasts'),
child: Row(
children: <Widget>[
Icon(Icons.all_out),
Padding(
padding: EdgeInsets.symmetric(horizontal: 5.0),
),
Text('All Podcasts'),
],
),
),
PopupMenuItem(
value: 2,
child: Text('New group'),
)
],
onSelected: (value) {
if (value == 1) {
Navigator.push(context, ScaleRoute(page: PodcastList()));
}
if (value == 2) {
showDialog(
context: context, builder: (BuildContext context) => AddGroup());
}
},
);
}
@ -167,72 +178,82 @@ 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(
systemNavigationBarColor: Colors.black.withOpacity(0.5),
statusBarColor: Colors.red,
),
child: AlertDialog(
elevation: 1,
contentPadding: EdgeInsets.symmetric(horizontal: 20),
titlePadding: EdgeInsets.only(top: 20, left: 20, right: 20, 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'),
)
],
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: Colors.blue, width: 2.0),
),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(color: Colors.blue, width: 2.0),
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]),
),
),
cursorRadius: Radius.circular(2),
autofocus: true,
maxLines: 1,
controller: _controller,
onChanged: (value) {
_newGroup = value;
},
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(),
),
],
),
Container(
alignment: Alignment.centerLeft,
child: (_error == 1)
? Text(
'Group existed',
style: TextStyle(color: Colors.red[400]),
)
: Center(),
),
],
),
),
);
),
));
}
}

141
lib/settings/settting.dart Normal file
View File

@ -0,0 +1,141 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:tsacdop/settings/theme.dart';
class Settings extends StatelessWidget {
@override
Widget build(BuildContext context) {
return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle(
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
systemNavigationBarColor: Theme.of(context).primaryColor,
statusBarColor: Theme.of(context).primaryColor,
),
child: SafeArea(
child: Scaffold(
appBar: AppBar(
title: Text('Settings'),
elevation: 0,
backgroundColor: Theme.of(context).primaryColor,
),
body: Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Padding(
padding: EdgeInsets.all(10.0),
),
Container(
height: 30.0,
padding: EdgeInsets.symmetric(horizontal: 80),
alignment: Alignment.centerLeft,
child: Text('Prefrence',
style: Theme.of(context)
.textTheme
.bodyText1
.copyWith(color: Theme.of(context).accentColor)),
),
ListView(
shrinkWrap: true,
scrollDirection: Axis.vertical,
children: <Widget>[
ListTile(
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ThemeSetting())),
contentPadding: EdgeInsets.symmetric(horizontal: 25.0),
leading: Icon(Icons.colorize),
title: Text('Appearance'),
subtitle: Text('Colors and themes'),
),
Divider(height: 2),
ListTile(
contentPadding: EdgeInsets.symmetric(horizontal: 25.0),
leading: Icon(Icons.network_check),
title: Text('Network'),
subtitle: Text('Download network setting'),
),
Divider(height: 2),
ListTile(
contentPadding: EdgeInsets.symmetric(horizontal: 25.0),
leading: Icon(Icons.storage),
title: Text('Cache'),
subtitle: Text('Manage and clear cache'),
),
Divider(height: 2),
ListTile(
contentPadding: EdgeInsets.symmetric(horizontal: 25.0),
leading: Icon(Icons.update),
title: Text('Update'),
subtitle: Text('Update in background'),
),
Divider(height: 2),
],
),
],
),
Padding(
padding: EdgeInsets.all(10.0),
),
Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(
height: 30.0,
padding: EdgeInsets.symmetric(horizontal: 80),
alignment: Alignment.centerLeft,
child: Text('Info',
style: Theme.of(context)
.textTheme
.bodyText1
.copyWith(color: Theme.of(context).accentColor)),
),
ListView(
shrinkWrap: true,
scrollDirection: Axis.vertical,
children: <Widget>[
ListTile(
contentPadding: EdgeInsets.symmetric(horizontal: 25.0),
leading: Icon(Icons.colorize),
title: Text('Changelog'),
subtitle: Text('List of chagnes'),
),
Divider(height: 2),
ListTile(
contentPadding: EdgeInsets.symmetric(horizontal: 25.0),
leading: Icon(Icons.network_check),
title: Text('Credit'),
subtitle: Text('Open source libraried in application'),
),
Divider(height: 2),
ListTile(
contentPadding: EdgeInsets.symmetric(horizontal: 25.0),
leading: Icon(Icons.storage),
title: Text('Cache'),
subtitle: Text('Manage and clear cache'),
),
Divider(height: 2),
ListTile(
contentPadding: EdgeInsets.symmetric(horizontal: 25.0),
leading: Icon(Icons.update),
title: Text('Update'),
subtitle: Text('Update in background'),
),
Divider(height: 2),
],
),
],
),
],
),
),
),
);
}
}

191
lib/settings/theme.dart Normal file
View File

@ -0,0 +1,191 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:flutter_colorpicker/flutter_colorpicker.dart';
import 'package:tsacdop/class/settingstate.dart';
class ThemeSetting extends StatelessWidget {
@override
Widget build(BuildContext context) {
var settings = Provider.of<SettingState>(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('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();
}),
],
),
),
))),
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),
],
),
],
),
],
),
),
),
);
}
}

29
lib/util/colorize.dart Normal file
View File

@ -0,0 +1,29 @@
import 'dart:convert';
import 'package:flutter/material.dart';
extension Colorize on String {
Color colorizedark() {
Color _c;
var color = json.decode(this);
if (color[0] > 200 && color[1] > 200 && color[2] > 200) {
_c =
Color.fromRGBO((255 - color[0]), 255 - color[1], 255 - color[2], 1.0);
} else {
_c = Color.fromRGBO(color[0], color[1] > 200 ? 190 : color[1],
color[2] > 200 ? 190 : color[2], 1);
}
return _c;
}
Color colorizeLight() {
Color _c;
var color = json.decode(this);
if (color[0] < 50 && color[1] < 50 && color[2] < 50) {
_c =
Color.fromRGBO((255 - color[0]), 255 - color[1], 255 - color[2], 1.0);
} else {
_c = Color.fromRGBO(color[0], color[1], color[2], 1.0);
}
return _c;
}
}

View File

@ -1,4 +1,3 @@
import 'dart:convert';
import 'dart:io';
import 'dart:isolate';
import 'dart:ui';
@ -9,6 +8,7 @@ import 'package:google_fonts/google_fonts.dart';
import 'package:tsacdop/class/episodebrief.dart';
import 'package:tsacdop/episodes/episodedetail.dart';
import 'package:tsacdop/util/pageroute.dart';
import 'package:tsacdop/util/colorize.dart';
class EpisodeGrid extends StatelessWidget {
final List<EpisodeBrief> podcast;
@ -28,8 +28,7 @@ class EpisodeGrid extends StatelessWidget {
@override
Widget build(BuildContext context) {
double _width = MediaQuery.of(context).size.width;
return
CustomScrollView(
return CustomScrollView(
physics: const AlwaysScrollableScrollPhysics(),
primary: false,
slivers: <Widget>[
@ -44,19 +43,10 @@ class EpisodeGrid extends StatelessWidget {
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
Color _c;
var color = json.decode(podcast[index].primaryColor);
if (Theme.of(context).brightness == Brightness.light) {
(color[0] > 200 && color[1] > 200 && color[2] > 200)
? _c = Color.fromRGBO(
(255 - color[0]), 255 - color[1], 255 - color[2], 1.0)
: _c = Color.fromRGBO(color[0], color[1], color[2], 1.0);
} else {
(color[0] < 50 && color[1] < 50 && color[2] < 50)
? _c = Color.fromRGBO(
(255 - color[0]), 255 - color[1], 255 - color[2], 1.0)
: _c = Color.fromRGBO(color[0], color[1], color[2], 1.0);
}
Color _c =
(Theme.of(context).brightness == Brightness.light)
? podcast[index].primaryColor.colorizedark()
: podcast[index].primaryColor.colorizeLight();
return Material(
color: Colors.transparent,
child: InkWell(
@ -101,15 +91,11 @@ class EpisodeGrid extends StatelessWidget {
Hero(
tag: podcast[index].enclosureUrl + heroTag,
child: Container(
child: ClipRRect(
borderRadius: BorderRadius.all(
Radius.circular(_width / 32)),
child: Container(
height: _width / 16,
width: _width / 16,
child: Image.file(File(
"${podcast[index].imagePath}")),
),
height: _width / 16,
width: _width / 16,
child: CircleAvatar(
backgroundImage: FileImage(
File("${podcast[index].imagePath}")),
),
),
),

View File

@ -118,6 +118,13 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.3"
flutter_colorpicker:
dependency: "direct dev"
description:
name: flutter_colorpicker
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.3.2"
flutter_downloader:
dependency: "direct dev"
description:
@ -149,6 +156,13 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.1.3"
font_awesome_flutter:
dependency: "direct dev"
description:
name: font_awesome_flutter
url: "https://pub.flutter-io.cn"
source: hosted
version: "8.7.0"
google_fonts:
dependency: "direct dev"
description:
@ -448,6 +462,13 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.0.8"
workmanager:
dependency: "direct dev"
description:
name: workmanager
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.2.0"
xml:
dependency: "direct dev"
description:

View File

@ -14,7 +14,7 @@ description: An easy-use podacasts player.
version: 0.1.1
environment:
sdk: ">=2.3.0 <3.0.0"
sdk: ">=2.6.0 <3.0.0"
dependencies:
flutter:
@ -27,14 +27,14 @@ dependencies:
dev_dependencies:
flutter_test:
sdk: flutter
json_annotation: any
sqflite: any
json_annotation: ^3.0.1
sqflite: ^1.2.1
flutter_html: ^0.11.1
path_provider: any
path_provider: ^1.6.1
color_thief_flutter: ^1.0.1
provider: ^4.0.1
google_fonts: ^0.3.2
dio: ^3.0.8
dio: ^3.0.9
file_picker: ^1.2.0
xml: ^3.5.0
marquee: ^1.3.1
@ -49,7 +49,9 @@ dev_dependencies:
uuid: ^2.0.4
tuple: ^1.0.3
cached_network_image: ^2.0.0
workmanager: ^0.2.0
font_awesome_flutter: ^8.7.0
flutter_colorpicker: ^0.3.2