Podcastindex search support.

This commit is contained in:
stonegate 2020-09-30 01:25:44 +08:00
parent 8b57619960
commit a70103c3eb
19 changed files with 910 additions and 301 deletions

BIN
assets/listennotes_logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB

BIN
assets/podcastindex.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -857,7 +857,7 @@ class SleepModeState extends State<SleepMode>
child: Text( child: Text(
context.s.sleepTimer, context.s.sleepTimer,
style: TextStyle( style: TextStyle(
color: Theme.of(context).accentColor, color: context.accentColor,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 16), fontSize: 16),
), ),

View File

@ -235,8 +235,7 @@ class _HomeState extends State<Home> with SingleTickerProviderStateMixin {
Text(s.featureDiscoveryOMPLDes), Text(s.featureDiscoveryOMPLDes),
FlatButton( FlatButton(
color: Colors.cyan[600], color: Colors.cyan[600],
padding: padding: EdgeInsets.zero,
const EdgeInsets.all(0),
child: Text(s.understood, child: Text(s.understood,
style: Theme.of(context) style: Theme.of(context)
.textTheme .textTheme
@ -882,20 +881,21 @@ class _RecentUpdateState extends State<_RecentUpdate>
); );
}), }),
) )
: Material( : Center();
color: Colors.transparent, // Material(
child: IconButton( // color: Colors.transparent,
tooltip: s.addNewEpisodeTooltip, // child: IconButton(
icon: SizedBox( // tooltip: s.addNewEpisodeTooltip,
height: 15, // icon: SizedBox(
width: 20, // height: 15,
child: CustomPaint( // width: 20,
painter: AddToPlaylistPainter( // child: CustomPaint(
context.textColor, // painter: AddToPlaylistPainter(
context.textColor, // context.textColor,
))), // context.textColor,
onPressed: () {}), // ))),
); // onPressed: () {}),
// );
}); });
} }
@ -951,9 +951,7 @@ class _RecentUpdateState extends State<_RecentUpdate>
color: Colors.white, color: Colors.white,
backgroundColor: context.accentColor, backgroundColor: context.accentColor,
semanticsLabel: s.refreshStarted, semanticsLabel: s.refreshStarted,
onRefresh: () async { onRefresh: _updateRssItem,
await _updateRssItem();
},
child: CustomScrollView( child: CustomScrollView(
key: PageStorageKey<String>('update'), key: PageStorageKey<String>('update'),
physics: physics:

View File

@ -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 { Future<List<OnlinePodcast>> _getTopPodcasts({int page}) async {
final searchEngine = ListenNotesSearch(); final searchEngine = ListenNotesSearch();
var searchResult = await searchEngine.fetchBestPodcast( var searchResult = await searchEngine.fetchBestPodcast(
@ -124,176 +159,163 @@ class DiscoveryPageState extends State<DiscoveryPage> {
return _podcastList; return _podcastList;
} }
Future<bool> _getHideDiscovery() async {
final storage = KeyValueStorage(hidePodcastDiscoveryKey);
return await storage.getBool(defaultValue: false);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final searchState = context.watch<SearchState>(); final searchState = context.watch<SearchState>();
return PodcastSlideup( return FutureBuilder<bool>(
child: _selectedGenre == null future: _getHideDiscovery(),
? SingleChildScrollView( initialData: true,
child: Column( builder: (context, snapshot) => snapshot.data
mainAxisAlignment: MainAxisAlignment.start, ? Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [_historyList(), Spacer()],
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,
),
),
)
],
),
) )
: _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),
),
); );
} }
} }

View File

