Load local folder as playlist.

This commit is contained in:
stonega 2021-02-17 23:25:05 +08:00
parent b3e57be396
commit 927907b78f
6 changed files with 452 additions and 240 deletions

View File

@ -15,6 +15,7 @@ import '../type/podcastlocal.dart';
import '../type/sub_history.dart'; import '../type/sub_history.dart';
enum Filter { downloaded, liked, search, all } enum Filter { downloaded, liked, search, all }
const localFolderId = "46e48103-06c7-4fe1-a0b1-68aa7205b7f0";
class DBHelper { class DBHelper {
static Database _db; static Database _db;
@ -130,7 +131,8 @@ class DBHelper {
} }
Future<void> _v7Update(Database db) async { Future<void> _v7Update(Database db) async {
await db.execute("ALTER TABLE PodcastLocal ADD hide_new_mark INTEGER DEFAULT 0"); await db.execute(
"ALTER TABLE PodcastLocal ADD hide_new_mark INTEGER DEFAULT 0");
} }
Future<List<PodcastLocal>> getPodcastLocal(List<String> podcasts, Future<List<PodcastLocal>> getPodcastLocal(List<String> podcasts,
@ -189,6 +191,7 @@ class DBHelper {
var podcastLocal = <PodcastLocal>[]; var podcastLocal = <PodcastLocal>[];
for (var i in list) { for (var i in list) {
if (i['id'] != localFolderId) {
podcastLocal.add(PodcastLocal( podcastLocal.add(PodcastLocal(
i['title'], i['title'],
i['imageUrl'], i['imageUrl'],
@ -202,6 +205,7 @@ class DBHelper {
List<String>.from(jsonDecode(list.first['funding'])), List<String>.from(jsonDecode(list.first['funding'])),
)); ));
} }
}
return podcastLocal; return podcastLocal;
} }
@ -339,7 +343,6 @@ class DBHelper {
Future savePodcastLocal(PodcastLocal podcastLocal) async { Future savePodcastLocal(PodcastLocal podcastLocal) async {
var milliseconds = DateTime.now().millisecondsSinceEpoch; var milliseconds = DateTime.now().millisecondsSinceEpoch;
print(podcastLocal.imagePath);
var dbClient = await database; var dbClient = await database;
await dbClient.transaction((txn) async { await dbClient.transaction((txn) async {
await txn.rawInsert( await txn.rawInsert(
@ -360,6 +363,7 @@ class DBHelper {
podcastLocal.link, podcastLocal.link,
jsonEncode(podcastLocal.funding) jsonEncode(podcastLocal.funding)
]); ]);
if (podcastLocal.id != localFolderId) {
await txn.rawInsert( await txn.rawInsert(
"""REPLACE INTO SubscribeHistory(id, title, rss_url, add_date) VALUES (?, ?, ?, ?)""", """REPLACE INTO SubscribeHistory(id, title, rss_url, add_date) VALUES (?, ?, ?, ?)""",
[ [
@ -368,6 +372,7 @@ class DBHelper {
podcastLocal.rssUrl, podcastLocal.rssUrl,
milliseconds milliseconds
]); ]);
}
}); });
} }
@ -416,6 +421,7 @@ class DBHelper {
} }
Future<void> saveHistory(PlayHistory history) async { Future<void> saveHistory(PlayHistory history) async {
if (history.url.substring(0, 7) != 'file://') {
var dbClient = await database; var dbClient = await database;
final milliseconds = DateTime.now().millisecondsSinceEpoch; final milliseconds = DateTime.now().millisecondsSinceEpoch;
var recent = await getPlayHistory(1); var recent = await getPlayHistory(1);
@ -437,6 +443,7 @@ class DBHelper {
]); ]);
}); });
} }
}
Future<List<PlayHistory>> getPlayHistory(int top) async { Future<List<PlayHistory>> getPlayHistory(int top) async {
var dbClient = await database; var dbClient = await database;
@ -786,6 +793,36 @@ class DBHelper {
} }
} }
Future<void> saveLocalEpisode(EpisodeBrief episode) async {
var dbClient = await database;
await dbClient.transaction((txn) async {
await txn.rawInsert(
"""INSERT OR REPLACE INTO Episodes(title, enclosure_url, enclosure_length, pubDate,
description, feed_id, milliseconds, duration, explicit, media_id, episode_image)
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
[
episode.title,
episode.enclosureUrl,
episode.enclosureLength,
'',
'',
localFolderId,
episode.pubDate,
episode.duration,
0,
episode.enclosureUrl,
episode.episodeImage
]);
});
}
Future<void> deleteLocalEpisodes(List<String> files) async {
var dbClient = await database;
var s = files.map<String>((e) => "'$e'").toList();
await dbClient.rawDelete(
'DELETE FROM Episodes WHERE enclosure_url in (${s.join(',')})');
}
Future<List<EpisodeBrief>> getRssItem(String id, int count, Future<List<EpisodeBrief>> getRssItem(String id, int count,
{bool reverse, {bool reverse,
Filter filter = Filter.all, Filter filter = Filter.all,
@ -1192,16 +1229,17 @@ class DBHelper {
"""SELECT E.title, E.enclosure_url, E.enclosure_length, E.is_new, """SELECT E.title, E.enclosure_url, E.enclosure_length, E.is_new,
E.milliseconds, P.title as feed_title, E.duration, E.explicit, E.milliseconds, P.title as feed_title, E.duration, E.explicit,
P.imagePath, P.primaryColor FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id P.imagePath, P.primaryColor FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
LEFT JOIN PlayHistory H ON E.enclosure_url = H.enclosure_url LEFT JOIN PlayHistory H ON E.enclosure_url = H.enclosure_url WHERE p.id != ?
GROUP BY E.enclosure_url HAVING SUM(H.listen_time) is null GROUP BY E.enclosure_url HAVING SUM(H.listen_time) is null
OR SUM(H.listen_time) = 0 ORDER BY E.milliseconds DESC LIMIT ? """, OR SUM(H.listen_time) = 0 ORDER BY E.milliseconds DESC LIMIT ? """,
[top]); [localFolderId, top]);
} else { } else {
list = await dbClient.rawQuery( list = await dbClient.rawQuery(
"""SELECT E.title, E.enclosure_url, E.enclosure_length, E.is_new, """SELECT E.title, E.enclosure_url, E.enclosure_length, E.is_new,
E.milliseconds, P.title as feed_title, E.duration, E.explicit, E.milliseconds, P.title as feed_title, E.duration, E.explicit,
P.imagePath, P.primaryColor FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id P.imagePath, P.primaryColor FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
ORDER BY E.milliseconds DESC LIMIT ? """, [top]); WHERE p.id != ? ORDER BY E.milliseconds DESC LIMIT ? """,
[localFolderId, top]);
} }
if (list.isNotEmpty) { if (list.isNotEmpty) {
for (var i in list) { for (var i in list) {
@ -1231,15 +1269,17 @@ class DBHelper {
"""SELECT E.title, E.enclosure_url, E.enclosure_length, E.is_new, """SELECT E.title, E.enclosure_url, E.enclosure_length, E.is_new,
E.milliseconds, P.title as feed_title, E.duration, E.explicit, E.milliseconds, P.title as feed_title, E.duration, E.explicit,
P.imagePath, P.primaryColor FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id P.imagePath, P.primaryColor FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
LEFT JOIN PlayHistory H ON E.enclosure_url = H.enclosure_url LEFT JOIN PlayHistory H ON E.enclosure_url = H.enclosure_url WHERE p.id != ?
GROUP BY E.enclosure_url HAVING SUM(H.listen_time) is null GROUP BY E.enclosure_url HAVING SUM(H.listen_time) is null
OR SUM(H.listen_time) = 0 ORDER BY RANDOM() LIMIT ? """, [random]); OR SUM(H.listen_time) = 0 ORDER BY RANDOM() LIMIT ? """,
[localFolderId, random]);
} else { } else {
list = await dbClient.rawQuery( list = await dbClient.rawQuery(
"""SELECT E.title, E.enclosure_url, E.enclosure_length, E.is_new, """SELECT E.title, E.enclosure_url, E.enclosure_length, E.is_new,
E.milliseconds, P.title as feed_title, E.duration, E.explicit, E.milliseconds, P.title as feed_title, E.duration, E.explicit,
P.imagePath, P.primaryColor FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id P.imagePath, P.primaryColor FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
ORDER BY RANDOM() LIMIT ? """, [random]); WHERE p.id != ? ORDER BY RANDOM() LIMIT ? """,
[localFolderId, random]);
} }
if (list.isNotEmpty) { if (list.isNotEmpty) {
for (var i in list) { for (var i in list) {

View File

@ -1,11 +1,20 @@
import 'dart:developer' as developer;
import 'dart:io';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:color_thief_flutter/color_thief_flutter.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:fluttertoast/fluttertoast.dart'; import 'package:fluttertoast/fluttertoast.dart';
import 'package:image/image.dart' as img;
import 'package:line_icons/line_icons.dart'; import 'package:line_icons/line_icons.dart';
import 'package:media_metadata_retriever/media_metadata_retriever.dart';
import 'package:path_provider/path_provider.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
import 'package:uuid/uuid.dart';
import '../home/home.dart'; import '../home/home.dart';
import '../local_storage/sqflite_localpodcast.dart'; import '../local_storage/sqflite_localpodcast.dart';
@ -14,6 +23,7 @@ import '../state/setting_state.dart';
import '../type/episodebrief.dart'; import '../type/episodebrief.dart';
import '../type/play_histroy.dart'; import '../type/play_histroy.dart';
import '../type/playlist.dart'; import '../type/playlist.dart';
import '../type/podcastlocal.dart';
import '../util/extension_helper.dart'; import '../util/extension_helper.dart';
import '../util/pageroute.dart'; import '../util/pageroute.dart';
import '../widgets/custom_widget.dart'; import '../widgets/custom_widget.dart';
@ -152,7 +162,9 @@ class _PlaylistHomeState extends State<PlaylistHome> {
splashRadius: 20, splashRadius: 20,
icon: Icon(Icons.skip_next), icon: Icon(Icons.skip_next),
onPressed: () { onPressed: () {
if (running) { if (running &&
!(data.item1.length == 1 &&
!data.item1.isQueue)) {
audio.playNext(); audio.playNext();
} }
}), }),
@ -187,7 +199,7 @@ class _PlaylistHomeState extends State<PlaylistHome> {
selector: (_, audio) => audio.lastPosition, selector: (_, audio) => audio.lastPosition,
builder: (_, position, __) { builder: (_, position, __) {
return Text( return Text(
'${(position ~/ 1000).toTime} / ${(data.item4?.duration??0).toTime}'); '${(position ~/ 1000).toTime} / ${(data.item4?.duration ?? 0).toTime}');
}, },
), ),
], ],
@ -196,6 +208,7 @@ class _PlaylistHomeState extends State<PlaylistHome> {
? ClipRRect( ? ClipRRect(
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
child: Stack( child: Stack(
alignment: Alignment.center,
children: [ children: [
SizedBox( SizedBox(
width: 80, width: 80,
@ -226,8 +239,8 @@ class _PlaylistHomeState extends State<PlaylistHome> {
child: CustomPaint( child: CustomPaint(
painter: CircleProgressIndicator( painter: CircleProgressIndicator(
progress, progress,
color: Colors.black38, color: context.primaryColor
), .withOpacity(0.9)),
), ),
); );
}, },
@ -333,14 +346,12 @@ class __QueueState extends State<_Queue> {
key: ValueKey(episode.enclosureUrl), key: ValueKey(episode.enclosureUrl),
); );
} else { } else {
return Padding( return EpisodeCard(episode,
padding: const EdgeInsets.symmetric(horizontal: 10),
child: EpisodeCard(episode,
key: ValueKey('playing'), key: ValueKey('playing'),
isPlaying: true, isPlaying: true,
canReorder: true, canReorder: true,
tileColor: context.primaryColorDark), havePadding: true,
); tileColor: context.primaryColorDark);
} }
}).toList() }).toList()
: episodes : episodes
@ -425,7 +436,7 @@ class __HistoryState extends State<_History> {
} }
Size _getMaskStop(double seekValue, int seconds) { Size _getMaskStop(double seekValue, int seconds) {
final Size size = (TextPainter( final size = (TextPainter(
text: TextSpan(text: seconds.toTime), text: TextSpan(text: seconds.toTime),
maxLines: 1, maxLines: 1,
textScaleFactor: MediaQuery.of(context).textScaleFactor, textScaleFactor: MediaQuery.of(context).textScaleFactor,
@ -711,19 +722,31 @@ class __PlaylistsState extends State<_Playlists> {
Text( Text(
'${queue.length} ${s.episode(queue.length).toLowerCase()}'), '${queue.length} ${s.episode(queue.length).toLowerCase()}'),
TextButton( TextButton(
style: OutlinedButton.styleFrom( style: TextButton.styleFrom(
side: BorderSide(
color: context.primaryColorDark),
primary: context.accentColor, primary: context.accentColor,
shape: RoundedRectangleBorder( textStyle: TextStyle(
borderRadius: BorderRadius.all( fontWeight: FontWeight.bold)),
Radius.circular(100)))),
onPressed: () { onPressed: () {
context context
.read<AudioPlayerNotifier>() .read<AudioPlayerNotifier>()
.playlistLoad(queue); .playlistLoad(queue);
}, },
child: Text(s.play)) child: Row(
children: <Widget>[
Text(s.play.toUpperCase(),
style: TextStyle(
color:
Theme.of(context).accentColor,
fontSize: 15,
fontWeight: FontWeight.bold,
)),
Icon(
Icons.play_arrow,
color: Theme.of(context).accentColor,
),
],
),
)
], ],
) )
], ],
@ -768,9 +791,25 @@ class __PlaylistsState extends State<_Playlists> {
title: Text(data[index].name), title: Text(data[index].name),
subtitle: Text( subtitle: Text(
'${data[index].length} ${s.episode(data[index].length).toLowerCase()}'), '${data[index].length} ${s.episode(data[index].length).toLowerCase()}'),
trailing: IconButton( trailing: TextButton(
splashRadius: 20, style: TextButton.styleFrom(
icon: Icon(LineIcons.playCircle, size: 30), primary: context.accentColor,
textStyle: TextStyle(fontWeight: FontWeight.bold)),
child: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(s.play.toUpperCase(),
style: TextStyle(
color: Theme.of(context).accentColor,
fontSize: 15,
fontWeight: FontWeight.bold,
)),
Icon(
Icons.play_arrow,
color: Theme.of(context).accentColor,
),
],
),
onPressed: () { onPressed: () {
context context
.read<AudioPlayerNotifier>() .read<AudioPlayerNotifier>()
@ -806,7 +845,7 @@ class __PlaylistsState extends State<_Playlists> {
} }
} }
enum NewPlaylistOption { blank, randon10, latest10 } enum NewPlaylistOption { blank, randon10, latest10, folder }
class _NewPlaylist extends StatefulWidget { class _NewPlaylist extends StatefulWidget {
_NewPlaylist({Key key}) : super(key: key); _NewPlaylist({Key key}) : super(key: key);
@ -819,11 +858,15 @@ class __NewPlaylistState extends State<_NewPlaylist> {
final _dbHelper = DBHelper(); final _dbHelper = DBHelper();
String _playlistName = ''; String _playlistName = '';
NewPlaylistOption _option; NewPlaylistOption _option;
bool _loadFolder;
FocusNode _focusNode;
int _error; int _error;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_loadFolder = false;
_focusNode = FocusNode();
_option = NewPlaylistOption.blank; _option = NewPlaylistOption.blank;
} }
@ -837,7 +880,7 @@ class __NewPlaylistState extends State<_NewPlaylist> {
Widget _createOption(NewPlaylistOption option) { Widget _createOption(NewPlaylistOption option) {
return Padding( return Padding(
padding: const EdgeInsets.all(8.0), padding: EdgeInsets.fromLTRB(0, 8, 8, 8),
child: InkWell( child: InkWell(
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
onTap: () { onTap: () {
@ -845,19 +888,15 @@ class __NewPlaylistState extends State<_NewPlaylist> {
}, },
child: AnimatedContainer( child: AnimatedContainer(
duration: Duration(milliseconds: 300), duration: Duration(milliseconds: 300),
padding: EdgeInsets.symmetric(horizontal: 8.0), padding: EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
color: _option == option color: _option == option
? context.accentColor ? context.accentColor
: context.primaryColorDark), : context.primaryColorDark),
height: 32,
child: Center(
child: Text(_optionLabel(option).first, child: Text(_optionLabel(option).first,
style: TextStyle( style: TextStyle(
color: _option == option color: _option == option ? Colors.white : context.textColor)),
? Colors.white
: context.textColor))),
), ),
), ),
); );
@ -874,12 +913,84 @@ class __NewPlaylistState extends State<_NewPlaylist> {
case NewPlaylistOption.latest10: case NewPlaylistOption.latest10:
return ['Latest 10', 'Add 10 latest updated episodes to playlist']; return ['Latest 10', 'Add 10 latest updated episodes to playlist'];
break; break;
case NewPlaylistOption.folder:
return ['Local folder', 'Choose a local folder'];
break;
default: default:
return ['', '']; return ['', ''];
break; break;
} }
} }
Future<List<EpisodeBrief>> _loadLocalFolder() async {
var episodes = <EpisodeBrief>[];
var dirPath;
try {
dirPath = await FilePicker.platform.getDirectoryPath();
} catch (e) {
developer.log(e, name: 'Failed to load dir.');
}
final localFolder = await _dbHelper.getPodcastLocal([localFolderId]);
if (localFolder.isEmpty) {
final localPodcast = PodcastLocal('Local Folder', '', '', '',
'Local Folder', localFolderId, '', '', '', []);
await _dbHelper.savePodcastLocal(localPodcast);
}
if (dirPath != null) {
var dir = Directory(dirPath);
for (var file in dir.listSync()) {
if (file is File) {
if (file.path.split('.').last == 'mp3') {
final episode = await _getEpisodeFromFile(file.path);
episodes.add(episode);
await _dbHelper.saveLocalEpisode(episode);
}
}
}
}
return episodes;
}
Future<EpisodeBrief> _getEpisodeFromFile(String path) async {
var metadataRetriever = MediaMetadataRetriever();
final fileLength = File(path).statSync().size;
final pubDate = DateTime.now().millisecondsSinceEpoch;
var primaryColor;
var imagePath;
await metadataRetriever.setFile(File(path));
if (metadataRetriever.albumArt != null) {
final dir = await getApplicationDocumentsDirectory();
final image = img.decodeImage(metadataRetriever.albumArt);
final thumbnail = img.copyResize(image, width: 300);
var uuid = Uuid().v4();
File("${dir.path}/$uuid.png")..writeAsBytesSync(img.encodePng(thumbnail));
imagePath = "${dir.path}/$uuid.png";
primaryColor = await _getColor(File(imagePath));
}
final fileName = path.split('/').last;
final metadata = await metadataRetriever.metadata;
return EpisodeBrief(
fileName,
'file://$path',
fileLength,
pubDate,
metadata.albumName ?? '',
primaryColor ?? '',
metadata.trackDuration,
0,
'',
0,
episodeImage: imagePath ?? '');
}
Future<String> _getColor(File file) async {
final imageProvider = FileImage(file);
var colorImage = await getImageFromProvider(imageProvider);
var color = await getColorFromImage(colorImage);
var primaryColor = color.toString();
return primaryColor;
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final s = context.s; final s = context.s;
@ -939,10 +1050,32 @@ class __NewPlaylistState extends State<_NewPlaylist> {
); );
await playlist.getPlaylist(); await playlist.getPlaylist();
break; break;
case NewPlaylistOption.folder:
_focusNode.unfocus();
setState(() {
_loadFolder = true;
});
final episodes = await _loadLocalFolder();
if (episodes.isNotEmpty) {
playlist = Playlist(
_playlistName,
isLocal: true,
episodeList: [for (var e in episodes) e.enclosureUrl],
);
await playlist.getPlaylist();
}
if (mounted) {
setState(() {
_loadFolder = false;
});
}
break;
default: default:
break; break;
} }
if (playlist != null) {
context.read<AudioPlayerNotifier>().addPlaylist(playlist); context.read<AudioPlayerNotifier>().addPlaylist(playlist);
}
Navigator.of(context).pop(); Navigator.of(context).pop();
} }
}, },
@ -952,10 +1085,21 @@ class __NewPlaylistState extends State<_NewPlaylist> {
], ],
title: SizedBox( title: SizedBox(
width: context.width - 160, child: Text(s.createNewPlaylist)), width: context.width - 160, child: Text(s.createNewPlaylist)),
content: Column( content: _loadFolder
? SizedBox(
height: 50,
child: Center(
child: Platform.isIOS
? CupertinoActivityIndicator()
: CircularProgressIndicator(),
),
)
: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
TextField( TextField(
focusNode: _focusNode,
decoration: InputDecoration( decoration: InputDecoration(
contentPadding: EdgeInsets.symmetric(horizontal: 10), contentPadding: EdgeInsets.symmetric(horizontal: 10),
hintText: s.createNewPlaylist, hintText: s.createNewPlaylist,
@ -977,25 +1121,25 @@ class __NewPlaylistState extends State<_NewPlaylist> {
_playlistName = value; _playlistName = value;
}, },
), ),
Container( Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: _error != null child: _error != null
? Text( ? Text(
_error == 1 ? s.playlistExisted : s.playlistNameEmpty, _error == 1
? s.playlistExisted
: s.playlistNameEmpty,
style: TextStyle(color: Colors.red[400]), style: TextStyle(color: Colors.red[400]),
) )
: Center()), : Center()),
SingleChildScrollView( SizedBox(height: 10),
scrollDirection: Axis.horizontal, Wrap(
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [ children: [
_createOption(NewPlaylistOption.blank), _createOption(NewPlaylistOption.blank),
_createOption(NewPlaylistOption.randon10), _createOption(NewPlaylistOption.randon10),
_createOption(NewPlaylistOption.latest10), _createOption(NewPlaylistOption.latest10),
_createOption(NewPlaylistOption.folder)
], ],
), ),
),
], ],
), ),
), ),

View File

@ -2,12 +2,13 @@ import 'dart:math' as math;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:tsacdop/widgets/general_dialog.dart'; import 'package:file_picker/file_picker.dart';
import '../state/audio_state.dart'; import '../state/audio_state.dart';
import '../type/episodebrief.dart'; import '../type/episodebrief.dart';
import '../type/playlist.dart'; import '../type/playlist.dart';
import '../util/extension_helper.dart'; import '../util/extension_helper.dart';
import '../widgets/general_dialog.dart';
class PlaylistDetail extends StatefulWidget { class PlaylistDetail extends StatefulWidget {
final Playlist playlist; final Playlist playlist;
@ -41,7 +42,9 @@ class _PlaylistDetailState extends State<PlaylistDetail> {
}, },
), ),
title: Text(_selectedEpisodes.isEmpty title: Text(_selectedEpisodes.isEmpty
? widget.playlist.isQueue ? s.queue :widget.playlist.name ? widget.playlist.isQueue
? s.queue
: widget.playlist.name
: s.selected(_selectedEpisodes.length)), : s.selected(_selectedEpisodes.length)),
actions: [ actions: [
if (_selectedEpisodes.isNotEmpty) if (_selectedEpisodes.isNotEmpty)
@ -85,8 +88,11 @@ class _PlaylistDetailState extends State<PlaylistDetail> {
body: Selector<AudioPlayerNotifier, List<Playlist>>( body: Selector<AudioPlayerNotifier, List<Playlist>>(
selector: (_, audio) => audio.playlists, selector: (_, audio) => audio.playlists,
builder: (_, data, __) { builder: (_, data, __) {
final playlist = data.firstWhere((e) => e == widget.playlist); final playlist = data.firstWhere(
final episodes = playlist.episodes; (e) => e == widget.playlist,
orElse: () => null,
);
final episodes = playlist?.episodes ?? [];
return ReorderableListView( return ReorderableListView(
onReorder: (oldIndex, newIndex) { onReorder: (oldIndex, newIndex) {
if (widget.playlist.isQueue) { if (widget.playlist.isQueue) {
@ -233,8 +239,7 @@ class __PlaylistItemState extends State<_PlaylistItem>
backgroundColor: c.withOpacity(0.5), backgroundColor: c.withOpacity(0.5),
backgroundImage: episode.avatarImage) backgroundImage: episode.avatarImage)
: CircleAvatar( : CircleAvatar(
backgroundColor: backgroundColor: context.accentColor.withAlpha(70),
context.accentColor.withAlpha(70),
child: Transform( child: Transform(
alignment: FractionalOffset.center, alignment: FractionalOffset.center,
transform: Matrix4.identity() transform: Matrix4.identity()
@ -258,8 +263,8 @@ class __PlaylistItemState extends State<_PlaylistItem>
width: 25.0, width: 25.0,
margin: EdgeInsets.only(right: 10.0), margin: EdgeInsets.only(right: 10.0),
alignment: Alignment.center, alignment: Alignment.center,
child: Text('E', child:
style: TextStyle(color: Colors.white))), Text('E', style: TextStyle(color: Colors.white))),
if (episode.duration != 0) if (episode.duration != 0)
_episodeTag( _episodeTag(
episode.duration == 0 episode.duration == 0
@ -281,7 +286,8 @@ class __PlaylistItemState extends State<_PlaylistItem>
height: 2, height: 2,
), ),
], ],
)); ),
);
} }
} }
@ -311,6 +317,7 @@ class __PlaylistSettingState extends State<_PlaylistSetting> {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (!widget.playlist.isLocal)
ListTile( ListTile(
onTap: () { onTap: () {
setState(() => _clearConfirm = true); setState(() => _clearConfirm = true);

View File

@ -66,7 +66,9 @@ class EpisodeBrief extends Equatable {
ImageProvider get avatarImage { ImageProvider get avatarImage {
return File(imagePath).existsSync() return File(imagePath).existsSync()
? FileImage(File(imagePath)) ? FileImage(File(imagePath))
: episodeImage != '' : File(episodeImage).existsSync()
? FileImage(File(episodeImage))
: (episodeImage != '')
? CachedNetworkImageProvider(episodeImage) ? CachedNetworkImageProvider(episodeImage)
: AssetImage('assets/avatar_backup.png'); : AssetImage('assets/avatar_backup.png');
} }

View File

@ -7,17 +7,24 @@ import 'episodebrief.dart';
class PlaylistEntity { class PlaylistEntity {
final String name; final String name;
final String id; final String id;
final bool isLocal;
final List<String> episodeList; final List<String> episodeList;
PlaylistEntity(this.name, this.id, this.episodeList); PlaylistEntity(this.name, this.id, this.isLocal, this.episodeList);
Map<String, Object> toJson() { Map<String, Object> toJson() {
return {'name': name, 'id': id, 'episodeList': episodeList}; return {
'name': name,
'id': id,
'isLocal': isLocal,
'episodeList': episodeList
};
} }
static PlaylistEntity fromJson(Map<String, Object> json) { static PlaylistEntity fromJson(Map<String, Object> json) {
var list = List<String>.from(json['episodeList']); var list = List<String>.from(json['episodeList']);
return PlaylistEntity(json['name'] as String, json['id'] as String, list); return PlaylistEntity(json['name'] as String, json['id'] as String,
json['isLocal'] == null ? false : json['isLocal'] as bool, list);
} }
} }
@ -28,6 +35,8 @@ class Playlist extends Equatable {
/// Unique id for playlist. /// Unique id for playlist.
final String id; final String id;
final bool isLocal;
/// Episode url list for playlist. /// Episode url list for playlist.
final List<String> episodeList; final List<String> episodeList;
@ -45,20 +54,24 @@ class Playlist extends Equatable {
bool contains(EpisodeBrief episode) => episodes.contains(episode); bool contains(EpisodeBrief episode) => episodes.contains(episode);
Playlist(this.name, Playlist(this.name,
{String id, List<String> episodeList, List<EpisodeBrief> episodes}) {String id,
this.isLocal = false,
List<String> episodeList,
List<EpisodeBrief> episodes})
: id = id ?? Uuid().v4(), : id = id ?? Uuid().v4(),
assert(name != ''), assert(name != ''),
episodeList = episodeList ?? [], episodeList = episodeList ?? [],
episodes = episodes ?? []; episodes = episodes ?? [];
PlaylistEntity toEntity() { PlaylistEntity toEntity() {
return PlaylistEntity(name, id, episodeList.toSet().toList()); return PlaylistEntity(name, id, isLocal, episodeList.toSet().toList());
} }
static Playlist fromEntity(PlaylistEntity entity) { static Playlist fromEntity(PlaylistEntity entity) {
return Playlist( return Playlist(
entity.name, entity.name,
id: entity.id, id: entity.id,
isLocal: entity.isLocal,
episodeList: entity.episodeList, episodeList: entity.episodeList,
); );
} }
@ -119,6 +132,9 @@ class Playlist extends Equatable {
episodes.removeWhere( episodes.removeWhere(
(episode) => episode.enclosureUrl == episodeBrief.enclosureUrl); (episode) => episode.enclosureUrl == episodeBrief.enclosureUrl);
episodeList.removeWhere((url) => url == episodeBrief.enclosureUrl); episodeList.removeWhere((url) => url == episodeBrief.enclosureUrl);
if (isLocal) {
_dbHelper.deleteLocalEpisodes([episodeBrief.enclosureUrl]);
}
return index; return index;
} }

View File

@ -39,6 +39,9 @@ dependencies:
fl_chart: ^0.12.2 fl_chart: ^0.12.2
line_icons: ^1.3.2 line_icons: ^1.3.2
marquee: ^1.6.1 marquee: ^1.6.1
media_metadata_retriever:
git:
url: https://github.com/stonega/media_metadata_retriever.git
google_fonts: ^1.1.1 google_fonts: ^1.1.1
image: ^2.1.19 image: ^2.1.19
intl: ^0.16.1 intl: ^0.16.1