pre-null safety migration code

This commit is contained in:
pato05 2021-08-30 14:51:51 +02:00
parent 34fc597bbb
commit 8d53162099
17 changed files with 1156 additions and 834 deletions

6
.gitmodules vendored
View file

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

View 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());
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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