diff --git a/lib/class/audiostate.dart b/lib/class/audiostate.dart index 5ad4e38..b42a615 100644 --- a/lib/class/audiostate.dart +++ b/lib/class/audiostate.dart @@ -234,11 +234,13 @@ class AudioPlayerNotifier extends ChangeNotifier { episodeLoad(EpisodeBrief episode, {int startPosition = 0}) async { final EpisodeBrief episodeNew = await dbHelper.getRssItemWithUrl(episode.enclosureUrl); + //TODO load episode from last position when player running if (_playerRunning) { PlayHistory history = PlayHistory(_episode.title, _episode.enclosureUrl, backgroundAudioPosition / 1000, seekSliderValue); await dbHelper.saveHistory(history); AudioService.addQueueItemAt(episodeNew.toMediaItem(), 0); + //if (startPosition > 0) AudioService.seekTo(startPosition); _queue.playlist .removeWhere((item) => item.enclosureUrl == episode.enclosureUrl); _queue.playlist.insert(0, episodeNew); @@ -256,13 +258,13 @@ class AudioPlayerNotifier extends ChangeNotifier { _audioState = BasicPlaybackState.connecting; notifyListeners(); //await _queue.savePlaylist(); - _startAudioService(startPosition); + _startAudioService(startPosition, episodeNew.enclosureUrl); if (episodeNew.isNew == 1) dbHelper.removeEpisodeNewMark(episodeNew.enclosureUrl); } } - _startAudioService(int position) async { + _startAudioService(int position, String url) async { _stopOnComplete = false; _sleepTimerMode = SleepTimerMode.undefined; if (!AudioService.connected) { @@ -292,7 +294,9 @@ class AudioPlayerNotifier extends ChangeNotifier { .listen((item) async { _episode = await dbHelper.getRssItemWithMediaId(item.id); _backgroundAudioDuration = item?.duration ?? 0; - if (position > 0 && _backgroundAudioDuration > 0) { + if (position > 0 && + _backgroundAudioDuration > 0 && + _episode.enclosureUrl == url) { AudioService.seekTo(position); position = 0; } @@ -393,7 +397,7 @@ class AudioPlayerNotifier extends ChangeNotifier { _audioState = BasicPlaybackState.connecting; _queueUpdate = !_queueUpdate; notifyListeners(); - _startAudioService(_lastPostion ?? 0); + _startAudioService(_lastPostion ?? 0, _queue.playlist.first.enclosureUrl); } playNext() async { @@ -698,7 +702,7 @@ class AudioPlayerTask extends BackgroundAudioTask { // } else { _playing = true; - if (_audioPlayer.playbackEvent.state != AudioPlaybackState.connecting && + if (_audioPlayer.playbackEvent.state != AudioPlaybackState.connecting || _audioPlayer.playbackEvent.state != AudioPlaybackState.none) _audioPlayer.play(); } @@ -712,7 +716,13 @@ class AudioPlayerTask extends BackgroundAudioTask { playFromStart() async { _playing = true; - _audioPlayer.play(); + if (_audioPlayer.playbackEvent.state != AudioPlaybackState.connecting || + _audioPlayer.playbackEvent.state != AudioPlaybackState.none) + try { + _audioPlayer.play(); + } catch (e) { + _setState(state: BasicPlaybackState.error); + } if (mediaItem.extras['skip'] > 0) { _audioPlayer.seek(Duration(seconds: mediaItem.extras['skip'])); } @@ -729,7 +739,7 @@ class AudioPlayerTask extends BackgroundAudioTask { @override void onSeekTo(int position) { - if (_audioPlayer.playbackEvent.state != AudioPlaybackState.connecting && + if (_audioPlayer.playbackEvent.state != AudioPlaybackState.connecting || _audioPlayer.playbackEvent.state != AudioPlaybackState.none) _audioPlayer.seek(Duration(milliseconds: position)); } @@ -825,6 +835,7 @@ class AudioPlayerTask extends BackgroundAudioTask { break; case 'setSpeed': await _audioPlayer.setSpeed(argument); + break; } } diff --git a/lib/episodes/episodedetail.dart b/lib/episodes/episodedetail.dart index 10b52fc..3241d7a 100644 --- a/lib/episodes/episodedetail.dart +++ b/lib/episodes/episodedetail.dart @@ -216,7 +216,7 @@ class _EpisodeDetailState extends State { data: _description, linkStyle: TextStyle( color: Theme.of(context).accentColor, - // decoration: TextDecoration.underline, + // decoration: TextDecoration.underline, textBaseline: TextBaseline.ideographic), onLinkTap: (url) { _launchUrl(url); @@ -243,8 +243,8 @@ class _EpisodeDetailState extends State { linkStyle: TextStyle( color: Theme.of(context).accentColor, - // decoration: - // TextDecoration.underline, + // decoration: + // TextDecoration.underline, ), ), ) @@ -280,7 +280,8 @@ class _EpisodeDetailState extends State { selector: (_, audio) => audio.playerRunning, builder: (_, data, __) { return Padding( - padding: EdgeInsets.only(bottom: data ? 60.0 : 0), + padding: EdgeInsets.only( + bottom: data ? 60.0 : 0), ); }), ], @@ -379,6 +380,22 @@ class _MenuBarState extends State { @override Widget build(BuildContext context) { var audio = Provider.of(context, listen: false); + OverlayEntry _createOverlayEntry() { + RenderBox renderBox = context.findRenderObject(); + var offset = renderBox.localToGlobal(Offset.zero); + return OverlayEntry( + builder: (constext) => Positioned( + left: offset.dx + 50, + top: offset.dy - 60, + child: Container( + width: 70, + height: 100, + //color: Colors.grey[200], + child: HeartOpen(width: 50, height: 80)), + ), + ); + } + return Container( height: 50.0, decoration: BoxDecoration( @@ -418,8 +435,14 @@ class _MenuBarState extends State { Icon( Icons.favorite_border, color: Colors.grey[700], - ), - () => saveLiked(widget.episodeItem.enclosureUrl)) + ), () async { + await saveLiked(widget.episodeItem.enclosureUrl); + OverlayEntry _overlayEntry; + _overlayEntry = _createOverlayEntry(); + Overlay.of(context).insert(_overlayEntry); + await Future.delayed(Duration(seconds: 2)); + _overlayEntry?.remove(); + }) : (snapshot.data && !_liked) ? _buttonOnMenu( Icon( @@ -427,18 +450,14 @@ class _MenuBarState extends State { color: Colors.red, ), () => setUnliked(widget.episodeItem.enclosureUrl)) - : Stack( - alignment: Alignment.center, - children: [ - LoveOpen(), - _buttonOnMenu( - Icon( - Icons.favorite, - color: Colors.red, - ), - () => setUnliked( - widget.episodeItem.enclosureUrl)), - ], + : _buttonOnMenu( + Icon( + Icons.favorite, + color: Colors.red, + ), + () { + setUnliked(widget.episodeItem.enclosureUrl); + }, ); }, ), diff --git a/lib/home/appbar/about.dart b/lib/home/appbar/about.dart index 0480aa8..d6b09c1 100644 --- a/lib/home/appbar/about.dart +++ b/lib/home/appbar/about.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:tsacdop/util/custompaint.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:line_icons/line_icons.dart'; @@ -43,6 +44,23 @@ class AboutApp extends StatelessWidget { @override Widget build(BuildContext context) { + OverlayEntry _createOverlayEntry(TapDownDetails detail) { + // RenderBox renderBox = context.findRenderObject(); + var offset = detail.globalPosition; + return OverlayEntry( + builder: (constext) => Positioned( + left: offset.dx - 5, + top: offset.dy - 120, + child: Container( + width: 20, + height: 120, + color: Colors.transparent, + alignment: Alignment.topCenter, + child: HeartSet(height: 120, width: 20)), + ), + ); + } + return AnnotatedRegion( value: SystemUiOverlayStyle( statusBarIconBrightness: Theme.of(context).accentColorBrightness, @@ -108,10 +126,10 @@ class AboutApp extends StatelessWidget { TextStyle(color: Theme.of(context).accentColor), ), ), - _listItem(context, 'GitHub', LineIcons.github, - 'https://github.com/stonaga/'), _listItem(context, 'Twitter', LineIcons.twitter, 'https://twitter.com/shimenmen'), + _listItem(context, 'GitHub', LineIcons.github_alt, + 'https://github.com/stonega'), _listItem(context, 'Medium', LineIcons.medium, 'https://medium.com/@stonegate'), ], @@ -121,28 +139,37 @@ class AboutApp extends StatelessWidget { Container( height: 50, alignment: Alignment.center, - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Image.asset( - 'assets/text.png', - height: 25, - ), - Padding( - padding: EdgeInsets.symmetric(horizontal: 5), - ), - Icon( - Icons.favorite, - color: Colors.blue, - ), - Padding( - padding: EdgeInsets.symmetric(horizontal: 5), - ), - FlutterLogo( - size: 18, - ), - ], + child: GestureDetector( + onTapDown: (detail) async { + OverlayEntry _overlayEntry; + _overlayEntry = _createOverlayEntry(detail); + Overlay.of(context).insert(_overlayEntry); + await Future.delayed(Duration(seconds: 2)); + _overlayEntry?.remove(); + }, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + 'assets/text.png', + height: 25, + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 5), + ), + Icon( + Icons.favorite, + color: Colors.blue, + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 5), + ), + FlutterLogo( + size: 18, + ), + ], + ), ), ), ], diff --git a/lib/home/appbar/addpodcast.dart b/lib/home/appbar/addpodcast.dart index 09b7df7..e77b84c 100644 --- a/lib/home/appbar/addpodcast.dart +++ b/lib/home/appbar/addpodcast.dart @@ -470,7 +470,7 @@ class _SearchResultState extends State setState(() => _issubscribe = true); Fluttertoast.showToast( msg: 'Podcast subscribed', - gravity: ToastGravity.TOP, + gravity: ToastGravity.BOTTOM, ); }) : OutlineButton( diff --git a/lib/podcasts/podcastdetail.dart b/lib/podcasts/podcastdetail.dart index bd685e6..560f675 100644 --- a/lib/podcasts/podcastdetail.dart +++ b/lib/podcasts/podcastdetail.dart @@ -590,7 +590,7 @@ class _AboutPodcastState extends State { var doc = parse(description); _description = parse(doc.body.text).documentElement.text; } - setState(() => _load = true); + if(mounted) setState(() => _load = true); } _launchUrl(String url) async { diff --git a/lib/podcasts/podcastmanage.dart b/lib/podcasts/podcastmanage.dart index ce6874a..6f58031 100644 --- a/lib/podcasts/podcastmanage.dart +++ b/lib/podcasts/podcastmanage.dart @@ -49,7 +49,7 @@ class _PodcastManageState extends State }); }); _menuAnimation = Tween(begin: 0.0, end: 1.0).animate( - CurvedAnimation(parent: _menuController, curve: Curves.easeInOutBack)) + CurvedAnimation(parent: _menuController, curve: Curves.easeIn)) ..addListener(() { if (mounted) setState(() => _menuValue = _menuAnimation.value); }); diff --git a/lib/settings/settting.dart b/lib/settings/settting.dart index 21b8407..a901367 100644 --- a/lib/settings/settting.dart +++ b/lib/settings/settting.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -20,7 +21,13 @@ import 'syncing.dart'; import 'libries.dart'; import 'play_setting.dart'; -class Settings extends StatelessWidget { +class Settings extends StatefulWidget { + @override + _SettingsState createState() => _SettingsState(); +} + +class _SettingsState extends State + with SingleTickerProviderStateMixin { _launchUrl(String url) async { if (await canLaunch(url)) { await launch(url); @@ -43,6 +50,48 @@ class Settings extends StatelessWidget { print(ompl.toString()); } + bool _showFeedback; + Animation _animation; + AnimationController _controller; + double _value; + @override + void initState() { + super.initState(); + _showFeedback = false; + _value = 0; + _controller = + AnimationController(vsync: this, duration: Duration(milliseconds: 300)); + _animation = Tween(begin: 0.0, end: 1.0).animate(_controller) + ..addListener(() { + setState(() { + _value = _animation.value; + }); + }); + } + + Widget _feedbackItem(IconData icon, String name, String url) => Material( + color: Colors.transparent, + child: InkWell( + onTap: () => _launchUrl(url), + child: Container( + padding: EdgeInsets.all(5), + alignment: Alignment.center, + child: Column( + children: [ + Icon( + icon, + size: 20 * _value, + ), + Padding( + padding: EdgeInsets.symmetric(vertical: 5), + ), + Text(name) + ], + ), + ), + ), + ); + @override Widget build(BuildContext context) { return AnnotatedRegion( @@ -110,12 +159,12 @@ class Settings extends StatelessWidget { leading: Icon(LineIcons.play_circle), title: Text('Play'), subtitle: Text('Playlist and player'), - // trailing: Selector( - // selector: (_, audio) => audio.autoPlay, - // builder: (_, data, __) => Switch( - // value: data, - // onChanged: (boo) => audio.autoPlaySwitch = boo), - // ), + // trailing: Selector( + // selector: (_, audio) => audio.autoPlay, + // builder: (_, data, __) => Switch( + // value: data, + // onChanged: (boo) => audio.autoPlaySwitch = boo), + // ), ), Divider(height: 2), ListTile( @@ -215,15 +264,51 @@ class Settings extends StatelessWidget { ), Divider(height: 2), ListTile( - onTap: () => _launchUrl( - 'mailto:?subject=Tsacdop Feedback'), + onTap: () { + if (_value == 0) + _controller.forward(); + else + _controller.reverse(); + _showFeedback = !_showFeedback; + }, contentPadding: EdgeInsets.symmetric(horizontal: 25.0), leading: Icon(LineIcons.bug_solid), title: Text('Feedback'), - subtitle: Text('Bugs and feature requests'), + subtitle: Text('Bugs and feature request'), + trailing: Transform.rotate( + angle: math.pi * _value, + child: Icon(Icons.keyboard_arrow_down), + ), + ), + _showFeedback + ? Row( + mainAxisAlignment: + MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + _feedbackItem( + LineIcons.github, + 'Submit issue', + 'https://github.com/stonega/tsacdop/issues'), + _feedbackItem( + LineIcons.telegram, + 'Join group', + 'https://t.me/joinchat/Bk3LkRpTHy40QYC78PK7Qg'), + _feedbackItem( + LineIcons.envelope_open_text_solid, + 'Write to me', + 'mailto:?subject=Tsacdop Feedback'), + _feedbackItem( + LineIcons.google_play, + 'Rate on Play', + 'https://play.google.com/store/apps/details?id=com.stonegate.tsacdop') + ], + ) + : Center(), + Divider( + height: 2, ), - Divider(height: 2), ListTile( onTap: () => Navigator.push( context, @@ -238,6 +323,9 @@ class Settings extends StatelessWidget { Divider(height: 2), ], ), + Padding( + padding: EdgeInsets.all(10.0), + ), ], ), ], diff --git a/lib/settings/storage.dart b/lib/settings/storage.dart index 8923262..9fb7b0c 100644 --- a/lib/settings/storage.dart +++ b/lib/settings/storage.dart @@ -55,7 +55,7 @@ class StorageSetting extends StatelessWidget { MaterialPageRoute( builder: (context) => DownloadsManage())), contentPadding: - EdgeInsets.only(left: 80.0, right: 25, bottom: 10), + EdgeInsets.only(left: 80.0, right: 25, bottom: 10, top: 10), title: Text('Ask before using cellular data'), subtitle: Text( 'Ask to confirm when using cellular data to download episodes.'), @@ -112,7 +112,7 @@ class StorageSetting extends StatelessWidget { contentPadding: EdgeInsets.symmetric(horizontal: 80.0), // leading: Icon(Icons.colorize), title: Text('Cache'), - subtitle: Text('Audio cache'), + subtitle: Text('App cache'), ), Divider(height: 2), ], diff --git a/lib/settings/theme.dart b/lib/settings/theme.dart index bb2cc28..8e0b8fa 100644 --- a/lib/settings/theme.dart +++ b/lib/settings/theme.dart @@ -123,7 +123,7 @@ class ThemeSetting extends StatelessWidget { contentPadding: EdgeInsets.only(left: 80.0, right: 20, bottom: 10), // leading: Icon(Icons.colorize), - title: Text('Real Dark'), + title: Text('Real Dark',), subtitle: Text( 'Turn on if you think the night is not dark enough'), trailing: Selector( diff --git a/lib/util/custompaint.dart b/lib/util/custompaint.dart index e96998f..e55f99c 100644 --- a/lib/util/custompaint.dart +++ b/lib/util/custompaint.dart @@ -478,7 +478,7 @@ class _LoveOpenState extends State super.initState(); _controller = AnimationController( vsync: this, - duration: Duration(milliseconds: 300), + duration: Duration(milliseconds: 1000), ); _animationA = Tween(begin: 0.0, end: 1.0).animate(_controller) @@ -553,3 +553,137 @@ class _LoveOpenState extends State ); } } + +//Heart rise +class HeartSet extends StatefulWidget { + final double height; + final double width; + HeartSet({Key key, this.height, this.width}) : super(key: key); + + @override + _HeartSetState createState() => _HeartSetState(); +} + +class _HeartSetState extends State + with SingleTickerProviderStateMixin { + Animation _animation; + AnimationController _controller; + double _value; + @override + void initState() { + super.initState(); + _value = 0; + _controller = AnimationController( + vsync: this, + duration: Duration(seconds: 2), + ); + + _animation = Tween(begin: 0.0, end: 1.0).animate(_controller) + ..addListener(() { + if (mounted) + setState(() { + _value = _animation.value; + }); + }); + + _controller.forward(); + _controller.addStatusListener((status) { + if (status == AnimationStatus.completed) { + _controller.reset(); + } + }); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Container( + height: widget.height, + width: widget.width, + alignment: Alignment(0.5, 1 - _value), + child: Icon(Icons.favorite, + color: Colors.blue.withOpacity(0.7), size: 20 * _value), + ); + } +} + +class HeartOpen extends StatefulWidget { + final double height; + final double width; + HeartOpen({Key key, this.height, this.width}) : super(key: key); + + @override + _HeartOpenState createState() => _HeartOpenState(); +} + +class _HeartOpenState extends State + with SingleTickerProviderStateMixin { + Animation _animation; + AnimationController _controller; + double _value; + @override + void initState() { + super.initState(); + _value = 0; + _controller = AnimationController( + vsync: this, + duration: Duration(seconds: 2), + ); + + _animation = Tween(begin: 0.0, end: 1.0).animate(_controller) + ..addListener(() { + if (mounted) + setState(() { + _value = _animation.value; + }); + }); + + _controller.forward(); + _controller.addStatusListener((status) { + if (status == AnimationStatus.completed) { + _controller.reset(); + } + }); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + Widget _position(int i) { + double scale = _list[i]; + double position = _list[i + 1]; + return Positioned( + left: widget.width * position, + bottom: widget.height * _value * scale, + child: Icon(Icons.favorite, + color: _value > 0.5 ? Colors.red.withOpacity(2 - _value*2) : Colors.red, size: 20 * _value * scale), + ); + } + + List _list = + List.generate(20, (index) => math.Random().nextDouble()); + List _index = List.generate(19, (index) => index); + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Container( + height: widget.height, + width: widget.width, + alignment: Alignment(0.5, 1 - _value), + child: Icon(Icons.favorite, + color: Colors.blue.withOpacity(0.7), size: 20 * _value), + ), + ..._index.map((e) => _position(e)).toList(), + ], + ); + } +} diff --git a/lib/util/duraiton_picker.dart b/lib/util/duraiton_picker.dart index b120304..a33185e 100644 --- a/lib/util/duraiton_picker.dart +++ b/lib/util/duraiton_picker.dart @@ -532,14 +532,13 @@ class _DurationPickerDialogState extends State<_DurationPickerDialog> { snapToMins: widget.snapToMins, ))); - final Widget actions = new ButtonTheme.bar( - child: new ButtonBar(children: [ + final Widget actions = ButtonBar(children: [ 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) { diff --git a/lib/util/open_container.dart b/lib/util/open_container.dart index 52cf682..14c6519 100644 --- a/lib/util/open_container.dart +++ b/lib/util/open_container.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; +import 'context_extension.dart'; /// Signature for a function that creates a [Widget] to be used within an /// [OpenContainer]. @@ -736,7 +737,8 @@ class _OpenContainerRoute extends ModalRoute { offset: Offset(rect.left, rect.top), child: SizedBox( width: rect.width, - height: rect.height, + height: rect.height * + (playerRunning ? (1 - 60 / context.width) : 1), child: Material( clipBehavior: Clip.antiAlias, animationDuration: Duration.zero, diff --git a/lib/util/pageroute.dart b/lib/util/pageroute.dart index f6ad16c..ea2ee60 100644 --- a/lib/util/pageroute.dart +++ b/lib/util/pageroute.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'context_extension.dart'; //Slide Transition class SlideLeftRoute extends PageRouteBuilder { @@ -27,6 +28,38 @@ class SlideLeftRoute extends PageRouteBuilder { ); } +class SlideLeftHideRoute extends PageRouteBuilder { + final Widget page; + SlideLeftHideRoute({this.page}) + : super( + pageBuilder: ( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + ) => + page, + transitionsBuilder: ( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget child, + ) => + SlideTransition( + position: Tween( + begin: const Offset(1, 0), + end: Offset.zero, + ).animate(animation), + child: Container( + alignment: Alignment.topLeft, + child: SizedBox( + width: context.width, + height: context.height, + child: child), + ), + ), + ); +} + class SlideUptRoute extends PageRouteBuilder { final Widget page; SlideUptRoute({this.page})