Add discovery page in search page.
This commit is contained in:
parent
d99e7a2e04
commit
f7dfb0b005
|
@ -8,6 +8,7 @@ import 'package:flutter/services.dart';
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.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 'package:tsacdop/state/search_state.dart';
|
||||||
import 'package:tuple/tuple.dart';
|
import 'package:tuple/tuple.dart';
|
||||||
|
|
||||||
import '../local_storage/key_value_storage.dart';
|
import '../local_storage/key_value_storage.dart';
|
||||||
|
|
|
@ -0,0 +1,385 @@
|
||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import '../local_storage/key_value_storage.dart';
|
||||||
|
import '../service/api_search.dart';
|
||||||
|
import '../state/search_state.dart';
|
||||||
|
import '../type/search_genre.dart';
|
||||||
|
import '../type/searchpodcast.dart';
|
||||||
|
import '../util/custom_widget.dart';
|
||||||
|
import '../util/extension_helper.dart';
|
||||||
|
import 'search_podcast.dart';
|
||||||
|
|
||||||
|
class DiscoveryPage extends StatefulWidget {
|
||||||
|
DiscoveryPage({this.onTap, Key key}) : super(key: key);
|
||||||
|
final ValueChanged<String> onTap;
|
||||||
|
@override
|
||||||
|
DiscoveryPageState createState() => DiscoveryPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class DiscoveryPageState extends State<DiscoveryPage> {
|
||||||
|
Genre _selectedGenre;
|
||||||
|
Genre get selectedGenre => _selectedGenre;
|
||||||
|
final List<OnlinePodcast> _podcastList = [];
|
||||||
|
bool _loading;
|
||||||
|
Future _searchTopPodcast;
|
||||||
|
int _page;
|
||||||
|
Future<List<String>> _getSearchHistory() {
|
||||||
|
final storage = KeyValueStorage(searchHistoryKey);
|
||||||
|
final history = storage.getStringList();
|
||||||
|
return history;
|
||||||
|
}
|
||||||
|
|
||||||
|
void backToHome() {
|
||||||
|
setState(() {
|
||||||
|
_selectedGenre = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_searchTopPodcast = _getTopPodcasts(page: 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _loadTopPodcasts() => Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(10), color: context.primaryColor),
|
||||||
|
width: 120,
|
||||||
|
margin: EdgeInsets.fromLTRB(10, 10, 0, 10),
|
||||||
|
padding: EdgeInsets.all(4),
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
flex: 2,
|
||||||
|
child: Center(
|
||||||
|
child: Container(
|
||||||
|
height: 50,
|
||||||
|
width: 50,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: context.primaryColorDark,
|
||||||
|
),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: SizedBox(
|
||||||
|
width: 20,
|
||||||
|
height: 2,
|
||||||
|
child: LinearProgressIndicator(value: 0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
flex: 1,
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 80,
|
||||||
|
height: context.textTheme.bodyText1.fontSize,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: context.primaryColorDark,
|
||||||
|
borderRadius: BorderRadius.circular(4)),
|
||||||
|
),
|
||||||
|
SizedBox(height: 10),
|
||||||
|
Container(
|
||||||
|
width: 40,
|
||||||
|
height: context.textTheme.bodyText1.fontSize,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: context.primaryColorDark,
|
||||||
|
borderRadius: BorderRadius.circular(4)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
flex: 1,
|
||||||
|
child: Center(
|
||||||
|
child: SizedBox(
|
||||||
|
height: 32,
|
||||||
|
child: 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(context.s.subscribe),
|
||||||
|
disabledBorderColor: Colors.grey[500],
|
||||||
|
onPressed: () {}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
));
|
||||||
|
|
||||||
|
Future<List<OnlinePodcast>> _getTopPodcasts({int page}) async {
|
||||||
|
final searchEngine = SearchEngine();
|
||||||
|
var searchResult = await searchEngine.fetchBestPodcast(
|
||||||
|
genre: '',
|
||||||
|
page: page,
|
||||||
|
);
|
||||||
|
final podcastTopList =
|
||||||
|
searchResult.podcasts.map((e) => e?.toOnlinePodcast).toList();
|
||||||
|
_podcastList.addAll(podcastTopList.cast());
|
||||||
|
_loading = false;
|
||||||
|
return _podcastList;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final searchState = context.watch<SearchState>();
|
||||||
|
return PodcastSlideup(
|
||||||
|
child: _selectedGenre == null
|
||||||
|
? SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
FutureBuilder<List<String>>(
|
||||||
|
future: _getSearchHistory(),
|
||||||
|
initialData: [],
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.hasData && snapshot.data.isNotEmpty) {
|
||||||
|
final history = snapshot.data;
|
||||||
|
return SizedBox(
|
||||||
|
height: 50,
|
||||||
|
child: Row(
|
||||||
|
children: history
|
||||||
|
.map<Widget>((e) => Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: FlatButton.icon(
|
||||||
|
color: Colors.accents[
|
||||||
|
math.Random().nextInt(10)]
|
||||||
|
.withAlpha(70),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius:
|
||||||
|
BorderRadius.circular(100.0),
|
||||||
|
),
|
||||||
|
onPressed: () => widget.onTap(e),
|
||||||
|
label: Text(e),
|
||||||
|
icon: Icon(
|
||||||
|
Icons.bookmark_border,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
))
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return SizedBox(
|
||||||
|
height: 1,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
SizedBox(
|
||||||
|
height: 200,
|
||||||
|
child: FutureBuilder<List<OnlinePodcast>>(
|
||||||
|
future: _searchTopPodcast,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
return ScrollConfiguration(
|
||||||
|
behavior: NoGrowBehavior(),
|
||||||
|
child: ListView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
children: snapshot.hasData
|
||||||
|
? snapshot.data.map<Widget>((podcast) {
|
||||||
|
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: () => searchState
|
||||||
|
.selectedPodcast = podcast,
|
||||||
|
child: Padding(
|
||||||
|
padding:
|
||||||
|
const 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()
|
||||||
|
: [
|
||||||
|
_loadTopPodcasts(),
|
||||||
|
_loadTopPodcasts(),
|
||||||
|
_loadTopPodcasts(),
|
||||||
|
_loadTopPodcasts(),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
ListView(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: NeverScrollableScrollPhysics(),
|
||||||
|
children: genres
|
||||||
|
.map<Widget>((e) => ListTile(
|
||||||
|
onTap: () => setState(() => _selectedGenre = e),
|
||||||
|
title: Text(e.name),
|
||||||
|
))
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
height: 40,
|
||||||
|
child: Center(
|
||||||
|
child: Image(
|
||||||
|
image: context.brightness == Brightness.light
|
||||||
|
? AssetImage('assets/listennotes.png')
|
||||||
|
: AssetImage('assets/listennotes_light.png'),
|
||||||
|
height: 15,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: _TopPodcastList(genre: _selectedGenre),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TopPodcastList extends StatefulWidget {
|
||||||
|
final Genre genre;
|
||||||
|
_TopPodcastList({this.genre, Key key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
__TopPodcastListState createState() => __TopPodcastListState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class __TopPodcastListState extends State<_TopPodcastList> {
|
||||||
|
final List<OnlinePodcast> _podcastList = [];
|
||||||
|
Future _searchFuture;
|
||||||
|
bool _loading;
|
||||||
|
int _page;
|
||||||
|
Future<List<OnlinePodcast>> _getTopPodcasts({Genre genre, int page}) async {
|
||||||
|
final searchEngine = SearchEngine();
|
||||||
|
var searchResult = await searchEngine.fetchBestPodcast(
|
||||||
|
genre: genre.id,
|
||||||
|
page: page,
|
||||||
|
);
|
||||||
|
final podcastTopList =
|
||||||
|
searchResult.podcasts.map((e) => e?.toOnlinePodcast).toList();
|
||||||
|
_podcastList.addAll(podcastTopList.cast());
|
||||||
|
_loading = false;
|
||||||
|
return _podcastList;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
_page = 1;
|
||||||
|
_searchFuture = _getTopPodcasts(genre: widget.genre, page: _page);
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return FutureBuilder(
|
||||||
|
future: _searchFuture,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (!snapshot.hasData) {
|
||||||
|
return Container(
|
||||||
|
padding: EdgeInsets.only(top: 200),
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final 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: 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;
|
||||||
|
_page++;
|
||||||
|
print(_page);
|
||||||
|
_searchFuture = _getTopPodcasts(
|
||||||
|
genre: widget.genre, page: _page);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,18 +7,22 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_html/flutter_html.dart';
|
import 'package:flutter_html/flutter_html.dart';
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:webfeed/webfeed.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:webfeed/webfeed.dart';
|
||||||
|
|
||||||
|
import '../local_storage/key_value_storage.dart';
|
||||||
import '../service/api_search.dart';
|
import '../service/api_search.dart';
|
||||||
import '../state/podcast_group.dart';
|
import '../state/podcast_group.dart';
|
||||||
|
import '../state/search_state.dart';
|
||||||
import '../type/searchepisodes.dart';
|
import '../type/searchepisodes.dart';
|
||||||
import '../type/searchpodcast.dart';
|
import '../type/searchpodcast.dart';
|
||||||
import '../util/extension_helper.dart';
|
import '../util/extension_helper.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,
|
||||||
|
@ -44,35 +48,53 @@ class MyHomePageDelegate extends SearchDelegate<int> {
|
||||||
child: Text(context.s.searchInvalidRss),
|
child: Text(context.s.searchInvalidRss),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void close(BuildContext context, int result) {
|
||||||
|
final selectedPodcast = context.read<SearchState>().selectedPodcast;
|
||||||
|
if (selectedPodcast != null) {
|
||||||
|
context.read<SearchState>().clearSelect();
|
||||||
|
} else {
|
||||||
|
if (_discoveryKey.currentState?.selectedGenre != null) {
|
||||||
|
_discoveryKey.currentState.backToHome();
|
||||||
|
} else {
|
||||||
|
context.read<SearchState>().clearList();
|
||||||
|
super.close(context, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ThemeData appBarTheme(BuildContext context) => Theme.of(context);
|
ThemeData appBarTheme(BuildContext context) => Theme.of(context);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget buildLeading(BuildContext context) {
|
Widget buildLeading(BuildContext context) {
|
||||||
return IconButton(
|
return WillPopScope(
|
||||||
tooltip: context.s.back,
|
onWillPop: () async {
|
||||||
icon: AnimatedIcon(
|
close(context, null);
|
||||||
icon: AnimatedIcons.menu_arrow,
|
return false;
|
||||||
progress: transitionAnimation,
|
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
close(context, 1);
|
|
||||||
},
|
},
|
||||||
|
child: IconButton(
|
||||||
|
tooltip: context.s.back,
|
||||||
|
icon: AnimatedIcon(
|
||||||
|
icon: AnimatedIcons.menu_arrow,
|
||||||
|
progress: transitionAnimation,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
close(context, 1);
|
||||||
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget buildSuggestions(BuildContext context) {
|
Widget buildSuggestions(BuildContext context) {
|
||||||
return Center(
|
return DiscoveryPage(
|
||||||
child: Container(
|
key: _discoveryKey,
|
||||||
padding: EdgeInsets.only(top: 100),
|
onTap: (history) {
|
||||||
child: Image(
|
query = history;
|
||||||
image: context.brightness == Brightness.light
|
showResults(context);
|
||||||
? AssetImage('assets/listennotes.png')
|
},
|
||||||
: AssetImage('assets/listennotes_light.png'),
|
);
|
||||||
height: 20,
|
|
||||||
),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -94,18 +116,24 @@ class MyHomePageDelegate extends SearchDelegate<int> {
|
||||||
@override
|
@override
|
||||||
Widget buildResults(BuildContext context) {
|
Widget buildResults(BuildContext context) {
|
||||||
if (query.isEmpty) {
|
if (query.isEmpty) {
|
||||||
return Container(
|
return DiscoveryPage(
|
||||||
height: 10,
|
key: _discoveryKey,
|
||||||
width: 10,
|
onTap: (history) {
|
||||||
margin: EdgeInsets.only(top: 400),
|
query = history;
|
||||||
child: SizedBox(
|
showResults(context);
|
||||||
height: 10,
|
});
|
||||||
child: Image.asset(
|
// return Container(
|
||||||
'assets/listennote.png',
|
// height: 10,
|
||||||
fit: BoxFit.fill,
|
// width: 10,
|
||||||
),
|
// margin: EdgeInsets.only(top: 400),
|
||||||
),
|
// child: SizedBox(
|
||||||
);
|
// height: 10,
|
||||||
|
// child: Image.asset(
|
||||||
|
// 'assets/listennotes.png',
|
||||||
|
// fit: BoxFit.fill,
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
} 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)),
|
||||||
|
@ -360,18 +388,30 @@ class _SearchListState extends State<SearchList> {
|
||||||
final List<OnlinePodcast> _podcastList = [];
|
final List<OnlinePodcast> _podcastList = [];
|
||||||
int _offset;
|
int _offset;
|
||||||
bool _loading;
|
bool _loading;
|
||||||
OnlinePodcast _selectedPodcast;
|
|
||||||
Future _searchFuture;
|
Future _searchFuture;
|
||||||
final List<OnlinePodcast> _subscribed = [];
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_searchFuture = _getList(widget.query, _nextOffset);
|
_searchFuture = _getList(widget.query, _nextOffset);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _saveHistory(String query) async {
|
||||||
|
final storage = KeyValueStorage(searchHistoryKey);
|
||||||
|
final history = await storage.getStringList();
|
||||||
|
if (!history.contains(query)) {
|
||||||
|
if (history.length == 10) {
|
||||||
|
history.removeLast();
|
||||||
|
}
|
||||||
|
history.insert(0, query);
|
||||||
|
await storage.saveStringList(history);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<List<OnlinePodcast>> _getList(
|
Future<List<OnlinePodcast>> _getList(
|
||||||
String searchText, int nextOffset) async {
|
String searchText, int nextOffset) async {
|
||||||
var searchEngine = SearchEngine();
|
if (nextOffset == 0) _saveHistory(searchText);
|
||||||
|
final searchEngine = SearchEngine();
|
||||||
var searchResult = await searchEngine.searchPodcasts(
|
var searchResult = await searchEngine.searchPodcasts(
|
||||||
searchText: searchText, nextOffset: nextOffset);
|
searchText: searchText, nextOffset: nextOffset);
|
||||||
_offset = searchResult.nextOffset;
|
_offset = searchResult.nextOffset;
|
||||||
|
@ -382,151 +422,88 @@ class _SearchListState extends State<SearchList> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Stack(
|
return PodcastSlideup(
|
||||||
alignment: Alignment.bottomCenter,
|
child: FutureBuilder<List>(
|
||||||
children: [
|
future: _searchFuture,
|
||||||
FutureBuilder<List>(
|
builder: (context, snapshot) {
|
||||||
future: _searchFuture,
|
if (!snapshot.hasData && widget.query != null) {
|
||||||
builder: (context, snapshot) {
|
return Container(
|
||||||
if (!snapshot.hasData && widget.query != null) {
|
padding: EdgeInsets.only(top: 200),
|
||||||
return Container(
|
alignment: Alignment.topCenter,
|
||||||
padding: EdgeInsets.only(top: 200),
|
child: CircularProgressIndicator(),
|
||||||
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: 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;
|
|
||||||
_nextOffset = _offset;
|
|
||||||
_searchFuture =
|
|
||||||
_getList(widget.query, _nextOffset);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
),
|
var content = snapshot.data;
|
||||||
if (_selectedPodcast != null)
|
return CustomScrollView(
|
||||||
Positioned.fill(
|
slivers: [
|
||||||
child: GestureDetector(
|
SliverList(
|
||||||
onTap: () => setState(() => _selectedPodcast = null),
|
delegate: SliverChildBuilderDelegate(
|
||||||
child: Container(
|
(context, index) {
|
||||||
color: context.scaffoldBackgroundColor.withOpacity(0.9),
|
return SearchResult(onlinePodcast: content[index]);
|
||||||
|
},
|
||||||
|
childCount: content.length,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
SliverToBoxAdapter(
|
||||||
),
|
child: Row(
|
||||||
if (_selectedPodcast != null)
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
LayoutBuilder(
|
mainAxisSize: MainAxisSize.min,
|
||||||
builder: (context, constrants) => SearchResultDetail(
|
children: [
|
||||||
_selectedPodcast,
|
Padding(
|
||||||
maxHeight: constrants.maxHeight,
|
padding: const EdgeInsets.only(top: 10.0, bottom: 20.0),
|
||||||
isSubscribed: _subscribed.contains(_selectedPodcast),
|
child: OutlineButton(
|
||||||
onClose: (option) {
|
highlightedBorderColor: context.accentColor,
|
||||||
setState(() => _selectedPodcast = null);
|
splashColor: context.accentColor.withOpacity(0.5),
|
||||||
},
|
shape: RoundedRectangleBorder(
|
||||||
onSubscribe: (onlinePodcast) {
|
borderRadius:
|
||||||
setState(() {
|
BorderRadius.all(Radius.circular(100))),
|
||||||
_subscribed.add(onlinePodcast);
|
child: _loading
|
||||||
});
|
? SizedBox(
|
||||||
},
|
height: 20,
|
||||||
),
|
width: 20,
|
||||||
),
|
child: CircularProgressIndicator(
|
||||||
],
|
strokeWidth: 2,
|
||||||
|
))
|
||||||
|
: Text(context.s.loadMore),
|
||||||
|
onPressed: () => _loading
|
||||||
|
? null
|
||||||
|
: setState(
|
||||||
|
() {
|
||||||
|
_loading = true;
|
||||||
|
_nextOffset = _offset;
|
||||||
|
_searchFuture =
|
||||||
|
_getList(widget.query, _nextOffset);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SearchResult extends StatelessWidget {
|
class SearchResult extends StatelessWidget {
|
||||||
final OnlinePodcast onlinePodcast;
|
final OnlinePodcast onlinePodcast;
|
||||||
final ValueChanged<OnlinePodcast> onSelect;
|
SearchResult({this.onlinePodcast, Key key}) : super(key: key);
|
||||||
final ValueChanged<OnlinePodcast> onSubscribe;
|
|
||||||
final bool isSubscribed;
|
|
||||||
SearchResult(
|
|
||||||
{this.onlinePodcast,
|
|
||||||
this.onSelect,
|
|
||||||
this.onSubscribe,
|
|
||||||
this.isSubscribed,
|
|
||||||
Key key})
|
|
||||||
: super(key: key);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var subscribeWorker = Provider.of<GroupList>(context, listen: false);
|
var searchState = context.watch<SearchState>();
|
||||||
final s = context.s;
|
return Column(
|
||||||
subscribePodcast(OnlinePodcast podcast) {
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
var item = SubscribeItem(podcast.rss, podcast.title,
|
mainAxisSize: MainAxisSize.min,
|
||||||
imgUrl: podcast.image, group: 'Home');
|
children: <Widget>[
|
||||||
subscribeWorker.setSubscribeItem(item);
|
ListTile(
|
||||||
onSubscribe(podcast);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border(
|
|
||||||
// bottom: Divider.createBorderSide(context),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: <Widget>[
|
|
||||||
ListTile(
|
|
||||||
contentPadding: EdgeInsets.fromLTRB(20, 10, 20, 10),
|
contentPadding: EdgeInsets.fromLTRB(20, 10, 20, 10),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
onSelect(onlinePodcast);
|
searchState.selectedPodcast = onlinePodcast;
|
||||||
|
// onSelect(onlinePodcast);
|
||||||
},
|
},
|
||||||
leading: ClipRRect(
|
leading: ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(25.0),
|
borderRadius: BorderRadius.circular(25.0),
|
||||||
|
@ -559,52 +536,18 @@ class SearchResult extends StatelessWidget {
|
||||||
),
|
),
|
||||||
title: Text(onlinePodcast.title),
|
title: Text(onlinePodcast.title),
|
||||||
subtitle: Text(onlinePodcast.publisher ?? ''),
|
subtitle: Text(onlinePodcast.publisher ?? ''),
|
||||||
trailing: !isSubscribed
|
trailing: SubscribeButton(onlinePodcast)),
|
||||||
? OutlineButton(
|
],
|
||||||
highlightedBorderColor: context.accentColor,
|
|
||||||
borderSide: BorderSide(color: 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: () {}),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Search podcast detail widget
|
||||||
class SearchResultDetail extends StatefulWidget {
|
class SearchResultDetail extends StatefulWidget {
|
||||||
SearchResultDetail(this.onlinePodcast,
|
SearchResultDetail(this.onlinePodcast,
|
||||||
{this.onClose,
|
{this.maxHeight, this.episodeList, this.isSubscribed, Key key})
|
||||||
this.maxHeight,
|
|
||||||
this.onSubscribe,
|
|
||||||
this.episodeList,
|
|
||||||
this.isSubscribed,
|
|
||||||
Key key})
|
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
final OnlinePodcast onlinePodcast;
|
final OnlinePodcast onlinePodcast;
|
||||||
final ValueChanged<bool> onClose;
|
|
||||||
final ValueChanged<OnlinePodcast> onSubscribe;
|
|
||||||
final double maxHeight;
|
final double maxHeight;
|
||||||
final List<OnlineEpisode> episodeList;
|
final List<OnlineEpisode> episodeList;
|
||||||
final bool isSubscribed;
|
final bool isSubscribed;
|
||||||
|
@ -739,22 +682,16 @@ class _SearchResultDetailState extends State<SearchResultDetail>
|
||||||
_initSize = _animation.value > _minHeight - 50 ? _minHeight : 100;
|
_initSize = _animation.value > _minHeight - 50 ? _minHeight : 100;
|
||||||
});
|
});
|
||||||
_controller.forward();
|
_controller.forward();
|
||||||
if (_animation.value < _minHeight - 50) widget.onClose(true);
|
if (_animation.value < _minHeight - 50) {
|
||||||
|
context.read<SearchState>().clearSelect();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var subscribeWorker = Provider.of<GroupList>(context, listen: false);
|
|
||||||
final s = context.s;
|
final s = context.s;
|
||||||
subscribePodcast(OnlinePodcast podcast) {
|
|
||||||
var item = SubscribeItem(podcast.rss, podcast.title,
|
|
||||||
imgUrl: podcast.image, group: 'Home');
|
|
||||||
subscribeWorker.setSubscribeItem(item);
|
|
||||||
widget.onSubscribe(podcast);
|
|
||||||
}
|
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onVerticalDragStart: _start,
|
onVerticalDragStart: _start,
|
||||||
onVerticalDragUpdate: _update,
|
onVerticalDragUpdate: _update,
|
||||||
|
@ -806,45 +743,7 @@ class _SearchResultDetailState extends State<SearchResultDetail>
|
||||||
overflow: TextOverflow.fade,
|
overflow: TextOverflow.fade,
|
||||||
style: TextStyle(color: context.accentColor),
|
style: TextStyle(color: context.accentColor),
|
||||||
),
|
),
|
||||||
!widget.isSubscribed
|
SubscribeButton(widget.onlinePodcast),
|
||||||
? OutlineButton(
|
|
||||||
highlightedBorderColor:
|
|
||||||
context.accentColor,
|
|
||||||
borderSide: BorderSide(
|
|
||||||
color: context.accentColor,
|
|
||||||
width: 2),
|
|
||||||
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);
|
|
||||||
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: () {})
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -1011,3 +910,128 @@ class _SearchResultDetailState extends State<SearchResultDetail>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SubscribeButton extends StatelessWidget {
|
||||||
|
SubscribeButton(this.onlinePodcast, {Key key}) : super(key: key);
|
||||||
|
final OnlinePodcast onlinePodcast;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final subscribeWorker = context.watch<GroupList>();
|
||||||
|
final searchState = context.watch<SearchState>();
|
||||||
|
final s = context.s;
|
||||||
|
subscribePodcast(OnlinePodcast podcast) {
|
||||||
|
var item = SubscribeItem(podcast.rss, podcast.title,
|
||||||
|
imgUrl: podcast.image, group: 'Home');
|
||||||
|
subscribeWorker.setSubscribeItem(item);
|
||||||
|
searchState.addPodcast(podcast);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Consumer<SearchState>(builder: (_, searchState, __) {
|
||||||
|
final subscribed = searchState.isSubscribed(onlinePodcast);
|
||||||
|
return !subscribed
|
||||||
|
? OutlineButton(
|
||||||
|
highlightedBorderColor: context.accentColor,
|
||||||
|
borderSide: BorderSide(color: 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);
|
||||||
|
searchState.addPodcast(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: () {});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PodcastSlideup extends StatelessWidget {
|
||||||
|
const PodcastSlideup({this.child, Key key}) : super(key: key);
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Consumer<SearchState>(builder: (_, searchState, __) {
|
||||||
|
final selectedPodcast = searchState.selectedPodcast;
|
||||||
|
final subscribed = searchState.subscribedList;
|
||||||
|
return Stack(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
children: [
|
||||||
|
child,
|
||||||
|
if (selectedPodcast != null)
|
||||||
|
Positioned.fill(
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: searchState.clearSelect,
|
||||||
|
child: Container(
|
||||||
|
color: context.scaffoldBackgroundColor.withOpacity(0.9),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (selectedPodcast != null)
|
||||||
|
LayoutBuilder(
|
||||||
|
builder: (context, constrants) => SearchResultDetail(
|
||||||
|
selectedPodcast,
|
||||||
|
maxHeight: constrants.maxHeight,
|
||||||
|
isSubscribed: subscribed.contains(selectedPodcast),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PodcastAvatar extends StatelessWidget {
|
||||||
|
const PodcastAvatar(this.podcast, {Key key}) : super(key: key);
|
||||||
|
final OnlinePodcast podcast;
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
height: 50,
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(25.0),
|
||||||
|
child: CachedNetworkImage(
|
||||||
|
height: 50.0,
|
||||||
|
width: 50.0,
|
||||||
|
fit: BoxFit.fitWidth,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
imageUrl: podcast.image,
|
||||||
|
progressIndicatorBuilder: (context, url, downloadProgress) =>
|
||||||
|
Container(
|
||||||
|
height: 50,
|
||||||
|
width: 50,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
color: context.primaryColorDark,
|
||||||
|
child: SizedBox(
|
||||||
|
width: 20,
|
||||||
|
height: 2,
|
||||||
|
child: LinearProgressIndicator(value: downloadProgress.progress),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
errorWidget: (context, url, error) => Container(
|
||||||
|
width: 50,
|
||||||
|
height: 50,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
color: context.primaryColorDark,
|
||||||
|
child: Icon(Icons.error)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -45,6 +45,7 @@ const String hideListenedKey = 'hideListenedKey';
|
||||||
const String notificationLayoutKey = 'notificationLayoutKey';
|
const String notificationLayoutKey = 'notificationLayoutKey';
|
||||||
const String showNotesFontKey = 'showNotesFontKey';
|
const String showNotesFontKey = 'showNotesFontKey';
|
||||||
const String speedListKey = 'speedListKey';
|
const String speedListKey = 'speedListKey';
|
||||||
|
const String searchHistoryKey = 'searchHistoryKey';
|
||||||
|
|
||||||
class KeyValueStorage {
|
class KeyValueStorage {
|
||||||
final String key;
|
final String key;
|
||||||
|
|
|
@ -13,6 +13,7 @@ import 'state/audio_state.dart';
|
||||||
import 'state/download_state.dart';
|
import 'state/download_state.dart';
|
||||||
import 'state/podcast_group.dart';
|
import 'state/podcast_group.dart';
|
||||||
import 'state/refresh_podcast.dart';
|
import 'state/refresh_podcast.dart';
|
||||||
|
import 'state/search_state.dart';
|
||||||
import 'state/setting_state.dart';
|
import 'state/setting_state.dart';
|
||||||
|
|
||||||
final SettingState themeSetting = SettingState();
|
final SettingState themeSetting = SettingState();
|
||||||
|
@ -30,6 +31,7 @@ Future main() async {
|
||||||
ChangeNotifierProvider(create: (_) => AudioPlayerNotifier()),
|
ChangeNotifierProvider(create: (_) => AudioPlayerNotifier()),
|
||||||
ChangeNotifierProvider(create: (_) => GroupList()),
|
ChangeNotifierProvider(create: (_) => GroupList()),
|
||||||
ChangeNotifierProvider(create: (_) => RefreshWorker()),
|
ChangeNotifierProvider(create: (_) => RefreshWorker()),
|
||||||
|
ChangeNotifierProvider(create: (_) => SearchState()),
|
||||||
ChangeNotifierProvider(
|
ChangeNotifierProvider(
|
||||||
lazy: false,
|
lazy: false,
|
||||||
create: (_) => DownloadState(),
|
create: (_) => DownloadState(),
|
||||||
|
|
|
@ -5,11 +5,12 @@ import 'package:dio/dio.dart';
|
||||||
import '../.env.dart';
|
import '../.env.dart';
|
||||||
import '../type/searchepisodes.dart';
|
import '../type/searchepisodes.dart';
|
||||||
import '../type/searchpodcast.dart';
|
import '../type/searchpodcast.dart';
|
||||||
|
import '../type/search_top_podcast.dart';
|
||||||
|
|
||||||
class SearchEngine {
|
class SearchEngine {
|
||||||
|
final apiKey = environment['apiKey'];
|
||||||
Future<SearchPodcast<dynamic>> searchPodcasts(
|
Future<SearchPodcast<dynamic>> searchPodcasts(
|
||||||
{String searchText, int nextOffset}) async {
|
{String searchText, int nextOffset}) async {
|
||||||
var apiKey = environment['apiKey'];
|
|
||||||
var url = "https://listen-api.listennotes.com/api/v2/search?q="
|
var url = "https://listen-api.listennotes.com/api/v2/search?q="
|
||||||
"${Uri.encodeComponent(searchText)}${"&sort_by_date=0&type=podcast&offset=$nextOffset"}";
|
"${Uri.encodeComponent(searchText)}${"&sort_by_date=0&type=podcast&offset=$nextOffset"}";
|
||||||
var response = await Dio().get(url,
|
var response = await Dio().get(url,
|
||||||
|
@ -24,7 +25,6 @@ class SearchEngine {
|
||||||
|
|
||||||
Future<SearchEpisodes<dynamic>> fetchEpisode(
|
Future<SearchEpisodes<dynamic>> fetchEpisode(
|
||||||
{String id, int nextEpisodeDate}) async {
|
{String id, int nextEpisodeDate}) async {
|
||||||
var apiKey = environment['apiKey'];
|
|
||||||
var url =
|
var url =
|
||||||
"https://listen-api.listennotes.com/api/v2/podcasts/$id?next_episode_pub_date=$nextEpisodeDate";
|
"https://listen-api.listennotes.com/api/v2/podcasts/$id?next_episode_pub_date=$nextEpisodeDate";
|
||||||
var response = await Dio().get(url,
|
var response = await Dio().get(url,
|
||||||
|
@ -36,4 +36,18 @@ class SearchEngine {
|
||||||
var searchResult = SearchEpisodes.fromJson(searchResultMap);
|
var searchResult = SearchEpisodes.fromJson(searchResultMap);
|
||||||
return searchResult;
|
return searchResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<SearchTopPodcast<dynamic>> fetchBestPodcast(
|
||||||
|
{String genre, int page, String region = 'us'}) async {
|
||||||
|
var url =
|
||||||
|
"https://listen-api.listennotes.com/api/v2/best_podcasts?genre_id=$genre&page=$page®ion=$region";
|
||||||
|
var response = await Dio().get(url,
|
||||||
|
options: Options(headers: {
|
||||||
|
'X-ListenAPI-Key': "$apiKey",
|
||||||
|
'Accept': "application/json"
|
||||||
|
}));
|
||||||
|
Map searchResultMap = jsonDecode(response.toString());
|
||||||
|
var searchResult = SearchTopPodcast.fromJson(searchResultMap);
|
||||||
|
return searchResult;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import '../type/searchpodcast.dart';
|
||||||
|
|
||||||
|
class SearchState extends ChangeNotifier {
|
||||||
|
final List<OnlinePodcast> _subscribedList = [];
|
||||||
|
bool _update;
|
||||||
|
List<OnlinePodcast> get subscribedList => _subscribedList;
|
||||||
|
bool get update => _update;
|
||||||
|
OnlinePodcast _selectedPodcast;
|
||||||
|
OnlinePodcast get selectedPodcast => _selectedPodcast;
|
||||||
|
set selectedPodcast(OnlinePodcast podcast) {
|
||||||
|
_selectedPodcast = podcast;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isSubscribed(OnlinePodcast podcast) => _subscribedList.contains(podcast);
|
||||||
|
|
||||||
|
void clearSelect() {
|
||||||
|
_selectedPodcast = null;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearList() {
|
||||||
|
_subscribedList.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void addPodcast(OnlinePodcast podcast) {
|
||||||
|
_subscribedList.add(podcast);
|
||||||
|
_update = !_update;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
class Genre {
|
||||||
|
String id;
|
||||||
|
String name;
|
||||||
|
Genre({this.id, this.name});
|
||||||
|
}
|
||||||
|
|
||||||
|
var genres = [
|
||||||
|
Genre(id: '144', name: 'Personal Finance'),
|
||||||
|
Genre(id: '151', name: 'Locally Focused'),
|
||||||
|
Genre(id: '68', name: 'TV & Film'),
|
||||||
|
Genre(id: '127', name: 'Technology'),
|
||||||
|
Genre(id: '135', name: 'True Crime'),
|
||||||
|
Genre(id: '100', name: 'Arts'),
|
||||||
|
Genre(id: '93', name: 'Business'),
|
||||||
|
Genre(id: '67', name: 'Comedy'),
|
||||||
|
Genre(id: '111', name: 'Education'),
|
||||||
|
Genre(id: '168', name: 'Fiction'),
|
||||||
|
Genre(id: '117', name: 'Government'),
|
||||||
|
Genre(id: '88', name: 'Health & Fitness'),
|
||||||
|
Genre(id: '125', name: 'History'),
|
||||||
|
Genre(id: '132', name: 'Kids & Family'),
|
||||||
|
Genre(id: '82', name: 'Leisure'),
|
||||||
|
Genre(id: '134', name: 'Music'),
|
||||||
|
Genre(id: '99', name: 'News'),
|
||||||
|
Genre(id: '69', name: 'Religion & Spirituality'),
|
||||||
|
Genre(id: '107', name: 'Science'),
|
||||||
|
Genre(id: '122', name: 'Society & Culture'),
|
||||||
|
Genre(id: '77', name: 'Sports'),
|
||||||
|
];
|
|
@ -0,0 +1,76 @@
|
||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
import 'searchpodcast.dart';
|
||||||
|
part 'search_top_podcast.g.dart';
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class SearchTopPodcast<T> {
|
||||||
|
@_ConvertT()
|
||||||
|
final List<T> podcasts;
|
||||||
|
final int id;
|
||||||
|
final int page;
|
||||||
|
final int total;
|
||||||
|
@JsonKey(name: 'has_next')
|
||||||
|
final bool hasNext;
|
||||||
|
SearchTopPodcast(
|
||||||
|
{this.podcasts, this.id, this.total, this.hasNext, this.page});
|
||||||
|
|
||||||
|
factory SearchTopPodcast.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SearchTopPodcastFromJson<T>(json);
|
||||||
|
Map<String, dynamic> toJson() => _$SearchTopPodcastToJson(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ConvertT<T> implements JsonConverter<T, Object> {
|
||||||
|
const _ConvertT();
|
||||||
|
@override
|
||||||
|
T fromJson(Object json) {
|
||||||
|
return OnlineTopPodcast.fromJson(json) as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Object toJson(T object) {
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class OnlineTopPodcast {
|
||||||
|
@JsonKey(name: 'earliest_pub_date_ms')
|
||||||
|
final int earliestPubDate;
|
||||||
|
@JsonKey(name: 'title')
|
||||||
|
final String title;
|
||||||
|
final String rss;
|
||||||
|
@JsonKey(name: 'latest_pub_date_ms')
|
||||||
|
final int latestPubDate;
|
||||||
|
@JsonKey(name: 'description')
|
||||||
|
final String description;
|
||||||
|
@JsonKey(name: 'total_episodes')
|
||||||
|
final int count;
|
||||||
|
final String image;
|
||||||
|
@JsonKey(name: 'publisher')
|
||||||
|
final String publisher;
|
||||||
|
final String id;
|
||||||
|
OnlineTopPodcast(
|
||||||
|
{this.earliestPubDate,
|
||||||
|
this.title,
|
||||||
|
this.count,
|
||||||
|
this.description,
|
||||||
|
this.image,
|
||||||
|
this.latestPubDate,
|
||||||
|
this.rss,
|
||||||
|
this.publisher,
|
||||||
|
this.id});
|
||||||
|
factory OnlineTopPodcast.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$OnlineTopPodcastFromJson(json);
|
||||||
|
Map<String, dynamic> toJson() => _$OnlineTopPodcastToJson(this);
|
||||||
|
|
||||||
|
OnlinePodcast get toOnlinePodcast => OnlinePodcast(
|
||||||
|
earliestPubDate: earliestPubDate,
|
||||||
|
title: title,
|
||||||
|
count: count,
|
||||||
|
description: description,
|
||||||
|
image: image,
|
||||||
|
latestPubDate: latestPubDate,
|
||||||
|
rss: rss,
|
||||||
|
publisher: publisher,
|
||||||
|
id: id);
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'search_top_podcast.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
SearchTopPodcast<T> _$SearchTopPodcastFromJson<T>(Map<String, dynamic> json) {
|
||||||
|
return SearchTopPodcast<T>(
|
||||||
|
podcasts:
|
||||||
|
(json['podcasts'] as List)?.map(_ConvertT<T>().fromJson)?.toList(),
|
||||||
|
id: json['id'] as int,
|
||||||
|
total: json['total'] as int,
|
||||||
|
hasNext: json['has_next'] as bool,
|
||||||
|
page: json['page'] as int,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> _$SearchTopPodcastToJson<T>(
|
||||||
|
SearchTopPodcast<T> instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'podcasts': instance.podcasts?.map(_ConvertT<T>().toJson)?.toList(),
|
||||||
|
'id': instance.id,
|
||||||
|
'page': instance.page,
|
||||||
|
'total': instance.total,
|
||||||
|
'has_next': instance.hasNext,
|
||||||
|
};
|
||||||
|
|
||||||
|
OnlineTopPodcast _$OnlineTopPodcastFromJson(Map<String, dynamic> json) {
|
||||||
|
return OnlineTopPodcast(
|
||||||
|
earliestPubDate: json['earliest_pub_date_ms'] as int,
|
||||||
|
title: json['title'] as String,
|
||||||
|
count: json['total_episodes'] as int,
|
||||||
|
description: json['description'] as String,
|
||||||
|
image: json['image'] as String,
|
||||||
|
latestPubDate: json['latest_pub_date_ms'] as int,
|
||||||
|
rss: json['rss'] as String,
|
||||||
|
publisher: json['publisher'] as String,
|
||||||
|
id: json['id'] as String,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> _$OnlineTopPodcastToJson(OnlineTopPodcast instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'earliest_pub_date_ms': instance.earliestPubDate,
|
||||||
|
'title': instance.title,
|
||||||
|
'rss': instance.rss,
|
||||||
|
'latest_pub_date_ms': instance.latestPubDate,
|
||||||
|
'description': instance.description,
|
||||||
|
'total_episodes': instance.count,
|
||||||
|
'image': instance.image,
|
||||||
|
'publisher': instance.publisher,
|
||||||
|
'id': instance.id,
|
||||||
|
};
|
Loading…
Reference in New Issue