mirror of
https://github.com/stonega/tsacdop
synced 2025-02-20 13:31:00 +01:00
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 {
|
class AudioPlayerNotifier extends ChangeNotifier {
|
||||||
DBHelper dbHelper = DBHelper();
|
DBHelper dbHelper = DBHelper();
|
||||||
KeyValueStorage storage = KeyValueStorage('audioposition');
|
KeyValueStorage positionStorage = KeyValueStorage(audioPositionKey);
|
||||||
|
KeyValueStorage autoPlayStorage = KeyValueStorage(autoPlayKey);
|
||||||
|
KeyValueStorage autoAddStorage = KeyValueStorage(autoAddKey);
|
||||||
|
|
||||||
EpisodeBrief _episode;
|
EpisodeBrief _episode;
|
||||||
Playlist _queue = Playlist();
|
Playlist _queue = Playlist();
|
||||||
bool _queueUpdate = false;
|
bool _queueUpdate = false;
|
||||||
@ -130,7 +133,10 @@ class AudioPlayerNotifier extends ChangeNotifier {
|
|||||||
bool _startSleepTimer = false;
|
bool _startSleepTimer = false;
|
||||||
double _switchValue = 0;
|
double _switchValue = 0;
|
||||||
SleepTimerMode _sleepTimerMode = SleepTimerMode.undefined;
|
SleepTimerMode _sleepTimerMode = SleepTimerMode.undefined;
|
||||||
|
//set autoplay episode in playlist
|
||||||
bool _autoPlay = true;
|
bool _autoPlay = true;
|
||||||
|
//Set auto add episodes to playlist
|
||||||
|
bool _autoAdd = false;
|
||||||
DateTime _current;
|
DateTime _current;
|
||||||
int _currentPosition;
|
int _currentPosition;
|
||||||
double _currentSpeed = 1;
|
double _currentSpeed = 1;
|
||||||
@ -151,6 +157,7 @@ class AudioPlayerNotifier extends ChangeNotifier {
|
|||||||
bool get startSleepTimer => _startSleepTimer;
|
bool get startSleepTimer => _startSleepTimer;
|
||||||
SleepTimerMode get sleepTimerMode => _sleepTimerMode;
|
SleepTimerMode get sleepTimerMode => _sleepTimerMode;
|
||||||
bool get autoPlay => _autoPlay;
|
bool get autoPlay => _autoPlay;
|
||||||
|
bool get autoAdd => _autoAdd;
|
||||||
int get timeLeft => _timeLeft;
|
int get timeLeft => _timeLeft;
|
||||||
double get switchValue => _switchValue;
|
double get switchValue => _switchValue;
|
||||||
double get currentSpeed => _currentSpeed;
|
double get currentSpeed => _currentSpeed;
|
||||||
@ -163,6 +170,31 @@ class AudioPlayerNotifier extends ChangeNotifier {
|
|||||||
set autoPlaySwitch(bool boo) {
|
set autoPlaySwitch(bool boo) {
|
||||||
_autoPlay = boo;
|
_autoPlay = boo;
|
||||||
notifyListeners();
|
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) {
|
set setSleepTimerMode(SleepTimerMode timer) {
|
||||||
@ -181,7 +213,10 @@ class AudioPlayerNotifier extends ChangeNotifier {
|
|||||||
|
|
||||||
loadPlaylist() async {
|
loadPlaylist() async {
|
||||||
await _queue.getPlaylist();
|
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) {
|
if (_lastPostion > 0 && _queue.playlist.length > 0) {
|
||||||
final EpisodeBrief episode = _queue.playlist.first;
|
final EpisodeBrief episode = _queue.playlist.first;
|
||||||
final int duration = episode.duration * 1000;
|
final int duration = episode.duration * 1000;
|
||||||
@ -190,6 +225,8 @@ class AudioPlayerNotifier extends ChangeNotifier {
|
|||||||
episode.title, episode.enclosureUrl, _lastPostion / 1000, seekValue);
|
episode.title, episode.enclosureUrl, _lastPostion / 1000, seekValue);
|
||||||
await dbHelper.saveHistory(history);
|
await dbHelper.saveHistory(history);
|
||||||
}
|
}
|
||||||
|
KeyValueStorage lastWorkStorage = KeyValueStorage(lastWorkKey);
|
||||||
|
await lastWorkStorage.saveInt(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
episodeLoad(EpisodeBrief episode, {int startPosition = 0}) async {
|
episodeLoad(EpisodeBrief episode, {int startPosition = 0}) async {
|
||||||
@ -268,7 +305,7 @@ class AudioPlayerNotifier extends ChangeNotifier {
|
|||||||
if (event.length == 0 || _stopOnComplete == true) {
|
if (event.length == 0 || _stopOnComplete == true) {
|
||||||
_queue.delFromPlaylist(_episode);
|
_queue.delFromPlaylist(_episode);
|
||||||
_lastPostion = 0;
|
_lastPostion = 0;
|
||||||
storage.saveInt(_lastPostion);
|
positionStorage.saveInt(_lastPostion);
|
||||||
final PlayHistory history = PlayHistory(
|
final PlayHistory history = PlayHistory(
|
||||||
_episode.title,
|
_episode.title,
|
||||||
_episode.enclosureUrl,
|
_episode.enclosureUrl,
|
||||||
@ -330,7 +367,7 @@ class AudioPlayerNotifier extends ChangeNotifier {
|
|||||||
|
|
||||||
if (_backgroundAudioPosition > 0) {
|
if (_backgroundAudioPosition > 0) {
|
||||||
_lastPostion = _backgroundAudioPosition;
|
_lastPostion = _backgroundAudioPosition;
|
||||||
storage.saveInt(_lastPostion);
|
positionStorage.saveInt(_lastPostion);
|
||||||
}
|
}
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
@ -358,12 +395,14 @@ class AudioPlayerNotifier extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
addToPlaylist(EpisodeBrief episode) async {
|
addToPlaylist(EpisodeBrief episode) async {
|
||||||
|
if (!_queue.playlist.contains(episode)) {
|
||||||
if (_playerRunning) {
|
if (_playerRunning) {
|
||||||
await AudioService.addQueueItem(episode.toMediaItem());
|
await AudioService.addQueueItem(episode.toMediaItem());
|
||||||
}
|
}
|
||||||
await _queue.addToPlayList(episode);
|
await _queue.addToPlayList(episode);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
addToPlaylistAt(EpisodeBrief episode, int index) async {
|
addToPlaylistAt(EpisodeBrief episode, int index) async {
|
||||||
if (_playerRunning) {
|
if (_playerRunning) {
|
||||||
@ -374,6 +413,22 @@ class AudioPlayerNotifier extends ChangeNotifier {
|
|||||||
notifyListeners();
|
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 {
|
updateMediaItem(EpisodeBrief episode) async {
|
||||||
int index = _queue.playlist
|
int index = _queue.playlist
|
||||||
.indexWhere((item) => item.enclosureUrl == episode.enclosureUrl);
|
.indexWhere((item) => item.enclosureUrl == episode.enclosureUrl);
|
||||||
@ -402,7 +457,7 @@ class AudioPlayerNotifier extends ChangeNotifier {
|
|||||||
} else {
|
} else {
|
||||||
await addToPlaylistAt(episode, 0);
|
await addToPlaylistAt(episode, 0);
|
||||||
_lastPostion = 0;
|
_lastPostion = 0;
|
||||||
storage.saveInt(_lastPostion);
|
positionStorage.saveInt(_lastPostion);
|
||||||
}
|
}
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
@ -596,7 +651,7 @@ class AudioPlayerTask extends BackgroundAudioTask {
|
|||||||
_skipState = null;
|
_skipState = null;
|
||||||
onStop();
|
onStop();
|
||||||
} else {
|
} else {
|
||||||
// AudioServiceBackground.setQueue(_queue);
|
AudioServiceBackground.setQueue(_queue);
|
||||||
AudioServiceBackground.setMediaItem(mediaItem);
|
AudioServiceBackground.setMediaItem(mediaItem);
|
||||||
await _audioPlayer.setUrl(mediaItem.id);
|
await _audioPlayer.setUrl(mediaItem.id);
|
||||||
Duration duration = await _audioPlayer.durationFuture ?? Duration.zero;
|
Duration duration = await _audioPlayer.durationFuture ?? Duration.zero;
|
||||||
@ -623,8 +678,17 @@ class AudioPlayerTask extends BackgroundAudioTask {
|
|||||||
AudioServiceBackground.setMediaItem(
|
AudioServiceBackground.setMediaItem(
|
||||||
mediaItem.copyWith(duration: duration.inMilliseconds));
|
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;
|
_playing = true;
|
||||||
_audioPlayer.play();
|
_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 imagePath;
|
||||||
final String mediaId;
|
final String mediaId;
|
||||||
final int isNew;
|
final int isNew;
|
||||||
|
final int skipSeconds;
|
||||||
EpisodeBrief(
|
EpisodeBrief(
|
||||||
this.title,
|
this.title,
|
||||||
this.enclosureUrl,
|
this.enclosureUrl,
|
||||||
@ -29,10 +30,11 @@ class EpisodeBrief {
|
|||||||
this.explicit,
|
this.explicit,
|
||||||
this.imagePath,
|
this.imagePath,
|
||||||
this.mediaId,
|
this.mediaId,
|
||||||
this.isNew);
|
this.isNew,
|
||||||
|
this.skipSeconds);
|
||||||
|
|
||||||
String dateToString() {
|
String dateToString() {
|
||||||
DateTime date = DateTime.fromMillisecondsSinceEpoch(pubDate, isUtc: true);
|
DateTime date = DateTime.fromMillisecondsSinceEpoch(pubDate,isUtc: true);
|
||||||
var diffrence = DateTime.now().toUtc().difference(date);
|
var diffrence = DateTime.now().toUtc().difference(date);
|
||||||
if (diffrence.inHours < 1) {
|
if (diffrence.inHours < 1) {
|
||||||
return '1 hour ago';
|
return '1 hour ago';
|
||||||
@ -43,8 +45,8 @@ class EpisodeBrief {
|
|||||||
} else if (diffrence.inDays < 7) {
|
} else if (diffrence.inDays < 7) {
|
||||||
return '${diffrence.inDays} days ago';
|
return '${diffrence.inDays} days ago';
|
||||||
} else {
|
} else {
|
||||||
return DateFormat.yMMMd()
|
return DateFormat.yMMMd().format(
|
||||||
.format(DateTime.fromMillisecondsSinceEpoch(pubDate, isUtc: true).toLocal());
|
DateTime.fromMillisecondsSinceEpoch(pubDate, isUtc: true).toLocal());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,7 +56,7 @@ class EpisodeBrief {
|
|||||||
title: title,
|
title: title,
|
||||||
artist: feedTitle,
|
artist: feedTitle,
|
||||||
album: 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 {
|
Future<void> refreshIsolateEntryPoint(SendPort sendPort) async {
|
||||||
|
KeyValueStorage refreshstorage = KeyValueStorage(refreshdateKey);
|
||||||
|
await refreshstorage.saveInt(DateTime.now().millisecondsSinceEpoch);
|
||||||
var dbHelper = DBHelper();
|
var dbHelper = DBHelper();
|
||||||
List<PodcastLocal> podcastList = await dbHelper.getPodcastLocalAll();
|
List<PodcastLocal> podcastList = await dbHelper.getPodcastLocalAll();
|
||||||
int i = 0;
|
//int i = 0;
|
||||||
await Future.forEach(podcastList, (podcastLocal) async {
|
await Future.forEach<PodcastLocal>(podcastList, (podcastLocal) async {
|
||||||
sendPort.send([podcastLocal.title, 1]);
|
sendPort.send([podcastLocal.title, 1]);
|
||||||
try {
|
await dbHelper.updatePodcastRss(podcastLocal);
|
||||||
i += await dbHelper.updatePodcastRss(podcastLocal);
|
|
||||||
print('Refresh ' + podcastLocal.title);
|
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');
|
||||||
KeyValueStorage refreshcountstorage = KeyValueStorage('refreshcount');
|
// await refreshcountstorage.saveInt(i);
|
||||||
await refreshcountstorage.saveInt(i);
|
|
||||||
sendPort.send("done");
|
sendPort.send("done");
|
||||||
}
|
}
|
||||||
|
@ -10,25 +10,30 @@ void callbackDispatcher() {
|
|||||||
Workmanager.executeTask((task, inputData) async {
|
Workmanager.executeTask((task, inputData) async {
|
||||||
var dbHelper = DBHelper();
|
var dbHelper = DBHelper();
|
||||||
List<PodcastLocal> podcastList = await dbHelper.getPodcastLocalAll();
|
List<PodcastLocal> podcastList = await dbHelper.getPodcastLocalAll();
|
||||||
await Future.forEach(podcastList, (podcastLocal) async {
|
//lastWork is a indicator for if the app was opened since last backgroundwork
|
||||||
await dbHelper.updatePodcastRss(podcastLocal);
|
//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);
|
print('Refresh ' + podcastLocal.title);
|
||||||
});
|
});
|
||||||
KeyValueStorage refreshstorage = KeyValueStorage('refreshdate');
|
await lastWorkStorage.saveInt(1);
|
||||||
|
KeyValueStorage refreshstorage = KeyValueStorage(refreshdateKey);
|
||||||
await refreshstorage.saveInt(DateTime.now().millisecondsSinceEpoch);
|
await refreshstorage.saveInt(DateTime.now().millisecondsSinceEpoch);
|
||||||
return Future.value(true);
|
return Future.value(true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class SettingState extends ChangeNotifier {
|
class SettingState extends ChangeNotifier {
|
||||||
KeyValueStorage themeStorage = KeyValueStorage('themes');
|
KeyValueStorage themeStorage = KeyValueStorage(themesKey);
|
||||||
KeyValueStorage accentStorage = KeyValueStorage('accents');
|
KeyValueStorage accentStorage = KeyValueStorage(accentsKey);
|
||||||
KeyValueStorage autoupdateStorage = KeyValueStorage('autoupdate');
|
KeyValueStorage autoupdateStorage = KeyValueStorage(autoAddKey);
|
||||||
KeyValueStorage intervalStorage = KeyValueStorage('updateInterval');
|
KeyValueStorage intervalStorage = KeyValueStorage(updateIntervalKey);
|
||||||
KeyValueStorage downloadUsingDataStorage =
|
KeyValueStorage downloadUsingDataStorage =
|
||||||
KeyValueStorage('downloadUsingData');
|
KeyValueStorage(downloadUsingDataKey);
|
||||||
KeyValueStorage introStorage = KeyValueStorage('intro');
|
KeyValueStorage introStorage = KeyValueStorage(introKey);
|
||||||
KeyValueStorage realDarkStorage = KeyValueStorage('realDark');
|
KeyValueStorage realDarkStorage = KeyValueStorage(realDarkKey);
|
||||||
|
|
||||||
Future initData() async {
|
Future initData() async {
|
||||||
await _getTheme();
|
await _getTheme();
|
||||||
|
@ -9,7 +9,6 @@ import 'package:url_launcher/url_launcher.dart';
|
|||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:tuple/tuple.dart';
|
import 'package:tuple/tuple.dart';
|
||||||
import 'package:audio_service/audio_service.dart';
|
|
||||||
import 'package:flutter_linkify/flutter_linkify.dart';
|
import 'package:flutter_linkify/flutter_linkify.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
|
||||||
@ -217,7 +216,7 @@ class _EpisodeDetailState extends State<EpisodeDetail> {
|
|||||||
data: _description,
|
data: _description,
|
||||||
linkStyle: TextStyle(
|
linkStyle: TextStyle(
|
||||||
color: Theme.of(context).accentColor,
|
color: Theme.of(context).accentColor,
|
||||||
decoration: TextDecoration.underline,
|
// decoration: TextDecoration.underline,
|
||||||
textBaseline: TextBaseline.ideographic),
|
textBaseline: TextBaseline.ideographic),
|
||||||
onLinkTap: (url) {
|
onLinkTap: (url) {
|
||||||
_launchUrl(url);
|
_launchUrl(url);
|
||||||
@ -244,8 +243,8 @@ class _EpisodeDetailState extends State<EpisodeDetail> {
|
|||||||
linkStyle: TextStyle(
|
linkStyle: TextStyle(
|
||||||
color:
|
color:
|
||||||
Theme.of(context).accentColor,
|
Theme.of(context).accentColor,
|
||||||
decoration:
|
// decoration:
|
||||||
TextDecoration.underline,
|
// TextDecoration.underline,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -539,12 +538,18 @@ class _MenuBarState extends State<MenuBar> {
|
|||||||
: Center();
|
: Center();
|
||||||
}),
|
}),
|
||||||
Spacer(),
|
Spacer(),
|
||||||
Selector<AudioPlayerNotifier,
|
Selector<AudioPlayerNotifier, Tuple2<EpisodeBrief, bool>>(
|
||||||
Tuple2<EpisodeBrief, BasicPlaybackState>>(
|
selector: (_, audio) => Tuple2(audio.episode, audio.playerRunning),
|
||||||
selector: (_, audio) => Tuple2(audio.episode, audio.audioState),
|
|
||||||
builder: (_, data, __) {
|
builder: (_, data, __) {
|
||||||
return (widget.episodeItem.title != data.item1?.title)
|
return (widget.episodeItem.title == data.item1?.title &&
|
||||||
? Material(
|
data.item2)
|
||||||
|
? Container(
|
||||||
|
padding: EdgeInsets.only(right: 30),
|
||||||
|
child: SizedBox(
|
||||||
|
width: 20,
|
||||||
|
height: 15,
|
||||||
|
child: WaveLoader(color: context.accentColor)))
|
||||||
|
: Material(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () {
|
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:dio/dio.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:provider/provider.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: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/searchpodcast.dart';
|
||||||
import '../../class/podcastlocal.dart';
|
|
||||||
import '../../class/subscribe_podcast.dart';
|
import '../../class/subscribe_podcast.dart';
|
||||||
import '../../local_storage/sqflite_localpodcast.dart';
|
|
||||||
import '../../home/appbar/popupmenu.dart';
|
import '../../home/appbar/popupmenu.dart';
|
||||||
import '../../util/context_extension.dart';
|
import '../../util/context_extension.dart';
|
||||||
import '../../webfeed/webfeed.dart';
|
import '../../webfeed/webfeed.dart';
|
||||||
@ -479,6 +468,10 @@ class _SearchResultState extends State<SearchResult>
|
|||||||
onPressed: () {
|
onPressed: () {
|
||||||
savePodcast(widget.onlinePodcast);
|
savePodcast(widget.onlinePodcast);
|
||||||
setState(() => _issubscribe = true);
|
setState(() => _issubscribe = true);
|
||||||
|
Fluttertoast.showToast(
|
||||||
|
msg: 'Podcast subscribed',
|
||||||
|
gravity: ToastGravity.TOP,
|
||||||
|
);
|
||||||
})
|
})
|
||||||
: OutlineButton(
|
: OutlineButton(
|
||||||
color: context.accentColor.withOpacity(0.8),
|
color: context.accentColor.withOpacity(0.8),
|
||||||
|
@ -55,10 +55,13 @@ class Import extends StatelessWidget {
|
|||||||
Consumer<RefreshWorker>(
|
Consumer<RefreshWorker>(
|
||||||
builder: (context, refreshWorker, child) {
|
builder: (context, refreshWorker, child) {
|
||||||
RefreshItem item = refreshWorker.currentRefreshItem;
|
RefreshItem item = refreshWorker.currentRefreshItem;
|
||||||
if (refreshWorker.complete) groupList.updateGroups();
|
if (refreshWorker.complete) {
|
||||||
|
groupList.updateGroups();
|
||||||
|
// audio.addNewEpisode('all');
|
||||||
|
}
|
||||||
switch (item.refreshState) {
|
switch (item.refreshState) {
|
||||||
case RefreshState.fetch:
|
case RefreshState.fetch:
|
||||||
return importColumn("Fetch data ${item.title}", context);
|
return importColumn("Update ${item.title}", context);
|
||||||
case RefreshState.error:
|
case RefreshState.error:
|
||||||
return importColumn("Update error ${item.title}", context);
|
return importColumn("Update error ${item.title}", context);
|
||||||
default:
|
default:
|
||||||
|
@ -2,30 +2,19 @@ import 'dart:io';
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:dio/dio.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:tsacdop/class/fireside_data.dart';
|
|
||||||
import 'package:tsacdop/class/refresh_podcast.dart';
|
import 'package:tsacdop/class/refresh_podcast.dart';
|
||||||
import 'package:tsacdop/class/subscribe_podcast.dart';
|
import 'package:tsacdop/class/subscribe_podcast.dart';
|
||||||
import 'package:tsacdop/local_storage/key_value_storage.dart';
|
import 'package:tsacdop/local_storage/key_value_storage.dart';
|
||||||
import 'package:xml/xml.dart' as xml;
|
import 'package:xml/xml.dart' as xml;
|
||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flutter/services.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:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:line_icons/line_icons.dart';
|
import 'package:line_icons/line_icons.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
import 'package:tsacdop/class/podcast_group.dart';
|
|
||||||
import 'package:tsacdop/settings/settting.dart';
|
import 'package:tsacdop/settings/settting.dart';
|
||||||
import 'about.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 {
|
class OmplOutline {
|
||||||
final String text;
|
final String text;
|
||||||
@ -47,14 +36,6 @@ class PopupMenu extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _PopupMenuState extends State<PopupMenu> {
|
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 {
|
Future<String> _getRefreshDate() async {
|
||||||
int refreshDate;
|
int refreshDate;
|
||||||
KeyValueStorage refreshstorage = KeyValueStorage('refreshdate');
|
KeyValueStorage refreshstorage = KeyValueStorage('refreshdate');
|
||||||
@ -68,14 +49,16 @@ class _PopupMenuState extends State<PopupMenu> {
|
|||||||
}
|
}
|
||||||
DateTime date = DateTime.fromMillisecondsSinceEpoch(refreshDate);
|
DateTime date = DateTime.fromMillisecondsSinceEpoch(refreshDate);
|
||||||
var diffrence = DateTime.now().difference(date);
|
var diffrence = DateTime.now().difference(date);
|
||||||
if (diffrence.inMinutes < 10) {
|
if (diffrence.inSeconds < 60) {
|
||||||
return 'Just now';
|
return '${diffrence.inSeconds} seconds ago';
|
||||||
|
} else if (diffrence.inMinutes < 10) {
|
||||||
|
return '${diffrence.inMinutes} minutes ago';
|
||||||
} else if (diffrence.inHours < 1) {
|
} else if (diffrence.inHours < 1) {
|
||||||
return '1 hour ago';
|
return 'an hour ago';
|
||||||
} else if (diffrence.inHours < 24) {
|
} else if (diffrence.inHours <= 24) {
|
||||||
return '${diffrence.inHours} hours ago';
|
return '${diffrence.inHours} hours ago';
|
||||||
} else if (diffrence.inDays < 7) {
|
} else if (diffrence.inDays < 7) {
|
||||||
return '${diffrence.inDays} day ago';
|
return '${diffrence.inDays} days ago';
|
||||||
} else {
|
} else {
|
||||||
return DateFormat.yMMMd()
|
return DateFormat.yMMMd()
|
||||||
.format(DateTime.fromMillisecondsSinceEpoch(refreshDate));
|
.format(DateTime.fromMillisecondsSinceEpoch(refreshDate));
|
||||||
@ -84,8 +67,6 @@ class _PopupMenuState extends State<PopupMenu> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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 refreshWorker = Provider.of<RefreshWorker>(context, listen: false);
|
||||||
var subscribeWorker = Provider.of<SubscribeWorker>(context, listen: false);
|
var subscribeWorker = Provider.of<SubscribeWorker>(context, listen: false);
|
||||||
// _refreshAll() async {
|
// _refreshAll() async {
|
||||||
@ -105,87 +86,6 @@ class _PopupMenuState extends State<PopupMenu> {
|
|||||||
// importOmpl.importState = ImportState.complete;
|
// importOmpl.importState = ImportState.complete;
|
||||||
// groupList.updateGroups();
|
// 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 {
|
void _saveOmpl(String path) async {
|
||||||
File file = File(path);
|
File file = File(path);
|
||||||
|
@ -28,6 +28,13 @@ class ScrollPodcasts extends StatefulWidget {
|
|||||||
|
|
||||||
class _ScrollPodcastsState extends State<ScrollPodcasts> {
|
class _ScrollPodcastsState extends State<ScrollPodcasts> {
|
||||||
int _groupIndex;
|
int _groupIndex;
|
||||||
|
|
||||||
|
Future<int> getPodcastCounts(String id) async {
|
||||||
|
var dbHelper = DBHelper();
|
||||||
|
List<int> list = await dbHelper.getPodcastCounts(id);
|
||||||
|
return list.first;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@ -260,20 +267,29 @@ class _ScrollPodcastsState extends State<ScrollPodcasts> {
|
|||||||
child: Image.file(File(
|
child: Image.file(File(
|
||||||
"${podcastLocal.imagePath}")),
|
"${podcastLocal.imagePath}")),
|
||||||
),
|
),
|
||||||
podcastLocal.upateCount > 0
|
FutureBuilder<int>(
|
||||||
|
future: getPodcastCounts(
|
||||||
|
podcastLocal.id),
|
||||||
|
initialData: 0,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
return snapshot.data > 0
|
||||||
? Container(
|
? Container(
|
||||||
alignment: Alignment.center,
|
alignment:
|
||||||
|
Alignment.center,
|
||||||
height: 10,
|
height: 10,
|
||||||
width: 40,
|
width: 40,
|
||||||
color: Colors.black54,
|
color: Colors.black54,
|
||||||
child: Text('New',
|
child: Text('New',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.red,
|
color:
|
||||||
|
Colors.red,
|
||||||
fontSize: 8,
|
fontSize: 8,
|
||||||
fontStyle: FontStyle
|
fontStyle:
|
||||||
|
FontStyle
|
||||||
.italic)),
|
.italic)),
|
||||||
)
|
)
|
||||||
: Center(),
|
: Center();
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -344,14 +360,16 @@ class _PodcastPreviewState extends State<PodcastPreview> {
|
|||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (snapshot.hasError) {
|
if (snapshot.hasError) {
|
||||||
print(snapshot.error);
|
print(snapshot.error);
|
||||||
Center(child: CircularProgressIndicator());
|
Center();
|
||||||
}
|
}
|
||||||
return (snapshot.hasData)
|
return (snapshot.hasData)
|
||||||
? ShowEpisode(
|
? ShowEpisode(
|
||||||
episodes: snapshot.data,
|
episodes: snapshot.data,
|
||||||
podcastLocal: widget.podcastLocal,
|
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')}';
|
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,
|
_showPopupMenu(Offset offset, EpisodeBrief episode, BuildContext context,
|
||||||
bool isPlaying, bool isInPlaylist) async {
|
bool isPlaying, bool isInPlaylist) async {
|
||||||
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
|
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
|
||||||
@ -422,7 +436,7 @@ class ShowEpisode extends StatelessWidget {
|
|||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.all(Radius.circular(10))),
|
borderRadius: BorderRadius.all(Radius.circular(10))),
|
||||||
context: context,
|
context: context,
|
||||||
position: RelativeRect.fromLTRB(left, top, _width - left, 0),
|
position: RelativeRect.fromLTRB(left, top, context.width - left, 0),
|
||||||
items: <PopupMenuEntry<int>>[
|
items: <PopupMenuEntry<int>>[
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: 0,
|
value: 0,
|
||||||
@ -478,6 +492,10 @@ class ShowEpisode extends StatelessWidget {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
double _width = context.width;
|
||||||
|
Offset offset;
|
||||||
return CustomScrollView(
|
return CustomScrollView(
|
||||||
physics: NeverScrollableScrollPhysics(),
|
physics: NeverScrollableScrollPhysics(),
|
||||||
primary: false,
|
primary: false,
|
||||||
|
@ -9,6 +9,7 @@ import 'package:tsacdop/home/playlist.dart';
|
|||||||
import 'package:tuple/tuple.dart';
|
import 'package:tuple/tuple.dart';
|
||||||
import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart';
|
import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart';
|
||||||
import 'package:line_icons/line_icons.dart';
|
import 'package:line_icons/line_icons.dart';
|
||||||
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
|
|
||||||
import 'package:tsacdop/class/audiostate.dart';
|
import 'package:tsacdop/class/audiostate.dart';
|
||||||
import 'package:tsacdop/class/episodebrief.dart';
|
import 'package:tsacdop/class/episodebrief.dart';
|
||||||
@ -332,7 +333,7 @@ class _RecentUpdate extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _RecentUpdateState extends State<_RecentUpdate>
|
class _RecentUpdateState extends State<_RecentUpdate>
|
||||||
with AutomaticKeepAliveClientMixin , SingleTickerProviderStateMixin{
|
with AutomaticKeepAliveClientMixin, SingleTickerProviderStateMixin {
|
||||||
Future<List<EpisodeBrief>> _getRssItem(int top, List<String> group) async {
|
Future<List<EpisodeBrief>> _getRssItem(int top, List<String> group) async {
|
||||||
var dbHelper = DBHelper();
|
var dbHelper = DBHelper();
|
||||||
List<EpisodeBrief> episodes;
|
List<EpisodeBrief> episodes;
|
||||||
@ -343,6 +344,16 @@ class _RecentUpdateState extends State<_RecentUpdate>
|
|||||||
return episodes;
|
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 {
|
_loadMoreEpisode() async {
|
||||||
if (mounted) setState(() => _loadMore = true);
|
if (mounted) setState(() => _loadMore = true);
|
||||||
await Future.delayed(Duration(seconds: 3));
|
await Future.delayed(Duration(seconds: 3));
|
||||||
@ -370,6 +381,7 @@ class _RecentUpdateState extends State<_RecentUpdate>
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
super.build(context);
|
super.build(context);
|
||||||
|
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
|
||||||
return FutureBuilder<List<EpisodeBrief>>(
|
return FutureBuilder<List<EpisodeBrief>>(
|
||||||
future: _getRssItem(_top, _group),
|
future: _getRssItem(_top, _group),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
@ -450,9 +462,38 @@ class _RecentUpdateState extends State<_RecentUpdate>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Spacer(),
|
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(
|
Material(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
|
tooltip: 'Change layout',
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
if (_layout == Layout.three)
|
if (_layout == Layout.three)
|
||||||
|
@ -199,7 +199,10 @@ class _SlideIntroState extends State<SlideIntro> {
|
|||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: 40,
|
height: 40,
|
||||||
width: 80,
|
width: 80,
|
||||||
child: Center(child: Text('Next'))))
|
child: Center(
|
||||||
|
child: Text('Next',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.black)))))
|
||||||
: InkWell(
|
: InkWell(
|
||||||
borderRadius:
|
borderRadius:
|
||||||
BorderRadius.all(Radius.circular(20)),
|
BorderRadius.all(Radius.circular(20)),
|
||||||
@ -217,7 +220,10 @@ class _SlideIntroState extends State<SlideIntro> {
|
|||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: 40,
|
height: 40,
|
||||||
width: 80,
|
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:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:tsacdop/class/podcast_group.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 {
|
class KeyValueStorage {
|
||||||
final String key;
|
final String key;
|
||||||
KeyValueStorage(this.key);
|
KeyValueStorage(this.key);
|
||||||
|
|
||||||
Future<List<GroupEntity>> getGroups() async {
|
Future<List<GroupEntity>> getGroups() async {
|
||||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
if (prefs.getString(key) == null) {
|
if (prefs.getString(key) == null) {
|
||||||
@ -15,7 +27,8 @@ class KeyValueStorage {
|
|||||||
key,
|
key,
|
||||||
json.encode({
|
json.encode({
|
||||||
'groups': [home.toEntity().toJson()]
|
'groups': [home.toEntity().toJson()]
|
||||||
}));}
|
}));
|
||||||
|
}
|
||||||
print(prefs.getString(key));
|
print(prefs.getString(key));
|
||||||
return json
|
return json
|
||||||
.decode(prefs.getString(key))['groups']
|
.decode(prefs.getString(key))['groups']
|
||||||
@ -32,37 +45,40 @@ class KeyValueStorage {
|
|||||||
{'groups': groupList.map((group) => group.toJson()).toList()}));
|
{'groups': groupList.map((group) => group.toJson()).toList()}));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> saveInt(int setting) async{
|
Future<bool> saveInt(int setting) async {
|
||||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
return prefs.setInt(key, setting);
|
return prefs.setInt(key, setting);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<int> getInt() async{
|
Future<int> getInt() async {
|
||||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
if(prefs.getInt(key) == null) await prefs.setInt(key, 0);
|
if (prefs.getInt(key) == null) await prefs.setInt(key, 0);
|
||||||
return prefs.getInt(key);
|
return prefs.getInt(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> saveStringList(List<String> playList) async{
|
Future<bool> saveStringList(List<String> playList) async {
|
||||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
return prefs.setStringList(key, playList);
|
return prefs.setStringList(key, playList);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<String>> getStringList() async{
|
Future<List<String>> getStringList() async {
|
||||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
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);
|
return prefs.getStringList(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> saveString(String string) async{
|
Future<bool> saveString(String string) async {
|
||||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
return prefs.setString(key, string);
|
return prefs.setString(key, string);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> getString() async{
|
Future<String> getString() async {
|
||||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
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);
|
return prefs.getString(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,7 @@ class DBHelper {
|
|||||||
var documentsDirectory = await getDatabasesPath();
|
var documentsDirectory = await getDatabasesPath();
|
||||||
String path = join(documentsDirectory, "podcasts.db");
|
String path = join(documentsDirectory, "podcasts.db");
|
||||||
Database theDb = await openDatabase(path,
|
Database theDb = await openDatabase(path,
|
||||||
version: 1, onCreate: _onCreate, onUpgrade: _onUpgrade);
|
version: 2, onCreate: _onCreate, onUpgrade: _onUpgrade);
|
||||||
return theDb;
|
return theDb;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -32,7 +32,7 @@ class DBHelper {
|
|||||||
imageUrl TEXT,rssUrl TEXT UNIQUE, primaryColor TEXT, author TEXT,
|
imageUrl TEXT,rssUrl TEXT UNIQUE, primaryColor TEXT, author TEXT,
|
||||||
description TEXT, add_date INTEGER, imagePath TEXT, provider TEXT, link TEXT,
|
description TEXT, add_date INTEGER, imagePath TEXT, provider TEXT, link TEXT,
|
||||||
background_image TEXT DEFAULT '',hosts TEXT DEFAULT '',update_count INTEGER DEFAULT 0,
|
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
|
await db
|
||||||
.execute("""CREATE TABLE Episodes(id INTEGER PRIMARY KEY,title TEXT,
|
.execute("""CREATE TABLE Episodes(id INTEGER PRIMARY KEY,title TEXT,
|
||||||
enclosure_url TEXT UNIQUE, enclosure_length INTEGER, pubDate 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 {
|
void _onUpgrade(Database db, int oldVersion, int newVersion) async {
|
||||||
if (oldVersion == 1) {
|
if (oldVersion == 1) {
|
||||||
await db.execute("ALTER TABLE Episodes ADD liked_date INTEGER DEFAULT 0");
|
await db.execute(
|
||||||
await db
|
"ALTER TABLE PodcastLocal ADD skip_seconds INTEGER DEFAULT 0");
|
||||||
.execute("ALTER TABLE PlayHistory ADD listen_time INTEGER DEFAULT 0");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,6 +110,19 @@ class DBHelper {
|
|||||||
return [list.first['update_count'], list.first['episode_count']];
|
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 {
|
Future<bool> checkPodcast(String url) async {
|
||||||
var dbClient = await database;
|
var dbClient = await database;
|
||||||
List<Map> list = await dbClient
|
List<Map> list = await dbClient
|
||||||
@ -298,7 +310,6 @@ class DBHelper {
|
|||||||
|
|
||||||
DateTime _parsePubDate(String pubDate) {
|
DateTime _parsePubDate(String pubDate) {
|
||||||
if (pubDate == null) return DateTime.now();
|
if (pubDate == null) return DateTime.now();
|
||||||
print(pubDate);
|
|
||||||
DateTime date;
|
DateTime date;
|
||||||
RegExp yyyy = RegExp(r'[1-2][0-9]{3}');
|
RegExp yyyy = RegExp(r'[1-2][0-9]{3}');
|
||||||
RegExp hhmm = RegExp(r'[0-2][0-9]\:[0-5][0-9]');
|
RegExp hhmm = RegExp(r'[0-2][0-9]\:[0-5][0-9]');
|
||||||
@ -338,12 +349,15 @@ class DBHelper {
|
|||||||
print(month);
|
print(month);
|
||||||
print(date.toString());
|
print(date.toString());
|
||||||
} else {
|
} 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) {
|
int _getExplicit(bool b) {
|
||||||
@ -394,8 +408,8 @@ class DBHelper {
|
|||||||
final title = feed.items[i].itunes.title ?? feed.items[i].title;
|
final title = feed.items[i].itunes.title ?? feed.items[i].title;
|
||||||
final length = feed.items[i]?.enclosure?.length;
|
final length = feed.items[i]?.enclosure?.length;
|
||||||
final pubDate = feed.items[i].pubDate;
|
final pubDate = feed.items[i].pubDate;
|
||||||
print(pubDate);
|
|
||||||
final date = _parsePubDate(pubDate);
|
final date = _parsePubDate(pubDate);
|
||||||
|
print(date);
|
||||||
final milliseconds = date.millisecondsSinceEpoch;
|
final milliseconds = date.millisecondsSinceEpoch;
|
||||||
final duration = feed.items[i].itunes.duration?.inSeconds ?? 0;
|
final duration = feed.items[i].itunes.duration?.inSeconds ?? 0;
|
||||||
final explicit = _getExplicit(feed.items[i].itunes.explicit);
|
final explicit = _getExplicit(feed.items[i].itunes.explicit);
|
||||||
@ -429,11 +443,13 @@ class DBHelper {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<int> updatePodcastRss(PodcastLocal podcastLocal) async {
|
Future<int> updatePodcastRss(PodcastLocal podcastLocal,
|
||||||
BaseOptions options = new BaseOptions(
|
{int removeMark = 0}) async {
|
||||||
|
BaseOptions options = BaseOptions(
|
||||||
connectTimeout: 20000,
|
connectTimeout: 20000,
|
||||||
receiveTimeout: 20000,
|
receiveTimeout: 20000,
|
||||||
);
|
);
|
||||||
|
try {
|
||||||
Response response = await Dio(options).get(podcastLocal.rssUrl);
|
Response response = await Dio(options).get(podcastLocal.rssUrl);
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
var feed = RssFeed.parse(response.data);
|
var feed = RssFeed.parse(response.data);
|
||||||
@ -445,11 +461,10 @@ class DBHelper {
|
|||||||
int count = Sqflite.firstIntValue(await dbClient.rawQuery(
|
int count = Sqflite.firstIntValue(await dbClient.rawQuery(
|
||||||
'SELECT COUNT(*) FROM Episodes WHERE feed_id = ?',
|
'SELECT COUNT(*) FROM Episodes WHERE feed_id = ?',
|
||||||
[podcastLocal.id]));
|
[podcastLocal.id]));
|
||||||
|
if (removeMark == 0)
|
||||||
await dbClient.rawUpdate(
|
await dbClient.rawUpdate(
|
||||||
"UPDATE Episodes SET is_new = 0 WHERE feed_id = ?",
|
"UPDATE Episodes SET is_new = 0 WHERE feed_id = ?",
|
||||||
[podcastLocal.id]);
|
[podcastLocal.id]);
|
||||||
|
|
||||||
for (int i = 0; i < result; i++) {
|
for (int i = 0; i < result; i++) {
|
||||||
print(feed.items[i].title);
|
print(feed.items[i].title);
|
||||||
description = _getDescription(
|
description = _getDescription(
|
||||||
@ -499,8 +514,11 @@ class DBHelper {
|
|||||||
"""UPDATE PodcastLocal SET update_count = ?, episode_count = ? WHERE id = ?""",
|
"""UPDATE PodcastLocal SET update_count = ?, episode_count = ? WHERE id = ?""",
|
||||||
[countUpdate - count, countUpdate, podcastLocal.id]);
|
[countUpdate - count, countUpdate, podcastLocal.id]);
|
||||||
return countUpdate - count;
|
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
|
List<Map> list = await dbClient
|
||||||
.rawQuery("""SELECT E.title, E.enclosure_url, E.enclosure_length,
|
.rawQuery("""SELECT E.title, E.enclosure_url, E.enclosure_length,
|
||||||
E.milliseconds, P.imagePath, P.title as feedTitle, E.duration, E.explicit, E.liked,
|
E.milliseconds, P.imagePath, P.title as feedTitle, E.duration, E.explicit, E.liked,
|
||||||
E.downloaded, P.primaryColor , E.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
|
FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
|
||||||
WHERE P.id = ? ORDER BY E.milliseconds ASC LIMIT ?""", [id, i]);
|
WHERE P.id = ? ORDER BY E.milliseconds ASC LIMIT ?""", [id, i]);
|
||||||
for (int x = 0; x < list.length; x++) {
|
for (int x = 0; x < list.length; x++) {
|
||||||
@ -528,13 +546,14 @@ class DBHelper {
|
|||||||
list[x]['explicit'],
|
list[x]['explicit'],
|
||||||
list[x]['imagePath'],
|
list[x]['imagePath'],
|
||||||
list[x]['media_id'],
|
list[x]['media_id'],
|
||||||
list[x]['is_new']));
|
list[x]['is_new'],
|
||||||
|
list[x]['skip_seconds']));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
List<Map> list = await dbClient
|
List<Map> list = await dbClient
|
||||||
.rawQuery("""SELECT E.title, E.enclosure_url, E.enclosure_length,
|
.rawQuery("""SELECT E.title, E.enclosure_url, E.enclosure_length,
|
||||||
E.milliseconds, P.imagePath, P.title as feedTitle, E.duration, E.explicit, E.liked,
|
E.milliseconds, P.imagePath, P.title as feedTitle, E.duration, E.explicit, E.liked,
|
||||||
E.downloaded, P.primaryColor , E.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
|
FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
|
||||||
WHERE P.id = ? ORDER BY E.milliseconds DESC LIMIT ?""", [id, i]);
|
WHERE P.id = ? ORDER BY E.milliseconds DESC LIMIT ?""", [id, i]);
|
||||||
for (int x = 0; x < list.length; x++) {
|
for (int x = 0; x < list.length; x++) {
|
||||||
@ -551,21 +570,35 @@ class DBHelper {
|
|||||||
list[x]['explicit'],
|
list[x]['explicit'],
|
||||||
list[x]['imagePath'],
|
list[x]['imagePath'],
|
||||||
list[x]['media_id'],
|
list[x]['media_id'],
|
||||||
list[x]['is_new']));
|
list[x]['is_new'],
|
||||||
|
list[x]['skip_seconds']));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return episodes;
|
return episodes;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<EpisodeBrief>> getRssItemTop(String id) async {
|
|
||||||
|
Future<List<EpisodeBrief>> getNewEpisodes(String id) async {
|
||||||
var dbClient = await database;
|
var dbClient = await database;
|
||||||
List<EpisodeBrief> episodes = List();
|
List<EpisodeBrief> episodes = [];
|
||||||
List<Map> list = await dbClient
|
List<Map> list;
|
||||||
.rawQuery("""SELECT E.title, E.enclosure_url, E.enclosure_length,
|
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.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
|
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++) {
|
for (int x = 0; x < list.length; x++) {
|
||||||
episodes.add(EpisodeBrief(
|
episodes.add(EpisodeBrief(
|
||||||
list[x]['title'],
|
list[x]['title'],
|
||||||
@ -580,7 +613,37 @@ class DBHelper {
|
|||||||
list[x]['explicit'],
|
list[x]['explicit'],
|
||||||
list[x]['imagePath'],
|
list[x]['imagePath'],
|
||||||
list[x]['media_id'],
|
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;
|
return episodes;
|
||||||
}
|
}
|
||||||
@ -591,7 +654,7 @@ class DBHelper {
|
|||||||
List<Map> list = await dbClient.rawQuery(
|
List<Map> list = await dbClient.rawQuery(
|
||||||
"""SELECT E.title, E.enclosure_url, E.enclosure_length,
|
"""SELECT E.title, E.enclosure_url, E.enclosure_length,
|
||||||
E.milliseconds, P.imagePath, P.title as feed_title, E.duration, E.explicit, E.liked,
|
E.milliseconds, P.imagePath, P.title as feed_title, E.duration, E.explicit, E.liked,
|
||||||
E.downloaded, P.primaryColor, E.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
|
FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
|
||||||
where E.enclosure_url = ? ORDER BY E.milliseconds DESC LIMIT 3""",
|
where E.enclosure_url = ? ORDER BY E.milliseconds DESC LIMIT 3""",
|
||||||
[url]);
|
[url]);
|
||||||
@ -610,7 +673,8 @@ class DBHelper {
|
|||||||
list.first['explicit'],
|
list.first['explicit'],
|
||||||
list.first['imagePath'],
|
list.first['imagePath'],
|
||||||
list.first['media_id'],
|
list.first['media_id'],
|
||||||
list.first['is_new']);
|
list.first['is_new'],
|
||||||
|
list.first['skip_seconds']);
|
||||||
return episode;
|
return episode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -620,7 +684,7 @@ class DBHelper {
|
|||||||
List<Map> list = await dbClient
|
List<Map> list = await dbClient
|
||||||
.rawQuery("""SELECT E.title, E.enclosure_url, E.enclosure_length,
|
.rawQuery("""SELECT E.title, E.enclosure_url, E.enclosure_length,
|
||||||
E.milliseconds, P.title as feed_title, E.duration, E.explicit, E.liked,
|
E.milliseconds, P.title as feed_title, E.duration, E.explicit, E.liked,
|
||||||
E.downloaded, P.imagePath, P.primaryColor, E.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
|
FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
|
||||||
ORDER BY E.milliseconds DESC LIMIT ? """, [top]);
|
ORDER BY E.milliseconds DESC LIMIT ? """, [top]);
|
||||||
for (int x = 0; x < list.length; x++) {
|
for (int x = 0; x < list.length; x++) {
|
||||||
@ -632,12 +696,13 @@ class DBHelper {
|
|||||||
list[x]['feed_title'],
|
list[x]['feed_title'],
|
||||||
list[x]['primaryColor'],
|
list[x]['primaryColor'],
|
||||||
list[x]['liked'],
|
list[x]['liked'],
|
||||||
list[x]['doanloaded'],
|
list[x]['downloaded'],
|
||||||
list[x]['duration'],
|
list[x]['duration'],
|
||||||
list[x]['explicit'],
|
list[x]['explicit'],
|
||||||
list[x]['imagePath'],
|
list[x]['imagePath'],
|
||||||
list[x]['media_id'],
|
list[x]['media_id'],
|
||||||
list[x]['is_new']));
|
list[x]['is_new'],
|
||||||
|
list[x]['skip_seconds']));
|
||||||
}
|
}
|
||||||
return episodes;
|
return episodes;
|
||||||
}
|
}
|
||||||
@ -651,7 +716,7 @@ class DBHelper {
|
|||||||
List<Map> list = await dbClient
|
List<Map> list = await dbClient
|
||||||
.rawQuery("""SELECT E.title, E.enclosure_url, E.enclosure_length,
|
.rawQuery("""SELECT E.title, E.enclosure_url, E.enclosure_length,
|
||||||
E.milliseconds, P.title as feed_title, E.duration, E.explicit, E.liked,
|
E.milliseconds, P.title as feed_title, E.duration, E.explicit, E.liked,
|
||||||
E.downloaded, P.imagePath, P.primaryColor, E.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
|
FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
|
||||||
WHERE P.id in (${s.join(',')})
|
WHERE P.id in (${s.join(',')})
|
||||||
ORDER BY E.milliseconds DESC LIMIT ? """, [top]);
|
ORDER BY E.milliseconds DESC LIMIT ? """, [top]);
|
||||||
@ -664,17 +729,97 @@ class DBHelper {
|
|||||||
list[x]['feed_title'],
|
list[x]['feed_title'],
|
||||||
list[x]['primaryColor'],
|
list[x]['primaryColor'],
|
||||||
list[x]['liked'],
|
list[x]['liked'],
|
||||||
list[x]['doanloaded'],
|
list[x]['downloaded'],
|
||||||
list[x]['duration'],
|
list[x]['duration'],
|
||||||
list[x]['explicit'],
|
list[x]['explicit'],
|
||||||
list[x]['imagePath'],
|
list[x]['imagePath'],
|
||||||
list[x]['media_id'],
|
list[x]['media_id'],
|
||||||
list[x]['is_new']));
|
list[x]['is_new'],
|
||||||
|
list[x]['skip_seconds']));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return episodes;
|
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 {
|
Future<List<EpisodeBrief>> getLikedRssItem(int i, int sortBy) async {
|
||||||
var dbClient = await database;
|
var dbClient = await database;
|
||||||
List<EpisodeBrief> episodes = List();
|
List<EpisodeBrief> episodes = List();
|
||||||
@ -682,7 +827,7 @@ class DBHelper {
|
|||||||
List<Map> list = await dbClient.rawQuery(
|
List<Map> list = await dbClient.rawQuery(
|
||||||
"""SELECT E.title, E.enclosure_url, E.enclosure_length, E.milliseconds, P.imagePath,
|
"""SELECT E.title, E.enclosure_url, E.enclosure_length, E.milliseconds, P.imagePath,
|
||||||
P.title as feed_title, E.duration, E.explicit, E.liked, E.downloaded,
|
P.title as feed_title, E.duration, E.explicit, E.liked, E.downloaded,
|
||||||
P.primaryColor, 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]);
|
WHERE E.liked = 1 ORDER BY E.milliseconds DESC LIMIT ?""", [i]);
|
||||||
for (int x = 0; x < list.length; x++) {
|
for (int x = 0; x < list.length; x++) {
|
||||||
episodes.add(EpisodeBrief(
|
episodes.add(EpisodeBrief(
|
||||||
@ -698,13 +843,14 @@ class DBHelper {
|
|||||||
list[x]['explicit'],
|
list[x]['explicit'],
|
||||||
list[x]['imagePath'],
|
list[x]['imagePath'],
|
||||||
list[x]['media_id'],
|
list[x]['media_id'],
|
||||||
list[x]['is_new']));
|
list[x]['is_new'],
|
||||||
|
list[x]['skip_seconds']));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
List<Map> list = await dbClient.rawQuery(
|
List<Map> list = await dbClient.rawQuery(
|
||||||
"""SELECT E.title, E.enclosure_url, E.enclosure_length, E.milliseconds, P.imagePath,
|
"""SELECT E.title, E.enclosure_url, E.enclosure_length, E.milliseconds, P.imagePath,
|
||||||
P.title as feed_title, E.duration, E.explicit, E.liked, E.downloaded,
|
P.title as feed_title, E.duration, E.explicit, E.liked, E.downloaded,
|
||||||
P.primaryColor, 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]);
|
WHERE E.liked = 1 ORDER BY E.liked_date DESC LIMIT ?""", [i]);
|
||||||
for (int x = 0; x < list.length; x++) {
|
for (int x = 0; x < list.length; x++) {
|
||||||
episodes.add(EpisodeBrief(
|
episodes.add(EpisodeBrief(
|
||||||
@ -720,7 +866,8 @@ class DBHelper {
|
|||||||
list[x]['explicit'],
|
list[x]['explicit'],
|
||||||
list[x]['imagePath'],
|
list[x]['imagePath'],
|
||||||
list[x]['media_id'],
|
list[x]['media_id'],
|
||||||
list[x]['is_new']));
|
list[x]['is_new'],
|
||||||
|
list[x]['skip_seconds']));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return episodes;
|
return episodes;
|
||||||
@ -782,7 +929,7 @@ class DBHelper {
|
|||||||
List<Map> list = await dbClient.rawQuery(
|
List<Map> list = await dbClient.rawQuery(
|
||||||
"""SELECT E.title, E.enclosure_url, E.enclosure_length, E.milliseconds, P.imagePath,
|
"""SELECT E.title, E.enclosure_url, E.enclosure_length, E.milliseconds, P.imagePath,
|
||||||
P.title as feed_title, E.duration, E.explicit, E.liked, E.downloaded,
|
P.title as feed_title, E.duration, E.explicit, E.liked, E.downloaded,
|
||||||
P.primaryColor, 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""");
|
WHERE E.downloaded != 'ND' ORDER BY E.download_date DESC""");
|
||||||
for (int x = 0; x < list.length; x++) {
|
for (int x = 0; x < list.length; x++) {
|
||||||
episodes.add(EpisodeBrief(
|
episodes.add(EpisodeBrief(
|
||||||
@ -798,7 +945,8 @@ class DBHelper {
|
|||||||
list[x]['explicit'],
|
list[x]['explicit'],
|
||||||
list[x]['imagePath'],
|
list[x]['imagePath'],
|
||||||
list[x]['media_id'],
|
list[x]['media_id'],
|
||||||
list[x]['is_new']));
|
list[x]['is_new'],
|
||||||
|
list[x]['skip_seconds']));
|
||||||
}
|
}
|
||||||
return episodes;
|
return episodes;
|
||||||
}
|
}
|
||||||
@ -825,7 +973,7 @@ class DBHelper {
|
|||||||
List<Map> list = await dbClient.rawQuery(
|
List<Map> list = await dbClient.rawQuery(
|
||||||
"""SELECT E.title, E.enclosure_url, E.enclosure_length, E.milliseconds, P.imagePath,
|
"""SELECT E.title, E.enclosure_url, E.enclosure_length, E.milliseconds, P.imagePath,
|
||||||
P.title as feed_title, E.duration, E.explicit, E.liked, E.downloaded,
|
P.title as feed_title, E.duration, E.explicit, E.liked, E.downloaded,
|
||||||
P.primaryColor, 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]);
|
WHERE E.enclosure_url = ?""", [url]);
|
||||||
if (list.length == 0) {
|
if (list.length == 0) {
|
||||||
return null;
|
return null;
|
||||||
@ -843,7 +991,8 @@ class DBHelper {
|
|||||||
list.first['explicit'],
|
list.first['explicit'],
|
||||||
list.first['imagePath'],
|
list.first['imagePath'],
|
||||||
list.first['media_id'],
|
list.first['media_id'],
|
||||||
list.first['is_new']);
|
list.first['is_new'],
|
||||||
|
list.first['skip_seconds']);
|
||||||
return episode;
|
return episode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -854,7 +1003,7 @@ class DBHelper {
|
|||||||
List<Map> list = await dbClient.rawQuery(
|
List<Map> list = await dbClient.rawQuery(
|
||||||
"""SELECT E.title, E.enclosure_url, E.enclosure_length, E.milliseconds, P.imagePath,
|
"""SELECT E.title, E.enclosure_url, E.enclosure_length, E.milliseconds, P.imagePath,
|
||||||
P.title as feed_title, E.duration, E.explicit, E.liked, E.downloaded,
|
P.title as feed_title, E.duration, E.explicit, E.liked, E.downloaded,
|
||||||
P.primaryColor, 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]);
|
WHERE E.media_id = ?""", [id]);
|
||||||
episode = EpisodeBrief(
|
episode = EpisodeBrief(
|
||||||
list.first['title'],
|
list.first['title'],
|
||||||
@ -869,7 +1018,8 @@ class DBHelper {
|
|||||||
list.first['explicit'],
|
list.first['explicit'],
|
||||||
list.first['imagePath'],
|
list.first['imagePath'],
|
||||||
list.first['media_id'],
|
list.first['media_id'],
|
||||||
list.first['is_new']);
|
list.first['is_new'],
|
||||||
|
list.first['skip_seconds']);
|
||||||
return episode;
|
return episode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,17 +12,17 @@ import 'package:fluttertoast/fluttertoast.dart';
|
|||||||
import 'package:flutter_linkify/flutter_linkify.dart';
|
import 'package:flutter_linkify/flutter_linkify.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:line_icons/line_icons.dart';
|
import 'package:line_icons/line_icons.dart';
|
||||||
|
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:tsacdop/class/podcastlocal.dart';
|
|
||||||
import 'package:tsacdop/class/episodebrief.dart';
|
import '../class/podcastlocal.dart';
|
||||||
import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
|
import '../class/episodebrief.dart';
|
||||||
import 'package:tsacdop/util/episodegrid.dart';
|
import '../local_storage/sqflite_localpodcast.dart';
|
||||||
import 'package:tsacdop/home/audioplayer.dart';
|
import '../util/episodegrid.dart';
|
||||||
import 'package:tsacdop/class/fireside_data.dart';
|
import '../home/audioplayer.dart';
|
||||||
import 'package:tsacdop/util/colorize.dart';
|
import '../class/fireside_data.dart';
|
||||||
import 'package:tsacdop/util/context_extension.dart';
|
import '../util/colorize.dart';
|
||||||
import 'package:tsacdop/util/custompaint.dart';
|
import '../util/context_extension.dart';
|
||||||
|
import '../util/custompaint.dart';
|
||||||
|
|
||||||
class PodcastDetail extends StatefulWidget {
|
class PodcastDetail extends StatefulWidget {
|
||||||
PodcastDetail({Key key, this.podcastLocal}) : super(key: key);
|
PodcastDetail({Key key, this.podcastLocal}) : super(key: key);
|
||||||
@ -215,7 +215,10 @@ class _PodcastDetailState extends State<PodcastDetail> {
|
|||||||
child: RefreshIndicator(
|
child: RefreshIndicator(
|
||||||
key: _refreshIndicatorKey,
|
key: _refreshIndicatorKey,
|
||||||
color: Theme.of(context).accentColor,
|
color: Theme.of(context).accentColor,
|
||||||
onRefresh: () => _updateRssItem(widget.podcastLocal),
|
onRefresh: () async {
|
||||||
|
await _updateRssItem(widget.podcastLocal);
|
||||||
|
// audio.addNewEpisode(widget.podcastLocal.id);
|
||||||
|
},
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Column(
|
Column(
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@ -6,11 +7,14 @@ import 'package:flutter/services.dart';
|
|||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import 'package:tsacdop/class/podcast_group.dart';
|
import '../class/podcast_group.dart';
|
||||||
import 'package:tsacdop/class/podcastlocal.dart';
|
import '../class/podcastlocal.dart';
|
||||||
import 'package:tsacdop/podcasts/podcastdetail.dart';
|
import '../local_storage/sqflite_localpodcast.dart';
|
||||||
import 'package:tsacdop/util/pageroute.dart';
|
import '../podcasts/podcastdetail.dart';
|
||||||
import 'package:tsacdop/util/colorize.dart';
|
import '../util/pageroute.dart';
|
||||||
|
import '../util/colorize.dart';
|
||||||
|
import '../util/duraiton_picker.dart';
|
||||||
|
import '../util/context_extension.dart';
|
||||||
|
|
||||||
class PodcastGroupList extends StatefulWidget {
|
class PodcastGroupList extends StatefulWidget {
|
||||||
final PodcastGroup group;
|
final PodcastGroup group;
|
||||||
@ -67,11 +71,32 @@ class PodcastCard extends StatefulWidget {
|
|||||||
_PodcastCardState createState() => _PodcastCardState();
|
_PodcastCardState createState() => _PodcastCardState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _PodcastCardState extends State<PodcastCard> {
|
class _PodcastCardState extends State<PodcastCard>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
bool _loadMenu;
|
bool _loadMenu;
|
||||||
bool _addGroup;
|
bool _addGroup;
|
||||||
List<PodcastGroup> _selectedGroups;
|
List<PodcastGroup> _selectedGroups;
|
||||||
List<PodcastGroup> _belongGroups;
|
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
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -79,6 +104,16 @@ class _PodcastCardState extends State<PodcastCard> {
|
|||||||
_loadMenu = false;
|
_loadMenu = false;
|
||||||
_addGroup = false;
|
_addGroup = false;
|
||||||
_selectedGroups = [widget.group];
|
_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(
|
Widget _buttonOnMenu(Widget widget, VoidCallback onTap) => Material(
|
||||||
@ -112,7 +147,15 @@ class _PodcastCardState extends State<PodcastCard> {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
InkWell(
|
InkWell(
|
||||||
onTap: () => setState(() => _loadMenu = !_loadMenu),
|
onTap: () => setState(
|
||||||
|
() {
|
||||||
|
_loadMenu = !_loadMenu;
|
||||||
|
if (_value == 0)
|
||||||
|
_controller.forward();
|
||||||
|
else
|
||||||
|
_controller.reverse();
|
||||||
|
},
|
||||||
|
),
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 12),
|
padding: EdgeInsets.symmetric(horizontal: 12),
|
||||||
height: 100,
|
height: 100,
|
||||||
@ -162,12 +205,30 @@ class _PodcastCardState extends State<PodcastCard> {
|
|||||||
child: Text(group.name));
|
child: Text(group.name));
|
||||||
}).toList(),
|
}).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(),
|
Spacer(),
|
||||||
Icon(_loadMenu
|
Transform.rotate(
|
||||||
? Icons.keyboard_arrow_up
|
angle: math.pi * _value,
|
||||||
: Icons.keyboard_arrow_down),
|
child: Icon(Icons.keyboard_arrow_down),
|
||||||
|
),
|
||||||
|
// Icon(_loadMenu
|
||||||
|
// ? Icons.keyboard_arrow_up
|
||||||
|
// : Icons.keyboard_arrow_down),
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 5.0),
|
padding: EdgeInsets.symmetric(horizontal: 5.0),
|
||||||
),
|
),
|
||||||
@ -262,7 +323,7 @@ class _PodcastCardState extends State<PodcastCard> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
_buttonOnMenu(
|
_buttonOnMenu(
|
||||||
Icon(Icons.fullscreen),
|
Icon(Icons.fullscreen, size: 20 * _value),
|
||||||
() => Navigator.push(
|
() => Navigator.push(
|
||||||
context,
|
context,
|
||||||
ScaleRoute(
|
ScaleRoute(
|
||||||
@ -270,16 +331,90 @@ class _PodcastCardState extends State<PodcastCard> {
|
|||||||
podcastLocal: widget.podcastLocal,
|
podcastLocal: widget.podcastLocal,
|
||||||
)),
|
)),
|
||||||
)),
|
)),
|
||||||
_buttonOnMenu(Icon(Icons.add), () {
|
_buttonOnMenu(Icon(Icons.add, size: 20 * _value),
|
||||||
|
() {
|
||||||
setState(() {
|
setState(() {
|
||||||
_addGroup = true;
|
_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(
|
_buttonOnMenu(
|
||||||
Icon(
|
Icon(
|
||||||
Icons.delete,
|
Icons.delete,
|
||||||
color: Colors.red,
|
color: Colors.red,
|
||||||
|
size: 20 * (_value),
|
||||||
), () {
|
), () {
|
||||||
showGeneralDialog(
|
showGeneralDialog(
|
||||||
context: context,
|
context: context,
|
||||||
|
@ -164,7 +164,8 @@ class _PodcastManageState extends State<PodcastManage>
|
|||||||
),
|
),
|
||||||
body: WillPopScope(
|
body: WillPopScope(
|
||||||
onWillPop: () async {
|
onWillPop: () async {
|
||||||
await Provider.of<GroupList>(context, listen: false).clearOrderChanged();
|
await Provider.of<GroupList>(context, listen: false)
|
||||||
|
.clearOrderChanged();
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
child: Consumer<GroupList>(builder: (_, groupList, __) {
|
child: Consumer<GroupList>(builder: (_, groupList, __) {
|
||||||
@ -347,17 +348,6 @@ class _PodcastManageState extends State<PodcastManage>
|
|||||||
15,
|
15,
|
||||||
15,
|
15,
|
||||||
1),
|
1),
|
||||||
// statusBarColor: Theme.of(
|
|
||||||
// context)
|
|
||||||
// .brightness ==
|
|
||||||
// Brightness.light
|
|
||||||
// ? Color.fromRGBO(
|
|
||||||
// 113,
|
|
||||||
// 113,
|
|
||||||
// 113,
|
|
||||||
// 1)
|
|
||||||
// : Color.fromRGBO(
|
|
||||||
// 5, 5, 5, 1),
|
|
||||||
),
|
),
|
||||||
child: AlertDialog(
|
child: AlertDialog(
|
||||||
elevation: 1,
|
elevation: 1,
|
||||||
@ -375,7 +365,8 @@ class _PodcastManageState extends State<PodcastManage>
|
|||||||
title: Text(
|
title: Text(
|
||||||
'Delete confirm'),
|
'Delete confirm'),
|
||||||
content: Text(
|
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>[
|
actions: <Widget>[
|
||||||
FlatButton(
|
FlatButton(
|
||||||
onPressed: () =>
|
onPressed: () =>
|
||||||
@ -533,9 +524,6 @@ class _AddGroupState extends State<AddGroup> {
|
|||||||
Theme.of(context).brightness == Brightness.light
|
Theme.of(context).brightness == Brightness.light
|
||||||
? Color.fromRGBO(113, 113, 113, 1)
|
? Color.fromRGBO(113, 113, 113, 1)
|
||||||
: Color.fromRGBO(5, 5, 5, 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(
|
child: AlertDialog(
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
|
89
lib/settings/play_setting.dart
Normal file
89
lib/settings/play_setting.dart
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import '../class/audiostate.dart';
|
||||||
|
|
||||||
|
class PlaySetting extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
|
||||||
|
return AnnotatedRegion<SystemUiOverlayStyle>(
|
||||||
|
value: SystemUiOverlayStyle(
|
||||||
|
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
|
||||||
|
systemNavigationBarColor: Theme.of(context).primaryColor,
|
||||||
|
systemNavigationBarIconBrightness:
|
||||||
|
Theme.of(context).accentColorBrightness,
|
||||||
|
),
|
||||||
|
child: Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text('Player Setting'),
|
||||||
|
elevation: 0,
|
||||||
|
backgroundColor: Theme.of(context).primaryColor,
|
||||||
|
),
|
||||||
|
body: SingleChildScrollView(
|
||||||
|
scrollDirection: Axis.vertical,
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.all(10.0),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
height: 30.0,
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 80),
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Text('Playlist',
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.bodyText1
|
||||||
|
.copyWith(color: Theme.of(context).accentColor)),
|
||||||
|
),
|
||||||
|
ListView(
|
||||||
|
physics: const BouncingScrollPhysics(),
|
||||||
|
shrinkWrap: true,
|
||||||
|
scrollDirection: Axis.vertical,
|
||||||
|
children: <Widget>[
|
||||||
|
ListTile(
|
||||||
|
contentPadding:
|
||||||
|
EdgeInsets.only(left: 80.0, right: 20, bottom: 0),
|
||||||
|
title: Text('Autoplay'),
|
||||||
|
subtitle: Text('Autoplay next episode in playlist'),
|
||||||
|
trailing: Selector<AudioPlayerNotifier, bool>(
|
||||||
|
selector: (_, audio) => audio.autoPlay,
|
||||||
|
builder: (_, data, __) => Switch(
|
||||||
|
value: data,
|
||||||
|
onChanged: (boo) => audio.autoPlaySwitch = boo),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Divider(height: 2),
|
||||||
|
// ListTile(
|
||||||
|
// contentPadding:
|
||||||
|
// EdgeInsets.only(left: 80.0, right: 20, bottom: 0),
|
||||||
|
// title: Text('Autoadd'),
|
||||||
|
// subtitle:
|
||||||
|
// Text('Autoadd new updated episodes to playlist'),
|
||||||
|
// trailing: Selector<AudioPlayerNotifier, bool>(
|
||||||
|
// selector: (_, audio) => audio.autoAdd,
|
||||||
|
// builder: (_, data, __) => Switch(
|
||||||
|
// value: data,
|
||||||
|
// onChanged: (boo) => audio.autoAddSwitch = boo),
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// Divider(height: 2),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -2,7 +2,6 @@ import 'dart:io';
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
import 'package:path/path.dart';
|
import 'package:path/path.dart';
|
||||||
import 'package:line_icons/line_icons.dart';
|
import 'package:line_icons/line_icons.dart';
|
||||||
import 'package:tsacdop/class/podcastlocal.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:path_provider/path_provider.dart';
|
||||||
import 'package:flutter_file_dialog/flutter_file_dialog.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/ompl_build.dart';
|
||||||
import 'package:tsacdop/util/context_extension.dart';
|
import 'package:tsacdop/util/context_extension.dart';
|
||||||
import 'package:tsacdop/intro_slider/app_intro.dart';
|
import 'package:tsacdop/intro_slider/app_intro.dart';
|
||||||
@ -20,6 +18,7 @@ import 'storage.dart';
|
|||||||
import 'history.dart';
|
import 'history.dart';
|
||||||
import 'syncing.dart';
|
import 'syncing.dart';
|
||||||
import 'libries.dart';
|
import 'libries.dart';
|
||||||
|
import 'play_setting.dart';
|
||||||
|
|
||||||
class Settings extends StatelessWidget {
|
class Settings extends StatelessWidget {
|
||||||
_launchUrl(String url) async {
|
_launchUrl(String url) async {
|
||||||
@ -46,7 +45,6 @@ class Settings extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
|
|
||||||
return AnnotatedRegion<SystemUiOverlayStyle>(
|
return AnnotatedRegion<SystemUiOverlayStyle>(
|
||||||
value: SystemUiOverlayStyle(
|
value: SystemUiOverlayStyle(
|
||||||
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
|
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
|
||||||
@ -103,17 +101,21 @@ class Settings extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
Divider(height: 2),
|
Divider(height: 2),
|
||||||
ListTile(
|
ListTile(
|
||||||
|
onTap: () => Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => PlaySetting())),
|
||||||
contentPadding:
|
contentPadding:
|
||||||
EdgeInsets.symmetric(horizontal: 25.0),
|
EdgeInsets.symmetric(horizontal: 25.0),
|
||||||
leading: Icon(LineIcons.play_circle),
|
leading: Icon(LineIcons.play_circle),
|
||||||
title: Text('AutoPlay'),
|
title: Text('Play'),
|
||||||
subtitle: Text('Autoplay next episode in playlist'),
|
subtitle: Text('Playlist and player'),
|
||||||
trailing: Selector<AudioPlayerNotifier, bool>(
|
// trailing: Selector<AudioPlayerNotifier, bool>(
|
||||||
selector: (_, audio) => audio.autoPlay,
|
// selector: (_, audio) => audio.autoPlay,
|
||||||
builder: (_, data, __) => Switch(
|
// builder: (_, data, __) => Switch(
|
||||||
value: data,
|
// value: data,
|
||||||
onChanged: (boo) => audio.autoPlaySwitch = boo),
|
// onChanged: (boo) => audio.autoPlaySwitch = boo),
|
||||||
),
|
// ),
|
||||||
),
|
),
|
||||||
Divider(height: 2),
|
Divider(height: 2),
|
||||||
ListTile(
|
ListTile(
|
||||||
@ -160,7 +162,7 @@ class Settings extends StatelessWidget {
|
|||||||
EdgeInsets.symmetric(horizontal: 25.0),
|
EdgeInsets.symmetric(horizontal: 25.0),
|
||||||
leading: Icon(LineIcons.file_code_solid),
|
leading: Icon(LineIcons.file_code_solid),
|
||||||
title: Text('Export'),
|
title: Text('Export'),
|
||||||
subtitle: Text('Export ompl file'),
|
subtitle: Text('Export ompl file of all podcasts'),
|
||||||
),
|
),
|
||||||
Divider(height: 2),
|
Divider(height: 2),
|
||||||
],
|
],
|
||||||
@ -214,7 +216,7 @@ class Settings extends StatelessWidget {
|
|||||||
Divider(height: 2),
|
Divider(height: 2),
|
||||||
ListTile(
|
ListTile(
|
||||||
onTap: () => _launchUrl(
|
onTap: () => _launchUrl(
|
||||||
'mailto:<tsacdop@stonegate.me>?subject=Tsacdop Feedback'),
|
'mailto:<tsacdop.app@gmail.com>?subject=Tsacdop Feedback'),
|
||||||
contentPadding:
|
contentPadding:
|
||||||
EdgeInsets.symmetric(horizontal: 25.0),
|
EdgeInsets.symmetric(horizontal: 25.0),
|
||||||
leading: Icon(LineIcons.bug_solid),
|
leading: Icon(LineIcons.bug_solid),
|
||||||
|
@ -55,7 +55,7 @@ class StorageSetting extends StatelessWidget {
|
|||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => DownloadsManage())),
|
builder: (context) => DownloadsManage())),
|
||||||
contentPadding:
|
contentPadding:
|
||||||
EdgeInsets.only(left: 80.0, right: 25),
|
EdgeInsets.only(left: 80.0, right: 25, bottom: 10),
|
||||||
title: Text('Ask before using cellular data'),
|
title: Text('Ask before using cellular data'),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
'Ask to confirm when using cellular data to download episodes.'),
|
'Ask to confirm when using cellular data to download episodes.'),
|
||||||
|
@ -63,10 +63,10 @@ class SyncingSetting extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
contentPadding: EdgeInsets.only(
|
contentPadding: EdgeInsets.only(
|
||||||
left: 80.0, right: 20, bottom: 20),
|
left: 80.0, right: 20, bottom: 10),
|
||||||
title: Text('Enable syncing'),
|
title: Text('Enable syncing'),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
'Refresh all podcasts in the background to get leatest episodes.'),
|
'Refresh all podcasts in the background to get leatest episodes'),
|
||||||
trailing: Switch(
|
trailing: Switch(
|
||||||
value: data.item1,
|
value: data.item1,
|
||||||
onChanged: (boo) async {
|
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
663
lib/util/duraiton_picker.dart
Normal file
663
lib/util/duraiton_picker.dart
Normal file
@ -0,0 +1,663 @@
|
|||||||
|
//Forked from https://github.com/cdharris/flutter_duration_picker
|
||||||
|
//Copyright https://github.com/cdharris
|
||||||
|
//License MIT https://github.com/cdharris/flutter_duration_picker/blob/master/LICENSE
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
const Duration _kDialAnimateDuration = const Duration(milliseconds: 200);
|
||||||
|
|
||||||
|
const double _kDurationPickerWidthPortrait = 328.0;
|
||||||
|
const double _kDurationPickerWidthLandscape = 512.0;
|
||||||
|
|
||||||
|
const double _kDurationPickerHeightPortrait = 380.0;
|
||||||
|
const double _kDurationPickerHeightLandscape = 304.0;
|
||||||
|
|
||||||
|
const double _kTwoPi = 2 * math.pi;
|
||||||
|
const double _kPiByTwo = math.pi / 2;
|
||||||
|
|
||||||
|
const double _kCircleTop = _kPiByTwo;
|
||||||
|
|
||||||
|
class _DialPainter extends CustomPainter {
|
||||||
|
const _DialPainter({
|
||||||
|
@required this.context,
|
||||||
|
@required this.labels,
|
||||||
|
@required this.backgroundColor,
|
||||||
|
@required this.accentColor,
|
||||||
|
@required this.theta,
|
||||||
|
@required this.textDirection,
|
||||||
|
@required this.selectedValue,
|
||||||
|
@required this.pct,
|
||||||
|
@required this.multiplier,
|
||||||
|
@required this.secondHand,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<TextPainter> labels;
|
||||||
|
final Color backgroundColor;
|
||||||
|
final Color accentColor;
|
||||||
|
final double theta;
|
||||||
|
final TextDirection textDirection;
|
||||||
|
final int selectedValue;
|
||||||
|
final BuildContext context;
|
||||||
|
|
||||||
|
final double pct;
|
||||||
|
final int multiplier;
|
||||||
|
final int secondHand;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
const double _epsilon = .001;
|
||||||
|
const double _sweep = _kTwoPi - _epsilon;
|
||||||
|
const double _startAngle = -math.pi / 2.0;
|
||||||
|
|
||||||
|
final double radius = size.shortestSide / 2.0;
|
||||||
|
final Offset center = new Offset(size.width / 2.0, size.height / 2.0);
|
||||||
|
final Offset centerPoint = center;
|
||||||
|
|
||||||
|
double pctTheta = (0.25 - (theta % _kTwoPi) / _kTwoPi) % 1.0;
|
||||||
|
|
||||||
|
// Draw the background outer ring
|
||||||
|
canvas.drawCircle(
|
||||||
|
centerPoint, radius, new Paint()..color = backgroundColor);
|
||||||
|
|
||||||
|
// Draw a translucent circle for every hour
|
||||||
|
for (int i = 0; i < multiplier; i = i + 1) {
|
||||||
|
canvas.drawCircle(centerPoint, radius,
|
||||||
|
new Paint()..color = accentColor.withOpacity((i == 0) ? 0.3 : 0.1));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw the inner background circle
|
||||||
|
canvas.drawCircle(centerPoint, radius * 0.88,
|
||||||
|
new Paint()..color = Theme.of(context).canvasColor);
|
||||||
|
|
||||||
|
// Get the offset point for an angle value of theta, and a distance of _radius
|
||||||
|
Offset getOffsetForTheta(double theta, double _radius) {
|
||||||
|
return center +
|
||||||
|
new Offset(_radius * math.cos(theta), -_radius * math.sin(theta));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw the handle that is used to drag and to indicate the position around the circle
|
||||||
|
final Paint handlePaint = new Paint()..color = accentColor;
|
||||||
|
final Offset handlePoint = getOffsetForTheta(theta, radius - 10.0);
|
||||||
|
canvas.drawCircle(handlePoint, 20.0, handlePaint);
|
||||||
|
|
||||||
|
// Draw the Text in the center of the circle which displays hours and mins
|
||||||
|
String minutes = (multiplier == 0) ? '' : "${multiplier}min ";
|
||||||
|
// int minutes = (pctTheta * 60).round();
|
||||||
|
// minutes = minutes == 60 ? 0 : minutes;
|
||||||
|
String seconds = "$secondHand";
|
||||||
|
|
||||||
|
TextPainter textDurationValuePainter = new TextPainter(
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
text: new TextSpan(
|
||||||
|
text: '$minutes$seconds',
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.headline4
|
||||||
|
.copyWith(fontSize: size.shortestSide * 0.15)),
|
||||||
|
textDirection: TextDirection.ltr)
|
||||||
|
..layout();
|
||||||
|
Offset middleForValueText = new Offset(
|
||||||
|
centerPoint.dx - (textDurationValuePainter.width / 2),
|
||||||
|
centerPoint.dy - textDurationValuePainter.height / 2);
|
||||||
|
textDurationValuePainter.paint(canvas, middleForValueText);
|
||||||
|
|
||||||
|
TextPainter textMinPainter = new TextPainter(
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
text: new TextSpan(
|
||||||
|
text: 'sec', //th: ${theta}',
|
||||||
|
style: Theme.of(context).textTheme.bodyText1),
|
||||||
|
textDirection: TextDirection.ltr)
|
||||||
|
..layout();
|
||||||
|
textMinPainter.paint(
|
||||||
|
canvas,
|
||||||
|
new Offset(
|
||||||
|
centerPoint.dx - (textMinPainter.width / 2),
|
||||||
|
centerPoint.dy +
|
||||||
|
(textDurationValuePainter.height / 2) -
|
||||||
|
textMinPainter.height / 2));
|
||||||
|
|
||||||
|
// Draw an arc around the circle for the amount of the circle that has elapsed.
|
||||||
|
var elapsedPainter = new Paint()
|
||||||
|
..style = PaintingStyle.stroke
|
||||||
|
..strokeCap = StrokeCap.round
|
||||||
|
..color = accentColor.withOpacity(0.3)
|
||||||
|
..isAntiAlias = true
|
||||||
|
..strokeWidth = radius * 0.12;
|
||||||
|
|
||||||
|
canvas.drawArc(
|
||||||
|
new Rect.fromCircle(
|
||||||
|
center: centerPoint,
|
||||||
|
radius: radius - radius * 0.12 / 2,
|
||||||
|
),
|
||||||
|
_startAngle,
|
||||||
|
_sweep * pctTheta,
|
||||||
|
false,
|
||||||
|
elapsedPainter,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Paint the labels (the minute strings)
|
||||||
|
void paintLabels(List<TextPainter> labels) {
|
||||||
|
if (labels == null) return;
|
||||||
|
final double labelThetaIncrement = -_kTwoPi / labels.length;
|
||||||
|
double labelTheta = _kPiByTwo;
|
||||||
|
|
||||||
|
for (TextPainter label in labels) {
|
||||||
|
final Offset labelOffset =
|
||||||
|
new Offset(-label.width / 2.0, -label.height / 2.0);
|
||||||
|
|
||||||
|
label.paint(
|
||||||
|
canvas, getOffsetForTheta(labelTheta, radius - 40.0) + labelOffset);
|
||||||
|
|
||||||
|
labelTheta += labelThetaIncrement;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
paintLabels(labels);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(_DialPainter oldPainter) {
|
||||||
|
return oldPainter.labels != labels ||
|
||||||
|
oldPainter.backgroundColor != backgroundColor ||
|
||||||
|
oldPainter.accentColor != accentColor ||
|
||||||
|
oldPainter.theta != theta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Dial extends StatefulWidget {
|
||||||
|
const _Dial(
|
||||||
|
{@required this.duration,
|
||||||
|
@required this.onChanged,
|
||||||
|
this.snapToMins = 1.0})
|
||||||
|
: assert(duration != null);
|
||||||
|
|
||||||
|
final Duration duration;
|
||||||
|
final ValueChanged<Duration> onChanged;
|
||||||
|
|
||||||
|
/// The resolution of mins of the dial, i.e. if snapToMins = 5.0, only durations of 5min intervals will be selectable.
|
||||||
|
final double snapToMins;
|
||||||
|
@override
|
||||||
|
_DialState createState() => new _DialState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DialState extends State<_Dial> with SingleTickerProviderStateMixin {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_thetaController = new AnimationController(
|
||||||
|
duration: _kDialAnimateDuration,
|
||||||
|
vsync: this,
|
||||||
|
);
|
||||||
|
_thetaTween =
|
||||||
|
new Tween<double>(begin: _getThetaForDuration(widget.duration));
|
||||||
|
_theta = _thetaTween.animate(new CurvedAnimation(
|
||||||
|
parent: _thetaController, curve: Curves.fastOutSlowIn))
|
||||||
|
..addListener(() => setState(() {}));
|
||||||
|
_thetaController.addStatusListener((status) {
|
||||||
|
// if (status == AnimationStatus.completed && _hours != _snappedHours) {
|
||||||
|
// _hours = _snappedHours;
|
||||||
|
if (status == AnimationStatus.completed) {
|
||||||
|
_minutes = _minuteHand(_turningAngle);
|
||||||
|
_seconds = _secondHand(_turningAngle);
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// _hours = widget.duration.inHours;
|
||||||
|
|
||||||
|
_turningAngle = _kPiByTwo - widget.duration.inSeconds / 60.0 * _kTwoPi;
|
||||||
|
_minutes = _minuteHand(_turningAngle);
|
||||||
|
_seconds = _secondHand(_turningAngle);
|
||||||
|
}
|
||||||
|
|
||||||
|
ThemeData themeData;
|
||||||
|
MaterialLocalizations localizations;
|
||||||
|
MediaQueryData media;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
assert(debugCheckHasMediaQuery(context));
|
||||||
|
themeData = Theme.of(context);
|
||||||
|
localizations = MaterialLocalizations.of(context);
|
||||||
|
media = MediaQuery.of(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_thetaController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Tween<double> _thetaTween;
|
||||||
|
Animation<double> _theta;
|
||||||
|
AnimationController _thetaController;
|
||||||
|
|
||||||
|
double _pct = 0.0;
|
||||||
|
int _seconds = 0;
|
||||||
|
bool _dragging = false;
|
||||||
|
int _minutes = 0;
|
||||||
|
double _turningAngle = 0.0;
|
||||||
|
|
||||||
|
static double _nearest(double target, double a, double b) {
|
||||||
|
return ((target - a).abs() < (target - b).abs()) ? a : b;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _animateTo(double targetTheta) {
|
||||||
|
final double currentTheta = _theta.value;
|
||||||
|
double beginTheta =
|
||||||
|
_nearest(targetTheta, currentTheta, currentTheta + _kTwoPi);
|
||||||
|
beginTheta = _nearest(targetTheta, beginTheta, currentTheta - _kTwoPi);
|
||||||
|
_thetaTween
|
||||||
|
..begin = beginTheta
|
||||||
|
..end = targetTheta;
|
||||||
|
_thetaController
|
||||||
|
..value = 0.0
|
||||||
|
..forward();
|
||||||
|
}
|
||||||
|
|
||||||
|
double _getThetaForDuration(Duration duration) {
|
||||||
|
return (_kPiByTwo - (duration.inSeconds % 60) / 60.0 * _kTwoPi) % _kTwoPi;
|
||||||
|
}
|
||||||
|
|
||||||
|
Duration _getTimeForTheta(double theta) {
|
||||||
|
return _angleToDuration(_turningAngle);
|
||||||
|
}
|
||||||
|
|
||||||
|
Duration _notifyOnChangedIfNeeded() {
|
||||||
|
// final Duration current = _getTimeForTheta(_theta.value);
|
||||||
|
// var d = Duration(hours: _hours, minutes: current.inMinutes % 60);
|
||||||
|
_minutes = _minuteHand(_turningAngle);
|
||||||
|
_seconds = _secondHand(_turningAngle);
|
||||||
|
|
||||||
|
var d = _angleToDuration(_turningAngle);
|
||||||
|
|
||||||
|
widget.onChanged(d);
|
||||||
|
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateThetaForPan() {
|
||||||
|
setState(() {
|
||||||
|
final Offset offset = _position - _center;
|
||||||
|
final double angle =
|
||||||
|
(math.atan2(offset.dx, offset.dy) - _kPiByTwo) % _kTwoPi;
|
||||||
|
|
||||||
|
// Stop accidental abrupt pans from making the dial seem like it starts from 1h.
|
||||||
|
// (happens when wanting to pan from 0 clockwise, but when doing so quickly, one actually pans from before 0 (e.g. setting the duration to 59mins, and then crossing 0, which would then mean 1h 1min).
|
||||||
|
if (angle >= _kCircleTop &&
|
||||||
|
_theta.value <= _kCircleTop &&
|
||||||
|
_theta.value >= 0.1 && // to allow the radians sign change at 15mins.
|
||||||
|
_minutes == 0) return;
|
||||||
|
|
||||||
|
_thetaTween
|
||||||
|
..begin = angle
|
||||||
|
..end = angle;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Offset _position;
|
||||||
|
Offset _center;
|
||||||
|
|
||||||
|
void _handlePanStart(DragStartDetails details) {
|
||||||
|
assert(!_dragging);
|
||||||
|
_dragging = true;
|
||||||
|
final RenderBox box = context.findRenderObject();
|
||||||
|
_position = box.globalToLocal(details.globalPosition);
|
||||||
|
_center = box.size.center(Offset.zero);
|
||||||
|
//_updateThetaForPan();
|
||||||
|
_notifyOnChangedIfNeeded();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handlePanUpdate(DragUpdateDetails details) {
|
||||||
|
double oldTheta = _theta.value;
|
||||||
|
_position += details.delta;
|
||||||
|
_updateThetaForPan();
|
||||||
|
double newTheta = _theta.value;
|
||||||
|
// _updateRotations(oldTheta, newTheta);
|
||||||
|
_updateTurningAngle(oldTheta, newTheta);
|
||||||
|
_notifyOnChangedIfNeeded();
|
||||||
|
}
|
||||||
|
|
||||||
|
int _minuteHand(double angle) {
|
||||||
|
return _angleToDuration(angle).inMinutes.toInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
int _secondHand(double angle) {
|
||||||
|
// Result is in [0; 59], even if overall time is >= 1 hour
|
||||||
|
return (_angleToSeconds(angle) % 60.0).toInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
Duration _angleToDuration(double angle) {
|
||||||
|
return _secondToDuration(_angleToSeconds(angle));
|
||||||
|
}
|
||||||
|
|
||||||
|
Duration _secondToDuration(seconds) {
|
||||||
|
return Duration(
|
||||||
|
minutes: (seconds ~/ 60).toInt(), seconds: (seconds % 60.0).toInt());
|
||||||
|
}
|
||||||
|
|
||||||
|
double _angleToSeconds(double angle) {
|
||||||
|
// Coordinate transformation from mathematical COS to dial COS
|
||||||
|
double dialAngle = _kPiByTwo - angle;
|
||||||
|
|
||||||
|
// Turn dial angle into minutes, may go beyond 60 minutes (multiple turns)
|
||||||
|
return dialAngle / _kTwoPi * 60.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateTurningAngle(double oldTheta, double newTheta) {
|
||||||
|
// Register any angle by which the user has turned the dial.
|
||||||
|
//
|
||||||
|
// The resulting turning angle fully captures the state of the dial,
|
||||||
|
// including multiple turns (= full hours). The [_turningAngle] is in
|
||||||
|
// mathematical coordinate system, i.e. 3-o-clock position being zero, and
|
||||||
|
// increasing counter clock wise.
|
||||||
|
|
||||||
|
// From positive to negative (in mathematical COS)
|
||||||
|
if (newTheta > 1.5 * math.pi && oldTheta < 0.5 * math.pi) {
|
||||||
|
_turningAngle = _turningAngle - ((_kTwoPi - newTheta) + oldTheta);
|
||||||
|
}
|
||||||
|
// From negative to positive (in mathematical COS)
|
||||||
|
else if (newTheta < 0.5 * math.pi && oldTheta > 1.5 * math.pi) {
|
||||||
|
_turningAngle = _turningAngle + ((_kTwoPi - oldTheta) + newTheta);
|
||||||
|
} else {
|
||||||
|
_turningAngle = _turningAngle + (newTheta - oldTheta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handlePanEnd(DragEndDetails details) {
|
||||||
|
assert(_dragging);
|
||||||
|
_dragging = false;
|
||||||
|
_position = null;
|
||||||
|
_center = null;
|
||||||
|
//_notifyOnChangedIfNeeded();
|
||||||
|
//_animateTo(_getThetaForDuration(widget.duration));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleTapUp(TapUpDetails details) {
|
||||||
|
final RenderBox box = context.findRenderObject();
|
||||||
|
_position = box.globalToLocal(details.globalPosition);
|
||||||
|
_center = box.size.center(Offset.zero);
|
||||||
|
_updateThetaForPan();
|
||||||
|
_notifyOnChangedIfNeeded();
|
||||||
|
|
||||||
|
_animateTo(_getThetaForDuration(_getTimeForTheta(_theta.value)));
|
||||||
|
_dragging = false;
|
||||||
|
_position = null;
|
||||||
|
_center = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<TextPainter> _buildSeconds(TextTheme textTheme) {
|
||||||
|
final TextStyle style = textTheme.subtitle1;
|
||||||
|
|
||||||
|
const List<Duration> _secondsMarkerValues = const <Duration>[
|
||||||
|
const Duration(seconds: 0),
|
||||||
|
const Duration(seconds: 5),
|
||||||
|
const Duration(seconds: 10),
|
||||||
|
const Duration(seconds: 15),
|
||||||
|
const Duration(seconds: 20),
|
||||||
|
const Duration(seconds: 25),
|
||||||
|
const Duration(seconds: 30),
|
||||||
|
const Duration(seconds: 35),
|
||||||
|
const Duration(seconds: 40),
|
||||||
|
const Duration(seconds: 45),
|
||||||
|
const Duration(seconds: 50),
|
||||||
|
const Duration(seconds: 55),
|
||||||
|
];
|
||||||
|
|
||||||
|
final List<TextPainter> labels = <TextPainter>[];
|
||||||
|
for (Duration duration in _secondsMarkerValues) {
|
||||||
|
var painter = new TextPainter(
|
||||||
|
text: new TextSpan(style: style, text: duration.inSeconds.toString()),
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
)..layout();
|
||||||
|
labels.add(painter);
|
||||||
|
}
|
||||||
|
return labels;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
Color backgroundColor;
|
||||||
|
switch (themeData.brightness) {
|
||||||
|
case Brightness.light:
|
||||||
|
backgroundColor = Colors.grey[200];
|
||||||
|
break;
|
||||||
|
case Brightness.dark:
|
||||||
|
backgroundColor = themeData.backgroundColor;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
final ThemeData theme = Theme.of(context);
|
||||||
|
|
||||||
|
int selectedDialValue;
|
||||||
|
_minutes = _minuteHand(_turningAngle);
|
||||||
|
_seconds = _secondHand(_turningAngle);
|
||||||
|
|
||||||
|
return new GestureDetector(
|
||||||
|
excludeFromSemantics: true,
|
||||||
|
onPanStart: _handlePanStart,
|
||||||
|
onPanUpdate: _handlePanUpdate,
|
||||||
|
onPanEnd: _handlePanEnd,
|
||||||
|
onTapUp: _handleTapUp,
|
||||||
|
child: new CustomPaint(
|
||||||
|
painter: new _DialPainter(
|
||||||
|
pct: _pct,
|
||||||
|
multiplier: _minutes,
|
||||||
|
secondHand: _seconds,
|
||||||
|
context: context,
|
||||||
|
selectedValue: selectedDialValue,
|
||||||
|
labels: _buildSeconds(theme.textTheme),
|
||||||
|
backgroundColor: backgroundColor,
|
||||||
|
accentColor: themeData.accentColor,
|
||||||
|
theta: _theta.value,
|
||||||
|
textDirection: Directionality.of(context),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A duration picker designed to appear inside a popup dialog.
|
||||||
|
///
|
||||||
|
/// Pass this widget to [showDialog]. The value returned by [showDialog] is the
|
||||||
|
/// selected [Duration] if the user taps the "OK" button, or null if the user
|
||||||
|
/// taps the "CANCEL" button. The selected time is reported by calling
|
||||||
|
/// [Navigator.pop].
|
||||||
|
class _DurationPickerDialog extends StatefulWidget {
|
||||||
|
/// Creates a duration picker.
|
||||||
|
///
|
||||||
|
/// [initialTime] must not be null.
|
||||||
|
const _DurationPickerDialog(
|
||||||
|
{Key key, @required this.initialTime, this.snapToMins})
|
||||||
|
: assert(initialTime != null),
|
||||||
|
super(key: key);
|
||||||
|
|
||||||
|
/// The duration initially selected when the dialog is shown.
|
||||||
|
final Duration initialTime;
|
||||||
|
final double snapToMins;
|
||||||
|
|
||||||
|
@override
|
||||||
|
_DurationPickerDialogState createState() => new _DurationPickerDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DurationPickerDialogState extends State<_DurationPickerDialog> {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_selectedDuration = widget.initialTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
localizations = MaterialLocalizations.of(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
Duration get selectedDuration => _selectedDuration;
|
||||||
|
Duration _selectedDuration;
|
||||||
|
|
||||||
|
MaterialLocalizations localizations;
|
||||||
|
|
||||||
|
void _handleTimeChanged(Duration value) {
|
||||||
|
setState(() {
|
||||||
|
_selectedDuration = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleCancel() {
|
||||||
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleOk() {
|
||||||
|
Navigator.pop(context, _selectedDuration);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
assert(debugCheckHasMediaQuery(context));
|
||||||
|
final ThemeData theme = Theme.of(context);
|
||||||
|
|
||||||
|
final Widget picker = new Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: new AspectRatio(
|
||||||
|
aspectRatio: 1.0,
|
||||||
|
child: new _Dial(
|
||||||
|
duration: _selectedDuration,
|
||||||
|
onChanged: _handleTimeChanged,
|
||||||
|
snapToMins: widget.snapToMins,
|
||||||
|
)));
|
||||||
|
|
||||||
|
final Widget actions = new ButtonTheme.bar(
|
||||||
|
child: new ButtonBar(children: <Widget>[
|
||||||
|
new FlatButton(
|
||||||
|
child: new Text(localizations.cancelButtonLabel),
|
||||||
|
onPressed: _handleCancel),
|
||||||
|
new FlatButton(
|
||||||
|
child: new Text(localizations.okButtonLabel), onPressed: _handleOk),
|
||||||
|
]));
|
||||||
|
|
||||||
|
final Dialog dialog = new Dialog(child: new OrientationBuilder(
|
||||||
|
builder: (BuildContext context, Orientation orientation) {
|
||||||
|
final Widget pickerAndActions = new Container(
|
||||||
|
color: theme.dialogBackgroundColor,
|
||||||
|
child: new Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
new Expanded(
|
||||||
|
child:
|
||||||
|
picker), // picker grows and shrinks with the available space
|
||||||
|
actions,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert(orientation != null);
|
||||||
|
switch (orientation) {
|
||||||
|
case Orientation.portrait:
|
||||||
|
return new SizedBox(
|
||||||
|
width: _kDurationPickerWidthPortrait,
|
||||||
|
height: _kDurationPickerHeightPortrait,
|
||||||
|
child: new Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: <Widget>[
|
||||||
|
new Expanded(
|
||||||
|
child: pickerAndActions,
|
||||||
|
),
|
||||||
|
]));
|
||||||
|
case Orientation.landscape:
|
||||||
|
return new SizedBox(
|
||||||
|
width: _kDurationPickerWidthLandscape,
|
||||||
|
height: _kDurationPickerHeightLandscape,
|
||||||
|
child: new Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: <Widget>[
|
||||||
|
new Flexible(
|
||||||
|
child: pickerAndActions,
|
||||||
|
),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}));
|
||||||
|
|
||||||
|
return new Theme(
|
||||||
|
data: theme.copyWith(
|
||||||
|
dialogBackgroundColor: Colors.transparent,
|
||||||
|
),
|
||||||
|
child: dialog,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shows a dialog containing the duration picker.
|
||||||
|
///
|
||||||
|
/// The returned Future resolves to the duration selected by the user when the user
|
||||||
|
/// closes the dialog. If the user cancels the dialog, null is returned.
|
||||||
|
///
|
||||||
|
/// To show a dialog with [initialTime] equal to the current time:
|
||||||
|
///
|
||||||
|
/// ```dart
|
||||||
|
/// showDurationPicker(
|
||||||
|
/// initialTime: new Duration.now(),
|
||||||
|
/// context: context,
|
||||||
|
/// );
|
||||||
|
/// ```
|
||||||
|
Future<Duration> showDurationPicker(
|
||||||
|
{@required BuildContext context,
|
||||||
|
@required Duration initialTime,
|
||||||
|
double snapToMins}) async {
|
||||||
|
assert(context != null);
|
||||||
|
assert(initialTime != null);
|
||||||
|
|
||||||
|
return await showDialog<Duration>(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) => new _DurationPickerDialog(
|
||||||
|
initialTime: initialTime, snapToMins: snapToMins),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class DurationPicker extends StatelessWidget {
|
||||||
|
final Duration duration;
|
||||||
|
final ValueChanged<Duration> onChange;
|
||||||
|
final double snapToMins;
|
||||||
|
|
||||||
|
final double width;
|
||||||
|
final double height;
|
||||||
|
|
||||||
|
DurationPicker(
|
||||||
|
{this.duration = const Duration(minutes: 0),
|
||||||
|
@required this.onChange,
|
||||||
|
this.snapToMins,
|
||||||
|
this.width,
|
||||||
|
this.height});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
width: width ?? _kDurationPickerWidthPortrait / 1.5,
|
||||||
|
height: height ?? _kDurationPickerHeightPortrait / 1.5,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: <Widget>[
|
||||||
|
Expanded(
|
||||||
|
child: _Dial(
|
||||||
|
duration: duration,
|
||||||
|
onChanged: onChange,
|
||||||
|
snapToMins: snapToMins,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user