Skip at beginning
Add new episode to playlist at one click
This commit is contained in:
parent
0a548b4441
commit
0040513380
|
@ -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,12 +395,14 @@ class AudioPlayerNotifier extends ChangeNotifier {
|
|||
}
|
||||
|
||||
addToPlaylist(EpisodeBrief episode) async {
|
||||
if (!_queue.playlist.contains(episode)) {
|
||||
if (_playerRunning) {
|
||||
await AudioService.addQueueItem(episode.toMediaItem());
|
||||
}
|
||||
await _queue.addToPlayList(episode);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
addToPlaylistAt(EpisodeBrief episode, int index) async {
|
||||
if (_playerRunning) {
|
||||
|
@ -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,7 +651,7 @@ class AudioPlayerTask extends BackgroundAudioTask {
|
|||
_skipState = null;
|
||||
onStop();
|
||||
} else {
|
||||
// AudioServiceBackground.setQueue(_queue);
|
||||
AudioServiceBackground.setQueue(_queue);
|
||||
AudioServiceBackground.setMediaItem(mediaItem);
|
||||
await _audioPlayer.setUrl(mediaItem.id);
|
||||
Duration duration = await _audioPlayer.durationFuture ?? Duration.zero;
|
||||
|
@ -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']));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ class EpisodeBrief {
|
|||
final String imagePath;
|
||||
final String mediaId;
|
||||
final int isNew;
|
||||
final int skipSeconds;
|
||||
EpisodeBrief(
|
||||
this.title,
|
||||
this.enclosureUrl,
|
||||
|
@ -29,7 +30,8 @@ class EpisodeBrief {
|
|||
this.explicit,
|
||||
this.imagePath,
|
||||
this.mediaId,
|
||||
this.isNew);
|
||||
this.isNew,
|
||||
this.skipSeconds);
|
||||
|
||||
String dateToString() {
|
||||
DateTime date = DateTime.fromMillisecondsSinceEpoch(pubDate,isUtc: true);
|
||||
|
@ -43,8 +45,8 @@ 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());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -54,7 +56,7 @@ class EpisodeBrief {
|
|||
title: title,
|
||||
artist: feedTitle,
|
||||
album: feedTitle,
|
||||
artUri: 'file://$imagePath');
|
||||
artUri: 'file://$imagePath',
|
||||
extras: {'skip': skipSeconds});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
await dbHelper.updatePodcastRss(podcastLocal);
|
||||
print('Refresh ' + podcastLocal.title);
|
||||
} catch (e) {
|
||||
sendPort.send([podcastLocal.title, 2]);
|
||||
await Future.delayed(Duration(seconds: 1));
|
||||
}
|
||||
});
|
||||
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");
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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)));
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
FutureBuilder<int>(
|
||||
future: getPodcastCounts(
|
||||
podcastLocal.id),
|
||||
initialData: 0,
|
||||
builder: (context, snapshot) {
|
||||
return snapshot.data > 0
|
||||
? Container(
|
||||
alignment: Alignment.center,
|
||||
alignment:
|
||||
Alignment.center,
|
||||
height: 10,
|
||||
width: 40,
|
||||
color: Colors.black54,
|
||||
child: Text('New',
|
||||
style: TextStyle(
|
||||
color: Colors.red,
|
||||
color:
|
||||
Colors.red,
|
||||
fontSize: 8,
|
||||
fontStyle: FontStyle
|
||||
fontStyle:
|
||||
FontStyle
|
||||
.italic)),
|
||||
)
|
||||
: Center(),
|
||||
: 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,10 +427,6 @@ 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);
|
||||
|
@ -422,7 +436,7 @@ class ShowEpisode extends StatelessWidget {
|
|||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(10))),
|
||||
context: context,
|
||||
position: RelativeRect.fromLTRB(left, top, _width - left, 0),
|
||||
position: RelativeRect.fromLTRB(left, top, context.width - left, 0),
|
||||
items: <PopupMenuEntry<int>>[
|
||||
PopupMenuItem(
|
||||
value: 0,
|
||||
|
@ -478,6 +492,10 @@ class ShowEpisode extends StatelessWidget {
|
|||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
double _width = context.width;
|
||||
Offset offset;
|
||||
return CustomScrollView(
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
primary: false,
|
||||
|
|
|
@ -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';
|
||||
|
@ -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)
|
||||
|
|
|
@ -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))))),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
@ -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,7 +27,8 @@ class KeyValueStorage {
|
|||
key,
|
||||
json.encode({
|
||||
'groups': [home.toEntity().toJson()]
|
||||
}));}
|
||||
}));
|
||||
}
|
||||
print(prefs.getString(key));
|
||||
return json
|
||||
.decode(prefs.getString(key))['groups']
|
||||
|
@ -50,7 +63,9 @@ class KeyValueStorage {
|
|||
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -61,8 +76,9 @@ class KeyValueStorage {
|
|||
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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,11 +443,13 @@ 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,
|
||||
);
|
||||
try {
|
||||
Response response = await Dio(options).get(podcastLocal.rssUrl);
|
||||
if (response.statusCode == 200) {
|
||||
var feed = RssFeed.parse(response.data);
|
||||
|
@ -445,11 +461,10 @@ class DBHelper {
|
|||
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(
|
||||
|
@ -499,8 +514,11 @@ class DBHelper {
|
|||
"""UPDATE PodcastLocal SET update_count = ?, episode_count = ? WHERE id = ?""",
|
||||
[countUpdate - count, countUpdate, podcastLocal.id]);
|
||||
return countUpdate - count;
|
||||
} else {
|
||||
throw ("network error");
|
||||
}
|
||||
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,35 @@ 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>> getRssItemTop(String id) async {
|
||||
|
||||
Future<List<EpisodeBrief>> getNewEpisodes(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,
|
||||
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
|
||||
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.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'],
|
||||
|
@ -580,7 +613,37 @@ 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>> 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, 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 2""", [id]);
|
||||
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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
|
|
|
@ -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.'),
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
]));
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue