Improve search page performance.

This commit is contained in:
stonega 2021-01-31 22:26:54 +08:00
parent 3405afce83
commit ba3347e31e
3 changed files with 280 additions and 247 deletions

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:line_icons/line_icons.dart'; import 'package:line_icons/line_icons.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../.env.dart';
import '../local_storage/key_value_storage.dart'; import '../local_storage/key_value_storage.dart';
import '../service/search_api.dart'; import '../service/search_api.dart';
import '../state/search_state.dart'; import '../state/search_state.dart';
@ -9,7 +10,6 @@ import '../type/search_api/search_genre.dart';
import '../type/search_api/searchpodcast.dart'; import '../type/search_api/searchpodcast.dart';
import '../util/extension_helper.dart'; import '../util/extension_helper.dart';
import '../widgets/custom_widget.dart'; import '../widgets/custom_widget.dart';
import '../.env.dart';
import 'search_podcast.dart'; import 'search_podcast.dart';
class DiscoveryPage extends StatefulWidget { class DiscoveryPage extends StatefulWidget {
@ -149,6 +149,51 @@ class DiscoveryPageState extends State<DiscoveryPage> {
); );
}); });
Widget _podcastCard(OnlinePodcast podcast, {VoidCallback onTap}) {
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10), color: context.primaryColor),
width: 120,
margin: EdgeInsets.fromLTRB(10, 10, 0, 10),
child: Material(
color: Colors.transparent,
borderRadius: BorderRadius.circular(10),
clipBehavior: Clip.hardEdge,
child: InkWell(
onTap: onTap,
child: Padding(
padding: EdgeInsets.all(4.0),
child: Column(
children: [
Expanded(
flex: 2,
child: Center(child: PodcastAvatar(podcast)),
),
Expanded(
flex: 1,
child: Text(
podcast.title,
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.fade,
style: TextStyle(fontWeight: FontWeight.bold),
),
),
Expanded(
flex: 1,
child: Center(
child:
SizedBox(height: 32, child: SubscribeButton(podcast)),
),
),
],
),
),
),
),
);
}
Future<List<OnlinePodcast>> _getTopPodcasts({int page}) async { Future<List<OnlinePodcast>> _getTopPodcasts({int page}) async {
final searchEngine = ListenNotesSearch(); final searchEngine = ListenNotesSearch();
var searchResult = await searchEngine.fetchBestPodcast( var searchResult = await searchEngine.fetchBestPodcast(
@ -168,7 +213,7 @@ class DiscoveryPageState extends State<DiscoveryPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final searchState = context.watch<SearchState>(); final searchState = context.read<SearchState>();
return FutureBuilder<bool>( return FutureBuilder<bool>(
future: _getHideDiscovery(), future: _getHideDiscovery(),
initialData: true, initialData: true,
@ -233,8 +278,12 @@ class DiscoveryPageState extends State<DiscoveryPage> {
) )
: PodcastSlideup( : PodcastSlideup(
searchEngine: SearchEngine.listenNotes, searchEngine: SearchEngine.listenNotes,
child: _selectedGenre == null child: Selector<SearchState, Genre>(
? SingleChildScrollView( selector: (_, searchState) => searchState.genre,
builder: (_, genre, __) => IndexedStack(
index: genre == null ? 0 : 1,
children: [
SingleChildScrollView(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -254,75 +303,19 @@ class DiscoveryPageState extends State<DiscoveryPage> {
return ScrollConfiguration( return ScrollConfiguration(
behavior: NoGrowBehavior(), behavior: NoGrowBehavior(),
child: ListView( child: ListView(
addAutomaticKeepAlives: true,
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
children: snapshot.hasData children: snapshot.hasData
? snapshot.data ? snapshot.data
.map<Widget>((podcast) { .map<Widget>((podcast) {
return Container( return _podcastCard(
decoration: BoxDecoration( podcast,
borderRadius: onTap: () {
BorderRadius.circular( searchState
10), .selectedPodcast =
color: podcast;
context.primaryColor), widget.onTap('');
width: 120, },
margin: EdgeInsets.fromLTRB(
10, 10, 0, 10),
child: Material(
color: Colors.transparent,
borderRadius:
BorderRadius.circular(
10),
clipBehavior: Clip.hardEdge,
child: InkWell(
onTap: () {
searchState
.selectedPodcast =
podcast;
widget.onTap('');
},
child: Padding(
padding:
EdgeInsets.all(4.0),
child: Column(
children: [
Expanded(
flex: 2,
child: Center(
child: PodcastAvatar(
podcast)),
),
Expanded(
flex: 1,
child: Text(
podcast.title,
textAlign:
TextAlign
.center,
maxLines: 2,
overflow:
TextOverflow
.fade,
style: TextStyle(
fontWeight:
FontWeight
.bold),
),
),
Expanded(
flex: 1,
child: Center(
child: SizedBox(
height: 32,
child: SubscribeButton(
podcast)),
),
),
],
),
),
),
),
); );
}).toList() }).toList()
: [ : [
@ -349,7 +342,7 @@ class DiscoveryPageState extends State<DiscoveryPage> {
EdgeInsets.fromLTRB(20, 0, 20, 0), EdgeInsets.fromLTRB(20, 0, 20, 0),
onTap: () { onTap: () {
widget.onTap(''); widget.onTap('');
setState(() => _selectedGenre = e); searchState.setGenre = e;
}, },
title: Text(e.name), title: Text(e.name),
)) ))
@ -369,8 +362,11 @@ class DiscoveryPageState extends State<DiscoveryPage> {
) )
], ],
), ),
) ),
: _TopPodcastList(genre: _selectedGenre), genre == null ? Center() : _TopPodcastList(genre: genre),
],
),
),
), ),
); );
} }

