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(
|
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),
|
||||||
),
|
),
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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®ion=$region";
|
"https://listen-api.listennotes.com/api/v2/best_podcasts?genre_id=$genre&page=$page®ion=$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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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);
|
_$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(
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue