pre-null safety migration code
This commit is contained in:
parent
34fc597bbb
commit
8d53162099
6
.gitmodules
vendored
6
.gitmodules
vendored
|
@ -1,6 +0,0 @@
|
|||
[submodule "audio_service"]
|
||||
path = audio_service
|
||||
url = https://git.freezer.life/exttex/audio_service.git
|
||||
[submodule "just_audio"]
|
||||
path = just_audio
|
||||
url = https://git.freezer.life/exttex/just_audio
|
138
android/app/src/main/java/f/f/freezer/FileUtils.java
Normal file
138
android/app/src/main/java/f/f/freezer/FileUtils.java
Normal file
|
@ -0,0 +1,138 @@
|
|||
package f.f.freezer;
|
||||
|
||||
// copied from https://gist.github.com/asifmujteba/d89ba9074bc941de1eaa#file-asfurihelper
|
||||
|
||||
import android.content.ContentUris;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Environment;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.provider.MediaStore;
|
||||
|
||||
public class FileUtils {
|
||||
@TargetApi(Build.VERSION_CODES.KITKAT)
|
||||
public static String getPath(final Context context, final Uri uri) {
|
||||
|
||||
final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
|
||||
|
||||
// DocumentProvider
|
||||
if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) {
|
||||
// ExternalStorageProvider
|
||||
if (isExternalStorageDocument(uri)) {
|
||||
final String docId = DocumentsContract.getDocumentId(uri);
|
||||
final String[] split = docId.split(":");
|
||||
final String type = split[0];
|
||||
|
||||
if ("primary".equalsIgnoreCase(type)) {
|
||||
return Environment.getExternalStorageDirectory() + "/" + split[1];
|
||||
}
|
||||
|
||||
// TODO handle non-primary volumes
|
||||
}
|
||||
// DownloadsProvider
|
||||
else if (isDownloadsDocument(uri)) {
|
||||
|
||||
final String id = DocumentsContract.getDocumentId(uri);
|
||||
final Uri contentUri = ContentUris.withAppendedId(
|
||||
Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));
|
||||
|
||||
return getDataColumn(context, contentUri, null, null);
|
||||
}
|
||||
// MediaProvider
|
||||
else if (isMediaDocument(uri)) {
|
||||
final String docId = DocumentsContract.getDocumentId(uri);
|
||||
final String[] split = docId.split(":");
|
||||
final String type = split[0];
|
||||
|
||||
Uri contentUri = null;
|
||||
if ("image".equals(type)) {
|
||||
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
|
||||
} else if ("video".equals(type)) {
|
||||
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
|
||||
} else if ("audio".equals(type)) {
|
||||
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
|
||||
}
|
||||
|
||||
final String selection = "_id=?";
|
||||
final String[] selectionArgs = new String[] {
|
||||
split[1]
|
||||
};
|
||||
|
||||
return getDataColumn(context, contentUri, selection, selectionArgs);
|
||||
}
|
||||
}
|
||||
// MediaStore (and general)
|
||||
else if ("content".equalsIgnoreCase(uri.getScheme())) {
|
||||
|
||||
// Return the remote address
|
||||
if (isGooglePhotosUri(uri))
|
||||
return uri.getLastPathSegment();
|
||||
|
||||
return getDataColumn(context, uri, null, null);
|
||||
}
|
||||
// File
|
||||
else if ("file".equalsIgnoreCase(uri.getScheme())) {
|
||||
return uri.getPath();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static String getDataColumn(Context context, Uri uri, String selection,
|
||||
String[] selectionArgs) {
|
||||
|
||||
Cursor cursor = null;
|
||||
final String column = "_data";
|
||||
final String[] projection = {
|
||||
column
|
||||
};
|
||||
|
||||
try {
|
||||
cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs,
|
||||
null);
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
final int index = cursor.getColumnIndexOrThrow(column);
|
||||
return cursor.getString(index);
|
||||
}
|
||||
} finally {
|
||||
if (cursor != null)
|
||||
cursor.close();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param uri The Uri to check.
|
||||
* @return Whether the Uri authority is ExternalStorageProvider.
|
||||
*/
|
||||
public static boolean isExternalStorageDocument(Uri uri) {
|
||||
return "com.android.externalstorage.documents".equals(uri.getAuthority());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param uri The Uri to check.
|
||||
* @return Whether the Uri authority is DownloadsProvider.
|
||||
*/
|
||||
public static boolean isDownloadsDocument(Uri uri) {
|
||||
return "com.android.providers.downloads.documents".equals(uri.getAuthority());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param uri The Uri to check.
|
||||
* @return Whether the Uri authority is MediaProvider.
|
||||
*/
|
||||
public static boolean isMediaDocument(Uri uri) {
|
||||
return "com.android.providers.media.documents".equals(uri.getAuthority());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param uri The Uri to check.
|
||||
* @return Whether the Uri authority is Google Photos.
|
||||
*/
|
||||
public static boolean isGooglePhotosUri(Uri uri) {
|
||||
return "com.google.android.apps.photos.content".equals(uri.getAuthority());
|
||||
}
|
||||
}
|
|
@ -15,6 +15,7 @@ import android.os.IBinder;
|
|||
import android.os.Message;
|
||||
import android.os.Messenger;
|
||||
import android.os.RemoteException;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.util.Log;
|
||||
import androidx.core.view.WindowCompat;
|
||||
|
||||
|
@ -51,6 +52,10 @@ import static f.f.freezer.Deezer.bytesToHex;
|
|||
public class MainActivity extends FlutterActivity {
|
||||
private static final String CHANNEL = "f.f.freezer/native";
|
||||
private static final String EVENT_CHANNEL = "f.f.freezer/downloads";
|
||||
|
||||
private static final int REQUEST_CODE_DIRECTORY_CHOOSER = MainActivity.class.hashCode() + 60;
|
||||
private MethodChannel.Result pendingResult;
|
||||
|
||||
EventChannel.EventSink eventSink;
|
||||
|
||||
boolean serviceBound = false;
|
||||
|
@ -224,7 +229,16 @@ public class MainActivity extends FlutterActivity {
|
|||
System.exit(0);
|
||||
}
|
||||
|
||||
result.error("0", "Not implemented!", "Not implemented!");
|
||||
// Open the directory picker native dialog
|
||||
if (call.method.equals("getDirectory")) {
|
||||
pendingResult = result;
|
||||
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
|
||||
intent.addCategory(Intent.CATEGORY_DEFAULT);
|
||||
startActivityForResult(Intent.createChooser(intent, call.argument("title")), REQUEST_CODE_DIRECTORY_CHOOSER);
|
||||
return;
|
||||
}
|
||||
|
||||
result.notImplemented();
|
||||
})));
|
||||
|
||||
//Event channel (for download updates)
|
||||
|
@ -242,6 +256,29 @@ public class MainActivity extends FlutterActivity {
|
|||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
if (requestCode == REQUEST_CODE_DIRECTORY_CHOOSER) {
|
||||
switch (resultCode) {
|
||||
case RESULT_OK:
|
||||
Uri uri = data.getData();
|
||||
Log.v("MainActivity", "trying to get path for uri "+uri.toString());
|
||||
Uri documentUri = DocumentsContract.buildDocumentUriUsingTree(uri, DocumentsContract.getTreeDocumentId(uri));
|
||||
String path = FileUtils.getPath(getContext(), documentUri);
|
||||
Log.v("MainActivity", "got path "+path);
|
||||
pendingResult.success(path);
|
||||
break;
|
||||
case RESULT_CANCELED:
|
||||
pendingResult.success(null);
|
||||
break;
|
||||
default:
|
||||
pendingResult.error("0", "Unknown result code", "code: "+resultCode+", uri data: "+data.getData());
|
||||
break;
|
||||
}
|
||||
}
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
|
||||
//Start/Bind/Reconnect to download service
|
||||
private void connectService() {
|
||||
if (serviceBound)
|
||||
|
|
|
@ -10,7 +10,6 @@ import 'dart:async';
|
|||
DeezerAPI deezerAPI = DeezerAPI();
|
||||
|
||||
class DeezerAPI {
|
||||
|
||||
DeezerAPI({this.arl});
|
||||
|
||||
String arl;
|
||||
|
@ -24,18 +23,22 @@ class DeezerAPI {
|
|||
|
||||
//Get headers
|
||||
Map<String, String> get headers => {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36",
|
||||
"Content-Language": '${settings.deezerLanguage??"en"}-${settings.deezerCountry??'US'}',
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36",
|
||||
"Content-Language":
|
||||
'${settings.deezerLanguage ?? "en"}-${settings.deezerCountry ?? 'US'}',
|
||||
"Cache-Control": "max-age=0",
|
||||
"Accept": "*/*",
|
||||
"Accept-Charset": "utf-8,ISO-8859-1;q=0.7,*;q=0.3",
|
||||
"Accept-Language": "${settings.deezerLanguage??"en"}-${settings.deezerCountry??'US'},${settings.deezerLanguage??"en"};q=0.9,en-US;q=0.8,en;q=0.7",
|
||||
"Accept-Language":
|
||||
"${settings.deezerLanguage ?? "en"}-${settings.deezerCountry ?? 'US'},${settings.deezerLanguage ?? "en"};q=0.9,en-US;q=0.8,en;q=0.7",
|
||||
"Connection": "keep-alive",
|
||||
"Cookie": "arl=$arl" + ((sid == null) ? '' : '; sid=$sid')
|
||||
};
|
||||
|
||||
//Call private API
|
||||
Future<Map<dynamic, dynamic>> callApi(String method, {Map<dynamic, dynamic> params, String gatewayInput}) async {
|
||||
Future<Map<dynamic, dynamic>> callApi(String method,
|
||||
{Map<dynamic, dynamic> params, String gatewayInput}) async {
|
||||
//Generate URL
|
||||
Uri uri = Uri.https('www.deezer.com', '/ajax/gw-light.php', {
|
||||
'api_version': '1.0',
|
||||
|
@ -43,11 +46,11 @@ class DeezerAPI {
|
|||
'input': '3',
|
||||
'method': method,
|
||||
//Used for homepage
|
||||
if (gatewayInput != null)
|
||||
'gateway_input': gatewayInput
|
||||
if (gatewayInput != null) 'gateway_input': gatewayInput
|
||||
});
|
||||
//Post
|
||||
http.Response res = await http.post(uri, headers: headers, body: jsonEncode(params));
|
||||
http.Response res =
|
||||
await http.post(uri, headers: headers, body: jsonEncode(params));
|
||||
dynamic body = jsonDecode(res.body);
|
||||
//Grab SID
|
||||
if (method == 'deezer.getUserData') {
|
||||
|
@ -58,14 +61,17 @@ class DeezerAPI {
|
|||
}
|
||||
}
|
||||
// In case of error "Invalid CSRF token" retrieve new one and retry the same call
|
||||
if (body['error'].isNotEmpty && body['error'].containsKey('VALID_TOKEN_REQUIRED') && await rawAuthorize()) {
|
||||
if (body['error'].isNotEmpty &&
|
||||
body['error'].containsKey('VALID_TOKEN_REQUIRED') &&
|
||||
await rawAuthorize()) {
|
||||
return callApi(method, params: params, gatewayInput: gatewayInput);
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
Future<Map<dynamic, dynamic>> callPublicApi(String path) async {
|
||||
http.Response res = await http.get('https://api.deezer.com/' + path);
|
||||
Uri uri = Uri(scheme: 'https', host: 'api.deezer.com', path: '/' + path);
|
||||
http.Response res = await http.get(uri);
|
||||
return jsonDecode(res.body);
|
||||
}
|
||||
|
||||
|
@ -83,12 +89,14 @@ class DeezerAPI {
|
|||
Digest digest = md5.convert(utf8.encode(password));
|
||||
String md5password = '$digest';
|
||||
//Get access token
|
||||
String url = "https://tv.deezer.com/smarttv/8caf9315c1740316053348a24d25afc7/user_auth.php?login=$email&password=$md5password&device=panasonic&output=json";
|
||||
http.Response response = await http.get(url);
|
||||
String url =
|
||||
"https://tv.deezer.com/smarttv/8caf9315c1740316053348a24d25afc7/user_auth.php?login=$email&password=$md5password&device=panasonic&output=json";
|
||||
http.Response response = await http.get(Uri.parse(url));
|
||||
String accessToken = jsonDecode(response.body)["access_token"];
|
||||
//Get SID
|
||||
url = "https://api.deezer.com/platform/generic/track/42069";
|
||||
response = await http.get(url, headers: {"Authorization": "Bearer $accessToken"});
|
||||
response = await http
|
||||
.get(Uri.parse(url), headers: {"Authorization": "Bearer $accessToken"});
|
||||
String sid;
|
||||
for (String cookieHeader in response.headers['set-cookie'].split(';')) {
|
||||
if (cookieHeader.startsWith('sid=')) {
|
||||
|
@ -97,12 +105,12 @@ class DeezerAPI {
|
|||
}
|
||||
if (sid == null) return null;
|
||||
//Get ARL
|
||||
url = "https://deezer.com/ajax/gw-light.php?api_version=1.0&api_token=null&input=3&method=user.getArl";
|
||||
response = await http.get(url, headers: {"Cookie": "sid=$sid"});
|
||||
url =
|
||||
"https://deezer.com/ajax/gw-light.php?api_version=1.0&api_token=null&input=3&method=user.getArl";
|
||||
response = await http.get(Uri.parse(url), headers: {"Cookie": "sid=$sid"});
|
||||
return jsonDecode(response.body)["results"];
|
||||
}
|
||||
|
||||
|
||||
//Authorize, bool = success
|
||||
Future<bool> rawAuthorize({Function onError}) async {
|
||||
try {
|
||||
|
@ -117,8 +125,7 @@ class DeezerAPI {
|
|||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
if (onError != null)
|
||||
onError(e);
|
||||
if (onError != null) onError(e);
|
||||
print('Login Error (D): ' + e.toString());
|
||||
return false;
|
||||
}
|
||||
|
@ -130,8 +137,10 @@ class DeezerAPI {
|
|||
//https://www.deezer.com/NOTHING_OR_COUNTRY/TYPE/ID
|
||||
if (uri.host == 'www.deezer.com' || uri.host == 'deezer.com') {
|
||||
if (uri.pathSegments.length < 2) return null;
|
||||
DeezerLinkType type = DeezerLinkResponse.typeFromString(uri.pathSegments[uri.pathSegments.length-2]);
|
||||
return DeezerLinkResponse(type: type, id: uri.pathSegments[uri.pathSegments.length-1]);
|
||||
DeezerLinkType type = DeezerLinkResponse.typeFromString(
|
||||
uri.pathSegments[uri.pathSegments.length - 2]);
|
||||
return DeezerLinkResponse(
|
||||
type: type, id: uri.pathSegments[uri.pathSegments.length - 1]);
|
||||
}
|
||||
//Share URL
|
||||
if (uri.host == 'deezer.page.link' || uri.host == 'www.deezer.page.link') {
|
||||
|
@ -163,7 +172,8 @@ class DeezerAPI {
|
|||
//Check if Deezer available in country
|
||||
static Future<bool> chceckAvailability() async {
|
||||
try {
|
||||
http.Response res = await http.get("https://api.deezer.com/infos");
|
||||
http.Response res =
|
||||
await http.get(Uri.parse('https://api.deezer.com/infos'));
|
||||
return jsonDecode(res.body)["open"];
|
||||
} catch (e) {
|
||||
return null;
|
||||
|
@ -172,16 +182,15 @@ class DeezerAPI {
|
|||
|
||||
//Search
|
||||
Future<SearchResults> search(String query) async {
|
||||
Map<dynamic, dynamic> data = await callApi('deezer.pageSearch', params: {
|
||||
'nb': 128,
|
||||
'query': query,
|
||||
'start': 0
|
||||
});
|
||||
Map<dynamic, dynamic> data = await callApi('deezer.pageSearch',
|
||||
params: {'nb': 128, 'query': query, 'start': 0});
|
||||
return SearchResults.fromPrivateJson(data['results']);
|
||||
}
|
||||
|
||||
Future<Track> track(String id) async {
|
||||
Map<dynamic, dynamic> data = await callApi('song.getListData', params: {'sng_ids': [id]});
|
||||
Map<dynamic, dynamic> data = await callApi('song.getListData', params: {
|
||||
'sng_ids': [id]
|
||||
});
|
||||
return Track.fromPrivateJson(data['results']['data'][0]);
|
||||
}
|
||||
|
||||
|
@ -190,47 +199,50 @@ class DeezerAPI {
|
|||
Map<dynamic, dynamic> data = await callApi('deezer.pageAlbum', params: {
|
||||
'alb_id': id,
|
||||
'header': true,
|
||||
'lang': settings.deezerLanguage??'en'
|
||||
'lang': settings.deezerLanguage ?? 'en'
|
||||
});
|
||||
return Album.fromPrivateJson(data['results']['DATA'], songsJson: data['results']['SONGS']);
|
||||
return Album.fromPrivateJson(data['results']['DATA'],
|
||||
songsJson: data['results']['SONGS']);
|
||||
}
|
||||
|
||||
//Get artist details
|
||||
Future<Artist> artist(String id) async {
|
||||
Map<dynamic, dynamic> data = await callApi('deezer.pageArtist', params: {
|
||||
'art_id': id,
|
||||
'lang': settings.deezerLanguage??'en',
|
||||
'lang': settings.deezerLanguage ?? 'en',
|
||||
});
|
||||
return Artist.fromPrivateJson(
|
||||
data['results']['DATA'],
|
||||
return Artist.fromPrivateJson(data['results']['DATA'],
|
||||
topJson: data['results']['TOP'],
|
||||
albumsJson: data['results']['ALBUMS'],
|
||||
highlight: data['results']['HIGHLIGHT']
|
||||
);
|
||||
highlight: data['results']['HIGHLIGHT']);
|
||||
}
|
||||
|
||||
//Get playlist tracks at offset
|
||||
Future<List<Track>> playlistTracksPage(String id, int start, {int nb = 50}) async {
|
||||
Future<List<Track>> playlistTracksPage(String id, int start,
|
||||
{int nb = 50}) async {
|
||||
Map data = await callApi('deezer.pagePlaylist', params: {
|
||||
'playlist_id': id,
|
||||
'lang': settings.deezerLanguage??'en',
|
||||
'lang': settings.deezerLanguage ?? 'en',
|
||||
'nb': nb,
|
||||
'tags': true,
|
||||
'start': start
|
||||
});
|
||||
return data['results']['SONGS']['data'].map<Track>((json) => Track.fromPrivateJson(json)).toList();
|
||||
return data['results']['SONGS']['data']
|
||||
.map<Track>((json) => Track.fromPrivateJson(json))
|
||||
.toList();
|
||||
}
|
||||
|
||||
//Get playlist details
|
||||
Future<Playlist> playlist(String id, {int nb = 100}) async {
|
||||
Map<dynamic, dynamic> data = await callApi('deezer.pagePlaylist', params: {
|
||||
'playlist_id': id,
|
||||
'lang': settings.deezerLanguage??'en',
|
||||
'lang': settings.deezerLanguage ?? 'en',
|
||||
'nb': nb,
|
||||
'tags': true,
|
||||
'start': 0
|
||||
});
|
||||
return Playlist.fromPrivateJson(data['results']['DATA'], songsJson: data['results']['SONGS']);
|
||||
return Playlist.fromPrivateJson(data['results']['DATA'],
|
||||
songsJson: data['results']['SONGS']);
|
||||
}
|
||||
|
||||
//Get playlist with all tracks
|
||||
|
@ -264,11 +276,14 @@ class DeezerAPI {
|
|||
}
|
||||
|
||||
//Add tracks to playlist
|
||||
Future addToPlaylist(String trackId, String playlistId, {int offset = -1}) async {
|
||||
Future addToPlaylist(String trackId, String playlistId,
|
||||
{int offset = -1}) async {
|
||||
await callApi('playlist.addSongs', params: {
|
||||
'offset': offset,
|
||||
'playlist_id': playlistId,
|
||||
'songs': [[trackId, 0]]
|
||||
'songs': [
|
||||
[trackId, 0]
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -276,83 +291,89 @@ class DeezerAPI {
|
|||
Future removeFromPlaylist(String trackId, String playlistId) async {
|
||||
await callApi('playlist.deleteSongs', params: {
|
||||
'playlist_id': playlistId,
|
||||
'songs': [[trackId, 0]]
|
||||
'songs': [
|
||||
[trackId, 0]
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
//Get users playlists
|
||||
Future<List<Playlist>> getPlaylists() async {
|
||||
Map data = await callApi('deezer.pageProfile', params: {
|
||||
'nb': 100,
|
||||
'tab': 'playlists',
|
||||
'user_id': this.userId
|
||||
});
|
||||
return data['results']['TAB']['playlists']['data'].map<Playlist>((json) => Playlist.fromPrivateJson(json, library: true)).toList();
|
||||
Map data = await callApi('deezer.pageProfile',
|
||||
params: {'nb': 100, 'tab': 'playlists', 'user_id': this.userId});
|
||||
return data['results']['TAB']['playlists']['data']
|
||||
.map<Playlist>((json) => Playlist.fromPrivateJson(json, library: true))
|
||||
.toList();
|
||||
}
|
||||
|
||||
//Get favorite albums
|
||||
Future<List<Album>> getAlbums() async {
|
||||
Map data = await callApi('deezer.pageProfile', params: {
|
||||
'nb': 50,
|
||||
'tab': 'albums',
|
||||
'user_id': this.userId
|
||||
});
|
||||
Map data = await callApi('deezer.pageProfile',
|
||||
params: {'nb': 50, 'tab': 'albums', 'user_id': this.userId});
|
||||
List albumList = data['results']['TAB']['albums']['data'];
|
||||
List<Album> albums = albumList.map<Album>((json) => Album.fromPrivateJson(json, library: true)).toList();
|
||||
List<Album> albums = albumList
|
||||
.map<Album>((json) => Album.fromPrivateJson(json, library: true))
|
||||
.toList();
|
||||
return albums;
|
||||
}
|
||||
|
||||
//Remove album from library
|
||||
Future removeAlbum(String id) async {
|
||||
await callApi('album.deleteFavorite', params: {
|
||||
'ALB_ID': id
|
||||
});
|
||||
await callApi('album.deleteFavorite', params: {'ALB_ID': id});
|
||||
}
|
||||
|
||||
//Remove track from favorites
|
||||
Future removeFavorite(String id) async {
|
||||
await callApi('favorite_song.remove', params: {
|
||||
'SNG_ID': id
|
||||
});
|
||||
await callApi('favorite_song.remove', params: {'SNG_ID': id});
|
||||
}
|
||||
|
||||
//Get favorite artists
|
||||
Future<List<Artist>> getArtists() async {
|
||||
Map data = await callApi('deezer.pageProfile', params: {
|
||||
'nb': 40,
|
||||
'tab': 'artists',
|
||||
'user_id': this.userId
|
||||
});
|
||||
return data['results']['TAB']['artists']['data'].map<Artist>((json) => Artist.fromPrivateJson(json, library: true)).toList();
|
||||
Map data = await callApi('deezer.pageProfile',
|
||||
params: {'nb': 40, 'tab': 'artists', 'user_id': this.userId});
|
||||
return data['results']['TAB']['artists']['data']
|
||||
.map<Artist>((json) => Artist.fromPrivateJson(json, library: true))
|
||||
.toList();
|
||||
}
|
||||
|
||||
//Get lyrics by track id
|
||||
Future<Lyrics> lyrics(String trackId) async {
|
||||
Map data = await callApi('song.getLyrics', params: {
|
||||
'sng_id': trackId
|
||||
});
|
||||
if (data['error'] != null && data['error'].length > 0) return Lyrics.error();
|
||||
Map data = await callApi('song.getLyrics', params: {'sng_id': trackId});
|
||||
if (data['error'] != null && data['error'].length > 0)
|
||||
return Lyrics.error();
|
||||
return Lyrics.fromPrivateJson(data['results']);
|
||||
}
|
||||
|
||||
Future<SmartTrackList> smartTrackList(String id) async {
|
||||
Map data = await callApi('deezer.pageSmartTracklist', params: {
|
||||
'smarttracklist_id': id
|
||||
});
|
||||
return SmartTrackList.fromPrivateJson(data['results']['DATA'], songsJson: data['results']['SONGS']);
|
||||
Map data = await callApi('deezer.pageSmartTracklist',
|
||||
params: {'smarttracklist_id': id});
|
||||
return SmartTrackList.fromPrivateJson(data['results']['DATA'],
|
||||
songsJson: data['results']['SONGS']);
|
||||
}
|
||||
|
||||
Future<List<Track>> flow() async {
|
||||
Map data = await callApi('radio.getUserRadio', params: {
|
||||
'user_id': userId
|
||||
});
|
||||
return data['results']['data'].map<Track>((json) => Track.fromPrivateJson(json)).toList();
|
||||
Map data = await callApi('radio.getUserRadio', params: {'user_id': userId});
|
||||
return data['results']['data']
|
||||
.map<Track>((json) => Track.fromPrivateJson(json))
|
||||
.toList();
|
||||
}
|
||||
|
||||
//Get homepage/music library from deezer
|
||||
Future<HomePage> homePage() async {
|
||||
List grid = ['album', 'artist', 'channel', 'flow', 'playlist', 'radio', 'show', 'smarttracklist', 'track', 'user'];
|
||||
Map data = await callApi('page.get', gatewayInput: jsonEncode({
|
||||
List grid = [
|
||||
'album',
|
||||
'artist',
|
||||
'channel',
|
||||
'flow',
|
||||
'playlist',
|
||||
'radio',
|
||||
'show',
|
||||
'smarttracklist',
|
||||
'track',
|
||||
'user'
|
||||
];
|
||||
Map data = await callApi('page.get',
|
||||
gatewayInput: jsonEncode({
|
||||
"PAGE": "home",
|
||||
"VERSION": "2.3",
|
||||
"SUPPORT": {
|
||||
|
@ -370,7 +391,7 @@ class DeezerAPI {
|
|||
"large-card": ["album", "playlist", "show", "video-link"],
|
||||
"ads": [] //Nope
|
||||
},
|
||||
"LANG": settings.deezerLanguage??'en',
|
||||
"LANG": settings.deezerLanguage ?? 'en',
|
||||
"OPTIONS": []
|
||||
}));
|
||||
return HomePage.fromPrivateJson(data['results']);
|
||||
|
@ -390,8 +411,20 @@ class DeezerAPI {
|
|||
}
|
||||
|
||||
Future<HomePage> getChannel(String target) async {
|
||||
List grid = ['album', 'artist', 'channel', 'flow', 'playlist', 'radio', 'show', 'smarttracklist', 'track', 'user'];
|
||||
Map data = await callApi('page.get', gatewayInput: jsonEncode({
|
||||
List grid = [
|
||||
'album',
|
||||
'artist',
|
||||
'channel',
|
||||
'flow',
|
||||
'playlist',
|
||||
'radio',
|
||||
'show',
|
||||
'smarttracklist',
|
||||
'track',
|
||||
'user'
|
||||
];
|
||||
Map data = await callApi('page.get',
|
||||
gatewayInput: jsonEncode({
|
||||
'PAGE': target,
|
||||
"VERSION": "2.3",
|
||||
"SUPPORT": {
|
||||
|
@ -409,7 +442,7 @@ class DeezerAPI {
|
|||
"large-card": ["album", "playlist", "show", "video-link"],
|
||||
"ads": [] //Nope
|
||||
},
|
||||
"LANG": settings.deezerLanguage??'en',
|
||||
"LANG": settings.deezerLanguage ?? 'en',
|
||||
"OPTIONS": []
|
||||
}));
|
||||
return HomePage.fromPrivateJson(data['results']);
|
||||
|
@ -417,30 +450,33 @@ class DeezerAPI {
|
|||
|
||||
//Add playlist to library
|
||||
Future addPlaylist(String id) async {
|
||||
await callApi('playlist.addFavorite', params: {
|
||||
'parent_playlist_id': int.parse(id)
|
||||
});
|
||||
await callApi('playlist.addFavorite',
|
||||
params: {'parent_playlist_id': int.parse(id)});
|
||||
}
|
||||
|
||||
//Remove playlist from library
|
||||
Future removePlaylist(String id) async {
|
||||
await callApi('playlist.deleteFavorite', params: {
|
||||
'playlist_id': int.parse(id)
|
||||
});
|
||||
await callApi('playlist.deleteFavorite',
|
||||
params: {'playlist_id': int.parse(id)});
|
||||
}
|
||||
|
||||
//Delete playlist
|
||||
Future deletePlaylist(String id) async {
|
||||
await callApi('playlist.delete', params: {
|
||||
'playlist_id': id
|
||||
});
|
||||
await callApi('playlist.delete', params: {'playlist_id': id});
|
||||
}
|
||||
|
||||
//Create playlist
|
||||
//Status 1 - private, 2 - collaborative
|
||||
Future<String> createPlaylist(String title, {String description = "", int status = 1, List<String> trackIds = const []}) async {
|
||||
Future<String> createPlaylist(String title,
|
||||
{String description = "",
|
||||
int status = 1,
|
||||
List<String> trackIds = const []}) async {
|
||||
Map data = await callApi('playlist.create', params: {
|
||||
'title': title,
|
||||
'description': description,
|
||||
'songs': trackIds.map<List>((id) => [int.parse(id), trackIds.indexOf(id)]).toList(),
|
||||
'songs': trackIds
|
||||
.map<List>((id) => [int.parse(id), trackIds.indexOf(id)])
|
||||
.toList(),
|
||||
'status': status
|
||||
});
|
||||
//Return playlistId
|
||||
|
@ -448,7 +484,8 @@ class DeezerAPI {
|
|||
}
|
||||
|
||||
//Get part of discography
|
||||
Future<List<Album>> discographyPage(String artistId, {int start = 0, int nb = 50}) async {
|
||||
Future<List<Album>> discographyPage(String artistId,
|
||||
{int start = 0, int nb = 50}) async {
|
||||
Map data = await callApi('album.getDiscography', params: {
|
||||
'art_id': int.parse(artistId),
|
||||
'discography_mode': 'all',
|
||||
|
@ -457,26 +494,29 @@ class DeezerAPI {
|
|||
'nb_songs': 30
|
||||
});
|
||||
|
||||
return data['results']['data'].map<Album>((a) => Album.fromPrivateJson(a)).toList();
|
||||
return data['results']['data']
|
||||
.map<Album>((a) => Album.fromPrivateJson(a))
|
||||
.toList();
|
||||
}
|
||||
|
||||
Future<List> searchSuggestions(String query) async {
|
||||
Map data = await callApi('search_getSuggestedQueries', params: {
|
||||
'QUERY': query
|
||||
});
|
||||
Map data =
|
||||
await callApi('search_getSuggestedQueries', params: {'QUERY': query});
|
||||
return data['results']['SUGGESTION'].map((s) => s['QUERY']).toList();
|
||||
}
|
||||
|
||||
//Get smart radio for artist id
|
||||
Future<List<Track>> smartRadio(String artistId) async {
|
||||
Map data = await callApi('smart.getSmartRadio', params: {
|
||||
'art_id': int.parse(artistId)
|
||||
});
|
||||
return data['results']['data'].map<Track>((t) => Track.fromPrivateJson(t)).toList();
|
||||
Map data = await callApi('smart.getSmartRadio',
|
||||
params: {'art_id': int.parse(artistId)});
|
||||
return data['results']['data']
|
||||
.map<Track>((t) => Track.fromPrivateJson(t))
|
||||
.toList();
|
||||
}
|
||||
|
||||
//Update playlist metadata, status = see createPlaylist
|
||||
Future updatePlaylist(String id, String title, String description, {int status = 1}) async {
|
||||
Future updatePlaylist(String id, String title, String description,
|
||||
{int status = 1}) async {
|
||||
await callApi('playlist.update', params: {
|
||||
'description': description,
|
||||
'title': title,
|
||||
|
@ -487,12 +527,12 @@ class DeezerAPI {
|
|||
}
|
||||
|
||||
//Get shuffled library
|
||||
Future<List<Track>> libraryShuffle({int start=0}) async {
|
||||
Map data = await callApi('tracklist.getShuffledCollection', params: {
|
||||
'nb': 50,
|
||||
'start': start
|
||||
});
|
||||
return data['results']['data'].map<Track>((t) => Track.fromPrivateJson(t)).toList();
|
||||
Future<List<Track>> libraryShuffle({int start = 0}) async {
|
||||
Map data = await callApi('tracklist.getShuffledCollection',
|
||||
params: {'nb': 50, 'start': start});
|
||||
return data['results']['data']
|
||||
.map<Track>((t) => Track.fromPrivateJson(t))
|
||||
.toList();
|
||||
}
|
||||
|
||||
//Get similar tracks for track with id [trackId]
|
||||
|
@ -500,7 +540,9 @@ class DeezerAPI {
|
|||
Map data = await callApi('song.getContextualTrackMix', params: {
|
||||
'sng_ids': [trackId]
|
||||
});
|
||||
return data['results']['data'].map<Track>((t) => Track.fromPrivateJson(t)).toList();
|
||||
return data['results']['data']
|
||||
.map<Track>((t) => Track.fromPrivateJson(t))
|
||||
.toList();
|
||||
}
|
||||
|
||||
Future<List<ShowEpisode>> allShowEpisodes(String showId) async {
|
||||
|
@ -512,6 +554,8 @@ class DeezerAPI {
|
|||
'start': 0,
|
||||
'user_id': int.parse(deezerAPI.userId)
|
||||
});
|
||||
return data['results']['EPISODES']['data'].map<ShowEpisode>((e) => ShowEpisode.fromPrivateJson(e)).toList();
|
||||
return data['results']['EPISODES']['data']
|
||||
.map<ShowEpisode>((e) => ShowEpisode.fromPrivateJson(e))
|
||||
.toList();
|
||||
}
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:freezer/api/cache.dart';
|
||||
|
@ -14,14 +16,14 @@ part 'definitions.g.dart';
|
|||
|
||||
@JsonSerializable()
|
||||
class Track {
|
||||
String id;
|
||||
String title;
|
||||
Album album;
|
||||
List<Artist> artists;
|
||||
Duration duration;
|
||||
ImageDetails albumArt;
|
||||
String/*!*//*!*/ id;
|
||||
String/*!*/ title;
|
||||
Album/*!*/ album;
|
||||
List<Artist>/*!*/ artists;
|
||||
Duration/*!*/ duration;
|
||||
ImageDetails/*!*/ albumArt;
|
||||
int trackNumber;
|
||||
bool offline;
|
||||
bool/*!*/ offline;
|
||||
Lyrics lyrics;
|
||||
bool favorite;
|
||||
int diskNumber;
|
||||
|
@ -59,7 +61,7 @@ class Track {
|
|||
displayTitle: this.title,
|
||||
displaySubtitle: this.artistString,
|
||||
displayDescription: this.album.title,
|
||||
artUri: this.albumArt.full,
|
||||
artUri: Uri.parse(this.albumArt.full),
|
||||
duration: this.duration,
|
||||
id: this.id,
|
||||
extras: {
|
||||
|
@ -95,8 +97,8 @@ class Track {
|
|||
artists: artists,
|
||||
album: album,
|
||||
id: mi.id,
|
||||
albumArt:
|
||||
ImageDetails(fullUrl: mi.artUri, thumbUrl: mi.extras['thumb']),
|
||||
albumArt: ImageDetails(
|
||||
fullUrl: mi.artUri.toString(), thumbUrl: mi.extras['thumb']),
|
||||
duration: mi.duration,
|
||||
playbackDetails: playbackDetails,
|
||||
lyrics:
|
||||
|
@ -470,6 +472,7 @@ class User {
|
|||
Map<String, dynamic> toJson() => _$UserToJson(this);
|
||||
}
|
||||
|
||||
// TODO: migrate to Uri instead of String
|
||||
@JsonSerializable()
|
||||
class ImageDetails {
|
||||
String fullUrl;
|
||||
|
@ -1001,7 +1004,7 @@ class ShowEpisode {
|
|||
},
|
||||
displayDescription: description,
|
||||
duration: duration,
|
||||
artUri: show.art.full,
|
||||
artUri: Uri.parse(show.art.full),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1058,3 +1061,5 @@ extension Reorder<T> on List<T> {
|
|||
oldIndex > newIndex ? newIndex : newIndex - 1, this.removeAt(oldIndex));
|
||||
}
|
||||
}
|
||||
|
||||
double hypot(num c1, num c2) => sqrt(pow(c1.abs(), 2) + pow(c2.abs(), 2));
|
||||
|
|
|
@ -20,10 +20,10 @@ import 'dart:async';
|
|||
DownloadManager downloadManager = DownloadManager();
|
||||
|
||||
class DownloadManager {
|
||||
|
||||
//Platform channels
|
||||
static MethodChannel platform = MethodChannel('f.f.freezer/native');
|
||||
static EventChannel eventChannel = EventChannel('f.f.freezer/downloads');
|
||||
static MethodChannel platform = const MethodChannel('f.f.freezer/native');
|
||||
static EventChannel eventChannel =
|
||||
const EventChannel('f.f.freezer/downloads');
|
||||
|
||||
bool running = false;
|
||||
int queueSize = 0;
|
||||
|
@ -53,9 +53,7 @@ class DownloadManager {
|
|||
|
||||
String dbPath = p.join((await getDatabasesPath()), 'offline2.db');
|
||||
//Open db
|
||||
db = await openDatabase(
|
||||
dbPath,
|
||||
version: 1,
|
||||
db = await openDatabase(dbPath, version: 1,
|
||||
onCreate: (Database db, int version) async {
|
||||
Batch b = db.batch();
|
||||
//Create tables, if doesn't exit
|
||||
|
@ -68,11 +66,11 @@ class DownloadManager {
|
|||
b.execute("""CREATE TABLE Playlists (
|
||||
id TEXT PRIMARY KEY, title TEXT, tracks TEXT, image TEXT, duration INTEGER, userId TEXT, userName TEXT, fans INTEGER, library INTEGER, description TEXT)""");
|
||||
await b.commit();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
//Create offline directory
|
||||
offlinePath = p.join((await getExternalStorageDirectory()).path, 'offline/');
|
||||
offlinePath =
|
||||
p.join((await getExternalStorageDirectory()).path, 'offline/');
|
||||
await Directory(offlinePath).create(recursive: true);
|
||||
|
||||
//Update settings
|
||||
|
@ -100,11 +98,16 @@ class DownloadManager {
|
|||
|
||||
//Insert track and metadata to DB
|
||||
Future _addTrackToDB(Batch batch, Track track, bool overwriteTrack) async {
|
||||
batch.insert('Tracks', track.toSQL(off: true), conflictAlgorithm: overwriteTrack?ConflictAlgorithm.replace:ConflictAlgorithm.ignore);
|
||||
batch.insert('Albums', track.album.toSQL(off: false), conflictAlgorithm: ConflictAlgorithm.ignore);
|
||||
batch.insert('Tracks', track.toSQL(off: true),
|
||||
conflictAlgorithm: overwriteTrack
|
||||
? ConflictAlgorithm.replace
|
||||
: ConflictAlgorithm.ignore);
|
||||
batch.insert('Albums', track.album.toSQL(off: false),
|
||||
conflictAlgorithm: ConflictAlgorithm.ignore);
|
||||
//Artists
|
||||
for (Artist a in track.artists) {
|
||||
batch.insert('Artists', a.toSQL(off: false), conflictAlgorithm: ConflictAlgorithm.ignore);
|
||||
batch.insert('Artists', a.toSQL(off: false),
|
||||
conflictAlgorithm: ConflictAlgorithm.ignore);
|
||||
}
|
||||
return batch;
|
||||
}
|
||||
|
@ -122,10 +125,7 @@ class DownloadManager {
|
|||
padding: EdgeInsets.fromLTRB(0, 12, 0, 2),
|
||||
child: Text(
|
||||
'Quality'.i18n,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 20.0
|
||||
),
|
||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20.0),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
|
@ -151,12 +151,12 @@ class DownloadManager {
|
|||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
return quality;
|
||||
}
|
||||
|
||||
Future<bool> addOfflineTrack(Track track, {private = true, BuildContext context, isSingleton = false}) async {
|
||||
Future<bool> addOfflineTrack(Track track,
|
||||
{private = true, BuildContext context, isSingleton = false}) async {
|
||||
//Permission
|
||||
if (!private && !(await checkPermission())) return false;
|
||||
|
||||
|
@ -168,8 +168,9 @@ class DownloadManager {
|
|||
}
|
||||
|
||||
//Fetch track if missing meta
|
||||
if (track.artists == null || track.artists.length == 0 || track.album == null)
|
||||
track = await deezerAPI.track(track.id);
|
||||
if (track.artists == null ||
|
||||
track.artists.length == 0 ||
|
||||
track.album == null) track = await deezerAPI.track(track.id);
|
||||
|
||||
//Add to DB
|
||||
if (private) {
|
||||
|
@ -184,12 +185,16 @@ class DownloadManager {
|
|||
|
||||
//Get path
|
||||
String path = _generatePath(track, private, isSingleton: isSingleton);
|
||||
await platform.invokeMethod('addDownloads', [await Download.jsonFromTrack(track, path, private: private, quality: quality)]);
|
||||
await platform.invokeMethod('addDownloads', [
|
||||
await Download.jsonFromTrack(track, path,
|
||||
private: private, quality: quality)
|
||||
]);
|
||||
await start();
|
||||
return true;
|
||||
}
|
||||
|
||||
Future addOfflineAlbum(Album album, {private = true, BuildContext context}) async {
|
||||
Future addOfflineAlbum(Album album,
|
||||
{private = true, BuildContext context}) async {
|
||||
//Permission
|
||||
if (!private && !(await checkPermission())) return;
|
||||
|
||||
|
@ -212,7 +217,8 @@ class DownloadManager {
|
|||
DefaultCacheManager().getSingleFile(album.art.full);
|
||||
|
||||
Batch b = db.batch();
|
||||
b.insert('Albums', album.toSQL(off: true), conflictAlgorithm: ConflictAlgorithm.replace);
|
||||
b.insert('Albums', album.toSQL(off: true),
|
||||
conflictAlgorithm: ConflictAlgorithm.replace);
|
||||
for (Track t in album.tracks) {
|
||||
b = await _addTrackToDB(b, t, false);
|
||||
}
|
||||
|
@ -222,31 +228,37 @@ class DownloadManager {
|
|||
//Create downloads
|
||||
List<Map> out = [];
|
||||
for (Track t in album.tracks) {
|
||||
out.add(await Download.jsonFromTrack(t, _generatePath(t, private), private: private, quality: quality));
|
||||
out.add(await Download.jsonFromTrack(t, _generatePath(t, private),
|
||||
private: private, quality: quality));
|
||||
}
|
||||
await platform.invokeMethod('addDownloads', out);
|
||||
await start();
|
||||
}
|
||||
|
||||
Future addOfflinePlaylist(Playlist playlist, {private = true, BuildContext context, AudioQuality quality}) async {
|
||||
Future addOfflinePlaylist(Playlist playlist,
|
||||
{private = true, BuildContext context, AudioQuality quality}) async {
|
||||
//Permission
|
||||
if (!private && !(await checkPermission())) return;
|
||||
|
||||
//Ask for quality
|
||||
if (!private && settings.downloadQuality == AudioQuality.ASK && quality == null) {
|
||||
if (!private &&
|
||||
settings.downloadQuality == AudioQuality.ASK &&
|
||||
quality == null) {
|
||||
quality = await qualitySelect(context);
|
||||
if (quality == null) return false;
|
||||
}
|
||||
|
||||
//Get tracks if missing
|
||||
if (playlist.tracks == null || playlist.tracks.length < playlist.trackCount) {
|
||||
if (playlist.tracks == null ||
|
||||
playlist.tracks.length < playlist.trackCount) {
|
||||
playlist = await deezerAPI.fullPlaylist(playlist.id);
|
||||
}
|
||||
|
||||
//Add to DB
|
||||
if (private) {
|
||||
Batch b = db.batch();
|
||||
b.insert('Playlists', playlist.toSQL(), conflictAlgorithm: ConflictAlgorithm.replace);
|
||||
b.insert('Playlists', playlist.toSQL(),
|
||||
conflictAlgorithm: ConflictAlgorithm.replace);
|
||||
for (Track t in playlist.tracks) {
|
||||
b = await _addTrackToDB(b, t, false);
|
||||
//Cache art
|
||||
|
@ -258,30 +270,35 @@ class DownloadManager {
|
|||
|
||||
//Generate downloads
|
||||
List<Map> out = [];
|
||||
for (int i=0; i<playlist.tracks.length; i++) {
|
||||
for (int i = 0; i < playlist.tracks.length; i++) {
|
||||
Track t = playlist.tracks[i];
|
||||
out.add(await Download.jsonFromTrack(t, _generatePath(
|
||||
out.add(await Download.jsonFromTrack(
|
||||
t,
|
||||
_generatePath(
|
||||
t,
|
||||
private,
|
||||
playlistName: playlist.title,
|
||||
playlistTrackNumber: i,
|
||||
), private: private, quality: quality));
|
||||
),
|
||||
private: private,
|
||||
quality: quality));
|
||||
}
|
||||
await platform.invokeMethod('addDownloads', out);
|
||||
await start();
|
||||
}
|
||||
|
||||
//Get track and meta from offline DB
|
||||
Future<Track> getOfflineTrack(String id, {Album album, List<Artist> artists}) async {
|
||||
Future<Track> getOfflineTrack(String id,
|
||||
{Album album, List<Artist> artists}) async {
|
||||
List tracks = await db.query('Tracks', where: 'id == ?', whereArgs: [id]);
|
||||
if (tracks.length == 0) return null;
|
||||
Track track = Track.fromSQL(tracks[0]);
|
||||
|
||||
//Get album
|
||||
if (album == null) {
|
||||
List rawAlbums = await db.query('Albums', where: 'id == ?', whereArgs: [track.album.id]);
|
||||
if (rawAlbums.length > 0)
|
||||
track.album = Album.fromSQL(rawAlbums[0]);
|
||||
List rawAlbums = await db
|
||||
.query('Albums', where: 'id == ?', whereArgs: [track.album.id]);
|
||||
if (rawAlbums.length > 0) track.album = Album.fromSQL(rawAlbums[0]);
|
||||
} else {
|
||||
track.album = album;
|
||||
}
|
||||
|
@ -290,12 +307,11 @@ class DownloadManager {
|
|||
if (artists == null) {
|
||||
List<Artist> newArtists = [];
|
||||
for (Artist artist in track.artists) {
|
||||
List rawArtist = await db.query('Artists', where: 'id == ?', whereArgs: [artist.id]);
|
||||
if (rawArtist.length > 0)
|
||||
newArtists.add(Artist.fromSQL(rawArtist[0]));
|
||||
List rawArtist =
|
||||
await db.query('Artists', where: 'id == ?', whereArgs: [artist.id]);
|
||||
if (rawArtist.length > 0) newArtists.add(Artist.fromSQL(rawArtist[0]));
|
||||
}
|
||||
if (newArtists.length > 0)
|
||||
track.artists = newArtists;
|
||||
if (newArtists.length > 0) track.artists = newArtists;
|
||||
} else {
|
||||
track.artists = artists;
|
||||
}
|
||||
|
@ -304,7 +320,8 @@ class DownloadManager {
|
|||
|
||||
//Get offline library tracks
|
||||
Future<List<Track>> getOfflineTracks() async {
|
||||
List rawTracks = await db.query('Tracks', where: 'library == 1 AND offline == 1', columns: ['id']);
|
||||
List rawTracks = await db.query('Tracks',
|
||||
where: 'library == 1 AND offline == 1', columns: ['id']);
|
||||
List<Track> out = [];
|
||||
//Load track meta individually
|
||||
for (Map rawTrack in rawTracks) {
|
||||
|
@ -315,7 +332,8 @@ class DownloadManager {
|
|||
|
||||
//Get all offline available tracks
|
||||
Future<List<Track>> allOfflineTracks() async {
|
||||
List rawTracks = await db.query('Tracks', where: 'offline == 1', columns: ['id']);
|
||||
List rawTracks =
|
||||
await db.query('Tracks', where: 'offline == 1', columns: ['id']);
|
||||
List<Track> out = [];
|
||||
//Load track meta individually
|
||||
for (Map rawTrack in rawTracks) {
|
||||
|
@ -326,7 +344,8 @@ class DownloadManager {
|
|||
|
||||
//Get all offline albums
|
||||
Future<List<Album>> getOfflineAlbums() async {
|
||||
List rawAlbums = await db.query('Albums', where: 'offline == 1', columns: ['id']);
|
||||
List rawAlbums =
|
||||
await db.query('Albums', where: 'offline == 1', columns: ['id']);
|
||||
List<Album> out = [];
|
||||
//Load each album
|
||||
for (Map rawAlbum in rawAlbums) {
|
||||
|
@ -337,20 +356,22 @@ class DownloadManager {
|
|||
|
||||
//Get offline album with meta
|
||||
Future<Album> getOfflineAlbum(String id) async {
|
||||
List rawAlbums = await db.query('Albums', where: 'id == ?', whereArgs: [id]);
|
||||
List rawAlbums =
|
||||
await db.query('Albums', where: 'id == ?', whereArgs: [id]);
|
||||
if (rawAlbums.length == 0) return null;
|
||||
Album album = Album.fromSQL(rawAlbums[0]);
|
||||
|
||||
List<Track> tracks = [];
|
||||
//Load tracks
|
||||
for (int i=0; i<album.tracks.length; i++) {
|
||||
for (int i = 0; i < album.tracks.length; i++) {
|
||||
tracks.add(await getOfflineTrack(album.tracks[i].id, album: album));
|
||||
}
|
||||
album.tracks = tracks;
|
||||
//Load artists
|
||||
List<Artist> artists = [];
|
||||
for (int i=0; i<album.artists.length; i++) {
|
||||
artists.add((await getOfflineArtist(album.artists[i].id))??album.artists[i]);
|
||||
for (int i = 0; i < album.artists.length; i++) {
|
||||
artists.add(
|
||||
(await getOfflineArtist(album.artists[i].id)) ?? album.artists[i]);
|
||||
}
|
||||
album.artists = artists;
|
||||
|
||||
|
@ -359,7 +380,8 @@ class DownloadManager {
|
|||
|
||||
//Get offline artist METADATA, not tracks
|
||||
Future<Artist> getOfflineArtist(String id) async {
|
||||
List rawArtists = await db.query("Artists", where: 'id == ?', whereArgs: [id]);
|
||||
List rawArtists =
|
||||
await db.query("Artists", where: 'id == ?', whereArgs: [id]);
|
||||
if (rawArtists.length == 0) return null;
|
||||
return Artist.fromSQL(rawArtists[0]);
|
||||
}
|
||||
|
@ -376,7 +398,8 @@ class DownloadManager {
|
|||
|
||||
//Get offline playlist
|
||||
Future<Playlist> getPlaylist(String id) async {
|
||||
List rawPlaylists = await db.query('Playlists', where: 'id == ?', whereArgs: [id]);
|
||||
List rawPlaylists =
|
||||
await db.query('Playlists', where: 'id == ?', whereArgs: [id]);
|
||||
if (rawPlaylists.length == 0) return null;
|
||||
Playlist playlist = Playlist.fromSQL(rawPlaylists[0]);
|
||||
//Load tracks
|
||||
|
@ -391,16 +414,21 @@ class DownloadManager {
|
|||
Future removeOfflineTracks(List<Track> tracks) async {
|
||||
for (Track t in tracks) {
|
||||
//Check if library
|
||||
List rawTrack = await db.query('Tracks', where: 'id == ?', whereArgs: [t.id], columns: ['favorite']);
|
||||
List rawTrack = await db.query('Tracks',
|
||||
where: 'id == ?', whereArgs: [t.id], columns: ['favorite']);
|
||||
if (rawTrack.length > 0) {
|
||||
//Count occurrences in playlists and albums
|
||||
List albums = await db.rawQuery('SELECT (id) FROM Albums WHERE tracks LIKE "%${t.id}%"');
|
||||
List playlists = await db.rawQuery('SELECT (id) FROM Playlists WHERE tracks LIKE "%${t.id}%"');
|
||||
if (albums.length + playlists.length == 0 && rawTrack[0]['favorite'] == 0) {
|
||||
List albums = await db
|
||||
.rawQuery('SELECT (id) FROM Albums WHERE tracks LIKE "%${t.id}%"');
|
||||
List playlists = await db.rawQuery(
|
||||
'SELECT (id) FROM Playlists WHERE tracks LIKE "%${t.id}%"');
|
||||
if (albums.length + playlists.length == 0 &&
|
||||
rawTrack[0]['favorite'] == 0) {
|
||||
//Safe to remove
|
||||
await db.delete('Tracks', where: 'id == ?', whereArgs: [t.id]);
|
||||
} else {
|
||||
await db.update('Tracks', {'offline': 0}, where: 'id == ?', whereArgs: [t.id]);
|
||||
await db.update('Tracks', {'offline': 0},
|
||||
where: 'id == ?', whereArgs: [t.id]);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -415,7 +443,8 @@ class DownloadManager {
|
|||
|
||||
Future removeOfflineAlbum(String id) async {
|
||||
//Get album
|
||||
List rawAlbums = await db.query('Albums', where: 'id == ?', whereArgs: [id]);
|
||||
List rawAlbums =
|
||||
await db.query('Albums', where: 'id == ?', whereArgs: [id]);
|
||||
if (rawAlbums.length == 0) return;
|
||||
Album album = Album.fromSQL(rawAlbums[0]);
|
||||
//Remove album
|
||||
|
@ -426,7 +455,8 @@ class DownloadManager {
|
|||
|
||||
Future removeOfflinePlaylist(String id) async {
|
||||
//Fetch playlist
|
||||
List rawPlaylists = await db.query('Playlists', where: 'id == ?', whereArgs: [id]);
|
||||
List rawPlaylists =
|
||||
await db.query('Playlists', where: 'id == ?', whereArgs: [id]);
|
||||
if (rawPlaylists.length == 0) return;
|
||||
Playlist playlist = Playlist.fromSQL(rawPlaylists[0]);
|
||||
//Remove playlist
|
||||
|
@ -435,22 +465,26 @@ class DownloadManager {
|
|||
}
|
||||
|
||||
//Check if album, track or playlist is offline
|
||||
Future<bool> checkOffline({Album album, Track track, Playlist playlist}) async {
|
||||
Future<bool> checkOffline(
|
||||
{Album album, Track track, Playlist playlist}) async {
|
||||
//Track
|
||||
if (track != null) {
|
||||
List res = await db.query('Tracks', where: 'id == ? AND offline == 1', whereArgs: [track.id]);
|
||||
List res = await db.query('Tracks',
|
||||
where: 'id == ? AND offline == 1', whereArgs: [track.id]);
|
||||
if (res.length == 0) return false;
|
||||
return true;
|
||||
}
|
||||
//Album
|
||||
if (album != null) {
|
||||
List res = await db.query('Albums', where: 'id == ? AND offline == 1', whereArgs: [album.id]);
|
||||
List res = await db.query('Albums',
|
||||
where: 'id == ? AND offline == 1', whereArgs: [album.id]);
|
||||
if (res.length == 0) return false;
|
||||
return true;
|
||||
}
|
||||
//Playlist
|
||||
if (playlist != null && playlist.id != null) {
|
||||
List res = await db.query('Playlists', where: 'id == ?', whereArgs: [playlist.id]);
|
||||
List res = await db
|
||||
.query('Playlists', where: 'id == ?', whereArgs: [playlist.id]);
|
||||
if (res.length == 0) return false;
|
||||
return true;
|
||||
}
|
||||
|
@ -459,19 +493,23 @@ class DownloadManager {
|
|||
|
||||
//Offline search
|
||||
Future<SearchResults> search(String query) async {
|
||||
SearchResults results = SearchResults(tracks: [], albums: [], artists: [], playlists: []);
|
||||
SearchResults results =
|
||||
SearchResults(tracks: [], albums: [], artists: [], playlists: []);
|
||||
//Tracks
|
||||
List tracksData = await db.rawQuery('SELECT * FROM Tracks WHERE offline == 1 AND title like "%$query%"');
|
||||
List tracksData = await db.rawQuery(
|
||||
'SELECT * FROM Tracks WHERE offline == 1 AND title like "%$query%"');
|
||||
for (Map trackData in tracksData) {
|
||||
results.tracks.add(await getOfflineTrack(trackData['id']));
|
||||
}
|
||||
//Albums
|
||||
List albumsData = await db.rawQuery('SELECT (id) FROM Albums WHERE offline == 1 AND title like "%$query%"');
|
||||
List albumsData = await db.rawQuery(
|
||||
'SELECT (id) FROM Albums WHERE offline == 1 AND title like "%$query%"');
|
||||
for (Map rawAlbum in albumsData) {
|
||||
results.albums.add(await getOfflineAlbum(rawAlbum['id']));
|
||||
}
|
||||
//Playlists
|
||||
List playlists = await db.rawQuery('SELECT * FROM Playlists WHERE title like "%$query%"');
|
||||
List playlists = await db
|
||||
.rawQuery('SELECT * FROM Playlists WHERE title like "%$query%"');
|
||||
for (Map playlist in playlists) {
|
||||
results.playlists.add(await getPlaylist(playlist['id']));
|
||||
}
|
||||
|
@ -485,7 +523,10 @@ class DownloadManager {
|
|||
}
|
||||
|
||||
//Generate track download path
|
||||
String _generatePath(Track track, bool private, {String playlistName, int playlistTrackNumber, bool isSingleton = false}) {
|
||||
String _generatePath(Track track, bool private,
|
||||
{String playlistName,
|
||||
int playlistTrackNumber,
|
||||
bool isSingleton = false}) {
|
||||
String path;
|
||||
if (private) {
|
||||
path = p.join(offlinePath, track.id);
|
||||
|
@ -496,23 +537,26 @@ class DownloadManager {
|
|||
if (settings.playlistFolder && playlistName != null)
|
||||
path = p.join(path, sanitize(playlistName));
|
||||
|
||||
if (settings.artistFolder)
|
||||
path = p.join(path, '%albumArtist%');
|
||||
if (settings.artistFolder) path = p.join(path, '%albumArtist%');
|
||||
|
||||
//Album folder / with disk number
|
||||
if (settings.albumFolder) {
|
||||
if (settings.albumDiscFolder) {
|
||||
path = p.join(path, '%album%' + ' - Disk ' + (track.diskNumber??1).toString());
|
||||
path = p.join(path,
|
||||
'%album%' + ' - Disk ' + (track.diskNumber ?? 1).toString());
|
||||
} else {
|
||||
path = p.join(path, '%album%');
|
||||
}
|
||||
}
|
||||
//Final path
|
||||
path = p.join(path, isSingleton ? settings.singletonFilename : settings.downloadFilename);
|
||||
path = p.join(path,
|
||||
isSingleton ? settings.singletonFilename : settings.downloadFilename);
|
||||
//Playlist track number variable (not accessible in service)
|
||||
if (playlistTrackNumber != null) {
|
||||
path = path.replaceAll('%playlistTrackNumber%', playlistTrackNumber.toString());
|
||||
path = path.replaceAll('%0playlistTrackNumber%', playlistTrackNumber.toString().padLeft(2, '0'));
|
||||
path = path.replaceAll(
|
||||
'%playlistTrackNumber%', playlistTrackNumber.toString());
|
||||
path = path.replaceAll('%0playlistTrackNumber%',
|
||||
playlistTrackNumber.toString().padLeft(2, '0'));
|
||||
} else {
|
||||
path = path.replaceAll('%playlistTrackNumber%', '');
|
||||
path = path.replaceAll('%0playlistTrackNumber%', '');
|
||||
|
@ -524,13 +568,19 @@ class DownloadManager {
|
|||
//Get stats for library screen
|
||||
Future<List<String>> getStats() async {
|
||||
//Get offline counts
|
||||
int trackCount = (await db.rawQuery('SELECT COUNT(*) FROM Tracks WHERE offline == 1'))[0]['COUNT(*)'];
|
||||
int albumCount = (await db.rawQuery('SELECT COUNT(*) FROM Albums WHERE offline == 1'))[0]['COUNT(*)'];
|
||||
int playlistCount = (await db.rawQuery('SELECT COUNT(*) FROM Playlists'))[0]['COUNT(*)'];
|
||||
int trackCount =
|
||||
(await db.rawQuery('SELECT COUNT(*) FROM Tracks WHERE offline == 1'))[0]
|
||||
['COUNT(*)'];
|
||||
int albumCount =
|
||||
(await db.rawQuery('SELECT COUNT(*) FROM Albums WHERE offline == 1'))[0]
|
||||
['COUNT(*)'];
|
||||
int playlistCount =
|
||||
(await db.rawQuery('SELECT COUNT(*) FROM Playlists'))[0]['COUNT(*)'];
|
||||
//Free space
|
||||
double diskSpace = await DiskSpace.getFreeDiskSpace;
|
||||
//Used space
|
||||
List<FileSystemEntity> offlineStat = await Directory(offlinePath).list().toList();
|
||||
List<FileSystemEntity> offlineStat =
|
||||
await Directory(offlinePath).list().toList();
|
||||
int offlineSize = 0;
|
||||
for (var fs in offlineStat) {
|
||||
offlineSize += (await fs.stat()).size;
|
||||
|
@ -547,7 +597,8 @@ class DownloadManager {
|
|||
|
||||
//Send settings to download service
|
||||
Future updateServiceSettings() async {
|
||||
await platform.invokeMethod('updateSettings', settings.getServiceSettings());
|
||||
await platform.invokeMethod(
|
||||
'updateSettings', settings.getServiceSettings());
|
||||
}
|
||||
|
||||
//Check storage permission
|
||||
|
@ -558,8 +609,7 @@ class DownloadManager {
|
|||
Fluttertoast.showToast(
|
||||
msg: 'Storage permission denied!'.i18n,
|
||||
toastLength: Toast.LENGTH_SHORT,
|
||||
gravity: ToastGravity.BOTTOM
|
||||
);
|
||||
gravity: ToastGravity.BOTTOM);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -576,9 +626,12 @@ class DownloadManager {
|
|||
|
||||
//Delete downloads by state
|
||||
Future removeDownloads(DownloadState state) async {
|
||||
await platform.invokeMethod('removeDownloads', {'state': DownloadState.values.indexOf(state)});
|
||||
await platform.invokeMethod(
|
||||
'removeDownloads', {'state': DownloadState.values.indexOf(state)});
|
||||
}
|
||||
|
||||
static Future<String> getDirectory(String title) =>
|
||||
platform.invokeMethod('getDirectory', <String, String>{'title': title});
|
||||
}
|
||||
|
||||
class Download {
|
||||
|
@ -596,12 +649,24 @@ class Download {
|
|||
int received;
|
||||
int filesize;
|
||||
|
||||
Download({this.id, this.path, this.private, this.trackId, this.md5origin, this.mediaVersion,
|
||||
this.title, this.image, this.state, this.received, this.filesize, this.quality});
|
||||
Download(
|
||||
{this.id,
|
||||
this.path,
|
||||
this.private,
|
||||
this.trackId,
|
||||
this.md5origin,
|
||||
this.mediaVersion,
|
||||
this.title,
|
||||
this.image,
|
||||
this.state,
|
||||
this.received,
|
||||
this.filesize,
|
||||
this.quality});
|
||||
|
||||
//Get progress between 0 - 1
|
||||
double get progress {
|
||||
return ((received.toDouble()??0.0)/(filesize.toDouble()??1.0)).toDouble();
|
||||
return ((received.toDouble() ?? 0.0) / (filesize.toDouble() ?? 1.0))
|
||||
.toDouble();
|
||||
}
|
||||
|
||||
factory Download.fromJson(Map<dynamic, dynamic> data) {
|
||||
|
@ -613,21 +678,22 @@ class Download {
|
|||
id: data['id'],
|
||||
state: DownloadState.values[data['state']],
|
||||
title: data['title'],
|
||||
quality: data['quality']
|
||||
);
|
||||
quality: data['quality']);
|
||||
}
|
||||
|
||||
//Change values from "update json"
|
||||
void updateFromJson(Map<dynamic, dynamic> data) {
|
||||
this.quality = data['quality'];
|
||||
this.received = data['received']??0;
|
||||
this.received = data['received'] ?? 0;
|
||||
this.state = DownloadState.values[data['state']];
|
||||
//Prevent null division later
|
||||
this.filesize = ((data['filesize']??0) <= 0) ? 1 : (data['filesize']??1);
|
||||
this.filesize =
|
||||
((data['filesize'] ?? 0) <= 0) ? 1 : (data['filesize'] ?? 1);
|
||||
}
|
||||
|
||||
//Track to download JSON for service
|
||||
static Future<Map> jsonFromTrack(Track t, String path, {private = true, AudioQuality quality}) async {
|
||||
static Future<Map> jsonFromTrack(Track t, String path,
|
||||
{private = true, AudioQuality quality}) async {
|
||||
//Get download info
|
||||
if (t.playbackDetails == null || t.playbackDetails == []) {
|
||||
t = await deezerAPI.track(t.id);
|
||||
|
@ -639,7 +705,7 @@ class Download {
|
|||
"mediaVersion": t.playbackDetails[1],
|
||||
"quality": private
|
||||
? settings.getQualityInt(settings.offlineQuality)
|
||||
: settings.getQualityInt((quality??settings.downloadQuality)),
|
||||
: settings.getQualityInt((quality ?? settings.downloadQuality)),
|
||||
"title": t.title,
|
||||
"path": path,
|
||||
"image": t.albumArt.thumb
|
||||
|
@ -648,11 +714,4 @@ class Download {
|
|||
}
|
||||
|
||||
//Has to be same order as in java
|
||||
enum DownloadState {
|
||||
NONE,
|
||||
DOWNLOADING,
|
||||
POST,
|
||||
DONE,
|
||||
DEEZER_ERROR,
|
||||
ERROR
|
||||
}
|
||||
enum DownloadState { NONE, DOWNLOADING, POST, DONE, DEEZER_ERROR, ERROR }
|
||||
|
|
|
@ -11,7 +11,6 @@ import 'package:path/path.dart' as p;
|
|||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:freezer/translations.i18n.dart';
|
||||
import 'package:scrobblenaut/scrobblenaut.dart';
|
||||
import 'package:extended_math/extended_math.dart';
|
||||
|
||||
import 'definitions.dart';
|
||||
import '../settings.dart';
|
||||
|
@ -339,9 +338,9 @@ class PlayerHelper {
|
|||
}
|
||||
|
||||
//Start visualizer
|
||||
Future startVisualizer() async {
|
||||
await AudioService.customAction('startVisualizer');
|
||||
}
|
||||
// Future startVisualizer() async {
|
||||
// await AudioService.customAction('startVisualizer');
|
||||
// }
|
||||
|
||||
//Stop visualizer
|
||||
Future stopVisualizer() async {
|
||||
|
@ -769,33 +768,32 @@ class AudioPlayerTask extends BackgroundAudioTask {
|
|||
this._queueIndex = args;
|
||||
break;
|
||||
//Start visualizer
|
||||
case 'startVisualizer':
|
||||
if (_visualizerSubscription != null) break;
|
||||
|
||||
_player.startVisualizer(
|
||||
enableWaveform: false,
|
||||
enableFft: true,
|
||||
captureRate: 15000,
|
||||
captureSize: 128);
|
||||
_visualizerSubscription = _player.visualizerFftStream.listen((event) {
|
||||
//Calculate actual values
|
||||
List<double> out = [];
|
||||
for (int i = 0; i < event.length / 2; i++) {
|
||||
int rfk = event[i * 2].toSigned(8);
|
||||
int ifk = event[i * 2 + 1].toSigned(8);
|
||||
out.add(log(hypot(rfk, ifk) + 1) / 5.2);
|
||||
}
|
||||
AudioServiceBackground.sendCustomEvent(
|
||||
{"action": "visualizer", "data": out});
|
||||
});
|
||||
break;
|
||||
//Stop visualizer
|
||||
case 'stopVisualizer':
|
||||
if (_visualizerSubscription != null) {
|
||||
_visualizerSubscription.cancel();
|
||||
_visualizerSubscription = null;
|
||||
}
|
||||
break;
|
||||
// case 'startVisualizer':
|
||||
// if (_visualizerSubscription != null) break;
|
||||
// _player.startVisualizer(
|
||||
// enableWaveform: false,
|
||||
// enableFft: true,
|
||||
// captureRate: 15000,
|
||||
// captureSize: 128);
|
||||
// _visualizerSubscription = _player.visualizerFftStream.listen((event) {
|
||||
// //Calculate actual values
|
||||
// List<double> out = [];
|
||||
// for (int i = 0; i < event.length / 2; i++) {
|
||||
// int rfk = event[i * 2].toSigned(8);
|
||||
// int ifk = event[i * 2 + 1].toSigned(8);
|
||||
// out.add(log(hypot(rfk, ifk) + 1) / 5.2);
|
||||
// }
|
||||
// AudioServiceBackground.sendCustomEvent(
|
||||
// {"action": "visualizer", "data": out});
|
||||
// });
|
||||
// break;
|
||||
// //Stop visualizer
|
||||
// case 'stopVisualizer':
|
||||
// if (_visualizerSubscription != null) {
|
||||
// _visualizerSubscription.cancel();
|
||||
// _visualizerSubscription = null;
|
||||
// }
|
||||
// break;
|
||||
//Authorize lastfm
|
||||
case 'authorizeLastFM':
|
||||
String username = args[0];
|
||||
|
|
|
@ -11,23 +11,26 @@ import 'dart:io';
|
|||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class SpotifyScrapper {
|
||||
|
||||
//Parse spotify URL to URI (spotify:track:1234)
|
||||
static String parseUrl(String url) {
|
||||
Uri uri = Uri.parse(url);
|
||||
if (uri.pathSegments.length > 3) return null; //Invalid URL
|
||||
if (uri.pathSegments.length == 3) return 'spotify:${uri.pathSegments[1]}:${uri.pathSegments[2]}';
|
||||
if (uri.pathSegments.length == 2) return 'spotify:${uri.pathSegments[0]}:${uri.pathSegments[1]}';
|
||||
if (uri.pathSegments.length == 3)
|
||||
return 'spotify:${uri.pathSegments[1]}:${uri.pathSegments[2]}';
|
||||
if (uri.pathSegments.length == 2)
|
||||
return 'spotify:${uri.pathSegments[0]}:${uri.pathSegments[1]}';
|
||||
return null;
|
||||
}
|
||||
|
||||
//Get spotify embed url from uri
|
||||
static String getEmbedUrl(String uri) => 'https://embed.spotify.com/?uri=$uri';
|
||||
static String getEmbedUrl(String uri) =>
|
||||
'https://embed.spotify.com/?uri=$uri';
|
||||
|
||||
//https://link.tospotify.com/ or https://spotify.app.link/
|
||||
static Future resolveLinkUrl(String url) async {
|
||||
http.Response response = await http.get(Uri.parse(url));
|
||||
Match match = RegExp(r'window\.top\.location = validate\("(.+)"\);').firstMatch(response.body);
|
||||
Match match = RegExp(r'window\.top\.location = validate\("(.+)"\);')
|
||||
.firstMatch(response.body);
|
||||
return match.group(1);
|
||||
}
|
||||
|
||||
|
@ -41,7 +44,7 @@ class SpotifyScrapper {
|
|||
//Extract JSON data form spotify embed page
|
||||
static Future<Map> getEmbedData(String url) async {
|
||||
//Fetch
|
||||
http.Response response = await http.get(url);
|
||||
http.Response response = await http.get(Uri.parse(url));
|
||||
//Parse
|
||||
dom.Document document = parse(response.body);
|
||||
dom.Element element = document.getElementById('resource');
|
||||
|
@ -78,7 +81,6 @@ class SpotifyScrapper {
|
|||
Map deezer = await deezerAPI.callPublicApi('album/upc:' + album.upc);
|
||||
return deezer['id'].toString();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class SpotifyTrack {
|
||||
|
@ -91,15 +93,14 @@ class SpotifyTrack {
|
|||
//JSON
|
||||
factory SpotifyTrack.fromJson(Map json) => SpotifyTrack(
|
||||
title: json['name'],
|
||||
artists: json['artists'].map<String>((a) => a["name"].toString()).toList(),
|
||||
isrc: json['external_ids']['isrc']
|
||||
);
|
||||
artists:
|
||||
json['artists'].map<String>((a) => a["name"].toString()).toList(),
|
||||
isrc: json['external_ids']['isrc']);
|
||||
|
||||
//Convert track to importer track
|
||||
ImporterTrack toImporter() {
|
||||
return ImporterTrack(title, artists, isrc: isrc);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class SpotifyPlaylist {
|
||||
|
@ -115,8 +116,9 @@ class SpotifyPlaylist {
|
|||
name: json['name'],
|
||||
description: json['description'],
|
||||
image: (json['images'].length > 0) ? json['images'][0]['url'] : null,
|
||||
tracks: json['tracks']['items'].map<SpotifyTrack>((j) => SpotifyTrack.fromJson(j['track'])).toList()
|
||||
);
|
||||
tracks: json['tracks']['items']
|
||||
.map<SpotifyTrack>((j) => SpotifyTrack.fromJson(j['track']))
|
||||
.toList());
|
||||
|
||||
//Convert to importer tracks
|
||||
List<ImporterTrack> toImporter() {
|
||||
|
@ -130,14 +132,11 @@ class SpotifyAlbum {
|
|||
SpotifyAlbum({this.upc});
|
||||
|
||||
//JSON
|
||||
factory SpotifyAlbum.fromJson(Map json) => SpotifyAlbum(
|
||||
upc: json['external_ids']['upc']
|
||||
);
|
||||
factory SpotifyAlbum.fromJson(Map json) =>
|
||||
SpotifyAlbum(upc: json['external_ids']['upc']);
|
||||
}
|
||||
|
||||
|
||||
class SpotifyAPIWrapper {
|
||||
|
||||
HttpServer _server;
|
||||
SpotifyApi spotify;
|
||||
User me;
|
||||
|
@ -145,15 +144,15 @@ class SpotifyAPIWrapper {
|
|||
//Try authorize with saved credentials
|
||||
Future<bool> trySaved() async {
|
||||
print(settings.spotifyCredentials);
|
||||
if (settings.spotifyClientId == null || settings.spotifyClientSecret == null || settings.spotifyCredentials == null) return false;
|
||||
if (settings.spotifyClientId == null ||
|
||||
settings.spotifyClientSecret == null ||
|
||||
settings.spotifyCredentials == null) return false;
|
||||
final credentials = SpotifyApiCredentials(
|
||||
settings.spotifyClientId,
|
||||
settings.spotifyClientSecret,
|
||||
settings.spotifyClientId, settings.spotifyClientSecret,
|
||||
accessToken: settings.spotifyCredentials.accessToken,
|
||||
refreshToken: settings.spotifyCredentials.refreshToken,
|
||||
scopes: settings.spotifyCredentials.scopes,
|
||||
expiration: settings.spotifyCredentials.expiration
|
||||
);
|
||||
expiration: settings.spotifyCredentials.expiration);
|
||||
spotify = SpotifyApi(credentials);
|
||||
me = await spotify.me.get();
|
||||
await _save();
|
||||
|
@ -162,7 +161,8 @@ class SpotifyAPIWrapper {
|
|||
|
||||
Future authorize(String clientId, String clientSecret) async {
|
||||
//Spotify
|
||||
SpotifyApiCredentials credentials = SpotifyApiCredentials(clientId, clientSecret);
|
||||
SpotifyApiCredentials credentials =
|
||||
SpotifyApiCredentials(clientId, clientSecret);
|
||||
spotify = SpotifyApi(credentials);
|
||||
//Create server
|
||||
_server = await HttpServer.bind(InternetAddress.loopbackIPv4, 42069);
|
||||
|
@ -170,14 +170,21 @@ class SpotifyAPIWrapper {
|
|||
//Get URL
|
||||
final grant = SpotifyApi.authorizationCodeGrant(credentials);
|
||||
final redirectUri = "http://localhost:42069";
|
||||
final scopes = ['user-read-private', 'playlist-read-private', 'playlist-read-collaborative', 'user-library-read'];
|
||||
final authUri = grant.getAuthorizationUrl(Uri.parse(redirectUri), scopes: scopes);
|
||||
final scopes = [
|
||||
'user-read-private',
|
||||
'playlist-read-private',
|
||||
'playlist-read-collaborative',
|
||||
'user-library-read'
|
||||
];
|
||||
final authUri =
|
||||
grant.getAuthorizationUrl(Uri.parse(redirectUri), scopes: scopes);
|
||||
launch(authUri.toString());
|
||||
//Wait for code
|
||||
await for (HttpRequest request in _server) {
|
||||
//Exit window
|
||||
request.response.headers.set("Content-Type", "text/html; charset=UTF-8");
|
||||
request.response.write("<body><h1>You can close this page and go back to Freezer.</h1></body><script>window.close();</script>");
|
||||
request.response.write(
|
||||
"<body><h1>You can close this page and go back to Freezer.</h1></body><script>window.close();</script>");
|
||||
request.response.close();
|
||||
//Get token
|
||||
if (request.uri.queryParameters["code"] != null) {
|
||||
|
@ -202,8 +209,7 @@ class SpotifyAPIWrapper {
|
|||
accessToken: spotifyCredentials.accessToken,
|
||||
refreshToken: spotifyCredentials.refreshToken,
|
||||
scopes: spotifyCredentials.scopes,
|
||||
expiration: spotifyCredentials.expiration
|
||||
);
|
||||
expiration: spotifyCredentials.expiration);
|
||||
settings.spotifyClientSecret = spotifyCredentials.clientId;
|
||||
settings.spotifyClientSecret = spotifyCredentials.clientSecret;
|
||||
settings.spotifyCredentials = saveCredentials;
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:custom_navigator/custom_navigator.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
@ -32,6 +31,8 @@ Function logOut;
|
|||
GlobalKey<NavigatorState> mainNavigatorKey = GlobalKey<NavigatorState>();
|
||||
GlobalKey<NavigatorState> navigatorKey;
|
||||
|
||||
// TODO: migrate to null-safety
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
|
@ -404,14 +405,52 @@ class _MainScreenState extends State<MainScreen>
|
|||
],
|
||||
)),
|
||||
body: AudioServiceWidget(
|
||||
child: CustomNavigator(
|
||||
child: _MainRouteNavigator(
|
||||
navigatorKey: navigatorKey,
|
||||
home: Focus(
|
||||
focusNode: screenFocusNode,
|
||||
skipTraversal: true,
|
||||
canRequestFocus: false,
|
||||
child: _screens[_selected]),
|
||||
pageRoute: PageRoutes.materialPageRoute),
|
||||
child: _screens[_selected])),
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
// hella simple reimplementation of custom_navigator, which is NOT null-safe
|
||||
class _MainRouteNavigator extends StatelessWidget with WidgetsBindingObserver {
|
||||
final Widget home;
|
||||
final GlobalKey<NavigatorState> navigatorKey;
|
||||
const _MainRouteNavigator({Key key, this.home, this.navigatorKey})
|
||||
: super(key: key);
|
||||
|
||||
// A system method that get invoked when user press back button on Android or back slide on iOS
|
||||
@override
|
||||
Future<bool> didPopRoute() async {
|
||||
final NavigatorState navigator = navigatorKey?.currentState;
|
||||
if (navigator == null) return false;
|
||||
return await navigator.maybePop();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> didPushRoute(String route) async {
|
||||
final NavigatorState navigator = navigatorKey?.currentState;
|
||||
if (navigator == null) return false;
|
||||
navigator.pushNamed(route);
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Navigator(
|
||||
initialRoute: Navigator.defaultRouteName,
|
||||
onGenerateRoute: _onGenerateRoute,
|
||||
);
|
||||
}
|
||||
|
||||
Route<dynamic> _onGenerateRoute(RouteSettings settings) {
|
||||
if (settings.name == Navigator.defaultRouteName) {
|
||||
return MaterialPageRoute(builder: (context) => home, settings: settings);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@ import 'package:freezer/ui/cached_image.dart';
|
|||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:ext_storage/ext_storage.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
|
@ -181,7 +180,8 @@ class Settings {
|
|||
_useArtColorSub =
|
||||
AudioService.currentMediaItemStream.listen((event) async {
|
||||
if (event == null || event.artUri == null) return;
|
||||
this.primaryColor = await imagesDatabase.getPrimaryColor(event.artUri);
|
||||
this.primaryColor =
|
||||
await imagesDatabase.getPrimaryColor(event.artUri.toString());
|
||||
updateTheme();
|
||||
});
|
||||
} else {
|
||||
|
@ -208,19 +208,20 @@ class Settings {
|
|||
}
|
||||
Settings s = Settings.fromJson({});
|
||||
//Set default path, because async
|
||||
s.downloadPath = (await ExtStorage.getExternalStoragePublicDirectory(
|
||||
ExtStorage.DIRECTORY_MUSIC));
|
||||
s.downloadPath =
|
||||
await getExternalStorageDirectories(type: StorageDirectory.music)
|
||||
.then((paths) => paths[0].path);
|
||||
s.save();
|
||||
return s;
|
||||
}
|
||||
|
||||
Future save() async {
|
||||
Future<void> save() async {
|
||||
File f = File(await getPath());
|
||||
await f.writeAsString(jsonEncode(this.toJson()));
|
||||
downloadManager.updateServiceSettings();
|
||||
}
|
||||
|
||||
Future updateAudioServiceQuality() async {
|
||||
Future<void> updateAudioServiceQuality() async {
|
||||
//Send wifi & mobile quality to audio service isolate
|
||||
await AudioService.customAction('updateQuality', {
|
||||
'mobileQuality': getQualityInt(mobileQuality),
|
||||
|
|
|
@ -5,7 +5,6 @@ import 'package:freezer/api/player.dart';
|
|||
import 'package:freezer/translations.i18n.dart';
|
||||
|
||||
class AndroidAuto {
|
||||
|
||||
//Prefix for "playable" MediaItem
|
||||
static const prefix = '_aa_';
|
||||
|
||||
|
@ -21,13 +20,14 @@ class AndroidAuto {
|
|||
//Fetch
|
||||
List<Playlist> playlists = await deezerAPI.getPlaylists();
|
||||
|
||||
List<MediaItem> out = playlists.map<MediaItem>((p) => MediaItem(
|
||||
List<MediaItem> out = playlists
|
||||
.map<MediaItem>((p) => MediaItem(
|
||||
id: '${prefix}playlist${p.id}',
|
||||
displayTitle: p.title,
|
||||
displaySubtitle: p.trackCount.toString() + ' ' + 'Tracks'.i18n,
|
||||
playable: true,
|
||||
artUri: p.image.thumb
|
||||
)).toList();
|
||||
artUri: Uri.parse(p.image.thumb)))
|
||||
.toList();
|
||||
return out;
|
||||
}
|
||||
|
||||
|
@ -35,13 +35,15 @@ class AndroidAuto {
|
|||
if (parentId == 'albums') {
|
||||
List<Album> albums = await deezerAPI.getAlbums();
|
||||
|
||||
List<MediaItem> out = albums.map<MediaItem>((a) => MediaItem(
|
||||
List<MediaItem> out = albums
|
||||
.map<MediaItem>((a) => MediaItem(
|
||||
id: '${prefix}album${a.id}',
|
||||
displayTitle: a.title,
|
||||
displaySubtitle: a.artistString,
|
||||
playable: true,
|
||||
artUri: a.art.thumb,
|
||||
)).toList();
|
||||
artUri: Uri.parse(a.art.thumb),
|
||||
))
|
||||
.toList();
|
||||
return out;
|
||||
}
|
||||
|
||||
|
@ -49,26 +51,29 @@ class AndroidAuto {
|
|||
if (parentId == 'artists') {
|
||||
List<Artist> artists = await deezerAPI.getArtists();
|
||||
|
||||
List<MediaItem> out = artists.map<MediaItem>((a) => MediaItem(
|
||||
List<MediaItem> out = artists
|
||||
.map<MediaItem>((a) => MediaItem(
|
||||
id: 'albums${a.id}',
|
||||
displayTitle: a.name,
|
||||
playable: false,
|
||||
artUri: a.picture.thumb
|
||||
)).toList();
|
||||
artUri: Uri.parse(a.picture.thumb)))
|
||||
.toList();
|
||||
return out;
|
||||
}
|
||||
|
||||
//Artist screen (albums, etc)
|
||||
if (parentId.startsWith('albums')) {
|
||||
List<Album> albums = await deezerAPI.discographyPage(parentId.replaceFirst('albums', ''));
|
||||
List<Album> albums =
|
||||
await deezerAPI.discographyPage(parentId.replaceFirst('albums', ''));
|
||||
|
||||
List<MediaItem> out = albums.map<MediaItem>((a) => MediaItem(
|
||||
List<MediaItem> out = albums
|
||||
.map<MediaItem>((a) => MediaItem(
|
||||
id: '${prefix}album${a.id}',
|
||||
displayTitle: a.title,
|
||||
displaySubtitle: a.artistString,
|
||||
playable: true,
|
||||
artUri: a.art.thumb
|
||||
)).toList();
|
||||
artUri: Uri.parse(a.art.thumb)))
|
||||
.toList();
|
||||
return out;
|
||||
}
|
||||
|
||||
|
@ -77,21 +82,19 @@ class AndroidAuto {
|
|||
HomePage hp = await deezerAPI.homePage();
|
||||
List<MediaItem> out = [];
|
||||
for (HomePageSection section in hp.sections) {
|
||||
for (int i=0; i<section.items.length; i++) {
|
||||
for (int i = 0; i < section.items.length; i++) {
|
||||
//Limit to max 5 items
|
||||
if (i == 5) break;
|
||||
|
||||
//Check type
|
||||
var data = section.items[i].value;
|
||||
switch (section.items[i].type) {
|
||||
|
||||
case HomePageItemType.PLAYLIST:
|
||||
out.add(MediaItem(
|
||||
id: '${prefix}playlist${data.id}',
|
||||
displayTitle: data.title,
|
||||
playable: true,
|
||||
artUri: data.image.thumb
|
||||
));
|
||||
artUri: data.image.thumb));
|
||||
break;
|
||||
|
||||
case HomePageItemType.ALBUM:
|
||||
|
@ -100,8 +103,7 @@ class AndroidAuto {
|
|||
displayTitle: data.title,
|
||||
displaySubtitle: data.artistString,
|
||||
playable: true,
|
||||
artUri: data.art.thumb
|
||||
));
|
||||
artUri: data.art.thumb));
|
||||
break;
|
||||
|
||||
case HomePageItemType.ARTIST:
|
||||
|
@ -109,8 +111,7 @@ class AndroidAuto {
|
|||
id: 'albums${data.id}',
|
||||
displayTitle: data.name,
|
||||
playable: false,
|
||||
artUri: data.picture.thumb
|
||||
));
|
||||
artUri: data.picture.thumb));
|
||||
break;
|
||||
|
||||
case HomePageItemType.SMARTTRACKLIST:
|
||||
|
@ -119,8 +120,7 @@ class AndroidAuto {
|
|||
displayTitle: data.title,
|
||||
displaySubtitle: data.subtitle,
|
||||
playable: true,
|
||||
artUri: data.cover.thumb
|
||||
));
|
||||
artUri: data.cover.thumb));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -133,12 +133,12 @@ class AndroidAuto {
|
|||
|
||||
//Load virtual mediaItem
|
||||
Future playItem(String id) async {
|
||||
|
||||
print(id);
|
||||
|
||||
//Play flow
|
||||
if (id == 'flow' || id == 'stlflow') {
|
||||
await playerHelper.playFromSmartTrackList(SmartTrackList(id: 'flow', title: 'Flow'.i18n));
|
||||
await playerHelper.playFromSmartTrackList(
|
||||
SmartTrackList(id: 'flow', title: 'Flow'.i18n));
|
||||
return;
|
||||
}
|
||||
//Play library tracks
|
||||
|
@ -146,20 +146,26 @@ class AndroidAuto {
|
|||
//Load tracks
|
||||
Playlist favPlaylist;
|
||||
try {
|
||||
favPlaylist = await deezerAPI.fullPlaylist(deezerAPI.favoritesPlaylistId);
|
||||
} catch (e) {print(e);}
|
||||
favPlaylist =
|
||||
await deezerAPI.fullPlaylist(deezerAPI.favoritesPlaylistId);
|
||||
} catch (e) {
|
||||
print(e);
|
||||
}
|
||||
if (favPlaylist == null || favPlaylist.tracks.length == 0) return;
|
||||
|
||||
await playerHelper.playFromTrackList(favPlaylist.tracks, favPlaylist.tracks[0].id, QueueSource(
|
||||
await playerHelper.playFromTrackList(
|
||||
favPlaylist.tracks,
|
||||
favPlaylist.tracks[0].id,
|
||||
QueueSource(
|
||||
id: 'allTracks',
|
||||
text: 'All offline tracks'.i18n,
|
||||
source: 'offline'
|
||||
));
|
||||
source: 'offline'));
|
||||
return;
|
||||
}
|
||||
//Play playlists
|
||||
if (id.startsWith('playlist')) {
|
||||
Playlist p = await deezerAPI.fullPlaylist(id.replaceFirst('playlist', ''));
|
||||
Playlist p =
|
||||
await deezerAPI.fullPlaylist(id.replaceFirst('playlist', ''));
|
||||
await playerHelper.playFromPlaylist(p, p.tracks[0].id);
|
||||
return;
|
||||
}
|
||||
|
@ -171,7 +177,8 @@ class AndroidAuto {
|
|||
}
|
||||
//Play smart track list
|
||||
if (id.startsWith('stl')) {
|
||||
SmartTrackList stl = await deezerAPI.smartTrackList(id.replaceFirst('stl', ''));
|
||||
SmartTrackList stl =
|
||||
await deezerAPI.smartTrackList(id.replaceFirst('stl', ''));
|
||||
await playerHelper.playFromSmartTrackList(stl);
|
||||
return;
|
||||
}
|
||||
|
@ -180,11 +187,7 @@ class AndroidAuto {
|
|||
//Homescreen items
|
||||
List<MediaItem> homeScreen() {
|
||||
return [
|
||||
MediaItem(
|
||||
id: '${prefix}flow',
|
||||
displayTitle: 'Flow'.i18n,
|
||||
playable: true
|
||||
),
|
||||
MediaItem(id: '${prefix}flow', displayTitle: 'Flow'.i18n, playable: true),
|
||||
MediaItem(
|
||||
id: 'homescreen',
|
||||
displayTitle: 'Home'.i18n,
|
||||
|
@ -212,5 +215,4 @@ class AndroidAuto {
|
|||
),
|
||||
];
|
||||
}
|
||||
|
||||
}
|
|
@ -78,7 +78,7 @@ class _LyricsScreenState extends State<LyricsScreen> {
|
|||
_load();
|
||||
|
||||
//Enable visualizer
|
||||
if (settings.lyricsVisualizer) playerHelper.startVisualizer();
|
||||
// if (settings.lyricsVisualizer) playerHelper.startVisualizer();
|
||||
Timer.periodic(Duration(milliseconds: 350), (timer) {
|
||||
_timer = timer;
|
||||
_currentIndex = lyrics?.lyrics?.lastIndexWhere(
|
||||
|
@ -107,7 +107,7 @@ class _LyricsScreenState extends State<LyricsScreen> {
|
|||
if (_timer != null) _timer.cancel();
|
||||
if (_mediaItemSub != null) _mediaItemSub.cancel();
|
||||
//Stop visualizer
|
||||
if (settings.lyricsVisualizer) playerHelper.stopVisualizer();
|
||||
// if (settings.lyricsVisualizer) playerHelper.stopVisualizer();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
@ -176,8 +176,8 @@ class _LyricsScreenState extends State<LyricsScreen> {
|
|||
},
|
||||
child: ListView.builder(
|
||||
controller: _controller,
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
0, 0, 0, settings.lyricsVisualizer ? 100 : 0),
|
||||
padding: EdgeInsets.fromLTRB(0, 0, 0,
|
||||
settings.lyricsVisualizer && false ? 100 : 0),
|
||||
itemCount: lyrics.lyrics.length,
|
||||
itemBuilder: (BuildContext context, int i) {
|
||||
return Padding(
|
||||
|
@ -212,30 +212,30 @@ class _LyricsScreenState extends State<LyricsScreen> {
|
|||
)),
|
||||
|
||||
//Visualizer
|
||||
if (settings.lyricsVisualizer)
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: StreamBuilder(
|
||||
stream: playerHelper.visualizerStream,
|
||||
builder: (BuildContext context, AsyncSnapshot snapshot) {
|
||||
List<double> data = snapshot.data ?? [];
|
||||
double width = MediaQuery.of(context).size.width /
|
||||
data.length; //- 0.25;
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: List.generate(
|
||||
data.length,
|
||||
(i) => AnimatedContainer(
|
||||
duration: Duration(milliseconds: 130),
|
||||
color: settings.primaryColor,
|
||||
height: data[i] * 100,
|
||||
width: width,
|
||||
)),
|
||||
);
|
||||
}),
|
||||
),
|
||||
//if (settings.lyricsVisualizer)
|
||||
// Positioned(
|
||||
// bottom: 0,
|
||||
// left: 0,
|
||||
// right: 0,
|
||||
// child: StreamBuilder(
|
||||
// stream: playerHelper.visualizerStream,
|
||||
// builder: (BuildContext context, AsyncSnapshot snapshot) {
|
||||
// List<double> data = snapshot.data ?? [];
|
||||
// double width = MediaQuery.of(context).size.width /
|
||||
// data.length; //- 0.25;
|
||||
// return Row(
|
||||
// crossAxisAlignment: CrossAxisAlignment.end,
|
||||
// children: List.generate(
|
||||
// data.length,
|
||||
// (i) => AnimatedContainer(
|
||||
// duration: Duration(milliseconds: 130),
|
||||
// color: settings.primaryColor,
|
||||
// height: data[i] * 100,
|
||||
// width: width,
|
||||
// )),
|
||||
// );
|
||||
// }),
|
||||
// ),
|
||||
],
|
||||
));
|
||||
}
|
||||
|
|
|
@ -671,7 +671,7 @@ class _BigAlbumArtState extends State<BigAlbumArt> {
|
|||
children: List.generate(
|
||||
AudioService.queue.length,
|
||||
(i) => ZoomableImage(
|
||||
url: AudioService.queue[i].artUri,
|
||||
url: AudioService.queue[i].artUri.toString(),
|
||||
)),
|
||||
),
|
||||
);
|
||||
|
|
|
@ -12,7 +12,6 @@ import 'package:fluttericon/font_awesome5_icons.dart';
|
|||
import 'package:fluttericon/web_symbols_icons.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:package_info/package_info.dart';
|
||||
import 'package:path_provider_ex/path_provider_ex.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:scrobblenaut/scrobblenaut.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
@ -161,7 +160,7 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
|
|||
SimpleDialogOption(
|
||||
child: Text('Light'.i18n),
|
||||
onPressed: () {
|
||||
setState(() => settings.theme = Themes.Light);
|
||||
settings.theme = Themes.Light;
|
||||
settings.save();
|
||||
updateTheme();
|
||||
Navigator.of(context).pop();
|
||||
|
@ -170,7 +169,7 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
|
|||
SimpleDialogOption(
|
||||
child: Text('Dark'.i18n),
|
||||
onPressed: () {
|
||||
setState(() => settings.theme = Themes.Dark);
|
||||
settings.theme = Themes.Dark;
|
||||
settings.save();
|
||||
updateTheme();
|
||||
Navigator.of(context).pop();
|
||||
|
@ -179,7 +178,7 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
|
|||
SimpleDialogOption(
|
||||
child: Text('Black (AMOLED)'.i18n),
|
||||
onPressed: () {
|
||||
setState(() => settings.theme = Themes.Black);
|
||||
settings.theme = Themes.Black;
|
||||
settings.save();
|
||||
updateTheme();
|
||||
Navigator.of(context).pop();
|
||||
|
@ -188,7 +187,7 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
|
|||
SimpleDialogOption(
|
||||
child: Text('Deezer (Dark)'.i18n),
|
||||
onPressed: () {
|
||||
setState(() => settings.theme = Themes.Deezer);
|
||||
settings.theme = Themes.Deezer;
|
||||
settings.save();
|
||||
updateTheme();
|
||||
Navigator.of(context).pop();
|
||||
|
@ -203,11 +202,10 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
|
|||
title: Text('Use system theme'.i18n),
|
||||
value: settings.useSystemTheme,
|
||||
onChanged: (bool v) async {
|
||||
setState(() {
|
||||
settings.useSystemTheme = v;
|
||||
});
|
||||
|
||||
settings.save();
|
||||
updateTheme();
|
||||
await settings.save();
|
||||
},
|
||||
secondary: Icon(Icons.android)),
|
||||
ListTile(
|
||||
|
@ -258,10 +256,11 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
|
|||
ListTile(
|
||||
title: Text('Primary color'.i18n),
|
||||
leading: Icon(Icons.format_paint),
|
||||
subtitle: Text(
|
||||
'Selected color'.i18n,
|
||||
style: TextStyle(color: settings.primaryColor),
|
||||
),
|
||||
trailing: Padding(
|
||||
padding: EdgeInsets.only(left: 8.0),
|
||||
child: CircleAvatar(
|
||||
backgroundColor: settings.primaryColor,
|
||||
)),
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
|
@ -285,9 +284,7 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
|
|||
allowShades: false,
|
||||
selectedColor: settings.primaryColor,
|
||||
onMainColorChange: (ColorSwatch color) {
|
||||
setState(() {
|
||||
settings.primaryColor = color;
|
||||
});
|
||||
settings.save();
|
||||
updateTheme();
|
||||
Navigator.of(context).pop();
|
||||
|
@ -808,16 +805,21 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
|
|||
subtitle: Text(settings.downloadPath),
|
||||
onTap: () async {
|
||||
//Check permissions
|
||||
if (!(await Permission.storage.request().isGranted)) return;
|
||||
if (!await Permission.storage.request().isGranted) return;
|
||||
DownloadManager.getDirectory('Pick-a-Path'.i18n).then((path) {
|
||||
if (path == null) return; // user canceled
|
||||
setState(() => settings.downloadPath = path);
|
||||
settings.save();
|
||||
});
|
||||
//Navigate
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (context) => DirectoryPicker(
|
||||
settings.downloadPath,
|
||||
onSelect: (String p) async {
|
||||
setState(() => settings.downloadPath = p);
|
||||
await settings.save();
|
||||
},
|
||||
)));
|
||||
// Navigator.of(context).push(MaterialPageRoute(
|
||||
// builder: (context) => DirectoryPicker(
|
||||
// settings.downloadPath,
|
||||
// onSelect: (String p) async {
|
||||
// setState(() => settings.downloadPath = p);
|
||||
// await settings.save();
|
||||
// },
|
||||
// )));
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
|
@ -1308,165 +1310,165 @@ class _LastFMLoginState extends State<LastFMLogin> {
|
|||
}
|
||||
}
|
||||
|
||||
class DirectoryPicker extends StatefulWidget {
|
||||
final String initialPath;
|
||||
final Function onSelect;
|
||||
DirectoryPicker(this.initialPath, {this.onSelect, Key key}) : super(key: key);
|
||||
// class DirectoryPicker extends StatefulWidget {
|
||||
// final String initialPath;
|
||||
// final Function onSelect;
|
||||
// DirectoryPicker(this.initialPath, {this.onSelect, Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_DirectoryPickerState createState() => _DirectoryPickerState();
|
||||
}
|
||||
// @override
|
||||
// _DirectoryPickerState createState() => _DirectoryPickerState();
|
||||
// }
|
||||
|
||||
class _DirectoryPickerState extends State<DirectoryPicker> {
|
||||
String _path;
|
||||
String _previous;
|
||||
String _root;
|
||||
// class _DirectoryPickerState extends State<DirectoryPicker> {
|
||||
// String _path;
|
||||
// String _previous;
|
||||
// String _root;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_path = widget.initialPath;
|
||||
super.initState();
|
||||
}
|
||||
// @override
|
||||
// void initState() {
|
||||
// _path = widget.initialPath;
|
||||
// super.initState();
|
||||
// }
|
||||
|
||||
Future _resetPath() async {
|
||||
StorageInfo si = (await PathProviderEx.getStorageInfo())[0];
|
||||
setState(() => _path = si.appFilesDir);
|
||||
}
|
||||
// Future _resetPath() async {
|
||||
// StorageInfo si = (await PathProviderEx.getStorageInfo())[0];
|
||||
// setState(() => _path = si.appFilesDir);
|
||||
// }
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: FreezerAppBar(
|
||||
'Pick-a-Path'.i18n,
|
||||
actions: <Widget>[
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.sd_card,
|
||||
semanticLabel: 'Select storage'.i18n,
|
||||
),
|
||||
onPressed: () {
|
||||
String path = '';
|
||||
//Chose storage
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: Text('Select storage'.i18n),
|
||||
content: FutureBuilder(
|
||||
future: PathProviderEx.getStorageInfo(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) return ErrorScreen();
|
||||
if (!snapshot.hasData)
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
CircularProgressIndicator()
|
||||
],
|
||||
),
|
||||
);
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: List<Widget>.generate(
|
||||
snapshot.data.length, (int i) {
|
||||
StorageInfo si = snapshot.data[i];
|
||||
return ListTile(
|
||||
title: Text(si.rootDir),
|
||||
leading: Icon(Icons.sd_card),
|
||||
trailing: Text(filesize(si.availableBytes)),
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_path = si.appFilesDir;
|
||||
//Android 5+ blocks sd card, so this prevents going outside
|
||||
//app data dir, until permission request fix.
|
||||
_root = si.rootDir;
|
||||
if (i != 0) _root = si.appFilesDir;
|
||||
});
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
);
|
||||
}));
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
})
|
||||
],
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
child: Icon(Icons.done),
|
||||
onPressed: () {
|
||||
//When folder confirmed
|
||||
if (widget.onSelect != null) widget.onSelect(_path);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
body: FutureBuilder(
|
||||
future: Directory(_path).list().toList(),
|
||||
builder: (BuildContext context, AsyncSnapshot snapshot) {
|
||||
//On error go to last good path
|
||||
if (snapshot.hasError)
|
||||
Future.delayed(Duration(milliseconds: 50), () {
|
||||
if (_previous == null) {
|
||||
_resetPath();
|
||||
return;
|
||||
}
|
||||
setState(() => _path = _previous);
|
||||
});
|
||||
if (!snapshot.hasData)
|
||||
return Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// return Scaffold(
|
||||
// appBar: FreezerAppBar(
|
||||
// 'Pick-a-Path'.i18n,
|
||||
// actions: <Widget>[
|
||||
// IconButton(
|
||||
// icon: Icon(
|
||||
// Icons.sd_card,
|
||||
// semanticLabel: 'Select storage'.i18n,
|
||||
// ),
|
||||
// onPressed: () {
|
||||
// String path = '';
|
||||
// //Chose storage
|
||||
// showDialog(
|
||||
// context: context,
|
||||
// builder: (context) {
|
||||
// return AlertDialog(
|
||||
// title: Text('Select storage'.i18n),
|
||||
// content: FutureBuilder(
|
||||
// future: PathProviderEx.getStorageInfo(),
|
||||
// builder: (context, snapshot) {
|
||||
// if (snapshot.hasError) return ErrorScreen();
|
||||
// if (!snapshot.hasData)
|
||||
// return Padding(
|
||||
// padding: EdgeInsets.symmetric(vertical: 8.0),
|
||||
// child: Row(
|
||||
// mainAxisAlignment: MainAxisAlignment.center,
|
||||
// children: <Widget>[
|
||||
// CircularProgressIndicator()
|
||||
// ],
|
||||
// ),
|
||||
// );
|
||||
// return Column(
|
||||
// mainAxisSize: MainAxisSize.min,
|
||||
// children: List<Widget>.generate(
|
||||
// snapshot.data.length, (int i) {
|
||||
// StorageInfo si = snapshot.data[i];
|
||||
// return ListTile(
|
||||
// title: Text(si.rootDir),
|
||||
// leading: Icon(Icons.sd_card),
|
||||
// trailing: Text(filesize(si.availableBytes)),
|
||||
// onTap: () {
|
||||
// setState(() {
|
||||
// _path = si.appFilesDir;
|
||||
// //Android 5+ blocks sd card, so this prevents going outside
|
||||
// //app data dir, until permission request fix.
|
||||
// _root = si.rootDir;
|
||||
// if (i != 0) _root = si.appFilesDir;
|
||||
// });
|
||||
// Navigator.of(context).pop();
|
||||
// },
|
||||
// );
|
||||
// }));
|
||||
// },
|
||||
// ),
|
||||
// );
|
||||
// });
|
||||
// })
|
||||
// ],
|
||||
// ),
|
||||
// floatingActionButton: FloatingActionButton(
|
||||
// child: Icon(Icons.done),
|
||||
// onPressed: () {
|
||||
// //When folder confirmed
|
||||
// if (widget.onSelect != null) widget.onSelect(_path);
|
||||
// Navigator.of(context).pop();
|
||||
// },
|
||||
// ),
|
||||
// body: FutureBuilder(
|
||||
// future: Directory(_path).list().toList(),
|
||||
// builder: (BuildContext context, AsyncSnapshot snapshot) {
|
||||
// //On error go to last good path
|
||||
// if (snapshot.hasError)
|
||||
// Future.delayed(Duration(milliseconds: 50), () {
|
||||
// if (_previous == null) {
|
||||
// _resetPath();
|
||||
// return;
|
||||
// }
|
||||
// setState(() => _path = _previous);
|
||||
// });
|
||||
// if (!snapshot.hasData)
|
||||
// return Center(
|
||||
// child: CircularProgressIndicator(),
|
||||
// );
|
||||
|
||||
List<FileSystemEntity> data = snapshot.data;
|
||||
return ListView(
|
||||
children: <Widget>[
|
||||
ListTile(
|
||||
title: Text(_path),
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Go up'.i18n),
|
||||
leading: Icon(Icons.arrow_upward),
|
||||
onTap: () {
|
||||
setState(() {
|
||||
if (_root == _path) {
|
||||
Fluttertoast.showToast(
|
||||
msg: 'Permission denied'.i18n,
|
||||
gravity: ToastGravity.BOTTOM);
|
||||
return;
|
||||
}
|
||||
_previous = _path;
|
||||
_path = Directory(_path).parent.path;
|
||||
});
|
||||
},
|
||||
),
|
||||
...List.generate(data.length, (i) {
|
||||
FileSystemEntity f = data[i];
|
||||
if (f is Directory) {
|
||||
return ListTile(
|
||||
title: Text(f.path.split('/').last),
|
||||
leading: Icon(Icons.folder),
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_previous = _path;
|
||||
_path = f.path;
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
return Container(
|
||||
height: 0,
|
||||
width: 0,
|
||||
);
|
||||
})
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
// List<FileSystemEntity> data = snapshot.data;
|
||||
// return ListView(
|
||||
// children: <Widget>[
|
||||
// ListTile(
|
||||
// title: Text(_path),
|
||||
// ),
|
||||
// ListTile(
|
||||
// title: Text('Go up'.i18n),
|
||||
// leading: Icon(Icons.arrow_upward),
|
||||
// onTap: () {
|
||||
// setState(() {
|
||||
// if (_root == _path) {
|
||||
// Fluttertoast.showToast(
|
||||
// msg: 'Permission denied'.i18n,
|
||||
// gravity: ToastGravity.BOTTOM);
|
||||
// return;
|
||||
// }
|
||||
// _previous = _path;
|
||||
// _path = Directory(_path).parent.path;
|
||||
// });
|
||||
// },
|
||||
// ),
|
||||
// ...List.generate(data.length, (i) {
|
||||
// FileSystemEntity f = data[i];
|
||||
// if (f is Directory) {
|
||||
// return ListTile(
|
||||
// title: Text(f.path.split('/').last),
|
||||
// leading: Icon(Icons.folder),
|
||||
// onTap: () {
|
||||
// setState(() {
|
||||
// _previous = _path;
|
||||
// _path = f.path;
|
||||
// });
|
||||
// },
|
||||
// );
|
||||
// }
|
||||
// return Container(
|
||||
// height: 0,
|
||||
// width: 0,
|
||||
// );
|
||||
// })
|
||||
// ],
|
||||
// );
|
||||
// },
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
||||
class CreditsScreen extends StatefulWidget {
|
||||
@override
|
||||
|
|
|
@ -198,7 +198,7 @@ class FreezerVersions {
|
|||
//Fetch from website API
|
||||
static Future<FreezerVersions> fetch() async {
|
||||
http.Response response =
|
||||
await http.get('https://freezer.life/api/versions');
|
||||
await http.get(Uri.parse('https://freezer.life/api/versions'));
|
||||
// http.Response response = await http.get('https://cum.freezerapp.workers.dev/api/versions');
|
||||
return FreezerVersions.fromJson(jsonDecode(response.body));
|
||||
}
|
||||
|
|
253
pubspec.lock
253
pubspec.lock
|
@ -7,14 +7,14 @@ packages:
|
|||
name: _fe_analyzer_shared
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "14.0.0"
|
||||
version: "24.0.0"
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: analyzer
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.41.2"
|
||||
version: "2.1.0"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -32,17 +32,17 @@ packages:
|
|||
audio_service:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: audio_service
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.15.1"
|
||||
name: audio_service
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.17.1"
|
||||
audio_session:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: audio_session
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.0.11"
|
||||
version: "0.1.6"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -56,42 +56,42 @@ packages:
|
|||
name: build
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.6.2"
|
||||
version: "2.1.0"
|
||||
build_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_config
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.4.6"
|
||||
version: "1.0.0"
|
||||
build_daemon:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_daemon
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.10"
|
||||
version: "3.0.0"
|
||||
build_resolvers:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_resolvers
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.5.3"
|
||||
version: "2.0.4"
|
||||
build_runner:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: build_runner
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.11.5"
|
||||
version: "2.1.1"
|
||||
build_runner_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_runner_core
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "6.1.10"
|
||||
version: "7.1.0"
|
||||
built_collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -112,7 +112,21 @@ packages:
|
|||
name: cached_network_image
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.3.3"
|
||||
version: "3.1.0"
|
||||
cached_network_image_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cached_network_image_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
cached_network_image_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cached_network_image_web
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -133,7 +147,7 @@ packages:
|
|||
name: checked_yaml
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
version: "2.0.1"
|
||||
cli_util:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -154,7 +168,7 @@ packages:
|
|||
name: code_builder
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.7.0"
|
||||
version: "4.1.0"
|
||||
collection:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -196,7 +210,7 @@ packages:
|
|||
name: convert
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
version: "3.0.1"
|
||||
cookie_jar:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -217,7 +231,7 @@ packages:
|
|||
name: crypto
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.5"
|
||||
version: "3.0.1"
|
||||
csslib:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -225,29 +239,20 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.17.0"
|
||||
custom_navigator:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: HEAD
|
||||
resolved-ref: "84bc85880abaa0d4a0f37098c9e6f4bd58b19b0a"
|
||||
url: "https://github.com/kjawadDeveloper/Custom-navigator.git"
|
||||
source: git
|
||||
version: "0.2.0"
|
||||
dart_style:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dart_style
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.3.12"
|
||||
version: "2.0.3"
|
||||
dio:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dio
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.10"
|
||||
version: "4.0.0"
|
||||
disk_space:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -265,24 +270,12 @@ packages:
|
|||
equalizer:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: equalizer
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
path: "."
|
||||
ref: HEAD
|
||||
resolved-ref: "84c15ca304a8129a1cad5a6891059fb411f0fc55"
|
||||
url: "https://github.com/gladson97/equalizer.git"
|
||||
source: git
|
||||
version: "0.0.2+2"
|
||||
ext_storage:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: ext_storage
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.3"
|
||||
extended_math:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: extended_math
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.0.29+1"
|
||||
fading_edge_scrollview:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -336,14 +329,14 @@ packages:
|
|||
name: flutter_blurhash
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.5.0"
|
||||
version: "0.6.0"
|
||||
flutter_cache_manager:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_cache_manager
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.4.2"
|
||||
version: "3.1.2"
|
||||
flutter_displaymode:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -364,21 +357,21 @@ packages:
|
|||
name: flutter_isolate
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.0+15"
|
||||
version: "2.0.0"
|
||||
flutter_local_notifications:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_local_notifications
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.0.1+2"
|
||||
version: "8.1.1+1"
|
||||
flutter_local_notifications_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_local_notifications_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.0+1"
|
||||
version: "4.0.1"
|
||||
flutter_localizations:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
|
@ -422,6 +415,13 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "8.0.8"
|
||||
frontend_server_client:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: frontend_server_client
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
gettext_parser:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -442,14 +442,14 @@ packages:
|
|||
name: google_fonts
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
version: "2.1.0"
|
||||
graphs:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: graphs
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
version: "2.0.0"
|
||||
html:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -463,21 +463,21 @@ packages:
|
|||
name: http
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.12.2"
|
||||
version: "0.13.3"
|
||||
http_multi_server:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_multi_server
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
version: "3.0.1"
|
||||
http_parser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_parser
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.1.4"
|
||||
version: "4.0.0"
|
||||
i18n_extension:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -505,7 +505,7 @@ packages:
|
|||
name: io
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.3.5"
|
||||
version: "1.0.3"
|
||||
js:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -519,35 +519,37 @@ packages:
|
|||
name: json_annotation
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.1.1"
|
||||
version: "4.1.0"
|
||||
json_serializable:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: json_serializable
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.5.1"
|
||||
version: "5.0.0"
|
||||
just_audio:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "just_audio/just_audio"
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.6.5"
|
||||
path: just_audio
|
||||
ref: dev
|
||||
resolved-ref: "7ac783939a758be2799faefc8877c34a84fe1554"
|
||||
url: "https://github.com/ryanheise/just_audio.git"
|
||||
source: git
|
||||
version: "0.9.7"
|
||||
just_audio_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
path: "just_audio/just_audio_platform_interface"
|
||||
relative: true
|
||||
source: path
|
||||
version: "2.0.0"
|
||||
name: just_audio_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
just_audio_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
path: "just_audio/just_audio_web"
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.2.1"
|
||||
name: just_audio_web
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.4.1"
|
||||
logging:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -603,14 +605,14 @@ packages:
|
|||
name: oauth2
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.6.3"
|
||||
version: "2.0.0"
|
||||
octo_image:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: octo_image
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.3.0"
|
||||
version: "1.0.0+1"
|
||||
open_file:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -624,7 +626,7 @@ packages:
|
|||
name: package_config
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.9.3"
|
||||
version: "2.0.0"
|
||||
package_info:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -652,42 +654,35 @@ packages:
|
|||
name: path_provider
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.6.28"
|
||||
path_provider_ex:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path_provider_ex
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
version: "2.0.2"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_linux
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.0.1+2"
|
||||
version: "2.0.2"
|
||||
path_provider_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_macos
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.0.4+8"
|
||||
version: "2.0.2"
|
||||
path_provider_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
version: "2.0.1"
|
||||
path_provider_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_windows
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.0.5"
|
||||
version: "2.0.3"
|
||||
pedantic:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -701,21 +696,21 @@ packages:
|
|||
name: permission_handler
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "5.1.0+2"
|
||||
version: "8.1.4+2"
|
||||
permission_handler_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
version: "3.6.1"
|
||||
petitparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: petitparser
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
version: "4.2.0"
|
||||
photo_view:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -736,7 +731,7 @@ packages:
|
|||
name: plugin_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.3"
|
||||
version: "2.0.1"
|
||||
pool:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -764,7 +759,7 @@ packages:
|
|||
name: pubspec_parse
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.1.8"
|
||||
version: "1.0.0"
|
||||
quick_actions:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -772,13 +767,6 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.5.0+1"
|
||||
quiver:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: quiver
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.5"
|
||||
random_string:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -792,14 +780,16 @@ packages:
|
|||
name: rxdart
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.24.1"
|
||||
version: "0.27.1"
|
||||
scrobblenaut:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: scrobblenaut
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.5"
|
||||
path: "."
|
||||
ref: main
|
||||
resolved-ref: a138aa57796cd1c1b3359d461b49515e58948baa
|
||||
url: "https://github.com/furgoose/Scrobblenaut.git"
|
||||
source: git
|
||||
version: "3.0.0"
|
||||
share:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -813,14 +803,14 @@ packages:
|
|||
name: shelf
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.7.9"
|
||||
version: "1.2.0"
|
||||
shelf_web_socket:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf_web_socket
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.2.4+1"
|
||||
version: "1.0.1"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
|
@ -832,7 +822,14 @@ packages:
|
|||
name: source_gen
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.9.10+3"
|
||||
version: "1.0.5"
|
||||
source_helper:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_helper
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
source_span:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -846,7 +843,7 @@ packages:
|
|||
name: spotify
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.5.1"
|
||||
version: "0.6.0"
|
||||
sprintf:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -860,14 +857,14 @@ packages:
|
|||
name: sqflite
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.3.2+4"
|
||||
version: "2.0.0+4"
|
||||
sqflite_common:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite_common
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.3+3"
|
||||
version: "2.0.1"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -902,7 +899,7 @@ packages:
|
|||
name: synchronized
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.2.0+2"
|
||||
version: "3.0.0"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -923,14 +920,14 @@ packages:
|
|||
name: timezone
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.6.1"
|
||||
version: "0.7.0"
|
||||
timing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: timing
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.1.1+3"
|
||||
version: "1.0.0"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -944,14 +941,21 @@ packages:
|
|||
name: uni_links
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.4.0"
|
||||
universal_io:
|
||||
version: "0.5.1"
|
||||
uni_links_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: universal_io
|
||||
name: uni_links_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
version: "1.0.0"
|
||||
uni_links_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: uni_links_web
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.1.0"
|
||||
url_launcher:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -1000,7 +1004,7 @@ packages:
|
|||
name: uuid
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.2.2"
|
||||
version: "3.0.4"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1063,7 +1067,7 @@ packages:
|
|||
name: web_socket_channel
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
version: "2.1.0"
|
||||
win32:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1077,14 +1081,14 @@ packages:
|
|||
name: xdg_directories
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.1.2"
|
||||
version: "0.2.0"
|
||||
xml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xml
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.5.1"
|
||||
version: "5.2.0"
|
||||
yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1092,13 +1096,6 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
zone_local:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: zone_local
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.1.2"
|
||||
sdks:
|
||||
dart: ">=2.13.0 <3.0.0"
|
||||
flutter: ">=2.0.0"
|
||||
flutter: ">=2.2.0"
|
||||
|
|
52
pubspec.yaml
52
pubspec.yaml
|
@ -27,17 +27,16 @@ dependencies:
|
|||
flutter_localizations:
|
||||
sdk: flutter
|
||||
|
||||
spotify: ^0.5.1
|
||||
spotify: ^0.6.0
|
||||
flutter_displaymode: ^0.3.2
|
||||
crypto: ^2.1.5
|
||||
http: ^0.12.2
|
||||
crypto: ^3.0.0
|
||||
http: ^0.13.0
|
||||
cookie_jar: ^3.0.1
|
||||
json_annotation: ^3.0.1
|
||||
path_provider: ^1.6.28
|
||||
json_annotation: ^4.0.0
|
||||
path_provider: ^2.0.0
|
||||
path: ^1.6.4
|
||||
sqflite: ^1.3.0+1
|
||||
ext_storage: ^1.0.3
|
||||
permission_handler: ^5.0.0+hotfix.6
|
||||
sqflite: ^2.0.0+3
|
||||
permission_handler: ^8.1.4+2
|
||||
connectivity: ^3.0.6
|
||||
intl: ^0.17.0
|
||||
filesize: ^2.0.1
|
||||
|
@ -48,42 +47,43 @@ dependencies:
|
|||
country_pickers: ^2.0.0
|
||||
package_info: ^2.0.2
|
||||
move_to_background: ^1.0.1
|
||||
flutter_local_notifications: ^4.0.1+2
|
||||
flutter_local_notifications: ^8.1.1+1
|
||||
collection: ^1.14.12
|
||||
disk_space: ^0.1.1
|
||||
path_provider_ex: ^1.0.1
|
||||
random_string: ^2.0.1
|
||||
async: ^2.4.1
|
||||
html: ^0.15.0
|
||||
flutter_screenutil: ^5.0.0+2
|
||||
marquee: ^2.2.0
|
||||
flutter_cache_manager: ^1.4.1
|
||||
cached_network_image: ^2.3.2+1
|
||||
flutter_cache_manager: ^3.0.0
|
||||
cached_network_image: ^3.1.0
|
||||
i18n_extension: ^4.0.0
|
||||
fluttericon: ^2.0.0
|
||||
url_launcher: ^6.0.5
|
||||
uni_links: ^0.4.0
|
||||
uni_links: ^0.5.1
|
||||
share: ^2.0.4
|
||||
numberpicker: ^2.1.1
|
||||
quick_actions: ^0.5.0+1
|
||||
photo_view: ^0.12.0
|
||||
draggable_scrollbar: ^0.1.0
|
||||
scrobblenaut: ^2.0.4
|
||||
scrobblenaut:
|
||||
git:
|
||||
url: https://github.com/furgoose/Scrobblenaut.git
|
||||
ref: main
|
||||
open_file: ^3.0.3
|
||||
version: ^2.0.0
|
||||
wakelock: ^0.5.3+3
|
||||
google_fonts: ^1.1.2
|
||||
equalizer: ^0.0.2+2
|
||||
extended_math: ^0.0.29+1
|
||||
custom_navigator:
|
||||
git:
|
||||
url: https://github.com/kjawadDeveloper/Custom-navigator.git
|
||||
google_fonts: ^2.1.0
|
||||
equalizer:
|
||||
git: https://github.com/gladson97/equalizer.git
|
||||
|
||||
audio_session: ^0.0.9
|
||||
audio_service:
|
||||
path: ./audio_service
|
||||
audio_session: ^0.1.6
|
||||
audio_service: ^0.17.1
|
||||
just_audio:
|
||||
path: ./just_audio/just_audio
|
||||
git:
|
||||
url: https://github.com/ryanheise/just_audio.git
|
||||
ref: dev
|
||||
path: just_audio/
|
||||
|
||||
# cupertino_icons: ^0.1.3
|
||||
|
||||
|
@ -91,8 +91,8 @@ dev_dependencies:
|
|||
flutter_test:
|
||||
sdk: flutter
|
||||
|
||||
json_serializable: ^3.3.0
|
||||
build_runner: ^1.10.0
|
||||
json_serializable: ^5.0.0
|
||||
build_runner: ^2.1.1
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
# following page: https://dart.dev/tools/pub/pubspec
|
||||
|
|
Loading…
Reference in a new issue