Change audio plugin to just_audio
Add storage setting Add history setting
|
@ -13,9 +13,7 @@ jobs:
|
||||||
- run:
|
- run:
|
||||||
name: Run Flutter doctor
|
name: Run Flutter doctor
|
||||||
command: flutter doctor
|
command: flutter doctor
|
||||||
- run:
|
|
||||||
name: flutter pub get
|
|
||||||
command: flutter pub get
|
|
||||||
-run:
|
-run:
|
||||||
name: flutter run
|
name: flutter run
|
||||||
command: flutter run
|
command: flutter run
|
||||||
|
|
14
README.md
|
@ -1,19 +1,21 @@
|
||||||
# Tsacdop
|
# Tsacdop
|
||||||
[![CircleCI](https://circleci.com/gh/stonega/tsacdop.svg?style=svg)](https://circleci.com/gh/stonega/workflows/tsacdop/)
|
[![CircleCI](https://circleci.com/gh/stonega/tsacdop.svg?style=svg)](https://circleci.com/gh/stonega/workflows/tsacdop/)
|
||||||
## About
|
## About
|
||||||
![logo](https://raw.githubusercontent.com/stonega/tsacdop/master/android/app/src/main/res/mipmap-hdpi/ic_launcher.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"/>
|
||||||
![tsacdop](https://raw.githubusercontent.com/stonega/tsacdop/master/android/app/src/main/res/mipmap-hdpi/text.png)
|
<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.
|
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.
|
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).
|
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
|
## License
|
||||||
|
|
||||||
Tsacdop is licensed under the [MIT](https://github.com/stonega/tsacdop/blob/master/LICENSE) license.
|
Tsacdop is licensed under the [MIT](https://github.com/stonega/tsacdop/blob/master/LICENSE) license.
|
||||||
|
|
|
@ -67,6 +67,7 @@ android {
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
signingConfig signingConfigs.release
|
signingConfig signingConfigs.release
|
||||||
|
shrinkResources false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,15 +6,16 @@
|
||||||
FlutterApplication and put your custom class here. -->
|
FlutterApplication and put your custom class here. -->
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||||
<application android:name="io.flutter.app.FlutterApplication" android:label="Tsacdop" android:icon="@mipmap/ic_launcher" android:networkSecurityConfig="@xml/network_security_config">
|
<application android:name="io.flutter.app.FlutterApplication" android:label="Tsacdop" android:icon="@mipmap/ic_launcher" android:networkSecurityConfig="@xml/network_security_config">
|
||||||
<activity android:name=".MainActivity" android:launchMode="singleTop" android:theme="@style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize">
|
<activity android:name=".MainActivity" android:launchMode="singleTop" android:theme="@style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</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>
|
</activity>
|
||||||
<service android:name="com.google.flutter.plugins.audiofileplayer.AudiofileplayerService">
|
<service android:name="com.ryanheise.audioservice.AudioService">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.media.browse.MediaBrowserService" />
|
<action android:name="android.media.browse.MediaBrowserService" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
After Width: | Height: | Size: 212 B |
After Width: | Height: | Size: 217 B |
After Width: | Height: | Size: 140 B |
After Width: | Height: | Size: 272 B |
After Width: | Height: | Size: 251 B |
After Width: | Height: | Size: 257 B |
After Width: | Height: | Size: 102 B |
After Width: | Height: | Size: 184 B |
After Width: | Height: | Size: 163 B |
After Width: | Height: | Size: 108 B |
After Width: | Height: | Size: 159 B |
After Width: | Height: | Size: 156 B |
After Width: | Height: | Size: 166 B |
After Width: | Height: | Size: 92 B |
After Width: | Height: | Size: 264 B |
After Width: | Height: | Size: 259 B |
After Width: | Height: | Size: 162 B |
After Width: | Height: | Size: 288 B |
After Width: | Height: | Size: 260 B |
After Width: | Height: | Size: 265 B |
After Width: | Height: | Size: 114 B |
After Width: | Height: | Size: 341 B |
After Width: | Height: | Size: 302 B |
After Width: | Height: | Size: 202 B |
After Width: | Height: | Size: 550 B |
After Width: | Height: | Size: 460 B |
After Width: | Height: | Size: 450 B |
After Width: | Height: | Size: 196 B |
After Width: | Height: | Size: 435 B |
After Width: | Height: | Size: 411 B |
After Width: | Height: | Size: 244 B |
After Width: | Height: | Size: 488 B |
After Width: | Height: | Size: 509 B |
After Width: | Height: | Size: 522 B |
After Width: | Height: | Size: 244 B |
After Width: | Height: | Size: 4.4 KiB |
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 5.3 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 14 KiB |
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name = "blackGrey">
|
||||||
|
#121212
|
||||||
|
</color>
|
||||||
|
</resources>
|
|
@ -1,7 +1,8 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<network-security-config>
|
<network-security-config>
|
||||||
<domain-config cleartextTrafficPermitted="true">
|
<base-config cleartextTrafficPermitted="true">
|
||||||
<domain includeSubdomains="true">lizhi.fm</domain>
|
<trust-anchors>
|
||||||
<domain includeSubdomains="true">xmcdn.com</domain>
|
<certificates src="system" />
|
||||||
</domain-config>
|
</trust-anchors>
|
||||||
|
</base-config>
|
||||||
</network-security-config>
|
</network-security-config>
|
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 14 KiB |
|
@ -1,17 +1,46 @@
|
||||||
import 'dart:typed_data';
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
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: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/key_value_storage.dart';
|
||||||
import 'package:tsacdop/local_storage/sqflite_localpodcast.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 {
|
class PlayHistory {
|
||||||
DBHelper dbHelper = DBHelper();
|
DBHelper dbHelper = DBHelper();
|
||||||
|
@ -19,7 +48,9 @@ class PlayHistory {
|
||||||
String url;
|
String url;
|
||||||
double seconds;
|
double seconds;
|
||||||
double seekValue;
|
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 _episode;
|
||||||
EpisodeBrief get episode => _episode;
|
EpisodeBrief get episode => _episode;
|
||||||
|
|
||||||
|
@ -31,32 +62,36 @@ class PlayHistory {
|
||||||
class Playlist {
|
class Playlist {
|
||||||
String name;
|
String name;
|
||||||
DBHelper dbHelper = DBHelper();
|
DBHelper dbHelper = DBHelper();
|
||||||
List<String> urls;
|
// list of urls
|
||||||
|
//List<String> _urls;
|
||||||
|
//list of episodes
|
||||||
List<EpisodeBrief> _playlist;
|
List<EpisodeBrief> _playlist;
|
||||||
|
//list of miediaitem
|
||||||
|
|
||||||
List<EpisodeBrief> get playlist => _playlist;
|
List<EpisodeBrief> get playlist => _playlist;
|
||||||
KeyValueStorage storage = KeyValueStorage('playlist');
|
KeyValueStorage storage = KeyValueStorage('playlist');
|
||||||
Playlist(this.name, {List<String> urls}) : urls = urls ?? [];
|
|
||||||
|
|
||||||
getPlaylist() async {
|
getPlaylist() async {
|
||||||
List<String> _urls = await storage.getStringList();
|
List<String> urls = await storage.getStringList();
|
||||||
if (_urls.length == 0) {
|
print(urls);
|
||||||
|
if (urls.length == 0) {
|
||||||
_playlist = [];
|
_playlist = [];
|
||||||
} else {
|
} else {
|
||||||
_playlist = [];
|
_playlist = [];
|
||||||
await Future.forEach(_urls, (url) async {
|
await Future.forEach(urls, (url) async {
|
||||||
EpisodeBrief episode = await dbHelper.getRssItemWithUrl(url);
|
EpisodeBrief episode = await dbHelper.getRssItemWithUrl(url);
|
||||||
print(episode.title);
|
print(episode.title);
|
||||||
_playlist.add(episode);
|
_playlist.add(episode);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
print(_playlist.length);
|
print('Playlist: ' + _playlist.length.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
savePlaylist() async {
|
savePlaylist() async {
|
||||||
urls = [];
|
List<String> urls = [];
|
||||||
urls.addAll(_playlist.map((e) => e.enclosureUrl));
|
urls.addAll(_playlist.map((e) => e.enclosureUrl));
|
||||||
print(urls);
|
print(urls);
|
||||||
await storage.saveStringlist(urls);
|
await storage.saveStringList(urls);
|
||||||
}
|
}
|
||||||
|
|
||||||
addToPlayList(EpisodeBrief episodeBrief) async {
|
addToPlayList(EpisodeBrief episodeBrief) async {
|
||||||
|
@ -64,6 +99,11 @@ class Playlist {
|
||||||
await savePlaylist();
|
await savePlaylist();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addToPlayListAt(EpisodeBrief episodeBrief, int index) async {
|
||||||
|
_playlist.insert(index, episodeBrief);
|
||||||
|
await savePlaylist();
|
||||||
|
}
|
||||||
|
|
||||||
delFromPlaylist(EpisodeBrief episodeBrief) async {
|
delFromPlaylist(EpisodeBrief episodeBrief) async {
|
||||||
_playlist
|
_playlist
|
||||||
.removeWhere((item) => item.enclosureUrl == episodeBrief.enclosureUrl);
|
.removeWhere((item) => item.enclosureUrl == episodeBrief.enclosureUrl);
|
||||||
|
@ -71,39 +111,32 @@ class Playlist {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AudioPlayer extends ChangeNotifier {
|
class AudioPlayerNotifier 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';
|
|
||||||
|
|
||||||
DBHelper dbHelper = DBHelper();
|
DBHelper dbHelper = DBHelper();
|
||||||
KeyValueStorage storage = KeyValueStorage('audioposition');
|
KeyValueStorage storage = KeyValueStorage('audioposition');
|
||||||
EpisodeBrief _episode;
|
EpisodeBrief _episode;
|
||||||
Playlist _queue = Playlist('now');
|
Playlist _queue = Playlist();
|
||||||
|
BasicPlaybackState _audioState = BasicPlaybackState.none;
|
||||||
bool _playerRunning = false;
|
bool _playerRunning = false;
|
||||||
Audio _backgroundAudio;
|
bool _noSlide = true;
|
||||||
bool _backgroundAudioPlaying = false;
|
int _backgroundAudioDuration = 0;
|
||||||
double _backgroundAudioDurationSeconds = 0;
|
int _backgroundAudioPosition = 0;
|
||||||
double _backgroundAudioPositionSeconds = 0;
|
|
||||||
bool _remoteAudioLoading = false;
|
|
||||||
String _remoteErrorMessage;
|
String _remoteErrorMessage;
|
||||||
|
|
||||||
double _seekSliderValue = 0.0;
|
double _seekSliderValue = 0.0;
|
||||||
int _lastPostion;
|
int _lastPostion = 0;
|
||||||
bool _skip = false;
|
|
||||||
bool _stopOnComplete = false;
|
bool _stopOnComplete = false;
|
||||||
Timer _stopTimer;
|
Timer _stopTimer;
|
||||||
//Show stopwatch after user setting timer.
|
//Show stopwatch after user setting timer.
|
||||||
bool _showStopWatch = false;
|
bool _showStopWatch = false;
|
||||||
|
bool _autoPlay = true;
|
||||||
|
DateTime _current;
|
||||||
final Logger _logger = Logger('audiofileplayer');
|
int _currentPosition;
|
||||||
|
|
||||||
bool get backgroundAudioPlaying => _backgroundAudioPlaying;
|
BasicPlaybackState get audioState => _audioState;
|
||||||
bool get remoteAudioLoading => _remoteAudioLoading;
|
|
||||||
double get backgroundAudioDuration => _backgroundAudioDurationSeconds;
|
int get backgroundAudioDuration => _backgroundAudioDuration;
|
||||||
double get backgroundAudioPosition => _backgroundAudioPositionSeconds;
|
int get backgroundAudioPosition => _backgroundAudioPosition;
|
||||||
double get seekSliderValue => _seekSliderValue;
|
double get seekSliderValue => _seekSliderValue;
|
||||||
String get remoteErrorMessage => _remoteErrorMessage;
|
String get remoteErrorMessage => _remoteErrorMessage;
|
||||||
bool get playerRunning => _playerRunning;
|
bool get playerRunning => _playerRunning;
|
||||||
|
@ -112,385 +145,470 @@ class AudioPlayer extends ChangeNotifier {
|
||||||
EpisodeBrief get episode => _episode;
|
EpisodeBrief get episode => _episode;
|
||||||
bool get stopOnComplete => _stopOnComplete;
|
bool get stopOnComplete => _stopOnComplete;
|
||||||
bool get showStopWatch => _showStopWatch;
|
bool get showStopWatch => _showStopWatch;
|
||||||
|
bool get autoPlay => _autoPlay;
|
||||||
|
|
||||||
set setStopOnComplete(bool boo) {
|
set setStopOnComplete(bool boo) {
|
||||||
_stopOnComplete = boo;
|
_stopOnComplete = boo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
set autoPlaySwitch(bool boo) {
|
||||||
|
_autoPlay = boo;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void addListener(VoidCallback listener) async {
|
||||||
|
super.addListener(listener);
|
||||||
|
await AudioService.connect();
|
||||||
|
}
|
||||||
|
|
||||||
loadPlaylist() async {
|
loadPlaylist() async {
|
||||||
await _queue.getPlaylist();
|
await _queue.getPlaylist();
|
||||||
_lastPostion = await storage.getInt();
|
_lastPostion = await storage.getInt();
|
||||||
}
|
}
|
||||||
|
|
||||||
episodeLoad(EpisodeBrief episode) async {
|
episodeLoad(EpisodeBrief episode) async {
|
||||||
if (_playerRunning && _episode != null) {
|
if (_playerRunning) {
|
||||||
PlayHistory history = PlayHistory(_episode.title, _episode.enclosureUrl,
|
PlayHistory history = PlayHistory(_episode.title, _episode.enclosureUrl,
|
||||||
backgroundAudioDuration, seekSliderValue);
|
backgroundAudioPosition / 1000, seekSliderValue);
|
||||||
await dbHelper.saveHistory(history);
|
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;
|
_startAudioService(int position) async {
|
||||||
await _queue.getPlaylist();
|
if (!AudioService.connected) {
|
||||||
_queue.playlist
|
await AudioService.connect();
|
||||||
.removeWhere((item) => item.enclosureUrl == _episode.enclosureUrl);
|
}
|
||||||
_queue.playlist.insert(0, _episode);
|
await AudioService.start(
|
||||||
await _queue.savePlaylist();
|
backgroundTaskEntrypoint: _audioPlayerTaskEntrypoint,
|
||||||
await _play(_episode);
|
androidNotificationChannelName: 'Tsacdop',
|
||||||
|
notificationColor: 0xFF2196f3,
|
||||||
|
androidNotificationIcon: 'mipmap/ic_launcher',
|
||||||
|
enableQueue: true,
|
||||||
|
androidStopOnRemoveTask: true,
|
||||||
|
);
|
||||||
_playerRunning = 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 {
|
playlistLoad() async {
|
||||||
_backgroundAudioPlaying = false;
|
|
||||||
await _queue.getPlaylist();
|
await _queue.getPlaylist();
|
||||||
|
_backgroundAudioDuration = 0;
|
||||||
|
_backgroundAudioPosition = 0;
|
||||||
|
_seekSliderValue = 0;
|
||||||
_episode = _queue.playlist.first;
|
_episode = _queue.playlist.first;
|
||||||
_skip = true;
|
|
||||||
await _play(_episode);
|
|
||||||
_playerRunning = true;
|
_playerRunning = true;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
_startAudioService(_lastPostion ?? 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
playNext() async {
|
playNext() async {
|
||||||
storage.saveInt(0);
|
AudioService.skipToNext();
|
||||||
_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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addToPlaylist(EpisodeBrief episode) async {
|
addToPlaylist(EpisodeBrief episode) async {
|
||||||
_queue.addToPlayList(episode);
|
if (_playerRunning) {
|
||||||
await _queue.getPlaylist();
|
await AudioService.addQueueItem(episode.toMediaItem());
|
||||||
|
}
|
||||||
|
print('add to playlist when not rnnning');
|
||||||
|
await _queue.addToPlayList(episode);
|
||||||
notifyListeners();
|
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 {
|
delFromPlaylist(EpisodeBrief episode) async {
|
||||||
_queue.delFromPlaylist(episode);
|
if (_playerRunning) {
|
||||||
await _queue.getPlaylist();
|
await AudioService.removeQueueItem(episode.toMediaItem());
|
||||||
|
}
|
||||||
|
await _queue.delFromPlaylist(episode);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
pauseAduio() async {
|
pauseAduio() async {
|
||||||
_pauseBackgroundAudio();
|
AudioService.pause();
|
||||||
notifyListeners();
|
|
||||||
PlayHistory history = PlayHistory(_episode.title, _episode.enclosureUrl,
|
|
||||||
backgroundAudioPosition, seekSliderValue);
|
|
||||||
await dbHelper.saveHistory(history);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resumeAudio() {
|
resumeAudio() async {
|
||||||
_resumeBackgroundAudio();
|
AudioService.play();
|
||||||
notifyListeners();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
forwardAudio(double s) {
|
forwardAudio(int s) {
|
||||||
_forwardBackgroundAudio(s);
|
int pos = _backgroundAudioPosition + s * 1000;
|
||||||
notifyListeners();
|
AudioService.seekTo(pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
sliderSeek(double val) {
|
sliderSeek(double val) async {
|
||||||
|
print(val.toString());
|
||||||
|
_noSlide = false;
|
||||||
_seekSliderValue = val;
|
_seekSliderValue = val;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
final double positionSeconds = val * _backgroundAudioDurationSeconds;
|
_currentPosition = (val * _backgroundAudioDuration).toInt();
|
||||||
_backgroundAudio.seek(positionSeconds);
|
await AudioService.seekTo(_currentPosition);
|
||||||
AudioSystem.instance.setPlaybackState(true, positionSeconds);
|
_noSlide = true;
|
||||||
}
|
}
|
||||||
//Set sleep time
|
|
||||||
|
//Set sleep time
|
||||||
sleepTimer(int mins) {
|
sleepTimer(int mins) {
|
||||||
_showStopWatch = true;
|
_showStopWatch = true;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
_stopTimer = Timer(Duration(minutes: mins),(){
|
_stopTimer = Timer(Duration(minutes: mins), () {
|
||||||
_stopOnComplete = false;
|
_stopOnComplete = false;
|
||||||
_backgroundAudioPlaying = false;
|
|
||||||
_remoteAudioLoading = false;
|
|
||||||
_playerRunning = false;
|
|
||||||
_showStopWatch = false;
|
_showStopWatch = false;
|
||||||
_disposeAudio();
|
AudioService.stop();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
//Cancel sleep timer
|
//Cancel sleep timer
|
||||||
cancelTimer(){
|
cancelTimer() {
|
||||||
_stopTimer.cancel();
|
_stopTimer.cancel();
|
||||||
_showStopWatch = false;
|
_showStopWatch = false;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
_disposeAudio() {
|
@override
|
||||||
pauseAduio();
|
void dispose() async {
|
||||||
AudioSystem.instance?.stopBackgroundDisplay();
|
await AudioService.stop();
|
||||||
AudioSystem.instance?.removeMediaEventListener(_mediaEventListener);
|
await AudioService.disconnect();
|
||||||
_backgroundAudio?.dispose();
|
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
|
@override
|
||||||
dispose() {
|
Future<void> onStart() async {
|
||||||
_disposeAudio();
|
print('start background task');
|
||||||
super.dispose();
|
var playerStateSubscription = _audioPlayer.playbackStateStream
|
||||||
}
|
.where((state) => state == AudioPlaybackState.completed)
|
||||||
|
.listen((state) {
|
||||||
_play(EpisodeBrief episodeBrief) async {
|
_handlePlaybackCompleted();
|
||||||
AudioSystem.instance.addMediaEventListener(_mediaEventListener);
|
|
||||||
String url = _queue.playlist.first.enclosureUrl;
|
|
||||||
_getFile(url).then((result) {
|
|
||||||
result == 'NotDownload'
|
|
||||||
? _initbackgroundAudioPlayer(url)
|
|
||||||
: _initbackgroundAudioPlayerLocal(result);
|
|
||||||
});
|
});
|
||||||
}
|
var eventSubscription = _audioPlayer.playbackEventStream.listen((event) {
|
||||||
|
BasicPlaybackState state;
|
||||||
Future<String> _getFile(String url) async {
|
if (event.buffering) {
|
||||||
final task = await FlutterDownloader.loadTasksWithRawQuery(
|
state = BasicPlaybackState.buffering;
|
||||||
query: "SELECT * FROM task WHERE url = '$url' AND status = 3");
|
} else {
|
||||||
if (task.length != 0) {
|
state = _stateToBasicState(event.state);
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
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 {
|
void playPause() {
|
||||||
final Uint8List imageBytes =
|
if (AudioServiceBackground.state.basicState == BasicPlaybackState.playing)
|
||||||
File('${_episode.imagePath}').readAsBytesSync();
|
onPause();
|
||||||
AudioSystem.instance.setMetadata(AudioMetadata(
|
else
|
||||||
title: episode.title,
|
onPlay();
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _resumeBackgroundAudio() async {
|
@override
|
||||||
_backgroundAudio.resume();
|
Future<void> onSkipToNext() async {
|
||||||
|
if (_playing == null) {
|
||||||
_backgroundAudioPlaying = true;
|
// First time, we want to start playing
|
||||||
|
_playing = true;
|
||||||
final Uint8List imageBytes =
|
} else {
|
||||||
File('${_episode.imagePath}').readAsBytesSync();
|
// Stop current item
|
||||||
AudioSystem.instance.setMetadata(AudioMetadata(
|
await _audioPlayer.stop();
|
||||||
title: _episode.title,
|
_queue.removeAt(0);
|
||||||
artist: _episode.feedTitle,
|
}
|
||||||
album: _episode.feedTitle,
|
AudioServiceBackground.setQueue(_queue);
|
||||||
genre: "Podcast",
|
AudioServiceBackground.setMediaItem(mediaItem);
|
||||||
durationSeconds: _backgroundAudioDurationSeconds,
|
_skipState = BasicPlaybackState.skippingToNext;
|
||||||
artBytes: imageBytes));
|
await _audioPlayer.setUrl(mediaItem.id);
|
||||||
|
print(mediaItem.id);
|
||||||
AudioSystem.instance
|
Duration duration = await _audioPlayer.durationFuture;
|
||||||
.setPlaybackState(true, _backgroundAudioPositionSeconds);
|
AudioServiceBackground.setMediaItem(
|
||||||
|
mediaItem.copyWith(duration: duration.inMilliseconds));
|
||||||
AudioSystem.instance.setAndroidNotificationButtons(<dynamic>[
|
_skipState = null;
|
||||||
AndroidMediaButtonType.pause,
|
// Resume playback if we were playing
|
||||||
_forwardButton,
|
if (_playing) {
|
||||||
AndroidMediaButtonType.stop,
|
onPlay();
|
||||||
], androidCompactIndices: <int>[
|
} else {
|
||||||
0,
|
_setState(state: BasicPlaybackState.paused);
|
||||||
1
|
}
|
||||||
]);
|
|
||||||
|
|
||||||
AudioSystem.instance.setSupportedMediaActions(<MediaActionType>{
|
|
||||||
MediaActionType.playPause,
|
|
||||||
MediaActionType.pause,
|
|
||||||
MediaActionType.next,
|
|
||||||
MediaActionType.previous,
|
|
||||||
MediaActionType.skipForward,
|
|
||||||
MediaActionType.skipBackward,
|
|
||||||
MediaActionType.seekTo,
|
|
||||||
MediaActionType.custom,
|
|
||||||
}, skipIntervalSeconds: 30);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _pauseBackgroundAudio() {
|
@override
|
||||||
_backgroundAudio?.pause();
|
void onPlay() async {
|
||||||
_backgroundAudioPlaying = false;
|
if (_skipState == null) {
|
||||||
AudioSystem.instance
|
if (_playing == null) {
|
||||||
.setPlaybackState(false, _backgroundAudioPositionSeconds);
|
_playing = true;
|
||||||
AudioSystem.instance.setAndroidNotificationButtons(<dynamic>[
|
AudioServiceBackground.setQueue(_queue);
|
||||||
AndroidMediaButtonType.play,
|
await _audioPlayer.setUrl(mediaItem.id);
|
||||||
_forwardButton,
|
Duration duration = await _audioPlayer.durationFuture;
|
||||||
AndroidMediaButtonType.stop,
|
AudioServiceBackground.setMediaItem(
|
||||||
], androidCompactIndices: <int>[
|
mediaItem.copyWith(duration: duration.inMilliseconds));
|
||||||
0,
|
}
|
||||||
1,
|
_playing = true;
|
||||||
]);
|
_audioPlayer.play();
|
||||||
|
}
|
||||||
AudioSystem.instance.setSupportedMediaActions(<MediaActionType>{
|
|
||||||
MediaActionType.playPause,
|
|
||||||
MediaActionType.play,
|
|
||||||
MediaActionType.next,
|
|
||||||
MediaActionType.previous,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _stopBackgroundAudio() {
|
@override
|
||||||
_backgroundAudio.pause();
|
void onPause() {
|
||||||
_backgroundAudio.dispose();
|
if (_skipState == null) {
|
||||||
_backgroundAudioPlaying = false;
|
if (_playing == null) {}
|
||||||
AudioSystem.instance.stopBackgroundDisplay();
|
_playing = false;
|
||||||
|
_audioPlayer.pause();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _forwardBackgroundAudio(double seconds) {
|
@override
|
||||||
final double forwardposition = _backgroundAudioPositionSeconds + seconds;
|
void onSeekTo(int position) {
|
||||||
_backgroundAudio.seek(forwardposition);
|
_audioPlayer.seek(Duration(milliseconds: position));
|
||||||
AudioSystem.instance
|
|
||||||
.setPlaybackState(true, _backgroundAudioPositionSeconds);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final _pauseButton = AndroidCustomMediaButton(
|
@override
|
||||||
'pausenow', pausenowButtonId, 'ic_stat_pause_circle_filled');
|
void onClick(MediaButton button) {
|
||||||
final _replay10Button = AndroidCustomMediaButton(
|
playPause();
|
||||||
'replay10', replay10ButtonId, 'ic_stat_replay_10');
|
}
|
||||||
final _forwardButton = AndroidCustomMediaButton(
|
|
||||||
'forward', forwardButtonId, 'ic_stat_forward_30');
|
@override
|
||||||
final _playnowButton = AndroidCustomMediaButton(
|
void onStop() async {
|
||||||
'playnow', likeButtonId, 'ic_stat_play_circle_filled');
|
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];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:audio_service/audio_service.dart';
|
||||||
|
|
||||||
class EpisodeBrief {
|
class EpisodeBrief {
|
||||||
final String title;
|
final String title;
|
||||||
|
@ -13,6 +14,7 @@ class EpisodeBrief {
|
||||||
final int duration;
|
final int duration;
|
||||||
final int explicit;
|
final int explicit;
|
||||||
final String imagePath;
|
final String imagePath;
|
||||||
|
final String mediaId;
|
||||||
EpisodeBrief(
|
EpisodeBrief(
|
||||||
this.title,
|
this.title,
|
||||||
this.enclosureUrl,
|
this.enclosureUrl,
|
||||||
|
@ -21,21 +23,31 @@ class EpisodeBrief {
|
||||||
this.feedTitle,
|
this.feedTitle,
|
||||||
this.primaryColor,
|
this.primaryColor,
|
||||||
this.liked,
|
this.liked,
|
||||||
this.downloaded,
|
this.downloaded,
|
||||||
this.duration,
|
this.duration,
|
||||||
this.explicit,
|
this.explicit,
|
||||||
this.imagePath
|
this.imagePath,
|
||||||
);
|
this.mediaId);
|
||||||
|
|
||||||
String dateToString(){
|
String dateToString() {
|
||||||
DateTime date = DateTime.fromMillisecondsSinceEpoch(pubDate);
|
DateTime date = DateTime.fromMillisecondsSinceEpoch(pubDate, isUtc: true);
|
||||||
var diffrence = DateTime.now().difference(date);
|
var diffrence = DateTime.now().difference(date);
|
||||||
if(diffrence.inHours < 24) {
|
if (diffrence.inHours < 24) {
|
||||||
return '${diffrence.inHours} hours ago';
|
return '${diffrence.inHours} hours ago';
|
||||||
} else if (diffrence.inDays < 7){
|
} else if (diffrence.inDays < 7) {
|
||||||
return '${diffrence.inDays} days ago';}
|
return '${diffrence.inDays} days ago';
|
||||||
else {
|
} else {
|
||||||
return DateFormat.yMMMd().format( DateTime.fromMillisecondsSinceEpoch(pubDate));
|
return DateFormat.yMMMd()
|
||||||
}
|
.format(DateTime.fromMillisecondsSinceEpoch(pubDate));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MediaItem toMediaItem() {
|
||||||
|
return MediaItem(
|
||||||
|
id: mediaId,
|
||||||
|
title: title,
|
||||||
|
artist: feedTitle,
|
||||||
|
album: feedTitle,
|
||||||
|
artUri: 'file://$imagePath');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,8 +55,12 @@ class PodcastGroup {
|
||||||
}
|
}
|
||||||
|
|
||||||
List<PodcastLocal> _podcasts;
|
List<PodcastLocal> _podcasts;
|
||||||
|
List<PodcastLocal> _orderedPodcasts;
|
||||||
|
List<PodcastLocal> get ordereddPodcasts => _orderedPodcasts;
|
||||||
List<PodcastLocal> get podcasts => _podcasts;
|
List<PodcastLocal> get podcasts => _podcasts;
|
||||||
|
set setOrderedPodcasts(List<PodcastLocal> list) {
|
||||||
|
_orderedPodcasts = list;
|
||||||
|
}
|
||||||
|
|
||||||
GroupEntity toEntity() {
|
GroupEntity toEntity() {
|
||||||
return GroupEntity(name, id, color, podcastList);
|
return GroupEntity(name, id, color, podcastList);
|
||||||
|
@ -82,9 +86,20 @@ class GroupList extends ChangeNotifier {
|
||||||
GroupList({List<PodcastGroup> groups}) : _groups = groups ?? [];
|
GroupList({List<PodcastGroup> groups}) : _groups = groups ?? [];
|
||||||
|
|
||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
|
|
||||||
bool get isLoading => _isLoading;
|
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
|
@override
|
||||||
void addListener(VoidCallback listener) {
|
void addListener(VoidCallback listener) {
|
||||||
super.addListener(listener);
|
super.addListener(listener);
|
||||||
|
@ -105,14 +120,15 @@ class GroupList extends ChangeNotifier {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future addGroup(PodcastGroup podcastGroup) async {
|
Future addGroup(PodcastGroup podcastGroup) async {
|
||||||
|
_isLoading = true;
|
||||||
_groups.add(podcastGroup);
|
_groups.add(podcastGroup);
|
||||||
_saveGroup();
|
_saveGroup();
|
||||||
|
_isLoading = false;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future delGroup(PodcastGroup podcastGroup) async {
|
Future delGroup(PodcastGroup podcastGroup) async {
|
||||||
_isLoading = true;
|
_isLoading = true;
|
||||||
notifyListeners();
|
|
||||||
podcastGroup.podcastList.forEach((podcast) {
|
podcastGroup.podcastList.forEach((podcast) {
|
||||||
if (!_groups.first.podcastList.contains(podcast)) {
|
if (!_groups.first.podcastList.contains(podcast)) {
|
||||||
_groups[0].podcastList.insert(0, podcast);
|
_groups[0].podcastList.insert(0, podcast);
|
||||||
|
@ -121,11 +137,11 @@ class GroupList extends ChangeNotifier {
|
||||||
_saveGroup();
|
_saveGroup();
|
||||||
_groups.remove(podcastGroup);
|
_groups.remove(podcastGroup);
|
||||||
await _groups[0].getPodcasts();
|
await _groups[0].getPodcasts();
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
updateGroup(PodcastGroup podcastGroup) async{
|
updateGroup(PodcastGroup podcastGroup) async {
|
||||||
var oldGroup = _groups.firstWhere((it) => it.id == podcastGroup.id);
|
var oldGroup = _groups.firstWhere((it) => it.id == podcastGroup.id);
|
||||||
var index = _groups.indexOf(oldGroup);
|
var index = _groups.indexOf(oldGroup);
|
||||||
_groups.replaceRange(index, index + 1, [podcastGroup]);
|
_groups.replaceRange(index, index + 1, [podcastGroup]);
|
||||||
|
@ -161,7 +177,11 @@ class GroupList extends ChangeNotifier {
|
||||||
_isLoading = true;
|
_isLoading = true;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
getPodcastGroup(id).forEach((group) {
|
getPodcastGroup(id).forEach((group) {
|
||||||
group.podcastList.remove(id);
|
if (list.contains(group)) {
|
||||||
|
list.remove(group);
|
||||||
|
} else {
|
||||||
|
group.podcastList.remove(id);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
list.forEach((s) {
|
list.forEach((s) {
|
||||||
s.podcastList.insert(0, id);
|
s.podcastList.insert(0, id);
|
||||||
|
@ -190,8 +210,8 @@ class GroupList extends ChangeNotifier {
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
saveOrder(PodcastGroup group, List<PodcastLocal> podcasts) async {
|
saveOrder(PodcastGroup group) async {
|
||||||
group.podcastList = podcasts.map((e) => e.id).toList();
|
group.podcastList = group.ordereddPodcasts.map((e) => e.id).toList();
|
||||||
_saveGroup();
|
_saveGroup();
|
||||||
await group.getPodcasts();
|
await group.getPodcasts();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
|
@ -6,16 +6,17 @@ import 'package:tsacdop/local_storage/key_value_storage.dart';
|
||||||
class SettingState extends ChangeNotifier {
|
class SettingState extends ChangeNotifier {
|
||||||
KeyValueStorage themestorage = KeyValueStorage('themes');
|
KeyValueStorage themestorage = KeyValueStorage('themes');
|
||||||
KeyValueStorage accentstorage = KeyValueStorage('accents');
|
KeyValueStorage accentstorage = KeyValueStorage('accents');
|
||||||
bool _isLoading;
|
KeyValueStorage autoupdatestorage = KeyValueStorage('autoupdate');
|
||||||
bool get isLoagding => _isLoading;
|
|
||||||
|
|
||||||
Future initData() async {
|
Future initData() async {
|
||||||
await _getTheme();
|
await _getTheme();
|
||||||
await _getAccentSetColor();
|
await _getAccentSetColor();
|
||||||
|
await _getAutoUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
ThemeMode _theme;
|
ThemeMode _theme;
|
||||||
ThemeMode get theme => _theme;
|
ThemeMode get theme => _theme;
|
||||||
|
|
||||||
set setTheme(ThemeMode mode) {
|
set setTheme(ThemeMode mode) {
|
||||||
_theme = mode;
|
_theme = mode;
|
||||||
_saveTheme();
|
_saveTheme();
|
||||||
|
@ -31,11 +32,20 @@ class SettingState extends ChangeNotifier {
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _autoUpdate;
|
||||||
|
bool get autoUpdate => _autoUpdate;
|
||||||
|
set autoUpdate(bool boo) {
|
||||||
|
_autoUpdate = boo;
|
||||||
|
_saveAutoUpdate();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void addListener(VoidCallback listener) {
|
void addListener(VoidCallback listener) {
|
||||||
super.addListener(listener);
|
super.addListener(listener);
|
||||||
_getTheme();
|
_getTheme();
|
||||||
_getAccentSetColor();
|
_getAccentSetColor();
|
||||||
|
_getAutoUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
_getTheme() async {
|
_getTheme() async {
|
||||||
|
@ -62,6 +72,14 @@ class SettingState extends ChangeNotifier {
|
||||||
_saveAccentSetColor() async {
|
_saveAccentSetColor() async {
|
||||||
await accentstorage
|
await accentstorage
|
||||||
.saveString(_accentSetColor.toString().substring(10, 16));
|
.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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
|
@ -10,6 +10,7 @@ import 'package:url_launcher/url_launcher.dart';
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:tuple/tuple.dart';
|
import 'package:tuple/tuple.dart';
|
||||||
|
import 'package:audio_service/audio_service.dart';
|
||||||
import 'package:tsacdop/class/audiostate.dart';
|
import 'package:tsacdop/class/audiostate.dart';
|
||||||
import 'package:tsacdop/class/episodebrief.dart';
|
import 'package:tsacdop/class/episodebrief.dart';
|
||||||
import 'package:tsacdop/local_storage/sqflite_localpodcast.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,
|
selector: (_, audio) => audio.playerRunning,
|
||||||
builder: (_, data, __) {
|
builder: (_, data, __) {
|
||||||
return Container(
|
return Container(
|
||||||
|
@ -290,7 +291,7 @@ class _MenuBarState extends State<MenuBar> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var audio = Provider.of<AudioPlayer>(context, listen: false);
|
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
|
||||||
return Container(
|
return Container(
|
||||||
height: 50.0,
|
height: 50.0,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
@ -346,7 +347,7 @@ class _MenuBarState extends State<MenuBar> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
DownloadButton(episodeBrief: widget.episodeItem),
|
DownloadButton(episodeBrief: widget.episodeItem),
|
||||||
Selector<AudioPlayer, List<String>>(
|
Selector<AudioPlayerNotifier, List<String>>(
|
||||||
selector: (_, audio) =>
|
selector: (_, audio) =>
|
||||||
audio.queue.playlist.map((e) => e.enclosureUrl).toList(),
|
audio.queue.playlist.map((e) => e.enclosureUrl).toList(),
|
||||||
builder: (_, data, __) {
|
builder: (_, data, __) {
|
||||||
|
@ -367,9 +368,9 @@ class _MenuBarState extends State<MenuBar> {
|
||||||
),
|
),
|
||||||
Spacer(),
|
Spacer(),
|
||||||
// Text(audio.audioState.toString()),
|
// Text(audio.audioState.toString()),
|
||||||
Selector<AudioPlayer, Tuple2<EpisodeBrief, bool>>(
|
Selector<AudioPlayerNotifier, Tuple2<EpisodeBrief, BasicPlaybackState>>(
|
||||||
selector: (_, audio) =>
|
selector: (_, audio) =>
|
||||||
Tuple2(audio.episode, audio.backgroundAudioPlaying),
|
Tuple2(audio.episode, audio.audioState),
|
||||||
builder: (_, data, __) {
|
builder: (_, data, __) {
|
||||||
return (widget.episodeItem.title != data.item1?.title)
|
return (widget.episodeItem.title != data.item1?.title)
|
||||||
? Material(
|
? Material(
|
||||||
|
@ -400,7 +401,7 @@ class _MenuBarState extends State<MenuBar> {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: (widget.episodeItem.title == data.item1?.title &&
|
: (widget.episodeItem.title == data.item1?.title &&
|
||||||
data.item2 == true)
|
data.item2 == BasicPlaybackState.playing)
|
||||||
? Container(
|
? Container(
|
||||||
padding: EdgeInsets.only(right: 30),
|
padding: EdgeInsets.only(right: 30),
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
|
@ -424,9 +425,10 @@ class _MenuBarState extends State<MenuBar> {
|
||||||
class LinePainter extends CustomPainter {
|
class LinePainter extends CustomPainter {
|
||||||
double _fraction;
|
double _fraction;
|
||||||
Paint _paint;
|
Paint _paint;
|
||||||
LinePainter(this._fraction) {
|
Color _maincolor;
|
||||||
|
LinePainter(this._fraction, this._maincolor) {
|
||||||
_paint = Paint()
|
_paint = Paint()
|
||||||
..color = Colors.blue
|
..color = _maincolor
|
||||||
..strokeWidth = 2.0
|
..strokeWidth = 2.0
|
||||||
..strokeCap = StrokeCap.round;
|
..strokeCap = StrokeCap.round;
|
||||||
}
|
}
|
||||||
|
@ -483,14 +485,15 @@ class _LineLoaderState extends State<LineLoader>
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return CustomPaint(painter: LinePainter(_fraction));
|
return CustomPaint(painter: LinePainter(_fraction, Theme.of(context).accentColor));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class WavePainter extends CustomPainter {
|
class WavePainter extends CustomPainter {
|
||||||
double _fraction;
|
double _fraction;
|
||||||
double _value;
|
double _value;
|
||||||
WavePainter(this._fraction);
|
Color _color;
|
||||||
|
WavePainter(this._fraction, this._color);
|
||||||
@override
|
@override
|
||||||
void paint(Canvas canvas, Size size) {
|
void paint(Canvas canvas, Size size) {
|
||||||
if (_fraction < 0.5) {
|
if (_fraction < 0.5) {
|
||||||
|
@ -500,7 +503,7 @@ class WavePainter extends CustomPainter {
|
||||||
}
|
}
|
||||||
Path _path = Path();
|
Path _path = Path();
|
||||||
Paint _paint = Paint()
|
Paint _paint = Paint()
|
||||||
..color = Colors.blue
|
..color = _color
|
||||||
..strokeWidth = 2.0
|
..strokeWidth = 2.0
|
||||||
..strokeCap = StrokeCap.round
|
..strokeCap = StrokeCap.round
|
||||||
..style = PaintingStyle.stroke;
|
..style = PaintingStyle.stroke;
|
||||||
|
@ -575,7 +578,7 @@ class _WaveLoaderState extends State<WaveLoader>
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return CustomPaint(painter: WavePainter(_fraction));
|
return CustomPaint(painter: WavePainter(_fraction, Theme.of(context).accentColor));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,17 @@
|
||||||
import 'dart:isolate';
|
import 'dart:isolate';
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'dart:async';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:path_provider/path_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:flutter_downloader/flutter_downloader.dart';
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:tsacdop/class/episodebrief.dart';
|
import 'package:tsacdop/class/episodebrief.dart';
|
||||||
|
import 'package:tsacdop/class/audiostate.dart';
|
||||||
import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
|
import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
|
||||||
|
|
||||||
class DownloadButton extends StatefulWidget {
|
class DownloadButton extends StatefulWidget {
|
||||||
|
@ -74,6 +77,7 @@ class _DownloadButtonState extends State<DownloadButton> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void _unbindBackgroundIsolate() {
|
void _unbindBackgroundIsolate() {
|
||||||
IsolateNameServer.removePortNameMapping('downloader_send_port');
|
IsolateNameServer.removePortNameMapping('downloader_send_port');
|
||||||
}
|
}
|
||||||
|
@ -96,6 +100,7 @@ class _DownloadButtonState extends State<DownloadButton> {
|
||||||
openFileFromNotification: false,
|
openFileFromNotification: false,
|
||||||
);
|
);
|
||||||
var dbHelper = DBHelper();
|
var dbHelper = DBHelper();
|
||||||
|
|
||||||
await dbHelper.saveDownloaded(task.link, task.taskId);
|
await dbHelper.saveDownloaded(task.link, task.taskId);
|
||||||
Fluttertoast.showToast(
|
Fluttertoast.showToast(
|
||||||
msg: 'Downloading',
|
msg: 'Downloading',
|
||||||
|
@ -103,6 +108,7 @@ class _DownloadButtonState extends State<DownloadButton> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void _deleteDownload(_TaskInfo task) async {
|
void _deleteDownload(_TaskInfo task) async {
|
||||||
await FlutterDownloader.remove(
|
await FlutterDownloader.remove(
|
||||||
taskId: task.taskId, shouldDeleteContent: true);
|
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 {
|
Future<Null> _prepare() async {
|
||||||
final tasks = await FlutterDownloader.loadTasks();
|
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);
|
print(_localPath);
|
||||||
final saveDir = Directory(_localPath);
|
final saveDir = Directory(_localPath);
|
||||||
bool hasExisted = await saveDir.exists();
|
bool hasExisted = await saveDir.exists();
|
||||||
|
@ -173,6 +189,8 @@ class _DownloadButtonState extends State<DownloadButton> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Future<bool> _checkPermmison() async {
|
Future<bool> _checkPermmison() async {
|
||||||
PermissionStatus permission = await PermissionHandler()
|
PermissionStatus permission = await PermissionHandler()
|
||||||
.checkPermissionStatus(PermissionGroup.storage);
|
.checkPermissionStatus(PermissionGroup.storage);
|
||||||
|
@ -284,6 +302,7 @@ class _DownloadButtonState extends State<DownloadButton> {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else if (task.status == DownloadTaskStatus.complete) {
|
} else if (task.status == DownloadTaskStatus.complete) {
|
||||||
|
_saveMediaId(task);
|
||||||
return _buttonOnMenu(
|
return _buttonOnMenu(
|
||||||
Icon(
|
Icon(
|
||||||
Icons.done_all,
|
Icons.done_all,
|
||||||
|
@ -307,4 +326,4 @@ class _TaskInfo {
|
||||||
DownloadTaskStatus status = DownloadTaskStatus.undefined;
|
DownloadTaskStatus status = DownloadTaskStatus.undefined;
|
||||||
|
|
||||||
_TaskInfo({this.name, this.link});
|
_TaskInfo({this.name, this.link});
|
||||||
}
|
}
|
|
@ -1,8 +1,7 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:line_icons/line_icons.dart';
|
||||||
|
|
||||||
class AboutApp extends StatelessWidget {
|
class AboutApp extends StatelessWidget {
|
||||||
_launchUrl(String url) async {
|
_launchUrl(String url) async {
|
||||||
if (await canLaunch(url)) {
|
if (await canLaunch(url)) {
|
||||||
|
@ -71,7 +70,7 @@ class AboutApp extends StatelessWidget {
|
||||||
image: AssetImage('assets/logo.png'),
|
image: AssetImage('assets/logo.png'),
|
||||||
height: 80,
|
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),
|
padding: EdgeInsets.symmetric(horizontal: 50),
|
||||||
height: 50,
|
height: 50,
|
||||||
child: Text(
|
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,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -111,17 +110,17 @@ class AboutApp extends StatelessWidget {
|
||||||
_listItem(
|
_listItem(
|
||||||
context,
|
context,
|
||||||
'GitHub',
|
'GitHub',
|
||||||
FontAwesomeIcons.githubSquare,
|
LineIcons.github,
|
||||||
'https://github.com/stonaga/tsacdop'),
|
'https://github.com/stonaga/'),
|
||||||
_listItem(
|
_listItem(
|
||||||
context,
|
context,
|
||||||
'Twitter',
|
'Twitter',
|
||||||
FontAwesomeIcons.twitterSquare,
|
LineIcons.twitter,
|
||||||
'https://twitter.com'),
|
'https://twitter.com'),
|
||||||
_listItem(
|
_listItem(
|
||||||
context,
|
context,
|
||||||
'Gmail',
|
'Stone Gate',
|
||||||
FontAwesomeIcons.envelopeSquare,
|
LineIcons.hat_cowboy_solid,
|
||||||
'mailto:<xijieyin@gmail.com>?subject=Tsacdop Feedback'),
|
'mailto:<xijieyin@gmail.com>?subject=Tsacdop Feedback'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
@ -36,6 +36,7 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||||
return AnnotatedRegion<SystemUiOverlayStyle>(
|
return AnnotatedRegion<SystemUiOverlayStyle>(
|
||||||
value: SystemUiOverlayStyle(
|
value: SystemUiOverlayStyle(
|
||||||
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
|
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
|
||||||
|
systemNavigationBarIconBrightness: Theme.of(context).accentColorBrightness,
|
||||||
systemNavigationBarColor: Theme.of(context).primaryColor,
|
systemNavigationBarColor: Theme.of(context).primaryColor,
|
||||||
statusBarColor: Theme.of(context).primaryColor,
|
statusBarColor: Theme.of(context).primaryColor,
|
||||||
),
|
),
|
||||||
|
|
|
@ -13,6 +13,7 @@ import 'package:color_thief_flutter/color_thief_flutter.dart';
|
||||||
import 'package:image/image.dart' as img;
|
import 'package:image/image.dart' as img;
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
|
import 'package:line_icons/line_icons.dart';
|
||||||
|
|
||||||
import 'package:tsacdop/class/podcast_group.dart';
|
import 'package:tsacdop/class/podcast_group.dart';
|
||||||
import 'package:tsacdop/settings/settting.dart';
|
import 'package:tsacdop/settings/settting.dart';
|
||||||
|
@ -142,7 +143,7 @@ class PopupMenu extends StatelessWidget {
|
||||||
|
|
||||||
void _saveOmpl(String path) async {
|
void _saveOmpl(String path) async {
|
||||||
File file = File(path);
|
File file = File(path);
|
||||||
String opml = file.readAsStringSync();
|
try{String opml = file.readAsStringSync();
|
||||||
|
|
||||||
var content = xml.parse(opml);
|
var content = xml.parse(opml);
|
||||||
var total = content
|
var total = content
|
||||||
|
@ -167,6 +168,15 @@ class PopupMenu extends StatelessWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
print('Import fisnished');
|
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),
|
padding: EdgeInsets.only(left: 10),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Icon(Icons.refresh),
|
Icon(LineIcons.cloud_download_alt_solid),
|
||||||
Padding(padding: EdgeInsets.symmetric(horizontal: 5.0),),
|
Padding(padding: EdgeInsets.symmetric(horizontal: 5.0),),
|
||||||
Text('Refresh All'),
|
Text('Refresh All'),
|
||||||
],
|
],
|
||||||
|
@ -208,7 +218,7 @@ class PopupMenu extends StatelessWidget {
|
||||||
padding: EdgeInsets.only(left: 10),
|
padding: EdgeInsets.only(left: 10),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Icon(Icons.attachment),
|
Icon(LineIcons.paperclip_solid),
|
||||||
Padding(padding: EdgeInsets.symmetric(horizontal: 5.0),),
|
Padding(padding: EdgeInsets.symmetric(horizontal: 5.0),),
|
||||||
Text('Import OMPL'),
|
Text('Import OMPL'),
|
||||||
],
|
],
|
||||||
|
@ -226,7 +236,7 @@ class PopupMenu extends StatelessWidget {
|
||||||
padding: EdgeInsets.only(left: 10),
|
padding: EdgeInsets.only(left: 10),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Icon(Icons.swap_calls),
|
Icon(LineIcons.cog_solid),
|
||||||
Padding(padding: EdgeInsets.symmetric(horizontal: 5.0),),
|
Padding(padding: EdgeInsets.symmetric(horizontal: 5.0),),
|
||||||
Text('Settings'),
|
Text('Settings'),
|
||||||
],
|
],
|
||||||
|
@ -239,7 +249,7 @@ class PopupMenu extends StatelessWidget {
|
||||||
padding: EdgeInsets.only(left: 10),
|
padding: EdgeInsets.only(left: 10),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Icon(Icons.info_outline),
|
Icon(LineIcons.info_circle_solid),
|
||||||
Padding(padding: EdgeInsets.symmetric(horizontal: 5.0),),
|
Padding(padding: EdgeInsets.symmetric(horizontal: 5.0),),
|
||||||
Text('About'),
|
Text('About'),
|
||||||
],
|
],
|
||||||
|
|
|
@ -7,6 +7,7 @@ import 'package:provider/provider.dart';
|
||||||
import 'package:marquee/marquee.dart';
|
import 'package:marquee/marquee.dart';
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:tuple/tuple.dart';
|
import 'package:tuple/tuple.dart';
|
||||||
|
import 'package:audio_service/audio_service.dart';
|
||||||
|
|
||||||
import 'package:tsacdop/class/episodebrief.dart';
|
import 'package:tsacdop/class/episodebrief.dart';
|
||||||
import 'package:tsacdop/class/audiostate.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/pageroute.dart';
|
||||||
import 'package:tsacdop/util/colorize.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 {
|
class PlayerWidget extends StatefulWidget {
|
||||||
@override
|
@override
|
||||||
_PlayerWidgetState createState() => _PlayerWidgetState();
|
_PlayerWidgetState createState() => _PlayerWidgetState();
|
||||||
|
@ -50,17 +142,17 @@ class _PlayerWidgetState extends State<PlayerWidget> {
|
||||||
_timeLeft = _minSelected;
|
_timeLeft = _minSelected;
|
||||||
_timer = Timer.periodic(Duration(minutes: 1), (timer) {
|
_timer = Timer.periodic(Duration(minutes: 1), (timer) {
|
||||||
setState(() {
|
setState(() {
|
||||||
if(_timeLeft < 1){
|
if (_timeLeft < 1) {
|
||||||
_timer.cancel();
|
_timer.cancel();
|
||||||
} else{
|
} else {
|
||||||
_timeLeft = _timeLeft - 1;
|
_timeLeft = _timeLeft - 1;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _sleepTimer(BuildContext context) {
|
Widget _sleepTimer(BuildContext context) {
|
||||||
var audio = Provider.of<AudioPlayer>(context);
|
var audio = Provider.of<AudioPlayerNotifier>(context);
|
||||||
return Container(
|
return Container(
|
||||||
height: 50,
|
height: 50,
|
||||||
margin: EdgeInsets.all(10.0),
|
margin: EdgeInsets.all(10.0),
|
||||||
|
@ -141,7 +233,7 @@ class _PlayerWidgetState extends State<PlayerWidget> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _expandedPanel(BuildContext context) {
|
Widget _expandedPanel(BuildContext context) {
|
||||||
var audio = Provider.of<AudioPlayer>(context, listen: false);
|
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
|
||||||
return Stack(
|
return Stack(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Container(
|
Container(
|
||||||
|
@ -156,7 +248,7 @@ class _PlayerWidgetState extends State<PlayerWidget> {
|
||||||
height: 80.0,
|
height: 80.0,
|
||||||
padding: EdgeInsets.all(20),
|
padding: EdgeInsets.all(20),
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: Selector<AudioPlayer, String>(
|
child: Selector<AudioPlayerNotifier, String>(
|
||||||
selector: (_, audio) => audio.episode.title,
|
selector: (_, audio) => audio.episode.title,
|
||||||
builder: (_, title, __) {
|
builder: (_, title, __) {
|
||||||
return Container(
|
return Container(
|
||||||
|
@ -201,8 +293,12 @@ class _PlayerWidgetState extends State<PlayerWidget> {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Consumer<AudioPlayer>(
|
Consumer<AudioPlayerNotifier>(
|
||||||
builder: (_, data, __) {
|
builder: (_, data, __) {
|
||||||
|
Color _c =
|
||||||
|
(Theme.of(context).brightness == Brightness.light)
|
||||||
|
? data.episode.primaryColor.colorizedark()
|
||||||
|
: data.episode.primaryColor.colorizeLight();
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
@ -215,10 +311,12 @@ class _PlayerWidgetState extends State<PlayerWidget> {
|
||||||
.accentColor
|
.accentColor
|
||||||
.withOpacity(0.5),
|
.withOpacity(0.5),
|
||||||
inactiveTrackColor: Colors.grey[300],
|
inactiveTrackColor: Colors.grey[300],
|
||||||
trackHeight: 3.0,
|
trackHeight: 2.0,
|
||||||
thumbColor: Theme.of(context).accentColor,
|
thumbColor: Theme.of(context).accentColor,
|
||||||
thumbShape: RoundSliderThumbShape(
|
thumbShape: MyRoundSliderThumpShape(
|
||||||
enabledThumbRadius: 6.0),
|
enabledThumbRadius: 5.0,
|
||||||
|
disabledThumbRadius: 5.0,
|
||||||
|
thumbCenterColor: _c),
|
||||||
overlayColor:
|
overlayColor:
|
||||||
Theme.of(context).accentColor.withAlpha(32),
|
Theme.of(context).accentColor.withAlpha(32),
|
||||||
overlayShape:
|
overlayShape:
|
||||||
|
@ -238,7 +336,7 @@ class _PlayerWidgetState extends State<PlayerWidget> {
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text(
|
Text(
|
||||||
_stringForSeconds(
|
_stringForSeconds(
|
||||||
data.backgroundAudioPosition) ??
|
data.backgroundAudioPosition / 1000) ??
|
||||||
'',
|
'',
|
||||||
style: TextStyle(fontSize: 10),
|
style: TextStyle(fontSize: 10),
|
||||||
),
|
),
|
||||||
|
@ -250,7 +348,12 @@ class _PlayerWidgetState extends State<PlayerWidget> {
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: const Color(0xFFFF0000)))
|
color: const Color(0xFFFF0000)))
|
||||||
: Text(
|
: Text(
|
||||||
data.remoteAudioLoading
|
data.audioState ==
|
||||||
|
BasicPlaybackState
|
||||||
|
.buffering ||
|
||||||
|
data.audioState ==
|
||||||
|
BasicPlaybackState
|
||||||
|
.connecting
|
||||||
? 'Buffring...'
|
? 'Buffring...'
|
||||||
: '',
|
: '',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
|
@ -261,7 +364,7 @@ class _PlayerWidgetState extends State<PlayerWidget> {
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
_stringForSeconds(
|
_stringForSeconds(
|
||||||
data.backgroundAudioDuration) ??
|
data.backgroundAudioDuration / 1000) ??
|
||||||
'',
|
'',
|
||||||
style: TextStyle(fontSize: 10),
|
style: TextStyle(fontSize: 10),
|
||||||
),
|
),
|
||||||
|
@ -274,8 +377,8 @@ class _PlayerWidgetState extends State<PlayerWidget> {
|
||||||
),
|
),
|
||||||
Container(
|
Container(
|
||||||
height: 100,
|
height: 100,
|
||||||
child: Selector<AudioPlayer, bool>(
|
child: Selector<AudioPlayerNotifier, BasicPlaybackState>(
|
||||||
selector: (_, audio) => audio.backgroundAudioPlaying,
|
selector: (_, audio) => audio.audioState,
|
||||||
builder: (_, backplay, __) {
|
builder: (_, backplay, __) {
|
||||||
return Material(
|
return Material(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
|
@ -285,22 +388,24 @@ class _PlayerWidgetState extends State<PlayerWidget> {
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 30.0),
|
padding: EdgeInsets.symmetric(horizontal: 30.0),
|
||||||
onPressed: backplay
|
onPressed:
|
||||||
? () => audio.forwardAudio(-10)
|
backplay == BasicPlaybackState.playing
|
||||||
: null,
|
? () => audio.forwardAudio(-10)
|
||||||
|
: null,
|
||||||
iconSize: 32.0,
|
iconSize: 32.0,
|
||||||
icon: Icon(Icons.replay_10),
|
icon: Icon(Icons.replay_10),
|
||||||
color:
|
color:
|
||||||
Theme.of(context).tabBarTheme.labelColor),
|
Theme.of(context).tabBarTheme.labelColor),
|
||||||
backplay
|
backplay == BasicPlaybackState.playing
|
||||||
? IconButton(
|
? IconButton(
|
||||||
padding:
|
padding:
|
||||||
EdgeInsets.symmetric(horizontal: 30.0),
|
EdgeInsets.symmetric(horizontal: 30.0),
|
||||||
onPressed: backplay
|
onPressed:
|
||||||
? () {
|
backplay == BasicPlaybackState.playing
|
||||||
audio.pauseAduio();
|
? () {
|
||||||
}
|
audio.pauseAduio();
|
||||||
: null,
|
}
|
||||||
|
: null,
|
||||||
iconSize: 40.0,
|
iconSize: 40.0,
|
||||||
icon: Icon(Icons.pause_circle_filled),
|
icon: Icon(Icons.pause_circle_filled),
|
||||||
color: Theme.of(context)
|
color: Theme.of(context)
|
||||||
|
@ -309,11 +414,12 @@ class _PlayerWidgetState extends State<PlayerWidget> {
|
||||||
: IconButton(
|
: IconButton(
|
||||||
padding:
|
padding:
|
||||||
EdgeInsets.symmetric(horizontal: 30.0),
|
EdgeInsets.symmetric(horizontal: 30.0),
|
||||||
onPressed: backplay
|
onPressed:
|
||||||
? null
|
backplay == BasicPlaybackState.playing
|
||||||
: () {
|
? null
|
||||||
audio.resumeAudio();
|
: () {
|
||||||
},
|
audio.resumeAudio();
|
||||||
|
},
|
||||||
iconSize: 40.0,
|
iconSize: 40.0,
|
||||||
icon: Icon(Icons.play_circle_filled),
|
icon: Icon(Icons.play_circle_filled),
|
||||||
color: Theme.of(context)
|
color: Theme.of(context)
|
||||||
|
@ -321,9 +427,10 @@ class _PlayerWidgetState extends State<PlayerWidget> {
|
||||||
.labelColor),
|
.labelColor),
|
||||||
IconButton(
|
IconButton(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 30.0),
|
padding: EdgeInsets.symmetric(horizontal: 30.0),
|
||||||
onPressed: backplay
|
onPressed:
|
||||||
? () => audio.forwardAudio(30)
|
backplay == BasicPlaybackState.playing
|
||||||
: null,
|
? () => audio.forwardAudio(30)
|
||||||
|
: null,
|
||||||
iconSize: 32.0,
|
iconSize: 32.0,
|
||||||
icon: Icon(Icons.forward_30),
|
icon: Icon(Icons.forward_30),
|
||||||
color:
|
color:
|
||||||
|
@ -344,7 +451,7 @@ class _PlayerWidgetState extends State<PlayerWidget> {
|
||||||
color: Theme.of(context).scaffoldBackgroundColor,
|
color: Theme.of(context).scaffoldBackgroundColor,
|
||||||
borderRadius: BorderRadius.all(Radius.circular(10.0)),
|
borderRadius: BorderRadius.all(Radius.circular(10.0)),
|
||||||
),
|
),
|
||||||
child: Selector<AudioPlayer,
|
child: Selector<AudioPlayerNotifier,
|
||||||
Tuple3<EpisodeBrief, bool, bool>>(
|
Tuple3<EpisodeBrief, bool, bool>>(
|
||||||
selector: (_, audio) => Tuple3(audio.episode,
|
selector: (_, audio) => Tuple3(audio.episode,
|
||||||
audio.stopOnComplete, audio.showStopWatch),
|
audio.stopOnComplete, audio.showStopWatch),
|
||||||
|
@ -394,6 +501,7 @@ class _PlayerWidgetState extends State<PlayerWidget> {
|
||||||
],
|
],
|
||||||
onSelected: (value) {
|
onSelected: (value) {
|
||||||
if (value == 1) {
|
if (value == 1) {
|
||||||
|
audio.sleepTimer(_minSelected);
|
||||||
audio.setStopOnComplete = true;
|
audio.setStopOnComplete = true;
|
||||||
} else if (value == 2) {
|
} else if (value == 2) {
|
||||||
setState(() => _showTimer = true);
|
setState(() => _showTimer = true);
|
||||||
|
@ -414,8 +522,7 @@ class _PlayerWidgetState extends State<PlayerWidget> {
|
||||||
color:
|
color:
|
||||||
Theme.of(context).accentColor,
|
Theme.of(context).accentColor,
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(_timeLeft.toString(),
|
||||||
_timeLeft.toString(),
|
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white)),
|
color: Colors.white)),
|
||||||
),
|
),
|
||||||
|
@ -475,7 +582,7 @@ class _PlayerWidgetState extends State<PlayerWidget> {
|
||||||
// margin: EdgeInsets.all(20),
|
// margin: EdgeInsets.all(20),
|
||||||
//padding: EdgeInsets.only(bottom: 10.0),
|
//padding: EdgeInsets.only(bottom: 10.0),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.all(Radius.circular(10.0)),
|
// borderRadius: BorderRadius.all(Radius.circular(10.0)),
|
||||||
color: Theme.of(context).scaffoldBackgroundColor,
|
color: Theme.of(context).scaffoldBackgroundColor,
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
@ -511,7 +618,7 @@ class _PlayerWidgetState extends State<PlayerWidget> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Selector<AudioPlayer, List<EpisodeBrief>>(
|
child: Selector<AudioPlayerNotifier, List<EpisodeBrief>>(
|
||||||
selector: (_, audio) => audio.queue.playlist,
|
selector: (_, audio) => audio.queue.playlist,
|
||||||
builder: (_, playlist, __) {
|
builder: (_, playlist, __) {
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
|
@ -623,7 +730,7 @@ class _PlayerWidgetState extends State<PlayerWidget> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _miniPanel(double width, BuildContext context) {
|
Widget _miniPanel(double width, BuildContext context) {
|
||||||
var audio = Provider.of<AudioPlayer>(context, listen: false);
|
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
|
||||||
return Container(
|
return Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Theme.of(context).primaryColor,
|
color: Theme.of(context).primaryColor,
|
||||||
|
@ -631,7 +738,7 @@ class _PlayerWidgetState extends State<PlayerWidget> {
|
||||||
height: 60,
|
height: 60,
|
||||||
child:
|
child:
|
||||||
Column(mainAxisAlignment: MainAxisAlignment.start, children: <Widget>[
|
Column(mainAxisAlignment: MainAxisAlignment.start, children: <Widget>[
|
||||||
Selector<AudioPlayer, Tuple2<String, double>>(
|
Selector<AudioPlayerNotifier, Tuple2<String, double>>(
|
||||||
selector: (_, audio) =>
|
selector: (_, audio) =>
|
||||||
Tuple2(audio.episode?.primaryColor, audio.seekSliderValue),
|
Tuple2(audio.episode?.primaryColor, audio.seekSliderValue),
|
||||||
builder: (_, data, __) {
|
builder: (_, data, __) {
|
||||||
|
@ -657,58 +764,33 @@ class _PlayerWidgetState extends State<PlayerWidget> {
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Expanded(
|
Expanded(
|
||||||
flex: 4,
|
flex: 4,
|
||||||
child: Selector<AudioPlayer, String>(
|
child: Selector<AudioPlayerNotifier, String>(
|
||||||
selector: (_, audio) => audio.episode.title,
|
selector: (_, audio) => audio.episode.title,
|
||||||
builder: (_, title, __) {
|
builder: (_, title, __) {
|
||||||
return LayoutBuilder(
|
return Text(
|
||||||
builder: (context, size) {
|
title,
|
||||||
var span = TextSpan(
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
text: title,
|
maxLines: 2,
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
overflow: TextOverflow.clip,
|
||||||
);
|
|
||||||
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),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
flex: 2,
|
flex: 2,
|
||||||
child: Selector<AudioPlayer, Tuple2<bool, double>>(
|
child: Selector<AudioPlayerNotifier,
|
||||||
|
Tuple2<BasicPlaybackState, double>>(
|
||||||
selector: (_, audio) => Tuple2(
|
selector: (_, audio) => Tuple2(
|
||||||
audio.remoteAudioLoading,
|
audio.audioState,
|
||||||
(audio.backgroundAudioDuration -
|
(audio.backgroundAudioDuration -
|
||||||
audio.backgroundAudioPosition)),
|
audio.backgroundAudioPosition) /
|
||||||
|
1000),
|
||||||
builder: (_, data, __) {
|
builder: (_, data, __) {
|
||||||
return Container(
|
return Container(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 10),
|
padding: EdgeInsets.symmetric(horizontal: 10),
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: data.item1
|
child: data.item1 == BasicPlaybackState.buffering ||
|
||||||
|
data.item1 == BasicPlaybackState.connecting
|
||||||
? Text(
|
? Text(
|
||||||
'Buffring...',
|
'Buffring...',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
|
@ -730,30 +812,32 @@ class _PlayerWidgetState extends State<PlayerWidget> {
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
flex: 2,
|
flex: 2,
|
||||||
child: Selector<AudioPlayer, bool>(
|
child: Selector<AudioPlayerNotifier, BasicPlaybackState>(
|
||||||
selector: (_, audio) => audio.backgroundAudioPlaying,
|
selector: (_, audio) => audio.audioState,
|
||||||
builder: (_, audioplay, __) {
|
builder: (_, audioplay, __) {
|
||||||
return Row(
|
return Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
children: [
|
children: [
|
||||||
audioplay
|
audioplay == BasicPlaybackState.playing
|
||||||
? InkWell(
|
? InkWell(
|
||||||
onTap: audioplay
|
onTap:
|
||||||
? () {
|
audioplay == BasicPlaybackState.playing
|
||||||
audio.pauseAduio();
|
? () {
|
||||||
}
|
audio.pauseAduio();
|
||||||
: null,
|
}
|
||||||
|
: null,
|
||||||
child: ImageRotate(
|
child: ImageRotate(
|
||||||
title: audio.episode.title,
|
title: audio.episode.title,
|
||||||
path: audio.episode.imagePath),
|
path: audio.episode.imagePath),
|
||||||
)
|
)
|
||||||
: InkWell(
|
: InkWell(
|
||||||
onTap: audioplay
|
onTap:
|
||||||
? null
|
audioplay == BasicPlaybackState.playing
|
||||||
: () {
|
? null
|
||||||
audio.resumeAudio();
|
: () {
|
||||||
},
|
audio.resumeAudio();
|
||||||
|
},
|
||||||
child: Stack(
|
child: Stack(
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
|
@ -781,11 +865,10 @@ class _PlayerWidgetState extends State<PlayerWidget> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: audioplay
|
onPressed:
|
||||||
? () => audio.forwardAudio(30)
|
() => audio.playNext(),
|
||||||
: null,
|
|
||||||
iconSize: 25.0,
|
iconSize: 25.0,
|
||||||
icon: Icon(Icons.forward_30),
|
icon: Icon(Icons.skip_next),
|
||||||
color:
|
color:
|
||||||
Theme.of(context).tabBarTheme.labelColor),
|
Theme.of(context).tabBarTheme.labelColor),
|
||||||
],
|
],
|
||||||
|
@ -803,7 +886,7 @@ class _PlayerWidgetState extends State<PlayerWidget> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
double _width = MediaQuery.of(context).size.width;
|
double _width = MediaQuery.of(context).size.width;
|
||||||
return Selector<AudioPlayer, bool>(
|
return Selector<AudioPlayerNotifier, bool>(
|
||||||
selector: (_, audio) => audio.playerRunning,
|
selector: (_, audio) => audio.playerRunning,
|
||||||
builder: (_, playerrunning, __) {
|
builder: (_, playerrunning, __) {
|
||||||
return !playerrunning
|
return !playerrunning
|
||||||
|
|
|
@ -31,7 +31,7 @@ class _HomeState extends State<Home> {
|
||||||
}
|
}
|
||||||
|
|
||||||
_getPlaylist() async {
|
_getPlaylist() async {
|
||||||
await Provider.of<AudioPlayer>(context, listen: false).loadPlaylist();
|
await Provider.of<AudioPlayerNotifier>(context, listen: false).loadPlaylist();
|
||||||
setState(() {
|
setState(() {
|
||||||
_loadPlay = true;
|
_loadPlay = true;
|
||||||
});
|
});
|
||||||
|
@ -39,7 +39,7 @@ class _HomeState extends State<Home> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var audio = Provider.of<AudioPlayer>(context, listen: false);
|
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
|
||||||
return Stack(children: <Widget>[
|
return Stack(children: <Widget>[
|
||||||
Column(
|
Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
@ -58,7 +58,7 @@ class _HomeState extends State<Home> {
|
||||||
bottom: 50,
|
bottom: 50,
|
||||||
right: _loadPlay ? 5 : -25,
|
right: _loadPlay ? 5 : -25,
|
||||||
child: Container(
|
child: Container(
|
||||||
child: Selector<AudioPlayer, Tuple3<bool, Playlist, int>>(
|
child: Selector<AudioPlayerNotifier, Tuple3<bool, Playlist, int>>(
|
||||||
selector: (_, audio) =>
|
selector: (_, audio) =>
|
||||||
Tuple3(audio.playerRunning, audio.queue, audio.lastPositin),
|
Tuple3(audio.playerRunning, audio.queue, audio.lastPositin),
|
||||||
builder: (_, data, __) => !_loadPlay
|
builder: (_, data, __) => !_loadPlay
|
||||||
|
@ -90,7 +90,7 @@ class _HomeState extends State<Home> {
|
||||||
offset: Offset(1, 1)),
|
offset: Offset(1, 1)),
|
||||||
]),
|
]),
|
||||||
height: 40,
|
height: 40,
|
||||||
child: Text(_stringForSeconds(data.item3) + '...',
|
child: Text(_stringForSeconds(data.item3~/1000) + '...',
|
||||||
style: TextStyle(color: Colors.white)),
|
style: TextStyle(color: Colors.white)),
|
||||||
),
|
),
|
||||||
CircleAvatar(
|
CircleAvatar(
|
||||||
|
|
|
@ -5,7 +5,9 @@ import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:fluttertoast/fluttertoast.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/episodebrief.dart';
|
||||||
import 'package:tsacdop/class/importompl.dart';
|
import 'package:tsacdop/class/importompl.dart';
|
||||||
import 'package:tsacdop/class/podcast_group.dart';
|
import 'package:tsacdop/class/podcast_group.dart';
|
||||||
|
@ -89,7 +91,9 @@ class _ScrollPodcastsState extends State<ScrollPodcasts> {
|
||||||
style: Theme.of(context)
|
style: Theme.of(context)
|
||||||
.textTheme
|
.textTheme
|
||||||
.bodyText1
|
.bodyText1
|
||||||
.copyWith(color: Colors.red[300]),
|
.copyWith(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.accentColor),
|
||||||
)),
|
)),
|
||||||
Spacer(),
|
Spacer(),
|
||||||
Container(
|
Container(
|
||||||
|
@ -184,7 +188,9 @@ class _ScrollPodcastsState extends State<ScrollPodcasts> {
|
||||||
style: Theme.of(context)
|
style: Theme.of(context)
|
||||||
.textTheme
|
.textTheme
|
||||||
.bodyText1
|
.bodyText1
|
||||||
.copyWith(color: Colors.red[300]),
|
.copyWith(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.accentColor),
|
||||||
)),
|
)),
|
||||||
Spacer(),
|
Spacer(),
|
||||||
Container(
|
Container(
|
||||||
|
@ -374,43 +380,78 @@ class ShowEpisode extends StatelessWidget {
|
||||||
final List<EpisodeBrief> podcast;
|
final List<EpisodeBrief> podcast;
|
||||||
final PodcastLocal podcastLocal;
|
final PodcastLocal podcastLocal;
|
||||||
ShowEpisode({Key key, this.podcast, this.podcastLocal}) : super(key: key);
|
ShowEpisode({Key key, this.podcast, this.podcastLocal}) : super(key: key);
|
||||||
|
Offset offset;
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
double _width = MediaQuery.of(context).size.width;
|
double _width = MediaQuery.of(context).size.width;
|
||||||
_showPopupMenu(Offset offset) async {
|
_showPopupMenu(Offset offset, EpisodeBrief episode, BuildContext context,
|
||||||
print(offset.dx);
|
bool isPlaying, bool isInPlaylist) async {
|
||||||
|
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
|
||||||
double left = offset.dx;
|
double left = offset.dx;
|
||||||
double top = offset.dy;
|
double top = offset.dy;
|
||||||
await showMenu(
|
await showMenu<int>(
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.all(Radius.circular(10))),
|
borderRadius: BorderRadius.all(Radius.circular(10))),
|
||||||
context: context,
|
context: context,
|
||||||
position: RelativeRect.fromLTRB(left, top, _width - left, 0),
|
position: RelativeRect.fromLTRB(left, top, _width - left, 0),
|
||||||
items: [
|
items: <PopupMenuEntry<int>>[
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
|
value: 0,
|
||||||
child: Row(
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Icon(Icons.play_circle_outline),
|
Icon(
|
||||||
Padding(padding: EdgeInsets.symmetric(horizontal: 2),),
|
LineIcons.play_circle_solid,
|
||||||
Text('Play')
|
color: Theme.of(context).accentColor,
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 2),
|
||||||
|
),
|
||||||
|
!isPlaying ? Text('Play') : Text('Playing'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
PopupMenuItem(child: Row(
|
PopupMenuItem(
|
||||||
children: <Widget>[
|
value: 1,
|
||||||
Icon(Icons.favorite_border),
|
child: Row(
|
||||||
Padding(padding: EdgeInsets.symmetric(horizontal: 2),),
|
children: <Widget>[
|
||||||
Text('Like')
|
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(
|
return CustomScrollView(
|
||||||
physics: const AlwaysScrollableScrollPhysics(),
|
// physics: const AlwaysScrollableScrollPhysics(),
|
||||||
|
physics: ClampingScrollPhysics(),
|
||||||
primary: false,
|
primary: false,
|
||||||
slivers: <Widget>[
|
slivers: <Widget>[
|
||||||
SliverPadding(
|
SliverPadding(
|
||||||
|
@ -427,88 +468,110 @@ class ShowEpisode extends StatelessWidget {
|
||||||
Color _c = (Theme.of(context).brightness == Brightness.light)
|
Color _c = (Theme.of(context).brightness == Brightness.light)
|
||||||
? podcastLocal.primaryColor.colorizedark()
|
? podcastLocal.primaryColor.colorizedark()
|
||||||
: podcastLocal.primaryColor.colorizeLight();
|
: podcastLocal.primaryColor.colorizeLight();
|
||||||
return GestureDetector(
|
return Selector<AudioPlayerNotifier,
|
||||||
onLongPressStart: (details) => _showPopupMenu(Offset(
|
Tuple2<EpisodeBrief, List<String>>>(
|
||||||
details.globalPosition.dx, details.globalPosition.dy)),
|
selector: (_, audio) => Tuple2(
|
||||||
onTap: () {
|
audio?.episode,
|
||||||
Navigator.push(
|
audio.queue.playlist.map((e) => e.enclosureUrl).toList(),
|
||||||
context,
|
),
|
||||||
ScaleRoute(
|
builder: (_, data, __) => Container(
|
||||||
page: EpisodeDetail(
|
|
||||||
episodeItem: podcast[index],
|
|
||||||
heroTag: 'scroll',
|
|
||||||
//unique hero tag
|
|
||||||
)),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: Container(
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.all(Radius.circular(5.0)),
|
borderRadius: BorderRadius.all(Radius.circular(5.0)),
|
||||||
color: Theme.of(context).scaffoldBackgroundColor,
|
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,
|
alignment: Alignment.center,
|
||||||
padding: EdgeInsets.all(10.0),
|
child: Material(
|
||||||
child: Column(
|
color: Colors.transparent,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
child: InkWell(
|
||||||
children: <Widget>[
|
borderRadius: BorderRadius.all(Radius.circular(5.0)),
|
||||||
Expanded(
|
onTapDown: (details) => offset = Offset(
|
||||||
flex: 2,
|
details.globalPosition.dx,
|
||||||
child: Row(
|
details.globalPosition.dy),
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
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>[
|
children: <Widget>[
|
||||||
Hero(
|
Expanded(
|
||||||
tag: podcast[index].enclosureUrl + 'scroll',
|
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(
|
child: Container(
|
||||||
height: _width / 18,
|
padding: EdgeInsets.only(top: 2.0),
|
||||||
width: _width / 18,
|
alignment: Alignment.topLeft,
|
||||||
child: CircleAvatar(
|
child: Text(
|
||||||
backgroundImage: FileImage(
|
podcast[index].title,
|
||||||
File("${podcastLocal.imagePath}")),
|
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
@ -2,7 +2,7 @@ import 'dart:ui';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:tsacdop/class/episodebrief.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/local_storage/sqflite_localpodcast.dart';
|
||||||
import 'package:tsacdop/util/episodegrid.dart';
|
import 'package:tsacdop/util/episodegrid.dart';
|
||||||
|
|
||||||
|
@ -49,8 +49,8 @@ class _MainTabState extends State<MainTab> with TickerProviderStateMixin {
|
||||||
],
|
],
|
||||||
onSelected: (value) {
|
onSelected: (value) {
|
||||||
if (value == 0) {
|
if (value == 0) {
|
||||||
Navigator.push(
|
Navigator.push(context,
|
||||||
context, MaterialPageRoute(builder: (context) => PlayedHistory()));
|
MaterialPageRoute(builder: (context) => PlayedHistory()));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -81,6 +81,7 @@ class _MainTabState extends State<MainTab> with TickerProviderStateMixin {
|
||||||
height: 50,
|
height: 50,
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
child: TabBar(
|
child: TabBar(
|
||||||
|
indicatorSize: TabBarIndicatorSize.tab,
|
||||||
isScrollable: true,
|
isScrollable: true,
|
||||||
labelPadding: EdgeInsets.all(10.0),
|
labelPadding: EdgeInsets.all(10.0),
|
||||||
controller: _controller,
|
controller: _controller,
|
||||||
|
@ -134,26 +135,73 @@ class RecentUpdate extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _RecentUpdateState extends State<RecentUpdate> {
|
class _RecentUpdateState extends State<RecentUpdate> {
|
||||||
Future<List<EpisodeBrief>> _getRssItem() async {
|
Future<List<EpisodeBrief>> _getRssItem(int top) async {
|
||||||
var dbHelper = DBHelper();
|
var dbHelper = DBHelper();
|
||||||
List<EpisodeBrief> episodes = await dbHelper.getRecentRssItem();
|
List<EpisodeBrief> episodes = await dbHelper.getRecentRssItem(top);
|
||||||
return episodes;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return FutureBuilder<List<EpisodeBrief>>(
|
return FutureBuilder<List<EpisodeBrief>>(
|
||||||
future: _getRssItem(),
|
future: _getRssItem(_top),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (snapshot.hasError) print(snapshot.error);
|
if (snapshot.hasError) print(snapshot.error);
|
||||||
return (snapshot.hasData)
|
return (snapshot.hasData)
|
||||||
? EpisodeGrid(
|
? CustomScrollView(
|
||||||
podcast: snapshot.data,
|
controller: _controller,
|
||||||
showDownload: false,
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
showFavorite: false,
|
primary: false,
|
||||||
showNumber: false,
|
slivers: <Widget>[
|
||||||
heroTag: 'recent',
|
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());
|
: Center(child: CircularProgressIndicator());
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -179,12 +227,18 @@ class _MyFavoriteState extends State<MyFavorite> {
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (snapshot.hasError) print(snapshot.error);
|
if (snapshot.hasError) print(snapshot.error);
|
||||||
return (snapshot.hasData)
|
return (snapshot.hasData)
|
||||||
? EpisodeGrid(
|
? CustomScrollView(
|
||||||
podcast: snapshot.data,
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
showDownload: false,
|
primary: false,
|
||||||
showFavorite: false,
|
slivers: <Widget>[
|
||||||
showNumber: false,
|
EpisodeGrid(
|
||||||
heroTag: 'favorite',
|
podcast: snapshot.data,
|
||||||
|
showDownload: false,
|
||||||
|
showFavorite: false,
|
||||||
|
showNumber: false,
|
||||||
|
heroTag: 'favorite',
|
||||||
|
)
|
||||||
|
],
|
||||||
)
|
)
|
||||||
: Center(child: CircularProgressIndicator());
|
: Center(child: CircularProgressIndicator());
|
||||||
},
|
},
|
||||||
|
@ -211,12 +265,19 @@ class _MyDownloadState extends State<MyDownload> {
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (snapshot.hasError) print(snapshot.error);
|
if (snapshot.hasError) print(snapshot.error);
|
||||||
return (snapshot.hasData)
|
return (snapshot.hasData)
|
||||||
? EpisodeGrid(
|
? CustomScrollView(
|
||||||
podcast: snapshot.data,
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
showDownload: true,
|
primary: false,
|
||||||
showFavorite: false,
|
slivers: <Widget>[
|
||||||
showNumber: false,
|
EpisodeGrid(
|
||||||
heroTag: 'download',
|
podcast: snapshot.data,
|
||||||
|
showDownload: true,
|
||||||
|
showFavorite: false,
|
||||||
|
showNumber: false,
|
||||||
|
heroTag: 'download',
|
||||||
|
)
|
||||||
|
],
|
||||||
|
|
||||||
)
|
)
|
||||||
: Center(child: CircularProgressIndicator());
|
: Center(child: CircularProgressIndicator());
|
||||||
},
|
},
|
||||||
|
|
|
@ -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(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -43,7 +43,7 @@ class KeyValueStorage {
|
||||||
return prefs.getInt(key);
|
return prefs.getInt(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> saveStringlist(List<String> playList) async{
|
Future<bool> saveStringList(List<String> playList) async{
|
||||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
return prefs.setStringList(key, playList);
|
return prefs.setStringList(key, playList);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import 'package:tsacdop/class/podcastlocal.dart';
|
||||||
import 'package:tsacdop/class/audiostate.dart';
|
import 'package:tsacdop/class/audiostate.dart';
|
||||||
import 'package:tsacdop/class/episodebrief.dart';
|
import 'package:tsacdop/class/episodebrief.dart';
|
||||||
import 'package:tsacdop/webfeed/webfeed.dart';
|
import 'package:tsacdop/webfeed/webfeed.dart';
|
||||||
|
import 'package:tsacdop/class/sub_history.dart';
|
||||||
|
|
||||||
class DBHelper {
|
class DBHelper {
|
||||||
static Database _db;
|
static Database _db;
|
||||||
|
@ -35,10 +36,13 @@ class DBHelper {
|
||||||
enclosure_url TEXT UNIQUE, enclosure_length INTEGER, pubDate TEXT,
|
enclosure_url TEXT UNIQUE, enclosure_length INTEGER, pubDate TEXT,
|
||||||
description TEXT, feed_id TEXT, feed_link TEXT, milliseconds INTEGER,
|
description TEXT, feed_id TEXT, feed_link TEXT, milliseconds INTEGER,
|
||||||
duration INTEGER DEFAULT 0, explicit INTEGER DEFAULT 0, liked INTEGER DEFAULT 0,
|
duration INTEGER DEFAULT 0, explicit INTEGER DEFAULT 0, liked INTEGER DEFAULT 0,
|
||||||
downloaded TEXT DEFAULT 'ND', download_date INTEGER DEFAULT 0)""");
|
downloaded TEXT DEFAULT 'ND', download_date INTEGER DEFAULT 0, media_id TEXT)""");
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"""CREATE TABLE PlayHistory(id INTEGER PRIMARY KEY, title TEXT, enclosure_url TEXT UNIQUE,
|
"""CREATE TABLE PlayHistory(id INTEGER PRIMARY KEY, title TEXT, enclosure_url TEXT UNIQUE,
|
||||||
seconds REAL, seek_value REAL, add_date INTEGER)""");
|
seconds REAL, seek_value REAL, add_date INTEGER)""");
|
||||||
|
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 {
|
Future<List<PodcastLocal>> getPodcastLocal(List<String> podcasts) async {
|
||||||
|
@ -97,7 +101,7 @@ class DBHelper {
|
||||||
int _milliseconds = DateTime.now().millisecondsSinceEpoch;
|
int _milliseconds = DateTime.now().millisecondsSinceEpoch;
|
||||||
var dbClient = await database;
|
var dbClient = await database;
|
||||||
await dbClient.transaction((txn) async {
|
await dbClient.transaction((txn) async {
|
||||||
return await txn.rawInsert(
|
await txn.rawInsert(
|
||||||
"""INSERT OR IGNORE INTO PodcastLocal (id, title, imageUrl, rssUrl,
|
"""INSERT OR IGNORE INTO PodcastLocal (id, title, imageUrl, rssUrl,
|
||||||
primaryColor, author, description, add_date, imagePath, provider, link) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
primaryColor, author, description, add_date, imagePath, provider, link) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
||||||
[
|
[
|
||||||
|
@ -114,6 +118,16 @@ class DBHelper {
|
||||||
podcastLocal.link
|
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 {
|
Future<int> saveFiresideData(List<String> list) async {
|
||||||
|
@ -146,6 +160,10 @@ class DBHelper {
|
||||||
print('Removed all download tasks');
|
print('Removed all download tasks');
|
||||||
}
|
}
|
||||||
await dbClient.rawDelete('DELETE FROM Episodes WHERE feed_id=?', [id]);
|
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 {
|
Future<int> saveHistory(PlayHistory history) async {
|
||||||
|
@ -174,16 +192,46 @@ class DBHelper {
|
||||||
""");
|
""");
|
||||||
List<PlayHistory> playHistory = [];
|
List<PlayHistory> playHistory = [];
|
||||||
list.forEach((record) {
|
list.forEach((record) {
|
||||||
playHistory.add(PlayHistory(
|
playHistory.add(PlayHistory(record['title'], record['enclosure_url'],
|
||||||
record['title'],
|
record['seconds'], record['seek_value'],
|
||||||
record['enclosure_url'],
|
playdate: DateTime.fromMillisecondsSinceEpoch(record['add_date'])));
|
||||||
record['seconds'],
|
|
||||||
record['seek_value'],
|
|
||||||
));
|
|
||||||
});
|
});
|
||||||
return playHistory;
|
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 {
|
Future<int> getPosition(EpisodeBrief episodeBrief) async {
|
||||||
var dbClient = await database;
|
var dbClient = await database;
|
||||||
List<Map> list = await dbClient.rawQuery(
|
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 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 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 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 {
|
try {
|
||||||
date = DateFormat('EEE, dd MMM yyyy HH:mm:ss Z', 'en_US').parse(pubDate);
|
date = DateFormat('EEE, dd MMM yyyy HH:mm:ss Z', 'en_US').parse(pubDate);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -209,7 +268,7 @@ class DBHelper {
|
||||||
try {
|
try {
|
||||||
date = DateFormat('EEE, dd MMM yyyy HH:mm Z', 'en_US').parse(pubDate);
|
date = DateFormat('EEE, dd MMM yyyy HH:mm Z', 'en_US').parse(pubDate);
|
||||||
} catch (e) {
|
} 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 year = yyyy.stringMatch(pubDate);
|
||||||
String time = hhmm.stringMatch(pubDate);
|
String time = hhmm.stringMatch(pubDate);
|
||||||
String month = ddmmm.stringMatch(pubDate);
|
String month = ddmmm.stringMatch(pubDate);
|
||||||
|
@ -221,12 +280,12 @@ class DBHelper {
|
||||||
date = DateFormat('mm-dd yyyy HH:mm', 'en_US')
|
date = DateFormat('mm-dd yyyy HH:mm', 'en_US')
|
||||||
.parse(month + ' ' + year + ' ' + time);
|
.parse(month + ' ' + year + ' ' + time);
|
||||||
} else {
|
} else {
|
||||||
date = DateTime.now();
|
date = DateTime.now().toUtc();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return date;
|
return date.add(Duration(hours: timezoneInt));
|
||||||
}
|
}
|
||||||
|
|
||||||
int getExplicit(bool b) {
|
int getExplicit(bool b) {
|
||||||
|
@ -276,7 +335,7 @@ class DBHelper {
|
||||||
await dbClient.transaction((txn) {
|
await dbClient.transaction((txn) {
|
||||||
return txn.rawInsert(
|
return txn.rawInsert(
|
||||||
"""INSERT OR IGNORE INTO Episodes(title, enclosure_url, enclosure_length, pubDate,
|
"""INSERT OR IGNORE INTO Episodes(title, enclosure_url, enclosure_length, pubDate,
|
||||||
description, feed_id, milliseconds, duration, explicit) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
description, feed_id, milliseconds, duration, explicit, media_id) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
||||||
[
|
[
|
||||||
_title,
|
_title,
|
||||||
_url,
|
_url,
|
||||||
|
@ -287,6 +346,7 @@ class DBHelper {
|
||||||
_milliseconds,
|
_milliseconds,
|
||||||
_duration,
|
_duration,
|
||||||
_explicit,
|
_explicit,
|
||||||
|
_url
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -333,7 +393,7 @@ class DBHelper {
|
||||||
await dbClient.transaction((txn) {
|
await dbClient.transaction((txn) {
|
||||||
return txn.rawInsert(
|
return txn.rawInsert(
|
||||||
"""INSERT OR IGNORE INTO Episodes(title, enclosure_url, enclosure_length, pubDate,
|
"""INSERT OR IGNORE INTO Episodes(title, enclosure_url, enclosure_length, pubDate,
|
||||||
description, feed_id, milliseconds, duration, explicit) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
description, feed_id, milliseconds, duration, explicit, media_id) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
||||||
[
|
[
|
||||||
_title,
|
_title,
|
||||||
_url,
|
_url,
|
||||||
|
@ -344,6 +404,7 @@ class DBHelper {
|
||||||
_milliseconds,
|
_milliseconds,
|
||||||
_duration,
|
_duration,
|
||||||
_explicit,
|
_explicit,
|
||||||
|
_url
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -358,7 +419,7 @@ class DBHelper {
|
||||||
List<Map> list = await dbClient
|
List<Map> list = await dbClient
|
||||||
.rawQuery("""SELECT E.title, E.enclosure_url, E.enclosure_length,
|
.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.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
|
FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
|
||||||
WHERE P.id = ? ORDER BY E.milliseconds DESC""", [id]);
|
WHERE P.id = ? ORDER BY E.milliseconds DESC""", [id]);
|
||||||
for (int x = 0; x < list.length; x++) {
|
for (int x = 0; x < list.length; x++) {
|
||||||
|
@ -373,7 +434,8 @@ class DBHelper {
|
||||||
list[x]['downloaded'],
|
list[x]['downloaded'],
|
||||||
list[x]['duration'],
|
list[x]['duration'],
|
||||||
list[x]['explicit'],
|
list[x]['explicit'],
|
||||||
list[x]['imagePath']));
|
list[x]['imagePath'],
|
||||||
|
list[x]['media_id']));
|
||||||
}
|
}
|
||||||
return episodes;
|
return episodes;
|
||||||
}
|
}
|
||||||
|
@ -384,7 +446,7 @@ class DBHelper {
|
||||||
List<Map> list = await dbClient
|
List<Map> list = await dbClient
|
||||||
.rawQuery("""SELECT E.title, E.enclosure_url, E.enclosure_length,
|
.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.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
|
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]);
|
where E.feed_id = ? ORDER BY E.milliseconds DESC LIMIT 3""", [id]);
|
||||||
for (int x = 0; x < list.length; x++) {
|
for (int x = 0; x < list.length; x++) {
|
||||||
|
@ -399,7 +461,8 @@ class DBHelper {
|
||||||
list[x]['downloaded'],
|
list[x]['downloaded'],
|
||||||
list[x]['duration'],
|
list[x]['duration'],
|
||||||
list[x]['explicit'],
|
list[x]['explicit'],
|
||||||
list[x]['imagePath']));
|
list[x]['imagePath'],
|
||||||
|
list[x]['media_id']));
|
||||||
}
|
}
|
||||||
return episodes;
|
return episodes;
|
||||||
}
|
}
|
||||||
|
@ -410,7 +473,7 @@ class DBHelper {
|
||||||
List<Map> list = await dbClient.rawQuery(
|
List<Map> list = await dbClient.rawQuery(
|
||||||
"""SELECT E.title, E.enclosure_url, E.enclosure_length,
|
"""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.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
|
FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
|
||||||
where E.enclosure_url = ? ORDER BY E.milliseconds DESC LIMIT 3""",
|
where E.enclosure_url = ? ORDER BY E.milliseconds DESC LIMIT 3""",
|
||||||
[url]);
|
[url]);
|
||||||
|
@ -427,19 +490,20 @@ class DBHelper {
|
||||||
list.first['downloaded'],
|
list.first['downloaded'],
|
||||||
list.first['duration'],
|
list.first['duration'],
|
||||||
list.first['explicit'],
|
list.first['explicit'],
|
||||||
list.first['imagePath']);
|
list.first['imagePath'],
|
||||||
|
list.first['media_id']);
|
||||||
return episode;
|
return episode;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<EpisodeBrief>> getRecentRssItem() async {
|
Future<List<EpisodeBrief>> getRecentRssItem(int top) async {
|
||||||
var dbClient = await database;
|
var dbClient = await database;
|
||||||
List<EpisodeBrief> episodes = List();
|
List<EpisodeBrief> episodes = List();
|
||||||
List<Map> list = await dbClient
|
List<Map> list = await dbClient
|
||||||
.rawQuery("""SELECT E.title, E.enclosure_url, E.enclosure_length,
|
.rawQuery("""SELECT E.title, E.enclosure_url, E.enclosure_length,
|
||||||
E.milliseconds, P.title as feed_title, E.duration, E.explicit, E.liked,
|
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
|
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++) {
|
for (int x = 0; x < list.length; x++) {
|
||||||
episodes.add(EpisodeBrief(
|
episodes.add(EpisodeBrief(
|
||||||
list[x]['title'],
|
list[x]['title'],
|
||||||
|
@ -452,7 +516,8 @@ class DBHelper {
|
||||||
list[x]['doanloaded'],
|
list[x]['doanloaded'],
|
||||||
list[x]['duration'],
|
list[x]['duration'],
|
||||||
list[x]['explicit'],
|
list[x]['explicit'],
|
||||||
list[x]['imagePath']));
|
list[x]['imagePath'],
|
||||||
|
list[x]['media_id']));
|
||||||
}
|
}
|
||||||
return episodes;
|
return episodes;
|
||||||
}
|
}
|
||||||
|
@ -463,7 +528,7 @@ class DBHelper {
|
||||||
List<Map> list = await dbClient.rawQuery(
|
List<Map> list = await dbClient.rawQuery(
|
||||||
"""SELECT E.title, E.enclosure_url, E.enclosure_length, E.milliseconds, P.imagePath,
|
"""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.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""");
|
WHERE E.liked = 1 ORDER BY E.milliseconds DESC LIMIT 99""");
|
||||||
for (int x = 0; x < list.length; x++) {
|
for (int x = 0; x < list.length; x++) {
|
||||||
episodes.add(EpisodeBrief(
|
episodes.add(EpisodeBrief(
|
||||||
|
@ -477,7 +542,8 @@ class DBHelper {
|
||||||
list[x]['downloaded'],
|
list[x]['downloaded'],
|
||||||
list[x]['duration'],
|
list[x]['duration'],
|
||||||
list[x]['explicit'],
|
list[x]['explicit'],
|
||||||
list[x]['imagePath']));
|
list[x]['imagePath'],
|
||||||
|
list[x]['media_id']));
|
||||||
}
|
}
|
||||||
return episodes;
|
return episodes;
|
||||||
}
|
}
|
||||||
|
@ -507,10 +573,21 @@ class DBHelper {
|
||||||
return count;
|
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 {
|
Future<int> delDownloaded(String url) async {
|
||||||
var dbClient = await database;
|
var dbClient = await database;
|
||||||
int count = await dbClient.rawUpdate(
|
int count = await dbClient.rawUpdate(
|
||||||
"UPDATE Episodes SET downloaded = 'ND' WHERE enclosure_url = ?", [url]);
|
"UPDATE Episodes SET downloaded = 'ND', media_id = ? WHERE enclosure_url = ?", [url, url]);
|
||||||
print('Deleted ' + url);
|
print('Deleted ' + url);
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
@ -521,7 +598,7 @@ class DBHelper {
|
||||||
List<Map> list = await dbClient.rawQuery(
|
List<Map> list = await dbClient.rawQuery(
|
||||||
"""SELECT E.title, E.enclosure_url, E.enclosure_length, E.milliseconds, P.imagePath,
|
"""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.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""");
|
WHERE E.downloaded != 'ND' ORDER BY E.download_date DESC LIMIT 99""");
|
||||||
for (int x = 0; x < list.length; x++) {
|
for (int x = 0; x < list.length; x++) {
|
||||||
episodes.add(EpisodeBrief(
|
episodes.add(EpisodeBrief(
|
||||||
|
@ -535,7 +612,8 @@ class DBHelper {
|
||||||
list[x]['downloaded'],
|
list[x]['downloaded'],
|
||||||
list[x]['duration'],
|
list[x]['duration'],
|
||||||
list[x]['explicit'],
|
list[x]['explicit'],
|
||||||
list[x]['imagePath']));
|
list[x]['imagePath'],
|
||||||
|
list[x]['media_id']));
|
||||||
}
|
}
|
||||||
return episodes;
|
return episodes;
|
||||||
}
|
}
|
||||||
|
@ -562,7 +640,7 @@ class DBHelper {
|
||||||
List<Map> list = await dbClient.rawQuery(
|
List<Map> list = await dbClient.rawQuery(
|
||||||
"""SELECT E.title, E.enclosure_url, E.enclosure_length, E.milliseconds, P.imagePath,
|
"""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.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]);
|
WHERE E.enclosure_url = ?""", [url]);
|
||||||
episode = EpisodeBrief(
|
episode = EpisodeBrief(
|
||||||
list.first['title'],
|
list.first['title'],
|
||||||
|
@ -575,7 +653,33 @@ class DBHelper {
|
||||||
list.first['downloaded'],
|
list.first['downloaded'],
|
||||||
list.first['duration'],
|
list.first['duration'],
|
||||||
list.first['explicit'],
|
list.first['explicit'],
|
||||||
list.first['imagePath']);
|
list.first['imagePath'],
|
||||||
|
list.first['media_id']);
|
||||||
return episode;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,57 +10,61 @@ import 'package:tsacdop/home/appbar/addpodcast.dart';
|
||||||
import 'package:tsacdop/class/audiostate.dart';
|
import 'package:tsacdop/class/audiostate.dart';
|
||||||
import 'package:tsacdop/class/importompl.dart';
|
import 'package:tsacdop/class/importompl.dart';
|
||||||
import 'package:tsacdop/class/settingstate.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();
|
final SettingState themeSetting = SettingState();
|
||||||
|
|
||||||
Future main() async {
|
Future main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
await themeSetting.initData();
|
await themeSetting.initData();
|
||||||
|
await FlutterDownloader.initialize();
|
||||||
runApp(
|
runApp(
|
||||||
MultiProvider(
|
MultiProvider(
|
||||||
providers: [
|
providers: [
|
||||||
ChangeNotifierProvider(create: (_) => themeSetting),
|
ChangeNotifierProvider(create: (_) => themeSetting),
|
||||||
ChangeNotifierProvider(create: (_) => AudioPlayer()),
|
ChangeNotifierProvider(create: (_) => AudioPlayerNotifier()),
|
||||||
ChangeNotifierProvider(create: (_) => GroupList()),
|
ChangeNotifierProvider(create: (_) => GroupList()),
|
||||||
ChangeNotifierProvider(create: (_) => ImportOmpl()),
|
ChangeNotifierProvider(create: (_) => ImportOmpl()),
|
||||||
],
|
],
|
||||||
child: MyApp(),
|
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(
|
await SystemChrome.setPreferredOrientations(
|
||||||
[DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]);
|
[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 {
|
class MyApp extends StatelessWidget {
|
||||||
|
void setWorkManager() {
|
||||||
|
Workmanager.initialize(
|
||||||
|
callbackDispatcher,
|
||||||
|
isInDebugMode: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
Workmanager.registerPeriodicTask("1", "update_podcasts",
|
||||||
|
frequency: Duration(hours: 12),
|
||||||
|
initialDelay: Duration(seconds: 5),
|
||||||
|
constraints: Constraints(
|
||||||
|
networkType: NetworkType.connected,
|
||||||
|
requiresBatteryNotLow: true,
|
||||||
|
));
|
||||||
|
}
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Consumer<SettingState>(
|
return Consumer<SettingState>(
|
||||||
builder: (_, setting, __) {
|
builder: (_, setting, __) {
|
||||||
|
if (setting.autoUpdate) setWorkManager();
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
themeMode: setting.theme,
|
themeMode: setting.theme,
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
|
@ -78,7 +82,6 @@ class MyApp extends StatelessWidget {
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
),
|
),
|
||||||
textTheme: TextTheme(
|
textTheme: TextTheme(
|
||||||
headline1: TextStyle(fontSize: 72.0, fontWeight: FontWeight.bold),
|
|
||||||
bodyText2:
|
bodyText2:
|
||||||
TextStyle(fontSize: 15.0, fontWeight: FontWeight.normal),
|
TextStyle(fontSize: 15.0, fontWeight: FontWeight.normal),
|
||||||
),
|
),
|
||||||
|
@ -89,6 +92,7 @@ class MyApp extends StatelessWidget {
|
||||||
),
|
),
|
||||||
darkTheme: ThemeData.dark().copyWith(
|
darkTheme: ThemeData.dark().copyWith(
|
||||||
accentColor: setting.accentSetColor,
|
accentColor: setting.accentSetColor,
|
||||||
|
appBarTheme: AppBarTheme(elevation: 0),
|
||||||
),
|
),
|
||||||
home: MyHomePage(),
|
home: MyHomePage(),
|
||||||
);
|
);
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,16 +7,13 @@ import 'package:flutter/services.dart';
|
||||||
import 'package:html/parser.dart';
|
import 'package:html/parser.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
|
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:tsacdop/class/podcastlocal.dart';
|
import 'package:tsacdop/class/podcastlocal.dart';
|
||||||
import 'package:tsacdop/class/episodebrief.dart';
|
import 'package:tsacdop/class/episodebrief.dart';
|
||||||
import 'package:tsacdop/episodes/episodedetail.dart';
|
|
||||||
import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
|
import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
|
||||||
import 'package:tsacdop/util/episodegrid.dart';
|
import 'package:tsacdop/util/episodegrid.dart';
|
||||||
import 'package:tsacdop/util/pageroute.dart';
|
|
||||||
import 'package:tsacdop/home/audioplayer.dart';
|
import 'package:tsacdop/home/audioplayer.dart';
|
||||||
import 'package:tsacdop/class/fireside_data.dart';
|
import 'package:tsacdop/class/fireside_data.dart';
|
||||||
import 'package:tsacdop/util/colorize.dart';
|
import 'package:tsacdop/util/colorize.dart';
|
||||||
|
@ -104,7 +101,9 @@ class _PodcastDetailState extends State<PodcastDetail> {
|
||||||
fit: BoxFit.cover)),
|
fit: BoxFit.cover)),
|
||||||
alignment: Alignment.centerRight,
|
alignment: Alignment.centerRight,
|
||||||
child: Container(
|
child: Container(
|
||||||
color: Theme.of(context).scaffoldBackgroundColor.withOpacity(0.5),
|
color: Theme.of(context)
|
||||||
|
.scaffoldBackgroundColor
|
||||||
|
.withOpacity(0.5),
|
||||||
padding: EdgeInsets.symmetric(vertical: 5.0),
|
padding: EdgeInsets.symmetric(vertical: 5.0),
|
||||||
width: MediaQuery.of(context).size.width,
|
width: MediaQuery.of(context).size.width,
|
||||||
alignment: Alignment.centerRight,
|
alignment: Alignment.centerRight,
|
||||||
|
@ -122,7 +121,8 @@ class _PodcastDetailState extends State<PodcastDetail> {
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
CircleAvatar(
|
CircleAvatar(
|
||||||
backgroundColor: Colors.grey[400],
|
backgroundColor: Colors.grey[400],
|
||||||
backgroundImage: CachedNetworkImageProvider(
|
backgroundImage:
|
||||||
|
CachedNetworkImageProvider(
|
||||||
host.image,
|
host.image,
|
||||||
)),
|
)),
|
||||||
Padding(
|
Padding(
|
||||||
|
@ -162,7 +162,7 @@ class _PodcastDetailState extends State<PodcastDetail> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
double _width = MediaQuery.of(context).size.width;
|
|
||||||
Color _color = widget.podcastLocal.primaryColor.colorizedark();
|
Color _color = widget.podcastLocal.primaryColor.colorizedark();
|
||||||
return AnnotatedRegion<SystemUiOverlayStyle>(
|
return AnnotatedRegion<SystemUiOverlayStyle>(
|
||||||
value: SystemUiOverlayStyle(
|
value: SystemUiOverlayStyle(
|
||||||
|
@ -283,7 +283,7 @@ class _PodcastDetailState extends State<PodcastDetail> {
|
||||||
'Hosted on ' +
|
'Hosted on ' +
|
||||||
widget.podcastLocal
|
widget.podcastLocal
|
||||||
.provider,
|
.provider,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white),
|
color: Colors.white),
|
||||||
)
|
)
|
||||||
|
@ -308,6 +308,8 @@ class _PodcastDetailState extends State<PodcastDetail> {
|
||||||
),
|
),
|
||||||
title: top < 70
|
title: top < 70
|
||||||
? Text(widget.podcastLocal.title,
|
? Text(widget.podcastLocal.title,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
style: TextStyle(color: Colors.white))
|
style: TextStyle(color: Colors.white))
|
||||||
: Center(),
|
: Center(),
|
||||||
);
|
);
|
||||||
|
@ -322,184 +324,15 @@ class _PodcastDetailState extends State<PodcastDetail> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SliverPadding(
|
SliverPadding(
|
||||||
padding:
|
padding: const EdgeInsets.symmetric(
|
||||||
const EdgeInsets.symmetric(horizontal: 15.0),
|
horizontal: 10.0),
|
||||||
sliver: SliverGrid(
|
sliver: EpisodeGrid(
|
||||||
gridDelegate:
|
podcast: snapshot.data,
|
||||||
SliverGridDelegateWithFixedCrossAxisCount(
|
showDownload: false,
|
||||||
childAspectRatio: 1.0,
|
showFavorite: true,
|
||||||
crossAxisCount: 3,
|
showNumber: true,
|
||||||
mainAxisSpacing: 6.0,
|
heroTag: 'podcast',
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
: Center(child: CircularProgressIndicator());
|
: Center(child: CircularProgressIndicator());
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:math' as math;
|
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:flutter_colorpicker/flutter_colorpicker.dart';
|
|
||||||
|
|
||||||
import 'package:tsacdop/class/podcast_group.dart';
|
import 'package:tsacdop/class/podcast_group.dart';
|
||||||
import 'package:tsacdop/class/podcastlocal.dart';
|
import 'package:tsacdop/class/podcastlocal.dart';
|
||||||
|
@ -21,87 +19,7 @@ class PodcastGroupList extends StatefulWidget {
|
||||||
_PodcastGroupListState createState() => _PodcastGroupListState();
|
_PodcastGroupListState createState() => _PodcastGroupListState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _PodcastGroupListState extends State<PodcastGroupList>
|
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();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var groupList = Provider.of<GroupList>(context, listen: false);
|
var groupList = Provider.of<GroupList>(context, listen: false);
|
||||||
|
@ -109,372 +27,38 @@ class _PodcastGroupListState extends State<PodcastGroupList>
|
||||||
? Container(
|
? Container(
|
||||||
color: Theme.of(context).primaryColor,
|
color: Theme.of(context).primaryColor,
|
||||||
)
|
)
|
||||||
: Stack(
|
: Container(
|
||||||
children: <Widget>[
|
color: Theme.of(context).primaryColor,
|
||||||
Container(
|
child: Stack(
|
||||||
color: Theme.of(context).primaryColor,
|
children: <Widget>[
|
||||||
child: Stack(
|
ReorderableListView(
|
||||||
children: <Widget>[
|
onReorder: (int oldIndex, int newIndex) {
|
||||||
ReorderableListView(
|
setState(() {
|
||||||
onReorder: (int oldIndex, int newIndex) {
|
if (newIndex > oldIndex) {
|
||||||
setState(() {
|
newIndex -= 1;
|
||||||
if (newIndex > oldIndex) {
|
}
|
||||||
newIndex -= 1;
|
final PodcastLocal podcast =
|
||||||
}
|
widget.group.podcasts.removeAt(oldIndex);
|
||||||
final PodcastLocal podcast =
|
widget.group.podcasts.insert(newIndex, podcast);
|
||||||
widget.group.podcasts.removeAt(oldIndex);
|
});
|
||||||
widget.group.podcasts.insert(newIndex, podcast);
|
widget.group.setOrderedPodcasts = widget.group.podcasts;
|
||||||
_controller.forward();
|
groupList.addToOrderChanged(widget.group.name);
|
||||||
});
|
},
|
||||||
},
|
children: widget.group.podcasts
|
||||||
children: widget.group.podcasts
|
.map<Widget>((PodcastLocal podcastLocal) {
|
||||||
.map<Widget>((PodcastLocal podcastLocal) {
|
return Container(
|
||||||
return Container(
|
decoration:
|
||||||
decoration: BoxDecoration(
|
BoxDecoration(color: Theme.of(context).primaryColor),
|
||||||
color: Theme.of(context).primaryColor),
|
key: ObjectKey(podcastLocal.title),
|
||||||
key: ObjectKey(podcastLocal.title),
|
child: PodcastCard(
|
||||||
child: PodcastCard(
|
podcastLocal: podcastLocal,
|
||||||
podcastLocal: podcastLocal,
|
group: widget.group,
|
||||||
group: widget.group,
|
),
|
||||||
),
|
);
|
||||||
);
|
}).toList(),
|
||||||
}).toList(),
|
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
bottom: 30,
|
|
||||||
right: 30,
|
|
||||||
child: _saveButton(context),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
_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)),
|
style: TextStyle(color: Theme.of(context).accentColor)),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
title: Text('Create new group'),
|
title: Text('Edit group name'),
|
||||||
content: Column(
|
content: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
TextField(
|
TextField(
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
contentPadding: EdgeInsets.symmetric(horizontal: 10),
|
contentPadding: EdgeInsets.symmetric(horizontal: 10),
|
||||||
hintText: 'New Group',
|
hintText: widget.group.name,
|
||||||
hintStyle: TextStyle(fontSize: 18),
|
hintStyle: TextStyle(fontSize: 18),
|
||||||
filled: true,
|
filled: true,
|
||||||
focusedBorder: UnderlineInputBorder(
|
focusedBorder: UnderlineInputBorder(
|
||||||
|
|
|
@ -1,23 +1,135 @@
|
||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:line_icons/line_icons.dart';
|
||||||
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:tsacdop/class/podcast_group.dart';
|
import 'package:tsacdop/class/podcast_group.dart';
|
||||||
import 'package:tsacdop/podcasts/podcastgroup.dart';
|
import 'package:tsacdop/podcasts/podcastgroup.dart';
|
||||||
import 'package:tsacdop/podcasts/podcastlist.dart';
|
import 'package:tsacdop/podcasts/podcastlist.dart';
|
||||||
import 'package:tsacdop/util/pageroute.dart';
|
import 'package:tsacdop/util/pageroute.dart';
|
||||||
|
import 'custom_tabview.dart';
|
||||||
|
|
||||||
class PodcastManage extends StatefulWidget {
|
class PodcastManage extends StatefulWidget {
|
||||||
@override
|
@override
|
||||||
_PodcastManageState createState() => _PodcastManageState();
|
_PodcastManageState createState() => _PodcastManageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _PodcastManageState extends State<PodcastManage> {
|
class _PodcastManageState extends State<PodcastManage>
|
||||||
Decoration getIndicator() {
|
with TickerProviderStateMixin {
|
||||||
return const UnderlineTabIndicator(
|
bool _showSetting;
|
||||||
borderSide: BorderSide(color: Colors.red, width: 0),
|
double _menuValue;
|
||||||
insets: EdgeInsets.only(
|
AnimationController _controller;
|
||||||
top: 10.0,
|
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) {
|
Widget build(BuildContext context) {
|
||||||
|
@ -53,59 +165,300 @@ class _PodcastManageState extends State<PodcastManage> {
|
||||||
List<PodcastGroup> _groups = groupList.groups;
|
List<PodcastGroup> _groups = groupList.groups;
|
||||||
return _isLoading
|
return _isLoading
|
||||||
? Center()
|
? Center()
|
||||||
: DefaultTabController(
|
: Stack(
|
||||||
length: _groups.length,
|
children: <Widget>[
|
||||||
child: Column(
|
CustomTabView(
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
itemCount: _groups.length,
|
||||||
mainAxisSize: MainAxisSize.min,
|
tabBuilder: (context, index) => Tab(
|
||||||
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(
|
|
||||||
child: Container(
|
child: Container(
|
||||||
child: TabBarView(
|
height: 30.0,
|
||||||
children: _groups.map<Widget>((group) {
|
padding: EdgeInsets.symmetric(horizontal: 10.0),
|
||||||
return Container(
|
alignment: Alignment.center,
|
||||||
key: ObjectKey(group),
|
decoration: BoxDecoration(
|
||||||
child: PodcastGroupList(group: group));
|
color: (_scroll - index).abs() > 1
|
||||||
}).toList(),
|
? 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(),
|
||||||
|
],
|
||||||
|
);
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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'),
|
||||||
|
];
|
|
@ -1,10 +1,30 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package: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/theme.dart';
|
||||||
|
import 'package:tsacdop/settings/storage.dart';
|
||||||
|
import 'package:tsacdop/settings/history.dart';
|
||||||
|
import 'libries.dart';
|
||||||
|
|
||||||
class Settings extends StatelessWidget {
|
class Settings extends StatelessWidget {
|
||||||
|
_launchUrl(String url) async {
|
||||||
|
if (await canLaunch(url)) {
|
||||||
|
await launch(url);
|
||||||
|
} else {
|
||||||
|
throw 'Could not launch $url';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
|
||||||
|
var settings = Provider.of<SettingState>(context, listen: false);
|
||||||
return AnnotatedRegion<SystemUiOverlayStyle>(
|
return AnnotatedRegion<SystemUiOverlayStyle>(
|
||||||
value: SystemUiOverlayStyle(
|
value: SystemUiOverlayStyle(
|
||||||
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
|
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
|
||||||
|
@ -18,121 +38,167 @@ class Settings extends StatelessWidget {
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
backgroundColor: Theme.of(context).primaryColor,
|
backgroundColor: Theme.of(context).primaryColor,
|
||||||
),
|
),
|
||||||
body: Column(
|
body: SingleChildScrollView(
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
mainAxisSize: MainAxisSize.min,
|
scrollDirection: Axis.vertical,
|
||||||
children: <Widget>[
|
child: Column(
|
||||||
Column(
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisSize: MainAxisSize.min,
|
||||||
mainAxisSize: MainAxisSize.min,
|
children: <Widget>[
|
||||||
children: <Widget>[
|
Column(
|
||||||
Padding(
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
padding: EdgeInsets.all(10.0),
|
mainAxisSize: MainAxisSize.min,
|
||||||
),
|
children: <Widget>[
|
||||||
Container(
|
Padding(
|
||||||
height: 30.0,
|
padding: EdgeInsets.all(10.0),
|
||||||
padding: EdgeInsets.symmetric(horizontal: 80),
|
),
|
||||||
alignment: Alignment.centerLeft,
|
Container(
|
||||||
child: Text('Prefrence',
|
height: 30.0,
|
||||||
style: Theme.of(context)
|
padding: EdgeInsets.symmetric(horizontal: 80),
|
||||||
.textTheme
|
alignment: Alignment.centerLeft,
|
||||||
.bodyText1
|
child: Text('Prefrence',
|
||||||
.copyWith(color: Theme.of(context).accentColor)),
|
style: Theme.of(context)
|
||||||
),
|
.textTheme
|
||||||
ListView(
|
.bodyText1
|
||||||
shrinkWrap: true,
|
.copyWith(color: Theme.of(context).accentColor)),
|
||||||
scrollDirection: Axis.vertical,
|
),
|
||||||
children: <Widget>[
|
ListView(
|
||||||
ListTile(
|
physics: ClampingScrollPhysics(),
|
||||||
onTap: () => Navigator.push(
|
shrinkWrap: true,
|
||||||
context,
|
scrollDirection: Axis.vertical,
|
||||||
MaterialPageRoute(
|
children: <Widget>[
|
||||||
builder: (context) => ThemeSetting())),
|
ListTile(
|
||||||
contentPadding: EdgeInsets.symmetric(horizontal: 25.0),
|
onTap: () => Navigator.push(
|
||||||
leading: Icon(Icons.colorize),
|
context,
|
||||||
title: Text('Appearance'),
|
MaterialPageRoute(
|
||||||
subtitle: Text('Colors and themes'),
|
builder: (context) => ThemeSetting())),
|
||||||
),
|
contentPadding:
|
||||||
Divider(height: 2),
|
EdgeInsets.symmetric(horizontal: 25.0),
|
||||||
ListTile(
|
leading: Icon(LineIcons.adjust_solid),
|
||||||
contentPadding: EdgeInsets.symmetric(horizontal: 25.0),
|
title: Text('Appearance'),
|
||||||
leading: Icon(Icons.network_check),
|
subtitle: Text('Colors and themes'),
|
||||||
title: Text('Network'),
|
),
|
||||||
subtitle: Text('Download network setting'),
|
Divider(height: 2),
|
||||||
),
|
ListTile(
|
||||||
Divider(height: 2),
|
contentPadding:
|
||||||
ListTile(
|
EdgeInsets.symmetric(horizontal: 25.0),
|
||||||
contentPadding: EdgeInsets.symmetric(horizontal: 25.0),
|
leading: Icon(LineIcons.play_circle),
|
||||||
leading: Icon(Icons.storage),
|
title: Text('AutoPlay'),
|
||||||
title: Text('Cache'),
|
subtitle: Text('Autoplay next episode in playlist'),
|
||||||
subtitle: Text('Manage and clear cache'),
|
trailing: Selector<AudioPlayerNotifier, bool>(
|
||||||
),
|
selector: (_, audio) => audio.autoPlay,
|
||||||
Divider(height: 2),
|
builder: (_, data, __) => Switch(
|
||||||
ListTile(
|
value: data,
|
||||||
contentPadding: EdgeInsets.symmetric(horizontal: 25.0),
|
onChanged: (boo) => audio.autoPlaySwitch = boo),
|
||||||
leading: Icon(Icons.update),
|
),
|
||||||
title: Text('Update'),
|
),
|
||||||
subtitle: Text('Update in background'),
|
Divider(height: 2),
|
||||||
),
|
ListTile(
|
||||||
Divider(height: 2),
|
contentPadding:
|
||||||
],
|
EdgeInsets.symmetric(horizontal: 25.0),
|
||||||
),
|
leading: Icon(LineIcons.cloud_download_alt_solid),
|
||||||
],
|
title: Text('AutoUpdate'),
|
||||||
),
|
subtitle: Text('Auto update feed every day'),
|
||||||
Padding(
|
trailing: Selector<SettingState, bool>(
|
||||||
padding: EdgeInsets.all(10.0),
|
selector: (_, settings) => settings.autoUpdate,
|
||||||
),
|
builder: (_, data, __) => Switch(
|
||||||
Column(
|
value: data,
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
onChanged: (boo) async {
|
||||||
mainAxisSize: MainAxisSize.min,
|
settings.autoUpdate = boo;
|
||||||
children: <Widget>[
|
if (!boo) await Workmanager.cancelAll();
|
||||||
Container(
|
}),
|
||||||
height: 30.0,
|
),
|
||||||
padding: EdgeInsets.symmetric(horizontal: 80),
|
),
|
||||||
alignment: Alignment.centerLeft,
|
Divider(height: 2),
|
||||||
child: Text('Info',
|
ListTile(
|
||||||
style: Theme.of(context)
|
onTap: () => Navigator.push(
|
||||||
.textTheme
|
context,
|
||||||
.bodyText1
|
MaterialPageRoute(
|
||||||
.copyWith(color: Theme.of(context).accentColor)),
|
builder: (context) => StorageSetting())),
|
||||||
),
|
contentPadding:
|
||||||
ListView(
|
EdgeInsets.symmetric(horizontal: 25.0),
|
||||||
shrinkWrap: true,
|
leading: Icon(LineIcons.save),
|
||||||
scrollDirection: Axis.vertical,
|
title: Text('Storage'),
|
||||||
children: <Widget>[
|
subtitle: Text('Manage cache and download storage'),
|
||||||
ListTile(
|
),
|
||||||
contentPadding: EdgeInsets.symmetric(horizontal: 25.0),
|
Divider(height: 2),
|
||||||
leading: Icon(Icons.colorize),
|
ListTile(
|
||||||
title: Text('Changelog'),
|
onTap: () => Navigator.push(
|
||||||
subtitle: Text('List of chagnes'),
|
context,
|
||||||
),
|
MaterialPageRoute(
|
||||||
Divider(height: 2),
|
builder: (context) => PlayedHistory())),
|
||||||
ListTile(
|
contentPadding:
|
||||||
contentPadding: EdgeInsets.symmetric(horizontal: 25.0),
|
EdgeInsets.symmetric(horizontal: 25.0),
|
||||||
leading: Icon(Icons.network_check),
|
leading: Icon(Icons.update),
|
||||||
title: Text('Credit'),
|
title: Text('History'),
|
||||||
subtitle: Text('Open source libraried in application'),
|
subtitle: Text('Listen data'),
|
||||||
),
|
),
|
||||||
Divider(height: 2),
|
Divider(height: 2),
|
||||||
ListTile(
|
],
|
||||||
contentPadding: EdgeInsets.symmetric(horizontal: 25.0),
|
),
|
||||||
leading: Icon(Icons.storage),
|
],
|
||||||
title: Text('Cache'),
|
),
|
||||||
subtitle: Text('Manage and clear cache'),
|
Padding(
|
||||||
),
|
padding: EdgeInsets.all(10.0),
|
||||||
Divider(height: 2),
|
),
|
||||||
ListTile(
|
Column(
|
||||||
contentPadding: EdgeInsets.symmetric(horizontal: 25.0),
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
leading: Icon(Icons.update),
|
mainAxisSize: MainAxisSize.min,
|
||||||
title: Text('Update'),
|
children: <Widget>[
|
||||||
subtitle: Text('Update in background'),
|
Container(
|
||||||
),
|
height: 30.0,
|
||||||
Divider(height: 2),
|
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),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -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),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,7 +7,7 @@ import 'package:tsacdop/class/settingstate.dart';
|
||||||
class ThemeSetting extends StatelessWidget {
|
class ThemeSetting extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var settings = Provider.of<SettingState>(context);
|
var settings = Provider.of<SettingState>(context, listen: false);
|
||||||
return AnnotatedRegion<SystemUiOverlayStyle>(
|
return AnnotatedRegion<SystemUiOverlayStyle>(
|
||||||
value: SystemUiOverlayStyle(
|
value: SystemUiOverlayStyle(
|
||||||
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
|
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
|
||||||
|
|
|
@ -22,7 +22,8 @@ extension Colorize on String {
|
||||||
_c =
|
_c =
|
||||||
Color.fromRGBO((255 - color[0]), 255 - color[1], 255 - color[2], 1.0);
|
Color.fromRGBO((255 - color[0]), 255 - color[1], 255 - color[2], 1.0);
|
||||||
} else {
|
} 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;
|
return _c;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,11 @@ import 'dart:ui';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_downloader/flutter_downloader.dart';
|
import 'package:flutter_downloader/flutter_downloader.dart';
|
||||||
import 'package:google_fonts/google_fonts.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/class/episodebrief.dart';
|
||||||
import 'package:tsacdop/episodes/episodedetail.dart';
|
import 'package:tsacdop/episodes/episodedetail.dart';
|
||||||
import 'package:tsacdop/util/pageroute.dart';
|
import 'package:tsacdop/util/pageroute.dart';
|
||||||
|
@ -24,32 +29,119 @@ class EpisodeGrid extends StatelessWidget {
|
||||||
this.showNumber,
|
this.showNumber,
|
||||||
this.heroTag})
|
this.heroTag})
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
|
Offset offset;
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
double _width = MediaQuery.of(context).size.width;
|
double _width = MediaQuery.of(context).size.width;
|
||||||
return CustomScrollView(
|
|
||||||
physics: const AlwaysScrollableScrollPhysics(),
|
_showPopupMenu(Offset offset, EpisodeBrief episode, BuildContext context,
|
||||||
primary: false,
|
bool isPlaying, bool isInPlaylist) async {
|
||||||
slivers: <Widget>[
|
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
|
||||||
SliverPadding(
|
double left = offset.dx;
|
||||||
padding: const EdgeInsets.all(5.0),
|
double top = offset.dy;
|
||||||
sliver: SliverGrid(
|
await showMenu<int>(
|
||||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
shape: RoundedRectangleBorder(
|
||||||
childAspectRatio: 1.0,
|
borderRadius: BorderRadius.all(Radius.circular(10))),
|
||||||
crossAxisCount: 3,
|
context: context,
|
||||||
mainAxisSpacing: 6.0,
|
position: RelativeRect.fromLTRB(left, top, _width - left, 0),
|
||||||
crossAxisSpacing: 6.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) {
|
PopupMenuItem(
|
||||||
Color _c =
|
value: 1,
|
||||||
(Theme.of(context).brightness == Brightness.light)
|
child: Row(
|
||||||
? podcast[index].primaryColor.colorizedark()
|
children: <Widget>[
|
||||||
: podcast[index].primaryColor.colorizeLight();
|
Icon(
|
||||||
return Material(
|
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,
|
color: Colors.transparent,
|
||||||
child: InkWell(
|
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: () {
|
onTap: () {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
|
@ -61,25 +153,17 @@ class EpisodeGrid extends StatelessWidget {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.all(Radius.circular(5.0)),
|
borderRadius: BorderRadius.all(Radius.circular(5.0)),
|
||||||
color: Theme.of(context).scaffoldBackgroundColor,
|
border: Border.all(
|
||||||
border: Border.all(
|
color:
|
||||||
color:
|
Theme.of(context).brightness == Brightness.light
|
||||||
Theme.of(context).brightness == Brightness.light
|
? Theme.of(context).primaryColor
|
||||||
? Theme.of(context).primaryColor
|
: Theme.of(context).scaffoldBackgroundColor,
|
||||||
: Theme.of(context).scaffoldBackgroundColor,
|
width: 1.0,
|
||||||
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(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
|
@ -94,6 +178,7 @@ class EpisodeGrid extends StatelessWidget {
|
||||||
height: _width / 16,
|
height: _width / 16,
|
||||||
width: _width / 16,
|
width: _width / 16,
|
||||||
child: CircleAvatar(
|
child: CircleAvatar(
|
||||||
|
backgroundColor: _c.withOpacity(0.5),
|
||||||
backgroundImage: FileImage(
|
backgroundImage: FileImage(
|
||||||
File("${podcast[index].imagePath}")),
|
File("${podcast[index].imagePath}")),
|
||||||
),
|
),
|
||||||
|
@ -140,7 +225,6 @@ class EpisodeGrid extends StatelessWidget {
|
||||||
alignment: Alignment.bottomLeft,
|
alignment: Alignment.bottomLeft,
|
||||||
child: Text(
|
child: Text(
|
||||||
podcast[index].dateToString(),
|
podcast[index].dateToString(),
|
||||||
//podcast[index].pubDate.substring(4, 16),
|
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: _width / 35,
|
fontSize: _width / 35,
|
||||||
color: _c,
|
color: _c,
|
||||||
|
@ -175,13 +259,13 @@ class EpisodeGrid extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
},
|
),
|
||||||
childCount: podcast.length,
|
);
|
||||||
),
|
},
|
||||||
),
|
childCount: podcast.length,
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
After Width: | Height: | Size: 122 KiB |
After Width: | Height: | Size: 141 KiB |
After Width: | Height: | Size: 175 KiB |
After Width: | Height: | Size: 216 KiB |
18
pubspec.yaml
|
@ -11,7 +11,7 @@ description: An easy-use podacasts player.
|
||||||
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
||||||
# Read more about iOS versioning at
|
# Read more about iOS versioning at
|
||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
version: 0.1.1
|
version: 0.1.2
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.6.0 <3.0.0"
|
sdk: ">=2.6.0 <3.0.0"
|
||||||
|
@ -28,7 +28,7 @@ dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
json_annotation: ^3.0.1
|
json_annotation: ^3.0.1
|
||||||
sqflite: ^1.2.1
|
sqflite: ^1.2.2+1
|
||||||
flutter_html: ^0.11.1
|
flutter_html: ^0.11.1
|
||||||
path_provider: ^1.6.1
|
path_provider: ^1.6.1
|
||||||
color_thief_flutter: ^1.0.1
|
color_thief_flutter: ^1.0.1
|
||||||
|
@ -38,7 +38,6 @@ dev_dependencies:
|
||||||
file_picker: ^1.4.3+2
|
file_picker: ^1.4.3+2
|
||||||
xml: ^3.5.0
|
xml: ^3.5.0
|
||||||
marquee: ^1.3.1
|
marquee: ^1.3.1
|
||||||
audiofileplayer: ^1.1.1
|
|
||||||
flutter_downloader: ^1.4.1
|
flutter_downloader: ^1.4.1
|
||||||
permission_handler: ^4.3.0
|
permission_handler: ^4.3.0
|
||||||
fluttertoast: ^3.1.3
|
fluttertoast: ^3.1.3
|
||||||
|
@ -49,11 +48,16 @@ dev_dependencies:
|
||||||
uuid: ^2.0.4
|
uuid: ^2.0.4
|
||||||
tuple: ^1.0.3
|
tuple: ^1.0.3
|
||||||
cached_network_image: ^2.0.0
|
cached_network_image: ^2.0.0
|
||||||
workmanager: ^0.2.0
|
workmanager: ^0.2.2
|
||||||
font_awesome_flutter: ^8.7.0
|
|
||||||
flutter_colorpicker: ^0.3.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
|
# For information on the generic Dart part of this file, see the
|
||||||
|
|