wip: null-safety, audio_service update

This commit is contained in:
pato05 2021-09-01 14:38:32 +02:00
parent 8d53162099
commit 2bd29f4cea
34 changed files with 3717 additions and 3576 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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