0.5.4 - Download fixes, C decryptor, gestures in full and small player, sharing, link support

This commit is contained in:
exttex 2020-10-14 21:09:16 +02:00
parent af49aeb974
commit 480800857e
170 changed files with 32703 additions and 293 deletions

View file

@ -38,7 +38,12 @@ class Cache {
ArtistSortType artistSort;
@JsonKey(defaultValue: PlaylistSortType.DEFAULT)
PlaylistSortType libraryPlaylistSort;
@JsonKey(defaultValue: SortType.DEFAULT)
SortType trackSort;
//If download threads warning was shown
@JsonKey(defaultValue: false)
bool threadsWarning;
Cache({this.libraryTracks});

View file

@ -28,7 +28,10 @@ Cache _$CacheFromJson(Map<String, dynamic> json) {
ArtistSortType.DEFAULT
..libraryPlaylistSort = _$enumDecodeNullable(
_$PlaylistSortTypeEnumMap, json['libraryPlaylistSort']) ??
PlaylistSortType.DEFAULT;
PlaylistSortType.DEFAULT
..trackSort = _$enumDecodeNullable(_$SortTypeEnumMap, json['trackSort']) ??
SortType.DEFAULT
..threadsWarning = json['threadsWarning'] as bool ?? false;
}
Map<String, dynamic> _$CacheToJson(Cache instance) => <String, dynamic>{
@ -40,6 +43,8 @@ Map<String, dynamic> _$CacheToJson(Cache instance) => <String, dynamic>{
'artistSort': _$ArtistSortTypeEnumMap[instance.artistSort],
'libraryPlaylistSort':
_$PlaylistSortTypeEnumMap[instance.libraryPlaylistSort],
'trackSort': _$SortTypeEnumMap[instance.trackSort],
'threadsWarning': instance.threadsWarning,
};
T _$enumDecode<T>(

View file

@ -513,7 +513,9 @@ class AudioPlayerTask extends BackgroundAudioTask {
List<AudioSource> sources = [];
for(int i=0; i<_queue.length; i++) {
sources.add(await _mediaItemToAudioSource(_queue[i]));
AudioSource s = await _mediaItemToAudioSource(_queue[i]);
if (s != null)
sources.add(s);
}
_audioSource = ConcatenatingAudioSource(children: sources);
@ -530,6 +532,7 @@ class AudioPlayerTask extends BackgroundAudioTask {
Future<AudioSource> _mediaItemToAudioSource(MediaItem mi) async {
String url = await _getTrackUrl(mi);
if (url == null) return null;
if (url.startsWith('http')) return ProgressiveAudioSource(Uri.parse(url));
return AudioSource.uri(Uri.parse(url));
}
@ -550,6 +553,7 @@ class AudioPlayerTask extends BackgroundAudioTask {
quality = mobileQuality;
if (conn == ConnectivityResult.wifi) quality = wifiQuality;
if ((playbackDetails??[]).length < 2) return null;
String url = 'https://dzcdn.net/?md5=${playbackDetails[0]}&mv=${playbackDetails[1]}&q=${quality.toString()}#${mediaItem.id}';
return url;
}
@ -663,7 +667,9 @@ class AudioPlayerTask extends BackgroundAudioTask {
_queue.insert(index, mi);
await AudioServiceBackground.setQueue(_queue);
await _audioSource.insert(index, await _mediaItemToAudioSource(mi));
AudioSource _newSource = await _mediaItemToAudioSource(mi);
if (_newSource != null)
await _audioSource.insert(index,_newSource);
_saveQueue();
}
@ -673,7 +679,9 @@ class AudioPlayerTask extends BackgroundAudioTask {
Future onAddQueueItem(MediaItem mi) async {
_queue.add(mi);
await AudioServiceBackground.setQueue(_queue);
await _audioSource.add(await _mediaItemToAudioSource(mi));
AudioSource _newSource = await _mediaItemToAudioSource(mi);
if (_newSource != null)
await _audioSource.add(_newSource);
_saveQueue();
}

View file

@ -226,8 +226,8 @@ const language_de_de = {
//0.5.2 Strings:
"Use system theme": "Systemvorgabe benutzen",
"Light": "Heller Modus",
//0.5.3 Strings:
//0.5.3 Strings:
"Popularity": "Beliebtheit",
"User": "Benutzer",
"Track count": "Anzahl der Titel",

View file

@ -228,6 +228,12 @@ const language_en_us = {
"Popularity": "Popularity",
"User": "User",
"Track count": "Track count",
"If you want to use custom directory naming - use '/' as directory separator.": "If you want to use custom directory naming - use '/' as directory separator."
"If you want to use custom directory naming - use '/' as directory separator.": "If you want to use custom directory naming - use '/' as directory separator.",
//0.5.4 Strings:
"Share": "Share",
"Save album cover": "Save album cover",
"Warning": "Warning",
"Using too many concurrent downloads on older/weaker devices might cause crashes!": "Using too many concurrent downloads on older/weaker devices might cause crashes!"
}
};

View file

@ -194,7 +194,7 @@ const language_fr_fr = {
"Track is not available on Deezer!":
"La piste n'est pas disponible sur Deezer!",
"Failed to download track! Please restart.":
"Echec du téléchargement de la piste ! Veuillez réessayer."
"Echec du téléchargement de la piste ! Veuillez réessayer.",
//0.5.0 Strings:
"Storage permission denied!": "Autorisation d'accès au stockage refusée!",
@ -215,23 +215,27 @@ const language_fr_fr = {
//Updated 0.5.1
"Download threads": "Téléchargements simultanés",
//0.5.0
"Lyrics unavailable, empty or failed to load!": "Paroles indisponibles, vides ou erreur de chargement !",
"Lyrics unavailable, empty or failed to load!":
"Paroles indisponibles, vides ou erreur de chargement !",
"About": "A propos",
"Telegram Channel": "Telegram Channel",
"To get latest releases": "Pour obtenir les dernières versions de l'app",
"Official chat": "Chat officiel",
"Telegram Group": "Groupe Telegram",
"Huge thanks to all the contributors! <3": "Un grand merci à tous les contributeurs ! <3",
"Huge thanks to all the contributors! <3":
"Un grand merci à tous les contributeurs ! <3",
"Edit playlist": "Modifier la playlist",
"Update": "Mettre à jour",
"Playlist updated!": "Playlist mise à jour !",
"Downloads added!": "Téléchargements ajoutés !",
//0.5.1 Strings:
"Save cover file for every track": "Sauvegarder la pochette pour chaque piste",
"Save cover file for every track":
"Sauvegarder la pochette pour chaque piste",
"Download Log": "Journal des téléchargements",
"Repository": "Dépôt",
"Source code, report issues there.": "Code source, signaler les problèmes ici.",
"Source code, report issues there.":
"Code source, signaler les problèmes ici.",
//0.5.2 Strings:
"Use system theme": "Utiliser le thème du système",
@ -241,6 +245,7 @@ const language_fr_fr = {
"Popularity": "Popularité",
"User": "Utilisateur",
"Track count": "Nombre de pistes",
"If you want to use custom directory naming - use '/' as directory separator.": "Si vous souhaitez utiliser un nom de répertoire personnalisé, utilisez '/' comme séparateur."
"If you want to use custom directory naming - use '/' as directory separator.":
"Si vous souhaitez utiliser un nom de répertoire personnalisé, utilisez '/' comme séparateur."
}
};

239
lib/languages/id_id.dart Normal file
View file

@ -0,0 +1,239 @@
/*
Translated by: LenteraMalam
*/
const language_id_id = {
"id_id": {
"Home": "Beranda",
"Search": "Cari",
"Library": "Perpustakaan",
"Offline mode, can't play flow or smart track lists.":
"Mode offline, tidak dapat memutar aliran atau daftar putar pintar.",
"Added to library": "Ditambahkan ke Perpustakaan",
"Download": "Unduh",
"Disk": "Disk",
"Offline": "Offline",
"Top Tracks": "Lagu Populer",
"Show more tracks": "Tampilkan lebih banyak lagu",
"Top": "Populer",
"Top Albums": "Album Populer",
"Show all albums": "Tampilkan semua album",
"Discography": "Diskografi",
"Default": "Default",
"Reverse": "Membalik",
"Alphabetic": "Alfabet",
"Artist": "Artis",
"Post processing...": "Sedang diproses...",
"Done": "Selesai",
"Delete": "Hapus",
"Are you sure you want to delete this download?":
"Apakah kamu yakin ingin menghapus unduhan ini?",
"Cancel": "Batalkan",
"Downloads": "Unduhan",
"Clear queue": "Bersihkan antrean",
"This won't delete currently downloading item":
"Ini tidak akan menghapus item yang sedang diunduh",
"Are you sure you want to delete all queued downloads?":
"Apakah kamu yakin ingin menghapus semua antrean yang terunduh?",
"Clear downloads history": "Bersihkan riwayat unduhan",
"WARNING: This will only clear non-offline (external downloads)":
"PERINGATAN: Ini hanya akan menghapus non-offline (unduhan eksternal)",
"Please check your connection and try again later...":
"Periksa kembali koneksi internet anda dan ulangi kembali...",
"Show more": "Tampilkan lebih banyak",
"Importer": "Pengimport",
"Currently supporting only Spotify, with 100 tracks limit":
"Saat ini hanya mendukung Spotify, dengan batas 100 lagu",
"Due to API limitations": "Karena keterbatasan API",
"Enter your playlist link below": "Masukkan link playlist Anda di bawah ini",
"Error loading URL!": "Gagal memuat URL!",
"Convert": "Konversikan",
"Download only": "Hanya mengunduh",
"Downloading is currently stopped, click here to resume.":
"Pengunduhan saat ini dihentikan, klik di sini untuk melanjutkan.",
"Tracks": "Lagu",
"Albums": "Album",
"Artists": "Artis",
"Playlists": "Daftar Putar",
"Import": "Impo",
"Import playlists from Spotify": "Impor playlist dari Spotify",
"Statistics": "Statistik",
"Offline tracks": "Lagu offline",
"Offline albums": "Album offline",
"Offline playlists": "Daftar putar offline",
"Offline size": "Ukuran offline",
"Free space": "Penyimpanan tersedia",
"Loved tracks": "Lagu yang disukai",
"Favorites": "Favorit",
"All offline tracks": "Semua lagu offline",
"Create new playlist": "Buat daftar putar baru",
"Cannot create playlists in offline mode":
"Tidak dapat membuat daftar putar di mode offline",
"Error": "Terjadi kesalahan",
"Error logging in! Please check your token and internet connection and try again.":
"Kesalahan saat masuk! Periksa token dan koneksi internet Anda, lalu coba lagi.",
"Dismiss": "Abaikan",
"Welcome to": "Selamat datang di",
"Please login using your Deezer account.":
"Silakan masuk menggunakan akun Deezer Anda.",
"Login using browser": "Masuk menggunakan browser",
"Login using token": "Masuk menggunakan token",
"Enter ARL": "Masukkan ARL",
"Token (ARL)": "Token (ARL)",
"Save": "Simpan",
"If you don't have account, you can register on deezer.com for free.":
"Jika Anda tidak memiliki akun, Anda dapat mendaftar di deezer.com secara gratis.",
"Open in browser": "Buka di browser",
"By using this app, you don't agree with the Deezer ToS":
"Dengan menggunakan aplikasi ini, Anda tidak setuju dengan ToS Deezer",
"Play next": "Putar selanjutnya",
"Add to queue": "Tambahkan ke antrean",
"Add track to favorites": "Tambahkan lagu ke favorit",
"Add to playlist": "Tambahkan ke daftar putar",
"Select playlist": "Pilih daftar putar",
"Track added to": "Lagu ditamhahkan ke",
"Remove from playlist": "Hapus dari daftar putar",
"Track removed from": "Lagu dihapus dari",
"Remove favorite": "Hapus favorit",
"Track removed from library": "Lagu dihapus dari perpustakaan",
"Go to": "Pergi ke",
"Make offline": "Buat offline",
"Add to library": "Tambahkan ke perpustakaan",
"Remove album": "Hapus album",
"Album removed": "Album dihapus",
"Remove from favorites": "Hapus dari favorit",
"Artist removed from library": "Artis dihapus dari perpustakaan",
"Add to favorites": "Tambahkan ke favorit",
"Remove from library": "Hapus dari perpustakaan",
"Add playlist to library": "Tambahkan daftar putar ke perpustakaan",
"Added playlist to library": "Menambahkan daftar putar ke perpustakaan",
"Make playlist offline": "Buat daftar putar offline",
"Download playlist": "Unduh daftar putar",
"Create playlist": "Buat daftar putar",
"Title": "Judul",
"Description": "Deskripsi",
"Private": "Pribadi",
"Collaborative": "Kolaboratif",
"Create": "Buat",
"Playlist created!": "Daftar putar berhasil dibuat!",
"Playing from:": "Memainkan:",
"Queue": "Antrean",
"Offline search": "Pencarian offline",
"Search Results": "Hasil perncarian",
"No results!": "Hasil tidak ditemukan!",
"Show all tracks": "Tampilkan semua lagu",
"Show all playlists": "Tampilkan semua daftar putar",
"Settings": "Pengaturan",
"General": "Umum",
"Appearance": "Tampilan",
"Quality": "Kualitas",
"Deezer": "Deezer",
"Theme": "Tema",
"Currently": "Saat ini",
"Select theme": "Pilih tema",
"Dark": "Gelap",
"Black (AMOLED)": "Hitam (AMOLED)",
"Deezer (Dark)": "Deezer (Gelap)",
"Primary color": "Warna utama",
"Selected color": "Warna yang dipilih",
"Use album art primary color": "Gunakan foto album sebagai warna utama",
"Warning: might be buggy": "Peringatan: masih ada bug",
"Mobile streaming": "Mobile streaming",
"Wifi streaming": "Wifi streaming",
"External downloads": "Unduhan eksternal",
"Content language": "Bahasa konten",
"Not app language, used in headers. Now":
"Bukan bahasa aplikasi, digunakan di header. Digunakan",
"Select language": "Pilih bahasa",
"Content country": "Wilayah konten",
"Country used in headers. Now": "Negara digunakan di header. Digunakan",
"Log tracks": "Catatan lagu",
"Send track listen logs to Deezer, enable it for features like Flow to work properly":
"Kirim catatan mendengarkan lagu ke Deezer, aktifkan agar fitur seperti Flow berfungsi dengan benar",
"Offline mode": "Mode offline",
"Will be overwritten on start.": "Akan ditimpa saat mulai.",
"Error logging in, check your internet connections.":
"Kesalahan saat masuk, periksa koneksi internet Anda.",
"Logging in...": "Masuk...",
"Download path": "Path unduhan",
"Downloads naming": "Penamaan unduhan",
"Downloaded tracks filename": "Nama file yang diunduh",
"Valid variables are": "Variabel yang valid",
"Reset": "Atur ulang",
"Clear": "Bersihkan",
"Create folders for artist": "Buat folder dari artis",
"Create folders for albums": "Buat folder dari album",
"Separate albums by discs": "Pisahkan album dengan disk",
"Overwrite already downloaded files": "Timpa file yang sudah diunduh",
"Copy ARL": "Salin ARL",
"Copy userToken/ARL Cookie for use in other apps.":
"Salin Token/ARL Cookie untuk digunakan di apps lain.",
"Copied": "Tersalin",
"Log out": "Keluar",
"Due to plugin incompatibility, login using browser is unavailable without restart.":
"Karena ketidakcocokan plugin, masuk menggunakan browser tidak tersedia tanpa restart.",
"(ARL ONLY) Continue": "(HANYA ARL) Lanjutkan",
"Log out & Exit": "Keluar",
"Pick-a-Path": "Pilih-sebuah-Jalur",
"Select storage": "Pilih penyimpanan",
"Go up": "Naik",
"Permission denied": "Akses dilarang",
"Language": "Bahasa",
"Language changed, please restart Freezer to apply!":
"Bahasa diganti, Mulai ulang aplikasi untuk menerapkannya!",
"Importing...": "Mengimpor...",
"Radio": "Radio",
"Flow": "Flow",
"Track is not available on Deezer!": "Lagu tidak tersedia di Deezer!",
"Failed to download track! Please restart.": "Gagal untuk mengunduh lagu! Ulangi kembali.",
//0.5.0 Strings:
"Storage permission denied!": "Izin penyimpanan ditolak!",
"Failed": "Gagal",
"Queued": "Dalam antrean",
//Updated in 0.5.1 - used in context of download:
"External": "Penyimpanan",
//0.5.0
"Restart failed downloads": "Gagal memulai ulang unduhan",
"Clear failed": "Gagal membersihkan",
"Download Settings": "Pengaturan unduhan",
"Create folder for playlist": "Buat folder dari daftar putar",
"Download .LRC lyrics": "Unduh lirik .LRC",
"Proxy": "Proxy",
"Not set": "Tidak diatur",
"Search or paste URL": "Cari atau masukkan URL",
"History": "Riwayat",
//Updated 0.5.1
"Download threads": "Unduh bersamaan",
//0.5.0
"Lyrics unavailable, empty or failed to load!": "Lirik tidak tersedia, kosong atau gagal untuk memuat!",
"About": "Tentang",
"Telegram Channel": "Channel Telegram",
"To get latest releases": "Untuk mendapatkan rilisan terbaru",
"Official chat": "Obrolan resmi",
"Telegram Group": "Grub Telegram",
"Huge thanks to all the contributors! <3": "Terima kasih banyak untuk semua kontributor! <3",
"Edit playlist": "Edit daftar putar",
"Update": "Perbarui",
"Playlist updated!": "Daftar putar diperbarui!",
"Downloads added!": "Unduhan ditambahkan!",
//0.5.1 Strings:
"Save cover file for every track": "Simpan cover foto dari setiap lagu",
"Download Log": "Catatan unduhan",
"Repository": "Repository",
"Source code, report issues there.": "Kode sumber, laporkan masalah disini.",
//0.5.2 Strings:
"Use system theme": "Gunakan tema sistem",
"Light": "Cerah",
//0.5.3 Strings:
"Popularity": "Popularitas",
"User": "Pengguna",
"Track count": "Jumlah lagu",
"If you want to use custom directory naming - use '/' as directory separator.": "Jika Anda ingin menggunakan penamaan direktori kustom - gunakan '/' sebagai pemisah direktori."
}
};

View file

@ -1,3 +1,5 @@
import 'dart:async';
import 'package:audio_service/audio_service.dart';
import 'package:custom_navigator/custom_navigator.dart';
import 'package:flutter/material.dart';
@ -11,6 +13,7 @@ import 'package:freezer/ui/search.dart';
import 'package:i18n_extension/i18n_widget.dart';
import 'package:move_to_background/move_to_background.dart';
import 'package:freezer/translations.i18n.dart';
import 'package:uni_links/uni_links.dart';
import 'api/deezer.dart';
import 'api/download.dart';
@ -42,6 +45,7 @@ class FreezerApp extends StatefulWidget {
}
class _FreezerAppState extends State<FreezerApp> {
@override
void initState() {
//Make update theme global
@ -144,16 +148,42 @@ class MainScreen extends StatefulWidget {
_MainScreenState createState() => _MainScreenState();
}
class _MainScreenState extends State<MainScreen> {
class _MainScreenState extends State<MainScreen> with SingleTickerProviderStateMixin{
List<Widget> _screens = [HomeScreen(), SearchScreen(), LibraryScreen()];
int _selected = 0;
StreamSubscription _urlLinkStream;
@override
void initState() {
navigatorKey = GlobalKey<NavigatorState>();
//Setup URLs
setupUniLinks();
super.initState();
}
@override
void dispose() {
if (_urlLinkStream != null)
_urlLinkStream.cancel();
super.dispose();
}
void setupUniLinks() async {
//Listen to URLs
_urlLinkStream = getUriLinksStream().listen((Uri uri) {
openScreenByURL(context, uri.toString());
}, onError: (err) {});
//Get initial link on cold start
try {
String link = await getInitialLink();
if (link != null && link.length > 4)
openScreenByURL(context, link);
} catch (e) {}
}
@override
Widget build(BuildContext context) {
return Scaffold(

View file

@ -60,6 +60,8 @@ class Settings {
bool downloadLyrics;
@JsonKey(defaultValue: false)
bool trackCover;
@JsonKey(defaultValue: true)
bool albumCover;
//Appearance
@JsonKey(defaultValue: Themes.Dark)
@ -111,7 +113,8 @@ class Settings {
"overwriteDownload": overwriteDownload,
"downloadLyrics": downloadLyrics,
"trackCover": trackCover,
"arl": arl
"arl": arl,
"albumCover": albumCover
};
}

View file

@ -34,6 +34,7 @@ Settings _$SettingsFromJson(Map<String, dynamic> json) {
..playlistFolder = json['playlistFolder'] as bool ?? false
..downloadLyrics = json['downloadLyrics'] as bool ?? true
..trackCover = json['trackCover'] as bool ?? false
..albumCover = json['albumCover'] as bool ?? true
..theme =
_$enumDecodeNullable(_$ThemesEnumMap, json['theme']) ?? Themes.Dark
..useSystemTheme = json['useSystemTheme'] as bool ?? false
@ -62,6 +63,7 @@ Map<String, dynamic> _$SettingsToJson(Settings instance) => <String, dynamic>{
'playlistFolder': instance.playlistFolder,
'downloadLyrics': instance.downloadLyrics,
'trackCover': instance.trackCover,
'albumCover': instance.albumCover,
'theme': _$ThemesEnumMap[instance.theme],
'useSystemTheme': instance.useSystemTheme,
'primaryColor': Settings._colorToJson(instance.primaryColor),

View file

@ -8,6 +8,7 @@ import 'package:freezer/languages/fil_ph.dart';
import 'package:freezer/languages/fr_fr.dart';
import 'package:freezer/languages/he_il.dart';
import 'package:freezer/languages/hr_hr.dart';
import 'package:freezer/languages/id_id.dart';
import 'package:freezer/languages/it_it.dart';
import 'package:freezer/languages/ko_ko.dart';
import 'package:freezer/languages/pt_br.dart';
@ -31,6 +32,7 @@ const supportedLocales = [
const Locale('he', 'IL'),
const Locale('tr', 'TR'),
const Locale('ro', 'RO'),
const Locale('id', 'ID'),
const Locale('fil', 'PH')
];
@ -38,7 +40,7 @@ extension Localization on String {
static var _t = Translations.byLocale("en_US") +
language_en_us + language_ar_ar + language_pt_br + language_it_it + language_de_de + language_ru_ru +
language_fil_ph + language_es_es + language_el_gr + language_hr_hr + language_ko_ko + language_fr_fr +
language_he_il + language_tr_tr + language_ro_ro;
language_he_il + language_tr_tr + language_ro_ro + language_id_id;
String get i18n => localize(this, _t);
}

View file

@ -676,7 +676,7 @@ class _PlaylistDetailsState extends State<PlaylistDetails> {
//Load tracks from api
void _load() async {
if (playlist.tracks.length < playlist.trackCount && !_loading) {
if (playlist.tracks.length < (playlist.trackCount??playlist.tracks.length) && !_loading) {
setState(() => _loading = true);
int pos = playlist.tracks.length;
//Get another page of tracks
@ -794,7 +794,7 @@ class _PlaylistDetailsState extends State<PlaylistDetails> {
size: 32.0,
),
Container(width: 8.0,),
Text(playlist.trackCount.toString(), style: TextStyle(fontSize: 16),)
Text((playlist.trackCount??playlist.tracks.length).toString(), style: TextStyle(fontSize: 16),)
],
),
Row(

View file

@ -208,9 +208,27 @@ class _LibraryTracksState extends State<LibraryTracks> {
List<Track> tracks = [];
List<Track> allTracks = [];
int trackCount;
SortType _sort = SortType.DEFAULT;
Playlist get _playlist => Playlist(id: deezerAPI.favoritesPlaylistId);
List<Track> get _sorted {
List<Track> tcopy = List.from(tracks);
switch (_sort) {
case SortType.ALPHABETIC:
tcopy.sort((a, b) => a.title.compareTo(b.title));
return tcopy;
case SortType.ARTIST:
tcopy.sort((a, b) => a.artists[0].name.toLowerCase().compareTo(b.artists[0].name.toLowerCase()));
return tcopy;
case SortType.REVERSE:
return tcopy.reversed.toList();
case SortType.DEFAULT:
default:
return tcopy;
}
}
Future _load() async {
//Already loaded
if (trackCount != null && tracks.length >= trackCount) {
@ -273,6 +291,22 @@ class _LibraryTracksState extends State<LibraryTracks> {
}
}
//Load all tracks
Future _loadFull() async {
if (tracks.length < (trackCount??0)) {
Playlist p;
try {
p = await deezerAPI.fullPlaylist(deezerAPI.favoritesPlaylistId);
} catch (e) {}
if (p != null) {
setState(() {
tracks = p.tracks;
trackCount = p.trackCount;
});
}
}
}
Future _loadOffline() async {
Playlist p = await downloadManager.getPlaylist(deezerAPI.favoritesPlaylistId);
if (p != null) setState(() {
@ -280,7 +314,7 @@ class _LibraryTracksState extends State<LibraryTracks> {
});
}
Future _loadAll() async {
Future _loadAllOffline() async {
List tracks = await downloadManager.allOfflineTracks();
setState(() {
allTracks = tracks;
@ -301,10 +335,14 @@ class _LibraryTracksState extends State<LibraryTracks> {
if (_scrollController.position.pixels > off) _load();
});
_load();
_sort = cache.trackSort??SortType.DEFAULT;
//Load all tracks
_loadAll();
_load();
//Load all offline tracks
_loadAllOffline();
if (_sort != SortType.DEFAULT)
_loadFull();
super.initState();
}
@ -312,7 +350,41 @@ class _LibraryTracksState extends State<LibraryTracks> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Tracks'.i18n),),
appBar: AppBar(
title: Text('Tracks'.i18n),
actions: [
PopupMenuButton(
child: Icon(Icons.sort, size: 32.0),
onSelected: (SortType s) async {
//Preload for sorting
if (tracks.length < (trackCount??0))
await _loadFull();
setState(() => _sort = s);
cache.trackSort = s;
await cache.save();
},
itemBuilder: (context) => <PopupMenuEntry<SortType>>[
PopupMenuItem(
value: SortType.DEFAULT,
child: Text('Default'.i18n),
),
PopupMenuItem(
value: SortType.REVERSE,
child: Text('Reverse'.i18n),
),
PopupMenuItem(
value: SortType.ALPHABETIC,
child: Text('Alphabetic'.i18n),
),
PopupMenuItem(
value: SortType.ARTIST,
child: Text('Artist'.i18n),
),
],
),
],
),
body: ListView(
controller: _scrollController,
children: <Widget>[
@ -352,11 +424,11 @@ class _LibraryTracksState extends State<LibraryTracks> {
),
//Loved tracks
...List.generate(tracks.length, (i) {
Track t = tracks[i];
Track t = (tracks.length == (trackCount??0))?_sorted[i]:tracks[i];
return TrackTile(
t,
onTap: () {
playerHelper.playFromTrackList(tracks, t.id, QueueSource(
playerHelper.playFromTrackList((tracks.length == (trackCount??0))?_sorted:tracks, t.id, QueueSource(
id: deezerAPI.favoritesPlaylistId,
text: 'Favorites'.i18n,
source: 'playlist'
@ -613,6 +685,7 @@ class _LibraryArtistsState extends State<LibraryArtists> {
artists.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
return artists;
}
return artists;
}
//Load data
@ -750,6 +823,7 @@ class _LibraryPlaylistsState extends State<LibraryPlaylists> {
playlists.sort((a, b) => a.title.toLowerCase().compareTo(b.title.toLowerCase()));
return playlists;
}
return playlists;
}
Future _load() async {
@ -862,7 +936,7 @@ class _LibraryPlaylistsState extends State<LibraryPlaylists> {
if (_playlists != null)
...List.generate(_playlists.length, (int i) {
Playlist p = _sorted[i];
Playlist p = (_sorted??[])[i];
return PlaylistTile(
p,
onTap: () => Navigator.of(context).push(MaterialPageRoute(

View file

@ -1,5 +1,3 @@
import 'dart:ffi';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:audio_service/audio_service.dart';
@ -10,6 +8,7 @@ import 'package:freezer/api/download.dart';
import 'package:freezer/ui/details_screens.dart';
import 'package:freezer/ui/error.dart';
import 'package:freezer/translations.i18n.dart';
import 'package:share/share.dart';
import '../api/definitions.dart';
import 'cached_image.dart';
@ -129,6 +128,7 @@ class MenuSheet {
(cache.checkTrackFavorite(track))?removeFavoriteTrack(track, onUpdate: onRemove):addTrackFavorite(track),
addToPlaylist(track),
downloadTrack(track),
shareTile('track', track.id),
showAlbum(track.album),
...List.generate(track.artists.length, (i) => showArtist(track.artists[i])),
...options
@ -297,6 +297,7 @@ class MenuSheet {
album.library?removeAlbum(album, onRemove: onRemove):libraryAlbum(album),
downloadAlbum(album),
offlineAlbum(album),
shareTile('album', album.id),
...options
]);
}
@ -363,6 +364,7 @@ class MenuSheet {
void defaultArtistMenu(Artist artist, {List<Widget> options = const [], Function onRemove}) {
show([
artist.library?removeArtist(artist, onRemove: onRemove):favoriteArtist(artist),
shareTile('artist', artist.id),
...options
]);
}
@ -409,6 +411,7 @@ class MenuSheet {
playlist.library?removePlaylistLibrary(playlist, onRemove: onRemove):addPlaylistLibrary(playlist),
addPlaylistOffline(playlist),
downloadPlaylist(playlist),
shareTile('playlist', playlist.id),
if (playlist.user.id == deezerAPI.userId)
editPlaylist(playlist, onUpdate: onUpdate),
...options
@ -508,6 +511,14 @@ class MenuSheet {
);
}
Widget shareTile(String type, String id) => ListTile(
title: Text('Share'.i18n),
leading: Icon(Icons.share),
onTap: () async {
Share.share('https://deezer.com/$type/$id');
},
);
void _close() => Navigator.of(context).pop();
}

View file

@ -15,55 +15,73 @@ class PlayerBar extends StatelessWidget {
}
double iconSize = 32;
bool _gestureRegistered = false;
@override
Widget build(BuildContext context) {
return StreamBuilder(
stream: Stream.periodic(Duration(milliseconds: 250)),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (AudioService.currentMediaItem == null) return Container(width: 0, height: 0,);
return Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(
color: Theme.of(context).bottomAppBarColor,
child: ListTile(
onTap: () => Navigator.of(context).push(MaterialPageRoute(builder: (BuildContext context) => PlayerScreen())),
leading: CachedImage(
width: 50,
height: 50,
url: AudioService.currentMediaItem.extras['thumb'] ?? AudioService.currentMediaItem.artUri,
),
title: Text(
AudioService.currentMediaItem.displayTitle,
overflow: TextOverflow.clip,
maxLines: 1,
),
subtitle: Text(
AudioService.currentMediaItem.displaySubtitle,
overflow: TextOverflow.clip,
maxLines: 1,
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
PrevNextButton(iconSize, prev: true, hidePrev: true,),
PlayPauseButton(iconSize),
PrevNextButton(iconSize)
],
)
),
),
Container(
height: 3.0,
child: LinearProgressIndicator(
backgroundColor: Theme.of(context).primaryColor.withOpacity(0.1),
value: progress,
),
)
],
);
return GestureDetector(
onHorizontalDragUpdate: (details) async {
if (_gestureRegistered) return;
final double sensitivity = 12.69;
//Right swipe
_gestureRegistered = true;
if (details.delta.dx > sensitivity) {
await AudioService.skipToPrevious();
}
//Left
if (details.delta.dx < -sensitivity) {
await AudioService.skipToNext();
}
_gestureRegistered = false;
return;
},
child: StreamBuilder(
stream: Stream.periodic(Duration(milliseconds: 250)),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (AudioService.currentMediaItem == null) return Container(width: 0, height: 0,);
return Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(
color: Theme.of(context).bottomAppBarColor,
child: ListTile(
onTap: () => Navigator.of(context).push(MaterialPageRoute(builder: (BuildContext context) => PlayerScreen())),
leading: CachedImage(
width: 50,
height: 50,
url: AudioService.currentMediaItem.extras['thumb'] ?? AudioService.currentMediaItem.artUri,
),
title: Text(
AudioService.currentMediaItem.displayTitle,
overflow: TextOverflow.clip,
maxLines: 1,
),
subtitle: Text(
AudioService.currentMediaItem.displaySubtitle,
overflow: TextOverflow.clip,
maxLines: 1,
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
PrevNextButton(iconSize, prev: true, hidePrev: true,),
PlayPauseButton(iconSize),
PrevNextButton(iconSize)
],
)
),
),
Container(
height: 3.0,
child: LinearProgressIndicator(
backgroundColor: Theme.of(context).primaryColor.withOpacity(0.1),
value: progress,
),
)
],
);
},
),
);
}
}

View file

@ -71,6 +71,9 @@ class _PlayerScreenHorizontalState extends State<PlayerScreenHorizontal> {
double iconSize = ScreenUtil().setWidth(64);
bool _lyrics = false;
PageController _pageController = PageController(
initialPage: playerHelper.queueIndex,
);
@override
Widget build(BuildContext context) {
@ -79,14 +82,20 @@ class _PlayerScreenHorizontalState extends State<PlayerScreenHorizontal> {
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
Padding(
padding: EdgeInsets.fromLTRB(16, 0, 16, 8),
child: Container(
padding: EdgeInsets.fromLTRB(16, 0, 16, 8),
child: Container(
width: ScreenUtil().setWidth(500),
child: Stack(
children: <Widget>[
CachedImage(
url: AudioService.currentMediaItem.artUri,
fullThumb: true,
PageView(
controller: _pageController,
onPageChanged: (int index) {
AudioService.skipToQueueItem(AudioService.queue[index].id);
},
children: List.generate(AudioService.queue.length, (i) => CachedImage(
url: AudioService.queue[i].artUri,
fullThumb: true,
)),
),
if (_lyrics) LyricsWidget(
artUri: AudioService.currentMediaItem.extras['thumb'],
@ -96,7 +105,7 @@ class _PlayerScreenHorizontalState extends State<PlayerScreenHorizontal> {
),
],
),
)
),
),
//Right side
SizedBox(
@ -226,6 +235,9 @@ class PlayerScreenVertical extends StatefulWidget {
class _PlayerScreenVerticalState extends State<PlayerScreenVertical> {
double iconSize = ScreenUtil().setWidth(100);
bool _lyrics = false;
PageController _pageController = PageController(
initialPage: playerHelper.queueIndex,
);
@override
Widget build(BuildContext context) {
@ -243,9 +255,15 @@ class _PlayerScreenVerticalState extends State<PlayerScreenVertical> {
height: ScreenUtil().setHeight(1050),
child: Stack(
children: <Widget>[
CachedImage(
url: AudioService.currentMediaItem.artUri,
fullThumb: true,
PageView(
controller: _pageController,
onPageChanged: (int index) {
AudioService.skipToQueueItem(AudioService.queue[index].id);
},
children: List.generate(AudioService.queue.length, (i) => CachedImage(
url: AudioService.queue[i].artUri,
fullThumb: true,
)),
),
if (_lyrics) LyricsWidget(
artUri: AudioService.currentMediaItem.extras['thumb'],
@ -255,7 +273,7 @@ class _PlayerScreenVerticalState extends State<PlayerScreenVertical> {
),
],
),
)
),
),
Column(
mainAxisSize: MainAxisSize.min,

View file

@ -8,6 +8,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_material_color_picker/flutter_material_color_picker.dart';
import 'package:fluttericon/font_awesome5_icons.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:freezer/api/cache.dart';
import 'package:freezer/api/deezer.dart';
import 'package:freezer/api/download.dart';
import 'package:freezer/ui/downloads_screen.dart';
@ -671,8 +672,31 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
_downloadThreads = settings.downloadThreads.toDouble();
});
await settings.save();
//Prevent null
if (val > 8 && cache.threadsWarning != true) {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text('Warning'.i18n),
content: Text('Using too many concurrent downloads on older/weaker devices might cause crashes!'.i18n),
actions: [
FlatButton(
child: Text('Dismiss'.i18n),
onPressed: () => Navigator.of(context).pop(),
)
],
);
}
);
cache.threadsWarning = true;
await cache.save();
}
}
),
Divider(),
ListTile(
title: Text('Create folders for artist'.i18n),
leading: Container(
@ -699,6 +723,20 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
),
),
),
ListTile(
title: Text('Create folder for playlist'.i18n),
leading: Container(
width: 30.0,
child: Checkbox(
value: settings.playlistFolder,
onChanged: (v) {
setState(() => settings.playlistFolder = v);
settings.save();
},
),
),
),
Divider(),
ListTile(
title: Text('Separate albums by discs'.i18n),
leading: Container(
@ -725,19 +763,6 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
),
),
),
ListTile(
title: Text('Create folder for playlist'.i18n),
leading: Container(
width: 30.0,
child: Checkbox(
value: settings.playlistFolder,
onChanged: (v) {
setState(() => settings.playlistFolder = v);
settings.save();
},
),
),
),
ListTile(
title: Text('Download .LRC lyrics'.i18n),
leading: Container(
@ -751,6 +776,7 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
),
),
),
Divider(),
ListTile(
title: Text('Save cover file for every track'.i18n),
leading: Container(
@ -764,6 +790,20 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
),
),
),
ListTile(
title: Text('Save album cover'.i18n),
leading: Container(
width: 30.0,
child: Checkbox(
value: settings.albumCover,
onChanged: (v) {
setState(() => settings.albumCover = v);
settings.save();
},
),
),
),
Divider(),
ListTile(
title: Text('Download Log'.i18n),
leading: Icon(Icons.sticky_note_2),
@ -1055,7 +1095,8 @@ class _CreditsScreenState extends State<CreditsScreen> {
['Fwwwwwwwwwweze', 'French'],
['kobyrevah', 'Hebrew'],
['HoScHaKaL', 'Turkish'],
['MicroMihai', 'Romanian']
['MicroMihai', 'Romanian'],
['LenteraMalam', 'Indonesian']
];
@override