Change audio plugin to just_audio

Add storage setting
Add history setting
This commit is contained in:
stonegate 2020-03-14 11:14:24 +08:00
parent f195d62b07
commit cb5b17bba1
84 changed files with 3033 additions and 1618 deletions

View File

@ -13,9 +13,7 @@ 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

View File

@ -1,19 +1,21 @@
# Tsacdop
[![CircleCI](https://circleci.com/gh/stonega/tsacdop.svg?style=svg)](https://circleci.com/gh/stonega/workflows/tsacdop/)
## About
![logo](https://raw.githubusercontent.com/stonega/tsacdop/master/android/app/src/main/res/mipmap-hdpi/ic_launcher.png)
![tsacdop](https://raw.githubusercontent.com/stonega/tsacdop/master/android/app/src/main/res/mipmap-hdpi/text.png)
<p align="center">
<img src="https://raw.githubusercontent.com/stonega/tsacdop/master/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png" art = "Logo"/>
<img src="https://raw.githubusercontent.com/stonega/tsacdop/master/android/app/src/main/res/mipmap-xxhdpi/text.png" art = "Tsacdop"/>
</p>
Enjoy podcasts with Tsacdop.
Tsacdop is a podcasts player developed with flutter, only for Android right now.
Tsacdop is a podcast player developed with flutter, only for Android right now.
The development is still on early stage.
Credit to flutter team and involved plugin developers, especially [webfeed](https://github.com/witochandra/webfeed) and [audiofileplayer](https://github.com/google/flutter.plugins/tree/master/packages/audiofileplayer/).
Credit to flutter team and involved plugin developers, especially [webfeed](https://github.com/witochandra/webfeed) and [Just_Audio](https://pub.dev/packages/just_audio).
The podcasts search engine is powered by [ListenNotes](https://listennotes.com).
## Preview
![homepage](https://raw.githubusercontent.com/stonega/tsacdop/master/preview/Screenshot_homepage.png) ![podcast](https://raw.githubusercontent.com/stonega/tsacdop/master/preview/Screenshot_podcast.png) ![episode](https://raw.githubusercontent.com/stonega/tsacdop/master/preview/Screenshot_episode.png) ![data](https://raw.githubusercontent.com/stonega/tsacdop/master/preview/Screenshot_data.png)
## License
Tsacdop is licensed under the [MIT](https://github.com/stonega/tsacdop/blob/master/LICENSE) license.

View File

@ -67,6 +67,7 @@ android {
buildTypes {
release {
signingConfig signingConfigs.release
shrinkResources false
}
}

View File

@ -6,15 +6,16 @@
FlutterApplication and put your custom class here. -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<application android:name="io.flutter.app.FlutterApplication" android:label="Tsacdop" android:icon="@mipmap/ic_launcher" android:networkSecurityConfig="@xml/network_security_config">
<activity android:name=".MainActivity" android:launchMode="singleTop" android:theme="@style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data android:name="io.flutter.embedding.android.SplashScreenUntilFirstFrame" android:value="true" />
<!-- <meta-data android:name="io.flutter.embedding.android.SplashScreenUntilFirstFrame" android:value="true" /> -->
</activity>
<service android:name="com.google.flutter.plugins.audiofileplayer.AudiofileplayerService">
<service android:name="com.ryanheise.audioservice.AudioService">
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 272 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 550 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 460 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 435 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 411 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 488 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 522 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<color name = "blackGrey">
#121212
</color>
</resources>

View File

@ -1,7 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">lizhi.fm</domain>
<domain includeSubdomains="true">xmcdn.com</domain>
</domain-config>
<base-config cleartextTrafficPermitted="true">
<trust-anchors>
<certificates src="system" />
</trust-anchors>
</base-config>
</network-security-config>

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
assets/text_light.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -1,17 +1,46 @@
import 'dart:typed_data';
import 'dart:async';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:audio_service/audio_service.dart';
import 'package:just_audio/just_audio.dart';
import 'package:tsacdop/class/episodebrief.dart';
import 'package:audiofileplayer/audiofileplayer.dart';
import 'package:flutter_downloader/flutter_downloader.dart';
import 'package:logging/logging.dart';
import 'package:audiofileplayer/audio_system.dart';
import 'package:tsacdop/local_storage/key_value_storage.dart';
import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
//enum AudioState { load, play, pause, complete, error, stop }
MediaControl playControl = MediaControl(
androidIcon: 'drawable/ic_stat_play_circle_filled',
label: 'Play',
action: MediaAction.play,
);
MediaControl pauseControl = MediaControl(
androidIcon: 'drawable/ic_stat_pause_circle_filled',
label: 'Pause',
action: MediaAction.pause,
);
MediaControl skipToNextControl = MediaControl(
androidIcon: 'drawable/baseline_skip_next_white_24',
label: 'Next',
action: MediaAction.skipToNext,
);
MediaControl skipToPreviousControl = MediaControl(
androidIcon: 'drawable/ic_action_skip_previous',
label: 'Previous',
action: MediaAction.skipToPrevious,
);
MediaControl stopControl = MediaControl(
androidIcon: 'drawable/baseline_close_white_24',
label: 'Stop',
action: MediaAction.stop,
);
MediaControl forward30 = MediaControl(
androidIcon: 'drawable/ic_stat_forward_30',
label: 'forward30',
action: MediaAction.fastForward,
);
void _audioPlayerTaskEntrypoint() async {
AudioServiceBackground.run(() => AudioPlayerTask());
}
class PlayHistory {
DBHelper dbHelper = DBHelper();
@ -19,7 +48,9 @@ class PlayHistory {
String url;
double seconds;
double seekValue;
PlayHistory(this.title, this.url, this.seconds, this.seekValue);
DateTime playdate;
PlayHistory(this.title, this.url, this.seconds, this.seekValue,
{this.playdate});
EpisodeBrief _episode;
EpisodeBrief get episode => _episode;
@ -31,32 +62,36 @@ class PlayHistory {
class Playlist {
String name;
DBHelper dbHelper = DBHelper();
List<String> urls;
// list of urls
//List<String> _urls;
//list of episodes
List<EpisodeBrief> _playlist;
//list of miediaitem
List<EpisodeBrief> get playlist => _playlist;
KeyValueStorage storage = KeyValueStorage('playlist');
Playlist(this.name, {List<String> urls}) : urls = urls ?? [];
getPlaylist() async {
List<String> _urls = await storage.getStringList();
if (_urls.length == 0) {
List<String> urls = await storage.getStringList();
print(urls);
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);
});
}
print(_playlist.length);
print('Playlist: ' + _playlist.length.toString());
}
savePlaylist() async {
urls = [];
List<String> urls = [];
urls.addAll(_playlist.map((e) => e.enclosureUrl));
print(urls);
await storage.saveStringlist(urls);
await storage.saveStringList(urls);
}
addToPlayList(EpisodeBrief episodeBrief) async {
@ -64,6 +99,11 @@ class Playlist {
await savePlaylist();
}
addToPlayListAt(EpisodeBrief episodeBrief, int index) async {
_playlist.insert(index, episodeBrief);
await savePlaylist();
}
delFromPlaylist(EpisodeBrief episodeBrief) async {
_playlist
.removeWhere((item) => item.enclosureUrl == episodeBrief.enclosureUrl);
@ -71,39 +111,32 @@ class Playlist {
}
}
class AudioPlayer extends ChangeNotifier {
static const String replay10ButtonId = 'replay10ButtonId';
static const String newReleasesButtonId = 'newReleasesButtonId';
static const String likeButtonId = 'likeButtonId';
static const String pausenowButtonId = 'pausenowButtonId';
static const String forwardButtonId = 'forwardButtonId';
class AudioPlayerNotifier extends ChangeNotifier {
DBHelper dbHelper = DBHelper();
KeyValueStorage storage = KeyValueStorage('audioposition');
EpisodeBrief _episode;
Playlist _queue = Playlist('now');
Playlist _queue = Playlist();
BasicPlaybackState _audioState = BasicPlaybackState.none;
bool _playerRunning = false;
Audio _backgroundAudio;
bool _backgroundAudioPlaying = false;
double _backgroundAudioDurationSeconds = 0;
double _backgroundAudioPositionSeconds = 0;
bool _remoteAudioLoading = false;
bool _noSlide = true;
int _backgroundAudioDuration = 0;
int _backgroundAudioPosition = 0;
String _remoteErrorMessage;
double _seekSliderValue = 0.0;
int _lastPostion;
bool _skip = false;
int _lastPostion = 0;
bool _stopOnComplete = false;
Timer _stopTimer;
//Show stopwatch after user setting timer.
bool _showStopWatch = false;
final Logger _logger = Logger('audiofileplayer');
bool _showStopWatch = false;
bool _autoPlay = true;
DateTime _current;
int _currentPosition;
bool get backgroundAudioPlaying => _backgroundAudioPlaying;
bool get remoteAudioLoading => _remoteAudioLoading;
double get backgroundAudioDuration => _backgroundAudioDurationSeconds;
double get backgroundAudioPosition => _backgroundAudioPositionSeconds;
BasicPlaybackState get audioState => _audioState;
int get backgroundAudioDuration => _backgroundAudioDuration;
int get backgroundAudioPosition => _backgroundAudioPosition;
double get seekSliderValue => _seekSliderValue;
String get remoteErrorMessage => _remoteErrorMessage;
bool get playerRunning => _playerRunning;
@ -112,385 +145,470 @@ class AudioPlayer extends ChangeNotifier {
EpisodeBrief get episode => _episode;
bool get stopOnComplete => _stopOnComplete;
bool get showStopWatch => _showStopWatch;
bool get autoPlay => _autoPlay;
set setStopOnComplete(bool boo) {
_stopOnComplete = boo;
}
set autoPlaySwitch(bool boo) {
_autoPlay = boo;
notifyListeners();
}
@override
void addListener(VoidCallback listener) async {
super.addListener(listener);
await AudioService.connect();
}
loadPlaylist() async {
await _queue.getPlaylist();
_lastPostion = await storage.getInt();
}
episodeLoad(EpisodeBrief episode) async {
if (_playerRunning && _episode != null) {
if (_playerRunning) {
PlayHistory history = PlayHistory(_episode.title, _episode.enclosureUrl,
backgroundAudioDuration, seekSliderValue);
backgroundAudioPosition / 1000, seekSliderValue);
await dbHelper.saveHistory(history);
AudioService.addQueueItemAt(episode.toMediaItem(), 0);
_queue.playlist
.removeWhere((item) => item.enclosureUrl == episode.enclosureUrl);
_queue.playlist.insert(0, episode);
notifyListeners();
await _queue.savePlaylist();
} else {
await _queue.getPlaylist();
_queue.playlist
.removeWhere((item) => item.enclosureUrl == episode.enclosureUrl);
_queue.playlist.insert(0, episode);
_queue.savePlaylist();
_backgroundAudioDuration = 0;
_backgroundAudioPosition = 0;
_seekSliderValue = 0;
_episode = episode;
_playerRunning = true;
notifyListeners();
await _queue.savePlaylist();
_startAudioService(0);
}
AudioSystem.instance.addMediaEventListener(_mediaEventListener);
_backgroundAudioPlaying = false;
_episode = episode;
await _queue.getPlaylist();
_queue.playlist
.removeWhere((item) => item.enclosureUrl == _episode.enclosureUrl);
_queue.playlist.insert(0, _episode);
await _queue.savePlaylist();
await _play(_episode);
}
_startAudioService(int position) async {
if (!AudioService.connected) {
await AudioService.connect();
}
await AudioService.start(
backgroundTaskEntrypoint: _audioPlayerTaskEntrypoint,
androidNotificationChannelName: 'Tsacdop',
notificationColor: 0xFF2196f3,
androidNotificationIcon: 'mipmap/ic_launcher',
enableQueue: true,
androidStopOnRemoveTask: true,
);
_playerRunning = true;
notifyListeners();
if (autoPlay) {
await Future.forEach(_queue.playlist, (episode) async {
await AudioService.addQueueItem(episode.toMediaItem());
});
} else {
await AudioService.addQueueItem(_queue.playlist.first.toMediaItem());
}
await AudioService.play();
AudioService.currentMediaItemStream.listen((item) async {
print(position);
print(_backgroundAudioDuration);
if (item != null) {
_episode = await dbHelper.getRssItemWithMediaId(item.id);
_backgroundAudioDuration = item?.duration ?? 0;
if (position > 0 && _backgroundAudioDuration > 0) {
AudioService.seekTo(position);
position = 0;
}
// _playerRunning = true;
}
notifyListeners();
});
AudioService.playbackStateStream.listen((event) async {
_current = DateTime.now();
_audioState = event?.basicState;
if (_audioState == BasicPlaybackState.skippingToNext &&
_episode != null) {
_queue.delFromPlaylist(_episode);
}
if (_audioState == BasicPlaybackState.paused ||
_audioState == BasicPlaybackState.skippingToNext &&
_episode != null) {
PlayHistory history = PlayHistory(_episode.title, _episode.enclosureUrl,
backgroundAudioPosition / 1000, seekSliderValue);
await dbHelper.saveHistory(history);
}
if (_audioState == BasicPlaybackState.stopped) {
_playerRunning = false;
}
_currentPosition = event?.currentPosition ?? 0;
notifyListeners();
});
Timer.periodic(Duration(milliseconds: 500), (timer) {
if (_noSlide) {
_audioState == BasicPlaybackState.playing
? (_backgroundAudioPosition < _backgroundAudioDuration)
? _backgroundAudioPosition = _currentPosition +
DateTime.now().difference(_current).inMilliseconds
: _backgroundAudioPosition = _backgroundAudioDuration
: _backgroundAudioPosition = _currentPosition;
if (_backgroundAudioDuration != null &&
_backgroundAudioDuration != 0 &&
_backgroundAudioPosition != null) {
_seekSliderValue =
_backgroundAudioPosition / _backgroundAudioDuration ?? 0;
}
if (_backgroundAudioPosition > 0) {
_lastPostion = _backgroundAudioPosition;
storage.saveInt(_lastPostion);
}
notifyListeners();
}
if (_audioState == BasicPlaybackState.stopped) {
timer.cancel();
}
});
}
playlistLoad() async {
_backgroundAudioPlaying = false;
await _queue.getPlaylist();
_backgroundAudioDuration = 0;
_backgroundAudioPosition = 0;
_seekSliderValue = 0;
_episode = _queue.playlist.first;
_skip = true;
await _play(_episode);
_playerRunning = true;
notifyListeners();
_startAudioService(_lastPostion ?? 0);
}
playNext() async {
storage.saveInt(0);
_lastPostion = 0;
PlayHistory history = PlayHistory(_episode.title, _episode.enclosureUrl,
backgroundAudioDuration, seekSliderValue);
await dbHelper.saveHistory(history);
await _queue.delFromPlaylist(_episode);
if (_queue.playlist.length > 0 && !_stopOnComplete) {
playlistLoad();
} else {
_stopOnComplete = false;
_backgroundAudioPlaying = false;
_remoteAudioLoading = false;
_playerRunning = false;
_disposeAudio();
notifyListeners();
}
AudioService.skipToNext();
}
addToPlaylist(EpisodeBrief episode) async {
_queue.addToPlayList(episode);
await _queue.getPlaylist();
if (_playerRunning) {
await AudioService.addQueueItem(episode.toMediaItem());
}
print('add to playlist when not rnnning');
await _queue.addToPlayList(episode);
notifyListeners();
}
addToPlaylistAt(EpisodeBrief episode, int index) async {
if (_playerRunning) {
await AudioService.addQueueItemAt(episode.toMediaItem(), index);
}
print('add to playlist when not rnnning');
await _queue.addToPlayListAt(episode, index);
notifyListeners();
}
updateMediaItem(EpisodeBrief episode) async {
int index = _queue.playlist
.indexWhere((item) => item.enclosureUrl == episode.enclosureUrl);
if (index > 0) {
await delFromPlaylist(episode);
await addToPlaylistAt(episode, index);
}
}
delFromPlaylist(EpisodeBrief episode) async {
_queue.delFromPlaylist(episode);
await _queue.getPlaylist();
if (_playerRunning) {
await AudioService.removeQueueItem(episode.toMediaItem());
}
await _queue.delFromPlaylist(episode);
notifyListeners();
}
pauseAduio() async {
_pauseBackgroundAudio();
notifyListeners();
PlayHistory history = PlayHistory(_episode.title, _episode.enclosureUrl,
backgroundAudioPosition, seekSliderValue);
await dbHelper.saveHistory(history);
AudioService.pause();
}
resumeAudio() {
_resumeBackgroundAudio();
notifyListeners();
resumeAudio() async {
AudioService.play();
}
forwardAudio(double s) {
_forwardBackgroundAudio(s);
notifyListeners();
forwardAudio(int s) {
int pos = _backgroundAudioPosition + s * 1000;
AudioService.seekTo(pos);
}
sliderSeek(double val) {
sliderSeek(double val) async {
print(val.toString());
_noSlide = false;
_seekSliderValue = val;
notifyListeners();
final double positionSeconds = val * _backgroundAudioDurationSeconds;
_backgroundAudio.seek(positionSeconds);
AudioSystem.instance.setPlaybackState(true, positionSeconds);
_currentPosition = (val * _backgroundAudioDuration).toInt();
await AudioService.seekTo(_currentPosition);
_noSlide = true;
}
//Set sleep time
//Set sleep time
sleepTimer(int mins) {
_showStopWatch = true;
notifyListeners();
_stopTimer = Timer(Duration(minutes: mins),(){
_stopTimer = Timer(Duration(minutes: mins), () {
_stopOnComplete = false;
_backgroundAudioPlaying = false;
_remoteAudioLoading = false;
_playerRunning = false;
_showStopWatch = false;
_disposeAudio();
AudioService.stop();
notifyListeners();
});
}
//Cancel sleep timer
cancelTimer(){
cancelTimer() {
_stopTimer.cancel();
_showStopWatch = false;
notifyListeners();
}
_disposeAudio() {
pauseAduio();
AudioSystem.instance?.stopBackgroundDisplay();
AudioSystem.instance?.removeMediaEventListener(_mediaEventListener);
_backgroundAudio?.dispose();
@override
void dispose() async {
await AudioService.stop();
await AudioService.disconnect();
super.dispose();
}
}
class AudioPlayerTask extends BackgroundAudioTask {
List<MediaItem> _queue = [];
AudioPlayer _audioPlayer = AudioPlayer();
Completer _completer = Completer();
BasicPlaybackState _skipState;
bool _playing;
bool get hasNext => _queue.length > 0;
MediaItem get mediaItem => _queue.first;
BasicPlaybackState _stateToBasicState(AudioPlaybackState state) {
switch (state) {
case AudioPlaybackState.none:
return BasicPlaybackState.none;
case AudioPlaybackState.stopped:
return BasicPlaybackState.stopped;
case AudioPlaybackState.paused:
return BasicPlaybackState.paused;
case AudioPlaybackState.playing:
return BasicPlaybackState.playing;
case AudioPlaybackState.connecting:
return _skipState ?? BasicPlaybackState.connecting;
case AudioPlaybackState.completed:
return BasicPlaybackState.stopped;
default:
throw Exception("Illegal state");
}
}
@override
dispose() {
_disposeAudio();
super.dispose();
}
_play(EpisodeBrief episodeBrief) async {
AudioSystem.instance.addMediaEventListener(_mediaEventListener);
String url = _queue.playlist.first.enclosureUrl;
_getFile(url).then((result) {
result == 'NotDownload'
? _initbackgroundAudioPlayer(url)
: _initbackgroundAudioPlayerLocal(result);
Future<void> onStart() async {
print('start background task');
var playerStateSubscription = _audioPlayer.playbackStateStream
.where((state) => state == AudioPlaybackState.completed)
.listen((state) {
_handlePlaybackCompleted();
});
}
Future<String> _getFile(String url) async {
final task = await FlutterDownloader.loadTasksWithRawQuery(
query: "SELECT * FROM task WHERE url = '$url' AND status = 3");
if (task.length != 0) {
String _filePath = task.first.savedDir + '/' + task.first.filename;
return _filePath;
}
return 'NotDownload';
}
ByteData _getAudio(String path) {
File audioFile = File(path);
Uint8List audio = audioFile.readAsBytesSync();
return ByteData.view(audio.buffer);
}
onDuration(double durationSeconds) {
_backgroundAudioDurationSeconds = durationSeconds;
_remoteAudioLoading = false;
_backgroundAudioPlaying = true;
if (_skip) {
_forwardBackgroundAudio(_lastPostion.toDouble());
_backgroundAudioPositionSeconds = _lastPostion.toDouble();
}
_skip = false;
_setNotification(true);
notifyListeners();
}
onPosition(double positionSeconds) {
if (_backgroundAudioPositionSeconds < _backgroundAudioDurationSeconds) {
_seekSliderValue =
_backgroundAudioPositionSeconds / _backgroundAudioDurationSeconds;
_backgroundAudioPositionSeconds = positionSeconds;
notifyListeners();
} else {
_seekSliderValue = 1;
_backgroundAudioPositionSeconds = _backgroundAudioDurationSeconds;
notifyListeners();
}
_lastPostion = positionSeconds.toInt();
storage.saveInt(_lastPostion);
}
onError(String message) {
_remoteErrorMessage = message;
_backgroundAudio.dispose();
_backgroundAudio = null;
_backgroundAudioPlaying = false;
_remoteAudioLoading = false;
}
void _initbackgroundAudioPlayerLocal(String path) {
_remoteErrorMessage = null;
_remoteAudioLoading = true;
ByteData audio = _getAudio(path);
_backgroundAudio?.pause();
_backgroundAudioPositionSeconds = 0;
_setNotification(false);
_backgroundAudio = Audio.loadFromByteData(audio,
onDuration: (double durationSeconds) => onDuration(durationSeconds),
onPosition: (double positionSeconds) => onPosition(positionSeconds),
onError: (String message) => onError(message),
onComplete: () => playNext(),
looping: false,
playInBackground: true)
..play();
}
void _initbackgroundAudioPlayer(String url) {
_remoteErrorMessage = null;
_remoteAudioLoading = true;
notifyListeners();
_backgroundAudio?.pause();
_backgroundAudioPositionSeconds = 0;
_setNotification(false);
_backgroundAudio = Audio.loadFromRemoteUrl(url,
onDuration: (double durationSeconds) => onDuration(durationSeconds),
onPosition: (double positionSeconds) => onPosition(positionSeconds),
onError: (String message) => onError(message),
onComplete: () => playNext(),
looping: false,
playInBackground: true)
..resume();
}
void _mediaEventListener(MediaEvent mediaEvent) {
_logger.info('App received media event of type: ${mediaEvent.type}');
final MediaActionType type = mediaEvent.type;
if (type == MediaActionType.play) {
_resumeBackgroundAudio();
} else if (type == MediaActionType.pause) {
_pauseBackgroundAudio();
} else if (type == MediaActionType.playPause) {
_backgroundAudioPlaying
? _pauseBackgroundAudio()
: _resumeBackgroundAudio();
} else if (type == MediaActionType.stop) {
_stopBackgroundAudio();
} else if (type == MediaActionType.seekTo) {
_backgroundAudio.seek(mediaEvent.seekToPositionSeconds);
AudioSystem.instance
.setPlaybackState(true, mediaEvent.seekToPositionSeconds);
} else if (type == MediaActionType.skipForward) {
final double skipIntervalSeconds = mediaEvent.skipIntervalSeconds;
_forwardBackgroundAudio(skipIntervalSeconds);
_logger.info(
'Skip-forward event had skipIntervalSeconds $skipIntervalSeconds.');
} else if (type == MediaActionType.skipBackward) {
final double skipIntervalSeconds = mediaEvent.skipIntervalSeconds;
_forwardBackgroundAudio(skipIntervalSeconds);
_logger.info(
'Skip-backward event had skipIntervalSeconds $skipIntervalSeconds.');
} else if (type == MediaActionType.custom) {
if (mediaEvent.customEventId == replay10ButtonId) {
_forwardBackgroundAudio(-10.0);
} else if (mediaEvent.customEventId == likeButtonId) {
_resumeBackgroundAudio();
} else if (mediaEvent.customEventId == forwardButtonId) {
_forwardBackgroundAudio(30.0);
} else if (mediaEvent.customEventId == pausenowButtonId) {
_pauseBackgroundAudio();
var eventSubscription = _audioPlayer.playbackEventStream.listen((event) {
BasicPlaybackState state;
if (event.buffering) {
state = BasicPlaybackState.buffering;
} else {
state = _stateToBasicState(event.state);
}
if (state != BasicPlaybackState.stopped) {
_setState(
state: state,
position: event.position.inMilliseconds,
);
}
});
await _completer.future;
playerStateSubscription.cancel();
eventSubscription.cancel();
}
void _handlePlaybackCompleted() {
if (hasNext) {
onSkipToNext();
} else {
onStop();
}
}
Future<void> _setNotification(bool boo) async {
final Uint8List imageBytes =
File('${_episode.imagePath}').readAsBytesSync();
AudioSystem.instance.setMetadata(AudioMetadata(
title: episode.title,
artist: episode.feedTitle,
album: episode.feedTitle,
genre: "Podcast",
durationSeconds: _backgroundAudioDurationSeconds,
artBytes: imageBytes));
AudioSystem.instance.setPlaybackState(boo, _backgroundAudioPositionSeconds);
AudioSystem.instance.setAndroidNotificationButtons(<dynamic>[
AndroidMediaButtonType.pause,
_forwardButton,
AndroidMediaButtonType.stop,
], androidCompactIndices: <int>[
0,
1
]);
AudioSystem.instance.setSupportedMediaActions(<MediaActionType>{
MediaActionType.playPause,
MediaActionType.pause,
MediaActionType.next,
MediaActionType.previous,
MediaActionType.skipForward,
MediaActionType.skipBackward,
MediaActionType.seekTo,
MediaActionType.custom,
}, skipIntervalSeconds: 30);
void playPause() {
if (AudioServiceBackground.state.basicState == BasicPlaybackState.playing)
onPause();
else
onPlay();
}
Future<void> _resumeBackgroundAudio() async {
_backgroundAudio.resume();
_backgroundAudioPlaying = true;
final Uint8List imageBytes =
File('${_episode.imagePath}').readAsBytesSync();
AudioSystem.instance.setMetadata(AudioMetadata(
title: _episode.title,
artist: _episode.feedTitle,
album: _episode.feedTitle,
genre: "Podcast",
durationSeconds: _backgroundAudioDurationSeconds,
artBytes: imageBytes));
AudioSystem.instance
.setPlaybackState(true, _backgroundAudioPositionSeconds);
AudioSystem.instance.setAndroidNotificationButtons(<dynamic>[
AndroidMediaButtonType.pause,
_forwardButton,
AndroidMediaButtonType.stop,
], androidCompactIndices: <int>[
0,
1
]);
AudioSystem.instance.setSupportedMediaActions(<MediaActionType>{
MediaActionType.playPause,
MediaActionType.pause,
MediaActionType.next,
MediaActionType.previous,
MediaActionType.skipForward,
MediaActionType.skipBackward,
MediaActionType.seekTo,
MediaActionType.custom,
}, skipIntervalSeconds: 30);
@override
Future<void> onSkipToNext() async {
if (_playing == null) {
// First time, we want to start playing
_playing = true;
} else {
// Stop current item
await _audioPlayer.stop();
_queue.removeAt(0);
}
AudioServiceBackground.setQueue(_queue);
AudioServiceBackground.setMediaItem(mediaItem);
_skipState = BasicPlaybackState.skippingToNext;
await _audioPlayer.setUrl(mediaItem.id);
print(mediaItem.id);
Duration duration = await _audioPlayer.durationFuture;
AudioServiceBackground.setMediaItem(
mediaItem.copyWith(duration: duration.inMilliseconds));
_skipState = null;
// Resume playback if we were playing
if (_playing) {
onPlay();
} else {
_setState(state: BasicPlaybackState.paused);
}
}
void _pauseBackgroundAudio() {
_backgroundAudio?.pause();
_backgroundAudioPlaying = false;
AudioSystem.instance
.setPlaybackState(false, _backgroundAudioPositionSeconds);
AudioSystem.instance.setAndroidNotificationButtons(<dynamic>[
AndroidMediaButtonType.play,
_forwardButton,
AndroidMediaButtonType.stop,
], androidCompactIndices: <int>[
0,
1,
]);
AudioSystem.instance.setSupportedMediaActions(<MediaActionType>{
MediaActionType.playPause,
MediaActionType.play,
MediaActionType.next,
MediaActionType.previous,
});
@override
void onPlay() async {
if (_skipState == null) {
if (_playing == null) {
_playing = true;
AudioServiceBackground.setQueue(_queue);
await _audioPlayer.setUrl(mediaItem.id);
Duration duration = await _audioPlayer.durationFuture;
AudioServiceBackground.setMediaItem(
mediaItem.copyWith(duration: duration.inMilliseconds));
}
_playing = true;
_audioPlayer.play();
}
}
void _stopBackgroundAudio() {
_backgroundAudio.pause();
_backgroundAudio.dispose();
_backgroundAudioPlaying = false;
AudioSystem.instance.stopBackgroundDisplay();
@override
void onPause() {
if (_skipState == null) {
if (_playing == null) {}
_playing = false;
_audioPlayer.pause();
}
}
void _forwardBackgroundAudio(double seconds) {
final double forwardposition = _backgroundAudioPositionSeconds + seconds;
_backgroundAudio.seek(forwardposition);
AudioSystem.instance
.setPlaybackState(true, _backgroundAudioPositionSeconds);
@override
void onSeekTo(int position) {
_audioPlayer.seek(Duration(milliseconds: position));
}
final _pauseButton = AndroidCustomMediaButton(
'pausenow', pausenowButtonId, 'ic_stat_pause_circle_filled');
final _replay10Button = AndroidCustomMediaButton(
'replay10', replay10ButtonId, 'ic_stat_replay_10');
final _forwardButton = AndroidCustomMediaButton(
'forward', forwardButtonId, 'ic_stat_forward_30');
final _playnowButton = AndroidCustomMediaButton(
'playnow', likeButtonId, 'ic_stat_play_circle_filled');
@override
void onClick(MediaButton button) {
playPause();
}
@override
void onStop() async {
await _audioPlayer.stop();
_setState(state: BasicPlaybackState.stopped);
_completer.complete();
}
@override
void onAddQueueItem(MediaItem mediaItem) async {
_queue.add(mediaItem);
AudioServiceBackground.setQueue(_queue);
}
@override
void onRemoveQueueItem(MediaItem mediaItem) async {
_queue.removeWhere((item) => item.id == mediaItem.id);
await AudioServiceBackground.setQueue(_queue);
}
@override
void onAddQueueItemAt(MediaItem mediaItem, int index) async {
if (index == 0) {
await _audioPlayer.stop();
_queue.removeWhere((item) => item.id == mediaItem.id);
_queue.insert(0, mediaItem);
AudioServiceBackground.setQueue(_queue);
AudioServiceBackground.setMediaItem(mediaItem);
await _audioPlayer.setUrl(mediaItem.id);
Duration duration = await _audioPlayer.durationFuture;
AudioServiceBackground.setMediaItem(
mediaItem.copyWith(duration: duration.inMilliseconds));
onPlay();
} else {
_queue.insert(index, mediaItem);
AudioServiceBackground.setQueue(_queue);
}
}
@override
void onFastForward() {
_audioPlayer.seek(Duration(
milliseconds: AudioServiceBackground.state.position + 30 * 1000));
}
@override
void onAudioFocusLost() {
if (_skipState == null) {
if (_playing == null) {}
_playing = false;
_audioPlayer.pause();
}
}
@override
void onAudioBecomingNoisy() {
if (_skipState == null) {
if (_playing == null) {}
_playing = false;
_audioPlayer.pause();
}
}
@override
void onAudioFocusGained() {
if (_skipState == null) {
if (_playing == null) {}
_playing = true;
_audioPlayer.play();
}
}
@override
void onCustomAction(funtion, argument) {
switch (funtion) {
case 'addQueue':
break;
case 'updateMedia':
break;
}
}
void _setState({@required BasicPlaybackState state, int position}) {
if (position == null) {
position = _audioPlayer.playbackEvent.position.inMilliseconds;
}
AudioServiceBackground.setState(
controls: getControls(state),
systemActions: [MediaAction.seekTo],
basicState: state,
position: position,
);
}
List<MediaControl> getControls(BasicPlaybackState state) {
if (_playing) {
return [pauseControl, forward30, skipToNextControl, stopControl];
} else {
return [playControl, forward30, skipToNextControl, stopControl];
}
}
}

View File

@ -1,4 +1,5 @@
import 'package:intl/intl.dart';
import 'package:audio_service/audio_service.dart';
class EpisodeBrief {
final String title;
@ -13,6 +14,7 @@ class EpisodeBrief {
final int duration;
final int explicit;
final String imagePath;
final String mediaId;
EpisodeBrief(
this.title,
this.enclosureUrl,
@ -21,21 +23,31 @@ class EpisodeBrief {
this.feedTitle,
this.primaryColor,
this.liked,
this.downloaded,
this.downloaded,
this.duration,
this.explicit,
this.imagePath
);
this.imagePath,
this.mediaId);
String dateToString(){
DateTime date = DateTime.fromMillisecondsSinceEpoch(pubDate);
String dateToString() {
DateTime date = DateTime.fromMillisecondsSinceEpoch(pubDate, isUtc: true);
var diffrence = DateTime.now().difference(date);
if(diffrence.inHours < 24) {
if (diffrence.inHours < 24) {
return '${diffrence.inHours} hours ago';
} else if (diffrence.inDays < 7){
return '${diffrence.inDays} days ago';}
else {
return DateFormat.yMMMd().format( DateTime.fromMillisecondsSinceEpoch(pubDate));
}
} else if (diffrence.inDays < 7) {
return '${diffrence.inDays} days ago';
} else {
return DateFormat.yMMMd()
.format(DateTime.fromMillisecondsSinceEpoch(pubDate));
}
}
MediaItem toMediaItem() {
return MediaItem(
id: mediaId,
title: title,
artist: feedTitle,
album: feedTitle,
artUri: 'file://$imagePath');
}
}

View File

@ -55,8 +55,12 @@ class PodcastGroup {
}
List<PodcastLocal> _podcasts;
List<PodcastLocal> _orderedPodcasts;
List<PodcastLocal> get ordereddPodcasts => _orderedPodcasts;
List<PodcastLocal> get podcasts => _podcasts;
set setOrderedPodcasts(List<PodcastLocal> list) {
_orderedPodcasts = list;
}
GroupEntity toEntity() {
return GroupEntity(name, id, color, podcastList);
@ -82,9 +86,20 @@ class GroupList extends ChangeNotifier {
GroupList({List<PodcastGroup> groups}) : _groups = groups ?? [];
bool _isLoading = false;
bool get isLoading => _isLoading;
List<String> _orderChanged = [];
List<String> get orderChanged => _orderChanged;
void addToOrderChanged(String name) {
_orderChanged.add(name);
notifyListeners();
}
void drlFromOrderChanged(String name) {
_orderChanged.remove(name);
notifyListeners();
}
@override
void addListener(VoidCallback listener) {
super.addListener(listener);
@ -105,14 +120,15 @@ class GroupList extends ChangeNotifier {
}
Future addGroup(PodcastGroup podcastGroup) async {
_isLoading = true;
_groups.add(podcastGroup);
_saveGroup();
_isLoading = false;
notifyListeners();
}
Future delGroup(PodcastGroup podcastGroup) async {
_isLoading = true;
notifyListeners();
podcastGroup.podcastList.forEach((podcast) {
if (!_groups.first.podcastList.contains(podcast)) {
_groups[0].podcastList.insert(0, podcast);
@ -121,11 +137,11 @@ class GroupList extends ChangeNotifier {
_saveGroup();
_groups.remove(podcastGroup);
await _groups[0].getPodcasts();
_isLoading = false;
_isLoading = false;
notifyListeners();
}
updateGroup(PodcastGroup podcastGroup) async{
updateGroup(PodcastGroup podcastGroup) async {
var oldGroup = _groups.firstWhere((it) => it.id == podcastGroup.id);
var index = _groups.indexOf(oldGroup);
_groups.replaceRange(index, index + 1, [podcastGroup]);
@ -161,7 +177,11 @@ class GroupList extends ChangeNotifier {
_isLoading = true;
notifyListeners();
getPodcastGroup(id).forEach((group) {
group.podcastList.remove(id);
if (list.contains(group)) {
list.remove(group);
} else {
group.podcastList.remove(id);
}
});
list.forEach((s) {
s.podcastList.insert(0, id);
@ -190,8 +210,8 @@ class GroupList extends ChangeNotifier {
notifyListeners();
}
saveOrder(PodcastGroup group, List<PodcastLocal> podcasts) async {
group.podcastList = podcasts.map((e) => e.id).toList();
saveOrder(PodcastGroup group) async {
group.podcastList = group.ordereddPodcasts.map((e) => e.id).toList();
_saveGroup();
await group.getPodcasts();
notifyListeners();

View File

@ -6,16 +6,17 @@ import 'package:tsacdop/local_storage/key_value_storage.dart';
class SettingState extends ChangeNotifier {
KeyValueStorage themestorage = KeyValueStorage('themes');
KeyValueStorage accentstorage = KeyValueStorage('accents');
bool _isLoading;
bool get isLoagding => _isLoading;
KeyValueStorage autoupdatestorage = KeyValueStorage('autoupdate');
Future initData() async {
await _getTheme();
await _getAccentSetColor();
await _getAutoUpdate();
}
ThemeMode _theme;
ThemeMode get theme => _theme;
set setTheme(ThemeMode mode) {
_theme = mode;
_saveTheme();
@ -31,11 +32,20 @@ class SettingState extends ChangeNotifier {
notifyListeners();
}
bool _autoUpdate;
bool get autoUpdate => _autoUpdate;
set autoUpdate(bool boo) {
_autoUpdate = boo;
_saveAutoUpdate();
notifyListeners();
}
@override
void addListener(VoidCallback listener) {
super.addListener(listener);
_getTheme();
_getAccentSetColor();
_getAutoUpdate();
}
_getTheme() async {
@ -62,6 +72,14 @@ class SettingState extends ChangeNotifier {
_saveAccentSetColor() async {
await accentstorage
.saveString(_accentSetColor.toString().substring(10, 16));
print(_accentSetColor.toString());
}
_getAutoUpdate() async {
int i = await autoupdatestorage.getInt();
_autoUpdate = i == 0 ? false : true;
}
_saveAutoUpdate() async {
await autoupdatestorage.saveInt(_autoUpdate ? 1 : 0);
}
}

View File

@ -0,0 +1,8 @@
class SubHistory {
DateTime subDate;
DateTime delDate;
bool status;
String title;
String rssUrl;
SubHistory(this.status, this.delDate, this.subDate, this.rssUrl, this.title);
}

View File

@ -10,6 +10,7 @@ import 'package:url_launcher/url_launcher.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:intl/intl.dart';
import 'package:tuple/tuple.dart';
import 'package:audio_service/audio_service.dart';
import 'package:tsacdop/class/audiostate.dart';
import 'package:tsacdop/class/episodebrief.dart';
import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
@ -211,7 +212,7 @@ class _EpisodeDetailState extends State<EpisodeDetail> {
],
),
),
Selector<AudioPlayer, bool>(
Selector<AudioPlayerNotifier, bool>(
selector: (_, audio) => audio.playerRunning,
builder: (_, data, __) {
return Container(
@ -290,7 +291,7 @@ class _MenuBarState extends State<MenuBar> {
@override
Widget build(BuildContext context) {
var audio = Provider.of<AudioPlayer>(context, listen: false);
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
return Container(
height: 50.0,
decoration: BoxDecoration(
@ -346,7 +347,7 @@ class _MenuBarState extends State<MenuBar> {
],
),
DownloadButton(episodeBrief: widget.episodeItem),
Selector<AudioPlayer, List<String>>(
Selector<AudioPlayerNotifier, List<String>>(
selector: (_, audio) =>
audio.queue.playlist.map((e) => e.enclosureUrl).toList(),
builder: (_, data, __) {
@ -367,9 +368,9 @@ class _MenuBarState extends State<MenuBar> {
),
Spacer(),
// Text(audio.audioState.toString()),
Selector<AudioPlayer, Tuple2<EpisodeBrief, bool>>(
Selector<AudioPlayerNotifier, Tuple2<EpisodeBrief, BasicPlaybackState>>(
selector: (_, audio) =>
Tuple2(audio.episode, audio.backgroundAudioPlaying),
Tuple2(audio.episode, audio.audioState),
builder: (_, data, __) {
return (widget.episodeItem.title != data.item1?.title)
? Material(
@ -400,7 +401,7 @@ class _MenuBarState extends State<MenuBar> {
),
)
: (widget.episodeItem.title == data.item1?.title &&
data.item2 == true)
data.item2 == BasicPlaybackState.playing)
? Container(
padding: EdgeInsets.only(right: 30),
child: SizedBox(
@ -424,9 +425,10 @@ class _MenuBarState extends State<MenuBar> {
class LinePainter extends CustomPainter {
double _fraction;
Paint _paint;
LinePainter(this._fraction) {
Color _maincolor;
LinePainter(this._fraction, this._maincolor) {
_paint = Paint()
..color = Colors.blue
..color = _maincolor
..strokeWidth = 2.0
..strokeCap = StrokeCap.round;
}
@ -483,14 +485,15 @@ class _LineLoaderState extends State<LineLoader>
@override
Widget build(BuildContext context) {
return CustomPaint(painter: LinePainter(_fraction));
return CustomPaint(painter: LinePainter(_fraction, Theme.of(context).accentColor));
}
}
class WavePainter extends CustomPainter {
double _fraction;
double _value;
WavePainter(this._fraction);
Color _color;
WavePainter(this._fraction, this._color);
@override
void paint(Canvas canvas, Size size) {
if (_fraction < 0.5) {
@ -500,7 +503,7 @@ class WavePainter extends CustomPainter {
}
Path _path = Path();
Paint _paint = Paint()
..color = Colors.blue
..color = _color
..strokeWidth = 2.0
..strokeCap = StrokeCap.round
..style = PaintingStyle.stroke;
@ -575,7 +578,7 @@ class _WaveLoaderState extends State<WaveLoader>
@override
Widget build(BuildContext context) {
return CustomPaint(painter: WavePainter(_fraction));
return CustomPaint(painter: WavePainter(_fraction, Theme.of(context).accentColor));
}
}

View File

@ -1,14 +1,17 @@
import 'dart:isolate';
import 'dart:ui';
import 'dart:io';
import 'dart:async';
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:provider/provider.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as path;
import 'package:flutter_downloader/flutter_downloader.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:tsacdop/class/episodebrief.dart';
import 'package:tsacdop/class/audiostate.dart';
import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
class DownloadButton extends StatefulWidget {
@ -74,6 +77,7 @@ class _DownloadButtonState extends State<DownloadButton> {
});
}
void _unbindBackgroundIsolate() {
IsolateNameServer.removePortNameMapping('downloader_send_port');
}
@ -96,6 +100,7 @@ class _DownloadButtonState extends State<DownloadButton> {
openFileFromNotification: false,
);
var dbHelper = DBHelper();
await dbHelper.saveDownloaded(task.link, task.taskId);
Fluttertoast.showToast(
msg: 'Downloading',
@ -103,6 +108,7 @@ class _DownloadButtonState extends State<DownloadButton> {
);
}
void _deleteDownload(_TaskInfo task) async {
await FlutterDownloader.remove(
taskId: task.taskId, shouldDeleteContent: true);
@ -146,6 +152,16 @@ class _DownloadButtonState extends State<DownloadButton> {
);
}
_saveMediaId(_TaskInfo task) async{
final completeTask = await FlutterDownloader.loadTasksWithRawQuery(
query: "SELECT * FROM task WHERE url = '${task.link}'");
String filePath = 'file://' + path.join(completeTask.first.savedDir, completeTask.first.filename);
var dbHelper = DBHelper();
await dbHelper.saveMediaId(task.link, filePath);
EpisodeBrief episode = await dbHelper.getRssItemWithUrl(task.link);
await Provider.of<AudioPlayerNotifier>(context, listen: false).updateMediaItem(episode);
}
Future<Null> _prepare() async {
final tasks = await FlutterDownloader.loadTasks();
@ -161,7 +177,7 @@ class _DownloadButtonState extends State<DownloadButton> {
}
});
_localPath = (await _getPath()) + '/' + widget.episodeBrief.feedTitle;
_localPath = path.join((await _getPath()) ,widget.episodeBrief.feedTitle);
print(_localPath);
final saveDir = Directory(_localPath);
bool hasExisted = await saveDir.exists();
@ -173,6 +189,8 @@ class _DownloadButtonState extends State<DownloadButton> {
});
}
Future<bool> _checkPermmison() async {
PermissionStatus permission = await PermissionHandler()
.checkPermissionStatus(PermissionGroup.storage);
@ -284,6 +302,7 @@ class _DownloadButtonState extends State<DownloadButton> {
),
);
} else if (task.status == DownloadTaskStatus.complete) {
_saveMediaId(task);
return _buttonOnMenu(
Icon(
Icons.done_all,
@ -307,4 +326,4 @@ class _TaskInfo {
DownloadTaskStatus status = DownloadTaskStatus.undefined;
_TaskInfo({this.name, this.link});
}
}

View File

@ -1,8 +1,7 @@
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';
import 'package:line_icons/line_icons.dart';
class AboutApp extends StatelessWidget {
_launchUrl(String url) async {
if (await canLaunch(url)) {
@ -71,7 +70,7 @@ class AboutApp extends StatelessWidget {
image: AssetImage('assets/logo.png'),
height: 80,
),
Text('Version: 0.1.1'),
Text('Version: 0.1.2'),
],
),
),
@ -79,7 +78,7 @@ class AboutApp extends StatelessWidget {
padding: EdgeInsets.symmetric(horizontal: 50),
height: 50,
child: Text(
'Tsacdop is a podcast client developed with flutter, a simple, beautiful, and easy-use player.',
'Tsacdop is a podcasts client developed influtter, a simple, beautiful, and easy-use player.',
textAlign: TextAlign.center,
),
),
@ -111,17 +110,17 @@ class AboutApp extends StatelessWidget {
_listItem(
context,
'GitHub',
FontAwesomeIcons.githubSquare,
'https://github.com/stonaga/tsacdop'),
LineIcons.github,
'https://github.com/stonaga/'),
_listItem(
context,
'Twitter',
FontAwesomeIcons.twitterSquare,
LineIcons.twitter,
'https://twitter.com'),
_listItem(
context,
'Gmail',
FontAwesomeIcons.envelopeSquare,
'Stone Gate',
LineIcons.hat_cowboy_solid,
'mailto:<xijieyin@gmail.com>?subject=Tsacdop Feedback'),
],
),

View File

@ -36,6 +36,7 @@ class _MyHomePageState extends State<MyHomePage> {
return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle(
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
systemNavigationBarIconBrightness: Theme.of(context).accentColorBrightness,
systemNavigationBarColor: Theme.of(context).primaryColor,
statusBarColor: Theme.of(context).primaryColor,
),

View File

@ -13,6 +13,7 @@ import 'package:color_thief_flutter/color_thief_flutter.dart';
import 'package:image/image.dart' as img;
import 'package:uuid/uuid.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:line_icons/line_icons.dart';
import 'package:tsacdop/class/podcast_group.dart';
import 'package:tsacdop/settings/settting.dart';
@ -142,7 +143,7 @@ class PopupMenu extends StatelessWidget {
void _saveOmpl(String path) async {
File file = File(path);
String opml = file.readAsStringSync();
try{String opml = file.readAsStringSync();
var content = xml.parse(opml);
var total = content
@ -167,6 +168,15 @@ class PopupMenu extends StatelessWidget {
}
}
print('Import fisnished');
}}
catch(e){
print(e);
Fluttertoast.showToast(
msg: 'File error, Subscribe failed',
gravity: ToastGravity.TOP,
);
await Future.delayed(Duration(seconds: 5));
importOmpl.importState = ImportState.stop;
}
}
@ -195,7 +205,7 @@ class PopupMenu extends StatelessWidget {
padding: EdgeInsets.only(left: 10),
child: Row(
children: <Widget>[
Icon(Icons.refresh),
Icon(LineIcons.cloud_download_alt_solid),
Padding(padding: EdgeInsets.symmetric(horizontal: 5.0),),
Text('Refresh All'),
],
@ -208,7 +218,7 @@ class PopupMenu extends StatelessWidget {
padding: EdgeInsets.only(left: 10),
child: Row(
children: <Widget>[
Icon(Icons.attachment),
Icon(LineIcons.paperclip_solid),
Padding(padding: EdgeInsets.symmetric(horizontal: 5.0),),
Text('Import OMPL'),
],
@ -226,7 +236,7 @@ class PopupMenu extends StatelessWidget {
padding: EdgeInsets.only(left: 10),
child: Row(
children: <Widget>[
Icon(Icons.swap_calls),
Icon(LineIcons.cog_solid),
Padding(padding: EdgeInsets.symmetric(horizontal: 5.0),),
Text('Settings'),
],
@ -239,7 +249,7 @@ class PopupMenu extends StatelessWidget {
padding: EdgeInsets.only(left: 10),
child: Row(
children: <Widget>[
Icon(Icons.info_outline),
Icon(LineIcons.info_circle_solid),
Padding(padding: EdgeInsets.symmetric(horizontal: 5.0),),
Text('About'),
],

View File

@ -7,6 +7,7 @@ import 'package:provider/provider.dart';
import 'package:marquee/marquee.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:tuple/tuple.dart';
import 'package:audio_service/audio_service.dart';
import 'package:tsacdop/class/episodebrief.dart';
import 'package:tsacdop/class/audiostate.dart';
@ -15,6 +16,97 @@ import 'package:tsacdop/home/audiopanel.dart';
import 'package:tsacdop/util/pageroute.dart';
import 'package:tsacdop/util/colorize.dart';
class MyRoundSliderThumpShape extends SliderComponentShape {
const MyRoundSliderThumpShape({
this.enabledThumbRadius = 10.0,
this.disabledThumbRadius,
this.thumbCenterColor,
});
final Color thumbCenterColor;
/// The preferred radius of the round thumb shape when the slider is enabled.
///
/// If it is not provided, then the material default of 10 is used.
final double enabledThumbRadius;
/// The preferred radius of the round thumb shape when the slider is disabled.
///
/// If no disabledRadius is provided, then it is equal to the
/// [enabledThumbRadius]
final double disabledThumbRadius;
double get _disabledThumbRadius => disabledThumbRadius ?? enabledThumbRadius;
@override
Size getPreferredSize(bool isEnabled, bool isDiscrete) {
return Size.fromRadius(
isEnabled == true ? enabledThumbRadius : _disabledThumbRadius);
}
@override
void paint(
PaintingContext context,
Offset center, {
Animation<double> activationAnimation,
@required Animation<double> enableAnimation,
bool isDiscrete,
TextPainter labelPainter,
RenderBox parentBox,
@required SliderThemeData sliderTheme,
TextDirection textDirection,
double value,
}) {
assert(context != null);
assert(center != null);
assert(enableAnimation != null);
assert(sliderTheme != null);
assert(sliderTheme.disabledThumbColor != null);
assert(sliderTheme.thumbColor != null);
final Canvas canvas = context.canvas;
final Tween<double> radiusTween = Tween<double>(
begin: _disabledThumbRadius,
end: enabledThumbRadius,
);
final ColorTween colorTween = ColorTween(
begin: sliderTheme.disabledThumbColor,
end: sliderTheme.thumbColor,
);
canvas.drawCircle(
center,
radiusTween.evaluate(enableAnimation),
Paint()
..color = thumbCenterColor
..style = PaintingStyle.stroke
..strokeWidth = 2,
);
canvas.drawLine(
Offset(center.dx - 6, center.dy),
Offset(center.dx + 6, center.dy),
Paint()
..color = Colors.grey[300]
..style = PaintingStyle.fill
..strokeWidth = 2,
);
canvas.drawCircle(
center,
radiusTween.evaluate(enableAnimation) - 2,
Paint()
..color = colorTween.evaluate(enableAnimation)
..style = PaintingStyle.fill
..strokeWidth = 2,
);
canvas.drawLine(
Offset(center.dx - 5, center.dy - 2),
Offset(center.dx + 5, center.dy + 2),
Paint()
..color = Colors.transparent
..style = PaintingStyle.fill
..strokeWidth = 2,
);
}
}
class PlayerWidget extends StatefulWidget {
@override
_PlayerWidgetState createState() => _PlayerWidgetState();
@ -50,17 +142,17 @@ class _PlayerWidgetState extends State<PlayerWidget> {
_timeLeft = _minSelected;
_timer = Timer.periodic(Duration(minutes: 1), (timer) {
setState(() {
if(_timeLeft < 1){
if (_timeLeft < 1) {
_timer.cancel();
} else{
} else {
_timeLeft = _timeLeft - 1;
}
});
});
});
}
Widget _sleepTimer(BuildContext context) {
var audio = Provider.of<AudioPlayer>(context);
var audio = Provider.of<AudioPlayerNotifier>(context);
return Container(
height: 50,
margin: EdgeInsets.all(10.0),
@ -141,7 +233,7 @@ class _PlayerWidgetState extends State<PlayerWidget> {
}
Widget _expandedPanel(BuildContext context) {
var audio = Provider.of<AudioPlayer>(context, listen: false);
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
return Stack(
children: <Widget>[
Container(
@ -156,7 +248,7 @@ class _PlayerWidgetState extends State<PlayerWidget> {
height: 80.0,
padding: EdgeInsets.all(20),
alignment: Alignment.center,
child: Selector<AudioPlayer, String>(
child: Selector<AudioPlayerNotifier, String>(
selector: (_, audio) => audio.episode.title,
builder: (_, title, __) {
return Container(
@ -201,8 +293,12 @@ class _PlayerWidgetState extends State<PlayerWidget> {
},
),
),
Consumer<AudioPlayer>(
Consumer<AudioPlayerNotifier>(
builder: (_, data, __) {
Color _c =
(Theme.of(context).brightness == Brightness.light)
? data.episode.primaryColor.colorizedark()
: data.episode.primaryColor.colorizeLight();
return Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
@ -215,10 +311,12 @@ class _PlayerWidgetState extends State<PlayerWidget> {
.accentColor
.withOpacity(0.5),
inactiveTrackColor: Colors.grey[300],
trackHeight: 3.0,
trackHeight: 2.0,
thumbColor: Theme.of(context).accentColor,
thumbShape: RoundSliderThumbShape(
enabledThumbRadius: 6.0),
thumbShape: MyRoundSliderThumpShape(
enabledThumbRadius: 5.0,
disabledThumbRadius: 5.0,
thumbCenterColor: _c),
overlayColor:
Theme.of(context).accentColor.withAlpha(32),
overlayShape:
@ -238,7 +336,7 @@ class _PlayerWidgetState extends State<PlayerWidget> {
children: <Widget>[
Text(
_stringForSeconds(
data.backgroundAudioPosition) ??
data.backgroundAudioPosition / 1000) ??
'',
style: TextStyle(fontSize: 10),
),
@ -250,7 +348,12 @@ class _PlayerWidgetState extends State<PlayerWidget> {
style: const TextStyle(
color: const Color(0xFFFF0000)))
: Text(
data.remoteAudioLoading
data.audioState ==
BasicPlaybackState
.buffering ||
data.audioState ==
BasicPlaybackState
.connecting
? 'Buffring...'
: '',
style: TextStyle(
@ -261,7 +364,7 @@ class _PlayerWidgetState extends State<PlayerWidget> {
),
Text(
_stringForSeconds(
data.backgroundAudioDuration) ??
data.backgroundAudioDuration / 1000) ??
'',
style: TextStyle(fontSize: 10),
),
@ -274,8 +377,8 @@ class _PlayerWidgetState extends State<PlayerWidget> {
),
Container(
height: 100,
child: Selector<AudioPlayer, bool>(
selector: (_, audio) => audio.backgroundAudioPlaying,
child: Selector<AudioPlayerNotifier, BasicPlaybackState>(
selector: (_, audio) => audio.audioState,
builder: (_, backplay, __) {
return Material(
color: Colors.transparent,
@ -285,22 +388,24 @@ class _PlayerWidgetState extends State<PlayerWidget> {
children: [
IconButton(
padding: EdgeInsets.symmetric(horizontal: 30.0),
onPressed: backplay
? () => audio.forwardAudio(-10)
: null,
onPressed:
backplay == BasicPlaybackState.playing
? () => audio.forwardAudio(-10)
: null,
iconSize: 32.0,
icon: Icon(Icons.replay_10),
color:
Theme.of(context).tabBarTheme.labelColor),
backplay
backplay == BasicPlaybackState.playing
? IconButton(
padding:
EdgeInsets.symmetric(horizontal: 30.0),
onPressed: backplay
? () {
audio.pauseAduio();
}
: null,
onPressed:
backplay == BasicPlaybackState.playing
? () {
audio.pauseAduio();
}
: null,
iconSize: 40.0,
icon: Icon(Icons.pause_circle_filled),
color: Theme.of(context)
@ -309,11 +414,12 @@ class _PlayerWidgetState extends State<PlayerWidget> {
: IconButton(
padding:
EdgeInsets.symmetric(horizontal: 30.0),
onPressed: backplay
? null
: () {
audio.resumeAudio();
},
onPressed:
backplay == BasicPlaybackState.playing
? null
: () {
audio.resumeAudio();
},
iconSize: 40.0,
icon: Icon(Icons.play_circle_filled),
color: Theme.of(context)
@ -321,9 +427,10 @@ class _PlayerWidgetState extends State<PlayerWidget> {
.labelColor),
IconButton(
padding: EdgeInsets.symmetric(horizontal: 30.0),
onPressed: backplay
? () => audio.forwardAudio(30)
: null,
onPressed:
backplay == BasicPlaybackState.playing
? () => audio.forwardAudio(30)
: null,
iconSize: 32.0,
icon: Icon(Icons.forward_30),
color:
@ -344,7 +451,7 @@ class _PlayerWidgetState extends State<PlayerWidget> {
color: Theme.of(context).scaffoldBackgroundColor,
borderRadius: BorderRadius.all(Radius.circular(10.0)),
),
child: Selector<AudioPlayer,
child: Selector<AudioPlayerNotifier,
Tuple3<EpisodeBrief, bool, bool>>(
selector: (_, audio) => Tuple3(audio.episode,
audio.stopOnComplete, audio.showStopWatch),
@ -394,6 +501,7 @@ class _PlayerWidgetState extends State<PlayerWidget> {
],
onSelected: (value) {
if (value == 1) {
audio.sleepTimer(_minSelected);
audio.setStopOnComplete = true;
} else if (value == 2) {
setState(() => _showTimer = true);
@ -414,8 +522,7 @@ class _PlayerWidgetState extends State<PlayerWidget> {
color:
Theme.of(context).accentColor,
),
child: Text(
_timeLeft.toString(),
child: Text(_timeLeft.toString(),
style: TextStyle(
color: Colors.white)),
),
@ -475,7 +582,7 @@ class _PlayerWidgetState extends State<PlayerWidget> {
// margin: EdgeInsets.all(20),
//padding: EdgeInsets.only(bottom: 10.0),
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(10.0)),
// borderRadius: BorderRadius.all(Radius.circular(10.0)),
color: Theme.of(context).scaffoldBackgroundColor,
),
child: Column(
@ -511,7 +618,7 @@ class _PlayerWidgetState extends State<PlayerWidget> {
),
),
Expanded(
child: Selector<AudioPlayer, List<EpisodeBrief>>(
child: Selector<AudioPlayerNotifier, List<EpisodeBrief>>(
selector: (_, audio) => audio.queue.playlist,
builder: (_, playlist, __) {
return ListView.builder(
@ -623,7 +730,7 @@ class _PlayerWidgetState extends State<PlayerWidget> {
}
Widget _miniPanel(double width, BuildContext context) {
var audio = Provider.of<AudioPlayer>(context, listen: false);
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
return Container(
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
@ -631,7 +738,7 @@ class _PlayerWidgetState extends State<PlayerWidget> {
height: 60,
child:
Column(mainAxisAlignment: MainAxisAlignment.start, children: <Widget>[
Selector<AudioPlayer, Tuple2<String, double>>(
Selector<AudioPlayerNotifier, Tuple2<String, double>>(
selector: (_, audio) =>
Tuple2(audio.episode?.primaryColor, audio.seekSliderValue),
builder: (_, data, __) {
@ -657,58 +764,33 @@ class _PlayerWidgetState extends State<PlayerWidget> {
children: <Widget>[
Expanded(
flex: 4,
child: Selector<AudioPlayer, String>(
child: Selector<AudioPlayerNotifier, String>(
selector: (_, audio) => audio.episode.title,
builder: (_, title, __) {
return LayoutBuilder(
builder: (context, size) {
var span = TextSpan(
text: title,
style: TextStyle(fontWeight: FontWeight.bold),
);
var tp = TextPainter(
text: span,
maxLines: 2,
textDirection: TextDirection.ltr);
tp.layout(maxWidth: size.maxWidth);
if (tp.didExceedMaxLines) {
return Marquee(
text: title,
style: TextStyle(fontWeight: FontWeight.bold),
scrollAxis: Axis.vertical,
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),
);
}
},
return Text(
title,
style: TextStyle(fontWeight: FontWeight.bold),
maxLines: 2,
overflow: TextOverflow.clip,
);
},
),
),
Expanded(
flex: 2,
child: Selector<AudioPlayer, Tuple2<bool, double>>(
child: Selector<AudioPlayerNotifier,
Tuple2<BasicPlaybackState, double>>(
selector: (_, audio) => Tuple2(
audio.remoteAudioLoading,
audio.audioState,
(audio.backgroundAudioDuration -
audio.backgroundAudioPosition)),
audio.backgroundAudioPosition) /
1000),
builder: (_, data, __) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 10),
alignment: Alignment.center,
child: data.item1
child: data.item1 == BasicPlaybackState.buffering ||
data.item1 == BasicPlaybackState.connecting
? Text(
'Buffring...',
style: TextStyle(
@ -730,30 +812,32 @@ class _PlayerWidgetState extends State<PlayerWidget> {
),
Expanded(
flex: 2,
child: Selector<AudioPlayer, bool>(
selector: (_, audio) => audio.backgroundAudioPlaying,
child: Selector<AudioPlayerNotifier, BasicPlaybackState>(
selector: (_, audio) => audio.audioState,
builder: (_, audioplay, __) {
return Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
audioplay
audioplay == BasicPlaybackState.playing
? InkWell(
onTap: audioplay
? () {
audio.pauseAduio();
}
: null,
onTap:
audioplay == BasicPlaybackState.playing
? () {
audio.pauseAduio();
}
: null,
child: ImageRotate(
title: audio.episode.title,
path: audio.episode.imagePath),
)
: InkWell(
onTap: audioplay
? null
: () {
audio.resumeAudio();
},
onTap:
audioplay == BasicPlaybackState.playing
? null
: () {
audio.resumeAudio();
},
child: Stack(
alignment: Alignment.center,
children: <Widget>[
@ -781,11 +865,10 @@ class _PlayerWidgetState extends State<PlayerWidget> {
),
),
IconButton(
onPressed: audioplay
? () => audio.forwardAudio(30)
: null,
onPressed:
() => audio.playNext(),
iconSize: 25.0,
icon: Icon(Icons.forward_30),
icon: Icon(Icons.skip_next),
color:
Theme.of(context).tabBarTheme.labelColor),
],
@ -803,7 +886,7 @@ class _PlayerWidgetState extends State<PlayerWidget> {
@override
Widget build(BuildContext context) {
double _width = MediaQuery.of(context).size.width;
return Selector<AudioPlayer, bool>(
return Selector<AudioPlayerNotifier, bool>(
selector: (_, audio) => audio.playerRunning,
builder: (_, playerrunning, __) {
return !playerrunning

View File

@ -31,7 +31,7 @@ class _HomeState extends State<Home> {
}
_getPlaylist() async {
await Provider.of<AudioPlayer>(context, listen: false).loadPlaylist();
await Provider.of<AudioPlayerNotifier>(context, listen: false).loadPlaylist();
setState(() {
_loadPlay = true;
});
@ -39,7 +39,7 @@ class _HomeState extends State<Home> {
@override
Widget build(BuildContext context) {
var audio = Provider.of<AudioPlayer>(context, listen: false);
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
return Stack(children: <Widget>[
Column(
mainAxisAlignment: MainAxisAlignment.start,
@ -58,7 +58,7 @@ class _HomeState extends State<Home> {
bottom: 50,
right: _loadPlay ? 5 : -25,
child: Container(
child: Selector<AudioPlayer, Tuple3<bool, Playlist, int>>(
child: Selector<AudioPlayerNotifier, Tuple3<bool, Playlist, int>>(
selector: (_, audio) =>
Tuple3(audio.playerRunning, audio.queue, audio.lastPositin),
builder: (_, data, __) => !_loadPlay
@ -90,7 +90,7 @@ class _HomeState extends State<Home> {
offset: Offset(1, 1)),
]),
height: 40,
child: Text(_stringForSeconds(data.item3) + '...',
child: Text(_stringForSeconds(data.item3~/1000) + '...',
style: TextStyle(color: Colors.white)),
),
CircleAvatar(

View File

@ -5,7 +5,9 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:tsacdop/class/audiostate.dart';
import 'package:tuple/tuple.dart';
import 'package:line_icons/line_icons.dart';
import 'package:tsacdop/class/episodebrief.dart';
import 'package:tsacdop/class/importompl.dart';
import 'package:tsacdop/class/podcast_group.dart';
@ -89,7 +91,9 @@ class _ScrollPodcastsState extends State<ScrollPodcasts> {
style: Theme.of(context)
.textTheme
.bodyText1
.copyWith(color: Colors.red[300]),
.copyWith(
color: Theme.of(context)
.accentColor),
)),
Spacer(),
Container(
@ -184,7 +188,9 @@ class _ScrollPodcastsState extends State<ScrollPodcasts> {
style: Theme.of(context)
.textTheme
.bodyText1
.copyWith(color: Colors.red[300]),
.copyWith(
color: Theme.of(context)
.accentColor),
)),
Spacer(),
Container(
@ -374,43 +380,78 @@ class ShowEpisode extends StatelessWidget {
final List<EpisodeBrief> podcast;
final PodcastLocal podcastLocal;
ShowEpisode({Key key, this.podcast, this.podcastLocal}) : super(key: key);
Offset offset;
@override
Widget build(BuildContext context) {
double _width = MediaQuery.of(context).size.width;
_showPopupMenu(Offset offset) async {
print(offset.dx);
_showPopupMenu(Offset offset, EpisodeBrief episode, BuildContext context,
bool isPlaying, bool isInPlaylist) async {
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
double left = offset.dx;
double top = offset.dy;
await showMenu(
await showMenu<int>(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(10))),
context: context,
position: RelativeRect.fromLTRB(left, top, _width - left, 0),
items: [
items: <PopupMenuEntry<int>>[
PopupMenuItem(
value: 0,
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Icon(Icons.play_circle_outline),
Padding(padding: EdgeInsets.symmetric(horizontal: 2),),
Text('Play')
Icon(
LineIcons.play_circle_solid,
color: Theme.of(context).accentColor,
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 2),
),
!isPlaying ? Text('Play') : Text('Playing'),
],
),
),
PopupMenuItem(child: Row(
children: <Widget>[
Icon(Icons.favorite_border),
Padding(padding: EdgeInsets.symmetric(horizontal: 2),),
Text('Like')
],
)),
PopupMenuItem(
value: 1,
child: Row(
children: <Widget>[
Icon(
LineIcons.clock_solid,
color: Colors.red,
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 2),
),
!isInPlaylist ? Text('Later') : Text('Remove')
],
)),
],
elevation: 8.0,
);
elevation: 5.0,
).then((value) {
if (value == 0) {
if (!isPlaying) audio.episodeLoad(episode);
} else if (value == 1) {
if (isInPlaylist) {
audio.addToPlaylist(episode);
Fluttertoast.showToast(
msg: 'Added to playlist',
gravity: ToastGravity.BOTTOM,
);
} else {
audio.delFromPlaylist(episode);
Fluttertoast.showToast(
msg: 'Removed from playlist',
gravity: ToastGravity.BOTTOM,
);
}
}
});
}
return CustomScrollView(
physics: const AlwaysScrollableScrollPhysics(),
// physics: const AlwaysScrollableScrollPhysics(),
physics: ClampingScrollPhysics(),
primary: false,
slivers: <Widget>[
SliverPadding(
@ -427,88 +468,110 @@ class ShowEpisode extends StatelessWidget {
Color _c = (Theme.of(context).brightness == Brightness.light)
? podcastLocal.primaryColor.colorizedark()
: podcastLocal.primaryColor.colorizeLight();
return GestureDetector(
onLongPressStart: (details) => _showPopupMenu(Offset(
details.globalPosition.dx, details.globalPosition.dy)),
onTap: () {
Navigator.push(
context,
ScaleRoute(
page: EpisodeDetail(
episodeItem: podcast[index],
heroTag: 'scroll',
//unique hero tag
)),
);
},
child: Container(
return Selector<AudioPlayerNotifier,
Tuple2<EpisodeBrief, List<String>>>(
selector: (_, audio) => Tuple2(
audio?.episode,
audio.queue.playlist.map((e) => e.enclosureUrl).toList(),
),
builder: (_, data, __) => Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(5.0)),
color: Theme.of(context).scaffoldBackgroundColor,
border: Border.all(
color: Theme.of(context).brightness == Brightness.light
? Theme.of(context).primaryColor
: Theme.of(context).scaffoldBackgroundColor,
// color: Theme.of(context).primaryColor,
width: 3.0,
),
),
alignment: Alignment.center,
padding: EdgeInsets.all(10.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Expanded(
flex: 2,
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.all(Radius.circular(5.0)),
onTapDown: (details) => offset = Offset(
details.globalPosition.dx,
details.globalPosition.dy),
onLongPress: () => _showPopupMenu(
offset,
podcast[index],
context,
data.item1 == podcast[index],
data.item2.contains(podcast[index].enclosureUrl)),
onTap: () {
Navigator.push(
context,
ScaleRoute(
page: EpisodeDetail(
episodeItem: podcast[index],
heroTag: 'scroll',
//unique hero tag
)),
);
},
child: Container(
// decoration: BoxDecoration(
// border: Border.all(
// color: Theme.of(context).brightness ==
// Brightness.light
// ? Theme.of(context).primaryColor
// : Theme.of(context).scaffoldBackgroundColor,
// width: 0.0,
// ),
// ),
padding: EdgeInsets.all(10.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Hero(
tag: podcast[index].enclosureUrl + 'scroll',
Expanded(
flex: 2,
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Hero(
tag: podcast[index].enclosureUrl +
'scroll',
child: Container(
height: _width / 18,
width: _width / 18,
child: CircleAvatar(
backgroundImage: FileImage(File(
"${podcastLocal.imagePath}")),
),
),
),
Spacer(),
],
),
),
Expanded(
flex: 5,
child: Container(
height: _width / 18,
width: _width / 18,
child: CircleAvatar(
backgroundImage: FileImage(
File("${podcastLocal.imagePath}")),
padding: EdgeInsets.only(top: 2.0),
alignment: Alignment.topLeft,
child: Text(
podcast[index].title,
style: TextStyle(
fontSize: _width / 32,
),
maxLines: 4,
overflow: TextOverflow.fade,
),
),
),
Spacer(),
Expanded(
flex: 1,
child: Container(
alignment: Alignment.bottomLeft,
child: Text(
podcast[index].dateToString(),
//podcast[index].pubDate.substring(4, 16),
style: TextStyle(
fontSize: _width / 35,
color: _c,
fontStyle: FontStyle.italic,
),
),
)),
],
),
),
Expanded(
flex: 5,
child: Container(
padding: EdgeInsets.only(top: 2.0),
alignment: Alignment.topLeft,
child: Text(
podcast[index].title,
style: TextStyle(
fontSize: _width / 32,
),
maxLines: 4,
overflow: TextOverflow.fade,
),
),
),
Expanded(
flex: 1,
child: Container(
alignment: Alignment.bottomLeft,
child: Text(
podcast[index].dateToString(),
//podcast[index].pubDate.substring(4, 16),
style: TextStyle(
fontSize: _width / 35,
color: _c,
fontStyle: FontStyle.italic,
),
),
),
),
],
),
),
),
);

View File

@ -2,7 +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/settings/history.dart';
import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
import 'package:tsacdop/util/episodegrid.dart';
@ -49,8 +49,8 @@ class _MainTabState extends State<MainTab> with TickerProviderStateMixin {
],
onSelected: (value) {
if (value == 0) {
Navigator.push(
context, MaterialPageRoute(builder: (context) => PlayedHistory()));
Navigator.push(context,
MaterialPageRoute(builder: (context) => PlayedHistory()));
}
},
);
@ -81,6 +81,7 @@ class _MainTabState extends State<MainTab> with TickerProviderStateMixin {
height: 50,
alignment: Alignment.centerLeft,
child: TabBar(
indicatorSize: TabBarIndicatorSize.tab,
isScrollable: true,
labelPadding: EdgeInsets.all(10.0),
controller: _controller,
@ -134,26 +135,73 @@ class RecentUpdate extends StatefulWidget {
}
class _RecentUpdateState extends State<RecentUpdate> {
Future<List<EpisodeBrief>> _getRssItem() async {
Future<List<EpisodeBrief>> _getRssItem(int top) async {
var dbHelper = DBHelper();
List<EpisodeBrief> episodes = await dbHelper.getRecentRssItem();
List<EpisodeBrief> episodes = await dbHelper.getRecentRssItem(top);
return episodes;
}
ScrollController _controller;
int _top;
bool _loadMore;
_scrollListener() async {
if (_controller.offset == _controller.position.maxScrollExtent) {
if (mounted) setState(() => _loadMore = true);
await Future.delayed(Duration(seconds: 3));
if (mounted)
setState(() {
_top = _top + 33;
_loadMore = false;
});
}
}
@override
void initState() {
super.initState();
_loadMore = false;
_top = 33;
_controller = ScrollController();
_controller.addListener(_scrollListener);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return FutureBuilder<List<EpisodeBrief>>(
future: _getRssItem(),
future: _getRssItem(_top),
builder: (context, snapshot) {
if (snapshot.hasError) print(snapshot.error);
return (snapshot.hasData)
? EpisodeGrid(
podcast: snapshot.data,
showDownload: false,
showFavorite: false,
showNumber: false,
heroTag: 'recent',
)
? CustomScrollView(
controller: _controller,
physics: const AlwaysScrollableScrollPhysics(),
primary: false,
slivers: <Widget>[
EpisodeGrid(
podcast: snapshot.data,
showDownload: false,
showFavorite: false,
showNumber: false,
heroTag: 'recent',
),
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return _loadMore
? Container(
height: 2, child: LinearProgressIndicator())
: Center();
},
childCount: 1,
),
),
])
: Center(child: CircularProgressIndicator());
},
);
@ -179,12 +227,18 @@ class _MyFavoriteState extends State<MyFavorite> {
builder: (context, snapshot) {
if (snapshot.hasError) print(snapshot.error);
return (snapshot.hasData)
? EpisodeGrid(
podcast: snapshot.data,
showDownload: false,
showFavorite: false,
showNumber: false,
heroTag: 'favorite',
? CustomScrollView(
physics: const AlwaysScrollableScrollPhysics(),
primary: false,
slivers: <Widget>[
EpisodeGrid(
podcast: snapshot.data,
showDownload: false,
showFavorite: false,
showNumber: false,
heroTag: 'favorite',
)
],
)
: Center(child: CircularProgressIndicator());
},
@ -211,12 +265,19 @@ class _MyDownloadState extends State<MyDownload> {
builder: (context, snapshot) {
if (snapshot.hasError) print(snapshot.error);
return (snapshot.hasData)
? EpisodeGrid(
podcast: snapshot.data,
showDownload: true,
showFavorite: false,
showNumber: false,
heroTag: 'download',
? CustomScrollView(
physics: const AlwaysScrollableScrollPhysics(),
primary: false,
slivers: <Widget>[
EpisodeGrid(
podcast: snapshot.data,
showDownload: true,
showFavorite: false,
showNumber: false,
heroTag: 'download',
)
],
)
: Center(child: CircularProgressIndicator());
},

View File

@ -1,73 +0,0 @@
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

@ -43,7 +43,7 @@ class KeyValueStorage {
return prefs.getInt(key);
}
Future<bool> saveStringlist(List<String> playList) async{
Future<bool> saveStringList(List<String> playList) async{
SharedPreferences prefs = await SharedPreferences.getInstance();
return prefs.setStringList(key, playList);
}

View File

@ -8,6 +8,7 @@ import 'package:tsacdop/class/podcastlocal.dart';
import 'package:tsacdop/class/audiostate.dart';
import 'package:tsacdop/class/episodebrief.dart';
import 'package:tsacdop/webfeed/webfeed.dart';
import 'package:tsacdop/class/sub_history.dart';
class DBHelper {
static Database _db;
@ -35,10 +36,13 @@ class DBHelper {
enclosure_url TEXT UNIQUE, enclosure_length INTEGER, pubDate TEXT,
description TEXT, feed_id TEXT, feed_link TEXT, milliseconds INTEGER,
duration INTEGER DEFAULT 0, explicit INTEGER DEFAULT 0, liked INTEGER DEFAULT 0,
downloaded TEXT DEFAULT 'ND', download_date INTEGER DEFAULT 0)""");
downloaded TEXT DEFAULT 'ND', download_date INTEGER DEFAULT 0, media_id TEXT)""");
await db.execute(
"""CREATE TABLE PlayHistory(id INTEGER PRIMARY KEY, title TEXT, enclosure_url TEXT UNIQUE,
seconds REAL, seek_value REAL, add_date INTEGER)""");
await db.execute(
"""CREATE TABLE SubscribeHistory(id TEXT PRIMARY KEY, title TEXT, rss_url TEXT UNIQUE,
add_date INTEGER, remove_date INTEGER DEFAULT 0, status INTEGER DEFAULT 0)""");
}
Future<List<PodcastLocal>> getPodcastLocal(List<String> podcasts) async {
@ -97,7 +101,7 @@ class DBHelper {
int _milliseconds = DateTime.now().millisecondsSinceEpoch;
var dbClient = await database;
await dbClient.transaction((txn) async {
return await txn.rawInsert(
await txn.rawInsert(
"""INSERT OR IGNORE INTO PodcastLocal (id, title, imageUrl, rssUrl,
primaryColor, author, description, add_date, imagePath, provider, link) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
[
@ -114,6 +118,16 @@ class DBHelper {
podcastLocal.link
]);
});
await dbClient.transaction((txn) async {
await txn.rawInsert(
"""REPLACE INTO SubscribeHistory(id, title, rss_url, add_date) VALUES (?, ?, ?, ?)""",
[
podcastLocal.id,
podcastLocal.title,
podcastLocal.rssUrl,
_milliseconds
]);
});
}
Future<int> saveFiresideData(List<String> list) async {
@ -146,6 +160,10 @@ class DBHelper {
print('Removed all download tasks');
}
await dbClient.rawDelete('DELETE FROM Episodes WHERE feed_id=?', [id]);
int _milliseconds = DateTime.now().millisecondsSinceEpoch;
await dbClient.rawUpdate(
"""UPDATE SubscribeHistory SET remove_date = ? , status = ? WHERE id = ?""",
[_milliseconds, 1, id]);
}
Future<int> saveHistory(PlayHistory history) async {
@ -174,16 +192,46 @@ class DBHelper {
""");
List<PlayHistory> playHistory = [];
list.forEach((record) {
playHistory.add(PlayHistory(
record['title'],
record['enclosure_url'],
record['seconds'],
record['seek_value'],
));
playHistory.add(PlayHistory(record['title'], record['enclosure_url'],
record['seconds'], record['seek_value'],
playdate: DateTime.fromMillisecondsSinceEpoch(record['add_date'])));
});
return playHistory;
}
Future<List<SubHistory>> getSubHistory() async{
var dbClient = await database;
List<Map> list = await dbClient.rawQuery(
"""SELECT title, rss_url, add_date, remove_date, status FROM SubscribeHistory
ORDER BY add_date DESC"""
);
return list.map((record) => SubHistory(
record['status']==0 ? true : false, DateTime.fromMillisecondsSinceEpoch(record['remove_date']),
DateTime.fromMillisecondsSinceEpoch(record['add_date']), record['rss_url'], record['title']
)).toList();
}
Future<double> listenMins(int day) async {
var dbClient = await database;
var now = DateTime.now();
var start = DateTime(now.year, now.month, now.day)
.subtract(Duration(days: day))
.millisecondsSinceEpoch;
var end = DateTime(now.year, now.month, now.day)
.subtract(Duration(days: (day - 1)))
.millisecondsSinceEpoch;
List<Map> list = await dbClient.rawQuery(
"SELECT seconds FROM PlayHistory WHERE add_date > ? AND add_date < ?",
[start, end]);
double sum = 0;
if (list.length == 0) {
sum = 0;
} else {
list.forEach((record) => sum += record['seconds']);
}
return (sum ~/ 60).toDouble();
}
Future<int> getPosition(EpisodeBrief episodeBrief) async {
var dbClient = await database;
List<Map> list = await dbClient.rawQuery(
@ -200,6 +248,17 @@ class DBHelper {
RegExp hhmm = RegExp(r'[0-2][0-9]\:[0-5][0-9]');
RegExp ddmmm = RegExp(r'[0-3][0-9]\s[A-Z][a-z]{2}');
RegExp mmDd = RegExp(r'([0-1]|\s)[0-9]\-[0-3][0-9]');
// RegExp timezone
RegExp z = RegExp(r'(\+|\-)[0-1][0-9]00');
String timezone = z.stringMatch(pubDate);
int timezoneInt = 0;
if(timezone!=null){
if(timezone.substring(0, 1) == '-'){
timezoneInt = int.parse(timezone.substring(1,2));
} else {
timezoneInt = -int.parse(timezone.substring(1,2));
}
}
try {
date = DateFormat('EEE, dd MMM yyyy HH:mm:ss Z', 'en_US').parse(pubDate);
} catch (e) {
@ -209,7 +268,7 @@ class DBHelper {
try {
date = DateFormat('EEE, dd MMM yyyy HH:mm Z', 'en_US').parse(pubDate);
} catch (e) {
//parse date using regex, bug in parse maonth/day
//parse date using regex, still have issue in parse maonth/day
String year = yyyy.stringMatch(pubDate);
String time = hhmm.stringMatch(pubDate);
String month = ddmmm.stringMatch(pubDate);
@ -221,12 +280,12 @@ class DBHelper {
date = DateFormat('mm-dd yyyy HH:mm', 'en_US')
.parse(month + ' ' + year + ' ' + time);
} else {
date = DateTime.now();
date = DateTime.now().toUtc();
}
}
}
}
return date;
return date.add(Duration(hours: timezoneInt));
}
int getExplicit(bool b) {
@ -276,7 +335,7 @@ class DBHelper {
await dbClient.transaction((txn) {
return txn.rawInsert(
"""INSERT OR IGNORE INTO Episodes(title, enclosure_url, enclosure_length, pubDate,
description, feed_id, milliseconds, duration, explicit) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)""",
description, feed_id, milliseconds, duration, explicit, media_id) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
[
_title,
_url,
@ -287,6 +346,7 @@ class DBHelper {
_milliseconds,
_duration,
_explicit,
_url
]);
});
}
@ -333,7 +393,7 @@ class DBHelper {
await dbClient.transaction((txn) {
return txn.rawInsert(
"""INSERT OR IGNORE INTO Episodes(title, enclosure_url, enclosure_length, pubDate,
description, feed_id, milliseconds, duration, explicit) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)""",
description, feed_id, milliseconds, duration, explicit, media_id) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
[
_title,
_url,
@ -344,6 +404,7 @@ class DBHelper {
_milliseconds,
_duration,
_explicit,
_url
]);
});
}
@ -358,7 +419,7 @@ class DBHelper {
List<Map> list = await dbClient
.rawQuery("""SELECT E.title, E.enclosure_url, E.enclosure_length,
E.milliseconds, P.imagePath, P.title as feedTitle, E.duration, E.explicit, E.liked,
E.downloaded, P.primaryColor
E.downloaded, P.primaryColor , E.media_id
FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
WHERE P.id = ? ORDER BY E.milliseconds DESC""", [id]);
for (int x = 0; x < list.length; x++) {
@ -373,7 +434,8 @@ class DBHelper {
list[x]['downloaded'],
list[x]['duration'],
list[x]['explicit'],
list[x]['imagePath']));
list[x]['imagePath'],
list[x]['media_id']));
}
return episodes;
}
@ -384,7 +446,7 @@ class DBHelper {
List<Map> list = await dbClient
.rawQuery("""SELECT E.title, E.enclosure_url, E.enclosure_length,
E.milliseconds, P.imagePath, P.title as feed_title, E.duration, E.explicit, E.liked,
E.downloaded, P.primaryColor
E.downloaded, P.primaryColor, E.media_id
FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
where E.feed_id = ? ORDER BY E.milliseconds DESC LIMIT 3""", [id]);
for (int x = 0; x < list.length; x++) {
@ -399,7 +461,8 @@ class DBHelper {
list[x]['downloaded'],
list[x]['duration'],
list[x]['explicit'],
list[x]['imagePath']));
list[x]['imagePath'],
list[x]['media_id']));
}
return episodes;
}
@ -410,7 +473,7 @@ class DBHelper {
List<Map> list = await dbClient.rawQuery(
"""SELECT E.title, E.enclosure_url, E.enclosure_length,
E.milliseconds, P.imagePath, P.title as feed_title, E.duration, E.explicit, E.liked,
E.downloaded, P.primaryColor
E.downloaded, P.primaryColor, E.media_id
FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
where E.enclosure_url = ? ORDER BY E.milliseconds DESC LIMIT 3""",
[url]);
@ -427,19 +490,20 @@ class DBHelper {
list.first['downloaded'],
list.first['duration'],
list.first['explicit'],
list.first['imagePath']);
list.first['imagePath'],
list.first['media_id']);
return episode;
}
Future<List<EpisodeBrief>> getRecentRssItem() async {
Future<List<EpisodeBrief>> getRecentRssItem(int top) async {
var dbClient = await database;
List<EpisodeBrief> episodes = List();
List<Map> list = await dbClient
.rawQuery("""SELECT E.title, E.enclosure_url, E.enclosure_length,
E.milliseconds, P.title as feed_title, E.duration, E.explicit, E.liked,
E.downloaded, P.imagePath, P.primaryColor
E.downloaded, P.imagePath, P.primaryColor, E.media_id
FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
ORDER BY E.milliseconds DESC LIMIT 99""");
ORDER BY E.milliseconds DESC LIMIT ? """, [top]);
for (int x = 0; x < list.length; x++) {
episodes.add(EpisodeBrief(
list[x]['title'],
@ -452,7 +516,8 @@ class DBHelper {
list[x]['doanloaded'],
list[x]['duration'],
list[x]['explicit'],
list[x]['imagePath']));
list[x]['imagePath'],
list[x]['media_id']));
}
return episodes;
}
@ -463,7 +528,7 @@ class DBHelper {
List<Map> list = await dbClient.rawQuery(
"""SELECT E.title, E.enclosure_url, E.enclosure_length, E.milliseconds, P.imagePath,
P.title as feed_title, E.duration, E.explicit, E.liked, E.downloaded,
P.primaryColor FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
P.primaryColor, E.media_id FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
WHERE E.liked = 1 ORDER BY E.milliseconds DESC LIMIT 99""");
for (int x = 0; x < list.length; x++) {
episodes.add(EpisodeBrief(
@ -477,7 +542,8 @@ class DBHelper {
list[x]['downloaded'],
list[x]['duration'],
list[x]['explicit'],
list[x]['imagePath']));
list[x]['imagePath'],
list[x]['media_id']));
}
return episodes;
}
@ -507,10 +573,21 @@ class DBHelper {
return count;
}
Future<int> saveMediaId(String url, String path) async {
var dbClient = await database;
int _milliseconds = DateTime.now().millisecondsSinceEpoch;
int count = await dbClient.rawUpdate(
"UPDATE Episodes SET media_id = ?, download_date = ? WHERE enclosure_url = ?",
[path, _milliseconds, url]);
return count;
}
Future<int> delDownloaded(String url) async {
var dbClient = await database;
int count = await dbClient.rawUpdate(
"UPDATE Episodes SET downloaded = 'ND' WHERE enclosure_url = ?", [url]);
"UPDATE Episodes SET downloaded = 'ND', media_id = ? WHERE enclosure_url = ?", [url, url]);
print('Deleted ' + url);
return count;
}
@ -521,7 +598,7 @@ class DBHelper {
List<Map> list = await dbClient.rawQuery(
"""SELECT E.title, E.enclosure_url, E.enclosure_length, E.milliseconds, P.imagePath,
P.title as feed_title, E.duration, E.explicit, E.liked, E.downloaded,
P.primaryColor FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
P.primaryColor, E.media_id FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
WHERE E.downloaded != 'ND' ORDER BY E.download_date DESC LIMIT 99""");
for (int x = 0; x < list.length; x++) {
episodes.add(EpisodeBrief(
@ -535,7 +612,8 @@ class DBHelper {
list[x]['downloaded'],
list[x]['duration'],
list[x]['explicit'],
list[x]['imagePath']));
list[x]['imagePath'],
list[x]['media_id']));
}
return episodes;
}
@ -562,7 +640,7 @@ class DBHelper {
List<Map> list = await dbClient.rawQuery(
"""SELECT E.title, E.enclosure_url, E.enclosure_length, E.milliseconds, P.imagePath,
P.title as feed_title, E.duration, E.explicit, E.liked, E.downloaded,
P.primaryColor FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
P.primaryColor, E.media_id FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
WHERE E.enclosure_url = ?""", [url]);
episode = EpisodeBrief(
list.first['title'],
@ -575,7 +653,33 @@ class DBHelper {
list.first['downloaded'],
list.first['duration'],
list.first['explicit'],
list.first['imagePath']);
list.first['imagePath'],
list.first['media_id']);
return episode;
}
Future<EpisodeBrief> getRssItemWithMediaId(String id) async {
var dbClient = await database;
EpisodeBrief episode;
List<Map> list = await dbClient.rawQuery(
"""SELECT E.title, E.enclosure_url, E.enclosure_length, E.milliseconds, P.imagePath,
P.title as feed_title, E.duration, E.explicit, E.liked, E.downloaded,
P.primaryColor, E.media_id FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
WHERE E.media_id = ?""", [id]);
episode = EpisodeBrief(
list.first['title'],
list.first['enclosure_url'],
list.first['enclosure_length'],
list.first['milliseconds'],
list.first['feed_title'],
list.first['primaryColor'],
list.first['liked'],
list.first['downloaded'],
list.first['duration'],
list.first['explicit'],
list.first['imagePath'],
list.first['media_id']);
return episode;
}
}

View File

@ -10,57 +10,61 @@ 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';
import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
void callbackDispatcher() {
Workmanager.executeTask((task, inputData) async {
var dbHelper = DBHelper();
print('Start task');
List<PodcastLocal> podcastList = await dbHelper.getPodcastLocalAll();
await Future.forEach(podcastList, (podcastLocal) async {
await dbHelper.updatePodcastRss(podcastLocal);
print('Refresh ' + podcastLocal.title);
});
return Future.value(true);
});
}
final SettingState themeSetting = SettingState();
Future main() async {
WidgetsFlutterBinding.ensureInitialized();
await themeSetting.initData();
await FlutterDownloader.initialize();
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => themeSetting),
ChangeNotifierProvider(create: (_) => AudioPlayer()),
ChangeNotifierProvider(create: (_) => AudioPlayerNotifier()),
ChangeNotifierProvider(create: (_) => GroupList()),
ChangeNotifierProvider(create: (_) => ImportOmpl()),
],
child: MyApp(),
),
);
Workmanager.initialize(
callbackDispatcher,
isInDebugMode: true,
);
Workmanager.registerPeriodicTask("2", "update_podcasts",
frequency: Duration(minutes: 1),
initialDelay: Duration(seconds: 5),
constraints: Constraints(
networkType: NetworkType.connected,
));
await FlutterDownloader.initialize();
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 true;
});
}
class MyApp extends StatelessWidget {
void setWorkManager() {
Workmanager.initialize(
callbackDispatcher,
isInDebugMode: true,
);
Workmanager.registerPeriodicTask("1", "update_podcasts",
frequency: Duration(hours: 12),
initialDelay: Duration(seconds: 5),
constraints: Constraints(
networkType: NetworkType.connected,
requiresBatteryNotLow: true,
));
}
@override
Widget build(BuildContext context) {
return Consumer<SettingState>(
builder: (_, setting, __) {
if (setting.autoUpdate) setWorkManager();
return MaterialApp(
themeMode: setting.theme,
debugShowCheckedModeBanner: false,
@ -78,7 +82,6 @@ class MyApp extends StatelessWidget {
elevation: 0,
),
textTheme: TextTheme(
headline1: TextStyle(fontSize: 72.0, fontWeight: FontWeight.bold),
bodyText2:
TextStyle(fontSize: 15.0, fontWeight: FontWeight.normal),
),
@ -89,6 +92,7 @@ class MyApp extends StatelessWidget {
),
darkTheme: ThemeData.dark().copyWith(
accentColor: setting.accentSetColor,
appBarTheme: AppBarTheme(elevation: 0),
),
home: MyHomePage(),
);

View File

@ -0,0 +1,146 @@
import 'package:flutter/material.dart';
class CustomTabView extends StatefulWidget {
final int itemCount;
final IndexedWidgetBuilder tabBuilder;
final IndexedWidgetBuilder pageBuilder;
final ValueChanged<int> onPositionChange;
final ValueChanged<double> onScroll;
final int initPosition;
CustomTabView({
@required this.itemCount,
@required this.tabBuilder,
@required this.pageBuilder,
this.onPositionChange,
this.onScroll,
this.initPosition,
});
@override
_CustomTabsState createState() => _CustomTabsState();
}
class _CustomTabsState extends State<CustomTabView>
with TickerProviderStateMixin {
TabController controller;
int _currentCount;
int _currentPosition;
@override
void initState() {
_currentPosition = widget.initPosition ?? 0;
controller = TabController(
length: widget.itemCount,
vsync: this,
initialIndex: _currentPosition,
);
controller.addListener(onPositionChange);
controller.animation.addListener(onScroll);
_currentCount = widget.itemCount;
super.initState();
}
@override
void didUpdateWidget(CustomTabView oldWidget) {
if (_currentCount != widget.itemCount) {
controller.animation.removeListener(onScroll);
controller.removeListener(onPositionChange);
controller.dispose();
if (widget.initPosition != null) {
_currentPosition = widget.initPosition;
}
if (_currentPosition > widget.itemCount - 1) {
_currentPosition = widget.itemCount - 1;
_currentPosition = _currentPosition < 0 ? 0 : _currentPosition;
if (widget.onPositionChange is ValueChanged<int>) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
widget.onPositionChange(_currentPosition);
}
});
}
}
_currentCount = widget.itemCount;
setState(() {
controller = TabController(
length: widget.itemCount,
vsync: this,
initialIndex: _currentPosition,
);
controller.addListener(onPositionChange);
controller.animation.addListener(onScroll);
});
} else if (widget.initPosition != null) {
controller.animateTo(widget.initPosition);
}
super.didUpdateWidget(oldWidget);
}
@override
void dispose() {
controller.animation.removeListener(onScroll);
controller.removeListener(onPositionChange);
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Container(
alignment: Alignment.centerLeft,
height: 50.0,
padding: EdgeInsets.all(10.0),
child: TabBar(
indicatorSize: TabBarIndicatorSize.label,
labelPadding: EdgeInsets.symmetric(horizontal: 5.0),
indicatorPadding: EdgeInsets.symmetric(horizontal: 5.0),
isScrollable: true,
controller: controller,
labelColor: Colors.white,
unselectedLabelColor: Colors.grey[700],
indicator: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(15)),
color: Theme.of(context).accentColor,
),
tabs: List.generate(
widget.itemCount,
(index) => widget.tabBuilder(context, index),
),
),
),
Expanded(
child: TabBarView(
controller: controller,
children: List.generate(
widget.itemCount,
(index) => widget.pageBuilder(context, index),
),
),
),
],
);
}
onPositionChange() {
if (!controller.indexIsChanging) {
_currentPosition = controller.index;
if (widget.onPositionChange is ValueChanged<int>) {
widget.onPositionChange(_currentPosition);
}
}
}
onScroll() {
if (widget.onScroll is ValueChanged<double>) {
widget.onScroll(controller.animation.value);
}
}
}

View File

@ -7,16 +7,13 @@ 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';
import 'package:tsacdop/episodes/episodedetail.dart';
import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
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';
@ -104,7 +101,9 @@ class _PodcastDetailState extends State<PodcastDetail> {
fit: BoxFit.cover)),
alignment: Alignment.centerRight,
child: Container(
color: Theme.of(context).scaffoldBackgroundColor.withOpacity(0.5),
color: Theme.of(context)
.scaffoldBackgroundColor
.withOpacity(0.5),
padding: EdgeInsets.symmetric(vertical: 5.0),
width: MediaQuery.of(context).size.width,
alignment: Alignment.centerRight,
@ -122,7 +121,8 @@ class _PodcastDetailState extends State<PodcastDetail> {
children: <Widget>[
CircleAvatar(
backgroundColor: Colors.grey[400],
backgroundImage: CachedNetworkImageProvider(
backgroundImage:
CachedNetworkImageProvider(
host.image,
)),
Padding(
@ -162,7 +162,7 @@ class _PodcastDetailState extends State<PodcastDetail> {
@override
Widget build(BuildContext context) {
double _width = MediaQuery.of(context).size.width;
Color _color = widget.podcastLocal.primaryColor.colorizedark();
return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle(
@ -283,7 +283,7 @@ class _PodcastDetailState extends State<PodcastDetail> {
'Hosted on ' +
widget.podcastLocal
.provider,
maxLines: 1,
maxLines: 1,
style: TextStyle(
color: Colors.white),
)
@ -308,6 +308,8 @@ class _PodcastDetailState extends State<PodcastDetail> {
),
title: top < 70
? Text(widget.podcastLocal.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(color: Colors.white))
: Center(),
);
@ -322,184 +324,15 @@ class _PodcastDetailState extends State<PodcastDetail> {
),
),
SliverPadding(
padding:
const EdgeInsets.symmetric(horizontal: 15.0),
sliver: SliverGrid(
gridDelegate:
SliverGridDelegateWithFixedCrossAxisCount(
childAspectRatio: 1.0,
crossAxisCount: 3,
mainAxisSpacing: 6.0,
crossAxisSpacing: 6.0,
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
EpisodeBrief episodeBrief =
snapshot.data[index];
Color _c = (Theme.of(context).brightness ==
Brightness.light)
? widget.podcastLocal.primaryColor
.colorizedark()
: widget.podcastLocal.primaryColor
.colorizeLight();
return Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
Navigator.push(
context,
ScaleRoute(
page: EpisodeDetail(
episodeItem: episodeBrief,
heroTag: 'podcast',
)),
);
},
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.all(
Radius.circular(5.0)),
color: Theme.of(context)
.scaffoldBackgroundColor,
border: Border.all(
color: Theme.of(context)
.brightness ==
Brightness.light
? Theme.of(context)
.primaryColor
: Theme.of(context)
.scaffoldBackgroundColor,
width: 3.0,
),
boxShadow: [
BoxShadow(
color: Theme.of(context)
.primaryColor,
blurRadius: 0.5,
spreadRadius: 0.5,
),
]),
alignment: Alignment.center,
padding: EdgeInsets.all(8.0),
child: Column(
mainAxisAlignment:
MainAxisAlignment.center,
children: <Widget>[
Expanded(
flex: 2,
child: Row(
mainAxisAlignment:
MainAxisAlignment.start,
children: <Widget>[
Hero(
tag: episodeBrief
.enclosureUrl +
'podcast',
child: Container(
height: _width / 16,
width: _width / 16,
child: CircleAvatar(
backgroundImage:
FileImage(File(
"${episodeBrief.imagePath}")),
),
),
),
Spacer(),
Container(
alignment:
Alignment.topRight,
child: Text(
(snapshot.data.length -
index)
.toString(),
style: GoogleFonts.teko(
textStyle: TextStyle(
fontSize:
_width / 24,
color: _c,
),
),
),
)
],
),
),
Expanded(
flex: 5,
child: Container(
alignment: Alignment.topLeft,
padding:
EdgeInsets.only(top: 2.0),
child: Text(
episodeBrief.title,
style: TextStyle(
fontSize: _width / 32,
),
maxLines: 4,
overflow: TextOverflow.fade,
),
),
),
Expanded(
flex: 1,
child: Row(
children: <Widget>[
Align(
alignment:
Alignment.bottomLeft,
child: Text(
episodeBrief
.dateToString(),
//podcast[index].pubDate.substring(4, 16),
style: TextStyle(
fontSize:
_width / 35,
color: _c,
fontStyle: FontStyle
.italic),
),
),
Spacer(),
DownloadIcon(
episodeBrief:
episodeBrief),
Padding(
padding:
EdgeInsets.all(1),
),
Container(
alignment:
Alignment.bottomRight,
child: (episodeBrief
.liked ==
0)
? Center()
: IconTheme(
data:
IconThemeData(
size: 15),
child: Icon(
Icons.favorite,
color:
Colors.red,
),
),
),
],
),
),
],
),
),
),
);
},
childCount: snapshot.data.length,
),
),
),
padding: const EdgeInsets.symmetric(
horizontal: 10.0),
sliver: EpisodeGrid(
podcast: snapshot.data,
showDownload: false,
showFavorite: true,
showNumber: true,
heroTag: 'podcast',
)),
],
)
: Center(child: CircularProgressIndicator());

View File

@ -1,12 +1,10 @@
import 'dart:io';
import 'dart:math' as math;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:provider/provider.dart';
import 'package:flutter_colorpicker/flutter_colorpicker.dart';
import 'package:tsacdop/class/podcast_group.dart';
import 'package:tsacdop/class/podcastlocal.dart';
@ -21,87 +19,7 @@ class PodcastGroupList extends StatefulWidget {
_PodcastGroupListState createState() => _PodcastGroupListState();
}
class _PodcastGroupListState extends State<PodcastGroupList>
with SingleTickerProviderStateMixin {
bool _showSetting;
AnimationController _controller;
Animation _animation;
double _fraction;
@override
void initState() {
super.initState();
_showSetting = false;
_fraction = 0;
_controller = AnimationController(
duration: const Duration(milliseconds: 500), vsync: this);
_animation = Tween(begin: 0.0, end: 1.0).animate(_controller)
..addListener(() {
if (mounted)
setState(() {
_fraction = _animation.value;
});
});
_controller.addStatusListener((status) {
if (status == AnimationStatus.completed) {
_controller.stop();
} else if (status == AnimationStatus.dismissed) {
_controller.stop();
}
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
Widget _saveButton(BuildContext context) {
var podcastList = widget.group.podcasts;
var _groupList = Provider.of<GroupList>(context, listen: false);
return Transform(
alignment: FractionalOffset(0.5, 0.5),
transform: Matrix4.rotationY(math.pi * _fraction),
child: Container(
child: InkWell(
child: Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: _fraction > 0.5 ? Colors.red : widget.group.getColor(),
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.grey[700],
blurRadius: 5,
offset: Offset(1, 1),
),
]),
alignment: Alignment.center,
child: Icon(
_fraction > 0.5 ? Icons.save : Icons.settings,
color: Colors.white,
)),
onTap: () async {
if (_fraction == 0) {
setState(() {
_showSetting = true;
});
} else {
await _groupList.saveOrder(widget.group, podcastList);
Fluttertoast.showToast(
msg: 'Setting Saved',
gravity: ToastGravity.BOTTOM,
);
_controller.reverse();
}
},
),
),
);
}
class _PodcastGroupListState extends State<PodcastGroupList> {
@override
Widget build(BuildContext context) {
var groupList = Provider.of<GroupList>(context, listen: false);
@ -109,372 +27,38 @@ class _PodcastGroupListState extends State<PodcastGroupList>
? Container(
color: Theme.of(context).primaryColor,
)
: Stack(
children: <Widget>[
Container(
color: Theme.of(context).primaryColor,
child: Stack(
children: <Widget>[
ReorderableListView(
onReorder: (int oldIndex, int newIndex) {
setState(() {
if (newIndex > oldIndex) {
newIndex -= 1;
}
final PodcastLocal podcast =
widget.group.podcasts.removeAt(oldIndex);
widget.group.podcasts.insert(newIndex, podcast);
_controller.forward();
});
},
children: widget.group.podcasts
.map<Widget>((PodcastLocal podcastLocal) {
return Container(
decoration: BoxDecoration(
color: Theme.of(context).primaryColor),
key: ObjectKey(podcastLocal.title),
child: PodcastCard(
podcastLocal: podcastLocal,
group: widget.group,
),
);
}).toList(),
),
Positioned(
bottom: 30,
right: 30,
child: _saveButton(context),
),
],
: Container(
color: Theme.of(context).primaryColor,
child: Stack(
children: <Widget>[
ReorderableListView(
onReorder: (int oldIndex, int newIndex) {
setState(() {
if (newIndex > oldIndex) {
newIndex -= 1;
}
final PodcastLocal podcast =
widget.group.podcasts.removeAt(oldIndex);
widget.group.podcasts.insert(newIndex, podcast);
});
widget.group.setOrderedPodcasts = widget.group.podcasts;
groupList.addToOrderChanged(widget.group.name);
},
children: widget.group.podcasts
.map<Widget>((PodcastLocal podcastLocal) {
return Container(
decoration:
BoxDecoration(color: Theme.of(context).primaryColor),
key: ObjectKey(podcastLocal.title),
child: PodcastCard(
podcastLocal: podcastLocal,
group: widget.group,
),
);
}).toList(),
),
),
_showSetting
? Positioned.fill(
child: GestureDetector(
onTap: () => setState(() => _showSetting = false),
child: Container(
color: Theme.of(context)
.scaffoldBackgroundColor
.withOpacity(0.5),
),
),
)
: Center(),
_showSetting
? Container(
alignment: Alignment.bottomCenter,
child: Container(
height: 150.0,
alignment: Alignment.center,
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
boxShadow: [
BoxShadow(
offset: Offset(0, -1),
blurRadius: 4,
color: Theme.of(context).brightness ==
Brightness.light
? Colors.grey[400]
: Colors.grey[800],
),
],
),
child: SingleChildScrollView(
scrollDirection: Axis.vertical,
child: Container(
height: 150,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
// mainAxisSize: MainAxisSize.min,
children: <Widget>[
Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
setState(() => _showSetting = false);
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) {
PodcastGroup newGroup =
PodcastGroup(
widget
.group.name,
color: value
.toString()
.substring(
10,
16),
id: widget
.group.id,
podcastList:
widget
.group
.podcastList);
groupList.updateGroup(
newGroup);
Navigator.of(context)
.pop();
},
pickerColor:
Colors.blue,
),
),
))));
},
child: Container(
height: 50.0,
padding:
EdgeInsets.symmetric(horizontal: 20),
child: Row(
children: <Widget>[
Icon(Icons.colorize),
Padding(
padding: EdgeInsets.symmetric(
horizontal: 5.0),
),
Text('Change Color'),
],
),
),
),
),
Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
setState(() => _showSetting = false);
widget.group.name == 'Home'
? Fluttertoast.showToast(
msg:
'Home group is not supported',
gravity: ToastGravity.BOTTOM,
)
: showGeneralDialog(
context: context,
barrierDismissible: true,
barrierLabel:
MaterialLocalizations.of(
context)
.modalBarrierDismissLabel,
barrierColor: Colors.black54,
transitionDuration:
const Duration(
milliseconds: 200),
pageBuilder: (BuildContext
context,
Animation animaiton,
Animation
secondaryAnimation) =>
RenameGroup(
group: widget.group,
));
},
child: Container(
height: 50.0,
padding:
EdgeInsets.symmetric(horizontal: 20),
child: Row(
children: <Widget>[
Icon(Icons.text_fields),
Padding(
padding: EdgeInsets.symmetric(
horizontal: 5.0),
),
Text('Rename'),
],
),
),
),
),
Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
setState(() => _showSetting = false);
widget.group.name == 'Home'
? Fluttertoast.showToast(
msg:
'Home group is not supported',
gravity: ToastGravity.BOTTOM,
)
: showGeneralDialog(
context: context,
barrierDismissible: true,
barrierLabel:
MaterialLocalizations.of(
context)
.modalBarrierDismissLabel,
barrierColor: Colors.black54,
transitionDuration:
const Duration(
milliseconds: 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,
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.all(
Radius.circular(
10.0))),
titlePadding:
EdgeInsets.only(
top: 20,
left: 20,
right: 200,
bottom: 20),
title: Text(
'Delete confirm'),
content: Text(
'Are you sure you want to delete this group? Podcasts will be moved to Home group.'),
actions: <Widget>[
FlatButton(
onPressed: () =>
Navigator.of(
context)
.pop(),
child: Text(
'CANCEL',
style: TextStyle(
color: Colors
.grey[
600]),
),
),
FlatButton(
onPressed: () {
groupList.delGroup(
widget.group);
Navigator.of(
context)
.pop();
},
child: Text(
'CONFIRM',
style: TextStyle(
color: Colors
.red),
),
)
],
),
),
));
},
child: Container(
height: 50,
padding:
EdgeInsets.symmetric(horizontal: 20),
child: Row(
children: <Widget>[
Icon(Icons.delete_outline),
Padding(
padding: EdgeInsets.symmetric(
horizontal: 5.0),
),
Text('Delete'),
],
),
),
),
),
],
),
),
),
),
)
: Center(),
],
],
),
);
}
}
@ -853,14 +437,14 @@ class _RenameGroupState extends State<RenameGroup> {
style: TextStyle(color: Theme.of(context).accentColor)),
)
],
title: Text('Create new group'),
title: Text('Edit group name'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
TextField(
decoration: InputDecoration(
contentPadding: EdgeInsets.symmetric(horizontal: 10),
hintText: 'New Group',
hintText: widget.group.name,
hintStyle: TextStyle(fontSize: 18),
filled: true,
focusedBorder: UnderlineInputBorder(

View File

@ -1,23 +1,135 @@
import 'dart:math' as math;
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:line_icons/line_icons.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:tsacdop/class/podcast_group.dart';
import 'package:tsacdop/podcasts/podcastgroup.dart';
import 'package:tsacdop/podcasts/podcastlist.dart';
import 'package:tsacdop/util/pageroute.dart';
import 'custom_tabview.dart';
class PodcastManage extends StatefulWidget {
@override
_PodcastManageState createState() => _PodcastManageState();
}
class _PodcastManageState extends State<PodcastManage> {
Decoration getIndicator() {
return const UnderlineTabIndicator(
borderSide: BorderSide(color: Colors.red, width: 0),
insets: EdgeInsets.only(
top: 10.0,
));
class _PodcastManageState extends State<PodcastManage>
with TickerProviderStateMixin {
bool _showSetting;
double _menuValue;
AnimationController _controller;
AnimationController _menuController;
Animation _animation;
Animation _menuAnimation;
double _fraction;
int _index;
double _scroll;
@override
void initState() {
super.initState();
_showSetting = false;
_fraction = 0;
_menuValue = 0;
_scroll = 0;
_index = 0;
_menuController = AnimationController(
duration: const Duration(milliseconds: 300), vsync: this);
_controller = AnimationController(
duration: const Duration(milliseconds: 500), vsync: this);
_animation = Tween(begin: 0.0, end: 1.0).animate(_controller)
..addListener(() {
if (mounted)
setState(() {
_fraction = _animation.value;
});
});
_menuAnimation = Tween(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _menuController, curve: Curves.easeInOutBack))
..addListener(() {
if (mounted) setState(() => _menuValue = _menuAnimation.value);
});
_controller.addStatusListener((status) {
if (status == AnimationStatus.completed) {
_controller.stop();
} else if (status == AnimationStatus.dismissed) {
_controller.stop();
}
});
}
@override
void dispose() {
_controller.dispose();
_menuController.dispose();
super.dispose();
}
Widget _saveButton(BuildContext context) {
return Consumer<GroupList>(
builder: (_, groupList, __) {
if (groupList.orderChanged.contains(groupList.groups[_index].name)) {
_controller.forward();
} else if (_fraction > 0) {
_controller.reverse();
}
return Transform(
alignment: FractionalOffset(0.5, 0.5),
transform: Matrix4.rotationY(math.pi * _fraction),
child: Container(
child: InkWell(
child: Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: _fraction > 0.5
? Colors.red
: Theme.of(context).accentColor,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.grey[700],
blurRadius: 5,
offset: Offset(1, 1),
),
]),
alignment: Alignment.center,
child: _fraction > 0.5
? Icon(LineIcons.save_solid, color: Colors.white)
: AnimatedIcon(
color: Colors.white,
icon: AnimatedIcons.menu_close,
progress: _menuController,
),
// color: Colors.white,
),
onTap: () async {
if (_fraction == 0) {
!_showSetting
? _menuController.forward()
: await _menuController.reverse();
setState(() {
_showSetting = !_showSetting;
});
} else {
groupList.saveOrder(groupList.groups[_index]);
groupList.drlFromOrderChanged(groupList.groups[_index].name);
Fluttertoast.showToast(
msg: 'Setting Saved',
gravity: ToastGravity.BOTTOM,
);
_controller.reverse();
}
},
),
),
);
},
);
}
Widget build(BuildContext context) {
@ -53,59 +165,300 @@ class _PodcastManageState extends State<PodcastManage> {
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.white,
unselectedLabelColor: Colors.black,
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: group.getColor(),
// 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(
: Stack(
children: <Widget>[
CustomTabView(
itemCount: _groups.length,
tabBuilder: (context, index) => Tab(
child: Container(
child: TabBarView(
children: _groups.map<Widget>((group) {
return Container(
key: ObjectKey(group),
child: PodcastGroupList(group: group));
}).toList(),
),
),
)
],
));
height: 30.0,
padding: EdgeInsets.symmetric(horizontal: 10.0),
alignment: Alignment.center,
decoration: BoxDecoration(
color: (_scroll - index).abs() > 1
? Colors.grey[300]
: Colors.grey[300]
.withOpacity((_scroll - index).abs()),
borderRadius:
BorderRadius.all(Radius.circular(15)),
),
child: Text(
_groups[index].name,
)),
),
pageBuilder: (context, index) => Container(
key: ObjectKey(_groups[index].name),
child: PodcastGroupList(group: _groups[index])),
onPositionChange: (value) =>
setState(() => _index = value),
onScroll: (value) => setState(() => _scroll = value),
),
_showSetting
? Positioned.fill(
child: GestureDetector(
onTap: () async {
await _menuController.reverse();
setState(() => _showSetting = false);
},
child: Container(
color: Theme.of(context)
.scaffoldBackgroundColor
.withOpacity(0.5 * _menuController.value),
),
),
)
: Center(),
Positioned(
right: 30,
bottom: 30,
child: _saveButton(context),
),
_showSetting
? Positioned(
right: 30 * _menuValue,
bottom: 100,
child: Container(
alignment: Alignment.centerRight,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
_menuController.reverse();
setState(() => _showSetting = false);
_index == 0
? Fluttertoast.showToast(
msg:
'Home group is not supported',
gravity: ToastGravity.BOTTOM,
)
: showGeneralDialog(
context: context,
barrierDismissible: true,
barrierLabel:
MaterialLocalizations.of(
context)
.modalBarrierDismissLabel,
barrierColor: Colors.black54,
transitionDuration:
const Duration(
milliseconds: 300),
pageBuilder: (BuildContext
context,
Animation animaiton,
Animation
secondaryAnimation) =>
RenameGroup(
group: _groups[_index],
));
},
child: Container(
height: 30.0,
decoration: BoxDecoration(
color: Colors.grey[700],
borderRadius: BorderRadius.all(
Radius.circular(10.0))),
padding: EdgeInsets.symmetric(
horizontal: 10),
child: Row(
children: <Widget>[
Icon(
Icons.text_fields,
color: Colors.white,
size: 15.0,
),
Padding(
padding: EdgeInsets.symmetric(
horizontal: 5.0),
),
Text('Edit Name',
style: TextStyle(
color: Colors.white)),
],
),
),
),
),
Padding(
padding: EdgeInsets.symmetric(
vertical: 10.0)),
Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
_menuController.reverse();
setState(() => _showSetting = false);
_index == 0
? Fluttertoast.showToast(
msg:
'Home group is not supported',
gravity: ToastGravity.BOTTOM,
)
: showGeneralDialog(
context: context,
barrierDismissible: true,
barrierLabel:
MaterialLocalizations.of(
context)
.modalBarrierDismissLabel,
barrierColor: Colors.black54,
transitionDuration:
const Duration(
milliseconds: 300),
pageBuilder: (BuildContext
context,
Animation animaiton,
Animation
secondaryAnimation) =>
AnnotatedRegion<
SystemUiOverlayStyle>(
value:
SystemUiOverlayStyle(
statusBarIconBrightness:
Brightness.light,
systemNavigationBarColor:
Theme.of(context)
.brightness ==
Brightness
.light
? Color
.fromRGBO(
113,
113,
113,
1)
: Color
.fromRGBO(
15,
15,
15,
1),
statusBarColor:
Theme.of(context)
.brightness ==
Brightness
.light
? Color
.fromRGBO(
113,
113,
113,
1)
: Color
.fromRGBO(
5,
5,
5,
1),
),
child: SafeArea(
child: AlertDialog(
elevation: 1,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius
.all(Radius
.circular(
10.0))),
titlePadding:
EdgeInsets.only(
top: 20,
left: 20,
right: 200,
bottom: 20),
title: Text(
'Delete confirm'),
content: Text(
'Are you sure you want to delete this group? Podcasts will be moved to Home group.'),
actions: <Widget>[
FlatButton(
onPressed: () =>
Navigator.of(
context)
.pop(),
child: Text(
'CANCEL',
style: TextStyle(
color: Colors
.grey[
600]),
),
),
FlatButton(
onPressed: () {
if (_index ==
groupList
.groups
.length -
1) {
setState(
() {
_index =
_index -
1;
_scroll =
0;
});
groupList.delGroup(_groups[
_index +
1]);
} else {
groupList.delGroup(
_groups[
_index]);
}
Navigator.of(
context)
.pop();
},
child: Text(
'CONFIRM',
style: TextStyle(
color: Colors
.red),
),
)
],
),
),
));
},
child: Container(
height: 30,
decoration: BoxDecoration(
color: Colors.grey[700],
borderRadius: BorderRadius.all(
Radius.circular(10.0))),
padding: EdgeInsets.symmetric(
horizontal: 10),
child: Row(
children: <Widget>[
Icon(
Icons.delete_outline,
color: Colors.red,
size: 15.0,
),
Padding(
padding: EdgeInsets.symmetric(
horizontal: 5.0),
),
Text('Delete',
style: TextStyle(
color: Colors.red)),
],
),
),
),
),
],
),
),
)
: Center(),
],
);
}),
),
),

View File

@ -0,0 +1,287 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_downloader/flutter_downloader.dart';
import 'package:path_provider/path_provider.dart';
import 'package:line_icons/line_icons.dart';
import 'package:tsacdop/class/episodebrief.dart';
import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
class DownloadsManage extends StatefulWidget {
@override
_DownloadsManageState createState() => _DownloadsManageState();
}
class _DownloadsManageState extends State<DownloadsManage> {
//Downloaded size
int _size;
//Downloaded files
int _fileNum;
bool _loadEpisodes;
bool _clearing;
List<EpisodeBrief> _selectedList;
List<EpisodeBrief> _episodes = [];
_getDownloadedRssItem() async {
_episodes = [];
final tasks = await FlutterDownloader.loadTasksWithRawQuery(
query: "SELECT * FROM task WHERE status = 3");
var dbHelper = DBHelper();
await Future.forEach(tasks, (task) async {
EpisodeBrief episode = await dbHelper.getRssItemWithUrl(task.url);
_episodes.add(episode);
});
setState(() {
_loadEpisodes = true;
});
}
_getStorageSize() async {
_size = 0;
_fileNum = 0;
var dir = await getExternalStorageDirectory();
dir.list().forEach((d) {
var fileDir = Directory(d.path);
fileDir.list().forEach((file) async {
await File(file.path).stat().then((value) {
_size += value.size;
_fileNum += 1;
setState(() {});
});
});
});
}
_delSelectedEpisodes() async {
setState(() => _clearing = true);
await Future.forEach(_selectedList, (EpisodeBrief episode) async {
print(episode.downloaded);
await FlutterDownloader.remove(
taskId: episode.downloaded, shouldDeleteContent: true);
var dbHelper = DBHelper();
await dbHelper.delDownloaded(episode.enclosureUrl);
setState(() =>
_episodes.removeWhere((e) => e.enclosureUrl == episode.enclosureUrl));
});
await Future.delayed(Duration(seconds: 1));
setState(() {
_clearing = false;
});
await Future.delayed(Duration(seconds: 1));
setState(() => _selectedList = []);
_getStorageSize();
}
int sumSelected() {
int sum = 0;
if (_selectedList.length == 0) {
return sum;
} else {
_selectedList.forEach((episode) {
sum += episode.enclosureLength;
});
return sum;
}
}
@override
void initState() {
super.initState();
_clearing = false;
_loadEpisodes = false;
_selectedList = [];
_getStorageSize();
_getDownloadedRssItem();
}
@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('Downloads'),
elevation: 0,
backgroundColor: Theme.of(context).primaryColor,
),
body: Stack(
children: <Widget>[
Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Padding(
padding: EdgeInsets.all(10.0),
),
Container(
height: 100.0,
padding: EdgeInsets.only(bottom: 40, left: 60),
alignment: Alignment.centerLeft,
child: RichText(
text: TextSpan(
text: 'Total ',
style: TextStyle(
color: Theme.of(context).accentColor,
fontSize: 20,
),
children: <TextSpan>[
TextSpan(
text: _fileNum.toString(),
style: TextStyle(
color: Theme.of(context).accentColor,
fontSize: 40,
fontWeight: FontWeight.bold)),
TextSpan(
text: ' episodes ',
style: TextStyle(
color: Theme.of(context).accentColor,
fontSize: 20,
)),
TextSpan(
text: (_size ~/ 1000000).toString(),
style: TextStyle(
color: Theme.of(context).accentColor,
fontSize: 60,
fontWeight: FontWeight.bold)),
TextSpan(
text: ' Mb',
style: TextStyle(
color: Theme.of(context).accentColor,
fontSize: 20,
)),
],
),
),
),
_loadEpisodes
? Expanded(
child: ListView.builder(
itemCount: _episodes.length,
shrinkWrap: true,
scrollDirection: Axis.vertical,
itemBuilder: (context, index) {
return Column(
children: <Widget>[
ListTile(
onTap: () {
if (_selectedList
.contains(_episodes[index])) {
setState(() => _selectedList
.removeWhere((episode) =>
episode.enclosureUrl ==
_episodes[index]
.enclosureUrl));
} else {
setState(() => _selectedList
.add(_episodes[index]));
}
},
leading: CircleAvatar(
backgroundImage: FileImage(File(
"${_episodes[index].imagePath}")),
),
title: Text(
_episodes[index].title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
subtitle: _episodes[index]
.enclosureLength !=
0
? Text(((_episodes[index]
.enclosureLength) ~/
1000000)
.toString() +
' Mb')
: Center(),
trailing: Checkbox(
value: _selectedList
.contains(_episodes[index]),
onChanged: (bool boo) {
print(boo);
if (boo) {
setState(() => _selectedList
.add(_episodes[index]));
} else {
setState(() => _selectedList
.removeWhere((episode) =>
episode.enclosureUrl ==
_episodes[index]
.enclosureUrl));
}
},
),
),
Divider(
height: 2,
),
],
);
}),
)
: CircularProgressIndicator(),
],
),
AnimatedPositioned(
duration: Duration(milliseconds: 800),
curve: Curves.elasticInOut,
left: MediaQuery.of(context).size.width / 2 - 50,
bottom: _selectedList.length == 0 ? -100 : 30,
child: InkWell(
onTap: () => _delSelectedEpisodes(),
child: Stack(
alignment: _clearing
? Alignment.centerLeft
: Alignment.centerRight,
children: <Widget>[
Container(
alignment: Alignment.center,
width: 100,
height: 40,
decoration: BoxDecoration(
borderRadius:
BorderRadius.all(Radius.circular(20.0)),
color: Theme.of(context).accentColor,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Icon(
LineIcons.trash_alt_solid,
color: Colors.white,
),
Text((sumSelected() ~/ 1000000).toString() + 'Mb',
style: TextStyle(color: Colors.white)),
],
),
),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: AnimatedContainer(
duration: Duration(milliseconds: 500),
alignment: Alignment.center,
width: _clearing ? 100 : 0,
height: _clearing ? 40 : 0,
decoration: BoxDecoration(
borderRadius:
BorderRadius.all(Radius.circular(20.0)),
color: Colors.red.withOpacity(0.6),
),
),
),
],
)),
),
],
),
),
),
);
}
}

390
lib/settings/history.dart Normal file
View File

@ -0,0 +1,390 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:intl/intl.dart';
import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
import 'package:tsacdop/class/audiostate.dart';
import 'package:tsacdop/class/sub_history.dart';
class PlayedHistory extends StatefulWidget {
@override
_PlayedHistoryState createState() => _PlayedHistoryState();
}
class _PlayedHistoryState extends State<PlayedHistory>
with SingleTickerProviderStateMixin {
Future<List<PlayHistory>> getPlayHistory() async {
DBHelper dbHelper = DBHelper();
List<PlayHistory> playHistory;
playHistory = await dbHelper.getPlayHistory();
await Future.forEach(playHistory, (playHistory) async {
await playHistory.getEpisode();
});
return playHistory;
}
Future<List<SubHistory>> getSubHistory() async {
DBHelper dbHelper = DBHelper();
return await dbHelper.getSubHistory();
}
static String _stringForSeconds(double seconds) {
if (seconds == null) return null;
return '${(seconds ~/ 60)}:${(seconds.truncate() % 60).toString().padLeft(2, '0')}';
}
TabController _controller;
List<int> list = const [0, 1, 2, 3, 4, 5, 6];
Future<List<FlSpot>> getData() async {
var dbHelper = DBHelper();
List<FlSpot> stats = [];
await Future.forEach(list, (day) async {
double mins = await dbHelper.listenMins(7 - day);
stats.add(FlSpot(day.toDouble(), mins));
});
return stats;
}
@override
void initState() {
super.initState();
_controller = TabController(length: 2, vsync: this);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
double top = 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(
body: NestedScrollView(
headerSliverBuilder:
(BuildContext context, bool innerBoxScrolled) {
return <Widget>[
SliverAppBar(
elevation: 0,
expandedHeight: 260,
floating: false,
pinned: true,
flexibleSpace: LayoutBuilder(
builder:
(BuildContext context, BoxConstraints constraints) {
top = constraints.biggest.height;
return FlexibleSpaceBar(
title: top < 70
? Text(
'History',
)
: Center(),
background: Padding(
padding: EdgeInsets.only(
top: 50, left: 50, right: 50, bottom: 30),
child: FutureBuilder<List<FlSpot>>(
future: getData(),
builder: (context, snapshot) {
return snapshot.hasData
? HistoryChart(snapshot.data)
: Center();
}),
),
);
},
),
),
SliverPersistentHeader(
delegate: _SliverAppBarDelegate(
TabBar(
controller: _controller,
tabs: <Widget>[
Tab(
child: Text('Listen'),
),
Tab(
child: Text('Subscribe'),
)
],
),
Theme.of(context).primaryColor),
pinned: true,
),
];
},
body: TabBarView(controller: _controller, children: <Widget>[
FutureBuilder<List<PlayHistory>>(
future: getPlayHistory(),
builder: (context, snapshot) {
double _width = MediaQuery.of(context).size.width;
return snapshot.hasData
? ListView.builder(
shrinkWrap: true,
scrollDirection: Axis.vertical,
itemCount: snapshot.data.length,
itemBuilder: (BuildContext context, int index) {
return Column(
children: <Widget>[
ListTile(
title: Column(
mainAxisAlignment:
MainAxisAlignment.start,
crossAxisAlignment:
CrossAxisAlignment.start,
children: <Widget>[
Text(
DateFormat.yMd().add_jm().format(
snapshot.data[index].playdate),
style: TextStyle(
color: const Color(0xff67727d),
fontSize: 15,
fontStyle: FontStyle.italic),
),
Text(
snapshot.data[index].title,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
subtitle: Container(
width: _width,
child: Row(
children: <Widget>[
Icon(Icons.timelapse, color: Colors.grey[400],),
Container(
height: 2,
decoration: BoxDecoration(
border: Border(bottom: BorderSide(color: Colors.grey[400], width: 2.0))
),
width: _width *
snapshot.data[index]
.seekValue <
(_width - 120)
? _width *
snapshot
.data[index].seekValue
: _width - 120,
),
Padding(
padding: EdgeInsets.symmetric(
horizontal: 2),
),
Container(
width: 50,
alignment: Alignment.center,
decoration: BoxDecoration(
color: Theme.of(context)
.accentColor,
borderRadius: BorderRadius.all(
Radius.circular(10))),
padding: EdgeInsets.all(2),
child: Text(
_stringForSeconds(
snapshot.data[index].seconds),
style: TextStyle(
color: Colors.white),
),
),
],
),
),
),
// Divider(height: 2),
],
);
})
: Center(
child: CircularProgressIndicator(),
);
},
),
FutureBuilder<List<SubHistory>>(
future: getSubHistory(),
builder: (context, snapshot) {
return snapshot.hasData
? ListView.builder(
shrinkWrap: true,
scrollDirection: Axis.vertical,
itemCount: snapshot.data.length,
itemBuilder: (BuildContext context, int index) {
bool _status = snapshot.data[index].status;
return Column(
children: <Widget>[
ListTile(
enabled: _status,
title: Column(
mainAxisAlignment:
MainAxisAlignment.start,
crossAxisAlignment:
CrossAxisAlignment.start,
children: <Widget>[
Text(
DateFormat.yMd().add_jm().format(
snapshot.data[index].subDate),
style: TextStyle(
color: const Color(0xff67727d),
fontSize: 15,
fontStyle: FontStyle.italic),
),
Text(snapshot.data[index].title),
],
),
subtitle: Row(
children: <Widget>[
_status
? Text(DateTime.now()
.difference(snapshot
.data[index].subDate)
.inDays
.toString() +
' days')
: Text(snapshot.data[index].delDate
.difference(snapshot
.data[index].subDate)
.inDays
.toString() +
' days'),
Spacer(),
!_status
? Text(
'Removed at ' +
DateFormat.yMd()
.add_jm()
.format(snapshot
.data[index]
.delDate),
style: TextStyle(
color: Colors.red),
)
: Center(),
],
),
),
Divider(
height: 2,
)
],
);
})
: Center(
child: CircularProgressIndicator(),
);
},
),
])),
),
),
);
}
}
class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
_SliverAppBarDelegate(this._tabBar, this._color);
final Color _color;
final TabBar _tabBar;
@override
double get minExtent => _tabBar.preferredSize.height;
@override
double get maxExtent => _tabBar.preferredSize.height;
@override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return new Container(
color: _color,
child: _tabBar,
);
}
@override
bool shouldRebuild(_SliverAppBarDelegate oldDelegate) {
return true;
}
}
class HistoryChart extends StatelessWidget {
final List<FlSpot> stats;
HistoryChart(this.stats);
@override
Widget build(BuildContext context) {
return LineChart(
LineChartData(
backgroundColor: Colors.transparent,
gridData: FlGridData(
show: true,
drawHorizontalLine: true,
getDrawingHorizontalLine: (value) {
return value % 60 == 0
? FlLine(
color: Theme.of(context).brightness == Brightness.light
? Colors.grey[400]
: Colors.grey[700],
strokeWidth: 1,
)
: FlLine(color: Colors.transparent);
},
),
titlesData: FlTitlesData(
show: true,
bottomTitles: SideTitles(
textStyle: TextStyle(
color: const Color(0xff67727d),
fontWeight: FontWeight.bold,
fontSize: 12,
),
showTitles: true,
reservedSize: 10,
getTitles: (value) {
return DateFormat.E().format(
DateTime.now().subtract(Duration(days: (7 - value.toInt()))));
},
margin: 5,
),
leftTitles: SideTitles(
showTitles: true,
textStyle: TextStyle(
color: const Color(0xff67727d),
fontWeight: FontWeight.bold,
fontSize: 12,
),
getTitles: (value) {
return value % 60 == 0 && value > 0 ? '${value ~/ 60}h' : '';
},
reservedSize: 20,
margin: 5,
),
),
borderData: FlBorderData(
show: false,
border: Border(
left: BorderSide(color: Colors.red, width: 2),
)),
lineBarsData: [
LineChartBarData(
spots: this.stats,
isCurved: false,
colors: [Theme.of(context).accentColor],
barWidth: 3,
isStrokeCapRound: true,
dotData: FlDotData(
show: true,
dotSize: 5,
dotColor: Theme.of(context).accentColor,
),
),
],
),
);
}
}

90
lib/settings/libries.dart Normal file
View File

@ -0,0 +1,90 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:url_launcher/url_launcher.dart';
import 'licenses.dart';
class Libries extends StatelessWidget {
_launchUrl(String url) async {
if (await canLaunch(url)) {
await launch(url);
} else {
throw 'Could not launch $url';
}
}
@override
Widget build(BuildContext context) {
return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle(
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
systemNavigationBarColor: Theme.of(context).primaryColor,
statusBarColor: Theme.of(context).primaryColor),
child: SafeArea(
child: Scaffold(
appBar: AppBar(
title: Text('Libraies'),
elevation: 0,
backgroundColor: Theme.of(context).primaryColor,
),
body: SingleChildScrollView(
scrollDirection: Axis.vertical,
child: 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('Google',
style: Theme.of(context)
.textTheme
.bodyText1
.copyWith(color: Theme.of(context).accentColor)),
),
Column(
children: google.map<Widget>(
(e) {
return ListTile(
contentPadding: EdgeInsets.symmetric(horizontal: 80),
onTap: () => _launchUrl(e.link),
title: Text(e.name),
subtitle: Text(e.license),
);
},
).toList(),
),
Container(
height: 30.0,
padding: EdgeInsets.symmetric(horizontal: 80),
alignment: Alignment.centerLeft,
child: Text('Plugins',
style: Theme.of(context)
.textTheme
.bodyText1
.copyWith(color: Theme.of(context).accentColor)),
),
Container(
child: Column(
children: plugins.map<Widget>(
(e) {
return ListTile(
onTap: () => _launchUrl(e.link),
contentPadding: EdgeInsets.symmetric(horizontal: 80),
title: Text(e.name),
subtitle: Text(e.license),
);
},
).toList(),
),
),
],
),
),
),
),
);
}
}

View File

@ -0,0 +1,44 @@
const String apacheLicense = "Apache License 2.0";
const String mit = "MIT License";
const String bsd ="BSD 3-Clause";
const String gpl = "GPL 3.0";
class Libries {
String name; String license; String link;
Libries(this.name, this.license, this.link);
}
List<Libries> google = [
Libries('Android X', apacheLicense, 'https://source.android.com/setup/start/licenses'),
Libries('Flutter', bsd, 'https://github.com/flutter/flutter/blob/master/LICENSE')
];
List<Libries> plugins = [
Libries('json_annotation',bsd, 'https://pub.dev/packages/json_annotation'),
Libries('sqflite', mit, 'https://pub.dev/packages/sqflite'),
Libries('flutter_html', mit, 'https://pub.dev/packages/flutter_html'),
Libries('path_provider', bsd, 'https://pub.dev/packages/path_provider'),
Libries('color_thief_flutter', mit, 'https://pub.dev/packages/color_thief_flutter'),
Libries('provider', mit, 'https://pub.dev/packages/provider'),
Libries('google_fonts', apacheLicense, 'https://pub.dev/packages/google_fonts'),
Libries('dio', mit, 'https://pub.dev/packages/dio'),
Libries('file_picker', mit, 'https://pub.dev/packages/file_picker'),
Libries('xml', mit, 'https://pub.dev/packages/xml'),
Libries('marquee', mit, 'https://pub.dev/packages/marquee'),
Libries('flutter_downloader', bsd, 'https://pub.dev/packages/flutter_downloader'),
Libries('permission_handler', mit, 'https://pub.dev/packages/permission_handler'),
Libries('fluttertoast', mit, 'https://pub.dev/packages/fluttertoast'),
Libries('intl', bsd, 'https://pub.dev/packages/intl'),
Libries('url_launcher', bsd, 'https://pub.dev/packages/url_launcher'),
Libries('image', apacheLicense, 'https://pub.dev/packages/image'),
Libries('shared_preferences', bsd, 'https://pub.dev/packages/shared_preferences'),
Libries('uuid', mit, 'https://pub.dev/packages/uuid'),
Libries('tuple', bsd, 'https://pub.dev/packages/tuple'),
Libries('cached_network_image', mit, 'https://pub.dev/packages/cached_network_image'),
Libries('workmanager', mit, 'https://pub.dev/packages/workmanager'),
Libries('flutter_colorpicker', mit, 'https://pub.dev/packages/flutter_colorpicker'),
Libries('app_settings', mit, 'https://pub.dev/packages/app_settings'),
Libries('fl_chart', bsd, 'https://pub.dev/packages/fl_chart'),
Libries('audio_service', mit, 'https://pub.dev/packages/audio_service'),
Libries('just_audio', apacheLicense, 'https://pub.dev/packages/just_audio'),
Libries('line_icons', gpl, 'https://pub.dev/packages/line_icons'),
];

View File

@ -1,10 +1,30 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:workmanager/workmanager.dart';
import 'package:line_icons/line_icons.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:tsacdop/class/audiostate.dart';
import 'package:tsacdop/class/settingstate.dart';
import 'package:tsacdop/settings/theme.dart';
import 'package:tsacdop/settings/storage.dart';
import 'package:tsacdop/settings/history.dart';
import 'libries.dart';
class Settings extends StatelessWidget {
_launchUrl(String url) async {
if (await canLaunch(url)) {
await launch(url);
} else {
throw 'Could not launch $url';
}
}
@override
Widget build(BuildContext context) {
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
var settings = Provider.of<SettingState>(context, listen: false);
return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle(
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
@ -18,121 +38,167 @@ class Settings extends StatelessWidget {
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),
],
),
],
),
],
body: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
scrollDirection: Axis.vertical,
child: 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(
physics: ClampingScrollPhysics(),
shrinkWrap: true,
scrollDirection: Axis.vertical,
children: <Widget>[
ListTile(
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ThemeSetting())),
contentPadding:
EdgeInsets.symmetric(horizontal: 25.0),
leading: Icon(LineIcons.adjust_solid),
title: Text('Appearance'),
subtitle: Text('Colors and themes'),
),
Divider(height: 2),
ListTile(
contentPadding:
EdgeInsets.symmetric(horizontal: 25.0),
leading: Icon(LineIcons.play_circle),
title: Text('AutoPlay'),
subtitle: Text('Autoplay next episode in playlist'),
trailing: Selector<AudioPlayerNotifier, bool>(
selector: (_, audio) => audio.autoPlay,
builder: (_, data, __) => Switch(
value: data,
onChanged: (boo) => audio.autoPlaySwitch = boo),
),
),
Divider(height: 2),
ListTile(
contentPadding:
EdgeInsets.symmetric(horizontal: 25.0),
leading: Icon(LineIcons.cloud_download_alt_solid),
title: Text('AutoUpdate'),
subtitle: Text('Auto update feed every day'),
trailing: Selector<SettingState, bool>(
selector: (_, settings) => settings.autoUpdate,
builder: (_, data, __) => Switch(
value: data,
onChanged: (boo) async {
settings.autoUpdate = boo;
if (!boo) await Workmanager.cancelAll();
}),
),
),
Divider(height: 2),
ListTile(
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => StorageSetting())),
contentPadding:
EdgeInsets.symmetric(horizontal: 25.0),
leading: Icon(LineIcons.save),
title: Text('Storage'),
subtitle: Text('Manage cache and download storage'),
),
Divider(height: 2),
ListTile(
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PlayedHistory())),
contentPadding:
EdgeInsets.symmetric(horizontal: 25.0),
leading: Icon(Icons.update),
title: Text('History'),
subtitle: Text('Listen data'),
),
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(
physics: ClampingScrollPhysics(),
shrinkWrap: true,
scrollDirection: Axis.vertical,
children: <Widget>[
ListTile(
onTap: () => _launchUrl(
'https://github.com/stonega/tsacdop/releases'),
contentPadding:
EdgeInsets.symmetric(horizontal: 25.0),
leading: Icon(LineIcons.map_signs_solid),
title: Text('Changelog'),
subtitle: Text('List of chagnes'),
),
Divider(height: 2),
ListTile(
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => Libries())),
contentPadding:
EdgeInsets.symmetric(horizontal: 25.0),
leading: Icon(LineIcons.book_open_solid),
title: Text('Libraries'),
subtitle:
Text('Open source libraried in application'),
),
Divider(height: 2),
ListTile(
onTap: () => _launchUrl(
'mailto:<xijieyin@gmail.com>?subject=Tsacdop Feedback'),
contentPadding:
EdgeInsets.symmetric(horizontal: 25.0),
leading: Icon(LineIcons.bug_solid),
title: Text('Feedback'),
subtitle: Text('Bugs and feature requests'),
),
Divider(height: 2),
],
),
],
),
],
),
),
),
),

74
lib/settings/storage.dart Normal file
View File

@ -0,0 +1,74 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:app_settings/app_settings.dart';
import 'package:tsacdop/settings/downloads_manage.dart';
class StorageSetting 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('Storage'),
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('Storage',
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) => DownloadsManage())),
contentPadding: EdgeInsets.symmetric(horizontal: 80.0),
title: Text('Downloads'),
subtitle: Text('Manage doanloaded audio files'),
),
Divider(height: 2),
ListTile(
onTap: () => AppSettings.openAppSettings(),
contentPadding: EdgeInsets.symmetric(horizontal: 80.0),
// leading: Icon(Icons.colorize),
title: Text('Cache'),
subtitle: Text('Audio cache'),
),
Divider(height: 2),
],
),
],
),
],
),
),
),
);
}
}

View File

@ -7,7 +7,7 @@ import 'package:tsacdop/class/settingstate.dart';
class ThemeSetting extends StatelessWidget {
@override
Widget build(BuildContext context) {
var settings = Provider.of<SettingState>(context);
var settings = Provider.of<SettingState>(context, listen: false);
return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle(
statusBarIconBrightness: Theme.of(context).accentColorBrightness,

View File

@ -22,7 +22,8 @@ extension Colorize on String {
_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);
_c = Color.fromRGBO(color[0] < 50 ? 100 : color[0],
color[1] < 50 ? 100 : color[1], color[2] < 50 ? 100 : color[2], 1.0);
}
return _c;
}

View File

@ -5,6 +5,11 @@ import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_downloader/flutter_downloader.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';
import 'package:tuple/tuple.dart';
import 'package:line_icons/line_icons.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:tsacdop/class/audiostate.dart';
import 'package:tsacdop/class/episodebrief.dart';
import 'package:tsacdop/episodes/episodedetail.dart';
import 'package:tsacdop/util/pageroute.dart';
@ -24,32 +29,119 @@ class EpisodeGrid extends StatelessWidget {
this.showNumber,
this.heroTag})
: super(key: key);
Offset offset;
@override
Widget build(BuildContext context) {
double _width = MediaQuery.of(context).size.width;
return CustomScrollView(
physics: const AlwaysScrollableScrollPhysics(),
primary: false,
slivers: <Widget>[
SliverPadding(
padding: const EdgeInsets.all(5.0),
sliver: SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
childAspectRatio: 1.0,
crossAxisCount: 3,
mainAxisSpacing: 6.0,
crossAxisSpacing: 6.0,
_showPopupMenu(Offset offset, EpisodeBrief episode, BuildContext context,
bool isPlaying, bool isInPlaylist) async {
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
double left = offset.dx;
double top = offset.dy;
await showMenu<int>(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(10))),
context: context,
position: RelativeRect.fromLTRB(left, top, _width - left, 0),
items: <PopupMenuEntry<int>>[
PopupMenuItem(
value: 0,
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Icon(
LineIcons.play_circle_solid,
color: Theme.of(context).accentColor,
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 2),
),
!isPlaying ? Text('Play') : Text('Playing'),
],
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
Color _c =
(Theme.of(context).brightness == Brightness.light)
? podcast[index].primaryColor.colorizedark()
: podcast[index].primaryColor.colorizeLight();
return Material(
),
PopupMenuItem(
value: 1,
child: Row(
children: <Widget>[
Icon(
LineIcons.clock_solid,
color: Colors.red,
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 2),
),
!isInPlaylist ? Text('Later') : Text('Remove')
],
)),
],
elevation: 5.0,
).then((value) {
if (value == 0) {
if (!isPlaying) audio.episodeLoad(episode);
} else if (value == 1) {
if (isInPlaylist) {
audio.addToPlaylist(episode);
Fluttertoast.showToast(
msg: 'Added to playlist',
gravity: ToastGravity.BOTTOM,
);
} else {
audio.delFromPlaylist(episode);
Fluttertoast.showToast(
msg: 'Removed from playlist',
gravity: ToastGravity.BOTTOM,
);
}
}
});
}
return SliverPadding(
padding: const EdgeInsets.all(5.0),
sliver: SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
childAspectRatio: 1.0,
crossAxisCount: 3,
mainAxisSpacing: 6.0,
crossAxisSpacing: 6.0,
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
Color _c = (Theme.of(context).brightness == Brightness.light)
? podcast[index].primaryColor.colorizedark()
: podcast[index].primaryColor.colorizeLight();
return Selector<AudioPlayerNotifier,
Tuple2<EpisodeBrief, List<String>>>(
selector: (_, audio) => Tuple2(audio?.episode,
audio.queue.playlist.map((e) => e.enclosureUrl).toList()),
builder: (_, data, __) => Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(5.0)),
color: Theme.of(context).scaffoldBackgroundColor,
boxShadow: [
BoxShadow(
color: Theme.of(context).primaryColor,
blurRadius: 0.5,
spreadRadius: 0.5,
),
]),
alignment: Alignment.center,
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.all(Radius.circular(5.0)),
onTapDown: (details) => offset = Offset(
details.globalPosition.dx, details.globalPosition.dy),
onLongPress: () => _showPopupMenu(
offset,
podcast[index],
context,
data.item1 == podcast[index],
data.item2.contains(podcast[index].enclosureUrl)),
onTap: () {
Navigator.push(
context,
@ -61,25 +153,17 @@ class EpisodeGrid extends StatelessWidget {
);
},
child: Container(
padding: const EdgeInsets.all(8.0),
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(5.0)),
color: Theme.of(context).scaffoldBackgroundColor,
border: Border.all(
color:
Theme.of(context).brightness == Brightness.light
? Theme.of(context).primaryColor
: Theme.of(context).scaffoldBackgroundColor,
width: 3.0,
),
boxShadow: [
BoxShadow(
color: Theme.of(context).primaryColor,
blurRadius: 0.5,
spreadRadius: 0.5,
),
]),
alignment: Alignment.center,
padding: EdgeInsets.all(8.0),
borderRadius: BorderRadius.all(Radius.circular(5.0)),
border: Border.all(
color:
Theme.of(context).brightness == Brightness.light
? Theme.of(context).primaryColor
: Theme.of(context).scaffoldBackgroundColor,
width: 1.0,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
@ -94,6 +178,7 @@ class EpisodeGrid extends StatelessWidget {
height: _width / 16,
width: _width / 16,
child: CircleAvatar(
backgroundColor: _c.withOpacity(0.5),
backgroundImage: FileImage(
File("${podcast[index].imagePath}")),
),
@ -140,7 +225,6 @@ class EpisodeGrid extends StatelessWidget {
alignment: Alignment.bottomLeft,
child: Text(
podcast[index].dateToString(),
//podcast[index].pubDate.substring(4, 16),
style: TextStyle(
fontSize: _width / 35,
color: _c,
@ -175,13 +259,13 @@ class EpisodeGrid extends StatelessWidget {
),
),
),
);
},
childCount: podcast.length,
),
),
),
),
);
},
childCount: podcast.length,
),
],
),
);
}
}

BIN
preview/Screenshot_data.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

View File

@ -11,7 +11,7 @@ description: An easy-use podacasts player.
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 0.1.1
version: 0.1.2
environment:
sdk: ">=2.6.0 <3.0.0"
@ -28,7 +28,7 @@ dev_dependencies:
flutter_test:
sdk: flutter
json_annotation: ^3.0.1
sqflite: ^1.2.1
sqflite: ^1.2.2+1
flutter_html: ^0.11.1
path_provider: ^1.6.1
color_thief_flutter: ^1.0.1
@ -38,7 +38,6 @@ dev_dependencies:
file_picker: ^1.4.3+2
xml: ^3.5.0
marquee: ^1.3.1
audiofileplayer: ^1.1.1
flutter_downloader: ^1.4.1
permission_handler: ^4.3.0
fluttertoast: ^3.1.3
@ -49,11 +48,16 @@ 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
workmanager: ^0.2.2
flutter_colorpicker: ^0.3.2
lazy_loading_list: ^1.0.0+1
app_settings: ^3.0.1
fl_chart: ^0.8.3
audio_service: ^0.6.2
just_audio: ^0.1.3
rxdart: ^0.23.1
line_icons:
git:
url: https://github.com/galonsos/line_icons.git
# For information on the generic Dart part of this file, see the