From b619be9a9bad9d85cb94a206fa259ebd2a4d413e Mon Sep 17 00:00:00 2001 From: stonegate Date: Wed, 22 Jul 2020 17:34:32 +0800 Subject: [PATCH] Search ui changed a lot, add podcast detail panel. Update audio service to latest version. --- android/app/src/main/AndroidManifest.xml | 12 +- lib/episodes/episode_detail.dart | 3 +- lib/home/about.dart | 2 +- lib/home/audioplayer.dart | 144 +-- lib/home/home.dart | 3 +- lib/home/home_groups.dart | 3 +- lib/home/home_menu.dart | 2 +- lib/home/import_ompl.dart | 2 +- lib/home/playlist.dart | 3 +- lib/home/search_podcast.dart | 991 +++++++++++++++----- lib/intro_slider/app_intro.dart | 2 +- lib/intro_slider/firstpage.dart | 2 +- lib/intro_slider/fourthpage.dart | 2 +- lib/intro_slider/secondpage.dart | 2 +- lib/intro_slider/thirdpage.dart | 2 +- lib/local_storage/key_value_storage.dart | 3 +- lib/local_storage/sqflite_localpodcast.dart | 2 +- lib/podcasts/podcast_detail.dart | 3 +- lib/podcasts/podcast_group.dart | 2 +- lib/podcasts/podcast_manage.dart | 2 +- lib/podcasts/podcastlist.dart | 2 +- lib/service/api_search.dart | 39 + lib/settings/data_backup.dart | 2 +- lib/settings/downloads_manage.dart | 2 +- lib/settings/history.dart | 4 +- lib/settings/languages.dart | 2 +- lib/settings/layouts.dart | 2 +- lib/settings/libries.dart | 2 +- lib/settings/play_setting.dart | 2 +- lib/settings/popup_menu.dart | 2 +- lib/settings/settting.dart | 2 +- lib/settings/storage.dart | 2 +- lib/settings/syncing.dart | 2 +- lib/settings/theme.dart | 2 +- lib/state/audio_state.dart | 616 ++++++------ lib/type/episodebrief.dart | 2 +- lib/type/play_histroy.dart | 19 + lib/type/playlist.dart | 58 ++ lib/type/searchepisodes.dart | 41 + lib/type/searchepisodes.g.dart | 36 + lib/type/searchpodcast.dart | 53 +- lib/type/searchpodcast.g.dart | 40 +- lib/{home => util}/audiopanel.dart | 4 +- lib/util/context_extension.dart | 15 - lib/util/episodegrid.dart | 3 +- lib/util/extension_helper.dart | 50 + lib/util/general_dialog.dart | 4 +- lib/util/open_container.dart | 2 +- pubspec.yaml | 68 +- 49 files changed, 1493 insertions(+), 772 deletions(-) create mode 100644 lib/service/api_search.dart create mode 100644 lib/type/play_histroy.dart create mode 100644 lib/type/playlist.dart create mode 100644 lib/type/searchepisodes.dart create mode 100644 lib/type/searchepisodes.g.dart rename lib/{home => util}/audiopanel.dart (98%) delete mode 100644 lib/util/context_extension.dart create mode 100644 lib/util/extension_helper.dart diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 9488428..fef065c 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,13 +1,12 @@ - + - - + + @@ -22,11 +21,8 @@ - - - - + diff --git a/lib/episodes/episode_detail.dart b/lib/episodes/episode_detail.dart index 20ad8f7..92d4b83 100644 --- a/lib/episodes/episode_detail.dart +++ b/lib/episodes/episode_detail.dart @@ -14,8 +14,9 @@ import 'package:google_fonts/google_fonts.dart'; import '../state/audio_state.dart'; import '../type/episodebrief.dart'; +import '../type/play_histroy.dart'; import '../local_storage/sqflite_localpodcast.dart'; -import '../util/context_extension.dart'; +import '../util/extension_helper.dart'; import '../util/custompaint.dart'; import 'episode_download.dart'; diff --git a/lib/home/about.dart b/lib/home/about.dart index 7c9d3c8..780dc2a 100644 --- a/lib/home/about.dart +++ b/lib/home/about.dart @@ -4,7 +4,7 @@ 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'; +import '../util/extension_helper.dart'; const String version = '0.4.7'; diff --git a/lib/home/audioplayer.dart b/lib/home/audioplayer.dart index d32f3f5..6276731 100644 --- a/lib/home/audioplayer.dart +++ b/lib/home/audioplayer.dart @@ -9,17 +9,18 @@ import 'package:audio_service/audio_service.dart'; import 'package:line_icons/line_icons.dart'; import '../type/episodebrief.dart'; +import '../type/play_histroy.dart'; import '../state/audio_state.dart'; import '../local_storage/sqflite_localpodcast.dart'; import '../local_storage/key_value_storage.dart'; import '../util/pageroute.dart'; import '../util/colorize.dart'; -import '../util/context_extension.dart'; +import '../util/extension_helper.dart'; import '../util/custompaint.dart'; import '../util/custom_slider.dart'; import '../episodes/episode_detail.dart'; import 'playlist.dart'; -import 'audiopanel.dart'; +import '../util/audiopanel.dart'; final List _customShadow = [ BoxShadow(blurRadius: 26, offset: Offset(-6, -6), color: Colors.white), @@ -438,9 +439,9 @@ class _PlayerWidgetState extends State { Expanded( flex: 2, child: Selector>( + Tuple3>( selector: (_, audio) => Tuple3( - audio.audioState, + audio.buffering, (audio.backgroundAudioDuration - audio.backgroundAudioPosition) / 1000, @@ -453,12 +454,7 @@ class _PlayerWidgetState extends State { ? Text(data.item3, style: const TextStyle( color: const Color(0xFFFF0000))) - : data.item1 == BasicPlaybackState.buffering || - data.item1 == - BasicPlaybackState.connecting || - data.item1 == - BasicPlaybackState.skippingToNext || - data.item1 == BasicPlaybackState.stopped + : data.item1 ? Text( s.buffering, style: TextStyle( @@ -475,35 +471,18 @@ class _PlayerWidgetState extends State { ), Expanded( flex: 2, - child: Selector( - selector: (_, audio) => audio.audioState, - builder: (_, audioplay, __) { + child: Selector>( + selector: (_, audio) => + Tuple2(audio.buffering, audio.playing), + builder: (_, data, __) { return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ //Spacer(), - audioplay == BasicPlaybackState.playing - ? InkWell( - onTap: - audioplay == BasicPlaybackState.playing - ? () { - audio.pauseAduio(); - } - : null, - child: ImageRotate( - title: audio.episode?.title, - path: audio.episode?.imagePath), - ) - : InkWell( - onTap: - audioplay == BasicPlaybackState.playing - ? null - : () { - audio.resumeAudio(); - }, - child: Stack( - alignment: Alignment.center, - children: [ + data.item1 + ? Stack( + alignment: Alignment.center, + children: [ Container( padding: EdgeInsets.symmetric( vertical: 10.0), @@ -521,13 +500,48 @@ class _PlayerWidgetState extends State { shape: BoxShape.circle, color: Colors.black), ), - Icon( - Icons.play_arrow, - color: Colors.white, - ) - ], - ), - ), + ]) + : data.item2 + ? InkWell( + onTap: data.item2 + ? () => audio.pauseAduio() + : null, + child: ImageRotate( + title: audio.episode?.title, + path: audio.episode?.imagePath), + ) + : InkWell( + onTap: data.item2 + ? null + : () => audio.resumeAudio(), + child: Stack( + alignment: Alignment.center, + children: [ + Container( + padding: EdgeInsets.symmetric( + vertical: 10.0), + child: Container( + height: 30.0, + width: 30.0, + child: CircleAvatar( + backgroundImage: FileImage(File( + "${audio.episode.imagePath}")), + )), + ), + Container( + height: 40.0, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.black), + ), + if (!data.item1) + Icon( + Icons.play_arrow, + color: Colors.white, + ) + ], + ), + ), IconButton( padding: EdgeInsets.zero, onPressed: () => audio.playNext(), @@ -1148,7 +1162,7 @@ class _ControlPanelState extends State Widget build(BuildContext context) { var audio = Provider.of(context, listen: false); return Container( - color: Theme.of(context).primaryColor, + color: context.primaryColor, height: 300, padding: EdgeInsets.symmetric(horizontal: 10.0), child: Stack( @@ -1159,10 +1173,9 @@ class _ControlPanelState extends State children: [ Consumer( builder: (_, data, __) { - Color _c = - (Theme.of(context).brightness == Brightness.light) - ? data.episode.primaryColor.colorizedark() - : data.episode.primaryColor.colorizeLight(); + Color _c = (context.brightness == Brightness.light) + ? data.episode.primaryColor.colorizedark() + : data.episode.primaryColor.colorizeLight(); return Column( mainAxisAlignment: MainAxisAlignment.start, mainAxisSize: MainAxisSize.min, @@ -1217,15 +1230,16 @@ class _ControlPanelState extends State color: const Color(0xFFFF0000))) : Text( data.audioState == - BasicPlaybackState + AudioProcessingState .buffering || data.audioState == - BasicPlaybackState + AudioProcessingState .connecting || data.audioState == - BasicPlaybackState.none || + AudioProcessingState + .none || data.audioState == - BasicPlaybackState + AudioProcessingState .skippingToNext ? context.s.buffering : '', @@ -1250,9 +1264,9 @@ class _ControlPanelState extends State ), Container( height: 100, - child: Selector( - selector: (_, audio) => audio.audioState, - builder: (_, backplay, __) { + child: Selector( + selector: (_, audio) => audio.playing, + builder: (_, playing, __) { return Material( color: Colors.transparent, child: Row( @@ -1261,10 +1275,9 @@ class _ControlPanelState extends State children: [ IconButton( padding: EdgeInsets.symmetric(horizontal: 25.0), - onPressed: - backplay == BasicPlaybackState.playing - ? () => audio.forwardAudio(-10) - : null, + onPressed: playing + ? () => audio.forwardAudio(-10) + : null, iconSize: 32.0, icon: Icon(Icons.replay_10), color: Colors.grey[500]), @@ -1285,14 +1298,13 @@ class _ControlPanelState extends State Brightness.dark ? _customShadowNight : _customShadow), - child: backplay == BasicPlaybackState.playing + child: playing ? Material( color: Colors.transparent, child: InkWell( borderRadius: BorderRadius.all( Radius.circular(30)), - onTap: backplay == - BasicPlaybackState.playing + onTap: playing ? () { audio.pauseAduio(); } @@ -1312,8 +1324,7 @@ class _ControlPanelState extends State child: InkWell( borderRadius: BorderRadius.all( Radius.circular(30)), - onTap: backplay == - BasicPlaybackState.playing + onTap: playing ? null : () { audio.resumeAudio(); @@ -1333,10 +1344,9 @@ class _ControlPanelState extends State ), IconButton( padding: EdgeInsets.symmetric(horizontal: 25.0), - onPressed: - backplay == BasicPlaybackState.playing - ? () => audio.forwardAudio(30) - : null, + onPressed: playing + ? () => audio.forwardAudio(30) + : null, iconSize: 32.0, icon: Icon(Icons.forward_30), color: Colors.grey[500]), diff --git a/lib/home/home.dart b/lib/home/home.dart index 5026860..ccaeb77 100644 --- a/lib/home/home.dart +++ b/lib/home/home.dart @@ -11,12 +11,13 @@ import 'package:fluttertoast/fluttertoast.dart'; import 'package:feature_discovery/feature_discovery.dart'; import '../state/audio_state.dart'; +import '../type/playlist.dart'; import '../type/episodebrief.dart'; import '../local_storage/sqflite_localpodcast.dart'; import '../local_storage/key_value_storage.dart'; import '../util/episodegrid.dart'; import '../util/mypopupmenu.dart'; -import '../util/context_extension.dart'; +import '../util/extension_helper.dart'; import '../util/custompaint.dart'; import '../state/download_state.dart'; import '../state/podcast_group.dart'; diff --git a/lib/home/home_groups.dart b/lib/home/home_groups.dart index e33987a..93d7589 100644 --- a/lib/home/home_groups.dart +++ b/lib/home/home_groups.dart @@ -12,6 +12,7 @@ import 'package:tuple/tuple.dart'; import 'package:line_icons/line_icons.dart'; import '../type/episodebrief.dart'; +import '../type/play_histroy.dart'; import '../state/podcast_group.dart'; import '../state/download_state.dart'; import '../type/podcastlocal.dart'; @@ -19,7 +20,7 @@ import '../state/audio_state.dart'; import '../util/custompaint.dart'; import '../util/pageroute.dart'; import '../util/colorize.dart'; -import '../util/context_extension.dart'; +import '../util/extension_helper.dart'; import '../local_storage/sqflite_localpodcast.dart'; import '../local_storage/key_value_storage.dart'; import '../episodes/episode_detail.dart'; diff --git a/lib/home/home_menu.dart b/lib/home/home_menu.dart index 0613a8b..99a82d7 100644 --- a/lib/home/home_menu.dart +++ b/lib/home/home_menu.dart @@ -15,7 +15,7 @@ import 'package:intl/intl.dart'; import '../settings/settting.dart'; import '../state/refresh_podcast.dart'; -import '../util/context_extension.dart'; +import '../util/extension_helper.dart'; import 'about.dart'; class PopupMenu extends StatefulWidget { diff --git a/lib/home/import_ompl.dart b/lib/home/import_ompl.dart index 382682c..7ba63ed 100644 --- a/lib/home/import_ompl.dart +++ b/lib/home/import_ompl.dart @@ -8,7 +8,7 @@ import '../state/podcast_group.dart'; import '../state/download_state.dart'; import '../state/refresh_podcast.dart'; import '../type/episodebrief.dart'; -import '../util/context_extension.dart'; +import '../util/extension_helper.dart'; class Import extends StatelessWidget { Widget importColumn(String text, BuildContext context) { diff --git a/lib/home/playlist.dart b/lib/home/playlist.dart index 2b8e8f5..1b11af4 100644 --- a/lib/home/playlist.dart +++ b/lib/home/playlist.dart @@ -10,7 +10,8 @@ import 'package:line_icons/line_icons.dart'; import '../state/audio_state.dart'; import '../type/episodebrief.dart'; -import '../util/context_extension.dart'; +import '../type/playlist.dart'; +import '../util/extension_helper.dart'; import '../util/custompaint.dart'; import '../util/colorize.dart'; diff --git a/lib/home/search_podcast.dart b/lib/home/search_podcast.dart index 247a874..b4f7b7d 100644 --- a/lib/home/search_podcast.dart +++ b/lib/home/search_podcast.dart @@ -1,5 +1,4 @@ import 'dart:io'; -import 'dart:convert'; import 'dart:async'; import 'dart:math' as math; @@ -7,16 +6,19 @@ import 'package:flutter/material.dart'; import 'package:color_thief_flutter/color_thief_flutter.dart'; import 'package:dio/dio.dart'; import 'package:flutter/services.dart'; +import 'package:google_fonts/google_fonts.dart'; import 'package:provider/provider.dart'; +import 'package:flutter_html/flutter_html.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:tsacdop/state/podcast_group.dart'; import '../type/searchpodcast.dart'; +import '../type/searchepisodes.dart'; +import '../service/api_search.dart'; import '../state/podcast_group.dart'; -import '../util/context_extension.dart'; +import '../util/extension_helper.dart'; import '../webfeed/webfeed.dart'; -import '../.env.dart'; class MyHomePageDelegate extends SearchDelegate { final String searchFieldLabel; @@ -33,13 +35,7 @@ class MyHomePageDelegate extends SearchDelegate { receiveTimeout: 10000, ); Response response = await Dio(options).get(url); - var p = RssFeed.parse(response.data); - return OnlinePodcast( - rss: url, - title: p.title, - publisher: p.author, - description: p.description, - image: p.itunes.image.href); + return RssFeed.parse(response.data); } catch (e) { throw e; } @@ -74,7 +70,7 @@ class MyHomePageDelegate extends SearchDelegate { // if (query.isEmpty) return Center( child: Container( - padding: EdgeInsets.only(top: 400), + padding: EdgeInsets.only(top: 100), child: Image( image: context.brightness == Brightness.light ? AssetImage('assets/listennotes.png') @@ -82,46 +78,6 @@ class MyHomePageDelegate extends SearchDelegate { height: 20, ), )); - // else if (rssExp.stringMatch(query) != null) - // return FutureBuilder( - // future: getRss(rssExp.stringMatch(query)), - // builder: (context, snapshot) { - // if (snapshot.hasError) - // return invalidRss(); - // else if (snapshot.hasData) - // return SearchResult( - // onlinePodcast: snapshot.data, - // ); - // else - // return Container( - // padding: EdgeInsets.only(top: 200), - // alignment: Alignment.topCenter, - // child: CircularProgressIndicator(), - // ); - // }, - // ); - // else - // return FutureBuilder( - // future: getList(query), - // builder: (BuildContext context, AsyncSnapshot snapshot) { - // if (!snapshot.hasData && query != null) - // return Container( - // padding: EdgeInsets.only(top: 200), - // alignment: Alignment.topCenter, - // child: CircularProgressIndicator(), - // ); - // List content = snapshot.data; - // return ListView.builder( - // scrollDirection: Axis.vertical, - // itemCount: content.length, - // itemBuilder: (BuildContext context, int index) { - // return SearchResult( - // onlinePodcast: content[index], - // ); - // }, - // ); - // }, - // ); } @override @@ -162,8 +118,9 @@ class MyHomePageDelegate extends SearchDelegate { if (snapshot.hasError) return invalidRss(context); else if (snapshot.hasData) - return SearchResult( - onlinePodcast: snapshot.data, + return RssResult( + url: rssExp.stringMatch(query), + rssFeed: snapshot.data, ); else return Container( @@ -177,27 +134,208 @@ class MyHomePageDelegate extends SearchDelegate { return SearchList( query: query, ); - // return FutureBuilder( - // future: getList(query), - // builder: (BuildContext context, AsyncSnapshot snapshot) { - // if (!snapshot.hasData && query != null) - // return Container( - // padding: EdgeInsets.only(top: 200), - // alignment: Alignment.topCenter, - // child: CircularProgressIndicator(), - // ); - // List content = snapshot.data; - // return ListView.builder( - // scrollDirection: Axis.vertical, - // itemCount: content.length, - // itemBuilder: (BuildContext context, int index) { - // return SearchResult( - // onlinePodcast: content[index], - // ); - // }, - // ); - // }, - // ); + } +} + +class RssResult extends StatefulWidget { + RssResult({this.url, this.rssFeed, Key key}) : super(key: key); + final RssFeed rssFeed; + final String url; + @override + _RssResultState createState() => _RssResultState(); +} + +class _RssResultState extends State { + OnlinePodcast _onlinePodcast; + bool _isSubscribed = false; + int _loadItems; + + @override + void initState() { + var p = widget.rssFeed; + _loadItems = 10; + _onlinePodcast = OnlinePodcast( + rss: widget.url, + title: p.title, + publisher: p.author, + description: p.description, + image: p.itunes.image.href, + count: p.items.length); + super.initState(); + } + + @override + Widget build(BuildContext context) { + var subscribeWorker = Provider.of(context, listen: false); + final s = context.s; + _subscribePodcast(OnlinePodcast podcast) { + SubscribeItem item = SubscribeItem(podcast.rss, podcast.title, + imgUrl: podcast.image, group: 'Home'); + subscribeWorker.setSubscribeItem(item); + } + + var items = widget.rssFeed.items; + return DefaultTabController( + length: 2, + child: Column( + children: [ + SizedBox( + height: 140, + child: Row( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Text(_onlinePodcast.title, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: context.textTheme.headline5), + ), + !_isSubscribed + ? OutlineButton( + highlightedBorderColor: context.accentColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(100.0), + side: + BorderSide(color: context.accentColor)), + splashColor: + context.accentColor.withOpacity(0.5), + child: Text(s.subscribe, + style: + TextStyle(color: context.accentColor)), + onPressed: () { + _subscribePodcast(_onlinePodcast); + setState(() { + _isSubscribed = true; + }); + Fluttertoast.showToast( + msg: s.podcastSubscribed, + gravity: ToastGravity.BOTTOM, + ); + }) + : OutlineButton( + color: context.accentColor.withOpacity(0.5), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(100.0), + side: BorderSide(color: Colors.grey[500])), + highlightedBorderColor: Colors.grey[500], + disabledTextColor: Colors.grey[500], + child: Text(s.subscribe), + disabledBorderColor: Colors.grey[500], + onPressed: () {}) + ], + ), + ), + ), + CachedNetworkImage( + height: 120.0, + width: 120.0, + fit: BoxFit.fitWidth, + alignment: Alignment.center, + imageUrl: _onlinePodcast.image, + progressIndicatorBuilder: (context, url, downloadProgress) => + Container( + height: 40, + width: 40, + alignment: Alignment.center, + color: context.primaryColorDark, + child: SizedBox( + width: 20, + height: 2, + child: LinearProgressIndicator( + value: downloadProgress.progress), + ), + ), + errorWidget: (context, url, error) => Container( + width: 40, + height: 40, + alignment: Alignment.center, + color: context.primaryColorDark, + child: Icon(Icons.error)), + ), + ], + ), + ), + Container( + height: 50, + color: context.scaffoldBackgroundColor, + child: TabBar( + indicatorColor: context.accentColor, + labelColor: context.textColor, + indicatorWeight: 3, + indicatorSize: TabBarIndicatorSize.label, + tabs: [ + Text(s.homeToprightMenuAbout), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text(s.episode(2)), + SizedBox(width: 2), + Container( + padding: const EdgeInsets.only( + left: 5, right: 5, top: 2, bottom: 2), + decoration: BoxDecoration( + color: context.accentColor, + borderRadius: BorderRadius.circular(100)), + child: Text(_onlinePodcast.count.toString(), + style: TextStyle(color: Colors.white))) + ], + ) + ]), + ), + Expanded( + child: TabBarView(children: [ + Html( + data: _onlinePodcast.description, + padding: EdgeInsets.only(left: 20.0, right: 20, bottom: 50), + defaultTextStyle: + // GoogleFonts.libreBaskerville( + GoogleFonts.martel( + textStyle: TextStyle( + height: 1.8, + ), + ), + ), + ListView.builder( + itemCount: math.min(_loadItems + 1, items.length), + itemBuilder: (context, index) { + if (index == _loadItems) + return Container( + padding: const EdgeInsets.only(top: 10.0, bottom: 20.0), + alignment: Alignment.center, + child: SizedBox( + width: 100, + child: OutlineButton( + highlightedBorderColor: context.accentColor, + splashColor: context.accentColor.withOpacity(0.5), + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.all(Radius.circular(100))), + child: Text(context.s.loadMore), + onPressed: () => setState( + () { + _loadItems += 10; + }, + ), + ), + ), + ); + return ListTile( + title: Text(items[index].title), + subtitle: Text('${items[index].pubDate}', + style: TextStyle(color: context.accentColor)), + ); + }) + ]), + ) + ], + ), + ); } } @@ -210,29 +348,24 @@ class SearchList extends StatefulWidget { } class _SearchListState extends State { - int _nextOffset; - List _podcastList; + int _nextOffset = 0; + List _podcastList = []; int _offset; bool _loading; + OnlinePodcast _selectedPodcast; + Future _searchFuture; + List _subscribed = []; @override void initState() { super.initState(); - _nextOffset = 0; - _podcastList = []; + _searchFuture = _getList(widget.query, _nextOffset); } - Future _getList(String searchText, int nextOffset) async { - String apiKey = environment['apiKey']; - String url = "https://listen-api.listennotes.com/api/v2/search?q=" + - Uri.encodeComponent(searchText) + - "&sort_by_date=0&type=podcast&offset=$nextOffset"; - Response response = await Dio().get(url, - options: Options(headers: { - 'X-ListenAPI-Key': "$apiKey", - 'Accept': "application/json" - })); - Map searchResultMap = jsonDecode(response.toString()); - var searchResult = SearchPodcast.fromJson(searchResultMap); + Future> _getList( + String searchText, int nextOffset) async { + SearchEngine searchEngine = SearchEngine(); + var searchResult = searchEngine.searchPodcasts( + searchText: searchText, nextOffset: nextOffset); _offset = searchResult.nextOffset; _podcastList.addAll(searchResult.results.cast()); _loading = false; @@ -241,41 +374,57 @@ class _SearchListState extends State { @override Widget build(BuildContext context) { - return FutureBuilder( - future: _getList(widget.query, _nextOffset), - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (!snapshot.hasData && widget.query != null) - return Container( - padding: EdgeInsets.only(top: 200), - alignment: Alignment.topCenter, - child: CircularProgressIndicator(), - ); - var content = snapshot.data; - return CustomScrollView( - slivers: [ - SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) { - return SearchResult( - onlinePodcast: content[index], - ); - }, - childCount: content.length, - ), - ), - SliverToBoxAdapter( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.only(top: 10.0, bottom: 20.0), - child: SizedBox( - height: 30, - child: OutlineButton( + return Stack( + alignment: Alignment.bottomCenter, + children: [ + FutureBuilder( + future: _searchFuture, + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (!snapshot.hasData && widget.query != null) + return Container( + padding: EdgeInsets.only(top: 200), + alignment: Alignment.topCenter, + child: CircularProgressIndicator(), + ); + var content = snapshot.data; + return CustomScrollView( + slivers: [ + SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + return SearchResult( + onlinePodcast: content[index], + isSubscribed: _subscribed.contains(content[index]), + onSelect: (onlinePodcast) { + setState(() { + _selectedPodcast = onlinePodcast; + }); + }, + onSubscribe: (onlinePodcast) { + setState(() { + _subscribed.add(onlinePodcast); + }); + }, + ); + }, + childCount: content.length, + ), + ), + SliverToBoxAdapter( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only(top: 10.0, bottom: 20.0), + child: SizedBox( + height: 30, + child: OutlineButton( + highlightedBorderColor: context.accentColor, + splashColor: context.accentColor.withOpacity(0.5), shape: RoundedRectangleBorder( borderRadius: - BorderRadius.all(Radius.circular(15))), + BorderRadius.all(Radius.circular(100))), child: _loading ? SizedBox( height: 20, @@ -286,56 +435,66 @@ class _SearchListState extends State { : Text(context.s.loadMore), onPressed: () => _loading ? null - : setState(() { - _loading = true; - _nextOffset = _offset; - }))), - ) - ], + : setState( + () { + _loading = true; + _nextOffset = _offset; + _searchFuture = + _getList(widget.query, _nextOffset); + }, + ), + ), + ), + ) + ], + ), + ) + ], + ); + }, + ), + if (_selectedPodcast != null) + Positioned.fill( + child: GestureDetector( + onTap: () => setState(() => _selectedPodcast = null), + child: Container( + color: + Theme.of(context).scaffoldBackgroundColor.withOpacity(0.8), ), - ) - ], - ); - }, + ), + ), + if (_selectedPodcast != null) + LayoutBuilder( + builder: (context, constrants) => SearchResultDetail( + _selectedPodcast, + maxHeight: constrants.maxHeight, + onClose: (option) { + setState(() => _selectedPodcast = null); + }, + onSubscribe: (onlinePodcast) { + setState(() { + _subscribed.add(onlinePodcast); + }); + }, + ), + ), + ], ); } } -class SearchResult extends StatefulWidget { +class SearchResult extends StatelessWidget { final OnlinePodcast onlinePodcast; - SearchResult({this.onlinePodcast, Key key}) : super(key: key); - @override - _SearchResultState createState() => _SearchResultState(); -} - -class _SearchResultState extends State - with SingleTickerProviderStateMixin { - bool _issubscribe; - bool _showDes; - AnimationController _controller; - Animation _animation; - double _value; - @override - void initState() { - super.initState(); - _issubscribe = false; - _showDes = 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; - }); - }); - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } + final ValueChanged onSelect; + final ValueChanged onSubscribe; + final bool isSubscribed; + SearchResult( + {this.onlinePodcast, + this.onSelect, + this.onSubscribe, + this.isSubscribed, + Key key}) + : super(key: key); Future getColor(File file) async { final imageProvider = FileImage(file); @@ -349,10 +508,11 @@ class _SearchResultState extends State Widget build(BuildContext context) { var subscribeWorker = Provider.of(context, listen: false); final s = context.s; - savePodcast(OnlinePodcast podcast) { + subscribePodcast(OnlinePodcast podcast) { SubscribeItem item = SubscribeItem(podcast.rss, podcast.title, imgUrl: podcast.image, group: 'Home'); subscribeWorker.setSubscribeItem(item); + onSubscribe(podcast); } return Container( @@ -368,13 +528,7 @@ class _SearchResultState extends State ListTile( contentPadding: EdgeInsets.symmetric(horizontal: 20.0), onTap: () { - setState(() { - _showDes = !_showDes; - if (_value == 0) - _controller.forward(); - else - _controller.reverse(); - }); + onSelect(onlinePodcast); }, leading: ClipRRect( borderRadius: BorderRadius.all(Radius.circular(20.0)), @@ -383,7 +537,7 @@ class _SearchResultState extends State width: 40.0, fit: BoxFit.fitWidth, alignment: Alignment.center, - imageUrl: widget.onlinePodcast.image, + imageUrl: onlinePodcast.image, progressIndicatorBuilder: (context, url, downloadProgress) => Container( height: 40, @@ -405,72 +559,431 @@ class _SearchResultState extends State child: Icon(Icons.error)), ), ), - title: Text(widget.onlinePodcast.title), - subtitle: Text(widget.onlinePodcast.publisher ?? ''), - trailing: Row( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Transform.rotate( - angle: math.pi * _value, - child: Icon(Icons.keyboard_arrow_down), - ), - Padding(padding: EdgeInsets.only(right: 10.0)), - !_issubscribe - ? OutlineButton( - highlightedBorderColor: context.accentColor, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(100.0), - side: BorderSide(color: context.accentColor)), - splashColor: context.accentColor.withOpacity(0.8), - child: Text(s.subscribe, - style: TextStyle(color: context.accentColor)), - onPressed: () { - savePodcast(widget.onlinePodcast); - setState(() => _issubscribe = true); - Fluttertoast.showToast( - msg: s.podcastSubscribed, - gravity: ToastGravity.BOTTOM, - ); - }) - : OutlineButton( - color: context.accentColor.withOpacity(0.8), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(100.0), - side: BorderSide(color: Colors.grey[500])), - highlightedBorderColor: Colors.grey[500], - disabledTextColor: Colors.grey[500], - child: Text(s.subscribe), - disabledBorderColor: Colors.grey[500], - onPressed: () {}), - ], - ), + title: Text(onlinePodcast.title), + subtitle: Text(onlinePodcast.publisher ?? ''), + trailing: !isSubscribed + ? OutlineButton( + highlightedBorderColor: context.accentColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(100.0), + side: BorderSide(color: context.accentColor)), + splashColor: context.accentColor.withOpacity(0.5), + child: Text(s.subscribe, + style: TextStyle(color: context.accentColor)), + onPressed: () { + subscribePodcast(onlinePodcast); + Fluttertoast.showToast( + msg: s.podcastSubscribed, + gravity: ToastGravity.BOTTOM, + ); + }) + : OutlineButton( + color: context.accentColor.withOpacity(0.5), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(100.0), + side: BorderSide(color: Colors.grey[500])), + highlightedBorderColor: Colors.grey[500], + disabledTextColor: Colors.grey[500], + child: Text(s.subscribe), + disabledBorderColor: Colors.grey[500], + onPressed: () {}), ), - _showDes - ? Container( - alignment: Alignment.centerLeft, - decoration: BoxDecoration( - color: Theme.of(context).accentColor, - borderRadius: BorderRadius.only( - topRight: Radius.circular(15.0), - bottomLeft: Radius.circular(15.0), - bottomRight: Radius.circular(15.0), - )), - margin: EdgeInsets.only(left: 70, right: 50, bottom: 10.0), - padding: EdgeInsets.all(15.0), - child: Text( - widget.onlinePodcast.description.trim(), - maxLines: 3, - overflow: TextOverflow.ellipsis, - style: Theme.of(context) - .textTheme - .bodyText1 - .copyWith(color: Colors.white), - ), - ) - : Center(), ], ), ); } } + +class SearchResultDetail extends StatefulWidget { + SearchResultDetail(this.onlinePodcast, + {this.onClose, + this.maxHeight, + this.onSubscribe, + this.episodeList, + Key key}) + : super(key: key); + final OnlinePodcast onlinePodcast; + final ValueChanged onClose; + final ValueChanged onSubscribe; + final double maxHeight; + final List episodeList; + @override + _SearchResultDetailState createState() => _SearchResultDetailState(); +} + +enum SlideDirection { up, down } + +class _SearchResultDetailState extends State + with SingleTickerProviderStateMixin { + /// Animation value. + double _initSize; + + /// Gesture tap start position. + double _startdy; + + /// Height of first open. + double _minHeight; + + /// Gesture move. + double _move = 0; + + AnimationController _controller; + Animation _animation; + + /// Gesture scroll direction. + SlideDirection _slideDirection; + + /// Search offset. + int _nextEpisdoeDate = DateTime.now().millisecondsSinceEpoch; + + /// Search result. + List _episodeList = []; + + Future _searchFuture; + + /// Subscribe indicator. + bool _isSubscribed = false; + + /// Episodes list load more. + bool _loading = false; + + @override + void initState() { + super.initState(); + _searchFuture = _getEpisodes( + id: widget.onlinePodcast.id, nextEpisodeDate: _nextEpisdoeDate); + _minHeight = widget.maxHeight / 2; + _initSize = _minHeight; + _slideDirection = SlideDirection.up; + _controller = + AnimationController(vsync: this, duration: Duration(milliseconds: 200)) + ..addListener(() { + if (mounted) setState(() {}); + }); + _animation = + Tween(begin: 170, end: _minHeight).animate(_controller); + _controller.forward(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + Future> _getEpisodes( + {String id, int nextEpisodeDate}) async { + SearchEngine searchEngine = SearchEngine(); + var searchResult = await searchEngine.fetchEpisode( + id: id, nextEpisodeDate: nextEpisodeDate); + _nextEpisdoeDate = searchResult.nextEpisodeDate; + _episodeList.addAll(searchResult.episodes.cast()); + _loading = false; + return _episodeList; + } + + _start(DragStartDetails event) { + setState(() { + _startdy = event.localPosition.dy; + _animation = + Tween(begin: _initSize, end: _initSize).animate(_controller); + }); + _controller.forward(); + } + + _update(DragUpdateDetails event) { + setState(() { + _move = _startdy - event.localPosition.dy; + _animation = Tween(begin: _initSize, end: _initSize + _move) + .animate(_controller); + _slideDirection = _move > 0 ? SlideDirection.up : SlideDirection.down; + }); + _controller.forward(); + } + + _end() { + if (_slideDirection == SlideDirection.up) { + if (_move > 20) { + setState(() { + _animation = + Tween(begin: _animation.value, end: widget.maxHeight) + .animate(_controller); + _initSize = widget.maxHeight; + }); + _controller.forward(); + } else { + setState(() { + _animation = Tween(begin: _animation.value, end: _minHeight) + .animate(_controller); + _initSize = _minHeight; + }); + _controller.forward(); + } + } else if (_slideDirection == SlideDirection.down) { + if (_move > -50) { + setState(() { + _animation = Tween( + begin: _animation.value, + end: _animation.value > _minHeight + ? widget.maxHeight + : _minHeight) + .animate(_controller); + _initSize = + _animation.value > _minHeight ? widget.maxHeight : _minHeight; + }); + _controller.forward(); + } else { + setState(() { + _animation = Tween( + begin: _animation.value, + end: _animation.value > _minHeight - 50 ? _minHeight : 1) + .animate(_controller); + _initSize = _animation.value > _minHeight - 50 ? _minHeight : 100; + }); + _controller.forward(); + if (_animation.value < _minHeight - 50) widget.onClose(true); + } + } + } + + @override + Widget build(BuildContext context) { + var subscribeWorker = Provider.of(context, listen: false); + final s = context.s; + subscribePodcast(OnlinePodcast podcast) { + SubscribeItem item = SubscribeItem(podcast.rss, podcast.title, + imgUrl: podcast.image, group: 'Home'); + subscribeWorker.setSubscribeItem(item); + widget.onSubscribe(podcast); + } + + return GestureDetector( + onVerticalDragStart: (event) => _start(event), + onVerticalDragUpdate: (event) => _update(event), + onVerticalDragEnd: (event) => _end(), + child: SingleChildScrollView( + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).primaryColor, + boxShadow: [ + BoxShadow( + offset: Offset(0, -0.5), + blurRadius: 1, + color: Theme.of(context).brightness == Brightness.light + ? Colors.grey[400].withOpacity(0.5) + : Colors.grey[800], + ), + ], + ), + height: _animation.value, + child: DefaultTabController( + length: 2, + child: Column( + children: [ + SizedBox( + height: 120, + child: Row( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: + const EdgeInsets.symmetric(vertical: 8), + child: Text(widget.onlinePodcast.title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: context.textTheme.headline5), + ), + Text( + '${widget.onlinePodcast.interval.toInterval(context)} | ' + '${widget.onlinePodcast.latestPubDate.toDate(context)}', + style: TextStyle(color: context.accentColor), + ), + Padding( + padding: const EdgeInsets.only(top: 8), + child: !_isSubscribed + ? OutlineButton( + highlightedBorderColor: + context.accentColor, + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(100.0), + side: BorderSide( + color: context.accentColor)), + splashColor: context.accentColor + .withOpacity(0.5), + child: Text(s.subscribe, + style: TextStyle( + color: context.accentColor)), + onPressed: () { + subscribePodcast( + widget.onlinePodcast); + setState(() { + _isSubscribed = true; + }); + Fluttertoast.showToast( + msg: s.podcastSubscribed, + gravity: ToastGravity.BOTTOM, + ); + }) + : OutlineButton( + color: context.accentColor + .withOpacity(0.5), + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(100.0), + side: BorderSide( + color: Colors.grey[500])), + highlightedBorderColor: + Colors.grey[500], + disabledTextColor: Colors.grey[500], + child: Text(s.subscribe), + disabledBorderColor: Colors.grey[500], + onPressed: () {}), + ) + ], + ), + ), + ), + CachedNetworkImage( + height: 120.0, + width: 120.0, + fit: BoxFit.fitWidth, + alignment: Alignment.center, + imageUrl: widget.onlinePodcast.image, + progressIndicatorBuilder: + (context, url, downloadProgress) => Container( + height: 40, + width: 40, + alignment: Alignment.center, + color: context.primaryColorDark, + child: SizedBox( + width: 20, + height: 2, + child: LinearProgressIndicator( + value: downloadProgress.progress), + ), + ), + errorWidget: (context, url, error) => Container( + width: 40, + height: 40, + alignment: Alignment.center, + color: context.primaryColorDark, + child: Icon(Icons.error)), + ), + ], + ), + ), + SizedBox( + height: 50, + child: TabBar( + indicatorColor: context.accentColor, + labelColor: context.textColor, + indicatorWeight: 3, + indicatorSize: TabBarIndicatorSize.label, + tabs: [ + Text(s.homeToprightMenuAbout), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text(s.episode(2)), + SizedBox(width: 2), + Container( + padding: const EdgeInsets.only( + left: 5, right: 5, top: 2, bottom: 2), + decoration: BoxDecoration( + color: context.accentColor, + borderRadius: BorderRadius.circular(100)), + child: Text( + widget.onlinePodcast.count.toString(), + style: TextStyle(color: Colors.white))) + ], + ) + ]), + ), + Expanded( + child: TabBarView(children: [ + Padding( + padding: const EdgeInsets.all(20.0), + child: Text(widget.onlinePodcast.description, + style: TextStyle(height: 1.8)), + ), + FutureBuilder>( + future: _searchFuture, + builder: (context, snapshot) { + if (snapshot.hasData) { + var content = snapshot.data; + return ListView.builder( + physics: _animation.value != widget.maxHeight + ? NeverScrollableScrollPhysics() + : null, + itemCount: content.length + 1, + itemBuilder: (context, index) { + if (index == content.length) + return Container( + padding: const EdgeInsets.only( + top: 10.0, bottom: 20.0), + alignment: Alignment.center, + child: SizedBox( + width: 100, + child: OutlineButton( + highlightedBorderColor: + context.accentColor, + splashColor: context.accentColor + .withOpacity(0.5), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(100))), + child: _loading + ? SizedBox( + height: 20, + width: 20, + child: + CircularProgressIndicator( + strokeWidth: 2, + )) + : Text(context.s.loadMore), + onPressed: () => _loading + ? null + : setState( + () { + _loading = true; + _searchFuture = _getEpisodes( + id: widget + .onlinePodcast.id, + nextEpisodeDate: + _nextEpisdoeDate); + }, + ), + ), + ), + ); + return ListTile( + title: Text(content[index].title), + subtitle: Text( + '${content[index].length.toTime} | ' + '${content[index].pubDate.toDate(context)}', + style: TextStyle( + color: context.accentColor)), + ); + }, + ); + } + return Center( + child: CircularProgressIndicator(), + ); + }) + ]), + ) + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/intro_slider/app_intro.dart b/lib/intro_slider/app_intro.dart index c27e299..895a12d 100644 --- a/lib/intro_slider/app_intro.dart +++ b/lib/intro_slider/app_intro.dart @@ -4,7 +4,7 @@ import 'package:provider/provider.dart'; import '../state/setting_state.dart'; import '../home/home.dart'; import '../util/pageroute.dart'; -import '../util/context_extension.dart'; +import '../util/extension_helper.dart'; import 'fourthpage.dart'; import 'secondpage.dart'; import 'thirdpage.dart'; diff --git a/lib/intro_slider/firstpage.dart b/lib/intro_slider/firstpage.dart index af790b3..e2da493 100644 --- a/lib/intro_slider/firstpage.dart +++ b/lib/intro_slider/firstpage.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flare_flutter/flare_actor.dart'; -import '../util/context_extension.dart'; +import '../util/extension_helper.dart'; class FirstPage extends StatefulWidget { FirstPage({Key key}) : super(key: key); diff --git a/lib/intro_slider/fourthpage.dart b/lib/intro_slider/fourthpage.dart index 29bbafd..c822c47 100644 --- a/lib/intro_slider/fourthpage.dart +++ b/lib/intro_slider/fourthpage.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flare_flutter/flare_actor.dart'; -import '../util/context_extension.dart'; +import '../util/extension_helper.dart'; class FourthPage extends StatefulWidget { FourthPage({Key key}) : super(key: key); diff --git a/lib/intro_slider/secondpage.dart b/lib/intro_slider/secondpage.dart index 1c26aea..52c7b86 100644 --- a/lib/intro_slider/secondpage.dart +++ b/lib/intro_slider/secondpage.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flare_flutter/flare_actor.dart'; -import '../util/context_extension.dart'; +import '../util/extension_helper.dart'; class SecondPage extends StatefulWidget { SecondPage({Key key}) : super(key: key); diff --git a/lib/intro_slider/thirdpage.dart b/lib/intro_slider/thirdpage.dart index cb183db..fe31f25 100644 --- a/lib/intro_slider/thirdpage.dart +++ b/lib/intro_slider/thirdpage.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flare_flutter/flare_actor.dart'; -import '../util/context_extension.dart'; +import '../util/extension_helper.dart'; class ThirdPage extends StatefulWidget { ThirdPage({Key key}) : super(key: key); diff --git a/lib/local_storage/key_value_storage.dart b/lib/local_storage/key_value_storage.dart index 584d1fa..99dc499 100644 --- a/lib/local_storage/key_value_storage.dart +++ b/lib/local_storage/key_value_storage.dart @@ -25,13 +25,14 @@ const String downloadLayoutKey = 'downloadLayoutKey'; const String autoDownloadNetworkKey = 'autoDownloadNetwork'; const String episodePopupMenuKey = 'episodePopupMenuKey'; const String autoDeleteKey = 'autoDeleteKey'; -//SleepTImer const String autoSleepTimerKey = 'autoSleepTimerKey'; const String autoSleepTimerStartKey = 'autoSleepTimerStartKey'; const String autoSleepTimerEndKey = 'autoSleepTimerEndKey'; const String defaultSleepTimerKey = 'defaultSleepTimerKey'; const String autoSleepTimerModeKey = 'autoSleepTimerModeKey'; const String tapToOpenPopupMenuKey = 'tapToOpenPopupMenuKey'; +const String fastForwardSecondsKey = 'fastForwardSecondsKey'; +const String rewindSecondsKey = 'rewindSecondsKey'; class KeyValueStorage { final String key; diff --git a/lib/local_storage/sqflite_localpodcast.dart b/lib/local_storage/sqflite_localpodcast.dart index 19a3fff..810f428 100644 --- a/lib/local_storage/sqflite_localpodcast.dart +++ b/lib/local_storage/sqflite_localpodcast.dart @@ -5,7 +5,7 @@ import 'package:intl/intl.dart'; import 'package:flutter_downloader/flutter_downloader.dart'; import 'package:dio/dio.dart'; import '../type/podcastlocal.dart'; -import '../state/audio_state.dart'; +import '../type/play_histroy.dart'; import '../type/episodebrief.dart'; import '../webfeed/webfeed.dart'; import '../type/sub_history.dart'; diff --git a/lib/podcasts/podcast_detail.dart b/lib/podcasts/podcast_detail.dart index e5fbd25..9d3851b 100644 --- a/lib/podcasts/podcast_detail.dart +++ b/lib/podcasts/podcast_detail.dart @@ -16,13 +16,14 @@ import 'package:cached_network_image/cached_network_image.dart'; import '../type/podcastlocal.dart'; import '../type/episodebrief.dart'; +import '../type/play_histroy.dart'; import '../local_storage/sqflite_localpodcast.dart'; import '../local_storage/key_value_storage.dart'; import '../util/episodegrid.dart'; import '../home/audioplayer.dart'; import '../type/fireside_data.dart'; import '../util/colorize.dart'; -import '../util/context_extension.dart'; +import '../util/extension_helper.dart'; import '../util/custompaint.dart'; import '../util/general_dialog.dart'; import '../state/audio_state.dart'; diff --git a/lib/podcasts/podcast_group.dart b/lib/podcasts/podcast_group.dart index 6fd1923..860025d 100644 --- a/lib/podcasts/podcast_group.dart +++ b/lib/podcasts/podcast_group.dart @@ -13,7 +13,7 @@ import '../type/podcastlocal.dart'; import '../local_storage/sqflite_localpodcast.dart'; import '../util/colorize.dart'; import '../util/duraiton_picker.dart'; -import '../util/context_extension.dart'; +import '../util/extension_helper.dart'; import '../util/general_dialog.dart'; class PodcastGroupList extends StatefulWidget { diff --git a/lib/podcasts/podcast_manage.dart b/lib/podcasts/podcast_manage.dart index cfd30e1..760f010 100644 --- a/lib/podcasts/podcast_manage.dart +++ b/lib/podcasts/podcast_manage.dart @@ -12,7 +12,7 @@ import '../state/podcast_group.dart'; import 'podcast_group.dart'; import 'podcastlist.dart'; import '../util/pageroute.dart'; -import '../util/context_extension.dart'; +import '../util/extension_helper.dart'; import '../util/general_dialog.dart'; import 'custom_tabview.dart'; diff --git a/lib/podcasts/podcastlist.dart b/lib/podcasts/podcastlist.dart index 3bc351a..eb4e11c 100644 --- a/lib/podcasts/podcastlist.dart +++ b/lib/podcasts/podcastlist.dart @@ -13,7 +13,7 @@ import '../type/podcastlocal.dart'; import '../local_storage/sqflite_localpodcast.dart'; import 'podcast_detail.dart'; import '../util/pageroute.dart'; -import '../util/context_extension.dart'; +import '../util/extension_helper.dart'; class AboutPodcast extends StatefulWidget { final PodcastLocal podcastLocal; diff --git a/lib/service/api_search.dart b/lib/service/api_search.dart new file mode 100644 index 0000000..38f607f --- /dev/null +++ b/lib/service/api_search.dart @@ -0,0 +1,39 @@ +import 'dart:convert'; + +import 'package:dio/dio.dart'; + +import '../type/searchpodcast.dart'; +import '../type/searchepisodes.dart'; +import '../.env.dart'; + +class SearchEngine { + searchPodcasts({String searchText, int nextOffset}) async { + String apiKey = environment['apiKey']; + String url = "https://listen-api.listennotes.com/api/v2/search?q=" + + Uri.encodeComponent(searchText) + + "&sort_by_date=0&type=podcast&offset=$nextOffset"; + Response response = await Dio().get(url, + options: Options(headers: { + 'X-ListenAPI-Key': "$apiKey", + 'Accept': "application/json" + })); + Map searchResultMap = jsonDecode(response.toString()); + var searchResult = SearchPodcast.fromJson(searchResultMap); + return searchResult; + } + + Future> fetchEpisode( + {String id, int nextEpisodeDate}) async { + String apiKey = environment['apiKey']; + String url = + "https://listen-api.listennotes.com/api/v2/podcasts/$id?next_episode_pub_date=$nextEpisodeDate"; + Response response = await Dio().get(url, + options: Options(headers: { + 'X-ListenAPI-Key': "$apiKey", + 'Accept': "application/json" + })); + Map searchResultMap = jsonDecode(response.toString()); + var searchResult = SearchEpisodes.fromJson(searchResultMap); + return searchResult; + } +} diff --git a/lib/settings/data_backup.dart b/lib/settings/data_backup.dart index cebaf06..fb48f9c 100644 --- a/lib/settings/data_backup.dart +++ b/lib/settings/data_backup.dart @@ -16,7 +16,7 @@ import 'package:wc_flutter_share/wc_flutter_share.dart'; import '../state/podcast_group.dart'; import '../state/setting_state.dart'; -import '../util/context_extension.dart'; +import '../util/extension_helper.dart'; import '../service/ompl_build.dart'; class DataBackup extends StatefulWidget { diff --git a/lib/settings/downloads_manage.dart b/lib/settings/downloads_manage.dart index 843a28d..ab39cff 100644 --- a/lib/settings/downloads_manage.dart +++ b/lib/settings/downloads_manage.dart @@ -9,7 +9,7 @@ import 'package:provider/provider.dart'; import 'package:google_fonts/google_fonts.dart'; import '../type/episodebrief.dart'; -import '../util/context_extension.dart'; +import '../util/extension_helper.dart'; import '../state/download_state.dart'; import '../local_storage/sqflite_localpodcast.dart'; diff --git a/lib/settings/history.dart b/lib/settings/history.dart index 8570902..e59efcc 100644 --- a/lib/settings/history.dart +++ b/lib/settings/history.dart @@ -11,8 +11,8 @@ import 'package:tsacdop/state/podcast_group.dart'; import '../local_storage/sqflite_localpodcast.dart'; import '../webfeed/webfeed.dart'; import '../type/searchpodcast.dart'; -import '../util/context_extension.dart'; -import '../state/audio_state.dart'; +import '../util/extension_helper.dart'; +import '../type/play_histroy.dart'; import '../state/podcast_group.dart'; import '../type/sub_history.dart'; diff --git a/lib/settings/languages.dart b/lib/settings/languages.dart index 94420ad..b189484 100644 --- a/lib/settings/languages.dart +++ b/lib/settings/languages.dart @@ -4,7 +4,7 @@ import 'package:intl/intl.dart'; import 'package:line_icons/line_icons.dart'; import 'package:url_launcher/url_launcher.dart'; -import '../util/context_extension.dart'; +import '../util/extension_helper.dart'; import '../generated/l10n.dart'; class LanguagesSetting extends StatefulWidget { diff --git a/lib/settings/layouts.dart b/lib/settings/layouts.dart index 7045b3d..9a49e5f 100644 --- a/lib/settings/layouts.dart +++ b/lib/settings/layouts.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import '../util/context_extension.dart'; +import '../util/extension_helper.dart'; import '../util/episodegrid.dart'; import '../util/custompaint.dart'; import '../local_storage/key_value_storage.dart'; diff --git a/lib/settings/libries.dart b/lib/settings/libries.dart index 0c9b9f6..fe23665 100644 --- a/lib/settings/libries.dart +++ b/lib/settings/libries.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:url_launcher/url_launcher.dart'; -import '../util/context_extension.dart'; +import '../util/extension_helper.dart'; import 'licenses.dart'; class Libries extends StatelessWidget { diff --git a/lib/settings/play_setting.dart b/lib/settings/play_setting.dart index 91d6d58..351d4c7 100644 --- a/lib/settings/play_setting.dart +++ b/lib/settings/play_setting.dart @@ -11,7 +11,7 @@ import 'package:flutter_time_picker_spinner/flutter_time_picker_spinner.dart'; import '../state/setting_state.dart'; import '../home/audioplayer.dart'; import '../util/general_dialog.dart'; -import '../util/context_extension.dart'; +import '../util/extension_helper.dart'; import '../util/custom_dropdown.dart'; String stringForMins(int mins) { diff --git a/lib/settings/popup_menu.dart b/lib/settings/popup_menu.dart index 29cde97..66501d0 100644 --- a/lib/settings/popup_menu.dart +++ b/lib/settings/popup_menu.dart @@ -3,7 +3,7 @@ import 'package:flutter/services.dart'; import 'package:line_icons/line_icons.dart'; import 'package:flare_flutter/flare_actor.dart'; -import '../util/context_extension.dart'; +import '../util/extension_helper.dart'; import '../util/custompaint.dart'; import '../local_storage/key_value_storage.dart'; diff --git a/lib/settings/settting.dart b/lib/settings/settting.dart index 34376c5..868ec4b 100644 --- a/lib/settings/settting.dart +++ b/lib/settings/settting.dart @@ -7,7 +7,7 @@ import 'package:url_launcher/url_launcher.dart'; import 'package:feature_discovery/feature_discovery.dart'; import 'package:fluttertoast/fluttertoast.dart'; -import '../util/context_extension.dart'; +import '../util/extension_helper.dart'; import '../intro_slider/app_intro.dart'; import '../home/home.dart'; import '../podcasts/podcast_manage.dart'; diff --git a/lib/settings/storage.dart b/lib/settings/storage.dart index 0bfc18d..cae2b1b 100644 --- a/lib/settings/storage.dart +++ b/lib/settings/storage.dart @@ -6,7 +6,7 @@ import 'package:google_fonts/google_fonts.dart'; import '../settings/downloads_manage.dart'; import '../state/setting_state.dart'; import '../local_storage/key_value_storage.dart'; -import '../util/context_extension.dart'; +import '../util/extension_helper.dart'; import '../util/custom_dropdown.dart'; class StorageSetting extends StatefulWidget { diff --git a/lib/settings/syncing.dart b/lib/settings/syncing.dart index cfecf13..88991d9 100644 --- a/lib/settings/syncing.dart +++ b/lib/settings/syncing.dart @@ -4,7 +4,7 @@ import 'package:provider/provider.dart'; import 'package:tuple/tuple.dart'; import '../state/setting_state.dart'; -import '../util/context_extension.dart'; +import '../util/extension_helper.dart'; import '../util/custom_dropdown.dart'; class SyncingSetting extends StatelessWidget { diff --git a/lib/settings/theme.dart b/lib/settings/theme.dart index 4593dc1..4ffe906 100644 --- a/lib/settings/theme.dart +++ b/lib/settings/theme.dart @@ -3,7 +3,7 @@ import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import '../state/setting_state.dart'; -import '../util/context_extension.dart'; +import '../util/extension_helper.dart'; import '../util/general_dialog.dart'; class ThemeSetting extends StatelessWidget { diff --git a/lib/state/audio_state.dart b/lib/state/audio_state.dart index 516ee76..a44eb64 100644 --- a/lib/state/audio_state.dart +++ b/lib/state/audio_state.dart @@ -1,19 +1,15 @@ import 'dart:async'; -import 'dart:io'; -import 'dart:math' as math; -import 'package:path/path.dart'; import 'package:flutter/foundation.dart'; import 'package:audio_service/audio_service.dart'; import 'package:just_audio/just_audio.dart'; -import 'package:rxdart/rxdart.dart'; import 'package:dio/dio.dart'; -import 'package:path_provider/path_provider.dart'; import '../type/episodebrief.dart'; +import '../type/play_histroy.dart'; +import '../type/playlist.dart'; import '../local_storage/key_value_storage.dart'; import '../local_storage/sqflite_localpodcast.dart'; -import '../.env.dart'; MediaControl playControl = MediaControl( androidIcon: 'drawable/ic_stat_play_circle_filled', @@ -50,144 +46,113 @@ void _audioPlayerTaskEntrypoint() async { AudioServiceBackground.run(() => AudioPlayerTask()); } -class PlayHistory { - DBHelper dbHelper = DBHelper(); - String title; - String url; - double seconds; - double seekValue; - DateTime playdate; - PlayHistory(this.title, this.url, this.seconds, this.seekValue, - {this.playdate}); - EpisodeBrief _episode; - EpisodeBrief get episode => _episode; - - getEpisode() async { - _episode = await dbHelper.getRssItemWithUrl(url); - } -} - -class Playlist { - String name; - DBHelper dbHelper = DBHelper(); - - List _playlist; - - List get playlist => _playlist; - KeyValueStorage storage = KeyValueStorage('playlist'); - - getPlaylist() async { - List urls = await storage.getStringList(); - if (urls.length == 0) { - _playlist = []; - } else { - _playlist = []; - - for (String url in urls) { - EpisodeBrief episode = await dbHelper.getRssItemWithUrl(url); - if (episode != null) _playlist.add(episode); - } - } - } - - savePlaylist() async { - List urls = []; - urls.addAll(_playlist.map((e) => e.enclosureUrl)); - await storage.saveStringList(urls.toSet().toList()); - } - - addToPlayList(EpisodeBrief episodeBrief) async { - if (!_playlist.contains(episodeBrief)) { - _playlist.add(episodeBrief); - await savePlaylist(); - dbHelper.removeEpisodeNewMark(episodeBrief.enclosureUrl); - } - } - - addToPlayListAt(EpisodeBrief episodeBrief, int index) async { - if (!_playlist.contains(episodeBrief)) { - _playlist.insert(index, episodeBrief); - await savePlaylist(); - dbHelper.removeEpisodeNewMark(episodeBrief.enclosureUrl); - } - } - - Future delFromPlaylist(EpisodeBrief episodeBrief) async { - int index = _playlist.indexOf(episodeBrief); - _playlist.removeWhere( - (episode) => episode.enclosureUrl == episodeBrief.enclosureUrl); - print('delete' + episodeBrief.title); - await savePlaylist(); - return index; - } -} - +/// Sleep timer mode. enum SleepTimerMode { endOfEpisode, timer, undefined } -enum ShareStatus { generate, download, complete, undefined, error } + +//enum ShareStatus { generate, download, complete, undefined, error } class AudioPlayerNotifier extends ChangeNotifier { DBHelper dbHelper = DBHelper(); - KeyValueStorage positionStorage = KeyValueStorage(audioPositionKey); - KeyValueStorage autoPlayStorage = KeyValueStorage(autoPlayKey); - KeyValueStorage autoSleepTimerStorage = KeyValueStorage(autoSleepTimerKey); - KeyValueStorage defaultSleepTimerStorage = - KeyValueStorage(defaultSleepTimerKey); - KeyValueStorage autoSleepTimerModeStorage = - KeyValueStorage(autoSleepTimerModeKey); - KeyValueStorage autoSleepTimerStartStorage = - KeyValueStorage(autoSleepTimerStartKey); - KeyValueStorage autoSleepTimerEndStorage = - KeyValueStorage(autoSleepTimerEndKey); + var positionStorage = KeyValueStorage(audioPositionKey); + var autoPlayStorage = KeyValueStorage(autoPlayKey); + var autoSleepTimerStorage = KeyValueStorage(autoSleepTimerKey); + var defaultSleepTimerStorage = KeyValueStorage(defaultSleepTimerKey); + var autoSleepTimerModeStorage = KeyValueStorage(autoSleepTimerModeKey); + var autoSleepTimerStartStorage = KeyValueStorage(autoSleepTimerStartKey); + var autoSleepTimerEndStorage = KeyValueStorage(autoSleepTimerEndKey); + var fastForwardSecondsStorage = KeyValueStorage(fastForwardSecondsKey); + var rewindSecondsStorage = KeyValueStorage(rewindSecondsKey); + /// Current playing episdoe. EpisodeBrief _episode; + + /// Current playlist. Playlist _queue = Playlist(); + + /// Notifier for playlist change. bool _queueUpdate = false; - BasicPlaybackState _audioState = BasicPlaybackState.none; - bool _playerRunning = false; + + /// Player state. + AudioProcessingState _audioState = AudioProcessingState.none; + + /// Player playing. + bool _playing = false; + + /// Fastforward second. + int _fastForwardSeconds; + + /// Rewind seconds. + int _rewindSeconds; + + /// No slide, set true if slide on seekbar. bool _noSlide = true; + + /// Current episode duration. int _backgroundAudioDuration = 0; + + /// Current episode positin. int _backgroundAudioPosition = 0; + + /// Erroe maeesage. String _remoteErrorMessage; + /// Seekbar value, min 0, max 1.0. double _seekSliderValue = 0.0; + + /// Record plyaer position. int _lastPostion = 0; + + /// Set true if sleep timer mode is end of episode. bool _stopOnComplete = false; + + /// Sleep timer timer. Timer _stopTimer; + + /// Sleep timer time left. int _timeLeft = 0; + + /// Start sleep timer. bool _startSleepTimer = false; + + /// Control sleep timer anamation. double _switchValue = 0; + + /// Sleep timer mode. SleepTimerMode _sleepTimerMode = SleepTimerMode.undefined; + //Auto stop at the end of episode when you start play at scheduled time. bool _autoSleepTimer; - //Default sleep timer time. - ShareStatus _shareStatus = ShareStatus.undefined; - String _shareFile = ''; + //set autoplay episode in playlist bool _autoPlay; + + /// Datetime now. DateTime _current; + + /// Current position. int _currentPosition; + + /// Current speed. double _currentSpeed = 1; - BehaviorSubject> queueSubject; + //Update episode card when setting changed bool _episodeState = false; - BasicPlaybackState get audioState => _audioState; - + AudioProcessingState get audioState => _audioState; int get backgroundAudioDuration => _backgroundAudioDuration; int get backgroundAudioPosition => _backgroundAudioPosition; double get seekSliderValue => _seekSliderValue; String get remoteErrorMessage => _remoteErrorMessage; - bool get playerRunning => _playerRunning; + bool get playerRunning => _audioState != AudioProcessingState.none; + bool get buffering => _audioState != AudioProcessingState.ready; int get lastPositin => _lastPostion; Playlist get queue => _queue; + bool get playing => _playing; bool get queueUpdate => _queueUpdate; EpisodeBrief get episode => _episode; bool get stopOnComplete => _stopOnComplete; bool get startSleepTimer => _startSleepTimer; SleepTimerMode get sleepTimerMode => _sleepTimerMode; - ShareStatus get shareStatus => _shareStatus; - String get shareFile => _shareFile; - //bool get autoPlay => _autoPlay; int get timeLeft => _timeLeft; double get switchValue => _switchValue; double get currentSpeed => _currentSpeed; @@ -199,11 +164,6 @@ class AudioPlayerNotifier extends ChangeNotifier { notifyListeners(); } - set setShareStatue(ShareStatus status) { - _shareStatus = status; - notifyListeners(); - } - set setEpisodeState(bool boo) { _episodeState = !_episodeState; notifyListeners(); @@ -234,11 +194,9 @@ class AudioPlayerNotifier extends ChangeNotifier { if (running) {} } - loadPlaylist() async { + Future loadPlaylist() async { await _queue.getPlaylist(); await _getAutoPlay(); - // await _getAutoAdd(); - // await addNewEpisode('all'); _lastPostion = await positionStorage.getInt(); if (_lastPostion > 0 && _queue.playlist.length > 0) { final EpisodeBrief episode = _queue.playlist.first; @@ -252,12 +210,13 @@ class AudioPlayerNotifier extends ChangeNotifier { await lastWorkStorage.saveInt(0); } - episodeLoad(EpisodeBrief episode, {int startPosition = 0}) async { + Future episodeLoad(EpisodeBrief episode, + {int startPosition = 0}) async { print(episode.enclosureUrl); final EpisodeBrief episodeNew = await dbHelper.getRssItemWithUrl(episode.enclosureUrl); //TODO load episode from last position when player running - if (_playerRunning) { + if (playerRunning) { PlayHistory history = PlayHistory(_episode.title, _episode.enclosureUrl, backgroundAudioPosition / 1000, seekSliderValue); await dbHelper.saveHistory(history); @@ -275,8 +234,7 @@ class AudioPlayerNotifier extends ChangeNotifier { _backgroundAudioPosition = 0; _seekSliderValue = 0; _episode = episodeNew; - _playerRunning = true; - _audioState = BasicPlaybackState.connecting; + _audioState = AudioProcessingState.connecting; notifyListeners(); //await _queue.savePlaylist(); _startAudioService(startPosition, episodeNew.enclosureUrl); @@ -289,18 +247,29 @@ class AudioPlayerNotifier extends ChangeNotifier { _startAudioService(int position, String url) async { _stopOnComplete = false; _sleepTimerMode = SleepTimerMode.undefined; + + /// Connect to audio service. if (!AudioService.connected) { await AudioService.connect(); } + + /// Get fastword and rewind seconds. + _fastForwardSeconds = + await fastForwardSecondsStorage.getInt(defaultValue: 30); + _rewindSeconds = await rewindSecondsStorage.getInt(defaultValue: 10); + + /// Start audio service. await AudioService.start( backgroundTaskEntrypoint: _audioPlayerTaskEntrypoint, androidNotificationChannelName: 'Tsacdop', - notificationColor: 0xFF4d91be, + androidNotificationColor: 0xFF4d91be, androidNotificationIcon: 'drawable/ic_notification', - enableQueue: true, - androidStopOnRemoveTask: true, - androidStopForegroundOnPause: true); - //Check autoplay setting + androidEnableQueue: true, + androidStopForegroundOnPause: true, + fastForwardInterval: Duration(seconds: _fastForwardSeconds), + rewindInterval: Duration(seconds: _rewindSeconds)); + + //Check autoplay setting, if true only add one episode, else add playlist. await _getAutoPlay(); if (_autoPlay) { for (var episode in _queue.playlist) @@ -315,7 +284,6 @@ class AudioPlayerNotifier extends ChangeNotifier { await autoSleepTimerStartStorage.getInt(defaultValue: 1380); int endTime = await autoSleepTimerEndStorage.getInt(defaultValue: 360); int currentTime = DateTime.now().hour * 60 + DateTime.now().minute; - print('CurrentTime' + currentTime.toString()); if ((startTime > endTime && (currentTime > startTime || currentTime < endTime)) || ((startTime < endTime) && @@ -327,78 +295,106 @@ class AudioPlayerNotifier extends ChangeNotifier { sleepTimer(defaultTimer); } } - _playerRunning = true; + await AudioService.play(); AudioService.currentMediaItemStream .where((event) => event != null) .listen((item) async { EpisodeBrief episode = await dbHelper.getRssItemWithMediaId(item.id); + + _backgroundAudioDuration = item.duration?.inMilliseconds ?? 0; if (episode != null) { _episode = episode; - _backgroundAudioDuration = item?.duration ?? 0; + _backgroundAudioDuration = item.duration.inMilliseconds ?? 0; if (position > 0 && _backgroundAudioDuration > 0 && _episode.enclosureUrl == url) { - AudioService.seekTo(position); + AudioService.seekTo(Duration(milliseconds: position)); position = 0; } notifyListeners(); } else { - _queue.playlist.removeAt(0); + // _queue.playlist.removeAt(0); AudioService.skipToNext(); } }); - queueSubject = BehaviorSubject>(); - queueSubject.addStream( - AudioService.queueStream.distinct().where((event) => event != null)); - queueSubject.stream.listen((event) { - if (event.length == _queue.playlist.length - 1 && - _audioState == BasicPlaybackState.skippingToNext) { - if (event.length == 0 || _stopOnComplete) { - _queue.delFromPlaylist(_episode); - _lastPostion = 0; - notifyListeners(); - positionStorage.saveInt(_lastPostion); - final PlayHistory history = PlayHistory( - _episode.title, - _episode.enclosureUrl, - backgroundAudioPosition / 1000, - seekSliderValue); - dbHelper.saveHistory(history); - } else if (event.first.id != _episode.mediaId) { - _lastPostion = 0; - notifyListeners(); - positionStorage.saveInt(_lastPostion); - _queue.delFromPlaylist(_episode); - final PlayHistory history = PlayHistory( - _episode.title, - _episode.enclosureUrl, - backgroundAudioPosition / 1000, - seekSliderValue); - dbHelper.saveHistory(history); - } + // queueSubject = BehaviorSubject>(); + // queueSubject.addStream( + // AudioService.queueStream.distinct().where((event) => event != null)); +//queueSubject.stream. + AudioService.customEventStream.distinct().listen((event) async { + if (event is String && _episode.title == event) { + print(event); + _queue.delFromPlaylist(_episode); + _lastPostion = 0; + notifyListeners(); + await positionStorage.saveInt(_lastPostion); + final PlayHistory history = PlayHistory( + _episode.title, + _episode.enclosureUrl, + backgroundAudioPosition / 1000, + seekSliderValue); + dbHelper.saveHistory(history); } }); - AudioService.playbackStateStream.listen((event) async { + // AudioService.queueStream + // .distinct() + // .where((event) => event != null) + // .listen((event) { + // if (event.length == _queue.playlist.length - 1 && + // _audioState == AudioProcessingState.skippingToNext) { + // if (event.length == 0 || _stopOnComplete) { + // _queue.delFromPlaylist(_episode); + // _lastPostion = 0; + // notifyListeners(); + // positionStorage.saveInt(_lastPostion); + // final PlayHistory history = PlayHistory( + // _episode.title, + // _episode.enclosureUrl, + // backgroundAudioPosition / 1000, + // seekSliderValue); + // dbHelper.saveHistory(history); + // } else if (event.first.id != _episode.mediaId) { + // _lastPostion = 0; + // notifyListeners(); + // positionStorage.saveInt(_lastPostion); + // _queue.delFromPlaylist(_episode); + // final PlayHistory history = PlayHistory( + // _episode.title, + // _episode.enclosureUrl, + // backgroundAudioPosition / 1000, + // seekSliderValue); + // dbHelper.saveHistory(history); + // } + // } + // }); + + AudioService.playbackStateStream + .distinct() + .where((event) => event != null) + .listen((event) async { _current = DateTime.now(); - _audioState = event?.basicState; - if (_audioState == BasicPlaybackState.stopped) { - _playerRunning = false; + _audioState = event?.processingState; + _playing = event?.playing; + _currentSpeed = event.speed; + _currentPosition = event.currentPosition.inMilliseconds ?? 0; + + if (_audioState == AudioProcessingState.stopped) { if (_switchValue > 0) _switchValue = 0; } - if (_audioState == BasicPlaybackState.error) { + /// Get error state. + if (_audioState == AudioProcessingState.error) { _remoteErrorMessage = 'Network Error'; } - if (_audioState != BasicPlaybackState.error && - _audioState != BasicPlaybackState.paused) { + + /// Reset error state. + if (_audioState != AudioProcessingState.error && _playing) { _remoteErrorMessage = null; } - - _currentPosition = event?.currentPosition ?? 0; notifyListeners(); }); @@ -407,7 +403,7 @@ class AudioPlayerNotifier extends ChangeNotifier { Timer.periodic(Duration(milliseconds: 500), (timer) { double s = _currentSpeed ?? 1.0; if (_noSlide) { - if (_audioState == BasicPlaybackState.playing) { + if (_playing) { getPosition = _currentPosition + ((DateTime.now().difference(_current).inMilliseconds) * s) .toInt(); @@ -432,7 +428,7 @@ class AudioPlayerNotifier extends ChangeNotifier { } notifyListeners(); } - if (_audioState == BasicPlaybackState.stopped) { + if (_audioState == AudioProcessingState.stopped) { timer.cancel(); } }); @@ -444,9 +440,8 @@ class AudioPlayerNotifier extends ChangeNotifier { _backgroundAudioPosition = 0; _seekSliderValue = 0; _episode = _queue.playlist.first; - _playerRunning = true; - _audioState = BasicPlaybackState.connecting; _queueUpdate = !_queueUpdate; + _audioState = AudioProcessingState.connecting; notifyListeners(); _startAudioService(_lastPostion ?? 0, _queue.playlist.first.enclosureUrl); } @@ -457,7 +452,7 @@ class AudioPlayerNotifier extends ChangeNotifier { addToPlaylist(EpisodeBrief episode) async { if (!_queue.playlist.contains(episode)) { - if (_playerRunning) { + if (playerRunning) { await AudioService.addQueueItem(episode.toMediaItem()); } await _queue.addToPlayList(episode); @@ -466,7 +461,7 @@ class AudioPlayerNotifier extends ChangeNotifier { } addToPlaylistAt(EpisodeBrief episode, int index) async { - if (_playerRunning) { + if (playerRunning) { await AudioService.addQueueItemAt(episode.toMediaItem(), index); } await _queue.addToPlayListAt(episode, index); @@ -500,7 +495,7 @@ class AudioPlayerNotifier extends ChangeNotifier { } Future delFromPlaylist(EpisodeBrief episode) async { - if (_playerRunning) { + if (playerRunning) { await AudioService.removeQueueItem(episode.toMediaItem()); } int index = await _queue.delFromPlaylist(episode); @@ -511,7 +506,7 @@ class AudioPlayerNotifier extends ChangeNotifier { moveToTop(EpisodeBrief episode) async { await delFromPlaylist(episode); - if (_playerRunning) { + if (playerRunning) { await addToPlaylistAt(episode, 1); } else { await addToPlaylistAt(episode, 0); @@ -526,29 +521,29 @@ class AudioPlayerNotifier extends ChangeNotifier { } resumeAudio() async { - if (_audioState != BasicPlaybackState.connecting && - _audioState != BasicPlaybackState.none) AudioService.play(); + if (_audioState != AudioProcessingState.connecting && + _audioState != AudioProcessingState.none) AudioService.play(); } forwardAudio(int s) { int pos = _backgroundAudioPosition + s * 1000; - AudioService.seekTo(pos); + AudioService.seekTo(Duration(milliseconds: pos)); } seekTo(int position) async { - if (_audioState != BasicPlaybackState.connecting && - _audioState != BasicPlaybackState.none) - await AudioService.seekTo(position); + if (_audioState != AudioProcessingState.connecting && + _audioState != AudioProcessingState.none) + await AudioService.seekTo(Duration(milliseconds: position)); } sliderSeek(double val) async { - if (_audioState != BasicPlaybackState.connecting && - _audioState != BasicPlaybackState.none) { + if (_audioState != AudioProcessingState.connecting && + _audioState != AudioProcessingState.none) { _noSlide = false; _seekSliderValue = val; notifyListeners(); _currentPosition = (val * _backgroundAudioDuration).toInt(); - await AudioService.seekTo(_currentPosition); + await AudioService.seekTo(Duration(milliseconds: _currentPosition)); _noSlide = true; } } @@ -579,9 +574,9 @@ class AudioPlayerNotifier extends ChangeNotifier { _stopOnComplete = false; _startSleepTimer = false; _switchValue = 0; - _playerRunning = false; - notifyListeners(); + //_playerRunning = false; AudioService.stop(); + notifyListeners(); AudioService.disconnect(); }); } else if (_sleepTimerMode == SleepTimerMode.endOfEpisode) { @@ -610,51 +605,9 @@ class AudioPlayerNotifier extends ChangeNotifier { } } - shareClip(int start, int duration) async { - _shareStatus = ShareStatus.generate; - notifyListeners(); - int length = math.min(duration, (_backgroundAudioDuration ~/ 1000 - start)); - final BaseOptions options = BaseOptions( - connectTimeout: 60000, - receiveTimeout: 120000, - ); - String imageUrl = await dbHelper.getImageUrl(_episode.enclosureUrl); - String url = "https://podcastapi.stonegate.me/clip?" + - "audio_link=${_episode.enclosureUrl}&image_link=$imageUrl&title=${_episode.feedTitle}" + - "&text=${_episode.title}&start=$start&length=$length"; - String shareKey = environment['shareKey']; - try { - Response response = await Dio(options).get(url, - options: Options(headers: { - 'X-Share-Key': "$shareKey", - })); - String shareLink = response.data; - print(shareLink); - String fileName = _episode.title + start.toString() + '.mp4'; - _shareStatus = ShareStatus.download; - notifyListeners(); - Directory dir = await getTemporaryDirectory(); - String shareDir = join(dir.path, 'share', fileName); - try { - await Dio().download(shareLink, shareDir); - _shareFile = shareDir; - _shareStatus = ShareStatus.complete; - notifyListeners(); - } on DioError catch (e) { - print(e); - _shareStatus = ShareStatus.error; - notifyListeners(); - } - } catch (e) { - print(e); - _shareStatus = ShareStatus.error; - notifyListeners(); - } - } - @override void dispose() async { - await AudioService.stop(); + // await AudioService.stop(); await AudioService.disconnect(); //_playerRunning = false; super.dispose(); @@ -666,66 +619,57 @@ class AudioPlayerTask extends BackgroundAudioTask { List _queue = []; AudioPlayer _audioPlayer = AudioPlayer(); - Completer _completer = Completer(); - BasicPlaybackState _skipState; - bool _lostFocus; + AudioProcessingState _skipState; bool _playing; + bool _interrupted = false; bool _stopAtEnd; int _cacheMax; bool get hasNext => _queue.length > 0; MediaItem get mediaItem => _queue.length > 0 ? _queue.first : null; - BasicPlaybackState _stateToBasicState(AudioPlaybackState state) { - switch (state) { - case AudioPlaybackState.none: - return BasicPlaybackState.none; - case AudioPlaybackState.stopped: - return _skipState ?? BasicPlaybackState.stopped; - case AudioPlaybackState.paused: - return BasicPlaybackState.paused; - case AudioPlaybackState.playing: - return BasicPlaybackState.playing; - case AudioPlaybackState.connecting: - return _skipState ?? BasicPlaybackState.connecting; - case AudioPlaybackState.completed: - return BasicPlaybackState.stopped; - default: - throw Exception("Illegal state"); - } - } + StreamSubscription _playerStateSubscription; + StreamSubscription _eventSubscription; @override - Future onStart() async { + Future onStart(Map params) async { _stopAtEnd = false; - _lostFocus = false; - - var playerStateSubscription = _audioPlayer.playbackStateStream + _playerStateSubscription = _audioPlayer.playbackStateStream .where((state) => state == AudioPlaybackState.completed) .listen((state) { _handlePlaybackCompleted(); }); - var eventSubscription = _audioPlayer.playbackEventStream.listen((event) { + + _eventSubscription = _audioPlayer.playbackEventStream.listen((event) { if (event.playbackError != null) { - _setState(state: _skipState ?? BasicPlaybackState.error); + _playing = false; + _setState(processingState: _skipState ?? AudioProcessingState.error); } - BasicPlaybackState state; - if (event.buffering) { - state = _skipState ?? BasicPlaybackState.buffering; - } else { - state = _stateToBasicState(event.state); - } - if (state != BasicPlaybackState.stopped) { - _setState( - state: state, - position: event.position.inMilliseconds, - speed: event.speed, - ); + final bufferingState = + event.buffering ? AudioProcessingState.buffering : null; + switch (event.state) { + case AudioPlaybackState.paused: + _setState( + processingState: bufferingState ?? AudioProcessingState.ready, + position: event.position, + ); + break; + case AudioPlaybackState.playing: + _setState( + processingState: bufferingState ?? AudioProcessingState.ready, + position: event.position, + ); + break; + case AudioPlaybackState.connecting: + _setState( + processingState: _skipState ?? AudioProcessingState.connecting, + position: event.position, + ); + break; + default: + break; } }); - await _completer.future; - playerStateSubscription.cancel(); - eventSubscription.cancel(); } void _handlePlaybackCompleted() async { @@ -740,7 +684,7 @@ class AudioPlayerTask extends BackgroundAudioTask { } void playPause() { - if (AudioServiceBackground.state.basicState == BasicPlaybackState.playing) + if (AudioServiceBackground.state.playing) onPause(); else onPlay(); @@ -748,24 +692,27 @@ class AudioPlayerTask extends BackgroundAudioTask { @override Future onSkipToNext() async { - _skipState = BasicPlaybackState.skippingToNext; + _skipState = AudioProcessingState.skippingToNext; + _playing = false; await _audioPlayer.stop(); - if (_queue.length > 0) _queue.removeAt(0); + if (_queue.length > 0) { + AudioServiceBackground.sendCustomEvent(_queue.first.title); + _queue.removeAt(0); + } await AudioServiceBackground.setQueue(_queue); // } if (_queue.length == 0 || _stopAtEnd) { - // await Future.delayed(Duration(milliseconds: 300)); _skipState = null; onStop(); } else { await AudioServiceBackground.setQueue(_queue); await AudioServiceBackground.setMediaItem(mediaItem); - await _audioPlayer.setUrl(mediaItem.id, _cacheMax); + await _audioPlayer.setUrl(mediaItem.id, cacheMax: _cacheMax); print(mediaItem.title); Duration duration = await _audioPlayer.durationFuture; if (duration != null) await AudioServiceBackground.setMediaItem( - mediaItem.copyWith(duration: duration.inMilliseconds)); + mediaItem.copyWith(duration: duration)); _skipState = null; // Resume playback if we were playing // if (_playing) { @@ -784,35 +731,22 @@ class AudioPlayerTask extends BackgroundAudioTask { _playing = true; _cacheMax = await cacheStorage.getInt( defaultValue: (200 * 1024 * 1024).toInt()); - // await AudioServiceBackground.setQueue(_queue); if (_cacheMax == 0) { await cacheStorage.saveInt((200 * 1024 * 1024).toInt()); _cacheMax = 200 * 1024 * 1024; } - await _audioPlayer.setUrl(mediaItem.id, _cacheMax); + await _audioPlayer.setUrl(mediaItem.id, cacheMax: _cacheMax); var duration = await _audioPlayer.durationFuture; if (duration != null) await AudioServiceBackground.setMediaItem( - mediaItem.copyWith(duration: duration.inMilliseconds)); + mediaItem.copyWith(duration: duration)); playFromStart(); - } - // if (mediaItem.extras['skip'] > 0) { - // await _audioPlayer.setClip( - // start: Duration(seconds: 60)); - // print(mediaItem.extras['skip']); - // print('set clip success'); - // } - else { + } else { _playing = true; if (_audioPlayer.playbackEvent.state != AudioPlaybackState.connecting || _audioPlayer.playbackEvent.state != AudioPlaybackState.none) _audioPlayer.play(); } - // if (mediaItem.extras['skip'] > - // _audioPlayer.playbackEvent.position.inSeconds ?? - // 0) { - // _audioPlayer.seek(Duration(seconds: mediaItem.extras['skip'])); - // } } } @@ -823,7 +757,7 @@ class AudioPlayerTask extends BackgroundAudioTask { try { _audioPlayer.play(); } catch (e) { - _setState(state: BasicPlaybackState.error); + _setState(processingState: AudioProcessingState.error); } if (mediaItem.extras['skip'] > 0) { _audioPlayer.seek(Duration(seconds: mediaItem.extras['skip'])); @@ -834,8 +768,7 @@ class AudioPlayerTask extends BackgroundAudioTask { void onPause() { if (_skipState == null) { if (_playing == null) { - } else if (_audioPlayer.playbackEvent.state == - AudioPlaybackState.playing) { + } else if (_playing) { _playing = false; _audioPlayer.pause(); } @@ -843,10 +776,10 @@ class AudioPlayerTask extends BackgroundAudioTask { } @override - void onSeekTo(int position) { + void onSeekTo(Duration position) { if (_audioPlayer.playbackEvent.state != AudioPlaybackState.connecting || _audioPlayer.playbackEvent.state != AudioPlaybackState.none) - _audioPlayer.seek(Duration(milliseconds: position)); + _audioPlayer.seek(position); } @override @@ -854,20 +787,26 @@ class AudioPlayerTask extends BackgroundAudioTask { if (button == MediaButton.media) playPause(); else if (button == MediaButton.next) - _audioPlayer.seek(Duration( - milliseconds: AudioServiceBackground.state.position + 30 * 1000)); - else if (button == MediaButton.previous) - _audioPlayer.seek(Duration( - milliseconds: AudioServiceBackground.state.position - 10 * 1000)); + _seekRelative(fastForwardInterval); + else if (button == MediaButton.previous) _seekRelative(-rewindInterval); + } + + Future _seekRelative(Duration offset) async { + var newPosition = _audioPlayer.playbackEvent.position + offset; + if (newPosition < Duration.zero) newPosition = Duration.zero; + if (newPosition > mediaItem.duration) newPosition = mediaItem.duration; + await _audioPlayer.seek(newPosition); } @override - void onStop() async { + Future onStop() async { await _audioPlayer.stop(); await _audioPlayer.dispose(); - _setState(state: BasicPlaybackState.stopped); - await Future.delayed(Duration(milliseconds: 300)); - _completer?.complete(); + _playing = false; + _playerStateSubscription.cancel(); + _eventSubscription.cancel(); + await _setState(processingState: AudioProcessingState.none); + await super.onStop(); } @override @@ -890,10 +829,10 @@ class AudioPlayerTask extends BackgroundAudioTask { _queue.insert(0, mediaItem); await AudioServiceBackground.setQueue(_queue); await AudioServiceBackground.setMediaItem(mediaItem); - await _audioPlayer.setUrl(mediaItem.id, _cacheMax); + await _audioPlayer.setUrl(mediaItem.id, cacheMax: _cacheMax); Duration duration = await _audioPlayer.durationFuture ?? Duration.zero; AudioServiceBackground.setMediaItem( - mediaItem.copyWith(duration: duration.inMilliseconds)); + mediaItem.copyWith(duration: duration)); playFromStart(); //onPlay(); } else { @@ -904,26 +843,26 @@ class AudioPlayerTask extends BackgroundAudioTask { @override void onFastForward() { - _audioPlayer.seek(Duration( - milliseconds: AudioServiceBackground.state.position + 30 * 1000)); + _seekRelative(fastForwardInterval); } @override void onRewind() { - _audioPlayer.seek(Duration( - milliseconds: AudioServiceBackground.state.position - 10 * 1000)); + _seekRelative(rewindInterval); } @override - void onAudioFocusLost() { - if (_skipState == null) { - if (_playing == null) { - } else if (_audioPlayer.playbackEvent.state == - AudioPlaybackState.playing) { - _playing = false; - _lostFocus = true; - _audioPlayer.pause(); - } + void onAudioFocusLost(AudioInterruption interruption) { + if (_playing) _interrupted = true; + switch (interruption) { + case AudioInterruption.pause: + case AudioInterruption.temporaryPause: + case AudioInterruption.unknownPause: + onPause(); + break; + case AudioInterruption.temporaryDuck: + _audioPlayer.setVolume(0.5); + break; } } @@ -940,14 +879,18 @@ class AudioPlayerTask extends BackgroundAudioTask { } @override - void onAudioFocusGained() { - if (_skipState == null) { - if (_lostFocus) { - _lostFocus = false; - _playing = true; - _audioPlayer.play(); - } + void onAudioFocusGained(AudioInterruption interruption) { + switch (interruption) { + case AudioInterruption.temporaryPause: + if (!_playing && _interrupted) onPlay(); + break; + case AudioInterruption.temporaryDuck: + _audioPlayer.setVolume(1.0); + break; + default: + break; } + _interrupted = false; } @override @@ -965,24 +908,27 @@ class AudioPlayerTask extends BackgroundAudioTask { } } - void _setState( - {@required BasicPlaybackState state, int position, double speed}) { + Future _setState({ + AudioProcessingState processingState, + Duration position, + Duration bufferedPosition, + }) async { if (position == null) { - position = _audioPlayer.playbackEvent.position.inMilliseconds; + position = _audioPlayer.playbackEvent.position; } - if (speed == null) { - speed = _audioPlayer.playbackEvent.speed; - } - AudioServiceBackground.setState( - controls: getControls(state), + await AudioServiceBackground.setState( + controls: getControls(), systemActions: [MediaAction.seekTo], - basicState: state, + processingState: + processingState ?? AudioServiceBackground.state.processingState, + playing: _playing, position: position, - speed: speed, + bufferedPosition: bufferedPosition ?? position, + speed: _audioPlayer.speed, ); } - List getControls(BasicPlaybackState state) { + List getControls() { if (_playing) { return [pauseControl, forward30, skipToNextControl, stopControl]; } else { diff --git a/lib/type/episodebrief.dart b/lib/type/episodebrief.dart index b9e9363..6293e3b 100644 --- a/lib/type/episodebrief.dart +++ b/lib/type/episodebrief.dart @@ -62,7 +62,7 @@ class EpisodeBrief { title: title, artist: feedTitle, album: feedTitle, - // duration: 0, + duration: Duration.zero, artUri: 'file://$imagePath', extras: {'skip': skipSeconds}); } diff --git a/lib/type/play_histroy.dart b/lib/type/play_histroy.dart new file mode 100644 index 0000000..9f5be25 --- /dev/null +++ b/lib/type/play_histroy.dart @@ -0,0 +1,19 @@ +import '../local_storage/sqflite_localpodcast.dart'; +import 'episodebrief.dart'; + +class PlayHistory { + DBHelper dbHelper = DBHelper(); + String title; + String url; + double seconds; + double seekValue; + DateTime playdate; + PlayHistory(this.title, this.url, this.seconds, this.seekValue, + {this.playdate}); + EpisodeBrief _episode; + EpisodeBrief get episode => _episode; + + getEpisode() async { + _episode = await dbHelper.getRssItemWithUrl(url); + } +} diff --git a/lib/type/playlist.dart b/lib/type/playlist.dart new file mode 100644 index 0000000..f969ec2 --- /dev/null +++ b/lib/type/playlist.dart @@ -0,0 +1,58 @@ +import '../local_storage/sqflite_localpodcast.dart'; +import '../local_storage/key_value_storage.dart'; +import 'episodebrief.dart'; + +class Playlist { + String name; + DBHelper dbHelper = DBHelper(); + + List _playlist; + + List get playlist => _playlist; + KeyValueStorage storage = KeyValueStorage('playlist'); + + getPlaylist() async { + List urls = await storage.getStringList(); + if (urls.length == 0) { + _playlist = []; + } else { + _playlist = []; + + for (String url in urls) { + EpisodeBrief episode = await dbHelper.getRssItemWithUrl(url); + if (episode != null) _playlist.add(episode); + } + } + } + + savePlaylist() async { + List urls = []; + urls.addAll(_playlist.map((e) => e.enclosureUrl)); + await storage.saveStringList(urls.toSet().toList()); + } + + addToPlayList(EpisodeBrief episodeBrief) async { + if (!_playlist.contains(episodeBrief)) { + _playlist.add(episodeBrief); + await savePlaylist(); + dbHelper.removeEpisodeNewMark(episodeBrief.enclosureUrl); + } + } + + addToPlayListAt(EpisodeBrief episodeBrief, int index) async { + if (!_playlist.contains(episodeBrief)) { + _playlist.insert(index, episodeBrief); + await savePlaylist(); + dbHelper.removeEpisodeNewMark(episodeBrief.enclosureUrl); + } + } + + Future delFromPlaylist(EpisodeBrief episodeBrief) async { + int index = _playlist.indexOf(episodeBrief); + _playlist.removeWhere( + (episode) => episode.enclosureUrl == episodeBrief.enclosureUrl); + print('delete' + episodeBrief.title); + await savePlaylist(); + return index; + } +} diff --git a/lib/type/searchepisodes.dart b/lib/type/searchepisodes.dart new file mode 100644 index 0000000..ac301d0 --- /dev/null +++ b/lib/type/searchepisodes.dart @@ -0,0 +1,41 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:tsacdop/type/searchpodcast.dart'; +part 'searchepisodes.g.dart'; + +@JsonSerializable() +class SearchEpisodes { + @_ConvertE() + final List episodes; + @JsonKey(name: 'next_episode_pub_date') + final int nextEpisodeDate; + SearchEpisodes({this.episodes, this.nextEpisodeDate}); + factory SearchEpisodes.fromJson(Map json) => + _$SearchEpisodesFromJson(json); + Map toJson() => _$SearchEpisodesToJson(this); +} + +class _ConvertE implements JsonConverter { + const _ConvertE(); + @override + E fromJson(Object json) { + return OnlineEpisode.fromJson(json) as E; + } + + @override + Object toJson(E object) { + return object; + } +} + +@JsonSerializable() +class OnlineEpisode { + final String title; + @JsonKey(name: 'pub_date_ms') + final int pubDate; + @JsonKey(name: 'audio_length_sec') + final int length; + OnlineEpisode({this.title, this.pubDate, this.length}); + factory OnlineEpisode.fromJson(Map json) => + _$OnlineEpisodeFromJson(json); + Map toJson() => _$OnlineEpisodeToJson(this); +} diff --git a/lib/type/searchepisodes.g.dart b/lib/type/searchepisodes.g.dart new file mode 100644 index 0000000..768008e --- /dev/null +++ b/lib/type/searchepisodes.g.dart @@ -0,0 +1,36 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'searchepisodes.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +SearchEpisodes _$SearchEpisodesFromJson(Map json) { + return SearchEpisodes( + episodes: + (json['episodes'] as List)?.map(_ConvertE().fromJson)?.toList(), + nextEpisodeDate: json['next_episode_pub_date'] as int, + ); +} + +Map _$SearchEpisodesToJson(SearchEpisodes instance) => + { + 'episodes': instance.episodes?.map(_ConvertE().toJson)?.toList(), + 'next_episode_pub_date': instance.nextEpisodeDate, + }; + +OnlineEpisode _$OnlineEpisodeFromJson(Map json) { + return OnlineEpisode( + title: json['title'] as String, + pubDate: json['pub_date_ms'] as int, + length: json['audio_length_sec'] as int, + ); +} + +Map _$OnlineEpisodeToJson(OnlineEpisode instance) => + { + 'title': instance.title, + 'pub_date_ms': instance.pubDate, + 'audio_length_sec': instance.length, + }; diff --git a/lib/type/searchpodcast.dart b/lib/type/searchpodcast.dart index 7f0358c..26e71eb 100644 --- a/lib/type/searchpodcast.dart +++ b/lib/type/searchpodcast.dart @@ -1,43 +1,44 @@ +import 'dart:ui'; + import 'package:json_annotation/json_annotation.dart'; part 'searchpodcast.g.dart'; @JsonSerializable() -class SearchPodcast

{ +class SearchPodcast

{ @_ConvertP() final List

results; @JsonKey(name: 'next_offset') final int nextOffset; final int total; final int count; - SearchPodcast( - {this.results, this.nextOffset, this.total, this.count} - ); - factory SearchPodcast.fromJson(Map json) => - _$SearchPodcastFromJson

(json); + SearchPodcast({this.results, this.nextOffset, this.total, this.count}); + factory SearchPodcast.fromJson(Map json) => + _$SearchPodcastFromJson

(json); Map toJson() => _$SearchPodcastToJson(this); } -class _ConvertP

implements JsonConverter{ +class _ConvertP

implements JsonConverter { const _ConvertP(); @override - P fromJson(Object json){ + P fromJson(Object json) { return OnlinePodcast.fromJson(json) as P; } + @override - Object toJson(P object){ + Object toJson(P object) { return object; } } @JsonSerializable() -class OnlinePodcast{ +class OnlinePodcast { @JsonKey(name: 'earliest_pub_date_ms') final int earliestPubDate; @JsonKey(name: 'title_original') final String title; final String rss; - @JsonKey(name: 'lastest_pub_date_ms') - final int lastestPubDate; + @JsonKey(name: 'latest_pub_date_ms') + final int latestPubDate; @JsonKey(name: 'description_original') final String description; @JsonKey(name: 'total_episodes') @@ -45,10 +46,30 @@ class OnlinePodcast{ final String image; @JsonKey(name: 'publisher_original') final String publisher; + final String id; OnlinePodcast( - {this.earliestPubDate, this.title, this.count, this.description, this.image, this.lastestPubDate, this.rss, this.publisher} - ); + {this.earliestPubDate, + this.title, + this.count, + this.description, + this.image, + this.latestPubDate, + this.rss, + this.publisher, + this.id}); factory OnlinePodcast.fromJson(Map json) => - _$OnlinePodcastFromJson(json); + _$OnlinePodcastFromJson(json); Map toJson() => _$OnlinePodcastToJson(this); -} \ No newline at end of file + + @override + bool operator ==(Object onlinePodcast) => + onlinePodcast is OnlinePodcast && onlinePodcast.id == id; + + @override + int get hashCode => hashValues(id, title); + + int get interval { + if (count < 1) return null; + return (latestPubDate - earliestPubDate) ~/ count; + } +} diff --git a/lib/type/searchpodcast.g.dart b/lib/type/searchpodcast.g.dart index b9a0dda..5a49a30 100644 --- a/lib/type/searchpodcast.g.dart +++ b/lib/type/searchpodcast.g.dart @@ -8,34 +8,33 @@ part of 'searchpodcast.dart'; SearchPodcast

_$SearchPodcastFromJson

(Map json) { return SearchPodcast

( - results: (json['results'] as List) - ?.map((e) => e == null ? null : _ConvertP

().fromJson(e)) - ?.toList(), - nextOffset: json['next_offset'] as int, - total: json['total'] as int, - count: json['count'] as int); + results: (json['results'] as List)?.map(_ConvertP

().fromJson)?.toList(), + nextOffset: json['next_offset'] as int, + total: json['total'] as int, + count: json['count'] as int, + ); } Map _$SearchPodcastToJson

(SearchPodcast

instance) => { - 'results': instance.results - ?.map((e) => e == null ? null : _ConvertP

().toJson(e)) - ?.toList(), + 'results': instance.results?.map(_ConvertP

().toJson)?.toList(), 'next_offset': instance.nextOffset, 'total': instance.total, - 'count': instance.count + 'count': instance.count, }; OnlinePodcast _$OnlinePodcastFromJson(Map json) { return OnlinePodcast( - earliestPubDate: json['earliest_pub_date_ms'] as int, - title: json['title_original'] as String, - count: json['total_episodes'] as int, - description: json['description_original'] as String, - image: json['image'] as String, - lastestPubDate: json['lastest_pub_date_ms'] as int, - rss: json['rss'] as String, - publisher: json['publisher_original'] as String); + earliestPubDate: json['earliest_pub_date_ms'] as int, + title: json['title_original'] as String, + count: json['total_episodes'] as int, + description: json['description_original'] as String, + image: json['image'] as String, + latestPubDate: json['latest_pub_date_ms'] as int, + rss: json['rss'] as String, + publisher: json['publisher_original'] as String, + id: json['id'] as String, + ); } Map _$OnlinePodcastToJson(OnlinePodcast instance) => @@ -43,9 +42,10 @@ Map _$OnlinePodcastToJson(OnlinePodcast instance) => 'earliest_pub_date_ms': instance.earliestPubDate, 'title_original': instance.title, 'rss': instance.rss, - 'lastest_pub_date_ms': instance.lastestPubDate, + 'latest_pub_date_ms': instance.latestPubDate, 'description_original': instance.description, 'total_episodes': instance.count, 'image': instance.image, - 'publisher_original': instance.publisher + 'publisher_original': instance.publisher, + 'id': instance.id, }; diff --git a/lib/home/audiopanel.dart b/lib/util/audiopanel.dart similarity index 98% rename from lib/home/audiopanel.dart rename to lib/util/audiopanel.dart index d4c1e1a..98a36b9 100644 --- a/lib/home/audiopanel.dart +++ b/lib/util/audiopanel.dart @@ -34,12 +34,12 @@ class _AudioPanelState extends State with TickerProviderStateMixin { _controller = AnimationController(vsync: this, duration: Duration(milliseconds: 50)) ..addListener(() { - setState(() {}); + if (mounted) setState(() {}); }); _slowController = AnimationController(vsync: this, duration: Duration(milliseconds: 200)) ..addListener(() { - setState(() {}); + if (mounted) setState(() {}); }); _animation = Tween(begin: initSize, end: initSize).animate(_controller); diff --git a/lib/util/context_extension.dart b/lib/util/context_extension.dart deleted file mode 100644 index b2be539..0000000 --- a/lib/util/context_extension.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:flutter/material.dart'; -import '../generated/l10n.dart'; - -extension ContextExtension on BuildContext { - Color get primaryColor => Theme.of(this).primaryColor; - Color get accentColor => Theme.of(this).accentColor; - Color get scaffoldBackgroundColor => Theme.of(this).scaffoldBackgroundColor; - Color get primaryColorDark => Theme.of(this).primaryColorDark; - Color get textColor => Theme.of(this).textTheme.bodyText1.color; - Brightness get brightness => Theme.of(this).brightness; - double get width => MediaQuery.of(this).size.width; - double get height => MediaQuery.of(this).size.width; - TextTheme get textTheme => Theme.of(this).textTheme; - S get s => S.of(this); -} diff --git a/lib/util/episodegrid.dart b/lib/util/episodegrid.dart index 25d02dc..e7956ea 100644 --- a/lib/util/episodegrid.dart +++ b/lib/util/episodegrid.dart @@ -16,11 +16,12 @@ import 'open_container.dart'; import '../state/audio_state.dart'; import '../state/download_state.dart'; import '../type/episodebrief.dart'; +import '../type/play_histroy.dart'; import '../episodes/episode_detail.dart'; import '../local_storage/sqflite_localpodcast.dart'; import '../local_storage/key_value_storage.dart'; import 'colorize.dart'; -import 'context_extension.dart'; +import 'extension_helper.dart'; import 'custompaint.dart'; enum Layout { three, two, one } diff --git a/lib/util/extension_helper.dart b/lib/util/extension_helper.dart new file mode 100644 index 0000000..805460e --- /dev/null +++ b/lib/util/extension_helper.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import '../generated/l10n.dart'; + +extension ContextExtension on BuildContext { + Color get primaryColor => Theme.of(this).primaryColor; + Color get accentColor => Theme.of(this).accentColor; + Color get scaffoldBackgroundColor => Theme.of(this).scaffoldBackgroundColor; + Color get primaryColorDark => Theme.of(this).primaryColorDark; + Color get textColor => Theme.of(this).textTheme.bodyText1.color; + Brightness get brightness => Theme.of(this).brightness; + double get width => MediaQuery.of(this).size.width; + double get height => MediaQuery.of(this).size.height; + TextTheme get textTheme => Theme.of(this).textTheme; + S get s => S.of(this); +} + +extension IntExtension on int { + String toDate(BuildContext context) { + if (this == null) return ''; + final s = context.s; + DateTime date = DateTime.fromMillisecondsSinceEpoch(this, isUtc: true); + var difference = DateTime.now().toUtc().difference(date); + if (difference.inHours < 24) { + return s.hoursAgo(difference.inHours); + } else if (difference.inDays < 7) { + return s.daysAgo(difference.inDays); + } else { + return DateFormat.yMMMd().format( + DateTime.fromMillisecondsSinceEpoch(this, isUtc: true).toLocal()); + } + } + + String get toTime => + '${(this ~/ 60)}:${(this.truncate() % 60).toString().padLeft(2, '0')}'; + + String toInterval(BuildContext context) { + if (this == null || this.isNegative) return ''; + final s = context.s; + var interval = Duration(milliseconds: this); + if (interval.inHours <= 48) + return 'Published daily'; + else if (interval.inDays > 2 && interval.inDays <= 14) + return 'Published weekly'; + else if (interval.inDays > 14 && interval.inDays < 60) + return 'Published monthly'; + else + return 'Published yearly'; + } +} diff --git a/lib/util/general_dialog.dart b/lib/util/general_dialog.dart index bfeee66..b4dd871 100644 --- a/lib/util/general_dialog.dart +++ b/lib/util/general_dialog.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'context_extension.dart'; +import 'extension_helper.dart'; generalDialog(BuildContext context, {Widget title, Widget content, List actions}) => @@ -26,7 +26,7 @@ generalDialog(BuildContext context, shape: RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(10.0))), titlePadding: EdgeInsets.all(20), - title: SizedBox(width: context.width-160, child: title), + title: SizedBox(width: context.width - 160, child: title), content: content, actionsPadding: EdgeInsets.all(10), actions: actions), diff --git a/lib/util/open_container.dart b/lib/util/open_container.dart index 1d2b11e..12d8e00 100644 --- a/lib/util/open_container.dart +++ b/lib/util/open_container.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; -import 'context_extension.dart'; +import 'extension_helper.dart'; /// Signature for a function that creates a [Widget] to be used within an /// [OpenContainer]. diff --git a/pubspec.yaml b/pubspec.yaml index a16b8d1..868920d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,41 +11,43 @@ dependencies: sdk: flutter flutter_localizations: sdk: flutter - cupertino_icons: ^0.1.3 - json_annotation: ^3.0.1 - sqflite: ^1.3.0 - flutter_html: ^0.11.1 - path_provider: ^1.6.8 - color_thief_flutter: ^1.0.2 - provider: ^4.1.2 - google_fonts: ^1.1.0 - dio: ^3.0.9 - file_picker: ^1.9.0+1 - marquee: ^1.3.1 - flutter_downloader: ^1.4.4 - permission_handler: ^5.0.0+hotfix.3 - fluttertoast: ^4.0.1 - intl: ^0.16.1 - url_launcher: ^5.4.10 - image: ^2.1.12 - shared_preferences: ^0.5.7 - uuid: ^2.2.0 - tuple: ^1.0.3 - cached_network_image: ^2.2.0+1 - workmanager: ^0.2.3 - fl_chart: ^0.10.1 - audio_service: ^0.8.0 - flutter_file_dialog: ^0.0.5 - flutter_linkify: ^3.1.3 - extended_nested_scroll_view: ^0.4.0 - connectivity: ^0.4.8+2 - flare_flutter: ^2.0.5 - rxdart: ^0.24.0 - wc_flutter_share: ^0.2.1 auto_animated: ^2.1.0 + audio_service: ^0.11.2 + cached_network_image: ^2.2.0+1 + color_thief_flutter: ^1.0.2 + cupertino_icons: ^0.1.3 + connectivity: ^0.4.9 + dio: ^3.0.9 + extended_nested_scroll_view: ^1.0.1 feature_discovery: ^0.10.0 + file_picker: ^1.12.0 + flutter_html: ^0.11.1 + flutter_downloader: ^1.4.4 + fluttertoast: ^4.0.0 flutter_isolate: ^1.0.0+14 flutter_time_picker_spinner: ^1.0.6+1 + flutter_linkify: ^3.1.3 + flutter_file_dialog: ^0.0.5 + flare_flutter: ^2.0.5 + fl_chart: ^0.10.1 + marquee: ^1.3.1 + google_fonts: ^1.1.0 + image: ^2.1.14 + intl: ^0.16.1 + json_serializable: ^3.3.0 + json_annotation: ^3.0.1 + path_provider: ^1.6.11 + permission_handler: ^5.0.1 + provider: ^4.3.1 + rxdart: ^0.24.1 + sqflite: ^1.3.1 + shared_preferences: ^0.5.8 + tuple: ^1.0.3 + url_launcher: ^5.5.0 + uuid: ^2.2.0 + xml: ^4.2.0 + workmanager: ^0.2.3 + wc_flutter_share: ^0.2.2 just_audio: git: url: https://github.com/stonega/just_audio.git @@ -59,9 +61,7 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - -dependency_overrides: - xml: "4.2.0" + build_runner: ^1.10.0 flutter: assets: