0.5.3 - Download fixes, shuffle fix, sorting in library

This commit is contained in:
exttex 2020-10-12 22:49:13 +02:00
parent 952cf0f508
commit 2f471268c6
18 changed files with 556 additions and 167 deletions

View File

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

View File

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

View File

@ -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 {
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 {
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,19 +531,19 @@ public class DownloadService extends Service {
JSONObject lyricsData = null;
//Lyrics
if (settings.downloadLyrics) {
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);
}
}
//Tag

View File

@ -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<String, SortType> playlistSort;
//Sort
@JsonKey(defaultValue: AlbumSortType.DEFAULT)
AlbumSortType albumSort;
@JsonKey(defaultValue: ArtistSortType.DEFAULT)
ArtistSortType artistSort;
@JsonKey(defaultValue: PlaylistSortType.DEFAULT)
PlaylistSortType libraryPlaylistSort;
Cache({this.libraryTracks});

View File

@ -19,7 +19,16 @@ Cache _$CacheFromJson(Map<String, dynamic> json) {
..playlistSort = (json['playlistSort'] as Map<String, dynamic>)?.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<String, dynamic> _$CacheToJson(Cache instance) => <String, dynamic>{
@ -27,6 +36,10 @@ Map<String, dynamic> _$CacheToJson(Cache instance) => <String, dynamic>{
'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<T>(
@ -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',
};

View File

@ -246,6 +246,7 @@ class AudioPlayerTask extends BackgroundAudioTask {
//Queue
List<MediaItem> _queue = <MediaItem>[];
List<int> _shuffleHistory = [];
int _queueIndex = 0;
ConcatenatingAudioSource _audioSource;
@ -363,13 +364,15 @@ class AudioPlayerTask extends BackgroundAudioTask {
Future<void> 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<void> 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);

View File

@ -208,7 +208,5 @@ const language_ar_ar = {
"User": "المستخدم",
"Track count": "عدد الاغاني",
"If you want to use custom directory naming - use '/' as directory separator.": "إذا كنت تريد استخدام تسمية مخصصة، استخدم '/' كفاصل بين المسار."
}
};

View File

@ -47,7 +47,8 @@ const language_el_gr = {
"Currently supporting only Spotify, with 100 tracks limit":
"Αυτήν τη στιγμή υποστηρίζεται μόνο το Spotify, με όριο 100 κομματιών",
"Due to API limitations": "Λόγω περιορισμών API",
"Enter your playlist link below": "Εισαγάγετε τον σύνδεσμο λίστας αναπαραγωγής παρακάτω",
"Enter your playlist link below":
"Εισαγάγετε τον σύνδεσμο λίστας αναπαραγωγής παρακάτω",
"Error loading URL!": "Σφάλμα φόρτωσης διεύθυνσης URL!",
"Convert": "Μετατροπή",
"Download only": "Μόνο λήψη",
@ -58,8 +59,9 @@ const language_el_gr = {
"Artists": "Καλλιτέχνες",
"Playlists": "Λίστες αναπαραγωγής",
"Import": "Εισαγωγή",
"Import playlists from Spotify": "Εισαγωγή λιστών αναπαραγωγής από το Spotify",
"Statistics": "Στατιστική",
"Import playlists from Spotify":
"Εισαγωγή λιστών αναπαραγωγής από το Spotify",
"Statistics": "Στατιστικά",
"Offline tracks": "Κομμάτια εκτός σύνδεσης",
"Offline albums": "Album εκτός σύνδεσης",
"Offline playlists": "Λίστες αναπαραγωγής εκτός σύνδεσης",
@ -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,7 +142,8 @@ 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",
@ -149,7 +153,8 @@ const language_el_gr = {
"Όχι γλώσσα εφαρμογής, χρησιμοποιείται στις κεφαλίδες. Τρέχουσα",
"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",
@ -187,7 +192,53 @@ const language_el_gr = {
"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": "Φωτεινο"
}
};

View File

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

View File

@ -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": "Светлая"
}
};

View File

@ -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": "ı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)": "ı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": "ık"
}
};

View File

@ -664,7 +664,7 @@ class _PlaylistDetailsState extends State<PlaylistDetails> {
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();

View File

@ -419,6 +419,13 @@ class _LibraryTracksState extends State<LibraryTracks> {
}
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<LibraryAlbums> {
List<Album> _albums;
AlbumSortType _sort = AlbumSortType.DEFAULT;
List<Album> get _sorted {
List<Album> 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<LibraryAlbums> {
@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) => <PopupMenuEntry<AlbumSortType>>[
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: <Widget>[
Container(height: 8.0,),
@ -459,7 +516,7 @@ class _LibraryAlbumsState extends State<LibraryAlbums> {
if (_albums != null)
...List.generate(_albums.length, (int i) {
Album a = _albums[i];
Album a = _sorted[i];
return AlbumTile(
a,
onTap: () {
@ -523,27 +580,118 @@ class _LibraryAlbumsState extends State<LibraryAlbums> {
}
}
enum ArtistSortType {
DEFAULT,
REVERSE,
POPULARITY,
ALPHABETIC
}
class LibraryArtists extends StatefulWidget {
@override
_LibraryArtistsState createState() => _LibraryArtistsState();
}
class _LibraryArtistsState extends State<LibraryArtists> {
List<Artist> _artists;
ArtistSortType _sort = ArtistSortType.DEFAULT;
bool _loading = true;
bool _error = false;
List<Artist> get _sorted {
List<Artist> 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<Artist> 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(
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) => <PopupMenuEntry<ArtistSortType>>[
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: <Widget>[
...List.generate(snapshot.data.length, (i) {
Artist a = snapshot.data[i];
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: () {
@ -554,19 +702,26 @@ class _LibraryArtistsState extends State<LibraryArtists> {
onHold: () {
MenuSheet m = MenuSheet(context);
m.defaultArtistMenu(a, onRemove: () {
setState(() => {});
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<LibraryPlaylists> {
List<Playlist> _playlists;
PlaylistSortType _sort = PlaylistSortType.DEFAULT;
List<Playlist> get _sorted {
List<Playlist> 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<LibraryPlaylists> {
@override
void initState() {
_sort = cache.libraryPlaylistSort;
_load();
super.initState();
}
@ -606,7 +782,41 @@ class _LibraryPlaylistsState extends State<LibraryPlaylists> {
@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) => <PopupMenuEntry<PlaylistSortType>>[
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: <Widget>[
ListTile(
@ -652,7 +862,7 @@ class _LibraryPlaylistsState extends State<LibraryPlaylists> {
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(

View File

@ -90,15 +90,19 @@ class _LoginWidgetState extends State<LoginWidget> {
//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();
}

View File

@ -498,7 +498,7 @@ class _DeezerSettingsState extends State<DeezerSettings> {
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<DownloadsSettings> {
),
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<DownloadsSettings> {
),
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<CreditsScreen> {
String _version = '';
//Title, Subtitle, URL
static final List<List<String>> 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<List<String>> translators = [
['Xandar Null', 'Arabic'],
['Markus', 'German'],
@ -1111,22 +1103,51 @@ class _CreditsScreenState extends State<CreditsScreen> {
),
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]),

View File

@ -205,7 +205,9 @@ class ArtistHorizontalTile extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListTile(
return Padding(
padding: EdgeInsets.symmetric(vertical: 2.0),
child: ListTile(
title: Text(
artist.name,
maxLines: 1,
@ -217,6 +219,7 @@ class ArtistHorizontalTile extends StatelessWidget {
onTap: onTap,
onLongPress: onHold,
trailing: trailing,
),
);
}
}

View File

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