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';
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) {

View File

@ -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)
],
),
],
),
),
],
),
),
);
}

View File

@ -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,

View File

@ -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) {

View File

@ -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;
}

View File

@ -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