Improve sleep timer

This commit is contained in:
stonegate 2020-04-12 01:23:12 +08:00
parent b268728da7
commit f1e49a2833
19 changed files with 2146 additions and 972 deletions

View File

@ -2,7 +2,7 @@ version: 2
jobs:
build:
docker:
- image: cirrusci/flutter:v1.15.17
- image: cirrusci/flutter:beta
branches:
only: master

View File

@ -3,6 +3,7 @@ import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:audio_service/audio_service.dart';
import 'package:just_audio/just_audio.dart';
import 'package:rxdart/rxdart.dart';
import 'package:tsacdop/class/episodebrief.dart';
import 'package:tsacdop/local_storage/key_value_storage.dart';
import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
@ -59,12 +60,11 @@ class PlayHistory {
}
}
class Playlist extends ChangeNotifier {
class Playlist {
String name;
DBHelper dbHelper = DBHelper();
List<EpisodeBrief> _playlist;
//list of miediaitem
List<EpisodeBrief> get playlist => _playlist;
KeyValueStorage storage = KeyValueStorage('playlist');
@ -80,7 +80,6 @@ class Playlist extends ChangeNotifier {
if (episode != null) _playlist.add(episode);
});
}
print('Playlist: ' + _playlist.length.toString());
}
savePlaylist() async {
@ -108,6 +107,8 @@ class Playlist extends ChangeNotifier {
}
}
enum SleepTimerMode { endOfEpisode, timer, undefined }
class AudioPlayerNotifier extends ChangeNotifier {
DBHelper dbHelper = DBHelper();
KeyValueStorage storage = KeyValueStorage('audioposition');
@ -128,6 +129,7 @@ class AudioPlayerNotifier extends ChangeNotifier {
int _timeLeft = 0;
bool _startSleepTimer = false;
double _switchValue = 0;
SleepTimerMode _sleepTimerMode = SleepTimerMode.undefined;
bool _autoPlay = true;
DateTime _current;
int _currentPosition;
@ -145,6 +147,7 @@ class AudioPlayerNotifier extends ChangeNotifier {
EpisodeBrief get episode => _episode;
bool get stopOnComplete => _stopOnComplete;
bool get startSleepTimer => _startSleepTimer;
SleepTimerMode get sleepTimerMode => _sleepTimerMode;
bool get autoPlay => _autoPlay;
int get timeLeft => _timeLeft;
double get switchValue => _switchValue;
@ -159,6 +162,11 @@ class AudioPlayerNotifier extends ChangeNotifier {
notifyListeners();
}
set setSleepTimerMode(SleepTimerMode timer) {
_sleepTimerMode = timer;
notifyListeners();
}
@override
void addListener(VoidCallback listener) async {
super.addListener(listener);
@ -173,7 +181,7 @@ class AudioPlayerNotifier extends ChangeNotifier {
_lastPostion = await storage.getInt();
if (_lastPostion > 0 && _queue.playlist.length > 0) {
final EpisodeBrief episode = _queue.playlist.first;
final int duration = episode.enclosureLength * 60;
final int duration = episode.duration * 1000;
final double seekValue = duration != 0 ? _lastPostion / duration : 1;
final PlayHistory history = PlayHistory(
episode.title, episode.enclosureUrl, _lastPostion / 1000, seekValue);
@ -196,22 +204,25 @@ class AudioPlayerNotifier extends ChangeNotifier {
await _queue.savePlaylist();
} else {
await _queue.getPlaylist();
_queue.playlist
.removeWhere((item) => item.enclosureUrl == episode.enclosureUrl);
_queue.playlist.insert(0, episodeNew);
_queue.savePlaylist();
// _queue.playlist
// .removeWhere((item) => item.enclosureUrl == episode.enclosureUrl);
await _queue.delFromPlaylist(episode);
await _queue.addToPlayListAt(episodeNew, 0);
_backgroundAudioDuration = 0;
_backgroundAudioPosition = 0;
_seekSliderValue = 0;
_episode = episodeNew;
_playerRunning = true;
_audioState = BasicPlaybackState.connecting;
notifyListeners();
await _queue.savePlaylist();
//await _queue.savePlaylist();
_startAudioService(0);
}
}
_startAudioService(int position) async {
_stopOnComplete = false;
_sleepTimerMode = SleepTimerMode.undefined;
if (!AudioService.connected) {
await AudioService.connect();
}
@ -223,7 +234,7 @@ class AudioPlayerNotifier extends ChangeNotifier {
enableQueue: true,
androidStopOnRemoveTask: true,
androidStopForegroundOnPause: true);
_playerRunning = true;
if (_autoPlay) {
await Future.forEach(_queue.playlist, (episode) async {
await AudioService.addQueueItem(episode.toMediaItem());
@ -231,33 +242,64 @@ class AudioPlayerNotifier extends ChangeNotifier {
} else {
await AudioService.addQueueItem(_queue.playlist.first.toMediaItem());
}
_playerRunning = true;
await AudioService.play();
AudioService.currentMediaItemStream.listen((item) async {
if (item != null) {
_episode = await dbHelper.getRssItemWithMediaId(item.id);
_backgroundAudioDuration = item?.duration ?? 0;
if (position > 0 && _backgroundAudioDuration > 0) {
AudioService.seekTo(position);
position = 0;
}
AudioService.currentMediaItemStream
.where((event) => event != null)
.listen((item) async {
_episode = await dbHelper.getRssItemWithMediaId(item.id);
_backgroundAudioDuration = item?.duration ?? 0;
if (position > 0 && _backgroundAudioDuration > 0) {
AudioService.seekTo(position);
position = 0;
}
notifyListeners();
});
var queueSubject = BehaviorSubject<List<MediaItem>>();
queueSubject.addStream(
AudioService.queueStream.distinct().where((event) => event != null));
queueSubject.stream.listen((event) {
print(event.length);
if (event.length == _queue.playlist.length - 1 &&
_audioState == BasicPlaybackState.skippingToNext) {
if (event.length == 0 || _stopOnComplete == true) {
_queue.delFromPlaylist(_episode);
_lastPostion = 0;
storage.saveInt(_lastPostion);
final PlayHistory history = PlayHistory(
_episode.title,
_episode.enclosureUrl,
backgroundAudioPosition / 1000,
seekSliderValue);
dbHelper.saveHistory(history);
} else if (event.first.id != _episode.mediaId) {
_queue.delFromPlaylist(_episode);
final PlayHistory history = PlayHistory(
_episode.title,
_episode.enclosureUrl,
backgroundAudioPosition / 1000,
seekSliderValue);
dbHelper.saveHistory(history);
}
}
});
AudioService.playbackStateStream.listen((event) async {
_current = DateTime.now();
_audioState = event?.basicState;
if (_audioState == BasicPlaybackState.skippingToNext &&
_episode != null) {
print(_episode.title);
_queue.delFromPlaylist(_episode);
}
if (_audioState == BasicPlaybackState.skippingToNext &&
_episode != null &&
_backgroundAudioPosition > 0) {
PlayHistory history = PlayHistory(_episode.title, _episode.enclosureUrl,
_backgroundAudioPosition / 1000, _seekSliderValue);
await dbHelper.saveHistory(history);
}
// if (_audioState == BasicPlaybackState.skippingToNext &&
// _episode != null) {
// print(_episode.title);
// _queue.delFromPlaylist(_episode);
// }
// if (_audioState == BasicPlaybackState.skippingToNext &&
// _episode != null &&
// _backgroundAudioPosition > 0) {
// PlayHistory history = PlayHistory(_episode.title, _episode.enclosureUrl,
// _backgroundAudioPosition / 1000, _seekSliderValue);
// await dbHelper.saveHistory(history);
// }
if (_audioState == BasicPlaybackState.stopped) _playerRunning = false;
if (_audioState == BasicPlaybackState.error) {
@ -296,23 +338,23 @@ class AudioPlayerNotifier extends ChangeNotifier {
storage.saveInt(_lastPostion);
}
if ((_queue.playlist.length == 1 || !_autoPlay) &&
_seekSliderValue > 0.9 &&
_episode != null) {
_queue.delFromPlaylist(_episode);
_lastPostion = 0;
storage.saveInt(_lastPostion);
final PlayHistory history = PlayHistory(
_episode.title,
_episode.enclosureUrl,
backgroundAudioPosition / 1000,
seekSliderValue);
dbHelper.saveHistory(history);
}
// if ((_queue.playlist.length == 1 || !_autoPlay) &&
// _seekSli;lderValue > 0.9 &&
// _episode != null &&
// _audioState != BasicPlaybackState.connecting) {
// _queue.delFromPlaylist(_episode);
// _lastPostion = 0;
// storage.saveInt(_lastPostion);
// final PlayHistory history = PlayHistory(
// _episode.title,
// _episode.enclosureUrl,
// backgroundAudioPosition / 1000,
// seekSliderValue);
// dbHelper.saveHistory(history);
// }
notifyListeners();
}
if (_audioState == BasicPlaybackState.stopped ||
_playerRunning == false) {
if (_audioState == BasicPlaybackState.stopped) {
timer.cancel();
}
});
@ -325,6 +367,7 @@ class AudioPlayerNotifier extends ChangeNotifier {
_seekSliderValue = 0;
_episode = _queue.playlist.first;
_playerRunning = true;
_audioState = BasicPlaybackState.connecting;
_queueUpdate = !_queueUpdate;
notifyListeners();
_startAudioService(_lastPostion ?? 0);
@ -338,7 +381,6 @@ class AudioPlayerNotifier extends ChangeNotifier {
if (_playerRunning) {
await AudioService.addQueueItem(episode.toMediaItem());
}
print('add to playlist when not rnnning');
await _queue.addToPlayList(episode);
notifyListeners();
}
@ -347,7 +389,6 @@ class AudioPlayerNotifier extends ChangeNotifier {
if (_playerRunning) {
await AudioService.addQueueItemAt(episode.toMediaItem(), index);
}
print('add to playlist when not rnnning');
await _queue.addToPlayListAt(episode, index);
_queueUpdate = !_queueUpdate;
notifyListeners();
@ -421,43 +462,60 @@ class AudioPlayerNotifier extends ChangeNotifier {
//Set sleep timer
sleepTimer(int mins) {
_startSleepTimer = true;
_switchValue = 1;
notifyListeners();
_timeLeft = mins * 60;
Timer.periodic(Duration(seconds: 1), (timer) {
if (_timeLeft == 0) {
timer.cancel();
notifyListeners();
} else {
_timeLeft = _timeLeft - 1;
notifyListeners();
}
});
_stopTimer = Timer(Duration(minutes: mins), () {
_stopOnComplete = false;
_startSleepTimer = false;
_switchValue = 0;
_playerRunning = false;
if (_sleepTimerMode == SleepTimerMode.timer) {
_startSleepTimer = true;
_switchValue = 1;
notifyListeners();
AudioService.stop();
AudioService.disconnect();
});
_timeLeft = mins * 60;
Timer.periodic(Duration(seconds: 1), (timer) {
if (_timeLeft == 0) {
timer.cancel();
notifyListeners();
} else {
_timeLeft = _timeLeft - 1;
notifyListeners();
}
});
_stopTimer = Timer(Duration(minutes: mins), () {
_stopOnComplete = false;
_startSleepTimer = false;
_switchValue = 0;
_playerRunning = false;
notifyListeners();
AudioService.stop();
AudioService.disconnect();
});
} else if (_sleepTimerMode == SleepTimerMode.endOfEpisode) {
_stopOnComplete = true;
_switchValue = 1;
notifyListeners();
if (_queue.playlist.length > 1 && _autoPlay) {
AudioService.customAction('stopAtEnd');
}
}
}
//Cancel sleep timer
cancelTimer() {
_stopTimer.cancel();
_timeLeft = 0;
_startSleepTimer = false;
_switchValue = 0;
notifyListeners();
if (_sleepTimerMode == SleepTimerMode.timer) {
_stopTimer.cancel();
_timeLeft = 0;
_startSleepTimer = false;
_switchValue = 0;
notifyListeners();
} else if (_sleepTimerMode == SleepTimerMode.endOfEpisode) {
AudioService.customAction('cancelStopAtEnd');
_switchValue = 0;
_stopOnComplete = false;
notifyListeners();
}
}
@override
void dispose() async {
await AudioService.stop();
await AudioService.disconnect();
//_playerRunning = false;
super.dispose();
}
}
@ -468,6 +526,7 @@ class AudioPlayerTask extends BackgroundAudioTask {
Completer _completer = Completer();
BasicPlaybackState _skipState;
bool _playing;
bool _stopAtEnd;
bool get hasNext => _queue.length > 0;
@ -494,20 +553,19 @@ class AudioPlayerTask extends BackgroundAudioTask {
@override
Future<void> onStart() async {
print('start background task');
_stopAtEnd = false;
var playerStateSubscription = _audioPlayer.playbackStateStream
.where((state) => state == AudioPlaybackState.completed)
.listen((state) {
_handlePlaybackCompleted();
});
var eventSubscription = _audioPlayer.playbackEventStream.listen((event) {
print('buffer position' + event.bufferedPosition.toString());
if (event.playbackError != null) {
_setState(state: BasicPlaybackState.error);
}
BasicPlaybackState state;
if (event.buffering) {
state = BasicPlaybackState.buffering;
state = _skipState ?? BasicPlaybackState.buffering;
} else {
state = _stateToBasicState(event.state);
}
@ -523,14 +581,13 @@ class AudioPlayerTask extends BackgroundAudioTask {
eventSubscription.cancel();
}
void _handlePlaybackCompleted() {
void _handlePlaybackCompleted() async {
if (hasNext) {
onSkipToNext();
} else {
_skipState = BasicPlaybackState.skippingToNext;
_audioPlayer.stop();
_queue.removeAt(0);
_skipState = null;
await AudioServiceBackground.setQueue(_queue);
onStop();
}
}
@ -544,32 +601,28 @@ class AudioPlayerTask extends BackgroundAudioTask {
@override
Future<void> onSkipToNext() async {
if (_playing == null) {
// First time, we want to start playing
_playing = true;
} else {
// Stop current item
await _audioPlayer.stop();
_queue.removeAt(0);
}
if (_queue.length == 0) {
_skipState = BasicPlaybackState.skippingToNext;
await _audioPlayer.stop();
_queue.removeAt(0);
await AudioServiceBackground.setQueue(_queue);
// }
if (_queue.length == 0 || _stopAtEnd) {
_skipState = null;
onStop();
} else {
AudioServiceBackground.setQueue(_queue);
// AudioServiceBackground.setQueue(_queue);
AudioServiceBackground.setMediaItem(mediaItem);
_skipState = BasicPlaybackState.skippingToNext;
await _audioPlayer.setUrl(mediaItem.id);
print(mediaItem.id);
Duration duration = await _audioPlayer.durationFuture ?? Duration.zero;
AudioServiceBackground.setMediaItem(
mediaItem.copyWith(duration: duration.inMilliseconds));
_skipState = null;
// Resume playback if we were playing
if (_playing) {
onPlay();
} else {
_setState(state: BasicPlaybackState.paused);
}
// if (_playing) {
onPlay();
// } else {
// _setState(state: BasicPlaybackState.paused);
// }
}
}
@ -578,7 +631,7 @@ class AudioPlayerTask extends BackgroundAudioTask {
if (_skipState == null) {
if (_playing == null) {
_playing = true;
AudioServiceBackground.setQueue(_queue);
// await AudioServiceBackground.setQueue(_queue);
await _audioPlayer.setUrl(mediaItem.id);
Duration duration = await _audioPlayer.durationFuture;
AudioServiceBackground.setMediaItem(
@ -618,7 +671,7 @@ class AudioPlayerTask extends BackgroundAudioTask {
@override
void onAddQueueItem(MediaItem mediaItem) async {
_queue.add(mediaItem);
AudioServiceBackground.setQueue(_queue);
await AudioServiceBackground.setQueue(_queue);
}
@override
@ -633,8 +686,8 @@ class AudioPlayerTask extends BackgroundAudioTask {
await _audioPlayer.stop();
_queue.removeWhere((item) => item.id == mediaItem.id);
_queue.insert(0, mediaItem);
AudioServiceBackground.setQueue(_queue);
AudioServiceBackground.setMediaItem(mediaItem);
await AudioServiceBackground.setQueue(_queue);
await AudioServiceBackground.setMediaItem(mediaItem);
await _audioPlayer.setUrl(mediaItem.id);
Duration duration = await _audioPlayer.durationFuture ?? Duration.zero;
AudioServiceBackground.setMediaItem(
@ -642,7 +695,7 @@ class AudioPlayerTask extends BackgroundAudioTask {
onPlay();
} else {
_queue.insert(index, mediaItem);
AudioServiceBackground.setQueue(_queue);
await AudioServiceBackground.setQueue(_queue);
}
}
@ -684,9 +737,11 @@ class AudioPlayerTask extends BackgroundAudioTask {
@override
void onCustomAction(funtion, argument) {
switch (funtion) {
case 'addQueue':
case 'stopAtEnd':
_stopAtEnd = true;
break;
case 'updateMedia':
case 'cancelStopAtEnd':
_stopAtEnd = false;
break;
}
}

View File

@ -90,13 +90,16 @@ class DownloadState extends ChangeNotifier {
Future _saveMediaId(EpisodeTask episodeTask) async {
episodeTask.status = DownloadTaskStatus.complete;
final completeTask = await FlutterDownloader.loadTasksWithRawQuery(
query:
"SELECT * FROM task WHERE task_id = '${episodeTask.taskId}'");
query: "SELECT * FROM task WHERE task_id = '${episodeTask.taskId}'");
String filePath = 'file://' +
path.join(completeTask.first.savedDir, completeTask.first.filename);
print(filePath);
dbHelper.saveMediaId(
episodeTask.episode.enclosureUrl, filePath, episodeTask.taskId);
EpisodeBrief episode =
await dbHelper.getRssItemWithUrl(episodeTask.episode.enclosureUrl);
_removeTask(episodeTask.episode);
_episodeTasks.add(EpisodeTask(episode, episodeTask.taskId,
progress: 100, status: DownloadTaskStatus.complete));
}
void _unbindBackgroundIsolate() {

View File

@ -125,7 +125,7 @@ class SettingState extends ChangeNotifier {
int color = int.parse('FF' + colorString.toUpperCase(), radix: 16);
_accentSetColor = Color(color).withOpacity(1.0);
} else {
_accentSetColor = Color.fromRGBO(35, 204, 198, 1);
_accentSetColor = Colors.teal[500];
}
}

View File

@ -1,5 +1,4 @@
import 'dart:io';
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@ -17,6 +16,7 @@ import 'package:tsacdop/class/audiostate.dart';
import 'package:tsacdop/class/episodebrief.dart';
import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
import 'package:tsacdop/util/context_extension.dart';
import 'package:tsacdop/util/custompaint.dart';
import 'episodedownload.dart';
class EpisodeDetail extends StatefulWidget {
@ -163,7 +163,7 @@ class _EpisodeDetailState extends State<EpisodeDetail> {
horizontal: 10.0),
alignment: Alignment.center,
child: Text(
(widget.episodeItem.duration)
(widget.episodeItem.duration ~/ 60)
.toString() +
'mins',
style: textstyle),
@ -253,10 +253,15 @@ class _EpisodeDetailState extends State<EpisodeDetail> {
'assets/shownote.png'),
height: 100.0,
),
Padding(padding: EdgeInsets.all(5.0)),
Padding(
padding: EdgeInsets.all(5.0)),
Text(
'Still no shownote received\n for this episode.', textAlign: TextAlign.center,
style: TextStyle(color: context.textTheme.bodyText1.color.withOpacity(0.5))),
'Still no shownote received\n for this episode.',
textAlign: TextAlign.center,
style: TextStyle(
color: context.textTheme
.bodyText1.color
.withOpacity(0.5))),
],
),
)
@ -313,7 +318,11 @@ class MenuBar extends StatefulWidget {
class _MenuBarState extends State<MenuBar> {
bool _liked;
int _like;
Future<PlayHistory> getPosition(EpisodeBrief episode) async {
var dbHelper = DBHelper();
return await dbHelper.getPosition(episode);
}
Future<int> saveLiked(String url) async {
var dbHelper = DBHelper();
@ -328,16 +337,25 @@ class _MenuBarState extends State<MenuBar> {
if (result == 1 && mounted)
setState(() {
_liked = false;
_like = 0;
// _like = 0;
});
return result;
}
Future<bool> _isLiked(EpisodeBrief episode) async {
DBHelper dbHelper = DBHelper();
return await dbHelper.isLiked(episode.enclosureUrl);
}
static String _stringForSeconds(double seconds) {
if (seconds == null) return null;
return '${(seconds ~/ 60)}:${(seconds.truncate() % 60).toString().padLeft(2, '0')}';
}
@override
void initState() {
super.initState();
_liked = false;
_like = widget.episodeItem.liked;
}
Widget _buttonOnMenu(Widget widget, VoidCallback onTap) => Material(
@ -384,32 +402,39 @@ class _MenuBarState extends State<MenuBar> {
),
),
),
(_like == 0 && !_liked)
? _buttonOnMenu(
Icon(
Icons.favorite_border,
color: Colors.grey[700],
),
() => saveLiked(widget.episodeItem.enclosureUrl))
: (_like == 1 && !_liked)
FutureBuilder<bool>(
future: _isLiked(widget.episodeItem),
initialData: false,
builder: (BuildContext context, AsyncSnapshot snapshot) {
return (!snapshot.data && !_liked)
? _buttonOnMenu(
Icon(
Icons.favorite,
color: Colors.red,
Icons.favorite_border,
color: Colors.grey[700],
),
() => setUnliked(widget.episodeItem.enclosureUrl))
: Stack(
alignment: Alignment.center,
children: <Widget>[
LoveOpen(),
_buttonOnMenu(
Icon(
Icons.favorite,
color: Colors.red,
),
() => setUnliked(widget.episodeItem.enclosureUrl)),
],
),
() => saveLiked(widget.episodeItem.enclosureUrl))
: (snapshot.data && !_liked)
? _buttonOnMenu(
Icon(
Icons.favorite,
color: Colors.red,
),
() => setUnliked(widget.episodeItem.enclosureUrl))
: Stack(
alignment: Alignment.center,
children: <Widget>[
LoveOpen(),
_buttonOnMenu(
Icon(
Icons.favorite,
color: Colors.red,
),
() => setUnliked(
widget.episodeItem.enclosureUrl)),
],
);
},
),
DownloadButton(episode: widget.episodeItem),
Selector<AudioPlayerNotifier, List<String>>(
selector: (_, audio) =>
@ -418,8 +443,13 @@ class _MenuBarState extends State<MenuBar> {
return data.contains(widget.episodeItem.enclosureUrl)
? _buttonOnMenu(
Icon(Icons.playlist_add_check,
color: Theme.of(context).accentColor),
() {})
color: Theme.of(context).accentColor), () {
audio.delFromPlaylist(widget.episodeItem);
Fluttertoast.showToast(
msg: 'Removed from playlist',
gravity: ToastGravity.BOTTOM,
);
})
: _buttonOnMenu(
Icon(Icons.playlist_add, color: Colors.grey[700]), () {
Fluttertoast.showToast(
@ -430,8 +460,61 @@ class _MenuBarState extends State<MenuBar> {
});
},
),
FutureBuilder<PlayHistory>(
future: getPosition(widget.episodeItem),
builder: (context, snapshot) {
if (snapshot.hasError) print(snapshot.error);
return snapshot.hasData
? snapshot.data.seekValue > 0.95
? Container(
height: 20,
padding: EdgeInsets.symmetric(horizontal:15),
child: SizedBox(
width: 20,
height: 20,
child: CustomPaint(
painter: ListenedPainter(context.accentColor,
stroke: 2.0),
),
),
)
: snapshot.data.seconds < 0.1
? Center()
: Container(
height: 50,
padding: EdgeInsets.symmetric(horizontal:15),
child: Row(
children: <Widget>[
SizedBox(
width: 20,
height: 20,
child: CustomPaint(
painter: ListenedPainter(
context.accentColor,
stroke: 2.0),
),
),
Padding(padding: EdgeInsets.symmetric(horizontal:2)),
Container(
height: 20,
padding:
EdgeInsets.symmetric(horizontal: 2),
decoration: BoxDecoration(
borderRadius: BorderRadius.all(
Radius.circular(10.0)),
color: context.accentColor,
),
child: Text(
_stringForSeconds(snapshot.data.seconds),
style: TextStyle(color: Colors.white),
),
),
],
),
)
: Center();
}),
Spacer(),
// Text(audio.audioState.toString()),
Selector<AudioPlayerNotifier,
Tuple2<EpisodeBrief, BasicPlaybackState>>(
selector: (_, audio) => Tuple2(audio.episode, audio.audioState),
@ -464,20 +547,12 @@ class _MenuBarState extends State<MenuBar> {
),
),
)
: (widget.episodeItem.title == data.item1?.title &&
data.item2 == BasicPlaybackState.playing)
? Container(
padding: EdgeInsets.only(right: 30),
child: SizedBox(
width: 20, height: 15, child: WaveLoader()))
: Container(
padding: EdgeInsets.only(right: 30),
child: SizedBox(
width: 20,
height: 15,
child: LineLoader(),
),
);
: Container(
padding: EdgeInsets.only(right: 30),
child: SizedBox(
width: 20,
height: 15,
child: WaveLoader(color: context.accentColor)));
},
),
],
@ -485,351 +560,3 @@ class _MenuBarState extends State<MenuBar> {
);
}
}
class LinePainter extends CustomPainter {
double _fraction;
Paint _paint;
Color _maincolor;
LinePainter(this._fraction, this._maincolor) {
_paint = Paint()
..color = _maincolor
..strokeWidth = 2.0
..strokeCap = StrokeCap.round;
}
@override
void paint(Canvas canvas, Size size) {
canvas.drawLine(Offset(0, size.height / 2.0),
Offset(size.width * _fraction, size.height / 2.0), _paint);
}
@override
bool shouldRepaint(LinePainter oldDelegate) {
return oldDelegate._fraction != _fraction;
}
}
class LineLoader extends StatefulWidget {
@override
_LineLoaderState createState() => _LineLoaderState();
}
class _LineLoaderState extends State<LineLoader>
with SingleTickerProviderStateMixin {
double _fraction = 0.0;
Animation animation;
AnimationController controller;
@override
void initState() {
super.initState();
controller =
AnimationController(vsync: this, duration: Duration(milliseconds: 500));
animation = Tween(begin: 0.0, end: 1.0).animate(controller)
..addListener(() {
if (mounted)
setState(() {
_fraction = animation.value;
});
});
controller.forward();
controller.addStatusListener((status) {
if (status == AnimationStatus.completed) {
controller.reset();
} else if (status == AnimationStatus.dismissed) {
controller.forward();
}
});
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: LinePainter(_fraction, Theme.of(context).accentColor));
}
}
class WavePainter extends CustomPainter {
double _fraction;
double _value;
Color _color;
WavePainter(this._fraction, this._color);
@override
void paint(Canvas canvas, Size size) {
if (_fraction < 0.5) {
_value = _fraction;
} else {
_value = 1 - _fraction;
}
Path _path = Path();
Paint _paint = Paint()
..color = _color
..strokeWidth = 2.0
..strokeCap = StrokeCap.round
..style = PaintingStyle.stroke;
_path.moveTo(0, size.height / 2);
_path.lineTo(0, size.height / 2 + size.height * _value * 0.2);
_path.moveTo(0, size.height / 2);
_path.lineTo(0, size.height / 2 - size.height * _value * 0.2);
_path.moveTo(size.width / 4, size.height / 2);
_path.lineTo(size.width / 4, size.height / 2 + size.height * _value * 0.8);
_path.moveTo(size.width / 4, size.height / 2);
_path.lineTo(size.width / 4, size.height / 2 - size.height * _value * 0.8);
_path.moveTo(size.width / 2, size.height / 2);
_path.lineTo(size.width / 2, size.height / 2 + size.height * _value * 0.5);
_path.moveTo(size.width / 2, size.height / 2);
_path.lineTo(size.width / 2, size.height / 2 - size.height * _value * 0.5);
_path.moveTo(size.width * 3 / 4, size.height / 2);
_path.lineTo(
size.width * 3 / 4, size.height / 2 + size.height * _value * 0.6);
_path.moveTo(size.width * 3 / 4, size.height / 2);
_path.lineTo(
size.width * 3 / 4, size.height / 2 - size.height * _value * 0.6);
_path.moveTo(size.width, size.height / 2);
_path.lineTo(size.width, size.height / 2 + size.height * _value * 0.2);
_path.moveTo(size.width, size.height / 2);
_path.lineTo(size.width, size.height / 2 - size.height * _value * 0.2);
canvas.drawPath(_path, _paint);
}
@override
bool shouldRepaint(WavePainter oldDelegate) {
return oldDelegate._fraction != _fraction;
}
}
class WaveLoader extends StatefulWidget {
@override
_WaveLoaderState createState() => _WaveLoaderState();
}
class _WaveLoaderState extends State<WaveLoader>
with SingleTickerProviderStateMixin {
double _fraction = 0.0;
Animation animation;
AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this, duration: Duration(milliseconds: 1000));
animation = Tween(begin: 0.0, end: 1.0).animate(_controller)
..addListener(() {
if (mounted)
setState(() {
_fraction = animation.value;
});
});
_controller.forward();
_controller.addStatusListener((status) {
if (status == AnimationStatus.completed) {
_controller.reset();
} else if (status == AnimationStatus.dismissed) {
_controller.forward();
}
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: WavePainter(_fraction, Theme.of(context).accentColor));
}
}
class ImageRotate extends StatefulWidget {
final String title;
final String path;
ImageRotate({this.title, this.path, Key key}) : super(key: key);
@override
_ImageRotateState createState() => _ImageRotateState();
}
class _ImageRotateState extends State<ImageRotate>
with SingleTickerProviderStateMixin {
Animation _animation;
AnimationController _controller;
double _value;
@override
void initState() {
super.initState();
_value = 0;
_controller = AnimationController(
vsync: this,
duration: Duration(milliseconds: 2000),
);
_animation = Tween(begin: 0.0, end: 1.0).animate(_controller)
..addListener(() {
if (mounted)
setState(() {
_value = _animation.value;
});
});
_controller.forward();
_controller.addStatusListener((status) {
if (status == AnimationStatus.completed) {
_controller.reset();
} else if (status == AnimationStatus.dismissed) {
_controller.forward();
}
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Transform.rotate(
angle: 2 * math.pi * _value,
child: Container(
padding: EdgeInsets.all(10.0),
child: ClipRRect(
borderRadius: BorderRadius.all(Radius.circular(15.0)),
child: Container(
height: 30.0,
width: 30.0,
color: Colors.white,
child: Image.file(File("${widget.path}")),
),
),
),
);
}
}
class LovePainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
Path _path = Path();
Paint _paint = Paint()
..color = Colors.red
..strokeWidth = 2.0
..strokeCap = StrokeCap.round;
_path.moveTo(size.width / 2, size.height / 6);
_path.quadraticBezierTo(size.width / 4, 0, size.width / 8, size.height / 6);
_path.quadraticBezierTo(
0, size.height / 3, size.width / 8, size.height * 0.55);
_path.quadraticBezierTo(
size.width / 4, size.height * 0.8, size.width / 2, size.height);
_path.quadraticBezierTo(size.width * 0.75, size.height * 0.8,
size.width * 7 / 8, size.height * 0.55);
_path.quadraticBezierTo(
size.width, size.height / 3, size.width * 7 / 8, size.height / 6);
_path.quadraticBezierTo(
size.width * 3 / 4, 0, size.width / 2, size.height / 6);
canvas.drawPath(_path, _paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
class LoveOpen extends StatefulWidget {
@override
_LoveOpenState createState() => _LoveOpenState();
}
class _LoveOpenState extends State<LoveOpen>
with SingleTickerProviderStateMixin {
Animation _animationA;
AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: Duration(milliseconds: 300),
);
_animationA = Tween(begin: 0.0, end: 1.0).animate(_controller)
..addListener(() {
if (mounted) setState(() {});
});
_controller.forward();
_controller.addStatusListener((status) {
if (status == AnimationStatus.completed) {
_controller.reset();
}
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
Widget _littleHeart(double scale, double value, double angle) => Container(
alignment: Alignment.centerLeft,
padding: EdgeInsets.only(left: value),
child: ScaleTransition(
scale: _animationA,
alignment: Alignment.center,
child: Transform.rotate(
angle: angle,
child: SizedBox(
height: 5 * scale,
width: 6 * scale,
child: CustomPaint(
painter: LovePainter(),
),
),
),
),
);
@override
Widget build(BuildContext context) {
return Container(
width: 50,
height: 50,
alignment: Alignment.center,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Row(
children: <Widget>[
_littleHeart(0.5, 10, -math.pi / 6),
_littleHeart(1.2, 3, 0),
],
),
Row(
children: <Widget>[
_littleHeart(0.8, 6, math.pi * 1.5),
_littleHeart(0.9, 24, math.pi / 2),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
_littleHeart(1, 8, -math.pi * 0.7),
_littleHeart(0.8, 8, math.pi),
_littleHeart(0.6, 3, -math.pi * 1.2)
],
),
],
),
);
}
}

View File

@ -1,5 +1,4 @@
import 'dart:io';
import 'dart:ui' as ui;
import 'dart:math' as math;
import 'package:flutter/material.dart';
@ -17,7 +16,9 @@ import 'package:tsacdop/episodes/episodedetail.dart';
import 'package:tsacdop/home/audiopanel.dart';
import 'package:tsacdop/util/pageroute.dart';
import 'package:tsacdop/util/colorize.dart';
import 'package:tsacdop/util/context_extension.dart';
import 'package:tsacdop/util/day_night_switch.dart';
import 'package:tsacdop/util/custompaint.dart';
class MyRectangularTrackShape extends RectangularSliderTrackShape {
Rect getPreferredRect({
@ -85,18 +86,6 @@ class MyRoundSliderThumpShape extends SliderComponentShape {
..strokeWidth = 2,
);
// Path _path = Path();
// _path.moveTo(center.dx - 12, center.dy + 10);
// _path.lineTo(center.dx - 12, center.dy - 12);
// _path.lineTo(center.dx -12, center.dy - 12);
// canvas.drawShadow(_path, Colors.black, 4, false);
// Path _pathLight = Path();
// _pathLight.moveTo(center.dx + 12, center.dy - 12);
// _pathLight.lineTo(center.dx + 12, center.dy + 10);
//// _pathLight.lineTo(center.dx - 12, center.dy + 10);
// canvas.drawShadow(_pathLight, Colors.black, 4, true);
canvas.drawRect(
Rect.fromLTRB(
center.dx - 10, center.dy + 10, center.dx + 10, center.dy - 10),
@ -117,33 +106,33 @@ class MyRoundSliderThumpShape extends SliderComponentShape {
}
}
final List<BoxShadow> _customShadow = [
BoxShadow(blurRadius: 26, offset: Offset(-6, -6), color: Colors.white),
BoxShadow(
blurRadius: 8,
offset: Offset(2, 2),
color: Colors.grey[600].withOpacity(0.4))
];
final List<BoxShadow> _customShadowNight = [
BoxShadow(
blurRadius: 6,
offset: Offset(-1, -1),
color: Colors.grey[100].withOpacity(0.3)),
BoxShadow(blurRadius: 8, offset: Offset(2, 2), color: Colors.black)
];
String _stringForSeconds(double seconds) {
if (seconds == null) return null;
return '${(seconds ~/ 60)}:${(seconds.truncate() % 60).toString().padLeft(2, '0')}';
}
class PlayerWidget extends StatefulWidget {
@override
_PlayerWidgetState createState() => _PlayerWidgetState();
}
class _PlayerWidgetState extends State<PlayerWidget> {
static String _stringForSeconds(double seconds) {
if (seconds == null) return null;
return '${(seconds ~/ 60)}:${(seconds.truncate() % 60).toString().padLeft(2, '0')}';
}
List<BoxShadow> _customShadow = [
BoxShadow(blurRadius: 26, offset: Offset(-6, -6), color: Colors.white),
BoxShadow(
blurRadius: 8,
offset: Offset(2, 2),
color: Colors.grey[600].withOpacity(0.4))
];
List<BoxShadow> _customShadowNight = [
BoxShadow(
blurRadius: 6,
offset: Offset(-1, -1),
color: Colors.grey[100].withOpacity(0.3)),
BoxShadow(blurRadius: 8, offset: Offset(2, 2), color: Colors.black)
];
List minsToSelect = [10, 15, 20, 25, 30, 45, 60, 70, 80, 90, 99];
int _minSelected;
final GlobalKey<AnimatedListState> miniPlaylistKey = GlobalKey();
@ -243,7 +232,9 @@ class _PlayerWidgetState extends State<PlayerWidget> {
moonColor: Colors.grey[600],
dayColor: Theme.of(context).primaryColorDark,
nightColor: Colors.black,
onDrag: (value) => audio.setSwitchValue = value,
onDrag: (value) {
audio.setSwitchValue = value;
},
onChanged: (value) {
if (value) {
audio.sleepTimer(_minSelected);
@ -380,7 +371,10 @@ class _PlayerWidgetState extends State<PlayerWidget> {
BasicPlaybackState
.buffering ||
data.audioState ==
BasicPlaybackState.connecting
BasicPlaybackState
.connecting ||
data.audioState ==
BasicPlaybackState.none
? 'Buffring...'
: '',
style: TextStyle(
@ -618,7 +612,7 @@ class _PlayerWidgetState extends State<PlayerWidget> {
// color: context.primaryColorDark,
alignment: Alignment.centerLeft,
child: Text(
'Playlist',
'Queue',
style: TextStyle(
color: Theme.of(context).accentColor,
fontWeight: FontWeight.bold,
@ -654,8 +648,6 @@ class _PlayerWidgetState extends State<PlayerWidget> {
audio.playNext();
miniPlaylistKey.currentState.removeItem(
0, (context, animation) => Container());
miniPlaylistKey.currentState.removeItem(
1, (context, animation) => Container());
miniPlaylistKey.currentState.insertItem(0);
},
child: SizedBox(
@ -689,8 +681,7 @@ class _PlayerWidgetState extends State<PlayerWidget> {
Navigator.push(
context,
SlideLeftRoute(page: PlaylistPage()),
)..then((value) =>
miniPlaylistKey.currentState.initState());
);
},
child: SizedBox(
height: 30.0,
@ -723,7 +714,7 @@ class _PlayerWidgetState extends State<PlayerWidget> {
itemBuilder: (context, index, animation) => ScaleTransition(
alignment: Alignment.center,
scale: animation,
child: index == 0 || index > data.item1.length -1
child: index == 0 || index > data.item1.length - 1
? Center()
: Column(
children: <Widget>[
@ -858,7 +849,7 @@ class _PlayerWidgetState extends State<PlayerWidget> {
children: <Widget>[
TabBarView(
children: <Widget>[
_sleppMode(context),
SleepMode(),
_controlPanel(context),
_playlist(context),
],
@ -1120,10 +1111,11 @@ class _LastPositionState extends State<LastPosition> {
builder: (context, snapshot) {
if (snapshot.hasError) print(snapshot.error);
return snapshot.hasData
? snapshot.data.seekValue > 0.95
? snapshot.data.seekValue > 0.90
? Container(
height: 20.0,
alignment: Alignment.center,
padding: EdgeInsets.symmetric(horizontal: 5),
decoration: BoxDecoration(
border: Border.all(
width: 1,
@ -1232,63 +1224,6 @@ class _ImageRotateState extends State<ImageRotate>
}
}
class StarSky extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final points = [
Offset(50, 100),
Offset(150, 75),
Offset(250, 250),
Offset(130, 200),
Offset(270, 150),
];
final pisces = [
Offset(9, 4),
Offset(11, 5),
Offset(7, 6),
Offset(10, 7),
Offset(8, 8),
Offset(9, 13),
Offset(12, 17),
Offset(5, 19),
Offset(7, 19)
].map((e) => e * 10).toList();
final orion = [
Offset(3, 1),
Offset(6, 1),
Offset(1, 4),
Offset(2, 4),
Offset(2, 7),
Offset(10, 8),
Offset(3, 10),
Offset(8, 10),
Offset(19, 11),
Offset(11, 13),
Offset(18, 14),
Offset(5, 19),
Offset(7, 19),
Offset(9, 18),
Offset(15, 19),
Offset(16, 18),
Offset(2, 25),
Offset(10, 26)
].map((e) => Offset(e.dx * 10 + 250, e.dy * 10)).toList();
Paint paint = Paint()
..color = Colors.white
..strokeWidth = 2.0
..strokeCap = StrokeCap.round;
canvas.drawPoints(ui.PointMode.points, pisces, paint);
canvas.drawPoints(ui.PointMode.points, points, paint);
canvas.drawPoints(ui.PointMode.points, orion, paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
class Meteor extends CustomPainter {
Paint _paint;
Meteor() {
@ -1365,3 +1300,301 @@ class _MeteorLoaderState extends State<MeteorLoader>
);
}
}
class SleepMode extends StatefulWidget {
SleepMode({Key key}) : super(key: key);
@override
SleepModeState createState() => SleepModeState();
}
class SleepModeState extends State<SleepMode>
with SingleTickerProviderStateMixin {
int _minSelected;
List minsToSelect = [10, 15, 20, 25, 30, 45, 60, 70, 80, 90, 99];
AnimationController _controller;
Animation<double> _animation;
@override
void initState() {
super.initState();
_minSelected = 30;
_controller =
AnimationController(vsync: this, duration: Duration(milliseconds: 400));
_animation = Tween<double>(begin: 0.0, end: 1.0).animate(_controller)
..addListener(() {
Provider.of<AudioPlayerNotifier>(context, listen: false)
.setSwitchValue = _animation.value;
});
_controller.addStatusListener((status) {
if (status == AnimationStatus.completed) {
Provider.of<AudioPlayerNotifier>(context, listen: false)
.sleepTimer(_minSelected);
}
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
List<BoxShadow> customShadow(double scale) => [
BoxShadow(
blurRadius: 26 * (1 - scale),
offset: Offset(-6, -6) * (1 - scale),
color: Colors.white),
BoxShadow(
blurRadius: 8 * (1 - scale),
offset: Offset(2, 2) * (1 - scale),
color: Colors.grey[600].withOpacity(0.4))
];
List<BoxShadow> customShadowNight(double scale) => [
BoxShadow(
blurRadius: 6 * (1 - scale),
offset: Offset(-1, -1) * (1 - scale),
color: Colors.grey[100].withOpacity(0.3)),
BoxShadow(
blurRadius: 8 * (1 - scale),
offset: Offset(2, 2) * (1 - scale),
color: Colors.black)
];
@override
Widget build(BuildContext context) {
final ColorTween _colorTween =
ColorTween(begin: context.primaryColor, end: Colors.black);
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
return Selector<AudioPlayerNotifier, Tuple3<int, double, SleepTimerMode>>(
selector: (_, audio) =>
Tuple3(audio.timeLeft, audio.switchValue, audio.sleepTimerMode),
builder: (_, data, __) {
double fraction = data.item2 < 0.5 ? data.item2 * 2 : 1;
double move = data.item2 > 0.5 ? data.item2 * 2 - 1 : 0;
return Container(
height: 300,
color: _colorTween.transform(move),
child: Stack(
children: <Widget>[
Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Padding(
padding: EdgeInsets.all(5),
),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Padding(
padding: EdgeInsets.symmetric(vertical: 20),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: minsToSelect
.map((e) => InkWell(
onTap: () => setState(() => _minSelected = e),
child: Stack(
alignment: Alignment.center,
children: <Widget>[
Container(
margin: EdgeInsets.symmetric(
horizontal: 10.0),
decoration: BoxDecoration(
boxShadow: !(e == _minSelected ||
fraction > 0)
? (Theme.of(context).brightness ==
Brightness.dark)
? customShadowNight(fraction)
: customShadow(fraction)
: null,
color: (e == _minSelected)
? Theme.of(context).accentColor
: Theme.of(context).primaryColor,
shape: BoxShape.circle,
),
alignment: Alignment.center,
height: 30,
width: 30,
child: Text(e.toString(),
style: TextStyle(
fontWeight: FontWeight.bold,
color: (e == _minSelected)
? Colors.white
: null)),
),
Container(
height: 30 * move,
width: 30 * move,
decoration: BoxDecoration(
color:
_colorTween.transform(fraction),
shape: BoxShape.circle),
),
],
),
))
.toList(),
),
),
),
Stack(
children: <Widget>[
Container(
height: 100,
alignment: Alignment.center,
),
Positioned(
left: data.item3 == SleepTimerMode.timer
? -context.width * (move) / 4
: context.width * (move) / 4,
child: Container(
height: 100,
width: context.width,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Container(
alignment: Alignment.center,
height: 40,
width: 120,
decoration: BoxDecoration(
border:
Border.all(color: context.primaryColor),
boxShadow:
context.brightness == Brightness.light
? customShadow(fraction)
: customShadowNight(fraction),
color: _colorTween.transform(move),
borderRadius:
BorderRadius.all(Radius.circular(20)),
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
audio.setSleepTimerMode =
SleepTimerMode.endOfEpisode;
if (fraction == 0) {
_controller.forward();
} else if (fraction == 1) {
_controller.reverse();
audio.cancelTimer();
}
},
borderRadius:
BorderRadius.all(Radius.circular(20)),
child: SizedBox(
height: 40,
width: 120,
child: Center(
child: Text(
'End of episode',
style: TextStyle(
// fontWeight: FontWeight.bold,
// fontSize: 20,
color: (move > 0
? Colors.white
: null)),
))),
),
),
),
Container(
height: 100 * (1 - fraction),
width: 2,
color: context.primaryColorDark,
),
Container(
height: 40,
width: 120,
alignment: Alignment.center,
decoration: BoxDecoration(
border:
Border.all(color: context.primaryColor),
boxShadow:
context.brightness == Brightness.light
? customShadow(fraction)
: customShadowNight(fraction),
color: _colorTween.transform(move),
borderRadius:
BorderRadius.all(Radius.circular(20)),
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
audio.setSleepTimerMode =
SleepTimerMode.timer;
if (fraction == 0) {
_controller.forward();
} else if (fraction == 1) {
_controller.reverse();
audio.cancelTimer();
}
},
borderRadius:
BorderRadius.all(Radius.circular(20)),
child: SizedBox(
height: 40,
width: 120,
child: Center(
child: Text(
data.item2 == 1
? _stringForSeconds(
data.item1.toDouble())
: _stringForSeconds(
(_minSelected * 60)
.toDouble()),
style: TextStyle(
// fontWeight: FontWeight.bold,
// fontSize: 20,
color: (move > 0
? Colors.white
: null)),
),
),
),
),
),
)
],
),
),
),
],
),
],
),
Positioned(
bottom: 50 + 20 * data.item2,
left: context.width / 2 - 100,
width: 200,
child: Container(
alignment: Alignment.center,
child: Text('Good Night',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 20,
color: Colors.white.withOpacity(fraction))),
),
),
Positioned(
bottom: 100 * (1 - data.item2) - 30,
left: context.width / 2 - 100,
width: 200,
child: Container(
alignment: Alignment.center,
child: Text('Sleep Timer',
style:
TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
),
),
data.item2 == 1 ? CustomPaint(painter: StarSky()) : Center(),
data.item2 == 1 ? MeteorLoader() : Center(),
],
),
);
},
);
}
}

View File

@ -51,81 +51,81 @@ class _DownloadListState extends State<DownloadList> {
@override
Widget build(BuildContext context) {
return SliverPadding(
padding: EdgeInsets.all(5.0),
padding: EdgeInsets.zero,
sliver: Consumer<DownloadState>(builder: (_, downloader, __) {
var tasks = downloader.episodeTasks
.where((task) => task.status.value != 3)
.toList();
return tasks.length > 0
? SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return ListTile(
onTap: () => Navigator.push(
context,
ScaleRoute(
page: EpisodeDetail(
episodeItem: tasks[index].episode,
)),
),
title: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Expanded(
flex: 5,
child: Container(
child: Text(
tasks[index].episode.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
? SliverPadding(
padding: EdgeInsets.all(5.0),
sliver: SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return ListTile(
onTap: () => Navigator.push(
context,
ScaleRoute(
page: EpisodeDetail(
episodeItem: tasks[index].episode,
)),
),
title: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Expanded(
flex: 5,
child: Container(
child: Text(
tasks[index].episode.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
),
),
Expanded(
flex: 1,
child: tasks[index].progress >= 0
? Container(
width: 40.0,
padding:
EdgeInsets.symmetric(horizontal: 2),
alignment: Alignment.center,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(
Radius.circular(6)),
color: Colors.red),
child: Text(
tasks[index].progress.toString() + '%',
style: TextStyle(color: Colors.white),
))
: Container(),
),
],
),
subtitle: SizedBox(
height: 2,
child: LinearProgressIndicator(
value: tasks[index].progress / 100,
Expanded(
flex: 1,
child: tasks[index].progress >= 0
? Container(
width: 40.0,
padding:
EdgeInsets.symmetric(horizontal: 2),
alignment: Alignment.center,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(
Radius.circular(6)),
color: Colors.red),
child: Text(
tasks[index].progress.toString() + '%',
style: TextStyle(color: Colors.white),
))
: Container(),
),
],
),
),
leading: CircleAvatar(
backgroundImage: FileImage(
File("${tasks[index].episode.imagePath}")),
),
trailing: _downloadButton(tasks[index], context),
);
},
childCount: tasks.length,
subtitle: SizedBox(
height: 2,
child: LinearProgressIndicator(
value: tasks[index].progress / 100,
),
),
leading: CircleAvatar(
backgroundImage: FileImage(
File("${tasks[index].episode.imagePath}")),
),
trailing: Container(
width: 50,
height: 50,
child: _downloadButton(tasks[index], context)),
);
},
childCount: tasks.length,
),
),
)
: SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Center();
},
childCount: 1,
),
: SliverToBoxAdapter(
child: Center(),
);
}),
);

View File

@ -18,6 +18,7 @@ import 'package:tsacdop/podcasts/podcastdetail.dart';
import 'package:tsacdop/podcasts/podcastmanage.dart';
import 'package:tsacdop/util/pageroute.dart';
import 'package:tsacdop/util/colorize.dart';
import 'package:tsacdop/util/context_extension.dart';
class ScrollPodcasts extends StatefulWidget {
@override
@ -231,6 +232,7 @@ class _ScrollPodcastsState extends State<ScrollPodcasts> {
height: 70,
width: _width,
alignment: Alignment.centerLeft,
color: context.scaffoldBackgroundColor,
child: TabBar(
labelPadding: EdgeInsets.only(
top: 5.0,
@ -243,7 +245,7 @@ class _ScrollPodcastsState extends State<ScrollPodcasts> {
isScrollable: true,
tabs: groups[_groupIndex]
.podcasts
.map<Tab>((PodcastLocal podcastLocal) {
.map<Widget>((PodcastLocal podcastLocal) {
return Tab(
child: ClipRRect(
borderRadius: BorderRadius.all(
@ -403,6 +405,10 @@ class ShowEpisode extends StatelessWidget {
final List<EpisodeBrief> episodes;
final PodcastLocal podcastLocal;
ShowEpisode({Key key, this.episodes, this.podcastLocal}) : super(key: key);
String _stringForSeconds(double seconds) {
if (seconds == null) return null;
return '${(seconds ~/ 60)}:${(seconds.truncate() % 60).toString().padLeft(2, '0')}';
}
@override
Widget build(BuildContext context) {
@ -597,17 +603,36 @@ class ShowEpisode extends StatelessWidget {
? Container(
alignment: Alignment.center,
child: Text(
(episodes[index].duration)
_stringForSeconds(
episodes[index]
.duration
.toDouble())
.toString() +
'mins',
'|',
style: TextStyle(
fontSize: _width / 35,
// color: _c,
// fontStyle: FontStyle.italic,
// color: _c,
// fontStyle: FontStyle.italic,
),
),
)
: Center(),
episodes[index].enclosureLength != null &&
episodes[index].enclosureLength !=
0
? Container(
alignment: Alignment.center,
child: Text(
((episodes[index]
.enclosureLength) ~/
1000000)
.toString() +
'MB',
style: TextStyle(
fontSize: _width / 35),
),
)
: Center(),
],
)),
],

View File

@ -4,15 +4,19 @@ import 'dart:io';
import 'package:flutter/material.dart' hide NestedScrollView;
import 'package:provider/provider.dart';
import 'package:tsacdop/class/download_state.dart';
import 'package:tsacdop/class/podcast_group.dart';
import 'package:tsacdop/home/playlist.dart';
import 'package:tuple/tuple.dart';
import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart';
import 'package:line_icons/line_icons.dart';
import 'package:tsacdop/class/audiostate.dart';
import 'package:tsacdop/class/episodebrief.dart';
import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
import 'package:tsacdop/util/episodegrid.dart';
import 'package:tsacdop/util/mypopupmenu.dart';
import 'package:tsacdop/util/context_extension.dart';
import 'package:tsacdop/util/custompaint.dart';
import 'package:tsacdop/home/appbar/importompl.dart';
import 'package:tsacdop/home/audioplayer.dart';
@ -328,10 +332,14 @@ class _RecentUpdate extends StatefulWidget {
}
class _RecentUpdateState extends State<_RecentUpdate>
with AutomaticKeepAliveClientMixin {
Future<List<EpisodeBrief>> _getRssItem(int top) async {
with AutomaticKeepAliveClientMixin , SingleTickerProviderStateMixin{
Future<List<EpisodeBrief>> _getRssItem(int top, List<String> group) async {
var dbHelper = DBHelper();
List<EpisodeBrief> episodes = await dbHelper.getRecentRssItem(top);
List<EpisodeBrief> episodes;
if (group.first == 'All')
episodes = await dbHelper.getRecentRssItem(top);
else
episodes = await dbHelper.getGroupRssItem(top, group);
return episodes;
}
@ -347,18 +355,23 @@ class _RecentUpdateState extends State<_RecentUpdate>
int _top = 99;
bool _loadMore;
String _groupName;
List<String> _group;
Layout _layout;
@override
void initState() {
super.initState();
_loadMore = false;
_groupName = 'All';
_group = ['All'];
_layout = Layout.three;
}
@override
Widget build(BuildContext context) {
super.build(context);
return FutureBuilder<List<EpisodeBrief>>(
future: _getRssItem(_top),
future: _getRssItem(_top, _group),
builder: (context, snapshot) {
if (snapshot.hasError) print(snapshot.error);
return (snapshot.hasData)
@ -373,8 +386,112 @@ class _RecentUpdateState extends State<_RecentUpdate>
key: PageStorageKey<String>('update'),
physics: const AlwaysScrollableScrollPhysics(),
slivers: <Widget>[
SliverToBoxAdapter(
child: Container(
height: 40,
color: context.primaryColor,
child: Row(
children: <Widget>[
Consumer<GroupList>(
builder: (context, groupList, child) =>
Material(
color: Colors.transparent,
child: PopupMenuButton<String>(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(10))),
elevation: 1,
tooltip: 'Groups fliter',
child: Container(
padding: EdgeInsets.symmetric(
horizontal: 20),
height: 50,
child: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(_groupName),
Padding(
padding: EdgeInsets.symmetric(
horizontal: 5),
),
Icon(
LineIcons.filter_solid,
size: 18,
)
],
)),
itemBuilder: (context) => [
PopupMenuItem(
child: Text('All'), value: 'All')
]..addAll(groupList.groups
.map<PopupMenuEntry<String>>((e) =>
PopupMenuItem(
value: e.name,
child: Text(e.name)))
.toList()),
onSelected: (value) {
if (value == 'All') {
setState(() {
_groupName = 'All';
_group = ['All'];
});
} else {
groupList.groups.forEach((group) {
if (group.name == value) {
setState(() {
_groupName = value;
_group = group.podcastList;
});
}
});
}
},
),
),
),
Spacer(),
Material(
color: Colors.transparent,
child: IconButton(
padding: EdgeInsets.zero,
onPressed: () {
if (_layout == Layout.three)
setState(() {
_layout = Layout.two;
});
else
setState(() {
_layout = Layout.three;
});
},
icon: _layout == Layout.three
? SizedBox(
height: 10,
width: 30,
child: CustomPaint(
painter: LayoutPainter(
0,
context.textTheme.bodyText1
.color),
),
)
: SizedBox(
height: 10,
width: 30,
child: CustomPaint(
painter: LayoutPainter(
1,
context.textTheme.bodyText1
.color),
),
),
)),
],
)),
),
EpisodeGrid(
episodes: snapshot.data,
layout: _layout,
),
SliverList(
delegate: SliverChildBuilderDelegate(
@ -405,9 +522,9 @@ class _MyFavorite extends StatefulWidget {
class _MyFavoriteState extends State<_MyFavorite>
with AutomaticKeepAliveClientMixin {
Future<List<EpisodeBrief>> _getLikedRssItem(_top) async {
Future<List<EpisodeBrief>> _getLikedRssItem(int top, int sortBy) async {
var dbHelper = DBHelper();
List<EpisodeBrief> episodes = await dbHelper.getLikedRssItem(_top);
List<EpisodeBrief> episodes = await dbHelper.getLikedRssItem(top, sortBy);
return episodes;
}
@ -423,18 +540,21 @@ class _MyFavoriteState extends State<_MyFavorite>
int _top = 99;
bool _loadMore;
Layout _layout;
int _sortBy;
@override
void initState() {
super.initState();
_loadMore = false;
_layout = Layout.three;
_sortBy = 0;
}
@override
Widget build(BuildContext context) {
super.build(context);
return FutureBuilder<List<EpisodeBrief>>(
future: _getLikedRssItem(_top),
future: _getLikedRssItem(_top, _sortBy),
builder: (context, snapshot) {
if (snapshot.hasError) print(snapshot.error);
return (snapshot.hasData)
@ -448,8 +568,103 @@ class _MyFavoriteState extends State<_MyFavorite>
child: CustomScrollView(
key: PageStorageKey<String>('favorite'),
slivers: <Widget>[
SliverToBoxAdapter(
child: Container(
height: 40,
color: context.primaryColor,
child: Row(
children: <Widget>[
Material(
color: Colors.transparent,
child: PopupMenuButton<int>(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(10))),
elevation: 1,
tooltip: 'Sort By',
child: Container(
height: 50,
padding:
EdgeInsets.symmetric(horizontal: 20),
child: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text('Sory by'),
Padding(
padding: EdgeInsets.symmetric(
horizontal: 5),
),
Icon(
_sortBy == 0
? LineIcons
.cloud_download_alt_solid
: LineIcons.heartbeat_solid,
size: 18,
)
],
)),
itemBuilder: (context) => [
PopupMenuItem(
value: 0,
child: Text('Update Date'),
),
PopupMenuItem(
value: 1,
child: Text('Like Date'),
)
],
onSelected: (value) {
if (value == 0)
setState(() => _sortBy = 0);
else if (value == 1)
setState(() => _sortBy = 1);
},
),
),
Spacer(),
Material(
color: Colors.transparent,
child: IconButton(
padding: EdgeInsets.zero,
onPressed: () {
if (_layout == Layout.three)
setState(() {
_layout = Layout.two;
});
else
setState(() {
_layout = Layout.three;
});
},
icon: _layout == Layout.three
? SizedBox(
height: 10,
width: 30,
child: CustomPaint(
painter: LayoutPainter(
0,
context
.textTheme.bodyText1.color),
),
)
: SizedBox(
height: 10,
width: 30,
child: CustomPaint(
painter: LayoutPainter(
1,
context
.textTheme.bodyText1.color),
),
),
),
),
],
)),
),
EpisodeGrid(
episodes: snapshot.data,
layout: _layout,
),
SliverList(
delegate: SliverChildBuilderDelegate(
@ -481,24 +696,65 @@ class _MyDownload extends StatefulWidget {
class _MyDownloadState extends State<_MyDownload>
with AutomaticKeepAliveClientMixin {
Layout _layout;
@override
void initState() {
super.initState();
_layout = Layout.three;
}
@override
Widget build(BuildContext context) {
super.build(context);
return CustomScrollView(
key: PageStorageKey<String>('downloas_list'),
key: PageStorageKey<String>('download_list'),
slivers: <Widget>[
DownloadList(),
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Container(
alignment: Alignment.centerLeft,
padding: EdgeInsets.all(15.0),
child: Text('Downloaded'),
);
},
childCount: 1,
),
SliverToBoxAdapter(
child: Container(
height: 40,
color: context.primaryColor,
child: Row(
children: <Widget>[
Container(
padding: EdgeInsets.symmetric(horizontal: 20),
child: Text('Downloaded')),
Spacer(),
Material(
color: Colors.transparent,
child: IconButton(
padding: EdgeInsets.zero,
onPressed: () {
if (_layout == Layout.three)
setState(() {
_layout = Layout.two;
});
else
setState(() {
_layout = Layout.three;
});
},
icon: _layout == Layout.three
? SizedBox(
height: 10,
width: 30,
child: CustomPaint(
painter: LayoutPainter(
0, context.textTheme.bodyText1.color),
),
)
: SizedBox(
height: 10,
width: 30,
child: CustomPaint(
painter: LayoutPainter(
1, context.textTheme.bodyText1.color),
),
),
),
),
],
)),
),
Consumer<DownloadState>(
builder: (_, downloader, __) {
@ -511,6 +767,7 @@ class _MyDownloadState extends State<_MyDownload>
.toList();
return EpisodeGrid(
episodes: episodes,
layout: _layout,
);
},
),
@ -520,4 +777,4 @@ class _MyDownloadState extends State<_MyDownload>
@override
bool get wantKeepAlive => true;
}
}

View File

@ -4,7 +4,6 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:tsacdop/episodes/episodedetail.dart';
import 'package:tuple/tuple.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:line_icons/line_icons.dart';
@ -12,7 +11,7 @@ import 'package:line_icons/line_icons.dart';
import 'package:tsacdop/class/audiostate.dart';
import 'package:tsacdop/class/episodebrief.dart';
import 'package:tsacdop/util/context_extension.dart';
import 'package:tsacdop/home/audioplayer.dart';
import 'package:tsacdop/util/custompaint.dart';
class PlaylistPage extends StatefulWidget {
@override
@ -28,7 +27,7 @@ class _PlaylistPageState extends State<PlaylistPage> {
return sum;
} else {
episodes.forEach((episode) {
sum += episode.duration;
sum += episode.duration ~/ 60;
});
return sum;
}
@ -78,7 +77,6 @@ class _PlaylistPageState extends State<PlaylistPage> {
selector: (_, audio) =>
Tuple3(audio.queue, audio.playerRunning, audio.queueUpdate),
builder: (_, data, __) {
print('update');
final List<EpisodeBrief> episodes = data.item1.playlist;
return Column(
mainAxisSize: MainAxisSize.min,
@ -150,15 +148,17 @@ class _PlaylistPageState extends State<PlaylistPage> {
child: Container(
padding: EdgeInsets.all(5.0),
margin: EdgeInsets.only(right: 20.0, bottom: 5.0),
decoration: data.item2 ? BoxDecoration(
color: context.brightness == Brightness.dark ? Colors.grey[800] : Colors.grey[200],
borderRadius:
BorderRadius.all(Radius.circular(10.0)),
) :
BoxDecoration(
shape: BoxShape.circle,
color: Colors.transparent
),
decoration: data.item2
? BoxDecoration(
color: context.brightness == Brightness.dark
? Colors.grey[800]
: Colors.grey[200],
borderRadius:
BorderRadius.all(Radius.circular(10.0)),
)
: BoxDecoration(
shape: BoxShape.circle,
color: Colors.transparent),
child: data.item2
? _topHeight < 90
? Row(
@ -178,7 +178,7 @@ class _PlaylistPageState extends State<PlaylistPage> {
child: SizedBox(
width: 20,
height: 15,
child: WaveLoader()),
child: WaveLoader(color: context.accentColor,)),
),
],
)
@ -210,13 +210,13 @@ class _PlaylistPageState extends State<PlaylistPage> {
child: SizedBox(
width: 20,
height: 15,
child: WaveLoader()),
child: WaveLoader(color: context.accentColor,)),
),
],
)
: IconButton(
padding: EdgeInsets.all(0),
alignment: Alignment.center,
padding: EdgeInsets.all(0),
alignment: Alignment.center,
icon: Icon(Icons.play_circle_filled,
size: 40,
color: Theme.of(context).accentColor),
@ -360,7 +360,8 @@ class _DismissibleContainerState extends State<DismissibleContainer> {
);
Scaffold.of(context).showSnackBar(SnackBar(
backgroundColor: Colors.grey[800],
content: Text('Episode removed', style: TextStyle(color: Colors.white)),
content: Text('Episode removed',
style: TextStyle(color: Colors.white)),
action: SnackBarAction(
textColor: context.accentColor,
label: 'Undo',
@ -403,7 +404,8 @@ class _DismissibleContainerState extends State<DismissibleContainer> {
: Center(),
widget.episode.duration != 0
? _episodeTag(
(widget.episode.duration).toString() + 'mins',
(widget.episode.duration ~/ 60).toString() +
'mins',
Colors.cyan[300])
: Center(),
widget.episode.enclosureLength != null

View File

@ -21,7 +21,8 @@ class DBHelper {
initDb() async {
var documentsDirectory = await getDatabasesPath();
String path = join(documentsDirectory, "podcasts.db");
Database theDb = await openDatabase(path, version: 1, onCreate: _onCreate);
Database theDb = await openDatabase(path,
version: 1, onCreate: _onCreate, onUpgrade: _onUpgrade);
return theDb;
}
@ -37,16 +38,24 @@ class DBHelper {
enclosure_url TEXT UNIQUE, enclosure_length INTEGER, pubDate TEXT,
description TEXT, feed_id TEXT, feed_link TEXT, milliseconds INTEGER,
duration INTEGER DEFAULT 0, explicit INTEGER DEFAULT 0, liked INTEGER DEFAULT 0,
downloaded TEXT DEFAULT 'ND', download_date INTEGER DEFAULT 0, media_id TEXT,
liked_date INTEGER DEFAULT 0, downloaded TEXT DEFAULT 'ND', download_date INTEGER DEFAULT 0, media_id TEXT,
is_new INTEGER DEFAULT 0)""");
await db.execute(
"""CREATE TABLE PlayHistory(id INTEGER PRIMARY KEY, title TEXT, enclosure_url TEXT UNIQUE,
seconds REAL, seek_value REAL, add_date INTEGER)""");
"""CREATE TABLE PlayHistory(id INTEGER PRIMARY KEY, title TEXT, enclosure_url TEXT,
seconds REAL, seek_value REAL, add_date INTEGER, listen_time INTEGER DEFAULT 0)""");
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)""");
}
void _onUpgrade(Database db, int oldVersion, int newVersion) async {
if (oldVersion == 1) {
await db.execute("ALTER TABLE Episodes ADD liked_date INTEGER DEFAULT 0");
await db
.execute("ALTER TABLE PlayHistory ADD listen_time INTEGER DEFAULT 0");
}
}
Future<List<PodcastLocal>> getPodcastLocal(List<String> podcasts) async {
var dbClient = await database;
List<PodcastLocal> podcastLocal = List();
@ -180,16 +189,24 @@ class DBHelper {
Future<int> saveHistory(PlayHistory history) async {
var dbClient = await database;
int _milliseconds = DateTime.now().millisecondsSinceEpoch;
List<PlayHistory> recent = await getPlayHistory(1);
if (recent.length == 1) {
if (recent.first.url == history.url) {
await dbClient.rawDelete("DELETE FROM PlayHistory WHERE add_date = ?",
[recent.first.playdate.millisecondsSinceEpoch]);
}
}
int result = await dbClient.transaction((txn) async {
return await txn.rawInsert(
"""REPLACE INTO PlayHistory (title, enclosure_url, seconds, seek_value, add_date)
VALUES (?, ?, ?, ?, ?) """,
"""REPLACE INTO PlayHistory (title, enclosure_url, seconds, seek_value, add_date, listen_time)
VALUES (?, ?, ?, ?, ?, ?) """,
[
history.title,
history.url,
history.seconds,
history.seekValue,
_milliseconds
_milliseconds,
history.seekValue > 0.95 ? 1 : 0
]);
});
return result;
@ -200,7 +217,7 @@ class DBHelper {
List<Map> list = await dbClient.rawQuery(
"""SELECT title, enclosure_url, seconds, seek_value, add_date FROM PlayHistory
ORDER BY add_date DESC LIMIT ?
""",[top]);
""", [top]);
List<PlayHistory> playHistory = [];
list.forEach((record) {
playHistory.add(PlayHistory(record['title'], record['enclosure_url'],
@ -210,6 +227,23 @@ class DBHelper {
return playHistory;
}
Future<int> isListened(String url) async {
var dbClient = await database;
int i = 0;
List<Map> list =
await dbClient.rawQuery("""SELECT listen_time FROM PlayHistory
WHERE enclosure_url = ?
""", [url]);
if (list.length == 0)
return 0;
else {
list.forEach((element) {
i += element['listen_time'];
});
return i;
}
}
Future<List<SubHistory>> getSubHistory() async {
var dbClient = await database;
List<Map> list = await dbClient.rawQuery(
@ -249,7 +283,8 @@ class DBHelper {
Future<PlayHistory> getPosition(EpisodeBrief episodeBrief) async {
var dbClient = await database;
List<Map> list = await dbClient.rawQuery(
"SELECT title, enclosure_url, seconds, seek_value, add_date FROM PlayHistory Where enclosure_url = ?",
"""SELECT title, enclosure_url, seconds, seek_value, add_date FROM PlayHistory
WHERE enclosure_url = ? ORDER BY add_date DESC LIMIT 1""",
[episodeBrief.enclosureUrl]);
return list.length > 0
? PlayHistory(list.first['title'], list.first['enclosure_url'],
@ -358,7 +393,7 @@ class DBHelper {
print(pubDate);
final date = _parsePubDate(pubDate);
final milliseconds = date.millisecondsSinceEpoch;
final duration = feed.items[i].itunes.duration?.inMinutes ?? 0;
final duration = feed.items[i].itunes.duration?.inSeconds ?? 0;
final explicit = _getExplicit(feed.items[i].itunes.explicit);
if (url != null) {
@ -420,7 +455,7 @@ class DBHelper {
final pubDate = feed.items[i].pubDate;
final date = _parsePubDate(pubDate);
final milliseconds = date.millisecondsSinceEpoch;
final duration = feed.items[i].itunes.duration?.inMinutes ?? 0;
final duration = feed.items[i].itunes.duration?.inSeconds ?? 0;
final explicit = _getExplicit(feed.items[i].itunes.explicit);
if (url != null) {
@ -452,30 +487,55 @@ class DBHelper {
return countUpdate - count;
}
Future<List<EpisodeBrief>> getRssItem(String id, int i) async {
Future<List<EpisodeBrief>> getRssItem(String id, int i, bool reverse) async {
var dbClient = await database;
List<EpisodeBrief> episodes = [];
List<Map> list = await dbClient
.rawQuery("""SELECT E.title, E.enclosure_url, E.enclosure_length,
if (reverse) {
List<Map> list = await dbClient
.rawQuery("""SELECT E.title, E.enclosure_url, E.enclosure_length,
E.milliseconds, P.imagePath, P.title as feedTitle, E.duration, E.explicit, E.liked,
E.downloaded, P.primaryColor , E.media_id, E.is_new
FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
WHERE P.id = ? ORDER BY E.milliseconds ASC LIMIT ?""", [id, i]);
for (int x = 0; x < list.length; x++) {
episodes.add(EpisodeBrief(
list[x]['title'],
list[x]['enclosure_url'],
list[x]['enclosure_length'],
list[x]['milliseconds'],
list[x]['feedTitle'],
list[x]['primaryColor'],
list[x]['liked'],
list[x]['downloaded'],
list[x]['duration'],
list[x]['explicit'],
list[x]['imagePath'],
list[x]['media_id'],
list[x]['is_new']));
}
} else {
List<Map> list = await dbClient
.rawQuery("""SELECT E.title, E.enclosure_url, E.enclosure_length,
E.milliseconds, P.imagePath, P.title as feedTitle, E.duration, E.explicit, E.liked,
E.downloaded, P.primaryColor , E.media_id, E.is_new
FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
WHERE P.id = ? ORDER BY E.milliseconds DESC LIMIT ?""", [id, i]);
for (int x = 0; x < list.length; x++) {
episodes.add(EpisodeBrief(
list[x]['title'],
list[x]['enclosure_url'],
list[x]['enclosure_length'],
list[x]['milliseconds'],
list[x]['feedTitle'],
list[x]['primaryColor'],
list[x]['liked'],
list[x]['downloaded'],
list[x]['duration'],
list[x]['explicit'],
list[x]['imagePath'],
list[x]['media_id'],
list[x]['is_new']));
for (int x = 0; x < list.length; x++) {
episodes.add(EpisodeBrief(
list[x]['title'],
list[x]['enclosure_url'],
list[x]['enclosure_length'],
list[x]['milliseconds'],
list[x]['feedTitle'],
list[x]['primaryColor'],
list[x]['liked'],
list[x]['downloaded'],
list[x]['duration'],
list[x]['explicit'],
list[x]['imagePath'],
list[x]['media_id'],
list[x]['is_new']));
}
}
return episodes;
}
@ -565,38 +625,96 @@ class DBHelper {
return episodes;
}
Future<List<EpisodeBrief>> getLikedRssItem(int i) async {
Future<List<EpisodeBrief>> getGroupRssItem(
int top, List<String> group) async {
var dbClient = await database;
List<EpisodeBrief> episodes = [];
if (group.length > 0) {
List<String> s = group.map<String>((e) => "'$e'").toList();
List<Map> list = await dbClient
.rawQuery("""SELECT E.title, E.enclosure_url, E.enclosure_length,
E.milliseconds, P.title as feed_title, E.duration, E.explicit, E.liked,
E.downloaded, P.imagePath, P.primaryColor, E.media_id, E.is_new
FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
WHERE P.id in (${s.join(',')})
ORDER BY E.milliseconds DESC LIMIT ? """, [top]);
for (int x = 0; x < list.length; x++) {
episodes.add(EpisodeBrief(
list[x]['title'],
list[x]['enclosure_url'],
list[x]['enclosure_length'],
list[x]['milliseconds'],
list[x]['feed_title'],
list[x]['primaryColor'],
list[x]['liked'],
list[x]['doanloaded'],
list[x]['duration'],
list[x]['explicit'],
list[x]['imagePath'],
list[x]['media_id'],
list[x]['is_new']));
}
}
return episodes;
}
Future<List<EpisodeBrief>> getLikedRssItem(int i, int sortBy) async {
var dbClient = await database;
List<EpisodeBrief> episodes = List();
List<Map> list = await dbClient.rawQuery(
"""SELECT E.title, E.enclosure_url, E.enclosure_length, E.milliseconds, P.imagePath,
if (sortBy == 0) {
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, E.is_new FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
WHERE E.liked = 1 ORDER BY E.milliseconds DESC LIMIT ?""", [i]);
for (int x = 0; x < list.length; x++) {
episodes.add(EpisodeBrief(
list[x]['title'],
list[x]['enclosure_url'],
list[x]['enclosure_length'],
list[x]['milliseconds'],
list[x]['feed_title'],
list[x]['primaryColor'],
list[x]['liked'],
list[x]['downloaded'],
list[x]['duration'],
list[x]['explicit'],
list[x]['imagePath'],
list[x]['media_id'],
list[x]['is_new']));
for (int x = 0; x < list.length; x++) {
episodes.add(EpisodeBrief(
list[x]['title'],
list[x]['enclosure_url'],
list[x]['enclosure_length'],
list[x]['milliseconds'],
list[x]['feed_title'],
list[x]['primaryColor'],
list[x]['liked'],
list[x]['downloaded'],
list[x]['duration'],
list[x]['explicit'],
list[x]['imagePath'],
list[x]['media_id'],
list[x]['is_new']));
}
} else {
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, E.is_new FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
WHERE E.liked = 1 ORDER BY E.liked_date DESC LIMIT ?""", [i]);
for (int x = 0; x < list.length; x++) {
episodes.add(EpisodeBrief(
list[x]['title'],
list[x]['enclosure_url'],
list[x]['enclosure_length'],
list[x]['milliseconds'],
list[x]['feed_title'],
list[x]['primaryColor'],
list[x]['liked'],
list[x]['downloaded'],
list[x]['duration'],
list[x]['explicit'],
list[x]['imagePath'],
list[x]['media_id'],
list[x]['is_new']));
}
}
return episodes;
}
Future<int> setLiked(String url) async {
var dbClient = await database;
int milliseconds = DateTime.now().millisecondsSinceEpoch;
int count = await dbClient.rawUpdate(
"UPDATE Episodes SET liked = 1 WHERE enclosure_url= ?", [url]);
print('liked');
"UPDATE Episodes SET liked = 1, liked_date = ? WHERE enclosure_url= ?",
[milliseconds, url]);
return count;
}
@ -604,10 +722,16 @@ class DBHelper {
var dbClient = await database;
int count = await dbClient.rawUpdate(
"UPDATE Episodes SET liked = 0 WHERE enclosure_url = ?", [url]);
print('unliked');
return count;
}
Future<bool> isLiked(String url) async {
var dbClient = await database;
List<Map> list = await dbClient
.rawQuery("SELECT liked FROM Episodes WHERE enclosure_url = ?", [url]);
return list.first['liked'] == 0 ? false : true;
}
Future<int> saveDownloaded(String url, String id) async {
var dbClient = await database;
int milliseconds = DateTime.now().millisecondsSinceEpoch;

View File

@ -11,6 +11,7 @@ import 'package:url_launcher/url_launcher.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:provider/provider.dart';
import 'package:line_icons/line_icons.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:tsacdop/class/podcastlocal.dart';
@ -20,6 +21,8 @@ import 'package:tsacdop/util/episodegrid.dart';
import 'package:tsacdop/home/audioplayer.dart';
import 'package:tsacdop/class/fireside_data.dart';
import 'package:tsacdop/util/colorize.dart';
import 'package:tsacdop/util/context_extension.dart';
import 'package:tsacdop/util/custompaint.dart';
class PodcastDetail extends StatefulWidget {
PodcastDetail({Key key, this.podcastLocal}) : super(key: key);
@ -36,25 +39,27 @@ class _PodcastDetailState extends State<PodcastDetail> {
Future _updateRssItem(PodcastLocal podcastLocal) async {
var dbHelper = DBHelper();
final result = await dbHelper.updatePodcastRss(podcastLocal);
if(result == 0)
{ Fluttertoast.showToast(
msg: 'No Update',
gravity: ToastGravity.TOP,
);}
else{
Fluttertoast.showToast(
msg: 'Updated $result Episodes',
gravity: ToastGravity.TOP,
);
Provider.of<GroupList>(context, listen: false).updatePodcast(podcastLocal);
}
if (result == 0) {
Fluttertoast.showToast(
msg: 'No Update',
gravity: ToastGravity.TOP,
);
} else {
Fluttertoast.showToast(
msg: 'Updated $result Episodes',
gravity: ToastGravity.TOP,
);
Provider.of<GroupList>(context, listen: false)
.updatePodcast(podcastLocal);
}
if (mounted) setState(() {});
}
Future<List<EpisodeBrief>> _getRssItem(
PodcastLocal podcastLocal, int i) async {
PodcastLocal podcastLocal, int i, bool reverse) async {
var dbHelper = DBHelper();
List<EpisodeBrief> episodes = await dbHelper.getRssItem(podcastLocal.id, i);
List<EpisodeBrief> episodes =
await dbHelper.getRssItem(podcastLocal.id, i, reverse);
if (podcastLocal.provider.contains('fireside')) {
FiresideData data = FiresideData(podcastLocal.id, podcastLocal.link);
await data.getData();
@ -168,12 +173,15 @@ class _PodcastDetailState extends State<PodcastDetail> {
ScrollController _controller;
int _top;
bool _loadMore;
Layout _layout;
bool _reverse;
@override
void initState() {
super.initState();
_loadMore = false;
_top = 99;
_layout = Layout.three;
_reverse = false;
_controller = ScrollController();
}
@ -210,7 +218,8 @@ class _PodcastDetailState extends State<PodcastDetail> {
children: <Widget>[
Expanded(
child: FutureBuilder<List<EpisodeBrief>>(
future: _getRssItem(widget.podcastLocal, _top),
future:
_getRssItem(widget.podcastLocal, _top, _reverse),
builder: (context, snapshot) {
if (snapshot.hasError) print(snapshot.error);
return (snapshot.hasData)
@ -232,7 +241,8 @@ class _PodcastDetailState extends State<PodcastDetail> {
});
}
}),
physics: const AlwaysScrollableScrollPhysics(),
physics:
const AlwaysScrollableScrollPhysics(),
//primary: true,
slivers: <Widget>[
SliverAppBar(
@ -392,18 +402,121 @@ class _PodcastDetailState extends State<PodcastDetail> {
);
}),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return hostsList(context, hosts);
},
childCount: 1,
),
SliverToBoxAdapter(
child: hostsList(context, hosts),
),
SliverToBoxAdapter(
child: Container(
height: 30,
child: Row(
children: <Widget>[
Material(
color: Colors.transparent,
child: PopupMenuButton<int>(
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.all(
Radius.circular(
10))),
elevation: 1,
tooltip: 'Sort By',
child: Container(
height: 30,
padding:
EdgeInsets.symmetric(
horizontal: 15),
child: Row(
mainAxisSize:
MainAxisSize.min,
children: <Widget>[
Text('Sort by'),
Padding(
padding: EdgeInsets
.symmetric(
horizontal:
5),
),
Icon(
_reverse
? LineIcons
.hourglass_start_solid
: LineIcons
.hourglass_end_solid,
size: 18,
)
],
)),
itemBuilder: (context) => [
PopupMenuItem(
value: 0,
child: Text('Newest first'),
),
PopupMenuItem(
value: 1,
child: Text('Oldest first'),
)
],
onSelected: (value) {
if (value == 0)
setState(() =>
_reverse = false);
else if (value == 1)
setState(() =>
_reverse = true);
},
),
),
Spacer(),
Material(
color: Colors.transparent,
child: IconButton(
padding: EdgeInsets.zero,
onPressed: () {
if (_layout == Layout.three)
setState(() {
_layout = Layout.two;
});
else
setState(() {
_layout = Layout.three;
});
},
icon: _layout == Layout.three
? SizedBox(
height: 10,
width: 30,
child: CustomPaint(
painter: LayoutPainter(
0,
context
.textTheme
.bodyText1
.color),
),
)
: SizedBox(
height: 10,
width: 30,
child: CustomPaint(
painter: LayoutPainter(
1,
context
.textTheme
.bodyText1
.color),
),
),
),
),
],
)),
),
EpisodeGrid(
episodes: snapshot.data,
showFavorite: true,
showNumber: true,
layout: _layout,
reverse: _reverse,
episodeCount:
widget.podcastLocal.episodeCount,
),
@ -519,12 +632,6 @@ class _AboutPodcastState extends State<AboutPodcast> {
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
Container(
alignment: Alignment.center,
child: Icon(
Icons.keyboard_arrow_down,
),
),
],
)
: Linkify(
@ -554,3 +661,4 @@ class _AboutPodcastState extends State<AboutPodcast> {
);
}
}

View File

@ -162,12 +162,6 @@ class _PodcastListState extends State<PodcastList> {
113, 113, 113, 1)
: Color.fromRGBO(
15, 15, 15, 1),
// statusBarColor: Theme.of(context)
// .brightness ==
// Brightness.light
// ? Color.fromRGBO(
// 113, 113, 113, 1)
// : Color.fromRGBO(5, 5, 5, 1),
),
child: AboutPodcast(
podcastLocal:

View File

@ -10,6 +10,7 @@ import 'package:tsacdop/class/podcast_group.dart';
import 'package:tsacdop/podcasts/podcastgroup.dart';
import 'package:tsacdop/podcasts/podcastlist.dart';
import 'package:tsacdop/util/pageroute.dart';
import 'package:tsacdop/util/context_extension.dart';
import 'custom_tabview.dart';
class PodcastManage extends StatefulWidget {
@ -173,31 +174,34 @@ class _PodcastManageState extends State<PodcastManage>
? Center()
: Stack(
children: <Widget>[
CustomTabView(
itemCount: _groups.length,
tabBuilder: (context, index) => Tab(
child: Container(
height: 30.0,
padding: EdgeInsets.symmetric(horizontal: 10.0),
alignment: Alignment.center,
decoration: BoxDecoration(
color: (_scroll - index).abs() > 1
? Colors.grey[300]
: Colors.grey[300]
.withOpacity((_scroll - index).abs()),
borderRadius:
BorderRadius.all(Radius.circular(15)),
),
child: Text(
_groups[index].name,
)),
Container(
color: context.scaffoldBackgroundColor,
child: CustomTabView(
itemCount: _groups.length,
tabBuilder: (context, index) => Tab(
child: Container(
height: 30.0,
padding: EdgeInsets.symmetric(horizontal: 10.0),
alignment: Alignment.center,
decoration: BoxDecoration(
color: (_scroll - index).abs() > 1
? Colors.grey[300]
: Colors.grey[300]
.withOpacity((_scroll - index).abs()),
borderRadius:
BorderRadius.all(Radius.circular(15)),
),
child: Text(
_groups[index].name,
)),
),
pageBuilder: (context, index) => Container(
key: ValueKey(_groups[index].name),
child: PodcastGroupList(group: _groups[index])),
onPositionChange: (value) =>
setState(() => _index = value),
onScroll: (value) => setState(() => _scroll = value),
),
pageBuilder: (context, index) => Container(
key: ValueKey(_groups[index].name),
child: PodcastGroupList(group: _groups[index])),
onPositionChange: (value) =>
setState(() => _index = value),
onScroll: (value) => setState(() => _scroll = value),
),
_showSetting
? Positioned.fill(

View File

@ -285,7 +285,7 @@ class _PlayedHistoryState extends State<PlayedHistory>
.data[index].subDate)
.inDays
.toString() +
' days')
' days ago')
: Text(snapshot.data[index].delDate
.difference(snapshot
.data[index].subDate)

View File

@ -46,4 +46,5 @@ List<Libries> plugins = [
Libries('flutter_linkify', mit, 'https://pub.dev/packages/flutter_linkify'),
Libries('extended_nested_scroll_view', mit, 'https://pub.dev/packages/extended_nested_scroll_view'),
Libries('connectivity', bsd, 'https://pub.dev/packages/connectivity'),
Libries('Rxdart', apacheLicense, 'https://pub.dev/packages/rxdart')
];

476
lib/util/custompaint.dart Normal file
View File

@ -0,0 +1,476 @@
import 'dart:io';
import 'dart:ui' as ui;
import 'dart:math' as math;
import 'package:flutter/material.dart';
//Layout change indicator
class LayoutPainter extends CustomPainter {
double scale;
Color color;
LayoutPainter(this.scale, this.color);
@override
void paint(Canvas canvas, Size size) {
Paint _paint = Paint()
..color = color
..strokeWidth = 1.0
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round;
canvas.drawRect(Rect.fromLTRB(0, 0, 10 + 5 * scale, 10), _paint);
canvas.drawRect(
Rect.fromLTRB(10 + 5 * scale, 0, 20 + 10 * scale, 10), _paint);
canvas.drawRect(
Rect.fromLTRB(20 + 5 * scale, 0, 30, 10 - 10 * scale), _paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
//Dark sky used in sleep timer
class StarSky extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final points = [
Offset(50, 100),
Offset(150, 75),
Offset(250, 250),
Offset(130, 200),
Offset(270, 150),
];
final pisces = [
Offset(9, 4),
Offset(11, 5),
Offset(7, 6),
Offset(10, 7),
Offset(8, 8),
Offset(9, 13),
Offset(12, 17),
Offset(5, 19),
Offset(7, 19)
].map((e) => e * 10).toList();
final orion = [
Offset(3, 1),
Offset(6, 1),
Offset(1, 4),
Offset(2, 4),
Offset(2, 7),
Offset(10, 8),
Offset(3, 10),
Offset(8, 10),
Offset(19, 11),
Offset(11, 13),
Offset(18, 14),
Offset(5, 19),
Offset(7, 19),
Offset(9, 18),
Offset(15, 19),
Offset(16, 18),
Offset(2, 25),
Offset(10, 26)
].map((e) => Offset(e.dx * 10 + 250, e.dy * 10)).toList();
Paint paint = Paint()
..color = Colors.white
..strokeWidth = 2.0
..strokeCap = StrokeCap.round;
canvas.drawPoints(ui.PointMode.points, pisces, paint);
canvas.drawPoints(ui.PointMode.points, points, paint);
canvas.drawPoints(ui.PointMode.points, orion, paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
//Listened indicator
class ListenedPainter extends CustomPainter {
Color _color;
double stroke;
ListenedPainter(this._color,{this.stroke = 1.0});
@override
void paint(Canvas canvas, Size size) {
Paint _paint = Paint()
..color = _color
..strokeWidth = stroke
..strokeCap = StrokeCap.round
..style = PaintingStyle.stroke;
Path _path = Path();
_path.moveTo(size.width / 6, size.height * 3 / 8);
_path.lineTo(size.width / 6, size.height * 5 / 8);
_path.moveTo(size.width / 3, size.height / 4);
_path.lineTo(size.width / 3, size.height * 3 / 4);
_path.moveTo(size.width / 2, size.height / 8);
_path.lineTo(size.width / 2, size.height * 7 / 8);
_path.moveTo(size.width * 5 / 6, size.height * 3 / 8);
_path.lineTo(size.width * 5 / 6, size.height * 5 / 8);
_path.moveTo(size.width * 2 / 3, size.height / 4);
_path.lineTo(size.width * 2 / 3, size.height * 3 / 4);
canvas.drawPath(_path, _paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
//Wave play indicator
class WavePainter extends CustomPainter {
double _fraction;
double _value;
Color _color;
WavePainter(this._fraction, this._color);
@override
void paint(Canvas canvas, Size size) {
if (_fraction < 0.5) {
_value = _fraction;
} else {
_value = 1 - _fraction;
}
Path _path = Path();
Paint _paint = Paint()
..color = _color
..strokeWidth = 2.0
..strokeCap = StrokeCap.round
..style = PaintingStyle.stroke;
_path.moveTo(0, size.height / 2);
_path.lineTo(0, size.height / 2 + size.height * _value * 0.2);
_path.moveTo(0, size.height / 2);
_path.lineTo(0, size.height / 2 - size.height * _value * 0.2);
_path.moveTo(size.width / 4, size.height / 2);
_path.lineTo(size.width / 4, size.height / 2 + size.height * _value * 0.8);
_path.moveTo(size.width / 4, size.height / 2);
_path.lineTo(size.width / 4, size.height / 2 - size.height * _value * 0.8);
_path.moveTo(size.width / 2, size.height / 2);
_path.lineTo(size.width / 2, size.height / 2 + size.height * _value * 0.5);
_path.moveTo(size.width / 2, size.height / 2);
_path.lineTo(size.width / 2, size.height / 2 - size.height * _value * 0.5);
_path.moveTo(size.width * 3 / 4, size.height / 2);
_path.lineTo(
size.width * 3 / 4, size.height / 2 + size.height * _value * 0.6);
_path.moveTo(size.width * 3 / 4, size.height / 2);
_path.lineTo(
size.width * 3 / 4, size.height / 2 - size.height * _value * 0.6);
_path.moveTo(size.width, size.height / 2);
_path.lineTo(size.width, size.height / 2 + size.height * _value * 0.2);
_path.moveTo(size.width, size.height / 2);
_path.lineTo(size.width, size.height / 2 - size.height * _value * 0.2);
canvas.drawPath(_path, _paint);
}
@override
bool shouldRepaint(WavePainter oldDelegate) {
return oldDelegate._fraction != _fraction;
}
}
class WaveLoader extends StatefulWidget {
final Color color;
WaveLoader({this.color, Key key}) : super(key: key);
@override
_WaveLoaderState createState() => _WaveLoaderState();
}
class _WaveLoaderState extends State<WaveLoader>
with SingleTickerProviderStateMixin {
double _fraction = 0.0;
Animation animation;
AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this, duration: Duration(milliseconds: 1000));
animation = Tween(begin: 0.0, end: 1.0).animate(_controller)
..addListener(() {
if (mounted)
setState(() {
_fraction = animation.value;
});
});
_controller.forward();
_controller.addStatusListener((status) {
if (status == AnimationStatus.completed) {
_controller.reset();
} else if (status == AnimationStatus.dismissed) {
_controller.forward();
}
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: WavePainter(_fraction, widget.color ?? Colors.white));
}
}
//Love shape
class LovePainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
Path _path = Path();
Paint _paint = Paint()
..color = Colors.red
..strokeWidth = 2.0
..strokeCap = StrokeCap.round;
_path.moveTo(size.width / 2, size.height / 6);
_path.quadraticBezierTo(size.width / 4, 0, size.width / 8, size.height / 6);
_path.quadraticBezierTo(
0, size.height / 3, size.width / 8, size.height * 0.55);
_path.quadraticBezierTo(
size.width / 4, size.height * 0.8, size.width / 2, size.height);
_path.quadraticBezierTo(size.width * 0.75, size.height * 0.8,
size.width * 7 / 8, size.height * 0.55);
_path.quadraticBezierTo(
size.width, size.height / 3, size.width * 7 / 8, size.height / 6);
_path.quadraticBezierTo(
size.width * 3 / 4, 0, size.width / 2, size.height / 6);
canvas.drawPath(_path, _paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
//Line buffer indicator
//Not used
class LinePainter extends CustomPainter {
double _fraction;
Paint _paint;
Color _maincolor;
LinePainter(this._fraction, this._maincolor) {
_paint = Paint()
..color = _maincolor
..strokeWidth = 2.0
..strokeCap = StrokeCap.round;
}
@override
void paint(Canvas canvas, Size size) {
canvas.drawLine(Offset(0, size.height / 2.0),
Offset(size.width * _fraction, size.height / 2.0), _paint);
}
@override
bool shouldRepaint(LinePainter oldDelegate) {
return oldDelegate._fraction != _fraction;
}
}
class LineLoader extends StatefulWidget {
@override
_LineLoaderState createState() => _LineLoaderState();
}
class _LineLoaderState extends State<LineLoader>
with SingleTickerProviderStateMixin {
double _fraction = 0.0;
Animation animation;
AnimationController controller;
@override
void initState() {
super.initState();
controller =
AnimationController(vsync: this, duration: Duration(milliseconds: 500));
animation = Tween(begin: 0.0, end: 1.0).animate(controller)
..addListener(() {
if (mounted)
setState(() {
_fraction = animation.value;
});
});
controller.forward();
controller.addStatusListener((status) {
if (status == AnimationStatus.completed) {
controller.reset();
} else if (status == AnimationStatus.dismissed) {
controller.forward();
}
});
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: LinePainter(_fraction, Theme.of(context).accentColor));
}
}
class ImageRotate extends StatefulWidget {
final String title;
final String path;
ImageRotate({this.title, this.path, Key key}) : super(key: key);
@override
_ImageRotateState createState() => _ImageRotateState();
}
class _ImageRotateState extends State<ImageRotate>
with SingleTickerProviderStateMixin {
Animation _animation;
AnimationController _controller;
double _value;
@override
void initState() {
super.initState();
_value = 0;
_controller = AnimationController(
vsync: this,
duration: Duration(milliseconds: 2000),
);
_animation = Tween(begin: 0.0, end: 1.0).animate(_controller)
..addListener(() {
if (mounted)
setState(() {
_value = _animation.value;
});
});
_controller.forward();
_controller.addStatusListener((status) {
if (status == AnimationStatus.completed) {
_controller.reset();
} else if (status == AnimationStatus.dismissed) {
_controller.forward();
}
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Transform.rotate(
angle: 2 * math.pi * _value,
child: Container(
padding: EdgeInsets.all(10.0),
child: ClipRRect(
borderRadius: BorderRadius.all(Radius.circular(15.0)),
child: Container(
height: 30.0,
width: 30.0,
color: Colors.white,
child: Image.file(File("${widget.path}")),
),
),
),
);
}
}
class LoveOpen extends StatefulWidget {
@override
_LoveOpenState createState() => _LoveOpenState();
}
class _LoveOpenState extends State<LoveOpen>
with SingleTickerProviderStateMixin {
Animation _animationA;
AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: Duration(milliseconds: 300),
);
_animationA = Tween(begin: 0.0, end: 1.0).animate(_controller)
..addListener(() {
if (mounted) setState(() {});
});
_controller.forward();
_controller.addStatusListener((status) {
if (status == AnimationStatus.completed) {
_controller.reset();
}
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
Widget _littleHeart(double scale, double value, double angle) => Container(
alignment: Alignment.centerLeft,
padding: EdgeInsets.only(left: value),
child: ScaleTransition(
scale: _animationA,
alignment: Alignment.center,
child: Transform.rotate(
angle: angle,
child: SizedBox(
height: 5 * scale,
width: 6 * scale,
child: CustomPaint(
painter: LovePainter(),
),
),
),
),
);
@override
Widget build(BuildContext context) {
return Container(
width: 50,
height: 50,
alignment: Alignment.center,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Row(
children: <Widget>[
_littleHeart(0.5, 10, -math.pi / 6),
_littleHeart(1.2, 3, 0),
],
),
Row(
children: <Widget>[
_littleHeart(0.8, 6, math.pi * 1.5),
_littleHeart(0.9, 24, math.pi / 2),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
_littleHeart(1, 8, -math.pi * 0.7),
_littleHeart(0.8, 8, math.pi),
_littleHeart(0.6, 3, -math.pi * 1.2)
],
),
],
),
);
}
}

View File

@ -13,6 +13,11 @@ import 'package:tsacdop/class/audiostate.dart';
import 'package:tsacdop/class/episodebrief.dart';
import 'package:tsacdop/episodes/episodedetail.dart';
import 'package:tsacdop/util/colorize.dart';
import 'package:tsacdop/util/context_extension.dart';
import 'package:tsacdop/util/custompaint.dart';
import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
enum Layout { two, three }
class EpisodeGrid extends StatelessWidget {
final List<EpisodeBrief> episodes;
@ -20,15 +25,33 @@ class EpisodeGrid extends StatelessWidget {
final bool showDownload;
final bool showNumber;
final int episodeCount;
EpisodeGrid(
{Key key,
@required this.episodes,
this.showDownload = false,
this.showFavorite = false,
this.showNumber = false,
this.episodeCount = 0
})
: super(key: key);
final Layout layout;
final bool reverse;
Future<int> _isListened(EpisodeBrief episode) async {
DBHelper dbHelper = DBHelper();
return await dbHelper.isListened(episode.enclosureUrl);
}
Future<bool> _isLiked(EpisodeBrief episode) async {
DBHelper dbHelper = DBHelper();
return await dbHelper.isLiked(episode.enclosureUrl);
}
String _stringForSeconds(double seconds) {
if (seconds == null) return null;
return '${(seconds ~/ 60)}:${(seconds.truncate() % 60).toString().padLeft(2, '0')}';
}
EpisodeGrid({
Key key,
@required this.episodes,
this.showDownload = false,
this.showFavorite = false,
this.showNumber = false,
this.episodeCount = 0,
this.layout = Layout.three,
this.reverse,
}) : super(key: key);
@override
Widget build(BuildContext context) {
@ -100,12 +123,12 @@ class EpisodeGrid extends StatelessWidget {
}
return SliverPadding(
padding:
const EdgeInsets.only(top: 10.0, bottom: 5.0, left: 15.0, right: 15.0),
padding: const EdgeInsets.only(
top: 10.0, bottom: 5.0, left: 15.0, right: 15.0),
sliver: SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
childAspectRatio: 1,
crossAxisCount: 3,
childAspectRatio: layout == Layout.three ? 1 : 1.5,
crossAxisCount: layout == Layout.three ? 3 : 2,
mainAxisSpacing: 6.0,
crossAxisSpacing: 6.0,
),
@ -120,147 +143,291 @@ class EpisodeGrid extends StatelessWidget {
audio.queue.playlist.map((e) => e.enclosureUrl).toList()),
builder: (_, data, __) => OpenContainerWrapper(
episode: episodes[index],
closedBuilder: (context, action, boo) => Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(5.0)),
color: Theme.of(context).scaffoldBackgroundColor,
boxShadow: [
BoxShadow(
color: Theme.of(context).primaryColor,
blurRadius: 0.5,
spreadRadius: 0.5,
),
]),
alignment: Alignment.center,
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.all(Radius.circular(5.0)),
onTapDown: (details) => _offset = Offset(
details.globalPosition.dx, details.globalPosition.dy),
onLongPress: () => _showPopupMenu(
_offset,
episodes[index],
context,
data.item1 == episodes[index],
data.item2.contains(episodes[index].enclosureUrl)),
onTap: action,
child: Container(
padding: const EdgeInsets.all(8.0),
closedBuilder: (context, action, boo) => FutureBuilder<int>(
future: _isListened(episodes[index]),
initialData: 0,
builder: (BuildContext context, AsyncSnapshot snapshot) {
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(5.0)),
border: Border.all(
color:
Theme.of(context).brightness == Brightness.light
? Theme.of(context).primaryColor
: Theme.of(context).scaffoldBackgroundColor,
width: 1.0,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Expanded(
flex: 2,
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Container(
height: _width / 16,
width: _width / 16,
child: boo
? Center()
: CircleAvatar(
backgroundColor:
_c.withOpacity(0.5),
backgroundImage: FileImage(File(
"${episodes[index].imagePath}")),
),
),
Spacer(),
episodes[index].isNew == 1
? Text('New',
style: TextStyle(
color: Colors.red,
fontStyle: FontStyle.italic))
: Center(),
Padding(
padding:
EdgeInsets.symmetric(horizontal: 2),
),
showNumber
? Container(
alignment: Alignment.topRight,
child: Text(
(episodeCount - index).toString(),
style: GoogleFonts.teko(
textStyle: TextStyle(
fontSize: _width / 24,
color: _c,
),
),
),
)
: Center(),
],
borderRadius:
BorderRadius.all(Radius.circular(5.0)),
color: snapshot.data > 0
? context.brightness == Brightness.light
? context.primaryColor
: Color.fromRGBO(40, 40, 40, 1)
: context.scaffoldBackgroundColor,
boxShadow: [
BoxShadow(
color: context.brightness == Brightness.light
? context.primaryColor
: Color.fromRGBO(40, 40, 40, 1),
blurRadius: 0.5,
spreadRadius: 0.5,
),
),
Expanded(
flex: 5,
child: Container(
alignment: Alignment.topLeft,
padding: EdgeInsets.only(top: 2.0),
child: Text(
episodes[index].title,
style: TextStyle(
// fontSize: _width / 32,
),
maxLines: 4,
overflow: TextOverflow.fade,
]),
alignment: Alignment.center,
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius:
BorderRadius.all(Radius.circular(5.0)),
onTapDown: (details) => _offset = Offset(
details.globalPosition.dx,
details.globalPosition.dy),
onLongPress: () => _showPopupMenu(
_offset,
episodes[index],
context,
data.item1 == episodes[index],
data.item2
.contains(episodes[index].enclosureUrl)),
onTap: action,
child: Container(
padding: const EdgeInsets.all(8.0),
decoration: BoxDecoration(
borderRadius:
BorderRadius.all(Radius.circular(5.0)),
border: Border.all(
color: context.brightness == Brightness.light
? context.primaryColor
: context.scaffoldBackgroundColor,
width: 1.0,
),
),
),
Expanded(
flex: 1,
child: Row(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Align(
alignment: Alignment.bottomLeft,
child: Text(
episodes[index].dateToString(),
style: TextStyle(
fontSize: _width / 35,
color: _c,
fontStyle: FontStyle.italic),
),
),
Spacer(),
Padding(
padding: EdgeInsets.all(1),
),
showFavorite
? Container(
alignment: Alignment.bottomRight,
child: (episodes[index].liked == 0)
Expanded(
flex: 2,
child: Row(
mainAxisAlignment:
MainAxisAlignment.start,
children: <Widget>[
Container(
height: _width / 16,
width: _width / 16,
child: boo
? Center()
: IconTheme(
data: IconThemeData(size: 15),
child: Icon(
Icons.favorite,
color: Colors.red,
: CircleAvatar(
backgroundColor:
_c.withOpacity(0.5),
backgroundImage: FileImage(File(
"${episodes[index].imagePath}")),
),
),
Spacer(),
episodes[index].isNew == 1
? Container(
padding: EdgeInsets.symmetric(
horizontal: 2),
child: Text('New',
style: TextStyle(
color: Colors.red,
fontStyle:
FontStyle.italic)),
)
: Center(),
Selector<AudioPlayerNotifier,
EpisodeBrief>(
selector: (_, audio) =>
audio.episode,
builder: (_, data, __) {
return (episodes[index]
.enclosureUrl ==
data?.enclosureUrl)
? Container(
height: 20,
width: 20,
margin:
EdgeInsets.symmetric(
horizontal: 2),
decoration: BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
),
child: SizedBox(
height: 8,
width: 15,
child: WaveLoader(
color: context
.accentColor)))
: layout == Layout.two &&
snapshot.data > 0
? Container(
height: 20,
width: 20,
margin: EdgeInsets
.symmetric(
horizontal:
2),
decoration:
BoxDecoration(
color: context
.accentColor,
shape:
BoxShape.circle,
),
child: SizedBox(
height: 10,
width: 15,
child: CustomPaint(
painter:
ListenedPainter(
Colors.white,
)),
))
: Center();
}),
showDownload || layout == Layout.two
? Container(
child: (episodes[index]
.enclosureUrl !=
episodes[index].mediaId)
? Container(
height: 20,
width: 20,
margin: EdgeInsets
.symmetric(
horizontal: 5),
padding: EdgeInsets
.symmetric(
horizontal: 2),
decoration:
BoxDecoration(
color: context
.accentColor,
shape:
BoxShape.circle,
),
child: Icon(
Icons.done_all,
size: 15,
color: Colors.white,
),
)
: Center(),
)
: Center(),
showNumber
? Container(
alignment: Alignment.topRight,
child: Text(
reverse
? (index + 1).toString()
: (episodeCount - index)
.toString(),
style: GoogleFonts.teko(
textStyle: TextStyle(
fontSize: _width / 24,
color: _c,
),
),
),
)
: Center(),
)
: Center(),
],
),
),
Expanded(
flex: 5,
child: Container(
alignment: Alignment.topLeft,
padding: EdgeInsets.only(top: 2.0),
child: Text(
episodes[index].title,
style: TextStyle(
// fontSize: _width / 32,
),
maxLines: 4,
overflow: TextOverflow.fade,
),
),
),
Expanded(
flex: 1,
child: Row(
children: <Widget>[
Align(
alignment: Alignment.bottomLeft,
child: Text(
episodes[index].dateToString(),
style: TextStyle(
fontSize: _width / 35,
color: _c,
fontStyle: FontStyle.italic),
),
),
Spacer(),
layout == Layout.two &&
episodes[index].duration != 0
? Container(
alignment: Alignment.center,
child: Text(
_stringForSeconds(
episodes[index]
.duration
.toDouble()) +
'|',
style: TextStyle(
fontSize: _width / 35),
),
)
: Center(),
layout == Layout.two &&
episodes[index]
.enclosureLength !=
null &&
episodes[index]
.enclosureLength !=
0
? Container(
alignment: Alignment.center,
child: Text(
((episodes[index]
.enclosureLength) ~/
1000000)
.toString() +
'MB',
style: TextStyle(
fontSize: _width / 35),
),
)
: Center(),
Padding(
padding: EdgeInsets.all(1),
),
showFavorite || layout == Layout.two
? FutureBuilder<bool>(
future:
_isLiked(episodes[index]),
initialData: false,
builder: (context, snapshot) =>
Container(
alignment:
Alignment.bottomRight,
child: (snapshot.data)
? IconTheme(
data: IconThemeData(
size: 15),
child: Icon(
Icons.favorite,
color: Colors.red,
),
)
: Center(),
),
)
: Center(),
],
),
),
],
),
),
],
),
),
),
),
),
),
);
}),
),
);
},
@ -271,7 +438,6 @@ class EpisodeGrid extends StatelessWidget {
}
}
class OpenContainerWrapper extends StatelessWidget {
const OpenContainerWrapper({
this.closedBuilder,
@ -318,5 +484,3 @@ class OpenContainerWrapper extends StatelessWidget {
);
}
}

View File

@ -64,6 +64,7 @@ dev_dependencies:
extended_nested_scroll_view: ^0.4.0
connectivity: ^0.4.8+2
flare_flutter: ^2.0.1
rxdart: ^0.23.1
# For information on the generic Dart part of this file, see the