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';
|
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) {
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
@ -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)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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');
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue