wip: null-safety, audio_service update
This commit is contained in:
parent
8d53162099
commit
2bd29f4cea
|
@ -2,6 +2,7 @@ package f.f.freezer;
|
|||
|
||||
// copied from https://gist.github.com/asifmujteba/d89ba9074bc941de1eaa#file-asfurihelper
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.ContentUris;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
|
|
|
@ -21,6 +21,9 @@ import androidx.core.view.WindowCompat;
|
|||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.ryanheise.audioservice.AudioServiceActivity;
|
||||
import com.ryanheise.audioservice.AudioServicePlugin;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
|
@ -49,7 +52,8 @@ import io.flutter.plugins.GeneratedPluginRegistrant;
|
|||
|
||||
import static f.f.freezer.Deezer.bytesToHex;
|
||||
|
||||
public class MainActivity extends FlutterActivity {
|
||||
// overriding AudioServiceActivity which basically provides the flutter engine thing
|
||||
public class MainActivity extends AudioServiceActivity {
|
||||
private static final String CHANNEL = "f.f.freezer/native";
|
||||
private static final String EVENT_CHANNEL = "f.f.freezer/downloads";
|
||||
|
||||
|
@ -77,10 +81,10 @@ public class MainActivity extends FlutterActivity {
|
|||
@Override
|
||||
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
|
||||
GeneratedPluginRegistrant.registerWith(flutterEngine);
|
||||
|
||||
Log.i("MainActivity", "configureFlutterEngine() was called");
|
||||
//Flutter method channel
|
||||
new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), CHANNEL).setMethodCallHandler((((call, result) -> {
|
||||
|
||||
Log.i("MethodChannelHandler", "received method "+ call.method);
|
||||
//Add downloads to DB, then refresh service
|
||||
if (call.method.equals("addDownloads")) {
|
||||
//TX
|
||||
|
|
|
@ -9,45 +9,43 @@ import 'dart:async';
|
|||
|
||||
part 'cache.g.dart';
|
||||
|
||||
Cache cache;
|
||||
late Cache cache;
|
||||
|
||||
//Cache for miscellaneous things
|
||||
@JsonSerializable()
|
||||
class Cache {
|
||||
//ID's of tracks that are in library
|
||||
List<String> libraryTracks = [];
|
||||
List<String>? libraryTracks = [];
|
||||
|
||||
//Track ID of logged track, to prevent duplicates
|
||||
@JsonKey(ignore: true)
|
||||
String loggedTrackId;
|
||||
String? loggedTrackId;
|
||||
|
||||
@JsonKey(defaultValue: [])
|
||||
List<Track> history = [];
|
||||
|
||||
//All sorting cached
|
||||
@JsonKey(defaultValue: [])
|
||||
List<Sorting> sorts = [];
|
||||
List<Sorting?> sorts = [];
|
||||
|
||||
//Sleep timer
|
||||
@JsonKey(ignore: true)
|
||||
DateTime sleepTimerTime;
|
||||
DateTime? sleepTimerTime;
|
||||
@JsonKey(ignore: true)
|
||||
StreamSubscription sleepTimer;
|
||||
// ignore: cancel_subscriptions
|
||||
StreamSubscription? sleepTimer;
|
||||
|
||||
//Search history
|
||||
@JsonKey(
|
||||
name: 'searchHistory2',
|
||||
toJson: _searchHistoryToJson,
|
||||
fromJson: _searchHistoryFromJson)
|
||||
List<SearchHistoryItem> searchHistory;
|
||||
@JsonKey(name: 'searchHistory2')
|
||||
List<SearchHistoryItem>? searchHistory;
|
||||
|
||||
//If download threads warning was shown
|
||||
@JsonKey(defaultValue: false)
|
||||
bool threadsWarning;
|
||||
bool? threadsWarning;
|
||||
|
||||
//Last time update check
|
||||
@JsonKey(defaultValue: 0)
|
||||
int lastUpdateCheck;
|
||||
int? lastUpdateCheck;
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
bool wakelock = false;
|
||||
|
@ -56,9 +54,9 @@ class Cache {
|
|||
|
||||
//Wrapper to test if track is favorite against cache
|
||||
bool checkTrackFavorite(Track t) {
|
||||
if (t.favorite != null && t.favorite) return true;
|
||||
if (libraryTracks == null || libraryTracks.length == 0) return false;
|
||||
return libraryTracks.contains(t.id);
|
||||
if (t.favorite != null && t.favorite!) return true;
|
||||
if (libraryTracks == null || libraryTracks!.length == 0) return false;
|
||||
return libraryTracks!.contains(t.id);
|
||||
}
|
||||
|
||||
//Add to history
|
||||
|
@ -66,19 +64,19 @@ class Cache {
|
|||
if (searchHistory == null) searchHistory = [];
|
||||
|
||||
// Remove duplicate
|
||||
int i = searchHistory.indexWhere((e) => e.data.id == item.id);
|
||||
int i = searchHistory!.indexWhere((e) => e.data.id == item.id);
|
||||
if (i != -1) {
|
||||
searchHistory.removeAt(i);
|
||||
searchHistory!.removeAt(i);
|
||||
}
|
||||
|
||||
if (item is Track)
|
||||
searchHistory.add(SearchHistoryItem(item, SearchHistoryItemType.TRACK));
|
||||
searchHistory!.add(SearchHistoryItem(item, SearchHistoryItemType.TRACK));
|
||||
if (item is Album)
|
||||
searchHistory.add(SearchHistoryItem(item, SearchHistoryItemType.ALBUM));
|
||||
searchHistory!.add(SearchHistoryItem(item, SearchHistoryItemType.ALBUM));
|
||||
if (item is Artist)
|
||||
searchHistory.add(SearchHistoryItem(item, SearchHistoryItemType.ARTIST));
|
||||
searchHistory!.add(SearchHistoryItem(item, SearchHistoryItemType.ARTIST));
|
||||
if (item is Playlist)
|
||||
searchHistory
|
||||
searchHistory!
|
||||
.add(SearchHistoryItem(item, SearchHistoryItemType.PLAYLIST));
|
||||
|
||||
await save();
|
||||
|
@ -115,47 +113,53 @@ class Cache {
|
|||
Map<String, dynamic> toJson() => _$CacheToJson(this);
|
||||
|
||||
//Search History JSON
|
||||
static List<SearchHistoryItem> _searchHistoryFromJson(List<dynamic> json) {
|
||||
return (json ?? [])
|
||||
.map<SearchHistoryItem>((i) => _searchHistoryItemFromJson(i))
|
||||
.toList();
|
||||
}
|
||||
// static List<SearchHistoryItem> _searchHistoryFromJson(List<dynamic>? json) {
|
||||
// return (json ?? [])
|
||||
// .map<SearchHistoryItem>((i) => _searchHistoryItemFromJson(i))
|
||||
// .toList();
|
||||
// }
|
||||
|
||||
static SearchHistoryItem _searchHistoryItemFromJson(
|
||||
Map<String, dynamic> json) {
|
||||
SearchHistoryItemType type = SearchHistoryItemType.values[json['type']];
|
||||
dynamic data;
|
||||
switch (type) {
|
||||
case SearchHistoryItemType.TRACK:
|
||||
data = Track.fromJson(json['data']);
|
||||
break;
|
||||
case SearchHistoryItemType.ALBUM:
|
||||
data = Album.fromJson(json['data']);
|
||||
break;
|
||||
case SearchHistoryItemType.ARTIST:
|
||||
data = Artist.fromJson(json['data']);
|
||||
break;
|
||||
case SearchHistoryItemType.PLAYLIST:
|
||||
data = Playlist.fromJson(json['data']);
|
||||
break;
|
||||
}
|
||||
return SearchHistoryItem(data, type);
|
||||
}
|
||||
// static SearchHistoryItem _searchHistoryItemFromJson(
|
||||
// Map<String, dynamic> json) {
|
||||
// SearchHistoryItemType type = SearchHistoryItemType.values[json['type']];
|
||||
// dynamic data;
|
||||
// switch (type) {
|
||||
// case SearchHistoryItemType.TRACK:
|
||||
// data = Track.fromJson(json['data']);
|
||||
// break;
|
||||
// case SearchHistoryItemType.ALBUM:
|
||||
// data = Album.fromJson(json['data']);
|
||||
// break;
|
||||
// case SearchHistoryItemType.ARTIST:
|
||||
// data = Artist.fromJson(json['data']);
|
||||
// break;
|
||||
// case SearchHistoryItemType.PLAYLIST:
|
||||
// data = Playlist.fromJson(json['data']);
|
||||
// break;
|
||||
// }
|
||||
// return SearchHistoryItem(data, type);
|
||||
// }
|
||||
|
||||
static List<Map<String, dynamic>> _searchHistoryToJson(
|
||||
List<SearchHistoryItem> data) =>
|
||||
(data ?? [])
|
||||
.map<Map<String, dynamic>>(
|
||||
(i) => {"type": i.type.index, "data": i.data.toJson()})
|
||||
.toList();
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class SearchHistoryItem {
|
||||
dynamic data;
|
||||
@JsonKey(
|
||||
toJson: _searchHistoryItemTypeToJson,
|
||||
fromJson: _searchHistoryItemTypeFromJson)
|
||||
SearchHistoryItemType type;
|
||||
|
||||
SearchHistoryItem(this.data, this.type);
|
||||
|
||||
Map<String, dynamic> toJson() => _$SearchHistoryItemToJson(this);
|
||||
factory SearchHistoryItem.fromJson(Map<String, dynamic> json) =>
|
||||
_$SearchHistoryItemFromJson(json);
|
||||
|
||||
static int _searchHistoryItemTypeToJson(SearchHistoryItemType type) =>
|
||||
type.index;
|
||||
static SearchHistoryItemType _searchHistoryItemTypeFromJson(int index) =>
|
||||
SearchHistoryItemType.values[index];
|
||||
}
|
||||
|
||||
enum SearchHistoryItemType { TRACK, ALBUM, ARTIST, PLAYLIST }
|
||||
|
|
|
@ -6,84 +6,42 @@ part of 'cache.dart';
|
|||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
Cache _$CacheFromJson(Map<String, dynamic> json) {
|
||||
return Cache(
|
||||
libraryTracks:
|
||||
(json['libraryTracks'] as List)?.map((e) => e as String)?.toList(),
|
||||
)
|
||||
..history = (json['history'] as List)
|
||||
?.map((e) =>
|
||||
e == null ? null : Track.fromJson(e as Map<String, dynamic>))
|
||||
?.toList() ??
|
||||
[]
|
||||
..sorts = (json['sorts'] as List)
|
||||
?.map((e) =>
|
||||
e == null ? null : Sorting.fromJson(e as Map<String, dynamic>))
|
||||
?.toList() ??
|
||||
[]
|
||||
..searchHistory =
|
||||
Cache._searchHistoryFromJson(json['searchHistory2'] as List)
|
||||
..threadsWarning = json['threadsWarning'] as bool ?? false
|
||||
..lastUpdateCheck = json['lastUpdateCheck'] as int ?? 0;
|
||||
}
|
||||
Cache _$CacheFromJson(Map<String, dynamic> json) => Cache(
|
||||
libraryTracks: (json['libraryTracks'] as List<dynamic>?)
|
||||
?.map((e) => e as String)
|
||||
.toList(),
|
||||
)
|
||||
..history = (json['history'] as List<dynamic>?)
|
||||
?.map((e) => Track.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
[]
|
||||
..sorts = (json['sorts'] as List<dynamic>?)
|
||||
?.map((e) => Sorting.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
[]
|
||||
..searchHistory = (json['searchHistory2'] as List<dynamic>?)
|
||||
?.map((e) => SearchHistoryItem.fromJson(e as Map<String, dynamic>))
|
||||
.toList()
|
||||
..threadsWarning = json['threadsWarning'] as bool? ?? false
|
||||
..lastUpdateCheck = json['lastUpdateCheck'] as int? ?? 0;
|
||||
|
||||
Map<String, dynamic> _$CacheToJson(Cache instance) => <String, dynamic>{
|
||||
'libraryTracks': instance.libraryTracks,
|
||||
'history': instance.history,
|
||||
'sorts': instance.sorts,
|
||||
'searchHistory2': Cache._searchHistoryToJson(instance.searchHistory),
|
||||
'searchHistory2': instance.searchHistory,
|
||||
'threadsWarning': instance.threadsWarning,
|
||||
'lastUpdateCheck': instance.lastUpdateCheck,
|
||||
};
|
||||
|
||||
SearchHistoryItem _$SearchHistoryItemFromJson(Map<String, dynamic> json) {
|
||||
return SearchHistoryItem(
|
||||
json['data'],
|
||||
_$enumDecodeNullable(_$SearchHistoryItemTypeEnumMap, json['type']),
|
||||
);
|
||||
}
|
||||
SearchHistoryItem _$SearchHistoryItemFromJson(Map<String, dynamic> json) =>
|
||||
SearchHistoryItem(
|
||||
json['data'],
|
||||
SearchHistoryItem._searchHistoryItemTypeFromJson(json['type'] as int),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$SearchHistoryItemToJson(SearchHistoryItem instance) =>
|
||||
<String, dynamic>{
|
||||
'data': instance.data,
|
||||
'type': _$SearchHistoryItemTypeEnumMap[instance.type],
|
||||
'type': SearchHistoryItem._searchHistoryItemTypeToJson(instance.type),
|
||||
};
|
||||
|
||||
T _$enumDecode<T>(
|
||||
Map<T, dynamic> enumValues,
|
||||
dynamic source, {
|
||||
T unknownValue,
|
||||
}) {
|
||||
if (source == null) {
|
||||
throw ArgumentError('A value must be provided. Supported values: '
|
||||
'${enumValues.values.join(', ')}');
|
||||
}
|
||||
|
||||
final value = enumValues.entries
|
||||
.singleWhere((e) => e.value == source, orElse: () => null)
|
||||
?.key;
|
||||
|
||||
if (value == null && unknownValue == null) {
|
||||
throw ArgumentError('`$source` is not one of the supported values: '
|
||||
'${enumValues.values.join(', ')}');
|
||||
}
|
||||
return value ?? unknownValue;
|
||||
}
|
||||
|
||||
T _$enumDecodeNullable<T>(
|
||||
Map<T, dynamic> enumValues,
|
||||
dynamic source, {
|
||||
T unknownValue,
|
||||
}) {
|
||||
if (source == null) {
|
||||
return null;
|
||||
}
|
||||
return _$enumDecode<T>(enumValues, source, unknownValue: unknownValue);
|
||||
}
|
||||
|
||||
const _$SearchHistoryItemTypeEnumMap = {
|
||||
SearchHistoryItemType.TRACK: 'TRACK',
|
||||
SearchHistoryItemType.ALBUM: 'ALBUM',
|
||||
SearchHistoryItemType.ARTIST: 'ARTIST',
|
||||
SearchHistoryItemType.PLAYLIST: 'PLAYLIST',
|
||||
};
|
||||
|
|
|
@ -12,14 +12,14 @@ DeezerAPI deezerAPI = DeezerAPI();
|
|||
class DeezerAPI {
|
||||
DeezerAPI({this.arl});
|
||||
|
||||
String arl;
|
||||
String token;
|
||||
String userId;
|
||||
String userName;
|
||||
String favoritesPlaylistId;
|
||||
String sid;
|
||||
String? arl;
|
||||
String? token;
|
||||
String? userId;
|
||||
String? userName;
|
||||
String? favoritesPlaylistId;
|
||||
String? sid;
|
||||
|
||||
Future _authorizing;
|
||||
Future? _authorizing;
|
||||
|
||||
//Get headers
|
||||
Map<String, String> get headers => {
|
||||
|
@ -38,7 +38,7 @@ class DeezerAPI {
|
|||
|
||||
//Call private API
|
||||
Future<Map<dynamic, dynamic>> callApi(String method,
|
||||
{Map<dynamic, dynamic> params, String gatewayInput}) async {
|
||||
{Map<dynamic, dynamic>? params, String? gatewayInput}) async {
|
||||
//Generate URL
|
||||
Uri uri = Uri.https('www.deezer.com', '/ajax/gw-light.php', {
|
||||
'api_version': '1.0',
|
||||
|
@ -54,7 +54,7 @@ class DeezerAPI {
|
|||
dynamic body = jsonDecode(res.body);
|
||||
//Grab SID
|
||||
if (method == 'deezer.getUserData') {
|
||||
for (String cookieHeader in res.headers['set-cookie'].split(';')) {
|
||||
for (String cookieHeader in res.headers['set-cookie']!.split(';')) {
|
||||
if (cookieHeader.startsWith('sid=')) {
|
||||
sid = cookieHeader.split('=')[1];
|
||||
}
|
||||
|
@ -69,14 +69,14 @@ class DeezerAPI {
|
|||
return body;
|
||||
}
|
||||
|
||||
Future<Map<dynamic, dynamic>> callPublicApi(String path) async {
|
||||
Future<Map> callPublicApi(String path) async {
|
||||
Uri uri = Uri(scheme: 'https', host: 'api.deezer.com', path: '/' + path);
|
||||
http.Response res = await http.get(uri);
|
||||
return jsonDecode(res.body);
|
||||
}
|
||||
|
||||
//Wrapper so it can be globally awaited
|
||||
Future authorize() async {
|
||||
Future? authorize() async {
|
||||
if (_authorizing == null) {
|
||||
this._authorizing = this.rawAuthorize();
|
||||
}
|
||||
|
@ -84,7 +84,7 @@ class DeezerAPI {
|
|||
}
|
||||
|
||||
//Login with email
|
||||
static Future<String> getArlByEmail(String email, String password) async {
|
||||
static Future<String?> getArlByEmail(String? email, String password) async {
|
||||
//Get MD5 of password
|
||||
Digest digest = md5.convert(utf8.encode(password));
|
||||
String md5password = '$digest';
|
||||
|
@ -92,13 +92,13 @@ class DeezerAPI {
|
|||
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"];
|
||||
String? accessToken = jsonDecode(response.body)["access_token"];
|
||||
//Get SID
|
||||
url = "https://api.deezer.com/platform/generic/track/42069";
|
||||
response = await http
|
||||
.get(Uri.parse(url), headers: {"Authorization": "Bearer $accessToken"});
|
||||
String sid;
|
||||
for (String cookieHeader in response.headers['set-cookie'].split(';')) {
|
||||
String? sid;
|
||||
for (String cookieHeader in response.headers['set-cookie']!.split(';')) {
|
||||
if (cookieHeader.startsWith('sid=')) {
|
||||
sid = cookieHeader.split('=')[1];
|
||||
}
|
||||
|
@ -112,7 +112,7 @@ class DeezerAPI {
|
|||
}
|
||||
|
||||
//Authorize, bool = success
|
||||
Future<bool> rawAuthorize({Function onError}) async {
|
||||
Future<bool> rawAuthorize({Function? onError}) async {
|
||||
try {
|
||||
Map<dynamic, dynamic> data = await callApi('deezer.getUserData');
|
||||
if (data['results']['USER']['USER_ID'] == 0) {
|
||||
|
@ -132,12 +132,12 @@ class DeezerAPI {
|
|||
}
|
||||
|
||||
//URL/Link parser
|
||||
Future<DeezerLinkResponse> parseLink(String url) async {
|
||||
Future<DeezerLinkResponse?> parseLink(String url) async {
|
||||
Uri uri = Uri.parse(url);
|
||||
//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(
|
||||
DeezerLinkType? type = DeezerLinkResponse.typeFromString(
|
||||
uri.pathSegments[uri.pathSegments.length - 2]);
|
||||
return DeezerLinkResponse(
|
||||
type: type, id: uri.pathSegments[uri.pathSegments.length - 1]);
|
||||
|
@ -147,7 +147,7 @@ class DeezerAPI {
|
|||
http.BaseRequest request = http.Request('HEAD', Uri.parse(url));
|
||||
request.followRedirects = false;
|
||||
http.StreamedResponse response = await request.send();
|
||||
String newUrl = response.headers['location'];
|
||||
String newUrl = response.headers['location']!;
|
||||
return parseLink(newUrl);
|
||||
}
|
||||
//Spotify
|
||||
|
@ -170,7 +170,7 @@ class DeezerAPI {
|
|||
}
|
||||
|
||||
//Check if Deezer available in country
|
||||
static Future<bool> chceckAvailability() async {
|
||||
static Future<bool?> chceckAvailability() async {
|
||||
try {
|
||||
http.Response res =
|
||||
await http.get(Uri.parse('https://api.deezer.com/infos'));
|
||||
|
@ -181,13 +181,13 @@ class DeezerAPI {
|
|||
}
|
||||
|
||||
//Search
|
||||
Future<SearchResults> search(String query) async {
|
||||
Future<SearchResults> search(String? query) async {
|
||||
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 {
|
||||
Future<Track> track(String? id) async {
|
||||
Map<dynamic, dynamic> data = await callApi('song.getListData', params: {
|
||||
'sng_ids': [id]
|
||||
});
|
||||
|
@ -195,7 +195,7 @@ class DeezerAPI {
|
|||
}
|
||||
|
||||
//Get album details, tracks
|
||||
Future<Album> album(String id) async {
|
||||
Future<Album> album(String? id) async {
|
||||
Map<dynamic, dynamic> data = await callApi('deezer.pageAlbum', params: {
|
||||
'alb_id': id,
|
||||
'header': true,
|
||||
|
@ -206,7 +206,7 @@ class DeezerAPI {
|
|||
}
|
||||
|
||||
//Get artist details
|
||||
Future<Artist> artist(String id) async {
|
||||
Future<Artist> artist(String? id) async {
|
||||
Map<dynamic, dynamic> data = await callApi('deezer.pageArtist', params: {
|
||||
'art_id': id,
|
||||
'lang': settings.deezerLanguage ?? 'en',
|
||||
|
@ -218,7 +218,7 @@ class DeezerAPI {
|
|||
}
|
||||
|
||||
//Get playlist tracks at offset
|
||||
Future<List<Track>> playlistTracksPage(String id, int start,
|
||||
Future<List<Track>?> playlistTracksPage(String? id, int start,
|
||||
{int nb = 50}) async {
|
||||
Map data = await callApi('deezer.pagePlaylist', params: {
|
||||
'playlist_id': id,
|
||||
|
@ -233,7 +233,7 @@ class DeezerAPI {
|
|||
}
|
||||
|
||||
//Get playlist details
|
||||
Future<Playlist> playlist(String id, {int nb = 100}) async {
|
||||
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',
|
||||
|
@ -246,7 +246,7 @@ class DeezerAPI {
|
|||
}
|
||||
|
||||
//Get playlist with all tracks
|
||||
Future<Playlist> fullPlaylist(String id) async {
|
||||
Future<Playlist> fullPlaylist(String? id) async {
|
||||
return await playlist(id, nb: 100000);
|
||||
}
|
||||
|
||||
|
@ -256,17 +256,17 @@ class DeezerAPI {
|
|||
}
|
||||
|
||||
//Add album to favorites/library
|
||||
Future addFavoriteAlbum(String id) async {
|
||||
Future addFavoriteAlbum(String? id) async {
|
||||
await callApi('album.addFavorite', params: {'ALB_ID': id});
|
||||
}
|
||||
|
||||
//Add artist to favorites/library
|
||||
Future addFavoriteArtist(String id) async {
|
||||
Future addFavoriteArtist(String? id) async {
|
||||
await callApi('artist.addFavorite', params: {'ART_ID': id});
|
||||
}
|
||||
|
||||
//Remove artist from favorites/library
|
||||
Future removeArtist(String id) async {
|
||||
Future removeArtist(String? id) async {
|
||||
await callApi('artist.deleteFavorite', params: {'ART_ID': id});
|
||||
}
|
||||
|
||||
|
@ -276,7 +276,7 @@ class DeezerAPI {
|
|||
}
|
||||
|
||||
//Add tracks to playlist
|
||||
Future addToPlaylist(String trackId, String playlistId,
|
||||
Future addToPlaylist(String trackId, String? playlistId,
|
||||
{int offset = -1}) async {
|
||||
await callApi('playlist.addSongs', params: {
|
||||
'offset': offset,
|
||||
|
@ -288,7 +288,7 @@ class DeezerAPI {
|
|||
}
|
||||
|
||||
//Remove track from playlist
|
||||
Future removeFromPlaylist(String trackId, String playlistId) async {
|
||||
Future removeFromPlaylist(String trackId, String? playlistId) async {
|
||||
await callApi('playlist.deleteSongs', params: {
|
||||
'playlist_id': playlistId,
|
||||
'songs': [
|
||||
|
@ -318,7 +318,7 @@ class DeezerAPI {
|
|||
}
|
||||
|
||||
//Remove album from library
|
||||
Future removeAlbum(String id) async {
|
||||
Future removeAlbum(String? id) async {
|
||||
await callApi('album.deleteFavorite', params: {'ALB_ID': id});
|
||||
}
|
||||
|
||||
|
@ -328,7 +328,7 @@ class DeezerAPI {
|
|||
}
|
||||
|
||||
//Get favorite artists
|
||||
Future<List<Artist>> getArtists() async {
|
||||
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']
|
||||
|
@ -337,21 +337,21 @@ class DeezerAPI {
|
|||
}
|
||||
|
||||
//Get lyrics by track id
|
||||
Future<Lyrics> lyrics(String trackId) async {
|
||||
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();
|
||||
return Lyrics.fromPrivateJson(data['results']);
|
||||
}
|
||||
|
||||
Future<SmartTrackList> smartTrackList(String id) async {
|
||||
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']);
|
||||
}
|
||||
|
||||
Future<List<Track>> flow() async {
|
||||
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))
|
||||
|
@ -410,7 +410,7 @@ class DeezerAPI {
|
|||
});
|
||||
}
|
||||
|
||||
Future<HomePage> getChannel(String target) async {
|
||||
Future<HomePage> getChannel(String? target) async {
|
||||
List grid = [
|
||||
'album',
|
||||
'artist',
|
||||
|
@ -461,15 +461,15 @@ class DeezerAPI {
|
|||
}
|
||||
|
||||
//Delete playlist
|
||||
Future deletePlaylist(String id) async {
|
||||
Future deletePlaylist(String? id) async {
|
||||
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,
|
||||
Future<String> createPlaylist(String? title,
|
||||
{String? description = "",
|
||||
int? status = 1,
|
||||
List<String> trackIds = const []}) async {
|
||||
Map data = await callApi('playlist.create', params: {
|
||||
'title': title,
|
||||
|
@ -484,7 +484,7 @@ class DeezerAPI {
|
|||
}
|
||||
|
||||
//Get part of discography
|
||||
Future<List<Album>> discographyPage(String artistId,
|
||||
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),
|
||||
|
@ -499,14 +499,14 @@ class DeezerAPI {
|
|||
.toList();
|
||||
}
|
||||
|
||||
Future<List> searchSuggestions(String query) async {
|
||||
Future<List?> searchSuggestions(String? query) async {
|
||||
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 {
|
||||
Future<List<Track>?> smartRadio(String artistId) async {
|
||||
Map data = await callApi('smart.getSmartRadio',
|
||||
params: {'art_id': int.parse(artistId)});
|
||||
return data['results']['data']
|
||||
|
@ -516,7 +516,7 @@ class DeezerAPI {
|
|||
|
||||
//Update playlist metadata, status = see createPlaylist
|
||||
Future updatePlaylist(String id, String title, String description,
|
||||
{int status = 1}) async {
|
||||
{int? status = 1}) async {
|
||||
await callApi('playlist.update', params: {
|
||||
'description': description,
|
||||
'title': title,
|
||||
|
@ -527,7 +527,7 @@ class DeezerAPI {
|
|||
}
|
||||
|
||||
//Get shuffled library
|
||||
Future<List<Track>> libraryShuffle({int start = 0}) async {
|
||||
Future<List<Track>?> libraryShuffle({int start = 0}) async {
|
||||
Map data = await callApi('tracklist.getShuffledCollection',
|
||||
params: {'nb': 50, 'start': start});
|
||||
return data['results']['data']
|
||||
|
@ -536,7 +536,7 @@ class DeezerAPI {
|
|||
}
|
||||
|
||||
//Get similar tracks for track with id [trackId]
|
||||
Future<List<Track>> playMix(String trackId) async {
|
||||
Future<List<Track>?> playMix(String? trackId) async {
|
||||
Map data = await callApi('song.getContextualTrackMix', params: {
|
||||
'sng_ids': [trackId]
|
||||
});
|
||||
|
@ -545,14 +545,14 @@ class DeezerAPI {
|
|||
.toList();
|
||||
}
|
||||
|
||||
Future<List<ShowEpisode>> allShowEpisodes(String showId) async {
|
||||
Future<List<ShowEpisode>?> allShowEpisodes(String? showId) async {
|
||||
Map data = await callApi('deezer.pageShow', params: {
|
||||
'country': settings.deezerCountry,
|
||||
'lang': settings.deezerLanguage,
|
||||
'nb': 1000,
|
||||
'show_id': showId,
|
||||
'start': 0,
|
||||
'user_id': int.parse(deezerAPI.userId)
|
||||
'user_id': int.parse(deezerAPI.userId!)
|
||||
});
|
||||
return data['results']['EPISODES']['data']
|
||||
.map<ShowEpisode>((e) => ShowEpisode.fromPrivateJson(e))
|
||||
|
|
|
@ -16,25 +16,25 @@ part 'definitions.g.dart';
|
|||
|
||||
@JsonSerializable()
|
||||
class Track {
|
||||
String/*!*//*!*/ id;
|
||||
String/*!*/ title;
|
||||
Album/*!*/ album;
|
||||
List<Artist>/*!*/ artists;
|
||||
Duration/*!*/ duration;
|
||||
ImageDetails/*!*/ albumArt;
|
||||
int trackNumber;
|
||||
bool/*!*/ offline;
|
||||
Lyrics lyrics;
|
||||
bool favorite;
|
||||
int diskNumber;
|
||||
bool explicit;
|
||||
String id;
|
||||
String? title;
|
||||
Album? album;
|
||||
List<Artist>? artists;
|
||||
Duration? duration;
|
||||
ImageDetails? albumArt;
|
||||
int? trackNumber;
|
||||
bool? offline;
|
||||
Lyrics? lyrics;
|
||||
bool? favorite;
|
||||
int? diskNumber;
|
||||
bool? explicit;
|
||||
//Date added to playlist / favorites
|
||||
int addedDate;
|
||||
int? addedDate;
|
||||
|
||||
List<dynamic> playbackDetails;
|
||||
List<dynamic>? playbackDetails;
|
||||
|
||||
Track(
|
||||
{this.id,
|
||||
{required this.id,
|
||||
this.title,
|
||||
this.duration,
|
||||
this.album,
|
||||
|
@ -49,57 +49,58 @@ class Track {
|
|||
this.explicit,
|
||||
this.addedDate});
|
||||
|
||||
String get artistString => artists.map<String>((art) => art.name).join(', ');
|
||||
String get artistString =>
|
||||
artists!.map<String?>((art) => art.name).join(', ');
|
||||
String get durationString =>
|
||||
"${duration.inMinutes}:${duration.inSeconds.remainder(60).toString().padLeft(2, '0')}";
|
||||
"${duration!.inMinutes}:${duration!.inSeconds.remainder(60).toString().padLeft(2, '0')}";
|
||||
|
||||
//MediaItem
|
||||
MediaItem toMediaItem() => MediaItem(
|
||||
title: this.title,
|
||||
album: this.album.title,
|
||||
artist: this.artists[0].name,
|
||||
title: this.title!,
|
||||
album: this.album!.title!,
|
||||
artist: this.artists![0].name,
|
||||
displayTitle: this.title,
|
||||
displaySubtitle: this.artistString,
|
||||
displayDescription: this.album.title,
|
||||
artUri: Uri.parse(this.albumArt.full),
|
||||
displayDescription: this.album!.title,
|
||||
artUri: Uri.parse(this.albumArt!.full!),
|
||||
duration: this.duration,
|
||||
id: this.id,
|
||||
extras: {
|
||||
"playbackDetails": jsonEncode(this.playbackDetails),
|
||||
"thumb": this.albumArt.thumb,
|
||||
"lyrics": jsonEncode(this.lyrics.toJson()),
|
||||
"albumId": this.album.id,
|
||||
"thumb": this.albumArt!.thumb,
|
||||
"lyrics": jsonEncode(this.lyrics!.toJson()),
|
||||
"albumId": this.album!.id,
|
||||
"artists": jsonEncode(
|
||||
this.artists.map<Map>((art) => art.toJson()).toList())
|
||||
this.artists!.map<Map>((art) => art.toJson()).toList())
|
||||
});
|
||||
|
||||
factory Track.fromMediaItem(MediaItem mi) {
|
||||
//Load album and artists.
|
||||
//It is stored separately, to save id and other metadata
|
||||
Album album = Album(title: mi.album);
|
||||
List<Artist> artists = [Artist(name: mi.displaySubtitle ?? mi.artist)];
|
||||
List<Artist>? artists = [Artist(name: mi.displaySubtitle ?? mi.artist)];
|
||||
if (mi.extras != null) {
|
||||
album.id = mi.extras['albumId'];
|
||||
if (mi.extras['artists'] != null) {
|
||||
artists = jsonDecode(mi.extras['artists'])
|
||||
album.id = mi.extras!['albumId'];
|
||||
if (mi.extras!['artists'] != null) {
|
||||
artists = jsonDecode(mi.extras!['artists'])
|
||||
.map<Artist>((j) => Artist.fromJson(j))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
List<String> playbackDetails;
|
||||
if (mi.extras['playbackDetails'] != null)
|
||||
playbackDetails = (jsonDecode(mi.extras['playbackDetails']) ?? [])
|
||||
List<String>? playbackDetails;
|
||||
if (mi.extras!['playbackDetails'] != null)
|
||||
playbackDetails = (jsonDecode(mi.extras!['playbackDetails']) ?? [])
|
||||
.map<String>((e) => e.toString())
|
||||
.toList();
|
||||
|
||||
return Track(
|
||||
title: mi.title ?? mi.displayTitle,
|
||||
artists: artists,
|
||||
title: mi.title,
|
||||
artists: artists!,
|
||||
album: album,
|
||||
id: mi.id,
|
||||
albumArt: ImageDetails(
|
||||
fullUrl: mi.artUri.toString(), thumbUrl: mi.extras['thumb']),
|
||||
duration: mi.duration,
|
||||
fullUrl: mi.artUri.toString(), thumbUrl: mi.extras!['thumb']),
|
||||
duration: mi.duration!,
|
||||
playbackDetails: playbackDetails,
|
||||
lyrics:
|
||||
Lyrics.fromJson(jsonDecode(((mi.extras ?? {})['lyrics']) ?? "{}")));
|
||||
|
@ -108,13 +109,13 @@ class Track {
|
|||
//JSON
|
||||
factory Track.fromPrivateJson(Map<dynamic, dynamic> json,
|
||||
{bool favorite = false}) {
|
||||
String title = json['SNG_TITLE'];
|
||||
String? title = json['SNG_TITLE'];
|
||||
if (json['VERSION'] != null && json['VERSION'] != '') {
|
||||
title = "${json['SNG_TITLE']} ${json['VERSION']}";
|
||||
}
|
||||
return Track(
|
||||
id: json['SNG_ID'].toString(),
|
||||
title: title,
|
||||
title: title!,
|
||||
duration: Duration(seconds: int.parse(json['DURATION'])),
|
||||
albumArt: ImageDetails.fromPrivateString(json['ALB_PICTURE']),
|
||||
album: Album.fromPrivateJson(json),
|
||||
|
@ -132,13 +133,13 @@ class Track {
|
|||
Map<String, dynamic> toSQL({off = false}) => {
|
||||
'id': id,
|
||||
'title': title,
|
||||
'album': album.id,
|
||||
'artists': artists.map<String>((dynamic a) => a.id).join(','),
|
||||
'duration': duration.inSeconds,
|
||||
'albumArt': albumArt.full,
|
||||
'album': album!.id,
|
||||
'artists': artists!.map<String?>((dynamic a) => a.id).join(','),
|
||||
'duration': duration?.inSeconds,
|
||||
'albumArt': albumArt!.full,
|
||||
'trackNumber': trackNumber,
|
||||
'offline': off ? 1 : 0,
|
||||
'lyrics': jsonEncode(lyrics.toJson()),
|
||||
'lyrics': jsonEncode(lyrics!.toJson()),
|
||||
'favorite': (favorite ?? false) ? 1 : 0,
|
||||
'diskNumber': diskNumber,
|
||||
'explicit': (explicit ?? false) ? 1 : 0,
|
||||
|
@ -169,17 +170,17 @@ enum AlbumType { ALBUM, SINGLE, FEATURED }
|
|||
|
||||
@JsonSerializable()
|
||||
class Album {
|
||||
String id;
|
||||
String title;
|
||||
List<Artist> artists;
|
||||
List<Track> tracks;
|
||||
ImageDetails art;
|
||||
int fans;
|
||||
bool offline; //If the album is offline, or just saved in db as metadata
|
||||
bool library;
|
||||
AlbumType type;
|
||||
String releaseDate;
|
||||
String favoriteDate;
|
||||
String? id;
|
||||
String? title;
|
||||
List<Artist>? artists;
|
||||
List<Track?>? tracks;
|
||||
ImageDetails? art;
|
||||
int? fans;
|
||||
bool? offline; //If the album is offline, or just saved in db as metadata
|
||||
bool? library;
|
||||
AlbumType? type;
|
||||
String? releaseDate;
|
||||
String? favoriteDate;
|
||||
|
||||
Album(
|
||||
{this.id,
|
||||
|
@ -194,9 +195,10 @@ class Album {
|
|||
this.releaseDate,
|
||||
this.favoriteDate});
|
||||
|
||||
String get artistString => artists.map<String>((art) => art.name).join(', ');
|
||||
String get artistString =>
|
||||
artists!.map<String?>((art) => art.name).join(', ');
|
||||
Duration get duration =>
|
||||
Duration(seconds: tracks.fold(0, (v, t) => v += t.duration.inSeconds));
|
||||
Duration(seconds: tracks!.fold(0, (v, t) => v += t!.duration!.inSeconds));
|
||||
String get durationString =>
|
||||
"${duration.inMinutes}:${duration.inSeconds.remainder(60).toString().padLeft(2, '0')}";
|
||||
String get fansString => NumberFormat.compact().format(fans);
|
||||
|
@ -229,13 +231,13 @@ class Album {
|
|||
Map<String, dynamic> toSQL({off = false}) => {
|
||||
'id': id,
|
||||
'title': title,
|
||||
'artists': (artists ?? []).map<String>((dynamic a) => a.id).join(','),
|
||||
'tracks': (tracks ?? []).map<String>((dynamic t) => t.id).join(','),
|
||||
'artists': (artists ?? []).map<String?>((dynamic a) => a.id).join(','),
|
||||
'tracks': (tracks ?? []).map<String?>((dynamic t) => t.id).join(','),
|
||||
'art': art?.full ?? '',
|
||||
'fans': fans,
|
||||
'offline': off ? 1 : 0,
|
||||
'library': (library ?? false) ? 1 : 0,
|
||||
'type': AlbumType.values.indexOf(type),
|
||||
'type': AlbumType.values.indexOf(type!),
|
||||
'releaseDate': releaseDate,
|
||||
//'favoriteDate': favoriteDate
|
||||
};
|
||||
|
@ -264,12 +266,12 @@ enum ArtistHighlightType { ALBUM }
|
|||
@JsonSerializable()
|
||||
class ArtistHighlight {
|
||||
dynamic data;
|
||||
ArtistHighlightType type;
|
||||
String title;
|
||||
ArtistHighlightType? type;
|
||||
String? title;
|
||||
|
||||
ArtistHighlight({this.data, this.type, this.title});
|
||||
|
||||
factory ArtistHighlight.fromPrivateJson(Map<dynamic, dynamic> json) {
|
||||
static ArtistHighlight? fromPrivateJson(Map<dynamic, dynamic>? json) {
|
||||
if (json == null || json['ITEM'] == null) return null;
|
||||
switch (json['TYPE']) {
|
||||
case 'album':
|
||||
|
@ -289,18 +291,18 @@ class ArtistHighlight {
|
|||
|
||||
@JsonSerializable()
|
||||
class Artist {
|
||||
String id;
|
||||
String name;
|
||||
List<Album> albums;
|
||||
int albumCount;
|
||||
List<Track> topTracks;
|
||||
ImageDetails picture;
|
||||
int fans;
|
||||
bool offline;
|
||||
bool library;
|
||||
bool radio;
|
||||
String favoriteDate;
|
||||
ArtistHighlight highlight;
|
||||
String? id;
|
||||
String? name;
|
||||
List<Album>? albums;
|
||||
int? albumCount;
|
||||
List<Track>? topTracks;
|
||||
ImageDetails? picture;
|
||||
int? fans;
|
||||
bool? offline;
|
||||
bool? library;
|
||||
bool? radio;
|
||||
String? favoriteDate;
|
||||
ArtistHighlight? highlight;
|
||||
|
||||
Artist(
|
||||
{this.id,
|
||||
|
@ -322,7 +324,7 @@ class Artist {
|
|||
factory Artist.fromPrivateJson(Map<dynamic, dynamic> json,
|
||||
{Map<dynamic, dynamic> albumsJson = const {},
|
||||
Map<dynamic, dynamic> topJson = const {},
|
||||
Map<dynamic, dynamic> highlight,
|
||||
Map<dynamic, dynamic>? highlight,
|
||||
bool library = false}) {
|
||||
//Get wether radio is available
|
||||
bool _radio = false;
|
||||
|
@ -349,14 +351,14 @@ class Artist {
|
|||
Map<String, dynamic> toSQL({off = false}) => {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'albums': albums.map<String>((dynamic a) => a.id).join(','),
|
||||
'topTracks': topTracks.map<String>((dynamic t) => t.id).join(','),
|
||||
'picture': picture.full,
|
||||
'albums': albums!.map<String?>((dynamic a) => a.id).join(','),
|
||||
'topTracks': topTracks!.map<String?>((dynamic t) => t.id).join(','),
|
||||
'picture': picture!.full,
|
||||
'fans': fans,
|
||||
'albumCount': this.albumCount ?? (this.albums ?? []).length,
|
||||
'offline': off ? 1 : 0,
|
||||
'library': (library ?? false) ? 1 : 0,
|
||||
'radio': radio ? 1 : 0,
|
||||
'radio': radio! ? 1 : 0,
|
||||
//'favoriteDate': favoriteDate
|
||||
};
|
||||
factory Artist.fromSQL(Map<String, dynamic> data) => Artist(
|
||||
|
@ -381,16 +383,16 @@ class Artist {
|
|||
|
||||
@JsonSerializable()
|
||||
class Playlist {
|
||||
String id;
|
||||
String title;
|
||||
List<Track> tracks;
|
||||
ImageDetails image;
|
||||
Duration duration;
|
||||
int trackCount;
|
||||
User user;
|
||||
int fans;
|
||||
bool library;
|
||||
String description;
|
||||
String? id;
|
||||
String? title;
|
||||
List<Track?>? tracks;
|
||||
ImageDetails? image;
|
||||
Duration? duration;
|
||||
int? trackCount;
|
||||
User? user;
|
||||
int? fans;
|
||||
bool? library;
|
||||
String? description;
|
||||
|
||||
Playlist(
|
||||
{this.id,
|
||||
|
@ -405,7 +407,7 @@ class Playlist {
|
|||
this.description});
|
||||
|
||||
String get durationString =>
|
||||
"${duration.inHours}:${duration.inMinutes.remainder(60).toString().padLeft(2, '0')}:${duration.inSeconds.remainder(60).toString().padLeft(2, '0')}";
|
||||
"${duration!.inHours}:${duration!.inMinutes.remainder(60).toString().padLeft(2, '0')}:${duration!.inSeconds.remainder(60).toString().padLeft(2, '0')}";
|
||||
|
||||
//JSON
|
||||
factory Playlist.fromPrivateJson(Map<dynamic, dynamic> json,
|
||||
|
@ -432,11 +434,11 @@ class Playlist {
|
|||
Map<String, dynamic> toSQL() => {
|
||||
'id': id,
|
||||
'title': title,
|
||||
'tracks': tracks.map<String>((dynamic t) => t.id).join(','),
|
||||
'image': image.full,
|
||||
'duration': duration.inSeconds,
|
||||
'userId': user.id,
|
||||
'userName': user.name,
|
||||
'tracks': tracks!.map<String?>((dynamic t) => t.id).join(','),
|
||||
'image': image!.full,
|
||||
'duration': duration!.inSeconds,
|
||||
'userId': user!.id,
|
||||
'userName': user!.name,
|
||||
'fans': fans,
|
||||
'description': description,
|
||||
'library': (library ?? false) ? 1 : 0
|
||||
|
@ -460,9 +462,9 @@ class Playlist {
|
|||
|
||||
@JsonSerializable()
|
||||
class User {
|
||||
String id;
|
||||
String name;
|
||||
ImageDetails picture;
|
||||
String? id;
|
||||
String? name;
|
||||
ImageDetails? picture;
|
||||
|
||||
User({this.id, this.name, this.picture});
|
||||
|
||||
|
@ -475,17 +477,18 @@ class User {
|
|||
// TODO: migrate to Uri instead of String
|
||||
@JsonSerializable()
|
||||
class ImageDetails {
|
||||
String fullUrl;
|
||||
String thumbUrl;
|
||||
String? fullUrl;
|
||||
String? thumbUrl;
|
||||
|
||||
ImageDetails({this.fullUrl, this.thumbUrl});
|
||||
|
||||
//Get full/thumb with fallback
|
||||
String get full => fullUrl ?? thumbUrl;
|
||||
String get thumb => thumbUrl ?? fullUrl;
|
||||
String? get full => fullUrl ?? thumbUrl;
|
||||
String? get thumb => thumbUrl ?? fullUrl;
|
||||
|
||||
//JSON
|
||||
factory ImageDetails.fromPrivateString(String art, {String type = 'cover'}) =>
|
||||
factory ImageDetails.fromPrivateString(String? art,
|
||||
{String? type = 'cover'}) =>
|
||||
ImageDetails(
|
||||
fullUrl:
|
||||
'https://e-cdns-images.dzcdn.net/images/$type/$art/1000x1000-000000-80-0-0.jpg',
|
||||
|
@ -501,12 +504,12 @@ class ImageDetails {
|
|||
}
|
||||
|
||||
class SearchResults {
|
||||
List<Track> tracks;
|
||||
List<Album> albums;
|
||||
List<Artist> artists;
|
||||
List<Playlist> playlists;
|
||||
List<Show> shows;
|
||||
List<ShowEpisode> episodes;
|
||||
List<Track>? tracks;
|
||||
List<Album>? albums;
|
||||
List<Artist>? artists;
|
||||
List<Playlist>? playlists;
|
||||
List<Show>? shows;
|
||||
List<ShowEpisode>? episodes;
|
||||
|
||||
SearchResults(
|
||||
{this.tracks,
|
||||
|
@ -518,12 +521,12 @@ class SearchResults {
|
|||
|
||||
//Check if no search results
|
||||
bool get empty {
|
||||
return ((tracks == null || tracks.length == 0) &&
|
||||
(albums == null || albums.length == 0) &&
|
||||
(artists == null || artists.length == 0) &&
|
||||
(playlists == null || playlists.length == 0) &&
|
||||
(shows == null || shows.length == 0) &&
|
||||
(episodes == null || episodes.length == 0));
|
||||
return ((tracks == null || tracks!.length == 0) &&
|
||||
(albums == null || albums!.length == 0) &&
|
||||
(artists == null || artists!.length == 0) &&
|
||||
(playlists == null || playlists!.length == 0) &&
|
||||
(shows == null || shows!.length == 0) &&
|
||||
(episodes == null || episodes!.length == 0));
|
||||
}
|
||||
|
||||
factory SearchResults.fromPrivateJson(Map<dynamic, dynamic> json) =>
|
||||
|
@ -551,9 +554,9 @@ class SearchResults {
|
|||
|
||||
@JsonSerializable()
|
||||
class Lyrics {
|
||||
String id;
|
||||
String writers;
|
||||
List<Lyric> lyrics;
|
||||
String? id;
|
||||
String? writers;
|
||||
List<Lyric>? lyrics;
|
||||
|
||||
Lyrics({this.id, this.writers, this.lyrics});
|
||||
|
||||
|
@ -572,7 +575,7 @@ class Lyrics {
|
|||
.map<Lyric>((l) => Lyric.fromPrivateJson(l))
|
||||
.toList());
|
||||
//Clean empty lyrics
|
||||
l.lyrics.removeWhere((l) => l.offset == null);
|
||||
l.lyrics!.removeWhere((l) => l.offset == null);
|
||||
return l;
|
||||
}
|
||||
|
||||
|
@ -582,9 +585,9 @@ class Lyrics {
|
|||
|
||||
@JsonSerializable()
|
||||
class Lyric {
|
||||
Duration offset;
|
||||
String text;
|
||||
String lrcTimestamp;
|
||||
Duration? offset;
|
||||
String? text;
|
||||
String? lrcTimestamp;
|
||||
|
||||
Lyric({this.offset, this.text, this.lrcTimestamp});
|
||||
|
||||
|
@ -605,9 +608,9 @@ class Lyric {
|
|||
|
||||
@JsonSerializable()
|
||||
class QueueSource {
|
||||
String id;
|
||||
String text;
|
||||
String source;
|
||||
String? id;
|
||||
String? text;
|
||||
String? source;
|
||||
|
||||
QueueSource({this.id, this.text, this.source});
|
||||
|
||||
|
@ -618,13 +621,13 @@ class QueueSource {
|
|||
|
||||
@JsonSerializable()
|
||||
class SmartTrackList {
|
||||
String id;
|
||||
String title;
|
||||
String subtitle;
|
||||
String description;
|
||||
int trackCount;
|
||||
List<Track> tracks;
|
||||
ImageDetails cover;
|
||||
String? id;
|
||||
String? title;
|
||||
String? subtitle;
|
||||
String? description;
|
||||
int? trackCount;
|
||||
List<Track>? tracks;
|
||||
ImageDetails? cover;
|
||||
|
||||
SmartTrackList(
|
||||
{this.id,
|
||||
|
@ -656,7 +659,7 @@ class SmartTrackList {
|
|||
|
||||
@JsonSerializable()
|
||||
class HomePage {
|
||||
List<HomePageSection> sections;
|
||||
List<HomePageSection>? sections;
|
||||
|
||||
HomePage({this.sections});
|
||||
|
||||
|
@ -666,7 +669,7 @@ class HomePage {
|
|||
return p.join(d.path, 'homescreen.json');
|
||||
}
|
||||
|
||||
Future exists() async {
|
||||
Future<bool> exists() async {
|
||||
String path = await _getPath();
|
||||
return await File(path).exists();
|
||||
}
|
||||
|
@ -679,7 +682,7 @@ class HomePage {
|
|||
Future<HomePage> load() async {
|
||||
String path = await _getPath();
|
||||
Map data = jsonDecode(await File(path).readAsString());
|
||||
return HomePage.fromJson(data);
|
||||
return HomePage.fromJson(data as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
Future wipe() async {
|
||||
|
@ -691,8 +694,8 @@ class HomePage {
|
|||
HomePage hp = HomePage(sections: []);
|
||||
//Parse every section
|
||||
for (var s in (json['sections'] ?? [])) {
|
||||
HomePageSection section = HomePageSection.fromPrivateJson(s);
|
||||
if (section != null) hp.sections.add(section);
|
||||
HomePageSection? section = HomePageSection.fromPrivateJson(s);
|
||||
if (section != null) hp.sections!.add(section);
|
||||
}
|
||||
return hp;
|
||||
}
|
||||
|
@ -704,28 +707,28 @@ class HomePage {
|
|||
|
||||
@JsonSerializable()
|
||||
class HomePageSection {
|
||||
String title;
|
||||
HomePageSectionLayout layout;
|
||||
String? title;
|
||||
HomePageSectionLayout? layout;
|
||||
|
||||
//For loading more items
|
||||
String pagePath;
|
||||
bool hasMore;
|
||||
String? pagePath;
|
||||
bool? hasMore;
|
||||
|
||||
@JsonKey(fromJson: _homePageItemFromJson, toJson: _homePageItemToJson)
|
||||
List<HomePageItem> items;
|
||||
List<HomePageItem>? items;
|
||||
|
||||
HomePageSection(
|
||||
{this.layout, this.items, this.title, this.pagePath, this.hasMore});
|
||||
|
||||
//JSON
|
||||
factory HomePageSection.fromPrivateJson(Map<dynamic, dynamic> json) {
|
||||
static HomePageSection? fromPrivateJson(Map<dynamic, dynamic> json) {
|
||||
HomePageSection hps = HomePageSection(
|
||||
title: json['title'],
|
||||
items: [],
|
||||
pagePath: json['target'],
|
||||
hasMore: json['hasMoreItems'] ?? false);
|
||||
|
||||
String layout = json['layout'];
|
||||
String? layout = json['layout'];
|
||||
switch (layout) {
|
||||
case 'ads':
|
||||
return null;
|
||||
|
@ -741,8 +744,8 @@ class HomePageSection {
|
|||
|
||||
//Parse items
|
||||
for (var i in (json['items'] ?? [])) {
|
||||
HomePageItem hpi = HomePageItem.fromPrivateJson(i);
|
||||
if (hpi != null) hps.items.add(hpi);
|
||||
HomePageItem? hpi = HomePageItem.fromPrivateJson(i);
|
||||
if (hpi != null) hps.items!.add(hpi);
|
||||
}
|
||||
return hps;
|
||||
}
|
||||
|
@ -757,13 +760,13 @@ class HomePageSection {
|
|||
}
|
||||
|
||||
class HomePageItem {
|
||||
HomePageItemType type;
|
||||
HomePageItemType? type;
|
||||
dynamic value;
|
||||
|
||||
HomePageItem({this.type, this.value});
|
||||
|
||||
factory HomePageItem.fromPrivateJson(Map<dynamic, dynamic> json) {
|
||||
String type = json['type'];
|
||||
static HomePageItem? fromPrivateJson(Map<dynamic, dynamic> json) {
|
||||
String? type = json['type'];
|
||||
switch (type) {
|
||||
//Smart Track List
|
||||
case 'flow':
|
||||
|
@ -797,7 +800,7 @@ class HomePageItem {
|
|||
}
|
||||
|
||||
factory HomePageItem.fromJson(Map<String, dynamic> json) {
|
||||
String _t = json['type'];
|
||||
String? _t = json['type'];
|
||||
switch (_t) {
|
||||
case 'SMARTTRACKLIST':
|
||||
return HomePageItem(
|
||||
|
@ -835,29 +838,30 @@ class HomePageItem {
|
|||
|
||||
@JsonSerializable()
|
||||
class DeezerChannel {
|
||||
String id;
|
||||
String target;
|
||||
String title;
|
||||
String? id;
|
||||
String? target;
|
||||
String? title;
|
||||
@JsonKey(fromJson: _colorFromJson, toJson: _colorToJson)
|
||||
Color backgroundColor;
|
||||
|
||||
DeezerChannel({this.id, this.title, this.backgroundColor, this.target});
|
||||
DeezerChannel(
|
||||
{this.id, this.title, this.backgroundColor = Colors.blue, this.target});
|
||||
|
||||
factory DeezerChannel.fromPrivateJson(Map<dynamic, dynamic> json) =>
|
||||
DeezerChannel(
|
||||
id: json['id'],
|
||||
title: json['title'],
|
||||
backgroundColor: Color(int.parse(
|
||||
json['background_color'].replaceFirst('#', 'FF'),
|
||||
radix: 16)),
|
||||
backgroundColor: Color(int.tryParse(
|
||||
json['background_color'].replaceFirst('#', 'FF'),
|
||||
radix: 16) ??
|
||||
Colors.blue.value),
|
||||
target: json['target'].replaceFirst('/', ''));
|
||||
|
||||
//JSON
|
||||
static _colorToJson(Color c) => c.value;
|
||||
static _colorFromJson(int v) => Color(v ?? Colors.blue.value);
|
||||
factory DeezerChannel.fromJson(Map<String, dynamic> json) =>
|
||||
_$DeezerChannelFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$DeezerChannelToJson(this);
|
||||
|
||||
static Color _colorFromJson(int color) => Color(color);
|
||||
static int _colorToJson(Color color) => color.value;
|
||||
}
|
||||
|
||||
enum HomePageItemType { SMARTTRACKLIST, PLAYLIST, ARTIST, CHANNEL, ALBUM, SHOW }
|
||||
|
@ -869,8 +873,8 @@ enum RepeatType { NONE, LIST, TRACK }
|
|||
enum DeezerLinkType { TRACK, ALBUM, ARTIST, PLAYLIST }
|
||||
|
||||
class DeezerLinkResponse {
|
||||
DeezerLinkType type;
|
||||
String id;
|
||||
DeezerLinkType? type;
|
||||
String? id;
|
||||
|
||||
DeezerLinkResponse({this.type, this.id});
|
||||
|
||||
|
@ -910,12 +914,12 @@ enum SortSourceTypes {
|
|||
|
||||
@JsonSerializable()
|
||||
class Sorting {
|
||||
SortType type;
|
||||
bool reverse;
|
||||
SortType? type;
|
||||
bool? reverse;
|
||||
|
||||
//For preserving sorting
|
||||
String id;
|
||||
SortSourceTypes sourceType;
|
||||
String? id;
|
||||
SortSourceTypes? sourceType;
|
||||
|
||||
Sorting(
|
||||
{this.type = SortType.DEFAULT,
|
||||
|
@ -924,19 +928,14 @@ class Sorting {
|
|||
this.sourceType});
|
||||
|
||||
//Find index of sorting from cache
|
||||
static int index(SortSourceTypes type, {String id}) {
|
||||
//Empty cache
|
||||
if (cache.sorts == null) {
|
||||
cache.sorts = [];
|
||||
cache.save();
|
||||
return null;
|
||||
}
|
||||
static int? index(SortSourceTypes type, {String? id}) {
|
||||
//Find index
|
||||
int index;
|
||||
if (id != null)
|
||||
index = cache.sorts.indexWhere((s) => s.sourceType == type && s.id == id);
|
||||
index =
|
||||
cache.sorts.indexWhere((s) => s!.sourceType == type && s.id == id);
|
||||
else
|
||||
index = cache.sorts.indexWhere((s) => s.sourceType == type);
|
||||
index = cache.sorts.indexWhere((s) => s!.sourceType == type);
|
||||
if (index == -1) return null;
|
||||
return index;
|
||||
}
|
||||
|
@ -948,10 +947,10 @@ class Sorting {
|
|||
|
||||
@JsonSerializable()
|
||||
class Show {
|
||||
String name;
|
||||
String description;
|
||||
ImageDetails art;
|
||||
String id;
|
||||
String? name;
|
||||
String? description;
|
||||
ImageDetails? art;
|
||||
String? id;
|
||||
|
||||
Show({this.name, this.description, this.art, this.id});
|
||||
|
||||
|
@ -968,14 +967,14 @@ class Show {
|
|||
|
||||
@JsonSerializable()
|
||||
class ShowEpisode {
|
||||
String id;
|
||||
String title;
|
||||
String description;
|
||||
String url;
|
||||
Duration duration;
|
||||
String publishedDate;
|
||||
String? id;
|
||||
String? title;
|
||||
String? description;
|
||||
String? url;
|
||||
Duration? duration;
|
||||
String? publishedDate;
|
||||
//Might not be fully available
|
||||
Show show;
|
||||
Show? show;
|
||||
|
||||
ShowEpisode(
|
||||
{this.id,
|
||||
|
@ -987,24 +986,24 @@ class ShowEpisode {
|
|||
this.show});
|
||||
|
||||
String get durationString =>
|
||||
"${duration.inMinutes}:${duration.inSeconds.remainder(60).toString().padLeft(2, '0')}";
|
||||
"${duration!.inMinutes}:${duration!.inSeconds.remainder(60).toString().padLeft(2, '0')}";
|
||||
|
||||
//Generate MediaItem for playback
|
||||
MediaItem toMediaItem(Show show) {
|
||||
return MediaItem(
|
||||
title: title,
|
||||
title: title!,
|
||||
displayTitle: title,
|
||||
displaySubtitle: show.name,
|
||||
album: show.name,
|
||||
id: id,
|
||||
album: show.name!,
|
||||
id: id!,
|
||||
extras: {
|
||||
'showUrl': url,
|
||||
'show': jsonEncode(show.toJson()),
|
||||
'thumb': show.art.thumb
|
||||
'thumb': show.art!.thumb
|
||||
},
|
||||
displayDescription: description,
|
||||
duration: duration,
|
||||
artUri: Uri.parse(show.art.full),
|
||||
artUri: Uri.parse(show.art!.full!),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1013,9 +1012,9 @@ class ShowEpisode {
|
|||
id: mi.id,
|
||||
title: mi.title,
|
||||
description: mi.displayDescription,
|
||||
url: mi.extras['showUrl'],
|
||||
url: mi.extras!['showUrl'],
|
||||
duration: mi.duration,
|
||||
show: Show.fromPrivateJson(mi.extras['show']));
|
||||
show: Show.fromPrivateJson(mi.extras!['show']));
|
||||
}
|
||||
|
||||
//JSON
|
||||
|
@ -1035,18 +1034,18 @@ class ShowEpisode {
|
|||
}
|
||||
|
||||
class StreamQualityInfo {
|
||||
String format;
|
||||
int size;
|
||||
String source;
|
||||
String? format;
|
||||
int? size;
|
||||
String? source;
|
||||
|
||||
StreamQualityInfo({this.format, this.size, this.source});
|
||||
|
||||
factory StreamQualityInfo.fromJson(Map json) => StreamQualityInfo(
|
||||
format: json['format'], size: json['size'], source: json['source']);
|
||||
|
||||
int bitrate(Duration duration) {
|
||||
int bitrate(Duration? duration) {
|
||||
if (size == null || size == 0) return 0;
|
||||
int bitrate = (((size * 8) / 1000) / duration.inSeconds).round();
|
||||
int bitrate = (((size! * 8) / 1000) / duration!.inSeconds).round();
|
||||
//Round to known values
|
||||
if (bitrate > 122 && bitrate < 134) return 128;
|
||||
if (bitrate > 315 && bitrate < 325) return 320;
|
||||
|
@ -1063,3 +1062,32 @@ extension Reorder<T> on List<T> {
|
|||
}
|
||||
|
||||
double hypot(num c1, num c2) => sqrt(pow(c1.abs(), 2) + pow(c2.abs(), 2));
|
||||
|
||||
Map<String, dynamic> mediaItemToJson(MediaItem mi) => {
|
||||
'id': mi.id,
|
||||
'title': mi.title,
|
||||
'artUri': mi.artUri?.toString(),
|
||||
'playable': mi.playable,
|
||||
'duration': mi.duration?.inMilliseconds,
|
||||
'extras': mi.extras,
|
||||
'album': mi.album,
|
||||
'artist': mi.artist,
|
||||
'displayTitle': mi.displayTitle,
|
||||
'displaySubtitle': mi.displaySubtitle,
|
||||
'displayDescription': mi.displayDescription,
|
||||
};
|
||||
MediaItem mediaItemFromJson(Map<String, dynamic> json) => MediaItem(
|
||||
id: json['id'],
|
||||
title: json['title'],
|
||||
artUri: json['artUri'] == null ? null : Uri.parse(json['artUri']),
|
||||
playable: json['playable'] as bool,
|
||||
duration: json['duration'] == null
|
||||
? null
|
||||
: Duration(milliseconds: json['duration'] as int),
|
||||
extras: json['extras'] as Map<String, dynamic>,
|
||||
album: json['album'],
|
||||
artist: json['artist'],
|
||||
displayTitle: json['displayTitle'],
|
||||
displaySubtitle: json['displaySubtitle'],
|
||||
displayDescription: json['displayDescription'],
|
||||
);
|
||||
|
|
|
@ -6,35 +6,32 @@ part of 'definitions.dart';
|
|||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
Track _$TrackFromJson(Map<String, dynamic> json) {
|
||||
return Track(
|
||||
id: json['id'] as String,
|
||||
title: json['title'] as String,
|
||||
duration: json['duration'] == null
|
||||
? null
|
||||
: Duration(microseconds: json['duration'] as int),
|
||||
album: json['album'] == null
|
||||
? null
|
||||
: Album.fromJson(json['album'] as Map<String, dynamic>),
|
||||
playbackDetails: json['playbackDetails'] as List,
|
||||
albumArt: json['albumArt'] == null
|
||||
? null
|
||||
: ImageDetails.fromJson(json['albumArt'] as Map<String, dynamic>),
|
||||
artists: (json['artists'] as List)
|
||||
?.map((e) =>
|
||||
e == null ? null : Artist.fromJson(e as Map<String, dynamic>))
|
||||
?.toList(),
|
||||
trackNumber: json['trackNumber'] as int,
|
||||
offline: json['offline'] as bool,
|
||||
lyrics: json['lyrics'] == null
|
||||
? null
|
||||
: Lyrics.fromJson(json['lyrics'] as Map<String, dynamic>),
|
||||
favorite: json['favorite'] as bool,
|
||||
diskNumber: json['diskNumber'] as int,
|
||||
explicit: json['explicit'] as bool,
|
||||
addedDate: json['addedDate'] as int,
|
||||
);
|
||||
}
|
||||
Track _$TrackFromJson(Map<String, dynamic> json) => Track(
|
||||
id: json['id'] as String,
|
||||
title: json['title'] as String?,
|
||||
duration: json['duration'] == null
|
||||
? null
|
||||
: Duration(microseconds: json['duration'] as int),
|
||||
album: json['album'] == null
|
||||
? null
|
||||
: Album.fromJson(json['album'] as Map<String, dynamic>),
|
||||
playbackDetails: json['playbackDetails'] as List<dynamic>?,
|
||||
albumArt: json['albumArt'] == null
|
||||
? null
|
||||
: ImageDetails.fromJson(json['albumArt'] as Map<String, dynamic>),
|
||||
artists: (json['artists'] as List<dynamic>?)
|
||||
?.map((e) => Artist.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
trackNumber: json['trackNumber'] as int?,
|
||||
offline: json['offline'] as bool?,
|
||||
lyrics: json['lyrics'] == null
|
||||
? null
|
||||
: Lyrics.fromJson(json['lyrics'] as Map<String, dynamic>),
|
||||
favorite: json['favorite'] as bool?,
|
||||
diskNumber: json['diskNumber'] as int?,
|
||||
explicit: json['explicit'] as bool?,
|
||||
addedDate: json['addedDate'] as int?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$TrackToJson(Track instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
|
@ -53,29 +50,25 @@ Map<String, dynamic> _$TrackToJson(Track instance) => <String, dynamic>{
|
|||
'playbackDetails': instance.playbackDetails,
|
||||
};
|
||||
|
||||
Album _$AlbumFromJson(Map<String, dynamic> json) {
|
||||
return Album(
|
||||
id: json['id'] as String,
|
||||
title: json['title'] as String,
|
||||
art: json['art'] == null
|
||||
? null
|
||||
: ImageDetails.fromJson(json['art'] as Map<String, dynamic>),
|
||||
artists: (json['artists'] as List)
|
||||
?.map((e) =>
|
||||
e == null ? null : Artist.fromJson(e as Map<String, dynamic>))
|
||||
?.toList(),
|
||||
tracks: (json['tracks'] as List)
|
||||
?.map(
|
||||
(e) => e == null ? null : Track.fromJson(e as Map<String, dynamic>))
|
||||
?.toList(),
|
||||
fans: json['fans'] as int,
|
||||
offline: json['offline'] as bool,
|
||||
library: json['library'] as bool,
|
||||
type: _$enumDecodeNullable(_$AlbumTypeEnumMap, json['type']),
|
||||
releaseDate: json['releaseDate'] as String,
|
||||
favoriteDate: json['favoriteDate'] as String,
|
||||
);
|
||||
}
|
||||
Album _$AlbumFromJson(Map<String, dynamic> json) => Album(
|
||||
id: json['id'] as String?,
|
||||
title: json['title'] as String?,
|
||||
art: json['art'] == null
|
||||
? null
|
||||
: ImageDetails.fromJson(json['art'] as Map<String, dynamic>),
|
||||
artists: (json['artists'] as List<dynamic>?)
|
||||
?.map((e) => Artist.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
tracks: (json['tracks'] as List<dynamic>?)
|
||||
?.map((e) => Track.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
fans: json['fans'] as int?,
|
||||
offline: json['offline'] as bool?,
|
||||
library: json['library'] as bool?,
|
||||
type: _$enumDecodeNullable(_$AlbumTypeEnumMap, json['type']),
|
||||
releaseDate: json['releaseDate'] as String?,
|
||||
favoriteDate: json['favoriteDate'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$AlbumToJson(Album instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
|
@ -91,36 +84,41 @@ Map<String, dynamic> _$AlbumToJson(Album instance) => <String, dynamic>{
|
|||
'favoriteDate': instance.favoriteDate,
|
||||
};
|
||||
|
||||
T _$enumDecode<T>(
|
||||
Map<T, dynamic> enumValues,
|
||||
dynamic source, {
|
||||
T unknownValue,
|
||||
K _$enumDecode<K, V>(
|
||||
Map<K, V> enumValues,
|
||||
Object? source, {
|
||||
K? unknownValue,
|
||||
}) {
|
||||
if (source == null) {
|
||||
throw ArgumentError('A value must be provided. Supported values: '
|
||||
'${enumValues.values.join(', ')}');
|
||||
throw ArgumentError(
|
||||
'A value must be provided. Supported values: '
|
||||
'${enumValues.values.join(', ')}',
|
||||
);
|
||||
}
|
||||
|
||||
final value = enumValues.entries
|
||||
.singleWhere((e) => e.value == source, orElse: () => null)
|
||||
?.key;
|
||||
|
||||
if (value == null && unknownValue == null) {
|
||||
throw ArgumentError('`$source` is not one of the supported values: '
|
||||
'${enumValues.values.join(', ')}');
|
||||
}
|
||||
return value ?? unknownValue;
|
||||
return enumValues.entries.singleWhere(
|
||||
(e) => e.value == source,
|
||||
orElse: () {
|
||||
if (unknownValue == null) {
|
||||
throw ArgumentError(
|
||||
'`$source` is not one of the supported values: '
|
||||
'${enumValues.values.join(', ')}',
|
||||
);
|
||||
}
|
||||
return MapEntry(unknownValue, enumValues.values.first);
|
||||
},
|
||||
).key;
|
||||
}
|
||||
|
||||
T _$enumDecodeNullable<T>(
|
||||
Map<T, dynamic> enumValues,
|
||||
K? _$enumDecodeNullable<K, V>(
|
||||
Map<K, V> enumValues,
|
||||
dynamic source, {
|
||||
T unknownValue,
|
||||
K? unknownValue,
|
||||
}) {
|
||||
if (source == null) {
|
||||
return null;
|
||||
}
|
||||
return _$enumDecode<T>(enumValues, source, unknownValue: unknownValue);
|
||||
return _$enumDecode<K, V>(enumValues, source, unknownValue: unknownValue);
|
||||
}
|
||||
|
||||
const _$AlbumTypeEnumMap = {
|
||||
|
@ -129,13 +127,12 @@ const _$AlbumTypeEnumMap = {
|
|||
AlbumType.FEATURED: 'FEATURED',
|
||||
};
|
||||
|
||||
ArtistHighlight _$ArtistHighlightFromJson(Map<String, dynamic> json) {
|
||||
return ArtistHighlight(
|
||||
data: json['data'],
|
||||
type: _$enumDecodeNullable(_$ArtistHighlightTypeEnumMap, json['type']),
|
||||
title: json['title'] as String,
|
||||
);
|
||||
}
|
||||
ArtistHighlight _$ArtistHighlightFromJson(Map<String, dynamic> json) =>
|
||||
ArtistHighlight(
|
||||
data: json['data'],
|
||||
type: _$enumDecodeNullable(_$ArtistHighlightTypeEnumMap, json['type']),
|
||||
title: json['title'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ArtistHighlightToJson(ArtistHighlight instance) =>
|
||||
<String, dynamic>{
|
||||
|
@ -148,32 +145,28 @@ const _$ArtistHighlightTypeEnumMap = {
|
|||
ArtistHighlightType.ALBUM: 'ALBUM',
|
||||
};
|
||||
|
||||
Artist _$ArtistFromJson(Map<String, dynamic> json) {
|
||||
return Artist(
|
||||
id: json['id'] as String,
|
||||
name: json['name'] as String,
|
||||
albums: (json['albums'] as List)
|
||||
?.map(
|
||||
(e) => e == null ? null : Album.fromJson(e as Map<String, dynamic>))
|
||||
?.toList(),
|
||||
albumCount: json['albumCount'] as int,
|
||||
topTracks: (json['topTracks'] as List)
|
||||
?.map(
|
||||
(e) => e == null ? null : Track.fromJson(e as Map<String, dynamic>))
|
||||
?.toList(),
|
||||
picture: json['picture'] == null
|
||||
? null
|
||||
: ImageDetails.fromJson(json['picture'] as Map<String, dynamic>),
|
||||
fans: json['fans'] as int,
|
||||
offline: json['offline'] as bool,
|
||||
library: json['library'] as bool,
|
||||
radio: json['radio'] as bool,
|
||||
favoriteDate: json['favoriteDate'] as String,
|
||||
highlight: json['highlight'] == null
|
||||
? null
|
||||
: ArtistHighlight.fromJson(json['highlight'] as Map<String, dynamic>),
|
||||
);
|
||||
}
|
||||
Artist _$ArtistFromJson(Map<String, dynamic> json) => Artist(
|
||||
id: json['id'] as String?,
|
||||
name: json['name'] as String?,
|
||||
albums: (json['albums'] as List<dynamic>?)
|
||||
?.map((e) => Album.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
albumCount: json['albumCount'] as int?,
|
||||
topTracks: (json['topTracks'] as List<dynamic>?)
|
||||
?.map((e) => Track.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
picture: json['picture'] == null
|
||||
? null
|
||||
: ImageDetails.fromJson(json['picture'] as Map<String, dynamic>),
|
||||
fans: json['fans'] as int?,
|
||||
offline: json['offline'] as bool?,
|
||||
library: json['library'] as bool?,
|
||||
radio: json['radio'] as bool?,
|
||||
favoriteDate: json['favoriteDate'] as String?,
|
||||
highlight: json['highlight'] == null
|
||||
? null
|
||||
: ArtistHighlight.fromJson(json['highlight'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ArtistToJson(Artist instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
|
@ -190,29 +183,26 @@ Map<String, dynamic> _$ArtistToJson(Artist instance) => <String, dynamic>{
|
|||
'highlight': instance.highlight,
|
||||
};
|
||||
|
||||
Playlist _$PlaylistFromJson(Map<String, dynamic> json) {
|
||||
return Playlist(
|
||||
id: json['id'] as String,
|
||||
title: json['title'] as String,
|
||||
tracks: (json['tracks'] as List)
|
||||
?.map(
|
||||
(e) => e == null ? null : Track.fromJson(e as Map<String, dynamic>))
|
||||
?.toList(),
|
||||
image: json['image'] == null
|
||||
? null
|
||||
: ImageDetails.fromJson(json['image'] as Map<String, dynamic>),
|
||||
trackCount: json['trackCount'] as int,
|
||||
duration: json['duration'] == null
|
||||
? null
|
||||
: Duration(microseconds: json['duration'] as int),
|
||||
user: json['user'] == null
|
||||
? null
|
||||
: User.fromJson(json['user'] as Map<String, dynamic>),
|
||||
fans: json['fans'] as int,
|
||||
library: json['library'] as bool,
|
||||
description: json['description'] as String,
|
||||
);
|
||||
}
|
||||
Playlist _$PlaylistFromJson(Map<String, dynamic> json) => Playlist(
|
||||
id: json['id'] as String?,
|
||||
title: json['title'] as String?,
|
||||
tracks: (json['tracks'] as List<dynamic>?)
|
||||
?.map((e) => Track.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
image: json['image'] == null
|
||||
? null
|
||||
: ImageDetails.fromJson(json['image'] as Map<String, dynamic>),
|
||||
trackCount: json['trackCount'] as int?,
|
||||
duration: json['duration'] == null
|
||||
? null
|
||||
: Duration(microseconds: json['duration'] as int),
|
||||
user: json['user'] == null
|
||||
? null
|
||||
: User.fromJson(json['user'] as Map<String, dynamic>),
|
||||
fans: json['fans'] as int?,
|
||||
library: json['library'] as bool?,
|
||||
description: json['description'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$PlaylistToJson(Playlist instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
|
@ -227,15 +217,13 @@ Map<String, dynamic> _$PlaylistToJson(Playlist instance) => <String, dynamic>{
|
|||
'description': instance.description,
|
||||
};
|
||||
|
||||
User _$UserFromJson(Map<String, dynamic> json) {
|
||||
return User(
|
||||
id: json['id'] as String,
|
||||
name: json['name'] as String,
|
||||
picture: json['picture'] == null
|
||||
? null
|
||||
: ImageDetails.fromJson(json['picture'] as Map<String, dynamic>),
|
||||
);
|
||||
}
|
||||
User _$UserFromJson(Map<String, dynamic> json) => User(
|
||||
id: json['id'] as String?,
|
||||
name: json['name'] as String?,
|
||||
picture: json['picture'] == null
|
||||
? null
|
||||
: ImageDetails.fromJson(json['picture'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$UserToJson(User instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
|
@ -243,12 +231,10 @@ Map<String, dynamic> _$UserToJson(User instance) => <String, dynamic>{
|
|||
'picture': instance.picture,
|
||||
};
|
||||
|
||||
ImageDetails _$ImageDetailsFromJson(Map<String, dynamic> json) {
|
||||
return ImageDetails(
|
||||
fullUrl: json['fullUrl'] as String,
|
||||
thumbUrl: json['thumbUrl'] as String,
|
||||
);
|
||||
}
|
||||
ImageDetails _$ImageDetailsFromJson(Map<String, dynamic> json) => ImageDetails(
|
||||
fullUrl: json['fullUrl'] as String?,
|
||||
thumbUrl: json['thumbUrl'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ImageDetailsToJson(ImageDetails instance) =>
|
||||
<String, dynamic>{
|
||||
|
@ -256,16 +242,13 @@ Map<String, dynamic> _$ImageDetailsToJson(ImageDetails instance) =>
|
|||
'thumbUrl': instance.thumbUrl,
|
||||
};
|
||||
|
||||
Lyrics _$LyricsFromJson(Map<String, dynamic> json) {
|
||||
return Lyrics(
|
||||
id: json['id'] as String,
|
||||
writers: json['writers'] as String,
|
||||
lyrics: (json['lyrics'] as List)
|
||||
?.map(
|
||||
(e) => e == null ? null : Lyric.fromJson(e as Map<String, dynamic>))
|
||||
?.toList(),
|
||||
);
|
||||
}
|
||||
Lyrics _$LyricsFromJson(Map<String, dynamic> json) => Lyrics(
|
||||
id: json['id'] as String?,
|
||||
writers: json['writers'] as String?,
|
||||
lyrics: (json['lyrics'] as List<dynamic>?)
|
||||
?.map((e) => Lyric.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$LyricsToJson(Lyrics instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
|
@ -273,15 +256,13 @@ Map<String, dynamic> _$LyricsToJson(Lyrics instance) => <String, dynamic>{
|
|||
'lyrics': instance.lyrics,
|
||||
};
|
||||
|
||||
Lyric _$LyricFromJson(Map<String, dynamic> json) {
|
||||
return Lyric(
|
||||
offset: json['offset'] == null
|
||||
? null
|
||||
: Duration(microseconds: json['offset'] as int),
|
||||
text: json['text'] as String,
|
||||
lrcTimestamp: json['lrcTimestamp'] as String,
|
||||
);
|
||||
}
|
||||
Lyric _$LyricFromJson(Map<String, dynamic> json) => Lyric(
|
||||
offset: json['offset'] == null
|
||||
? null
|
||||
: Duration(microseconds: json['offset'] as int),
|
||||
text: json['text'] as String?,
|
||||
lrcTimestamp: json['lrcTimestamp'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$LyricToJson(Lyric instance) => <String, dynamic>{
|
||||
'offset': instance.offset?.inMicroseconds,
|
||||
|
@ -289,13 +270,11 @@ Map<String, dynamic> _$LyricToJson(Lyric instance) => <String, dynamic>{
|
|||
'lrcTimestamp': instance.lrcTimestamp,
|
||||
};
|
||||
|
||||
QueueSource _$QueueSourceFromJson(Map<String, dynamic> json) {
|
||||
return QueueSource(
|
||||
id: json['id'] as String,
|
||||
text: json['text'] as String,
|
||||
source: json['source'] as String,
|
||||
);
|
||||
}
|
||||
QueueSource _$QueueSourceFromJson(Map<String, dynamic> json) => QueueSource(
|
||||
id: json['id'] as String?,
|
||||
text: json['text'] as String?,
|
||||
source: json['source'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$QueueSourceToJson(QueueSource instance) =>
|
||||
<String, dynamic>{
|
||||
|
@ -304,22 +283,20 @@ Map<String, dynamic> _$QueueSourceToJson(QueueSource instance) =>
|
|||
'source': instance.source,
|
||||
};
|
||||
|
||||
SmartTrackList _$SmartTrackListFromJson(Map<String, dynamic> json) {
|
||||
return SmartTrackList(
|
||||
id: json['id'] as String,
|
||||
title: json['title'] as String,
|
||||
description: json['description'] as String,
|
||||
trackCount: json['trackCount'] as int,
|
||||
tracks: (json['tracks'] as List)
|
||||
?.map(
|
||||
(e) => e == null ? null : Track.fromJson(e as Map<String, dynamic>))
|
||||
?.toList(),
|
||||
cover: json['cover'] == null
|
||||
? null
|
||||
: ImageDetails.fromJson(json['cover'] as Map<String, dynamic>),
|
||||
subtitle: json['subtitle'] as String,
|
||||
);
|
||||
}
|
||||
SmartTrackList _$SmartTrackListFromJson(Map<String, dynamic> json) =>
|
||||
SmartTrackList(
|
||||
id: json['id'] as String?,
|
||||
title: json['title'] as String?,
|
||||
description: json['description'] as String?,
|
||||
trackCount: json['trackCount'] as int?,
|
||||
tracks: (json['tracks'] as List<dynamic>?)
|
||||
?.map((e) => Track.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
cover: json['cover'] == null
|
||||
? null
|
||||
: ImageDetails.fromJson(json['cover'] as Map<String, dynamic>),
|
||||
subtitle: json['subtitle'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$SmartTrackListToJson(SmartTrackList instance) =>
|
||||
<String, dynamic>{
|
||||
|
@ -332,30 +309,25 @@ Map<String, dynamic> _$SmartTrackListToJson(SmartTrackList instance) =>
|
|||
'cover': instance.cover,
|
||||
};
|
||||
|
||||
HomePage _$HomePageFromJson(Map<String, dynamic> json) {
|
||||
return HomePage(
|
||||
sections: (json['sections'] as List)
|
||||
?.map((e) => e == null
|
||||
? null
|
||||
: HomePageSection.fromJson(e as Map<String, dynamic>))
|
||||
?.toList(),
|
||||
);
|
||||
}
|
||||
HomePage _$HomePageFromJson(Map<String, dynamic> json) => HomePage(
|
||||
sections: (json['sections'] as List<dynamic>?)
|
||||
?.map((e) => HomePageSection.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$HomePageToJson(HomePage instance) => <String, dynamic>{
|
||||
'sections': instance.sections,
|
||||
};
|
||||
|
||||
HomePageSection _$HomePageSectionFromJson(Map<String, dynamic> json) {
|
||||
return HomePageSection(
|
||||
layout:
|
||||
_$enumDecodeNullable(_$HomePageSectionLayoutEnumMap, json['layout']),
|
||||
items: HomePageSection._homePageItemFromJson(json['items']),
|
||||
title: json['title'] as String,
|
||||
pagePath: json['pagePath'] as String,
|
||||
hasMore: json['hasMore'] as bool,
|
||||
);
|
||||
}
|
||||
HomePageSection _$HomePageSectionFromJson(Map<String, dynamic> json) =>
|
||||
HomePageSection(
|
||||
layout:
|
||||
_$enumDecodeNullable(_$HomePageSectionLayoutEnumMap, json['layout']),
|
||||
items: HomePageSection._homePageItemFromJson(json['items']),
|
||||
title: json['title'] as String?,
|
||||
pagePath: json['pagePath'] as String?,
|
||||
hasMore: json['hasMore'] as bool?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$HomePageSectionToJson(HomePageSection instance) =>
|
||||
<String, dynamic>{
|
||||
|
@ -371,15 +343,15 @@ const _$HomePageSectionLayoutEnumMap = {
|
|||
HomePageSectionLayout.GRID: 'GRID',
|
||||
};
|
||||
|
||||
DeezerChannel _$DeezerChannelFromJson(Map<String, dynamic> json) {
|
||||
return DeezerChannel(
|
||||
id: json['id'] as String,
|
||||
title: json['title'] as String,
|
||||
backgroundColor:
|
||||
DeezerChannel._colorFromJson(json['backgroundColor'] as int),
|
||||
target: json['target'] as String,
|
||||
);
|
||||
}
|
||||
DeezerChannel _$DeezerChannelFromJson(Map<String, dynamic> json) =>
|
||||
DeezerChannel(
|
||||
id: json['id'] as String?,
|
||||
title: json['title'] as String?,
|
||||
backgroundColor: json['backgroundColor'] == null
|
||||
? Colors.blue
|
||||
: DeezerChannel._colorFromJson(json['backgroundColor'] as int),
|
||||
target: json['target'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$DeezerChannelToJson(DeezerChannel instance) =>
|
||||
<String, dynamic>{
|
||||
|
@ -389,15 +361,14 @@ Map<String, dynamic> _$DeezerChannelToJson(DeezerChannel instance) =>
|
|||
'backgroundColor': DeezerChannel._colorToJson(instance.backgroundColor),
|
||||
};
|
||||
|
||||
Sorting _$SortingFromJson(Map<String, dynamic> json) {
|
||||
return Sorting(
|
||||
type: _$enumDecodeNullable(_$SortTypeEnumMap, json['type']),
|
||||
reverse: json['reverse'] as bool,
|
||||
id: json['id'] as String,
|
||||
sourceType:
|
||||
_$enumDecodeNullable(_$SortSourceTypesEnumMap, json['sourceType']),
|
||||
);
|
||||
}
|
||||
Sorting _$SortingFromJson(Map<String, dynamic> json) => Sorting(
|
||||
type: _$enumDecodeNullable(_$SortTypeEnumMap, json['type']) ??
|
||||
SortType.DEFAULT,
|
||||
reverse: json['reverse'] as bool? ?? false,
|
||||
id: json['id'] as String?,
|
||||
sourceType:
|
||||
_$enumDecodeNullable(_$SortSourceTypesEnumMap, json['sourceType']),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$SortingToJson(Sorting instance) => <String, dynamic>{
|
||||
'type': _$SortTypeEnumMap[instance.type],
|
||||
|
@ -426,16 +397,14 @@ const _$SortSourceTypesEnumMap = {
|
|||
SortSourceTypes.PLAYLIST: 'PLAYLIST',
|
||||
};
|
||||
|
||||
Show _$ShowFromJson(Map<String, dynamic> json) {
|
||||
return Show(
|
||||
name: json['name'] as String,
|
||||
description: json['description'] as String,
|
||||
art: json['art'] == null
|
||||
? null
|
||||
: ImageDetails.fromJson(json['art'] as Map<String, dynamic>),
|
||||
id: json['id'] as String,
|
||||
);
|
||||
}
|
||||
Show _$ShowFromJson(Map<String, dynamic> json) => Show(
|
||||
name: json['name'] as String?,
|
||||
description: json['description'] as String?,
|
||||
art: json['art'] == null
|
||||
? null
|
||||
: ImageDetails.fromJson(json['art'] as Map<String, dynamic>),
|
||||
id: json['id'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ShowToJson(Show instance) => <String, dynamic>{
|
||||
'name': instance.name,
|
||||
|
@ -444,21 +413,19 @@ Map<String, dynamic> _$ShowToJson(Show instance) => <String, dynamic>{
|
|||
'id': instance.id,
|
||||
};
|
||||
|
||||
ShowEpisode _$ShowEpisodeFromJson(Map<String, dynamic> json) {
|
||||
return ShowEpisode(
|
||||
id: json['id'] as String,
|
||||
title: json['title'] as String,
|
||||
description: json['description'] as String,
|
||||
url: json['url'] as String,
|
||||
duration: json['duration'] == null
|
||||
? null
|
||||
: Duration(microseconds: json['duration'] as int),
|
||||
publishedDate: json['publishedDate'] as String,
|
||||
show: json['show'] == null
|
||||
? null
|
||||
: Show.fromJson(json['show'] as Map<String, dynamic>),
|
||||
);
|
||||
}
|
||||
ShowEpisode _$ShowEpisodeFromJson(Map<String, dynamic> json) => ShowEpisode(
|
||||
id: json['id'] as String?,
|
||||
title: json['title'] as String?,
|
||||
description: json['description'] as String?,
|
||||
url: json['url'] as String?,
|
||||
duration: json['duration'] == null
|
||||
? null
|
||||
: Duration(microseconds: json['duration'] as int),
|
||||
publishedDate: json['publishedDate'] as String?,
|
||||
show: json['show'] == null
|
||||
? null
|
||||
: Show.fromJson(json['show'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ShowEpisodeToJson(ShowEpisode instance) =>
|
||||
<String, dynamic>{
|
||||
|
|
|
@ -25,12 +25,12 @@ class DownloadManager {
|
|||
static EventChannel eventChannel =
|
||||
const EventChannel('f.f.freezer/downloads');
|
||||
|
||||
bool running = false;
|
||||
int queueSize = 0;
|
||||
bool? running = false;
|
||||
int? queueSize = 0;
|
||||
|
||||
StreamController serviceEvents = StreamController.broadcast();
|
||||
String offlinePath;
|
||||
Database db;
|
||||
late String offlinePath;
|
||||
late Database db;
|
||||
|
||||
//Start/Resume downloads
|
||||
Future start() async {
|
||||
|
@ -70,7 +70,7 @@ class DownloadManager {
|
|||
|
||||
//Create offline directory
|
||||
offlinePath =
|
||||
p.join((await getExternalStorageDirectory()).path, 'offline/');
|
||||
p.join((await getExternalStorageDirectory())!.path, 'offline/');
|
||||
await Directory(offlinePath).create(recursive: true);
|
||||
|
||||
//Update settings
|
||||
|
@ -92,7 +92,8 @@ class DownloadManager {
|
|||
|
||||
//Get all downloads from db
|
||||
Future<List<Download>> getDownloads() async {
|
||||
List raw = await platform.invokeMethod('getDownloads');
|
||||
List raw = await (platform.invokeMethod('getDownloads')
|
||||
as FutureOr<List<dynamic>>);
|
||||
return raw.map((d) => Download.fromJson(d)).toList();
|
||||
}
|
||||
|
||||
|
@ -102,10 +103,10 @@ class DownloadManager {
|
|||
conflictAlgorithm: overwriteTrack
|
||||
? ConflictAlgorithm.replace
|
||||
: ConflictAlgorithm.ignore);
|
||||
batch.insert('Albums', track.album.toSQL(off: false),
|
||||
batch.insert('Albums', track.album!.toSQL(off: false),
|
||||
conflictAlgorithm: ConflictAlgorithm.ignore);
|
||||
//Artists
|
||||
for (Artist a in track.artists) {
|
||||
for (Artist a in track.artists!) {
|
||||
batch.insert('Artists', a.toSQL(off: false),
|
||||
conflictAlgorithm: ConflictAlgorithm.ignore);
|
||||
}
|
||||
|
@ -114,7 +115,7 @@ class DownloadManager {
|
|||
|
||||
//Quality selector for custom quality
|
||||
Future qualitySelect(BuildContext context) async {
|
||||
AudioQuality quality;
|
||||
AudioQuality? quality;
|
||||
await showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
|
@ -156,35 +157,35 @@ class DownloadManager {
|
|||
}
|
||||
|
||||
Future<bool> addOfflineTrack(Track track,
|
||||
{private = true, BuildContext context, isSingleton = false}) async {
|
||||
{private = true, BuildContext? context, isSingleton = false}) async {
|
||||
//Permission
|
||||
if (!private && !(await checkPermission())) return false;
|
||||
|
||||
//Ask for quality
|
||||
AudioQuality quality;
|
||||
AudioQuality? quality;
|
||||
if (!private && settings.downloadQuality == AudioQuality.ASK) {
|
||||
quality = await qualitySelect(context);
|
||||
quality = await (qualitySelect(context!) as FutureOr<AudioQuality?>);
|
||||
if (quality == null) return false;
|
||||
}
|
||||
|
||||
//Fetch track if missing meta
|
||||
if (track.artists == null ||
|
||||
track.artists.length == 0 ||
|
||||
track.artists!.length == 0 ||
|
||||
track.album == null) track = await deezerAPI.track(track.id);
|
||||
|
||||
//Add to DB
|
||||
if (private) {
|
||||
Batch b = db.batch();
|
||||
b = await _addTrackToDB(b, track, true);
|
||||
b = await (_addTrackToDB(b, track, true) as FutureOr<Batch>);
|
||||
await b.commit();
|
||||
|
||||
//Cache art
|
||||
DefaultCacheManager().getSingleFile(track.albumArt.thumb);
|
||||
DefaultCacheManager().getSingleFile(track.albumArt.full);
|
||||
DefaultCacheManager().getSingleFile(track.albumArt!.thumb!);
|
||||
DefaultCacheManager().getSingleFile(track.albumArt!.full!);
|
||||
}
|
||||
|
||||
//Get path
|
||||
String path = _generatePath(track, private, isSingleton: isSingleton);
|
||||
String? path = _generatePath(track, private, isSingleton: isSingleton);
|
||||
await platform.invokeMethod('addDownloads', [
|
||||
await Download.jsonFromTrack(track, path,
|
||||
private: private, quality: quality)
|
||||
|
@ -193,50 +194,50 @@ class DownloadManager {
|
|||
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;
|
||||
|
||||
//Ask for quality
|
||||
AudioQuality quality;
|
||||
AudioQuality? quality;
|
||||
if (!private && settings.downloadQuality == AudioQuality.ASK) {
|
||||
quality = await qualitySelect(context);
|
||||
quality = await (qualitySelect(context!) as FutureOr<AudioQuality?>);
|
||||
if (quality == null) return false;
|
||||
}
|
||||
|
||||
//Get from API if no tracks
|
||||
if (album.tracks == null || album.tracks.length == 0) {
|
||||
if (album!.tracks == null || album.tracks!.length == 0) {
|
||||
album = await deezerAPI.album(album.id);
|
||||
}
|
||||
|
||||
//Add to DB
|
||||
if (private) {
|
||||
//Cache art
|
||||
DefaultCacheManager().getSingleFile(album.art.thumb);
|
||||
DefaultCacheManager().getSingleFile(album.art.full);
|
||||
DefaultCacheManager().getSingleFile(album.art!.thumb!);
|
||||
DefaultCacheManager().getSingleFile(album.art!.full!);
|
||||
|
||||
Batch b = db.batch();
|
||||
b.insert('Albums', album.toSQL(off: true),
|
||||
conflictAlgorithm: ConflictAlgorithm.replace);
|
||||
for (Track t in album.tracks) {
|
||||
b = await _addTrackToDB(b, t, false);
|
||||
for (Track? t in album.tracks!) {
|
||||
b = await (_addTrackToDB(b, t!, false) as FutureOr<Batch>);
|
||||
}
|
||||
await b.commit();
|
||||
}
|
||||
|
||||
//Create downloads
|
||||
List<Map> out = [];
|
||||
for (Track t in album.tracks) {
|
||||
out.add(await Download.jsonFromTrack(t, _generatePath(t, private),
|
||||
for (Track? t in album.tracks!) {
|
||||
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;
|
||||
|
||||
|
@ -244,13 +245,13 @@ class DownloadManager {
|
|||
if (!private &&
|
||||
settings.downloadQuality == AudioQuality.ASK &&
|
||||
quality == null) {
|
||||
quality = await qualitySelect(context);
|
||||
quality = await (qualitySelect(context!) as FutureOr<AudioQuality?>);
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -259,19 +260,19 @@ class DownloadManager {
|
|||
Batch b = db.batch();
|
||||
b.insert('Playlists', playlist.toSQL(),
|
||||
conflictAlgorithm: ConflictAlgorithm.replace);
|
||||
for (Track t in playlist.tracks) {
|
||||
b = await _addTrackToDB(b, t, false);
|
||||
for (Track? t in playlist.tracks!) {
|
||||
b = await (_addTrackToDB(b, t!, false) as FutureOr<Batch>);
|
||||
//Cache art
|
||||
DefaultCacheManager().getSingleFile(t.albumArt.thumb);
|
||||
DefaultCacheManager().getSingleFile(t.albumArt.full);
|
||||
DefaultCacheManager().getSingleFile(t.albumArt!.thumb!);
|
||||
DefaultCacheManager().getSingleFile(t.albumArt!.full!);
|
||||
}
|
||||
await b.commit();
|
||||
}
|
||||
|
||||
//Generate downloads
|
||||
List<Map> out = [];
|
||||
for (int i = 0; i < playlist.tracks.length; i++) {
|
||||
Track t = playlist.tracks[i];
|
||||
for (int i = 0; i < playlist.tracks!.length; i++) {
|
||||
Track t = playlist.tracks![i]!;
|
||||
out.add(await Download.jsonFromTrack(
|
||||
t,
|
||||
_generatePath(
|
||||
|
@ -288,8 +289,8 @@ class DownloadManager {
|
|||
}
|
||||
|
||||
//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]);
|
||||
|
@ -297,7 +298,7 @@ class DownloadManager {
|
|||
//Get album
|
||||
if (album == null) {
|
||||
List rawAlbums = await db
|
||||
.query('Albums', where: 'id == ?', whereArgs: [track.album.id]);
|
||||
.query('Albums', where: 'id == ?', whereArgs: [track.album?.id]);
|
||||
if (rawAlbums.length > 0) track.album = Album.fromSQL(rawAlbums[0]);
|
||||
} else {
|
||||
track.album = album;
|
||||
|
@ -306,7 +307,7 @@ class DownloadManager {
|
|||
//Get artists
|
||||
if (artists == null) {
|
||||
List<Artist> newArtists = [];
|
||||
for (Artist artist in track.artists) {
|
||||
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]));
|
||||
|
@ -319,24 +320,24 @@ class DownloadManager {
|
|||
}
|
||||
|
||||
//Get offline library tracks
|
||||
Future<List<Track>> getOfflineTracks() async {
|
||||
Future<List<Track?>> getOfflineTracks() async {
|
||||
List rawTracks = await db.query('Tracks',
|
||||
where: 'library == 1 AND offline == 1', columns: ['id']);
|
||||
List<Track> out = [];
|
||||
List<Track?> out = [];
|
||||
//Load track meta individually
|
||||
for (Map rawTrack in rawTracks) {
|
||||
for (Map rawTrack in rawTracks as Iterable<Map<dynamic, dynamic>>) {
|
||||
out.add(await getOfflineTrack(rawTrack['id']));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
//Get all offline available tracks
|
||||
Future<List<Track>> allOfflineTracks() async {
|
||||
Future<List<Track?>> allOfflineTracks() async {
|
||||
List rawTracks =
|
||||
await db.query('Tracks', where: 'offline == 1', columns: ['id']);
|
||||
List<Track> out = [];
|
||||
List<Track?> out = [];
|
||||
//Load track meta individually
|
||||
for (Map rawTrack in rawTracks) {
|
||||
for (Map rawTrack in rawTracks as Iterable<Map<dynamic, dynamic>>) {
|
||||
out.add(await getOfflineTrack(rawTrack['id']));
|
||||
}
|
||||
return out;
|
||||
|
@ -348,7 +349,7 @@ class DownloadManager {
|
|||
await db.query('Albums', where: 'offline == 1', columns: ['id']);
|
||||
List<Album> out = [];
|
||||
//Load each album
|
||||
for (Map rawAlbum in rawAlbums) {
|
||||
for (Map rawAlbum in rawAlbums as Iterable<Map<dynamic, dynamic>>) {
|
||||
out.add(await getOfflineAlbum(rawAlbum['id']));
|
||||
}
|
||||
return out;
|
||||
|
@ -358,20 +359,20 @@ class DownloadManager {
|
|||
Future<Album> getOfflineAlbum(String id) async {
|
||||
List rawAlbums =
|
||||
await db.query('Albums', where: 'id == ?', whereArgs: [id]);
|
||||
if (rawAlbums.length == 0) return null;
|
||||
if (rawAlbums.length == 0) throw Exception();
|
||||
Album album = Album.fromSQL(rawAlbums[0]);
|
||||
|
||||
List<Track> tracks = [];
|
||||
List<Track?> tracks = [];
|
||||
//Load tracks
|
||||
for (int i = 0; i < album.tracks.length; i++) {
|
||||
tracks.add(await getOfflineTrack(album.tracks[i].id, album: album));
|
||||
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++) {
|
||||
for (int i = 0; i < album.artists!.length; i++) {
|
||||
artists.add(
|
||||
(await getOfflineArtist(album.artists[i].id)) ?? album.artists[i]);
|
||||
(await getOfflineArtist(album.artists![i].id)) ?? album.artists![i]);
|
||||
}
|
||||
album.artists = artists;
|
||||
|
||||
|
@ -379,7 +380,7 @@ class DownloadManager {
|
|||
}
|
||||
|
||||
//Get offline artist METADATA, not tracks
|
||||
Future<Artist> getOfflineArtist(String id) async {
|
||||
Future<Artist?> getOfflineArtist(String? id) async {
|
||||
List rawArtists =
|
||||
await db.query("Artists", where: 'id == ?', whereArgs: [id]);
|
||||
if (rawArtists.length == 0) return null;
|
||||
|
@ -387,35 +388,35 @@ class DownloadManager {
|
|||
}
|
||||
|
||||
//Get all offline playlists
|
||||
Future<List<Playlist>> getOfflinePlaylists() async {
|
||||
Future<List<Playlist?>> getOfflinePlaylists() async {
|
||||
List rawPlaylists = await db.query('Playlists', columns: ['id']);
|
||||
List<Playlist> out = [];
|
||||
for (Map rawPlaylist in rawPlaylists) {
|
||||
List<Playlist?> out = [];
|
||||
for (Map rawPlaylist in rawPlaylists as Iterable<Map<dynamic, dynamic>>) {
|
||||
out.add(await getPlaylist(rawPlaylist['id']));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
//Get offline playlist
|
||||
Future<Playlist> getPlaylist(String id) async {
|
||||
Future<Playlist?> getPlaylist(String? id) async {
|
||||
List rawPlaylists =
|
||||
await db.query('Playlists', where: 'id == ?', whereArgs: [id]);
|
||||
if (rawPlaylists.length == 0) return null;
|
||||
Playlist playlist = Playlist.fromSQL(rawPlaylists[0]);
|
||||
//Load tracks
|
||||
List<Track> tracks = [];
|
||||
for (Track t in playlist.tracks) {
|
||||
tracks.add(await getOfflineTrack(t.id));
|
||||
List<Track?> tracks = [];
|
||||
for (Track? t in playlist.tracks!) {
|
||||
tracks.add(await getOfflineTrack(t!.id));
|
||||
}
|
||||
playlist.tracks = tracks;
|
||||
return playlist;
|
||||
}
|
||||
|
||||
Future removeOfflineTracks(List<Track> tracks) async {
|
||||
for (Track t in tracks) {
|
||||
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']);
|
||||
where: 'id == ?', whereArgs: [t!.id], columns: ['favorite']);
|
||||
if (rawTrack.length > 0) {
|
||||
//Count occurrences in playlists and albums
|
||||
List albums = await db
|
||||
|
@ -441,7 +442,7 @@ class DownloadManager {
|
|||
}
|
||||
}
|
||||
|
||||
Future removeOfflineAlbum(String id) async {
|
||||
Future removeOfflineAlbum(String? id) async {
|
||||
//Get album
|
||||
List rawAlbums =
|
||||
await db.query('Albums', where: 'id == ?', whereArgs: [id]);
|
||||
|
@ -450,10 +451,10 @@ class DownloadManager {
|
|||
//Remove album
|
||||
await db.delete('Albums', where: 'id == ?', whereArgs: [id]);
|
||||
//Remove tracks
|
||||
await removeOfflineTracks(album.tracks);
|
||||
await removeOfflineTracks(album.tracks!);
|
||||
}
|
||||
|
||||
Future removeOfflinePlaylist(String id) async {
|
||||
Future removeOfflinePlaylist(String? id) async {
|
||||
//Fetch playlist
|
||||
List rawPlaylists =
|
||||
await db.query('Playlists', where: 'id == ?', whereArgs: [id]);
|
||||
|
@ -461,12 +462,12 @@ class DownloadManager {
|
|||
Playlist playlist = Playlist.fromSQL(rawPlaylists[0]);
|
||||
//Remove playlist
|
||||
await db.delete('Playlists', where: 'id == ?', whereArgs: [id]);
|
||||
await removeOfflineTracks(playlist.tracks);
|
||||
await removeOfflineTracks(playlist.tracks!);
|
||||
}
|
||||
|
||||
//Check if album, track or playlist is offline
|
||||
Future<bool> checkOffline(
|
||||
{Album album, Track track, Playlist playlist}) async {
|
||||
{Album? album, Track? track, Playlist? playlist}) async {
|
||||
//Track
|
||||
if (track != null) {
|
||||
List res = await db.query('Tracks',
|
||||
|
@ -492,26 +493,26 @@ class DownloadManager {
|
|||
}
|
||||
|
||||
//Offline search
|
||||
Future<SearchResults> search(String query) async {
|
||||
Future<SearchResults> search(String? query) async {
|
||||
SearchResults results =
|
||||
SearchResults(tracks: [], albums: [], artists: [], playlists: []);
|
||||
//Tracks
|
||||
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']));
|
||||
for (Map trackData in tracksData as Iterable<Map<dynamic, dynamic>>) {
|
||||
results.tracks!.add((await getOfflineTrack(trackData['id']))!);
|
||||
}
|
||||
//Albums
|
||||
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']));
|
||||
for (Map rawAlbum in albumsData as Iterable<Map<dynamic, dynamic>>) {
|
||||
results.albums!.add((await getOfflineAlbum(rawAlbum['id'])));
|
||||
}
|
||||
//Playlists
|
||||
List playlists = await db
|
||||
.rawQuery('SELECT * FROM Playlists WHERE title like "%$query%"');
|
||||
for (Map playlist in playlists) {
|
||||
results.playlists.add(await getPlaylist(playlist['id']));
|
||||
for (Map playlist in playlists as Iterable<Map<dynamic, dynamic>>) {
|
||||
results.playlists!.add((await getPlaylist(playlist['id']))!);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
@ -523,33 +524,33 @@ class DownloadManager {
|
|||
}
|
||||
|
||||
//Generate track download path
|
||||
String _generatePath(Track track, bool private,
|
||||
{String playlistName,
|
||||
int playlistTrackNumber,
|
||||
String? _generatePath(Track? track, bool private,
|
||||
{String? playlistName,
|
||||
int? playlistTrackNumber,
|
||||
bool isSingleton = false}) {
|
||||
String path;
|
||||
String? path;
|
||||
if (private) {
|
||||
path = p.join(offlinePath, track.id);
|
||||
path = p.join(offlinePath, track!.id);
|
||||
} else {
|
||||
//Download path
|
||||
path = settings.downloadPath;
|
||||
|
||||
if (settings.playlistFolder && playlistName != null)
|
||||
path = p.join(path, sanitize(playlistName));
|
||||
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());
|
||||
if (settings.albumFolder!) {
|
||||
if (settings.albumDiscFolder!) {
|
||||
path = p.join(path!,
|
||||
'%album%' + ' - Disk ' + (track!.diskNumber ?? 1).toString());
|
||||
} else {
|
||||
path = p.join(path, '%album%');
|
||||
path = p.join(path!, '%album%');
|
||||
}
|
||||
}
|
||||
//Final path
|
||||
path = p.join(path,
|
||||
path = p.join(path!,
|
||||
isSingleton ? settings.singletonFilename : settings.downloadFilename);
|
||||
//Playlist track number variable (not accessible in service)
|
||||
if (playlistTrackNumber != null) {
|
||||
|
@ -568,16 +569,16 @@ class DownloadManager {
|
|||
//Get stats for library screen
|
||||
Future<List<String>> getStats() async {
|
||||
//Get offline counts
|
||||
int trackCount =
|
||||
int? trackCount =
|
||||
(await db.rawQuery('SELECT COUNT(*) FROM Tracks WHERE offline == 1'))[0]
|
||||
['COUNT(*)'];
|
||||
int albumCount =
|
||||
['COUNT(*)'] as int?;
|
||||
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(*)'];
|
||||
['COUNT(*)'] as int?;
|
||||
int? playlistCount = (await db
|
||||
.rawQuery('SELECT COUNT(*) FROM Playlists'))[0]['COUNT(*)'] as int?;
|
||||
//Free space
|
||||
double diskSpace = await DiskSpace.getFreeDiskSpace;
|
||||
double diskSpace = await (DiskSpace.getFreeDiskSpace as FutureOr<double>);
|
||||
//Used space
|
||||
List<FileSystemEntity> offlineStat =
|
||||
await Directory(offlinePath).list().toList();
|
||||
|
@ -615,7 +616,7 @@ class DownloadManager {
|
|||
}
|
||||
|
||||
//Remove download from queue/finished
|
||||
Future removeDownload(int id) async {
|
||||
Future removeDownload(int? id) async {
|
||||
await platform.invokeMethod('removeDownload', {'id': id});
|
||||
}
|
||||
|
||||
|
@ -630,24 +631,24 @@ class DownloadManager {
|
|||
'removeDownloads', {'state': DownloadState.values.indexOf(state)});
|
||||
}
|
||||
|
||||
static Future<String> getDirectory(String title) =>
|
||||
static Future<String?> getDirectory(String title) =>
|
||||
platform.invokeMethod('getDirectory', <String, String>{'title': title});
|
||||
}
|
||||
|
||||
class Download {
|
||||
int id;
|
||||
String path;
|
||||
bool private;
|
||||
String trackId;
|
||||
String md5origin;
|
||||
String mediaVersion;
|
||||
String title;
|
||||
String image;
|
||||
int quality;
|
||||
int? id;
|
||||
String? path;
|
||||
bool? private;
|
||||
String? trackId;
|
||||
String? md5origin;
|
||||
String? mediaVersion;
|
||||
String? title;
|
||||
String? image;
|
||||
int? quality;
|
||||
//Dynamic
|
||||
DownloadState state;
|
||||
int received;
|
||||
int filesize;
|
||||
DownloadState? state;
|
||||
int? received;
|
||||
int? filesize;
|
||||
|
||||
Download(
|
||||
{this.id,
|
||||
|
@ -665,7 +666,7 @@ class Download {
|
|||
|
||||
//Get progress between 0 - 1
|
||||
double get progress {
|
||||
return ((received.toDouble() ?? 0.0) / (filesize.toDouble() ?? 1.0))
|
||||
return ((received?.toDouble() ?? 0.0) / (filesize?.toDouble() ?? 1.0))
|
||||
.toDouble();
|
||||
}
|
||||
|
||||
|
@ -692,8 +693,8 @@ class Download {
|
|||
}
|
||||
|
||||
//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);
|
||||
|
@ -701,14 +702,14 @@ class Download {
|
|||
return {
|
||||
"private": private,
|
||||
"trackId": t.id,
|
||||
"md5origin": t.playbackDetails[0],
|
||||
"mediaVersion": t.playbackDetails[1],
|
||||
"md5origin": t.playbackDetails![0],
|
||||
"mediaVersion": t.playbackDetails![1],
|
||||
"quality": private
|
||||
? settings.getQualityInt(settings.offlineQuality)
|
||||
: settings.getQualityInt((quality ?? settings.downloadQuality)),
|
||||
"title": t.title,
|
||||
"path": path,
|
||||
"image": t.albumArt.thumb
|
||||
"image": t.albumArt?.thumb
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,35 +12,42 @@ class Importer {
|
|||
bool download = false;
|
||||
|
||||
//Preserve context
|
||||
BuildContext context;
|
||||
String title;
|
||||
String description;
|
||||
List<ImporterTrack> tracks;
|
||||
String playlistId;
|
||||
Playlist playlist;
|
||||
BuildContext? context;
|
||||
String? title;
|
||||
String? description;
|
||||
late List<ImporterTrack> tracks;
|
||||
String? playlistId;
|
||||
Playlist? playlist;
|
||||
|
||||
bool done = false;
|
||||
bool busy = false;
|
||||
Future _future;
|
||||
StreamController _streamController;
|
||||
Future? _future;
|
||||
late StreamController _streamController;
|
||||
|
||||
Stream get updateStream => _streamController.stream;
|
||||
int get ok => tracks.fold(0, (v, t) => (t.state == TrackImportState.OK) ? v+1 : v);
|
||||
int get error => tracks.fold(0, (v, t) => (t.state == TrackImportState.ERROR) ? v+1 : v);
|
||||
int get ok =>
|
||||
tracks.fold(0, (v, t) => (t.state == TrackImportState.OK) ? v + 1 : v);
|
||||
int get error =>
|
||||
tracks.fold(0, (v, t) => (t.state == TrackImportState.ERROR) ? v + 1 : v);
|
||||
|
||||
Importer();
|
||||
|
||||
//Start importing wrapper
|
||||
Future<void> start(BuildContext context, String title, String description, List<ImporterTrack> tracks) async {
|
||||
Future<void> start(BuildContext context, String? title, String? description,
|
||||
List<ImporterTrack> tracks) async {
|
||||
//Save variables
|
||||
this.playlist = null;
|
||||
this.context = context;
|
||||
this.title = title;
|
||||
this.description = description??'';
|
||||
this.tracks = tracks.map((t) {t.state = TrackImportState.NONE; return t;}).toList();
|
||||
this.description = description ?? '';
|
||||
this.tracks = tracks.map((t) {
|
||||
t.state = TrackImportState.NONE;
|
||||
return t;
|
||||
}).toList();
|
||||
|
||||
//Create playlist
|
||||
playlistId = await deezerAPI.createPlaylist(title, description: description);
|
||||
playlistId =
|
||||
await deezerAPI.createPlaylist(title, description: description);
|
||||
|
||||
busy = true;
|
||||
done = false;
|
||||
|
@ -50,9 +57,9 @@ class Importer {
|
|||
|
||||
//Start importer
|
||||
Future _start() async {
|
||||
for (int i=0; i<tracks.length; i++) {
|
||||
for (int i = 0; i < tracks.length; i++) {
|
||||
try {
|
||||
String id = await _searchTrack(tracks[i]);
|
||||
String? id = await _searchTrack(tracks[i]);
|
||||
//Not found
|
||||
if (id == null) {
|
||||
tracks[i].state = TrackImportState.ERROR;
|
||||
|
@ -70,11 +77,12 @@ class Importer {
|
|||
}
|
||||
//Get full playlist
|
||||
playlist = await deezerAPI.playlist(playlistId, nb: 10000);
|
||||
playlist.library = true;
|
||||
playlist!.library = true;
|
||||
|
||||
//Download
|
||||
if (download) {
|
||||
await downloadManager.addOfflinePlaylist(playlist, private: false, context: context);
|
||||
await downloadManager.addOfflinePlaylist(playlist,
|
||||
private: false, context: context);
|
||||
}
|
||||
|
||||
//Mark as done
|
||||
|
@ -86,23 +94,30 @@ class Importer {
|
|||
}
|
||||
|
||||
//Find track on Deezer servers
|
||||
Future<String> _searchTrack(ImporterTrack track) async {
|
||||
Future<String?> _searchTrack(ImporterTrack track) async {
|
||||
//Try by ISRC
|
||||
if (track.isrc != null && track.isrc.length == 12) {
|
||||
Map deezer = await deezerAPI.callPublicApi('track/isrc:' + track.isrc);
|
||||
if (track.isrc != null && track.isrc!.length == 12) {
|
||||
Map deezer = (await deezerAPI.callPublicApi('track/isrc:' + track.isrc!));
|
||||
if (deezer["id"] != null) {
|
||||
return deezer["id"].toString();
|
||||
}
|
||||
}
|
||||
|
||||
//Search
|
||||
String cleanedTitle = track.title.trim().toLowerCase().replaceAll("-", "").replaceAll("&", "").replaceAll("+", "");
|
||||
SearchResults results = await deezerAPI.search("${track.artists[0]} $cleanedTitle");
|
||||
for (Track t in results.tracks) {
|
||||
String cleanedTitle = track.title!
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replaceAll("-", "")
|
||||
.replaceAll("&", "")
|
||||
.replaceAll("+", "");
|
||||
SearchResults results =
|
||||
await deezerAPI.search("${track.artists![0]} $cleanedTitle");
|
||||
for (Track t in results.tracks!) {
|
||||
//Match title
|
||||
if (_cleanMatching(t.title) == _cleanMatching(track.title)) {
|
||||
if (_cleanMatching(t.title!) == _cleanMatching(track.title!)) {
|
||||
//Match artist
|
||||
if (_matchArtists(track.artists, t.artists.map((a) => a.name))) {
|
||||
if (_matchArtists(
|
||||
track.artists!, t.artists!.map((a) => a.name) as List<String?>)) {
|
||||
return t.id;
|
||||
}
|
||||
}
|
||||
|
@ -111,23 +126,22 @@ class Importer {
|
|||
|
||||
//Clean title for matching
|
||||
String _cleanMatching(String t) {
|
||||
return t.toLowerCase()
|
||||
.replaceAll(",", "")
|
||||
.replaceAll("-", "")
|
||||
.replaceAll(" ", "")
|
||||
.replaceAll("&", "")
|
||||
.replaceAll("+", "")
|
||||
.replaceAll("/", "");
|
||||
return t
|
||||
.toLowerCase()
|
||||
.replaceAll(",", "")
|
||||
.replaceAll("-", "")
|
||||
.replaceAll(" ", "")
|
||||
.replaceAll("&", "")
|
||||
.replaceAll("+", "")
|
||||
.replaceAll("/", "");
|
||||
}
|
||||
|
||||
String _cleanArtist(String a) {
|
||||
return a.toLowerCase()
|
||||
.replaceAll(" ", "")
|
||||
.replaceAll(",", "");
|
||||
String _cleanArtist(String? a) {
|
||||
return a!.toLowerCase().replaceAll(" ", "").replaceAll(",", "");
|
||||
}
|
||||
|
||||
//Match at least 1 artist
|
||||
bool _matchArtists(List<String> a, List<String> b) {
|
||||
bool _matchArtists(List<String?> a, List<String?> b) {
|
||||
//Clean
|
||||
List<String> _a = a.map(_cleanArtist).toList();
|
||||
List<String> _b = b.map(_cleanArtist).toList();
|
||||
|
@ -139,33 +153,32 @@ class Importer {
|
|||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class ImporterTrack {
|
||||
String title;
|
||||
List<String> artists;
|
||||
String isrc;
|
||||
String? title;
|
||||
List<String?>? artists;
|
||||
String? isrc;
|
||||
TrackImportState state;
|
||||
|
||||
ImporterTrack(this.title, this.artists, {this.isrc, this.state = TrackImportState.NONE});
|
||||
ImporterTrack(this.title, this.artists,
|
||||
{this.isrc, this.state = TrackImportState.NONE});
|
||||
}
|
||||
|
||||
enum TrackImportState {
|
||||
NONE,
|
||||
ERROR,
|
||||
OK
|
||||
}
|
||||
enum TrackImportState { NONE, ERROR, OK }
|
||||
|
||||
extension TrackImportStateExtension on TrackImportState {
|
||||
Widget get icon {
|
||||
switch (this) {
|
||||
case TrackImportState.ERROR:
|
||||
return Icon(Icons.error, color: Colors.red,);
|
||||
return Icon(
|
||||
Icons.error,
|
||||
color: Colors.red,
|
||||
);
|
||||
case TrackImportState.OK:
|
||||
return Icon(Icons.done, color: Colors.green);
|
||||
default:
|
||||
return Container(width: 0, height: 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -12,7 +12,7 @@ import 'package:url_launcher/url_launcher.dart';
|
|||
|
||||
class SpotifyScrapper {
|
||||
//Parse spotify URL to URI (spotify:track:1234)
|
||||
static String parseUrl(String url) {
|
||||
static String? parseUrl(String url) {
|
||||
Uri uri = Uri.parse(url);
|
||||
if (uri.pathSegments.length > 3) return null; //Invalid URL
|
||||
if (uri.pathSegments.length == 3)
|
||||
|
@ -27,14 +27,14 @@ class SpotifyScrapper {
|
|||
'https://embed.spotify.com/?uri=$uri';
|
||||
|
||||
//https://link.tospotify.com/ or https://spotify.app.link/
|
||||
static Future resolveLinkUrl(String url) async {
|
||||
static Future<String> resolveLinkUrl(String url) async {
|
||||
http.Response response = await http.get(Uri.parse(url));
|
||||
Match match = RegExp(r'window\.top\.location = validate\("(.+)"\);')
|
||||
.firstMatch(response.body);
|
||||
return match.group(1);
|
||||
.firstMatch(response.body)!;
|
||||
return match.group(1)!;
|
||||
}
|
||||
|
||||
static Future resolveUrl(String url) async {
|
||||
static Future<String?> resolveUrl(String url) async {
|
||||
if (url.contains("link.tospotify") || url.contains("spotify.app.link")) {
|
||||
return parseUrl(await resolveLinkUrl(url));
|
||||
}
|
||||
|
@ -47,7 +47,7 @@ class SpotifyScrapper {
|
|||
http.Response response = await http.get(Uri.parse(url));
|
||||
//Parse
|
||||
dom.Document document = parse(response.body);
|
||||
dom.Element element = document.getElementById('resource');
|
||||
dom.Element element = document.getElementById('resource')!;
|
||||
|
||||
//Some are URL encoded
|
||||
try {
|
||||
|
@ -70,7 +70,7 @@ class SpotifyScrapper {
|
|||
static Future<String> convertTrack(String uri) async {
|
||||
Map data = await getEmbedData(getEmbedUrl(uri));
|
||||
SpotifyTrack track = SpotifyTrack.fromJson(data);
|
||||
Map deezer = await deezerAPI.callPublicApi('track/isrc:' + track.isrc);
|
||||
Map deezer = await deezerAPI.callPublicApi('track/isrc:' + track.isrc!);
|
||||
return deezer['id'].toString();
|
||||
}
|
||||
|
||||
|
@ -78,15 +78,15 @@ class SpotifyScrapper {
|
|||
static Future<String> convertAlbum(String uri) async {
|
||||
Map data = await getEmbedData(getEmbedUrl(uri));
|
||||
SpotifyAlbum album = SpotifyAlbum.fromJson(data);
|
||||
Map deezer = await deezerAPI.callPublicApi('album/upc:' + album.upc);
|
||||
Map deezer = await deezerAPI.callPublicApi('album/upc:' + album.upc!);
|
||||
return deezer['id'].toString();
|
||||
}
|
||||
}
|
||||
|
||||
class SpotifyTrack {
|
||||
String title;
|
||||
List<String> artists;
|
||||
String isrc;
|
||||
String? title;
|
||||
List<String>? artists;
|
||||
String? isrc;
|
||||
|
||||
SpotifyTrack({this.title, this.artists, this.isrc});
|
||||
|
||||
|
@ -104,10 +104,10 @@ class SpotifyTrack {
|
|||
}
|
||||
|
||||
class SpotifyPlaylist {
|
||||
String name;
|
||||
String description;
|
||||
List<SpotifyTrack> tracks;
|
||||
String image;
|
||||
String? name;
|
||||
String? description;
|
||||
List<SpotifyTrack>? tracks;
|
||||
String? image;
|
||||
|
||||
SpotifyPlaylist({this.name, this.description, this.tracks, this.image});
|
||||
|
||||
|
@ -122,12 +122,12 @@ class SpotifyPlaylist {
|
|||
|
||||
//Convert to importer tracks
|
||||
List<ImporterTrack> toImporter() {
|
||||
return tracks.map((t) => t.toImporter()).toList();
|
||||
return tracks!.map((t) => t.toImporter()).toList();
|
||||
}
|
||||
}
|
||||
|
||||
class SpotifyAlbum {
|
||||
String upc;
|
||||
String? upc;
|
||||
|
||||
SpotifyAlbum({this.upc});
|
||||
|
||||
|
@ -137,9 +137,9 @@ class SpotifyAlbum {
|
|||
}
|
||||
|
||||
class SpotifyAPIWrapper {
|
||||
HttpServer _server;
|
||||
SpotifyApi spotify;
|
||||
User me;
|
||||
late HttpServer _server;
|
||||
late SpotifyApi spotify;
|
||||
late User me;
|
||||
|
||||
//Try authorize with saved credentials
|
||||
Future<bool> trySaved() async {
|
||||
|
@ -149,24 +149,24 @@ class SpotifyAPIWrapper {
|
|||
settings.spotifyCredentials == null) return false;
|
||||
final credentials = SpotifyApiCredentials(
|
||||
settings.spotifyClientId, settings.spotifyClientSecret,
|
||||
accessToken: settings.spotifyCredentials.accessToken,
|
||||
refreshToken: settings.spotifyCredentials.refreshToken,
|
||||
scopes: settings.spotifyCredentials.scopes,
|
||||
expiration: settings.spotifyCredentials.expiration);
|
||||
accessToken: settings.spotifyCredentials!.accessToken,
|
||||
refreshToken: settings.spotifyCredentials!.refreshToken,
|
||||
scopes: settings.spotifyCredentials!.scopes,
|
||||
expiration: settings.spotifyCredentials!.expiration);
|
||||
spotify = SpotifyApi(credentials);
|
||||
me = await spotify.me.get();
|
||||
await _save();
|
||||
return true;
|
||||
}
|
||||
|
||||
Future authorize(String clientId, String clientSecret) async {
|
||||
Future authorize(String? clientId, String? clientSecret) async {
|
||||
//Spotify
|
||||
SpotifyApiCredentials credentials =
|
||||
SpotifyApiCredentials(clientId, clientSecret);
|
||||
spotify = SpotifyApi(credentials);
|
||||
//Create server
|
||||
_server = await HttpServer.bind(InternetAddress.loopbackIPv4, 42069);
|
||||
String responseUri;
|
||||
late String responseUri;
|
||||
//Get URL
|
||||
final grant = SpotifyApi.authorizationCodeGrant(credentials);
|
||||
final redirectUri = "http://localhost:42069";
|
||||
|
@ -189,7 +189,6 @@ class SpotifyAPIWrapper {
|
|||
//Get token
|
||||
if (request.uri.queryParameters["code"] != null) {
|
||||
_server.close();
|
||||
_server = null;
|
||||
responseUri = request.uri.toString();
|
||||
break;
|
||||
}
|
||||
|
@ -218,9 +217,6 @@ class SpotifyAPIWrapper {
|
|||
|
||||
//Cancel authorization
|
||||
void cancelAuthorize() {
|
||||
if (_server != null) {
|
||||
_server.close(force: true);
|
||||
_server = null;
|
||||
}
|
||||
_server.close(force: true);
|
||||
}
|
||||
}
|
||||
|
|
195
lib/main.dart
195
lib/main.dart
|
@ -26,12 +26,10 @@ import 'settings.dart';
|
|||
import 'ui/home_screen.dart';
|
||||
import 'ui/player_bar.dart';
|
||||
|
||||
Function updateTheme;
|
||||
Function logOut;
|
||||
late Function updateTheme;
|
||||
late Function logOut;
|
||||
GlobalKey<NavigatorState> mainNavigatorKey = GlobalKey<NavigatorState>();
|
||||
GlobalKey<NavigatorState> navigatorKey;
|
||||
|
||||
// TODO: migrate to null-safety
|
||||
GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
@ -44,6 +42,20 @@ void main() async {
|
|||
//Do on BG
|
||||
playerHelper.authorizeLastFM();
|
||||
|
||||
// initialize our audiohandler instance
|
||||
audioHandler = await AudioService.init<AudioPlayerTask>(
|
||||
builder: () => AudioPlayerTask(),
|
||||
config: AudioServiceConfig(
|
||||
androidStopForegroundOnPause: false,
|
||||
androidNotificationOngoing: false,
|
||||
androidNotificationClickStartsActivity: true,
|
||||
androidNotificationChannelDescription: 'Freezer',
|
||||
androidNotificationChannelName: 'Freezer',
|
||||
androidNotificationIcon: 'drawable/ic_logo',
|
||||
preloadArtwork: true,
|
||||
),
|
||||
);
|
||||
|
||||
runApp(FreezerApp());
|
||||
}
|
||||
|
||||
|
@ -71,17 +83,17 @@ class _FreezerAppState extends State<FreezerApp> {
|
|||
settings.themeData;
|
||||
});
|
||||
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
|
||||
systemNavigationBarColor: settings.themeData.bottomAppBarColor,
|
||||
systemNavigationBarColor: settings.themeData!.bottomAppBarColor,
|
||||
systemNavigationBarIconBrightness:
|
||||
settings.isDark ? Brightness.light : Brightness.dark,
|
||||
));
|
||||
}
|
||||
|
||||
Locale _locale() {
|
||||
if (settings.language == null || settings.language.split('_').length < 2)
|
||||
Locale? _locale() {
|
||||
if (settings.language == null || settings.language!.split('_').length < 2)
|
||||
return null;
|
||||
return Locale(
|
||||
settings.language.split('_')[0], settings.language.split('_')[1]);
|
||||
settings.language!.split('_')[0], settings.language!.split('_')[1]);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -104,9 +116,8 @@ class _FreezerAppState extends State<FreezerApp> {
|
|||
supportedLocales: supportedLocales,
|
||||
home: WillPopScope(
|
||||
onWillPop: () async {
|
||||
//For some reason AudioServiceWidget caused the app to freeze after 2 back button presses. "fix"
|
||||
if (navigatorKey.currentState.canPop()) {
|
||||
await navigatorKey.currentState.maybePop();
|
||||
if (navigatorKey.currentState!.canPop()) {
|
||||
await navigatorKey.currentState!.maybePop();
|
||||
return false;
|
||||
}
|
||||
await MoveToBackground.moveTaskToBack();
|
||||
|
@ -137,7 +148,7 @@ class _LoginMainWrapperState extends State<LoginMainWrapper> {
|
|||
//Load token on background
|
||||
deezerAPI.arl = settings.arl;
|
||||
settings.offlineMode = true;
|
||||
deezerAPI.authorize().then((b) async {
|
||||
deezerAPI.authorize()!.then((b) async {
|
||||
if (b) setState(() => settings.offlineMode = false);
|
||||
});
|
||||
}
|
||||
|
@ -175,20 +186,18 @@ class MainScreen extends StatefulWidget {
|
|||
class _MainScreenState extends State<MainScreen>
|
||||
with SingleTickerProviderStateMixin, WidgetsBindingObserver {
|
||||
List<Widget> _screens = [HomeScreen(), SearchScreen(), LibraryScreen()];
|
||||
int _selected = 0;
|
||||
StreamSubscription _urlLinkStream;
|
||||
final _selected = ValueNotifier<int>(0);
|
||||
StreamSubscription? _urlLinkStream;
|
||||
int _keyPressed = 0;
|
||||
bool textFieldVisited = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
navigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
//Set display mode
|
||||
if (settings.displayMode != null && settings.displayMode >= 0) {
|
||||
if (settings.displayMode != null && settings.displayMode! >= 0) {
|
||||
FlutterDisplayMode.supported.then((modes) async {
|
||||
if (modes.length - 1 >= settings.displayMode)
|
||||
FlutterDisplayMode.setPreferredMode(modes[settings.displayMode]);
|
||||
if (modes.length - 1 >= settings.displayMode!)
|
||||
FlutterDisplayMode.setPreferredMode(modes[settings.displayMode!]);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -205,7 +214,7 @@ class _MainScreenState extends State<MainScreen>
|
|||
});
|
||||
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
WidgetsBinding.instance!.addObserver(this);
|
||||
}
|
||||
|
||||
void _startStreamingServer() async {
|
||||
|
@ -216,7 +225,7 @@ class _MainScreenState extends State<MainScreen>
|
|||
void _prepareQuickActions() {
|
||||
final QuickActions quickActions = QuickActions();
|
||||
quickActions.initialize((type) {
|
||||
if (type != null) _startPreload(type);
|
||||
_startPreload(type);
|
||||
});
|
||||
|
||||
//Actions
|
||||
|
@ -237,12 +246,13 @@ class _MainScreenState extends State<MainScreen>
|
|||
}
|
||||
if (type == 'favorites') {
|
||||
Playlist p = await deezerAPI.fullPlaylist(deezerAPI.favoritesPlaylistId);
|
||||
playerHelper.playFromPlaylist(p, p.tracks[0].id);
|
||||
playerHelper.playFromPlaylist(p, p.tracks![0]!.id);
|
||||
}
|
||||
}
|
||||
|
||||
void _loadPreloadInfo() async {
|
||||
String info = await DownloadManager.platform.invokeMethod('getPreloadInfo');
|
||||
String? info =
|
||||
await DownloadManager.platform.invokeMethod('getPreloadInfo');
|
||||
if (info != null) {
|
||||
//Used if started from android auto
|
||||
await deezerAPI.authorize();
|
||||
|
@ -252,8 +262,8 @@ class _MainScreenState extends State<MainScreen>
|
|||
|
||||
@override
|
||||
void dispose() {
|
||||
if (_urlLinkStream != null) _urlLinkStream.cancel();
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
_urlLinkStream?.cancel();
|
||||
WidgetsBinding.instance!.removeObserver(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
@ -268,12 +278,13 @@ class _MainScreenState extends State<MainScreen>
|
|||
|
||||
void _setupUniLinks() async {
|
||||
//Listen to URLs
|
||||
_urlLinkStream = getUriLinksStream().listen((Uri uri) {
|
||||
openScreenByURL(context, uri.toString());
|
||||
_urlLinkStream = linkStream.listen((String? link) {
|
||||
if (link == null) return;
|
||||
openScreenByURL(context, link);
|
||||
}, onError: (err) {});
|
||||
//Get initial link on cold start
|
||||
try {
|
||||
String link = await getInitialLink();
|
||||
String? link = await getInitialLink();
|
||||
if (link != null && link.length > 4) openScreenByURL(context, link);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
@ -281,10 +292,10 @@ class _MainScreenState extends State<MainScreen>
|
|||
ValueChanged<RawKeyEvent> _handleKey(
|
||||
FocusScopeNode navigationBarFocusNode, FocusNode screenFocusNode) {
|
||||
return (event) {
|
||||
FocusNode primaryFocus = FocusManager.instance.primaryFocus;
|
||||
FocusNode primaryFocus = FocusManager.instance.primaryFocus!;
|
||||
// After visiting text field, something goes wrong and KeyDown events are not sent, only KeyUp-s.
|
||||
// So, set this flag to indicate a transition to other "mode"
|
||||
if (primaryFocus.context.widget.runtimeType.toString() ==
|
||||
if (primaryFocus.context!.widget.runtimeType.toString() ==
|
||||
'EditableText') {
|
||||
setState(() {
|
||||
textFieldVisited = true;
|
||||
|
@ -313,7 +324,7 @@ class _MainScreenState extends State<MainScreen>
|
|||
// If it's bottom row, go to navigation bar
|
||||
var row = primaryFocus.parent;
|
||||
if (row != null) {
|
||||
var column = row.parent;
|
||||
var column = row.parent!;
|
||||
if (column.children.last == row) {
|
||||
focusToNavbar(navigationBarFocusNode);
|
||||
}
|
||||
|
@ -321,7 +332,7 @@ class _MainScreenState extends State<MainScreen>
|
|||
break;
|
||||
case 19: // UP
|
||||
if (navigationBarFocusNode.hasFocus) {
|
||||
screenFocusNode.parent.parent.children
|
||||
screenFocusNode.parent!.parent!.children
|
||||
.last // children.last is used for handling "playlists" screen in library. Under CustomNavigator 2 screens appears.
|
||||
.nextFocus(); // nextFocus is used instead of requestFocus because it focuses on last, bottom, non-visible tile of main page
|
||||
|
||||
|
@ -332,14 +343,15 @@ class _MainScreenState extends State<MainScreen>
|
|||
// After visiting text field, something goes wrong and KeyDown events are not sent, only KeyUp-s.
|
||||
// Focus moving works only on KeyDown events, so here we simulate keys handling as it's done in Flutter
|
||||
if (textFieldVisited && event.runtimeType.toString() == 'RawKeyUpEvent') {
|
||||
Map<LogicalKeySet, Intent> shortcuts = Shortcuts.of(context).shortcuts;
|
||||
final BuildContext primaryContext = primaryFocus?.context;
|
||||
Intent intent = shortcuts[LogicalKeySet(event.logicalKey)];
|
||||
Map<LogicalKeySet, Intent> shortcuts =
|
||||
Shortcuts.of(context).shortcuts as Map<LogicalKeySet, Intent>;
|
||||
final BuildContext? primaryContext = primaryFocus.context;
|
||||
Intent? intent = shortcuts[LogicalKeySet(event.logicalKey)];
|
||||
if (intent != null) {
|
||||
Actions.invoke(primaryContext, intent);
|
||||
Actions.invoke(primaryContext!, intent);
|
||||
}
|
||||
// WA for "Search field -> navigator -> UP -> DOWN" case. Prevents focus hanging.
|
||||
FocusNode newFocus = FocusManager.instance.primaryFocus;
|
||||
FocusNode? newFocus = FocusManager.instance.primaryFocus;
|
||||
if (newFocus is FocusScopeNode) {
|
||||
navigationBarFocusNode.requestFocus();
|
||||
}
|
||||
|
@ -363,77 +375,75 @@ class _MainScreenState extends State<MainScreen>
|
|||
focusNode: FocusNode(),
|
||||
onKey: _handleKey(navigationBarFocusNode, screenFocusNode),
|
||||
child: Scaffold(
|
||||
bottomNavigationBar: FocusScope(
|
||||
node: navigationBarFocusNode,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
PlayerBar(),
|
||||
BottomNavigationBar(
|
||||
backgroundColor: Theme.of(context).bottomAppBarColor,
|
||||
currentIndex: _selected,
|
||||
onTap: (int s) async {
|
||||
//Pop all routes until home screen
|
||||
while (navigatorKey.currentState.canPop()) {
|
||||
await navigatorKey.currentState.maybePop();
|
||||
}
|
||||
bottomNavigationBar: FocusScope(
|
||||
node: navigationBarFocusNode,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
PlayerBar(),
|
||||
ValueListenableBuilder<int>(
|
||||
valueListenable: _selected,
|
||||
builder: (context, value, _) {
|
||||
return BottomNavigationBar(
|
||||
backgroundColor: Theme.of(context).bottomAppBarColor,
|
||||
currentIndex: value,
|
||||
onTap: (int s) async {
|
||||
//Pop all routes until home screen
|
||||
while (navigatorKey.currentState!.canPop()) {
|
||||
await navigatorKey.currentState!.maybePop();
|
||||
}
|
||||
|
||||
await navigatorKey.currentState.maybePop();
|
||||
setState(() {
|
||||
_selected = s;
|
||||
});
|
||||
|
||||
//Fix statusbar
|
||||
SystemChrome.setSystemUIOverlayStyle(
|
||||
SystemUiOverlayStyle(
|
||||
statusBarColor: Colors.transparent,
|
||||
));
|
||||
},
|
||||
selectedItemColor: Theme.of(context).primaryColor,
|
||||
items: <BottomNavigationBarItem>[
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.home), label: 'Home'.i18n),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.search),
|
||||
label: 'Search'.i18n,
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.library_music),
|
||||
label: 'Library'.i18n)
|
||||
],
|
||||
)
|
||||
],
|
||||
)),
|
||||
body: AudioServiceWidget(
|
||||
child: _MainRouteNavigator(
|
||||
navigatorKey: navigatorKey,
|
||||
home: Focus(
|
||||
focusNode: screenFocusNode,
|
||||
skipTraversal: true,
|
||||
canRequestFocus: false,
|
||||
child: _screens[_selected])),
|
||||
)));
|
||||
await navigatorKey.currentState!.maybePop();
|
||||
_selected.value = s;
|
||||
},
|
||||
selectedItemColor: Theme.of(context).primaryColor,
|
||||
items: <BottomNavigationBarItem>[
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.home), label: 'Home'.i18n),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.search),
|
||||
label: 'Search'.i18n,
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.library_music),
|
||||
label: 'Library'.i18n)
|
||||
],
|
||||
);
|
||||
})
|
||||
],
|
||||
)),
|
||||
body: _MainRouteNavigator(
|
||||
navigatorKey: navigatorKey,
|
||||
home: Focus(
|
||||
focusNode: screenFocusNode,
|
||||
skipTraversal: true,
|
||||
canRequestFocus: false,
|
||||
child: ValueListenableBuilder<int>(
|
||||
valueListenable: _selected,
|
||||
builder: (context, value, _) => _screens[value]))),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// hella simple reimplementation of custom_navigator, which is NOT null-safe
|
||||
// hella simple null-safe 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})
|
||||
const _MainRouteNavigator(
|
||||
{Key? key, required this.home, required 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;
|
||||
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;
|
||||
final NavigatorState? navigator = navigatorKey.currentState;
|
||||
if (navigator == null) return false;
|
||||
navigator.pushNamed(route);
|
||||
return true;
|
||||
|
@ -442,12 +452,13 @@ class _MainRouteNavigator extends StatelessWidget with WidgetsBindingObserver {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Navigator(
|
||||
key: navigatorKey,
|
||||
initialRoute: Navigator.defaultRouteName,
|
||||
onGenerateRoute: _onGenerateRoute,
|
||||
);
|
||||
}
|
||||
|
||||
Route<dynamic> _onGenerateRoute(RouteSettings settings) {
|
||||
Route<dynamic>? _onGenerateRoute(RouteSettings settings) {
|
||||
if (settings.name == Navigator.defaultRouteName) {
|
||||
return MaterialPageRoute(builder: (context) => home, settings: settings);
|
||||
}
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:freezer/api/download.dart';
|
||||
import 'package:freezer/main.dart';
|
||||
import 'package:freezer/ui/cached_image.dart';
|
||||
import 'package:freezer/api/player.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
@ -15,66 +13,66 @@ import 'dart:async';
|
|||
|
||||
part 'settings.g.dart';
|
||||
|
||||
Settings settings;
|
||||
late Settings settings;
|
||||
|
||||
@JsonSerializable()
|
||||
class Settings {
|
||||
//Language
|
||||
@JsonKey(defaultValue: null)
|
||||
String language;
|
||||
String? language;
|
||||
|
||||
//Main
|
||||
@JsonKey(defaultValue: false)
|
||||
bool ignoreInterruptions;
|
||||
bool? ignoreInterruptions;
|
||||
@JsonKey(defaultValue: false)
|
||||
bool enableEqualizer;
|
||||
bool? enableEqualizer;
|
||||
|
||||
//Account
|
||||
String arl;
|
||||
String? arl;
|
||||
@JsonKey(ignore: true)
|
||||
bool offlineMode = false;
|
||||
|
||||
//Quality
|
||||
@JsonKey(defaultValue: AudioQuality.MP3_320)
|
||||
AudioQuality wifiQuality;
|
||||
AudioQuality? wifiQuality;
|
||||
@JsonKey(defaultValue: AudioQuality.MP3_128)
|
||||
AudioQuality mobileQuality;
|
||||
AudioQuality? mobileQuality;
|
||||
@JsonKey(defaultValue: AudioQuality.FLAC)
|
||||
AudioQuality offlineQuality;
|
||||
AudioQuality? offlineQuality;
|
||||
@JsonKey(defaultValue: AudioQuality.FLAC)
|
||||
AudioQuality downloadQuality;
|
||||
AudioQuality? downloadQuality;
|
||||
|
||||
//Download options
|
||||
String downloadPath;
|
||||
String? downloadPath;
|
||||
|
||||
@JsonKey(defaultValue: "%artist% - %title%")
|
||||
String downloadFilename;
|
||||
String? downloadFilename;
|
||||
@JsonKey(defaultValue: true)
|
||||
bool albumFolder;
|
||||
bool? albumFolder;
|
||||
@JsonKey(defaultValue: true)
|
||||
bool artistFolder;
|
||||
bool? artistFolder;
|
||||
@JsonKey(defaultValue: false)
|
||||
bool albumDiscFolder;
|
||||
bool? albumDiscFolder;
|
||||
@JsonKey(defaultValue: false)
|
||||
bool overwriteDownload;
|
||||
bool? overwriteDownload;
|
||||
@JsonKey(defaultValue: 2)
|
||||
int downloadThreads;
|
||||
int? downloadThreads;
|
||||
@JsonKey(defaultValue: false)
|
||||
bool playlistFolder;
|
||||
bool? playlistFolder;
|
||||
@JsonKey(defaultValue: true)
|
||||
bool downloadLyrics;
|
||||
bool? downloadLyrics;
|
||||
@JsonKey(defaultValue: false)
|
||||
bool trackCover;
|
||||
bool? trackCover;
|
||||
@JsonKey(defaultValue: true)
|
||||
bool albumCover;
|
||||
bool? albumCover;
|
||||
@JsonKey(defaultValue: false)
|
||||
bool nomediaFiles;
|
||||
bool? nomediaFiles;
|
||||
@JsonKey(defaultValue: ", ")
|
||||
String artistSeparator;
|
||||
String? artistSeparator;
|
||||
@JsonKey(defaultValue: "%artist% - %title%")
|
||||
String singletonFilename;
|
||||
String? singletonFilename;
|
||||
@JsonKey(defaultValue: 1400)
|
||||
int albumArtResolution;
|
||||
int? albumArtResolution;
|
||||
@JsonKey(defaultValue: [
|
||||
"title",
|
||||
"album",
|
||||
|
@ -93,74 +91,73 @@ class Settings {
|
|||
"contributors",
|
||||
"art"
|
||||
])
|
||||
List<String> tags;
|
||||
List<String>? tags;
|
||||
|
||||
//Appearance
|
||||
@JsonKey(defaultValue: Themes.Dark)
|
||||
Themes theme;
|
||||
Themes? theme;
|
||||
@JsonKey(defaultValue: false)
|
||||
bool useSystemTheme;
|
||||
bool? useSystemTheme;
|
||||
@JsonKey(defaultValue: true)
|
||||
bool colorGradientBackground;
|
||||
bool? colorGradientBackground;
|
||||
@JsonKey(defaultValue: false)
|
||||
bool blurPlayerBackground;
|
||||
bool? blurPlayerBackground;
|
||||
@JsonKey(defaultValue: "Deezer")
|
||||
String font;
|
||||
String? font;
|
||||
@JsonKey(defaultValue: false)
|
||||
bool lyricsVisualizer;
|
||||
bool? lyricsVisualizer;
|
||||
@JsonKey(defaultValue: null)
|
||||
int displayMode;
|
||||
int? displayMode;
|
||||
|
||||
//Colors
|
||||
@JsonKey(toJson: _colorToJson, fromJson: _colorFromJson)
|
||||
Color primaryColor = Colors.blue;
|
||||
|
||||
static _colorToJson(Color c) => c.value;
|
||||
static _colorFromJson(int v) => Color(v ?? Colors.blue.value);
|
||||
static _colorFromJson(int? v) => Color(v ?? Colors.blue.value);
|
||||
|
||||
@JsonKey(defaultValue: false)
|
||||
bool useArtColor = false;
|
||||
StreamSubscription _useArtColorSub;
|
||||
|
||||
//Deezer
|
||||
@JsonKey(defaultValue: 'en')
|
||||
String deezerLanguage;
|
||||
String? deezerLanguage;
|
||||
@JsonKey(defaultValue: 'US')
|
||||
String deezerCountry;
|
||||
String? deezerCountry;
|
||||
@JsonKey(defaultValue: false)
|
||||
bool logListen;
|
||||
bool? logListen;
|
||||
@JsonKey(defaultValue: null)
|
||||
String proxyAddress;
|
||||
String? proxyAddress;
|
||||
|
||||
//LastFM
|
||||
@JsonKey(defaultValue: null)
|
||||
String lastFMUsername;
|
||||
String? lastFMUsername;
|
||||
@JsonKey(defaultValue: null)
|
||||
String lastFMPassword;
|
||||
String? lastFMPassword;
|
||||
|
||||
//Spotify
|
||||
@JsonKey(defaultValue: null)
|
||||
String spotifyClientId;
|
||||
String? spotifyClientId;
|
||||
@JsonKey(defaultValue: null)
|
||||
String spotifyClientSecret;
|
||||
String? spotifyClientSecret;
|
||||
@JsonKey(defaultValue: null)
|
||||
SpotifyCredentialsSave spotifyCredentials;
|
||||
SpotifyCredentialsSave? spotifyCredentials;
|
||||
|
||||
Settings({this.downloadPath, this.arl});
|
||||
|
||||
ThemeData get themeData {
|
||||
ThemeData? get themeData {
|
||||
//System theme
|
||||
if (useSystemTheme) {
|
||||
if (SchedulerBinding.instance.window.platformBrightness ==
|
||||
if (useSystemTheme!) {
|
||||
if (SchedulerBinding.instance!.window.platformBrightness ==
|
||||
Brightness.light) {
|
||||
return _themeData[Themes.Light];
|
||||
} else {
|
||||
if (theme == Themes.Light) return _themeData[Themes.Dark];
|
||||
return _themeData[theme];
|
||||
return _themeData[theme!];
|
||||
}
|
||||
}
|
||||
//Theme
|
||||
return _themeData[theme] ?? ThemeData();
|
||||
return _themeData[theme!] ?? ThemeData();
|
||||
}
|
||||
|
||||
//Get all available fonts
|
||||
|
@ -175,22 +172,23 @@ class Settings {
|
|||
|
||||
void updateUseArtColor(bool v) {
|
||||
useArtColor = v;
|
||||
if (v) {
|
||||
//On media item change set color
|
||||
_useArtColorSub =
|
||||
AudioService.currentMediaItemStream.listen((event) async {
|
||||
if (event == null || event.artUri == null) return;
|
||||
this.primaryColor =
|
||||
await imagesDatabase.getPrimaryColor(event.artUri.toString());
|
||||
updateTheme();
|
||||
});
|
||||
} else {
|
||||
//Cancel stream subscription
|
||||
if (_useArtColorSub != null) {
|
||||
_useArtColorSub.cancel();
|
||||
_useArtColorSub = null;
|
||||
}
|
||||
}
|
||||
//TODO: let's reimplement this somewhere better
|
||||
//if (v) {
|
||||
// //On media item change set color
|
||||
// _useArtColorSub =
|
||||
// AudioService.currentMediaItemStream.listen((event) async {
|
||||
// if (event == null || event.artUri == null) return;
|
||||
// this.primaryColor =
|
||||
// await imagesDatabase.getPrimaryColor(event.artUri.toString());
|
||||
// updateTheme();
|
||||
// });
|
||||
//} else {
|
||||
// //Cancel stream subscription
|
||||
// if (_useArtColorSub != null) {
|
||||
// _useArtColorSub!.cancel();
|
||||
// _useArtColorSub = null;
|
||||
// }
|
||||
//}
|
||||
}
|
||||
|
||||
SliderThemeData get _sliderTheme => SliderThemeData(
|
||||
|
@ -210,7 +208,7 @@ class Settings {
|
|||
//Set default path, because async
|
||||
s.downloadPath =
|
||||
await getExternalStorageDirectories(type: StorageDirectory.music)
|
||||
.then((paths) => paths[0].path);
|
||||
.then((paths) => paths![0].path);
|
||||
s.save();
|
||||
return s;
|
||||
}
|
||||
|
@ -223,14 +221,14 @@ class Settings {
|
|||
|
||||
Future<void> updateAudioServiceQuality() async {
|
||||
//Send wifi & mobile quality to audio service isolate
|
||||
await AudioService.customAction('updateQuality', {
|
||||
await audioHandler.customAction('updateQuality', {
|
||||
'mobileQuality': getQualityInt(mobileQuality),
|
||||
'wifiQuality': getQualityInt(wifiQuality)
|
||||
});
|
||||
}
|
||||
|
||||
//AudioQuality to deezer int
|
||||
int getQualityInt(AudioQuality q) {
|
||||
int getQualityInt(AudioQuality? q) {
|
||||
switch (q) {
|
||||
case AudioQuality.MP3_128:
|
||||
return 1;
|
||||
|
@ -260,8 +258,8 @@ class Settings {
|
|||
|
||||
//Check if is dark, can't use theme directly, because of system themes, and Theme.of(context).brightness broke
|
||||
bool get isDark {
|
||||
if (useSystemTheme) {
|
||||
if (SchedulerBinding.instance.window.platformBrightness ==
|
||||
if (useSystemTheme!) {
|
||||
if (SchedulerBinding.instance!.window.platformBrightness ==
|
||||
Brightness.light) return false;
|
||||
return true;
|
||||
}
|
||||
|
@ -271,14 +269,14 @@ class Settings {
|
|||
|
||||
static const deezerBg = Color(0xFF1F1A16);
|
||||
static const deezerBottom = Color(0xFF1b1714);
|
||||
TextTheme get _textTheme => (font == 'Deezer')
|
||||
TextTheme? get _textTheme => (font == 'Deezer')
|
||||
? null
|
||||
: GoogleFonts.getTextTheme(
|
||||
font,
|
||||
font!,
|
||||
this.isDark
|
||||
? ThemeData.dark().textTheme
|
||||
: ThemeData.light().textTheme);
|
||||
String get _fontFamily => (font == 'Deezer') ? 'MabryPro' : null;
|
||||
String? get _fontFamily => (font == 'Deezer') ? 'MabryPro' : null;
|
||||
|
||||
Map<Themes, ThemeData> get _themeData => {
|
||||
Themes.Light: ThemeData(
|
||||
|
@ -287,7 +285,10 @@ class Settings {
|
|||
brightness: Brightness.light,
|
||||
primarySwatch: _primarySwatch,
|
||||
primaryColor: primaryColor,
|
||||
accentColor: primaryColor,
|
||||
colorScheme: ColorScheme.fromSwatch(
|
||||
primarySwatch: _primarySwatch,
|
||||
accentColor: primaryColor,
|
||||
brightness: Brightness.light),
|
||||
sliderTheme: _sliderTheme,
|
||||
toggleableActiveColor: primaryColor,
|
||||
bottomAppBarColor: Color(0xfff5f5f5),
|
||||
|
@ -298,7 +299,10 @@ class Settings {
|
|||
brightness: Brightness.dark,
|
||||
primarySwatch: _primarySwatch,
|
||||
primaryColor: primaryColor,
|
||||
accentColor: primaryColor,
|
||||
colorScheme: ColorScheme.fromSwatch(
|
||||
primarySwatch: _primarySwatch,
|
||||
accentColor: primaryColor,
|
||||
brightness: Brightness.dark),
|
||||
sliderTheme: _sliderTheme,
|
||||
toggleableActiveColor: primaryColor,
|
||||
),
|
||||
|
@ -308,7 +312,10 @@ class Settings {
|
|||
brightness: Brightness.dark,
|
||||
primarySwatch: _primarySwatch,
|
||||
primaryColor: primaryColor,
|
||||
accentColor: primaryColor,
|
||||
colorScheme: ColorScheme.fromSwatch(
|
||||
primarySwatch: _primarySwatch,
|
||||
accentColor: primaryColor,
|
||||
brightness: Brightness.dark),
|
||||
sliderTheme: _sliderTheme,
|
||||
toggleableActiveColor: primaryColor,
|
||||
backgroundColor: deezerBg,
|
||||
|
@ -324,7 +331,10 @@ class Settings {
|
|||
brightness: Brightness.dark,
|
||||
primarySwatch: _primarySwatch,
|
||||
primaryColor: primaryColor,
|
||||
accentColor: primaryColor,
|
||||
colorScheme: ColorScheme.fromSwatch(
|
||||
primarySwatch: _primarySwatch,
|
||||
accentColor: primaryColor,
|
||||
brightness: Brightness.dark),
|
||||
backgroundColor: Colors.black,
|
||||
scaffoldBackgroundColor: Colors.black,
|
||||
bottomAppBarColor: Colors.black,
|
||||
|
@ -352,10 +362,10 @@ enum Themes { Light, Dark, Deezer, Black }
|
|||
|
||||
@JsonSerializable()
|
||||
class SpotifyCredentialsSave {
|
||||
String accessToken;
|
||||
String refreshToken;
|
||||
List<String> scopes;
|
||||
DateTime expiration;
|
||||
String? accessToken;
|
||||
String? refreshToken;
|
||||
List<String>? scopes;
|
||||
DateTime? expiration;
|
||||
|
||||
SpotifyCredentialsSave(
|
||||
{this.accessToken, this.refreshToken, this.scopes, this.expiration});
|
||||
|
|
|
@ -6,84 +6,84 @@ part of 'settings.dart';
|
|||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
Settings _$SettingsFromJson(Map<String, dynamic> json) {
|
||||
return Settings(
|
||||
downloadPath: json['downloadPath'] as String,
|
||||
arl: json['arl'] as String,
|
||||
)
|
||||
..language = json['language'] as String
|
||||
..ignoreInterruptions = json['ignoreInterruptions'] as bool ?? false
|
||||
..enableEqualizer = json['enableEqualizer'] as bool ?? false
|
||||
..wifiQuality =
|
||||
_$enumDecodeNullable(_$AudioQualityEnumMap, json['wifiQuality']) ??
|
||||
AudioQuality.MP3_320
|
||||
..mobileQuality =
|
||||
_$enumDecodeNullable(_$AudioQualityEnumMap, json['mobileQuality']) ??
|
||||
AudioQuality.MP3_128
|
||||
..offlineQuality =
|
||||
_$enumDecodeNullable(_$AudioQualityEnumMap, json['offlineQuality']) ??
|
||||
AudioQuality.FLAC
|
||||
..downloadQuality =
|
||||
_$enumDecodeNullable(_$AudioQualityEnumMap, json['downloadQuality']) ??
|
||||
AudioQuality.FLAC
|
||||
..downloadFilename =
|
||||
json['downloadFilename'] as String ?? '%artist% - %title%'
|
||||
..albumFolder = json['albumFolder'] as bool ?? true
|
||||
..artistFolder = json['artistFolder'] as bool ?? true
|
||||
..albumDiscFolder = json['albumDiscFolder'] as bool ?? false
|
||||
..overwriteDownload = json['overwriteDownload'] as bool ?? false
|
||||
..downloadThreads = json['downloadThreads'] as int ?? 2
|
||||
..playlistFolder = json['playlistFolder'] as bool ?? false
|
||||
..downloadLyrics = json['downloadLyrics'] as bool ?? true
|
||||
..trackCover = json['trackCover'] as bool ?? false
|
||||
..albumCover = json['albumCover'] as bool ?? true
|
||||
..nomediaFiles = json['nomediaFiles'] as bool ?? false
|
||||
..artistSeparator = json['artistSeparator'] as String ?? ', '
|
||||
..singletonFilename =
|
||||
json['singletonFilename'] as String ?? '%artist% - %title%'
|
||||
..albumArtResolution = json['albumArtResolution'] as int ?? 1400
|
||||
..tags = (json['tags'] as List)?.map((e) => e as String)?.toList() ??
|
||||
[
|
||||
'title',
|
||||
'album',
|
||||
'artist',
|
||||
'track',
|
||||
'disc',
|
||||
'albumArtist',
|
||||
'date',
|
||||
'label',
|
||||
'isrc',
|
||||
'upc',
|
||||
'trackTotal',
|
||||
'bpm',
|
||||
'lyrics',
|
||||
'genre',
|
||||
'contributors',
|
||||
'art'
|
||||
]
|
||||
..theme =
|
||||
_$enumDecodeNullable(_$ThemesEnumMap, json['theme']) ?? Themes.Dark
|
||||
..useSystemTheme = json['useSystemTheme'] as bool ?? false
|
||||
..colorGradientBackground = json['colorGradientBackground'] as bool ?? true
|
||||
..blurPlayerBackground = json['blurPlayerBackground'] as bool ?? false
|
||||
..font = json['font'] as String ?? 'Deezer'
|
||||
..lyricsVisualizer = json['lyricsVisualizer'] as bool ?? false
|
||||
..displayMode = json['displayMode'] as int
|
||||
..primaryColor = Settings._colorFromJson(json['primaryColor'] as int)
|
||||
..useArtColor = json['useArtColor'] as bool ?? false
|
||||
..deezerLanguage = json['deezerLanguage'] as String ?? 'en'
|
||||
..deezerCountry = json['deezerCountry'] as String ?? 'US'
|
||||
..logListen = json['logListen'] as bool ?? false
|
||||
..proxyAddress = json['proxyAddress'] as String
|
||||
..lastFMUsername = json['lastFMUsername'] as String
|
||||
..lastFMPassword = json['lastFMPassword'] as String
|
||||
..spotifyClientId = json['spotifyClientId'] as String
|
||||
..spotifyClientSecret = json['spotifyClientSecret'] as String
|
||||
..spotifyCredentials = json['spotifyCredentials'] == null
|
||||
? null
|
||||
: SpotifyCredentialsSave.fromJson(
|
||||
json['spotifyCredentials'] as Map<String, dynamic>);
|
||||
}
|
||||
Settings _$SettingsFromJson(Map<String, dynamic> json) => Settings(
|
||||
downloadPath: json['downloadPath'] as String?,
|
||||
arl: json['arl'] as String?,
|
||||
)
|
||||
..language = json['language'] as String?
|
||||
..ignoreInterruptions = json['ignoreInterruptions'] as bool? ?? false
|
||||
..enableEqualizer = json['enableEqualizer'] as bool? ?? false
|
||||
..wifiQuality =
|
||||
_$enumDecodeNullable(_$AudioQualityEnumMap, json['wifiQuality']) ??
|
||||
AudioQuality.MP3_320
|
||||
..mobileQuality =
|
||||
_$enumDecodeNullable(_$AudioQualityEnumMap, json['mobileQuality']) ??
|
||||
AudioQuality.MP3_128
|
||||
..offlineQuality =
|
||||
_$enumDecodeNullable(_$AudioQualityEnumMap, json['offlineQuality']) ??
|
||||
AudioQuality.FLAC
|
||||
..downloadQuality = _$enumDecodeNullable(
|
||||
_$AudioQualityEnumMap, json['downloadQuality']) ??
|
||||
AudioQuality.FLAC
|
||||
..downloadFilename =
|
||||
json['downloadFilename'] as String? ?? '%artist% - %title%'
|
||||
..albumFolder = json['albumFolder'] as bool? ?? true
|
||||
..artistFolder = json['artistFolder'] as bool? ?? true
|
||||
..albumDiscFolder = json['albumDiscFolder'] as bool? ?? false
|
||||
..overwriteDownload = json['overwriteDownload'] as bool? ?? false
|
||||
..downloadThreads = json['downloadThreads'] as int? ?? 2
|
||||
..playlistFolder = json['playlistFolder'] as bool? ?? false
|
||||
..downloadLyrics = json['downloadLyrics'] as bool? ?? true
|
||||
..trackCover = json['trackCover'] as bool? ?? false
|
||||
..albumCover = json['albumCover'] as bool? ?? true
|
||||
..nomediaFiles = json['nomediaFiles'] as bool? ?? false
|
||||
..artistSeparator = json['artistSeparator'] as String? ?? ', '
|
||||
..singletonFilename =
|
||||
json['singletonFilename'] as String? ?? '%artist% - %title%'
|
||||
..albumArtResolution = json['albumArtResolution'] as int? ?? 1400
|
||||
..tags =
|
||||
(json['tags'] as List<dynamic>?)?.map((e) => e as String).toList() ??
|
||||
[
|
||||
'title',
|
||||
'album',
|
||||
'artist',
|
||||
'track',
|
||||
'disc',
|
||||
'albumArtist',
|
||||
'date',
|
||||
'label',
|
||||
'isrc',
|
||||
'upc',
|
||||
'trackTotal',
|
||||
'bpm',
|
||||
'lyrics',
|
||||
'genre',
|
||||
'contributors',
|
||||
'art'
|
||||
]
|
||||
..theme =
|
||||
_$enumDecodeNullable(_$ThemesEnumMap, json['theme']) ?? Themes.Dark
|
||||
..useSystemTheme = json['useSystemTheme'] as bool? ?? false
|
||||
..colorGradientBackground =
|
||||
json['colorGradientBackground'] as bool? ?? true
|
||||
..blurPlayerBackground = json['blurPlayerBackground'] as bool? ?? false
|
||||
..font = json['font'] as String? ?? 'Deezer'
|
||||
..lyricsVisualizer = json['lyricsVisualizer'] as bool? ?? false
|
||||
..displayMode = json['displayMode'] as int?
|
||||
..primaryColor = Settings._colorFromJson(json['primaryColor'] as int?)
|
||||
..useArtColor = json['useArtColor'] as bool? ?? false
|
||||
..deezerLanguage = json['deezerLanguage'] as String? ?? 'en'
|
||||
..deezerCountry = json['deezerCountry'] as String? ?? 'US'
|
||||
..logListen = json['logListen'] as bool? ?? false
|
||||
..proxyAddress = json['proxyAddress'] as String?
|
||||
..lastFMUsername = json['lastFMUsername'] as String?
|
||||
..lastFMPassword = json['lastFMPassword'] as String?
|
||||
..spotifyClientId = json['spotifyClientId'] as String?
|
||||
..spotifyClientSecret = json['spotifyClientSecret'] as String?
|
||||
..spotifyCredentials = json['spotifyCredentials'] == null
|
||||
? null
|
||||
: SpotifyCredentialsSave.fromJson(
|
||||
json['spotifyCredentials'] as Map<String, dynamic>);
|
||||
|
||||
Map<String, dynamic> _$SettingsToJson(Settings instance) => <String, dynamic>{
|
||||
'language': instance.language,
|
||||
|
@ -130,36 +130,41 @@ Map<String, dynamic> _$SettingsToJson(Settings instance) => <String, dynamic>{
|
|||
'spotifyCredentials': instance.spotifyCredentials,
|
||||
};
|
||||
|
||||
T _$enumDecode<T>(
|
||||
Map<T, dynamic> enumValues,
|
||||
dynamic source, {
|
||||
T unknownValue,
|
||||
K _$enumDecode<K, V>(
|
||||
Map<K, V> enumValues,
|
||||
Object? source, {
|
||||
K? unknownValue,
|
||||
}) {
|
||||
if (source == null) {
|
||||
throw ArgumentError('A value must be provided. Supported values: '
|
||||
'${enumValues.values.join(', ')}');
|
||||
throw ArgumentError(
|
||||
'A value must be provided. Supported values: '
|
||||
'${enumValues.values.join(', ')}',
|
||||
);
|
||||
}
|
||||
|
||||
final value = enumValues.entries
|
||||
.singleWhere((e) => e.value == source, orElse: () => null)
|
||||
?.key;
|
||||
|
||||
if (value == null && unknownValue == null) {
|
||||
throw ArgumentError('`$source` is not one of the supported values: '
|
||||
'${enumValues.values.join(', ')}');
|
||||
}
|
||||
return value ?? unknownValue;
|
||||
return enumValues.entries.singleWhere(
|
||||
(e) => e.value == source,
|
||||
orElse: () {
|
||||
if (unknownValue == null) {
|
||||
throw ArgumentError(
|
||||
'`$source` is not one of the supported values: '
|
||||
'${enumValues.values.join(', ')}',
|
||||
);
|
||||
}
|
||||
return MapEntry(unknownValue, enumValues.values.first);
|
||||
},
|
||||
).key;
|
||||
}
|
||||
|
||||
T _$enumDecodeNullable<T>(
|
||||
Map<T, dynamic> enumValues,
|
||||
K? _$enumDecodeNullable<K, V>(
|
||||
Map<K, V> enumValues,
|
||||
dynamic source, {
|
||||
T unknownValue,
|
||||
K? unknownValue,
|
||||
}) {
|
||||
if (source == null) {
|
||||
return null;
|
||||
}
|
||||
return _$enumDecode<T>(enumValues, source, unknownValue: unknownValue);
|
||||
return _$enumDecode<K, V>(enumValues, source, unknownValue: unknownValue);
|
||||
}
|
||||
|
||||
const _$AudioQualityEnumMap = {
|
||||
|
@ -177,16 +182,16 @@ const _$ThemesEnumMap = {
|
|||
};
|
||||
|
||||
SpotifyCredentialsSave _$SpotifyCredentialsSaveFromJson(
|
||||
Map<String, dynamic> json) {
|
||||
return SpotifyCredentialsSave(
|
||||
accessToken: json['accessToken'] as String,
|
||||
refreshToken: json['refreshToken'] as String,
|
||||
scopes: (json['scopes'] as List)?.map((e) => e as String)?.toList(),
|
||||
expiration: json['expiration'] == null
|
||||
? null
|
||||
: DateTime.parse(json['expiration'] as String),
|
||||
);
|
||||
}
|
||||
Map<String, dynamic> json) =>
|
||||
SpotifyCredentialsSave(
|
||||
accessToken: json['accessToken'] as String?,
|
||||
refreshToken: json['refreshToken'] as String?,
|
||||
scopes:
|
||||
(json['scopes'] as List<dynamic>?)?.map((e) => e as String).toList(),
|
||||
expiration: json['expiration'] == null
|
||||
? null
|
||||
: DateTime.parse(json['expiration'] as String),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$SpotifyCredentialsSaveToJson(
|
||||
SpotifyCredentialsSave instance) =>
|
||||
|
|
|
@ -9,7 +9,7 @@ class AndroidAuto {
|
|||
static const prefix = '_aa_';
|
||||
|
||||
//Get media items for parent id
|
||||
Future<List<MediaItem>> getScreen(String parentId) async {
|
||||
Future<List<MediaItem>> getScreen(String? parentId) async {
|
||||
print(parentId);
|
||||
|
||||
//Homescreen
|
||||
|
@ -24,9 +24,11 @@ class AndroidAuto {
|
|||
.map<MediaItem>((p) => MediaItem(
|
||||
id: '${prefix}playlist${p.id}',
|
||||
displayTitle: p.title,
|
||||
title: p.title!,
|
||||
album: '',
|
||||
displaySubtitle: p.trackCount.toString() + ' ' + 'Tracks'.i18n,
|
||||
playable: true,
|
||||
artUri: Uri.parse(p.image.thumb)))
|
||||
artUri: Uri.parse(p.image!.thumb!)))
|
||||
.toList();
|
||||
return out;
|
||||
}
|
||||
|
@ -39,9 +41,11 @@ class AndroidAuto {
|
|||
.map<MediaItem>((a) => MediaItem(
|
||||
id: '${prefix}album${a.id}',
|
||||
displayTitle: a.title,
|
||||
album: a.title!,
|
||||
title: '',
|
||||
displaySubtitle: a.artistString,
|
||||
playable: true,
|
||||
artUri: Uri.parse(a.art.thumb),
|
||||
artUri: Uri.parse(a.art!.thumb!),
|
||||
))
|
||||
.toList();
|
||||
return out;
|
||||
|
@ -49,30 +53,34 @@ class AndroidAuto {
|
|||
|
||||
//Artists screen
|
||||
if (parentId == 'artists') {
|
||||
List<Artist> artists = await deezerAPI.getArtists();
|
||||
List<Artist> artists = (await deezerAPI.getArtists())!;
|
||||
|
||||
List<MediaItem> out = artists
|
||||
.map<MediaItem>((a) => MediaItem(
|
||||
title: '',
|
||||
album: '',
|
||||
id: 'albums${a.id}',
|
||||
displayTitle: a.name,
|
||||
playable: false,
|
||||
artUri: Uri.parse(a.picture.thumb)))
|
||||
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(
|
||||
id: '${prefix}album${a.id}',
|
||||
displayTitle: a.title,
|
||||
title: '',
|
||||
album: a.title ?? '',
|
||||
displaySubtitle: a.artistString,
|
||||
playable: true,
|
||||
artUri: Uri.parse(a.art.thumb)))
|
||||
artUri: Uri.parse(a.art!.thumb!)))
|
||||
.toList();
|
||||
return out;
|
||||
}
|
||||
|
@ -81,16 +89,18 @@ class AndroidAuto {
|
|||
if (parentId == 'homescreen') {
|
||||
HomePage hp = await deezerAPI.homePage();
|
||||
List<MediaItem> out = [];
|
||||
for (HomePageSection section in hp.sections) {
|
||||
for (int i = 0; i < section.items.length; i++) {
|
||||
for (HomePageSection section in hp.sections!) {
|
||||
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) {
|
||||
var data = section.items![i].value;
|
||||
switch (section.items![i].type) {
|
||||
case HomePageItemType.PLAYLIST:
|
||||
out.add(MediaItem(
|
||||
title: data.title ?? '',
|
||||
album: '',
|
||||
id: '${prefix}playlist${data.id}',
|
||||
displayTitle: data.title,
|
||||
playable: true,
|
||||
|
@ -101,6 +111,8 @@ class AndroidAuto {
|
|||
out.add(MediaItem(
|
||||
id: '${prefix}album${data.id}',
|
||||
displayTitle: data.title,
|
||||
album: data.title ?? '',
|
||||
title: '',
|
||||
displaySubtitle: data.artistString,
|
||||
playable: true,
|
||||
artUri: data.art.thumb));
|
||||
|
@ -109,6 +121,8 @@ class AndroidAuto {
|
|||
case HomePageItemType.ARTIST:
|
||||
out.add(MediaItem(
|
||||
id: 'albums${data.id}',
|
||||
title: '',
|
||||
album: data.title ?? '',
|
||||
displayTitle: data.name,
|
||||
playable: false,
|
||||
artUri: data.picture.thumb));
|
||||
|
@ -116,11 +130,16 @@ class AndroidAuto {
|
|||
|
||||
case HomePageItemType.SMARTTRACKLIST:
|
||||
out.add(MediaItem(
|
||||
title: data.title ?? '',
|
||||
album: '',
|
||||
id: '${prefix}stl${data.id}',
|
||||
displayTitle: data.title,
|
||||
displaySubtitle: data.subtitle,
|
||||
playable: true,
|
||||
artUri: data.cover.thumb));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -132,7 +151,7 @@ class AndroidAuto {
|
|||
}
|
||||
|
||||
//Load virtual mediaItem
|
||||
Future playItem(String id) async {
|
||||
Future playItem(String? id) async {
|
||||
print(id);
|
||||
|
||||
//Play flow
|
||||
|
@ -144,18 +163,18 @@ class AndroidAuto {
|
|||
//Play library tracks
|
||||
if (id == 'tracks') {
|
||||
//Load tracks
|
||||
Playlist favPlaylist;
|
||||
Playlist? favPlaylist;
|
||||
try {
|
||||
favPlaylist =
|
||||
await deezerAPI.fullPlaylist(deezerAPI.favoritesPlaylistId);
|
||||
} catch (e) {
|
||||
print(e);
|
||||
}
|
||||
if (favPlaylist == null || favPlaylist.tracks.length == 0) return;
|
||||
if (favPlaylist == null || favPlaylist.tracks!.length == 0) return;
|
||||
|
||||
await playerHelper.playFromTrackList(
|
||||
favPlaylist.tracks,
|
||||
favPlaylist.tracks[0].id,
|
||||
favPlaylist.tracks!,
|
||||
favPlaylist.tracks![0]!.id,
|
||||
QueueSource(
|
||||
id: 'allTracks',
|
||||
text: 'All offline tracks'.i18n,
|
||||
|
@ -163,16 +182,16 @@ class AndroidAuto {
|
|||
return;
|
||||
}
|
||||
//Play playlists
|
||||
if (id.startsWith('playlist')) {
|
||||
if (id!.startsWith('playlist')) {
|
||||
Playlist p =
|
||||
await deezerAPI.fullPlaylist(id.replaceFirst('playlist', ''));
|
||||
await playerHelper.playFromPlaylist(p, p.tracks[0].id);
|
||||
await playerHelper.playFromPlaylist(p, p.tracks![0]!.id);
|
||||
return;
|
||||
}
|
||||
//Play albums
|
||||
if (id.startsWith('album')) {
|
||||
Album a = await deezerAPI.album(id.replaceFirst('album', ''));
|
||||
await playerHelper.playFromAlbum(a, a.tracks[0].id);
|
||||
await playerHelper.playFromAlbum(a, a.tracks![0]!.id);
|
||||
return;
|
||||
}
|
||||
//Play smart track list
|
||||
|
@ -187,29 +206,44 @@ 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,
|
||||
title: 'Flow'.i18n,
|
||||
album: ''),
|
||||
MediaItem(
|
||||
id: 'homescreen',
|
||||
title: 'Home'.i18n,
|
||||
album: '',
|
||||
displayTitle: 'Home'.i18n,
|
||||
playable: false,
|
||||
),
|
||||
MediaItem(
|
||||
id: '${prefix}tracks',
|
||||
title: 'Loved tracks'.i18n,
|
||||
album: '',
|
||||
displayTitle: 'Loved tracks'.i18n,
|
||||
playable: true,
|
||||
),
|
||||
MediaItem(
|
||||
id: 'playlists',
|
||||
title: 'Playlists'.i18n,
|
||||
album: '',
|
||||
displayTitle: 'Playlists'.i18n,
|
||||
playable: false,
|
||||
),
|
||||
MediaItem(
|
||||
id: 'albums',
|
||||
title: 'Albums'.i18n,
|
||||
album: '',
|
||||
displayTitle: 'Albums'.i18n,
|
||||
playable: false,
|
||||
),
|
||||
MediaItem(
|
||||
id: 'artists',
|
||||
title: 'Artists'.i18n,
|
||||
album: '',
|
||||
displayTitle: 'Artists'.i18n,
|
||||
playable: false,
|
||||
),
|
||||
|
|
|
@ -33,15 +33,15 @@ class ImagesDatabase {
|
|||
}
|
||||
|
||||
class CachedImage extends StatefulWidget {
|
||||
final String url;
|
||||
final double width;
|
||||
final double height;
|
||||
final String? url;
|
||||
final double? width;
|
||||
final double? height;
|
||||
final bool circular;
|
||||
final bool fullThumb;
|
||||
final bool rounded;
|
||||
|
||||
const CachedImage(
|
||||
{Key key,
|
||||
{Key? key,
|
||||
this.url,
|
||||
this.height,
|
||||
this.width,
|
||||
|
@ -80,15 +80,15 @@ class _CachedImageState extends State<CachedImage> {
|
|||
fullThumb: widget.fullThumb,
|
||||
));
|
||||
|
||||
if (!widget.url.startsWith('http'))
|
||||
if (!widget.url!.startsWith('http'))
|
||||
return Image.asset(
|
||||
widget.url,
|
||||
widget.url!,
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
);
|
||||
|
||||
return CachedNetworkImage(
|
||||
imageUrl: widget.url,
|
||||
imageUrl: widget.url!,
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
placeholder: (context, url) {
|
||||
|
@ -110,18 +110,18 @@ class _CachedImageState extends State<CachedImage> {
|
|||
}
|
||||
|
||||
class ZoomableImage extends StatefulWidget {
|
||||
final String url;
|
||||
final String? url;
|
||||
final bool rounded;
|
||||
final double width;
|
||||
final double? width;
|
||||
|
||||
ZoomableImage({@required this.url, this.rounded = false, this.width});
|
||||
ZoomableImage({required this.url, this.rounded = false, this.width});
|
||||
|
||||
@override
|
||||
_ZoomableImageState createState() => _ZoomableImageState();
|
||||
}
|
||||
|
||||
class _ZoomableImageState extends State<ZoomableImage> {
|
||||
PhotoViewController controller;
|
||||
PhotoViewController? controller;
|
||||
bool photoViewOpened = false;
|
||||
|
||||
@override
|
||||
|
@ -132,7 +132,7 @@ class _ZoomableImageState extends State<ZoomableImage> {
|
|||
|
||||
// Listener of PhotoView scale changes. Used for closing PhotoView by pinch-in
|
||||
void listener(PhotoViewControllerValue value) {
|
||||
if (value.scale < 0.16 && photoViewOpened) {
|
||||
if (value.scale! < 0.16 && photoViewOpened) {
|
||||
Navigator.pop(context);
|
||||
photoViewOpened =
|
||||
false; // to avoid multiple pop() when picture are being scaled out too slowly
|
||||
|
@ -157,7 +157,7 @@ class _ZoomableImageState extends State<ZoomableImage> {
|
|||
pageBuilder: (context, _, __) {
|
||||
photoViewOpened = true;
|
||||
return PhotoView(
|
||||
imageProvider: CachedNetworkImageProvider(widget.url),
|
||||
imageProvider: CachedNetworkImageProvider(widget.url!),
|
||||
maxScale: 8.0,
|
||||
minScale: 0.2,
|
||||
controller: controller,
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -11,22 +11,29 @@ import 'cached_image.dart';
|
|||
|
||||
import 'dart:async';
|
||||
|
||||
|
||||
class DownloadsScreen extends StatefulWidget {
|
||||
@override
|
||||
_DownloadsScreenState createState() => _DownloadsScreenState();
|
||||
}
|
||||
|
||||
class _DownloadsScreenState extends State<DownloadsScreen> {
|
||||
|
||||
List<Download> downloads = [];
|
||||
StreamSubscription _stateSubscription;
|
||||
StreamSubscription? _stateSubscription;
|
||||
|
||||
//Sublists
|
||||
List<Download> get downloading => downloads.where((d) => d.state == DownloadState.DOWNLOADING || d.state == DownloadState.POST).toList();
|
||||
List<Download> get queued => downloads.where((d) => d.state == DownloadState.NONE).toList();
|
||||
List<Download> get failed => downloads.where((d) => d.state == DownloadState.ERROR || d.state == DownloadState.DEEZER_ERROR).toList();
|
||||
List<Download> get finished => downloads.where((d) => d.state == DownloadState.DONE).toList();
|
||||
List<Download> get downloading => downloads
|
||||
.where((d) =>
|
||||
d.state == DownloadState.DOWNLOADING || d.state == DownloadState.POST)
|
||||
.toList();
|
||||
List<Download> get queued =>
|
||||
downloads.where((d) => d.state == DownloadState.NONE).toList();
|
||||
List<Download> get failed => downloads
|
||||
.where((d) =>
|
||||
d.state == DownloadState.ERROR ||
|
||||
d.state == DownloadState.DEEZER_ERROR)
|
||||
.toList();
|
||||
List<Download> get finished =>
|
||||
downloads.where((d) => d.state == DownloadState.DONE).toList();
|
||||
|
||||
Future _load() async {
|
||||
//Load downloads
|
||||
|
@ -50,7 +57,9 @@ class _DownloadsScreenState extends State<DownloadsScreen> {
|
|||
if (e['action'] == 'onProgress') {
|
||||
setState(() {
|
||||
for (Map su in e['data']) {
|
||||
downloads.firstWhere((d) => d.id == su['id'], orElse: () => Download()).updateFromJson(su);
|
||||
downloads
|
||||
.firstWhere((d) => d.id == su['id'], orElse: () => Download())
|
||||
.updateFromJson(su);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -61,8 +70,7 @@ class _DownloadsScreenState extends State<DownloadsScreen> {
|
|||
|
||||
@override
|
||||
void dispose() {
|
||||
if (_stateSubscription != null)
|
||||
_stateSubscription.cancel();
|
||||
_stateSubscription?.cancel();
|
||||
_stateSubscription = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
@ -71,24 +79,30 @@ class _DownloadsScreenState extends State<DownloadsScreen> {
|
|||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: FreezerAppBar(
|
||||
'Downloads'.i18n,
|
||||
'Downloads'.i18n,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.delete_sweep, semanticLabel: "Clear all".i18n,),
|
||||
icon: Icon(
|
||||
Icons.delete_sweep,
|
||||
semanticLabel: "Clear all".i18n,
|
||||
),
|
||||
onPressed: () async {
|
||||
await downloadManager.removeDownloads(DownloadState.ERROR);
|
||||
await downloadManager.removeDownloads(DownloadState.DEEZER_ERROR);
|
||||
await downloadManager
|
||||
.removeDownloads(DownloadState.DEEZER_ERROR);
|
||||
await downloadManager.removeDownloads(DownloadState.DONE);
|
||||
await _load();
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon:
|
||||
Icon(downloadManager.running ? Icons.stop : Icons.play_arrow,
|
||||
semanticLabel: downloadManager.running ? "Stop".i18n : "Start".i18n,),
|
||||
icon: Icon(
|
||||
downloadManager.running! ? Icons.stop : Icons.play_arrow,
|
||||
semanticLabel:
|
||||
downloadManager.running! ? "Stop".i18n : "Start".i18n,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
if (downloadManager.running)
|
||||
if (downloadManager.running!)
|
||||
downloadManager.stop();
|
||||
else
|
||||
downloadManager.start();
|
||||
|
@ -101,10 +115,13 @@ class _DownloadsScreenState extends State<DownloadsScreen> {
|
|||
children: [
|
||||
//Now downloading
|
||||
Container(height: 2.0),
|
||||
Column(children: List.generate(downloading.length, (int i) => DownloadTile(
|
||||
downloading[i],
|
||||
updateCallback: () => _load(),
|
||||
))),
|
||||
Column(
|
||||
children: List.generate(
|
||||
downloading.length,
|
||||
(int i) => DownloadTile(
|
||||
downloading[i],
|
||||
updateCallback: () => _load(),
|
||||
))),
|
||||
Container(height: 8.0),
|
||||
|
||||
//Queued
|
||||
|
@ -112,15 +129,15 @@ class _DownloadsScreenState extends State<DownloadsScreen> {
|
|||
Text(
|
||||
'Queued'.i18n,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 24.0,
|
||||
fontWeight: FontWeight.bold
|
||||
),
|
||||
style: TextStyle(fontSize: 24.0, fontWeight: FontWeight.bold),
|
||||
),
|
||||
Column(children: List.generate(queued.length, (int i) => DownloadTile(
|
||||
queued[i],
|
||||
updateCallback: () => _load(),
|
||||
))),
|
||||
Column(
|
||||
children: List.generate(
|
||||
queued.length,
|
||||
(int i) => DownloadTile(
|
||||
queued[i],
|
||||
updateCallback: () => _load(),
|
||||
))),
|
||||
if (queued.length > 0)
|
||||
ListTile(
|
||||
title: Text('Clear queue'.i18n),
|
||||
|
@ -136,15 +153,15 @@ class _DownloadsScreenState extends State<DownloadsScreen> {
|
|||
Text(
|
||||
'Failed'.i18n,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 24.0,
|
||||
fontWeight: FontWeight.bold
|
||||
),
|
||||
style: TextStyle(fontSize: 24.0, fontWeight: FontWeight.bold),
|
||||
),
|
||||
Column(children: List.generate(failed.length, (int i) => DownloadTile(
|
||||
failed[i],
|
||||
updateCallback: () => _load(),
|
||||
))),
|
||||
Column(
|
||||
children: List.generate(
|
||||
failed.length,
|
||||
(int i) => DownloadTile(
|
||||
failed[i],
|
||||
updateCallback: () => _load(),
|
||||
))),
|
||||
//Restart failed
|
||||
if (failed.length > 0)
|
||||
ListTile(
|
||||
|
@ -161,7 +178,8 @@ class _DownloadsScreenState extends State<DownloadsScreen> {
|
|||
leading: Icon(Icons.delete),
|
||||
onTap: () async {
|
||||
await downloadManager.removeDownloads(DownloadState.ERROR);
|
||||
await downloadManager.removeDownloads(DownloadState.DEEZER_ERROR);
|
||||
await downloadManager
|
||||
.removeDownloads(DownloadState.DEEZER_ERROR);
|
||||
await _load();
|
||||
},
|
||||
),
|
||||
|
@ -171,15 +189,15 @@ class _DownloadsScreenState extends State<DownloadsScreen> {
|
|||
Text(
|
||||
'Done'.i18n,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 24.0,
|
||||
fontWeight: FontWeight.bold
|
||||
),
|
||||
style: TextStyle(fontSize: 24.0, fontWeight: FontWeight.bold),
|
||||
),
|
||||
Column(children: List.generate(finished.length, (int i) => DownloadTile(
|
||||
finished[i],
|
||||
updateCallback: () => _load(),
|
||||
))),
|
||||
Column(
|
||||
children: List.generate(
|
||||
finished.length,
|
||||
(int i) => DownloadTile(
|
||||
finished[i],
|
||||
updateCallback: () => _load(),
|
||||
))),
|
||||
if (finished.length > 0)
|
||||
ListTile(
|
||||
title: Text('Clear downloads history'.i18n),
|
||||
|
@ -189,26 +207,26 @@ class _DownloadsScreenState extends State<DownloadsScreen> {
|
|||
await _load();
|
||||
},
|
||||
),
|
||||
|
||||
],
|
||||
)
|
||||
);
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
class DownloadTile extends StatelessWidget {
|
||||
|
||||
final Download download;
|
||||
final Function updateCallback;
|
||||
final Function? updateCallback;
|
||||
DownloadTile(this.download, {this.updateCallback});
|
||||
|
||||
String subtitle() {
|
||||
String out = '';
|
||||
|
||||
if (download.state != DownloadState.DOWNLOADING && download.state != DownloadState.POST) {
|
||||
if (download.state != DownloadState.DOWNLOADING &&
|
||||
download.state != DownloadState.POST) {
|
||||
//Download type
|
||||
if (download.private) out += 'Offline'.i18n;
|
||||
else out += 'External'.i18n;
|
||||
if (download.private!)
|
||||
out += 'Offline'.i18n;
|
||||
else
|
||||
out += 'External'.i18n;
|
||||
out += ' | ';
|
||||
}
|
||||
|
||||
|
@ -223,39 +241,42 @@ class DownloadTile extends StatelessWidget {
|
|||
|
||||
//Downloading show progress
|
||||
if (download.state == DownloadState.DOWNLOADING) {
|
||||
out += ' | ${filesize(download.received, 2)} / ${filesize(download.filesize, 2)}';
|
||||
double progress = download.received.toDouble() / download.filesize.toDouble();
|
||||
out += ' ${(progress*100.0).toStringAsFixed(2)}%';
|
||||
out +=
|
||||
' | ${filesize(download.received, 2)} / ${filesize(download.filesize, 2)}';
|
||||
double progress =
|
||||
download.received!.toDouble() / download.filesize!.toDouble();
|
||||
out += ' ${(progress * 100.0).toStringAsFixed(2)}%';
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
Future onClick(BuildContext context) async {
|
||||
if (download.state != DownloadState.DOWNLOADING && download.state != DownloadState.POST) {
|
||||
if (download.state != DownloadState.DOWNLOADING &&
|
||||
download.state != DownloadState.POST) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: Text('Delete'.i18n),
|
||||
content: Text('Are you sure you want to delete this download?'.i18n),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: Text('Cancel'.i18n),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
TextButton(
|
||||
child: Text('Delete'.i18n),
|
||||
onPressed: () async {
|
||||
await downloadManager.removeDownload(download.id);
|
||||
if (updateCallback != null) updateCallback();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
);
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: Text('Delete'.i18n),
|
||||
content:
|
||||
Text('Are you sure you want to delete this download?'.i18n),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: Text('Cancel'.i18n),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
TextButton(
|
||||
child: Text('Delete'.i18n),
|
||||
onPressed: () async {
|
||||
await downloadManager.removeDownload(download.id);
|
||||
if (updateCallback != null) updateCallback!();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
)
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -267,30 +288,21 @@ class DownloadTile extends StatelessWidget {
|
|||
Icons.query_builder,
|
||||
);
|
||||
case DownloadState.DOWNLOADING:
|
||||
return Icon(
|
||||
Icons.download_rounded
|
||||
);
|
||||
return Icon(Icons.download_rounded);
|
||||
case DownloadState.POST:
|
||||
return Icon(
|
||||
Icons.miscellaneous_services
|
||||
);
|
||||
return Icon(Icons.miscellaneous_services);
|
||||
case DownloadState.DONE:
|
||||
return Icon(
|
||||
Icons.done,
|
||||
color: Colors.green,
|
||||
);
|
||||
case DownloadState.DEEZER_ERROR:
|
||||
return Icon(
|
||||
Icons.error,
|
||||
color: Colors.blue
|
||||
);
|
||||
return Icon(Icons.error, color: Colors.blue);
|
||||
case DownloadState.ERROR:
|
||||
return Icon(
|
||||
Icons.error,
|
||||
color: Colors.red
|
||||
);
|
||||
return Icon(Icons.error, color: Colors.red);
|
||||
default:
|
||||
return Container();
|
||||
}
|
||||
return Container();
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -298,16 +310,16 @@ class DownloadTile extends StatelessWidget {
|
|||
return Column(
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(download.title),
|
||||
title: Text(download.title!),
|
||||
leading: CachedImage(url: download.image),
|
||||
subtitle: Text(subtitle(), maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
subtitle:
|
||||
Text(subtitle(), maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
trailing: trailing(),
|
||||
onTap: () => onClick(context),
|
||||
),
|
||||
if (download.state == DownloadState.DOWNLOADING)
|
||||
LinearProgressIndicator(value: download.progress),
|
||||
if (download.state == DownloadState.POST)
|
||||
LinearProgressIndicator(),
|
||||
if (download.state == DownloadState.POST) LinearProgressIndicator(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
@ -319,12 +331,12 @@ class DownloadLogViewer extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _DownloadLogViewerState extends State<DownloadLogViewer> {
|
||||
|
||||
List<String> data = [];
|
||||
|
||||
//Load log from file
|
||||
Future _load() async {
|
||||
String path = p.join((await getExternalStorageDirectory()).path, 'download.log');
|
||||
String path =
|
||||
p.join((await getExternalStorageDirectory())!.path, 'download.log');
|
||||
File file = File(path);
|
||||
if (await file.exists()) {
|
||||
String _d = await file.readAsString();
|
||||
|
@ -335,7 +347,7 @@ class _DownloadLogViewerState extends State<DownloadLogViewer> {
|
|||
}
|
||||
|
||||
//Get color by log type
|
||||
Color color(String line) {
|
||||
Color? color(String line) {
|
||||
if (line.startsWith('E:')) return Colors.red;
|
||||
if (line.startsWith('W:')) return Colors.orange[600];
|
||||
return null;
|
||||
|
@ -350,22 +362,18 @@ class _DownloadLogViewerState extends State<DownloadLogViewer> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: FreezerAppBar('Download Log'.i18n),
|
||||
body: ListView.builder(
|
||||
itemCount: data.length,
|
||||
itemBuilder: (context, i) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
data[i],
|
||||
style: TextStyle(
|
||||
fontSize: 14.0,
|
||||
color: color(data[i])
|
||||
appBar: FreezerAppBar('Download Log'.i18n),
|
||||
body: ListView.builder(
|
||||
itemCount: data.length,
|
||||
itemBuilder: (context, i) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
data[i],
|
||||
style: TextStyle(fontSize: 14.0, color: color(data[i])),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
);
|
||||
);
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:freezer/settings.dart';
|
||||
|
||||
class LeadingIcon extends StatelessWidget {
|
||||
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
final Color? color;
|
||||
LeadingIcon(this.icon, {this.color});
|
||||
|
||||
@override
|
||||
|
@ -13,9 +13,8 @@ class LeadingIcon extends StatelessWidget {
|
|||
width: 42.0,
|
||||
height: 42.0,
|
||||
decoration: BoxDecoration(
|
||||
color: (color??Theme.of(context).primaryColor).withOpacity(1.0),
|
||||
shape: BoxShape.circle
|
||||
),
|
||||
color: (color ?? Theme.of(context).primaryColor).withOpacity(1.0),
|
||||
shape: BoxShape.circle),
|
||||
child: Icon(
|
||||
icon,
|
||||
color: Colors.white,
|
||||
|
@ -32,35 +31,39 @@ class EmptyLeading extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
class FreezerAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
|
||||
final String title;
|
||||
final String? title;
|
||||
final List<Widget> actions;
|
||||
final Widget bottom;
|
||||
final Widget? bottom;
|
||||
//Should be specified if bottom is specified
|
||||
final double height;
|
||||
|
||||
FreezerAppBar(this.title, {this.actions = const [], this.bottom, this.height = 56.0});
|
||||
|
||||
FreezerAppBar(this.title,
|
||||
{this.actions = const [], this.bottom, this.height = 56.0});
|
||||
|
||||
Size get preferredSize => Size.fromHeight(this.height);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Theme(
|
||||
data: ThemeData(primaryColor: (Theme.of(context).brightness == Brightness.light)?Colors.white:Colors.black),
|
||||
data: ThemeData(
|
||||
primaryColor: (Theme.of(context).brightness == Brightness.light)
|
||||
? Colors.white
|
||||
: Colors.black),
|
||||
child: AppBar(
|
||||
brightness: Theme.of(context).brightness,
|
||||
systemOverlayStyle: Theme.of(context).brightness == Brightness.dark
|
||||
? SystemUiOverlayStyle.dark
|
||||
: SystemUiOverlayStyle.light,
|
||||
elevation: 0.0,
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
title: Text(
|
||||
title,
|
||||
title!,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
actions: actions,
|
||||
bottom: bottom,
|
||||
bottom: bottom as PreferredSizeWidget?,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -78,7 +81,5 @@ class FreezerDivider extends StatelessWidget {
|
|||
}
|
||||
|
||||
TextStyle popupMenuTextStyle() {
|
||||
return TextStyle(
|
||||
color: settings.isDark?Colors.white:Colors.black
|
||||
);
|
||||
}
|
||||
return TextStyle(color: settings.isDark ? Colors.white : Colors.black);
|
||||
}
|
||||
|
|
|
@ -6,8 +6,8 @@ import 'package:freezer/translations.i18n.dart';
|
|||
int counter = 0;
|
||||
|
||||
class ErrorScreen extends StatefulWidget {
|
||||
final String message;
|
||||
const ErrorScreen({this.message, Key key}) : super(key: key);
|
||||
final String? message;
|
||||
const ErrorScreen({this.message, Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_ErrorScreenState createState() => _ErrorScreenState();
|
||||
|
|
|
@ -47,24 +47,24 @@ class FreezerTitle extends StatelessWidget {
|
|||
}
|
||||
|
||||
class HomePageScreen extends StatefulWidget {
|
||||
final HomePage homePage;
|
||||
final DeezerChannel channel;
|
||||
HomePageScreen({this.homePage, this.channel, Key key}) : super(key: key);
|
||||
final HomePage? homePage;
|
||||
final DeezerChannel? channel;
|
||||
HomePageScreen({this.homePage, this.channel, Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_HomePageScreenState createState() => _HomePageScreenState();
|
||||
}
|
||||
|
||||
class _HomePageScreenState extends State<HomePageScreen> {
|
||||
HomePage _homePage;
|
||||
HomePage? _homePage;
|
||||
bool _cancel = false;
|
||||
bool _error = false;
|
||||
|
||||
void _loadChannel() async {
|
||||
HomePage _hp;
|
||||
HomePage? _hp;
|
||||
//Fetch channel from api
|
||||
try {
|
||||
_hp = await deezerAPI.getChannel(widget.channel.target);
|
||||
_hp = await deezerAPI.getChannel(widget.channel!.target);
|
||||
} catch (e) {}
|
||||
if (_hp == null) {
|
||||
//On error
|
||||
|
@ -84,13 +84,11 @@ class _HomePageScreenState extends State<HomePageScreen> {
|
|||
try {
|
||||
if (settings.offlineMode) await deezerAPI.authorize();
|
||||
HomePage _hp = await deezerAPI.homePage();
|
||||
if (_hp != null) {
|
||||
if (_cancel) return;
|
||||
if (_hp.sections.length == 0) return;
|
||||
setState(() => _homePage = _hp);
|
||||
//Save to cache
|
||||
await _homePage.save();
|
||||
}
|
||||
if (_cancel) return;
|
||||
if (_hp.sections!.length == 0) return;
|
||||
setState(() => _homePage = _hp);
|
||||
//Save to cache
|
||||
await _homePage!.save();
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
|
@ -103,8 +101,8 @@ class _HomePageScreenState extends State<HomePageScreen> {
|
|||
_loadHomePage();
|
||||
return;
|
||||
}
|
||||
if (widget.homePage.sections == null ||
|
||||
widget.homePage.sections.length == 0) {
|
||||
if (widget.homePage!.sections == null ||
|
||||
widget.homePage!.sections!.length == 0) {
|
||||
_loadHomePage();
|
||||
return;
|
||||
}
|
||||
|
@ -135,15 +133,15 @@ class _HomePageScreenState extends State<HomePageScreen> {
|
|||
if (_error) return ErrorScreen();
|
||||
return Column(
|
||||
children: List.generate(
|
||||
_homePage.sections.length,
|
||||
_homePage!.sections!.length,
|
||||
(i) {
|
||||
switch (_homePage.sections[i].layout) {
|
||||
switch (_homePage!.sections![i].layout) {
|
||||
case HomePageSectionLayout.ROW:
|
||||
return HomepageRowSection(_homePage.sections[i]);
|
||||
return HomepageRowSection(_homePage!.sections![i]);
|
||||
case HomePageSectionLayout.GRID:
|
||||
return HomePageGridSection(_homePage.sections[i]);
|
||||
return HomePageGridSection(_homePage!.sections![i]);
|
||||
default:
|
||||
return HomepageRowSection(_homePage.sections[i]);
|
||||
return HomepageRowSection(_homePage!.sections![i]);
|
||||
}
|
||||
},
|
||||
));
|
||||
|
@ -171,9 +169,9 @@ class HomepageRowSection extends StatelessWidget {
|
|||
subtitle: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: List.generate(section.items.length + 1, (j) {
|
||||
children: List.generate(section.items!.length + 1, (j) {
|
||||
//Has more items
|
||||
if (j == section.items.length) {
|
||||
if (j == section.items!.length) {
|
||||
if (section.hasMore ?? false) {
|
||||
return TextButton(
|
||||
child: Text(
|
||||
|
@ -197,7 +195,7 @@ class HomepageRowSection extends StatelessWidget {
|
|||
}
|
||||
|
||||
//Show item
|
||||
HomePageItem item = section.items[j];
|
||||
HomePageItem item = section.items![j];
|
||||
return HomePageItemWidget(item);
|
||||
}),
|
||||
),
|
||||
|
@ -225,9 +223,9 @@ class HomePageGridSection extends StatelessWidget {
|
|||
),
|
||||
subtitle: Wrap(
|
||||
alignment: WrapAlignment.spaceAround,
|
||||
children: List.generate(section.items.length, (i) {
|
||||
children: List.generate(section.items!.length, (i) {
|
||||
//Item
|
||||
return HomePageItemWidget(section.items[i]);
|
||||
return HomePageItemWidget(section.items![i]);
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
@ -306,7 +304,8 @@ class HomePageItemWidget extends StatelessWidget {
|
|||
builder: (context) => ShowScreen(item.value)));
|
||||
},
|
||||
);
|
||||
default:
|
||||
return const SizedBox(height: 0, width: 0);
|
||||
}
|
||||
return Container(height: 0, width: 0);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,11 +18,10 @@ class SpotifyImporterV1 extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _SpotifyImporterV1State extends State<SpotifyImporterV1> {
|
||||
|
||||
String _url;
|
||||
late String _url;
|
||||
bool _error = false;
|
||||
bool _loading = false;
|
||||
SpotifyPlaylist _data;
|
||||
SpotifyPlaylist? _data;
|
||||
|
||||
//Load URL
|
||||
Future _load() async {
|
||||
|
@ -31,7 +30,7 @@ class _SpotifyImporterV1State extends State<SpotifyImporterV1> {
|
|||
_loading = true;
|
||||
});
|
||||
try {
|
||||
String uri = await SpotifyScrapper.resolveUrl(_url);
|
||||
String? uri = await SpotifyScrapper.resolveUrl(_url);
|
||||
|
||||
//Error/NonPlaylist
|
||||
if (uri == null || uri.split(':')[1] != 'playlist') {
|
||||
|
@ -41,7 +40,6 @@ class _SpotifyImporterV1State extends State<SpotifyImporterV1> {
|
|||
SpotifyPlaylist data = await SpotifyScrapper.playlist(uri);
|
||||
setState(() => _data = data);
|
||||
return;
|
||||
|
||||
} catch (e, st) {
|
||||
print('$e, $st');
|
||||
setState(() {
|
||||
|
@ -54,8 +52,8 @@ class _SpotifyImporterV1State extends State<SpotifyImporterV1> {
|
|||
|
||||
//Start importing
|
||||
Future _start() async {
|
||||
List<ImporterTrack> tracks = _data.toImporter();
|
||||
await importer.start(context, _data.name, _data.description, tracks);
|
||||
List<ImporterTrack> tracks = _data!.toImporter();
|
||||
await importer.start(context, _data!.name, _data!.description, tracks);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -65,7 +63,9 @@ class _SpotifyImporterV1State extends State<SpotifyImporterV1> {
|
|||
body: ListView(
|
||||
children: <Widget>[
|
||||
ListTile(
|
||||
title: Text('Currently supporting only Spotify, with 100 tracks limit'.i18n),
|
||||
title: Text(
|
||||
'Currently supporting only Spotify, with 100 tracks limit'
|
||||
.i18n),
|
||||
subtitle: Text('Due to API limitations'.i18n),
|
||||
leading: Icon(
|
||||
Icons.warning,
|
||||
|
@ -73,13 +73,13 @@ class _SpotifyImporterV1State extends State<SpotifyImporterV1> {
|
|||
),
|
||||
),
|
||||
FreezerDivider(),
|
||||
Container(height: 16.0,),
|
||||
Container(
|
||||
height: 16.0,
|
||||
),
|
||||
Text(
|
||||
'Enter your playlist link below'.i18n,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 20.0
|
||||
),
|
||||
style: TextStyle(fontSize: 20.0),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||
|
@ -92,55 +92,59 @@ class _SpotifyImporterV1State extends State<SpotifyImporterV1> {
|
|||
_url = s;
|
||||
_load();
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
hintText: 'URL'
|
||||
),
|
||||
decoration: InputDecoration(hintText: 'URL'),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.search, semanticLabel: "Search".i18n,),
|
||||
icon: Icon(
|
||||
Icons.search,
|
||||
semanticLabel: "Search".i18n,
|
||||
),
|
||||
onPressed: () => _load(),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(height: 8.0,),
|
||||
Container(
|
||||
height: 8.0,
|
||||
),
|
||||
|
||||
if (_data == null && _loading)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
CircularProgressIndicator()
|
||||
],
|
||||
children: <Widget>[CircularProgressIndicator()],
|
||||
),
|
||||
if (_error)
|
||||
ListTile(
|
||||
title: Text('Error loading URL!'.i18n),
|
||||
leading: Icon(Icons.error, color: Colors.red,),
|
||||
leading: Icon(
|
||||
Icons.error,
|
||||
color: Colors.red,
|
||||
),
|
||||
),
|
||||
//Playlist
|
||||
if (_data != null)
|
||||
...[
|
||||
FreezerDivider(),
|
||||
ListTile(
|
||||
title: Text(_data.name),
|
||||
subtitle: Text((_data.description ?? '') == '' ? '${_data.tracks.length} tracks' : _data.description),
|
||||
leading: Image.network(_data.image??'http://cdn-images.deezer.com/images/cover//256x256-000000-80-0-0.jpg')
|
||||
if (_data != null) ...[
|
||||
FreezerDivider(),
|
||||
ListTile(
|
||||
title: Text(_data!.name!),
|
||||
subtitle: Text((_data!.description ?? '') == ''
|
||||
? '${_data!.tracks!.length} tracks'
|
||||
: _data!.description!),
|
||||
leading: Image.network(_data!.image ??
|
||||
'http://cdn-images.deezer.com/images/cover//256x256-000000-80-0-0.jpg')),
|
||||
ImporterSettings(),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 4.0, horizontal: 16.0),
|
||||
child: ElevatedButton(
|
||||
child: Text('Start import'.i18n),
|
||||
onPressed: () async {
|
||||
await _start();
|
||||
Navigator.of(context).pushReplacement(MaterialPageRoute(
|
||||
builder: (context) => ImporterStatusScreen()));
|
||||
},
|
||||
),
|
||||
ImporterSettings(),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 4.0, horizontal: 16.0),
|
||||
child: ElevatedButton(
|
||||
child: Text('Start import'.i18n),
|
||||
onPressed: () async {
|
||||
await _start();
|
||||
Navigator.of(context).pushReplacement(MaterialPageRoute(
|
||||
builder: (context) => ImporterStatusScreen()
|
||||
));
|
||||
},
|
||||
),
|
||||
),
|
||||
]
|
||||
),
|
||||
]
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@ -176,9 +180,8 @@ class ImporterStatusScreen extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _ImporterStatusScreenState extends State<ImporterStatusScreen> {
|
||||
|
||||
bool _done = false;
|
||||
StreamSubscription _subscription;
|
||||
StreamSubscription? _subscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
@ -195,22 +198,19 @@ class _ImporterStatusScreenState extends State<ImporterStatusScreen> {
|
|||
if (importer.done) {
|
||||
_done = true;
|
||||
importer.done = false;
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (_subscription != null)
|
||||
_subscription.cancel();
|
||||
_subscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
|
@ -223,52 +223,74 @@ class _ImporterStatusScreenState extends State<ImporterStatusScreen> {
|
|||
padding: EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
CircularProgressIndicator()
|
||||
],
|
||||
children: <Widget>[CircularProgressIndicator()],
|
||||
),
|
||||
),
|
||||
|
||||
// Progress indicator
|
||||
Container(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: <Widget>[
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Icon(Icons.import_export, size: 24.0,),
|
||||
Container(width: 4.0,),
|
||||
Text('${importer.ok+importer.error}/${importer.tracks.length}', style: TextStyle(fontSize: 24.0),)
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Icon(Icons.done, size: 24.0,),
|
||||
Container(width: 4.0,),
|
||||
Text('${importer.ok}', style: TextStyle(fontSize: 24.0),)
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Icon(Icons.error, size: 24.0,),
|
||||
Container(width: 4.0,),
|
||||
Text('${importer.error}', style: TextStyle(fontSize: 24.0),),
|
||||
],
|
||||
),
|
||||
|
||||
//When Done
|
||||
if (_done)
|
||||
TextButton(
|
||||
child: Text('Playlist menu'.i18n),
|
||||
onPressed: () {
|
||||
MenuSheet m = MenuSheet(context);
|
||||
m.defaultPlaylistMenu(importer.playlist);
|
||||
},
|
||||
// Progress indicator
|
||||
Container(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: <Widget>[
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
Icons.import_export,
|
||||
size: 24.0,
|
||||
),
|
||||
Container(
|
||||
width: 4.0,
|
||||
),
|
||||
Text(
|
||||
'${importer.ok + importer.error}/${importer.tracks.length}',
|
||||
style: TextStyle(fontSize: 24.0),
|
||||
)
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
Icons.done,
|
||||
size: 24.0,
|
||||
),
|
||||
Container(
|
||||
width: 4.0,
|
||||
),
|
||||
Text(
|
||||
'${importer.ok}',
|
||||
style: TextStyle(fontSize: 24.0),
|
||||
)
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
Icons.error,
|
||||
size: 24.0,
|
||||
),
|
||||
Container(
|
||||
width: 4.0,
|
||||
),
|
||||
Text(
|
||||
'${importer.error}',
|
||||
style: TextStyle(fontSize: 24.0),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
//When Done
|
||||
if (_done)
|
||||
TextButton(
|
||||
child: Text('Playlist menu'.i18n),
|
||||
onPressed: () {
|
||||
MenuSheet m = MenuSheet(context);
|
||||
m.defaultPlaylistMenu(importer.playlist!);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -280,14 +302,13 @@ class _ImporterStatusScreenState extends State<ImporterStatusScreen> {
|
|||
ImporterTrack t = importer.tracks[i];
|
||||
return ListTile(
|
||||
leading: t.state.icon,
|
||||
title: Text(t.title),
|
||||
title: Text(t.title!),
|
||||
subtitle: Text(
|
||||
t.artists.join(", "),
|
||||
t.artists!.join(", "),
|
||||
maxLines: 1,
|
||||
),
|
||||
);
|
||||
})
|
||||
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@ -300,16 +321,14 @@ class SpotifyImporterV2 extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _SpotifyImporterV2State extends State<SpotifyImporterV2> {
|
||||
|
||||
bool _authorizing = false;
|
||||
String _clientId;
|
||||
String _clientSecret;
|
||||
SpotifyAPIWrapper spotify;
|
||||
String? _clientId;
|
||||
String? _clientSecret;
|
||||
final SpotifyAPIWrapper spotify = SpotifyAPIWrapper();
|
||||
|
||||
//Spotify authorization flow
|
||||
Future _authorize() async {
|
||||
setState(() => _authorizing = true);
|
||||
spotify = SpotifyAPIWrapper();
|
||||
await spotify.authorize(_clientId, _clientSecret);
|
||||
//Save credentials
|
||||
settings.spotifyClientId = _clientId;
|
||||
|
@ -318,8 +337,7 @@ class _SpotifyImporterV2State extends State<SpotifyImporterV2> {
|
|||
setState(() => _authorizing = false);
|
||||
//Redirect
|
||||
Navigator.of(context).pushReplacement(MaterialPageRoute(
|
||||
builder: (context) => SpotifyImporterV2Main(spotify)
|
||||
));
|
||||
builder: (context) => SpotifyImporterV2Main(spotify)));
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -328,30 +346,24 @@ class _SpotifyImporterV2State extends State<SpotifyImporterV2> {
|
|||
_clientSecret = settings.spotifyClientSecret;
|
||||
|
||||
//Try saved
|
||||
spotify = SpotifyAPIWrapper();
|
||||
spotify.trySaved().then((r) {
|
||||
if (r) {
|
||||
Navigator.of(context).pushReplacement(MaterialPageRoute(
|
||||
builder: (context) => SpotifyImporterV2Main(spotify)
|
||||
));
|
||||
builder: (context) => SpotifyImporterV2Main(spotify)));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
//Stop server
|
||||
if (spotify != null) {
|
||||
spotify.cancelAuthorize();
|
||||
}
|
||||
spotify.cancelAuthorize();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
|
@ -361,7 +373,8 @@ class _SpotifyImporterV2State extends State<SpotifyImporterV2> {
|
|||
Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 12.0, horizontal: 8.0),
|
||||
child: Text(
|
||||
"This importer requires Spotify Client ID and Client Secret. To obtain them:".i18n,
|
||||
"This importer requires Spotify Client ID and Client Secret. To obtain them:"
|
||||
.i18n,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 18.0,
|
||||
|
@ -369,15 +382,15 @@ class _SpotifyImporterV2State extends State<SpotifyImporterV2> {
|
|||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: Text(
|
||||
"1. Go to: developer.spotify.com/dashboard and create an app.".i18n,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 16.0,
|
||||
),
|
||||
)
|
||||
),
|
||||
padding: EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: Text(
|
||||
"1. Go to: developer.spotify.com/dashboard and create an app."
|
||||
.i18n,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 16.0,
|
||||
),
|
||||
)),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 4.0, horizontal: 16.0),
|
||||
child: ElevatedButton(
|
||||
|
@ -389,22 +402,27 @@ class _SpotifyImporterV2State extends State<SpotifyImporterV2> {
|
|||
),
|
||||
Container(height: 16.0),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: Text(
|
||||
"2. In the app you just created go to settings, and set the Redirect URL to: ".i18n + "http://localhost:42069",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 16.0,
|
||||
),
|
||||
)
|
||||
),
|
||||
padding: EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: Text(
|
||||
"2. In the app you just created go to settings, and set the Redirect URL to: "
|
||||
.i18n +
|
||||
"http://localhost:42069",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 16.0,
|
||||
),
|
||||
)),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 4.0, horizontal: 16.0),
|
||||
child: ElevatedButton(
|
||||
child: Text("Copy the Redirect URL".i18n),
|
||||
onPressed: () async {
|
||||
await Clipboard.setData(new ClipboardData(text: "http://localhost:42069"));
|
||||
Fluttertoast.showToast(msg: "Copied".i18n, gravity: ToastGravity.BOTTOM, toastLength: Toast.LENGTH_SHORT);
|
||||
await Clipboard.setData(
|
||||
new ClipboardData(text: "http://localhost:42069"));
|
||||
Fluttertoast.showToast(
|
||||
msg: "Copied".i18n,
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
toastLength: Toast.LENGTH_SHORT);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
@ -416,9 +434,7 @@ class _SpotifyImporterV2State extends State<SpotifyImporterV2> {
|
|||
Flexible(
|
||||
child: TextField(
|
||||
controller: TextEditingController(text: _clientId),
|
||||
decoration: InputDecoration(
|
||||
labelText: "Client ID".i18n
|
||||
),
|
||||
decoration: InputDecoration(labelText: "Client ID".i18n),
|
||||
onChanged: (v) => setState(() => _clientId = v),
|
||||
),
|
||||
),
|
||||
|
@ -427,9 +443,8 @@ class _SpotifyImporterV2State extends State<SpotifyImporterV2> {
|
|||
child: TextField(
|
||||
controller: TextEditingController(text: _clientSecret),
|
||||
obscureText: true,
|
||||
decoration: InputDecoration(
|
||||
labelText: "Client Secret".i18n
|
||||
),
|
||||
decoration:
|
||||
InputDecoration(labelText: "Client Secret".i18n),
|
||||
onChanged: (v) => setState(() => _clientSecret = v),
|
||||
),
|
||||
),
|
||||
|
@ -439,20 +454,19 @@ class _SpotifyImporterV2State extends State<SpotifyImporterV2> {
|
|||
Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 4.0, horizontal: 16.0),
|
||||
child: ElevatedButton(
|
||||
child: Text("Authorize".i18n),
|
||||
onPressed: (_clientId != null && _clientSecret != null && !_authorizing)
|
||||
? () => _authorize()
|
||||
: null
|
||||
),
|
||||
child: Text("Authorize".i18n),
|
||||
onPressed: (_clientId != null &&
|
||||
_clientSecret != null &&
|
||||
!_authorizing)
|
||||
? () => _authorize()
|
||||
: null),
|
||||
),
|
||||
if (_authorizing)
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CircularProgressIndicator()
|
||||
],
|
||||
children: [CircularProgressIndicator()],
|
||||
),
|
||||
)
|
||||
],
|
||||
|
@ -462,21 +476,19 @@ class _SpotifyImporterV2State extends State<SpotifyImporterV2> {
|
|||
}
|
||||
|
||||
class SpotifyImporterV2Main extends StatefulWidget {
|
||||
|
||||
SpotifyAPIWrapper spotify;
|
||||
SpotifyImporterV2Main(this.spotify, {Key key}): super(key: key);
|
||||
final SpotifyAPIWrapper spotify;
|
||||
SpotifyImporterV2Main(this.spotify, {Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_SpotifyImporterV2MainState createState() => _SpotifyImporterV2MainState();
|
||||
}
|
||||
|
||||
class _SpotifyImporterV2MainState extends State<SpotifyImporterV2Main> {
|
||||
|
||||
String _url;
|
||||
late String _url;
|
||||
bool _urlLoading = false;
|
||||
spotify.Playlist _urlPlaylist;
|
||||
spotify.Playlist? _urlPlaylist;
|
||||
bool _playlistsLoading = true;
|
||||
List<spotify.PlaylistSimple> _playlists;
|
||||
List<spotify.PlaylistSimple>? _playlists;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
@ -486,7 +498,7 @@ class _SpotifyImporterV2MainState extends State<SpotifyImporterV2Main> {
|
|||
|
||||
//Load playlists
|
||||
Future _loadPlaylists() async {
|
||||
var pages = widget.spotify.spotify.users.playlists(widget.spotify.me.id);
|
||||
var pages = widget.spotify.spotify.users.playlists(widget.spotify.me.id!);
|
||||
_playlists = List.from(await pages.all());
|
||||
setState(() => _playlistsLoading = false);
|
||||
}
|
||||
|
@ -495,56 +507,62 @@ class _SpotifyImporterV2MainState extends State<SpotifyImporterV2Main> {
|
|||
setState(() => _urlLoading = true);
|
||||
//Resolve URL
|
||||
try {
|
||||
String uri = await SpotifyScrapper.resolveUrl(_url);
|
||||
String? uri = await SpotifyScrapper.resolveUrl(_url);
|
||||
//Error/NonPlaylist
|
||||
if (uri == null || uri.split(':')[1] != 'playlist') {
|
||||
throw Exception();
|
||||
}
|
||||
//Get playlist
|
||||
spotify.Playlist playlist = await widget.spotify.spotify.playlists.get(uri.split(":")[2]);
|
||||
spotify.Playlist playlist =
|
||||
await widget.spotify.spotify.playlists.get(uri.split(":")[2]);
|
||||
setState(() {
|
||||
_urlLoading = false;
|
||||
_urlPlaylist = playlist;
|
||||
});
|
||||
} catch (e) {
|
||||
Fluttertoast.showToast(msg: "Invalid/Unsupported URL".i18n, gravity: ToastGravity.BOTTOM, toastLength: Toast.LENGTH_SHORT);
|
||||
Fluttertoast.showToast(
|
||||
msg: "Invalid/Unsupported URL".i18n,
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
toastLength: Toast.LENGTH_SHORT);
|
||||
setState(() => _urlLoading = false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Future _startImport(String title, String description, String id) async {
|
||||
Future _startImport(String? title, String? description, String? id) async {
|
||||
//Show loading dialog
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => WillPopScope(
|
||||
onWillPop: () => Future.value(false),
|
||||
child: AlertDialog(
|
||||
title: Text("Please wait...".i18n),
|
||||
content: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [CircularProgressIndicator()],
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => WillPopScope(
|
||||
onWillPop: () => Future.value(false),
|
||||
child: AlertDialog(
|
||||
title: Text("Please wait...".i18n),
|
||||
content: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [CircularProgressIndicator()],
|
||||
))));
|
||||
|
||||
try {
|
||||
//Fetch entire playlist
|
||||
var pages = widget.spotify.spotify.playlists.getTracksByPlaylistId(id);
|
||||
var all = await pages.all();
|
||||
//Map to importer track
|
||||
List<ImporterTrack> tracks = all.map((t) => ImporterTrack(t.name, t.artists.map((a) => a.name).toList(), isrc: t.externalIds.isrc)).toList();
|
||||
List<ImporterTrack> tracks = all
|
||||
.map((t) => ImporterTrack(
|
||||
t.name, t.artists!.map((a) => a.name).toList(),
|
||||
isrc: t.externalIds!.isrc))
|
||||
.toList();
|
||||
await importer.start(context, title, description, tracks);
|
||||
//Route
|
||||
Navigator.of(context).pop();
|
||||
Navigator.of(context).pushReplacement(MaterialPageRoute(
|
||||
builder: (context) => ImporterStatusScreen()
|
||||
));
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(builder: (context) => ImporterStatusScreen()));
|
||||
} catch (e) {
|
||||
Fluttertoast.showToast(msg: e.toString(), gravity: ToastGravity.BOTTOM, toastLength: Toast.LENGTH_SHORT);
|
||||
Fluttertoast.showToast(
|
||||
msg: e.toString(),
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
toastLength: Toast.LENGTH_SHORT);
|
||||
Navigator.of(context).pop();
|
||||
return;
|
||||
}
|
||||
|
@ -553,30 +571,24 @@ class _SpotifyImporterV2MainState extends State<SpotifyImporterV2Main> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: FreezerAppBar("Spotify Importer v2".i18n),
|
||||
body: ListView(
|
||||
children: [
|
||||
appBar: FreezerAppBar("Spotify Importer v2".i18n),
|
||||
body: ListView(
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
|
||||
child: Text(
|
||||
'Logged in as: '.i18n + widget.spotify.me.displayName,
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 18.0,
|
||||
fontWeight: FontWeight.bold
|
||||
)
|
||||
),
|
||||
'Logged in as: '.i18n + widget.spotify.me.displayName!,
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.center,
|
||||
style:
|
||||
TextStyle(fontSize: 18.0, fontWeight: FontWeight.bold)),
|
||||
),
|
||||
FreezerDivider(),
|
||||
Container(height: 4.0),
|
||||
Text(
|
||||
"Options".i18n,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 18.0,
|
||||
fontWeight: FontWeight.bold
|
||||
),
|
||||
style: TextStyle(fontSize: 18.0, fontWeight: FontWeight.bold),
|
||||
),
|
||||
ImporterSettings(),
|
||||
FreezerDivider(),
|
||||
|
@ -584,30 +596,23 @@ class _SpotifyImporterV2MainState extends State<SpotifyImporterV2Main> {
|
|||
Text(
|
||||
"Import playlists by URL".i18n,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 18.0,
|
||||
fontWeight: FontWeight.bold
|
||||
),
|
||||
style: TextStyle(fontSize: 18.0, fontWeight: FontWeight.bold),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
decoration: InputDecoration(
|
||||
hintText: "URL".i18n
|
||||
),
|
||||
onChanged: (v) => setState(() => _url = v)
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
decoration: InputDecoration(hintText: "URL".i18n),
|
||||
onChanged: (v) => setState(() => _url = v)),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.search),
|
||||
onPressed: () => _loadUrl(),
|
||||
)
|
||||
],
|
||||
)
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.search),
|
||||
onPressed: () => _loadUrl(),
|
||||
)
|
||||
],
|
||||
)),
|
||||
if (_urlLoading)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
|
@ -620,32 +625,26 @@ class _SpotifyImporterV2MainState extends State<SpotifyImporterV2Main> {
|
|||
),
|
||||
if (_urlPlaylist != null)
|
||||
ListTile(
|
||||
title: Text(_urlPlaylist.name),
|
||||
subtitle: Text(_urlPlaylist.description ?? ''),
|
||||
leading: Image.network(_urlPlaylist.images.first?.url??"http://cdn-images.deezer.com/images/cover//256x256-000000-80-0-0.jpg")
|
||||
),
|
||||
title: Text(_urlPlaylist!.name!),
|
||||
subtitle: Text(_urlPlaylist!.description ?? ''),
|
||||
leading: Image.network(_urlPlaylist!.images!.first.url ??
|
||||
"http://cdn-images.deezer.com/images/cover//256x256-000000-80-0-0.jpg")),
|
||||
if (_urlPlaylist != null)
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: ElevatedButton(
|
||||
child: Text("Import".i18n),
|
||||
onPressed: () {
|
||||
_startImport(_urlPlaylist.name, _urlPlaylist.description, _urlPlaylist.id);
|
||||
}
|
||||
)
|
||||
),
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: ElevatedButton(
|
||||
child: Text("Import".i18n),
|
||||
onPressed: () {
|
||||
_startImport(_urlPlaylist!.name,
|
||||
_urlPlaylist!.description, _urlPlaylist!.id);
|
||||
})),
|
||||
|
||||
// Playlists
|
||||
FreezerDivider(),
|
||||
Container(height: 4.0),
|
||||
Text(
|
||||
"Playlists".i18n,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 18.0,
|
||||
fontWeight: FontWeight.bold
|
||||
)
|
||||
),
|
||||
Text("Playlists".i18n,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 18.0, fontWeight: FontWeight.bold)),
|
||||
Container(height: 4.0),
|
||||
if (_playlistsLoading)
|
||||
Row(
|
||||
|
@ -658,19 +657,19 @@ class _SpotifyImporterV2MainState extends State<SpotifyImporterV2Main> {
|
|||
],
|
||||
),
|
||||
if (!_playlistsLoading && _playlists != null)
|
||||
...List.generate(_playlists.length, (i) {
|
||||
spotify.PlaylistSimple p = _playlists[i];
|
||||
...List.generate(_playlists!.length, (i) {
|
||||
spotify.PlaylistSimple p = _playlists![i];
|
||||
return ListTile(
|
||||
title: Text(p.name, maxLines: 1),
|
||||
subtitle: Text(p.owner.displayName, maxLines: 1),
|
||||
leading: Image.network(p.images.first?.url??"http://cdn-images.deezer.com/images/cover//256x256-000000-80-0-0.jpg"),
|
||||
title: Text(p.name!, maxLines: 1),
|
||||
subtitle: Text(p.owner!.displayName!, maxLines: 1),
|
||||
leading: Image.network(p.images!.first.url ??
|
||||
"http://cdn-images.deezer.com/images/cover//256x256-000000-80-0-0.jpg"),
|
||||
onTap: () {
|
||||
_startImport(p.name, "", p.id);
|
||||
},
|
||||
);
|
||||
})
|
||||
],
|
||||
)
|
||||
);
|
||||
],
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -65,7 +65,7 @@ class LibraryScreen extends StatelessWidget {
|
|||
Container(
|
||||
height: 4.0,
|
||||
),
|
||||
if (!downloadManager.running && downloadManager.queueSize > 0)
|
||||
if (!downloadManager.running! && downloadManager.queueSize! > 0)
|
||||
ListTile(
|
||||
title: Text('Downloads'.i18n),
|
||||
leading: LeadingIcon(Icons.file_download, color: Colors.grey),
|
||||
|
@ -82,7 +82,7 @@ class LibraryScreen extends StatelessWidget {
|
|||
title: Text('Shuffle'.i18n),
|
||||
leading: LeadingIcon(Icons.shuffle, color: Color(0xffeca704)),
|
||||
onTap: () async {
|
||||
List<Track> tracks = await deezerAPI.libraryShuffle();
|
||||
List<Track> tracks = (await deezerAPI.libraryShuffle())!;
|
||||
playerHelper.playFromTrackList(
|
||||
tracks,
|
||||
tracks[0].id,
|
||||
|
@ -197,7 +197,7 @@ class LibraryScreen extends StatelessWidget {
|
|||
children: <Widget>[CircularProgressIndicator()],
|
||||
),
|
||||
);
|
||||
List<String> data = snapshot.data;
|
||||
List<String> data = snapshot.data! as List<String>;
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
ListTile(
|
||||
|
@ -246,38 +246,38 @@ class _LibraryTracksState extends State<LibraryTracks> {
|
|||
bool _loading = false;
|
||||
bool _loadingTracks = false;
|
||||
ScrollController _scrollController = ScrollController();
|
||||
List<Track> tracks = [];
|
||||
List<Track> allTracks = [];
|
||||
int trackCount;
|
||||
Sorting _sort = Sorting(sourceType: SortSourceTypes.TRACKS);
|
||||
List<Track?>? tracks = [];
|
||||
List<Track?> allTracks = [];
|
||||
int? trackCount;
|
||||
Sorting? _sort = Sorting(sourceType: SortSourceTypes.TRACKS);
|
||||
|
||||
Playlist get _playlist => Playlist(id: deezerAPI.favoritesPlaylistId);
|
||||
|
||||
List<Track> get _sorted {
|
||||
List<Track> tcopy = List.from(tracks);
|
||||
tcopy.sort((a, b) => a.addedDate.compareTo(b.addedDate));
|
||||
switch (_sort.type) {
|
||||
List<Track> tcopy = List.from(tracks!);
|
||||
tcopy.sort((a, b) => a.addedDate!.compareTo(b.addedDate!));
|
||||
switch (_sort!.type) {
|
||||
case SortType.ALPHABETIC:
|
||||
tcopy.sort((a, b) => a.title.compareTo(b.title));
|
||||
tcopy.sort((a, b) => a.title!.compareTo(b.title!));
|
||||
break;
|
||||
case SortType.ARTIST:
|
||||
tcopy.sort((a, b) => a.artists[0].name
|
||||
tcopy.sort((a, b) => a.artists![0].name!
|
||||
.toLowerCase()
|
||||
.compareTo(b.artists[0].name.toLowerCase()));
|
||||
.compareTo(b.artists![0].name!.toLowerCase()));
|
||||
break;
|
||||
case SortType.DEFAULT:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
//Reverse
|
||||
if (_sort.reverse) return tcopy.reversed.toList();
|
||||
if (_sort!.reverse!) return tcopy.reversed.toList();
|
||||
return tcopy;
|
||||
}
|
||||
|
||||
Future _reverse() async {
|
||||
setState(() => _sort.reverse = !_sort.reverse);
|
||||
setState(() => _sort!.reverse = !_sort!.reverse!);
|
||||
//Save sorting in cache
|
||||
int index = Sorting.index(SortSourceTypes.TRACKS);
|
||||
int? index = Sorting.index(SortSourceTypes.TRACKS);
|
||||
if (index != null) {
|
||||
cache.sorts[index] = _sort;
|
||||
} else {
|
||||
|
@ -286,17 +286,17 @@ class _LibraryTracksState extends State<LibraryTracks> {
|
|||
await cache.save();
|
||||
|
||||
//Preload for sorting
|
||||
if (tracks.length < (trackCount ?? 0)) _loadFull();
|
||||
if (tracks!.length < (trackCount ?? 0)) _loadFull();
|
||||
}
|
||||
|
||||
Future _load() async {
|
||||
//Already loaded
|
||||
if (trackCount != null && tracks.length >= trackCount) {
|
||||
if (trackCount != null && tracks!.length >= trackCount!) {
|
||||
//Update tracks cache if fully loaded
|
||||
if (cache.libraryTracks == null ||
|
||||
cache.libraryTracks.length != trackCount) {
|
||||
cache.libraryTracks!.length != trackCount) {
|
||||
setState(() {
|
||||
cache.libraryTracks = tracks.map((t) => t.id).toList();
|
||||
cache.libraryTracks = tracks!.map((t) => t!.id).toList();
|
||||
});
|
||||
await cache.save();
|
||||
}
|
||||
|
@ -306,11 +306,11 @@ class _LibraryTracksState extends State<LibraryTracks> {
|
|||
ConnectivityResult connectivity = await Connectivity().checkConnectivity();
|
||||
if (connectivity != ConnectivityResult.none) {
|
||||
setState(() => _loading = true);
|
||||
int pos = tracks.length;
|
||||
int pos = tracks!.length;
|
||||
|
||||
if (trackCount == null || tracks.length == 0) {
|
||||
if (trackCount == null || tracks!.length == 0) {
|
||||
//Load tracks as a playlist
|
||||
Playlist favPlaylist;
|
||||
Playlist? favPlaylist;
|
||||
try {
|
||||
favPlaylist = await deezerAPI.playlist(deezerAPI.favoritesPlaylistId);
|
||||
} catch (e) {}
|
||||
|
@ -321,8 +321,8 @@ class _LibraryTracksState extends State<LibraryTracks> {
|
|||
}
|
||||
//Update
|
||||
setState(() {
|
||||
trackCount = favPlaylist.trackCount;
|
||||
if (tracks.length == 0) tracks = favPlaylist.tracks;
|
||||
trackCount = favPlaylist!.trackCount;
|
||||
if (tracks!.length == 0) tracks = favPlaylist.tracks;
|
||||
_makeFavorite();
|
||||
_loading = false;
|
||||
});
|
||||
|
@ -333,7 +333,7 @@ class _LibraryTracksState extends State<LibraryTracks> {
|
|||
if (_loadingTracks) return;
|
||||
_loadingTracks = true;
|
||||
|
||||
List<Track> _t;
|
||||
List<Track>? _t;
|
||||
try {
|
||||
_t = await deezerAPI.playlistTracksPage(
|
||||
deezerAPI.favoritesPlaylistId, pos);
|
||||
|
@ -344,7 +344,7 @@ class _LibraryTracksState extends State<LibraryTracks> {
|
|||
return;
|
||||
}
|
||||
setState(() {
|
||||
tracks.addAll(_t);
|
||||
tracks!.addAll(_t!);
|
||||
_makeFavorite();
|
||||
_loading = false;
|
||||
_loadingTracks = false;
|
||||
|
@ -354,14 +354,14 @@ class _LibraryTracksState extends State<LibraryTracks> {
|
|||
|
||||
//Load all tracks
|
||||
Future _loadFull() async {
|
||||
if (tracks.length == 0 || tracks.length < (trackCount ?? 0)) {
|
||||
Playlist p;
|
||||
if (tracks!.length == 0 || tracks!.length < (trackCount ?? 0)) {
|
||||
Playlist? p;
|
||||
try {
|
||||
p = await deezerAPI.fullPlaylist(deezerAPI.favoritesPlaylistId);
|
||||
} catch (e) {}
|
||||
if (p != null) {
|
||||
setState(() {
|
||||
tracks = p.tracks;
|
||||
tracks = p!.tracks;
|
||||
trackCount = p.trackCount;
|
||||
_sort = _sort;
|
||||
});
|
||||
|
@ -370,7 +370,7 @@ class _LibraryTracksState extends State<LibraryTracks> {
|
|||
}
|
||||
|
||||
Future _loadOffline() async {
|
||||
Playlist p =
|
||||
Playlist? p =
|
||||
await downloadManager.getPlaylist(deezerAPI.favoritesPlaylistId);
|
||||
if (p != null)
|
||||
setState(() {
|
||||
|
@ -381,13 +381,13 @@ class _LibraryTracksState extends State<LibraryTracks> {
|
|||
Future _loadAllOffline() async {
|
||||
List tracks = await downloadManager.allOfflineTracks();
|
||||
setState(() {
|
||||
allTracks = tracks;
|
||||
allTracks = tracks as List<Track?>;
|
||||
});
|
||||
}
|
||||
|
||||
//Update tracks with favorite true
|
||||
void _makeFavorite() {
|
||||
for (int i = 0; i < tracks.length; i++) tracks[i].favorite = true;
|
||||
for (int i = 0; i < tracks!.length; i++) tracks![i]!.favorite = true;
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -403,10 +403,10 @@ class _LibraryTracksState extends State<LibraryTracks> {
|
|||
_loadAllOffline();
|
||||
|
||||
//Load sorting
|
||||
int index = Sorting.index(SortSourceTypes.TRACKS);
|
||||
int? index = Sorting.index(SortSourceTypes.TRACKS);
|
||||
if (index != null) setState(() => _sort = cache.sorts[index]);
|
||||
|
||||
if (_sort.type != SortType.DEFAULT || _sort.reverse) _loadFull();
|
||||
if (_sort!.type != SortType.DEFAULT || _sort!.reverse!) _loadFull();
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
@ -419,10 +419,10 @@ class _LibraryTracksState extends State<LibraryTracks> {
|
|||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
_sort.reverse
|
||||
_sort!.reverse!
|
||||
? FontAwesome5.sort_alpha_up
|
||||
: FontAwesome5.sort_alpha_down,
|
||||
semanticLabel: _sort.reverse
|
||||
semanticLabel: _sort!.reverse!
|
||||
? "Sort descending".i18n
|
||||
: "Sort ascending".i18n,
|
||||
),
|
||||
|
@ -438,11 +438,11 @@ class _LibraryTracksState extends State<LibraryTracks> {
|
|||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
onSelected: (SortType s) async {
|
||||
//Preload for sorting
|
||||
if (tracks.length < (trackCount ?? 0)) await _loadFull();
|
||||
if (tracks!.length < (trackCount ?? 0)) await _loadFull();
|
||||
|
||||
setState(() => _sort.type = s);
|
||||
setState(() => _sort!.type = s);
|
||||
//Save sorting in cache
|
||||
int index = Sorting.index(SortSourceTypes.TRACKS);
|
||||
int? index = Sorting.index(SortSourceTypes.TRACKS);
|
||||
if (index != null) {
|
||||
cache.sorts[index] = _sort;
|
||||
} else {
|
||||
|
@ -504,18 +504,18 @@ class _LibraryTracksState extends State<LibraryTracks> {
|
|||
)),
|
||||
FreezerDivider(),
|
||||
//Loved tracks
|
||||
...List.generate(tracks.length, (i) {
|
||||
Track t = (tracks.length == (trackCount ?? 0))
|
||||
...List.generate(tracks!.length, (i) {
|
||||
Track? t = (tracks!.length == (trackCount ?? 0))
|
||||
? _sorted[i]
|
||||
: tracks[i];
|
||||
: tracks![i];
|
||||
return TrackTile(
|
||||
t,
|
||||
onTap: () {
|
||||
playerHelper.playFromTrackList(
|
||||
(tracks.length == (trackCount ?? 0))
|
||||
(tracks!.length == (trackCount ?? 0))
|
||||
? _sorted
|
||||
: tracks,
|
||||
t.id,
|
||||
: tracks!,
|
||||
t!.id,
|
||||
QueueSource(
|
||||
id: deezerAPI.favoritesPlaylistId,
|
||||
text: 'Favorites'.i18n,
|
||||
|
@ -523,9 +523,9 @@ class _LibraryTracksState extends State<LibraryTracks> {
|
|||
},
|
||||
onHold: () {
|
||||
MenuSheet m = MenuSheet(context);
|
||||
m.defaultTrackMenu(t, onRemove: () {
|
||||
m.defaultTrackMenu(t!, onRemove: () {
|
||||
setState(() {
|
||||
tracks.removeWhere((track) => t.id == track.id);
|
||||
tracks!.removeWhere((track) => t.id == track!.id);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
@ -551,13 +551,13 @@ class _LibraryTracksState extends State<LibraryTracks> {
|
|||
height: 8,
|
||||
),
|
||||
...List.generate(allTracks.length, (i) {
|
||||
Track t = allTracks[i];
|
||||
Track? t = allTracks[i];
|
||||
return TrackTile(
|
||||
t,
|
||||
onTap: () {
|
||||
playerHelper.playFromTrackList(
|
||||
allTracks,
|
||||
t.id,
|
||||
t!.id,
|
||||
QueueSource(
|
||||
id: 'allTracks',
|
||||
text: 'All offline tracks'.i18n,
|
||||
|
@ -565,7 +565,7 @@ class _LibraryTracksState extends State<LibraryTracks> {
|
|||
},
|
||||
onHold: () {
|
||||
MenuSheet m = MenuSheet(context);
|
||||
m.defaultTrackMenu(t);
|
||||
m.defaultTrackMenu(t!);
|
||||
},
|
||||
);
|
||||
})
|
||||
|
@ -580,34 +580,34 @@ class LibraryAlbums extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _LibraryAlbumsState extends State<LibraryAlbums> {
|
||||
List<Album> _albums;
|
||||
Sorting _sort = Sorting(sourceType: SortSourceTypes.ALBUMS);
|
||||
List<Album>? _albums;
|
||||
Sorting? _sort = Sorting(sourceType: SortSourceTypes.ALBUMS);
|
||||
ScrollController _scrollController = ScrollController();
|
||||
|
||||
List<Album> get _sorted {
|
||||
List<Album> albums = List.from(_albums);
|
||||
albums.sort((a, b) => a.favoriteDate.compareTo(b.favoriteDate));
|
||||
switch (_sort.type) {
|
||||
List<Album> albums = List.from(_albums!);
|
||||
albums.sort((a, b) => a.favoriteDate!.compareTo(b.favoriteDate!));
|
||||
switch (_sort!.type) {
|
||||
case SortType.DEFAULT:
|
||||
break;
|
||||
case SortType.ALPHABETIC:
|
||||
albums.sort(
|
||||
(a, b) => a.title.toLowerCase().compareTo(b.title.toLowerCase()));
|
||||
(a, b) => a.title!.toLowerCase().compareTo(b.title!.toLowerCase()));
|
||||
break;
|
||||
case SortType.ARTIST:
|
||||
albums.sort((a, b) => a.artists[0].name
|
||||
albums.sort((a, b) => a.artists![0].name!
|
||||
.toLowerCase()
|
||||
.compareTo(b.artists[0].name.toLowerCase()));
|
||||
.compareTo(b.artists![0].name!.toLowerCase()));
|
||||
break;
|
||||
case SortType.RELEASE_DATE:
|
||||
albums.sort((a, b) => DateTime.parse(a.releaseDate)
|
||||
.compareTo(DateTime.parse(b.releaseDate)));
|
||||
albums.sort((a, b) => DateTime.parse(a.releaseDate!)
|
||||
.compareTo(DateTime.parse(b.releaseDate!)));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
//Reverse
|
||||
if (_sort.reverse) return albums.reversed.toList();
|
||||
if (_sort!.reverse!) return albums.reversed.toList();
|
||||
return albums;
|
||||
}
|
||||
|
||||
|
@ -623,16 +623,16 @@ class _LibraryAlbumsState extends State<LibraryAlbums> {
|
|||
void initState() {
|
||||
_load();
|
||||
//Load sorting
|
||||
int index = Sorting.index(SortSourceTypes.ALBUMS);
|
||||
int? index = Sorting.index(SortSourceTypes.ALBUMS);
|
||||
if (index != null) _sort = cache.sorts[index];
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
Future _reverse() async {
|
||||
setState(() => _sort.reverse = !_sort.reverse);
|
||||
setState(() => _sort!.reverse = !_sort!.reverse!);
|
||||
//Save sorting in cache
|
||||
int index = Sorting.index(SortSourceTypes.ALBUMS);
|
||||
int? index = Sorting.index(SortSourceTypes.ALBUMS);
|
||||
if (index != null) {
|
||||
cache.sorts[index] = _sort;
|
||||
} else {
|
||||
|
@ -649,10 +649,10 @@ class _LibraryAlbumsState extends State<LibraryAlbums> {
|
|||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
_sort.reverse
|
||||
_sort!.reverse!
|
||||
? FontAwesome5.sort_alpha_up
|
||||
: FontAwesome5.sort_alpha_down,
|
||||
semanticLabel: _sort.reverse
|
||||
semanticLabel: _sort!.reverse!
|
||||
? "Sort descending".i18n
|
||||
: "Sort ascending".i18n,
|
||||
),
|
||||
|
@ -662,9 +662,9 @@ class _LibraryAlbumsState extends State<LibraryAlbums> {
|
|||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
child: Icon(Icons.sort, size: 32.0),
|
||||
onSelected: (SortType s) async {
|
||||
setState(() => _sort.type = s);
|
||||
setState(() => _sort!.type = s);
|
||||
//Save to cache
|
||||
int index = Sorting.index(SortSourceTypes.ALBUMS);
|
||||
int? index = Sorting.index(SortSourceTypes.ALBUMS);
|
||||
if (index == null) {
|
||||
cache.sorts.add(_sort);
|
||||
} else {
|
||||
|
@ -709,7 +709,7 @@ class _LibraryAlbumsState extends State<LibraryAlbums> {
|
|||
children: <Widget>[CircularProgressIndicator()],
|
||||
),
|
||||
if (_albums != null)
|
||||
...List.generate(_albums.length, (int i) {
|
||||
...List.generate(_albums!.length, (int i) {
|
||||
Album a = _sorted[i];
|
||||
return AlbumTile(
|
||||
a,
|
||||
|
@ -720,7 +720,7 @@ class _LibraryAlbumsState extends State<LibraryAlbums> {
|
|||
onHold: () async {
|
||||
MenuSheet m = MenuSheet(context);
|
||||
m.defaultAlbumMenu(a, onRemove: () {
|
||||
setState(() => _albums.remove(a));
|
||||
setState(() => _albums!.remove(a));
|
||||
});
|
||||
},
|
||||
);
|
||||
|
@ -730,13 +730,13 @@ class _LibraryAlbumsState extends State<LibraryAlbums> {
|
|||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError ||
|
||||
!snapshot.hasData ||
|
||||
snapshot.data.length == 0)
|
||||
(snapshot.data! as List).length == 0)
|
||||
return Container(
|
||||
height: 0,
|
||||
width: 0,
|
||||
);
|
||||
|
||||
List<Album> albums = snapshot.data;
|
||||
List<Album> albums = snapshot.data as List<Album>;
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
FreezerDivider(),
|
||||
|
@ -759,7 +759,7 @@ class _LibraryAlbumsState extends State<LibraryAlbums> {
|
|||
m.defaultAlbumMenu(a, onRemove: () {
|
||||
setState(() {
|
||||
albums.remove(a);
|
||||
_albums.remove(a);
|
||||
_albums!.remove(a);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
@ -781,30 +781,30 @@ class LibraryArtists extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _LibraryArtistsState extends State<LibraryArtists> {
|
||||
List<Artist> _artists;
|
||||
Sorting _sort = Sorting(sourceType: SortSourceTypes.ARTISTS);
|
||||
late List<Artist> _artists;
|
||||
Sorting? _sort = Sorting(sourceType: SortSourceTypes.ARTISTS);
|
||||
bool _loading = true;
|
||||
bool _error = false;
|
||||
ScrollController _scrollController = ScrollController();
|
||||
|
||||
List<Artist> get _sorted {
|
||||
List<Artist> artists = List.from(_artists);
|
||||
artists.sort((a, b) => a.favoriteDate.compareTo(b.favoriteDate));
|
||||
switch (_sort.type) {
|
||||
artists.sort((a, b) => a.favoriteDate!.compareTo(b.favoriteDate!));
|
||||
switch (_sort!.type) {
|
||||
case SortType.DEFAULT:
|
||||
break;
|
||||
case SortType.POPULARITY:
|
||||
artists.sort((a, b) => b.fans - a.fans);
|
||||
artists.sort((a, b) => b.fans! - a.fans!);
|
||||
break;
|
||||
case SortType.ALPHABETIC:
|
||||
artists.sort(
|
||||
(a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
|
||||
(a, b) => a.name!.toLowerCase().compareTo(b.name!.toLowerCase()));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
//Reverse
|
||||
if (_sort.reverse) return artists.reversed.toList();
|
||||
if (_sort!.reverse!) return artists.reversed.toList();
|
||||
return artists;
|
||||
}
|
||||
|
||||
|
@ -812,7 +812,7 @@ class _LibraryArtistsState extends State<LibraryArtists> {
|
|||
Future _load() async {
|
||||
setState(() => _loading = true);
|
||||
//Fetch
|
||||
List<Artist> data;
|
||||
List<Artist>? data;
|
||||
try {
|
||||
data = await deezerAPI.getArtists();
|
||||
} catch (e) {}
|
||||
|
@ -828,9 +828,9 @@ class _LibraryArtistsState extends State<LibraryArtists> {
|
|||
}
|
||||
|
||||
Future _reverse() async {
|
||||
setState(() => _sort.reverse = !_sort.reverse);
|
||||
setState(() => _sort!.reverse = !_sort!.reverse!);
|
||||
//Save sorting in cache
|
||||
int index = Sorting.index(SortSourceTypes.ARTISTS);
|
||||
int? index = Sorting.index(SortSourceTypes.ARTISTS);
|
||||
if (index != null) {
|
||||
cache.sorts[index] = _sort;
|
||||
} else {
|
||||
|
@ -842,7 +842,7 @@ class _LibraryArtistsState extends State<LibraryArtists> {
|
|||
@override
|
||||
void initState() {
|
||||
//Restore sort
|
||||
int index = Sorting.index(SortSourceTypes.ARTISTS);
|
||||
int? index = Sorting.index(SortSourceTypes.ARTISTS);
|
||||
if (index != null) _sort = cache.sorts[index];
|
||||
|
||||
_load();
|
||||
|
@ -857,10 +857,10 @@ class _LibraryArtistsState extends State<LibraryArtists> {
|
|||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
_sort.reverse
|
||||
_sort!.reverse!
|
||||
? FontAwesome5.sort_alpha_up
|
||||
: FontAwesome5.sort_alpha_down,
|
||||
semanticLabel: _sort.reverse
|
||||
semanticLabel: _sort!.reverse!
|
||||
? "Sort descending".i18n
|
||||
: "Sort ascending".i18n,
|
||||
),
|
||||
|
@ -870,9 +870,9 @@ class _LibraryArtistsState extends State<LibraryArtists> {
|
|||
child: Icon(Icons.sort, size: 32.0),
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
onSelected: (SortType s) async {
|
||||
setState(() => _sort.type = s);
|
||||
setState(() => _sort!.type = s);
|
||||
//Save
|
||||
int index = Sorting.index(SortSourceTypes.ARTISTS);
|
||||
int? index = Sorting.index(SortSourceTypes.ARTISTS);
|
||||
if (index == null) {
|
||||
cache.sorts.add(_sort);
|
||||
} else {
|
||||
|
@ -944,49 +944,49 @@ class LibraryPlaylists extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _LibraryPlaylistsState extends State<LibraryPlaylists> {
|
||||
List<Playlist> _playlists;
|
||||
Sorting _sort = Sorting(sourceType: SortSourceTypes.PLAYLISTS);
|
||||
List<Playlist>? _playlists;
|
||||
Sorting? _sort = Sorting(sourceType: SortSourceTypes.PLAYLISTS);
|
||||
ScrollController _scrollController = ScrollController();
|
||||
String _filter = '';
|
||||
|
||||
List<Playlist> get _sorted {
|
||||
List<Playlist> playlists = List.from(_playlists
|
||||
.where((p) => p.title.toLowerCase().contains(_filter.toLowerCase())));
|
||||
switch (_sort.type) {
|
||||
List<Playlist> playlists = List.from(_playlists!
|
||||
.where((p) => p.title!.toLowerCase().contains(_filter.toLowerCase())));
|
||||
switch (_sort!.type) {
|
||||
case SortType.DEFAULT:
|
||||
break;
|
||||
case SortType.USER:
|
||||
playlists.sort((a, b) => (a.user.name ?? deezerAPI.userName)
|
||||
playlists.sort((a, b) => (a.user!.name ?? deezerAPI.userName)!
|
||||
.toLowerCase()
|
||||
.compareTo((b.user.name ?? deezerAPI.userName).toLowerCase()));
|
||||
.compareTo((b.user!.name ?? deezerAPI.userName)!.toLowerCase()));
|
||||
break;
|
||||
case SortType.TRACK_COUNT:
|
||||
playlists.sort((a, b) => b.trackCount - a.trackCount);
|
||||
playlists.sort((a, b) => b.trackCount! - a.trackCount!);
|
||||
break;
|
||||
case SortType.ALPHABETIC:
|
||||
playlists.sort(
|
||||
(a, b) => a.title.toLowerCase().compareTo(b.title.toLowerCase()));
|
||||
(a, b) => a.title!.toLowerCase().compareTo(b.title!.toLowerCase()));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (_sort.reverse) return playlists.reversed.toList();
|
||||
if (_sort!.reverse!) return playlists.reversed.toList();
|
||||
return playlists;
|
||||
}
|
||||
|
||||
Future _load() async {
|
||||
if (!settings.offlineMode) {
|
||||
try {
|
||||
List<Playlist> playlists = await deezerAPI.getPlaylists();
|
||||
List<Playlist>? playlists = await deezerAPI.getPlaylists();
|
||||
setState(() => _playlists = playlists);
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
Future _reverse() async {
|
||||
setState(() => _sort.reverse = !_sort.reverse);
|
||||
setState(() => _sort!.reverse = !_sort!.reverse!);
|
||||
//Save sorting in cache
|
||||
int index = Sorting.index(SortSourceTypes.PLAYLISTS);
|
||||
int? index = Sorting.index(SortSourceTypes.PLAYLISTS);
|
||||
if (index != null) {
|
||||
cache.sorts[index] = _sort;
|
||||
} else {
|
||||
|
@ -998,7 +998,7 @@ class _LibraryPlaylistsState extends State<LibraryPlaylists> {
|
|||
@override
|
||||
void initState() {
|
||||
//Restore sort
|
||||
int index = Sorting.index(SortSourceTypes.PLAYLISTS);
|
||||
int? index = Sorting.index(SortSourceTypes.PLAYLISTS);
|
||||
if (index != null) _sort = cache.sorts[index];
|
||||
|
||||
_load();
|
||||
|
@ -1022,10 +1022,10 @@ class _LibraryPlaylistsState extends State<LibraryPlaylists> {
|
|||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
_sort.reverse
|
||||
_sort!.reverse!
|
||||
? FontAwesome5.sort_alpha_up
|
||||
: FontAwesome5.sort_alpha_down,
|
||||
semanticLabel: _sort.reverse
|
||||
semanticLabel: _sort!.reverse!
|
||||
? "Sort descending".i18n
|
||||
: "Sort ascending".i18n,
|
||||
),
|
||||
|
@ -1035,9 +1035,9 @@ class _LibraryPlaylistsState extends State<LibraryPlaylists> {
|
|||
child: Icon(Icons.sort, size: 32.0),
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
onSelected: (SortType s) async {
|
||||
setState(() => _sort.type = s);
|
||||
setState(() => _sort!.type = s);
|
||||
//Save to cache
|
||||
int index = Sorting.index(SortSourceTypes.PLAYLISTS);
|
||||
int? index = Sorting.index(SortSourceTypes.PLAYLISTS);
|
||||
if (index == null)
|
||||
cache.sorts.add(_sort);
|
||||
else
|
||||
|
@ -1131,7 +1131,7 @@ class _LibraryPlaylistsState extends State<LibraryPlaylists> {
|
|||
|
||||
if (_playlists != null)
|
||||
...List.generate(_sorted.length, (int i) {
|
||||
Playlist p = (_sorted ?? [])[i];
|
||||
Playlist p = _sorted[i];
|
||||
return PlaylistTile(
|
||||
p,
|
||||
onTap: () => Navigator.of(context).push(MaterialPageRoute(
|
||||
|
@ -1139,7 +1139,7 @@ class _LibraryPlaylistsState extends State<LibraryPlaylists> {
|
|||
onHold: () {
|
||||
MenuSheet m = MenuSheet(context);
|
||||
m.defaultPlaylistMenu(p, onRemove: () {
|
||||
setState(() => _playlists.remove(p));
|
||||
setState(() => _playlists!.remove(p));
|
||||
}, onUpdate: () {
|
||||
_load();
|
||||
});
|
||||
|
@ -1155,13 +1155,13 @@ class _LibraryPlaylistsState extends State<LibraryPlaylists> {
|
|||
height: 0,
|
||||
width: 0,
|
||||
);
|
||||
if (snapshot.data.length == 0)
|
||||
if ((snapshot.data! as List).length == 0)
|
||||
return Container(
|
||||
height: 0,
|
||||
width: 0,
|
||||
);
|
||||
|
||||
List<Playlist> playlists = snapshot.data;
|
||||
List<Playlist> playlists = snapshot.data! as List<Playlist>;
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
FreezerDivider(),
|
||||
|
@ -1183,7 +1183,7 @@ class _LibraryPlaylistsState extends State<LibraryPlaylists> {
|
|||
m.defaultPlaylistMenu(p, onRemove: () {
|
||||
setState(() {
|
||||
playlists.remove(p);
|
||||
_playlists.remove(p);
|
||||
_playlists!.remove(p);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
@ -1229,7 +1229,7 @@ class _HistoryScreenState extends State<HistoryScreen> {
|
|||
backgroundColor: Theme.of(context).primaryColor,
|
||||
child: ListView.builder(
|
||||
controller: _scrollController,
|
||||
itemCount: (cache.history ?? []).length,
|
||||
itemCount: cache.history.length,
|
||||
itemBuilder: (BuildContext context, int i) {
|
||||
Track t = cache.history[cache.history.length - i - 1];
|
||||
return TrackTile(
|
||||
|
|
|
@ -11,16 +11,16 @@ import '../api/definitions.dart';
|
|||
import 'home_screen.dart';
|
||||
|
||||
class LoginWidget extends StatefulWidget {
|
||||
final Function callback;
|
||||
LoginWidget({this.callback, Key key}) : super(key: key);
|
||||
final Function? callback;
|
||||
LoginWidget({this.callback, Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_LoginWidgetState createState() => _LoginWidgetState();
|
||||
}
|
||||
|
||||
class _LoginWidgetState extends State<LoginWidget> {
|
||||
String _arl;
|
||||
String _error;
|
||||
late String _arl;
|
||||
String? _error;
|
||||
|
||||
//Initialize deezer etc
|
||||
Future _init() async {
|
||||
|
@ -40,14 +40,14 @@ class _LoginWidgetState extends State<LoginWidget> {
|
|||
void _start() async {
|
||||
if (settings.arl != null) {
|
||||
_init().then((_) {
|
||||
if (widget.callback != null) widget.callback();
|
||||
if (widget.callback != null) widget.callback!();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//Check if deezer available in current country
|
||||
void _checkAvailability() async {
|
||||
bool available = await DeezerAPI.chceckAvailability();
|
||||
bool? available = await DeezerAPI.chceckAvailability();
|
||||
if (!(available ?? true)) {
|
||||
showDialog(
|
||||
context: context,
|
||||
|
@ -116,9 +116,9 @@ class _LoginWidgetState extends State<LoginWidget> {
|
|||
onError: (e) => setState(() => _error = e.toString()));
|
||||
if (resp == false) {
|
||||
//false, not null
|
||||
if (settings.arl.length != 192) {
|
||||
if (settings.arl!.length != 192) {
|
||||
if (_error == null) _error = '';
|
||||
_error += 'Invalid ARL length!';
|
||||
_error = 'Invalid ARL length!';
|
||||
}
|
||||
setState(() => settings.arl = null);
|
||||
errorDialog();
|
||||
|
@ -136,7 +136,7 @@ class _LoginWidgetState extends State<LoginWidget> {
|
|||
}
|
||||
|
||||
// ARL auth: called on "Save" click, Enter and DPAD_Center press
|
||||
void goARL(FocusNode node, TextEditingController _controller) {
|
||||
void goARL(FocusNode? node, TextEditingController _controller) {
|
||||
if (node != null) {
|
||||
node.unfocus();
|
||||
}
|
||||
|
@ -291,7 +291,7 @@ class _LoginWidgetState extends State<LoginWidget> {
|
|||
),
|
||||
),
|
||||
));
|
||||
return null;
|
||||
return const SizedBox();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -305,9 +305,10 @@ class LoginBrowser extends StatelessWidget {
|
|||
child: InAppWebView(
|
||||
initialUrlRequest:
|
||||
URLRequest(url: Uri.parse('https://deezer.com/login')),
|
||||
onLoadStart: (InAppWebViewController controller, Uri uri) async {
|
||||
onLoadStart: (InAppWebViewController controller, Uri? uri) async {
|
||||
//Offers URL
|
||||
if (!uri.path.contains('/login') && !uri.path.contains('/register')) {
|
||||
if (!uri!.path.contains('/login') &&
|
||||
!uri.path.contains('/register')) {
|
||||
controller.evaluateJavascript(
|
||||
source: 'window.location.href = "/open_app"');
|
||||
}
|
||||
|
@ -316,8 +317,8 @@ class LoginBrowser extends StatelessWidget {
|
|||
if (uri.scheme == 'intent' && uri.host == 'deezer.page.link') {
|
||||
try {
|
||||
//Actual url is in `link` query parameter
|
||||
Uri linkUri = Uri.parse(uri.queryParameters['link']);
|
||||
String arl = linkUri.queryParameters['arl'];
|
||||
Uri linkUri = Uri.parse(uri.queryParameters['link']!);
|
||||
String? arl = linkUri.queryParameters['arl'];
|
||||
if (arl != null) {
|
||||
settings.arl = arl;
|
||||
Navigator.of(context).pop();
|
||||
|
@ -335,24 +336,24 @@ class LoginBrowser extends StatelessWidget {
|
|||
|
||||
class EmailLogin extends StatefulWidget {
|
||||
final Function callback;
|
||||
EmailLogin(this.callback, {Key key}) : super(key: key);
|
||||
EmailLogin(this.callback, {Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_EmailLoginState createState() => _EmailLoginState();
|
||||
}
|
||||
|
||||
class _EmailLoginState extends State<EmailLogin> {
|
||||
String _email;
|
||||
String _password;
|
||||
String? _email;
|
||||
String? _password;
|
||||
bool _loading = false;
|
||||
|
||||
Future _login() async {
|
||||
setState(() => _loading = true);
|
||||
//Try logging in
|
||||
String arl;
|
||||
String exception;
|
||||
String? arl;
|
||||
late String exception;
|
||||
try {
|
||||
arl = await DeezerAPI.getArlByEmail(_email, _password);
|
||||
arl = await DeezerAPI.getArlByEmail(_email, _password!);
|
||||
} catch (e, st) {
|
||||
exception = e.toString();
|
||||
print(e);
|
||||
|
|
|
@ -2,6 +2,7 @@ import 'dart:async';
|
|||
|
||||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:freezer/api/deezer.dart';
|
||||
import 'package:freezer/api/definitions.dart';
|
||||
import 'package:freezer/api/player.dart';
|
||||
|
@ -9,60 +10,52 @@ import 'package:freezer/settings.dart';
|
|||
import 'package:freezer/ui/elements.dart';
|
||||
import 'package:freezer/translations.i18n.dart';
|
||||
import 'package:freezer/ui/error.dart';
|
||||
import 'package:freezer/ui/player_bar.dart';
|
||||
|
||||
class LyricsScreen extends StatefulWidget {
|
||||
final Lyrics lyrics;
|
||||
final String trackId;
|
||||
|
||||
LyricsScreen({this.lyrics, this.trackId, Key key}) : super(key: key);
|
||||
LyricsScreen({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_LyricsScreenState createState() => _LyricsScreenState();
|
||||
}
|
||||
|
||||
class _LyricsScreenState extends State<LyricsScreen> {
|
||||
Lyrics lyrics;
|
||||
bool _loading = true;
|
||||
bool _error = false;
|
||||
int _currentIndex = -1;
|
||||
int _prevIndex = -1;
|
||||
Timer _timer;
|
||||
late StreamSubscription _mediaItemSub;
|
||||
late StreamSubscription _playbackStateSub;
|
||||
int? _currentIndex = -1;
|
||||
int? _prevIndex = -1;
|
||||
ScrollController _controller = ScrollController();
|
||||
StreamSubscription _mediaItemSub;
|
||||
final double height = 90;
|
||||
Lyrics? lyrics;
|
||||
bool _loading = true;
|
||||
Object? _error;
|
||||
|
||||
bool _freeScroll = false;
|
||||
bool _animatedScroll = false;
|
||||
|
||||
Future _load() async {
|
||||
//Already available
|
||||
if (this.lyrics != null) return;
|
||||
if (widget.lyrics?.lyrics != null && widget.lyrics.lyrics.length > 0) {
|
||||
setState(() {
|
||||
lyrics = widget.lyrics;
|
||||
_loading = false;
|
||||
_error = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
Future _loadForId(String trackId) async {
|
||||
//Fetch
|
||||
if (_loading == false && lyrics != null)
|
||||
setState(() {
|
||||
_loading = true;
|
||||
lyrics = null;
|
||||
});
|
||||
try {
|
||||
Lyrics l = await deezerAPI.lyrics(widget.trackId);
|
||||
Lyrics l = await deezerAPI.lyrics(trackId);
|
||||
setState(() {
|
||||
_loading = false;
|
||||
lyrics = l;
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_error = true;
|
||||
_error = e;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _scrollToLyric() async {
|
||||
//Lyric height, screen height, appbar height
|
||||
double _scrollTo = (height * _currentIndex) -
|
||||
double _scrollTo = (height * _currentIndex!) -
|
||||
(MediaQuery.of(context).size.height / 2) +
|
||||
(height / 2) +
|
||||
56;
|
||||
|
@ -75,17 +68,14 @@ class _LyricsScreenState extends State<LyricsScreen> {
|
|||
|
||||
@override
|
||||
void initState() {
|
||||
_load();
|
||||
|
||||
//Enable visualizer
|
||||
// if (settings.lyricsVisualizer) playerHelper.startVisualizer();
|
||||
Timer.periodic(Duration(milliseconds: 350), (timer) {
|
||||
_timer = timer;
|
||||
_currentIndex = lyrics?.lyrics?.lastIndexWhere(
|
||||
(l) => l.offset <= AudioService.playbackState.currentPosition);
|
||||
_playbackStateSub = AudioService.position.listen((position) {
|
||||
if (_loading) return;
|
||||
_currentIndex =
|
||||
lyrics?.lyrics?.lastIndexWhere((l) => l.offset! <= position);
|
||||
//Scroll to current lyric
|
||||
if (_currentIndex < 0) return;
|
||||
if (_currentIndex! < 0) return;
|
||||
if (_prevIndex == _currentIndex) return;
|
||||
//Update current lyric index
|
||||
setState(() => null);
|
||||
|
@ -93,10 +83,14 @@ class _LyricsScreenState extends State<LyricsScreen> {
|
|||
if (_freeScroll) return;
|
||||
_scrollToLyric();
|
||||
});
|
||||
if (audioHandler.mediaItem.value != null)
|
||||
_loadForId(audioHandler.mediaItem.value!.id);
|
||||
|
||||
//Track change = exit lyrics
|
||||
_mediaItemSub = AudioService.currentMediaItemStream.listen((event) {
|
||||
if (event.id != widget.trackId) Navigator.of(context).pop();
|
||||
/// Track change = ~exit~ reload lyrics
|
||||
_mediaItemSub = audioHandler.mediaItem.listen((mediaItem) {
|
||||
if (mediaItem == null) return;
|
||||
_controller.jumpTo(0.0);
|
||||
_loadForId(mediaItem.id);
|
||||
});
|
||||
|
||||
super.initState();
|
||||
|
@ -104,8 +98,8 @@ class _LyricsScreenState extends State<LyricsScreen> {
|
|||
|
||||
@override
|
||||
void dispose() {
|
||||
if (_timer != null) _timer.cancel();
|
||||
if (_mediaItemSub != null) _mediaItemSub.cancel();
|
||||
_mediaItemSub.cancel();
|
||||
_playbackStateSub.cancel();
|
||||
//Stop visualizer
|
||||
// if (settings.lyricsVisualizer) playerHelper.stopVisualizer();
|
||||
super.dispose();
|
||||
|
@ -113,130 +107,144 @@ class _LyricsScreenState extends State<LyricsScreen> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: FreezerAppBar('Lyrics'.i18n,
|
||||
height: _freeScroll ? 100 : 56,
|
||||
bottom: _freeScroll
|
||||
? PreferredSize(
|
||||
preferredSize: Size.fromHeight(46),
|
||||
child: Theme(
|
||||
data: settings.themeData.copyWith(
|
||||
textButtonTheme: TextButtonThemeData(
|
||||
style: ButtonStyle(
|
||||
foregroundColor: MaterialStateProperty.all(
|
||||
Colors.white)))),
|
||||
child: Container(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
setState(() => _freeScroll = false);
|
||||
_scrollToLyric();
|
||||
},
|
||||
child: Text(
|
||||
_currentIndex >= 0
|
||||
? lyrics.lyrics[_currentIndex].text
|
||||
: '...',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
style: ButtonStyle(
|
||||
foregroundColor:
|
||||
MaterialStateProperty.all(Colors.white)))
|
||||
],
|
||||
)),
|
||||
))
|
||||
: null),
|
||||
body: Stack(
|
||||
children: [
|
||||
//Lyrics
|
||||
_error
|
||||
?
|
||||
//Shouldn't really happen, empty lyrics have own text
|
||||
ErrorScreen()
|
||||
:
|
||||
// Loading lyrics
|
||||
_loading
|
||||
? Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [CircularProgressIndicator()],
|
||||
),
|
||||
)
|
||||
: NotificationListener(
|
||||
onNotification: (Notification notification) {
|
||||
if (_freeScroll ||
|
||||
notification is! ScrollStartNotification)
|
||||
return false;
|
||||
if (!_animatedScroll)
|
||||
setState(() => _freeScroll = true);
|
||||
return false;
|
||||
},
|
||||
child: ListView.builder(
|
||||
controller: _controller,
|
||||
padding: EdgeInsets.fromLTRB(0, 0, 0,
|
||||
settings.lyricsVisualizer && false ? 100 : 0),
|
||||
itemCount: lyrics.lyrics.length,
|
||||
itemBuilder: (BuildContext context, int i) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
color: _currentIndex == i
|
||||
? Colors.grey.withOpacity(0.25)
|
||||
: Colors.transparent,
|
||||
),
|
||||
height: height,
|
||||
child: InkWell(
|
||||
borderRadius:
|
||||
BorderRadius.circular(8.0),
|
||||
onTap: lyrics.id != null
|
||||
? () => AudioService.seekTo(
|
||||
lyrics.lyrics[i].offset)
|
||||
: null,
|
||||
child: Center(
|
||||
child: Text(
|
||||
lyrics.lyrics[i].text,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 26.0,
|
||||
fontWeight: (_currentIndex == i)
|
||||
? FontWeight.bold
|
||||
: FontWeight.normal),
|
||||
),
|
||||
))));
|
||||
},
|
||||
)),
|
||||
return AnnotatedRegion<SystemUiOverlayStyle>(
|
||||
value: Theme.of(context).brightness == Brightness.dark
|
||||
? SystemUiOverlayStyle.dark
|
||||
: SystemUiOverlayStyle.light,
|
||||
child: Scaffold(
|
||||
appBar: FreezerAppBar('Lyrics'.i18n),
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
Theme(
|
||||
data: settings.themeData!.copyWith(
|
||||
textButtonTheme: TextButtonThemeData(
|
||||
style: ButtonStyle(
|
||||
foregroundColor:
|
||||
MaterialStateProperty.all(Colors.white)))),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (_freeScroll && !_loading)
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
setState(() => _freeScroll = false);
|
||||
_scrollToLyric();
|
||||
},
|
||||
child: Text(
|
||||
_currentIndex! >= 0
|
||||
? (lyrics?.lyrics?[_currentIndex!].text ??
|
||||
'...')
|
||||
: '...',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
style: ButtonStyle(
|
||||
foregroundColor:
|
||||
MaterialStateProperty.all(Colors.white)))
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Stack(children: [
|
||||
//Lyrics
|
||||
_error != null
|
||||
?
|
||||
//Shouldn't really happen, empty lyrics have own text
|
||||
ErrorScreen(message: _error.toString())
|
||||
:
|
||||
// Loading lyrics
|
||||
_loading
|
||||
? Center(child: CircularProgressIndicator())
|
||||
: NotificationListener(
|
||||
onNotification: (Notification notification) {
|
||||
if (_freeScroll ||
|
||||
notification is! ScrollStartNotification)
|
||||
return false;
|
||||
if (!_animatedScroll && !_loading)
|
||||
setState(() => _freeScroll = true);
|
||||
return false;
|
||||
},
|
||||
child: ListView.builder(
|
||||
controller: _controller,
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
settings.lyricsVisualizer! && false
|
||||
? 100
|
||||
: 0),
|
||||
itemCount: lyrics!.lyrics!.length,
|
||||
itemBuilder: (BuildContext context, int i) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 8.0),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius:
|
||||
BorderRadius.circular(8.0),
|
||||
color: _currentIndex == i
|
||||
? Colors.grey
|
||||
.withOpacity(0.25)
|
||||
: Colors.transparent,
|
||||
),
|
||||
height: height,
|
||||
child: InkWell(
|
||||
borderRadius:
|
||||
BorderRadius.circular(8.0),
|
||||
onTap: lyrics!.id != null
|
||||
? () => audioHandler.seek(
|
||||
lyrics!
|
||||
.lyrics![i].offset!)
|
||||
: null,
|
||||
child: Center(
|
||||
child: Text(
|
||||
lyrics!.lyrics![i].text!,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 26.0,
|
||||
fontWeight:
|
||||
(_currentIndex == i)
|
||||
? FontWeight
|
||||
.bold
|
||||
: FontWeight
|
||||
.normal),
|
||||
),
|
||||
))));
|
||||
},
|
||||
)),
|
||||
|
||||
//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,
|
||||
// )),
|
||||
// );
|
||||
// }),
|
||||
// ),
|
||||
],
|
||||
));
|
||||
//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,
|
||||
// )),
|
||||
// );
|
||||
// }),
|
||||
// ),
|
||||
]),
|
||||
),
|
||||
PlayerBar(shouldHandleClicks: false),
|
||||
],
|
||||
),
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
127
lib/ui/menu.dart
127
lib/ui/menu.dart
|
@ -3,7 +3,6 @@ import 'dart:async';
|
|||
import 'package:freezer/main.dart';
|
||||
import 'package:wakelock/wakelock.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:freezer/api/cache.dart';
|
||||
import 'package:freezer/api/deezer.dart';
|
||||
|
@ -20,7 +19,7 @@ import 'package:url_launcher/url_launcher.dart';
|
|||
|
||||
class MenuSheet {
|
||||
BuildContext context;
|
||||
Function navigateCallback;
|
||||
Function? navigateCallback;
|
||||
|
||||
MenuSheet(this.context, {this.navigateCallback});
|
||||
|
||||
|
@ -68,7 +67,7 @@ class MenuSheet {
|
|||
children: <Widget>[
|
||||
Semantics(
|
||||
child: CachedImage(
|
||||
url: track.albumArt.full,
|
||||
url: track.albumArt!.full,
|
||||
height: 128,
|
||||
width: 128,
|
||||
),
|
||||
|
@ -81,7 +80,7 @@ class MenuSheet {
|
|||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
track.title,
|
||||
track.title!,
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
|
@ -99,7 +98,7 @@ class MenuSheet {
|
|||
height: 8.0,
|
||||
),
|
||||
Text(
|
||||
track.album.title,
|
||||
track.album!.title!,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
|
@ -131,7 +130,7 @@ class MenuSheet {
|
|||
|
||||
//Default track options
|
||||
void defaultTrackMenu(Track track,
|
||||
{List<Widget> options = const [], Function onRemove}) {
|
||||
{List<Widget> options = const [], Function? onRemove}) {
|
||||
showWithTrack(track, [
|
||||
addToQueueNext(track),
|
||||
addToQueue(track),
|
||||
|
@ -143,9 +142,9 @@ class MenuSheet {
|
|||
offlineTrack(track),
|
||||
shareTile('track', track.id),
|
||||
playMix(track),
|
||||
showAlbum(track.album),
|
||||
showAlbum(track.album!),
|
||||
...List.generate(
|
||||
track.artists.length, (i) => showArtist(track.artists[i])),
|
||||
track.artists!.length, (i) => showArtist(track.artists![i])),
|
||||
...options
|
||||
]);
|
||||
}
|
||||
|
@ -159,7 +158,7 @@ class MenuSheet {
|
|||
leading: Icon(Icons.playlist_play),
|
||||
onTap: () async {
|
||||
//-1 = next
|
||||
await AudioService.addQueueItemAt(t.toMediaItem(), -1);
|
||||
await audioHandler.insertQueueItem(-1, t.toMediaItem());
|
||||
_close();
|
||||
});
|
||||
|
||||
|
@ -167,7 +166,7 @@ class MenuSheet {
|
|||
title: Text('Add to queue'.i18n),
|
||||
leading: Icon(Icons.playlist_add),
|
||||
onTap: () async {
|
||||
await AudioService.addQueueItem(t.toMediaItem());
|
||||
await audioHandler.addQueueItem(t.toMediaItem());
|
||||
_close();
|
||||
});
|
||||
|
||||
|
@ -187,7 +186,7 @@ class MenuSheet {
|
|||
toastLength: Toast.LENGTH_SHORT);
|
||||
//Add to cache
|
||||
if (cache.libraryTracks == null) cache.libraryTracks = [];
|
||||
cache.libraryTracks.add(t.id);
|
||||
cache.libraryTracks!.add(t.id);
|
||||
|
||||
_close();
|
||||
});
|
||||
|
@ -230,11 +229,11 @@ class MenuSheet {
|
|||
},
|
||||
);
|
||||
|
||||
Widget removeFromPlaylist(Track t, Playlist p) => ListTile(
|
||||
Widget removeFromPlaylist(Track t, Playlist? p) => ListTile(
|
||||
title: Text('Remove from playlist'.i18n),
|
||||
leading: Icon(Icons.delete),
|
||||
onTap: () async {
|
||||
await deezerAPI.removeFromPlaylist(t.id, p.id);
|
||||
await deezerAPI.removeFromPlaylist(t.id, p!.id);
|
||||
Fluttertoast.showToast(
|
||||
msg: 'Track removed from'.i18n + ' ${p.title}',
|
||||
toastLength: Toast.LENGTH_SHORT,
|
||||
|
@ -256,7 +255,7 @@ class MenuSheet {
|
|||
}
|
||||
//Remove from cache
|
||||
if (cache.libraryTracks != null)
|
||||
cache.libraryTracks.removeWhere((i) => i == t.id);
|
||||
cache.libraryTracks!.removeWhere((i) => i == t.id);
|
||||
Fluttertoast.showToast(
|
||||
msg: 'Track removed from library'.i18n,
|
||||
toastLength: Toast.LENGTH_SHORT,
|
||||
|
@ -276,11 +275,11 @@ class MenuSheet {
|
|||
leading: Icon(Icons.recent_actors),
|
||||
onTap: () {
|
||||
_close();
|
||||
navigatorKey.currentState
|
||||
navigatorKey.currentState!
|
||||
.push(MaterialPageRoute(builder: (context) => ArtistDetails(a)));
|
||||
|
||||
if (this.navigateCallback != null) {
|
||||
this.navigateCallback();
|
||||
this.navigateCallback!();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
@ -294,11 +293,11 @@ class MenuSheet {
|
|||
leading: Icon(Icons.album),
|
||||
onTap: () {
|
||||
_close();
|
||||
navigatorKey.currentState
|
||||
navigatorKey.currentState!
|
||||
.push(MaterialPageRoute(builder: (context) => AlbumDetails(a)));
|
||||
|
||||
if (this.navigateCallback != null) {
|
||||
this.navigateCallback();
|
||||
this.navigateCallback!();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
@ -307,7 +306,7 @@ class MenuSheet {
|
|||
title: Text('Play mix'.i18n),
|
||||
leading: Icon(Icons.online_prediction),
|
||||
onTap: () async {
|
||||
playerHelper.playMix(track.id, track.title);
|
||||
playerHelper.playMix(track.id, track.title!);
|
||||
_close();
|
||||
},
|
||||
);
|
||||
|
@ -315,7 +314,7 @@ class MenuSheet {
|
|||
Widget offlineTrack(Track track) => FutureBuilder(
|
||||
future: downloadManager.checkOffline(track: track),
|
||||
builder: (context, snapshot) {
|
||||
bool isOffline = snapshot.data ?? (track.offline ?? false);
|
||||
bool isOffline = (snapshot.data as bool?) ?? (track.offline ?? false);
|
||||
return ListTile(
|
||||
title: Text(isOffline ? 'Remove offline'.i18n : 'Offline'.i18n),
|
||||
leading: Icon(Icons.offline_pin),
|
||||
|
@ -342,9 +341,9 @@ class MenuSheet {
|
|||
|
||||
//Default album options
|
||||
void defaultAlbumMenu(Album album,
|
||||
{List<Widget> options = const [], Function onRemove}) {
|
||||
{List<Widget> options = const [], Function? onRemove}) {
|
||||
show([
|
||||
album.library
|
||||
album.library!
|
||||
? removeAlbum(album, onRemove: onRemove)
|
||||
: libraryAlbum(album),
|
||||
downloadAlbum(album),
|
||||
|
@ -391,7 +390,7 @@ class MenuSheet {
|
|||
);
|
||||
|
||||
//Remove album from favorites
|
||||
Widget removeAlbum(Album a, {Function onRemove}) => ListTile(
|
||||
Widget removeAlbum(Album a, {Function? onRemove}) => ListTile(
|
||||
title: Text('Remove album'.i18n),
|
||||
leading: Icon(Icons.delete),
|
||||
onTap: () async {
|
||||
|
@ -412,9 +411,9 @@ class MenuSheet {
|
|||
//===================
|
||||
|
||||
void defaultArtistMenu(Artist artist,
|
||||
{List<Widget> options = const [], Function onRemove}) {
|
||||
{List<Widget> options = const [], Function? onRemove}) {
|
||||
show([
|
||||
artist.library
|
||||
artist.library!
|
||||
? removeArtist(artist, onRemove: onRemove)
|
||||
: favoriteArtist(artist),
|
||||
shareTile('artist', artist.id),
|
||||
|
@ -426,7 +425,7 @@ class MenuSheet {
|
|||
// ARTIST OPTIONS
|
||||
//===================
|
||||
|
||||
Widget removeArtist(Artist a, {Function onRemove}) => ListTile(
|
||||
Widget removeArtist(Artist a, {Function? onRemove}) => ListTile(
|
||||
title: Text('Remove from favorites'.i18n),
|
||||
leading: Icon(Icons.delete),
|
||||
onTap: () async {
|
||||
|
@ -458,15 +457,17 @@ class MenuSheet {
|
|||
//===================
|
||||
|
||||
void defaultPlaylistMenu(Playlist playlist,
|
||||
{List<Widget> options = const [], Function onRemove, Function onUpdate}) {
|
||||
{List<Widget> options = const [],
|
||||
Function? onRemove,
|
||||
Function? onUpdate}) {
|
||||
show([
|
||||
playlist.library
|
||||
playlist.library!
|
||||
? removePlaylistLibrary(playlist, onRemove: onRemove)
|
||||
: addPlaylistLibrary(playlist),
|
||||
addPlaylistOffline(playlist),
|
||||
downloadPlaylist(playlist),
|
||||
shareTile('playlist', playlist.id),
|
||||
if (playlist.user.id == deezerAPI.userId)
|
||||
if (playlist.user!.id == deezerAPI.userId)
|
||||
editPlaylist(playlist, onUpdate: onUpdate),
|
||||
...options
|
||||
]);
|
||||
|
@ -476,16 +477,16 @@ class MenuSheet {
|
|||
// PLAYLIST OPTIONS
|
||||
//===================
|
||||
|
||||
Widget removePlaylistLibrary(Playlist p, {Function onRemove}) => ListTile(
|
||||
Widget removePlaylistLibrary(Playlist p, {Function? onRemove}) => ListTile(
|
||||
title: Text('Remove from library'.i18n),
|
||||
leading: Icon(Icons.delete),
|
||||
onTap: () async {
|
||||
if (p.user.id.trim() == deezerAPI.userId) {
|
||||
if (p.user!.id!.trim() == deezerAPI.userId) {
|
||||
//Delete playlist if own
|
||||
await deezerAPI.deletePlaylist(p.id);
|
||||
} else {
|
||||
//Just remove from library
|
||||
await deezerAPI.removePlaylist(p.id);
|
||||
await deezerAPI.removePlaylist(p.id!);
|
||||
}
|
||||
downloadManager.removeOfflinePlaylist(p.id);
|
||||
if (onRemove != null) onRemove();
|
||||
|
@ -497,7 +498,7 @@ class MenuSheet {
|
|||
title: Text('Add playlist to library'.i18n),
|
||||
leading: Icon(Icons.favorite),
|
||||
onTap: () async {
|
||||
await deezerAPI.addPlaylist(p.id);
|
||||
await deezerAPI.addPlaylist(p.id!);
|
||||
Fluttertoast.showToast(
|
||||
msg: 'Added playlist to library'.i18n,
|
||||
gravity: ToastGravity.BOTTOM);
|
||||
|
@ -510,7 +511,7 @@ class MenuSheet {
|
|||
leading: Icon(Icons.offline_pin),
|
||||
onTap: () async {
|
||||
//Add to library
|
||||
await deezerAPI.addPlaylist(p.id);
|
||||
await deezerAPI.addPlaylist(p.id!);
|
||||
downloadManager.addOfflinePlaylist(p, private: true);
|
||||
_close();
|
||||
showDownloadStartedToast();
|
||||
|
@ -528,7 +529,7 @@ class MenuSheet {
|
|||
},
|
||||
);
|
||||
|
||||
Widget editPlaylist(Playlist p, {Function onUpdate}) => ListTile(
|
||||
Widget editPlaylist(Playlist p, {Function? onUpdate}) => ListTile(
|
||||
title: Text('Edit playlist'.i18n),
|
||||
leading: Icon(Icons.edit),
|
||||
onTap: () async {
|
||||
|
@ -554,7 +555,7 @@ class MenuSheet {
|
|||
]);
|
||||
}
|
||||
|
||||
Widget shareShow(String id) => ListTile(
|
||||
Widget shareShow(String? id) => ListTile(
|
||||
title: Text('Share show'.i18n),
|
||||
leading: Icon(Icons.share),
|
||||
onTap: () async {
|
||||
|
@ -567,7 +568,7 @@ class MenuSheet {
|
|||
title: Text('Download externally'.i18n),
|
||||
leading: Icon(Icons.file_download),
|
||||
onTap: () async {
|
||||
launch(e.url);
|
||||
launch(e.url!);
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -591,7 +592,7 @@ class MenuSheet {
|
|||
});
|
||||
}
|
||||
|
||||
Widget shareTile(String type, String id) => ListTile(
|
||||
Widget shareTile(String type, String? id) => ListTile(
|
||||
title: Text('Share'.i18n),
|
||||
leading: Icon(Icons.share),
|
||||
onTap: () async {
|
||||
|
@ -616,7 +617,6 @@ class MenuSheet {
|
|||
leading: Icon(Icons.screen_lock_portrait),
|
||||
onTap: () async {
|
||||
_close();
|
||||
if (cache.wakelock == null) cache.wakelock = false;
|
||||
//Enable
|
||||
if (!cache.wakelock) {
|
||||
Wakelock.enable();
|
||||
|
@ -646,7 +646,7 @@ class _SleepTimerDialogState extends State<SleepTimerDialog> {
|
|||
int minutes = 30;
|
||||
|
||||
String _endTime() {
|
||||
return '${cache.sleepTimerTime.hour.toString().padLeft(2, '0')}:${cache.sleepTimerTime.minute.toString().padLeft(2, '0')}';
|
||||
return '${cache.sleepTimerTime!.hour.toString().padLeft(2, '0')}:${cache.sleepTimerTime!.minute.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -704,7 +704,7 @@ class _SleepTimerDialogState extends State<SleepTimerDialog> {
|
|||
TextButton(
|
||||
child: Text('Cancel current timer'.i18n),
|
||||
onPressed: () {
|
||||
cache.sleepTimer.cancel();
|
||||
cache.sleepTimer!.cancel();
|
||||
cache.sleepTimer = null;
|
||||
cache.sleepTimerTime = null;
|
||||
Navigator.of(context).pop();
|
||||
|
@ -715,13 +715,13 @@ class _SleepTimerDialogState extends State<SleepTimerDialog> {
|
|||
onPressed: () {
|
||||
Duration duration = Duration(hours: hours, minutes: minutes);
|
||||
if (cache.sleepTimer != null) {
|
||||
cache.sleepTimer.cancel();
|
||||
cache.sleepTimer!.cancel();
|
||||
}
|
||||
//Create timer
|
||||
cache.sleepTimer =
|
||||
Stream.fromFuture(Future.delayed(duration)).listen((_) {
|
||||
AudioService.pause();
|
||||
cache.sleepTimer.cancel();
|
||||
audioHandler.pause();
|
||||
cache.sleepTimer!.cancel();
|
||||
cache.sleepTimerTime = null;
|
||||
cache.sleepTimer = null;
|
||||
});
|
||||
|
@ -735,9 +735,9 @@ class _SleepTimerDialogState extends State<SleepTimerDialog> {
|
|||
}
|
||||
|
||||
class SelectPlaylistDialog extends StatefulWidget {
|
||||
final Track track;
|
||||
final Function callback;
|
||||
SelectPlaylistDialog({this.track, this.callback, Key key}) : super(key: key);
|
||||
final Track? track;
|
||||
final Function? callback;
|
||||
SelectPlaylistDialog({this.track, this.callback, Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_SelectPlaylistDialogState createState() => _SelectPlaylistDialogState();
|
||||
|
@ -774,19 +774,19 @@ class _SelectPlaylistDialogState extends State<SelectPlaylistDialog> {
|
|||
),
|
||||
);
|
||||
|
||||
List<Playlist> playlists = snapshot.data;
|
||||
List<Playlist> playlists = snapshot.data! as List<Playlist>;
|
||||
return SingleChildScrollView(
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
...List.generate(
|
||||
playlists.length,
|
||||
(i) => ListTile(
|
||||
title: Text(playlists[i].title),
|
||||
title: Text(playlists[i].title!),
|
||||
leading: CachedImage(
|
||||
url: playlists[i].image.thumb,
|
||||
url: playlists[i].image!.thumb,
|
||||
),
|
||||
onTap: () {
|
||||
if (widget.callback != null) {
|
||||
widget.callback(playlists[i]);
|
||||
widget.callback!(playlists[i]);
|
||||
}
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
|
@ -809,21 +809,22 @@ class _SelectPlaylistDialogState extends State<SelectPlaylistDialog> {
|
|||
}
|
||||
|
||||
class CreatePlaylistDialog extends StatefulWidget {
|
||||
final List<Track> tracks;
|
||||
final List<Track?>? tracks;
|
||||
//If playlist not null, update
|
||||
final Playlist playlist;
|
||||
CreatePlaylistDialog({this.tracks, this.playlist, Key key}) : super(key: key);
|
||||
final Playlist? playlist;
|
||||
CreatePlaylistDialog({this.tracks, this.playlist, Key? key})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
_CreatePlaylistDialogState createState() => _CreatePlaylistDialogState();
|
||||
}
|
||||
|
||||
class _CreatePlaylistDialogState extends State<CreatePlaylistDialog> {
|
||||
int _playlistType = 1;
|
||||
int? _playlistType = 1;
|
||||
String _title = '';
|
||||
String _description = '';
|
||||
TextEditingController _titleController;
|
||||
TextEditingController _descController;
|
||||
TextEditingController? _titleController;
|
||||
TextEditingController? _descController;
|
||||
|
||||
//Create or edit mode
|
||||
bool get edit => widget.playlist != null;
|
||||
|
@ -832,9 +833,9 @@ class _CreatePlaylistDialogState extends State<CreatePlaylistDialog> {
|
|||
void initState() {
|
||||
//Edit playlist mode
|
||||
if (edit) {
|
||||
_titleController = TextEditingController(text: widget.playlist.title);
|
||||
_titleController = TextEditingController(text: widget.playlist!.title);
|
||||
_descController =
|
||||
TextEditingController(text: widget.playlist.description);
|
||||
TextEditingController(text: widget.playlist!.description);
|
||||
}
|
||||
|
||||
super.initState();
|
||||
|
@ -862,7 +863,7 @@ class _CreatePlaylistDialogState extends State<CreatePlaylistDialog> {
|
|||
),
|
||||
DropdownButton<int>(
|
||||
value: _playlistType,
|
||||
onChanged: (int v) {
|
||||
onChanged: (int? v) {
|
||||
setState(() => _playlistType = v);
|
||||
},
|
||||
items: [
|
||||
|
@ -888,15 +889,15 @@ class _CreatePlaylistDialogState extends State<CreatePlaylistDialog> {
|
|||
onPressed: () async {
|
||||
if (edit) {
|
||||
//Update
|
||||
await deezerAPI.updatePlaylist(widget.playlist.id,
|
||||
_titleController.value.text, _descController.value.text,
|
||||
await deezerAPI.updatePlaylist(widget.playlist!.id!,
|
||||
_titleController!.value.text, _descController!.value.text,
|
||||
status: _playlistType);
|
||||
Fluttertoast.showToast(
|
||||
msg: 'Playlist updated!'.i18n, gravity: ToastGravity.BOTTOM);
|
||||
} else {
|
||||
List<String> tracks = [];
|
||||
if (widget.tracks != null) {
|
||||
tracks = widget.tracks.map<String>((t) => t.id).toList();
|
||||
tracks = widget.tracks!.map<String>((t) => t!.id).toList();
|
||||
}
|
||||
await deezerAPI.createPlaylist(_title,
|
||||
status: _playlistType,
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:freezer/settings.dart';
|
||||
import 'package:freezer/translations.i18n.dart';
|
||||
|
||||
|
@ -10,17 +7,25 @@ import '../api/player.dart';
|
|||
import 'cached_image.dart';
|
||||
import 'player_screen.dart';
|
||||
|
||||
class PlayerBar extends StatelessWidget {
|
||||
double get progress {
|
||||
if (AudioService.playbackState == null) return 0.0;
|
||||
if (AudioService.currentMediaItem == null) return 0.0;
|
||||
if (AudioService.currentMediaItem.duration.inSeconds == 0)
|
||||
class PlayerBar extends StatefulWidget {
|
||||
final bool shouldHandleClicks;
|
||||
const PlayerBar({Key? key, this.shouldHandleClicks = true}) : super(key: key);
|
||||
|
||||
@override
|
||||
_PlayerBarState createState() => _PlayerBarState();
|
||||
}
|
||||
|
||||
class _PlayerBarState extends State<PlayerBar> {
|
||||
final double iconSize = 28;
|
||||
|
||||
double parsePosition(Duration position) {
|
||||
if (audioHandler.mediaItem.value == null) return 0.0;
|
||||
if (audioHandler.mediaItem.value!.duration!.inSeconds == 0)
|
||||
return 0.0; //Division by 0
|
||||
return AudioService.playbackState.currentPosition.inSeconds /
|
||||
AudioService.currentMediaItem.duration.inSeconds;
|
||||
return position.inSeconds /
|
||||
audioHandler.mediaItem.value!.duration!.inSeconds;
|
||||
}
|
||||
|
||||
double iconSize = 28;
|
||||
bool _gestureRegistered = false;
|
||||
|
||||
@override
|
||||
|
@ -33,85 +38,85 @@ class PlayerBar extends StatelessWidget {
|
|||
//Right swipe
|
||||
_gestureRegistered = true;
|
||||
if (details.delta.dx > sensitivity) {
|
||||
await AudioService.skipToPrevious();
|
||||
await audioHandler.skipToPrevious();
|
||||
}
|
||||
//Left
|
||||
if (details.delta.dx < -sensitivity) {
|
||||
await AudioService.skipToNext();
|
||||
await audioHandler.skipToNext();
|
||||
}
|
||||
_gestureRegistered = false;
|
||||
return;
|
||||
},
|
||||
child: StreamBuilder(
|
||||
stream: Stream.periodic(Duration(milliseconds: 250)),
|
||||
builder: (BuildContext context, AsyncSnapshot snapshot) {
|
||||
if (AudioService.currentMediaItem == null)
|
||||
return Container(
|
||||
width: 0,
|
||||
height: 0,
|
||||
);
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
// For Android TV: indicate focus by grey
|
||||
color: focusNode.hasFocus
|
||||
? Colors.black26
|
||||
: Theme.of(context).bottomAppBarColor,
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
focusNode: focusNode,
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 8.0),
|
||||
onTap: () {
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (BuildContext context) =>
|
||||
PlayerScreen()));
|
||||
},
|
||||
leading: CachedImage(
|
||||
width: 50,
|
||||
height: 50,
|
||||
url: AudioService.currentMediaItem.extras['thumb'] ??
|
||||
AudioService.currentMediaItem.artUri,
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: <Widget>[
|
||||
StreamBuilder<MediaItem?>(
|
||||
stream: audioHandler.mediaItem,
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) return const SizedBox();
|
||||
final currentMediaItem = snapshot.data!;
|
||||
return DecoratedBox(
|
||||
// For Android TV: indicate focus by grey
|
||||
decoration: BoxDecoration(
|
||||
color: focusNode.hasFocus
|
||||
? Colors.black26
|
||||
: Theme.of(context).bottomAppBarColor),
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
focusNode: focusNode,
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 8.0),
|
||||
onTap: widget.shouldHandleClicks
|
||||
? () {
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (BuildContext context) =>
|
||||
PlayerScreen()));
|
||||
}
|
||||
: null,
|
||||
leading: CachedImage(
|
||||
width: 50,
|
||||
height: 50,
|
||||
url: currentMediaItem.extras!['thumb'] ??
|
||||
audioHandler.mediaItem.value!.artUri as String?,
|
||||
),
|
||||
title: Text(
|
||||
currentMediaItem.displayTitle!,
|
||||
overflow: TextOverflow.clip,
|
||||
maxLines: 1,
|
||||
),
|
||||
subtitle: Text(
|
||||
currentMediaItem.displaySubtitle ?? '',
|
||||
overflow: TextOverflow.clip,
|
||||
maxLines: 1,
|
||||
),
|
||||
trailing: IconTheme(
|
||||
data: IconThemeData(
|
||||
color: settings.isDark
|
||||
? Colors.white
|
||||
: Colors.grey[600]),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
PrevNextButton(
|
||||
iconSize,
|
||||
prev: true,
|
||||
),
|
||||
PlayPauseButton(iconSize),
|
||||
PrevNextButton(iconSize)
|
||||
],
|
||||
),
|
||||
title: Text(
|
||||
AudioService.currentMediaItem.displayTitle,
|
||||
overflow: TextOverflow.clip,
|
||||
maxLines: 1,
|
||||
),
|
||||
subtitle: Text(
|
||||
AudioService.currentMediaItem.displaySubtitle ?? '',
|
||||
overflow: TextOverflow.clip,
|
||||
maxLines: 1,
|
||||
),
|
||||
trailing: IconTheme(
|
||||
data: IconThemeData(
|
||||
color: settings.isDark
|
||||
? Colors.white
|
||||
: Colors.grey[600]),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
PrevNextButton(
|
||||
iconSize,
|
||||
prev: true,
|
||||
hidePrev: true,
|
||||
),
|
||||
PlayPauseButton(iconSize),
|
||||
PrevNextButton(iconSize)
|
||||
],
|
||||
),
|
||||
))),
|
||||
Container(
|
||||
height: 3.0,
|
||||
child: LinearProgressIndicator(
|
||||
backgroundColor:
|
||||
Theme.of(context).primaryColor.withOpacity(0.1),
|
||||
value: progress,
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}),
|
||||
)));
|
||||
}),
|
||||
SizedBox(
|
||||
height: 3.0,
|
||||
child: StreamBuilder<Duration>(
|
||||
stream: AudioService.position,
|
||||
builder: (context, snapshot) {
|
||||
return LinearProgressIndicator(
|
||||
backgroundColor:
|
||||
Theme.of(context).primaryColor.withOpacity(0.1),
|
||||
value: parsePosition(snapshot.data ?? Duration.zero),
|
||||
);
|
||||
}),
|
||||
),
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -124,9 +129,9 @@ class PrevNextButton extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return StreamBuilder(
|
||||
stream: AudioService.queueStream,
|
||||
builder: (context, _snapshot) {
|
||||
return StreamBuilder<List<MediaItem?>>(
|
||||
stream: audioHandler.queue,
|
||||
builder: (context, snapshot) {
|
||||
if (!prev) {
|
||||
return IconButton(
|
||||
icon: Icon(
|
||||
|
@ -135,19 +140,22 @@ class PrevNextButton extends StatelessWidget {
|
|||
),
|
||||
iconSize: size,
|
||||
onPressed:
|
||||
playerHelper.queueIndex == (AudioService.queue ?? []).length - 1
|
||||
playerHelper.queueIndex == (snapshot.data ?? []).length - 1
|
||||
? null
|
||||
: () => AudioService.skipToNext(),
|
||||
: () => audioHandler.skipToNext(),
|
||||
);
|
||||
}
|
||||
if (hidePrev) return const SizedBox(width: 0.0, height: 0.0);
|
||||
final canGoPrev = playerHelper.queueIndex > 0;
|
||||
|
||||
if (!canGoPrev && hidePrev)
|
||||
return const SizedBox(width: 0.0, height: 0.0);
|
||||
return IconButton(
|
||||
icon: Icon(
|
||||
Icons.skip_previous,
|
||||
semanticLabel: "Play previous".i18n,
|
||||
),
|
||||
iconSize: size,
|
||||
onPressed: () => AudioService.skipToPrevious(),
|
||||
onPressed: canGoPrev ? () => audioHandler.skipToPrevious() : null,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@ -156,7 +164,7 @@ class PrevNextButton extends StatelessWidget {
|
|||
|
||||
class PlayPauseButton extends StatefulWidget {
|
||||
final double size;
|
||||
PlayPauseButton(this.size, {Key key}) : super(key: key);
|
||||
PlayPauseButton(this.size, {Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_PlayPauseButtonState createState() => _PlayPauseButtonState();
|
||||
|
@ -164,15 +172,14 @@ class PlayPauseButton extends StatefulWidget {
|
|||
|
||||
class _PlayPauseButtonState extends State<PlayPauseButton>
|
||||
with SingleTickerProviderStateMixin {
|
||||
AnimationController _controller;
|
||||
Animation<double> _animation;
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _animation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_controller =
|
||||
AnimationController(vsync: this, duration: Duration(milliseconds: 200));
|
||||
_animation = Tween<double>(begin: 0, end: 1)
|
||||
.animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut));
|
||||
_animation = CurvedAnimation(parent: _controller, curve: Curves.easeInOut);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
|
@ -185,15 +192,13 @@ class _PlayPauseButtonState extends State<PlayPauseButton>
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return StreamBuilder(
|
||||
stream: AudioService.playbackStateStream,
|
||||
stream: audioHandler.playbackState,
|
||||
builder: (context, snapshot) {
|
||||
//Animated icon by pato05
|
||||
bool _playing = AudioService.playbackState?.playing ?? false;
|
||||
bool _playing = audioHandler.playbackState.value.playing;
|
||||
if (_playing ||
|
||||
AudioService.playbackState?.processingState ==
|
||||
AudioProcessingState.ready ||
|
||||
AudioService.playbackState?.processingState ==
|
||||
AudioProcessingState.none) {
|
||||
audioHandler.playbackState.value.processingState ==
|
||||
AudioProcessingState.ready) {
|
||||
if (_playing)
|
||||
_controller.forward();
|
||||
else
|
||||
|
@ -208,22 +213,21 @@ class _PlayPauseButtonState extends State<PlayPauseButton>
|
|||
),
|
||||
iconSize: widget.size,
|
||||
onPressed: _playing
|
||||
? () => AudioService.pause()
|
||||
: () => AudioService.play());
|
||||
? () => audioHandler.pause()
|
||||
: () => audioHandler.play());
|
||||
}
|
||||
|
||||
switch (AudioService.playbackState.processingState) {
|
||||
switch (audioHandler.playbackState.value.processingState) {
|
||||
//Stopped/Error
|
||||
case AudioProcessingState.error:
|
||||
case AudioProcessingState.none:
|
||||
case AudioProcessingState.stopped:
|
||||
return Container(width: widget.size, height: widget.size);
|
||||
case AudioProcessingState.idle:
|
||||
return SizedBox(width: widget.size, height: widget.size);
|
||||
//Loading, connecting, rewinding...
|
||||
default:
|
||||
return Container(
|
||||
return SizedBox(
|
||||
width: widget.size,
|
||||
height: widget.size,
|
||||
child: CircularProgressIndicator(),
|
||||
child: const CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
},
|
||||
|
|
|
@ -34,7 +34,7 @@ import 'dart:async';
|
|||
bool pageViewLock = false;
|
||||
|
||||
//So can be updated when going back from lyrics
|
||||
Function updateColor;
|
||||
late Function updateColor;
|
||||
|
||||
class PlayerScreen extends StatefulWidget {
|
||||
static const _blurStrength = 50.0;
|
||||
|
@ -44,24 +44,24 @@ class PlayerScreen extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _PlayerScreenState extends State<PlayerScreen> {
|
||||
LinearGradient _bgGradient;
|
||||
StreamSubscription _mediaItemSub;
|
||||
StreamSubscription _playerStateSub;
|
||||
ImageProvider _blurImage;
|
||||
LinearGradient? _bgGradient;
|
||||
late StreamSubscription _mediaItemSub;
|
||||
late StreamSubscription _playerStateSub;
|
||||
ImageProvider? _blurImage;
|
||||
bool _wasConnected = true;
|
||||
|
||||
//Calculate background color
|
||||
Future _updateColor() async {
|
||||
if (!settings.colorGradientBackground && !settings.blurPlayerBackground)
|
||||
if (!settings.colorGradientBackground! && !settings.blurPlayerBackground!)
|
||||
return;
|
||||
final imageProvider = CachedNetworkImageProvider(
|
||||
AudioService.currentMediaItem.extras['thumb'] ??
|
||||
AudioService.currentMediaItem.artUri);
|
||||
audioHandler.mediaItem.value!.extras!['thumb'] ??
|
||||
audioHandler.mediaItem.value!.artUri as String);
|
||||
//BG Image
|
||||
if (settings.blurPlayerBackground)
|
||||
if (settings.blurPlayerBackground!)
|
||||
setState(() => _blurImage = imageProvider);
|
||||
|
||||
if (settings.colorGradientBackground) {
|
||||
if (settings.colorGradientBackground!) {
|
||||
//Run in isolate
|
||||
PaletteGenerator palette =
|
||||
await PaletteGenerator.fromImageProvider(imageProvider);
|
||||
|
@ -70,7 +70,7 @@ class _PlayerScreenState extends State<PlayerScreen> {
|
|||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
palette.dominantColor.color.withOpacity(0.7),
|
||||
palette.dominantColor!.color.withOpacity(0.7),
|
||||
Color.fromARGB(0, 0, 0, 0)
|
||||
],
|
||||
stops: [
|
||||
|
@ -81,22 +81,22 @@ class _PlayerScreenState extends State<PlayerScreen> {
|
|||
}
|
||||
|
||||
void _playbackStateChanged() {
|
||||
if (AudioService.currentMediaItem == null) {
|
||||
playerHelper.startService();
|
||||
setState(() => _wasConnected = false);
|
||||
} else if (!_wasConnected) setState(() => _wasConnected = true);
|
||||
// if (audioHandler.mediaItem.value == null) {
|
||||
// //playerHelper.startService();
|
||||
// setState(() => _wasConnected = false);
|
||||
// } else if (!_wasConnected) setState(() => _wasConnected = true);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
Future.delayed(Duration(milliseconds: 600), _updateColor);
|
||||
_playbackStateChanged();
|
||||
_mediaItemSub = AudioService.currentMediaItemStream.listen((event) {
|
||||
_mediaItemSub = audioHandler.mediaItem.listen((event) {
|
||||
_playbackStateChanged();
|
||||
_updateColor();
|
||||
});
|
||||
_playerStateSub =
|
||||
AudioService.playbackStateStream.listen((_) => _playbackStateChanged());
|
||||
audioHandler.playbackState.listen((_) => _playbackStateChanged());
|
||||
|
||||
updateColor = this._updateColor;
|
||||
super.initState();
|
||||
|
@ -112,7 +112,7 @@ class _PlayerScreenState extends State<PlayerScreen> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final hasBackground =
|
||||
settings.blurPlayerBackground || settings.colorGradientBackground;
|
||||
settings.blurPlayerBackground! || settings.colorGradientBackground!;
|
||||
final color = hasBackground
|
||||
? Colors.transparent
|
||||
: Theme.of(context).scaffoldBackgroundColor;
|
||||
|
@ -140,7 +140,7 @@ class _PlayerScreenState extends State<PlayerScreen> {
|
|||
image: _blurImage == null
|
||||
? null
|
||||
: DecorationImage(
|
||||
image: _blurImage,
|
||||
image: _blurImage!,
|
||||
fit: BoxFit.cover,
|
||||
colorFilter: ColorFilter.mode(
|
||||
Colors.white.withOpacity(0.5),
|
||||
|
@ -212,11 +212,11 @@ class _PlayerScreenHorizontalState extends State<PlayerScreenHorizontal> {
|
|||
children: <Widget>[
|
||||
Container(
|
||||
height: ScreenUtil().setSp(50),
|
||||
child: AudioService
|
||||
.currentMediaItem.displayTitle.length >=
|
||||
child: audioHandler
|
||||
.mediaItem.value!.displayTitle!.length >=
|
||||
22
|
||||
? Marquee(
|
||||
text: AudioService.currentMediaItem.displayTitle,
|
||||
text: audioHandler.mediaItem.value!.displayTitle!,
|
||||
style: TextStyle(
|
||||
fontSize: ScreenUtil().setSp(40),
|
||||
fontWeight: FontWeight.bold),
|
||||
|
@ -226,7 +226,7 @@ class _PlayerScreenHorizontalState extends State<PlayerScreenHorizontal> {
|
|||
pauseAfterRound: Duration(seconds: 2),
|
||||
)
|
||||
: Text(
|
||||
AudioService.currentMediaItem.displayTitle,
|
||||
audioHandler.mediaItem.value!.displayTitle!,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
|
@ -235,7 +235,7 @@ class _PlayerScreenHorizontalState extends State<PlayerScreenHorizontal> {
|
|||
)),
|
||||
const SizedBox(height: 4.0),
|
||||
Text(
|
||||
AudioService.currentMediaItem.displaySubtitle ?? '',
|
||||
audioHandler.mediaItem.value!.displaySubtitle ?? '',
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.clip,
|
||||
|
@ -267,9 +267,7 @@ class _PlayerScreenHorizontalState extends State<PlayerScreenHorizontal> {
|
|||
),
|
||||
onPressed: () {
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (context) => LyricsScreen(
|
||||
trackId:
|
||||
AudioService.currentMediaItem.id)));
|
||||
builder: (context) => LyricsScreen()));
|
||||
},
|
||||
),
|
||||
QualityInfoWidget(),
|
||||
|
@ -320,9 +318,9 @@ class _PlayerScreenVerticalState extends State<PlayerScreenVertical> {
|
|||
children: <Widget>[
|
||||
Container(
|
||||
height: ScreenUtil().setSp(80),
|
||||
child: AudioService.currentMediaItem.displayTitle.length >= 26
|
||||
child: audioHandler.mediaItem.value!.displayTitle!.length >= 26
|
||||
? Marquee(
|
||||
text: AudioService.currentMediaItem.displayTitle,
|
||||
text: audioHandler.mediaItem.value!.displayTitle!,
|
||||
style: TextStyle(
|
||||
fontSize: ScreenUtil().setSp(64),
|
||||
fontWeight: FontWeight.bold),
|
||||
|
@ -332,7 +330,7 @@ class _PlayerScreenVerticalState extends State<PlayerScreenVertical> {
|
|||
pauseAfterRound: Duration(seconds: 2),
|
||||
)
|
||||
: Text(
|
||||
AudioService.currentMediaItem.displayTitle,
|
||||
audioHandler.mediaItem.value!.displayTitle!,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
|
@ -341,7 +339,7 @@ class _PlayerScreenVerticalState extends State<PlayerScreenVertical> {
|
|||
)),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
AudioService.currentMediaItem.displaySubtitle ?? '',
|
||||
audioHandler.mediaItem.value!.displaySubtitle ?? '',
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.clip,
|
||||
|
@ -370,12 +368,11 @@ class _PlayerScreenVerticalState extends State<PlayerScreenVertical> {
|
|||
//Fix bottom buttons
|
||||
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
|
||||
systemNavigationBarColor:
|
||||
settings.themeData.bottomAppBarColor,
|
||||
settings.themeData!.bottomAppBarColor,
|
||||
statusBarColor: Colors.transparent));
|
||||
|
||||
await Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (context) => LyricsScreen(
|
||||
trackId: AudioService.currentMediaItem.id)));
|
||||
await Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => LyricsScreen()));
|
||||
|
||||
updateColor();
|
||||
},
|
||||
|
@ -386,7 +383,7 @@ class _PlayerScreenVerticalState extends State<PlayerScreenVertical> {
|
|||
semanticLabel: "Download".i18n,
|
||||
),
|
||||
onPressed: () async {
|
||||
Track t = Track.fromMediaItem(AudioService.currentMediaItem);
|
||||
Track t = Track.fromMediaItem(audioHandler.mediaItem.value!);
|
||||
if (await downloadManager.addOfflineTrack(t,
|
||||
private: false,
|
||||
context: context,
|
||||
|
@ -416,18 +413,18 @@ class QualityInfoWidget extends StatefulWidget {
|
|||
|
||||
class _QualityInfoWidgetState extends State<QualityInfoWidget> {
|
||||
String value = '';
|
||||
StreamSubscription streamSubscription;
|
||||
late StreamSubscription streamSubscription;
|
||||
|
||||
//Load data from native
|
||||
void _load() async {
|
||||
if (AudioService.currentMediaItem == null) return;
|
||||
Map data = await DownloadManager.platform.invokeMethod(
|
||||
"getStreamInfo", {"id": AudioService.currentMediaItem.id});
|
||||
if (audioHandler.mediaItem.value == null) return;
|
||||
Map? data = await DownloadManager.platform.invokeMethod(
|
||||
"getStreamInfo", {"id": audioHandler.mediaItem.value!.id});
|
||||
//N/A
|
||||
if (data == null) {
|
||||
setState(() => value = '');
|
||||
//If not show, try again later
|
||||
if (AudioService.currentMediaItem.extras['show'] == null)
|
||||
if (audioHandler.mediaItem.value!.extras!['show'] == null)
|
||||
Future.delayed(Duration(milliseconds: 200), _load);
|
||||
|
||||
return;
|
||||
|
@ -436,24 +433,22 @@ class _QualityInfoWidgetState extends State<QualityInfoWidget> {
|
|||
StreamQualityInfo info = StreamQualityInfo.fromJson(data);
|
||||
setState(() {
|
||||
value =
|
||||
'${info.format} ${info.bitrate(AudioService.currentMediaItem.duration)}kbps';
|
||||
'${info.format} ${info.bitrate(audioHandler.mediaItem.value!.duration)}kbps';
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_load();
|
||||
if (streamSubscription == null)
|
||||
streamSubscription =
|
||||
AudioService.currentMediaItemStream.listen((event) async {
|
||||
_load();
|
||||
});
|
||||
streamSubscription = audioHandler.mediaItem.listen((event) async {
|
||||
_load();
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (streamSubscription != null) streamSubscription.cancel();
|
||||
streamSubscription.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
@ -479,17 +474,17 @@ class PlayerMenuButton extends StatelessWidget {
|
|||
semanticLabel: "Options".i18n,
|
||||
),
|
||||
onPressed: () {
|
||||
Track t = Track.fromMediaItem(AudioService.currentMediaItem);
|
||||
final currentMediaItem = audioHandler.mediaItem.value!;
|
||||
Track t = Track.fromMediaItem(currentMediaItem);
|
||||
MenuSheet m = MenuSheet(context, navigateCallback: () {
|
||||
Navigator.of(context).pop();
|
||||
});
|
||||
if (AudioService.currentMediaItem.extras['show'] == null)
|
||||
if (currentMediaItem.extras!['show'] == null)
|
||||
m.defaultTrackMenu(t, options: [m.sleepTimer(), m.wakelock()]);
|
||||
else
|
||||
m.defaultShowEpisodeMenu(
|
||||
Show.fromJson(
|
||||
jsonDecode(AudioService.currentMediaItem.extras['show'])),
|
||||
ShowEpisode.fromMediaItem(AudioService.currentMediaItem),
|
||||
Show.fromJson(jsonDecode(currentMediaItem.extras!['show'])),
|
||||
ShowEpisode.fromMediaItem(currentMediaItem),
|
||||
options: [m.sleepTimer(), m.wakelock()]);
|
||||
},
|
||||
);
|
||||
|
@ -498,7 +493,7 @@ class PlayerMenuButton extends StatelessWidget {
|
|||
|
||||
class RepeatButton extends StatefulWidget {
|
||||
final double iconSize;
|
||||
RepeatButton(this.iconSize, {Key key}) : super(key: key);
|
||||
RepeatButton(this.iconSize, {Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_RepeatButtonState createState() => _RepeatButtonState();
|
||||
|
@ -545,7 +540,7 @@ class _RepeatButtonState extends State<RepeatButton> {
|
|||
|
||||
class PlaybackControls extends StatefulWidget {
|
||||
final double iconSize;
|
||||
PlaybackControls(this.iconSize, {Key key}) : super(key: key);
|
||||
PlaybackControls(this.iconSize, {Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_PlaybackControlsState createState() => _PlaybackControlsState();
|
||||
|
@ -554,7 +549,7 @@ class PlaybackControls extends StatefulWidget {
|
|||
class _PlaybackControlsState extends State<PlaybackControls> {
|
||||
Icon get libraryIcon {
|
||||
if (cache.checkTrackFavorite(
|
||||
Track.fromMediaItem(AudioService.currentMediaItem))) {
|
||||
Track.fromMediaItem(audioHandler.mediaItem.value!))) {
|
||||
return Icon(
|
||||
Icons.favorite,
|
||||
size: widget.iconSize * 0.64,
|
||||
|
@ -583,10 +578,10 @@ class _PlaybackControlsState extends State<PlaybackControls> {
|
|||
semanticLabel: "Dislike".i18n,
|
||||
),
|
||||
onPressed: () async {
|
||||
await deezerAPI.dislikeTrack(AudioService.currentMediaItem.id);
|
||||
await deezerAPI.dislikeTrack(audioHandler.mediaItem.value!.id);
|
||||
if (playerHelper.queueIndex <
|
||||
(AudioService.queue ?? []).length - 1) {
|
||||
AudioService.skipToNext();
|
||||
audioHandler.queue.value.length - 1) {
|
||||
audioHandler.skipToNext();
|
||||
}
|
||||
}),
|
||||
PrevNextButton(widget.iconSize, prev: true),
|
||||
|
@ -598,19 +593,19 @@ class _PlaybackControlsState extends State<PlaybackControls> {
|
|||
if (cache.libraryTracks == null) cache.libraryTracks = [];
|
||||
|
||||
if (cache.checkTrackFavorite(
|
||||
Track.fromMediaItem(AudioService.currentMediaItem))) {
|
||||
Track.fromMediaItem(audioHandler.mediaItem.value!))) {
|
||||
//Remove from library
|
||||
setState(() => cache.libraryTracks
|
||||
.remove(AudioService.currentMediaItem.id));
|
||||
setState(() => cache.libraryTracks!
|
||||
.remove(audioHandler.mediaItem.value!.id));
|
||||
await deezerAPI
|
||||
.removeFavorite(AudioService.currentMediaItem.id);
|
||||
.removeFavorite(audioHandler.mediaItem.value!.id);
|
||||
await cache.save();
|
||||
} else {
|
||||
//Add
|
||||
setState(() =>
|
||||
cache.libraryTracks.add(AudioService.currentMediaItem.id));
|
||||
cache.libraryTracks!.add(audioHandler.mediaItem.value!.id));
|
||||
await deezerAPI
|
||||
.addFavoriteTrack(AudioService.currentMediaItem.id);
|
||||
.addFavoriteTrack(audioHandler.mediaItem.value!.id);
|
||||
await cache.save();
|
||||
}
|
||||
},
|
||||
|
@ -630,12 +625,12 @@ class _BigAlbumArtState extends State<BigAlbumArt> {
|
|||
PageController _pageController = PageController(
|
||||
initialPage: playerHelper.queueIndex,
|
||||
);
|
||||
StreamSubscription _currentItemSub;
|
||||
StreamSubscription? _currentItemSub;
|
||||
bool _animationLock = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_currentItemSub = AudioService.currentMediaItemStream.listen((event) async {
|
||||
_currentItemSub = audioHandler.mediaItem.listen((event) async {
|
||||
_animationLock = true;
|
||||
await _pageController.animateToPage(playerHelper.queueIndex,
|
||||
duration: Duration(milliseconds: 300), curve: Curves.easeInOut);
|
||||
|
@ -646,7 +641,7 @@ class _BigAlbumArtState extends State<BigAlbumArt> {
|
|||
|
||||
@override
|
||||
void dispose() {
|
||||
if (_currentItemSub != null) _currentItemSub.cancel();
|
||||
_currentItemSub?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
@ -666,12 +661,12 @@ class _BigAlbumArtState extends State<BigAlbumArt> {
|
|||
return;
|
||||
}
|
||||
if (_animationLock) return;
|
||||
AudioService.skipToQueueItem(AudioService.queue[index].id);
|
||||
audioHandler.skipToQueueItem(index);
|
||||
},
|
||||
children: List.generate(
|
||||
AudioService.queue.length,
|
||||
audioHandler.queue.value.length,
|
||||
(i) => ZoomableImage(
|
||||
url: AudioService.queue[i].artUri.toString(),
|
||||
url: audioHandler.queue.value[i].artUri.toString(),
|
||||
)),
|
||||
),
|
||||
);
|
||||
|
@ -680,10 +675,10 @@ class _BigAlbumArtState extends State<BigAlbumArt> {
|
|||
|
||||
//Top row containing QueueSource, queue...
|
||||
class PlayerScreenTopRow extends StatelessWidget {
|
||||
final double textSize;
|
||||
final double iconSize;
|
||||
final double textWidth;
|
||||
final bool short;
|
||||
final double? textSize;
|
||||
final double? iconSize;
|
||||
final double? textWidth;
|
||||
final bool? short;
|
||||
PlayerScreenTopRow(
|
||||
{this.textSize, this.iconSize, this.textWidth, this.short});
|
||||
|
||||
|
@ -698,7 +693,7 @@ class PlayerScreenTopRow extends StatelessWidget {
|
|||
width: this.textWidth ?? ScreenUtil().setWidth(800),
|
||||
child: Text(
|
||||
(short ?? false)
|
||||
? (playerHelper.queueSource.text ?? '')
|
||||
? (playerHelper.queueSource!.text ?? '')
|
||||
: 'Playing from:'.i18n +
|
||||
' ' +
|
||||
(playerHelper.queueSource?.text ?? ''),
|
||||
|
@ -729,80 +724,87 @@ class SeekBar extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _SeekBarState extends State<SeekBar> {
|
||||
double _pos;
|
||||
bool _seeking = false;
|
||||
late StreamSubscription _subscription;
|
||||
final position = ValueNotifier<Duration>(Duration.zero);
|
||||
|
||||
double get position {
|
||||
if (_pos != null) return _pos;
|
||||
if (AudioService.playbackState == null) return 0.0;
|
||||
double p =
|
||||
AudioService.playbackState.currentPosition.inMilliseconds.toDouble() ??
|
||||
0.0;
|
||||
if (p > duration) return duration;
|
||||
return p;
|
||||
@override
|
||||
void initState() {
|
||||
_subscription = AudioService.position.listen((position) {
|
||||
if (_seeking) return; // user is seeking
|
||||
this.position.value = position;
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_subscription.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
double parseDuration(Duration position) {
|
||||
if (position > duration) return duration.inMilliseconds.toDouble();
|
||||
return position.inMilliseconds.toDouble();
|
||||
}
|
||||
|
||||
//Duration to mm:ss
|
||||
String _timeString(double pos) {
|
||||
Duration d = Duration(milliseconds: pos.toInt());
|
||||
String _timeString(Duration d) {
|
||||
return "${d.inMinutes}:${d.inSeconds.remainder(60).toString().padLeft(2, '0')}";
|
||||
}
|
||||
|
||||
double get duration {
|
||||
if (AudioService.currentMediaItem == null) return 1.0;
|
||||
return AudioService.currentMediaItem.duration.inMilliseconds.toDouble();
|
||||
Duration get duration {
|
||||
if (audioHandler.mediaItem.value == null) return Duration.zero;
|
||||
return audioHandler.mediaItem.value!.duration!;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return StreamBuilder(
|
||||
stream: Stream.periodic(Duration(milliseconds: 250)),
|
||||
builder: (BuildContext context, AsyncSnapshot snapshot) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 24.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
_timeString(position),
|
||||
style: TextStyle(fontSize: ScreenUtil().setSp(35)),
|
||||
),
|
||||
Text(
|
||||
_timeString(duration),
|
||||
style: TextStyle(fontSize: ScreenUtil().setSp(35)),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
Slider(
|
||||
focusNode: FocusNode(
|
||||
canRequestFocus: false,
|
||||
skipTraversal:
|
||||
true), // Don't focus on Slider - it doesn't work (and not needed)
|
||||
value: position,
|
||||
max: duration,
|
||||
onChangeStart: (double d) {
|
||||
setState(() {
|
||||
_pos = d;
|
||||
});
|
||||
},
|
||||
onChanged: (double d) {
|
||||
setState(() {
|
||||
_pos = d;
|
||||
});
|
||||
},
|
||||
onChangeEnd: (double d) async {
|
||||
await AudioService.seekTo(Duration(milliseconds: d.round()));
|
||||
setState(() {
|
||||
_pos = null;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 24.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
ValueListenableBuilder<Duration>(
|
||||
valueListenable: position,
|
||||
builder: (context, value, _) => Text(
|
||||
_timeString(value),
|
||||
style: TextStyle(fontSize: ScreenUtil().setSp(35)),
|
||||
)),
|
||||
StreamBuilder<MediaItem?>(
|
||||
stream: audioHandler.mediaItem,
|
||||
builder: (context, snapshot) => Text(
|
||||
_timeString(snapshot.data?.duration ?? Duration.zero),
|
||||
style: TextStyle(fontSize: ScreenUtil().setSp(35)),
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
ValueListenableBuilder<Duration>(
|
||||
valueListenable: position,
|
||||
builder: (context, value, _) => Slider(
|
||||
focusNode: FocusNode(
|
||||
canRequestFocus: false,
|
||||
skipTraversal:
|
||||
true), // Don't focus on Slider - it doesn't work (and not needed)
|
||||
value: parseDuration(value),
|
||||
max: duration.inMilliseconds.toDouble(),
|
||||
onChangeStart: (double d) {
|
||||
_seeking = true;
|
||||
position.value = Duration(milliseconds: d.toInt());
|
||||
},
|
||||
onChanged: (double d) {
|
||||
position.value = Duration(milliseconds: d.toInt());
|
||||
},
|
||||
onChangeEnd: (double d) {
|
||||
_seeking = false;
|
||||
audioHandler.seek(Duration(milliseconds: d.toInt()));
|
||||
},
|
||||
)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -813,25 +815,27 @@ class QueueScreen extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _QueueScreenState extends State<QueueScreen> {
|
||||
StreamSubscription _queueSub;
|
||||
late StreamSubscription _queueSub;
|
||||
|
||||
/// Basically a simple list that keeps itself synchronized with [AudioService.queue],
|
||||
/// Basically a simple list that keeps itself synchronized with [AudioHandler.queue],
|
||||
/// so that the [ReorderableListView] is updated instanly (as it should be)
|
||||
List<MediaItem> _queueCache = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_queueCache = AudioService.queue;
|
||||
_queueSub = AudioService.queueStream.listen((event) {
|
||||
_queueCache = AudioService.queue;
|
||||
setState(() {});
|
||||
_queueCache = audioHandler.queue.value;
|
||||
_queueSub = audioHandler.queue.listen((newQueue) {
|
||||
print('got queue $newQueue');
|
||||
// avoid rebuilding if the cache has got the right update
|
||||
if (listEquals(_queueCache, newQueue)) return;
|
||||
setState(() => _queueCache = newQueue);
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (_queueSub != null) _queueSub.cancel();
|
||||
_queueSub.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
@ -861,24 +865,36 @@ class _QueueScreenState extends State<QueueScreen> {
|
|||
},
|
||||
itemCount: _queueCache.length,
|
||||
itemBuilder: (BuildContext context, int i) {
|
||||
Track track = Track.fromMediaItem(AudioService.queue[i]);
|
||||
return TrackTile(
|
||||
track,
|
||||
onTap: () {
|
||||
pageViewLock = true;
|
||||
AudioService.skipToQueueItem(track.id)
|
||||
.then((value) => Navigator.of(context).pop());
|
||||
},
|
||||
Track track = Track.fromMediaItem(audioHandler.queue.value[i]);
|
||||
return Dismissible(
|
||||
key: Key(track.id),
|
||||
trailing: IconButton(
|
||||
icon: Icon(
|
||||
Icons.close,
|
||||
semanticLabel: "Close".i18n,
|
||||
),
|
||||
onPressed: () async {
|
||||
await AudioService.removeQueueItem(track.toMediaItem());
|
||||
setState(() {});
|
||||
background: DecoratedBox(
|
||||
decoration: BoxDecoration(color: Colors.red),
|
||||
child: Align(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Icon(Icons.delete)),
|
||||
alignment: Alignment.centerLeft)),
|
||||
secondaryBackground: DecoratedBox(
|
||||
decoration: BoxDecoration(color: Colors.red),
|
||||
child: Align(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Icon(Icons.delete)),
|
||||
alignment: Alignment.centerRight)),
|
||||
onDismissed: (_) {
|
||||
audioHandler.removeQueueItemAt(i);
|
||||
setState(() => _queueCache.removeAt(i));
|
||||
},
|
||||
child: TrackTile(
|
||||
track,
|
||||
onTap: () {
|
||||
pageViewLock = true;
|
||||
audioHandler
|
||||
.skipToQueueItem(i)
|
||||
.then((value) => Navigator.of(context).pop());
|
||||
},
|
||||
key: Key(track.id),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
|
|
@ -19,7 +19,7 @@ import '../api/definitions.dart';
|
|||
import 'error.dart';
|
||||
|
||||
openScreenByURL(BuildContext context, String url) async {
|
||||
DeezerLinkResponse res = await deezerAPI.parseLink(url);
|
||||
DeezerLinkResponse? res = await deezerAPI.parseLink(url);
|
||||
if (res == null) return;
|
||||
|
||||
switch (res.type) {
|
||||
|
@ -42,6 +42,8 @@ openScreenByURL(BuildContext context, String url) async {
|
|||
Navigator.of(context)
|
||||
.push(MaterialPageRoute(builder: (context) => PlaylistDetails(p)));
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -51,23 +53,23 @@ class SearchScreen extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _SearchScreenState extends State<SearchScreen> {
|
||||
String _query;
|
||||
String? _query;
|
||||
bool _offline = false;
|
||||
bool _loading = false;
|
||||
TextEditingController _controller = new TextEditingController();
|
||||
List _suggestions = [];
|
||||
List? _suggestions = [];
|
||||
bool _cancel = false;
|
||||
bool _showCards = true;
|
||||
//FocusNode _focus = FocusNode();
|
||||
|
||||
void _submit(BuildContext context, {String query}) async {
|
||||
void _submit(BuildContext context, {String? query}) async {
|
||||
if (query != null) _query = query;
|
||||
|
||||
//URL
|
||||
if (_query.startsWith('http')) {
|
||||
if (_query!.startsWith('http')) {
|
||||
setState(() => _loading = true);
|
||||
try {
|
||||
await openScreenByURL(context, _query);
|
||||
await openScreenByURL(context, _query!);
|
||||
} catch (e) {}
|
||||
setState(() => _loading = false);
|
||||
return;
|
||||
|
@ -96,13 +98,13 @@ class _SearchScreenState extends State<SearchScreen> {
|
|||
|
||||
//Load search suggestions
|
||||
Future<void> _loadSuggestions() async {
|
||||
if (_query == null || _query.length < 2 || _query.startsWith('http'))
|
||||
if (_query == null || _query!.length < 2 || _query!.startsWith('http'))
|
||||
return null;
|
||||
String q = _query;
|
||||
String? q = _query;
|
||||
await Future.delayed(Duration(milliseconds: 300));
|
||||
if (q != _query) return null;
|
||||
//Load
|
||||
List sugg;
|
||||
List? sugg;
|
||||
try {
|
||||
sugg = await deezerAPI.searchSuggestions(_query);
|
||||
} catch (e) {
|
||||
|
@ -119,7 +121,7 @@ class _SearchScreenState extends State<SearchScreen> {
|
|||
semanticLabel: "Remove".i18n,
|
||||
),
|
||||
onPressed: () async {
|
||||
if (cache.searchHistory != null) cache.searchHistory.removeAt(index);
|
||||
if (cache.searchHistory != null) cache.searchHistory!.removeAt(index);
|
||||
setState(() {});
|
||||
await cache.save();
|
||||
});
|
||||
|
@ -301,21 +303,21 @@ class _SearchScreenState extends State<SearchScreen> {
|
|||
//History
|
||||
if (!_showCards &&
|
||||
cache.searchHistory != null &&
|
||||
cache.searchHistory.length > 0 &&
|
||||
cache.searchHistory!.length > 0 &&
|
||||
(_query ?? '').length < 2)
|
||||
...List.generate(
|
||||
cache.searchHistory.length > 10
|
||||
cache.searchHistory!.length > 10
|
||||
? 10
|
||||
: cache.searchHistory.length, (int i) {
|
||||
dynamic data = cache.searchHistory[i].data;
|
||||
switch (cache.searchHistory[i].type) {
|
||||
: cache.searchHistory!.length, (int i) {
|
||||
dynamic data = cache.searchHistory![i].data;
|
||||
switch (cache.searchHistory![i].type) {
|
||||
case SearchHistoryItemType.TRACK:
|
||||
return TrackTile(
|
||||
data,
|
||||
onTap: () {
|
||||
List<Track> queue = cache.searchHistory
|
||||
List<Track?> queue = cache.searchHistory!
|
||||
.where((h) => h.type == SearchHistoryItemType.TRACK)
|
||||
.map<Track>((t) => t.data)
|
||||
.map<Track?>((t) => t.data)
|
||||
.toList();
|
||||
playerHelper.playFromTrackList(
|
||||
queue,
|
||||
|
@ -370,12 +372,13 @@ class _SearchScreenState extends State<SearchScreen> {
|
|||
},
|
||||
trailing: _removeHistoryItemWidget(i),
|
||||
);
|
||||
default:
|
||||
return const SizedBox();
|
||||
}
|
||||
return Container();
|
||||
}),
|
||||
|
||||
//Clear history
|
||||
if (cache.searchHistory != null && cache.searchHistory.length > 2)
|
||||
if (cache.searchHistory != null && cache.searchHistory!.length > 2)
|
||||
ListTile(
|
||||
title: Text('Clear search history'.i18n),
|
||||
leading: Icon(Icons.clear_all),
|
||||
|
@ -390,10 +393,10 @@ class _SearchScreenState extends State<SearchScreen> {
|
|||
...List.generate(
|
||||
(_suggestions ?? []).length,
|
||||
(i) => ListTile(
|
||||
title: Text(_suggestions[i]),
|
||||
title: Text(_suggestions![i]),
|
||||
leading: Icon(Icons.search),
|
||||
onTap: () {
|
||||
setState(() => _query = _suggestions[i]);
|
||||
setState(() => _query = _suggestions![i]);
|
||||
_submit(context);
|
||||
},
|
||||
))
|
||||
|
@ -405,13 +408,13 @@ class _SearchScreenState extends State<SearchScreen> {
|
|||
|
||||
class SearchBrowseCard extends StatelessWidget {
|
||||
final Color color;
|
||||
final Widget icon;
|
||||
final Widget? icon;
|
||||
final Function onTap;
|
||||
final String text;
|
||||
SearchBrowseCard(
|
||||
{@required this.color,
|
||||
@required this.onTap,
|
||||
@required this.text,
|
||||
{required this.color,
|
||||
required this.onTap,
|
||||
required this.text,
|
||||
this.icon});
|
||||
|
||||
@override
|
||||
|
@ -419,7 +422,7 @@ class SearchBrowseCard extends StatelessWidget {
|
|||
return Card(
|
||||
color: color,
|
||||
child: InkWell(
|
||||
onTap: this.onTap,
|
||||
onTap: this.onTap as void Function()?,
|
||||
child: Container(
|
||||
width: MediaQuery.of(context).size.width / 2 - 32,
|
||||
height: 75,
|
||||
|
@ -427,7 +430,7 @@ class SearchBrowseCard extends StatelessWidget {
|
|||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (icon != null) icon,
|
||||
if (icon != null) icon!,
|
||||
if (icon != null) Container(width: 8.0),
|
||||
Text(
|
||||
text,
|
||||
|
@ -449,8 +452,8 @@ class SearchBrowseCard extends StatelessWidget {
|
|||
}
|
||||
|
||||
class SearchResultsScreen extends StatelessWidget {
|
||||
final String query;
|
||||
final bool offline;
|
||||
final String? query;
|
||||
final bool? offline;
|
||||
|
||||
SearchResultsScreen(this.query, {this.offline});
|
||||
|
||||
|
@ -492,7 +495,7 @@ class SearchResultsScreen extends StatelessWidget {
|
|||
|
||||
//Tracks
|
||||
List<Widget> tracks = [];
|
||||
if (results.tracks != null && results.tracks.length != 0) {
|
||||
if (results.tracks != null && results.tracks!.length != 0) {
|
||||
tracks = [
|
||||
Padding(
|
||||
padding:
|
||||
|
@ -505,18 +508,18 @@ class SearchResultsScreen extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
...List.generate(3, (i) {
|
||||
if (results.tracks.length <= i)
|
||||
if (results.tracks!.length <= i)
|
||||
return Container(
|
||||
width: 0,
|
||||
height: 0,
|
||||
);
|
||||
Track t = results.tracks[i];
|
||||
Track? t = results.tracks![i];
|
||||
return TrackTile(
|
||||
t,
|
||||
onTap: () {
|
||||
cache.addToSearchHistory(t);
|
||||
playerHelper.playFromTrackList(
|
||||
results.tracks,
|
||||
results.tracks!,
|
||||
t.id,
|
||||
QueueSource(
|
||||
text: 'Search'.i18n,
|
||||
|
@ -547,7 +550,7 @@ class SearchResultsScreen extends StatelessWidget {
|
|||
|
||||
//Albums
|
||||
List<Widget> albums = [];
|
||||
if (results.albums != null && results.albums.length != 0) {
|
||||
if (results.albums != null && results.albums!.length != 0) {
|
||||
albums = [
|
||||
Padding(
|
||||
padding:
|
||||
|
@ -560,12 +563,12 @@ class SearchResultsScreen extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
...List.generate(3, (i) {
|
||||
if (results.albums.length <= i)
|
||||
if (results.albums!.length <= i)
|
||||
return Container(
|
||||
height: 0,
|
||||
width: 0,
|
||||
);
|
||||
Album a = results.albums[i];
|
||||
Album? a = results.albums![i];
|
||||
return AlbumTile(
|
||||
a,
|
||||
onHold: () {
|
||||
|
@ -592,7 +595,7 @@ class SearchResultsScreen extends StatelessWidget {
|
|||
|
||||
//Artists
|
||||
List<Widget> artists = [];
|
||||
if (results.artists != null && results.artists.length != 0) {
|
||||
if (results.artists != null && results.artists!.length != 0) {
|
||||
artists = [
|
||||
Padding(
|
||||
padding:
|
||||
|
@ -608,8 +611,8 @@ class SearchResultsScreen extends StatelessWidget {
|
|||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: List.generate(results.artists.length, (int i) {
|
||||
Artist a = results.artists[i];
|
||||
children: List.generate(results.artists!.length, (int i) {
|
||||
Artist a = results.artists![i];
|
||||
return ArtistTile(
|
||||
a,
|
||||
onTap: () {
|
||||
|
@ -630,7 +633,7 @@ class SearchResultsScreen extends StatelessWidget {
|
|||
|
||||
//Playlists
|
||||
List<Widget> playlists = [];
|
||||
if (results.playlists != null && results.playlists.length != 0) {
|
||||
if (results.playlists != null && results.playlists!.length != 0) {
|
||||
playlists = [
|
||||
Padding(
|
||||
padding:
|
||||
|
@ -643,12 +646,12 @@ class SearchResultsScreen extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
...List.generate(3, (i) {
|
||||
if (results.playlists.length <= i)
|
||||
if (results.playlists!.length <= i)
|
||||
return Container(
|
||||
height: 0,
|
||||
width: 0,
|
||||
);
|
||||
Playlist p = results.playlists[i];
|
||||
Playlist? p = results.playlists![i];
|
||||
return PlaylistTile(
|
||||
p,
|
||||
onTap: () {
|
||||
|
@ -676,7 +679,7 @@ class SearchResultsScreen extends StatelessWidget {
|
|||
|
||||
//Shows
|
||||
List<Widget> shows = [];
|
||||
if (results.shows != null && results.shows.length != 0) {
|
||||
if (results.shows != null && results.shows!.length != 0) {
|
||||
shows = [
|
||||
Padding(
|
||||
padding:
|
||||
|
@ -689,12 +692,12 @@ class SearchResultsScreen extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
...List.generate(3, (i) {
|
||||
if (results.shows.length <= i)
|
||||
if (results.shows!.length <= i)
|
||||
return Container(
|
||||
height: 0,
|
||||
width: 0,
|
||||
);
|
||||
Show s = results.shows[i];
|
||||
Show s = results.shows![i];
|
||||
return ShowTile(
|
||||
s,
|
||||
onTap: () async {
|
||||
|
@ -716,7 +719,7 @@ class SearchResultsScreen extends StatelessWidget {
|
|||
|
||||
//Episodes
|
||||
List<Widget> episodes = [];
|
||||
if (results.episodes != null && results.episodes.length != 0) {
|
||||
if (results.episodes != null && results.episodes!.length != 0) {
|
||||
episodes = [
|
||||
Padding(
|
||||
padding:
|
||||
|
@ -729,12 +732,12 @@ class SearchResultsScreen extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
...List.generate(3, (i) {
|
||||
if (results.episodes.length <= i)
|
||||
if (results.episodes!.length <= i)
|
||||
return Container(
|
||||
height: 0,
|
||||
width: 0,
|
||||
);
|
||||
ShowEpisode e = results.episodes[i];
|
||||
ShowEpisode e = results.episodes![i];
|
||||
return ShowEpisodeTile(
|
||||
e,
|
||||
trailing: IconButton(
|
||||
|
@ -744,14 +747,14 @@ class SearchResultsScreen extends StatelessWidget {
|
|||
),
|
||||
onPressed: () {
|
||||
MenuSheet m = MenuSheet(context);
|
||||
m.defaultShowEpisodeMenu(e.show, e);
|
||||
m.defaultShowEpisodeMenu(e.show!, e);
|
||||
},
|
||||
),
|
||||
onTap: () async {
|
||||
//Load entire show, then play
|
||||
List<ShowEpisode> episodes =
|
||||
await deezerAPI.allShowEpisodes(e.show.id);
|
||||
await playerHelper.playShowEpisode(e.show, episodes,
|
||||
(await deezerAPI.allShowEpisodes(e.show!.id))!;
|
||||
await playerHelper.playShowEpisode(e.show!, episodes,
|
||||
index: episodes.indexWhere((ep) => e.id == ep.id));
|
||||
},
|
||||
);
|
||||
|
@ -802,7 +805,7 @@ class SearchResultsScreen extends StatelessWidget {
|
|||
//List all tracks
|
||||
class TrackListScreen extends StatelessWidget {
|
||||
final QueueSource queueSource;
|
||||
final List<Track> tracks;
|
||||
final List<Track?>? tracks;
|
||||
|
||||
TrackListScreen(this.tracks, this.queueSource);
|
||||
|
||||
|
@ -811,17 +814,17 @@ class TrackListScreen extends StatelessWidget {
|
|||
return Scaffold(
|
||||
appBar: FreezerAppBar('Tracks'.i18n),
|
||||
body: ListView.builder(
|
||||
itemCount: tracks.length,
|
||||
itemCount: tracks!.length,
|
||||
itemBuilder: (BuildContext context, int i) {
|
||||
Track t = tracks[i];
|
||||
Track? t = tracks![i];
|
||||
return TrackTile(
|
||||
t,
|
||||
onTap: () {
|
||||
playerHelper.playFromTrackList(tracks, t.id, queueSource);
|
||||
playerHelper.playFromTrackList(tracks!, t!.id, queueSource);
|
||||
},
|
||||
onHold: () {
|
||||
MenuSheet m = MenuSheet(context);
|
||||
m.defaultTrackMenu(t);
|
||||
m.defaultTrackMenu(t!);
|
||||
},
|
||||
);
|
||||
},
|
||||
|
@ -832,7 +835,7 @@ class TrackListScreen extends StatelessWidget {
|
|||
|
||||
//List all albums
|
||||
class AlbumListScreen extends StatelessWidget {
|
||||
final List<Album> albums;
|
||||
final List<Album?>? albums;
|
||||
AlbumListScreen(this.albums);
|
||||
|
||||
@override
|
||||
|
@ -840,9 +843,9 @@ class AlbumListScreen extends StatelessWidget {
|
|||
return Scaffold(
|
||||
appBar: FreezerAppBar('Albums'.i18n),
|
||||
body: ListView.builder(
|
||||
itemCount: albums.length,
|
||||
itemCount: albums!.length,
|
||||
itemBuilder: (context, i) {
|
||||
Album a = albums[i];
|
||||
Album? a = albums![i];
|
||||
return AlbumTile(
|
||||
a,
|
||||
onTap: () {
|
||||
|
@ -851,7 +854,7 @@ class AlbumListScreen extends StatelessWidget {
|
|||
},
|
||||
onHold: () {
|
||||
MenuSheet m = MenuSheet(context);
|
||||
m.defaultAlbumMenu(a);
|
||||
m.defaultAlbumMenu(a!);
|
||||
},
|
||||
);
|
||||
},
|
||||
|
@ -861,7 +864,7 @@ class AlbumListScreen extends StatelessWidget {
|
|||
}
|
||||
|
||||
class SearchResultPlaylists extends StatelessWidget {
|
||||
final List<Playlist> playlists;
|
||||
final List<Playlist?>? playlists;
|
||||
SearchResultPlaylists(this.playlists);
|
||||
|
||||
@override
|
||||
|
@ -869,9 +872,9 @@ class SearchResultPlaylists extends StatelessWidget {
|
|||
return Scaffold(
|
||||
appBar: FreezerAppBar('Playlists'.i18n),
|
||||
body: ListView.builder(
|
||||
itemCount: playlists.length,
|
||||
itemCount: playlists!.length,
|
||||
itemBuilder: (context, i) {
|
||||
Playlist p = playlists[i];
|
||||
Playlist? p = playlists![i];
|
||||
return PlaylistTile(
|
||||
p,
|
||||
onTap: () {
|
||||
|
@ -880,7 +883,7 @@ class SearchResultPlaylists extends StatelessWidget {
|
|||
},
|
||||
onHold: () {
|
||||
MenuSheet m = MenuSheet(context);
|
||||
m.defaultPlaylistMenu(p);
|
||||
m.defaultPlaylistMenu(p!);
|
||||
},
|
||||
);
|
||||
},
|
||||
|
@ -890,7 +893,7 @@ class SearchResultPlaylists extends StatelessWidget {
|
|||
}
|
||||
|
||||
class ShowListScreen extends StatelessWidget {
|
||||
final List<Show> shows;
|
||||
final List<Show>? shows;
|
||||
ShowListScreen(this.shows);
|
||||
|
||||
@override
|
||||
|
@ -898,9 +901,9 @@ class ShowListScreen extends StatelessWidget {
|
|||
return Scaffold(
|
||||
appBar: FreezerAppBar('Shows'.i18n),
|
||||
body: ListView.builder(
|
||||
itemCount: shows.length,
|
||||
itemCount: shows!.length,
|
||||
itemBuilder: (context, i) {
|
||||
Show s = shows[i];
|
||||
Show s = shows![i];
|
||||
return ShowTile(
|
||||
s,
|
||||
onTap: () {
|
||||
|
@ -915,7 +918,7 @@ class ShowListScreen extends StatelessWidget {
|
|||
}
|
||||
|
||||
class EpisodeListScreen extends StatelessWidget {
|
||||
final List<ShowEpisode> episodes;
|
||||
final List<ShowEpisode>? episodes;
|
||||
EpisodeListScreen(this.episodes);
|
||||
|
||||
@override
|
||||
|
@ -923,9 +926,9 @@ class EpisodeListScreen extends StatelessWidget {
|
|||
return Scaffold(
|
||||
appBar: FreezerAppBar('Episodes'.i18n),
|
||||
body: ListView.builder(
|
||||
itemCount: episodes.length,
|
||||
itemCount: episodes!.length,
|
||||
itemBuilder: (context, i) {
|
||||
ShowEpisode e = episodes[i];
|
||||
ShowEpisode e = episodes![i];
|
||||
return ShowEpisodeTile(
|
||||
e,
|
||||
trailing: IconButton(
|
||||
|
@ -935,14 +938,14 @@ class EpisodeListScreen extends StatelessWidget {
|
|||
),
|
||||
onPressed: () {
|
||||
MenuSheet m = MenuSheet(context);
|
||||
m.defaultShowEpisodeMenu(e.show, e);
|
||||
m.defaultShowEpisodeMenu(e.show!, e);
|
||||
},
|
||||
),
|
||||
onTap: () async {
|
||||
//Load entire show, then play
|
||||
List<ShowEpisode> episodes =
|
||||
await deezerAPI.allShowEpisodes(e.show.id);
|
||||
await playerHelper.playShowEpisode(e.show, episodes,
|
||||
(await deezerAPI.allShowEpisodes(e.show!.id))!;
|
||||
await playerHelper.playShowEpisode(e.show!, episodes,
|
||||
index: episodes.indexWhere((ep) => e.id == ep.id));
|
||||
},
|
||||
);
|
||||
|
|
|
@ -1,9 +1,5 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:country_pickers/country.dart';
|
||||
import 'package:country_pickers/country_picker_dialog.dart';
|
||||
import 'package:filesize/filesize.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_displaymode/flutter_displaymode.dart';
|
||||
|
@ -22,7 +18,6 @@ import 'package:freezer/api/download.dart';
|
|||
import 'package:freezer/api/player.dart';
|
||||
import 'package:freezer/ui/downloads_screen.dart';
|
||||
import 'package:freezer/ui/elements.dart';
|
||||
import 'package:freezer/ui/error.dart';
|
||||
import 'package:freezer/ui/home_screen.dart';
|
||||
import 'package:freezer/ui/updater.dart';
|
||||
import 'package:freezer/translations.i18n.dart';
|
||||
|
@ -200,7 +195,7 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
|
|||
),
|
||||
SwitchListTile(
|
||||
title: Text('Use system theme'.i18n),
|
||||
value: settings.useSystemTheme,
|
||||
value: settings.useSystemTheme!,
|
||||
onChanged: (bool v) async {
|
||||
settings.useSystemTheme = v;
|
||||
|
||||
|
@ -211,7 +206,7 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
|
|||
ListTile(
|
||||
title: Text('Font'.i18n),
|
||||
leading: Icon(Icons.font_download),
|
||||
subtitle: Text(settings.font),
|
||||
subtitle: Text(settings.font!),
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
|
@ -222,7 +217,7 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
|
|||
SwitchListTile(
|
||||
title: Text('Player gradient background'.i18n),
|
||||
secondary: Icon(Icons.colorize),
|
||||
value: settings.colorGradientBackground,
|
||||
value: settings.colorGradientBackground!,
|
||||
onChanged: (bool v) async {
|
||||
setState(() => settings.colorGradientBackground = v);
|
||||
await settings.save();
|
||||
|
@ -232,7 +227,7 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
|
|||
title: Text('Blur player background'.i18n),
|
||||
subtitle: Text('Might have impact on performance'.i18n),
|
||||
secondary: Icon(Icons.blur_on),
|
||||
value: settings.blurPlayerBackground,
|
||||
value: settings.blurPlayerBackground!,
|
||||
onChanged: (bool v) async {
|
||||
setState(() => settings.blurPlayerBackground = v);
|
||||
await settings.save();
|
||||
|
@ -244,7 +239,7 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
|
|||
'Show visualizers on lyrics page. WARNING: Requires microphone permission!'
|
||||
.i18n),
|
||||
secondary: Icon(Icons.equalizer),
|
||||
value: settings.lyricsVisualizer,
|
||||
value: settings.lyricsVisualizer!,
|
||||
onChanged: (bool v) async {
|
||||
if (await Permission.microphone.request().isGranted) {
|
||||
setState(() => settings.lyricsVisualizer = v);
|
||||
|
@ -283,7 +278,8 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
|
|||
],
|
||||
allowShades: false,
|
||||
selectedColor: settings.primaryColor,
|
||||
onMainColorChange: (ColorSwatch color) {
|
||||
onMainColorChange: (ColorSwatch? color) {
|
||||
if (color == null) return;
|
||||
settings.primaryColor = color;
|
||||
settings.save();
|
||||
updateTheme();
|
||||
|
@ -338,7 +334,7 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
|
|||
class FontSelector extends StatefulWidget {
|
||||
final Function callback;
|
||||
|
||||
FontSelector(this.callback, {Key key}) : super(key: key);
|
||||
FontSelector(this.callback, {Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_FontSelectorState createState() => _FontSelectorState();
|
||||
|
@ -450,14 +446,14 @@ class _QualitySettingsState extends State<QualitySettings> {
|
|||
|
||||
class QualityPicker extends StatefulWidget {
|
||||
final String field;
|
||||
QualityPicker(this.field, {Key key}) : super(key: key);
|
||||
QualityPicker(this.field, {Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_QualityPickerState createState() => _QualityPickerState();
|
||||
}
|
||||
|
||||
class _QualityPickerState extends State<QualityPicker> {
|
||||
AudioQuality _quality;
|
||||
AudioQuality? _quality;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
@ -484,7 +480,7 @@ class _QualityPickerState extends State<QualityPicker> {
|
|||
}
|
||||
|
||||
//Update quality in settings
|
||||
void _updateQuality(AudioQuality q) async {
|
||||
void _updateQuality(AudioQuality? q) async {
|
||||
setState(() {
|
||||
_quality = q;
|
||||
});
|
||||
|
@ -516,26 +512,26 @@ class _QualityPickerState extends State<QualityPicker> {
|
|||
title: Text('MP3 128kbps'),
|
||||
groupValue: _quality,
|
||||
value: AudioQuality.MP3_128,
|
||||
onChanged: (q) => _updateQuality(q),
|
||||
onChanged: (dynamic q) => _updateQuality(q),
|
||||
),
|
||||
RadioListTile(
|
||||
title: Text('MP3 320kbps'),
|
||||
groupValue: _quality,
|
||||
value: AudioQuality.MP3_320,
|
||||
onChanged: (q) => _updateQuality(q),
|
||||
onChanged: (dynamic q) => _updateQuality(q),
|
||||
),
|
||||
RadioListTile(
|
||||
title: Text('FLAC'),
|
||||
groupValue: _quality,
|
||||
value: AudioQuality.FLAC,
|
||||
onChanged: (q) => _updateQuality(q),
|
||||
onChanged: (dynamic q) => _updateQuality(q),
|
||||
),
|
||||
if (widget.field == 'download')
|
||||
RadioListTile(
|
||||
title: Text('Ask before downloading'.i18n),
|
||||
groupValue: _quality,
|
||||
value: AudioQuality.ASK,
|
||||
onChanged: (q) => _updateQuality(q),
|
||||
onChanged: (dynamic q) => _updateQuality(q),
|
||||
)
|
||||
],
|
||||
);
|
||||
|
@ -647,7 +643,7 @@ class _DeezerSettingsState extends State<DeezerSettings> {
|
|||
subtitle: Text(
|
||||
'Send track listen logs to Deezer, enable it for features like Flow to work properly'
|
||||
.i18n),
|
||||
value: settings.logListen,
|
||||
value: settings.logListen!,
|
||||
secondary: Icon(Icons.history_toggle_off),
|
||||
onChanged: (bool v) {
|
||||
setState(() => settings.logListen = v);
|
||||
|
@ -710,9 +706,9 @@ class _DeezerSettingsState extends State<DeezerSettings> {
|
|||
}
|
||||
|
||||
class FilenameTemplateDialog extends StatefulWidget {
|
||||
final String initial;
|
||||
final String? initial;
|
||||
final Function onSave;
|
||||
FilenameTemplateDialog(this.initial, this.onSave, {Key key})
|
||||
FilenameTemplateDialog(this.initial, this.onSave, {Key? key})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
|
@ -720,13 +716,13 @@ class FilenameTemplateDialog extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _FilenameTemplateDialogState extends State<FilenameTemplateDialog> {
|
||||
TextEditingController _controller;
|
||||
String _new;
|
||||
TextEditingController? _controller;
|
||||
String? _new;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_controller = TextEditingController(text: widget.initial);
|
||||
_new = _controller.value.text;
|
||||
_new = _controller!.value.text;
|
||||
super.initState();
|
||||
}
|
||||
|
||||
|
@ -762,14 +758,14 @@ class _FilenameTemplateDialogState extends State<FilenameTemplateDialog> {
|
|||
TextButton(
|
||||
child: Text('Reset'.i18n),
|
||||
onPressed: () {
|
||||
_controller.value =
|
||||
_controller.value.copyWith(text: '%artist% - %title%');
|
||||
_controller!.value =
|
||||
_controller!.value.copyWith(text: '%artist% - %title%');
|
||||
_new = '%artist% - %title%';
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
child: Text('Clear'.i18n),
|
||||
onPressed: () => _controller.clear(),
|
||||
onPressed: () => _controller!.clear(),
|
||||
),
|
||||
TextButton(
|
||||
child: Text('Save'.i18n),
|
||||
|
@ -789,7 +785,7 @@ class DownloadsSettings extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _DownloadsSettingsState extends State<DownloadsSettings> {
|
||||
double _downloadThreads = settings.downloadThreads.toDouble();
|
||||
double _downloadThreads = settings.downloadThreads!.toDouble();
|
||||
TextEditingController _artistSeparatorController =
|
||||
TextEditingController(text: settings.artistSeparator);
|
||||
|
||||
|
@ -802,7 +798,7 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
|
|||
ListTile(
|
||||
title: Text('Download path'.i18n),
|
||||
leading: Icon(Icons.folder),
|
||||
subtitle: Text(settings.downloadPath),
|
||||
subtitle: Text(settings.downloadPath!),
|
||||
onTap: () async {
|
||||
//Check permissions
|
||||
if (!await Permission.storage.request().isGranted) return;
|
||||
|
@ -874,7 +870,7 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
|
|||
_downloadThreads = val;
|
||||
setState(() {
|
||||
settings.downloadThreads = _downloadThreads.round();
|
||||
_downloadThreads = settings.downloadThreads.toDouble();
|
||||
_downloadThreads = settings.downloadThreads!.toDouble();
|
||||
});
|
||||
await settings.save();
|
||||
|
||||
|
@ -910,7 +906,7 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
|
|||
),
|
||||
SwitchListTile(
|
||||
title: Text('Create folders for artist'.i18n),
|
||||
value: settings.artistFolder,
|
||||
value: settings.artistFolder!,
|
||||
onChanged: (v) {
|
||||
setState(() => settings.artistFolder = v);
|
||||
settings.save();
|
||||
|
@ -919,7 +915,7 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
|
|||
),
|
||||
SwitchListTile(
|
||||
title: Text('Create folders for albums'.i18n),
|
||||
value: settings.albumFolder,
|
||||
value: settings.albumFolder!,
|
||||
onChanged: (v) {
|
||||
setState(() => settings.albumFolder = v);
|
||||
settings.save();
|
||||
|
@ -927,7 +923,7 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
|
|||
secondary: Icon(Icons.folder)),
|
||||
SwitchListTile(
|
||||
title: Text('Create folder for playlist'.i18n),
|
||||
value: settings.playlistFolder,
|
||||
value: settings.playlistFolder!,
|
||||
onChanged: (v) {
|
||||
setState(() => settings.playlistFolder = v);
|
||||
settings.save();
|
||||
|
@ -936,7 +932,7 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
|
|||
FreezerDivider(),
|
||||
SwitchListTile(
|
||||
title: Text('Separate albums by discs'.i18n),
|
||||
value: settings.albumDiscFolder,
|
||||
value: settings.albumDiscFolder!,
|
||||
onChanged: (v) {
|
||||
setState(() => settings.albumDiscFolder = v);
|
||||
settings.save();
|
||||
|
@ -944,7 +940,7 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
|
|||
secondary: Icon(Icons.album)),
|
||||
SwitchListTile(
|
||||
title: Text('Overwrite already downloaded files'.i18n),
|
||||
value: settings.overwriteDownload,
|
||||
value: settings.overwriteDownload!,
|
||||
onChanged: (v) {
|
||||
setState(() => settings.overwriteDownload = v);
|
||||
settings.save();
|
||||
|
@ -952,7 +948,7 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
|
|||
secondary: Icon(Icons.delete)),
|
||||
SwitchListTile(
|
||||
title: Text('Download .LRC lyrics'.i18n),
|
||||
value: settings.downloadLyrics,
|
||||
value: settings.downloadLyrics!,
|
||||
onChanged: (v) {
|
||||
setState(() => settings.downloadLyrics = v);
|
||||
settings.save();
|
||||
|
@ -961,7 +957,7 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
|
|||
FreezerDivider(),
|
||||
SwitchListTile(
|
||||
title: Text('Save cover file for every track'.i18n),
|
||||
value: settings.trackCover,
|
||||
value: settings.trackCover!,
|
||||
onChanged: (v) {
|
||||
setState(() => settings.trackCover = v);
|
||||
settings.save();
|
||||
|
@ -969,7 +965,7 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
|
|||
secondary: Icon(Icons.image)),
|
||||
SwitchListTile(
|
||||
title: Text('Save album cover'.i18n),
|
||||
value: settings.albumCover,
|
||||
value: settings.albumCover!,
|
||||
onChanged: (v) {
|
||||
setState(() => settings.albumCover = v);
|
||||
settings.save();
|
||||
|
@ -992,7 +988,7 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
|
|||
child: Text(i.toString()),
|
||||
))
|
||||
.toList(),
|
||||
onChanged: (int n) async {
|
||||
onChanged: (int? n) async {
|
||||
setState(() {
|
||||
settings.albumArtResolution = n;
|
||||
});
|
||||
|
@ -1003,7 +999,7 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
|
|||
title: Text('Create .nomedia files'.i18n),
|
||||
subtitle:
|
||||
Text('To prevent gallery being filled with album art'.i18n),
|
||||
value: settings.nomediaFiles,
|
||||
value: settings.nomediaFiles!,
|
||||
onChanged: (v) {
|
||||
setState(() => settings.nomediaFiles = v);
|
||||
settings.save();
|
||||
|
@ -1077,13 +1073,13 @@ class _TagSelectionScreenState extends State<TagSelectionScreen> {
|
|||
(i) => ListTile(
|
||||
title: Text(tags[i].title),
|
||||
leading: Switch(
|
||||
value: settings.tags.contains(tags[i].value),
|
||||
value: settings.tags!.contains(tags[i].value),
|
||||
onChanged: (v) async {
|
||||
//Update
|
||||
if (v)
|
||||
settings.tags.add(tags[i].value);
|
||||
settings.tags!.add(tags[i].value);
|
||||
else
|
||||
settings.tags.remove(tags[i].value);
|
||||
settings.tags!.remove(tags[i].value);
|
||||
setState(() {});
|
||||
await settings.save();
|
||||
},
|
||||
|
@ -1119,7 +1115,7 @@ class _GeneralSettingsState extends State<GeneralSettings> {
|
|||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
deezerAPI.authorize().then((v) {
|
||||
deezerAPI.authorize()!.then((v) {
|
||||
if (v) {
|
||||
setState(() => settings.offlineMode = false);
|
||||
} else {
|
||||
|
@ -1148,7 +1144,7 @@ class _GeneralSettingsState extends State<GeneralSettings> {
|
|||
'Might enable some equalizer apps to work. Requires restart of Freezer'
|
||||
.i18n),
|
||||
secondary: Icon(Icons.equalizer),
|
||||
value: settings.enableEqualizer,
|
||||
value: settings.enableEqualizer!,
|
||||
onChanged: (v) async {
|
||||
setState(() => settings.enableEqualizer = v);
|
||||
settings.save();
|
||||
|
@ -1158,7 +1154,7 @@ class _GeneralSettingsState extends State<GeneralSettings> {
|
|||
title: Text('Ignore interruptions'.i18n),
|
||||
subtitle: Text('Requires app restart to apply!'.i18n),
|
||||
secondary: Icon(Icons.not_interested),
|
||||
value: settings.ignoreInterruptions,
|
||||
value: settings.ignoreInterruptions!,
|
||||
onChanged: (bool v) async {
|
||||
setState(() => settings.ignoreInterruptions = v);
|
||||
await settings.save();
|
||||
|
@ -1178,7 +1174,7 @@ class _GeneralSettingsState extends State<GeneralSettings> {
|
|||
settings.lastFMUsername = null;
|
||||
settings.lastFMPassword = null;
|
||||
await settings.save();
|
||||
await AudioService.customAction("disableLastFM");
|
||||
await audioHandler.customAction("disableLastFM", {});
|
||||
setState(() {});
|
||||
Fluttertoast.showToast(msg: 'Logged out!'.i18n);
|
||||
return;
|
||||
|
@ -1220,7 +1216,7 @@ class _GeneralSettingsState extends State<GeneralSettings> {
|
|||
child: Text('Log out & Exit'.i18n),
|
||||
onPressed: () async {
|
||||
try {
|
||||
AudioService.stop();
|
||||
audioHandler.stop();
|
||||
} catch (e) {}
|
||||
await logOut();
|
||||
await DownloadManager.platform
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:fluttericon/octicons_icons.dart';
|
||||
import 'package:freezer/api/deezer.dart';
|
||||
import 'package:freezer/api/download.dart';
|
||||
import 'package:freezer/api/player.dart';
|
||||
import 'package:freezer/translations.i18n.dart';
|
||||
|
||||
import '../api/definitions.dart';
|
||||
|
@ -10,45 +11,44 @@ import 'cached_image.dart';
|
|||
|
||||
import 'dart:async';
|
||||
|
||||
|
||||
class TrackTile extends StatefulWidget {
|
||||
final Track? track;
|
||||
final void Function()? onTap;
|
||||
final void Function()? onHold;
|
||||
final Widget? trailing;
|
||||
|
||||
final Track track;
|
||||
final Function onTap;
|
||||
final Function onHold;
|
||||
final Widget trailing;
|
||||
|
||||
TrackTile(this.track, {this.onTap, this.onHold, this.trailing, Key key}): super(key: key);
|
||||
TrackTile(this.track, {this.onTap, this.onHold, this.trailing, Key? key})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
_TrackTileState createState() => _TrackTileState();
|
||||
}
|
||||
|
||||
class _TrackTileState extends State<TrackTile> {
|
||||
|
||||
StreamSubscription _subscription;
|
||||
StreamSubscription? _subscription;
|
||||
bool _isOffline = false;
|
||||
|
||||
bool get nowPlaying {
|
||||
if (AudioService.currentMediaItem == null) return false;
|
||||
return AudioService.currentMediaItem.id == widget.track.id;
|
||||
}
|
||||
bool _isHighlighted = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
//Listen to media item changes, update text color if currently playing
|
||||
_subscription = AudioService.currentMediaItemStream.listen((event) {
|
||||
setState(() {});
|
||||
_subscription = audioHandler.mediaItem.listen((mediaItem) {
|
||||
if (mediaItem == null) return;
|
||||
if (mediaItem.id == widget.track?.id)
|
||||
setState(() => _isHighlighted = true);
|
||||
else if (_isHighlighted) setState(() => _isHighlighted = false);
|
||||
});
|
||||
//Check if offline
|
||||
downloadManager.checkOffline(track: widget.track).then((b) => setState(() => _isOffline = b));
|
||||
downloadManager.checkOffline(track: widget.track).then((isOffline) {
|
||||
if (isOffline) setState(() => _isOffline = isOffline);
|
||||
});
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (_subscription != null) _subscription.cancel();
|
||||
_subscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
@ -56,19 +56,18 @@ class _TrackTileState extends State<TrackTile> {
|
|||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
title: Text(
|
||||
widget.track.title,
|
||||
widget.track!.title!,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.clip,
|
||||
style: TextStyle(
|
||||
color: nowPlaying?Theme.of(context).primaryColor:null
|
||||
),
|
||||
color: _isHighlighted ? Theme.of(context).primaryColor : null),
|
||||
),
|
||||
subtitle: Text(
|
||||
widget.track.artistString,
|
||||
widget.track!.artistString,
|
||||
maxLines: 1,
|
||||
),
|
||||
leading: CachedImage(
|
||||
url: widget.track.albumArt.thumb,
|
||||
url: widget.track!.albumArt!.thumb!,
|
||||
width: 48,
|
||||
),
|
||||
onTap: widget.onTap,
|
||||
|
@ -76,7 +75,7 @@ class _TrackTileState extends State<TrackTile> {
|
|||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if ((_isOffline??false))
|
||||
if (_isOffline)
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 2.0),
|
||||
child: Icon(
|
||||
|
@ -85,24 +84,22 @@ class _TrackTileState extends State<TrackTile> {
|
|||
size: 12.0,
|
||||
),
|
||||
),
|
||||
if (widget.track.explicit??false)
|
||||
if (widget.track!.explicit ?? false)
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 2.0),
|
||||
child: Text(
|
||||
'E',
|
||||
style: TextStyle(
|
||||
color: Colors.red
|
||||
),
|
||||
style: TextStyle(color: Colors.red),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: 42.0,
|
||||
child: Text(
|
||||
widget.track.durationString,
|
||||
widget.track!.durationString,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
widget.trailing??Container(width: 0, height: 0)
|
||||
widget.trailing ?? const SizedBox(width: 0, height: 0)
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@ -110,11 +107,10 @@ class _TrackTileState extends State<TrackTile> {
|
|||
}
|
||||
|
||||
class AlbumTile extends StatelessWidget {
|
||||
|
||||
final Album album;
|
||||
final Function onTap;
|
||||
final Function onHold;
|
||||
final Widget trailing;
|
||||
final Album? album;
|
||||
final Function? onTap;
|
||||
final Function? onHold;
|
||||
final Widget? trailing;
|
||||
|
||||
AlbumTile(this.album, {this.onTap, this.onHold, this.trailing});
|
||||
|
||||
|
@ -122,113 +118,116 @@ class AlbumTile extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
title: Text(
|
||||
album.title,
|
||||
album!.title!,
|
||||
maxLines: 1,
|
||||
),
|
||||
subtitle: Text(
|
||||
album.artistString,
|
||||
album!.artistString,
|
||||
maxLines: 1,
|
||||
),
|
||||
leading: CachedImage(
|
||||
url: album.art.thumb,
|
||||
url: album!.art!.thumb,
|
||||
width: 48,
|
||||
),
|
||||
onTap: onTap,
|
||||
onLongPress: onHold,
|
||||
onTap: onTap as void Function()?,
|
||||
onLongPress: onHold as void Function()?,
|
||||
trailing: trailing,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ArtistTile extends StatelessWidget {
|
||||
|
||||
final Artist artist;
|
||||
final Function onTap;
|
||||
final Function onHold;
|
||||
final Artist? artist;
|
||||
final Function? onTap;
|
||||
final Function? onHold;
|
||||
|
||||
ArtistTile(this.artist, {this.onTap, this.onHold});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: 150,
|
||||
child: Container(
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
onLongPress: onHold,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Container(height: 4,),
|
||||
CachedImage(
|
||||
url: artist.picture.thumb,
|
||||
circular: true,
|
||||
width: 100,
|
||||
),
|
||||
Container(height: 8,),
|
||||
Text(
|
||||
artist.name,
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 14.0
|
||||
width: 150,
|
||||
child: Container(
|
||||
child: InkWell(
|
||||
onTap: onTap as void Function()?,
|
||||
onLongPress: onHold as void Function()?,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
height: 4,
|
||||
),
|
||||
),
|
||||
Container(height: 4,),
|
||||
],
|
||||
CachedImage(
|
||||
url: artist!.picture!.thumb,
|
||||
circular: true,
|
||||
width: 100,
|
||||
),
|
||||
Container(
|
||||
height: 8,
|
||||
),
|
||||
Text(
|
||||
artist!.name!,
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(fontSize: 14.0),
|
||||
),
|
||||
Container(
|
||||
height: 4,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
class PlaylistTile extends StatelessWidget {
|
||||
|
||||
final Playlist playlist;
|
||||
final Function onTap;
|
||||
final Function onHold;
|
||||
final Widget trailing;
|
||||
final Playlist? playlist;
|
||||
final Function? onTap;
|
||||
final Function? onHold;
|
||||
final Widget? trailing;
|
||||
|
||||
PlaylistTile(this.playlist, {this.onHold, this.onTap, this.trailing});
|
||||
|
||||
String get subtitle {
|
||||
if (playlist.user == null || playlist.user.name == null || playlist.user.name == '' || playlist.user.id == deezerAPI.userId) {
|
||||
if (playlist.trackCount == null) return '';
|
||||
return '${playlist.trackCount} ' + 'Tracks'.i18n;
|
||||
String? get subtitle {
|
||||
if (playlist!.user == null ||
|
||||
playlist!.user!.name == null ||
|
||||
playlist!.user!.name == '' ||
|
||||
playlist!.user!.id == deezerAPI.userId) {
|
||||
if (playlist!.trackCount == null) return '';
|
||||
return '${playlist!.trackCount} ' + 'Tracks'.i18n;
|
||||
}
|
||||
return playlist.user.name;
|
||||
return playlist!.user!.name;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
title: Text(
|
||||
playlist.title,
|
||||
playlist!.title!,
|
||||
maxLines: 1,
|
||||
),
|
||||
subtitle: Text(
|
||||
subtitle,
|
||||
subtitle!,
|
||||
maxLines: 1,
|
||||
),
|
||||
leading: CachedImage(
|
||||
url: playlist.image.thumb,
|
||||
url: playlist!.image!.thumb,
|
||||
width: 48,
|
||||
),
|
||||
onTap: onTap,
|
||||
onLongPress: onHold,
|
||||
onTap: onTap as void Function()?,
|
||||
onLongPress: onHold as void Function()?,
|
||||
trailing: trailing,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ArtistHorizontalTile extends StatelessWidget {
|
||||
|
||||
final Artist artist;
|
||||
final Function onTap;
|
||||
final Function onHold;
|
||||
final Widget trailing;
|
||||
final Artist? artist;
|
||||
final Function? onTap;
|
||||
final Function? onHold;
|
||||
final Widget? trailing;
|
||||
|
||||
ArtistHorizontalTile(this.artist, {this.onHold, this.onTap, this.trailing});
|
||||
|
||||
|
@ -238,15 +237,15 @@ class ArtistHorizontalTile extends StatelessWidget {
|
|||
padding: EdgeInsets.symmetric(vertical: 2.0),
|
||||
child: ListTile(
|
||||
title: Text(
|
||||
artist.name,
|
||||
artist!.name!,
|
||||
maxLines: 1,
|
||||
),
|
||||
leading: CachedImage(
|
||||
url: artist.picture.thumb,
|
||||
url: artist!.picture!.thumb,
|
||||
circular: true,
|
||||
),
|
||||
onTap: onTap,
|
||||
onLongPress: onHold,
|
||||
onTap: onTap as void Function()?,
|
||||
onLongPress: onHold as void Function()?,
|
||||
trailing: trailing,
|
||||
),
|
||||
);
|
||||
|
@ -254,55 +253,53 @@ class ArtistHorizontalTile extends StatelessWidget {
|
|||
}
|
||||
|
||||
class PlaylistCardTile extends StatelessWidget {
|
||||
|
||||
final Playlist playlist;
|
||||
final Function onTap;
|
||||
final Function onHold;
|
||||
final Playlist? playlist;
|
||||
final Function? onTap;
|
||||
final Function? onHold;
|
||||
PlaylistCardTile(this.playlist, {this.onTap, this.onHold});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: 180.0,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
onLongPress: onHold,
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: EdgeInsets.all(8),
|
||||
child: CachedImage(
|
||||
url: playlist.image.thumb,
|
||||
width: 128,
|
||||
height: 128,
|
||||
rounded: true,
|
||||
height: 180.0,
|
||||
child: InkWell(
|
||||
onTap: onTap as void Function()?,
|
||||
onLongPress: onHold as void Function()?,
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: EdgeInsets.all(8),
|
||||
child: CachedImage(
|
||||
url: playlist!.image!.thumb,
|
||||
width: 128,
|
||||
height: 128,
|
||||
rounded: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(height: 2.0),
|
||||
Container(
|
||||
width: 144,
|
||||
child: Text(
|
||||
playlist.title,
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(fontSize: 14.0),
|
||||
Container(height: 2.0),
|
||||
Container(
|
||||
width: 144,
|
||||
child: Text(
|
||||
playlist!.title!,
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(fontSize: 14.0),
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(height: 4.0,)
|
||||
],
|
||||
),
|
||||
)
|
||||
);
|
||||
Container(
|
||||
height: 4.0,
|
||||
)
|
||||
],
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class SmartTrackListTile extends StatelessWidget {
|
||||
|
||||
final SmartTrackList smartTrackList;
|
||||
final Function onTap;
|
||||
final Function onHold;
|
||||
final SmartTrackList? smartTrackList;
|
||||
final Function? onTap;
|
||||
final Function? onHold;
|
||||
SmartTrackListTile(this.smartTrackList, {this.onHold, this.onTap});
|
||||
|
||||
@override
|
||||
|
@ -310,58 +307,56 @@ class SmartTrackListTile extends StatelessWidget {
|
|||
return Container(
|
||||
height: 210.0,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
onLongPress: onHold,
|
||||
onTap: onTap as void Function()?,
|
||||
onLongPress: onHold as void Function()?,
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: Stack(
|
||||
children: [
|
||||
CachedImage(
|
||||
width: 128,
|
||||
height: 128,
|
||||
url: smartTrackList.cover.thumb,
|
||||
rounded: true,
|
||||
),
|
||||
Container(
|
||||
width: 128.0,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8.0, vertical: 6.0),
|
||||
child: Text(
|
||||
smartTrackList.title,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 18.0,
|
||||
shadows: [
|
||||
Shadow(
|
||||
offset: Offset(1, 1),
|
||||
blurRadius: 2,
|
||||
color: Colors.black
|
||||
)
|
||||
],
|
||||
color: Colors.white
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: Stack(
|
||||
children: [
|
||||
CachedImage(
|
||||
width: 128,
|
||||
height: 128,
|
||||
url: smartTrackList!.cover!.thumb,
|
||||
rounded: true,
|
||||
),
|
||||
Container(
|
||||
width: 128.0,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 8.0, vertical: 6.0),
|
||||
child: Text(
|
||||
smartTrackList!.title!,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 18.0,
|
||||
shadows: [
|
||||
Shadow(
|
||||
offset: Offset(1, 1),
|
||||
blurRadius: 2,
|
||||
color: Colors.black)
|
||||
],
|
||||
color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
),
|
||||
)
|
||||
],
|
||||
)),
|
||||
Container(
|
||||
width: 144.0,
|
||||
child: Text(
|
||||
smartTrackList.subtitle,
|
||||
smartTrackList!.subtitle!,
|
||||
maxLines: 3,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 14.0
|
||||
),
|
||||
style: TextStyle(fontSize: 14.0),
|
||||
),
|
||||
),
|
||||
Container(height: 8.0,)
|
||||
Container(
|
||||
height: 8.0,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -370,73 +365,70 @@ class SmartTrackListTile extends StatelessWidget {
|
|||
}
|
||||
|
||||
class AlbumCard extends StatelessWidget {
|
||||
|
||||
final Album album;
|
||||
final Function onTap;
|
||||
final Function onHold;
|
||||
final Album? album;
|
||||
final Function? onTap;
|
||||
final Function? onHold;
|
||||
|
||||
AlbumCard(this.album, {this.onTap, this.onHold});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
onLongPress: onHold,
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: CachedImage(
|
||||
child: InkWell(
|
||||
onTap: onTap as void Function()?,
|
||||
onLongPress: onHold as void Function()?,
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: CachedImage(
|
||||
width: 128.0,
|
||||
height: 128.0,
|
||||
url: album.art.thumb,
|
||||
rounded: true
|
||||
),
|
||||
url: album!.art!.thumb,
|
||||
rounded: true),
|
||||
),
|
||||
Container(
|
||||
width: 144.0,
|
||||
child: Text(
|
||||
album!.title!,
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(fontSize: 14.0),
|
||||
),
|
||||
Container(
|
||||
width: 144.0,
|
||||
child: Text(
|
||||
album.title,
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 14.0
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(height: 4.0),
|
||||
Container(
|
||||
width: 144.0,
|
||||
child: Text(
|
||||
album.artistString,
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
),
|
||||
Container(height: 4.0),
|
||||
Container(
|
||||
width: 144.0,
|
||||
child: Text(
|
||||
album!.artistString,
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 12.0,
|
||||
color: (Theme.of(context).brightness == Brightness.light) ? Colors.grey[800] : Colors.white70
|
||||
),
|
||||
),
|
||||
color: (Theme.of(context).brightness == Brightness.light)
|
||||
? Colors.grey[800]
|
||||
: Colors.white70),
|
||||
),
|
||||
Container(height: 8.0,)
|
||||
],
|
||||
),
|
||||
)
|
||||
);
|
||||
),
|
||||
Container(
|
||||
height: 8.0,
|
||||
)
|
||||
],
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
class ChannelTile extends StatelessWidget {
|
||||
|
||||
final DeezerChannel channel;
|
||||
final Function onTap;
|
||||
final DeezerChannel? channel;
|
||||
final Function? onTap;
|
||||
ChannelTile(this.channel, {this.onTap});
|
||||
|
||||
Color _textColor() {
|
||||
double luminance = channel.backgroundColor.computeLuminance();
|
||||
return (luminance>0.5)?Colors.black:Colors.white;
|
||||
double luminance = channel!.backgroundColor.computeLuminance();
|
||||
return (luminance > 0.5) ? Colors.black : Colors.white;
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -444,37 +436,34 @@ class ChannelTile extends StatelessWidget {
|
|||
return Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 4.0),
|
||||
child: Card(
|
||||
color: channel.backgroundColor,
|
||||
child: InkWell(
|
||||
onTap: this.onTap,
|
||||
child: Container(
|
||||
width: 150,
|
||||
height: 75,
|
||||
child: Center(
|
||||
child: Text(
|
||||
channel.title,
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 18.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: _textColor()
|
||||
color: channel!.backgroundColor,
|
||||
child: InkWell(
|
||||
onTap: this.onTap as void Function()?,
|
||||
child: Container(
|
||||
width: 150,
|
||||
height: 75,
|
||||
child: Center(
|
||||
child: Text(
|
||||
channel!.title!,
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 18.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: _textColor()),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
),
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ShowCard extends StatelessWidget {
|
||||
|
||||
final Show show;
|
||||
final Function onTap;
|
||||
final Function onHold;
|
||||
final Show? show;
|
||||
final Function? onTap;
|
||||
final Function? onHold;
|
||||
|
||||
ShowCard(this.show, {this.onTap, this.onHold});
|
||||
|
||||
|
@ -482,15 +471,15 @@ class ShowCard extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
onLongPress: onHold,
|
||||
onTap: onTap as void Function()?,
|
||||
onLongPress: onHold as void Function()?,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: CachedImage(
|
||||
url: show.art.thumb,
|
||||
url: show!.art!.thumb,
|
||||
width: 128.0,
|
||||
height: 128.0,
|
||||
rounded: true,
|
||||
|
@ -499,13 +488,11 @@ class ShowCard extends StatelessWidget {
|
|||
Container(
|
||||
width: 144.0,
|
||||
child: Text(
|
||||
show.name,
|
||||
show!.name!,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 14.0
|
||||
),
|
||||
style: TextStyle(fontSize: 14.0),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -516,10 +503,9 @@ class ShowCard extends StatelessWidget {
|
|||
}
|
||||
|
||||
class ShowTile extends StatelessWidget {
|
||||
|
||||
final Show show;
|
||||
final Function onTap;
|
||||
final Function onHold;
|
||||
final Function? onTap;
|
||||
final Function? onHold;
|
||||
|
||||
ShowTile(this.show, {this.onTap, this.onHold});
|
||||
|
||||
|
@ -527,56 +513,57 @@ class ShowTile extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
title: Text(
|
||||
show.name,
|
||||
show.name!,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
subtitle: Text(
|
||||
show.description,
|
||||
show.description!,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
onTap: onTap,
|
||||
onLongPress: onHold,
|
||||
onTap: onTap as void Function()?,
|
||||
onLongPress: onHold as void Function()?,
|
||||
leading: CachedImage(
|
||||
url: show.art.thumb,
|
||||
url: show.art!.thumb,
|
||||
width: 48,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class ShowEpisodeTile extends StatelessWidget {
|
||||
|
||||
final ShowEpisode episode;
|
||||
final Function onTap;
|
||||
final Function onHold;
|
||||
final Widget trailing;
|
||||
final Function? onTap;
|
||||
final Function? onHold;
|
||||
final Widget? trailing;
|
||||
|
||||
ShowEpisodeTile(this.episode, {this.onTap, this.onHold, this.trailing});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onLongPress: onHold,
|
||||
onTap: onTap,
|
||||
onLongPress: onHold as void Function()?,
|
||||
onTap: onTap as void Function()?,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(episode.title, maxLines: 2),
|
||||
title: Text(episode.title!, maxLines: 2),
|
||||
trailing: trailing,
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Text(
|
||||
episode.description,
|
||||
episode.description!,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).textTheme.subtitle1.color.withOpacity(0.9)
|
||||
),
|
||||
color: Theme.of(context)
|
||||
.textTheme
|
||||
.subtitle1!
|
||||
.color!
|
||||
.withOpacity(0.9)),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
|
@ -588,10 +575,13 @@ class ShowEpisodeTile extends StatelessWidget {
|
|||
'${episode.publishedDate} | ${episode.durationString}',
|
||||
textAlign: TextAlign.left,
|
||||
style: TextStyle(
|
||||
fontSize: 12.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).textTheme.subtitle1.color.withOpacity(0.6)
|
||||
),
|
||||
fontSize: 12.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context)
|
||||
.textTheme
|
||||
.subtitle1!
|
||||
.color!
|
||||
.withOpacity(0.6)),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:collection/collection.dart' show IterableExtension;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:freezer/api/cache.dart';
|
||||
|
@ -24,9 +25,9 @@ class UpdaterScreen extends StatefulWidget {
|
|||
class _UpdaterScreenState extends State<UpdaterScreen> {
|
||||
bool _loading = true;
|
||||
bool _error = false;
|
||||
FreezerVersions _versions;
|
||||
String _current;
|
||||
String _arch;
|
||||
late FreezerVersions _versions;
|
||||
String? _current;
|
||||
String? _arch;
|
||||
double _progress = 0.0;
|
||||
bool _buttonEnabled = true;
|
||||
|
||||
|
@ -47,34 +48,33 @@ class _UpdaterScreenState extends State<UpdaterScreen> {
|
|||
_loading = false;
|
||||
});
|
||||
} catch (e, st) {
|
||||
print(e + st);
|
||||
print(e.toString() + st.toString());
|
||||
_error = true;
|
||||
_loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
FreezerDownload get _versionDownload {
|
||||
return _versions.versions[0].downloads.firstWhere(
|
||||
(d) => d.version.toLowerCase().contains(_arch.toLowerCase()),
|
||||
orElse: () => null);
|
||||
FreezerDownload? get _versionDownload {
|
||||
return _versions.versions![0].downloads!.firstWhereOrNull(
|
||||
(d) => d.version!.toLowerCase().contains(_arch!.toLowerCase()));
|
||||
}
|
||||
|
||||
Future _download() async {
|
||||
String url = _versionDownload.directUrl;
|
||||
String url = _versionDownload!.directUrl!;
|
||||
//Start request
|
||||
http.Client client = new http.Client();
|
||||
http.StreamedResponse res =
|
||||
await client.send(http.Request('GET', Uri.parse(url)));
|
||||
int size = res.contentLength;
|
||||
int? size = res.contentLength;
|
||||
//Open file
|
||||
String path =
|
||||
p.join((await getExternalStorageDirectory()).path, 'update.apk');
|
||||
p.join((await getExternalStorageDirectory())!.path, 'update.apk');
|
||||
File file = File(path);
|
||||
IOSink fileSink = file.openWrite();
|
||||
//Update progress
|
||||
Future.doWhile(() async {
|
||||
int received = await file.length();
|
||||
setState(() => _progress = received / size);
|
||||
setState(() => _progress = received / size!);
|
||||
return received != size;
|
||||
});
|
||||
//Pipe
|
||||
|
@ -124,14 +124,14 @@ class _UpdaterScreenState extends State<UpdaterScreen> {
|
|||
Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
'New update available!'.i18n + ' ' + _versions.latest,
|
||||
'New update available!'.i18n + ' ' + _versions.latest!,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 20.0, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Current version: ' + _current,
|
||||
'Current version: ' + _current!,
|
||||
style:
|
||||
TextStyle(fontSize: 14.0, fontStyle: FontStyle.italic),
|
||||
),
|
||||
|
@ -146,7 +146,7 @@ class _UpdaterScreenState extends State<UpdaterScreen> {
|
|||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(16, 4, 16, 8),
|
||||
child: Text(
|
||||
_versions.versions[0].changelog,
|
||||
_versions.versions![0].changelog!,
|
||||
style: TextStyle(fontSize: 16.0),
|
||||
),
|
||||
),
|
||||
|
@ -157,7 +157,7 @@ class _UpdaterScreenState extends State<UpdaterScreen> {
|
|||
Column(children: [
|
||||
ElevatedButton(
|
||||
child: Text('Download'.i18n +
|
||||
' (${_versionDownload.version})'),
|
||||
' (${_versionDownload!.version})'),
|
||||
onPressed: _buttonEnabled
|
||||
? () {
|
||||
setState(() => _buttonEnabled = false);
|
||||
|
@ -184,8 +184,8 @@ class _UpdaterScreenState extends State<UpdaterScreen> {
|
|||
}
|
||||
|
||||
class FreezerVersions {
|
||||
String latest;
|
||||
List<FreezerVersion> versions;
|
||||
String? latest;
|
||||
List<FreezerVersion>? versions;
|
||||
|
||||
FreezerVersions({this.latest, this.versions});
|
||||
|
||||
|
@ -218,12 +218,11 @@ class FreezerVersions {
|
|||
if (Version.parse(versions.latest) <= Version.parse(info.version)) return;
|
||||
|
||||
//Get architecture
|
||||
String _arch = await DownloadManager.platform.invokeMethod("arch");
|
||||
String? _arch = await DownloadManager.platform.invokeMethod("arch");
|
||||
if (_arch == 'armv8l') _arch = 'arm32';
|
||||
//Check compatible architecture
|
||||
if (versions.versions[0].downloads.firstWhere(
|
||||
(d) => d.version.toLowerCase().contains(_arch.toLowerCase()),
|
||||
orElse: () => null) ==
|
||||
if (versions.versions![0].downloads!.firstWhereOrNull(
|
||||
(d) => d.version!.toLowerCase().contains(_arch!.toLowerCase())) ==
|
||||
null) return;
|
||||
|
||||
//Show notification
|
||||
|
@ -245,9 +244,9 @@ class FreezerVersions {
|
|||
}
|
||||
|
||||
class FreezerVersion {
|
||||
String version;
|
||||
String changelog;
|
||||
List<FreezerDownload> downloads;
|
||||
String? version;
|
||||
String? changelog;
|
||||
List<FreezerDownload>? downloads;
|
||||
|
||||
FreezerVersion({this.version, this.changelog, this.downloads});
|
||||
|
||||
|
@ -260,8 +259,8 @@ class FreezerVersion {
|
|||
}
|
||||
|
||||
class FreezerDownload {
|
||||
String version;
|
||||
String directUrl;
|
||||
String? version;
|
||||
String? directUrl;
|
||||
|
||||
FreezerDownload({this.version, this.directUrl});
|
||||
|
||||
|
|
73
pubspec.lock
73
pubspec.lock
|
@ -9,7 +9,7 @@ packages:
|
|||
source: hosted
|
||||
version: "24.0.0"
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
dependency: "direct overridden"
|
||||
description:
|
||||
name: analyzer
|
||||
url: "https://pub.dartlang.org"
|
||||
|
@ -28,14 +28,28 @@ packages:
|
|||
name: async
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.7.0"
|
||||
version: "2.8.1"
|
||||
audio_service:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: audio_service
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.17.1"
|
||||
version: "0.18.0-beta.0"
|
||||
audio_service_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: audio_service_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.1.0"
|
||||
audio_service_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: audio_service_web
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.1.0"
|
||||
audio_session:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -203,7 +217,7 @@ packages:
|
|||
name: connectivity_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
version: "2.0.1"
|
||||
convert:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -256,9 +270,11 @@ packages:
|
|||
disk_space:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: disk_space
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
path: "."
|
||||
ref: HEAD
|
||||
resolved-ref: "1cd2555ff2b78ea3cd2667e484bd4f1d35ff6a19"
|
||||
url: "https://github.com/phipps980316/disk_space"
|
||||
source: git
|
||||
version: "0.1.1"
|
||||
draggable_scrollbar:
|
||||
dependency: "direct main"
|
||||
|
@ -276,6 +292,13 @@ packages:
|
|||
url: "https://github.com/gladson97/equalizer.git"
|
||||
source: git
|
||||
version: "0.0.2+2"
|
||||
equatable:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: equatable
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.3"
|
||||
fading_edge_scrollview:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -351,20 +374,13 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "5.3.2"
|
||||
flutter_isolate:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_isolate
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
flutter_local_notifications:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_local_notifications
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "8.1.1+1"
|
||||
version: "8.1.1+2"
|
||||
flutter_local_notifications_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -484,7 +500,7 @@ packages:
|
|||
name: i18n_extension
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
version: "4.1.1"
|
||||
infinite_listview:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -766,7 +782,14 @@ packages:
|
|||
name: quick_actions
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.5.0+1"
|
||||
version: "0.6.0+6"
|
||||
quick_actions_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: quick_actions_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
random_string:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -786,8 +809,8 @@ packages:
|
|||
description:
|
||||
path: "."
|
||||
ref: main
|
||||
resolved-ref: a138aa57796cd1c1b3359d461b49515e58948baa
|
||||
url: "https://github.com/furgoose/Scrobblenaut.git"
|
||||
resolved-ref: d819904911782da678f499fbda300ed69c76e833
|
||||
url: "https://github.com/Pato05/Scrobblenaut.git"
|
||||
source: git
|
||||
version: "3.0.0"
|
||||
share:
|
||||
|
@ -822,7 +845,7 @@ packages:
|
|||
name: source_gen
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.5"
|
||||
version: "1.1.0"
|
||||
source_helper:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -913,14 +936,14 @@ packages:
|
|||
name: test_api
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.4.1"
|
||||
version: "0.4.2"
|
||||
timezone:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: timezone
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.7.0"
|
||||
version: "0.8.0"
|
||||
timing:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -962,7 +985,7 @@ packages:
|
|||
name: url_launcher
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "6.0.5"
|
||||
version: "6.0.9"
|
||||
url_launcher_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -983,7 +1006,7 @@ packages:
|
|||
name: url_launcher_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
version: "2.0.4"
|
||||
url_launcher_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1074,7 +1097,7 @@ packages:
|
|||
name: win32
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.2.7"
|
||||
version: "2.2.8"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
24
pubspec.yaml
24
pubspec.yaml
|
@ -18,15 +18,13 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
|||
version: 0.6.14+1
|
||||
|
||||
environment:
|
||||
sdk: ">=2.8.0 <3.0.0"
|
||||
sdk: '>=2.12.0 <3.0.0'
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
|
||||
flutter_localizations:
|
||||
sdk: flutter
|
||||
|
||||
spotify: ^0.6.0
|
||||
flutter_displaymode: ^0.3.2
|
||||
crypto: ^3.0.0
|
||||
|
@ -48,27 +46,28 @@ dependencies:
|
|||
package_info: ^2.0.2
|
||||
move_to_background: ^1.0.1
|
||||
flutter_local_notifications: ^8.1.1+1
|
||||
collection: ^1.14.12
|
||||
disk_space: ^0.1.1
|
||||
collection: ^1.15.0-nullsafety.4
|
||||
disk_space:
|
||||
git: https://github.com/phipps980316/disk_space
|
||||
random_string: ^2.0.1
|
||||
async: ^2.4.1
|
||||
async: ^2.8.1
|
||||
html: ^0.15.0
|
||||
flutter_screenutil: ^5.0.0+2
|
||||
marquee: ^2.2.0
|
||||
flutter_cache_manager: ^3.0.0
|
||||
cached_network_image: ^3.1.0
|
||||
i18n_extension: ^4.0.0
|
||||
i18n_extension: ^4.1.1
|
||||
fluttericon: ^2.0.0
|
||||
url_launcher: ^6.0.5
|
||||
uni_links: ^0.5.1
|
||||
share: ^2.0.4
|
||||
numberpicker: ^2.1.1
|
||||
quick_actions: ^0.5.0+1
|
||||
quick_actions: ^0.6.0+6
|
||||
photo_view: ^0.12.0
|
||||
draggable_scrollbar: ^0.1.0
|
||||
scrobblenaut:
|
||||
git:
|
||||
url: https://github.com/furgoose/Scrobblenaut.git
|
||||
url: https://github.com/Pato05/Scrobblenaut.git
|
||||
ref: main
|
||||
open_file: ^3.0.3
|
||||
version: ^2.0.0
|
||||
|
@ -76,21 +75,20 @@ dependencies:
|
|||
google_fonts: ^2.1.0
|
||||
equalizer:
|
||||
git: https://github.com/gladson97/equalizer.git
|
||||
|
||||
audio_session: ^0.1.6
|
||||
audio_service: ^0.17.1
|
||||
audio_service: ^0.18.0-beta.0
|
||||
just_audio:
|
||||
git:
|
||||
url: https://github.com/ryanheise/just_audio.git
|
||||
ref: dev
|
||||
path: just_audio/
|
||||
|
||||
# cupertino_icons: ^0.1.3
|
||||
dependency_overrides:
|
||||
analyzer: ^2.0.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
||||
json_serializable: ^5.0.0
|
||||
build_runner: ^2.1.1
|
||||
|
||||
|
|
Loading…
Reference in New Issue