Skip at beginning

Add new episode to playlist at one click
This commit is contained in:
stonegate 2020-04-23 02:10:57 +08:00
parent 0a548b4441
commit 0040513380
22 changed files with 1506 additions and 897 deletions

View File

@ -111,7 +111,10 @@ enum SleepTimerMode { endOfEpisode, timer, undefined }
class AudioPlayerNotifier extends ChangeNotifier {
DBHelper dbHelper = DBHelper();
KeyValueStorage storage = KeyValueStorage('audioposition');
KeyValueStorage positionStorage = KeyValueStorage(audioPositionKey);
KeyValueStorage autoPlayStorage = KeyValueStorage(autoPlayKey);
KeyValueStorage autoAddStorage = KeyValueStorage(autoAddKey);
EpisodeBrief _episode;
Playlist _queue = Playlist();
bool _queueUpdate = false;
@ -130,7 +133,10 @@ class AudioPlayerNotifier extends ChangeNotifier {
bool _startSleepTimer = false;
double _switchValue = 0;
SleepTimerMode _sleepTimerMode = SleepTimerMode.undefined;
//set autoplay episode in playlist
bool _autoPlay = true;
//Set auto add episodes to playlist
bool _autoAdd = false;
DateTime _current;
int _currentPosition;
double _currentSpeed = 1;
@ -151,6 +157,7 @@ class AudioPlayerNotifier extends ChangeNotifier {
bool get startSleepTimer => _startSleepTimer;
SleepTimerMode get sleepTimerMode => _sleepTimerMode;
bool get autoPlay => _autoPlay;
bool get autoAdd => _autoAdd;
int get timeLeft => _timeLeft;
double get switchValue => _switchValue;
double get currentSpeed => _currentSpeed;
@ -163,6 +170,31 @@ class AudioPlayerNotifier extends ChangeNotifier {
set autoPlaySwitch(bool boo) {
_autoPlay = boo;
notifyListeners();
_setAutoPlay();
}
Future _getAutoPlay() async {
int i = await autoPlayStorage.getInt();
_autoAdd = i == 0 ? true : false;
}
Future _setAutoPlay() async {
await autoPlayStorage.saveInt(_autoPlay ? 1 : 0);
}
set autoAddSwitch(bool boo) {
_autoAdd = boo;
notifyListeners();
_setAutoAdd();
}
Future _getAutoAdd() async {
int i = await autoAddStorage.getInt();
_autoAdd = i == 0 ? false : true;
}
Future _setAutoAdd() async {
await autoAddStorage.saveInt(_autoAdd ? 1 : 0);
}
set setSleepTimerMode(SleepTimerMode timer) {
@ -181,7 +213,10 @@ class AudioPlayerNotifier extends ChangeNotifier {
loadPlaylist() async {
await _queue.getPlaylist();
_lastPostion = await storage.getInt();
await _getAutoPlay();
// await _getAutoAdd();
// await addNewEpisode('all');
_lastPostion = await positionStorage.getInt();
if (_lastPostion > 0 && _queue.playlist.length > 0) {
final EpisodeBrief episode = _queue.playlist.first;
final int duration = episode.duration * 1000;
@ -190,6 +225,8 @@ class AudioPlayerNotifier extends ChangeNotifier {
episode.title, episode.enclosureUrl, _lastPostion / 1000, seekValue);
await dbHelper.saveHistory(history);
}
KeyValueStorage lastWorkStorage = KeyValueStorage(lastWorkKey);
await lastWorkStorage.saveInt(0);
}
episodeLoad(EpisodeBrief episode, {int startPosition = 0}) async {
@ -268,7 +305,7 @@ class AudioPlayerNotifier extends ChangeNotifier {
if (event.length == 0 || _stopOnComplete == true) {
_queue.delFromPlaylist(_episode);
_lastPostion = 0;
storage.saveInt(_lastPostion);
positionStorage.saveInt(_lastPostion);
final PlayHistory history = PlayHistory(
_episode.title,
_episode.enclosureUrl,
@ -330,7 +367,7 @@ class AudioPlayerNotifier extends ChangeNotifier {
if (_backgroundAudioPosition > 0) {
_lastPostion = _backgroundAudioPosition;
storage.saveInt(_lastPostion);
positionStorage.saveInt(_lastPostion);
}
notifyListeners();
}
@ -358,11 +395,13 @@ class AudioPlayerNotifier extends ChangeNotifier {
}
addToPlaylist(EpisodeBrief episode) async {
if (_playerRunning) {
await AudioService.addQueueItem(episode.toMediaItem());
if (!_queue.playlist.contains(episode)) {
if (_playerRunning) {
await AudioService.addQueueItem(episode.toMediaItem());
}
await _queue.addToPlayList(episode);
notifyListeners();
}
await _queue.addToPlayList(episode);
notifyListeners();
}
addToPlaylistAt(EpisodeBrief episode, int index) async {
@ -374,6 +413,22 @@ class AudioPlayerNotifier extends ChangeNotifier {
notifyListeners();
}
addNewEpisode(List<String> group) async {
List<EpisodeBrief> newEpisodes = [];
if (group.first == 'All')
newEpisodes = await dbHelper.getRecentNewRssItem();
else
newEpisodes = await dbHelper.getGroupNewRssItem(group);
if (newEpisodes.length > 0)
await Future.forEach(newEpisodes, (episode) async {
await addToPlaylist(episode);
});
if (group.first == 'All')
await dbHelper.removeAllNewMark();
else
await dbHelper.removeGroupNewMark(group);
}
updateMediaItem(EpisodeBrief episode) async {
int index = _queue.playlist
.indexWhere((item) => item.enclosureUrl == episode.enclosureUrl);
@ -402,7 +457,7 @@ class AudioPlayerNotifier extends ChangeNotifier {
} else {
await addToPlaylistAt(episode, 0);
_lastPostion = 0;
storage.saveInt(_lastPostion);
positionStorage.saveInt(_lastPostion);
}
notifyListeners();
}
@ -596,12 +651,12 @@ class AudioPlayerTask extends BackgroundAudioTask {
_skipState = null;
onStop();
} else {
// AudioServiceBackground.setQueue(_queue);
AudioServiceBackground.setMediaItem(mediaItem);
await _audioPlayer.setUrl(mediaItem.id);
Duration duration = await _audioPlayer.durationFuture ?? Duration.zero;
AudioServiceBackground.setMediaItem(
mediaItem.copyWith(duration: duration.inMilliseconds));
AudioServiceBackground.setQueue(_queue);
AudioServiceBackground.setMediaItem(mediaItem);
await _audioPlayer.setUrl(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) {
@ -623,8 +678,17 @@ class AudioPlayerTask extends BackgroundAudioTask {
AudioServiceBackground.setMediaItem(
mediaItem.copyWith(duration: duration.inMilliseconds));
}
// if (mediaItem.extras['skip'] > 0) {
// await _audioPlayer.setClip(
// start: Duration(seconds: 60));
// print(mediaItem.extras['skip']);
// print('set clip success');
// }
_playing = true;
_audioPlayer.play();
if (mediaItem.extras['skip'] > 0) {
_audioPlayer.seek(Duration(seconds: mediaItem.extras['skip']));
}
}
}

View File

@ -16,6 +16,7 @@ class EpisodeBrief {
final String imagePath;
final String mediaId;
final int isNew;
final int skipSeconds;
EpisodeBrief(
this.title,
this.enclosureUrl,
@ -29,10 +30,11 @@ class EpisodeBrief {
this.explicit,
this.imagePath,
this.mediaId,
this.isNew);
this.isNew,
this.skipSeconds);
String dateToString() {
DateTime date = DateTime.fromMillisecondsSinceEpoch(pubDate, isUtc: true);
DateTime date = DateTime.fromMillisecondsSinceEpoch(pubDate,isUtc: true);
var diffrence = DateTime.now().toUtc().difference(date);
if (diffrence.inHours < 1) {
return '1 hour ago';
@ -43,18 +45,18 @@ class EpisodeBrief {
} else if (diffrence.inDays < 7) {
return '${diffrence.inDays} days ago';
} else {
return DateFormat.yMMMd()
.format(DateTime.fromMillisecondsSinceEpoch(pubDate, isUtc: true).toLocal());
return DateFormat.yMMMd().format(
DateTime.fromMillisecondsSinceEpoch(pubDate, isUtc: true).toLocal());
}
}
MediaItem toMediaItem() {
return MediaItem(
id: mediaId,
title: title,
artist: feedTitle,
album: feedTitle,
artUri: 'file://$imagePath');
artUri: 'file://$imagePath',
extras: {'skip': skipSeconds});
}
}

View File

@ -67,22 +67,18 @@ class RefreshWorker extends ChangeNotifier {
}
Future<void> refreshIsolateEntryPoint(SendPort sendPort) async {
KeyValueStorage refreshstorage = KeyValueStorage(refreshdateKey);
await refreshstorage.saveInt(DateTime.now().millisecondsSinceEpoch);
var dbHelper = DBHelper();
List<PodcastLocal> podcastList = await dbHelper.getPodcastLocalAll();
int i = 0;
await Future.forEach(podcastList, (podcastLocal) async {
//int i = 0;
await Future.forEach<PodcastLocal>(podcastList, (podcastLocal) async {
sendPort.send([podcastLocal.title, 1]);
try {
i += await dbHelper.updatePodcastRss(podcastLocal);
print('Refresh ' + podcastLocal.title);
} catch (e) {
sendPort.send([podcastLocal.title, 2]);
await Future.delayed(Duration(seconds: 1));
}
await dbHelper.updatePodcastRss(podcastLocal);
print('Refresh ' + podcastLocal.title);
});
KeyValueStorage refreshstorage = KeyValueStorage('refreshdate');
await refreshstorage.saveInt(DateTime.now().millisecondsSinceEpoch);
KeyValueStorage refreshcountstorage = KeyValueStorage('refreshcount');
await refreshcountstorage.saveInt(i);
// KeyValueStorage refreshcountstorage = KeyValueStorage('refreshcount');
// await refreshcountstorage.saveInt(i);
sendPort.send("done");
}

View File

@ -10,25 +10,30 @@ void callbackDispatcher() {
Workmanager.executeTask((task, inputData) async {
var dbHelper = DBHelper();
List<PodcastLocal> podcastList = await dbHelper.getPodcastLocalAll();
await Future.forEach(podcastList, (podcastLocal) async {
await dbHelper.updatePodcastRss(podcastLocal);
//lastWork is a indicator for if the app was opened since last backgroundwork
//if the app wes opend,then the old marked new episode would be marked not new.
KeyValueStorage lastWorkStorage = KeyValueStorage(lastWorkKey);
int lastWork = await lastWorkStorage.getInt();
await Future.forEach<PodcastLocal>(podcastList, (podcastLocal) async {
await dbHelper.updatePodcastRss(podcastLocal, removeMark: lastWork);
print('Refresh ' + podcastLocal.title);
});
KeyValueStorage refreshstorage = KeyValueStorage('refreshdate');
await lastWorkStorage.saveInt(1);
KeyValueStorage refreshstorage = KeyValueStorage(refreshdateKey);
await refreshstorage.saveInt(DateTime.now().millisecondsSinceEpoch);
return Future.value(true);
});
}
class SettingState extends ChangeNotifier {
KeyValueStorage themeStorage = KeyValueStorage('themes');
KeyValueStorage accentStorage = KeyValueStorage('accents');
KeyValueStorage autoupdateStorage = KeyValueStorage('autoupdate');
KeyValueStorage intervalStorage = KeyValueStorage('updateInterval');
KeyValueStorage themeStorage = KeyValueStorage(themesKey);
KeyValueStorage accentStorage = KeyValueStorage(accentsKey);
KeyValueStorage autoupdateStorage = KeyValueStorage(autoAddKey);
KeyValueStorage intervalStorage = KeyValueStorage(updateIntervalKey);
KeyValueStorage downloadUsingDataStorage =
KeyValueStorage('downloadUsingData');
KeyValueStorage introStorage = KeyValueStorage('intro');
KeyValueStorage realDarkStorage = KeyValueStorage('realDark');
KeyValueStorage(downloadUsingDataKey);
KeyValueStorage introStorage = KeyValueStorage(introKey);
KeyValueStorage realDarkStorage = KeyValueStorage(realDarkKey);
Future initData() async {
await _getTheme();

View File

@ -9,7 +9,6 @@ import 'package:url_launcher/url_launcher.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:intl/intl.dart';
import 'package:tuple/tuple.dart';
import 'package:audio_service/audio_service.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:google_fonts/google_fonts.dart';
@ -217,7 +216,7 @@ class _EpisodeDetailState extends State<EpisodeDetail> {
data: _description,
linkStyle: TextStyle(
color: Theme.of(context).accentColor,
decoration: TextDecoration.underline,
// decoration: TextDecoration.underline,
textBaseline: TextBaseline.ideographic),
onLinkTap: (url) {
_launchUrl(url);
@ -244,8 +243,8 @@ class _EpisodeDetailState extends State<EpisodeDetail> {
linkStyle: TextStyle(
color:
Theme.of(context).accentColor,
decoration:
TextDecoration.underline,
// decoration:
// TextDecoration.underline,
),
),
)
@ -539,12 +538,18 @@ class _MenuBarState extends State<MenuBar> {
: Center();
}),
Spacer(),
Selector<AudioPlayerNotifier,
Tuple2<EpisodeBrief, BasicPlaybackState>>(
selector: (_, audio) => Tuple2(audio.episode, audio.audioState),
Selector<AudioPlayerNotifier, Tuple2<EpisodeBrief, bool>>(
selector: (_, audio) => Tuple2(audio.episode, audio.playerRunning),
builder: (_, data, __) {
return (widget.episodeItem.title != data.item1?.title)
? Material(
return (widget.episodeItem.title == data.item1?.title &&
data.item2)
? Container(
padding: EdgeInsets.only(right: 30),
child: SizedBox(
width: 20,
height: 15,
child: WaveLoader(color: context.accentColor)))
: Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
@ -570,13 +575,7 @@ class _MenuBarState extends State<MenuBar> {
),
),
),
)
: Container(
padding: EdgeInsets.only(right: 30),
child: SizedBox(
width: 20,
height: 15,
child: WaveLoader(color: context.accentColor)));
);
},
),
],

View File

@ -8,21 +8,10 @@ import 'package:color_thief_flutter/color_thief_flutter.dart';
import 'package:dio/dio.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:path_provider/path_provider.dart';
import 'package:image/image.dart' as img;
import 'package:fluttertoast/fluttertoast.dart';
import 'package:tsacdop/class/audiostate.dart';
import 'package:tsacdop/class/download_state.dart';
import 'package:tsacdop/class/fireside_data.dart';
import 'package:tsacdop/class/refresh_podcast.dart';
import 'package:uuid/uuid.dart';
import '../../class/importompl.dart';
import '../../class/podcast_group.dart';
import '../../class/searchpodcast.dart';
import '../../class/podcastlocal.dart';
import '../../class/subscribe_podcast.dart';
import '../../local_storage/sqflite_localpodcast.dart';
import '../../home/appbar/popupmenu.dart';
import '../../util/context_extension.dart';
import '../../webfeed/webfeed.dart';
@ -479,6 +468,10 @@ class _SearchResultState extends State<SearchResult>
onPressed: () {
savePodcast(widget.onlinePodcast);
setState(() => _issubscribe = true);
Fluttertoast.showToast(
msg: 'Podcast subscribed',
gravity: ToastGravity.TOP,
);
})
: OutlineButton(
color: context.accentColor.withOpacity(0.8),

View File

@ -55,10 +55,13 @@ class Import extends StatelessWidget {
Consumer<RefreshWorker>(
builder: (context, refreshWorker, child) {
RefreshItem item = refreshWorker.currentRefreshItem;
if (refreshWorker.complete) groupList.updateGroups();
if (refreshWorker.complete) {
groupList.updateGroups();
// audio.addNewEpisode('all');
}
switch (item.refreshState) {
case RefreshState.fetch:
return importColumn("Fetch data ${item.title}", context);
return importColumn("Update ${item.title}", context);
case RefreshState.error:
return importColumn("Update error ${item.title}", context);
default:

View File

@ -2,30 +2,19 @@ import 'dart:io';
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:dio/dio.dart';
import 'package:provider/provider.dart';
import 'package:tsacdop/class/fireside_data.dart';
import 'package:tsacdop/class/refresh_podcast.dart';
import 'package:tsacdop/class/subscribe_podcast.dart';
import 'package:tsacdop/local_storage/key_value_storage.dart';
import 'package:xml/xml.dart' as xml;
import 'package:file_picker/file_picker.dart';
import 'package:flutter/services.dart';
import 'package:path_provider/path_provider.dart';
import 'package:color_thief_flutter/color_thief_flutter.dart';
import 'package:image/image.dart' as img;
import 'package:uuid/uuid.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:line_icons/line_icons.dart';
import 'package:intl/intl.dart';
import 'package:tsacdop/class/podcast_group.dart';
import 'package:tsacdop/settings/settting.dart';
import 'about.dart';
import 'package:tsacdop/class/podcastlocal.dart';
import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
import 'package:tsacdop/class/importompl.dart';
import 'package:tsacdop/webfeed/webfeed.dart';
class OmplOutline {
final String text;
@ -47,14 +36,6 @@ class PopupMenu extends StatefulWidget {
}
class _PopupMenuState extends State<PopupMenu> {
Future<String> _getColor(File file) async {
final imageProvider = FileImage(file);
var colorImage = await getImageFromProvider(imageProvider);
var color = await getColorFromImage(colorImage);
String primaryColor = color.toString();
return primaryColor;
}
Future<String> _getRefreshDate() async {
int refreshDate;
KeyValueStorage refreshstorage = KeyValueStorage('refreshdate');
@ -68,14 +49,16 @@ class _PopupMenuState extends State<PopupMenu> {
}
DateTime date = DateTime.fromMillisecondsSinceEpoch(refreshDate);
var diffrence = DateTime.now().difference(date);
if (diffrence.inMinutes < 10) {
return 'Just now';
if (diffrence.inSeconds < 60) {
return '${diffrence.inSeconds} seconds ago';
} else if (diffrence.inMinutes < 10) {
return '${diffrence.inMinutes} minutes ago';
} else if (diffrence.inHours < 1) {
return '1 hour ago';
} else if (diffrence.inHours < 24) {
return 'an hour ago';
} else if (diffrence.inHours <= 24) {
return '${diffrence.inHours} hours ago';
} else if (diffrence.inDays < 7) {
return '${diffrence.inDays} day ago';
return '${diffrence.inDays} days ago';
} else {
return DateFormat.yMMMd()
.format(DateTime.fromMillisecondsSinceEpoch(refreshDate));
@ -84,8 +67,6 @@ class _PopupMenuState extends State<PopupMenu> {
@override
Widget build(BuildContext context) {
// ImportOmpl importOmpl = Provider.of<ImportOmpl>(context, listen: false);
// GroupList groupList = Provider.of<GroupList>(context, listen: false);
var refreshWorker = Provider.of<RefreshWorker>(context, listen: false);
var subscribeWorker = Provider.of<SubscribeWorker>(context, listen: false);
// _refreshAll() async {
@ -105,87 +86,6 @@ class _PopupMenuState extends State<PopupMenu> {
// importOmpl.importState = ImportState.complete;
// groupList.updateGroups();
// }
//
// saveOmpl(String rss) async {
// var dbHelper = DBHelper();
// importOmpl.importState = ImportState.import;
// BaseOptions options = new BaseOptions(
// connectTimeout: 20000,
// receiveTimeout: 20000,
// );
// Response response = await Dio(options).get(rss);
// if (response.statusCode == 200) {
// var _p = RssFeed.parse(response.data);
//
// var dir = await getApplicationDocumentsDirectory();
//
// String _realUrl =
// response.redirects.isEmpty ? rss : response.realUri.toString();
//
// print(_realUrl);
// bool _checkUrl = await dbHelper.checkPodcast(_realUrl);
//
// if (_checkUrl) {
// Response<List<int>> imageResponse = await Dio().get<List<int>>(
// _p.itunes.image.href,
// options: Options(responseType: ResponseType.bytes));
// img.Image image = img.decodeImage(imageResponse.data);
// img.Image thumbnail = img.copyResize(image, width: 300);
// String _uuid = Uuid().v4();
// File("${dir.path}/$_uuid.png")
// ..writeAsBytesSync(img.encodePng(thumbnail));
//
// String _imagePath = "${dir.path}/$_uuid.png";
// String _primaryColor =
// await _getColor(File("${dir.path}/$_uuid.png"));
// String _author = _p.itunes.author ?? _p.author ?? '';
// String _provider = _p.generator ?? '';
// String _link = _p.link ?? '';
// PodcastLocal podcastLocal = PodcastLocal(
// _p.title,
// _p.itunes.image.href,
// _realUrl,
// _primaryColor,
// _author,
// _uuid,
// _imagePath,
// _provider,
// _link,
// description: _p.description);
//
// await groupList.subscribe(podcastLocal);
//
// if (_provider.contains('fireside')) {
// FiresideData data = FiresideData(_uuid, _link);
// await data.fatchData();
// }
//
// importOmpl.importState = ImportState.parse;
//
// await dbHelper.savePodcastRss(_p, _uuid);
// groupList.updatePodcast(podcastLocal.id);
// importOmpl.importState = ImportState.complete;
// } else {
// importOmpl.importState = ImportState.error;
//
// Fluttertoast.showToast(
// msg: 'Podcast Subscribed Already',
// gravity: ToastGravity.TOP,
// );
// await Future.delayed(Duration(seconds: 5));
// importOmpl.importState = ImportState.stop;
// }
// } else {
// importOmpl.importState = ImportState.error;
//
// Fluttertoast.showToast(
// msg: 'Network error, Subscribe failed',
// gravity: ToastGravity.TOP,
// );
// await Future.delayed(Duration(seconds: 5));
// importOmpl.importState = ImportState.stop;
// }
// }
//
void _saveOmpl(String path) async {
File file = File(path);

View File

@ -28,6 +28,13 @@ class ScrollPodcasts extends StatefulWidget {
class _ScrollPodcastsState extends State<ScrollPodcasts> {
int _groupIndex;
Future<int> getPodcastCounts(String id) async {
var dbHelper = DBHelper();
List<int> list = await dbHelper.getPodcastCounts(id);
return list.first;
}
@override
void initState() {
super.initState();
@ -260,20 +267,29 @@ class _ScrollPodcastsState extends State<ScrollPodcasts> {
child: Image.file(File(
"${podcastLocal.imagePath}")),
),
podcastLocal.upateCount > 0
? Container(
alignment: Alignment.center,
height: 10,
width: 40,
color: Colors.black54,
child: Text('New',
style: TextStyle(
color: Colors.red,
fontSize: 8,
fontStyle: FontStyle
.italic)),
)
: Center(),
FutureBuilder<int>(
future: getPodcastCounts(
podcastLocal.id),
initialData: 0,
builder: (context, snapshot) {
return snapshot.data > 0
? Container(
alignment:
Alignment.center,
height: 10,
width: 40,
color: Colors.black54,
child: Text('New',
style: TextStyle(
color:
Colors.red,
fontSize: 8,
fontStyle:
FontStyle
.italic)),
)
: Center();
}),
],
),
),
@ -344,14 +360,16 @@ class _PodcastPreviewState extends State<PodcastPreview> {
builder: (context, snapshot) {
if (snapshot.hasError) {
print(snapshot.error);
Center(child: CircularProgressIndicator());
Center();
}
return (snapshot.hasData)
? ShowEpisode(
episodes: snapshot.data,
podcastLocal: widget.podcastLocal,
)
: Center(child: CircularProgressIndicator());
: Container(
padding: EdgeInsets.all(5.0),
);
},
),
),
@ -409,75 +427,75 @@ class ShowEpisode extends StatelessWidget {
return '${(seconds ~/ 60)}:${(seconds.truncate() % 60).toString().padLeft(2, '0')}';
}
@override
Widget build(BuildContext context) {
double _width = MediaQuery.of(context).size.width;
Offset offset;
_showPopupMenu(Offset offset, EpisodeBrief episode, BuildContext context,
bool isPlaying, bool isInPlaylist) async {
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
double left = offset.dx;
double top = offset.dy;
await showMenu<int>(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(10))),
context: context,
position: RelativeRect.fromLTRB(left, top, _width - left, 0),
items: <PopupMenuEntry<int>>[
PopupMenuItem(
value: 0,
_showPopupMenu(Offset offset, EpisodeBrief episode, BuildContext context,
bool isPlaying, bool isInPlaylist) async {
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
double left = offset.dx;
double top = offset.dy;
await showMenu<int>(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(10))),
context: context,
position: RelativeRect.fromLTRB(left, top, context.width - left, 0),
items: <PopupMenuEntry<int>>[
PopupMenuItem(
value: 0,
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Icon(
LineIcons.play_circle_solid,
color: Theme.of(context).accentColor,
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 2),
),
!isPlaying ? Text('Play') : Text('Playing'),
],
),
),
PopupMenuItem(
value: 1,
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Icon(
LineIcons.play_circle_solid,
color: Theme.of(context).accentColor,
LineIcons.clock_solid,
color: Colors.red,
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 2),
),
!isPlaying ? Text('Play') : Text('Playing'),
!isInPlaylist ? Text('Later') : Text('Remove')
],
),
),
PopupMenuItem(
value: 1,
child: Row(
children: <Widget>[
Icon(
LineIcons.clock_solid,
color: Colors.red,
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 2),
),
!isInPlaylist ? Text('Later') : Text('Remove')
],
)),
],
elevation: 5.0,
).then((value) {
if (value == 0) {
if (!isPlaying) audio.episodeLoad(episode);
} else if (value == 1) {
if (!isInPlaylist) {
audio.addToPlaylist(episode);
Fluttertoast.showToast(
msg: 'Added to playlist',
gravity: ToastGravity.BOTTOM,
);
} else {
audio.delFromPlaylist(episode);
Fluttertoast.showToast(
msg: 'Removed from playlist',
gravity: ToastGravity.BOTTOM,
);
}
)),
],
elevation: 5.0,
).then((value) {
if (value == 0) {
if (!isPlaying) audio.episodeLoad(episode);
} else if (value == 1) {
if (!isInPlaylist) {
audio.addToPlaylist(episode);
Fluttertoast.showToast(
msg: 'Added to playlist',
gravity: ToastGravity.BOTTOM,
);
} else {
audio.delFromPlaylist(episode);
Fluttertoast.showToast(
msg: 'Removed from playlist',
gravity: ToastGravity.BOTTOM,
);
}
});
}
}
});
}
@override
Widget build(BuildContext context) {
double _width = context.width;
Offset offset;
return CustomScrollView(
physics: NeverScrollableScrollPhysics(),
primary: false,

View File

@ -9,6 +9,7 @@ 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:fluttertoast/fluttertoast.dart';
import 'package:tsacdop/class/audiostate.dart';
import 'package:tsacdop/class/episodebrief.dart';
@ -332,7 +333,7 @@ class _RecentUpdate extends StatefulWidget {
}
class _RecentUpdateState extends State<_RecentUpdate>
with AutomaticKeepAliveClientMixin , SingleTickerProviderStateMixin{
with AutomaticKeepAliveClientMixin, SingleTickerProviderStateMixin {
Future<List<EpisodeBrief>> _getRssItem(int top, List<String> group) async {
var dbHelper = DBHelper();
List<EpisodeBrief> episodes;
@ -343,6 +344,16 @@ class _RecentUpdateState extends State<_RecentUpdate>
return episodes;
}
Future<int> _getUpdateCounts(List<String> group) async {
var dbHelper = DBHelper();
List<EpisodeBrief> episodes = [];
if (group.first == 'All')
episodes = await dbHelper.getRecentNewRssItem();
else
episodes = await dbHelper.getGroupNewRssItem(group);
return episodes.length;
}
_loadMoreEpisode() async {
if (mounted) setState(() => _loadMore = true);
await Future.delayed(Duration(seconds: 3));
@ -370,6 +381,7 @@ class _RecentUpdateState extends State<_RecentUpdate>
@override
Widget build(BuildContext context) {
super.build(context);
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
return FutureBuilder<List<EpisodeBrief>>(
future: _getRssItem(_top, _group),
builder: (context, snapshot) {
@ -450,9 +462,38 @@ class _RecentUpdateState extends State<_RecentUpdate>
),
),
Spacer(),
FutureBuilder<int>(
future: _getUpdateCounts(_group),
initialData: 0,
builder: (context, snapshot) {
return snapshot.data > 0
? Material(
color: Colors.transparent,
child: IconButton(
tooltip:
'Add new episodes to playlist',
icon: Icon(
LineIcons.tasks_solid),
onPressed: () async {
await audio
.addNewEpisode(_group);
if (mounted)
setState(() {});
Fluttertoast.showToast(
msg: _groupName == 'All'
? '${snapshot.data} episode added to playlist'
: '${snapshot.data} episode in $_groupName added to playlist',
gravity:
ToastGravity.BOTTOM,
);
}),
)
: Center();
}),
Material(
color: Colors.transparent,
child: IconButton(
tooltip: 'Change layout',
padding: EdgeInsets.zero,
onPressed: () {
if (_layout == Layout.three)
@ -717,8 +758,8 @@ class _MyDownloadState extends State<_MyDownload>
child: Row(
children: <Widget>[
Container(
padding: EdgeInsets.symmetric(horizontal: 20),
child: Text('Downloaded')),
padding: EdgeInsets.symmetric(horizontal: 20),
child: Text('Downloaded')),
Spacer(),
Material(
color: Colors.transparent,
@ -777,4 +818,4 @@ class _MyDownloadState extends State<_MyDownload>
@override
bool get wantKeepAlive => true;
}
}

View File

@ -199,7 +199,10 @@ class _SlideIntroState extends State<SlideIntro> {
child: SizedBox(
height: 40,
width: 80,
child: Center(child: Text('Next'))))
child: Center(
child: Text('Next',
style: TextStyle(
color: Colors.black)))))
: InkWell(
borderRadius:
BorderRadius.all(Radius.circular(20)),
@ -217,7 +220,10 @@ class _SlideIntroState extends State<SlideIntro> {
child: SizedBox(
height: 40,
width: 80,
child: Center(child: Text('Done')))),
child: Center(
child: Text('Done',
style: TextStyle(
color: Colors.black))))),
),
),
],

View File

@ -3,10 +3,22 @@ import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:tsacdop/class/podcast_group.dart';
const String autoPlayKey = 'autoPlay';
const String autoAddKey = 'autoAdd';
const String audioPositionKey = 'audioposition';
const String lastWorkKey = 'lastWork';
const String refreshdateKey = 'refreshdate';
const String themesKey = 'themes';
const String accentsKey = 'accents';
const String autoUpdateKey = 'autoupdate';
const String updateIntervalKey = 'updateInterval';
const String downloadUsingDataKey = 'downloadUsingData';
const String introKey = 'intro';
const String realDarkKey = 'realDark';
class KeyValueStorage {
final String key;
KeyValueStorage(this.key);
Future<List<GroupEntity>> getGroups() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
if (prefs.getString(key) == null) {
@ -15,15 +27,16 @@ class KeyValueStorage {
key,
json.encode({
'groups': [home.toEntity().toJson()]
}));}
print(prefs.getString(key));
return json
.decode(prefs.getString(key))['groups']
.cast<Map<String, Object>>()
.map<GroupEntity>(GroupEntity.fromJson)
.toList(growable: false);
}));
}
print(prefs.getString(key));
return json
.decode(prefs.getString(key))['groups']
.cast<Map<String, Object>>()
.map<GroupEntity>(GroupEntity.fromJson)
.toList(growable: false);
}
Future<bool> saveGroup(List<GroupEntity> groupList) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
return prefs.setString(
@ -32,37 +45,40 @@ class KeyValueStorage {
{'groups': groupList.map((group) => group.toJson()).toList()}));
}
Future<bool> saveInt(int setting) async{
Future<bool> saveInt(int setting) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
return prefs.setInt(key, setting);
}
Future<int> getInt() async{
Future<int> getInt() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
if(prefs.getInt(key) == null) await prefs.setInt(key, 0);
if (prefs.getInt(key) == null) await prefs.setInt(key, 0);
return prefs.getInt(key);
}
Future<bool> saveStringList(List<String> playList) async{
Future<bool> saveStringList(List<String> playList) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
return prefs.setStringList(key, playList);
}
Future<List<String>> getStringList() async{
Future<List<String>> getStringList() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
if(prefs.getStringList(key) == null) {await prefs.setStringList(key, []);}
if (prefs.getStringList(key) == null) {
await prefs.setStringList(key, []);
}
return prefs.getStringList(key);
}
Future<bool> saveString(String string) async{
Future<bool> saveString(String string) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
return prefs.setString(key, string);
}
Future<String> getString() async{
Future<String> getString() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
if(prefs.getString(key) == null) {await prefs.setString(key, '');}
if (prefs.getString(key) == null) {
await prefs.setString(key, '');
}
return prefs.getString(key);
}
}

View File

@ -22,7 +22,7 @@ class DBHelper {
var documentsDirectory = await getDatabasesPath();
String path = join(documentsDirectory, "podcasts.db");
Database theDb = await openDatabase(path,
version: 1, onCreate: _onCreate, onUpgrade: _onUpgrade);
version: 2, onCreate: _onCreate, onUpgrade: _onUpgrade);
return theDb;
}
@ -32,7 +32,7 @@ class DBHelper {
imageUrl TEXT,rssUrl TEXT UNIQUE, primaryColor TEXT, author TEXT,
description TEXT, add_date INTEGER, imagePath TEXT, provider TEXT, link TEXT,
background_image TEXT DEFAULT '',hosts TEXT DEFAULT '',update_count INTEGER DEFAULT 0,
episode_count INTEGER DEFAULT 0)""");
episode_count INTEGER DEFAULT 0, skip_seconds INTEGER DEFAULT 0)""");
await db
.execute("""CREATE TABLE Episodes(id INTEGER PRIMARY KEY,title TEXT,
enclosure_url TEXT UNIQUE, enclosure_length INTEGER, pubDate TEXT,
@ -50,9 +50,8 @@ class DBHelper {
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");
await db.execute(
"ALTER TABLE PodcastLocal ADD skip_seconds INTEGER DEFAULT 0");
}
}
@ -111,6 +110,19 @@ class DBHelper {
return [list.first['update_count'], list.first['episode_count']];
}
Future<int> getSkipSeconds(String id) async {
var dbClient = await database;
List<Map> list = await dbClient
.rawQuery('SELECT skip_seconds FROM PodcastLocal WHERE id = ?', [id]);
return list.first['skip_seconds'];
}
Future<int> saveSkipSeconds(String id, int seconds) async {
var dbClient = await database;
return await dbClient.rawUpdate(
"UPDATE PodcastLocal SET skip_seconds = ? WHERE id = ?", [seconds, id]);
}
Future<bool> checkPodcast(String url) async {
var dbClient = await database;
List<Map> list = await dbClient
@ -298,7 +310,6 @@ class DBHelper {
DateTime _parsePubDate(String pubDate) {
if (pubDate == null) return DateTime.now();
print(pubDate);
DateTime date;
RegExp yyyy = RegExp(r'[1-2][0-9]{3}');
RegExp hhmm = RegExp(r'[0-2][0-9]\:[0-5][0-9]');
@ -338,12 +349,15 @@ class DBHelper {
print(month);
print(date.toString());
} else {
date = DateTime.now().toUtc();
date = DateTime.now();
}
}
}
}
return date.add(Duration(hours: timezoneInt));
DateTime result = date
.add(Duration(hours: timezoneInt))
.add(DateTime.now().timeZoneOffset);
return result;
}
int _getExplicit(bool b) {
@ -394,8 +408,8 @@ class DBHelper {
final title = feed.items[i].itunes.title ?? feed.items[i].title;
final length = feed.items[i]?.enclosure?.length;
final pubDate = feed.items[i].pubDate;
print(pubDate);
final date = _parsePubDate(pubDate);
print(date);
final milliseconds = date.millisecondsSinceEpoch;
final duration = feed.items[i].itunes.duration?.inSeconds ?? 0;
final explicit = _getExplicit(feed.items[i].itunes.explicit);
@ -429,78 +443,82 @@ class DBHelper {
return result;
}
Future<int> updatePodcastRss(PodcastLocal podcastLocal) async {
BaseOptions options = new BaseOptions(
Future<int> updatePodcastRss(PodcastLocal podcastLocal,
{int removeMark = 0}) async {
BaseOptions options = BaseOptions(
connectTimeout: 20000,
receiveTimeout: 20000,
);
Response response = await Dio(options).get(podcastLocal.rssUrl);
if (response.statusCode == 200) {
var feed = RssFeed.parse(response.data);
String url, description;
feed.items.removeWhere((item) => item == null);
int result = feed.items.length;
try {
Response response = await Dio(options).get(podcastLocal.rssUrl);
if (response.statusCode == 200) {
var feed = RssFeed.parse(response.data);
String url, description;
feed.items.removeWhere((item) => item == null);
int result = feed.items.length;
var dbClient = await database;
int count = Sqflite.firstIntValue(await dbClient.rawQuery(
'SELECT COUNT(*) FROM Episodes WHERE feed_id = ?',
[podcastLocal.id]));
var dbClient = await database;
int count = Sqflite.firstIntValue(await dbClient.rawQuery(
'SELECT COUNT(*) FROM Episodes WHERE feed_id = ?',
[podcastLocal.id]));
if (removeMark == 0)
await dbClient.rawUpdate(
"UPDATE Episodes SET is_new = 0 WHERE feed_id = ?",
[podcastLocal.id]);
for (int i = 0; i < result; i++) {
print(feed.items[i].title);
description = _getDescription(
feed.items[i].content.value ?? '',
feed.items[i].description ?? '',
feed.items[i].itunes.summary ?? '');
await dbClient.rawUpdate(
"UPDATE Episodes SET is_new = 0 WHERE feed_id = ?",
[podcastLocal.id]);
if (feed.items[i].enclosure?.url != null) {
_isXimalaya(feed.items[i].enclosure.url)
? url = feed.items[i].enclosure.url.split('=').last
: url = feed.items[i].enclosure.url;
}
for (int i = 0; i < result; i++) {
print(feed.items[i].title);
description = _getDescription(
feed.items[i].content.value ?? '',
feed.items[i].description ?? '',
feed.items[i].itunes.summary ?? '');
final title = feed.items[i].itunes.title ?? feed.items[i].title;
final length = feed.items[i]?.enclosure?.length ?? 0;
final pubDate = feed.items[i].pubDate;
final date = _parsePubDate(pubDate);
final milliseconds = date.millisecondsSinceEpoch;
final duration = feed.items[i].itunes.duration?.inSeconds ?? 0;
final explicit = _getExplicit(feed.items[i].itunes.explicit);
if (feed.items[i].enclosure?.url != null) {
_isXimalaya(feed.items[i].enclosure.url)
? url = feed.items[i].enclosure.url.split('=').last
: url = feed.items[i].enclosure.url;
}
final title = feed.items[i].itunes.title ?? feed.items[i].title;
final length = feed.items[i]?.enclosure?.length ?? 0;
final pubDate = feed.items[i].pubDate;
final date = _parsePubDate(pubDate);
final milliseconds = date.millisecondsSinceEpoch;
final duration = feed.items[i].itunes.duration?.inSeconds ?? 0;
final explicit = _getExplicit(feed.items[i].itunes.explicit);
if (url != null) {
await dbClient.transaction((txn) async {
await txn.rawInsert(
"""INSERT OR IGNORE INTO Episodes(title, enclosure_url, enclosure_length, pubDate,
if (url != null) {
await dbClient.transaction((txn) async {
await txn.rawInsert(
"""INSERT OR IGNORE INTO Episodes(title, enclosure_url, enclosure_length, pubDate,
description, feed_id, milliseconds, duration, explicit, media_id, is_new) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1)""",
[
title,
url,
length,
pubDate,
description,
podcastLocal.id,
milliseconds,
duration,
explicit,
url,
]);
});
[
title,
url,
length,
pubDate,
description,
podcastLocal.id,
milliseconds,
duration,
explicit,
url,
]);
});
}
}
}
int countUpdate = Sqflite.firstIntValue(await dbClient.rawQuery(
'SELECT COUNT(*) FROM Episodes WHERE feed_id = ?',
[podcastLocal.id]));
int countUpdate = Sqflite.firstIntValue(await dbClient.rawQuery(
'SELECT COUNT(*) FROM Episodes WHERE feed_id = ?',
[podcastLocal.id]));
await dbClient.rawUpdate(
"""UPDATE PodcastLocal SET update_count = ?, episode_count = ? WHERE id = ?""",
[countUpdate - count, countUpdate, podcastLocal.id]);
return countUpdate - count;
} else {
throw ("network error");
await dbClient.rawUpdate(
"""UPDATE PodcastLocal SET update_count = ?, episode_count = ? WHERE id = ?""",
[countUpdate - count, countUpdate, podcastLocal.id]);
return countUpdate - count;
}
return 0;
} catch (e) {
print(e);
return 0;
}
}
@ -511,7 +529,7 @@ class DBHelper {
List<Map> list = await dbClient
.rawQuery("""SELECT E.title, E.enclosure_url, E.enclosure_length,
E.milliseconds, P.imagePath, P.title as feedTitle, E.duration, E.explicit, E.liked,
E.downloaded, P.primaryColor , E.media_id, E.is_new
E.downloaded, P.primaryColor , E.media_id, E.is_new, P.skip_seconds
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++) {
@ -528,13 +546,14 @@ class DBHelper {
list[x]['explicit'],
list[x]['imagePath'],
list[x]['media_id'],
list[x]['is_new']));
list[x]['is_new'],
list[x]['skip_seconds']));
}
} 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
E.downloaded, P.primaryColor , E.media_id, E.is_new, P.skip_seconds
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++) {
@ -551,21 +570,64 @@ class DBHelper {
list[x]['explicit'],
list[x]['imagePath'],
list[x]['media_id'],
list[x]['is_new']));
list[x]['is_new'],
list[x]['skip_seconds']));
}
}
return episodes;
}
Future<List<EpisodeBrief>> getNewEpisodes(String id) async {
var dbClient = await database;
List<EpisodeBrief> episodes = [];
List<Map> list;
if (id == 'all')
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, P.skip_seconds
FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
WHERE E.is_new = 1 ORDER BY E.milliseconds ASC""",
);
else
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, P.skip_seconds
FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
WHERE E.is_new = 1 AND E.feed_id = ? ORDER BY E.milliseconds ASC""",
[id]);
if (list.length > 0)
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'],
list[x]['skip_seconds']));
}
return episodes;
}
Future<List<EpisodeBrief>> getRssItemTop(String id) 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, P.title as feed_title, E.duration, E.explicit, E.liked,
E.downloaded, P.primaryColor, E.media_id, E.is_new
E.downloaded, P.primaryColor, E.media_id, E.is_new, P.skip_seconds
FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
where E.feed_id = ? ORDER BY E.milliseconds DESC LIMIT 3""", [id]);
where E.feed_id = ? ORDER BY E.milliseconds DESC LIMIT 2""", [id]);
for (int x = 0; x < list.length; x++) {
episodes.add(EpisodeBrief(
list[x]['title'],
@ -580,7 +642,8 @@ class DBHelper {
list[x]['explicit'],
list[x]['imagePath'],
list[x]['media_id'],
list[x]['is_new']));
list[x]['is_new'],
list[x]['skip_seconds']));
}
return episodes;
}
@ -591,7 +654,7 @@ class DBHelper {
List<Map> list = await dbClient.rawQuery(
"""SELECT E.title, E.enclosure_url, E.enclosure_length,
E.milliseconds, P.imagePath, P.title as feed_title, E.duration, E.explicit, E.liked,
E.downloaded, P.primaryColor, E.media_id, E.is_new
E.downloaded, P.primaryColor, E.media_id, E.is_new, P.skip_seconds
FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
where E.enclosure_url = ? ORDER BY E.milliseconds DESC LIMIT 3""",
[url]);
@ -610,7 +673,8 @@ class DBHelper {
list.first['explicit'],
list.first['imagePath'],
list.first['media_id'],
list.first['is_new']);
list.first['is_new'],
list.first['skip_seconds']);
return episode;
}
@ -620,7 +684,7 @@ class DBHelper {
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
E.downloaded, P.imagePath, P.primaryColor, E.media_id, E.is_new, P.skip_seconds
FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
ORDER BY E.milliseconds DESC LIMIT ? """, [top]);
for (int x = 0; x < list.length; x++) {
@ -632,12 +696,13 @@ class DBHelper {
list[x]['feed_title'],
list[x]['primaryColor'],
list[x]['liked'],
list[x]['doanloaded'],
list[x]['downloaded'],
list[x]['duration'],
list[x]['explicit'],
list[x]['imagePath'],
list[x]['media_id'],
list[x]['is_new']));
list[x]['is_new'],
list[x]['skip_seconds']));
}
return episodes;
}
@ -651,7 +716,7 @@ class DBHelper {
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
E.downloaded, P.imagePath, P.primaryColor, E.media_id, E.is_new, P.skip_seconds
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]);
@ -664,17 +729,97 @@ class DBHelper {
list[x]['feed_title'],
list[x]['primaryColor'],
list[x]['liked'],
list[x]['doanloaded'],
list[x]['downloaded'],
list[x]['duration'],
list[x]['explicit'],
list[x]['imagePath'],
list[x]['media_id'],
list[x]['is_new']));
list[x]['is_new'],
list[x]['skip_seconds']));
}
}
return episodes;
}
Future<List<EpisodeBrief>> getRecentNewRssItem() async {
var dbClient = await database;
List<EpisodeBrief> episodes = [];
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, P.skip_seconds
FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
WHERE is_new = 1 ORDER BY E.milliseconds DESC """,
);
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'],
list[x]['skip_seconds']));
}
return episodes;
}
Future<int> removeAllNewMark() async {
var dbClient = await database;
return await dbClient.rawUpdate("UPDATE Episodes SET is_new = 0 ");
}
Future<List<EpisodeBrief>> getGroupNewRssItem(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, P.skip_seconds
FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
WHERE P.id in (${s.join(',')}) AND is_new = 1
ORDER BY E.milliseconds DESC""",
);
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'],
list[x]['skip_seconds']));
}
}
return episodes;
}
Future<int> removeGroupNewMark(List<String> group) async {
var dbClient = await database;
if (group.length > 0) {
List<String> s = group.map<String>((e) => "'$e'").toList();
return await dbClient.rawUpdate(
"UPDATE Episodes SET is_new = 0 WHERE feed_id in (${s.join(',')})");
}
return 0;
}
Future<List<EpisodeBrief>> getLikedRssItem(int i, int sortBy) async {
var dbClient = await database;
List<EpisodeBrief> episodes = List();
@ -682,7 +827,7 @@ class DBHelper {
List<Map> list = await dbClient.rawQuery(
"""SELECT E.title, E.enclosure_url, E.enclosure_length, E.milliseconds, P.imagePath,
P.title as feed_title, E.duration, E.explicit, E.liked, E.downloaded,
P.primaryColor, E.media_id, E.is_new FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
P.primaryColor, E.media_id, E.is_new, P.skip_seconds 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(
@ -698,13 +843,14 @@ class DBHelper {
list[x]['explicit'],
list[x]['imagePath'],
list[x]['media_id'],
list[x]['is_new']));
list[x]['is_new'],
list[x]['skip_seconds']));
}
} 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
P.primaryColor, E.media_id, E.is_new, P.skip_seconds 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(
@ -720,7 +866,8 @@ class DBHelper {
list[x]['explicit'],
list[x]['imagePath'],
list[x]['media_id'],
list[x]['is_new']));
list[x]['is_new'],
list[x]['skip_seconds']));
}
}
return episodes;
@ -782,7 +929,7 @@ class DBHelper {
List<Map> list = await dbClient.rawQuery(
"""SELECT E.title, E.enclosure_url, E.enclosure_length, E.milliseconds, P.imagePath,
P.title as feed_title, E.duration, E.explicit, E.liked, E.downloaded,
P.primaryColor, E.media_id, E.is_new FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
P.primaryColor, E.media_id, E.is_new, P.skip_seconds FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
WHERE E.downloaded != 'ND' ORDER BY E.download_date DESC""");
for (int x = 0; x < list.length; x++) {
episodes.add(EpisodeBrief(
@ -798,7 +945,8 @@ class DBHelper {
list[x]['explicit'],
list[x]['imagePath'],
list[x]['media_id'],
list[x]['is_new']));
list[x]['is_new'],
list[x]['skip_seconds']));
}
return episodes;
}
@ -825,7 +973,7 @@ class DBHelper {
List<Map> list = await dbClient.rawQuery(
"""SELECT E.title, E.enclosure_url, E.enclosure_length, E.milliseconds, P.imagePath,
P.title as feed_title, E.duration, E.explicit, E.liked, E.downloaded,
P.primaryColor, E.media_id, E.is_new FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
P.primaryColor, E.media_id, E.is_new, P.skip_seconds FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
WHERE E.enclosure_url = ?""", [url]);
if (list.length == 0) {
return null;
@ -843,7 +991,8 @@ class DBHelper {
list.first['explicit'],
list.first['imagePath'],
list.first['media_id'],
list.first['is_new']);
list.first['is_new'],
list.first['skip_seconds']);
return episode;
}
}
@ -854,7 +1003,7 @@ class DBHelper {
List<Map> list = await dbClient.rawQuery(
"""SELECT E.title, E.enclosure_url, E.enclosure_length, E.milliseconds, P.imagePath,
P.title as feed_title, E.duration, E.explicit, E.liked, E.downloaded,
P.primaryColor, E.media_id, E.is_new FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
P.primaryColor, E.media_id, E.is_new, P.skip_seconds FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
WHERE E.media_id = ?""", [id]);
episode = EpisodeBrief(
list.first['title'],
@ -869,7 +1018,8 @@ class DBHelper {
list.first['explicit'],
list.first['imagePath'],
list.first['media_id'],
list.first['is_new']);
list.first['is_new'],
list.first['skip_seconds']);
return episode;
}
}

View File

@ -12,17 +12,17 @@ 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';
import 'package:tsacdop/class/episodebrief.dart';
import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
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';
import '../class/podcastlocal.dart';
import '../class/episodebrief.dart';
import '../local_storage/sqflite_localpodcast.dart';
import '../util/episodegrid.dart';
import '../home/audioplayer.dart';
import '../class/fireside_data.dart';
import '../util/colorize.dart';
import '../util/context_extension.dart';
import '../util/custompaint.dart';
class PodcastDetail extends StatefulWidget {
PodcastDetail({Key key, this.podcastLocal}) : super(key: key);
@ -215,7 +215,10 @@ class _PodcastDetailState extends State<PodcastDetail> {
child: RefreshIndicator(
key: _refreshIndicatorKey,
color: Theme.of(context).accentColor,
onRefresh: () => _updateRssItem(widget.podcastLocal),
onRefresh: () async {
await _updateRssItem(widget.podcastLocal);
// audio.addNewEpisode(widget.podcastLocal.id);
},
child: Stack(
children: <Widget>[
Column(

View File

@ -1,4 +1,5 @@
import 'dart:io';
import 'dart:math' as math;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
@ -6,11 +7,14 @@ import 'package:flutter/services.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:provider/provider.dart';
import 'package:tsacdop/class/podcast_group.dart';
import 'package:tsacdop/class/podcastlocal.dart';
import 'package:tsacdop/podcasts/podcastdetail.dart';
import 'package:tsacdop/util/pageroute.dart';
import 'package:tsacdop/util/colorize.dart';
import '../class/podcast_group.dart';
import '../class/podcastlocal.dart';
import '../local_storage/sqflite_localpodcast.dart';
import '../podcasts/podcastdetail.dart';
import '../util/pageroute.dart';
import '../util/colorize.dart';
import '../util/duraiton_picker.dart';
import '../util/context_extension.dart';
class PodcastGroupList extends StatefulWidget {
final PodcastGroup group;
@ -67,11 +71,32 @@ class PodcastCard extends StatefulWidget {
_PodcastCardState createState() => _PodcastCardState();
}
class _PodcastCardState extends State<PodcastCard> {
class _PodcastCardState extends State<PodcastCard>
with SingleTickerProviderStateMixin {
bool _loadMenu;
bool _addGroup;
List<PodcastGroup> _selectedGroups;
List<PodcastGroup> _belongGroups;
AnimationController _controller;
Animation _animation;
double _value;
int _seconds;
Future<int> getSkipSecond(String id) async {
var dbHelper = DBHelper();
int seconds = await dbHelper.getSkipSeconds(id);
return seconds;
}
saveSkipSeconds(String id, int seconds) async {
var dbHelper = DBHelper();
await dbHelper.saveSkipSeconds(id, seconds);
}
String _stringForSeconds(double seconds) {
if (seconds == null) return null;
return '${(seconds ~/ 60)}:${(seconds.truncate() % 60).toString().padLeft(2, '0')}';
}
@override
void initState() {
@ -79,6 +104,16 @@ class _PodcastCardState extends State<PodcastCard> {
_loadMenu = false;
_addGroup = false;
_selectedGroups = [widget.group];
_value = 0;
_seconds = 0;
_controller =
AnimationController(vsync: this, duration: Duration(milliseconds: 300));
_animation = Tween<double>(begin: 0.0, end: 1.0).animate(_controller)
..addListener(() {
setState(() {
_value = _animation.value;
});
});
}
Widget _buttonOnMenu(Widget widget, VoidCallback onTap) => Material(
@ -112,7 +147,15 @@ class _PodcastCardState extends State<PodcastCard> {
mainAxisSize: MainAxisSize.min,
children: <Widget>[
InkWell(
onTap: () => setState(() => _loadMenu = !_loadMenu),
onTap: () => setState(
() {
_loadMenu = !_loadMenu;
if (_value == 0)
_controller.forward();
else
_controller.reverse();
},
),
child: Container(
padding: EdgeInsets.symmetric(horizontal: 12),
height: 100,
@ -162,12 +205,30 @@ class _PodcastCardState extends State<PodcastCard> {
child: Text(group.name));
}).toList(),
),
FutureBuilder<int>(
future: getSkipSecond(widget.podcastLocal.id),
initialData: 0,
builder: (context, snapshot) {
return snapshot.data == 0
? Center()
: Container(
alignment: Alignment.centerLeft,
child: Text('Skip ' +
_stringForSeconds(
snapshot.data.toDouble())),
);
},
),
],
)),
Spacer(),
Icon(_loadMenu
? Icons.keyboard_arrow_up
: Icons.keyboard_arrow_down),
Transform.rotate(
angle: math.pi * _value,
child: Icon(Icons.keyboard_arrow_down),
),
// Icon(_loadMenu
// ? Icons.keyboard_arrow_up
// : Icons.keyboard_arrow_down),
Padding(
padding: EdgeInsets.symmetric(horizontal: 5.0),
),
@ -262,7 +323,7 @@ class _PodcastCardState extends State<PodcastCard> {
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
_buttonOnMenu(
Icon(Icons.fullscreen),
Icon(Icons.fullscreen, size: 20 * _value),
() => Navigator.push(
context,
ScaleRoute(
@ -270,16 +331,90 @@ class _PodcastCardState extends State<PodcastCard> {
podcastLocal: widget.podcastLocal,
)),
)),
_buttonOnMenu(Icon(Icons.add), () {
_buttonOnMenu(Icon(Icons.add, size: 20 * _value),
() {
setState(() {
_addGroup = true;
});
}),
// _buttonOnMenu(Icon(Icons.notifications), () {}),
_buttonOnMenu(
Icon(
Icons.fast_forward,
size: 20 * (_value),
), () {
showGeneralDialog(
context: context,
barrierDismissible: true,
barrierLabel:
MaterialLocalizations.of(context)
.modalBarrierDismissLabel,
barrierColor: Colors.black54,
transitionDuration:
const Duration(milliseconds: 200),
pageBuilder: (BuildContext context,
Animation animaiton,
Animation secondaryAnimation) =>
AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle(
statusBarIconBrightness: Brightness.light,
systemNavigationBarColor:
Theme.of(context).brightness ==
Brightness.light
? Color.fromRGBO(113, 113, 113, 1)
: Color.fromRGBO(15, 15, 15, 1),
),
child: AlertDialog(
elevation: 1,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(10.0))),
titlePadding: EdgeInsets.only(
top: 20,
left: 20,
right: 100,
bottom: 20),
title: Text('Skip seconds at the beginning'),
content: DurationPicker(
duration: Duration.zero,
onChange: (value) =>
_seconds = value.inSeconds,
),
// content: Text('test'),
actionsPadding: EdgeInsets.all(10),
actions: <Widget>[
FlatButton(
onPressed: () {
Navigator.of(context).pop();
_seconds = 0;
},
child: Text(
'CANCEL',
style: TextStyle(
color: Colors.grey[600]),
),
),
FlatButton(
onPressed: () {
Navigator.of(context).pop();
saveSkipSeconds(
widget.podcastLocal.id,
_seconds);
},
child: Text(
'CONFIRM',
style: TextStyle(color: context.accentColor),
),
)
],
),
),
);
}),
_buttonOnMenu(
Icon(
Icons.delete,
color: Colors.red,
size: 20 * (_value),
), () {
showGeneralDialog(
context: context,

View File

@ -164,7 +164,8 @@ class _PodcastManageState extends State<PodcastManage>
),
body: WillPopScope(
onWillPop: () async {
await Provider.of<GroupList>(context, listen: false).clearOrderChanged();
await Provider.of<GroupList>(context, listen: false)
.clearOrderChanged();
return true;
},
child: Consumer<GroupList>(builder: (_, groupList, __) {
@ -347,17 +348,6 @@ class _PodcastManageState extends State<PodcastManage>
15,
15,
1),
// statusBarColor: Theme.of(
// context)
// .brightness ==
// Brightness.light
// ? Color.fromRGBO(
// 113,
// 113,
// 113,
// 1)
// : Color.fromRGBO(
// 5, 5, 5, 1),
),
child: AlertDialog(
elevation: 1,
@ -375,7 +365,8 @@ class _PodcastManageState extends State<PodcastManage>
title: Text(
'Delete confirm'),
content: Text(
'Are you sure you want to delete this group? Podcasts will be moved to Home group.'),
'Are you sure you want to delete this group?' +
'Podcasts will be moved to Home group.'),
actions: <Widget>[
FlatButton(
onPressed: () =>
@ -533,9 +524,6 @@ class _AddGroupState extends State<AddGroup> {
Theme.of(context).brightness == Brightness.light
? Color.fromRGBO(113, 113, 113, 1)
: Color.fromRGBO(5, 5, 5, 1),
// statusBarColor: Theme.of(context).brightness == Brightness.light
// ? Color.fromRGBO(113, 113, 113, 1)
// : Color.fromRGBO(15, 15, 15, 1),
),
child: AlertDialog(
shape: RoundedRectangleBorder(

View File

@ -0,0 +1,89 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import '../class/audiostate.dart';
class PlaySetting extends StatelessWidget {
@override
Widget build(BuildContext context) {
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle(
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
systemNavigationBarColor: Theme.of(context).primaryColor,
systemNavigationBarIconBrightness:
Theme.of(context).accentColorBrightness,
),
child: Scaffold(
appBar: AppBar(
title: Text('Player Setting'),
elevation: 0,
backgroundColor: Theme.of(context).primaryColor,
),
body: SingleChildScrollView(
scrollDirection: Axis.vertical,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Padding(
padding: EdgeInsets.all(10.0),
),
Container(
height: 30.0,
padding: EdgeInsets.symmetric(horizontal: 80),
alignment: Alignment.centerLeft,
child: Text('Playlist',
style: Theme.of(context)
.textTheme
.bodyText1
.copyWith(color: Theme.of(context).accentColor)),
),
ListView(
physics: const BouncingScrollPhysics(),
shrinkWrap: true,
scrollDirection: Axis.vertical,
children: <Widget>[
ListTile(
contentPadding:
EdgeInsets.only(left: 80.0, right: 20, bottom: 0),
title: Text('Autoplay'),
subtitle: Text('Autoplay next episode in playlist'),
trailing: Selector<AudioPlayerNotifier, bool>(
selector: (_, audio) => audio.autoPlay,
builder: (_, data, __) => Switch(
value: data,
onChanged: (boo) => audio.autoPlaySwitch = boo),
),
),
Divider(height: 2),
// ListTile(
// contentPadding:
// EdgeInsets.only(left: 80.0, right: 20, bottom: 0),
// title: Text('Autoadd'),
// subtitle:
// Text('Autoadd new updated episodes to playlist'),
// trailing: Selector<AudioPlayerNotifier, bool>(
// selector: (_, audio) => audio.autoAdd,
// builder: (_, data, __) => Switch(
// value: data,
// onChanged: (boo) => audio.autoAddSwitch = boo),
// ),
// ),
// Divider(height: 2),
],
),
],
),
],
),
),
),
);
}
}

View File

@ -2,7 +2,6 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:path/path.dart';
import 'package:line_icons/line_icons.dart';
import 'package:tsacdop/class/podcastlocal.dart';
@ -11,7 +10,6 @@ import 'package:url_launcher/url_launcher.dart';
import 'package:path_provider/path_provider.dart';
import 'package:flutter_file_dialog/flutter_file_dialog.dart';
import 'package:tsacdop/class/audiostate.dart';
import 'package:tsacdop/util/ompl_build.dart';
import 'package:tsacdop/util/context_extension.dart';
import 'package:tsacdop/intro_slider/app_intro.dart';
@ -20,6 +18,7 @@ import 'storage.dart';
import 'history.dart';
import 'syncing.dart';
import 'libries.dart';
import 'play_setting.dart';
class Settings extends StatelessWidget {
_launchUrl(String url) async {
@ -46,7 +45,6 @@ class Settings extends StatelessWidget {
@override
Widget build(BuildContext context) {
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle(
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
@ -103,17 +101,21 @@ class Settings extends StatelessWidget {
),
Divider(height: 2),
ListTile(
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PlaySetting())),
contentPadding:
EdgeInsets.symmetric(horizontal: 25.0),
leading: Icon(LineIcons.play_circle),
title: Text('AutoPlay'),
subtitle: Text('Autoplay next episode in playlist'),
trailing: Selector<AudioPlayerNotifier, bool>(
selector: (_, audio) => audio.autoPlay,
builder: (_, data, __) => Switch(
value: data,
onChanged: (boo) => audio.autoPlaySwitch = boo),
),
title: Text('Play'),
subtitle: Text('Playlist and player'),
// trailing: Selector<AudioPlayerNotifier, bool>(
// selector: (_, audio) => audio.autoPlay,
// builder: (_, data, __) => Switch(
// value: data,
// onChanged: (boo) => audio.autoPlaySwitch = boo),
// ),
),
Divider(height: 2),
ListTile(
@ -160,7 +162,7 @@ class Settings extends StatelessWidget {
EdgeInsets.symmetric(horizontal: 25.0),
leading: Icon(LineIcons.file_code_solid),
title: Text('Export'),
subtitle: Text('Export ompl file'),
subtitle: Text('Export ompl file of all podcasts'),
),
Divider(height: 2),
],
@ -214,7 +216,7 @@ class Settings extends StatelessWidget {
Divider(height: 2),
ListTile(
onTap: () => _launchUrl(
'mailto:<tsacdop@stonegate.me>?subject=Tsacdop Feedback'),
'mailto:<tsacdop.app@gmail.com>?subject=Tsacdop Feedback'),
contentPadding:
EdgeInsets.symmetric(horizontal: 25.0),
leading: Icon(LineIcons.bug_solid),

View File

@ -55,7 +55,7 @@ class StorageSetting extends StatelessWidget {
MaterialPageRoute(
builder: (context) => DownloadsManage())),
contentPadding:
EdgeInsets.only(left: 80.0, right: 25),
EdgeInsets.only(left: 80.0, right: 25, bottom: 10),
title: Text('Ask before using cellular data'),
subtitle: Text(
'Ask to confirm when using cellular data to download episodes.'),

View File

@ -63,10 +63,10 @@ class SyncingSetting extends StatelessWidget {
}
},
contentPadding: EdgeInsets.only(
left: 80.0, right: 20, bottom: 20),
left: 80.0, right: 20, bottom: 10),
title: Text('Enable syncing'),
subtitle: Text(
'Refresh all podcasts in the background to get leatest episodes.'),
'Refresh all podcasts in the background to get leatest episodes'),
trailing: Switch(
value: data.item1,
onChanged: (boo) async {

View File

@ -1,464 +0,0 @@
//Fork from https://github.com/divyanshub024/day_night_switch
//Copyright https://github.com/divyanshub024
//Apache License 2.0 https://github.com/divyanshub024/day_night_switch/blob/master/LICENSE
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
const double _kTrackHeight = 80.0;
const double _kTrackWidth = 160.0;
const double _kTrackRadius = _kTrackHeight / 2.0;
const double _kThumbRadius = 36.0;
const double _kSwitchWidth =
_kTrackWidth - 2 * _kTrackRadius + 2 * kRadialReactionRadius;
const double _kSwitchHeight = 2 * kRadialReactionRadius + 8.0;
class DayNightSwitch extends StatefulWidget {
const DayNightSwitch({
@required this.value,
@required this.onChanged,
@required this.onDrag,
this.dragStartBehavior = DragStartBehavior.start,
this.height,
this.moonImage,
this.sunImage,
this.sunColor,
this.moonColor,
this.dayColor,
this.nightColor,
});
final bool value;
final ValueChanged<bool> onChanged;
final ValueChanged<double> onDrag;
final DragStartBehavior dragStartBehavior;
final double height;
final ImageProvider sunImage;
final ImageProvider moonImage;
final Color sunColor;
final Color moonColor;
final Color dayColor;
final Color nightColor;
@override
_DayNightSwitchState createState() => _DayNightSwitchState();
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(FlagProperty('value',
value: value, ifTrue: 'on', ifFalse: 'off', showName: true));
properties.add(ObjectFlagProperty<ValueChanged<bool>>(
'onChanged',
onChanged,
ifNull: 'disabled',
));
}
}
class _DayNightSwitchState extends State<DayNightSwitch>
with TickerProviderStateMixin {
@override
Widget build(BuildContext context) {
final Color moonColor = widget.moonColor ?? const Color(0xFFf5f3ce);
final Color nightColor = widget.nightColor ?? const Color(0xFF003366);
Color sunColor = widget.sunColor ?? const Color(0xFFFDB813);
Color dayColor = widget.dayColor ?? const Color(0xFF87CEEB);
return _SwitchRenderObjectWidget(
dragStartBehavior: widget.dragStartBehavior,
value: widget.value,
activeColor: moonColor,
inactiveColor: sunColor,
moonImage: widget.moonImage,
sunImage: widget.sunImage,
activeTrackColor: nightColor,
inactiveTrackColor: dayColor,
configuration: createLocalImageConfiguration(context),
onChanged: widget.onChanged,
onDrag: widget.onDrag,
additionalConstraints:
BoxConstraints.tight(Size(_kSwitchWidth, _kSwitchHeight)),
vsync: this,
);
}
}
class _SwitchRenderObjectWidget extends LeafRenderObjectWidget {
const _SwitchRenderObjectWidget({
Key key,
this.value,
this.activeColor,
this.inactiveColor,
this.moonImage,
this.sunImage,
this.activeTrackColor,
this.inactiveTrackColor,
this.configuration,
this.onChanged,
this.onDrag,
this.vsync,
this.additionalConstraints,
this.dragStartBehavior,
}) : super(key: key);
final bool value;
final Color activeColor;
final Color inactiveColor;
final ImageProvider moonImage;
final ImageProvider sunImage;
final Color activeTrackColor;
final Color inactiveTrackColor;
final ImageConfiguration configuration;
final ValueChanged<bool> onChanged;
final ValueChanged<double> onDrag;
final TickerProvider vsync;
final BoxConstraints additionalConstraints;
final DragStartBehavior dragStartBehavior;
@override
_RenderSwitch createRenderObject(BuildContext context) {
return _RenderSwitch(
dragStartBehavior: dragStartBehavior,
value: value,
activeColor: activeColor,
inactiveColor: inactiveColor,
moonImage: moonImage,
sunImage: sunImage,
activeTrackColor: activeTrackColor,
inactiveTrackColor: inactiveTrackColor,
configuration: configuration,
onChanged: onChanged,
onDrag: onDrag,
textDirection: Directionality.of(context),
additionalConstraints: additionalConstraints,
vSync: vsync,
);
}
@override
void updateRenderObject(BuildContext context, _RenderSwitch renderObject) {
renderObject
..value = value
..activeColor = activeColor
..inactiveColor = inactiveColor
..activeThumbImage = moonImage
..inactiveThumbImage = sunImage
..activeTrackColor = activeTrackColor
..inactiveTrackColor = inactiveTrackColor
..configuration = configuration
..onChanged = onChanged
..onDrag = onDrag
..textDirection = Directionality.of(context)
..additionalConstraints = additionalConstraints
..dragStartBehavior = dragStartBehavior
..vsync = vsync;
}
}
class _RenderSwitch extends RenderToggleable {
ValueChanged<double> onDrag;
_RenderSwitch({
bool value,
Color activeColor,
Color inactiveColor,
ImageProvider moonImage,
ImageProvider sunImage,
Color activeTrackColor,
Color inactiveTrackColor,
ImageConfiguration configuration,
BoxConstraints additionalConstraints,
@required TextDirection textDirection,
ValueChanged<bool> onChanged,
this.onDrag,
@required TickerProvider vSync,
DragStartBehavior dragStartBehavior,
}) : assert(textDirection != null),
_activeThumbImage = moonImage,
_inactiveThumbImage = sunImage,
_activeTrackColor = activeTrackColor,
_inactiveTrackColor = inactiveTrackColor,
_configuration = configuration,
_textDirection = textDirection,
super(
value: value,
tristate: false,
activeColor: activeColor,
inactiveColor: inactiveColor,
onChanged: onChanged,
additionalConstraints: additionalConstraints,
vsync: vSync,
) {
_drag = HorizontalDragGestureRecognizer()
..onStart = _handleDragStart
..onUpdate = _handleDragUpdate
..onEnd = _handleDragEnd
..dragStartBehavior = dragStartBehavior;
}
ImageProvider get activeThumbImage => _activeThumbImage;
ImageProvider _activeThumbImage;
set activeThumbImage(ImageProvider value) {
if (value == _activeThumbImage) return;
_activeThumbImage = value;
markNeedsPaint();
}
ImageProvider get inactiveThumbImage => _inactiveThumbImage;
ImageProvider _inactiveThumbImage;
set inactiveThumbImage(ImageProvider value) {
if (value == _inactiveThumbImage) return;
_inactiveThumbImage = value;
markNeedsPaint();
}
Color get activeTrackColor => _activeTrackColor;
Color _activeTrackColor;
set activeTrackColor(Color value) {
assert(value != null);
if (value == _activeTrackColor) return;
_activeTrackColor = value;
markNeedsPaint();
}
Color get inactiveTrackColor => _inactiveTrackColor;
Color _inactiveTrackColor;
set inactiveTrackColor(Color value) {
assert(value != null);
if (value == _inactiveTrackColor) return;
_inactiveTrackColor = value;
markNeedsPaint();
}
ImageConfiguration get configuration => _configuration;
ImageConfiguration _configuration;
set configuration(ImageConfiguration value) {
assert(value != null);
if (value == _configuration) return;
_configuration = value;
markNeedsPaint();
}
TextDirection get textDirection => _textDirection;
TextDirection _textDirection;
set textDirection(TextDirection value) {
assert(value != null);
if (_textDirection == value) return;
_textDirection = value;
markNeedsPaint();
}
DragStartBehavior get dragStartBehavior => _drag.dragStartBehavior;
set dragStartBehavior(DragStartBehavior value) {
assert(value != null);
if (_drag.dragStartBehavior == value) return;
_drag.dragStartBehavior = value;
}
@override
void detach() {
_cachedThumbPainter?.dispose();
_cachedThumbPainter = null;
super.detach();
}
double get _trackInnerLength => size.width - 2.0 * kRadialReactionRadius;
HorizontalDragGestureRecognizer _drag;
void _handleDragStart(DragStartDetails details) {
if (isInteractive) reactionController.forward();
}
void _handleDragUpdate(DragUpdateDetails details) {
if (isInteractive) {
position
..curve = null
..reverseCurve = null;
final double delta = details.primaryDelta / _trackInnerLength;
switch (textDirection) {
case TextDirection.rtl:
positionController.value -= delta;
break;
case TextDirection.ltr:
positionController.value += delta;
break;
}
positionController.addListener(() {
onDrag(positionController.value);
});
}
}
void _handleDragEnd(DragEndDetails details) {
if (position.value >= 0.5)
positionController.forward();
else
positionController.reverse();
reactionController.reverse();
}
@override
void handleEvent(PointerEvent event, BoxHitTestEntry entry) {
assert(debugHandleEvent(event, entry));
if (event is PointerDownEvent && onChanged != null) _drag.addPointer(event);
super.handleEvent(event, entry);
}
Color _cachedThumbColor;
ImageProvider _cachedThumbImage;
BoxPainter _cachedThumbPainter;
BoxDecoration _createDefaultThumbDecoration(
Color color, ImageProvider image) {
return BoxDecoration(
color: color,
image: image == null ? null : DecorationImage(image: image),
shape: BoxShape.circle,
boxShadow: kElevationToShadow[1],
);
}
bool _isPainting = false;
void _handleDecorationChanged() {
// If the image decoration is available synchronously, we'll get called here
// during paint. There's no reason to mark ourselves as needing paint if we
// are already in the middle of painting. (In fact, doing so would trigger
// an assert).
if (!_isPainting) markNeedsPaint();
}
@override
void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
config.isToggled = value == true;
}
@override
void paint(PaintingContext context, Offset offset) {
final Canvas canvas = context.canvas;
final bool isEnabled = onChanged != null;
final double currentValue = position.value;
double visualPosition;
switch (textDirection) {
case TextDirection.rtl:
visualPosition = 1.0 - currentValue;
break;
case TextDirection.ltr:
visualPosition = currentValue;
break;
}
final Color trackColor = isEnabled
? Color.lerp(inactiveTrackColor, activeTrackColor, currentValue)
: inactiveTrackColor;
final Color thumbColor = isEnabled
? Color.lerp(inactiveColor, activeColor, currentValue)
: inactiveColor;
final ImageProvider thumbImage = isEnabled
? (currentValue < 0.5 ? inactiveThumbImage : activeThumbImage)
: inactiveThumbImage;
// Paint the track
final Paint paint = Paint()..color = trackColor;
const double trackHorizontalPadding = kRadialReactionRadius - _kTrackRadius;
final Rect trackRect = Rect.fromLTWH(
offset.dx + trackHorizontalPadding,
offset.dy + (size.height - _kTrackHeight) / 2.0,
size.width - 2.0 * trackHorizontalPadding,
_kTrackHeight,
);
final RRect trackRRect = RRect.fromRectAndRadius(
trackRect, const Radius.circular(_kTrackRadius));
canvas.drawRRect(trackRRect, paint);
final Offset thumbPosition = Offset(
kRadialReactionRadius + visualPosition * _trackInnerLength,
size.height / 2.0,
);
paintRadialReaction(canvas, offset, thumbPosition);
var linePaint = Paint()
..color = Colors.white
..strokeWidth = 4 + (6 * (1 - currentValue))
..strokeCap = StrokeCap.round
..style = PaintingStyle.stroke;
canvas.drawLine(
Offset(offset.dx + _kSwitchWidth * 0.1, offset.dy),
Offset(
offset.dx +
(_kSwitchWidth * 0.1) +
(_kSwitchWidth / 2 * (1 - currentValue)),
offset.dy),
linePaint,
);
canvas.drawLine(
Offset(offset.dx + _kSwitchWidth * 0.2, offset.dy + _kSwitchHeight),
Offset(
offset.dx +
(_kSwitchWidth * 0.2) +
(_kSwitchWidth / 2 * (1 - currentValue)),
offset.dy + _kSwitchHeight),
linePaint,
);
var starPaint = Paint()
..strokeWidth = 2 + (6 * (1 - currentValue))
..strokeCap = StrokeCap.round
..style = PaintingStyle.stroke
..color = Color.fromARGB((255 * currentValue).floor(), 255, 255, 255);
canvas.drawLine(
Offset(offset.dx, offset.dy + _kSwitchHeight * 0.7),
Offset(offset.dx, offset.dy + _kSwitchHeight * 0.7),
starPaint,
);
try {
_isPainting = true;
BoxPainter thumbPainter;
if (_cachedThumbPainter == null ||
thumbColor != _cachedThumbColor ||
thumbImage != _cachedThumbImage) {
_cachedThumbColor = thumbColor;
_cachedThumbImage = thumbImage;
_cachedThumbPainter =
_createDefaultThumbDecoration(thumbColor, thumbImage)
.createBoxPainter(_handleDecorationChanged);
}
thumbPainter = _cachedThumbPainter;
// The thumb contracts slightly during the animation
final double inset = 1.0 - (currentValue - 0.5).abs() * 2.0;
final double radius = _kThumbRadius - inset;
thumbPainter.paint(
canvas,
thumbPosition + offset - Offset(radius, radius),
configuration.copyWith(size: Size.fromRadius(radius)),
);
} finally {
_isPainting = false;
}
canvas.drawLine(
Offset(offset.dx + _kSwitchWidth * 0.3, offset.dy + _kSwitchHeight * 0.5),
Offset(
offset.dx +
(_kSwitchWidth * 0.3) +
(_kSwitchWidth / 2 * (1 - currentValue)),
offset.dy + _kSwitchHeight * 0.5),
linePaint,
);
}
}

View File

@ -0,0 +1,663 @@
//Forked from https://github.com/cdharris/flutter_duration_picker
//Copyright https://github.com/cdharris
//License MIT https://github.com/cdharris/flutter_duration_picker/blob/master/LICENSE
import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
const Duration _kDialAnimateDuration = const Duration(milliseconds: 200);
const double _kDurationPickerWidthPortrait = 328.0;
const double _kDurationPickerWidthLandscape = 512.0;
const double _kDurationPickerHeightPortrait = 380.0;
const double _kDurationPickerHeightLandscape = 304.0;
const double _kTwoPi = 2 * math.pi;
const double _kPiByTwo = math.pi / 2;
const double _kCircleTop = _kPiByTwo;
class _DialPainter extends CustomPainter {
const _DialPainter({
@required this.context,
@required this.labels,
@required this.backgroundColor,
@required this.accentColor,
@required this.theta,
@required this.textDirection,
@required this.selectedValue,
@required this.pct,
@required this.multiplier,
@required this.secondHand,
});
final List<TextPainter> labels;
final Color backgroundColor;
final Color accentColor;
final double theta;
final TextDirection textDirection;
final int selectedValue;
final BuildContext context;
final double pct;
final int multiplier;
final int secondHand;
@override
void paint(Canvas canvas, Size size) {
const double _epsilon = .001;
const double _sweep = _kTwoPi - _epsilon;
const double _startAngle = -math.pi / 2.0;
final double radius = size.shortestSide / 2.0;
final Offset center = new Offset(size.width / 2.0, size.height / 2.0);
final Offset centerPoint = center;
double pctTheta = (0.25 - (theta % _kTwoPi) / _kTwoPi) % 1.0;
// Draw the background outer ring
canvas.drawCircle(
centerPoint, radius, new Paint()..color = backgroundColor);
// Draw a translucent circle for every hour
for (int i = 0; i < multiplier; i = i + 1) {
canvas.drawCircle(centerPoint, radius,
new Paint()..color = accentColor.withOpacity((i == 0) ? 0.3 : 0.1));
}
// Draw the inner background circle
canvas.drawCircle(centerPoint, radius * 0.88,
new Paint()..color = Theme.of(context).canvasColor);
// Get the offset point for an angle value of theta, and a distance of _radius
Offset getOffsetForTheta(double theta, double _radius) {
return center +
new Offset(_radius * math.cos(theta), -_radius * math.sin(theta));
}
// Draw the handle that is used to drag and to indicate the position around the circle
final Paint handlePaint = new Paint()..color = accentColor;
final Offset handlePoint = getOffsetForTheta(theta, radius - 10.0);
canvas.drawCircle(handlePoint, 20.0, handlePaint);
// Draw the Text in the center of the circle which displays hours and mins
String minutes = (multiplier == 0) ? '' : "${multiplier}min ";
// int minutes = (pctTheta * 60).round();
// minutes = minutes == 60 ? 0 : minutes;
String seconds = "$secondHand";
TextPainter textDurationValuePainter = new TextPainter(
textAlign: TextAlign.center,
text: new TextSpan(
text: '$minutes$seconds',
style: Theme.of(context)
.textTheme
.headline4
.copyWith(fontSize: size.shortestSide * 0.15)),
textDirection: TextDirection.ltr)
..layout();
Offset middleForValueText = new Offset(
centerPoint.dx - (textDurationValuePainter.width / 2),
centerPoint.dy - textDurationValuePainter.height / 2);
textDurationValuePainter.paint(canvas, middleForValueText);
TextPainter textMinPainter = new TextPainter(
textAlign: TextAlign.center,
text: new TextSpan(
text: 'sec', //th: ${theta}',
style: Theme.of(context).textTheme.bodyText1),
textDirection: TextDirection.ltr)
..layout();
textMinPainter.paint(
canvas,
new Offset(
centerPoint.dx - (textMinPainter.width / 2),
centerPoint.dy +
(textDurationValuePainter.height / 2) -
textMinPainter.height / 2));
// Draw an arc around the circle for the amount of the circle that has elapsed.
var elapsedPainter = new Paint()
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round
..color = accentColor.withOpacity(0.3)
..isAntiAlias = true
..strokeWidth = radius * 0.12;
canvas.drawArc(
new Rect.fromCircle(
center: centerPoint,
radius: radius - radius * 0.12 / 2,
),
_startAngle,
_sweep * pctTheta,
false,
elapsedPainter,
);
// Paint the labels (the minute strings)
void paintLabels(List<TextPainter> labels) {
if (labels == null) return;
final double labelThetaIncrement = -_kTwoPi / labels.length;
double labelTheta = _kPiByTwo;
for (TextPainter label in labels) {
final Offset labelOffset =
new Offset(-label.width / 2.0, -label.height / 2.0);
label.paint(
canvas, getOffsetForTheta(labelTheta, radius - 40.0) + labelOffset);
labelTheta += labelThetaIncrement;
}
}
paintLabels(labels);
}
@override
bool shouldRepaint(_DialPainter oldPainter) {
return oldPainter.labels != labels ||
oldPainter.backgroundColor != backgroundColor ||
oldPainter.accentColor != accentColor ||
oldPainter.theta != theta;
}
}
class _Dial extends StatefulWidget {
const _Dial(
{@required this.duration,
@required this.onChanged,
this.snapToMins = 1.0})
: assert(duration != null);
final Duration duration;
final ValueChanged<Duration> onChanged;
/// The resolution of mins of the dial, i.e. if snapToMins = 5.0, only durations of 5min intervals will be selectable.
final double snapToMins;
@override
_DialState createState() => new _DialState();
}
class _DialState extends State<_Dial> with SingleTickerProviderStateMixin {
@override
void initState() {
super.initState();
_thetaController = new AnimationController(
duration: _kDialAnimateDuration,
vsync: this,
);
_thetaTween =
new Tween<double>(begin: _getThetaForDuration(widget.duration));
_theta = _thetaTween.animate(new CurvedAnimation(
parent: _thetaController, curve: Curves.fastOutSlowIn))
..addListener(() => setState(() {}));
_thetaController.addStatusListener((status) {
// if (status == AnimationStatus.completed && _hours != _snappedHours) {
// _hours = _snappedHours;
if (status == AnimationStatus.completed) {
_minutes = _minuteHand(_turningAngle);
_seconds = _secondHand(_turningAngle);
setState(() {});
}
});
// _hours = widget.duration.inHours;
_turningAngle = _kPiByTwo - widget.duration.inSeconds / 60.0 * _kTwoPi;
_minutes = _minuteHand(_turningAngle);
_seconds = _secondHand(_turningAngle);
}
ThemeData themeData;
MaterialLocalizations localizations;
MediaQueryData media;
@override
void didChangeDependencies() {
super.didChangeDependencies();
assert(debugCheckHasMediaQuery(context));
themeData = Theme.of(context);
localizations = MaterialLocalizations.of(context);
media = MediaQuery.of(context);
}
@override
void dispose() {
_thetaController.dispose();
super.dispose();
}
Tween<double> _thetaTween;
Animation<double> _theta;
AnimationController _thetaController;
double _pct = 0.0;
int _seconds = 0;
bool _dragging = false;
int _minutes = 0;
double _turningAngle = 0.0;
static double _nearest(double target, double a, double b) {
return ((target - a).abs() < (target - b).abs()) ? a : b;
}
void _animateTo(double targetTheta) {
final double currentTheta = _theta.value;
double beginTheta =
_nearest(targetTheta, currentTheta, currentTheta + _kTwoPi);
beginTheta = _nearest(targetTheta, beginTheta, currentTheta - _kTwoPi);
_thetaTween
..begin = beginTheta
..end = targetTheta;
_thetaController
..value = 0.0
..forward();
}
double _getThetaForDuration(Duration duration) {
return (_kPiByTwo - (duration.inSeconds % 60) / 60.0 * _kTwoPi) % _kTwoPi;
}
Duration _getTimeForTheta(double theta) {
return _angleToDuration(_turningAngle);
}
Duration _notifyOnChangedIfNeeded() {
// final Duration current = _getTimeForTheta(_theta.value);
// var d = Duration(hours: _hours, minutes: current.inMinutes % 60);
_minutes = _minuteHand(_turningAngle);
_seconds = _secondHand(_turningAngle);
var d = _angleToDuration(_turningAngle);
widget.onChanged(d);
return d;
}
void _updateThetaForPan() {
setState(() {
final Offset offset = _position - _center;
final double angle =
(math.atan2(offset.dx, offset.dy) - _kPiByTwo) % _kTwoPi;
// Stop accidental abrupt pans from making the dial seem like it starts from 1h.
// (happens when wanting to pan from 0 clockwise, but when doing so quickly, one actually pans from before 0 (e.g. setting the duration to 59mins, and then crossing 0, which would then mean 1h 1min).
if (angle >= _kCircleTop &&
_theta.value <= _kCircleTop &&
_theta.value >= 0.1 && // to allow the radians sign change at 15mins.
_minutes == 0) return;
_thetaTween
..begin = angle
..end = angle;
});
}
Offset _position;
Offset _center;
void _handlePanStart(DragStartDetails details) {
assert(!_dragging);
_dragging = true;
final RenderBox box = context.findRenderObject();
_position = box.globalToLocal(details.globalPosition);
_center = box.size.center(Offset.zero);
//_updateThetaForPan();
_notifyOnChangedIfNeeded();
}
void _handlePanUpdate(DragUpdateDetails details) {
double oldTheta = _theta.value;
_position += details.delta;
_updateThetaForPan();
double newTheta = _theta.value;
// _updateRotations(oldTheta, newTheta);
_updateTurningAngle(oldTheta, newTheta);
_notifyOnChangedIfNeeded();
}
int _minuteHand(double angle) {
return _angleToDuration(angle).inMinutes.toInt();
}
int _secondHand(double angle) {
// Result is in [0; 59], even if overall time is >= 1 hour
return (_angleToSeconds(angle) % 60.0).toInt();
}
Duration _angleToDuration(double angle) {
return _secondToDuration(_angleToSeconds(angle));
}
Duration _secondToDuration(seconds) {
return Duration(
minutes: (seconds ~/ 60).toInt(), seconds: (seconds % 60.0).toInt());
}
double _angleToSeconds(double angle) {
// Coordinate transformation from mathematical COS to dial COS
double dialAngle = _kPiByTwo - angle;
// Turn dial angle into minutes, may go beyond 60 minutes (multiple turns)
return dialAngle / _kTwoPi * 60.0;
}
void _updateTurningAngle(double oldTheta, double newTheta) {
// Register any angle by which the user has turned the dial.
//
// The resulting turning angle fully captures the state of the dial,
// including multiple turns (= full hours). The [_turningAngle] is in
// mathematical coordinate system, i.e. 3-o-clock position being zero, and
// increasing counter clock wise.
// From positive to negative (in mathematical COS)
if (newTheta > 1.5 * math.pi && oldTheta < 0.5 * math.pi) {
_turningAngle = _turningAngle - ((_kTwoPi - newTheta) + oldTheta);
}
// From negative to positive (in mathematical COS)
else if (newTheta < 0.5 * math.pi && oldTheta > 1.5 * math.pi) {
_turningAngle = _turningAngle + ((_kTwoPi - oldTheta) + newTheta);
} else {
_turningAngle = _turningAngle + (newTheta - oldTheta);
}
}
void _handlePanEnd(DragEndDetails details) {
assert(_dragging);
_dragging = false;
_position = null;
_center = null;
//_notifyOnChangedIfNeeded();
//_animateTo(_getThetaForDuration(widget.duration));
}
void _handleTapUp(TapUpDetails details) {
final RenderBox box = context.findRenderObject();
_position = box.globalToLocal(details.globalPosition);
_center = box.size.center(Offset.zero);
_updateThetaForPan();
_notifyOnChangedIfNeeded();
_animateTo(_getThetaForDuration(_getTimeForTheta(_theta.value)));
_dragging = false;
_position = null;
_center = null;
}
List<TextPainter> _buildSeconds(TextTheme textTheme) {
final TextStyle style = textTheme.subtitle1;
const List<Duration> _secondsMarkerValues = const <Duration>[
const Duration(seconds: 0),
const Duration(seconds: 5),
const Duration(seconds: 10),
const Duration(seconds: 15),
const Duration(seconds: 20),
const Duration(seconds: 25),
const Duration(seconds: 30),
const Duration(seconds: 35),
const Duration(seconds: 40),
const Duration(seconds: 45),
const Duration(seconds: 50),
const Duration(seconds: 55),
];
final List<TextPainter> labels = <TextPainter>[];
for (Duration duration in _secondsMarkerValues) {
var painter = new TextPainter(
text: new TextSpan(style: style, text: duration.inSeconds.toString()),
textDirection: TextDirection.ltr,
)..layout();
labels.add(painter);
}
return labels;
}
@override
Widget build(BuildContext context) {
Color backgroundColor;
switch (themeData.brightness) {
case Brightness.light:
backgroundColor = Colors.grey[200];
break;
case Brightness.dark:
backgroundColor = themeData.backgroundColor;
break;
}
final ThemeData theme = Theme.of(context);
int selectedDialValue;
_minutes = _minuteHand(_turningAngle);
_seconds = _secondHand(_turningAngle);
return new GestureDetector(
excludeFromSemantics: true,
onPanStart: _handlePanStart,
onPanUpdate: _handlePanUpdate,
onPanEnd: _handlePanEnd,
onTapUp: _handleTapUp,
child: new CustomPaint(
painter: new _DialPainter(
pct: _pct,
multiplier: _minutes,
secondHand: _seconds,
context: context,
selectedValue: selectedDialValue,
labels: _buildSeconds(theme.textTheme),
backgroundColor: backgroundColor,
accentColor: themeData.accentColor,
theta: _theta.value,
textDirection: Directionality.of(context),
),
));
}
}
/// A duration picker designed to appear inside a popup dialog.
///
/// Pass this widget to [showDialog]. The value returned by [showDialog] is the
/// selected [Duration] if the user taps the "OK" button, or null if the user
/// taps the "CANCEL" button. The selected time is reported by calling
/// [Navigator.pop].
class _DurationPickerDialog extends StatefulWidget {
/// Creates a duration picker.
///
/// [initialTime] must not be null.
const _DurationPickerDialog(
{Key key, @required this.initialTime, this.snapToMins})
: assert(initialTime != null),
super(key: key);
/// The duration initially selected when the dialog is shown.
final Duration initialTime;
final double snapToMins;
@override
_DurationPickerDialogState createState() => new _DurationPickerDialogState();
}
class _DurationPickerDialogState extends State<_DurationPickerDialog> {
@override
void initState() {
super.initState();
_selectedDuration = widget.initialTime;
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
localizations = MaterialLocalizations.of(context);
}
Duration get selectedDuration => _selectedDuration;
Duration _selectedDuration;
MaterialLocalizations localizations;
void _handleTimeChanged(Duration value) {
setState(() {
_selectedDuration = value;
});
}
void _handleCancel() {
Navigator.pop(context);
}
void _handleOk() {
Navigator.pop(context, _selectedDuration);
}
@override
Widget build(BuildContext context) {
assert(debugCheckHasMediaQuery(context));
final ThemeData theme = Theme.of(context);
final Widget picker = new Padding(
padding: const EdgeInsets.all(16.0),
child: new AspectRatio(
aspectRatio: 1.0,
child: new _Dial(
duration: _selectedDuration,
onChanged: _handleTimeChanged,
snapToMins: widget.snapToMins,
)));
final Widget actions = new ButtonTheme.bar(
child: new ButtonBar(children: <Widget>[
new FlatButton(
child: new Text(localizations.cancelButtonLabel),
onPressed: _handleCancel),
new FlatButton(
child: new Text(localizations.okButtonLabel), onPressed: _handleOk),
]));
final Dialog dialog = new Dialog(child: new OrientationBuilder(
builder: (BuildContext context, Orientation orientation) {
final Widget pickerAndActions = new Container(
color: theme.dialogBackgroundColor,
child: new Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
new Expanded(
child:
picker), // picker grows and shrinks with the available space
actions,
],
),
);
assert(orientation != null);
switch (orientation) {
case Orientation.portrait:
return new SizedBox(
width: _kDurationPickerWidthPortrait,
height: _kDurationPickerHeightPortrait,
child: new Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
new Expanded(
child: pickerAndActions,
),
]));
case Orientation.landscape:
return new SizedBox(
width: _kDurationPickerWidthLandscape,
height: _kDurationPickerHeightLandscape,
child: new Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
new Flexible(
child: pickerAndActions,
),
]));
}
return null;
}));
return new Theme(
data: theme.copyWith(
dialogBackgroundColor: Colors.transparent,
),
child: dialog,
);
}
@override
void dispose() {
super.dispose();
}
}
/// Shows a dialog containing the duration picker.
///
/// The returned Future resolves to the duration selected by the user when the user
/// closes the dialog. If the user cancels the dialog, null is returned.
///
/// To show a dialog with [initialTime] equal to the current time:
///
/// ```dart
/// showDurationPicker(
/// initialTime: new Duration.now(),
/// context: context,
/// );
/// ```
Future<Duration> showDurationPicker(
{@required BuildContext context,
@required Duration initialTime,
double snapToMins}) async {
assert(context != null);
assert(initialTime != null);
return await showDialog<Duration>(
context: context,
builder: (BuildContext context) => new _DurationPickerDialog(
initialTime: initialTime, snapToMins: snapToMins),
);
}
class DurationPicker extends StatelessWidget {
final Duration duration;
final ValueChanged<Duration> onChange;
final double snapToMins;
final double width;
final double height;
DurationPicker(
{this.duration = const Duration(minutes: 0),
@required this.onChange,
this.snapToMins,
this.width,
this.height});
@override
Widget build(BuildContext context) {
return SizedBox(
width: width ?? _kDurationPickerWidthPortrait / 1.5,
height: height ?? _kDurationPickerHeightPortrait / 1.5,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Expanded(
child: _Dial(
duration: duration,
onChanged: onChange,
snapToMins: snapToMins,
),
),
]));
}
}