Skip at begin

One click to add new episodes to playlist
Improve feedback options
Bugs fixed
This commit is contained in:
stonegate 2020-04-25 21:50:27 +08:00
parent 599fc75647
commit db54bf0bfa
13 changed files with 384 additions and 71 deletions

View File

@ -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;
}
}

View File

@ -216,7 +216,7 @@ class _EpisodeDetailState extends State<EpisodeDetail> {
data: _description,
linkStyle: TextStyle(
color: Theme.of(context).accentColor,
// decoration: TextDecoration.underline,
// decoration: TextDecoration.underline,
textBaseline: TextBaseline.ideographic),
onLinkTap: (url) {
_launchUrl(url);
@ -243,8 +243,8 @@ class _EpisodeDetailState extends State<EpisodeDetail> {
linkStyle: TextStyle(
color:
Theme.of(context).accentColor,
// decoration:
// TextDecoration.underline,
// decoration:
// TextDecoration.underline,
),
),
)
@ -280,7 +280,8 @@ class _EpisodeDetailState extends State<EpisodeDetail> {
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<MenuBar> {
@override
Widget build(BuildContext context) {
var audio = Provider.of<AudioPlayerNotifier>(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<MenuBar> {
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<MenuBar> {
color: Colors.red,
),
() => setUnliked(widget.episodeItem.enclosureUrl))
: Stack(
alignment: Alignment.center,
children: <Widget>[
LoveOpen(),
_buttonOnMenu(
Icon(
Icons.favorite,
color: Colors.red,
),
() => setUnliked(
widget.episodeItem.enclosureUrl)),
],
: _buttonOnMenu(
Icon(
Icons.favorite,
color: Colors.red,
),
() {
setUnliked(widget.episodeItem.enclosureUrl);
},
);
},
),

View File

@ -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<SystemUiOverlayStyle>(
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: <Widget>[
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: <Widget>[
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,
),
],
),
),
),
],

View File

@ -470,7 +470,7 @@ class _SearchResultState extends State<SearchResult>
setState(() => _issubscribe = true);
Fluttertoast.showToast(
msg: 'Podcast subscribed',
gravity: ToastGravity.TOP,
gravity: ToastGravity.BOTTOM,
);
})
: OutlineButton(

View File

@ -590,7 +590,7 @@ class _AboutPodcastState extends State<AboutPodcast> {
var doc = parse(description);
_description = parse(doc.body.text).documentElement.text;
}
setState(() => _load = true);
if(mounted) setState(() => _load = true);
}
_launchUrl(String url) async {

View File

@ -49,7 +49,7 @@ class _PodcastManageState extends State<PodcastManage>
});
});
_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);
});

View File

@ -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<Settings>
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<double>(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: <Widget>[
Icon(
icon,
size: 20 * _value,
),
Padding(
padding: EdgeInsets.symmetric(vertical: 5),
),
Text(name)
],
),
),
),
);
@override
Widget build(BuildContext context) {
return AnnotatedRegion<SystemUiOverlayStyle>(
@ -110,12 +159,12 @@ class Settings extends StatelessWidget {
leading: Icon(LineIcons.play_circle),
title: Text('Play'),
subtitle: Text('Playlist and player'),
// trailing: Selector<AudioPlayerNotifier, bool>(
// selector: (_, audio) => audio.autoPlay,
// builder: (_, data, __) => Switch(
// value: data,
// onChanged: (boo) => audio.autoPlaySwitch = boo),
// ),
// trailing: Selector<AudioPlayerNotifier, bool>(
// 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:<tsacdop.app@gmail.com>?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: <Widget>[
_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:<tsacdop.app@gmail.com>?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),
),
],
),
],

View File

@ -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),
],

View File

@ -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<SettingState, bool>(

View File

@ -478,7 +478,7 @@ class _LoveOpenState extends State<LoveOpen>
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<LoveOpen>
);
}
}
//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<HeartSet>
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<HeartOpen>
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<double> _list =
List<double>.generate(20, (index) => math.Random().nextDouble());
List<int> _index = List<int>.generate(19, (index) => index);
@override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
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<Widget>((e) => _position(e)).toList(),
],
);
}
}

View File

@ -532,14 +532,13 @@ class _DurationPickerDialogState extends State<_DurationPickerDialog> {
snapToMins: widget.snapToMins,
)));
final Widget actions = new ButtonTheme.bar(
child: new ButtonBar(children: <Widget>[
final Widget actions = 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) {

View File

@ -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<void> {
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,

View File

@ -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<double> animation,
Animation<double> secondaryAnimation,
) =>
page,
transitionsBuilder: (
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) =>
SlideTransition(
position: Tween<Offset>(
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})