diff --git a/lib/api/download.dart b/lib/api/download.dart index daec483..68f0aa5 100644 --- a/lib/api/download.dart +++ b/lib/api/download.dart @@ -136,7 +136,8 @@ class DownloadManager { } updateQueue(); } - ).catchError((err) async { + ).catchError((e, st) async { + print('Download error: $e\n$st'); //Catch download errors _download = null; _cancelNotifications = true; @@ -455,6 +456,24 @@ class DownloadManager { ]); } + //Delete download from db + Future removeDownload(Download download) async { + await db.delete('downloads', where: 'trackId == ?', whereArgs: [download.track.id]); + queue.removeWhere((d) => d.track.id == download.track.id); + //TODO: remove files for downloaded + } + + //Delete queue + Future clearQueue() async { + for (int i=queue.length-1; i>0; i--) { + await removeDownload(queue[i]); + } + } + + //Remove non-private downloads + Future cleanDownloadHistory() async { + await db.delete('downloads', where: 'private == 0'); + } } @@ -479,25 +498,29 @@ class Download { if (!this.private) { String ext = this.path; //Get track details - this.track = await deezerAPI.track(track.id); + Map rawTrack = (await deezerAPI.callApi('song.getListData', params: {'sng_ids': [track.id]}))['results']['data'][0]; + this.track = Track.fromPrivateJson(rawTrack); + + //Get path if public RegExp sanitize = RegExp(r'[\/\\\?\%\*\:\|\"\<\>]'); //Download path - if (settings.downloadFolderStructure) { - this.path = p.join( - settings.downloadPath ?? (await ExtStorage.getExternalStoragePublicDirectory(ExtStorage.DIRECTORY_MUSIC)), - track.artists[0].name.replaceAll(sanitize, ''), - track.album.title.replaceAll(sanitize, ''), - ); - } else { - this.path = settings.downloadPath; + this.path = settings.downloadPath ?? (await ExtStorage.getExternalStoragePublicDirectory(ExtStorage.DIRECTORY_MUSIC)); + if (settings.artistFolder) + this.path = p.join(this.path, track.artists[0].name.replaceAll(sanitize, '')); + if (settings.albumFolder) { + String folderName = track.album.title.replaceAll(sanitize, ''); + //Add disk number + if (settings.albumDiscFolder) folderName += ' - Disk ${rawTrack["DISK_NUMBER"]}'; + + this.path = p.join(this.path, folderName); } //Make dirs await Directory(this.path).create(recursive: true); //Grab cover _cover = p.join(this.path, 'cover.jpg'); - if (!settings.downloadFolderStructure) _cover = p.join(this.path, randomAlpha(12) + '_cover.jpg'); + if (!settings.albumFolder) _cover = p.join(this.path, randomAlpha(12) + '_cover.jpg'); if (!await File(_cover).exists()) { try { @@ -508,11 +531,22 @@ class Download { } catch (e) {print('Error downloading cover');} } - //Add filename - String _filename = '${track.trackNumber.toString().padLeft(2, '0')}. ${track.title.replaceAll(sanitize, "")}.$ext'; - //Different naming types - if (settings.downloadNaming == DownloadNaming.STANDALONE) - _filename = '${track.artistString.replaceAll(sanitize, "")} - ${track.title.replaceAll(sanitize, "")}.$ext'; + //Create filename + String _filename = settings.downloadFilename; + //Filters + Map vars = { + '%artists%': track.artistString.replaceAll(sanitize, ''), + '%artist%': track.artists[0].name.replaceAll(sanitize, ''), + '%title%': track.title.replaceAll(sanitize, ''), + '%album%': track.album.title.replaceAll(sanitize, ''), + '%trackNumber%': track.trackNumber.toString(), + '%0trackNumber%': track.trackNumber.toString().padLeft(2, '0') + }; + //Replace + vars.forEach((key, value) { + _filename = _filename.replaceAll(key, value); + }); + _filename += '.$ext'; this.path = p.join(this.path, _filename); } @@ -551,7 +585,7 @@ class Download { } //Remove encrypted await File(path + '.ENC').delete(); - if (!settings.downloadFolderStructure) await File(_cover).delete(); + if (!settings.albumFolder) await File(_cover).delete(); this.state = DownloadState.DONE; onDone(); return; diff --git a/lib/api/player.dart b/lib/api/player.dart index aadb323..a2b6ac2 100644 --- a/lib/api/player.dart +++ b/lib/api/player.dart @@ -585,4 +585,4 @@ class Seeker { void stop() { _running = false; } -} +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index b3f2ff7..61d493f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -186,7 +186,7 @@ class _MainScreenState extends State { body: AudioServiceWidget( child: CustomNavigator( navigatorKey: navigatorKey, - home: _screens[_selected], + home: _screens[_selected], pageRoute: PageRoutes.materialPageRoute, ), ) diff --git a/lib/settings.dart b/lib/settings.dart index 02567bb..5766d71 100644 --- a/lib/settings.dart +++ b/lib/settings.dart @@ -36,10 +36,16 @@ class Settings { //Download options String downloadPath; - @JsonKey(defaultValue: DownloadNaming.DEFAULT) - DownloadNaming downloadNaming; + + @JsonKey(defaultValue: "%artists% - %title%") + String downloadFilename; @JsonKey(defaultValue: true) - bool downloadFolderStructure; + bool albumFolder; + @JsonKey(defaultValue: true) + bool artistFolder; + @JsonKey(defaultValue: false) + bool albumDiscFolder; + //Appearance @JsonKey(defaultValue: Themes.Light) @@ -207,10 +213,4 @@ enum Themes { Dark, Deezer, Black -} - -enum DownloadNaming { - DEFAULT, - STANDALONE, - } \ No newline at end of file diff --git a/lib/settings.g.dart b/lib/settings.g.dart index eedd0d3..ee59dc9 100644 --- a/lib/settings.g.dart +++ b/lib/settings.g.dart @@ -23,10 +23,11 @@ Settings _$SettingsFromJson(Map json) { ..downloadQuality = _$enumDecodeNullable(_$AudioQualityEnumMap, json['downloadQuality']) ?? AudioQuality.FLAC - ..downloadNaming = - _$enumDecodeNullable(_$DownloadNamingEnumMap, json['downloadNaming']) ?? - DownloadNaming.DEFAULT - ..downloadFolderStructure = json['downloadFolderStructure'] as bool ?? true + ..downloadFilename = + json['downloadFilename'] as String ?? '%artists% - %title%' + ..albumFolder = json['albumFolder'] as bool ?? true + ..artistFolder = json['artistFolder'] as bool ?? true + ..albumDiscFolder = json['albumDiscFolder'] as bool ?? false ..theme = _$enumDecodeNullable(_$ThemesEnumMap, json['theme']) ?? Themes.Light ..primaryColor = Settings._colorFromJson(json['primaryColor'] as int) @@ -43,8 +44,10 @@ Map _$SettingsToJson(Settings instance) => { 'offlineQuality': _$AudioQualityEnumMap[instance.offlineQuality], 'downloadQuality': _$AudioQualityEnumMap[instance.downloadQuality], 'downloadPath': instance.downloadPath, - 'downloadNaming': _$DownloadNamingEnumMap[instance.downloadNaming], - 'downloadFolderStructure': instance.downloadFolderStructure, + 'downloadFilename': instance.downloadFilename, + 'albumFolder': instance.albumFolder, + 'artistFolder': instance.artistFolder, + 'albumDiscFolder': instance.albumDiscFolder, 'theme': _$ThemesEnumMap[instance.theme], 'primaryColor': Settings._colorToJson(instance.primaryColor), 'useArtColor': instance.useArtColor, @@ -91,11 +94,6 @@ const _$AudioQualityEnumMap = { AudioQuality.FLAC: 'FLAC', }; -const _$DownloadNamingEnumMap = { - DownloadNaming.DEFAULT: 'DEFAULT', - DownloadNaming.STANDALONE: 'STANDALONE', -}; - const _$ThemesEnumMap = { Themes.Light: 'Light', Themes.Dark: 'Dark', diff --git a/lib/ui/downloads_screen.dart b/lib/ui/downloads_screen.dart index 371585f..4da5f8b 100644 --- a/lib/ui/downloads_screen.dart +++ b/lib/ui/downloads_screen.dart @@ -8,7 +8,8 @@ import '../api/download.dart'; class DownloadTile extends StatelessWidget { final Download download; - DownloadTile(this.download); + Function onDelete; + DownloadTile(this.download, {this.onDelete}); String get subtitle { switch (download.state) { @@ -53,6 +54,34 @@ class DownloadTile extends StatelessWidget { url: download.track.albumArt.thumb, ), trailing: trailing, + onTap: () { + //Delete if none + if (download.state == DownloadState.NONE) { + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Text('Delete'), + content: Text('Are you sure, you want to delete this download?'), + actions: [ + FlatButton( + child: Text('Cancel'), + onPressed: () => Navigator.of(context).pop(), + ), + FlatButton( + child: Text('Delete'), + onPressed: () { + downloadManager.removeDownload(download); + if (this.onDelete != null) this.onDelete(); + Navigator.of(context).pop(); + }, + ) + ], + ); + } + ); + } + }, ), progressBar ], @@ -60,54 +89,99 @@ class DownloadTile extends StatelessWidget { } } -class DownloadsScreen extends StatelessWidget { +class DownloadsScreen extends StatefulWidget { + @override + _DownloadsScreenState createState() => _DownloadsScreenState(); +} + +class _DownloadsScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - title: Text('Downloads'), - ), - body: ListView( - children: [ - StreamBuilder( - stream: Stream.periodic(Duration(milliseconds: 500)), //Periodic to get current download progress - builder: (BuildContext context, AsyncSnapshot snapshot) { + appBar: AppBar( + title: Text('Downloads'), + actions: [ + IconButton( + icon: Icon(Icons.delete_sweep), + onPressed: () { + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Text('Delete'), + content: Text('Are you sure, you want to delete all queued downloads?'), + actions: [ + FlatButton( + child: Text('Cancel'), + onPressed: () => Navigator.of(context).pop(), + ), + FlatButton( + child: Text('Delete'), + onPressed: () async { + await downloadManager.clearQueue(); + Navigator.of(context).pop(); + }, + ) + ], + ); + } + ); + }, + ) + ], + ), + body: ListView( + children: [ + StreamBuilder( + stream: Stream.periodic(Duration(milliseconds: 500)).asBroadcastStream(), //Periodic to get current download progress + builder: (BuildContext context, AsyncSnapshot snapshot) { - if (downloadManager.queue.length == 0) - return Container(width: 0, height: 0,); + if (downloadManager.queue.length == 0) + return Container(width: 0, height: 0,); - return Column( - children: List.generate(downloadManager.queue.length, (i) { - return DownloadTile(downloadManager.queue[i]); - }) - ); - }, - ), - FutureBuilder( - future: downloadManager.getFinishedDownloads(), - builder: (context, snapshot) { - if (!snapshot.hasData || snapshot.data.length == 0) return Container(height: 0, width: 0,); + return Column( + children: List.generate(downloadManager.queue.length, (i) { + return DownloadTile(downloadManager.queue[i], onDelete: () => setState(() => {})); + }) + ); + }, + ), + FutureBuilder( + future: downloadManager.getFinishedDownloads(), + builder: (context, snapshot) { + if (!snapshot.hasData || snapshot.data.length == 0) return Container(height: 0, width: 0,); - return Column( - children: [ - Divider(), - Text( - 'History', - style: TextStyle( - fontSize: 24.0, - fontWeight: FontWeight.bold + return Column( + children: [ + Divider(), + Text( + 'History', + style: TextStyle( + fontSize: 24.0, + fontWeight: FontWeight.bold + ), ), - ), - ...List.generate(snapshot.data.length, (i) { - Download d = snapshot.data[i]; - return DownloadTile(d); - }) - ], - ); - }, - ) - ], - ) + ...List.generate(snapshot.data.length, (i) { + Download d = snapshot.data[i]; + return DownloadTile(d); + }), + ListTile( + title: Text('Clear downloads history'), + leading: Icon(Icons.delete), + subtitle: Text('WARNING: This will only clear non-offline (external downloads)'), + onTap: () async { + await downloadManager.cleanDownloadHistory(); + setState(() {}); + }, + ), + ], + ); + }, + ) + ], + ) ); } -} \ No newline at end of file +} + + diff --git a/lib/ui/home_screen.dart b/lib/ui/home_screen.dart index 755432e..3395081 100644 --- a/lib/ui/home_screen.dart +++ b/lib/ui/home_screen.dart @@ -123,8 +123,8 @@ class _HomePageScreenState extends State { @override void initState() { - _load(); super.initState(); + _load(); } @override diff --git a/lib/ui/settings_screen.dart b/lib/ui/settings_screen.dart index f3795b4..8c6d706 100644 --- a/lib/ui/settings_screen.dart +++ b/lib/ui/settings_screen.dart @@ -474,31 +474,62 @@ class _GeneralSettingsState extends State { ), ListTile( title: Text('Downloads naming'), + subtitle: Text('Currently: ${settings.downloadFilename}'), leading: Icon(Icons.text_format), onTap: () { showDialog( context: context, builder: (context) { - return SimpleDialog( - children: [ - ListTile( - title: Text('Default naming'), - subtitle: Text('01. Title'), - onTap: () { - settings.downloadNaming = DownloadNaming.DEFAULT; - Navigator.of(context).pop(); - settings.save(); + + TextEditingController _controller = TextEditingController(); + String filename = settings.downloadFilename; + _controller.value = _controller.value.copyWith(text: filename); + + //Dialog with filename format + return AlertDialog( + title: Text('Downloaded tracks filename'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + controller: _controller, + ), + Container(height: 8.0), + Text( + 'Valid variables are: %artists%, %artist%, %title%, %album%, %trackNumber%, %0trackNumber%', + style: TextStyle( + fontSize: 12.0, + ), + ) + ], + ), + actions: [ + FlatButton( + child: Text('Cancel'), + onPressed: () => Navigator.of(context).pop(), + ), + FlatButton( + child: Text('Reset'), + onPressed: () { + _controller.value = _controller.value.copyWith( + text: '%artists% - %title%' + ); }, ), - ListTile( - title: Text('Standalone naming'), - subtitle: Text('Artist - Title'), - onTap: () { - settings.downloadNaming = DownloadNaming.STANDALONE; - Navigator.of(context).pop(); - settings.save(); - }, + FlatButton( + child: Text('Clear'), + onPressed: () => _controller.clear(), ), + FlatButton( + child: Text('Save'), + onPressed: () { + setState(() { + settings.downloadFilename = _controller.text; + settings.save(); + Navigator.of(context).pop(); + }); + }, + ) ], ); } @@ -506,12 +537,31 @@ class _GeneralSettingsState extends State { }, ), ListTile( - title: Text('Create download folder structure'), - subtitle: Text('Artist/Album/Track'), + title: Text('Create folders for artist'), leading: Switch( - value: settings.downloadFolderStructure, + value: settings.artistFolder, onChanged: (v) { - setState(() => settings.downloadFolderStructure = v); + setState(() => settings.artistFolder = v); + settings.save(); + }, + ), + ), + ListTile( + title: Text('Create folders for albums'), + leading: Switch( + value: settings.albumFolder, + onChanged: (v) { + setState(() => settings.albumFolder = v); + settings.save(); + }, + ), + ), + ListTile( + title: Text('Separate albums by discs'), + leading: Switch( + value: settings.albumDiscFolder, + onChanged: (v) { + setState(() => settings.albumDiscFolder = v); settings.save(); }, ),