Add feature discovery

Add privacy page
This commit is contained in:
stonegate 2020-06-07 20:47:28 +08:00
parent 62256c7c93
commit 9c13450a9c
14 changed files with 886 additions and 456 deletions

View File

@ -71,9 +71,12 @@ class _EpisodeDetailState extends State<EpisodeDetail> {
_markListened(EpisodeBrief episode) async {
DBHelper dbHelper = DBHelper();
final PlayHistory history =
PlayHistory(episode.title, episode.enclosureUrl, 0, 1);
await dbHelper.saveHistory(history);
bool marked = await dbHelper.checkMarked(episode);
if (!marked) {
final PlayHistory history =
PlayHistory(episode.title, episode.enclosureUrl, 0, 1);
await dbHelper.saveHistory(history);
}
}
@override
@ -110,7 +113,7 @@ class _EpisodeDetailState extends State<EpisodeDetail> {
PopupMenuButton(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(10))),
elevation: 1,
elevation: 2,
tooltip: 'Menu',
itemBuilder: (context) => [
PopupMenuItem(
@ -126,13 +129,13 @@ class _EpisodeDetailState extends State<EpisodeDetail> {
child: CustomPaint(
painter: ListenedAllPainter(
context.textTheme.bodyText1.color,
stroke: 1.5)),
stroke: 2)),
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 5.0),
),
Text(
'Mark listened',
'Mark Listened',
),
],
),
@ -143,7 +146,7 @@ class _EpisodeDetailState extends State<EpisodeDetail> {
switch (value) {
case 0:
await _markListened(widget.episodeItem);
setState(() {});
if (mounted) setState(() {});
Fluttertoast.showToast(
msg: 'Mark as listened',
gravity: ToastGravity.BOTTOM,
@ -413,13 +416,6 @@ class _MenuBarState extends State<MenuBar> {
return '${(seconds ~/ 60)}:${(seconds.truncate() % 60).toString().padLeft(2, '0')}';
}
_markListened(EpisodeBrief episode) async {
DBHelper dbHelper = DBHelper();
final PlayHistory history =
PlayHistory(episode.title, episode.enclosureUrl, 0, 1);
await dbHelper.saveHistory(history);
}
@override
void initState() {
super.initState();
@ -577,36 +573,6 @@ class _MenuBarState extends State<MenuBar> {
),
)
: snapshot.data.seconds < 0.1
// ? Material(
// color: Colors.transparent,
// child: InkWell(
// onTap: () async {
// await _markListened(widget.episodeItem);
// setState(() {});
// Fluttertoast.showToast(
// msg: 'Mark as listened',
// gravity: ToastGravity.BOTTOM,
// );
// },
// child: Container(
// height: 50,
// padding: EdgeInsets.only(
// left: 15,
// right: 15,
// top: 12,
// bottom: 12),
// child: SizedBox(
// width: 22,
// height: 22,
// child: CustomPaint(
// painter: MarkListenedPainter(
// Colors.grey[700],
// stroke: 2.0),
// ),
// ),
// ),
// ),
// )
? SizedBox(
width: 1,
)
@ -668,8 +634,7 @@ class _MenuBarState extends State<MenuBar> {
),
),
Selector<AudioPlayerNotifier, Tuple2<EpisodeBrief, bool>>(
selector: (_, audio) =>
Tuple2(audio.episode, audio.playerRunning),
selector: (_, audio) => Tuple2(audio.episode, audio.playerRunning),
builder: (_, data, __) {
return (widget.episodeItem.title == data.item1?.title &&
data.item2)

View File

@ -4,6 +4,8 @@ import 'package:tsacdop/util/custompaint.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:line_icons/line_icons.dart';
import '../util/context_extension.dart';
const String version = '0.3.3';
class AboutApp extends StatelessWidget {
@ -97,13 +99,38 @@ class AboutApp extends StatelessWidget {
],
),
),
Container(
Padding(
padding: EdgeInsets.symmetric(horizontal: 50),
child: Text(
'Tsacdop is a podcast player developed in flutter, a clean, simply beautiful and friendly app.',
textAlign: TextAlign.center,
),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
FlatButton(
onPressed: () => _launchUrl(
'https://tsacdop.stonegate.me/#/privacy'),
child: Text('Privacy Policy',
style: TextStyle(color: context.accentColor)),
),
Container(
height: 4,
width: 4,
decoration: BoxDecoration(
color: context.accentColor,
shape: BoxShape.circle),
),
FlatButton(
onPressed: () => _launchUrl(
'https://tsacdop.stonegate.me/#/changelog'),
child: Text('Changelogs',
style: TextStyle(color: context.accentColor)),
),
],
),
Padding(
padding: EdgeInsets.all(10.0),
),

View File

@ -8,6 +8,7 @@ import 'package:tuple/tuple.dart';
import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart';
import 'package:line_icons/line_icons.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:feature_discovery/feature_discovery.dart';
import '../state/audiostate.dart';
import '../type/episodebrief.dart';
@ -28,6 +29,13 @@ import 'popupmenu.dart';
import 'home_groups.dart';
import 'download_list.dart';
const String addFeature = 'addFeature';
const String menuFeature = 'menuFeature';
const String playlistFeature = 'playlistFeature';
const String longTapFeature = 'longTapFeature';
const String groupsFeature = 'groupsFeature';
const String podcastFeature = 'podcastFeature';
class Home extends StatefulWidget {
@override
_HomeState createState() => _HomeState();
@ -51,11 +59,28 @@ class _HomeState extends State<Home> with SingleTickerProviderStateMixin {
}
var _androidAppRetain = MethodChannel("android_app_retain");
var feature1OverflowMode = OverflowMode.clipContent;
var feature1EnablePulsingAnimation = false;
@override
void initState() {
super.initState();
_controller = TabController(length: 3, vsync: this);
FeatureDiscovery.isDisplayed(context, addFeature).then((value) {
if (!value)
WidgetsBinding.instance.addPostFrameCallback((_) {
FeatureDiscovery.discoverFeatures(
context,
const <String>{
addFeature,
menuFeature,
playlistFeature,
groupsFeature,
podcastFeature,
},
);
});
});
}
@override
@ -110,16 +135,67 @@ class _HomeState extends State<Home> with SingleTickerProviderStateMixin {
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: <Widget>[
IconButton(
tooltip: 'Add',
icon: const Icon(
Icons.add_circle_outline),
onPressed: () async {
await showSearch<int>(
context: context,
delegate: _delegate,
);
DescribedFeatureOverlay(
featureId: addFeature,
tapTarget:
Icon(Icons.add_circle_outline),
title: const Text(
'Tap to search podcast'),
backgroundColor: Colors.cyan[600],
overflowMode: feature1OverflowMode,
onDismiss: () {
return Future.value(true);
},
description: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: <Widget>[
const Text(
'You can search podcast title , key word or RSS link to subscribe new podcast.'),
FlatButton(
color: Colors.cyan[500],
padding:
const EdgeInsets.all(0),
child: Text('Understood',
style: Theme.of(context)
.textTheme
.button
.copyWith(
color: Colors
.white)),
onPressed: () async =>
FeatureDiscovery
.completeCurrentStep(
context),
),
FlatButton(
color: Colors.cyan[500],
padding:
const EdgeInsets.all(0),
child: Text('Dismiss',
style: Theme.of(context)
.textTheme
.button
.copyWith(
color: Colors
.white)),
onPressed: () =>
FeatureDiscovery
.dismissAll(context),
),
],
),
child: IconButton(
tooltip: 'Add',
icon: const Icon(
Icons.add_circle_outline),
onPressed: () async {
await showSearch<int>(
context: context,
delegate: _delegate,
);
},
),
),
Image(
image: Theme.of(context)
@ -130,7 +206,55 @@ class _HomeState extends State<Home> with SingleTickerProviderStateMixin {
'assets/text_light.png'),
height: 30,
),
PopupMenu(),
DescribedFeatureOverlay(
featureId: menuFeature,
tapTarget: Icon(Icons.more_vert),
backgroundColor: Colors.cyan[500],
onDismiss: () =>
Future.value(true),
title: const Text(
'Tap to import OMPL'),
description: Column(
crossAxisAlignment:
CrossAxisAlignment.end,
children: <Widget>[
const Text(
'You can import OMPL file, open setting or refresh all podcast at once here.'),
FlatButton(
color: Colors.cyan[600],
padding:
const EdgeInsets.all(0),
child: Text('Understood',
style: Theme.of(context)
.textTheme
.button
.copyWith(
color: Colors
.white)),
onPressed: () async =>
FeatureDiscovery
.completeCurrentStep(
context),
),
FlatButton(
color: Colors.cyan[600],
padding:
const EdgeInsets.all(0),
child: Text('Dismiss',
style: Theme.of(context)
.textTheme
.button
.copyWith(
color: Colors
.white)),
onPressed: () =>
FeatureDiscovery
.dismissAll(
context),
),
],
),
child: PopupMenu()),
],
),
),
@ -141,10 +265,69 @@ class _HomeState extends State<Home> with SingleTickerProviderStateMixin {
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return SizedBox(
height: height,
width: width,
child: ScrollPodcasts(),
return DescribedFeatureOverlay(
featureId: groupsFeature,
tapTarget: Center(
child: Text(
'Podcast View',
textAlign: TextAlign.center,
)),
backgroundColor: Colors.cyan[500],
enablePulsingAnimation: false,
onDismiss: () => Future.value(true),
title: const Text(
'Scroll vertically to switch groups'),
description: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: <Widget>[
const Text(
'You can tap See All to add groups or manage podcasts.'),
Row(
children: [
FlatButton(
color: Colors.cyan[600],
padding:
const EdgeInsets.all(0),
child: Text('Understood',
style: Theme.of(context)
.textTheme
.button
.copyWith(
color:
Colors.white)),
onPressed: () async =>
FeatureDiscovery
.completeCurrentStep(
context),
),
Padding(
padding: EdgeInsets.symmetric(
horizontal: 5)),
FlatButton(
color: Colors.cyan[600],
padding:
const EdgeInsets.all(0),
child: Text('Dismiss',
style: Theme.of(context)
.textTheme
.button
.copyWith(
color:
Colors.white)),
onPressed: () =>
FeatureDiscovery.dismissAll(
context),
),
],
),
],
),
child: SizedBox(
height: height,
width: width,
child: ScrollPodcasts(),
),
);
},
childCount: 1,
@ -178,7 +361,63 @@ class _HomeState extends State<Home> with SingleTickerProviderStateMixin {
controller: _controller,
children: <Widget>[
NestedScrollViewInnerScrollPositionKeyWidget(
Key('tab0'), _RecentUpdate()),
Key('tab0'),
DescribedFeatureOverlay(
featureId: podcastFeature,
tapTarget: Text('Episode View',
textAlign: TextAlign.center),
backgroundColor: Colors.cyan[500],
enablePulsingAnimation: false,
onDismiss: () => Future.value(true),
title: const Text(
'Long tap to play episode instantly'),
description: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: <Widget>[
const Text(
'You can long tap to play episode or add episode to playlist.'),
Row(
children: [
FlatButton(
color: Colors.cyan[600],
padding:
const EdgeInsets.all(0),
child: Text('Understood',
style: Theme.of(context)
.textTheme
.button
.copyWith(
color:
Colors.white)),
onPressed: () async =>
FeatureDiscovery
.completeCurrentStep(
context),
),
Padding(
padding: EdgeInsets.symmetric(
horizontal: 5)),
FlatButton(
color: Colors.cyan[600],
padding:
const EdgeInsets.all(0),
child: Text('Dismiss',
style: Theme.of(context)
.textTheme
.button
.copyWith(
color:
Colors.white)),
onPressed: () =>
FeatureDiscovery.dismissAll(
context),
),
],
),
],
),
child: _RecentUpdate())),
NestedScrollViewInnerScrollPositionKeyWidget(
Key('tab1'), _MyFavorite()),
NestedScrollViewInnerScrollPositionKeyWidget(
@ -227,7 +466,41 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
children: <Widget>[
_tabBar,
Spacer(),
PlaylistButton(),
DescribedFeatureOverlay(
featureId: playlistFeature,
tapTarget: Icon(Icons.playlist_play),
backgroundColor: Colors.cyan[500],
title: const Text('Tap to open playlist'),
onDismiss: () => Future.value(true),
description: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
const Text(
'You can add episode to playlist by yourself. Episode will be auto removed from playlist when played.'),
FlatButton(
color: Colors.cyan[600],
padding: const EdgeInsets.all(0),
child: Text('Understood',
style: Theme.of(context)
.textTheme
.button
.copyWith(color: Colors.white)),
onPressed: () async =>
FeatureDiscovery.completeCurrentStep(context),
),
FlatButton(
color: Colors.cyan[600],
padding: const EdgeInsets.all(0),
child: Text('Dismiss',
style: Theme.of(context)
.textTheme
.button
.copyWith(color: Colors.white)),
onPressed: () => FeatureDiscovery.dismissAll(context),
),
],
),
child: PlaylistButton()),
],
),
Container(height: 2, color: context.primaryColor),

View File

@ -7,6 +7,7 @@ import 'package:provider/provider.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:tuple/tuple.dart';
import 'package:line_icons/line_icons.dart';
import 'package:feature_discovery/feature_discovery.dart';
import '../type/episodebrief.dart';
import '../state/podcast_group.dart';

View File

@ -316,6 +316,15 @@ class DBHelper {
: PlayHistory(episodeBrief.title, episodeBrief.enclosureUrl, 0, 0);
}
Future<bool> checkMarked(EpisodeBrief episodeBrief) async {
var dbClient = await database;
List<Map> list = await dbClient.rawQuery(
"""SELECT title, enclosure_url, seconds, seek_value, add_date FROM PlayHistory
WHERE enclosure_url = ? AND seek_value = 1 ORDER BY add_date DESC LIMIT 1""",
[episodeBrief.enclosureUrl]);
return list.length > 0;
}
DateTime _parsePubDate(String pubDate) {
if (pubDate == null) return DateTime.now();
DateTime date;
@ -512,7 +521,6 @@ class DBHelper {
url,
]);
});
}
}
int countUpdate = Sqflite.firstIntValue(await dbClient.rawQuery(
@ -534,7 +542,32 @@ class DBHelper {
Future<List<EpisodeBrief>> getRssItem(String id, int i, bool reverse) async {
var dbClient = await database;
List<EpisodeBrief> episodes = [];
if (reverse) {
if (i == -1) {
List<Map> list = await dbClient
.rawQuery("""SELECT E.title, E.enclosure_url, E.enclosure_length,
E.milliseconds, P.imagePath, P.title as feedTitle, E.duration, E.explicit, E.liked,
E.downloaded, P.primaryColor , E.media_id, E.is_new, P.skip_seconds
FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
WHERE P.id = ? ORDER BY E.milliseconds ASC""", [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]['feedTitle'],
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;
} else if (reverse) {
List<Map> list = await dbClient
.rawQuery("""SELECT E.title, E.enclosure_url, E.enclosure_length,
E.milliseconds, P.imagePath, P.title as feedTitle, E.duration, E.explicit, E.liked,
@ -558,6 +591,7 @@ class DBHelper {
list[x]['is_new'],
list[x]['skip_seconds']));
}
return episodes;
} else {
List<Map> list = await dbClient
.rawQuery("""SELECT E.title, E.enclosure_url, E.enclosure_length,
@ -582,8 +616,8 @@ class DBHelper {
list[x]['is_new'],
list[x]['skip_seconds']));
}
return episodes;
}
return episodes;
}
Future<List<EpisodeBrief>> getNewEpisodes(String id) async {
@ -1041,13 +1075,12 @@ class DBHelper {
}
}
Future<String> getImageUrl(String url) async{
Future<String> getImageUrl(String url) async {
var dbClient = await database;
List<Map> list = await dbClient.rawQuery(
"""SELECT P.imageUrl FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
WHERE E.enclosure_url = ?""", [url]);
if(list.length ==0)
return null;
if (list.length == 0) return null;
return list.first["imageUrl"];
}
}

View File

@ -1,8 +1,10 @@
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:provider/provider.dart';
import 'package:flutter/services.dart';
import 'package:flutter_downloader/flutter_downloader.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:feature_discovery/feature_discovery.dart';
import 'generated/l10n.dart';
import 'state/podcast_group.dart';
@ -16,6 +18,7 @@ import 'intro_slider/app_intro.dart';
final SettingState themeSetting = SettingState();
Future main() async {
timeDilation = 1.0;
WidgetsFlutterBinding.ensureInitialized();
await themeSetting.initData();
runApp(
@ -49,31 +52,34 @@ class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return Consumer<SettingState>(
builder: (_, setting, __) {
return MaterialApp(
themeMode: setting.theme,
debugShowCheckedModeBanner: false,
title: 'Tsacdop',
theme: lightTheme.copyWith(
accentColor: setting.accentSetColor,
cursorColor: setting.accentSetColor,
toggleableActiveColor: setting.accentSetColor),
darkTheme: ThemeData.dark().copyWith(
accentColor: setting.accentSetColor,
primaryColorDark: Colors.grey[800],
scaffoldBackgroundColor: setting.realDark ? Colors.black87 : null,
primaryColor: setting.realDark ? Colors.black : null,
popupMenuTheme: PopupMenuThemeData()
.copyWith(color: setting.realDark ? Colors.black87 : null),
appBarTheme: AppBarTheme(elevation: 0),
cursorColor: setting.accentSetColor),
localizationsDelegates: [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: S.delegate.supportedLocales,
home: setting.showIntro ? SlideIntro(goto: Goto.home) : Home(),
return FeatureDiscovery(
child: MaterialApp(
themeMode: setting.theme,
debugShowCheckedModeBanner: false,
title: 'Tsacdop',
theme: lightTheme.copyWith(
accentColor: setting.accentSetColor,
cursorColor: setting.accentSetColor,
toggleableActiveColor: setting.accentSetColor),
darkTheme: ThemeData.dark().copyWith(
accentColor: setting.accentSetColor,
primaryColorDark: Colors.grey[800],
scaffoldBackgroundColor:
setting.realDark ? Colors.black87 : null,
primaryColor: setting.realDark ? Colors.black : null,
popupMenuTheme: PopupMenuThemeData()
.copyWith(color: setting.realDark ? Colors.black87 : null),
appBarTheme: AppBarTheme(elevation: 0),
cursorColor: setting.accentSetColor),
localizationsDelegates: [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: S.delegate.supportedLocales,
home: setting.showIntro ? SlideIntro(goto: Goto.home) : Home(),
),
);
},
);

View File

@ -22,6 +22,7 @@ import '../type/fireside_data.dart';
import '../util/colorize.dart';
import '../util/context_extension.dart';
import '../util/custompaint.dart';
import '../util/general_dialog.dart';
import '../state/audiostate.dart';
class PodcastDetail extends StatefulWidget {
@ -95,6 +96,21 @@ class _PodcastDetailState extends State<PodcastDetail> {
}
}
_markListened(String podcastId) async {
DBHelper dbHelper = DBHelper();
List<EpisodeBrief> episodes =
await dbHelper.getRssItem(podcastId, -1, true);
await Future.forEach(episodes, (episode) async {
bool marked = await dbHelper.checkMarked(episode);
if (!marked) {
final PlayHistory history =
PlayHistory(episode.title, episode.enclosureUrl, 0, 1);
await dbHelper.saveHistory(history);
if (mounted) setState(() {});
}
});
}
Widget podcastInfo(BuildContext context) {
return Container(
height: 170,
@ -183,6 +199,33 @@ class _PodcastDetailState extends State<PodcastDetail> {
);
}
_confirmMarkListened(BuildContext context) => generalDialog(
context,
title: Text('Mark confirm'),
content: Text('Confirm mark all episodes listened?'),
actions: <Widget>[
FlatButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text(
'CANCEL',
style: TextStyle(color: Colors.grey[600]),
),
),
FlatButton(
onPressed: () async {
Navigator.of(context).pop();
await _markListened(widget.podcastLocal.id);
},
child: Text(
'CONFIRM',
style: TextStyle(color: context.accentColor),
),
)
],
);
double _topHeight = 0;
ScrollController _controller;
@ -258,7 +301,9 @@ class _PodcastDetailState extends State<PodcastDetail> {
_loadMore = false;
});
}
if (_controller.offset > 0 && mounted && !_scroll )
if (_controller.offset > 0 &&
mounted &&
!_scroll)
setState(() {
_scroll = true;
});
@ -270,7 +315,7 @@ class _PodcastDetailState extends State<PodcastDetail> {
SliverAppBar(
brightness: Brightness.dark,
actions: <Widget>[
PopupMenuButton<String>(
PopupMenuButton<int>(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(10))),
@ -279,8 +324,7 @@ class _PodcastDetailState extends State<PodcastDetail> {
itemBuilder: (context) => [
widget.podcastLocal.link != null
? PopupMenuItem(
value: widget
.podcastLocal.link,
value: 0,
child: Container(
padding: EdgeInsets.only(
left: 10),
@ -304,7 +348,7 @@ class _PodcastDetailState extends State<PodcastDetail> {
)
: Center(),
PopupMenuItem(
value: widget.podcastLocal.rssUrl,
value: 1,
child: Container(
padding:
EdgeInsets.only(left: 10),
@ -326,9 +370,54 @@ class _PodcastDetailState extends State<PodcastDetail> {
),
),
),
PopupMenuItem(
value: 2,
child: Container(
padding:
EdgeInsets.only(left: 10),
child: Row(
crossAxisAlignment:
CrossAxisAlignment.center,
children: <Widget>[
SizedBox(
width: 25,
height: 25,
child: CustomPaint(
painter:
ListenedAllPainter(
context
.textTheme
.bodyText1
.color,
stroke: 2)),
),
Padding(
padding:
EdgeInsets.symmetric(
horizontal: 5.0),
),
Text(
'Mark All Listened',
),
],
),
),
),
],
onSelected: (url) {
_launchUrl(url);
onSelected: (int value) {
switch (value) {
case 0:
_launchUrl(
widget.podcastLocal.link);
break;
case 1:
_launchUrl(
widget.podcastLocal.rssUrl);
break;
case 2:
_confirmMarkListened(context);
break;
}
},
)
],

View File

@ -15,6 +15,8 @@ import '../util/pageroute.dart';
import '../util/colorize.dart';
import '../util/duraiton_picker.dart';
import '../util/context_extension.dart';
import '../util/general_dialog.dart';
import 'podcastmanage.dart';
class PodcastGroupList extends StatefulWidget {
final PodcastGroup group;
@ -343,76 +345,41 @@ class _PodcastCardState extends State<PodcastCard>
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: context.width / 3,
bottom: 20),
title:
Text('Skip seconds at the beginning'),
content: DurationPicker(
duration: Duration(
seconds: _skipSeconds ?? 0),
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),
),
)
],
),
generalDialog(
context,
title: Text('Skip seconds at start',
maxLines: 2),
content: DurationPicker(
duration:
Duration(seconds: _skipSeconds ?? 0),
onChange: (value) =>
_seconds = value.inSeconds,
),
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(
@ -421,64 +388,33 @@ class _PodcastCardState extends State<PodcastCard>
color: Colors.red,
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),
generalDialog(
context,
title: Text('Remove confirm'),
content: Text(
'Are you sure you want to unsubscribe?'),
actions: <Widget>[
FlatButton(
onPressed: () =>
Navigator.of(context).pop(),
child: Text(
'CANCEL',
style:
TextStyle(color: Colors.grey[600]),
),
),
child: AlertDialog(
elevation: 1,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(10.0))),
titlePadding: EdgeInsets.only(
top: 20,
left: 20,
right: context.width / 3,
bottom: 20),
title: Text('Remove confirm'),
content: Text(
'Are you sure you want to unsubscribe?'),
actions: <Widget>[
FlatButton(
onPressed: () =>
Navigator.of(context).pop(),
child: Text(
'CANCEL',
style: TextStyle(
color: Colors.grey[600]),
),
),
FlatButton(
onPressed: () {
groupList.removePodcast(
widget.podcastLocal.id);
Navigator.of(context).pop();
},
child: Text(
'CONFIRM',
style: TextStyle(color: Colors.red),
),
)
],
),
),
FlatButton(
onPressed: () {
groupList.removePodcast(
widget.podcastLocal.id);
Navigator.of(context).pop();
},
child: Text(
'CONFIRM',
style: TextStyle(color: Colors.red),
),
)
],
);
}),
],
@ -534,7 +470,7 @@ class _RenameGroupState extends State<RenameGroup> {
elevation: 1,
contentPadding: EdgeInsets.symmetric(horizontal: 20),
titlePadding: EdgeInsets.only(
top: 20, left: 20, right: context.width / 3, bottom: 20),
top: 20, left: 20, right: 20, bottom: 20),
actionsPadding: EdgeInsets.all(0),
actions: <Widget>[
FlatButton(
@ -561,7 +497,7 @@ class _RenameGroupState extends State<RenameGroup> {
style: TextStyle(color: Theme.of(context).accentColor)),
)
],
title: Text('Edit group name'),
title: SizedBox(width: context.width - 160, child: Text('Edit group name')),
content: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[

View File

@ -6,14 +6,20 @@ import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:line_icons/line_icons.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:feature_discovery/feature_discovery.dart';
import '../state/podcast_group.dart';
import '../podcasts/podcastgroup.dart';
import '../podcasts/podcastlist.dart';
import '../util/pageroute.dart';
import '../util/context_extension.dart';
import '../util/general_dialog.dart';
import 'custom_tabview.dart';
const String addGroupFeature = 'addGroupFeature';
const String configureGroup = 'configureFeature';
const String configurePodcast = 'configurePodcast';
class PodcastManage extends StatefulWidget {
@override
_PodcastManageState createState() => _PodcastManageState();
@ -62,6 +68,16 @@ class _PodcastManageState extends State<PodcastManage>
_controller.stop();
}
});
FeatureDiscovery.isDisplayed(context, addGroupFeature).then((value) {
if (!value)
WidgetsBinding.instance.addPostFrameCallback((_) {
FeatureDiscovery.discoverFeatures(context, const <String>{
addGroupFeature,
configureGroup,
configurePodcast
});
});
});
}
@override
@ -79,54 +95,94 @@ class _PodcastManageState extends State<PodcastManage>
} else if (_fraction > 0) {
_controller.reverse();
}
return Transform(
alignment: FractionalOffset(0.5, 0.5),
transform: Matrix4.rotationY(math.pi * _fraction),
child: Container(
child: InkWell(
child: Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: _fraction > 0.5
? Colors.red
: Theme.of(context).accentColor,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.grey[700].withOpacity(0.5),
blurRadius: 1,
offset: Offset(1, 1),
),
]),
alignment: Alignment.center,
child: _fraction > 0.5
? Icon(LineIcons.save_solid, color: Colors.white)
: AnimatedIcon(
color: Colors.white,
icon: AnimatedIcons.menu_close,
progress: _menuController,
),
// color: Colors.white,
return DescribedFeatureOverlay(
featureId: configureGroup,
tapTarget: Icon(Icons.menu),
title: Padding(
padding: const EdgeInsets.only(top: 20.0),
child: const Text('Tap to edit group'),
),
overflowMode: OverflowMode.clipContent,
backgroundColor: Colors.cyan[600],
onDismiss: () => Future.value(true),
description: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
const Text('You can change group name or delete group here,' +
'but home group can not be edited or deleted.'),
FlatButton(
color: Colors.cyan[500],
padding: const EdgeInsets.all(0),
child: Text('Understood',
style: Theme.of(context)
.textTheme
.button
.copyWith(color: Colors.white)),
onPressed: () async =>
FeatureDiscovery.completeCurrentStep(context),
),
FlatButton(
color: Colors.cyan[500],
padding: const EdgeInsets.all(0),
child: Text('Dismiss',
style: Theme.of(context)
.textTheme
.button
.copyWith(color: Colors.white)),
onPressed: () => FeatureDiscovery.dismissAll(context),
),
],
),
child: Transform(
alignment: FractionalOffset(0.5, 0.5),
transform: Matrix4.rotationY(math.pi * _fraction),
child: Container(
child: InkWell(
child: Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: _fraction > 0.5
? Colors.red
: Theme.of(context).accentColor,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.grey[700].withOpacity(0.5),
blurRadius: 1,
offset: Offset(1, 1),
),
]),
alignment: Alignment.center,
child: _fraction > 0.5
? Icon(LineIcons.save_solid, color: Colors.white)
: AnimatedIcon(
color: Colors.white,
icon: AnimatedIcons.menu_close,
progress: _menuController,
),
// color: Colors.white,
),
onTap: () async {
if (_fraction == 0) {
!_showSetting
? _menuController.forward()
: await _menuController.reverse();
setState(() {
_showSetting = !_showSetting;
});
} else {
groupList.saveOrder(groupList.groups[_index]);
groupList
.drlFromOrderChanged(groupList.groups[_index].name);
Fluttertoast.showToast(
msg: 'Setting Saved',
gravity: ToastGravity.BOTTOM,
);
_controller.reverse();
}
},
),
onTap: () async {
if (_fraction == 0) {
!_showSetting
? _menuController.forward()
: await _menuController.reverse();
setState(() {
_showSetting = !_showSetting;
});
} else {
groupList.saveOrder(groupList.groups[_index]);
groupList.drlFromOrderChanged(groupList.groups[_index].name);
Fluttertoast.showToast(
msg: 'Setting Saved',
gravity: ToastGravity.BOTTOM,
);
_controller.reverse();
}
},
),
),
);
@ -148,18 +204,55 @@ class _PodcastManageState extends State<PodcastManage>
centerTitle: true,
title: Text('Groups'),
actions: <Widget>[
IconButton(
onPressed: () => 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) =>
AddGroup()),
icon: Icon(Icons.add)),
DescribedFeatureOverlay(
featureId: addGroupFeature,
tapTarget: Icon(Icons.add),
title: const Text('Tap to add group'),
overflowMode: OverflowMode.clipContent,
backgroundColor: Colors.cyan[600],
onDismiss: () => Future.value(true),
description: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
const Text(
'Default group is home for new podcast, you can create new group and move ' +
'podcast to new group, podcast can be added to muilti-groups.'),
FlatButton(
color: Colors.cyan[500],
padding: const EdgeInsets.all(0),
child: Text('Understood',
style: Theme.of(context)
.textTheme
.button
.copyWith(color: Colors.white)),
onPressed: () async =>
FeatureDiscovery.completeCurrentStep(context),
),
FlatButton(
color: Colors.cyan[500],
padding: const EdgeInsets.all(0),
child: Text('Dismiss',
style: Theme.of(context)
.textTheme
.button
.copyWith(color: Colors.white)),
onPressed: () => FeatureDiscovery.dismissAll(context),
),
],
),
child: IconButton(
onPressed: () => 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) =>
AddGroup()),
icon: Icon(Icons.add)),
),
OrderMenu(),
],
),
@ -197,9 +290,49 @@ class _PodcastManageState extends State<PodcastManage>
_groups[index].name,
)),
),
pageBuilder: (context, index) => Container(
key: ValueKey(_groups[index].name),
child: PodcastGroupList(group: _groups[index])),
pageBuilder: (context, index) =>
DescribedFeatureOverlay(
featureId: configurePodcast,
tapTarget: Text('Podcast'),
title: const Text('Long tap to reorder podcast'),
overflowMode: OverflowMode.clipContent,
onDismiss: () => Future.value(true),
enablePulsingAnimation: false,
backgroundColor: Colors.cyan[600],
description: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
const Text('You can tap to see more options,' +
' or long tap to reorder podcast in group.'),
FlatButton(
color: Colors.cyan[500],
padding: const EdgeInsets.all(0),
child: Text('Understood',
style: Theme.of(context)
.textTheme
.button
.copyWith(color: Colors.white)),
onPressed: () async =>
FeatureDiscovery.completeCurrentStep(
context),
),
FlatButton(
color: Colors.cyan[500],
padding: const EdgeInsets.all(0),
child: Text('Dismiss',
style: Theme.of(context)
.textTheme
.button
.copyWith(color: Colors.white)),
onPressed: () =>
FeatureDiscovery.dismissAll(context),
),
],
),
child: Container(
key: ValueKey(_groups[index].name),
child: PodcastGroupList(group: _groups[index])),
),
onPositionChange: (value) =>
setState(() => _index = value),
onScroll: (value) => setState(() => _scroll = value),
@ -310,116 +443,52 @@ class _PodcastManageState extends State<PodcastManage>
'Home group is not supported',
gravity: ToastGravity.BOTTOM,
)
: showGeneralDialog(
context: context,
barrierDismissible: true,
barrierLabel:
MaterialLocalizations.of(
context)
.modalBarrierDismissLabel,
barrierColor: Colors.black54,
transitionDuration:
const Duration(
milliseconds: 300),
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: context
.width /
3,
bottom: 20),
title: Text(
'Delete confirm'),
content: Text(
'Are you sure you want to delete this group?' +
'Podcasts will be moved to Home group.'),
actions: <Widget>[
FlatButton(
onPressed: () =>
Navigator.of(
context)
.pop(),
child: Text(
'CANCEL',
style: TextStyle(
color: Colors
.grey[
600]),
),
),
FlatButton(
onPressed: () {
if (_index ==
groupList
.groups
.length -
1) {
setState(() {
_index =
_index -
1;
_scroll = 0;
});
groupList.delGroup(
_groups[
_index +
1]);
} else {
groupList.delGroup(
_groups[
_index]);
}
Navigator.of(
context)
.pop();
},
child: Text(
'CONFIRM',
style: TextStyle(
color: Colors
.red),
),
)
],
),
));
: generalDialog(
context,
title: Text('Delete confirm'),
content: Text(
'Are you sure you want to delete this group?' +
'Podcasts will be moved to Home group.'),
actions: <Widget>[
FlatButton(
onPressed: () =>
Navigator.of(context)
.pop(),
child: Text(
'CANCEL',
style: TextStyle(
color: Colors
.grey[600]),
),
),
FlatButton(
onPressed: () {
if (_index ==
groupList.groups
.length -
1) {
setState(() {
_index = _index - 1;
_scroll = 0;
});
groupList.delGroup(
_groups[
_index + 1]);
} else {
groupList.delGroup(
_groups[_index]);
}
Navigator.of(context)
.pop();
},
child: Text(
'CONFIRM',
style: TextStyle(
color: Colors.red),
),
)
],
);
},
child: Container(
height: 30,
@ -533,8 +602,7 @@ class _AddGroupState extends State<AddGroup> {
borderRadius: BorderRadius.all(Radius.circular(10))),
elevation: 1,
contentPadding: EdgeInsets.symmetric(horizontal: 20),
titlePadding: EdgeInsets.only(
top: 20, left: 20, right: context.width / 3, bottom: 20),
titlePadding: EdgeInsets.only(top: 20, left: 20, right: 20, bottom: 20),
actionsPadding: EdgeInsets.all(0),
actions: <Widget>[
FlatButton(
@ -557,7 +625,8 @@ class _AddGroupState extends State<AddGroup> {
style: TextStyle(color: Theme.of(context).accentColor)),
)
],
title: Text('Create new group'),
title: SizedBox(
width: context.width - 160, child: Text('Create new group')),
content: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[

View File

@ -8,12 +8,16 @@ import 'package:line_icons/line_icons.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:path_provider/path_provider.dart';
import 'package:flutter_file_dialog/flutter_file_dialog.dart';
import 'package:feature_discovery/feature_discovery.dart';
import 'package:fluttertoast/fluttertoast.dart';
import '../util/ompl_build.dart';
import '../util/context_extension.dart';
import '../intro_slider/app_intro.dart';
import '../type/podcastlocal.dart';
import '../local_storage/sqflite_localpodcast.dart';
import '../home/home.dart';
import '../podcasts/podcastmanage.dart';
import 'theme.dart';
import 'layouts.dart';
import 'storage.dart';
@ -256,16 +260,6 @@ class _SettingsState extends State<Settings>
shrinkWrap: true,
scrollDirection: Axis.vertical,
children: <Widget>[
ListTile(
onTap: () => _launchUrl(
'https://github.com/stonega/tsacdop/releases'),
contentPadding:
EdgeInsets.symmetric(horizontal: 25.0),
leading: Icon(LineIcons.map_signs_solid),
title: Text('Changelog'),
subtitle: Text('List of changes'),
),
Divider(height: 2),
ListTile(
onTap: () => Navigator.push(
context,
@ -331,6 +325,31 @@ class _SettingsState extends State<Settings>
Divider(
height: 2,
),
ListTile(
onTap: () {
FeatureDiscovery.clearPreferences(
context, const <String>{
addFeature,
menuFeature,
playlistFeature,
groupsFeature,
addGroupFeature,
configureGroup,
configurePodcast,
podcastFeature
});
Fluttertoast.showToast(
msg:
'Discovery Feature Reopened, pleast restart the app',
gravity: ToastGravity.BOTTOM,
);
},
contentPadding:
EdgeInsets.symmetric(horizontal: 25.0),
leading: Icon(LineIcons.capsules_solid),
title: Text('Discovery Features Again'),
),
Divider(height: 2),
ListTile(
onTap: () => Navigator.push(
context,

View File

@ -4,6 +4,7 @@ import 'package:provider/provider.dart';
import '../state/settingstate.dart';
import '../util/context_extension.dart';
import '../util/general_dialog.dart';
class ThemeSetting extends StatelessWidget {
@override
@ -134,47 +135,20 @@ class ThemeSetting extends StatelessWidget {
),
Divider(height: 2),
ListTile(
onTap: () => 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,
titlePadding: EdgeInsets.only(
top: 20,
left: 40,
right: context.width / 3,
bottom: 0),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(10.0))),
title: Text.rich(
TextSpan(text: 'Choose a ', children: [
TextSpan(
text: 'color',
style: TextStyle(
fontWeight: FontWeight.bold,
color: context.accentColor))
])),
content: ColorPicker(
onColorChanged: (value) =>
settings.setAccentColor = value,
),
))),
onTap: () => generalDialog(
context,
title: Text.rich(TextSpan(text: 'Choose a ', children: [
TextSpan(
text: 'color',
style: TextStyle(
fontWeight: FontWeight.bold,
color: context.accentColor))
])),
content: ColorPicker(
onColorChanged: (value) =>
settings.setAccentColor = value,
),
),
contentPadding: EdgeInsets.only(left: 80.0, right: 25),
title: Text('Accent color'),
subtitle: Text('Include the overlay color'),

View File

@ -137,7 +137,10 @@ class DownloadState extends ChangeNotifier {
now.month.toString() +
now.day.toString() +
now.second.toString();
String fileName = episode.title +
String title = episode.title.trim().substring(0, 1) == '#'
? episode.title.trim().substring(1)
: episode.title.trim();
String fileName = title +
datePlus +
'.' +
episode.enclosureUrl.split('/').last.split('.').last;

View File

@ -0,0 +1,34 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'context_extension.dart';
generalDialog(BuildContext context,
{Widget title, Widget content, List<Widget> actions}) =>
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.all(20),
title: SizedBox(width: context.width-160, child: title),
content: content,
actionsPadding: EdgeInsets.all(10),
actions: actions),
),
);

View File

@ -44,6 +44,7 @@ dependencies:
wc_flutter_share: ^0.2.1
video_player: ^0.10.11
auto_animated: ^2.1.0
feature_discovery: ^0.10.0
just_audio:
git:
url: https://github.com/stonega/just_audio.git