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,18 +191,20 @@ class DBHelper {
var podcastLocal = <PodcastLocal>[]; var podcastLocal = <PodcastLocal>[];
for (var i in list) { for (var i in list) {
podcastLocal.add(PodcastLocal( if (i['id'] != localFolderId) {
i['title'], podcastLocal.add(PodcastLocal(
i['imageUrl'], i['title'],
i['rssUrl'], i['imageUrl'],
i['primaryColor'], i['rssUrl'],
i['author'], i['primaryColor'],
i['id'], i['author'],
i['imagePath'], i['id'],
i['provider'], i['imagePath'],
i['link'], i['provider'],
List<String>.from(jsonDecode(list.first['funding'])), i['link'],
)); List<String>.from(jsonDecode(list.first['funding'])),
));
}
} }
return podcastLocal; return podcastLocal;
} }
@ -261,7 +265,7 @@ class DBHelper {
[boo ? 1 : 0, id]); [boo ? 1 : 0, id]);
} }
Future<bool> getHideNewMark(String id) async { Future<bool> getHideNewMark(String id) async {
var dbClient = await database; var dbClient = await database;
List<Map> list = await dbClient List<Map> list = await dbClient
.rawQuery('SELECT hide_new_mark FROM PodcastLocal WHERE id = ?', [id]); .rawQuery('SELECT hide_new_mark FROM PodcastLocal WHERE id = ?', [id]);
@ -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,14 +363,16 @@ class DBHelper {
podcastLocal.link, podcastLocal.link,
jsonEncode(podcastLocal.funding) jsonEncode(podcastLocal.funding)
]); ]);
await txn.rawInsert( if (podcastLocal.id != localFolderId) {
"""REPLACE INTO SubscribeHistory(id, title, rss_url, add_date) VALUES (?, ?, ?, ?)""", await txn.rawInsert(
[ """REPLACE INTO SubscribeHistory(id, title, rss_url, add_date) VALUES (?, ?, ?, ?)""",
podcastLocal.id, [
podcastLocal.title, podcastLocal.id,
podcastLocal.rssUrl, podcastLocal.title,
milliseconds podcastLocal.rssUrl,
]); milliseconds
]);
}
}); });
} }
@ -416,26 +421,28 @@ class DBHelper {
} }
Future<void> saveHistory(PlayHistory history) async { Future<void> saveHistory(PlayHistory history) async {
var dbClient = await database; if (history.url.substring(0, 7) != 'file://') {
final milliseconds = DateTime.now().millisecondsSinceEpoch; var dbClient = await database;
var recent = await getPlayHistory(1); final milliseconds = DateTime.now().millisecondsSinceEpoch;
if (recent.isNotEmpty && recent.first.title == history.title) { var recent = await getPlayHistory(1);
await dbClient.rawDelete("DELETE FROM PlayHistory WHERE add_date = ?", if (recent.isNotEmpty && recent.first.title == history.title) {
[recent.first.playdate.millisecondsSinceEpoch]); await dbClient.rawDelete("DELETE FROM PlayHistory WHERE add_date = ?",
} [recent.first.playdate.millisecondsSinceEpoch]);
await dbClient.transaction((txn) async { }
return await txn.rawInsert( await dbClient.transaction((txn) async {
"""INSERT INTO PlayHistory (title, enclosure_url, seconds, seek_value, add_date, listen_time) return await txn.rawInsert(
"""INSERT INTO PlayHistory (title, enclosure_url, seconds, seek_value, add_date, listen_time)
VALUES (?, ?, ?, ?, ?, ?) """, VALUES (?, ?, ?, ?, ?, ?) """,
[ [
history.title, history.title,
history.url, history.url,
history.seconds, history.seconds,
history.seekValue, history.seekValue,
milliseconds, milliseconds,
history.seekValue > 0.95 ? 1 : 0 history.seekValue > 0.95 ? 1 : 0
]); ]);
}); });
}
} }
Future<List<PlayHistory>> getPlayHistory(int top) async { Future<List<PlayHistory>> getPlayHistory(int top) async {
@ -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,
@ -225,9 +238,9 @@ class _PlaylistHomeState extends State<PlaylistHome> {
width: 80, width: 80,
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), key: ValueKey('playing'),
child: EpisodeCard(episode, isPlaying: true,
key: ValueKey('playing'), canReorder: true,
isPlaying: true, havePadding: true,
canReorder: true, tileColor: context.primaryColorDark);
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( primary: context.accentColor,
color: context.primaryColorDark), textStyle: TextStyle(
primary: context.accentColor, fontWeight: FontWeight.bold)),
shape: RoundedRectangleBorder( onPressed: () {
borderRadius: BorderRadius.all( context
Radius.circular(100)))), .read<AudioPlayerNotifier>()
onPressed: () { .playlistLoad(queue);
context },
.read<AudioPlayerNotifier>() child: Row(
.playlistLoad(queue); children: <Widget>[
}, Text(s.play.toUpperCase(),
child: Text(s.play)) 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: Text(_optionLabel(option).first,
child: Center( style: TextStyle(
child: Text(_optionLabel(option).first, color: _option == option ? Colors.white : context.textColor)),
style: TextStyle(
color: _option == option
? 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;
} }
context.read<AudioPlayerNotifier>().addPlaylist(playlist); if (playlist != null) {
context.read<AudioPlayerNotifier>().addPlaylist(playlist);
}
Navigator.of(context).pop(); Navigator.of(context).pop();
} }
}, },
@ -952,52 +1085,63 @@ 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
mainAxisSize: MainAxisSize.min, ? SizedBox(
children: <Widget>[ height: 50,
TextField( child: Center(
decoration: InputDecoration( child: Platform.isIOS
contentPadding: EdgeInsets.symmetric(horizontal: 10), ? CupertinoActivityIndicator()
hintText: s.createNewPlaylist, : CircularProgressIndicator(),
hintStyle: TextStyle(fontSize: 18),
filled: true,
focusedBorder: UnderlineInputBorder(
borderSide:
BorderSide(color: context.accentColor, width: 2.0),
), ),
enabledBorder: UnderlineInputBorder( )
borderSide: : Column(
BorderSide(color: context.accentColor, width: 2.0), mainAxisSize: MainAxisSize.min,
), crossAxisAlignment: CrossAxisAlignment.start,
), children: <Widget>[
cursorRadius: Radius.circular(2), TextField(
autofocus: true, focusNode: _focusNode,
maxLines: 1, decoration: InputDecoration(
onChanged: (value) { contentPadding: EdgeInsets.symmetric(horizontal: 10),
_playlistName = value; hintText: s.createNewPlaylist,
}, hintStyle: TextStyle(fontSize: 18),
), filled: true,
Container( focusedBorder: UnderlineInputBorder(
alignment: Alignment.centerLeft, borderSide:
child: _error != null BorderSide(color: context.accentColor, width: 2.0),
? Text( ),
_error == 1 ? s.playlistExisted : s.playlistNameEmpty, enabledBorder: UnderlineInputBorder(
style: TextStyle(color: Colors.red[400]), borderSide:
) BorderSide(color: context.accentColor, width: 2.0),
: Center()), ),
SingleChildScrollView( ),
scrollDirection: Axis.horizontal, cursorRadius: Radius.circular(2),
child: Row( autofocus: true,
mainAxisAlignment: MainAxisAlignment.start, maxLines: 1,
children: [ onChanged: (value) {
_createOption(NewPlaylistOption.blank), _playlistName = value;
_createOption(NewPlaylistOption.randon10), },
_createOption(NewPlaylistOption.latest10), ),
Align(
alignment: Alignment.centerLeft,
child: _error != null
? Text(
_error == 1
? s.playlistExisted
: s.playlistNameEmpty,
style: TextStyle(color: Colors.red[400]),
)
: Center()),
SizedBox(height: 10),
Wrap(
children: [
_createOption(NewPlaylistOption.blank),
_createOption(NewPlaylistOption.randon10),
_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) {
@ -193,95 +199,95 @@ class __PlaylistItemState extends State<_PlaylistItem>
final episode = widget.episode; final episode = widget.episode;
final c = episode.backgroudColor(context); final c = episode.backgroudColor(context);
return SizedBox( return SizedBox(
height: 90.0, height: 90.0,
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround, mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[ children: <Widget>[
Expanded( Expanded(
child: ListTile( child: ListTile(
contentPadding: EdgeInsets.symmetric(vertical: 8), contentPadding: EdgeInsets.symmetric(vertical: 8),
onTap: () { onTap: () {
if (_fraction == 0) { if (_fraction == 0) {
_controller.forward(); _controller.forward();
widget.onSelect(episode); widget.onSelect(episode);
} else { } else {
_controller.reverse(); _controller.reverse();
widget.onRemove(episode); widget.onRemove(episode);
} }
}, },
title: Container( title: Container(
padding: EdgeInsets.fromLTRB(0, 5.0, 20.0, 5.0), padding: EdgeInsets.fromLTRB(0, 5.0, 20.0, 5.0),
child: Text( child: Text(
episode.title, episode.title,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
),
), ),
leading: Row( ),
mainAxisAlignment: MainAxisAlignment.start, leading: Row(
crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center,
children: [ mainAxisSize: MainAxisSize.min,
Icon(Icons.unfold_more, color: c), children: [
Transform( Icon(Icons.unfold_more, color: c),
alignment: FractionalOffset.center, Transform(
transform: Matrix4.identity() alignment: FractionalOffset.center,
..setEntry(3, 2, 0.001) transform: Matrix4.identity()
..rotateY(math.pi * _fraction), ..setEntry(3, 2, 0.001)
child: _fraction < 0.5 ..rotateY(math.pi * _fraction),
? CircleAvatar( child: _fraction < 0.5
backgroundColor: c.withOpacity(0.5), ? CircleAvatar(
backgroundImage: episode.avatarImage) backgroundColor: c.withOpacity(0.5),
: CircleAvatar( backgroundImage: episode.avatarImage)
backgroundColor: : CircleAvatar(
context.accentColor.withAlpha(70), backgroundColor: context.accentColor.withAlpha(70),
child: Transform( child: Transform(
alignment: FractionalOffset.center, alignment: FractionalOffset.center,
transform: Matrix4.identity() transform: Matrix4.identity()
..setEntry(3, 2, 0.001) ..setEntry(3, 2, 0.001)
..rotateY(math.pi), ..rotateY(math.pi),
child: Icon(Icons.done)), 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]),
], ],
), ),
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, Divider(
), height: 2,
], ),
)); ],
),
);
} }
} }
@ -311,19 +317,20 @@ class __PlaylistSettingState extends State<_PlaylistSetting> {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
ListTile( if (!widget.playlist.isLocal)
onTap: () { ListTile(
setState(() => _clearConfirm = true); onTap: () {
}, setState(() => _clearConfirm = true);
dense: true, },
title: Row( dense: true,
children: [ title: Row(
Icon(Icons.clear_all_outlined, size: 18), children: [
SizedBox(width: 20), Icon(Icons.clear_all_outlined, size: 18),
Text(s.clearAll, style: textStyle), SizedBox(width: 20),
], Text(s.clearAll, style: textStyle),
],
),
), ),
),
if (_clearConfirm) if (_clearConfirm)
Container( Container(
width: double.infinity, width: double.infinity,

View File

@ -66,9 +66,11 @@ 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()
? CachedNetworkImageProvider(episodeImage) ? FileImage(File(episodeImage))
: AssetImage('assets/avatar_backup.png'); : (episodeImage != '')
? CachedNetworkImageProvider(episodeImage)
: AssetImage('assets/avatar_backup.png');
} }
Color backgroudColor(BuildContext context) { Color backgroudColor(BuildContext context) {

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