Custom playlist support (working)

This commit is contained in:
stonega 2020-12-20 17:35:39 +08:00
parent 0a8f622f85
commit 1d8db22dde
21 changed files with 2259 additions and 407 deletions

View File

@ -76,7 +76,8 @@ If you want to build the app, you need to create a new file named `.env.dart` in
You can get your own API key on [ListenNotes](https://www.listennotes.com/api/), remember that you need to get pro plan API, because basic plan dosen't provide rss link for serach result. If no api key is added, the search function in the app won't work. But you can still add podcasts by using an RSS link or importing an OMPL file.
``` dart
final environment = {"apiKey":"APIKEY"};
final environment = {"apiKey":"APIKEY", "podcastIndexApiKey": "PODCASTINDEXAPIKEY",
"podcastIndexApiSecret": "PODCASTINDEXAPISECRET"};
```
4. Run the app with Android Studio or Visual Studio. Or the command line.
@ -112,21 +113,21 @@ If you have an issue or found a bug, please raise a GitHub issue. Pull requests
```
UI
src
──home
──home
├──home.dart [Homepage]
├──searc_podcast.dart [Search Page]
└──playlist.dart [Playlist Page]
──podcasts
──podcasts
├──podcast_manage.dart [Group Page]
└──podcast_detail.dart [Podcast Page]
──episodes
──episodes
└──episode_detail.dart [Episode Page]
──settings
──settings
└──setting.dart [Setting Page]
STATE
src
──state
──state
├──audio_state.dart [Audio State]
├──download_state.dart [Episode Download]
├──podcast_group.dart [Podcast Groups]
@ -135,8 +136,9 @@ src
Service
src
──service
──service
├──api_service.dart [Podcast Search]
├──gpodder_api.dart [Gpodder intergate]
└──ompl_builde.dart [OMPL export]
```

View File

@ -487,13 +487,10 @@ class __MenuBarState extends State<_MenuBar> {
},
),
DownloadButton(episode: widget.episodeItem),
Selector<AudioPlayerNotifier,
Tuple2<List<EpisodeBrief>, bool>>(
selector: (_, audio) =>
Tuple2(audio.queue.playlist, audio.queueUpdate),
Selector<AudioPlayerNotifier, List<EpisodeBrief>>(
selector: (_, audio) => audio.queue.episodes,
builder: (_, data, __) {
final inPlaylist =
data.item1.contains(widget.episodeItem);
final inPlaylist = data.contains(widget.episodeItem);
return inPlaylist
? _buttonOnMenu(
child: Icon(Icons.playlist_add_check,

View File

@ -413,12 +413,10 @@ class _PlaylistWidgetState extends State<PlaylistWidget> {
child: Column(
children: <Widget>[
Expanded(
child:
Selector<AudioPlayerNotifier, Tuple2<List<EpisodeBrief>, bool>>(
selector: (_, audio) =>
Tuple2(audio.queue.playlist, audio.queueUpdate),
child: Selector<AudioPlayerNotifier, List<EpisodeBrief>>(
selector: (_, audio) => audio.playlist.episodes,
builder: (_, data, __) {
var episodesToPlay = data.item1.sublist(1);
var episodesToPlay = data.sublist(1);
return AnimatedList(
key: miniPlaylistKey,
shrinkWrap: true,
@ -436,7 +434,7 @@ class _PlaylistWidgetState extends State<PlaylistWidget> {
color: Colors.transparent,
child: InkWell(
onTap: () {
audio.episodeLoad(data.item1[index]);
audio.episodeLoad(data[index]);
miniPlaylistKey.currentState.removeItem(
index,
(context, animation) => Center());
@ -506,8 +504,7 @@ class _PlaylistWidgetState extends State<PlaylistWidget> {
duration: Duration(milliseconds: 100));
await Future.delayed(
Duration(milliseconds: 100));
await audio
.moveToTop(data.item1[index + 1]);
await audio.moveToTop(data[index + 1]);
},
child: SizedBox(
height: 30.0,

View File

@ -13,6 +13,7 @@ import 'package:tuple/tuple.dart';
import '../local_storage/key_value_storage.dart';
import '../local_storage/sqflite_localpodcast.dart';
import '../playlists/playlist_home.dart';
import '../state/audio_state.dart';
import '../state/download_state.dart';
import '../state/podcast_group.dart';
@ -190,7 +191,8 @@ class _HomeState extends State<Home> with SingleTickerProviderStateMixin {
description:
s.featureDiscoveryOMPLDes,
child: Padding(
padding: const EdgeInsets.only(right: 5.0),
padding: const EdgeInsets.only(
right: 5.0),
child: PopupMenu(),
)),
],
@ -319,7 +321,7 @@ class __PlaylistButtonState extends State<_PlaylistButton> {
bool _loadPlay;
Future<void> _getPlaylist() async {
await context.read<AudioPlayerNotifier>().loadPlaylist();
await context.read<AudioPlayerNotifier>().initPlaylist();
if (mounted) {
setState(() {
_loadPlay = true;
@ -336,7 +338,6 @@ class __PlaylistButtonState extends State<_PlaylistButton> {
@override
Widget build(BuildContext context) {
final audio = context.watch<AudioPlayerNotifier>();
final s = context.s;
return Material(
color: Colors.transparent,
@ -365,7 +366,7 @@ class __PlaylistButtonState extends State<_PlaylistButton> {
? SizedBox(
height: 8.0,
)
: data.item1 || data.item2.playlist.length == 0
: data.item1 || data.item2.episodes.isEmpty
? SizedBox(
height: 8.0,
)
@ -374,7 +375,9 @@ class __PlaylistButtonState extends State<_PlaylistButton> {
topLeft: Radius.circular(10.0),
topRight: Radius.circular(10.0)),
onTap: () {
audio.playlistLoad();
context
.read<AudioPlayerNotifier>()
.playFromLastPosition();
Navigator.pop<int>(context);
},
child: Column(
@ -388,7 +391,7 @@ class __PlaylistButtonState extends State<_PlaylistButton> {
CircleAvatar(
radius: 20,
backgroundImage: data
.item2.playlist.first.avatarImage),
.item2.episodes.first.avatarImage),
Container(
height: 40.0,
width: 40.0,
@ -416,7 +419,7 @@ class __PlaylistButtonState extends State<_PlaylistButton> {
// TextStyle(color: Colors.white)
),
Text(
data.item2.playlist.first.title,
data.item2.episodes.first.title,
maxLines: 2,
textAlign: TextAlign.center,
overflow: TextOverflow.fade,
@ -476,9 +479,7 @@ class __PlaylistButtonState extends State<_PlaylistButton> {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PlaylistPage(
initPage: InitPage.playlist,
),
builder: (context) => PlaylistHome(),
),
);
} else if (value == 2) {

View File

@ -85,14 +85,12 @@ class _ScrollPodcastsState extends State<ScrollPodcasts>
Widget build(BuildContext context) {
final width = MediaQuery.of(context).size.width;
final s = context.s;
return Selector<GroupList, Tuple3<List<PodcastGroup>, bool, bool>>(
selector: (_, groupList) =>
Tuple3(groupList.groups, groupList.created, groupList.isLoading),
return Selector<GroupList, Tuple2<List<PodcastGroup>, bool>>(
selector: (_, groupList) => Tuple2(groupList.groups, groupList.created),
builder: (_, data, __) {
var groups = data.item1;
var import = data.item2;
var isLoading = data.item3;
return isLoading
return groups.isEmpty
? Container(
height: (width - 20) / 3 + 140,
)
@ -246,7 +244,7 @@ class _ScrollPodcastsState extends State<ScrollPodcasts>
),
)
: DefaultTabController(
length: groups[_groupIndex].podcastList.length,
length: groups[_groupIndex].podcasts.length,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
@ -696,7 +694,7 @@ class ShowEpisode extends StatelessWidget {
Tuple2<EpisodeBrief, List<String>>>(
selector: (_, audio) => Tuple2(
audio?.episode,
audio.queue.playlist
audio.queue.episodes
.map((e) => e.enclosureUrl)
.toList(),
),

View File

@ -77,11 +77,11 @@ class _PlaylistPageState extends State<PlaylistPage> {
),
body: SafeArea(
child: Selector<AudioPlayerNotifier,
Tuple4<Playlist, bool, bool, EpisodeBrief>>(
selector: (_, audio) => Tuple4(audio.queue, audio.playerRunning,
audio.queueUpdate, audio.episode),
Tuple3<Playlist, bool, EpisodeBrief>>(
selector: (_, audio) =>
Tuple3(audio.playlist, audio.playerRunning, audio.episode),
builder: (_, data, __) {
var episodes = data.item1.playlist;
var episodes = data.item1.episodes;
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
@ -227,12 +227,12 @@ class _PlaylistPageState extends State<PlaylistPage> {
CircleAvatar(
radius: 15,
backgroundImage:
data.item4.avatarImage),
data.item3.avatarImage),
Container(
width: 150,
alignment: Alignment.center,
child: Text(
data.item4.title,
data.item3.title,
maxLines: 1,
overflow: TextOverflow.fade,
textAlign: TextAlign.center,
@ -254,7 +254,7 @@ class _PlaylistPageState extends State<PlaylistPage> {
icon: Icon(Icons.play_circle_filled,
size: 40, color: context.accentColor),
onPressed: () {
audio.playlistLoad();
//audio.playlistLoad();
// setState(() {});
}),
),
@ -293,7 +293,7 @@ class __ReorderablePlaylistState extends State<_ReorderablePlaylist> {
return Selector<AudioPlayerNotifier, Tuple2<Playlist, bool>>(
selector: (_, audio) => Tuple2(audio.queue, audio.playerRunning),
builder: (_, data, __) {
var episodes = data.item1.playlist;
var episodes = data.item1.episodes;
return ReorderableListView(
onReorder: (oldIndex, newIndex) {
if (newIndex > oldIndex) {
@ -708,19 +708,12 @@ class __HistoryListState extends State<_HistoryList> {
SizedBox(
child: Selector<
AudioPlayerNotifier,
Tuple2<
List<EpisodeBrief>,
bool>>(
List<EpisodeBrief>>(
selector: (_, audio) =>
Tuple2(
audio.queue
.playlist,
audio
.queueUpdate),
audio.queue.episodes,
builder: (_, data, __) {
return data.item1
.contains(
episode)
return data.contains(
episode)
? IconButton(
icon: Icon(
Icons

View File

@ -59,6 +59,7 @@ const String markListenedAfterSkipKey = 'markListenedAfterSkipKey';
const String downloadPositionKey = 'downloadPositionKey';
const String deleteAfterPlayedKey = 'removeAfterPlayedKey';
const String playlistsAllKey = 'playlistsAllKey';
const String playerStateKey = 'playerStateKey';
class KeyValueStorage {
final String key;
@ -92,13 +93,14 @@ class KeyValueStorage {
var prefs = await SharedPreferences.getInstance();
if (prefs.getString(key) == null) {
var episodeList = prefs.getStringList(playlistKey);
var playlist = Playlist('Playlist', episodeList: episodeList);
var playlist = Playlist('Queue', episodeList: episodeList);
await prefs.setString(
key,
json.encode({
'playlists': [playlist.toEntity().toJson()]
}));
}
print(prefs.getString(key));
return json
.decode(prefs.getString(key))['playlists']
.cast<Map<String, Object>>()
@ -115,6 +117,21 @@ class KeyValueStorage {
}));
}
Future<bool> savePlayerState(
String playlist, String episode, int position) async {
var prefs = await SharedPreferences.getInstance();
return prefs.setStringList(key, [playlist, episode, position.toString()]);
}
Future<List<String>> getPlayerState() async {
var prefs = await SharedPreferences.getInstance();
if (prefs.getStringList(key) == null) {
final position = prefs.getInt(audioPositionKey) ?? 0;
await savePlayerState('', '', position);
}
return prefs.getStringList(key);
}
Future<bool> saveInt(int setting) async {
var prefs = await SharedPreferences.getInstance();
return prefs.setInt(key, setting);
@ -212,7 +229,7 @@ class KeyValueStorage {
return prefs.setDouble(key, data);
}
Future<double> getDoubel({double defaultValue = 0.0}) async {
Future<double> getDouble({double defaultValue = 0.0}) async {
var prefs = await SharedPreferences.getInstance();
if (prefs.getDouble(key) == null) {
await prefs.setDouble(key, defaultValue);

View File

@ -109,7 +109,7 @@ class DBHelper {
list.first['imagePath'],
list.first['provider'],
list.first['link'],
upateCount: list.first['update_count'],
updateCount: list.first['update_count'],
episodeCount: list.first['episode_count']));
}
}
@ -166,7 +166,7 @@ class DBHelper {
list.first['imagePath'],
list.first['provider'],
list.first['link'],
upateCount: list.first['update_count'],
updateCount: list.first['update_count'],
episodeCount: list.first['episode_count']);
}
return null;
@ -1376,7 +1376,7 @@ class DBHelper {
await txn.rawUpdate(
"UPDATE Episodes SET is_new = 0 WHERE enclosure_url = ?", [url]);
});
developer.log('remove new episode $url');
developer.log('remove episode mark $url');
}
Future<List<EpisodeBrief>> getLikedRssItem(int i, int sortBy,

View File

@ -0,0 +1,948 @@
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:line_icons/line_icons.dart';
import 'package:provider/provider.dart';
import 'package:tuple/tuple.dart';
import '../local_storage/sqflite_localpodcast.dart';
import '../state/audio_state.dart';
import '../type/episodebrief.dart';
import '../type/play_histroy.dart';
import '../type/playlist.dart';
import '../util/extension_helper.dart';
import '../widgets/custom_widget.dart';
import '../widgets/dismissible_container.dart';
import 'playlist_page.dart';
class PlaylistHome extends StatefulWidget {
PlaylistHome({Key key}) : super(key: key);
@override
_PlaylistHomeState createState() => _PlaylistHomeState();
}
class _PlaylistHomeState extends State<PlaylistHome> {
Widget _body;
String _selected;
@override
void initState() {
super.initState();
_selected = 'PlayNext';
_body = _Queue();
}
Widget _tabWidget(
{Widget icon,
String label,
Function onTap,
bool isSelected,
Color color}) {
return OutlinedButton.icon(
style: OutlinedButton.styleFrom(
side: BorderSide(color: context.scaffoldBackgroundColor),
primary: color,
backgroundColor:
isSelected ? context.primaryColorDark : Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(100)))),
icon: icon,
label: isSelected ? Text(label) : Center(),
onPressed: onTap);
}
@override
Widget build(BuildContext context) {
return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle(
systemNavigationBarIconBrightness:
Theme.of(context).accentColorBrightness,
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
systemNavigationBarColor: Theme.of(context).primaryColor,
),
child: Scaffold(
appBar: AppBar(
leading: CustomBackButton(),
backgroundColor: context.scaffoldBackgroundColor,
),
body: Column(
children: [
SizedBox(
height: 100,
child: Selector<AudioPlayerNotifier,
Tuple4<Playlist, bool, bool, EpisodeBrief>>(
selector: (_, audio) => Tuple4(audio.playlist,
audio.playerRunning, audio.playing, audio.episode),
builder: (_, data, __) {
final running = data.item2;
final playing = data.item3;
final audio = context.read<AudioPlayerNotifier>();
return Row(
children: [
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
IconButton(
icon: Icon(Icons.fast_rewind),
onPressed: () {
if (running) {
audio.rewind();
}
}),
SizedBox(width: 30),
IconButton(
padding: EdgeInsets.zero,
icon: Icon(
playing
? LineIcons.pause_solid
: LineIcons.play_solid,
size: 40),
onPressed: () {
if (running) {
playing
? audio.pauseAduio()
: audio.resumeAudio();
} else {
context
.read<AudioPlayerNotifier>()
.playFromLastPosition();
}
}),
SizedBox(width: 30),
IconButton(
icon: Icon(Icons.fast_forward),
onPressed: () {
if (running) {
audio.fastForward();
}
})
],
),
data.item4 != null
? Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20.0),
child: Text(data.item4.title, maxLines: 1),
)
: Center(),
],
)),
data.item3 != null
? ClipRRect(
borderRadius: BorderRadius.circular(10),
child: SizedBox(
width: 80,
height: 80,
child:
Image(image: data.item4.avatarImage)),
)
: Container(
decoration: BoxDecoration(
color: context.accentColor.withAlpha(70),
borderRadius: BorderRadius.circular(10)),
width: 80,
height: 80),
SizedBox(
width: 20,
),
],
);
},
),
),
SizedBox(
height: 50,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_tabWidget(
icon: Icon(Icons.queue_music_rounded),
label: 'Play Next',
color: Colors.blue,
isSelected: _selected == 'PlayNext',
onTap: () => setState(() {
_body = _Queue();
_selected = 'PlayNext';
})),
_tabWidget(
icon: Icon(Icons.history),
label: 'History',
color: Colors.green,
isSelected: _selected == 'Histtory',
onTap: () => setState(() {
_body = _History();
_selected = 'Histtory';
})),
_tabWidget(
icon: Icon(Icons.playlist_play),
label: 'Playlists',
color: Colors.purple,
isSelected: _selected == 'Playlists',
onTap: () => setState(() {
_body = _Playlists();
_selected = 'Playlists';
})),
],
),
),
Divider(height: 1),
Expanded(
child: AnimatedSwitcher(
duration: Duration(milliseconds: 300), child: _body))
],
)),
);
}
}
class _Queue extends StatefulWidget {
const _Queue({Key key}) : super(key: key);
@override
__QueueState createState() => __QueueState();
}
class __QueueState extends State<_Queue> {
@override
Widget build(BuildContext context) {
final s = context.s;
return Selector<AudioPlayerNotifier, Tuple2<Playlist, bool>>(
selector: (_, audio) => Tuple2(audio.playlist, audio.playerRunning),
builder: (_, data, __) {
var episodes = data.item1.episodes.toSet().toList();
var queue = data.item1;
return queue.name == 'Queue'
? ReorderableListView(
onReorder: (oldIndex, newIndex) {
if (newIndex > oldIndex) {
newIndex -= 1;
}
context
.read<AudioPlayerNotifier>()
.reorderPlaylist(oldIndex, newIndex);
setState(() {});
},
scrollDirection: Axis.vertical,
children: data.item2
? episodes.map<Widget>((episode) {
if (episode.enclosureUrl !=
episodes.first.enclosureUrl) {
return DismissibleContainer(
episode: episode,
onRemove: (value) => setState(() {}),
key: ValueKey(episode.enclosureUrl),
);
} else {
return Container(
key: ValueKey('sd'),
);
}
}).toList()
: episodes
.map<Widget>((episode) => DismissibleContainer(
episode: episode,
onRemove: (value) => setState(() {}),
key: ValueKey(episode.enclosureUrl),
))
.toList())
: ListView.builder(
itemCount: queue.episodeList.length,
itemBuilder: (context, index) {
final episode = queue.episodes[index];
final c = episode.backgroudColor(context);
return SizedBox(
height: 90.0,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
Expanded(
child: ListTile(
contentPadding: EdgeInsets.symmetric(vertical: 8),
onTap: () async {
await context
.read<AudioPlayerNotifier>()
.episodeLoad(episode);
},
title: Container(
padding: EdgeInsets.fromLTRB(0, 5.0, 20.0, 5.0),
child: Text(
episode.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
leading: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.unfold_more, color: c),
CircleAvatar(
backgroundColor: c.withOpacity(0.5),
backgroundImage: episode.avatarImage),
],
),
subtitle: Container(
padding: EdgeInsets.only(top: 5, bottom: 5),
height: 35,
child: Row(
children: <Widget>[
if (episode.explicit == 1)
Container(
decoration: BoxDecoration(
color: Colors.red[800],
shape: BoxShape.circle),
height: 25.0,
width: 25.0,
margin: EdgeInsets.only(right: 10.0),
alignment: Alignment.center,
child: Text('E',
style: TextStyle(
color: Colors.white))),
if (episode.duration != 0)
episodeTag(
episode.duration == 0
? ''
: s.minsCount(
episode.duration ~/ 60),
Colors.cyan[300]),
if (episode.enclosureLength != null)
episodeTag(
episode.enclosureLength == 0
? ''
: '${(episode.enclosureLength) ~/ 1000000}MB',
Colors.lightBlue[300]),
],
),
),
//trailing: Icon(Icons.menu),
),
),
Divider(
height: 2,
),
],
),
);
});
});
}
}
class _History extends StatefulWidget {
const _History({Key key}) : super(key: key);
@override
__HistoryState createState() => __HistoryState();
}
class __HistoryState extends State<_History> {
var dbHelper = DBHelper();
bool _loadMore = false;
Future _getData;
Future<List<PlayHistory>> getPlayRecords(int top) async {
List<PlayHistory> playHistory;
playHistory = await dbHelper.getPlayRecords(top);
for (var record in playHistory) {
await record.getEpisode();
}
return playHistory;
}
Future<void> _loadMoreData() async {
if (mounted) {
setState(() {
_loadMore = true;
});
}
await Future.delayed(Duration(milliseconds: 500));
_top = _top + 20;
if (mounted) {
setState(() {
_getData = getPlayRecords(_top);
_loadMore = false;
});
}
}
int _top;
@override
void initState() {
super.initState();
_top = 20;
_getData = getPlayRecords(_top);
}
@override
Widget build(BuildContext context) {
final s = context.s;
final audio = context.watch<AudioPlayerNotifier>();
return FutureBuilder<List<PlayHistory>>(
future: _getData,
builder: (context, snapshot) {
return snapshot.hasData
? NotificationListener<ScrollNotification>(
onNotification: (scrollInfo) {
if (scrollInfo.metrics.pixels ==
scrollInfo.metrics.maxScrollExtent &&
snapshot.data.length == _top) {
if (!_loadMore) {
_loadMoreData();
}
}
return true;
},
child: ListView.builder(
scrollDirection: Axis.vertical,
itemCount: snapshot.data.length + 1,
itemBuilder: (context, index) {
if (index == snapshot.data.length) {
return SizedBox(
height: 2,
child: _loadMore
? LinearProgressIndicator()
: Center());
} else {
final seekValue = snapshot.data[index].seekValue;
final seconds = snapshot.data[index].seconds;
final date = snapshot
.data[index].playdate.millisecondsSinceEpoch;
final episode = snapshot.data[index].episode;
final c = episode?.backgroudColor(context);
return episode == null
? Center()
: SizedBox(
height: 90.0,
child: Column(
mainAxisAlignment:
MainAxisAlignment.spaceAround,
children: [
Expanded(
child: Center(
child: ListTile(
contentPadding: EdgeInsets.fromLTRB(
24, 8, 20, 8),
onTap: () => audio.episodeLoad(
episode,
startPosition: seekValue < 0.9
? (seconds * 1000).toInt()
: 0),
leading: CircleAvatar(
backgroundColor:
c?.withOpacity(0.5),
backgroundImage:
episode.avatarImage),
title: Padding(
padding: EdgeInsets.symmetric(
vertical: 5.0),
child: Text(
snapshot.data[index].title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
subtitle: SizedBox(
height: 40,
child: Row(
mainAxisAlignment:
MainAxisAlignment.start,
crossAxisAlignment:
CrossAxisAlignment.center,
children: <Widget>[
if (seekValue < 0.9)
Padding(
padding:
EdgeInsets.symmetric(
vertical: 5.0),
child: Material(
color:
Colors.transparent,
child: InkWell(
onTap: () {
audio.episodeLoad(
episode,
startPosition:
(seconds *
1000)
.toInt());
},
borderRadius:
BorderRadius
.circular(20),
child: Stack(
children: [
ShaderMask(
shaderCallback:
(bounds) {
return LinearGradient(
begin: Alignment
.centerLeft,
colors: <
Color>[
Colors
.cyan[600]
.withOpacity(0.8),
Colors
.white70
],
stops: [
seekValue,
seekValue
],
tileMode:
TileMode
.mirror,
).createShader(
bounds);
},
child:
Container(
height: 25,
alignment:
Alignment
.center,
padding: EdgeInsets.symmetric(
horizontal:
20),
decoration:
BoxDecoration(
borderRadius:
BorderRadius.circular(
20.0),
color: context
.accentColor,
),
child: Text(
seconds
.toTime,
style: TextStyle(
color:
Colors.white),
),
),
),
]),
),
),
),
SizedBox(
child: Selector<
AudioPlayerNotifier,
List<EpisodeBrief>>(
selector: (_, audio) =>
audio.queue.episodes,
builder: (_, data, __) {
return data.contains(
episode)
? IconButton(
icon: Icon(
Icons
.playlist_add_check,
color: context
.accentColor),
onPressed:
() async {
audio.delFromPlaylist(
episode);
Fluttertoast
.showToast(
msg: s
.toastRemovePlaylist,
gravity:
ToastGravity
.BOTTOM,
);
})
: IconButton(
icon: Icon(
Icons
.playlist_add,
color: Colors
.grey[
700]),
onPressed:
() async {
audio.addToPlaylist(
episode);
Fluttertoast
.showToast(
msg: s
.toastAddPlaylist,
gravity:
ToastGravity
.BOTTOM,
);
});
},
),
),
Spacer(),
Text(
date.toDate(context),
style: TextStyle(
fontSize: 15,
),
),
],
),
),
),
),
),
Divider(height: 1)
],
),
);
}
}),
)
: Center(
child: SizedBox(
height: 25,
width: 25,
child: CircularProgressIndicator()),
);
});
}
}
class _Playlists extends StatefulWidget {
const _Playlists({Key key}) : super(key: key);
@override
__PlaylistsState createState() => __PlaylistsState();
}
class __PlaylistsState extends State<_Playlists> {
Future<EpisodeBrief> _getEpisode(String url) async {
var dbHelper = DBHelper();
return await dbHelper.getRssItemWithUrl(url);
}
@override
Widget build(BuildContext context) {
final s = context.s;
return Selector<AudioPlayerNotifier, List<Playlist>>(
selector: (_, audio) => audio.playlists,
builder: (_, data, __) {
return ScrollConfiguration(
behavior: NoGrowBehavior(),
child: ListView.builder(
itemCount: data.length + 1,
itemBuilder: (context, index) {
if (index == 0) {
final queue = data.first;
return InkWell(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
fullscreenDialog: true,
builder: (context) =>
PlaylistDetail(data[index])),
);
},
child: Padding(
padding: const EdgeInsets.all(15.0),
child: Row(
children: [
Container(
height: 80,
width: 80,
color: context.primaryColorDark,
child: GridView.builder(
gridDelegate:
SliverGridDelegateWithFixedCrossAxisCount(
childAspectRatio: 1,
crossAxisCount: 2,
mainAxisSpacing: 0.0,
crossAxisSpacing: 0.0,
),
itemCount: math.min(queue.episodes.length, 4),
itemBuilder: (_, index) {
if (index < queue.episodeList.length) {
return Image(
image:
queue.episodes[index].avatarImage,
);
}
return Center();
}),
),
SizedBox(width: 15),
Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Queue',
style: context.textTheme.headline6,
),
Text('${queue.episodes.length} episodes'),
OutlinedButton(
onPressed: () {
context
.read<AudioPlayerNotifier>()
.playlistLoad(queue);
},
child: Text('Play'))
],
)
],
),
),
);
}
if (index < data.length) {
final episodeList = data[index].episodeList;
return ListTile(
onTap: () async {
await context
.read<AudioPlayerNotifier>()
.updatePlaylist(data[index], updateEpisodes: true);
Navigator.push(
context,
MaterialPageRoute(
fullscreenDialog: true,
builder: (context) =>
PlaylistDetail(data[index])),
);
},
leading: Container(
height: 50,
width: 50,
color: context.primaryColorDark,
child: episodeList.isEmpty
? Center()
: FutureBuilder<EpisodeBrief>(
future: _getEpisode(episodeList.first),
builder: (_, snapshot) {
if (snapshot.data != null) {
return SizedBox(
height: 50,
width: 50,
child: Image(
image: snapshot.data.avatarImage));
}
return Center();
}),
),
title: Text(data[index].name),
subtitle: Text(episodeList.isNotEmpty
? s.episode(data[index].episodeList.length)
: '0 episode'),
trailing: IconButton(
splashRadius: 20,
icon: Icon(Icons.play_arrow),
onPressed: () {
context
.read<AudioPlayerNotifier>()
.playlistLoad(data[index]);
},
),
);
}
return ListTile(
onTap: () {
showGeneralDialog(
context: context,
barrierDismissible: true,
barrierLabel: MaterialLocalizations.of(context)
.modalBarrierDismissLabel,
barrierColor: Colors.black54,
transitionDuration: const Duration(milliseconds: 200),
pageBuilder:
(context, animaiton, secondaryAnimation) =>
_NewPlaylist());
},
leading: Container(
height: 50,
width: 50,
color: context.primaryColorDark,
child: Center(child: Icon(Icons.add)),
),
title: Text('Create new playlist'),
);
}),
);
});
}
}
enum NewPlaylistOption { blank, randon10, latest10 }
class _NewPlaylist extends StatefulWidget {
_NewPlaylist({Key key}) : super(key: key);
@override
__NewPlaylistState createState() => __NewPlaylistState();
}
class __NewPlaylistState extends State<_NewPlaylist> {
String _playlistName = '';
NewPlaylistOption _option;
int _error;
@override
void initState() {
super.initState();
_option = NewPlaylistOption.blank;
}
Widget _createOption(NewPlaylistOption option) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: InkWell(
borderRadius: BorderRadius.circular(20),
onTap: () {
setState(() => _option = option);
},
child: AnimatedContainer(
duration: Duration(milliseconds: 300),
padding: EdgeInsets.symmetric(horizontal: 8.0),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: _option == option
? context.accentColor
: context.primaryColorDark),
height: 32,
child: Center(
child: Text(_optionLabel(option).first,
style: TextStyle(
color: _option == option
? Colors.white
: context.textColor))),
),
),
);
}
List<String> _optionLabel(NewPlaylistOption option) {
switch (option) {
case NewPlaylistOption.blank:
return ['Empty', 'Add episodes later'];
break;
case NewPlaylistOption.randon10:
return ['Randon 10', 'Add 10 random episodes to playlists'];
break;
case NewPlaylistOption.latest10:
return ['Latest 10', 'Add 10 latest updated episodes to playlist'];
break;
default:
return ['', ''];
break;
}
}
@override
Widget build(BuildContext context) {
final s = context.s;
return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle(
statusBarIconBrightness: Brightness.light,
systemNavigationBarColor:
Theme.of(context).brightness == Brightness.light
? Color.fromRGBO(113, 113, 113, 1)
: Color.fromRGBO(5, 5, 5, 1),
),
child: AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
elevation: 1,
contentPadding: EdgeInsets.symmetric(horizontal: 20),
titlePadding: EdgeInsets.all(20),
actionsPadding: EdgeInsets.zero,
actions: <Widget>[
FlatButton(
splashColor: context.accentColor.withAlpha(70),
onPressed: () => Navigator.of(context).pop(),
child: Text(
s.cancel,
style: TextStyle(color: Colors.grey[600]),
),
),
FlatButton(
splashColor: context.accentColor.withAlpha(70),
onPressed: () async {
if (context
.read<AudioPlayerNotifier>()
.playlistExisted(_playlistName)) {
setState(() => _error = 1);
} else {
var playlist;
switch (_option) {
case NewPlaylistOption.blank:
playlist = Playlist(
_playlistName,
);
break;
case NewPlaylistOption.latest10:
case NewPlaylistOption.randon10:
break;
}
context.read<AudioPlayerNotifier>().addPlaylist(playlist);
Navigator.of(context).pop();
}
},
child:
Text(s.confirm, style: TextStyle(color: context.accentColor)),
)
],
title:
SizedBox(width: context.width - 160, child: Text('New playlist')),
content: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
TextField(
decoration: InputDecoration(
contentPadding: EdgeInsets.symmetric(horizontal: 10),
hintText: 'New playlist',
hintStyle: TextStyle(fontSize: 18),
filled: true,
focusedBorder: UnderlineInputBorder(
borderSide:
BorderSide(color: context.accentColor, width: 2.0),
),
enabledBorder: UnderlineInputBorder(
borderSide:
BorderSide(color: context.accentColor, width: 2.0),
),
),
cursorRadius: Radius.circular(2),
autofocus: true,
maxLines: 1,
onChanged: (value) {
_playlistName = value;
},
),
Container(
alignment: Alignment.centerLeft,
child: (_error == 1)
? Text(
'Playlist existed',
style: TextStyle(color: Colors.red[400]),
)
: Center(),
),
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
_createOption(NewPlaylistOption.blank),
_createOption(NewPlaylistOption.randon10),
_createOption(NewPlaylistOption.latest10),
],
),
],
),
),
);
}
}

View File

@ -0,0 +1,255 @@
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../state/audio_state.dart';
import '../type/episodebrief.dart';
import '../type/playlist.dart';
import '../util/extension_helper.dart';
class PlaylistDetail extends StatefulWidget {
final Playlist playlist;
PlaylistDetail(this.playlist, {Key key}) : super(key: key);
@override
_PlaylistDetailState createState() => _PlaylistDetailState();
}
class _PlaylistDetailState extends State<PlaylistDetail> {
final List<EpisodeBrief> _selectedEpisodes = [];
@override
Widget build(BuildContext context) {
final s = context.s;
return Scaffold(
appBar: AppBar(
title: Text(_selectedEpisodes.isEmpty
? widget.playlist.name
: '${_selectedEpisodes.length} selected'),
actions: [
IconButton(
icon: Icon(Icons.delete),
onPressed: () {
context.read<AudioPlayerNotifier>().removeEpisodeFromPlaylist(
widget.playlist,
episodes: _selectedEpisodes);
setState(_selectedEpisodes.clear);
}),
SizedBox(
height: 40,
width: 40,
child: Material(
color: Colors.transparent,
borderRadius: BorderRadius.circular(100),
clipBehavior: Clip.hardEdge,
child: SizedBox(
height: 40,
width: 40,
child: PopupMenuButton<int>(
icon: Icon(Icons.more_vert),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10)),
elevation: 1,
tooltip: s.menu,
itemBuilder: (context) => [
PopupMenuItem(value: 1, child: Text('Clear all')),
],
onSelected: (value) {
if (value == 1) {
context
.read<AudioPlayerNotifier>()
.clearPlaylist(widget.playlist);
}
}),
),
),
)
],
),
body: Selector<AudioPlayerNotifier, List<Playlist>>(
selector: (_, audio) => audio.playlists,
builder: (_, data, __) {
final playlist = data.firstWhere((e) => e == widget.playlist);
final episodes = playlist.episodes;
return ReorderableListView(
onReorder: (oldIndex, newIndex) {
context.read<AudioPlayerNotifier>().reorderEpisodesInPlaylist(
widget.playlist,
oldIndex: oldIndex,
newIndex: newIndex);
setState(() {});
},
scrollDirection: Axis.vertical,
children: episodes.map<Widget>((episode) {
return _PlaylistItem(episode,
key: ValueKey(episode.enclosureUrl), onSelect: (episode) {
_selectedEpisodes.add(episode);
setState(() {});
}, onRemove: (episode) {
_selectedEpisodes.remove(episode);
setState(() {});
});
}).toList());
}),
);
}
}
class _PlaylistItem extends StatefulWidget {
final EpisodeBrief episode;
final ValueChanged<EpisodeBrief> onSelect;
final ValueChanged<EpisodeBrief> onRemove;
_PlaylistItem(this.episode,
{@required this.onSelect, @required this.onRemove, Key key})
: super(key: key);
@override
__PlaylistItemState createState() => __PlaylistItemState();
}
class __PlaylistItemState extends State<_PlaylistItem>
with SingleTickerProviderStateMixin {
AnimationController _controller;
Animation _animation;
double _fraction;
@override
void initState() {
super.initState();
_fraction = 0;
_controller = AnimationController(
vsync: this, duration: const Duration(milliseconds: 500));
_animation = Tween(begin: 0.0, end: 1.0).animate(_controller)
..addListener(() {
if (mounted) {
setState(() => _fraction = _animation.value);
}
});
_controller.addStatusListener((status) {
if (status == AnimationStatus.completed) {
_controller.stop();
} else if (status == AnimationStatus.dismissed) {
_controller.stop();
}
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
Widget _episodeTag(String text, Color color) {
if (text == '') {
return Center();
}
return Container(
decoration: BoxDecoration(
color: color, borderRadius: BorderRadius.circular(15.0)),
height: 25.0,
margin: EdgeInsets.only(right: 10.0),
padding: EdgeInsets.symmetric(horizontal: 8.0),
alignment: Alignment.center,
child: Text(text, style: TextStyle(fontSize: 14.0, color: Colors.black)),
);
}
@override
Widget build(BuildContext context) {
final s = context.s;
final episode = widget.episode;
final c = episode.backgroudColor(context);
return SizedBox(
height: 90.0,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
Expanded(
child: ListTile(
contentPadding: EdgeInsets.symmetric(vertical: 8),
onTap: () {
if (_fraction == 0) {
_controller.forward();
widget.onSelect(episode);
} else {
_controller.reverse();
widget.onRemove(episode);
}
},
title: Container(
padding: EdgeInsets.fromLTRB(0, 5.0, 20.0, 5.0),
child: Text(
episode.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
leading: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.unfold_more, color: c),
Transform(
alignment: FractionalOffset.center,
transform: Matrix4.identity()
..setEntry(3, 2, 0.001)
..rotateY(math.pi * _fraction),
child: _fraction < 0.5
? CircleAvatar(
backgroundColor: c.withOpacity(0.5),
backgroundImage: episode.avatarImage)
: CircleAvatar(
backgroundColor:
context.accentColor.withAlpha(70),
child: Transform(
alignment: FractionalOffset.center,
transform: Matrix4.identity()
..setEntry(3, 2, 0.001)
..rotateY(math.pi),
child: Icon(Icons.done)),
),
),
],
),
subtitle: Container(
padding: EdgeInsets.only(top: 5, bottom: 5),
height: 35,
child: Row(
children: <Widget>[
if (episode.explicit == 1)
Container(
decoration: BoxDecoration(
color: Colors.red[800], shape: BoxShape.circle),
height: 25.0,
width: 25.0,
margin: EdgeInsets.only(right: 10.0),
alignment: Alignment.center,
child: Text('E',
style: TextStyle(color: Colors.white))),
if (episode.duration != 0)
_episodeTag(
episode.duration == 0
? ''
: s.minsCount(episode.duration ~/ 60),
Colors.cyan[300]),
if (episode.enclosureLength != null)
_episodeTag(
episode.enclosureLength == 0
? ''
: '${(episode.enclosureLength) ~/ 1000000}MB',
Colors.lightBlue[300]),
],
),
),
),
),
Divider(
height: 2,
),
],
));
}
}

View File

@ -23,38 +23,47 @@ class PodcastGroupList extends StatefulWidget {
}
class _PodcastGroupListState extends State<PodcastGroupList> {
PodcastGroup _group;
@override
void initState() {
super.initState();
_group = widget.group;
}
@override
void didUpdateWidget(PodcastGroupList oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.group != widget.group) setState(() => _group = widget.group);
}
@override
Widget build(BuildContext context) {
var groupList = Provider.of<GroupList>(context, listen: false);
return widget.group.podcastList.length == 0
return _group.podcastList.isEmpty
? Container(
color: Theme.of(context).primaryColor,
color: context.primaryColor,
)
: Container(
color: Theme.of(context).primaryColor,
color: context.primaryColor,
child: ReorderableListView(
onReorder: (oldIndex, newIndex) {
setState(() {
if (newIndex > oldIndex) {
newIndex -= 1;
}
final podcast = widget.group.podcasts.removeAt(oldIndex);
widget.group.podcasts.insert(newIndex, podcast);
_group.reorderGroup(oldIndex, newIndex);
});
widget.group.orderedPodcasts = widget.group.podcasts;
groupList.addToOrderChanged(widget.group);
context.read<GroupList>().addToOrderChanged(_group);
},
children: widget.group.podcasts.map<Widget>((podcastLocal) {
return Container(
decoration:
BoxDecoration(color: Theme.of(context).primaryColor),
key: ObjectKey(podcastLocal.title),
child: _PodcastCard(
podcastLocal: podcastLocal,
group: widget.group,
),
);
}).toList(),
children: _group.podcasts.map<Widget>(
(podcastLocal) {
return Container(
decoration:
BoxDecoration(color: Theme.of(context).primaryColor),
key: ObjectKey(podcastLocal.title),
child: _PodcastCard(
podcastLocal: podcastLocal,
group: _group,
),
);
},
).toList(),
),
);
}
@ -310,7 +319,7 @@ class __PodcastCardState extends State<_PodcastCard>
_addGroup = false;
});
await groupList.changeGroup(
widget.podcastLocal.id,
widget.podcastLocal,
_selectedGroups,
);
Fluttertoast.showToast(
@ -530,8 +539,8 @@ class _RenameGroupState extends State<RenameGroup> {
borderRadius: BorderRadius.all(Radius.circular(10))),
elevation: 1,
contentPadding: EdgeInsets.symmetric(horizontal: 20),
titlePadding: EdgeInsets.only(top: 20, left: 20, right: 20, bottom: 20),
actionsPadding: EdgeInsets.all(0),
titlePadding: EdgeInsets.all(20),
actionsPadding: EdgeInsets.zero,
actions: <Widget>[
FlatButton(
splashColor: context.accentColor.withAlpha(70),

View File

@ -195,15 +195,14 @@ class _PodcastManageState extends State<PodcastManage>
),
body: WillPopScope(
onWillPop: () async {
await Provider.of<GroupList>(context, listen: false)
.clearOrderChanged();
await context.read<GroupList>().clearOrderChanged();
return true;
},
child: Consumer<GroupList>(
builder: (_, groupList, __) {
var _isLoading = groupList.isLoading;
// var _isLoading = groupList.isLoading;
var _groups = groupList.groups;
return _isLoading
return _groups.isEmpty
? Center()
: Stack(
children: <Widget>[

View File

@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:convert';
import 'dart:math' as math;
import 'package:audio_service/audio_service.dart';
@ -57,7 +58,6 @@ void _audioPlayerTaskEntrypoint() async {
/// Sleep timer mode.
enum SleepTimerMode { endOfEpisode, timer, undefined }
enum PlayerHeight { short, mid, tall }
//enum ShareStatus { generate, download, complete, undefined, error }
class AudioPlayerNotifier extends ChangeNotifier {
final _dbHelper = DBHelper();
@ -77,15 +77,20 @@ class AudioPlayerNotifier extends ChangeNotifier {
final _volumeGainStorage = KeyValueStorage(volumeGainKey);
final _markListenedAfterSkipStorage =
KeyValueStorage(markListenedAfterSkipKey);
final _playlistsStorgae = KeyValueStorage(playlistsAllKey);
final _playerStateStorage = KeyValueStorage(playerStateKey);
/// Current playing episdoe.
/// Playing episdoe.
EpisodeBrief _episode;
/// Current playlist.
Playlist _queue = Playlist('Playlist');
/// Playlists include queue and playlists created by user.
List<Playlist> _playlists;
/// Notifier for playlist change.
bool _queueUpdate = false;
/// Playing playlist.
Playlist _playlist;
/// Queue is the first playlist by default.
Playlist get _queue => _playlists.first;
/// Player state.
AudioProcessingState _audioState = AudioProcessingState.none;
@ -170,6 +175,20 @@ class AudioPlayerNotifier extends ChangeNotifier {
bool _markListened;
@override
void addListener(VoidCallback listener) {
super.addListener(listener);
_initAudioData();
AudioService.connect();
}
@override
void dispose() async {
await AudioService.disconnect();
super.dispose();
}
/// Audio playing state.
AudioProcessingState get audioState => _audioState;
int get backgroundAudioDuration => _backgroundAudioDuration;
int get backgroundAudioPosition => _backgroundAudioPosition;
@ -177,11 +196,16 @@ class AudioPlayerNotifier extends ChangeNotifier {
String get remoteErrorMessage => _remoteErrorMessage;
bool get playerRunning => _playerRunning;
bool get buffering => _audioState != AudioProcessingState.ready;
int get lastPositin => _lastPostion;
Playlist get queue => _queue;
bool get playing => _playing;
bool get queueUpdate => _queueUpdate;
EpisodeBrief get episode => _episode;
/// Playlist provider.
int get lastPositin => _lastPostion;
List<Playlist> get playlists => _playlists;
Playlist get queue => _playlists.first;
bool get playing => _playing;
Playlist get playlist => _playlist;
/// Player control.
bool get stopOnComplete => _stopOnComplete;
bool get startSleepTimer => _startSleepTimer;
SleepTimerMode get sleepTimerMode => _sleepTimerMode;
@ -213,19 +237,10 @@ class AudioPlayerNotifier extends ChangeNotifier {
_savePlayerHeight();
}
set setVolumeGain(int volumeGain) {
_volumeGain = volumeGain;
if (_playerRunning && _boostVolume) {
setBoostVolume(boostVolume: _boostVolume, gain: _volumeGain);
}
notifyListeners();
_volumeGainStorage.saveInt(volumeGain);
}
Future<void> _initAudioData() async {
var index = await _playerHeightStorage.getInt(defaultValue: 0);
_playerHeight = PlayerHeight.values[index];
_currentSpeed = await _speedStorage.getDoubel(defaultValue: 1.0);
_currentSpeed = await _speedStorage.getDouble(defaultValue: 1.0);
_skipSilence = await _skipSilenceStorage.getBool(defaultValue: false);
_boostVolume = await _boostVolumeStorage.getBool(defaultValue: false);
_volumeGain = await _volumeGainStorage.getInt(defaultValue: 3000);
@ -245,45 +260,73 @@ class AudioPlayerNotifier extends ChangeNotifier {
_autoSleepTimer = i == 1;
}
set setSleepTimerMode(SleepTimerMode timer) {
_sleepTimerMode = timer;
notifyListeners();
}
@override
void addListener(VoidCallback listener) {
super.addListener(listener);
_initAudioData();
AudioService.connect();
}
Future<void> loadPlaylist() async {
await _queue.getPlaylist();
Future<void> initPlaylist() async {
var playlistEntities = await _playlistsStorgae.getPlaylists();
_playlists = [
for (var entity in playlistEntities) Playlist.fromEntity(entity)
];
await _playlists.first.getPlaylist();
await _getAutoPlay();
_lastPostion = await _positionStorage.getInt();
if (_lastPostion > 0 && _queue.playlist.length > 0) {
final episode = _queue.playlist.first;
final duration = episode.duration * 1000;
final seekValue = duration != 0 ? _lastPostion / duration : 1.0;
final history = PlayHistory(
episode.title, episode.enclosureUrl, _lastPostion ~/ 1000, seekValue);
await _dbHelper.saveHistory(history);
var state = await _playerStateStorage.getPlayerState();
if (state[0] != '') {
_playlist = _playlists.firstWhere((p) => p.id == state[0],
orElse: () => _playlists.first);
} else {
_playlist = _playlists.first;
}
var lastWorkStorage = KeyValueStorage(lastWorkKey);
await lastWorkStorage.saveInt(0);
await _playlist.getPlaylist();
if (state[1] != '') {
_episode = await _dbHelper.getRssItemWithUrl(state[1]);
} else {
_episode = _queue.episodes.isNotEmpty ? _queue.episodes.first : null;
}
_lastPostion = int.parse(state[2] ?? '0');
/// Save plays history if app is closed accidentally.
/// if (_lastPostion > 0 && _queue.episodes.isNotEmpty) {
/// final episode = _queue.episodes.first;
/// final duration = episode.duration * 1000;
/// final seekValue = duration != 0 ? _lastPostion / duration : 1.0;
/// final history = PlayHistory(
/// episode.title, episode.enclosureUrl, _lastPostion ~/ 1000, seekValue);
/// await _dbHelper.saveHistory(history);
/// }
await KeyValueStorage(lastWorkKey).saveInt(0);
}
Future<void> playlistLoad() async {
await _queue.getPlaylist();
_backgroundAudioDuration = 0;
_backgroundAudioPosition = 0;
_seekSliderValue = 0;
_episode = _queue.playlist.first;
_queueUpdate = !_queueUpdate;
Future<void> playFromLastPosition() async {
_audioState = AudioProcessingState.none;
_playerRunning = true;
notifyListeners();
_startAudioService(_lastPostion ?? 0, _queue.playlist.first.enclosureUrl);
_startAudioService(_playlist,
position: _lastPostion ?? 0,
index: _playlist.episodes.indexOf(_episode));
}
Future<void> playlistLoad(Playlist playlist) async {
var p = playlist;
if(playlist.name != 'Queue') {
await updatePlaylist(p, updateEpisodes: true);
}
_playlist = p;
notifyListeners();
if (playlist.episodeList.isNotEmpty) {
if (playerRunning) {
AudioService.customAction('setIsQueue', playlist.name == 'Queue');
AudioService.customAction('changeQueue',
[for (var e in p.episodes) jsonEncode(e.toMediaItem().toJson())]);
} else {
_backgroundAudioDuration = 0;
_backgroundAudioPosition = 0;
_seekSliderValue = 0;
_episode = playlist.episodes.first;
_audioState = AudioProcessingState.none;
_playerRunning = true;
notifyListeners();
_startAudioService(_playlist, position: _lastPostion ?? 0, index: 0);
}
}
}
Future<void> episodeLoad(EpisodeBrief episode,
@ -298,13 +341,10 @@ class AudioPlayerNotifier extends ChangeNotifier {
if (startPosition > 0) {
await AudioService.seekTo(Duration(milliseconds: startPosition));
}
_queue.playlist.removeAt(0);
_queue.playlist.removeWhere((item) => item == episode);
_queue.playlist.insert(0, episodeNew);
_queueUpdate != _queueUpdate;
_queue.addToPlayListAt(episodeNew, 0);
await updatePlaylist(_queue);
_remoteErrorMessage = null;
notifyListeners();
await _queue.savePlaylist();
if (episodeNew.isNew == 1) {
await _dbHelper.removeEpisodeNewMark(episodeNew.enclosureUrl);
}
@ -317,11 +357,12 @@ class AudioPlayerNotifier extends ChangeNotifier {
_episode = episodeNew;
_playerRunning = true;
notifyListeners();
_startAudioService(startPosition, episodeNew.enclosureUrl);
_startAudioService(_queue, position: startPosition);
}
}
Future<void> _startAudioService(int position, String url) async {
Future<void> _startAudioService(playlist,
{int index = 0, int position = 0}) async {
_stopOnComplete = false;
_sleepTimerMode = SleepTimerMode.undefined;
_switchValue = 0;
@ -343,6 +384,7 @@ class AudioPlayerNotifier extends ChangeNotifier {
/// Start audio service.
await AudioService.start(
backgroundTaskEntrypoint: _audioPlayerTaskEntrypoint,
params: {'index': index, 'isQueue': playlist.name == 'Queue'},
androidNotificationChannelName: 'Tsacdop',
androidNotificationColor: 0xFF4d91be,
androidNotificationIcon: 'drawable/ic_notification',
@ -354,11 +396,11 @@ class AudioPlayerNotifier extends ChangeNotifier {
//Check autoplay setting, if true only add one episode, else add playlist.
await _getAutoPlay();
if (_autoPlay) {
for (var episode in _queue.playlist) {
for (var episode in playlist.episodes) {
await AudioService.addQueueItem(episode.toMediaItem());
}
} else {
await AudioService.addQueueItem(_queue.playlist.first.toMediaItem());
await AudioService.addQueueItem(playlist.episodes[index].toMediaItem());
}
//Check auto sleep timer setting
await _getAutoSleepTimer();
@ -408,7 +450,7 @@ class AudioPlayerNotifier extends ChangeNotifier {
_backgroundAudioDuration = item.duration.inMilliseconds ?? 0;
if (position > 0 &&
_backgroundAudioDuration > 0 &&
_episode.enclosureUrl == url) {
_episode.enclosureUrl == _playlist.episodeList[index]) {
await AudioService.seekTo(Duration(milliseconds: position));
position = 0;
}
@ -445,13 +487,17 @@ class AudioPlayerNotifier extends ChangeNotifier {
});
AudioService.customEventStream.distinct().listen((event) async {
if (event is String &&
_queue.playlist.isNotEmpty &&
_queue.playlist.first.title == event) {
_queue.delFromPlaylist(_episode);
if (event is String) {
if (_playlist.name == 'Queue' &&
_queue.episodes.isNotEmpty &&
_queue.episodes.first.title == event) {
_queue.delFromPlaylist(_episode);
}
_lastPostion = 0;
notifyListeners();
await _positionStorage.saveInt(_lastPostion);
// await _positionStorage.saveInt(_lastPostion);
await _playerStateStorage.savePlayerState(
_playlist.id, _episode.enclosureUrl, _lastPostion);
var history;
if (_markListened) {
history = PlayHistory(_episode.title, _episode.enclosureUrl,
@ -501,7 +547,9 @@ class AudioPlayerNotifier extends ChangeNotifier {
if (_backgroundAudioPosition > 0 &&
_backgroundAudioPosition < _backgroundAudioDuration) {
_lastPostion = _backgroundAudioPosition;
_positionStorage.saveInt(_lastPostion);
// _positionStorage.saveInt(_lastPostion);
_playerStateStorage.savePlayerState(
_playlist.id, _episode.enclosureUrl, _lastPostion);
}
notifyListeners();
}
@ -511,33 +559,31 @@ class AudioPlayerNotifier extends ChangeNotifier {
});
}
Future<void> playNext() async {
_remoteErrorMessage = null;
await AudioService.skipToNext();
_queueUpdate = !_queueUpdate;
notifyListeners();
}
/// Playlists management.
Future<void> addToPlaylist(EpisodeBrief episode) async {
var episodeNew = await _dbHelper.getRssItemWithUrl(episode.enclosureUrl);
if (!_queue.playlist.contains(episodeNew)) {
if (playerRunning) {
if (episodeNew.isNew == 1) {
await _dbHelper.removeEpisodeNewMark(episodeNew.enclosureUrl);
}
if (!_queue.episodes.contains(episodeNew)) {
if (playerRunning && _playlist.name == 'Queue') {
await AudioService.addQueueItem(episodeNew.toMediaItem());
}
await _queue.addToPlayList(episodeNew);
_queueUpdate = !_queueUpdate;
notifyListeners();
_queue.addToPlayList(episodeNew);
await updatePlaylist(_queue, updateEpisodes: false);
}
}
Future<void> addToPlaylistAt(EpisodeBrief episode, int index) async {
var episodeNew = await _dbHelper.getRssItemWithUrl(episode.enclosureUrl);
if (playerRunning) {
if (episodeNew.isNew == 1) {
await _dbHelper.removeEpisodeNewMark(episodeNew.enclosureUrl);
}
if (playerRunning && _playlist.name == 'Queue') {
await AudioService.addQueueItemAt(episodeNew.toMediaItem(), index);
}
await _queue.addToPlayListAt(episodeNew, index);
_queueUpdate = !_queueUpdate;
notifyListeners();
_queue.addToPlayListAt(episodeNew, index);
await updatePlaylist(_queue, updateEpisodes: false);
}
Future<void> addNewEpisode(List<String> group) async {
@ -560,40 +606,34 @@ class AudioPlayerNotifier extends ChangeNotifier {
}
Future<void> updateMediaItem(EpisodeBrief episode) async {
if (episode.enclosureUrl == episode.mediaId) {
var index = _queue.playlist
.indexWhere((item) => item.enclosureUrl == episode.enclosureUrl);
if (index > 0) {
var episodeNew =
await _dbHelper.getRssItemWithUrl(episode.enclosureUrl);
await delFromPlaylist(episode);
await addToPlaylistAt(episodeNew, index);
}
if (episode.enclosureUrl == episode.mediaId && _episode != episode) {
var episodeNew = await _dbHelper.getRssItemWithUrl(episode.enclosureUrl);
_playlist.updateEpisode(episodeNew);
}
}
Future<int> delFromPlaylist(EpisodeBrief episode) async {
var episodeNew = await _dbHelper.getRssItemWithUrl(episode.enclosureUrl);
if (playerRunning) {
if (playerRunning && _playlist.name == 'Queue') {
await AudioService.removeQueueItem(episodeNew.toMediaItem());
}
var index = await _queue.delFromPlaylist(episodeNew);
var index = _queue.delFromPlaylist(episodeNew);
if (index == 0) {
_lastPostion = 0;
await _positionStorage.saveInt(0);
}
_queueUpdate = !_queueUpdate;
notifyListeners();
updatePlaylist(_queue, updateEpisodes: false);
return index;
}
Future reorderPlaylist(int oldIndex, int newIndex) async {
var episode = _queue.playlist[oldIndex];
if (playerRunning) {
var episode = _queue.episodes[oldIndex];
if (playerRunning && _playlist.name == 'Queue') {
await AudioService.removeQueueItem(episode.toMediaItem());
await AudioService.addQueueItemAt(episode.toMediaItem(), newIndex);
}
await _queue.addToPlayListAt(episode, newIndex);
_queue.addToPlayListAt(episode, newIndex);
updatePlaylist(_queue, updateEpisodes: false);
if (newIndex == 0) {
_lastPostion = 0;
await _positionStorage.saveInt(0);
@ -602,21 +642,102 @@ class AudioPlayerNotifier extends ChangeNotifier {
Future<bool> moveToTop(EpisodeBrief episode) async {
await delFromPlaylist(episode);
if (playerRunning) {
if (playerRunning && _playlist.name == 'Queue') {
final episodeNew =
await _dbHelper.getRssItemWithUrl(episode.enclosureUrl);
await AudioService.addQueueItemAt(episodeNew.toMediaItem(), 1);
await _queue.addToPlayListAt(episode, 1, existed: false);
_queue.addToPlayListAt(episode, 1, existed: false);
} else {
await _queue.addToPlayListAt(episode, 0, existed: false);
_queue.addToPlayListAt(episode, 0, existed: false);
_lastPostion = 0;
_positionStorage.saveInt(_lastPostion);
}
_queueUpdate = !_queueUpdate;
notifyListeners();
updatePlaylist(_queue, updateEpisodes: false);
return true;
}
void addPlaylist(Playlist playlist) {
_playlists = [..._playlists, playlist];
notifyListeners();
_savePlaylists();
}
void deletePlaylist(Playlist playlist) {
_playlists = [
for (var p in _playlists)
if (p != playlist) p
];
notifyListeners();
_savePlaylists();
}
void addEpisodesToPlaylist(Playlist playlist, {List<EpisodeBrief> episodes}) {
for (var e in episodes) {
playlist.addToPlayList(e);
if (playerRunning && playlist == _playlist) {
AudioService.addQueueItem(e.toMediaItem());
}
}
updatePlaylist(playlist, updateEpisodes: false);
}
void removeEpisodeFromPlaylist(Playlist playlist,
{List<EpisodeBrief> episodes}) {
for (var e in episodes) {
playlist.delFromPlaylist(e);
if (playerRunning && playlist == _playlist) {
AudioService.removeQueueItem(e.toMediaItem());
}
}
updatePlaylist(playlist, updateEpisodes: false);
}
void reorderEpisodesInPlaylist(Playlist playlist,
{int oldIndex, int newIndex}) async {
playlist.reorderPlaylist(oldIndex, newIndex);
if (playerRunning && playlist == _playlist) {
if (newIndex > oldIndex) {
newIndex -= 1;
}
await AudioService.removeQueueItem(episode.toMediaItem());
await AudioService.addQueueItemAt(episode.toMediaItem(), newIndex);
}
updatePlaylist(playlist, updateEpisodes: false);
}
void clearPlaylist(Playlist playlist) {
playlist.clear();
updatePlaylist(playlist, updateEpisodes: false);
}
Future<void> updatePlaylist(Playlist playlist,
{bool updateEpisodes = true}) async {
if (updateEpisodes) await playlist.getPlaylist();
_playlists = [for (var p in _playlists) p.id == playlist.id ? playlist : p];
notifyListeners();
_savePlaylists();
}
bool playlistExisted(String name) {
for (var p in _playlists) {
if (p.name == name) return true;
}
return false;
}
void _updateAllPlaylists() {
_playlists = [..._playlists];
notifyListeners();
_savePlaylists();
}
Future<void> _savePlaylists() async {
await _playlistsStorgae
.savePlaylists([for (var p in _playlists) p.toEntity()]);
}
/// Audio control.
Future<void> pauseAduio() async {
await AudioService.pause();
}
@ -630,6 +751,12 @@ class AudioPlayerNotifier extends ChangeNotifier {
}
}
Future<void> playNext() async {
_remoteErrorMessage = null;
await AudioService.skipToNext();
notifyListeners();
}
Future<void> forwardAudio(int s) async {
var pos = _backgroundAudioPosition + s * 1000;
await AudioService.seekTo(Duration(milliseconds: pos));
@ -670,6 +797,7 @@ class AudioPlayerNotifier extends ChangeNotifier {
notifyListeners();
}
/// Set skip silence.
Future<void> setSkipSilence({@required bool skipSilence}) async {
await AudioService.customAction('setSkipSilence', skipSilence);
_skipSilence = skipSilence;
@ -677,6 +805,15 @@ class AudioPlayerNotifier extends ChangeNotifier {
notifyListeners();
}
set setVolumeGain(int volumeGain) {
_volumeGain = volumeGain;
if (_playerRunning && _boostVolume) {
setBoostVolume(boostVolume: _boostVolume, gain: _volumeGain);
}
notifyListeners();
_volumeGainStorage.saveInt(volumeGain);
}
Future<void> setBoostVolume({@required bool boostVolume, int gain}) async {
await AudioService.customAction(
'setBoostVolume', [boostVolume, _volumeGain]);
@ -715,12 +852,17 @@ class AudioPlayerNotifier extends ChangeNotifier {
_stopOnComplete = true;
_switchValue = 1;
notifyListeners();
if (_queue.playlist.length > 1 && _autoPlay) {
if (_queue.episodes.length > 1 && _autoPlay) {
AudioService.customAction('stopAtEnd');
}
}
}
set setSleepTimerMode(SleepTimerMode timer) {
_sleepTimerMode = timer;
notifyListeners();
}
//Cancel sleep timer
void cancelTimer() {
if (_sleepTimerMode == SleepTimerMode.timer) {
@ -737,11 +879,8 @@ class AudioPlayerNotifier extends ChangeNotifier {
}
}
@override
void dispose() async {
await AudioService.disconnect();
super.dispose();
}
/// Save player state.
Future<void> _savePlayerPosiont() {}
}
class AudioPlayerTask extends BackgroundAudioTask {
@ -755,9 +894,11 @@ class AudioPlayerTask extends BackgroundAudioTask {
bool _interrupted = false;
bool _stopAtEnd;
int _cacheMax;
int _index = 0;
bool _isQueue;
bool get hasNext => _queue.length > 0;
MediaItem get mediaItem => _queue.length > 0 ? _queue.first : null;
MediaItem get mediaItem => _queue.length > 0 ? _queue[_index] : null;
StreamSubscription<AudioPlaybackState> _playerStateSubscription;
StreamSubscription<AudioPlaybackEvent> _eventSubscription;
@ -766,6 +907,8 @@ class AudioPlayerTask extends BackgroundAudioTask {
Future<void> onStart(Map<String, dynamic> params) async {
_stopAtEnd = false;
_session = await AudioSession.instance;
_index = params['index'] ?? 0;
_isQueue = params['isQueue'] ?? true;
await _session.configure(AudioSessionConfiguration.speech());
_handleInterruption(_session);
_playerStateSubscription = _audioPlayer.playbackStateStream
@ -876,19 +1019,23 @@ class AudioPlayerTask extends BackgroundAudioTask {
_skipState = AudioProcessingState.skippingToNext;
_playing = false;
await _audioPlayer.stop();
if (_queue.length > 0) {
AudioServiceBackground.sendCustomEvent(_queue.first.title);
_queue.removeAt(0);
AudioServiceBackground.sendCustomEvent(_queue.first.title);
if (_isQueue) {
if (_queue.length > 0) {
_queue.removeAt(0);
}
await AudioServiceBackground.setQueue(_queue);
} else {
_index += 1;
}
await AudioServiceBackground.setQueue(_queue);
if (_queue.length == 0 || _stopAtEnd) {
_skipState = null;
await Future.delayed(Duration(milliseconds: 200));
await onStop();
} else {
await AudioServiceBackground.setQueue(_queue);
// await AudioServiceBackground.setQueue(_queue);
await AudioServiceBackground.setMediaItem(mediaItem);
await _audioPlayer.setUrl(mediaItem.id, cacheMax: _cacheMax);
var duration = await _audioPlayer.durationFuture;
if (duration != null) {
@ -1017,13 +1164,15 @@ class AudioPlayerTask extends BackgroundAudioTask {
@override
Future<void> onRemoveQueueItem(MediaItem mediaItem) async {
var index = _queue.indexOf(mediaItem);
if (index < _index) _index -= 1;
_queue.removeWhere((item) => item.id == mediaItem.id);
await AudioServiceBackground.setQueue(_queue);
}
@override
Future<void> onAddQueueItemAt(MediaItem mediaItem, int index) async {
if (index == 0) {
if (index == 0 && _isQueue) {
await _audioPlayer.stop();
_queue.removeAt(0);
_queue.removeWhere((item) => item.id == mediaItem.id);
@ -1038,6 +1187,7 @@ class AudioPlayerTask extends BackgroundAudioTask {
//onPlay();
} else {
_queue.insert(index, mediaItem);
if (index < _index) _index += 1;
await AudioServiceBackground.setQueue(_queue);
}
}
@ -1070,9 +1220,28 @@ class AudioPlayerTask extends BackgroundAudioTask {
case 'setBoostVolume':
await _setBoostVolume(argument[0], argument[1]);
break;
case 'setIsQueue':
_isQueue = argument;
break;
case 'changeQueue':
await _changeQueue(argument);
}
}
Future _changeQueue(List<dynamic> items) async {
var queue = [for (var i in items) MediaItem.fromJson(json.decode(i))];
await _audioPlayer.stop();
_queue.clear();
_queue.addAll(queue);
_index = 0;
await AudioServiceBackground.setQueue(_queue);
await AudioServiceBackground.setMediaItem(mediaItem);
await _audioPlayer.setUrl(mediaItem.id, cacheMax: _cacheMax);
var duration = await _audioPlayer.durationFuture ?? Duration.zero;
AudioServiceBackground.setMediaItem(mediaItem.copyWith(duration: duration));
_playFromStart();
}
Future _setSkipSilence(bool boo) async {
await _audioPlayer.setSkipSilence(boo);
var duration = await _audioPlayer.durationFuture ?? Duration.zero;

View File

@ -66,22 +66,32 @@ class PodcastGroup extends Equatable {
final String color;
/// Id lists of podcasts in group.
List<String> podcastList;
final List<String> podcastList;
final List<PodcastLocal> podcasts;
PodcastGroup(this.name,
{this.color = '#000000', String id, List<String> podcastList})
{this.color = '#000000',
String id,
List<String> podcastList,
List<PodcastLocal> podcasts})
: id = id ?? Uuid().v4(),
podcastList = podcastList ?? [];
podcastList = podcastList ?? [],
podcasts = podcasts ?? [];
final _dbHelper = DBHelper();
Future<void> getPodcasts() async {
var dbHelper = DBHelper();
if (podcastList != []) {
podcasts.clear();
if (podcastList.isNotEmpty) {
try {
_podcasts = await dbHelper.getPodcastLocal(podcastList);
var result = await _dbHelper.getPodcastLocal(podcastList);
if (podcasts.isEmpty) podcasts.addAll(result);
} catch (e) {
await Future.delayed(Duration(milliseconds: 200));
try {
_podcasts = await dbHelper.getPodcastLocal(podcastList);
var result = await _dbHelper.getPodcastLocal(podcastList);
if (podcasts.isEmpty) podcasts.addAll(result);
} catch (e) {
developer.log(e.toString());
}
@ -89,6 +99,45 @@ class PodcastGroup extends Equatable {
}
}
Future<PodcastGroup> updatePodcast(PodcastLocal podcast) async {
var count = await _dbHelper.getPodcastCounts(podcast.id);
var list = [
for (var p in podcasts)
p == podcast ? podcast.copyWith(updateCount: count) : p
];
return PodcastGroup(name,
id: id, color: color, podcastList: podcastList, podcasts: list);
}
void reorderGroup(int oldIndex, int newIndex) {
if (newIndex > oldIndex) {
newIndex -= 1;
}
final podcast = podcasts.removeAt(oldIndex);
podcasts.insert(newIndex, podcast);
podcastList.removeAt(oldIndex);
podcastList.insert(newIndex, podcast.id);
}
void addToGroup(PodcastLocal podcast) {
if (!podcasts.contains(podcast)) {
podcasts.add(podcast);
podcastList.add(podcast.id);
}
}
void addToGroupAt(PodcastLocal podcast, {int index = 0}) {
if (!podcasts.contains(podcast)) {
podcasts.insert(index, podcast);
podcastList.insert(index, podcast.id);
}
}
void deleteFromGroup(PodcastLocal podcast) {
podcasts.remove(podcast);
podcastList.remove(podcast.id);
}
Color getColor() {
if (color != '#000000') {
var colorInt = int.parse('FF${color.toUpperCase()}', radix: 16);
@ -98,15 +147,11 @@ class PodcastGroup extends Equatable {
}
}
///Podcast in group.
List<PodcastLocal> _podcasts;
List<PodcastLocal> get podcasts => _podcasts;
///Ordered podcast list.
List<PodcastLocal> _orderedPodcasts;
List<PodcastLocal> get orderedPodcasts => _orderedPodcasts;
//List<PodcastLocal> _orderedPodcasts;
//List<PodcastLocal> get orderedPodcasts => _orderedPodcasts;
set orderedPodcasts(list) => _orderedPodcasts = list;
//set orderedPodcasts(list) => _orderedPodcasts = list;
GroupEntity toEntity() {
return GroupEntity(name, id, color, podcastList);
@ -117,7 +162,7 @@ class PodcastGroup extends Equatable {
entity.name,
id: entity.id,
color: entity.color,
podcastList: entity.podcastList,
podcastList: entity.podcastList.toSet().toList(),
);
}
@ -158,25 +203,12 @@ class SubscribeItem {
class GroupList extends ChangeNotifier {
/// List of all gourps.
final List<PodcastGroup> _groups = [];
List<PodcastGroup> _groups = [];
List<PodcastGroup> get groups => _groups;
final DBHelper _dbHelper = DBHelper();
/// Groups save in shared_prefrences.
final KeyValueStorage _groupStorage = KeyValueStorage(groupsKey);
//GroupList({List<PodcastGroup> groups}) : _groups = groups ?? [];
/// Default false, true during loading groups from storage.
bool _isLoading = false;
bool get isLoading => _isLoading;
/// Svae ordered gourps info before saved.
final List<PodcastGroup> _orderChanged = [];
List<PodcastGroup> _orderChanged = [];
List<PodcastGroup> get orderChanged => _orderChanged;
//GroupList({List<PodcastGroup> groups}) : _groups = groups ?? [];
/// Subscribe worker isolate
FlutterIsolate subIsolate;
@ -187,11 +219,31 @@ class GroupList extends ChangeNotifier {
SubscribeItem _currentSubscribeItem = SubscribeItem('', '');
SubscribeItem get currentSubscribeItem => _currentSubscribeItem;
bool _created = false;
/// Default false, true if subscribe isolate is created.
bool _created = false;
bool get created => _created;
final DBHelper _dbHelper = DBHelper();
/// Groups save in shared_prefrences.
final KeyValueStorage _groupStorage = KeyValueStorage(groupsKey);
@override
void addListener(VoidCallback listener) {
if (_groups.isEmpty) {
loadGroups().then((value) => super.addListener(listener));
gpodderSyncNow();
}
}
@override
void dispose() {
subIsolate?.kill();
subIsolate = null;
super.dispose();
}
/// Subscribe podcast via isolate.
/// Add subsribe item
SubscribeItem _subscribeItem;
setSubscribeItem(SubscribeItem item, {bool syncGpodder = true}) async {
@ -283,7 +335,8 @@ class GroupList extends ChangeNotifier {
for (var rssLink in removeList) {
final exist = await _dbHelper.checkPodcast(rssLink);
if (exist != '') {
await _unsubscribe(exist);
var podcast = await _dbHelper.getPodcastWithUrl(rssLink);
await _unsubscribe(podcast);
}
}
await _remoteAddStorage.clearList();
@ -322,146 +375,118 @@ class GroupList extends ChangeNotifier {
developer.log('work job cancelled');
}
void addToOrderChanged(PodcastGroup group) {
_orderChanged.add(group);
notifyListeners();
}
void drlFromOrderChanged(String name) {
_orderChanged.removeWhere((group) => group.name == name);
notifyListeners();
}
Future<void> clearOrderChanged() async {
if (_orderChanged.length > 0) {
for (var group in _orderChanged) {
await group.getPodcasts();
}
_orderChanged.clear();
// notifyListeners();
}
}
@override
void addListener(VoidCallback listener) {
loadGroups().then((value) => super.addListener(listener));
gpodderSyncNow();
}
@override
void dispose() {
subIsolate?.kill();
subIsolate = null;
super.dispose();
}
/// Mange groups states in app.
/// Load groups from storage at start.
Future<void> loadGroups() async {
_isLoading = true;
notifyListeners();
_groupStorage.getGroups().then((loadgroups) async {
_groups.addAll(loadgroups.map(PodcastGroup.fromEntity));
for (var group in _groups) {
await group.getPodcasts();
}
_isLoading = false;
_groups = [...groups];
notifyListeners();
});
}
void addToOrderChanged(PodcastGroup group) {
if (_orderChanged.contains(group)) {
_orderChanged = [for (var g in _orderChanged) g == group ? group : g];
} else {
_orderChanged = [..._orderChanged, group];
}
notifyListeners();
}
void drlFromOrderChanged(String name) {
_orderChanged = [
for (var group in _orderChanged)
if (group.name != name) group
];
notifyListeners();
}
void clearOrderChanged() {
_orderChanged.clear();
}
/// Update podcasts of each group
Future<void> updateGroups() async {
for (var group in _groups) {
await group.getPodcasts();
}
_groups = [..._groups];
notifyListeners();
}
/// Add new group.
Future<void> addGroup(PodcastGroup podcastGroup) async {
_isLoading = true;
_groups.add(podcastGroup);
await _saveGroup();
_isLoading = false;
_groups = [..._groups, podcastGroup];
notifyListeners();
await _saveGroup();
}
/// Remove group.
Future<void> delGroup(PodcastGroup podcastGroup) async {
_isLoading = true;
for (var podcast in podcastGroup.podcastList) {
if (!_groups.first.podcastList.contains(podcast)) {
_groups[0].podcastList.insert(0, podcast);
for (var podcast in podcastGroup.podcasts) {
if (!_groups.first.podcasts.contains(podcast)) {
_groups.first.addToGroup(podcast);
}
}
await _saveGroup();
_groups.remove(podcastGroup);
await _groups[0].getPodcasts();
_isLoading = false;
_groups = [
for (var group in _groups)
if (group.id != podcastGroup) group
];
notifyListeners();
await _saveGroup();
}
Future<void> updateGroup(PodcastGroup podcastGroup) async {
var oldGroup = _groups.firstWhere((it) => it.id == podcastGroup.id);
var index = _groups.indexOf(oldGroup);
_groups.replaceRange(index, index + 1, [podcastGroup]);
await podcastGroup.getPodcasts();
_groups = [
for (var group in _groups) group == podcastGroup ? podcastGroup : group
];
notifyListeners();
_saveGroup();
}
Future<void> _updateGroups() async {
_groups = [..._groups];
notifyListeners();
await _saveGroup();
}
Future<void> _saveGroup() async {
await _groupStorage.saveGroup(_groups.map((it) => it.toEntity()).toList());
}
/// Subscribe podcast from search result.
Future subscribe(PodcastLocal podcastLocal) async {
_groups[0].podcastList.insert(0, podcastLocal.id);
await _saveGroup();
await _dbHelper.savePodcastLocal(podcastLocal);
await _groups[0].getPodcasts();
notifyListeners();
}
Future updatePodcast(String id) async {
var counts = await _dbHelper.getPodcastCounts(id);
for (var group in _groups) {
if (group.podcastList.contains(id)) {
group.podcasts.firstWhere((podcast) => podcast.id == id)
..episodeCount = counts;
notifyListeners();
}
}
Future subscribe(PodcastLocal podcast) async {
await _dbHelper.savePodcastLocal(podcast);
_groups.first.addToGroupAt(podcast);
_updateGroups();
}
/// Subscribe podcast from OPML.
Future<bool> _subscribeNewPodcast(
{String id, String groupName = 'Home'}) async {
//List<String> groupNames = _groups.map((e) => e.name).toList();
var podcasts = await _dbHelper.getPodcastLocal([id]);
for (var group in _groups) {
if (group.name == groupName) {
if (group.podcastList.contains(id)) {
return true;
} else {
_isLoading = true;
notifyListeners();
group.podcastList.insert(0, id);
await _saveGroup();
await group.getPodcasts();
_isLoading = false;
notifyListeners();
group.addToGroupAt(podcasts.first);
_updateGroups();
return true;
}
}
}
_isLoading = true;
_groups = [
..._groups,
PodcastGroup(groupName, podcastList: [id], podcasts: podcasts)
];
notifyListeners();
_groups.add(PodcastGroup(groupName, podcastList: [id]));
//_groups.last.podcastList.insert(0, id);
await _saveGroup();
await _groups.last.getPodcasts();
_isLoading = false;
notifyListeners();
return true;
}
@ -476,26 +501,19 @@ class GroupList extends ChangeNotifier {
}
//Change podcast groups
Future<void> changeGroup(String id, List<PodcastGroup> list) async {
_isLoading = true;
notifyListeners();
for (var group in getPodcastGroup(id)) {
Future<void> changeGroup(
PodcastLocal podcast, List<PodcastGroup> list) async {
for (var group in getPodcastGroup(podcast.id)) {
if (list.contains(group)) {
list.remove(group);
} else {
group.podcastList.remove(id);
group.deleteFromGroup(podcast);
}
}
for (var s in list) {
s.podcastList.insert(0, id);
s.addToGroup(podcast);
}
await _saveGroup();
for (var group in _groups) {
await group.getPodcasts();
}
_isLoading = false;
notifyListeners();
_updateGroups();
}
/// Unsubscribe podcast
@ -506,35 +524,32 @@ class GroupList extends ChangeNotifier {
}
}
Future<void> _unsubscribe(String id) async {
_isLoading = true;
notifyListeners();
Future<void> _unsubscribe(PodcastLocal podcast) async {
for (var group in _groups) {
group.podcastList.remove(id);
group.deleteFromGroup(podcast);
}
await _saveGroup();
await _dbHelper.delPodcastLocal(id);
for (var group in _groups) {
await group.getPodcasts();
}
_isLoading = false;
notifyListeners();
_updateGroups();
await _dbHelper.delPodcastLocal(podcast.id);
}
/// Delete podcsat from device.
Future<void> removePodcast(
PodcastLocal podcast,
) async {
_syncRemove(podcast.rssUrl);
await _unsubscribe(podcast.id);
await _unsubscribe(podcast);
await File(podcast.imagePath)?.delete();
}
Future<void> saveOrder(PodcastGroup group) async {
group.podcastList = group.orderedPodcasts.map((e) => e.id).toList();
await _saveGroup();
await group.getPodcasts();
// group.podcastList = group.orderedPodcasts.map((e) => e.id).toList();
var orderedGroup;
for (var g in _orderChanged) {
if (g == group) orderedGroup = g;
}
_groups = [for (var g in _groups) g == orderedGroup ? orderedGroup : g];
notifyListeners();
await _saveGroup();
}
}

View File

@ -48,6 +48,7 @@ class RefreshWorker extends ChangeNotifier {
_currentRefreshItem = RefreshItem('', RefreshState.none);
_complete = true;
notifyListeners();
_complete = false;
refreshIsolate?.kill();
refreshIsolate = null;
_created = false;

View File

@ -1,6 +1,6 @@
import 'package:equatable/equatable.dart';
import 'package:uuid/uuid.dart';
import '../local_storage/key_value_storage.dart';
import '../local_storage/sqflite_localpodcast.dart';
import 'episodebrief.dart';
@ -21,7 +21,7 @@ class PlaylistEntity {
}
}
class Playlist {
class Playlist extends Equatable {
/// Playlist name. the default playlist is named "Playlist".
final String name;
@ -31,12 +31,19 @@ class Playlist {
/// Episode url list for playlist.
final List<String> episodeList;
Playlist(this.name, {String id, List<String> episodeList})
/// Eposides in playlist.
final List<EpisodeBrief> episodes;
bool get isEmpty => episodeList.isEmpty;
Playlist(this.name,
{String id, List<String> episodeList, List<EpisodeBrief> episodes})
: id = id ?? Uuid().v4(),
episodeList = episodeList ?? [];
episodeList = episodeList ?? [],
episodes = episodes ?? [];
PlaylistEntity toEntity() {
return PlaylistEntity(name, id, episodeList);
return PlaylistEntity(name, id, episodeList.toSet().toList());
}
static Playlist fromEntity(PlaylistEntity entity) {
@ -48,58 +55,70 @@ class Playlist {
}
final DBHelper _dbHelper = DBHelper();
List<EpisodeBrief> _playlist = [];
List<EpisodeBrief> get playlist => _playlist;
final KeyValueStorage _playlistStorage = KeyValueStorage(playlistKey);
// final KeyValueStorage _playlistStorage = KeyValueStorage(playlistKey);
Future<void> getPlaylist() async {
var urls = await _playlistStorage.getStringList();
episodeList.addAll(urls);
if (urls.length == 0) {
_playlist = [];
} else {
_playlist = [];
for (var url in urls) {
episodes.clear();
if (episodeList.isNotEmpty) {
for (var url in episodeList) {
print(url);
var episode = await _dbHelper.getRssItemWithUrl(url);
if (episode != null) _playlist.add(episode);
if (episode != null) episodes.add(episode);
}
}
}
Future<void> savePlaylist() async {
var urls = <String>[];
urls.addAll(_playlist.map((e) => e.enclosureUrl));
await _playlistStorage.saveStringList(urls.toSet().toList());
}
// Future<void> savePlaylist() async {
// var urls = <String>[];
// urls.addAll(_playlist.map((e) => e.enclosureUrl));
// await _playlistStorage.saveStringList(urls.toSet().toList());
// }
Future<void> addToPlayList(EpisodeBrief episodeBrief) async {
if (!_playlist.contains(episodeBrief)) {
_playlist.add(episodeBrief);
await savePlaylist();
if (episodeBrief.isNew == 1) {
await _dbHelper.removeEpisodeNewMark(episodeBrief.enclosureUrl);
}
void addToPlayList(EpisodeBrief episodeBrief) {
if (!episodes.contains(episodeBrief)) {
episodes.add(episodeBrief);
episodeList.add(episodeBrief.enclosureUrl);
}
}
Future<void> addToPlayListAt(EpisodeBrief episodeBrief, int index,
{bool existed = true}) async {
void addToPlayListAt(EpisodeBrief episodeBrief, int index,
{bool existed = true}) {
if (existed) {
_playlist.removeWhere(
(episode) => episode.enclosureUrl == episodeBrief.enclosureUrl);
if (episodeBrief.isNew == 1) {
await _dbHelper.removeEpisodeNewMark(episodeBrief.enclosureUrl);
}
episodes.removeWhere((episode) => episode == episodeBrief);
episodeList.removeWhere((url) => url == episodeBrief.enclosureUrl);
}
_playlist.insert(index, episodeBrief);
await savePlaylist();
episodes.insert(index, episodeBrief);
episodeList.insert(index, episodeBrief.enclosureUrl);
}
Future<int> delFromPlaylist(EpisodeBrief episodeBrief) async {
var index = _playlist.indexOf(episodeBrief);
_playlist.removeWhere(
void updateEpisode(EpisodeBrief episode) {
var index = episodes.indexOf(episode);
if (index != -1) episodes[index] = episode;
}
int delFromPlaylist(EpisodeBrief episodeBrief) {
var index = episodes.indexOf(episodeBrief);
episodes.removeWhere(
(episode) => episode.enclosureUrl == episodeBrief.enclosureUrl);
await savePlaylist();
episodeList.removeWhere((url) => url == episodeBrief.enclosureUrl);
return index;
}
void reorderPlaylist(int oldIndex, int newIndex) {
if (newIndex > oldIndex) {
newIndex -= 1;
}
final episode = episodes.removeAt(oldIndex);
episodes.insert(newIndex, episode);
episodeList.removeAt(oldIndex);
episodeList.insert(newIndex, episode.enclosureUrl);
}
void clear() {
episodeList.clear();
episodes.clear();
}
@override
List<Object> get props => [id, name];
}

View File

@ -19,20 +19,19 @@ class PodcastLocal extends Equatable {
final String description;
int _upateCount;
int get updateCount => _upateCount;
set updateCount(i) => _upateCount = i;
final int updateCount;
final int episodeCount;
int _episodeCount;
int get episodeCount => _episodeCount;
set episodeCount(i) => _episodeCount = i;
//set setUpdateCount(i) => updateCount = i;
//set setEpisodeCount(i) => episodeCount = i;
PodcastLocal(this.title, this.imageUrl, this.rssUrl, this.primaryColor,
this.author, this.id, this.imagePath, this.provider, this.link,
{this.description = '', int upateCount, int episodeCount})
{this.description = '', int updateCount, int episodeCount})
: assert(rssUrl != null),
_episodeCount = episodeCount ?? 0,
_upateCount = upateCount ?? 0;
episodeCount = episodeCount ?? 0,
updateCount = updateCount ?? 0;
ImageProvider get avatarImage {
return File(imagePath).existsSync()
@ -46,6 +45,14 @@ class PodcastLocal extends Equatable {
: primaryColor.colorizeLight();
}
PodcastLocal copyWith({int updateCount, int episodeCount}) {
return PodcastLocal(title, imageUrl, rssUrl, primaryColor, author, id,
imagePath, provider, link,
description: description,
updateCount: updateCount ?? 0,
episodeCount: episodeCount ?? 0);
}
@override
List<Object> get props => [id, rssUrl];
}

View File

@ -1220,3 +1220,19 @@ class CustomBackButton extends StatelessWidget {
);
}
}
// Episode tag widget.
Widget episodeTag(String text, Color color) {
if (text == '') {
return Center();
}
return Container(
decoration:
BoxDecoration(color: color, borderRadius: BorderRadius.circular(15.0)),
height: 25.0,
margin: EdgeInsets.only(right: 10.0),
padding: EdgeInsets.symmetric(horizontal: 8.0),
alignment: Alignment.center,
child: Text(text, style: TextStyle(fontSize: 14.0, color: Colors.black)),
);
}

View File

@ -0,0 +1,179 @@
import 'package:flutter/material.dart';
import 'package:line_icons/line_icons.dart';
import 'package:provider/provider.dart';
import '../state/audio_state.dart';
import '../type/episodebrief.dart';
import '../util/extension_helper.dart';
import 'custom_widget.dart';
class DismissibleContainer extends StatefulWidget {
final EpisodeBrief episode;
final ValueChanged<bool> onRemove;
DismissibleContainer({this.episode, this.onRemove, Key key})
: super(key: key);
@override
_DismissibleContainerState createState() => _DismissibleContainerState();
}
class _DismissibleContainerState extends State<DismissibleContainer> {
bool _delete;
@override
void initState() {
_delete = false;
super.initState();
}
@override
Widget build(BuildContext context) {
final s = context.s;
final c = widget.episode.backgroudColor(context);
return AnimatedContainer(
duration: Duration(milliseconds: 300),
curve: Curves.easeInSine,
alignment: Alignment.center,
height: _delete ? 0 : 90.0,
child: _delete
? Container(
color: Colors.transparent,
)
: Dismissible(
key: ValueKey('${widget.episode.enclosureUrl}dis'),
background: Container(
padding: EdgeInsets.symmetric(horizontal: 20.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Container(
decoration: BoxDecoration(
shape: BoxShape.circle, color: Colors.red),
padding: EdgeInsets.all(5),
alignment: Alignment.center,
child: Icon(
LineIcons.trash_alt_solid,
color: Colors.white,
size: 15,
),
),
Container(
decoration: BoxDecoration(
shape: BoxShape.circle, color: Colors.red),
padding: EdgeInsets.all(5),
alignment: Alignment.center,
child: Icon(
LineIcons.trash_alt_solid,
color: Colors.white,
size: 15,
),
),
],
),
height: 30,
color: context.accentColor,
),
onDismissed: (direction) async {
setState(() {
_delete = true;
});
var index = await context
.read<AudioPlayerNotifier>()
.delFromPlaylist(widget.episode);
widget.onRemove(true);
final episodeRemove = widget.episode;
Scaffold.of(context).removeCurrentSnackBar();
Scaffold.of(context).showSnackBar(SnackBar(
behavior: SnackBarBehavior.floating,
backgroundColor: Colors.grey[800],
content: Text(s.toastRemovePlaylist,
style: TextStyle(color: Colors.white)),
action: SnackBarAction(
textColor: context.accentColor,
label: s.undo,
onPressed: () async {
await context
.read<AudioPlayerNotifier>()
.addToPlaylistAt(episodeRemove, index);
widget.onRemove(false);
}),
));
},
child: SizedBox(
height: 90.0,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
Expanded(
child: ListTile(
contentPadding: EdgeInsets.symmetric(vertical: 8),
onTap: () async {
await context
.read<AudioPlayerNotifier>()
.episodeLoad(widget.episode);
widget.onRemove(true);
},
title: Container(
padding: EdgeInsets.fromLTRB(0, 5.0, 20.0, 5.0),
child: Text(
widget.episode.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
leading: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.unfold_more, color: c),
CircleAvatar(
backgroundColor: c.withOpacity(0.5),
backgroundImage: widget.episode.avatarImage),
],
),
subtitle: Container(
padding: EdgeInsets.only(top: 5, bottom: 5),
height: 35,
child: Row(
children: <Widget>[
if (widget.episode.explicit == 1)
Container(
decoration: BoxDecoration(
color: Colors.red[800],
shape: BoxShape.circle),
height: 25.0,
width: 25.0,
margin: EdgeInsets.only(right: 10.0),
alignment: Alignment.center,
child: Text('E',
style: TextStyle(color: Colors.white))),
if (widget.episode.duration != 0)
episodeTag(
widget.episode.duration == 0
? ''
: s.minsCount(
widget.episode.duration ~/ 60),
Colors.cyan[300]),
if (widget.episode.enclosureLength != null)
episodeTag(
widget.episode.enclosureLength == 0
? ''
: '${(widget.episode.enclosureLength) ~/ 1000000}MB',
Colors.lightBlue[300]),
],
),
),
//trailing: Icon(Icons.menu),
),
),
Divider(
height: 2,
),
],
),
),
),
);
}
}

View File

@ -549,7 +549,7 @@ class EpisodeGrid extends StatelessWidget {
Tuple3<EpisodeBrief, List<String>, bool>>(
selector: (_, audio) => Tuple3(
audio?.episode,
audio.queue.playlist.map((e) => e.enclosureUrl).toList(),
audio.queue.episodes.map((e) => e.enclosureUrl).toList(),
audio.episodeState),
builder: (_, data, __) => OpenContainerWrapper(
avatarSize: layout == Layout.one

View File

@ -1,5 +1,6 @@
import 'package:connectivity/connectivity.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:provider/provider.dart';
@ -10,6 +11,7 @@ import '../state/audio_state.dart';
import '../state/download_state.dart';
import '../type/episodebrief.dart';
import '../type/play_histroy.dart';
import '../type/playlist.dart';
import '../util/extension_helper.dart';
import 'custom_widget.dart';
import 'general_dialog.dart';
@ -44,6 +46,7 @@ class _MultiSelectMenuBarState extends State<MultiSelectMenuBar> {
bool _marked;
bool _inPlaylist;
bool _downloaded;
bool _showPlaylists;
final _dbHelper = DBHelper();
@override
@ -53,6 +56,7 @@ class _MultiSelectMenuBarState extends State<MultiSelectMenuBar> {
_marked = false;
_downloaded = false;
_inPlaylist = false;
_showPlaylists = false;
}
@override
@ -63,6 +67,7 @@ class _MultiSelectMenuBarState extends State<MultiSelectMenuBar> {
_marked = false;
_downloaded = false;
_inPlaylist = false;
_showPlaylists = false;
});
super.didUpdateWidget(oldWidget);
}
@ -179,6 +184,11 @@ class _MultiSelectMenuBarState extends State<MultiSelectMenuBar> {
}
}
Future<EpisodeBrief> _getEpisode(String url) async {
var dbHelper = DBHelper();
return await dbHelper.getRssItemWithUrl(url);
}
Widget _buttonOnMenu({Widget child, VoidCallback onTap}) => Material(
color: Colors.transparent,
child: InkWell(
@ -190,6 +200,106 @@ class _MultiSelectMenuBarState extends State<MultiSelectMenuBar> {
),
),
);
Widget _playlistList() => SizedBox(
height: 50,
child: Selector<AudioPlayerNotifier, List<Playlist>>(
selector: (_, audio) => audio.playlists,
builder: (_, data, child) {
return Align(
alignment: Alignment.centerLeft,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
for (var p in data)
if (p.name == 'Queue')
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: InkWell(
onTap: () {
setState(() => _showPlaylists = false);
showGeneralDialog(
context: context,
barrierDismissible: true,
barrierLabel: MaterialLocalizations.of(context)
.modalBarrierDismissLabel,
barrierColor: Colors.black54,
transitionDuration:
const Duration(milliseconds: 200),
pageBuilder:
(context, animaiton, secondaryAnimation) =>
_NewPlaylist(widget.selectedList));
},
child: Container(
height: 30,
child: Row(
children: [
Container(
height: 30,
width: 30,
color: context.primaryColorDark,
child: Center(child: Icon(Icons.add)),
),
SizedBox(width: 10),
Text('New')
],
),
),
),
)
else
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: InkWell(
onTap: () {
context
.read<AudioPlayerNotifier>()
.addEpisodesToPlaylist(p,
episodes: widget.selectedList);
setState(() {
_showPlaylists = false;
});
},
child: Container(
height: 30,
child: Row(
children: [
Container(
height: 30,
width: 30,
color: context.primaryColorDark,
child: p.episodeList.isEmpty
? Center()
: FutureBuilder<EpisodeBrief>(
future:
_getEpisode(p.episodeList.first),
builder: (_, snapshot) {
if (snapshot.data != null) {
return SizedBox(
height: 30,
width: 30,
child: Image(
image: snapshot
.data.avatarImage));
}
return Center();
}),
),
SizedBox(width: 10),
Text(p.name),
],
),
),
),
),
],
),
),
);
},
));
OverlayEntry _createOverlayEntry() {
RenderBox renderBox = context.findRenderObject();
var offset = renderBox.localToGlobal(Offset.zero);
@ -214,7 +324,13 @@ class _MultiSelectMenuBarState extends State<MultiSelectMenuBar> {
tween: Tween<double>(begin: 0, end: 1),
duration: Duration(milliseconds: 500),
builder: (context, value, child) => Container(
height: widget.selectAll == null ? 40 : 90.0 * value,
height: widget.selectAll == null
? _showPlaylists
? 90
: 40
: _showPlaylists
? 140
: 90.0 * value,
decoration: BoxDecoration(color: context.primaryColor),
child: SingleChildScrollView(
child: Column(
@ -296,6 +412,7 @@ class _MultiSelectMenuBarState extends State<MultiSelectMenuBar> {
)
],
),
if (_showPlaylists) _playlistList(),
Row(
children: [
if (!widget.hideFavorite)
@ -421,6 +538,18 @@ class _MultiSelectMenuBarState extends State<MultiSelectMenuBar> {
}
}
}),
_buttonOnMenu(
child: Icon(
Icons.add_box_outlined,
color: Colors.grey[700],
),
onTap: () {
if (widget.selectedList.isNotEmpty) {
setState(() {
_showPlaylists = !_showPlaylists;
});
}
}),
Spacer(),
if (widget.selectAll == null)
SizedBox(
@ -446,3 +575,104 @@ class _MultiSelectMenuBarState extends State<MultiSelectMenuBar> {
);
}
}
class _NewPlaylist extends StatefulWidget {
final List<EpisodeBrief> episodes;
_NewPlaylist(this.episodes, {Key key}) : super(key: key);
@override
__NewPlaylistState createState() => __NewPlaylistState();
}
class __NewPlaylistState extends State<_NewPlaylist> {
String _playlistName;
int _error;
@override
Widget build(BuildContext context) {
final s = context.s;
return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle(
statusBarIconBrightness: Brightness.light,
systemNavigationBarColor:
Theme.of(context).brightness == Brightness.light
? Color.fromRGBO(113, 113, 113, 1)
: Color.fromRGBO(5, 5, 5, 1),
),
child: AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
elevation: 1,
contentPadding: EdgeInsets.symmetric(horizontal: 20),
titlePadding: EdgeInsets.all(20),
actionsPadding: EdgeInsets.zero,
actions: <Widget>[
FlatButton(
splashColor: context.accentColor.withAlpha(70),
onPressed: () => Navigator.of(context).pop(),
child: Text(
s.cancel,
style: TextStyle(color: Colors.grey[600]),
),
),
FlatButton(
splashColor: context.accentColor.withAlpha(70),
onPressed: () async {
if (context
.read<AudioPlayerNotifier>()
.playlistExisted(_playlistName)) {
setState(() => _error = 1);
} else {
final episodesList =
widget.episodes.map((e) => e.enclosureUrl).toList();
final playlist = Playlist(_playlistName,
episodeList: episodesList, episodes: widget.episodes);
context.read<AudioPlayerNotifier>().addPlaylist(playlist);
Navigator.of(context).pop();
}
},
child:
Text(s.confirm, style: TextStyle(color: context.accentColor)),
)
],
title:
SizedBox(width: context.width - 160, child: Text('New playlist')),
content: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
TextField(
decoration: InputDecoration(
contentPadding: EdgeInsets.symmetric(horizontal: 10),
hintText: 'New playlist',
hintStyle: TextStyle(fontSize: 18),
filled: true,
focusedBorder: UnderlineInputBorder(
borderSide:
BorderSide(color: context.accentColor, width: 2.0),
),
enabledBorder: UnderlineInputBorder(
borderSide:
BorderSide(color: context.accentColor, width: 2.0),
),
),
cursorRadius: Radius.circular(2),
autofocus: true,
maxLines: 1,
onChanged: (value) {
_playlistName = value;
},
),
Container(
alignment: Alignment.centerLeft,
child: (_error == 1)
? Text(
'Playlist existed',
style: TextStyle(color: Colors.red[400]),
)
: Center(),
),
],
),
),
);
}
}