Podcastindex search support.
This commit is contained in:
parent
8b57619960
commit
a70103c3eb
Binary file not shown.
After Width: | Height: | Size: 189 KiB |
Binary file not shown.
After Width: | Height: | Size: 22 KiB |
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
|
@ -857,7 +857,7 @@ class SleepModeState extends State<SleepMode>
|
|||
child: Text(
|
||||
context.s.sleepTimer,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).accentColor,
|
||||
color: context.accentColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16),
|
||||
),
|
||||
|
|
|
@ -235,8 +235,7 @@ class _HomeState extends State<Home> with SingleTickerProviderStateMixin {
|
|||
Text(s.featureDiscoveryOMPLDes),
|
||||
FlatButton(
|
||||
color: Colors.cyan[600],
|
||||
padding:
|
||||
const EdgeInsets.all(0),
|
||||
padding: EdgeInsets.zero,
|
||||
child: Text(s.understood,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
|
@ -882,20 +881,21 @@ class _RecentUpdateState extends State<_RecentUpdate>
|
|||
);
|
||||
}),
|
||||
)
|
||||
: Material(
|
||||
color: Colors.transparent,
|
||||
child: IconButton(
|
||||
tooltip: s.addNewEpisodeTooltip,
|
||||
icon: SizedBox(
|
||||
height: 15,
|
||||
width: 20,
|
||||
child: CustomPaint(
|
||||
painter: AddToPlaylistPainter(
|
||||
context.textColor,
|
||||
context.textColor,
|
||||
))),
|
||||
onPressed: () {}),
|
||||
);
|
||||
: Center();
|
||||
// Material(
|
||||
// color: Colors.transparent,
|
||||
// child: IconButton(
|
||||
// tooltip: s.addNewEpisodeTooltip,
|
||||
// icon: SizedBox(
|
||||
// height: 15,
|
||||
// width: 20,
|
||||
// child: CustomPaint(
|
||||
// painter: AddToPlaylistPainter(
|
||||
// context.textColor,
|
||||
// context.textColor,
|
||||
// ))),
|
||||
// onPressed: () {}),
|
||||
// );
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -951,9 +951,7 @@ class _RecentUpdateState extends State<_RecentUpdate>
|
|||
color: Colors.white,
|
||||
backgroundColor: context.accentColor,
|
||||
semanticsLabel: s.refreshStarted,
|
||||
onRefresh: () async {
|
||||
await _updateRssItem();
|
||||
},
|
||||
onRefresh: _updateRssItem,
|
||||
child: CustomScrollView(
|
||||
key: PageStorageKey<String>('update'),
|
||||
physics:
|
||||
|
|
|
@ -112,6 +112,41 @@ class DiscoveryPageState extends State<DiscoveryPage> {
|
|||
],
|
||||
));
|
||||
|
||||
Widget _historyList() => FutureBuilder<List<String>>(
|
||||
future: _getSearchHistory(),
|
||||
initialData: [],
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData && snapshot.data.isNotEmpty) {
|
||||
final history = snapshot.data;
|
||||
return SizedBox(
|
||||
child: Wrap(
|
||||
direction: Axis.horizontal,
|
||||
children: history
|
||||
.map<Widget>((e) => Padding(
|
||||
padding: const EdgeInsets.fromLTRB(8, 2, 0, 0),
|
||||
child: FlatButton.icon(
|
||||
color:
|
||||
Colors.accents[history.indexOf(e)].withAlpha(70),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(100.0),
|
||||
),
|
||||
onPressed: () => widget.onTap(e),
|
||||
label: Text(e),
|
||||
icon: Icon(
|
||||
Icons.search,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
return SizedBox(
|
||||
height: 0,
|
||||
);
|
||||
});
|
||||
|
||||
Future<List<OnlinePodcast>> _getTopPodcasts({int page}) async {
|
||||
final searchEngine = ListenNotesSearch();
|
||||
var searchResult = await searchEngine.fetchBestPodcast(
|
||||
|
@ -124,176 +159,163 @@ class DiscoveryPageState extends State<DiscoveryPage> {
|
|||
return _podcastList;
|
||||
}
|
||||
|
||||
Future<bool> _getHideDiscovery() async {
|
||||
final storage = KeyValueStorage(hidePodcastDiscoveryKey);
|
||||
return await storage.getBool(defaultValue: false);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final searchState = context.watch<SearchState>();
|
||||
return PodcastSlideup(
|
||||
child: _selectedGenre == null
|
||||
? SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
FutureBuilder<List<String>>(
|
||||
future: _getSearchHistory(),
|
||||
initialData: [],
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData && snapshot.data.isNotEmpty) {
|
||||
final history = snapshot.data;
|
||||
return SizedBox(
|
||||
child: Wrap(
|
||||
direction: Axis.horizontal,
|
||||
children: history
|
||||
.map<Widget>((e) => Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
8, 2, 0, 0),
|
||||
child: FlatButton.icon(
|
||||
color: Colors
|
||||
.accents[history.indexOf(e)]
|
||||
.withAlpha(70),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius:
|
||||
BorderRadius.circular(100.0),
|
||||
),
|
||||
onPressed: () => widget.onTap(e),
|
||||
label: Text(e),
|
||||
icon: Icon(
|
||||
Icons.search,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
return SizedBox(
|
||||
height: 0,
|
||||
);
|
||||
}),
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(20, 10, 10, 4),
|
||||
child: Text('Popular',
|
||||
style: context.textTheme.headline6
|
||||
.copyWith(color: context.accentColor)),
|
||||
),
|
||||
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;
|
||||
widget.onTap('');
|
||||
},
|
||||
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(),
|
||||
]),
|
||||
);
|
||||
}),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(20, 10, 10, 4),
|
||||
child: Text('Categories',
|
||||
style: context.textTheme.headline6
|
||||
.copyWith(color: context.accentColor)),
|
||||
),
|
||||
ListView(
|
||||
shrinkWrap: true,
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
children: genres
|
||||
.map<Widget>((e) => ListTile(
|
||||
contentPadding: EdgeInsets.fromLTRB(20, 0, 20, 0),
|
||||
onTap: () {
|
||||
widget.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,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
return FutureBuilder<bool>(
|
||||
future: _getHideDiscovery(),
|
||||
initialData: true,
|
||||
builder: (context, snapshot) => snapshot.data
|
||||
? Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [_historyList(), Spacer()],
|
||||
)
|
||||
: _TopPodcastList(genre: _selectedGenre),
|
||||
: PodcastSlideup(
|
||||
searchEngine: SearchEngine.listenNotes,
|
||||
child: _selectedGenre == null
|
||||
? SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_historyList(),
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(20, 10, 10, 4),
|
||||
child: Text('Popular',
|
||||
style: context.textTheme.headline6
|
||||
.copyWith(color: context.accentColor)),
|
||||
),
|
||||
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;
|
||||
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()
|
||||
: [
|
||||
_loadTopPodcasts(),
|
||||
_loadTopPodcasts(),
|
||||
_loadTopPodcasts(),
|
||||
_loadTopPodcasts(),
|
||||
]),
|
||||
);
|
||||
}),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(20, 10, 10, 4),
|
||||
child: Text('Categories',
|
||||
style: context.textTheme.headline6
|
||||
.copyWith(color: context.accentColor)),
|
||||
),
|
||||
ListView(
|
||||
shrinkWrap: true,
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
children: genres
|
||||
.map<Widget>((e) => ListTile(
|
||||
contentPadding:
|
||||
EdgeInsets.fromLTRB(20, 0, 20, 0),
|
||||
onTap: () {
|
||||
widget.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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import 'package:flutter/services.dart';
|
|||
import 'package:flutter_html/flutter_html.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tsacdop/type/search_api/index_episode.dart';
|
||||
import 'package:webfeed/webfeed.dart';
|
||||
|
||||
import '../local_storage/key_value_storage.dart';
|
||||
|
@ -16,6 +17,7 @@ import '../state/podcast_group.dart';
|
|||
import '../state/search_state.dart';
|
||||
import '../type/search_api/searchepisodes.dart';
|
||||
import '../type/search_api/searchpodcast.dart';
|
||||
import '../util/custom_widget.dart';
|
||||
import '../util/extension_helper.dart';
|
||||
import 'pocast_discovery.dart';
|
||||
|
||||
|
@ -27,12 +29,12 @@ class MyHomePageDelegate extends SearchDelegate<int> {
|
|||
: super(
|
||||
searchFieldLabel: searchFieldLabel,
|
||||
);
|
||||
|
||||
static Future getRss(String url) async {
|
||||
var _searchEngine;
|
||||
static Future _getRss(String url) async {
|
||||
try {
|
||||
var options = BaseOptions(
|
||||
connectTimeout: 10000,
|
||||
receiveTimeout: 10000,
|
||||
final options = BaseOptions(
|
||||
connectTimeout: 30000,
|
||||
receiveTimeout: 90000,
|
||||
);
|
||||
var response = await Dio(options).get(url);
|
||||
return RssFeed.parse(response.data);
|
||||
|
@ -41,7 +43,17 @@ class MyHomePageDelegate extends SearchDelegate<int> {
|
|||
}
|
||||
}
|
||||
|
||||
Future<SearchEngine> _getSearchEngine() async {
|
||||
final storage = KeyValueStorage(searchEngineKey);
|
||||
final index = await storage.getInt(defaultValue: 1);
|
||||
if (_searchEngine == null) {
|
||||
_searchEngine = SearchEngine.values[index];
|
||||
}
|
||||
return _searchEngine;
|
||||
}
|
||||
|
||||
RegExp rssExp = RegExp(r'^(https?):\/\/(.*)');
|
||||
|
||||
Widget invalidRss(BuildContext context) => Container(
|
||||
height: 50,
|
||||
alignment: Alignment.center,
|
||||
|
@ -100,9 +112,7 @@ class MyHomePageDelegate extends SearchDelegate<int> {
|
|||
@override
|
||||
List<Widget> buildActions(BuildContext context) {
|
||||
return <Widget>[
|
||||
if (query.isEmpty)
|
||||
Center()
|
||||
else
|
||||
if (query.isNotEmpty)
|
||||
IconButton(
|
||||
tooltip: context.s.clear,
|
||||
icon: const Icon(Icons.clear),
|
||||
|
@ -111,6 +121,71 @@ class MyHomePageDelegate extends SearchDelegate<int> {
|
|||
showResults(context);
|
||||
},
|
||||
),
|
||||
FutureBuilder<SearchEngine>(
|
||||
future: _getSearchEngine(),
|
||||
initialData: SearchEngine.podcastIndex,
|
||||
builder: (context, snapshot) => PopupMenuButton<SearchEngine>(
|
||||
shape:
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
elevation: 1,
|
||||
icon: snapshot.data == SearchEngine.podcastIndex
|
||||
? SizedBox(
|
||||
height: 30,
|
||||
width: 30,
|
||||
child: CircleAvatar(
|
||||
backgroundImage: AssetImage('assets/podcastindex_logo.png'),
|
||||
backgroundColor: Colors.redAccent[700].withAlpha(70),
|
||||
maxRadius: 25,
|
||||
),
|
||||
)
|
||||
: SizedBox(
|
||||
height: 30,
|
||||
width: 30,
|
||||
child: CircleAvatar(
|
||||
backgroundImage: AssetImage('assets/listennotes_logo.png'),
|
||||
backgroundColor: Colors.red.withAlpha(70),
|
||||
maxRadius: 25,
|
||||
),
|
||||
),
|
||||
onSelected: (value) {
|
||||
_searchEngine = value;
|
||||
showSuggestions(context);
|
||||
if (query != '') {
|
||||
showResults(context);
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
value: SearchEngine.listenNotes,
|
||||
child: Container(
|
||||
padding: EdgeInsets.only(left: 10),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Text('ListenNotes'),
|
||||
Spacer(),
|
||||
if (_searchEngine == SearchEngine.listenNotes)
|
||||
DotIndicator()
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: SearchEngine.podcastIndex,
|
||||
child: Container(
|
||||
padding: EdgeInsets.only(left: 10),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Text('PodcastIndex'),
|
||||
Spacer(),
|
||||
if (_searchEngine == SearchEngine.podcastIndex)
|
||||
DotIndicator()
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -125,7 +200,7 @@ class MyHomePageDelegate extends SearchDelegate<int> {
|
|||
});
|
||||
} else if (rssExp.stringMatch(query) != null) {
|
||||
return FutureBuilder(
|
||||
future: getRss(rssExp.stringMatch(query)),
|
||||
future: _getRss(rssExp.stringMatch(query)),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
return invalidRss(context);
|
||||
|
@ -144,9 +219,16 @@ class MyHomePageDelegate extends SearchDelegate<int> {
|
|||
},
|
||||
);
|
||||
} else {
|
||||
return SearchList(
|
||||
query: query,
|
||||
);
|
||||
switch (_searchEngine) {
|
||||
case SearchEngine.listenNotes:
|
||||
return _ListenNotesSearch(query: query);
|
||||
break;
|
||||
case SearchEngine.podcastIndex:
|
||||
return _PodcastIndexSearch(query: query);
|
||||
default:
|
||||
return Center();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -323,15 +405,15 @@ class _RssResultState extends State<RssResult> {
|
|||
}
|
||||
}
|
||||
|
||||
class SearchList extends StatefulWidget {
|
||||
class _ListenNotesSearch extends StatefulWidget {
|
||||
final String query;
|
||||
SearchList({this.query, Key key}) : super(key: key);
|
||||
_ListenNotesSearch({this.query, Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_SearchListState createState() => _SearchListState();
|
||||
__ListenNotesSearchState createState() => __ListenNotesSearchState();
|
||||
}
|
||||
|
||||
class _SearchListState extends State<SearchList> {
|
||||
class __ListenNotesSearchState extends State<_ListenNotesSearch> {
|
||||
int _nextOffset = 0;
|
||||
final List<OnlinePodcast> _podcastList = [];
|
||||
int _offset;
|
||||
|
@ -341,7 +423,7 @@ class _SearchListState extends State<SearchList> {
|
|||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_searchFuture = _getList(widget.query, _nextOffset);
|
||||
_searchFuture = _getListenNotesList(widget.query, _nextOffset);
|
||||
}
|
||||
|
||||
Future<void> _saveHistory(String query) async {
|
||||
|
@ -356,7 +438,7 @@ class _SearchListState extends State<SearchList> {
|
|||
}
|
||||
}
|
||||
|
||||
Future<List<OnlinePodcast>> _getList(
|
||||
Future<List<OnlinePodcast>> _getListenNotesList(
|
||||
String searchText, int nextOffset) async {
|
||||
if (nextOffset == 0) _saveHistory(searchText);
|
||||
final searchEngine = ListenNotesSearch();
|
||||
|
@ -371,6 +453,7 @@ class _SearchListState extends State<SearchList> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PodcastSlideup(
|
||||
searchEngine: SearchEngine.listenNotes,
|
||||
child: FutureBuilder<List>(
|
||||
future: _searchFuture,
|
||||
builder: (context, snapshot) {
|
||||
|
@ -403,8 +486,7 @@ class _SearchListState extends State<SearchList> {
|
|||
highlightedBorderColor: context.accentColor,
|
||||
splashColor: context.accentColor.withOpacity(0.5),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius:
|
||||
BorderRadius.all(Radius.circular(100))),
|
||||
borderRadius: BorderRadius.circular(100)),
|
||||
child: _loading
|
||||
? SizedBox(
|
||||
height: 20,
|
||||
|
@ -419,8 +501,8 @@ class _SearchListState extends State<SearchList> {
|
|||
() {
|
||||
_loading = true;
|
||||
_nextOffset = _offset;
|
||||
_searchFuture =
|
||||
_getList(widget.query, _nextOffset);
|
||||
_searchFuture = _getListenNotesList(
|
||||
widget.query, _nextOffset);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
@ -436,6 +518,122 @@ class _SearchListState extends State<SearchList> {
|
|||
}
|
||||
}
|
||||
|
||||
class _PodcastIndexSearch extends StatefulWidget {
|
||||
final String query;
|
||||
_PodcastIndexSearch({this.query, Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
__PodcastIndexSearchState createState() => __PodcastIndexSearchState();
|
||||
}
|
||||
|
||||
class __PodcastIndexSearchState extends State<_PodcastIndexSearch> {
|
||||
int _limit;
|
||||
bool _loading;
|
||||
Future _searchFuture;
|
||||
List _podcastList = [];
|
||||
|
||||
Future<void> _saveHistory(String query) async {
|
||||
final storage = KeyValueStorage(searchHistoryKey);
|
||||
final history = await storage.getStringList();
|
||||
if (!history.contains(query)) {
|
||||
if (history.length >= 6) {
|
||||
history.removeLast();
|
||||
}
|
||||
history.insert(0, query);
|
||||
await storage.saveStringList(history);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loading = false;
|
||||
_limit = 10;
|
||||
_searchFuture = _getPodcatsIndexList(widget.query, limit: _limit);
|
||||
}
|
||||
|
||||
Future<List<OnlinePodcast>> _getPodcatsIndexList(String searchText,
|
||||
{int limit}) async {
|
||||
if (_limit == 20) _saveHistory(searchText);
|
||||
final searchEngine = PodcastsIndexSearch();
|
||||
var searchResult =
|
||||
await searchEngine.searchPodcasts(searchText: searchText, limit: limit);
|
||||
var list = searchResult.feeds.cast();
|
||||
_podcastList = <OnlinePodcast>[
|
||||
for (var podcast in list) podcast.toOnlinePodcast
|
||||
];
|
||||
_loading = false;
|
||||
return _podcastList;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PodcastSlideup(
|
||||
searchEngine: SearchEngine.podcastIndex,
|
||||
child: FutureBuilder<List>(
|
||||
future: _searchFuture,
|
||||
builder: (context, 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: OutlineButton(
|
||||
highlightedBorderColor: context.accentColor,
|
||||
splashColor: context.accentColor.withOpacity(0.5),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(100)),
|
||||
child: _loading
|
||||
? SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
))
|
||||
: Text(context.s.loadMore),
|
||||
onPressed: () => _loading
|
||||
? null
|
||||
: setState(
|
||||
() {
|
||||
_loading = true;
|
||||
_limit += 10;
|
||||
_searchFuture = _getPodcatsIndexList(
|
||||
widget.query,
|
||||
limit: _limit);
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SearchResult extends StatelessWidget {
|
||||
final OnlinePodcast onlinePodcast;
|
||||
SearchResult({this.onlinePodcast, Key key}) : super(key: key);
|
||||
|
@ -451,7 +649,6 @@ class SearchResult extends StatelessWidget {
|
|||
contentPadding: EdgeInsets.fromLTRB(20, 10, 20, 10),
|
||||
onTap: () {
|
||||
searchState.selectedPodcast = onlinePodcast;
|
||||
// onSelect(onlinePodcast);
|
||||
},
|
||||
leading: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(25.0),
|
||||
|
@ -483,7 +680,11 @@ class SearchResult extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
title: Text(onlinePodcast.title),
|
||||
subtitle: Text(onlinePodcast.publisher ?? ''),
|
||||
subtitle: Text(
|
||||
onlinePodcast.publisher ?? '',
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
trailing: SubscribeButton(onlinePodcast)),
|
||||
],
|
||||
);
|
||||
|
@ -493,12 +694,12 @@ class SearchResult extends StatelessWidget {
|
|||
/// Search podcast detail widget
|
||||
class SearchResultDetail extends StatefulWidget {
|
||||
SearchResultDetail(this.onlinePodcast,
|
||||
{this.maxHeight, this.episodeList, this.isSubscribed, Key key})
|
||||
{this.maxHeight, this.isSubscribed, this.searchEngine, Key key})
|
||||
: super(key: key);
|
||||
final OnlinePodcast onlinePodcast;
|
||||
final double maxHeight;
|
||||
final List<OnlineEpisode> episodeList;
|
||||
final bool isSubscribed;
|
||||
final SearchEngine searchEngine;
|
||||
@override
|
||||
_SearchResultDetailState createState() => _SearchResultDetailState();
|
||||
}
|
||||
|
@ -539,8 +740,11 @@ class _SearchResultDetailState extends State<SearchResultDetail>
|
|||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_searchFuture = _getEpisodes(
|
||||
id: widget.onlinePodcast.id, nextEpisodeDate: _nextEpisdoeDate);
|
||||
|
||||
_searchFuture = widget.searchEngine == SearchEngine.listenNotes
|
||||
? _getListenNotesEpisodes(
|
||||
id: widget.onlinePodcast.id, nextEpisodeDate: _nextEpisdoeDate)
|
||||
: _getIndexEpisodes(id: widget.onlinePodcast.rss);
|
||||
_minHeight = widget.maxHeight / 2;
|
||||
_initSize = _minHeight;
|
||||
_slideDirection = SlideDirection.up;
|
||||
|
@ -560,7 +764,7 @@ class _SearchResultDetailState extends State<SearchResultDetail>
|
|||
super.dispose();
|
||||
}
|
||||
|
||||
Future<List<OnlineEpisode>> _getEpisodes(
|
||||
Future<List<OnlineEpisode>> _getListenNotesEpisodes(
|
||||
{String id, int nextEpisodeDate}) async {
|
||||
var searchEngine = ListenNotesSearch();
|
||||
var searchResult = await searchEngine.fetchEpisode(
|
||||
|
@ -571,6 +775,18 @@ class _SearchResultDetailState extends State<SearchResultDetail>
|
|||
return _episodeList;
|
||||
}
|
||||
|
||||
Future<List<OnlineEpisode>> _getIndexEpisodes(
|
||||
{String id, int nextEpisodeDate}) async {
|
||||
var searchEngine = PodcastsIndexSearch();
|
||||
var searchResult = await searchEngine.fetchEpisode(rssUrl: id);
|
||||
var episodes = searchResult.items.cast();
|
||||
for (var episode in episodes) {
|
||||
_episodeList.add(episode.toOnlineWEpisode);
|
||||
}
|
||||
_loading = false;
|
||||
return _episodeList;
|
||||
}
|
||||
|
||||
void _start(DragStartDetails event) {
|
||||
setState(() {
|
||||
_startdy = event.localPosition.dy;
|
||||
|
@ -646,7 +862,7 @@ class _SearchResultDetailState extends State<SearchResultDetail>
|
|||
onVerticalDragEnd: (event) => _end(),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).primaryColor,
|
||||
color: context.primaryColor,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
offset: Offset(0, -0.5),
|
||||
|
@ -685,8 +901,12 @@ class _SearchResultDetailState extends State<SearchResultDetail>
|
|||
style: context.textTheme.headline5),
|
||||
),
|
||||
Text(
|
||||
'${widget.onlinePodcast.interval.toInterval(context)} | '
|
||||
'${widget.onlinePodcast.latestPubDate.toDate(context)}',
|
||||
widget.onlinePodcast.interval
|
||||
.toInterval(context) !=
|
||||
''
|
||||
? '${widget.onlinePodcast.interval.toInterval(context)} | '
|
||||
'${widget.onlinePodcast.latestPubDate.toDate(context)}'
|
||||
: '${widget.onlinePodcast.latestPubDate.toDate(context)}',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.fade,
|
||||
style: TextStyle(color: context.accentColor),
|
||||
|
@ -746,16 +966,18 @@ class _SearchResultDetailState extends State<SearchResultDetail>
|
|||
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)))
|
||||
if (widget.onlinePodcast.count > 0)
|
||||
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)))
|
||||
],
|
||||
)
|
||||
]),
|
||||
|
@ -804,41 +1026,51 @@ class _SearchResultDetailState extends State<SearchResultDetail>
|
|||
alignment: Alignment.center,
|
||||
child: SizedBox(
|
||||
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);
|
||||
},
|
||||
),
|
||||
),
|
||||
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: () {
|
||||
if (widget.searchEngine ==
|
||||
SearchEngine.listenNotes) {
|
||||
_loading
|
||||
? null
|
||||
: setState(
|
||||
() {
|
||||
_loading = true;
|
||||
_searchFuture =
|
||||
_getListenNotesEpisodes(
|
||||
id: widget
|
||||
.onlinePodcast
|
||||
.id,
|
||||
nextEpisodeDate:
|
||||
_nextEpisdoeDate);
|
||||
},
|
||||
);
|
||||
}
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
return ListTile(
|
||||
title: Text(content[index].title),
|
||||
subtitle: Text(
|
||||
'${content[index].length.toTime} | '
|
||||
'${content[index].pubDate.toDate(context)}',
|
||||
content[index].length == 0
|
||||
? '${content[index].pubDate.toDate(context)}'
|
||||
: '${content[index].length.toTime} | '
|
||||
'${content[index].pubDate.toDate(context)}',
|
||||
style:
|
||||
TextStyle(color: context.accentColor)),
|
||||
);
|
||||
|
@ -878,40 +1110,48 @@ class SubscribeButton extends StatelessWidget {
|
|||
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: () {
|
||||
Fluttertoast.showToast(
|
||||
msg: s.podcastSubscribed,
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
);
|
||||
subscribePodcast(onlinePodcast);
|
||||
searchState.addPodcast(onlinePodcast);
|
||||
})
|
||||
: 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: () {});
|
||||
? ButtonTheme(
|
||||
height: 32,
|
||||
child: 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: () {
|
||||
Fluttertoast.showToast(
|
||||
msg: s.podcastSubscribed,
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
);
|
||||
subscribePodcast(onlinePodcast);
|
||||
searchState.addPodcast(onlinePodcast);
|
||||
}),
|
||||
)
|
||||
: ButtonTheme(
|
||||
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(s.subscribe),
|
||||
disabledBorderColor: Colors.grey[500],
|
||||
onPressed: () {}),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class PodcastSlideup extends StatelessWidget {
|
||||
const PodcastSlideup({this.child, Key key}) : super(key: key);
|
||||
const PodcastSlideup({this.child, this.searchEngine, Key key})
|
||||
: super(key: key);
|
||||
final Widget child;
|
||||
final SearchEngine searchEngine;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
|
@ -53,6 +53,8 @@ const String gpodderSyncStatusKey = 'gpodderSyncStatusKey';
|
|||
const String gpodderSyncDateTimeKey = 'gpodderSyncDateTimeKey';
|
||||
const String gpodderRemoteAddKey = 'gpodderRemoteAddKey';
|
||||
const String gpodderRemoteRemoveKey = 'gpodderRemoteRemoveKey';
|
||||
const String hidePodcastDiscoveryKey = 'hidePodcastDiscoveryKey';
|
||||
const String searchEngineKey = 'searchEngineKey';
|
||||
|
||||
class KeyValueStorage {
|
||||
final String key;
|
||||
|
|
|
@ -1,21 +1,30 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:convert/convert.dart';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import '../.env.dart';
|
||||
import '../home/about.dart';
|
||||
import '../type/search_api/index_episode.dart';
|
||||
import '../type/search_api/index_podcast.dart';
|
||||
import '../type/search_api/itunes_podcast.dart';
|
||||
import '../type/search_api/search_top_podcast.dart';
|
||||
import '../type/search_api/searchepisodes.dart';
|
||||
import '../type/search_api/searchpodcast.dart';
|
||||
|
||||
enum SearchEngine { listenNotes, podcastIndex }
|
||||
|
||||
class ListenNotesSearch {
|
||||
final apiKey = environment['apiKey'];
|
||||
final _apiKey = environment['apiKey'];
|
||||
|
||||
Future<SearchPodcast<dynamic>> searchPodcasts(
|
||||
{String searchText, int nextOffset}) async {
|
||||
var url = "https://listen-api.listennotes.com/api/v2/search?q="
|
||||
"${Uri.encodeComponent(searchText)}${"&sort_by_date=0&type=podcast&offset=$nextOffset"}";
|
||||
var response = await Dio().get(url,
|
||||
options: Options(headers: {
|
||||
'X-ListenAPI-Key': "$apiKey",
|
||||
'X-ListenAPI-Key': "$_apiKey",
|
||||
'Accept': "application/json"
|
||||
}));
|
||||
Map searchResultMap = jsonDecode(response.toString());
|
||||
|
@ -29,7 +38,7 @@ class ListenNotesSearch {
|
|||
"https://listen-api.listennotes.com/api/v2/podcasts/$id?next_episode_pub_date=$nextEpisodeDate";
|
||||
var response = await Dio().get(url,
|
||||
options: Options(headers: {
|
||||
'X-ListenAPI-Key': "$apiKey",
|
||||
'X-ListenAPI-Key': "$_apiKey",
|
||||
'Accept': "application/json"
|
||||
}));
|
||||
Map searchResultMap = jsonDecode(response.toString());
|
||||
|
@ -43,7 +52,7 @@ class ListenNotesSearch {
|
|||
"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",
|
||||
'X-ListenAPI-Key': "$_apiKey",
|
||||
'Accept': "application/json"
|
||||
}));
|
||||
Map searchResultMap = jsonDecode(response.toString());
|
||||
|
@ -53,14 +62,64 @@ class ListenNotesSearch {
|
|||
}
|
||||
|
||||
class ItunesSearch {
|
||||
Future<SearchPodcast<dynamic>> searchPodcasts(
|
||||
Future<ItunesSearchResult<dynamic>> searchPodcasts(
|
||||
{String searchText, int limit}) async {
|
||||
var url = "https://itunes.apple.com/search/search?q="
|
||||
"${Uri.encodeComponent(searchText)}${"&media=podcast&entity=podcast&limit=$limit"}";
|
||||
var response = await Dio()
|
||||
final url = "https://itunes.apple.com/search?term="
|
||||
"${Uri.encodeComponent(searchText)}&media=podcast&entity=podcast&limit=$limit";
|
||||
final response = await Dio()
|
||||
.get(url, options: Options(headers: {'Accept': "application/json"}));
|
||||
print(response.toString());
|
||||
Map searchResultMap = jsonDecode(response.toString());
|
||||
var searchResult = SearchPodcast.fromJson(searchResultMap);
|
||||
final searchResult = ItunesSearchResult.fromJson(searchResultMap);
|
||||
return searchResult;
|
||||
}
|
||||
}
|
||||
|
||||
class PodcastsIndexSearch {
|
||||
final _dio = Dio(BaseOptions(connectTimeout: 30000, receiveTimeout: 90000));
|
||||
final _baseUrl = 'https://api.podcastindex.org';
|
||||
Map<String, String> _initSearch() {
|
||||
final unixTime =
|
||||
(DateTime.now().millisecondsSinceEpoch / 1000).round().toString();
|
||||
final apiKey = environment['podcastIndexApiKey'];
|
||||
final apiSecret = environment['podcastIndexApiSecret'];
|
||||
final firstChunk = utf8.encode(apiKey);
|
||||
final secondChunk = utf8.encode(apiSecret);
|
||||
final thirdChunk = utf8.encode(unixTime);
|
||||
var output = AccumulatorSink<Digest>();
|
||||
var input = sha1.startChunkedConversion(output);
|
||||
input.add(firstChunk);
|
||||
input.add(secondChunk);
|
||||
input.add(thirdChunk);
|
||||
input.close();
|
||||
var digest = output.events.single;
|
||||
|
||||
var headers = <String, String>{
|
||||
"X-Auth-Date": unixTime,
|
||||
"X-Auth-Key": apiKey,
|
||||
"Authorization": digest.toString(),
|
||||
"User-Agent": "Tsacdop/$version"
|
||||
};
|
||||
return headers;
|
||||
}
|
||||
|
||||
Future<PodcastIndexSearchResult<dynamic>> searchPodcasts(
|
||||
{String searchText, int limit}) async {
|
||||
final url = "$_baseUrl/api/1.0/search/byterm"
|
||||
"?q=${Uri.encodeComponent(searchText)}&max=$limit";
|
||||
final headers = _initSearch();
|
||||
final response = await _dio.get(url, options: Options(headers: headers));
|
||||
Map searchResultMap = jsonDecode(response.toString());
|
||||
final searchResult = PodcastIndexSearchResult.fromJson(searchResultMap);
|
||||
return searchResult;
|
||||
}
|
||||
|
||||
Future<IndexEpisodeResult<dynamic>> fetchEpisode({String rssUrl}) async {
|
||||
final url = "$_baseUrl/api/1.0/episodes/byfeedurl?url=$rssUrl";
|
||||
final headers = _initSearch();
|
||||
final response = await _dio.get(url, options: Options(headers: headers));
|
||||
Map searchResultMap = jsonDecode(response.toString());
|
||||
final searchResult = IndexEpisodeResult.fromJson(searchResultMap);
|
||||
return searchResult;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -188,7 +188,7 @@ class _PlayedHistoryState extends State<PlayedHistory>
|
|||
FutureBuilder<List<PlayHistory>>(
|
||||
future: _getPlayHistory(_top),
|
||||
builder: (context, snapshot) {
|
||||
var width = MediaQuery.of(context).size.width;
|
||||
var width = context.width;
|
||||
return snapshot.hasData
|
||||
? NotificationListener<ScrollNotification>(
|
||||
onNotification: (scrollInfo) {
|
||||
|
|
|
@ -8,6 +8,7 @@ import '../util/custom_dropdown.dart';
|
|||
import '../util/custom_widget.dart';
|
||||
import '../util/episodegrid.dart';
|
||||
import '../util/extension_helper.dart';
|
||||
import '../service/search_api.dart';
|
||||
import 'popup_menu.dart';
|
||||
|
||||
class LayoutSetting extends StatefulWidget {
|
||||
|
@ -18,12 +19,22 @@ class LayoutSetting extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _LayoutSettingState extends State<LayoutSetting> {
|
||||
final _hideDiscoveyStorage = KeyValueStorage(hidePodcastDiscoveryKey);
|
||||
Future<Layout> _getLayout(String key) async {
|
||||
var keyValueStorage = KeyValueStorage(key);
|
||||
var layout = await keyValueStorage.getInt();
|
||||
return Layout.values[layout];
|
||||
}
|
||||
|
||||
Future<bool> _getHideDiscovery() async {
|
||||
return await _hideDiscoveyStorage.getBool(defaultValue: false);
|
||||
}
|
||||
|
||||
Future<void> _saveHideDiscovery(bool boo) async {
|
||||
await _hideDiscoveyStorage.saveBool(boo);
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
Future<bool> _hideListened() async {
|
||||
var hideListenedStorage = KeyValueStorage(hideListenedKey);
|
||||
var hideListened = await hideListenedStorage.getBool(defaultValue: false);
|
||||
|
@ -36,6 +47,18 @@ class _LayoutSettingState extends State<LayoutSetting> {
|
|||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
Future<SearchEngine> _getSearchEngine() async {
|
||||
final storage = KeyValueStorage(searchEngineKey);
|
||||
final index = await storage.getInt(defaultValue: 1);
|
||||
return SearchEngine.values[index];
|
||||
}
|
||||
|
||||
Future<void> _saveSearchEngine(SearchEngine engine) async {
|
||||
final storage = KeyValueStorage(searchEngineKey);
|
||||
await storage.saveInt(engine.index);
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
String _getHeightString(PlayerHeight mode) {
|
||||
final s = context.s;
|
||||
switch (mode) {
|
||||
|
@ -188,9 +211,7 @@ class _LayoutSettingState extends State<LayoutSetting> {
|
|||
padding: const EdgeInsets.symmetric(horizontal: 70),
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(s.settingsPopupMenu,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyText1
|
||||
style: context.textTheme.bodyText1
|
||||
.copyWith(color: context.accentColor)),
|
||||
),
|
||||
ListTile(
|
||||
|
@ -217,8 +238,7 @@ class _LayoutSettingState extends State<LayoutSetting> {
|
|||
.copyWith(color: Theme.of(context).accentColor)),
|
||||
),
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.only(
|
||||
left: 70.0, right: 20, bottom: 10, top: 10),
|
||||
contentPadding: EdgeInsets.fromLTRB(70, 10, 10, 10),
|
||||
title: Text(s.settingsPlayerHeight),
|
||||
subtitle: Text(s.settingsPlayerHeightDes),
|
||||
trailing: Selector<AudioPlayerNotifier, PlayerHeight>(
|
||||
|
@ -242,6 +262,56 @@ class _LayoutSettingState extends State<LayoutSetting> {
|
|||
Padding(
|
||||
padding: EdgeInsets.all(10.0),
|
||||
),
|
||||
Container(
|
||||
height: 30.0,
|
||||
padding: EdgeInsets.symmetric(horizontal: 70),
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text('Podcast search',
|
||||
style: context.textTheme.bodyText1
|
||||
.copyWith(color: context.accentColor)),
|
||||
),
|
||||
FutureBuilder<bool>(
|
||||
future: _getHideDiscovery(),
|
||||
initialData: false,
|
||||
builder: (context, snapshot) => ListTile(
|
||||
contentPadding: EdgeInsets.fromLTRB(70, 10, 10, 10),
|
||||
onTap: () => _saveHideDiscovery(!snapshot.data),
|
||||
title: Text('Hide podcast discovery'),
|
||||
subtitle: Text('Hide podcast discovery in search page'),
|
||||
trailing: Transform.scale(
|
||||
scale: 0.9,
|
||||
child: Switch(
|
||||
value: snapshot.data, onChanged: _saveHideDiscovery),
|
||||
),
|
||||
),
|
||||
),
|
||||
FutureBuilder(
|
||||
future: _getSearchEngine(),
|
||||
initialData: SearchEngine.listenNotes,
|
||||
builder: (context, snapshot) => ListTile(
|
||||
contentPadding: EdgeInsets.fromLTRB(70, 10, 10, 10),
|
||||
title: Text('Default search engine'),
|
||||
subtitle: Text('Choose default search engine'),
|
||||
trailing: MyDropdownButton(
|
||||
hint: Text(''),
|
||||
underline: Center(),
|
||||
elevation: 1,
|
||||
value: snapshot.data,
|
||||
items: [
|
||||
DropdownMenuItem<SearchEngine>(
|
||||
value: SearchEngine.listenNotes,
|
||||
child: Text('ListenNotes')),
|
||||
DropdownMenuItem<SearchEngine>(
|
||||
value: SearchEngine.podcastIndex,
|
||||
child: Text('PodcastIndex')),
|
||||
],
|
||||
onChanged: (value) => _saveSearchEngine(value)),
|
||||
),
|
||||
),
|
||||
Divider(height: 1),
|
||||
Padding(
|
||||
padding: EdgeInsets.all(10.0),
|
||||
),
|
||||
Container(
|
||||
height: 30.0,
|
||||
padding: EdgeInsets.symmetric(horizontal: 70),
|
||||
|
|
|
@ -8,6 +8,7 @@ class SearchState extends ChangeNotifier {
|
|||
bool get update => _update;
|
||||
OnlinePodcast _selectedPodcast;
|
||||
OnlinePodcast get selectedPodcast => _selectedPodcast;
|
||||
|
||||
set selectedPodcast(OnlinePodcast podcast) {
|
||||
_selectedPodcast = podcast;
|
||||
notifyListeners();
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:tsacdop/type/search_api/searchepisodes.dart';
|
||||
|
||||
part 'index_episode.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class IndexEpisodeResult<P> {
|
||||
@_ConvertP()
|
||||
final List<P> items;
|
||||
final String status;
|
||||
final int count;
|
||||
IndexEpisodeResult({this.items, this.status, this.count});
|
||||
factory IndexEpisodeResult.fromJson(Map<String, dynamic> json) =>
|
||||
_$IndexEpisodeResultFromJson<P>(json);
|
||||
Map<String, dynamic> toJson() => _$IndexEpisodeResultToJson(this);
|
||||
}
|
||||
|
||||
class _ConvertP<P> implements JsonConverter<P, Object> {
|
||||
const _ConvertP();
|
||||
@override
|
||||
P fromJson(Object json) {
|
||||
return IndexEpisode.fromJson(json) as P;
|
||||
}
|
||||
|
||||
@override
|
||||
Object toJson(P object) {
|
||||
return object;
|
||||
}
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class IndexEpisode {
|
||||
final String title;
|
||||
final String description;
|
||||
final int datePublished;
|
||||
final String enclosureUrl;
|
||||
final int enclosureLength;
|
||||
IndexEpisode(
|
||||
{this.title,
|
||||
this.description,
|
||||
this.datePublished,
|
||||
this.enclosureLength,
|
||||
this.enclosureUrl});
|
||||
|
||||
factory IndexEpisode.fromJson(Map<String, dynamic> json) =>
|
||||
_$IndexEpisodeFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$IndexEpisodeToJson(this);
|
||||
|
||||
OnlineEpisode get toOnlineWEpisode =>
|
||||
OnlineEpisode(title: title, pubDate: datePublished * 1000, length: 0);
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'index_episode.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
IndexEpisodeResult<P> _$IndexEpisodeResultFromJson<P>(
|
||||
Map<String, dynamic> json) {
|
||||
return IndexEpisodeResult<P>(
|
||||
items: (json['items'] as List)?.map(_ConvertP<P>().fromJson)?.toList(),
|
||||
status: json['status'] as String,
|
||||
count: json['count'] as int,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$IndexEpisodeResultToJson<P>(
|
||||
IndexEpisodeResult<P> instance) =>
|
||||
<String, dynamic>{
|
||||
'items': instance.items?.map(_ConvertP<P>().toJson)?.toList(),
|
||||
'status': instance.status,
|
||||
'count': instance.count,
|
||||
};
|
||||
|
||||
IndexEpisode _$IndexEpisodeFromJson(Map<String, dynamic> json) {
|
||||
return IndexEpisode(
|
||||
title: json['title'] as String,
|
||||
description: json['description'] as String,
|
||||
datePublished: json['datePublished'] as int,
|
||||
enclosureLength: json['enclosureLength'] as int,
|
||||
enclosureUrl: json['enclosureUrl'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$IndexEpisodeToJson(IndexEpisode instance) =>
|
||||
<String, dynamic>{
|
||||
'title': instance.title,
|
||||
'description': instance.description,
|
||||
'datePublished': instance.datePublished,
|
||||
'enclosureUrl': instance.enclosureUrl,
|
||||
'enclosureLength': instance.enclosureLength,
|
||||
};
|
|
@ -0,0 +1,68 @@
|
|||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
import 'searchpodcast.dart';
|
||||
|
||||
part 'index_podcast.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class PodcastIndexSearchResult<P> {
|
||||
@_ConvertP()
|
||||
final List<P> feeds;
|
||||
final String status;
|
||||
final int count;
|
||||
PodcastIndexSearchResult({this.feeds, this.status, this.count});
|
||||
|
||||
factory PodcastIndexSearchResult.fromJson(Map<String, dynamic> json) =>
|
||||
_$PodcastIndexSearchResultFromJson<P>(json);
|
||||
Map<String, dynamic> toJson() => _$PodcastIndexSearchResultToJson(this);
|
||||
}
|
||||
|
||||
class _ConvertP<P> implements JsonConverter<P, Object> {
|
||||
const _ConvertP();
|
||||
@override
|
||||
P fromJson(Object json) {
|
||||
return IndexPodcast.fromJson(json) as P;
|
||||
}
|
||||
|
||||
@override
|
||||
Object toJson(P object) {
|
||||
return object;
|
||||
}
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class IndexPodcast {
|
||||
final int id;
|
||||
final String title;
|
||||
final String url;
|
||||
final String link;
|
||||
final String description;
|
||||
final String author;
|
||||
final String image;
|
||||
final int lastUpdateTime;
|
||||
final int itunesId;
|
||||
IndexPodcast(
|
||||
{this.id,
|
||||
this.title,
|
||||
this.url,
|
||||
this.link,
|
||||
this.description,
|
||||
this.author,
|
||||
this.image,
|
||||
this.lastUpdateTime,
|
||||
this.itunesId});
|
||||
factory IndexPodcast.fromJson(Map<String, dynamic> json) =>
|
||||
_$IndexPodcastFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$IndexPodcastToJson(this);
|
||||
|
||||
OnlinePodcast get toOnlinePodcast => OnlinePodcast(
|
||||
earliestPubDate: 0,
|
||||
title: title,
|
||||
count: 0,
|
||||
description: description,
|
||||
image: image,
|
||||
latestPubDate: lastUpdateTime * 1000,
|
||||
rss: url,
|
||||
publisher: author,
|
||||
id: itunesId.toString());
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'index_podcast.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
PodcastIndexSearchResult<P> _$PodcastIndexSearchResultFromJson<P>(
|
||||
Map<String, dynamic> json) {
|
||||
return PodcastIndexSearchResult<P>(
|
||||
feeds: (json['feeds'] as List)?.map(_ConvertP<P>().fromJson)?.toList(),
|
||||
status: json['status'] as String,
|
||||
count: json['count'] as int,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$PodcastIndexSearchResultToJson<P>(
|
||||
PodcastIndexSearchResult<P> instance) =>
|
||||
<String, dynamic>{
|
||||
'feeds': instance.feeds?.map(_ConvertP<P>().toJson)?.toList(),
|
||||
'status': instance.status,
|
||||
'count': instance.count,
|
||||
};
|
||||
|
||||
IndexPodcast _$IndexPodcastFromJson(Map<String, dynamic> json) {
|
||||
return IndexPodcast(
|
||||
id: json['id'] as int,
|
||||
title: json['title'] as String,
|
||||
url: json['url'] as String,
|
||||
link: json['link'] as String,
|
||||
description: json['description'] as String,
|
||||
author: json['author'] as String,
|
||||
image: json['image'] as String,
|
||||
lastUpdateTime: json['lastUpdateTime'] as int,
|
||||
itunesId: json['itunesId'] as int,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$IndexPodcastToJson(IndexPodcast instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'title': instance.title,
|
||||
'url': instance.url,
|
||||
'link': instance.link,
|
||||
'description': instance.description,
|
||||
'author': instance.author,
|
||||
'image': instance.image,
|
||||
'lastUpdateTime': instance.lastUpdateTime,
|
||||
'itunesId': instance.itunesId,
|
||||
};
|
|
@ -51,7 +51,7 @@ class ItunesPodcast {
|
|||
_$ItunesPodcastFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$ItunesPodcastToJson(this);
|
||||
|
||||
int get latestPubDate => DateFormat('YYYY-MM-DDTHH:MM:SSZ', 'en_US')
|
||||
int get latestPubDate => DateFormat('yyyy-MM-DDTHH:MM:SSZ', 'en_US')
|
||||
.parse(releaseDate)
|
||||
.millisecondsSinceEpoch;
|
||||
OnlinePodcast get toOnlinePodcast => OnlinePodcast(
|
||||
|
|
|
@ -28,6 +28,7 @@ ItunesPodcast _$ItunesPodcastFromJson(Map<String, dynamic> json) {
|
|||
feedUrl: json['feedUrl'] as String,
|
||||
artworkUrl600: json['artworkUrl600'] as String,
|
||||
releaseDate: json['releaseDate'] as String,
|
||||
collectionId: json['collectionId'] as int,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -38,4 +39,5 @@ Map<String, dynamic> _$ItunesPodcastToJson(ItunesPodcast instance) =>
|
|||
'feedUrl': instance.feedUrl,
|
||||
'artworkUrl600': instance.artworkUrl600,
|
||||
'releaseDate': instance.releaseDate,
|
||||
'collectionId': instance.collectionId,
|
||||
};
|
||||
|
|
|
@ -19,6 +19,8 @@ dependencies:
|
|||
cookie_jar: ^1.0.1
|
||||
cupertino_icons: ^1.0.0
|
||||
connectivity: ^0.4.9
|
||||
convert: ^2.1.1
|
||||
crypto: ^2.1.5
|
||||
device_info: ^0.4.2+7
|
||||
dio: ^3.0.10
|
||||
dio_cookie_manager: ^1.0.0
|
||||
|
|
Loading…
Reference in New Issue