Load local folder as playlist.
This commit is contained in:
parent
b3e57be396
commit
927907b78f
|
@ -15,6 +15,7 @@ import '../type/podcastlocal.dart';
|
|||
import '../type/sub_history.dart';
|
||||
|
||||
enum Filter { downloaded, liked, search, all }
|
||||
const localFolderId = "46e48103-06c7-4fe1-a0b1-68aa7205b7f0";
|
||||
|
||||
class DBHelper {
|
||||
static Database _db;
|
||||
|
@ -130,7 +131,8 @@ class DBHelper {
|
|||
}
|
||||
|
||||
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,
|
||||
|
@ -189,18 +191,20 @@ class DBHelper {
|
|||
var podcastLocal = <PodcastLocal>[];
|
||||
|
||||
for (var i in list) {
|
||||
podcastLocal.add(PodcastLocal(
|
||||
i['title'],
|
||||
i['imageUrl'],
|
||||
i['rssUrl'],
|
||||
i['primaryColor'],
|
||||
i['author'],
|
||||
i['id'],
|
||||
i['imagePath'],
|
||||
i['provider'],
|
||||
i['link'],
|
||||
List<String>.from(jsonDecode(list.first['funding'])),
|
||||
));
|
||||
if (i['id'] != localFolderId) {
|
||||
podcastLocal.add(PodcastLocal(
|
||||
i['title'],
|
||||
i['imageUrl'],
|
||||
i['rssUrl'],
|
||||
i['primaryColor'],
|
||||
i['author'],
|
||||
i['id'],
|
||||
i['imagePath'],
|
||||
i['provider'],
|
||||
i['link'],
|
||||
List<String>.from(jsonDecode(list.first['funding'])),
|
||||
));
|
||||
}
|
||||
}
|
||||
return podcastLocal;
|
||||
}
|
||||
|
@ -261,7 +265,7 @@ class DBHelper {
|
|||
[boo ? 1 : 0, id]);
|
||||
}
|
||||
|
||||
Future<bool> getHideNewMark(String id) async {
|
||||
Future<bool> getHideNewMark(String id) async {
|
||||
var dbClient = await database;
|
||||
List<Map> list = await dbClient
|
||||
.rawQuery('SELECT hide_new_mark FROM PodcastLocal WHERE id = ?', [id]);
|
||||
|
@ -339,7 +343,6 @@ class DBHelper {
|
|||
|
||||
Future savePodcastLocal(PodcastLocal podcastLocal) async {
|
||||
var milliseconds = DateTime.now().millisecondsSinceEpoch;
|
||||
print(podcastLocal.imagePath);
|
||||
var dbClient = await database;
|
||||
await dbClient.transaction((txn) async {
|
||||
await txn.rawInsert(
|
||||
|
@ -360,14 +363,16 @@ class DBHelper {
|
|||
podcastLocal.link,
|
||||
jsonEncode(podcastLocal.funding)
|
||||
]);
|
||||
await txn.rawInsert(
|
||||
"""REPLACE INTO SubscribeHistory(id, title, rss_url, add_date) VALUES (?, ?, ?, ?)""",
|
||||
[
|
||||
podcastLocal.id,
|
||||
podcastLocal.title,
|
||||
podcastLocal.rssUrl,
|
||||
milliseconds
|
||||
]);
|
||||
if (podcastLocal.id != localFolderId) {
|
||||
await txn.rawInsert(
|
||||
"""REPLACE INTO SubscribeHistory(id, title, rss_url, add_date) VALUES (?, ?, ?, ?)""",
|
||||
[
|
||||
podcastLocal.id,
|
||||
podcastLocal.title,
|
||||
podcastLocal.rssUrl,
|
||||
milliseconds
|
||||
]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -416,26 +421,28 @@ class DBHelper {
|
|||
}
|
||||
|
||||
Future<void> saveHistory(PlayHistory history) async {
|
||||
var dbClient = await database;
|
||||
final milliseconds = DateTime.now().millisecondsSinceEpoch;
|
||||
var recent = await getPlayHistory(1);
|
||||
if (recent.isNotEmpty && recent.first.title == history.title) {
|
||||
await dbClient.rawDelete("DELETE FROM PlayHistory WHERE add_date = ?",
|
||||
[recent.first.playdate.millisecondsSinceEpoch]);
|
||||
}
|
||||
await dbClient.transaction((txn) async {
|
||||
return await txn.rawInsert(
|
||||
"""INSERT INTO PlayHistory (title, enclosure_url, seconds, seek_value, add_date, listen_time)
|
||||
if (history.url.substring(0, 7) != 'file://') {
|
||||
var dbClient = await database;
|
||||
final milliseconds = DateTime.now().millisecondsSinceEpoch;
|
||||
var recent = await getPlayHistory(1);
|
||||
if (recent.isNotEmpty && recent.first.title == history.title) {
|
||||
await dbClient.rawDelete("DELETE FROM PlayHistory WHERE add_date = ?",
|
||||
[recent.first.playdate.millisecondsSinceEpoch]);
|
||||
}
|
||||
await dbClient.transaction((txn) async {
|
||||
return await txn.rawInsert(
|
||||
"""INSERT INTO PlayHistory (title, enclosure_url, seconds, seek_value, add_date, listen_time)
|
||||
VALUES (?, ?, ?, ?, ?, ?) """,
|
||||
[
|
||||
history.title,
|
||||
history.url,
|
||||
history.seconds,
|
||||
history.seekValue,
|
||||
milliseconds,
|
||||
history.seekValue > 0.95 ? 1 : 0
|
||||
]);
|
||||
});
|
||||
[
|
||||
history.title,
|
||||
history.url,
|
||||
history.seconds,
|
||||
history.seekValue,
|
||||
milliseconds,
|
||||
history.seekValue > 0.95 ? 1 : 0
|
||||
]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
{bool reverse,
|
||||
Filter filter = Filter.all,
|
||||
|
@ -1192,16 +1229,17 @@ class DBHelper {
|
|||
"""SELECT E.title, E.enclosure_url, E.enclosure_length, E.is_new,
|
||||
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
|
||||
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
|
||||
OR SUM(H.listen_time) = 0 ORDER BY E.milliseconds DESC LIMIT ? """,
|
||||
[top]);
|
||||
[localFolderId, top]);
|
||||
} else {
|
||||
list = await dbClient.rawQuery(
|
||||
"""SELECT E.title, E.enclosure_url, E.enclosure_length, E.is_new,
|
||||
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
|
||||
ORDER BY E.milliseconds DESC LIMIT ? """, [top]);
|
||||
WHERE p.id != ? ORDER BY E.milliseconds DESC LIMIT ? """,
|
||||
[localFolderId, top]);
|
||||
}
|
||||
if (list.isNotEmpty) {
|
||||
for (var i in list) {
|
||||
|
@ -1231,15 +1269,17 @@ class DBHelper {
|
|||
"""SELECT E.title, E.enclosure_url, E.enclosure_length, E.is_new,
|
||||
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
|
||||
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
|
||||
OR SUM(H.listen_time) = 0 ORDER BY RANDOM() LIMIT ? """, [random]);
|
||||
OR SUM(H.listen_time) = 0 ORDER BY RANDOM() LIMIT ? """,
|
||||
[localFolderId, random]);
|
||||
} else {
|
||||
list = await dbClient.rawQuery(
|
||||
"""SELECT E.title, E.enclosure_url, E.enclosure_length, E.is_new,
|
||||
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
|
||||
ORDER BY RANDOM() LIMIT ? """, [random]);
|
||||
WHERE p.id != ? ORDER BY RANDOM() LIMIT ? """,
|
||||
[localFolderId, random]);
|
||||
}
|
||||
if (list.isNotEmpty) {
|
||||
for (var i in list) {
|
||||
|
|
|
@ -1,11 +1,20 @@
|
|||
import 'dart:developer' as developer;
|
||||
import 'dart:io';
|
||||
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/services.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:image/image.dart' as img;
|
||||
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:tuple/tuple.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../home/home.dart';
|
||||
import '../local_storage/sqflite_localpodcast.dart';
|
||||
|
@ -14,6 +23,7 @@ import '../state/setting_state.dart';
|
|||
import '../type/episodebrief.dart';
|
||||
import '../type/play_histroy.dart';
|
||||
import '../type/playlist.dart';
|
||||
import '../type/podcastlocal.dart';
|
||||
import '../util/extension_helper.dart';
|
||||
import '../util/pageroute.dart';
|
||||
import '../widgets/custom_widget.dart';
|
||||
|
@ -152,7 +162,9 @@ class _PlaylistHomeState extends State<PlaylistHome> {
|
|||
splashRadius: 20,
|
||||
icon: Icon(Icons.skip_next),
|
||||
onPressed: () {
|
||||
if (running) {
|
||||
if (running &&
|
||||
!(data.item1.length == 1 &&
|
||||
!data.item1.isQueue)) {
|
||||
audio.playNext();
|
||||
}
|
||||
}),
|
||||
|
@ -187,7 +199,7 @@ class _PlaylistHomeState extends State<PlaylistHome> {
|
|||
selector: (_, audio) => audio.lastPosition,
|
||||
builder: (_, position, __) {
|
||||
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(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 80,
|
||||
|
@ -225,9 +238,9 @@ class _PlaylistHomeState extends State<PlaylistHome> {
|
|||
width: 80,
|
||||
child: CustomPaint(
|
||||
painter: CircleProgressIndicator(
|
||||
progress,
|
||||
color: Colors.black38,
|
||||
),
|
||||
progress,
|
||||
color: context.primaryColor
|
||||
.withOpacity(0.9)),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
@ -333,14 +346,12 @@ class __QueueState extends State<_Queue> {
|
|||
key: ValueKey(episode.enclosureUrl),
|
||||
);
|
||||
} else {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
child: EpisodeCard(episode,
|
||||
key: ValueKey('playing'),
|
||||
isPlaying: true,
|
||||
canReorder: true,
|
||||
tileColor: context.primaryColorDark),
|
||||
);
|
||||
return EpisodeCard(episode,
|
||||
key: ValueKey('playing'),
|
||||
isPlaying: true,
|
||||
canReorder: true,
|
||||
havePadding: true,
|
||||
tileColor: context.primaryColorDark);
|
||||
}
|
||||
}).toList()
|
||||
: episodes
|
||||
|
@ -425,7 +436,7 @@ class __HistoryState extends State<_History> {
|
|||
}
|
||||
|
||||
Size _getMaskStop(double seekValue, int seconds) {
|
||||
final Size size = (TextPainter(
|
||||
final size = (TextPainter(
|
||||
text: TextSpan(text: seconds.toTime),
|
||||
maxLines: 1,
|
||||
textScaleFactor: MediaQuery.of(context).textScaleFactor,
|
||||
|
@ -711,19 +722,31 @@ class __PlaylistsState extends State<_Playlists> {
|
|||
Text(
|
||||
'${queue.length} ${s.episode(queue.length).toLowerCase()}'),
|
||||
TextButton(
|
||||
style: OutlinedButton.styleFrom(
|
||||
side: BorderSide(
|
||||
color: context.primaryColorDark),
|
||||
primary: context.accentColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(100)))),
|
||||
onPressed: () {
|
||||
context
|
||||
.read<AudioPlayerNotifier>()
|
||||
.playlistLoad(queue);
|
||||
},
|
||||
child: Text(s.play))
|
||||
style: TextButton.styleFrom(
|
||||
primary: context.accentColor,
|
||||
textStyle: TextStyle(
|
||||
fontWeight: FontWeight.bold)),
|
||||
onPressed: () {
|
||||
context
|
||||
.read<AudioPlayerNotifier>()
|
||||
.playlistLoad(queue);
|
||||
},
|
||||
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),
|
||||
subtitle: Text(
|
||||
'${data[index].length} ${s.episode(data[index].length).toLowerCase()}'),
|
||||
trailing: IconButton(
|
||||
splashRadius: 20,
|
||||
icon: Icon(LineIcons.playCircle, size: 30),
|
||||
trailing: TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
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: () {
|
||||
context
|
||||
.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 {
|
||||
_NewPlaylist({Key key}) : super(key: key);
|
||||
|
@ -819,11 +858,15 @@ class __NewPlaylistState extends State<_NewPlaylist> {
|
|||
final _dbHelper = DBHelper();
|
||||
String _playlistName = '';
|
||||
NewPlaylistOption _option;
|
||||
bool _loadFolder;
|
||||
FocusNode _focusNode;
|
||||
int _error;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadFolder = false;
|
||||
_focusNode = FocusNode();
|
||||
_option = NewPlaylistOption.blank;
|
||||
}
|
||||
|
||||
|
@ -837,7 +880,7 @@ class __NewPlaylistState extends State<_NewPlaylist> {
|
|||
|
||||
Widget _createOption(NewPlaylistOption option) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
padding: EdgeInsets.fromLTRB(0, 8, 8, 8),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
onTap: () {
|
||||
|
@ -845,19 +888,15 @@ class __NewPlaylistState extends State<_NewPlaylist> {
|
|||
},
|
||||
child: AnimatedContainer(
|
||||
duration: Duration(milliseconds: 300),
|
||||
padding: EdgeInsets.symmetric(horizontal: 8.0),
|
||||
padding: EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
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))),
|
||||
child: Text(_optionLabel(option).first,
|
||||
style: TextStyle(
|
||||
color: _option == option ? Colors.white : context.textColor)),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -874,12 +913,84 @@ class __NewPlaylistState extends State<_NewPlaylist> {
|
|||
case NewPlaylistOption.latest10:
|
||||
return ['Latest 10', 'Add 10 latest updated episodes to playlist'];
|
||||
break;
|
||||
case NewPlaylistOption.folder:
|
||||
return ['Local folder', 'Choose a local folder'];
|
||||
break;
|
||||
default:
|
||||
return ['', ''];
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
final s = context.s;
|
||||
|
@ -939,10 +1050,32 @@ class __NewPlaylistState extends State<_NewPlaylist> {
|
|||
);
|
||||
await playlist.getPlaylist();
|
||||
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:
|
||||
break;
|
||||
}
|
||||
context.read<AudioPlayerNotifier>().addPlaylist(playlist);
|
||||
if (playlist != null) {
|
||||
context.read<AudioPlayerNotifier>().addPlaylist(playlist);
|
||||
}
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
|
@ -952,52 +1085,63 @@ class __NewPlaylistState extends State<_NewPlaylist> {
|
|||
],
|
||||
title: SizedBox(
|
||||
width: context.width - 160, child: Text(s.createNewPlaylist)),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
TextField(
|
||||
decoration: InputDecoration(
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 10),
|
||||
hintText: s.createNewPlaylist,
|
||||
hintStyle: TextStyle(fontSize: 18),
|
||||
filled: true,
|
||||
focusedBorder: UnderlineInputBorder(
|
||||
borderSide:
|
||||
BorderSide(color: context.accentColor, width: 2.0),
|
||||
content: _loadFolder
|
||||
? SizedBox(
|
||||
height: 50,
|
||||
child: Center(
|
||||
child: Platform.isIOS
|
||||
? CupertinoActivityIndicator()
|
||||
: CircularProgressIndicator(),
|
||||
),
|
||||
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 != null
|
||||
? Text(
|
||||
_error == 1 ? s.playlistExisted : s.playlistNameEmpty,
|
||||
style: TextStyle(color: Colors.red[400]),
|
||||
)
|
||||
: Center()),
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
_createOption(NewPlaylistOption.blank),
|
||||
_createOption(NewPlaylistOption.randon10),
|
||||
_createOption(NewPlaylistOption.latest10),
|
||||
)
|
||||
: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
TextField(
|
||||
focusNode: _focusNode,
|
||||
decoration: InputDecoration(
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 10),
|
||||
hintText: s.createNewPlaylist,
|
||||
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;
|
||||
},
|
||||
),
|
||||
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)
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -2,12 +2,13 @@ import 'dart:math' as math;
|
|||
|
||||
import 'package:flutter/material.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 '../type/episodebrief.dart';
|
||||
import '../type/playlist.dart';
|
||||
import '../util/extension_helper.dart';
|
||||
import '../widgets/general_dialog.dart';
|
||||
|
||||
class PlaylistDetail extends StatefulWidget {
|
||||
final Playlist playlist;
|
||||
|
@ -41,7 +42,9 @@ class _PlaylistDetailState extends State<PlaylistDetail> {
|
|||
},
|
||||
),
|
||||
title: Text(_selectedEpisodes.isEmpty
|
||||
? widget.playlist.isQueue ? s.queue :widget.playlist.name
|
||||
? widget.playlist.isQueue
|
||||
? s.queue
|
||||
: widget.playlist.name
|
||||
: s.selected(_selectedEpisodes.length)),
|
||||
actions: [
|
||||
if (_selectedEpisodes.isNotEmpty)
|
||||
|
@ -85,8 +88,11 @@ class _PlaylistDetailState extends State<PlaylistDetail> {
|
|||
body: Selector<AudioPlayerNotifier, List<Playlist>>(
|
||||
selector: (_, audio) => audio.playlists,
|
||||
builder: (_, data, __) {
|
||||
final playlist = data.firstWhere((e) => e == widget.playlist);
|
||||
final episodes = playlist.episodes;
|
||||
final playlist = data.firstWhere(
|
||||
(e) => e == widget.playlist,
|
||||
orElse: () => null,
|
||||
);
|
||||
final episodes = playlist?.episodes ?? [];
|
||||
return ReorderableListView(
|
||||
onReorder: (oldIndex, newIndex) {
|
||||
if (widget.playlist.isQueue) {
|
||||
|
@ -193,95 +199,95 @@ class __PlaylistItemState extends State<_PlaylistItem>
|
|||
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,
|
||||
),
|
||||
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)),
|
||||
),
|
||||
),
|
||||
),
|
||||
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]),
|
||||
],
|
||||
),
|
||||
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(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ListTile(
|
||||
onTap: () {
|
||||
setState(() => _clearConfirm = true);
|
||||
},
|
||||
dense: true,
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(Icons.clear_all_outlined, size: 18),
|
||||
SizedBox(width: 20),
|
||||
Text(s.clearAll, style: textStyle),
|
||||
],
|
||||
if (!widget.playlist.isLocal)
|
||||
ListTile(
|
||||
onTap: () {
|
||||
setState(() => _clearConfirm = true);
|
||||
},
|
||||
dense: true,
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(Icons.clear_all_outlined, size: 18),
|
||||
SizedBox(width: 20),
|
||||
Text(s.clearAll, style: textStyle),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_clearConfirm)
|
||||
Container(
|
||||
width: double.infinity,
|
||||
|
|
|
@ -66,9 +66,11 @@ class EpisodeBrief extends Equatable {
|
|||
ImageProvider get avatarImage {
|
||||
return File(imagePath).existsSync()
|
||||
? FileImage(File(imagePath))
|
||||
: episodeImage != ''
|
||||
? CachedNetworkImageProvider(episodeImage)
|
||||
: AssetImage('assets/avatar_backup.png');
|
||||
: File(episodeImage).existsSync()
|
||||
? FileImage(File(episodeImage))
|
||||
: (episodeImage != '')
|
||||
? CachedNetworkImageProvider(episodeImage)
|
||||
: AssetImage('assets/avatar_backup.png');
|
||||
}
|
||||
|
||||
Color backgroudColor(BuildContext context) {
|
||||
|
|
|
@ -7,17 +7,24 @@ import 'episodebrief.dart';
|
|||
class PlaylistEntity {
|
||||
final String name;
|
||||
final String id;
|
||||
final bool isLocal;
|
||||
final List<String> episodeList;
|
||||
|
||||
PlaylistEntity(this.name, this.id, this.episodeList);
|
||||
PlaylistEntity(this.name, this.id, this.isLocal, this.episodeList);
|
||||
|
||||
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) {
|
||||
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.
|
||||
final String id;
|
||||
|
||||
final bool isLocal;
|
||||
|
||||
/// Episode url list for playlist.
|
||||
final List<String> episodeList;
|
||||
|
||||
|
@ -45,20 +54,24 @@ class Playlist extends Equatable {
|
|||
bool contains(EpisodeBrief episode) => episodes.contains(episode);
|
||||
|
||||
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(),
|
||||
assert(name != ''),
|
||||
episodeList = episodeList ?? [],
|
||||
episodes = episodes ?? [];
|
||||
|
||||
PlaylistEntity toEntity() {
|
||||
return PlaylistEntity(name, id, episodeList.toSet().toList());
|
||||
return PlaylistEntity(name, id, isLocal, episodeList.toSet().toList());
|
||||
}
|
||||
|
||||
static Playlist fromEntity(PlaylistEntity entity) {
|
||||
return Playlist(
|
||||
entity.name,
|
||||
id: entity.id,
|
||||
isLocal: entity.isLocal,
|
||||
episodeList: entity.episodeList,
|
||||
);
|
||||
}
|
||||
|
@ -119,6 +132,9 @@ class Playlist extends Equatable {
|
|||
episodes.removeWhere(
|
||||
(episode) => episode.enclosureUrl == episodeBrief.enclosureUrl);
|
||||
episodeList.removeWhere((url) => url == episodeBrief.enclosureUrl);
|
||||
if (isLocal) {
|
||||
_dbHelper.deleteLocalEpisodes([episodeBrief.enclosureUrl]);
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
|
|
|
@ -39,6 +39,9 @@ dependencies:
|
|||
fl_chart: ^0.12.2
|
||||
line_icons: ^1.3.2
|
||||
marquee: ^1.6.1
|
||||
media_metadata_retriever:
|
||||
git:
|
||||
url: https://github.com/stonega/media_metadata_retriever.git
|
||||
google_fonts: ^1.1.1
|
||||
image: ^2.1.19
|
||||
intl: ^0.16.1
|
||||
|
|
Loading…
Reference in New Issue