@ -8,6 +8,7 @@ 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:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:tsacdop/type/search_api/index_episode.dart';
import 'package:webfeed/webfeed.dart'; import 'package:webfeed/webfeed.dart';
import '../local_storage/key_value_storage.dart'; import '../local_storage/key_value_storage.dart';
@ -16,6 +17,7 @@ import '../state/podcast_group.dart';
import '../state/search_state.dart'; import '../state/search_state.dart';
import '../type/search_api/searchepisodes.dart'; import '../type/search_api/searchepisodes.dart';
import '../type/search_api/searchpodcast.dart'; import '../type/search_api/searchpodcast.dart';
import '../util/custom_widget.dart';
import '../util/extension_helper.dart'; import '../util/extension_helper.dart';
import 'pocast_discovery.dart'; import 'pocast_discovery.dart';
@ -27,12 +29,12 @@ class MyHomePageDelegate extends SearchDelegate<int> {
: super( : super(
searchFieldLabel: searchFieldLabel, searchFieldLabel: searchFieldLabel,
); );
var _searchEngine;
static Future getRss(String url) async { static Future _getRss(String url) async {
try { try {
var options = BaseOptions( final options = BaseOptions(
connectTimeout: 10000, connectTimeout: 30000,
receiveTimeout: 10000, receiveTimeout: 90000,
); );
var response = await Dio(options).get(url); var response = await Dio(options).get(url);
return RssFeed.parse(response.data); 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?):\/\/(.*)'); RegExp rssExp = RegExp(r'^(https?):\/\/(.*)');
Widget invalidRss(BuildContext context) => Container( Widget invalidRss(BuildContext context) => Container(
height: 50, height: 50,
alignment: Alignment.center, alignment: Alignment.center,
@ -100,9 +112,7 @@ class MyHomePageDelegate extends SearchDelegate<int> {
@override @override
List<Widget> buildActions(BuildContext context) { List<Widget> buildActions(BuildContext context) {
return <Widget>[ return <Widget>[
if (query.isEmpty) if (query.isNotEmpty)
Center()
else
IconButton( IconButton(
tooltip: context.s.clear, tooltip: context.s.clear,
icon: const Icon(Icons.clear), icon: const Icon(Icons.clear),
@ -111,6 +121,71 @@ class MyHomePageDelegate extends SearchDelegate<int> {
showResults(context); 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) { } else if (rssExp.stringMatch(query) != null) {
return FutureBuilder( return FutureBuilder(
future: getRss(rssExp.stringMatch(query)), future: _getRss(rssExp.stringMatch(query)),
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.hasError) { if (snapshot.hasError) {
return invalidRss(context); return invalidRss(context);
@ -144,9 +219,16 @@ class MyHomePageDelegate extends SearchDelegate<int> {
}, },
); );
} else { } else {
return SearchList( switch (_searchEngine) {
query: query, 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; final String query;
SearchList({this.query, Key key}) : super(key: key); _ListenNotesSearch({this.query, Key key}) : super(key: key);
@override @override
_SearchListState createState() => _SearchListState(); __ListenNotesSearchState createState() => __ListenNotesSearchState();
} }
class _SearchListState extends State<SearchList> { class __ListenNotesSearchState extends State<_ListenNotesSearch> {
int _nextOffset = 0; int _nextOffset = 0;
final List<OnlinePodcast> _podcastList = []; final List<OnlinePodcast> _podcastList = [];
int _offset; int _offset;
@ -341,7 +423,7 @@ class _SearchListState extends State<SearchList> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_searchFuture = _getList(widget.query, _nextOffset); _searchFuture = _getListenNotesList(widget.query, _nextOffset);
} }
Future<void> _saveHistory(String query) async { 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 { String searchText, int nextOffset) async {
if (nextOffset == 0) _saveHistory(searchText); if (nextOffset == 0) _saveHistory(searchText);
final searchEngine = ListenNotesSearch(); final searchEngine = ListenNotesSearch();
@ -371,6 +453,7 @@ class _SearchListState extends State<SearchList> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return PodcastSlideup( return PodcastSlideup(
searchEngine: SearchEngine.listenNotes,
child: FutureBuilder<List>( child: FutureBuilder<List>(
future: _searchFuture, future: _searchFuture,
builder: (context, snapshot) { builder: (context, snapshot) {
@ -403,8 +486,7 @@ class _SearchListState extends State<SearchList> {
highlightedBorderColor: context.accentColor, highlightedBorderColor: context.accentColor,
splashColor: context.accentColor.withOpacity(0.5), splashColor: context.accentColor.withOpacity(0.5),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: borderRadius: BorderRadius.circular(100)),
BorderRadius.all(Radius.circular(100))),
child: _loading child: _loading
? SizedBox( ? SizedBox(
height: 20, height: 20,
@ -419,8 +501,8 @@ class _SearchListState extends State<SearchList> {
() { () {
_loading = true; _loading = true;
_nextOffset = _offset; _nextOffset = _offset;
_searchFuture = _searchFuture = _getListenNotesList(
_getList(widget.query, _nextOffset); 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 { class SearchResult extends StatelessWidget {
final OnlinePodcast onlinePodcast; final OnlinePodcast onlinePodcast;
SearchResult({this.onlinePodcast, Key key}) : super(key: key); SearchResult({this.onlinePodcast, Key key}) : super(key: key);
@ -451,7 +649,6 @@ class SearchResult extends StatelessWidget {
contentPadding: EdgeInsets.fromLTRB(20, 10, 20, 10), contentPadding: EdgeInsets.fromLTRB(20, 10, 20, 10),
onTap: () { onTap: () {
searchState.selectedPodcast = onlinePodcast; searchState.selectedPodcast = onlinePodcast;
// onSelect(onlinePodcast);
}, },
leading: ClipRRect( leading: ClipRRect(
borderRadius: BorderRadius.circular(25.0), borderRadius: BorderRadius.circular(25.0),
@ -483,7 +680,11 @@ class SearchResult extends StatelessWidget {
), ),
), ),
title: Text(onlinePodcast.title), title: Text(onlinePodcast.title),
subtitle: Text(onlinePodcast.publisher ?? ''), subtitle: Text(
onlinePodcast.publisher ?? '',
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
trailing: SubscribeButton(onlinePodcast)), trailing: SubscribeButton(onlinePodcast)),
], ],
); );
@ -493,12 +694,12 @@ class SearchResult extends StatelessWidget {
/// Search podcast detail widget /// Search podcast detail widget
class SearchResultDetail extends StatefulWidget { class SearchResultDetail extends StatefulWidget {
SearchResultDetail(this.onlinePodcast, SearchResultDetail(this.onlinePodcast,
{this.maxHeight, this.episodeList, this.isSubscribed, Key key}) {this.maxHeight, this.isSubscribed, this.searchEngine, Key key})
: super(key: key); : super(key: key);
final OnlinePodcast onlinePodcast; final OnlinePodcast onlinePodcast;
final double maxHeight; final double maxHeight;
final List<OnlineEpisode> episodeList;
final bool isSubscribed; final bool isSubscribed;
final SearchEngine searchEngine;
@override @override
_SearchResultDetailState createState() => _SearchResultDetailState(); _SearchResultDetailState createState() => _SearchResultDetailState();
} }
@ -539,8 +740,11 @@ class _SearchResultDetailState extends State<SearchResultDetail>
@override @override
void initState() { void initState() {
super.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; _minHeight = widget.maxHeight / 2;
_initSize = _minHeight; _initSize = _minHeight;
_slideDirection = SlideDirection.up; _slideDirection = SlideDirection.up;
@ -560,7 +764,7 @@ class _SearchResultDetailState extends State<SearchResultDetail>
super.dispose(); super.dispose();
} }
Future<List<OnlineEpisode>> _getEpisodes( Future<List<OnlineEpisode>> _getListenNotesEpisodes(
{String id, int nextEpisodeDate}) async { {String id, int nextEpisodeDate}) async {
var searchEngine = ListenNotesSearch(); var searchEngine = ListenNotesSearch();
var searchResult = await searchEngine.fetchEpisode( var searchResult = await searchEngine.fetchEpisode(
@ -571,6 +775,18 @@ class _SearchResultDetailState extends State<SearchResultDetail>
return _episodeList; 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) { void _start(DragStartDetails event) {
setState(() { setState(() {
_startdy = event.localPosition.dy; _startdy = event.localPosition.dy;
@ -646,7 +862,7 @@ class _SearchResultDetailState extends State<SearchResultDetail>
onVerticalDragEnd: (event) => _end(), onVerticalDragEnd: (event) => _end(),
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).primaryColor, color: context.primaryColor,
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
offset: Offset(0, -0.5), offset: Offset(0, -0.5),
@ -685,8 +901,12 @@ class _SearchResultDetailState extends State<SearchResultDetail>
style: context.textTheme.headline5), style: context.textTheme.headline5),
), ),
Text( Text(
'${widget.onlinePodcast.interval.toInterval(context)} | ' widget.onlinePodcast.interval
'${widget.onlinePodcast.latestPubDate.toDate(context)}', .toInterval(context) !=
''
? '${widget.onlinePodcast.interval.toInterval(context)} | '
'${widget.onlinePodcast.latestPubDate.toDate(context)}'
: '${widget.onlinePodcast.latestPubDate.toDate(context)}',
maxLines: 1, maxLines: 1,
overflow: TextOverflow.fade, overflow: TextOverflow.fade,
style: TextStyle(color: context.accentColor), style: TextStyle(color: context.accentColor),
@ -746,16 +966,18 @@ class _SearchResultDetailState extends State<SearchResultDetail>
children: [ children: [
Text(s.episode(2)), Text(s.episode(2)),
SizedBox(width: 2), SizedBox(width: 2),
Container( if (widget.onlinePodcast.count > 0)
padding: const EdgeInsets.only( Container(
left: 5, right: 5, top: 2, bottom: 2), padding: const EdgeInsets.only(
decoration: BoxDecoration( left: 5, right: 5, top: 2, bottom: 2),
color: context.accentColor, decoration: BoxDecoration(
borderRadius: color: context.accentColor,
BorderRadius.circular(100)), borderRadius:
child: Text( BorderRadius.circular(100)),
widget.onlinePodcast.count.toString(), child: Text(
style: TextStyle(color: Colors.white))) widget.onlinePodcast.count.toString(),
style:
TextStyle(color: Colors.white)))
], ],
) )
]), ]),
@ -804,41 +1026,51 @@ class _SearchResultDetailState extends State<SearchResultDetail>
alignment: Alignment.center, alignment: Alignment.center,
child: SizedBox( child: SizedBox(
child: OutlineButton( child: OutlineButton(
highlightedBorderColor: highlightedBorderColor:
context.accentColor, context.accentColor,
splashColor: splashColor: context.accentColor
context.accentColor.withOpacity(0.5), .withOpacity(0.5),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all( borderRadius: BorderRadius.all(
Radius.circular(100))), Radius.circular(100))),
child: _loading child: _loading
? SizedBox( ? SizedBox(
height: 20, height: 20,
width: 20, width: 20,
child: CircularProgressIndicator( child:
strokeWidth: 2, CircularProgressIndicator(
)) strokeWidth: 2,
: Text(context.s.loadMore), ))
onPressed: () => _loading : Text(context.s.loadMore),
? null onPressed: () {
: setState( if (widget.searchEngine ==
() { SearchEngine.listenNotes) {
_loading = true; _loading
_searchFuture = _getEpisodes( ? null
id: widget.onlinePodcast.id, : setState(
nextEpisodeDate: () {
_nextEpisdoeDate); _loading = true;
}, _searchFuture =
), _getListenNotesEpisodes(
), id: widget
.onlinePodcast
.id,
nextEpisodeDate:
_nextEpisdoeDate);
},
);
}
}),
), ),
); );
} }
return ListTile( return ListTile(
title: Text(content[index].title), title: Text(content[index].title),
subtitle: Text( subtitle: Text(
'${content[index].length.toTime} | ' content[index].length == 0
'${content[index].pubDate.toDate(context)}', ? '${content[index].pubDate.toDate(context)}'
: '${content[index].length.toTime} | '
'${content[index].pubDate.toDate(context)}',
style: style:
TextStyle(color: context.accentColor)), TextStyle(color: context.accentColor)),
); );
@ -878,40 +1110,48 @@ class SubscribeButton extends StatelessWidget {
return Consumer<SearchState>(builder: (_, searchState, __) { return Consumer<SearchState>(builder: (_, searchState, __) {
final subscribed = searchState.isSubscribed(onlinePodcast); final subscribed = searchState.isSubscribed(onlinePodcast);
return !subscribed return !subscribed
? OutlineButton( ? ButtonTheme(
highlightedBorderColor: context.accentColor, height: 32,
borderSide: BorderSide(color: context.accentColor), child: OutlineButton(
shape: RoundedRectangleBorder( highlightedBorderColor: context.accentColor,
borderRadius: BorderRadius.circular(100.0), borderSide: BorderSide(color: context.accentColor),
side: BorderSide(color: context.accentColor)), shape: RoundedRectangleBorder(
splashColor: context.accentColor.withOpacity(0.5), borderRadius: BorderRadius.circular(100.0),
child: Text(s.subscribe, side: BorderSide(color: context.accentColor)),
style: TextStyle(color: context.accentColor)), splashColor: context.accentColor.withOpacity(0.5),
onPressed: () { child: Text(s.subscribe,
Fluttertoast.showToast( style: TextStyle(color: context.accentColor)),
msg: s.podcastSubscribed, onPressed: () {
gravity: ToastGravity.BOTTOM, Fluttertoast.showToast(
); msg: s.podcastSubscribed,
subscribePodcast(onlinePodcast); gravity: ToastGravity.BOTTOM,
searchState.addPodcast(onlinePodcast); );
}) subscribePodcast(onlinePodcast);
: OutlineButton( searchState.addPodcast(onlinePodcast);
color: context.accentColor.withOpacity(0.5), }),
shape: RoundedRectangleBorder( )
borderRadius: BorderRadius.circular(100.0), : ButtonTheme(
side: BorderSide(color: Colors.grey[500])), height: 32,
highlightedBorderColor: Colors.grey[500], child: OutlineButton(
disabledTextColor: Colors.grey[500], color: context.accentColor.withOpacity(0.5),
child: Text(s.subscribe), shape: RoundedRectangleBorder(
disabledBorderColor: Colors.grey[500], borderRadius: BorderRadius.circular(100.0),
onPressed: () {}); 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 { 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 Widget child;
final SearchEngine searchEngine;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@ -53,6 +53,8 @@ const String gpodderSyncStatusKey = 'gpodderSyncStatusKey';
const String gpodderSyncDateTimeKey = 'gpodderSyncDateTimeKey'; const String gpodderSyncDateTimeKey = 'gpodderSyncDateTimeKey';
const String gpodderRemoteAddKey = 'gpodderRemoteAddKey'; const String gpodderRemoteAddKey = 'gpodderRemoteAddKey';
const String gpodderRemoteRemoveKey = 'gpodderRemoteRemoveKey'; const String gpodderRemoteRemoveKey = 'gpodderRemoteRemoveKey';
const String hidePodcastDiscoveryKey = 'hidePodcastDiscoveryKey';
const String searchEngineKey = 'searchEngineKey';
class KeyValueStorage { class KeyValueStorage {
final String key; final String key;

View File

@ -1,21 +1,30 @@
import 'dart:convert'; import 'dart:convert';
import 'package:convert/convert.dart';
import 'package:crypto/crypto.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import '../.env.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/search_top_podcast.dart';
import '../type/search_api/searchepisodes.dart'; import '../type/search_api/searchepisodes.dart';
import '../type/search_api/searchpodcast.dart'; import '../type/search_api/searchpodcast.dart';
enum SearchEngine { listenNotes, podcastIndex }
class ListenNotesSearch { class ListenNotesSearch {
final apiKey = environment['apiKey']; final _apiKey = environment['apiKey'];
Future<SearchPodcast<dynamic>> searchPodcasts( Future<SearchPodcast<dynamic>> searchPodcasts(
{String searchText, int nextOffset}) async { {String searchText, int nextOffset}) async {
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,
options: Options(headers: { options: Options(headers: {
'X-ListenAPI-Key': "$apiKey", 'X-ListenAPI-Key': "$_apiKey",
'Accept': "application/json" 'Accept': "application/json"
})); }));
Map searchResultMap = jsonDecode(response.toString()); 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"; "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,
options: Options(headers: { options: Options(headers: {
'X-ListenAPI-Key': "$apiKey", 'X-ListenAPI-Key': "$_apiKey",
'Accept': "application/json" 'Accept': "application/json"
})); }));
Map searchResultMap = jsonDecode(response.toString()); 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&region=$region"; "https://listen-api.listennotes.com/api/v2/best_podcasts?genre_id=$genre&page=$page&region=$region";
var response = await Dio().get(url, var response = await Dio().get(url,
options: Options(headers: { options: Options(headers: {
'X-ListenAPI-Key': "$apiKey", 'X-ListenAPI-Key': "$_apiKey",
'Accept': "application/json" 'Accept': "application/json"
})); }));
Map searchResultMap = jsonDecode(response.toString()); Map searchResultMap = jsonDecode(response.toString());
@ -53,14 +62,64 @@ class ListenNotesSearch {
} }
class ItunesSearch { class ItunesSearch {
Future<SearchPodcast<dynamic>> searchPodcasts( Future<ItunesSearchResult<dynamic>> searchPodcasts(
{String searchText, int limit}) async { {String searchText, int limit}) async {
var url = "https://itunes.apple.com/search/search?q=" final url = "https://itunes.apple.com/search?term="
"${Uri.encodeComponent(searchText)}${"&media=podcast&entity=podcast&limit=$limit"}"; "${Uri.encodeComponent(searchText)}&media=podcast&entity=podcast&limit=$limit";
var response = await Dio() final response = await Dio()
.get(url, options: Options(headers: {'Accept': "application/json"})); .get(url, options: Options(headers: {'Accept': "application/json"}));
print(response.toString());
Map searchResultMap = jsonDecode(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; return searchResult;
} }
} }

View File

@ -188,7 +188,7 @@ class _PlayedHistoryState extends State<PlayedHistory>
FutureBuilder<List<PlayHistory>>( FutureBuilder<List<PlayHistory>>(
future: _getPlayHistory(_top), future: _getPlayHistory(_top),
builder: (context, snapshot) { builder: (context, snapshot) {
var width = MediaQuery.of(context).size.width; var width = context.width;
return snapshot.hasData return snapshot.hasData
? NotificationListener<ScrollNotification>( ? NotificationListener<ScrollNotification>(
onNotification: (scrollInfo) { onNotification: (scrollInfo) {

View File

@ -8,6 +8,7 @@ import '../util/custom_dropdown.dart';
import '../util/custom_widget.dart'; import '../util/custom_widget.dart';
import '../util/episodegrid.dart'; import '../util/episodegrid.dart';
import '../util/extension_helper.dart'; import '../util/extension_helper.dart';
import '../service/search_api.dart';
import 'popup_menu.dart'; import 'popup_menu.dart';
class LayoutSetting extends StatefulWidget { class LayoutSetting extends StatefulWidget {
@ -18,12 +19,22 @@ class LayoutSetting extends StatefulWidget {
} }
class _LayoutSettingState extends State<LayoutSetting> { class _LayoutSettingState extends State<LayoutSetting> {
final _hideDiscoveyStorage = KeyValueStorage(hidePodcastDiscoveryKey);
Future<Layout> _getLayout(String key) async { Future<Layout> _getLayout(String key) async {
var keyValueStorage = KeyValueStorage(key); var keyValueStorage = KeyValueStorage(key);
var layout = await keyValueStorage.getInt(); var layout = await keyValueStorage.getInt();
return Layout.values[layout]; 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 { Future<bool> _hideListened() async {
var hideListenedStorage = KeyValueStorage(hideListenedKey); var hideListenedStorage = KeyValueStorage(hideListenedKey);
var hideListened = await hideListenedStorage.getBool(defaultValue: false); var hideListened = await hideListenedStorage.getBool(defaultValue: false);
@ -36,6 +47,18 @@ class _LayoutSettingState extends State<LayoutSetting> {
if (mounted) setState(() {}); 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) { String _getHeightString(PlayerHeight mode) {
final s = context.s; final s = context.s;
switch (mode) { switch (mode) {
@ -188,9 +211,7 @@ class _LayoutSettingState extends State<LayoutSetting> {
padding: const EdgeInsets.symmetric(horizontal: 70), padding: const EdgeInsets.symmetric(horizontal: 70),
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: Text(s.settingsPopupMenu, child: Text(s.settingsPopupMenu,
style: Theme.of(context) style: context.textTheme.bodyText1
.textTheme
.bodyText1
.copyWith(color: context.accentColor)), .copyWith(color: context.accentColor)),
), ),
ListTile( ListTile(
@ -217,8 +238,7 @@ class _LayoutSettingState extends State<LayoutSetting> {
.copyWith(color: Theme.of(context).accentColor)), .copyWith(color: Theme.of(context).accentColor)),
), ),
ListTile( ListTile(
contentPadding: EdgeInsets.only( contentPadding: EdgeInsets.fromLTRB(70, 10, 10, 10),
left: 70.0, right: 20, bottom: 10, top: 10),
title: Text(s.settingsPlayerHeight), title: Text(s.settingsPlayerHeight),
subtitle: Text(s.settingsPlayerHeightDes), subtitle: Text(s.settingsPlayerHeightDes),
trailing: Selector<AudioPlayerNotifier, PlayerHeight>( trailing: Selector<AudioPlayerNotifier, PlayerHeight>(
@ -242,6 +262,56 @@ class _LayoutSettingState extends State<LayoutSetting> {
Padding( Padding(
padding: EdgeInsets.all(10.0), 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( Container(
height: 30.0, height: 30.0,
padding: EdgeInsets.symmetric(horizontal: 70), padding: EdgeInsets.symmetric(horizontal: 70),

View File

@ -8,6 +8,7 @@ class SearchState extends ChangeNotifier {
bool get update => _update; bool get update => _update;
OnlinePodcast _selectedPodcast; OnlinePodcast _selectedPodcast;
OnlinePodcast get selectedPodcast => _selectedPodcast; OnlinePodcast get selectedPodcast => _selectedPodcast;
set selectedPodcast(OnlinePodcast podcast) { set selectedPodcast(OnlinePodcast podcast) {
_selectedPodcast = podcast; _selectedPodcast = podcast;
notifyListeners(); notifyListeners();

View File

@ -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);
}

View File

@ -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,
};

View File

@ -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());
}

View File

@ -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,
};

View File

@ -51,7 +51,7 @@ class ItunesPodcast {
_$ItunesPodcastFromJson(json); _$ItunesPodcastFromJson(json);
Map<String, dynamic> toJson() => _$ItunesPodcastToJson(this); 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) .parse(releaseDate)
.millisecondsSinceEpoch; .millisecondsSinceEpoch;
OnlinePodcast get toOnlinePodcast => OnlinePodcast( OnlinePodcast get toOnlinePodcast => OnlinePodcast(

View File

@ -28,6 +28,7 @@ ItunesPodcast _$ItunesPodcastFromJson(Map<String, dynamic> json) {
feedUrl: json['feedUrl'] as String, feedUrl: json['feedUrl'] as String,
artworkUrl600: json['artworkUrl600'] as String, artworkUrl600: json['artworkUrl600'] as String,
releaseDate: json['releaseDate'] 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, 'feedUrl': instance.feedUrl,
'artworkUrl600': instance.artworkUrl600, 'artworkUrl600': instance.artworkUrl600,
'releaseDate': instance.releaseDate, 'releaseDate': instance.releaseDate,
'collectionId': instance.collectionId,
}; };

View File

@ -19,6 +19,8 @@ dependencies:
cookie_jar: ^1.0.1 cookie_jar: ^1.0.1
cupertino_icons: ^1.0.0 cupertino_icons: ^1.0.0
connectivity: ^0.4.9 connectivity: ^0.4.9
convert: ^2.1.1
crypto: ^2.1.5
device_info: ^0.4.2+7 device_info: ^0.4.2+7
dio: ^3.0.10 dio: ^3.0.10
dio_cookie_manager: ^1.0.0 dio_cookie_manager: ^1.0.0