From 2f471268c6b0b43516c6e13a71088b752fc423da Mon Sep 17 00:00:00 2001 From: exttex Date: Mon, 12 Oct 2020 22:49:13 +0200 Subject: [PATCH] 0.5.3 - Download fixes, shuffle fix, sorting in library --- .../app/src/main/java/f/f/freezer/Deezer.java | 14 + .../src/main/java/f/f/freezer/Download.java | 5 + .../java/f/f/freezer/DownloadService.java | 31 +- lib/api/cache.dart | 9 + lib/api/cache.g.dart | 37 ++- lib/api/player.dart | 28 +- lib/languages/ar_ar.dart | 6 +- lib/languages/de_de.dart | 40 +-- lib/languages/el_gr.dart | 107 +++++-- lib/languages/es_es.dart | 3 +- lib/languages/ru_ru.dart | 5 +- lib/languages/tr_tr.dart | 60 ++-- lib/ui/details_screens.dart | 2 +- lib/ui/library.dart | 276 +++++++++++++++--- lib/ui/login_screen.dart | 10 +- lib/ui/settings_screen.dart | 63 ++-- lib/ui/tiles.dart | 25 +- pubspec.yaml | 2 +- 18 files changed, 556 insertions(+), 167 deletions(-) diff --git a/android/app/src/main/java/f/f/freezer/Deezer.java b/android/app/src/main/java/f/f/freezer/Deezer.java index f27e777..217ca06 100644 --- a/android/app/src/main/java/f/f/freezer/Deezer.java +++ b/android/app/src/main/java/f/f/freezer/Deezer.java @@ -312,6 +312,20 @@ public class Deezer { return original + ".mp3"; } + public static String generateUserUploadedMP3Filename(String original, JSONObject privateJson) throws Exception { + //Remove unavailable tags + String[] ignored = {"%feats%", "%trackNumber%", "%0trackNumber%", "%year%", "%date%"}; + for (String i : ignored) { + original = original.replaceAll(i, ""); + } + //Basic tags + original = original.replaceAll("%title%", privateJson.getString("SNG_TITLE")); + original = original.replaceAll("%album%", privateJson.getString("ALB_TITLE")); + original = original.replaceAll("%artist%", privateJson.getString("ART_NAME")); + original = original.replaceAll("%artists%", privateJson.getString("ART_NAME")); + return original; + } + //Tag track with data from API public static void tagTrack(String path, JSONObject publicTrack, JSONObject publicAlbum, String cover, JSONObject lyricsData) throws Exception { TagOptionSingleton.getInstance().setAndroid(true); diff --git a/android/app/src/main/java/f/f/freezer/Download.java b/android/app/src/main/java/f/f/freezer/Download.java index f761933..b6fa03c 100644 --- a/android/app/src/main/java/f/f/freezer/Download.java +++ b/android/app/src/main/java/f/f/freezer/Download.java @@ -51,6 +51,11 @@ public class Download { } } + //Negative TrackIDs = User uploaded MP3s. + public boolean isUserUploaded() { + return trackId.startsWith("-"); + } + //Get download from SQLite cursor, HAS TO ALIGN static Download fromSQL(Cursor cursor) { return new Download(cursor.getInt(0), cursor.getString(1), cursor.getInt(2) == 1, cursor.getInt(3), DownloadState.values()[cursor.getInt(4)], diff --git a/android/app/src/main/java/f/f/freezer/DownloadService.java b/android/app/src/main/java/f/f/freezer/DownloadService.java index be5e860..d3eb6f7 100644 --- a/android/app/src/main/java/f/f/freezer/DownloadService.java +++ b/android/app/src/main/java/f/f/freezer/DownloadService.java @@ -274,6 +274,7 @@ public class DownloadService extends Service { File outFile; JSONObject trackJson; JSONObject albumJson; + JSONObject privateJson; boolean stopDownload = false; DownloadThread(Download download) { this.download = download; @@ -293,8 +294,13 @@ public class DownloadService extends Service { //Fetch metadata try { - trackJson = Deezer.callPublicAPI("track", download.trackId); - albumJson = Deezer.callPublicAPI("album", Integer.toString(trackJson.getJSONObject("album").getInt("id"))); + JSONObject privateRaw = deezer.callGWAPI("song.getListData", "{\"sng_ids\": [" + download.trackId + "]}"); + privateJson = privateRaw.getJSONObject("results").getJSONArray("data").getJSONObject(0); + //Don't fetch meta if user uploaded mp3 + if (!download.isUserUploaded()) { + trackJson = Deezer.callPublicAPI("track", download.trackId); + albumJson = Deezer.callPublicAPI("album", Integer.toString(trackJson.getJSONObject("album").getInt("id"))); + } } catch (Exception e) { logger.error("Unable to fetch track and album metadata! " + e.toString(), download); e.printStackTrace(); @@ -305,7 +311,7 @@ public class DownloadService extends Service { //ISRC Fallback try { - if (trackJson.has("available_countries") && trackJson.getJSONArray("available_countries").length() == 0) { + if (!download.isUserUploaded() && trackJson.has("available_countries") && trackJson.getJSONArray("available_countries").length() == 0) { logger.warn("ISRC Fallback!", download); JSONObject newTrackJson = Deezer.callPublicAPI("track", "isrc:" + trackJson.getString("isrc")); //Same track check @@ -349,7 +355,11 @@ public class DownloadService extends Service { if (!download.priv) { //Check file try { - outFile = new File(Deezer.generateFilename(download.path, trackJson, albumJson, newQuality)); + if (download.isUserUploaded()) { + outFile = new File(Deezer.generateUserUploadedMP3Filename(download.path, privateJson)); + } else { + outFile = new File(Deezer.generateFilename(download.path, trackJson, albumJson, newQuality)); + } parentDir = new File(outFile.getParent()); } catch (Exception e) { logger.error("Error generating track filename (" + download.path + "): " + e.toString(), download); @@ -486,7 +496,8 @@ public class DownloadService extends Service { } } - if (!download.priv) { + //Cover & Tags, ignore on user uploaded + if (!download.priv && !download.isUserUploaded()) { //Download cover for each track File coverFile = new File(outFile.getPath().substring(0, outFile.getPath().lastIndexOf('.')) + ".jpg"); @@ -520,18 +531,18 @@ public class DownloadService extends Service { JSONObject lyricsData = null; //Lyrics - if (settings.downloadLyrics) { - try { - lyricsData = deezer.callGWAPI("song.getLyrics", "{\"sng_id\": " + download.trackId + "}"); + try { + lyricsData = deezer.callGWAPI("song.getLyrics", "{\"sng_id\": " + download.trackId + "}"); + if (settings.downloadLyrics) { String lrcData = Deezer.generateLRC(lyricsData, trackJson); //Create file String lrcFilename = outFile.getPath().substring(0, outFile.getPath().lastIndexOf(".")+1) + "lrc"; FileOutputStream fileOutputStream = new FileOutputStream(lrcFilename); fileOutputStream.write(lrcData.getBytes()); fileOutputStream.close(); - } catch (Exception e) { - logger.warn("Error downloading lyrics! " + e.toString(), download); } + } catch (Exception e) { + logger.warn("Error downloading lyrics! " + e.toString(), download); } diff --git a/lib/api/cache.dart b/lib/api/cache.dart index 18cb132..d293537 100644 --- a/lib/api/cache.dart +++ b/lib/api/cache.dart @@ -1,6 +1,7 @@ import 'package:freezer/api/deezer.dart'; import 'package:freezer/api/definitions.dart'; import 'package:freezer/ui/details_screens.dart'; +import 'package:freezer/ui/library.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:path_provider/path_provider.dart'; import 'package:path/path.dart' as p; @@ -30,6 +31,14 @@ class Cache { @JsonKey(defaultValue: {}) Map playlistSort; + //Sort + @JsonKey(defaultValue: AlbumSortType.DEFAULT) + AlbumSortType albumSort; + @JsonKey(defaultValue: ArtistSortType.DEFAULT) + ArtistSortType artistSort; + @JsonKey(defaultValue: PlaylistSortType.DEFAULT) + PlaylistSortType libraryPlaylistSort; + Cache({this.libraryTracks}); diff --git a/lib/api/cache.g.dart b/lib/api/cache.g.dart index 6274d71..565358e 100644 --- a/lib/api/cache.g.dart +++ b/lib/api/cache.g.dart @@ -19,7 +19,16 @@ Cache _$CacheFromJson(Map json) { ..playlistSort = (json['playlistSort'] as Map)?.map( (k, e) => MapEntry(k, _$enumDecodeNullable(_$SortTypeEnumMap, e)), ) ?? - {}; + {} + ..albumSort = + _$enumDecodeNullable(_$AlbumSortTypeEnumMap, json['albumSort']) ?? + AlbumSortType.DEFAULT + ..artistSort = + _$enumDecodeNullable(_$ArtistSortTypeEnumMap, json['artistSort']) ?? + ArtistSortType.DEFAULT + ..libraryPlaylistSort = _$enumDecodeNullable( + _$PlaylistSortTypeEnumMap, json['libraryPlaylistSort']) ?? + PlaylistSortType.DEFAULT; } Map _$CacheToJson(Cache instance) => { @@ -27,6 +36,10 @@ Map _$CacheToJson(Cache instance) => { 'history': instance.history, 'playlistSort': instance.playlistSort ?.map((k, e) => MapEntry(k, _$SortTypeEnumMap[e])), + 'albumSort': _$AlbumSortTypeEnumMap[instance.albumSort], + 'artistSort': _$ArtistSortTypeEnumMap[instance.artistSort], + 'libraryPlaylistSort': + _$PlaylistSortTypeEnumMap[instance.libraryPlaylistSort], }; T _$enumDecode( @@ -67,3 +80,25 @@ const _$SortTypeEnumMap = { SortType.ALPHABETIC: 'ALPHABETIC', SortType.ARTIST: 'ARTIST', }; + +const _$AlbumSortTypeEnumMap = { + AlbumSortType.DEFAULT: 'DEFAULT', + AlbumSortType.REVERSE: 'REVERSE', + AlbumSortType.ALPHABETIC: 'ALPHABETIC', + AlbumSortType.ARTIST: 'ARTIST', +}; + +const _$ArtistSortTypeEnumMap = { + ArtistSortType.DEFAULT: 'DEFAULT', + ArtistSortType.REVERSE: 'REVERSE', + ArtistSortType.POPULARITY: 'POPULARITY', + ArtistSortType.ALPHABETIC: 'ALPHABETIC', +}; + +const _$PlaylistSortTypeEnumMap = { + PlaylistSortType.DEFAULT: 'DEFAULT', + PlaylistSortType.REVERSE: 'REVERSE', + PlaylistSortType.ALPHABETIC: 'ALPHABETIC', + PlaylistSortType.USER: 'USER', + PlaylistSortType.TRACK_COUNT: 'TRACK_COUNT', +}; diff --git a/lib/api/player.dart b/lib/api/player.dart index be805cf..7001208 100644 --- a/lib/api/player.dart +++ b/lib/api/player.dart @@ -246,6 +246,7 @@ class AudioPlayerTask extends BackgroundAudioTask { //Queue List _queue = []; + List _shuffleHistory = []; int _queueIndex = 0; ConcatenatingAudioSource _audioSource; @@ -363,13 +364,15 @@ class AudioPlayerTask extends BackgroundAudioTask { Future onSkipToNext() async { //Shuffle if (_player.shuffleModeEnabled??false) { - int newIndex = Random().nextInt(_queue.length)-1; + int newIndex = Random().nextInt(_queue.length-1); //Update state _skipState = newIndex > _queueIndex ? AudioProcessingState.skippingToNext : AudioProcessingState.skippingToPrevious; + if (_shuffleHistory.length == 0) _shuffleHistory.add(_queueIndex); _queueIndex = newIndex; + _shuffleHistory.add(newIndex); await _player.seek(Duration.zero, index: _queueIndex); _skipState = null; return; @@ -385,9 +388,24 @@ class AudioPlayerTask extends BackgroundAudioTask { @override Future onSkipToPrevious() async { - if (_queueIndex == 0) return; + if (_queueIndex == 0 && !(_player.shuffleModeEnabled??false)) return; //Update buffering state _skipState = AudioProcessingState.skippingToPrevious; + + //Shuffle history + if ((_player.shuffleModeEnabled??false) && _shuffleHistory.length > 1) { + _shuffleHistory.removeLast(); + if (_shuffleHistory.last < _queue.length) { + _queueIndex = _shuffleHistory.last; + await _player.seek(Duration.zero, index: _queueIndex); + _skipState = null; + return; + } else { + _shuffleHistory = []; + } + } + + //Normal skip to previous _queueIndex--; await _player.seekToPrevious(); _skipState = null; @@ -553,9 +571,11 @@ class AudioPlayerTask extends BackgroundAudioTask { if (name == 'repeatType') { _player.setLoopMode(LoopMode.values[args]); } - if (name == 'saveQueue') await this._saveQueue(); + if (name == 'saveQueue') + await this._saveQueue(); //Load queue after some initialization in frontend - if (name == 'load') await this._loadQueueFile(); + if (name == 'load') + await this._loadQueueFile(); //Shuffle if (name == 'shuffle') { await _player.setShuffleModeEnabled(args); diff --git a/lib/languages/ar_ar.dart b/lib/languages/ar_ar.dart index 1494318..954b18a 100644 --- a/lib/languages/ar_ar.dart +++ b/lib/languages/ar_ar.dart @@ -202,13 +202,11 @@ const language_ar_ar = { //0.5.2 Strings: "Use system theme": "استخدم ثيم النظام", "Light": "ابيض", - - //0.5.3 Strings: + + //0.5.3 Strings: "Popularity": "الشعبية", "User": "المستخدم", "Track count": "عدد الاغاني", "If you want to use custom directory naming - use '/' as directory separator.": "إذا كنت تريد استخدام تسمية مخصصة، استخدم '/' كفاصل بين المسار." - - } }; diff --git a/lib/languages/de_de.dart b/lib/languages/de_de.dart index 93c3c2b..79e66d8 100644 --- a/lib/languages/de_de.dart +++ b/lib/languages/de_de.dart @@ -9,7 +9,7 @@ const language_de_de = { "Search": "Suche", "Library": "Mediathek", "Offline mode, can't play flow or smart track lists.": - "Offline-Modus, kann keine Flow- oder Smart Track-Listen abspielen.", + "Offline-Modus, kann keine Flow- oder Smart Track-Listen abspielen.", "Added to library": "Zur Mediathek hinzufügen", "Download": "Download", "Disk": "Disk", @@ -28,31 +28,31 @@ const language_de_de = { "Done": "Erledigt", "Delete": "Gelöscht", "Are you sure you want to delete this download?": - "Bist du sicher, dass du diesen Download löschen willst?", + "Bist du sicher, dass du diesen Download löschen willst?", "Cancel": "Abbrechen", "Downloads": "Downloads", "Clear queue": "Warteschleife löschen", "This won't delete currently downloading item": - "Dies löscht das derzeit heruntergeladene Element nicht", + "Dies löscht das derzeit heruntergeladene Element nicht", "Are you sure you want to delete all queued downloads?": - "Bist du sicher, dass du alle Downloads aus der Warteschleife löschen willst?", + "Bist du sicher, dass du alle Downloads aus der Warteschleife löschen willst?", "Clear downloads history": "Download-Verlauf löschen", "WARNING: This will only clear non-offline (external downloads)": - "ACHTUNG: (Externe Downloads) werden entfernt", + "ACHTUNG: (Externe Downloads) werden entfernt", "Please check your connection and try again later...": - "Bitte überprüfe deine Verbindung und versuche es später noch einmal...", + "Bitte überprüfe deine Verbindung und versuche es später noch einmal...", "Show more": "Mehr anzeigen", "Importer": "Importieren", "Currently supporting only Spotify, with 100 tracks limit": - "Derzeit begrenzt auf maximal 100 Titel", + "Derzeit begrenzt auf maximal 100 Titel", "Due to API limitations": "Aufgrund von API-Einschränkungen", "Enter your playlist link below": - "Gebe deinen Wiedergabelisten-Link unten ein", + "Gebe deinen Wiedergabelisten-Link unten ein", "Error loading URL!": "Fehler beim Laden der URL!", "Convert": "Konvertieren", "Download only": "Nur Herunterladen", "Downloading is currently stopped, click here to resume.": - "Das Herunterladen ist derzeit gestoppt, klicke hier, um fortzufahren.", + "Das Herunterladen ist derzeit gestoppt, klicke hier, um fortzufahren.", "Tracks": "Titel", "Albums": "Alben", "Artists": "Künstler", @@ -70,24 +70,24 @@ const language_de_de = { "All offline tracks": "Alle Offline-Titel", "Create new playlist": "Neue Wiedergabeliste erstellen", "Cannot create playlists in offline mode": - "Wiedergabelisten können im Offline-Modus nicht erstellt werden", + "Wiedergabelisten können im Offline-Modus nicht erstellt werden", "Error": "Fehler", "Error logging in! Please check your token and internet connection and try again.": - "Fehler beim Einloggen! Bitte überprüfe dein Token und deine Internetverbindung und versuche es erneut.", + "Fehler beim Einloggen! Bitte überprüfe dein Token und deine Internetverbindung und versuche es erneut.", "Dismiss": "Verwerfen", "Welcome to": "Willkommen bei", "Please login using your Deezer account.": - "Bitte melde dich mit deinem Deezer-Konto an.", + "Bitte melde dich mit deinem Deezer-Konto an.", "Login using browser": "Anmeldung über Browser", "Login using token": "Anmeldung per Token", "Enter ARL": "ARL eingeben", "Token (ARL)": "Token (ARL)", "Save": "Speichern", "If you don't have account, you can register on deezer.com for free.": - "Wenn Du noch kein Konto hast, kannst Du Dich kostenlos auf deezer.com registrieren.", + "Wenn Du noch kein Konto hast, kannst Du Dich kostenlos auf deezer.com registrieren.", "Open in browser": "Im Browser öffnen", "By using this app, you don't agree with the Deezer ToS": - "Wenn Du diese Anwendung verwendest, bist Du nicht mit den Deezer ToS einverstanden", + "Wenn Du diese Anwendung verwendest, bist Du nicht mit den Deezer ToS einverstanden", "Play next": "Als nächstes spielen", "Add to queue": "Zur Warteschleife hinzufügen", "Add track to favorites": "Titel zu Favoriten hinzufügen", @@ -151,11 +151,11 @@ const language_de_de = { "Country used in headers. Now": "Aktuell", "Log tracks": "Protokolliere Titel", "Send track listen logs to Deezer, enable it for features like Flow to work properly": - "Gehörte Titel-Protokolle an Deezer senden, damit Flow richtig funktioniert", + "Gehörte Titel-Protokolle an Deezer senden, damit Flow richtig funktioniert", "Offline mode": "Offline-Modus", "Will be overwritten on start.": "Wird beim Start überschrieben.", "Error logging in, check your internet connections.": - "Fehler beim Anmelden, überprüfe deine Internetverbindung.", + "Fehler beim Anmelden, überprüfe deine Internetverbindung.", "Logging in...": "Angemeldet...", "Download path": "Download-Pfad", "Downloads naming": "Benennung der Downloads", @@ -167,14 +167,14 @@ const language_de_de = { "Create folders for albums": "Ordner für Alben erstellen", "Separate albums by discs": "Alben nach Discs trennen", "Overwrite already downloaded files": - "Bereits heruntergeladene Dateien überschreiben", + "Bereits heruntergeladene Dateien überschreiben", "Copy ARL": "ARL kopieren", "Copy userToken/ARL Cookie for use in other apps.": - "UserToken / ARL-Cookie zur Verwendung in anderen Anwendungen kopieren.", + "UserToken / ARL-Cookie zur Verwendung in anderen Anwendungen kopieren.", "Copied": "Kopiert", "Log out": "Abmelden", "Due to plugin incompatibility, login using browser is unavailable without restart.": - "Aufgrund von Plugin-Inkompatibilität ist die Anmeldung mit dem Browser ohne Neustart nicht möglich.", + "Aufgrund von Plugin-Inkompatibilität ist die Anmeldung mit dem Browser ohne Neustart nicht möglich.", "(ARL ONLY) Continue": "(NUR ARL) Fortfahren", "Log out & Exit": "Abmelden & Beenden", "Pick-a-Path": "Wähle einen Pfad", @@ -183,7 +183,7 @@ const language_de_de = { "Permission denied": "Zugriff verweigert", "Language": "Sprache", "Language changed, please restart Freezer to apply!": - "Sprache geändert, bitte Freezer neu starten!", + "Sprache geändert, bitte Freezer neu starten!", "Importing...": "Importiere...", "Radio": "Radio", //0.5.0 Strings: diff --git a/lib/languages/el_gr.dart b/lib/languages/el_gr.dart index aef355e..00c9bfa 100644 --- a/lib/languages/el_gr.dart +++ b/lib/languages/el_gr.dart @@ -10,7 +10,7 @@ const language_el_gr = { "Search": "Αναζήτηση", "Library": "Βιβλιοθήκη", "Offline mode, can't play flow or smart track lists.": - "Λειτουργία εκτός σύνδεσης, δεν είναι δυνατή η αναπαραγωγή flow ή έξυπνων λιστών κομματιών.", + "Λειτουργία εκτός σύνδεσης, δεν είναι δυνατή η αναπαραγωγή flow ή έξυπνων λιστών κομματιών.", "Added to library": "Προστέθηκε στη βιβλιοθήκη", "Download": "Λήψη", "Disk": "Δίσκος", @@ -29,37 +29,39 @@ const language_el_gr = { "Done": "Ολοκληρώθηκε", "Delete": "Διαγραφή", "Are you sure you want to delete this download?": - "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτήν τη λήψη;", + "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτήν τη λήψη;", "Cancel": "Άκυρο", "Downloads": "Λήψεις", "Clear queue": "Εκκαθάριση ουράς", "This won't delete currently downloading item": - "Αυτό δεν θα διαγράψει το τρέχον αντικείμενο λήψης", + "Αυτό δεν θα διαγράψει το τρέχον αντικείμενο λήψης", "Are you sure you want to delete all queued downloads?": - "Είστε βέβαιοι ότι θέλετε να διαγράψετε όλες τις λήψεις στην ουρά;", + "Είστε βέβαιοι ότι θέλετε να διαγράψετε όλες τις λήψεις στην ουρά;", "Clear downloads history": "Διαγραφή ιστορικού λήψεων", "WARNING: This will only clear non-offline (external downloads)": - "ΠΡΟΕΙΔΟΠΟΙΗΣΗ: Αυτό θα καθαρίσει μόνο τις εκτός σύνδεσης (εξωτερικές) λήψεις", + "ΠΡΟΕΙΔΟΠΟΙΗΣΗ: Αυτό θα καθαρίσει μόνο τις εκτός σύνδεσης (εξωτερικές) λήψεις", "Please check your connection and try again later...": - "Ελέγξτε τη σύνδεσή σας και δοκιμάστε ξανά αργότερα...", + "Ελέγξτε τη σύνδεσή σας και δοκιμάστε ξανά αργότερα...", "Show more": "Δείτε περισσότερα", "Importer": "Εισαγωγέας", "Currently supporting only Spotify, with 100 tracks limit": - "Αυτήν τη στιγμή υποστηρίζεται μόνο το Spotify, με όριο 100 κομματιών", + "Αυτήν τη στιγμή υποστηρίζεται μόνο το Spotify, με όριο 100 κομματιών", "Due to API limitations": "Λόγω περιορισμών API", - "Enter your playlist link below": "Εισαγάγετε τον σύνδεσμο λίστας αναπαραγωγής παρακάτω", + "Enter your playlist link below": + "Εισαγάγετε τον σύνδεσμο λίστας αναπαραγωγής παρακάτω", "Error loading URL!": "Σφάλμα φόρτωσης διεύθυνσης URL!", "Convert": "Μετατροπή", "Download only": "Μόνο λήψη", "Downloading is currently stopped, click here to resume.": - "Η λήψη έχει σταματήσει, κάντε κλικ εδώ για να συνεχίσετε.", + "Η λήψη έχει σταματήσει, κάντε κλικ εδώ για να συνεχίσετε.", "Tracks": "Κομμάτια", "Albums": "Album", "Artists": "Καλλιτέχνες", "Playlists": "Λίστες αναπαραγωγής", "Import": "Εισαγωγή", - "Import playlists from Spotify": "Εισαγωγή λιστών αναπαραγωγής από το Spotify", - "Statistics": "Στατιστική", + "Import playlists from Spotify": + "Εισαγωγή λιστών αναπαραγωγής από το Spotify", + "Statistics": "Στατιστικά", "Offline tracks": "Κομμάτια εκτός σύνδεσης", "Offline albums": "Album εκτός σύνδεσης", "Offline playlists": "Λίστες αναπαραγωγής εκτός σύνδεσης", @@ -70,24 +72,24 @@ const language_el_gr = { "All offline tracks": "Όλα τα κομμάτια εκτός σύνδεσης", "Create new playlist": "Δημιουργία λίστας αναπαραγωγής", "Cannot create playlists in offline mode": - "Δεν είναι δυνατή η δημιουργία λιστών αναπαραγωγής σε λειτουργία εκτός σύνδεσης", + "Δεν είναι δυνατή η δημιουργία λιστών αναπαραγωγής σε λειτουργία εκτός σύνδεσης", "Error": "Σφάλμα", "Error logging in! Please check your token and internet connection and try again.": - "Σφάλμα σύνδεσης! Ελέγξτε το token και τη σύνδεσή σας στο δίκτυο και δοκιμάστε ξανά.", + "Σφάλμα σύνδεσης! Ελέγξτε το token και τη σύνδεσή σας στο δίκτυο και δοκιμάστε ξανά.", "Dismiss": "Απόρριψη", "Welcome to": "Καλωσήρθατε στο", "Please login using your Deezer account.": - "Συνδεθείτε χρησιμοποιώντας τον λογαριασμό σας στο Deezer.", + "Συνδεθείτε χρησιμοποιώντας τον λογαριασμό σας στο Deezer.", "Login using browser": "Σύνδεση χρησιμοποιώντας το πρόγραμμα περιήγησης", "Login using token": "Σύνδεση χρησιμοποιώντας token", "Enter ARL": "Εισαγωγή ARL", "Token (ARL)": "Token (ARL)", "Save": "Αποθήκευση", "If you don't have account, you can register on deezer.com for free.": - "Εάν δεν έχετε λογαριασμό, μπορείτε να εγγραφείτε δωρεάν στο deezer.com.", + "Εάν δεν έχετε λογαριασμό, μπορείτε να εγγραφείτε δωρεάν στο deezer.com.", "Open in browser": "Ανοιγμα σε πρόγραμμα περιήγησης", "By using this app, you don't agree with the Deezer ToS": - "Χρησιμοποιώντας αυτήν την εφαρμογή, δεν συμφωνείτε με τους κανονισμούς χρήσης Deezer", + "Χρησιμοποιώντας αυτήν την εφαρμογή, δεν συμφωνείτε με τους κανονισμούς χρήσης Deezer", "Play next": "Παίξε αμέσως μετά", "Add to queue": "Προσθήκη στην ουρά", "Add track to favorites": "Προσθήκη κομμάτι στα αγαπημένα", @@ -104,7 +106,8 @@ const language_el_gr = { "Remove album": "Κατάργηση album", "Album removed": "Το album καταργήθηκε", "Remove from favorites": "Κατάργηση από τα αγαπημένα", - "Artist removed from library": "Ο καλλιτέχνης καταργήθηκε από τη βιβλιοθήκη", + "Artist removed from library": + "Ο καλλιτέχνης καταργήθηκε από τη βιβλιοθήκη", "Add to favorites": "Προσθήκη στα αγαπημένα", "Remove from library": "Κατάργηση από τη βιβλιοθήκη", "Add playlist to library": "Προσθήκη λίστας αναπαραγωγής στη βιβλιοθήκη", @@ -126,7 +129,7 @@ const language_el_gr = { "Show all tracks": "Εμφάνιση όλων των κομματιών", "Show all playlists": "Εμφάνιση όλων των λιστών αναπαραγωγής", "Settings": "Ρυθμίσεις", - "General": "Γενικός", + "General": "Γενικά", "Appearance": "Εμφάνιση", "Quality": "Ποιότητα", "Deezer": "Deezer", @@ -139,24 +142,26 @@ const language_el_gr = { "Deezer (Dark)": "Deezer (Σκούρο)", "Primary color": "Πρωτεύον χρώμα", "Selected color": "Επιλεγμένο χρώμα", - "Use album art primary color": "Χρησιμοποιήστε το πρωτεύον χρώμα του εξώφυλλου του album", + "Use album art primary color": + "Χρησιμοποιήστε το πρωτεύον χρώμα του εξώφυλλου του album", "Warning: might be buggy": "Προειδοποίηση: μπορεί να μη λειτουργεί σωστά", "Mobile streaming": "Ροή μέσω δεδομένων κινητού δικτύου", "Wifi streaming": "Ροή μέσω WIFI", "External downloads": "Εξωτερικές λήψεις", "Content language": "Γλώσσα περιεχομένου", "Not app language, used in headers. Now": - "Όχι γλώσσα εφαρμογής, χρησιμοποιείται στις κεφαλίδες. Τρέχουσα", + "Όχι γλώσσα εφαρμογής, χρησιμοποιείται στις κεφαλίδες. Τρέχουσα", "Select language": "Επιλογή γλώσσας", "Content country": "Χώρα περιεχομένου", - "Country used in headers. Now": "Χώρα που χρησιμοποιείται στις κεφαλίδες. Τρέχουσα", + "Country used in headers. Now": + "Χώρα που χρησιμοποιείται στις κεφαλίδες. Τρέχουσα", "Log tracks": "Αρχεία καταγραφής", "Send track listen logs to Deezer, enable it for features like Flow to work properly": - "Αποστολή αρχείων καταγραφής ακρόασης στο Deezer, ενεργοποιήστε το για ορθή λειτουργία υπηρεσιών όπως το Flow", + "Αποστολή αρχείων καταγραφής ακρόασης στο Deezer, ενεργοποιήστε το για ορθή λειτουργία υπηρεσιών όπως το Flow", "Offline mode": "Λειτουργία εκτός σύνδεσης", "Will be overwritten on start.": "Θα αντικατασταθεί κατά την εκκίνηση.", "Error logging in, check your internet connections.": - "Σφάλμα σύνδεσης, ελέγξτε την σύνδεσή σας στο Δίκτυο.", + "Σφάλμα σύνδεσης, ελέγξτε την σύνδεσή σας στο Δίκτυο.", "Logging in...": "Σύνδεση...", "Download path": "Διαδρομή λήψεων", "Downloads naming": "Ονομασία λήψεων", @@ -170,11 +175,11 @@ const language_el_gr = { "Overwrite already downloaded files": "Αντικατάσταση ήδη ληφθέντων αρχείων", "Copy ARL": "Αντιγραφή ARL", "Copy userToken/ARL Cookie for use in other apps.": - "Αντιγραφή userToken/ARL Cookie για χρήση σε άλλες εφαρμογές.", + "Αντιγραφή userToken/ARL Cookie για χρήση σε άλλες εφαρμογές.", "Copied": "Αντιγράφηκε", "Log out": "Αποσύνδεση", "Due to plugin incompatibility, login using browser is unavailable without restart.": - "Λόγω ασυμβατότητας προσθηκών, η σύνδεση μέσω προγράμματος περιήγησης δεν είναι διαθέσιμη χωρίς επανεκκίνηση.", + "Λόγω ασυμβατότητας προσθηκών, η σύνδεση μέσω προγράμματος περιήγησης δεν είναι διαθέσιμη χωρίς επανεκκίνηση.", "(ARL ONLY) Continue": "(ARL ΜΟΝΟ) Συνέχεια", "Log out & Exit": "Αποσύνδεση & Έξοδος", "Pick-a-Path": "Διαλέξτε ένα μονοπάτι", @@ -183,11 +188,57 @@ const language_el_gr = { "Permission denied": "Η άδεια απορρίφθηκε", "Language": "Γλώσσα", "Language changed, please restart Freezer to apply!": - "Η γλώσσα άλλαξε, κάντε επανεκκίνηση του Freezer για εφαρμογή!", + "Η γλώσσα άλλαξε, κάντε επανεκκίνηση του Freezer για εφαρμογή!", "Importing...": "Εισαγωγή...", "Radio": "Ραδιόφωνο", "Flow": "Flow", - "Track is not available on Deezer!": "Το κομμάτι δεν είναι διαθέσιμο στο Deezer!", - "Failed to download track! Please restart.": "Αποτυχία λήψης κομματιού! Κάντε επανεκκίνηση. " + "Track is not available on Deezer!": + "Το κομμάτι δεν είναι διαθέσιμο στο Deezer!", + "Failed to download track! Please restart.": + "Αποτυχία λήψης κομματιού! Κάντε επανεκκίνηση. ", + + //0.5.0 Strings: + "Storage permission denied!": "Η άδεια χώρου αποθήκευσης απορρίφθηκε!", + "Failed": "Απέτυχαν", + "Queued": "Σε ουρά", + //Updated in 0.5.1 - used in context of download: + "External": "Χώρος αποθήκευσης", + //0.5.0 + "Restart failed downloads": "Επανεκκίνηση αποτυχημένων λήψεων", + "Clear failed": "Εκκαθάριση αποτυχημένων", + "Download Settings": "Ρυθμίσεις Λήψεων", + "Create folder for playlist": "Δημιουργία φακέλου για λίστα αναπαραγωγής", + "Download .LRC lyrics": "Λήψη στίχων .LRC", + "Proxy": "Μεσολαβητής", + "Not set": "Δεν ρυθμίστηκε", + "Search or paste URL": "Αναζήτηση ή επικόλληση διεύθυνσης URL", + "History": "Ιστορικό", + //Updated 0.5.1 + "Download threads": "Ταυτόχρονες λήψεις", + //0.5.0 + "Lyrics unavailable, empty or failed to load!": + "Οι στίχοι δεν είναι διαθέσιμοι, είναι άδειοι ή δεν φορτώθηκαν!", + "About": "Σχετικά", + "Telegram Channel": "Κανάλι Telegram ", + "To get latest releases": "Για να λάβετε τις τελευταίες κυκλοφορίες", + "Official chat": "Επίσημη συνομιλία", + "Telegram Group": "Ομάδα Telegram", + "Huge thanks to all the contributors! <3": + "Πολλά ευχαριστώ σε όλους τους συνεισφέροντες! <3", + "Edit playlist": "Edit playlist", + "Update": "Ενημέρωση", + "Playlist updated!": "Η λίστα αναπαραγωγής ενημερώθηκε!", + "Downloads added!": "Προστέθηκαν λήψεις!", + + //0.5.1 Strings: + "Save cover file for every track": "Αποθήκευση εξώφυλλου για κάθε κομμάτι", + "Download Log": "Αρχείο καταγραφής λήψεων", + "Repository": "Repository", + "Source code, report issues there.": + "Πηγαίος κώδικας, αναφέρετε ζητήματα εκεί.", + + //0.5.2 Strings: + "Use system theme": "Χρησιμοποίηση θέματος συστήματος", + "Light": "Φωτεινο" } }; diff --git a/lib/languages/es_es.dart b/lib/languages/es_es.dart index e604b11..a626fe0 100644 --- a/lib/languages/es_es.dart +++ b/lib/languages/es_es.dart @@ -215,7 +215,8 @@ const language_es_es = { "To get latest releases": "Para obtener los últimos lanzamientos", "Official chat": "Chat oficial", "Telegram Group": "Grupo de Telegram", - "Huge thanks to all the contributors! <3": "Muchas gracias a todos los contribuyentes! <3", + "Huge thanks to all the contributors! <3": + "Muchas gracias a todos los contribuyentes contributors! <3", "Edit playlist": "Editar lista de reproducción", "Update": "Actualizar", "Playlist updated!": "Lista de reproducción actualizada!", diff --git a/lib/languages/ru_ru.dart b/lib/languages/ru_ru.dart index 9db9384..2a2c193 100644 --- a/lib/languages/ru_ru.dart +++ b/lib/languages/ru_ru.dart @@ -224,6 +224,9 @@ const language_ru_ru = { "Save cover file for every track": "Обложки для каждого трека отдельным файлом", "Download Log": "Лог загрузок (технические данные)", "Repository": "Репозиторий", - "Source code, report issues there.": "Исходный код, вопросы, предложения." + "Source code, report issues there.": "Исходный код, вопросы, предложения.", + //0.5.2 Strings: + "Use system theme": "Использовать тему системы", + "Light": "Светлая" } }; \ No newline at end of file diff --git a/lib/languages/tr_tr.dart b/lib/languages/tr_tr.dart index 57c2cea..b687da8 100644 --- a/lib/languages/tr_tr.dart +++ b/lib/languages/tr_tr.dart @@ -44,7 +44,7 @@ const language_tr_tr = { "Please check your connection and try again later...": "Lütfen bağlantınızı kontrol edin ve daha sonra tekrar deneyin ...", "Show more": "Daha fazla göster", - "Importer": "Importer", + "Importer": "Aktar", "Currently supporting only Spotify, with 100 tracks limit": "Şu anda 100 parça sınırıyla yalnızca Spotify'ı destekliyor", "Due to API limitations": "API sınırlamaları nedeniyle", @@ -74,8 +74,8 @@ const language_tr_tr = { "Çevrimdışı modda oynatma listeleri oluşturulamaz", "Error": "Hata", "Error logging in! Please check your token and internet connection and try again.": - "Oturum açma hatası! Lütfen tokeninizi ve internet bağlantınızı kontrol edin ve tekrar deneyin.", - "Dismiss": "Reddet", + "Oturum açılamadı! Lütfen tokeninizi ve internet bağlantınızı kontrol edin ve tekrar deneyin.", + "Dismiss": "Kapat", "Welcome to": "Hoşgeldiniz", "Please login using your Deezer account.": "Lütfen Deezer hesabınızı kullanarak giriş yapın.", @@ -91,37 +91,37 @@ const language_tr_tr = { "Bu uygulamayı kullanarak Deezer Hizmet Şartları'nı kabul etmiyorsunuz", "Play next": "Sonrakini çal", "Add to queue": "Sıraya ekle", - "Add track to favorites": "Favorilere parça ekle", + "Add track to favorites": "Parçayı favorilere ekle", "Add to playlist": "Oynatma listesine ekle", "Select playlist": "Oynatma listesi seçin", "Track added to": "Parça şuraya eklendi", "Remove from playlist": "Oynatma listesinden kaldır", "Track removed from": "Parça şuradan kaldırıldı", - "Remove favorite": "Favoriyi kaldır", + "Remove favorite": "Favorilerden kaldır", "Track removed from library": "Parça kütüphaneden kaldırıldı", "Go to": "Git", "Make offline": "Çevrimdışı yap", "Add to library": "Kütüphaneye ekle", "Remove album": "Albümü kaldır", "Album removed": "Albüm kaldırıldı", - "Remove from favorites": "Favorilerden çıkar", + "Remove from favorites": "Favorilerden kaldır", "Artist removed from library": "Sanatçı kütüphaneden kaldırıldı", "Add to favorites": "Favorilere ekle", "Remove from library": "Kütüphaneden kaldır", "Add playlist to library": "Oynatma listesini kütüphaneye ekleyin", - "Added playlist to library": "Kütüphaneye oynatma listesi eklendi", + "Added playlist to library": "Oynatma listesi kütüphaneye eklendi", "Make playlist offline": "Oynatma listesini çevrimdışı yapın", "Download playlist": "Oynatma listesini indirin", "Create playlist": "Oynatma listesi oluştur", "Title": "Başlık", "Description": "Açıklama", "Private": "Özel", - "Collaborative": "İşbirlikçi", + "Collaborative": "Paylaşılan", "Create": "Oluştur", "Playlist created!": "Oynatma listesi oluşturuldu!", "Playing from:": "Şuradan oynatılıyor:", "Queue": "Kuyruk", - "Offline search": "Offline search", + "Offline search": "Çevrimdışı arama", "Search Results": "Arama Sonuçları", "No results!": "Sonuç yok!", "Show all tracks": "Tüm parçaları göster", @@ -134,24 +134,24 @@ const language_tr_tr = { "Theme": "Tema", "Currently": "Şu anda", "Select theme": "Tema seçin", - "Light (default)": "Light (Varsayılan)", - "Dark": "Dark", - "Black (AMOLED)": "Black (AMOLED)", + "Light (default)": "Açık (Varsayılan)", + "Dark": "Koyu", + "Black (AMOLED)": "Siyah (AMOLED)", "Deezer (Dark)": "Deezer (Dark)", "Primary color": "Ana renk", "Selected color": "Seçilen renk", - "Use album art primary color": "Albüm resmi ana rengini kullan", + "Use album art primary color": "Albüm resmini ana renk olarak kullan", "Warning: might be buggy": "Uyarı: hatalı olabilir", - "Mobile streaming": "Mobil akış", - "Wifi streaming": "Wifi akışı", + "Mobile streaming": "Mobil veri", + "Wifi streaming": "Wifi", "External downloads": "Harici indirmeler", "Content language": "İçerik dili", "Not app language, used in headers. Now": - "Not app language, used in headers. Now", + "Uygulama dili değil, başlıklarda kullanılacak. Şuan", "Select language": "Dil seçin", "Content country": "İçerik ülkesi", - "Country used in headers. Now": "Başlıklarda kullanılan ülke. Şimdi", - "Log tracks": "Log tracks", + "Country used in headers. Now": "Başlıklarda kullanılan ülke. Şuan", + "Log tracks": "Parça günlükleri", "Send track listen logs to Deezer, enable it for features like Flow to work properly": "Parça dinleme günlüklerini Deezer'a gönderin, Flow gibi özelliklerin düzgün çalışması için etkinleştirin", "Offline mode": "Çevrimdışı mod", @@ -159,25 +159,25 @@ const language_tr_tr = { "Error logging in, check your internet connections.": "Giriş hatası, internet bağlantılarınızı kontrol edin.", "Logging in...": "Giriş yapılıyor...", - "Download path": "İndirme yolu", - "Downloads naming": "İndirilenler adlandırma", + "Download path": "İndirme konumu", + "Downloads naming": "İndirilenleri adlandır", "Downloaded tracks filename": "İndirilen parçaların dosya adı", "Valid variables are": "Geçerli değişkenler", "Reset": "Sıfırla", "Clear": "Temizle", - "Create folders for artist": "Sanatçı için klasörler oluşturun", + "Create folders for artist": "Sanatçılar için klasörler oluşturun", "Create folders for albums": "Albümler için klasörler oluşturun", "Separate albums by discs": "Albümleri disklere göre ayırın", - "Overwrite already downloaded files": "Zaten indirilmiş dosyaların üzerine yaz", + "Overwrite already downloaded files": "İndirilmiş dosyaların üzerine yaz", "Copy ARL": "ARL kopyala", "Copy userToken/ARL Cookie for use in other apps.": "Diğer uygulamalarda kullanmak için userToken / ARL Cookie'yi kopyalayın.", "Copied": "Kopyalandı", "Log out": "Çıkış yap", "Due to plugin incompatibility, login using browser is unavailable without restart.": - "Eklenti uyumsuzluğu nedeniyle, yeniden başlatmadan tarayıcı kullanarak oturum açılamaz.", + "Eklenti uyumsuzluğu nedeniyle, yeniden başlatmadan tarayıcı kullanılarak oturum açılamaz.", "(ARL ONLY) Continue": "(SADECE ARL) Devam et", - "Log out & Exit": "Çıkış yap & Çık", + "Log out & Exit": "Çıkış yap & Kapat", "Pick-a-Path": "Konum seç", "Select storage": "Depolama seç", "Go up": "Yukarı git", @@ -196,7 +196,7 @@ const language_tr_tr = { "Failed": "Başarısız", "Queued": "Sıraya alındı", //Updated in 0.5.1 - used in context of download: - "External": "Storage", + "External": "Depolama", //0.5.0 "Restart failed downloads": "Başarısız indirmeleri yeniden başlatın", "Clear failed": "Silinemedi", @@ -213,7 +213,7 @@ const language_tr_tr = { "Lyrics unavailable, empty or failed to load!": "Sözler mevcut değil, boş veya yüklenemedi!", "About": "Hakkında", "Telegram Channel": "Telegram Kanalı", - "To get latest releases": "En son sürümleri almak için", + "To get latest releases": "En son sürümleri indirmek için", "Official chat": "Resmi sohbet", "Telegram Group": "Telegram Grubu", "Huge thanks to all the contributors! <3": "Katkıda bulunanlara çok teşekkürler! <3", @@ -225,7 +225,11 @@ const language_tr_tr = { //0.5.1 Strings: "Save cover file for every track": "Her parça için kapak dosyasını kaydedin", "Download Log": "İndirme Kayıtları", - "Repository": "Depo", - "Source code, report issues there.": "Kaynak kodu, sorunları bildirin" + "Repository": "Repo", + "Source code, report issues there.": "Kaynak kodu, sorunları bildirin", + + //0.5.2 Strings: + "Use system theme": "Sistem temasını kullan", + "Light": "Açık" } }; \ No newline at end of file diff --git a/lib/ui/details_screens.dart b/lib/ui/details_screens.dart index 55fc86d..af9a5ec 100644 --- a/lib/ui/details_screens.dart +++ b/lib/ui/details_screens.dart @@ -664,7 +664,7 @@ class _PlaylistDetailsState extends State { tracks.sort((a, b) => a.title.compareTo(b.title)); return tracks; case SortType.ARTIST: - tracks.sort((a, b) => a.artists[0].name.compareTo(b.artists[0].name)); + tracks.sort((a, b) => a.artists[0].name.toLowerCase().compareTo(b.artists[0].name.toLowerCase())); return tracks; case SortType.REVERSE: return tracks.reversed.toList(); diff --git a/lib/ui/library.dart b/lib/ui/library.dart index 2d2327e..8c6fd71 100644 --- a/lib/ui/library.dart +++ b/lib/ui/library.dart @@ -419,6 +419,13 @@ class _LibraryTracksState extends State { } +enum AlbumSortType { + DEFAULT, + REVERSE, + ALPHABETIC, + ARTIST +} + class LibraryAlbums extends StatefulWidget { @override _LibraryAlbumsState createState() => _LibraryAlbumsState(); @@ -427,6 +434,25 @@ class LibraryAlbums extends StatefulWidget { class _LibraryAlbumsState extends State { List _albums; + AlbumSortType _sort = AlbumSortType.DEFAULT; + + List get _sorted { + List albums = List.from(_albums); + switch (_sort) { + case AlbumSortType.DEFAULT: + return _albums; + case AlbumSortType.REVERSE: + return _albums.reversed.toList(); + case AlbumSortType.ALPHABETIC: + albums.sort((a, b) => a.title.toLowerCase().compareTo(b.title.toLowerCase())); + return albums; + case AlbumSortType.ARTIST: + albums.sort((a, b) => a.artists[0].name.toLowerCase().compareTo(b.artists[0].name.toLowerCase())); + return albums; + } + return albums; + } + Future _load() async { if (settings.offlineMode) return; @@ -439,13 +465,44 @@ class _LibraryAlbumsState extends State { @override void initState() { _load(); + _sort = cache.albumSort??AlbumSortType.DEFAULT; super.initState(); } @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: Text('Albums'.i18n),), + appBar: AppBar( + title: Text('Albums'.i18n), + actions: [ + PopupMenuButton( + child: Icon(Icons.sort, size: 32.0), + onSelected: (AlbumSortType s) async { + setState(() => _sort = s); + cache.albumSort = s; + await cache.save(); + }, + itemBuilder: (context) => >[ + PopupMenuItem( + value: AlbumSortType.DEFAULT, + child: Text('Default'.i18n), + ), + PopupMenuItem( + value: AlbumSortType.REVERSE, + child: Text('Reverse'.i18n), + ), + PopupMenuItem( + value: AlbumSortType.ALPHABETIC, + child: Text('Alphabetic'.i18n), + ), + PopupMenuItem( + value: AlbumSortType.ARTIST, + child: Text('Artist'.i18n), + ), + ], + ), + ], + ), body: ListView( children: [ Container(height: 8.0,), @@ -459,7 +516,7 @@ class _LibraryAlbumsState extends State { if (_albums != null) ...List.generate(_albums.length, (int i) { - Album a = _albums[i]; + Album a = _sorted[i]; return AlbumTile( a, onTap: () { @@ -523,50 +580,148 @@ class _LibraryAlbumsState extends State { } } +enum ArtistSortType { + DEFAULT, + REVERSE, + POPULARITY, + ALPHABETIC +} + class LibraryArtists extends StatefulWidget { @override _LibraryArtistsState createState() => _LibraryArtistsState(); } class _LibraryArtistsState extends State { + + List _artists; + ArtistSortType _sort = ArtistSortType.DEFAULT; + bool _loading = true; + bool _error = false; + + List get _sorted { + List artists = List.from(_artists); + switch (_sort) { + case ArtistSortType.DEFAULT: + return _artists; + case ArtistSortType.REVERSE: + return _artists.reversed.toList(); + case ArtistSortType.POPULARITY: + artists.sort((a, b) => b.fans - a.fans); + return artists; + case ArtistSortType.ALPHABETIC: + artists.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase())); + return artists; + } + } + + //Load data + Future _load() async { + setState(() => _loading = true); + //Fetch + List data; + try { + data = await deezerAPI.getArtists(); + } catch (e) {} + //Update UI + setState(() { + if (data != null) { + _artists = data; + } else { + _error = true; + } + _loading = false; + }); + } + + @override + void initState() { + _sort = cache.artistSort; + _load(); + super.initState(); + } + @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: Text('Artists'.i18n),), - body: FutureBuilder( - future: deezerAPI.getArtists(), - builder: (BuildContext context, AsyncSnapshot snapshot) { - - if (snapshot.hasError) return ErrorScreen(); - if (!snapshot.hasData) return Center(child: CircularProgressIndicator(),); - - return ListView( - children: [ - ...List.generate(snapshot.data.length, (i) { - Artist a = snapshot.data[i]; - return ArtistHorizontalTile( - a, - onTap: () { - Navigator.of(context).push( - MaterialPageRoute(builder: (context) => ArtistDetails(a)) - ); - }, - onHold: () { - MenuSheet m = MenuSheet(context); - m.defaultArtistMenu(a, onRemove: () { - setState(() => {}); - }); - }, - ); - }), + appBar: AppBar( + title: Text('Artists'.i18n), + actions: [ + PopupMenuButton( + child: Icon(Icons.sort, size: 32.0), + onSelected: (ArtistSortType s) async { + setState(() => _sort = s); + cache.artistSort = s; + await cache.save(); + }, + itemBuilder: (context) => >[ + PopupMenuItem( + value: ArtistSortType.DEFAULT, + child: Text('Default'.i18n), + ), + PopupMenuItem( + value: ArtistSortType.REVERSE, + child: Text('Reverse'.i18n), + ), + PopupMenuItem( + value: ArtistSortType.ALPHABETIC, + child: Text('Alphabetic'.i18n), + ), + PopupMenuItem( + value: ArtistSortType.POPULARITY, + child: Text('Popularity'.i18n), + ), ], - ); - }, + ) + ], + ), + body: ListView( + children: [ + if (_loading) + Padding( + padding: EdgeInsets.all(8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [CircularProgressIndicator()], + ), + ), + + if (_error) + Center(child: ErrorScreen()), + + if (!_loading && !_error) + ...List.generate(_artists.length, (i) { + Artist a = _sorted[i]; + return ArtistHorizontalTile( + a, + onTap: () { + Navigator.of(context).push( + MaterialPageRoute(builder: (context) => ArtistDetails(a)) + ); + }, + onHold: () { + MenuSheet m = MenuSheet(context); + m.defaultArtistMenu(a, onRemove: () { + setState(() { + _artists.remove(a); + }); + }); + }, + ); + }), + ], ), ); } } +enum PlaylistSortType { + DEFAULT, + REVERSE, + ALPHABETIC, + USER, + TRACK_COUNT +} class LibraryPlaylists extends StatefulWidget { @override @@ -576,6 +731,26 @@ class LibraryPlaylists extends StatefulWidget { class _LibraryPlaylistsState extends State { List _playlists; + PlaylistSortType _sort = PlaylistSortType.DEFAULT; + + List get _sorted { + List playlists = List.from(_playlists); + switch (_sort) { + case PlaylistSortType.DEFAULT: + return _playlists; + case PlaylistSortType.REVERSE: + return _playlists.reversed.toList(); + case PlaylistSortType.USER: + playlists.sort((a, b) => (a.user.name??deezerAPI.userName).toLowerCase().compareTo((b.user.name??deezerAPI.userName).toLowerCase())); + return playlists; + case PlaylistSortType.TRACK_COUNT: + playlists.sort((a, b) => b.trackCount - a.trackCount); + return playlists; + case PlaylistSortType.ALPHABETIC: + playlists.sort((a, b) => a.title.toLowerCase().compareTo(b.title.toLowerCase())); + return playlists; + } + } Future _load() async { if (!settings.offlineMode) { @@ -588,6 +763,7 @@ class _LibraryPlaylistsState extends State { @override void initState() { + _sort = cache.libraryPlaylistSort; _load(); super.initState(); } @@ -606,7 +782,41 @@ class _LibraryPlaylistsState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: Text('Playlists'.i18n),), + appBar: AppBar( + title: Text('Playlists'.i18n), + actions: [ + PopupMenuButton( + child: Icon(Icons.sort, size: 32.0), + onSelected: (PlaylistSortType s) async { + setState(() => _sort = s); + cache.libraryPlaylistSort = s; + await cache.save(); + }, + itemBuilder: (context) => >[ + PopupMenuItem( + value: PlaylistSortType.DEFAULT, + child: Text('Default'.i18n), + ), + PopupMenuItem( + value: PlaylistSortType.REVERSE, + child: Text('Reverse'.i18n), + ), + PopupMenuItem( + value: PlaylistSortType.USER, + child: Text('User'.i18n), + ), + PopupMenuItem( + value: PlaylistSortType.TRACK_COUNT, + child: Text('Track count'.i18n), + ), + PopupMenuItem( + value: PlaylistSortType.ALPHABETIC, + child: Text('Alphabetic'.i18n), + ), + ], + ) + ], + ), body: ListView( children: [ ListTile( @@ -652,7 +862,7 @@ class _LibraryPlaylistsState extends State { if (_playlists != null) ...List.generate(_playlists.length, (int i) { - Playlist p = _playlists[i]; + Playlist p = _sorted[i]; return PlaylistTile( p, onTap: () => Navigator.of(context).push(MaterialPageRoute( diff --git a/lib/ui/login_screen.dart b/lib/ui/login_screen.dart index 8b5fcaa..07e1212 100644 --- a/lib/ui/login_screen.dart +++ b/lib/ui/login_screen.dart @@ -90,15 +90,19 @@ class _LoginWidgetState extends State { //Try logging in try { deezerAPI.arl = settings.arl; - bool resp = await deezerAPI.rawAuthorize(onError: (e) => _error = e.toString()); + bool resp = await deezerAPI.rawAuthorize(onError: (e) => setState(() => _error = e.toString())); if (resp == false) { //false, not null + if (settings.arl.length != 192) { + if (_error == null) _error = ''; + _error += 'Invalid ARL length!'; + } setState(() => settings.arl = null); errorDialog(); } //On error show dialog and reset to null } catch (e) { - _error = e; - print('Login error: ' + e); + _error = e.toString(); + print('Login error: ' + e.toString()); setState(() => settings.arl = null); errorDialog(); } diff --git a/lib/ui/settings_screen.dart b/lib/ui/settings_screen.dart index 751fe85..f041541 100644 --- a/lib/ui/settings_screen.dart +++ b/lib/ui/settings_screen.dart @@ -498,7 +498,7 @@ class _DeezerSettingsState extends State { ListTile( title: Text('Proxy'.i18n), leading: Icon(Icons.vpn_key), - subtitle: Text(settings.proxyAddress??'Not set'), + subtitle: Text(settings.proxyAddress??'Not set'.i18n), onTap: () { String _new; showDialog( @@ -606,7 +606,8 @@ class _DownloadsSettingsState extends State { ), Container(height: 8.0), Text( - 'Valid variables are'.i18n + ': %artists%, %artist%, %title%, %album%, %trackNumber%, %0trackNumber%, %feats%, %playlistTrackNumber%, %0playlistTrackNumber%, %year%, %date%', + 'Valid variables are'.i18n + ': %artists%, %artist%, %title%, %album%, %trackNumber%, %0trackNumber%, %feats%, %playlistTrackNumber%, %0playlistTrackNumber%, %year%, %date%\n\n' + + "If you want to use custom directory naming - use '/' as directory separator.".i18n, style: TextStyle( fontSize: 12.0, ), @@ -658,8 +659,8 @@ class _DownloadsSettingsState extends State { ), Slider( min: 1, - max: 6, - divisions: 5, + max: 16, + divisions: 15, value: _downloadThreads, label: _downloadThreads.round().toString(), onChanged: (double v) => setState(() => _downloadThreads = v), @@ -1040,15 +1041,6 @@ class _CreditsScreenState extends State { String _version = ''; - //Title, Subtitle, URL - static final List> credits = [ - ['exttex', 'Developer'], - ['Bas Curtiz', 'Icon, logo, banner, design suggestions, tester'], - ['Deemix', 'Better app <3', 'https://codeberg.org/RemixDev/deemix'], - ['Tobs, Xandar Null, Francesco', 'Beta testers'], - ['Annexhack', 'Android Auto help'] - ]; - static final List> translators = [ ['Xandar Null', 'Arabic'], ['Markus', 'German'], @@ -1111,22 +1103,51 @@ class _CreditsScreenState extends State { ), ListTile( title: Text('Repository'.i18n), - subtitle: Text('Source code, report issues there.'), + subtitle: Text('Source code, report issues there.'.i18n), leading: Icon(Icons.code, color: Colors.green, size: 36.0), onTap: () { launch('https://notabug.org/exttex/freezer'); }, ), Divider(), - ...List.generate(credits.length, (i) => ListTile( - title: Text(credits[i][0]), - subtitle: Text(credits[i][1]), + ListTile( + title: Text('exttex'), + subtitle: Text('Developer'), + ), + ListTile( + title: Text('Bas Curtiz'), + subtitle: Text('Icon, logo, banner, design suggestions, tester'), + ), + ListTile( + title: Text('Tobs'), + subtitle: Text('Alpha testers'), + ), + ListTile( + title: Text('Deemix'), + subtitle: Text('Better app <3'), onTap: () { - if (credits[i].length >= 3) { - launch(credits[i][2]); - } + launch('https://codeberg.org/RemixDev/deemix'); }, - )), + ), + ListTile( + title: Text('Xandar Null'), + subtitle: Text('Tester, translations help'), + ), + ListTile( + title: Text('Francesco'), + subtitle: Text('Tester'), + onTap: () { + setState(() { + settings.primaryColor = Color(0xff333333); + }); + updateTheme(); + settings.save(); + }, + ), + ListTile( + title: Text('Annexhack'), + subtitle: Text('Android Auto help'), + ), Divider(), ...List.generate(translators.length, (i) => ListTile( title: Text(translators[i][0]), diff --git a/lib/ui/tiles.dart b/lib/ui/tiles.dart index 7f846cb..a5aeb6e 100644 --- a/lib/ui/tiles.dart +++ b/lib/ui/tiles.dart @@ -205,18 +205,21 @@ class ArtistHorizontalTile extends StatelessWidget { @override Widget build(BuildContext context) { - return ListTile( - title: Text( - artist.name, - maxLines: 1, + return Padding( + padding: EdgeInsets.symmetric(vertical: 2.0), + child: ListTile( + title: Text( + artist.name, + maxLines: 1, + ), + leading: CachedImage( + url: artist.picture.thumb, + circular: true, + ), + onTap: onTap, + onLongPress: onHold, + trailing: trailing, ), - leading: CachedImage( - url: artist.picture.thumb, - circular: true, - ), - onTap: onTap, - onLongPress: onHold, - trailing: trailing, ); } } diff --git a/pubspec.yaml b/pubspec.yaml index 25b2434..63d9e31 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,7 +15,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 0.5.2+1 +version: 0.5.3+1 environment: sdk: ">=2.8.0 <3.0.0"