View File

@ -10,6 +10,7 @@ import 'package:fluttertoast/fluttertoast.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:webfeed/webfeed.dart'; import 'package:webfeed/webfeed.dart';
import '../.env.dart';
import '../local_storage/key_value_storage.dart'; import '../local_storage/key_value_storage.dart';
import '../service/search_api.dart'; import '../service/search_api.dart';
import '../state/podcast_group.dart'; import '../state/podcast_group.dart';
@ -18,13 +19,10 @@ import '../type/search_api/searchepisodes.dart';
import '../type/search_api/searchpodcast.dart'; import '../type/search_api/searchpodcast.dart';
import '../util/extension_helper.dart'; import '../util/extension_helper.dart';
import '../widgets/custom_widget.dart'; import '../widgets/custom_widget.dart';
import '../.env.dart';
import 'pocast_discovery.dart'; import 'pocast_discovery.dart';
class MyHomePageDelegate extends SearchDelegate<int> { class MyHomePageDelegate extends SearchDelegate<int> {
final String searchFieldLabel; final String searchFieldLabel;
final GlobalKey<DiscoveryPageState> _discoveryKey =
GlobalKey<DiscoveryPageState>();
MyHomePageDelegate({this.searchFieldLabel}) MyHomePageDelegate({this.searchFieldLabel})
: super( : super(
searchFieldLabel: searchFieldLabel, searchFieldLabel: searchFieldLabel,
@ -43,15 +41,6 @@ class MyHomePageDelegate extends SearchDelegate<int> {
} }
} }
Future<SearchEngine> _getSearchEngine() async {
final storage = KeyValueStorage(searchEngineKey);
final index = await storage.getInt();
if (_searchEngine == null) {
_searchEngine = SearchEngine.values[index];
}
return _searchEngine;
}
RegExp rssExp = RegExp(r'^(https?):\/\/(.*)'); RegExp rssExp = RegExp(r'^(https?):\/\/(.*)');
Widget _invalidRss(BuildContext context) => Container( Widget _invalidRss(BuildContext context) => Container(
@ -63,14 +52,15 @@ class MyHomePageDelegate extends SearchDelegate<int> {
@override @override
void close(BuildContext context, int result) { void close(BuildContext context, int result) {
final selectedPodcast = context.read<SearchState>().selectedPodcast; final searchState = context.read<SearchState>();
final selectedPodcast = searchState.selectedPodcast;
if (selectedPodcast != null) { if (selectedPodcast != null) {
context.read<SearchState>().clearSelect(); searchState.clearSelect();
} else { } else {
if (_discoveryKey.currentState?.selectedGenre != null) { if (searchState.genre != null) {
_discoveryKey.currentState.backToHome(); searchState.clearGenre();
} else { } else {
context.read<SearchState>().clearList(); searchState.clearList();
super.close(context, result); super.close(context, result);
} }
} }
@ -79,6 +69,9 @@ class MyHomePageDelegate extends SearchDelegate<int> {
@override @override
ThemeData appBarTheme(BuildContext context) => Theme.of(context); ThemeData appBarTheme(BuildContext context) => Theme.of(context);
@override
TextStyle get searchFieldStyle => TextStyle(fontSize: 20);
@override @override
Widget buildLeading(BuildContext context) { Widget buildLeading(BuildContext context) {
return WillPopScope( return WillPopScope(
@ -103,7 +96,6 @@ class MyHomePageDelegate extends SearchDelegate<int> {
@override @override
Widget buildSuggestions(BuildContext context) { Widget buildSuggestions(BuildContext context) {
return DiscoveryPage( return DiscoveryPage(
key: _discoveryKey,
onTap: (history) { onTap: (history) {
query = history; query = history;
showResults(context); showResults(context);
@ -124,76 +116,25 @@ class MyHomePageDelegate extends SearchDelegate<int> {
showResults(context); showResults(context);
}, },
), ),
FutureBuilder<SearchEngine>( _SearchPopupMenu(
future: _getSearchEngine(), onSelected: (searchEngine) {
initialData: SearchEngine.podcastIndex, _searchEngine = searchEngine;
builder: (context, snapshot) => PopupMenuButton<SearchEngine>( showSuggestions(context);
shape: if (query != '') {
RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), showResults(context);
elevation: 1, }
icon: SizedBox( },
height: 30,
width: 30,
child: CircleAvatar(
backgroundImage: snapshot.data == SearchEngine.podcastIndex
? AssetImage('assets/podcastindex_logo.png')
: AssetImage('assets/listennotes_logo.png'),
maxRadius: 25,
),
),
onSelected: (value) {
_searchEngine = value;
showSuggestions(context);
if (query != '') {
showResults(context);
}
},
itemBuilder: (context) => [
PopupMenuItem(
value: SearchEngine.podcastIndex,
child: Container(
padding: EdgeInsets.only(left: 10),
child: Row(
children: <Widget>[
Text('Podcastindex'),
Spacer(),
if (_searchEngine == SearchEngine.podcastIndex)
DotIndicator()
],
),
),
),
if(environment['apiKey'] != '')
PopupMenuItem(
value: SearchEngine.listenNotes,
child: Container(
padding: EdgeInsets.only(left: 10),
child: Row(
children: <Widget>[
Text('ListenNotes'),
Spacer(),
if (_searchEngine == SearchEngine.listenNotes)
DotIndicator()
],
),
),
),
],
),
), ),
SizedBox(width: 10),
]; ];
} }
@override @override
Widget buildResults(BuildContext context) { Widget buildResults(BuildContext context) {
if (query.isEmpty) { if (query.isEmpty) {
return DiscoveryPage( return DiscoveryPage(onTap: (history) {
key: _discoveryKey, query = history;
onTap: (history) { showResults(context);
query = history; });
showResults(context);
});
} else if (rssExp.stringMatch(query) != null) { } else if (rssExp.stringMatch(query) != null) {
return FutureBuilder( return FutureBuilder(
future: _getRss(rssExp.stringMatch(query)), future: _getRss(rssExp.stringMatch(query)),
@ -401,6 +342,85 @@ class _RssResultState extends State<RssResult> {
} }
} }
class _SearchPopupMenu extends StatefulWidget {
final ValueChanged<SearchEngine> onSelected;
_SearchPopupMenu({this.onSelected, Key key}) : super(key: key);
@override
__SearchPopupMenuState createState() => __SearchPopupMenuState();
}
class __SearchPopupMenuState extends State<_SearchPopupMenu> {
SearchEngine _searchEngine;
@override
void initState() {
super.initState();
_searchEngine = SearchEngine.podcastIndex;
_getSearchEngine();
}
Future<void> _getSearchEngine() async {
final storage = KeyValueStorage(searchEngineKey);
final index = await storage.getInt();
setState(() => _searchEngine = SearchEngine.values[index]);
widget.onSelected(_searchEngine);
}
@override
Widget build(BuildContext context) {
return PopupMenuButton<SearchEngine>(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
elevation: 1,
icon: SizedBox(
height: 20,
width: 20,
child: CircleAvatar(
backgroundImage: _searchEngine == SearchEngine.podcastIndex
? AssetImage('assets/podcastindex_logo.png')
: AssetImage('assets/listennotes_logo.png'),
maxRadius: 25,
),
),
onSelected: (searchEngine) {
widget.onSelected(searchEngine);
setState(() {
_searchEngine = searchEngine;
});
},
itemBuilder: (context) => [
PopupMenuItem(
value: SearchEngine.podcastIndex,
child: Padding(
padding: EdgeInsets.only(left: 10),
child: Row(
children: <Widget>[
Text('Podcastindex'),
Spacer(),
if (_searchEngine == SearchEngine.podcastIndex) DotIndicator()
],
),
),
),
if (environment['apiKey'] != '')
PopupMenuItem(
value: SearchEngine.listenNotes,
child: Padding(
padding: EdgeInsets.only(left: 10),
child: Row(
children: <Widget>[
Text('ListenNotes'),
Spacer(),
if (_searchEngine == SearchEngine.listenNotes) DotIndicator()
],
),
),
),
],
);
}
}
class _ListenNotesSearch extends StatefulWidget { class _ListenNotesSearch extends StatefulWidget {
final String query; final String query;
_ListenNotesSearch({this.query, Key key}) : super(key: key); _ListenNotesSearch({this.query, Key key}) : super(key: key);
@ -1058,103 +1078,107 @@ class _SearchResultDetailState extends State<SearchResultDetail>
), ),
), ),
Expanded( Expanded(
child: TabBarView(children: [ child: Container(
ListView( color: context.scaffoldBackgroundColor,
physics: _animation.value != widget.maxHeight child: TabBarView(children: [
? NeverScrollableScrollPhysics() ListView(
: null, physics: _animation.value != widget.maxHeight
children: [ ? NeverScrollableScrollPhysics()
Html( : null,
onLinkTap: (url) { children: [
url.launchUrl; Html(
}, onLinkTap: (url) {
linkStyle: TextStyle( url.launchUrl;
color: context.accentColor, },
textBaseline: TextBaseline.ideographic), linkStyle: TextStyle(
shrinkToFit: true, color: context.accentColor,
data: widget.onlinePodcast.description, textBaseline: TextBaseline.ideographic),
padding: const EdgeInsets.only( shrinkToFit: true,
left: 20.0, right: 20, bottom: 20), data: widget.onlinePodcast.description,
defaultTextStyle: TextStyle( padding: const EdgeInsets.only(
height: 1.8, left: 20.0, right: 20, bottom: 20),
defaultTextStyle: TextStyle(
height: 1.8,
),
), ),
), ],
], ),
), FutureBuilder<List<OnlineEpisode>>(
FutureBuilder<List<OnlineEpisode>>( future: _searchFuture,
future: _searchFuture, builder: (context, snapshot) {
builder: (context, snapshot) { if (snapshot.hasData) {
if (snapshot.hasData) { var content = snapshot.data;
var content = snapshot.data; return ListView.builder(
return ListView.builder( physics: _animation.value != widget.maxHeight
physics: _animation.value != widget.maxHeight ? NeverScrollableScrollPhysics()
? NeverScrollableScrollPhysics() : null,
: null, itemCount: content.length + 1,
itemCount: content.length + 1, itemBuilder: (context, index) {
itemBuilder: (context, index) { if (index == content.length) {
if (index == content.length) { return Container(
return Container( padding: const EdgeInsets.only(
padding: const EdgeInsets.only( top: 10.0, bottom: 20.0),
top: 10.0, bottom: 20.0), alignment: Alignment.center,
alignment: Alignment.center, child: SizedBox(
child: SizedBox( child: OutlineButton(
child: OutlineButton( highlightedBorderColor:
highlightedBorderColor: context.accentColor,
context.accentColor, splashColor: context.accentColor
splashColor: context.accentColor .withOpacity(0.5),
.withOpacity(0.5), shape: RoundedRectangleBorder(
shape: RoundedRectangleBorder( borderRadius: BorderRadius.all(
borderRadius: BorderRadius.all( Radius.circular(100))),
Radius.circular(100))), child: _loading
child: _loading ? SizedBox(
? SizedBox( height: 20,
height: 20, width: 20,
width: 20, child:
child: CircularProgressIndicator(
CircularProgressIndicator( strokeWidth: 2,
strokeWidth: 2, ))
)) : Text(context.s.loadMore),
: Text(context.s.loadMore), onPressed: () {
onPressed: () { if (widget.searchEngine ==
if (widget.searchEngine == SearchEngine.listenNotes) {
SearchEngine.listenNotes) { _loading
_loading ? null
? null : setState(
: setState( () {
() { _loading = true;
_loading = true; _searchFuture =
_searchFuture = _getListenNotesEpisodes(
_getListenNotesEpisodes( id: widget
id: widget .onlinePodcast
.onlinePodcast .id,
.id, nextEpisodeDate:
nextEpisodeDate: _nextEpisdoeDate);
_nextEpisdoeDate); },
}, );
); }
} }),
}), ),
), );
}
return ListTile(
title: Text(content[index].title),
tileColor: Colors.transparent,
subtitle: Text(
content[index].length == 0
? '${content[index].pubDate.toDate(context)}'
: '${content[index].length.toTime} | '
'${content[index].pubDate.toDate(context)}',
style: TextStyle(
color: context.accentColor)),
); );
} },
return ListTile( );
title: Text(content[index].title), }
subtitle: Text( return Center(
content[index].length == 0 child: CircularProgressIndicator(),
? '${content[index].pubDate.toDate(context)}'
: '${content[index].length.toTime} | '
'${content[index].pubDate.toDate(context)}',
style:
TextStyle(color: context.accentColor)),
);
},
); );
} })
return Center( ]),
child: CircularProgressIndicator(), ),
);
})
]),
) )
], ],
), ),

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:tsacdop/type/search_api/search_genre.dart';
import '../type/search_api/searchpodcast.dart'; import '../type/search_api/searchpodcast.dart';
class SearchState extends ChangeNotifier { class SearchState extends ChangeNotifier {
@ -8,12 +9,19 @@ class SearchState extends ChangeNotifier {
bool get update => _update; bool get update => _update;
OnlinePodcast _selectedPodcast; OnlinePodcast _selectedPodcast;
OnlinePodcast get selectedPodcast => _selectedPodcast; OnlinePodcast get selectedPodcast => _selectedPodcast;
Genre _genre;
Genre get genre => _genre;
set selectedPodcast(OnlinePodcast podcast) { set selectedPodcast(OnlinePodcast podcast) {
_selectedPodcast = podcast; _selectedPodcast = podcast;
notifyListeners(); notifyListeners();
} }
set setGenre(Genre genre) {
_genre = genre;
notifyListeners();
}
bool isSubscribed(OnlinePodcast podcast) => _subscribedList.contains(podcast); bool isSubscribed(OnlinePodcast podcast) => _subscribedList.contains(podcast);
void clearSelect() { void clearSelect() {
@ -25,6 +33,11 @@ class SearchState extends ChangeNotifier {
_subscribedList.clear(); _subscribedList.clear();
} }
void clearGenre(){
_genre = null;
notifyListeners();
}
void addPodcast(OnlinePodcast podcast) { void addPodcast(OnlinePodcast podcast) {
_subscribedList.add(podcast); _subscribedList.add(podcast);
_update = !_update; _update = !_update;