diff --git a/lib/local_storage/sqflite_localpodcast.dart b/lib/local_storage/sqflite_localpodcast.dart index 221c306..75a15fc 100644 --- a/lib/local_storage/sqflite_localpodcast.dart +++ b/lib/local_storage/sqflite_localpodcast.dart @@ -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 _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> getPodcastLocal(List podcasts, @@ -189,18 +191,20 @@ class DBHelper { var 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.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.from(jsonDecode(list.first['funding'])), + )); + } } return podcastLocal; } @@ -261,7 +265,7 @@ class DBHelper { [boo ? 1 : 0, id]); } - Future getHideNewMark(String id) async { + Future getHideNewMark(String id) async { var dbClient = await database; List 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 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> getPlayHistory(int top) async { @@ -786,6 +793,36 @@ class DBHelper { } } + Future 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 deleteLocalEpisodes(List files) async { + var dbClient = await database; + var s = files.map((e) => "'$e'").toList(); + await dbClient.rawDelete( + 'DELETE FROM Episodes WHERE enclosure_url in (${s.join(',')})'); + } + Future> 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) { diff --git a/lib/playlists/playlist_home.dart b/lib/playlists/playlist_home.dart index 08cdf1e..de1502d 100644 --- a/lib/playlists/playlist_home.dart +++ b/lib/playlists/playlist_home.dart @@ -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 { 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 { 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 { ? ClipRRect( borderRadius: BorderRadius.circular(10), child: Stack( + alignment: Alignment.center, children: [ SizedBox( width: 80, @@ -225,9 +238,9 @@ class _PlaylistHomeState extends State { 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() - .playlistLoad(queue); - }, - child: Text(s.play)) + style: TextButton.styleFrom( + primary: context.accentColor, + textStyle: TextStyle( + fontWeight: FontWeight.bold)), + onPressed: () { + context + .read() + .playlistLoad(queue); + }, + child: Row( + children: [ + 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: [ + 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() @@ -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> _loadLocalFolder() async { + var episodes = []; + 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 _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 _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().addPlaylist(playlist); + if (playlist != null) { + context.read().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: [ - 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: [ + 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) + ], + ), ], ), - ), - ], - ), ), ); } diff --git a/lib/playlists/playlist_page.dart b/lib/playlists/playlist_page.dart index 34fc341..8f3d365 100644 --- a/lib/playlists/playlist_page.dart +++ b/lib/playlists/playlist_page.dart @@ -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 { }, ), 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 { body: Selector>( 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: [ - 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: [ + 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: [ + 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: [ - 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, diff --git a/lib/type/episodebrief.dart b/lib/type/episodebrief.dart index 34966a0..31a6329 100644 --- a/lib/type/episodebrief.dart +++ b/lib/type/episodebrief.dart @@ -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) { diff --git a/lib/type/playlist.dart b/lib/type/playlist.dart index 0d1f823..58c3d6d 100644 --- a/lib/type/playlist.dart +++ b/lib/type/playlist.dart @@ -7,17 +7,24 @@ import 'episodebrief.dart'; class PlaylistEntity { final String name; final String id; + final bool isLocal; final List episodeList; - PlaylistEntity(this.name, this.id, this.episodeList); + PlaylistEntity(this.name, this.id, this.isLocal, this.episodeList); Map toJson() { - return {'name': name, 'id': id, 'episodeList': episodeList}; + return { + 'name': name, + 'id': id, + 'isLocal': isLocal, + 'episodeList': episodeList + }; } static PlaylistEntity fromJson(Map json) { var list = List.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 episodeList; @@ -45,20 +54,24 @@ class Playlist extends Equatable { bool contains(EpisodeBrief episode) => episodes.contains(episode); Playlist(this.name, - {String id, List episodeList, List episodes}) + {String id, + this.isLocal = false, + List episodeList, + List 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; } diff --git a/pubspec.yaml b/pubspec.yaml index 371dbda..62b8a0a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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