From 2bd29f4cea74303515b70adc3a0fe0ac1465888b Mon Sep 17 00:00:00 2001 From: pato05 Date: Wed, 1 Sep 2021 14:38:32 +0200 Subject: [PATCH] wip: null-safety, audio_service update --- .../src/main/java/f/f/freezer/FileUtils.java | 1 + .../main/java/f/f/freezer/MainActivity.java | 10 +- lib/api/cache.dart | 110 +- lib/api/cache.g.dart | 92 +- lib/api/deezer.dart | 100 +- lib/api/definitions.dart | 430 ++--- lib/api/definitions.g.dart | 459 +++--- lib/api/download.dart | 241 +-- lib/api/importer.dart | 113 +- lib/api/player.dart | 548 +++---- lib/api/spotify.dart | 58 +- lib/main.dart | 195 +-- lib/settings.dart | 176 ++- lib/settings.g.dart | 219 +-- lib/ui/android_auto.dart | 76 +- lib/ui/cached_image.dart | 26 +- lib/ui/details_screens.dart | 1403 +++++++++-------- lib/ui/downloads_screen.dart | 244 +-- lib/ui/elements.dart | 39 +- lib/ui/error.dart | 4 +- lib/ui/home_screen.dart | 51 +- lib/ui/importer_screen.dart | 477 +++--- lib/ui/library.dart | 226 +-- lib/ui/login_screen.dart | 41 +- lib/ui/lyrics.dart | 332 ++-- lib/ui/menu.dart | 127 +- lib/ui/player_bar.dart | 220 +-- lib/ui/player_screen.dart | 340 ++-- lib/ui/search.dart | 149 +- lib/ui/settings_screen.dart | 94 +- lib/ui/tiles.dart | 542 ++++--- lib/ui/updater.dart | 53 +- pubspec.lock | 73 +- pubspec.yaml | 24 +- 34 files changed, 3717 insertions(+), 3576 deletions(-) diff --git a/android/app/src/main/java/f/f/freezer/FileUtils.java b/android/app/src/main/java/f/f/freezer/FileUtils.java index d166d7b..2bf40f2 100644 --- a/android/app/src/main/java/f/f/freezer/FileUtils.java +++ b/android/app/src/main/java/f/f/freezer/FileUtils.java @@ -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; diff --git a/android/app/src/main/java/f/f/freezer/MainActivity.java b/android/app/src/main/java/f/f/freezer/MainActivity.java index b2a5cbf..00b2003 100644 --- a/android/app/src/main/java/f/f/freezer/MainActivity.java +++ b/android/app/src/main/java/f/f/freezer/MainActivity.java @@ -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 diff --git a/lib/api/cache.dart b/lib/api/cache.dart index 70816c4..1f2d5e7 100644 --- a/lib/api/cache.dart +++ b/lib/api/cache.dart @@ -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 libraryTracks = []; + List? libraryTracks = []; //Track ID of logged track, to prevent duplicates @JsonKey(ignore: true) - String loggedTrackId; + String? loggedTrackId; @JsonKey(defaultValue: []) List history = []; //All sorting cached @JsonKey(defaultValue: []) - List sorts = []; + List 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 searchHistory; + @JsonKey(name: 'searchHistory2') + List? 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 toJson() => _$CacheToJson(this); //Search History JSON - static List _searchHistoryFromJson(List json) { - return (json ?? []) - .map((i) => _searchHistoryItemFromJson(i)) - .toList(); - } + // static List _searchHistoryFromJson(List? json) { + // return (json ?? []) + // .map((i) => _searchHistoryItemFromJson(i)) + // .toList(); + // } - static SearchHistoryItem _searchHistoryItemFromJson( - Map 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 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> _searchHistoryToJson( - List data) => - (data ?? []) - .map>( - (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 toJson() => _$SearchHistoryItemToJson(this); + factory SearchHistoryItem.fromJson(Map json) => + _$SearchHistoryItemFromJson(json); + + static int _searchHistoryItemTypeToJson(SearchHistoryItemType type) => + type.index; + static SearchHistoryItemType _searchHistoryItemTypeFromJson(int index) => + SearchHistoryItemType.values[index]; } enum SearchHistoryItemType { TRACK, ALBUM, ARTIST, PLAYLIST } diff --git a/lib/api/cache.g.dart b/lib/api/cache.g.dart index d9a2114..700f793 100644 --- a/lib/api/cache.g.dart +++ b/lib/api/cache.g.dart @@ -6,84 +6,42 @@ part of 'cache.dart'; // JsonSerializableGenerator // ************************************************************************** -Cache _$CacheFromJson(Map 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)) - ?.toList() ?? - [] - ..sorts = (json['sorts'] as List) - ?.map((e) => - e == null ? null : Sorting.fromJson(e as Map)) - ?.toList() ?? - [] - ..searchHistory = - Cache._searchHistoryFromJson(json['searchHistory2'] as List) - ..threadsWarning = json['threadsWarning'] as bool ?? false - ..lastUpdateCheck = json['lastUpdateCheck'] as int ?? 0; -} +Cache _$CacheFromJson(Map json) => Cache( + libraryTracks: (json['libraryTracks'] as List?) + ?.map((e) => e as String) + .toList(), + ) + ..history = (json['history'] as List?) + ?.map((e) => Track.fromJson(e as Map)) + .toList() ?? + [] + ..sorts = (json['sorts'] as List?) + ?.map((e) => Sorting.fromJson(e as Map)) + .toList() ?? + [] + ..searchHistory = (json['searchHistory2'] as List?) + ?.map((e) => SearchHistoryItem.fromJson(e as Map)) + .toList() + ..threadsWarning = json['threadsWarning'] as bool? ?? false + ..lastUpdateCheck = json['lastUpdateCheck'] as int? ?? 0; Map _$CacheToJson(Cache instance) => { '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 json) { - return SearchHistoryItem( - json['data'], - _$enumDecodeNullable(_$SearchHistoryItemTypeEnumMap, json['type']), - ); -} +SearchHistoryItem _$SearchHistoryItemFromJson(Map json) => + SearchHistoryItem( + json['data'], + SearchHistoryItem._searchHistoryItemTypeFromJson(json['type'] as int), + ); Map _$SearchHistoryItemToJson(SearchHistoryItem instance) => { 'data': instance.data, - 'type': _$SearchHistoryItemTypeEnumMap[instance.type], + 'type': SearchHistoryItem._searchHistoryItemTypeToJson(instance.type), }; - -T _$enumDecode( - Map 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( - Map enumValues, - dynamic source, { - T unknownValue, -}) { - if (source == null) { - return null; - } - return _$enumDecode(enumValues, source, unknownValue: unknownValue); -} - -const _$SearchHistoryItemTypeEnumMap = { - SearchHistoryItemType.TRACK: 'TRACK', - SearchHistoryItemType.ALBUM: 'ALBUM', - SearchHistoryItemType.ARTIST: 'ARTIST', - SearchHistoryItemType.PLAYLIST: 'PLAYLIST', -}; diff --git a/lib/api/deezer.dart b/lib/api/deezer.dart index fe28222..5911092 100644 --- a/lib/api/deezer.dart +++ b/lib/api/deezer.dart @@ -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 get headers => { @@ -38,7 +38,7 @@ class DeezerAPI { //Call private API Future> callApi(String method, - {Map params, String gatewayInput}) async { + {Map? 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> callPublicApi(String path) async { + Future 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 getArlByEmail(String email, String password) async { + static Future 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 rawAuthorize({Function onError}) async { + Future rawAuthorize({Function? onError}) async { try { Map data = await callApi('deezer.getUserData'); if (data['results']['USER']['USER_ID'] == 0) { @@ -132,12 +132,12 @@ class DeezerAPI { } //URL/Link parser - Future parseLink(String url) async { + Future 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 chceckAvailability() async { + static Future chceckAvailability() async { try { http.Response res = await http.get(Uri.parse('https://api.deezer.com/infos')); @@ -181,13 +181,13 @@ class DeezerAPI { } //Search - Future search(String query) async { + Future search(String? query) async { Map data = await callApi('deezer.pageSearch', params: {'nb': 128, 'query': query, 'start': 0}); return SearchResults.fromPrivateJson(data['results']); } - Future track(String id) async { + Future track(String? id) async { Map data = await callApi('song.getListData', params: { 'sng_ids': [id] }); @@ -195,7 +195,7 @@ class DeezerAPI { } //Get album details, tracks - Future album(String id) async { + Future album(String? id) async { Map data = await callApi('deezer.pageAlbum', params: { 'alb_id': id, 'header': true, @@ -206,7 +206,7 @@ class DeezerAPI { } //Get artist details - Future artist(String id) async { + Future artist(String? id) async { Map 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> playlistTracksPage(String id, int start, + Future?> 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(String id, {int nb = 100}) async { + Future playlist(String? id, {int nb = 100}) async { Map 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 fullPlaylist(String id) async { + Future 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> getArtists() async { + Future?> 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(String trackId) async { + Future 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(String id) async { + Future 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> flow() async { + Future?> flow() async { Map data = await callApi('radio.getUserRadio', params: {'user_id': userId}); return data['results']['data'] .map((json) => Track.fromPrivateJson(json)) @@ -410,7 +410,7 @@ class DeezerAPI { }); } - Future getChannel(String target) async { + Future 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 createPlaylist(String title, - {String description = "", - int status = 1, + Future createPlaylist(String? title, + {String? description = "", + int? status = 1, List trackIds = const []}) async { Map data = await callApi('playlist.create', params: { 'title': title, @@ -484,7 +484,7 @@ class DeezerAPI { } //Get part of discography - Future> discographyPage(String artistId, + Future?> 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 searchSuggestions(String query) async { + Future 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> smartRadio(String artistId) async { + Future?> 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> libraryShuffle({int start = 0}) async { + Future?> 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> playMix(String trackId) async { + Future?> playMix(String? trackId) async { Map data = await callApi('song.getContextualTrackMix', params: { 'sng_ids': [trackId] }); @@ -545,14 +545,14 @@ class DeezerAPI { .toList(); } - Future> allShowEpisodes(String showId) async { + Future?> 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((e) => ShowEpisode.fromPrivateJson(e)) diff --git a/lib/api/definitions.dart b/lib/api/definitions.dart index 0193584..ae764ae 100644 --- a/lib/api/definitions.dart +++ b/lib/api/definitions.dart @@ -16,25 +16,25 @@ part 'definitions.g.dart'; @JsonSerializable() class Track { - String/*!*//*!*/ id; - String/*!*/ title; - Album/*!*/ album; - List/*!*/ artists; - Duration/*!*/ duration; - ImageDetails/*!*/ albumArt; - int trackNumber; - bool/*!*/ offline; - Lyrics lyrics; - bool favorite; - int diskNumber; - bool explicit; + String id; + String? title; + Album? album; + List? 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 playbackDetails; + List? 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((art) => art.name).join(', '); + String get artistString => + artists!.map((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((art) => art.toJson()).toList()) + this.artists!.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 artists = [Artist(name: mi.displaySubtitle ?? mi.artist)]; + List? 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((j) => Artist.fromJson(j)) .toList(); } } - List playbackDetails; - if (mi.extras['playbackDetails'] != null) - playbackDetails = (jsonDecode(mi.extras['playbackDetails']) ?? []) + List? playbackDetails; + if (mi.extras!['playbackDetails'] != null) + playbackDetails = (jsonDecode(mi.extras!['playbackDetails']) ?? []) .map((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 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 toSQL({off = false}) => { 'id': id, 'title': title, - 'album': album.id, - 'artists': artists.map((dynamic a) => a.id).join(','), - 'duration': duration.inSeconds, - 'albumArt': albumArt.full, + 'album': album!.id, + 'artists': artists!.map((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 artists; - List 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? artists; + List? 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((art) => art.name).join(', '); + String get artistString => + artists!.map((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 toSQL({off = false}) => { 'id': id, 'title': title, - 'artists': (artists ?? []).map((dynamic a) => a.id).join(','), - 'tracks': (tracks ?? []).map((dynamic t) => t.id).join(','), + 'artists': (artists ?? []).map((dynamic a) => a.id).join(','), + 'tracks': (tracks ?? []).map((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 json) { + static ArtistHighlight? fromPrivateJson(Map? 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 albums; - int albumCount; - List topTracks; - ImageDetails picture; - int fans; - bool offline; - bool library; - bool radio; - String favoriteDate; - ArtistHighlight highlight; + String? id; + String? name; + List? albums; + int? albumCount; + List? 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 json, {Map albumsJson = const {}, Map topJson = const {}, - Map highlight, + Map? highlight, bool library = false}) { //Get wether radio is available bool _radio = false; @@ -349,14 +351,14 @@ class Artist { Map toSQL({off = false}) => { 'id': id, 'name': name, - 'albums': albums.map((dynamic a) => a.id).join(','), - 'topTracks': topTracks.map((dynamic t) => t.id).join(','), - 'picture': picture.full, + 'albums': albums!.map((dynamic a) => a.id).join(','), + 'topTracks': topTracks!.map((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 data) => Artist( @@ -381,16 +383,16 @@ class Artist { @JsonSerializable() class Playlist { - String id; - String title; - List tracks; - ImageDetails image; - Duration duration; - int trackCount; - User user; - int fans; - bool library; - String description; + String? id; + String? title; + List? 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 json, @@ -432,11 +434,11 @@ class Playlist { Map toSQL() => { 'id': id, 'title': title, - 'tracks': tracks.map((dynamic t) => t.id).join(','), - 'image': image.full, - 'duration': duration.inSeconds, - 'userId': user.id, - 'userName': user.name, + 'tracks': tracks!.map((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 tracks; - List albums; - List artists; - List playlists; - List shows; - List episodes; + List? tracks; + List? albums; + List? artists; + List? playlists; + List? shows; + List? 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 json) => @@ -551,9 +554,9 @@ class SearchResults { @JsonSerializable() class Lyrics { - String id; - String writers; - List lyrics; + String? id; + String? writers; + List? lyrics; Lyrics({this.id, this.writers, this.lyrics}); @@ -572,7 +575,7 @@ class Lyrics { .map((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 tracks; - ImageDetails cover; + String? id; + String? title; + String? subtitle; + String? description; + int? trackCount; + List? tracks; + ImageDetails? cover; SmartTrackList( {this.id, @@ -656,7 +659,7 @@ class SmartTrackList { @JsonSerializable() class HomePage { - List sections; + List? sections; HomePage({this.sections}); @@ -666,7 +669,7 @@ class HomePage { return p.join(d.path, 'homescreen.json'); } - Future exists() async { + Future exists() async { String path = await _getPath(); return await File(path).exists(); } @@ -679,7 +682,7 @@ class HomePage { Future load() async { String path = await _getPath(); Map data = jsonDecode(await File(path).readAsString()); - return HomePage.fromJson(data); + return HomePage.fromJson(data as Map); } 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 items; + List? items; HomePageSection( {this.layout, this.items, this.title, this.pagePath, this.hasMore}); //JSON - factory HomePageSection.fromPrivateJson(Map json) { + static HomePageSection? fromPrivateJson(Map 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 json) { - String type = json['type']; + static HomePageItem? fromPrivateJson(Map json) { + String? type = json['type']; switch (type) { //Smart Track List case 'flow': @@ -797,7 +800,7 @@ class HomePageItem { } factory HomePageItem.fromJson(Map 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 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 json) => _$DeezerChannelFromJson(json); Map 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 on List { } double hypot(num c1, num c2) => sqrt(pow(c1.abs(), 2) + pow(c2.abs(), 2)); + +Map 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 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, + album: json['album'], + artist: json['artist'], + displayTitle: json['displayTitle'], + displaySubtitle: json['displaySubtitle'], + displayDescription: json['displayDescription'], + ); diff --git a/lib/api/definitions.g.dart b/lib/api/definitions.g.dart index ab0830a..971751f 100644 --- a/lib/api/definitions.g.dart +++ b/lib/api/definitions.g.dart @@ -6,35 +6,32 @@ part of 'definitions.dart'; // JsonSerializableGenerator // ************************************************************************** -Track _$TrackFromJson(Map 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), - playbackDetails: json['playbackDetails'] as List, - albumArt: json['albumArt'] == null - ? null - : ImageDetails.fromJson(json['albumArt'] as Map), - artists: (json['artists'] as List) - ?.map((e) => - e == null ? null : Artist.fromJson(e as Map)) - ?.toList(), - trackNumber: json['trackNumber'] as int, - offline: json['offline'] as bool, - lyrics: json['lyrics'] == null - ? null - : Lyrics.fromJson(json['lyrics'] as Map), - favorite: json['favorite'] as bool, - diskNumber: json['diskNumber'] as int, - explicit: json['explicit'] as bool, - addedDate: json['addedDate'] as int, - ); -} +Track _$TrackFromJson(Map 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), + playbackDetails: json['playbackDetails'] as List?, + albumArt: json['albumArt'] == null + ? null + : ImageDetails.fromJson(json['albumArt'] as Map), + artists: (json['artists'] as List?) + ?.map((e) => Artist.fromJson(e as Map)) + .toList(), + trackNumber: json['trackNumber'] as int?, + offline: json['offline'] as bool?, + lyrics: json['lyrics'] == null + ? null + : Lyrics.fromJson(json['lyrics'] as Map), + favorite: json['favorite'] as bool?, + diskNumber: json['diskNumber'] as int?, + explicit: json['explicit'] as bool?, + addedDate: json['addedDate'] as int?, + ); Map _$TrackToJson(Track instance) => { 'id': instance.id, @@ -53,29 +50,25 @@ Map _$TrackToJson(Track instance) => { 'playbackDetails': instance.playbackDetails, }; -Album _$AlbumFromJson(Map json) { - return Album( - id: json['id'] as String, - title: json['title'] as String, - art: json['art'] == null - ? null - : ImageDetails.fromJson(json['art'] as Map), - artists: (json['artists'] as List) - ?.map((e) => - e == null ? null : Artist.fromJson(e as Map)) - ?.toList(), - tracks: (json['tracks'] as List) - ?.map( - (e) => e == null ? null : Track.fromJson(e as Map)) - ?.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 json) => Album( + id: json['id'] as String?, + title: json['title'] as String?, + art: json['art'] == null + ? null + : ImageDetails.fromJson(json['art'] as Map), + artists: (json['artists'] as List?) + ?.map((e) => Artist.fromJson(e as Map)) + .toList(), + tracks: (json['tracks'] as List?) + ?.map((e) => Track.fromJson(e as Map)) + .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 _$AlbumToJson(Album instance) => { 'id': instance.id, @@ -91,36 +84,41 @@ Map _$AlbumToJson(Album instance) => { 'favoriteDate': instance.favoriteDate, }; -T _$enumDecode( - Map enumValues, - dynamic source, { - T unknownValue, +K _$enumDecode( + Map 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( - Map enumValues, +K? _$enumDecodeNullable( + Map enumValues, dynamic source, { - T unknownValue, + K? unknownValue, }) { if (source == null) { return null; } - return _$enumDecode(enumValues, source, unknownValue: unknownValue); + return _$enumDecode(enumValues, source, unknownValue: unknownValue); } const _$AlbumTypeEnumMap = { @@ -129,13 +127,12 @@ const _$AlbumTypeEnumMap = { AlbumType.FEATURED: 'FEATURED', }; -ArtistHighlight _$ArtistHighlightFromJson(Map json) { - return ArtistHighlight( - data: json['data'], - type: _$enumDecodeNullable(_$ArtistHighlightTypeEnumMap, json['type']), - title: json['title'] as String, - ); -} +ArtistHighlight _$ArtistHighlightFromJson(Map json) => + ArtistHighlight( + data: json['data'], + type: _$enumDecodeNullable(_$ArtistHighlightTypeEnumMap, json['type']), + title: json['title'] as String?, + ); Map _$ArtistHighlightToJson(ArtistHighlight instance) => { @@ -148,32 +145,28 @@ const _$ArtistHighlightTypeEnumMap = { ArtistHighlightType.ALBUM: 'ALBUM', }; -Artist _$ArtistFromJson(Map 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)) - ?.toList(), - albumCount: json['albumCount'] as int, - topTracks: (json['topTracks'] as List) - ?.map( - (e) => e == null ? null : Track.fromJson(e as Map)) - ?.toList(), - picture: json['picture'] == null - ? null - : ImageDetails.fromJson(json['picture'] as Map), - 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), - ); -} +Artist _$ArtistFromJson(Map json) => Artist( + id: json['id'] as String?, + name: json['name'] as String?, + albums: (json['albums'] as List?) + ?.map((e) => Album.fromJson(e as Map)) + .toList(), + albumCount: json['albumCount'] as int?, + topTracks: (json['topTracks'] as List?) + ?.map((e) => Track.fromJson(e as Map)) + .toList(), + picture: json['picture'] == null + ? null + : ImageDetails.fromJson(json['picture'] as Map), + 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), + ); Map _$ArtistToJson(Artist instance) => { 'id': instance.id, @@ -190,29 +183,26 @@ Map _$ArtistToJson(Artist instance) => { 'highlight': instance.highlight, }; -Playlist _$PlaylistFromJson(Map 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)) - ?.toList(), - image: json['image'] == null - ? null - : ImageDetails.fromJson(json['image'] as Map), - 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), - fans: json['fans'] as int, - library: json['library'] as bool, - description: json['description'] as String, - ); -} +Playlist _$PlaylistFromJson(Map json) => Playlist( + id: json['id'] as String?, + title: json['title'] as String?, + tracks: (json['tracks'] as List?) + ?.map((e) => Track.fromJson(e as Map)) + .toList(), + image: json['image'] == null + ? null + : ImageDetails.fromJson(json['image'] as Map), + 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), + fans: json['fans'] as int?, + library: json['library'] as bool?, + description: json['description'] as String?, + ); Map _$PlaylistToJson(Playlist instance) => { 'id': instance.id, @@ -227,15 +217,13 @@ Map _$PlaylistToJson(Playlist instance) => { 'description': instance.description, }; -User _$UserFromJson(Map json) { - return User( - id: json['id'] as String, - name: json['name'] as String, - picture: json['picture'] == null - ? null - : ImageDetails.fromJson(json['picture'] as Map), - ); -} +User _$UserFromJson(Map json) => User( + id: json['id'] as String?, + name: json['name'] as String?, + picture: json['picture'] == null + ? null + : ImageDetails.fromJson(json['picture'] as Map), + ); Map _$UserToJson(User instance) => { 'id': instance.id, @@ -243,12 +231,10 @@ Map _$UserToJson(User instance) => { 'picture': instance.picture, }; -ImageDetails _$ImageDetailsFromJson(Map json) { - return ImageDetails( - fullUrl: json['fullUrl'] as String, - thumbUrl: json['thumbUrl'] as String, - ); -} +ImageDetails _$ImageDetailsFromJson(Map json) => ImageDetails( + fullUrl: json['fullUrl'] as String?, + thumbUrl: json['thumbUrl'] as String?, + ); Map _$ImageDetailsToJson(ImageDetails instance) => { @@ -256,16 +242,13 @@ Map _$ImageDetailsToJson(ImageDetails instance) => 'thumbUrl': instance.thumbUrl, }; -Lyrics _$LyricsFromJson(Map 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)) - ?.toList(), - ); -} +Lyrics _$LyricsFromJson(Map json) => Lyrics( + id: json['id'] as String?, + writers: json['writers'] as String?, + lyrics: (json['lyrics'] as List?) + ?.map((e) => Lyric.fromJson(e as Map)) + .toList(), + ); Map _$LyricsToJson(Lyrics instance) => { 'id': instance.id, @@ -273,15 +256,13 @@ Map _$LyricsToJson(Lyrics instance) => { 'lyrics': instance.lyrics, }; -Lyric _$LyricFromJson(Map 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 json) => Lyric( + offset: json['offset'] == null + ? null + : Duration(microseconds: json['offset'] as int), + text: json['text'] as String?, + lrcTimestamp: json['lrcTimestamp'] as String?, + ); Map _$LyricToJson(Lyric instance) => { 'offset': instance.offset?.inMicroseconds, @@ -289,13 +270,11 @@ Map _$LyricToJson(Lyric instance) => { 'lrcTimestamp': instance.lrcTimestamp, }; -QueueSource _$QueueSourceFromJson(Map json) { - return QueueSource( - id: json['id'] as String, - text: json['text'] as String, - source: json['source'] as String, - ); -} +QueueSource _$QueueSourceFromJson(Map json) => QueueSource( + id: json['id'] as String?, + text: json['text'] as String?, + source: json['source'] as String?, + ); Map _$QueueSourceToJson(QueueSource instance) => { @@ -304,22 +283,20 @@ Map _$QueueSourceToJson(QueueSource instance) => 'source': instance.source, }; -SmartTrackList _$SmartTrackListFromJson(Map 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)) - ?.toList(), - cover: json['cover'] == null - ? null - : ImageDetails.fromJson(json['cover'] as Map), - subtitle: json['subtitle'] as String, - ); -} +SmartTrackList _$SmartTrackListFromJson(Map 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?) + ?.map((e) => Track.fromJson(e as Map)) + .toList(), + cover: json['cover'] == null + ? null + : ImageDetails.fromJson(json['cover'] as Map), + subtitle: json['subtitle'] as String?, + ); Map _$SmartTrackListToJson(SmartTrackList instance) => { @@ -332,30 +309,25 @@ Map _$SmartTrackListToJson(SmartTrackList instance) => 'cover': instance.cover, }; -HomePage _$HomePageFromJson(Map json) { - return HomePage( - sections: (json['sections'] as List) - ?.map((e) => e == null - ? null - : HomePageSection.fromJson(e as Map)) - ?.toList(), - ); -} +HomePage _$HomePageFromJson(Map json) => HomePage( + sections: (json['sections'] as List?) + ?.map((e) => HomePageSection.fromJson(e as Map)) + .toList(), + ); Map _$HomePageToJson(HomePage instance) => { 'sections': instance.sections, }; -HomePageSection _$HomePageSectionFromJson(Map 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 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 _$HomePageSectionToJson(HomePageSection instance) => { @@ -371,15 +343,15 @@ const _$HomePageSectionLayoutEnumMap = { HomePageSectionLayout.GRID: 'GRID', }; -DeezerChannel _$DeezerChannelFromJson(Map 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 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 _$DeezerChannelToJson(DeezerChannel instance) => { @@ -389,15 +361,14 @@ Map _$DeezerChannelToJson(DeezerChannel instance) => 'backgroundColor': DeezerChannel._colorToJson(instance.backgroundColor), }; -Sorting _$SortingFromJson(Map 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 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 _$SortingToJson(Sorting instance) => { 'type': _$SortTypeEnumMap[instance.type], @@ -426,16 +397,14 @@ const _$SortSourceTypesEnumMap = { SortSourceTypes.PLAYLIST: 'PLAYLIST', }; -Show _$ShowFromJson(Map json) { - return Show( - name: json['name'] as String, - description: json['description'] as String, - art: json['art'] == null - ? null - : ImageDetails.fromJson(json['art'] as Map), - id: json['id'] as String, - ); -} +Show _$ShowFromJson(Map json) => Show( + name: json['name'] as String?, + description: json['description'] as String?, + art: json['art'] == null + ? null + : ImageDetails.fromJson(json['art'] as Map), + id: json['id'] as String?, + ); Map _$ShowToJson(Show instance) => { 'name': instance.name, @@ -444,21 +413,19 @@ Map _$ShowToJson(Show instance) => { 'id': instance.id, }; -ShowEpisode _$ShowEpisodeFromJson(Map 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), - ); -} +ShowEpisode _$ShowEpisodeFromJson(Map 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), + ); Map _$ShowEpisodeToJson(ShowEpisode instance) => { diff --git a/lib/api/download.dart b/lib/api/download.dart index 3d2faa7..6a64654 100644 --- a/lib/api/download.dart +++ b/lib/api/download.dart @@ -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> getDownloads() async { - List raw = await platform.invokeMethod('getDownloads'); + List raw = await (platform.invokeMethod('getDownloads') + as FutureOr>); 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 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); 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); 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); 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); } await b.commit(); } //Create downloads List 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); 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); //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 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 getOfflineTrack(String id, - {Album album, List artists}) async { + Future getOfflineTrack(String? id, + {Album? album, List? 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 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> getOfflineTracks() async { + Future> getOfflineTracks() async { List rawTracks = await db.query('Tracks', where: 'library == 1 AND offline == 1', columns: ['id']); - List out = []; + List out = []; //Load track meta individually - for (Map rawTrack in rawTracks) { + for (Map rawTrack in rawTracks as Iterable>) { out.add(await getOfflineTrack(rawTrack['id'])); } return out; } //Get all offline available tracks - Future> allOfflineTracks() async { + Future> allOfflineTracks() async { List rawTracks = await db.query('Tracks', where: 'offline == 1', columns: ['id']); - List out = []; + List out = []; //Load track meta individually - for (Map rawTrack in rawTracks) { + for (Map rawTrack in rawTracks as Iterable>) { out.add(await getOfflineTrack(rawTrack['id'])); } return out; @@ -348,7 +349,7 @@ class DownloadManager { await db.query('Albums', where: 'offline == 1', columns: ['id']); List out = []; //Load each album - for (Map rawAlbum in rawAlbums) { + for (Map rawAlbum in rawAlbums as Iterable>) { out.add(await getOfflineAlbum(rawAlbum['id'])); } return out; @@ -358,20 +359,20 @@ class DownloadManager { Future 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 tracks = []; + List 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 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 getOfflineArtist(String id) async { + Future 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> getOfflinePlaylists() async { + Future> getOfflinePlaylists() async { List rawPlaylists = await db.query('Playlists', columns: ['id']); - List out = []; - for (Map rawPlaylist in rawPlaylists) { + List out = []; + for (Map rawPlaylist in rawPlaylists as Iterable>) { out.add(await getPlaylist(rawPlaylist['id'])); } return out; } //Get offline playlist - Future getPlaylist(String id) async { + Future 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 tracks = []; - for (Track t in playlist.tracks) { - tracks.add(await getOfflineTrack(t.id)); + List tracks = []; + for (Track? t in playlist.tracks!) { + tracks.add(await getOfflineTrack(t!.id)); } playlist.tracks = tracks; return playlist; } - Future removeOfflineTracks(List tracks) async { - for (Track t in tracks) { + Future removeOfflineTracks(List 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 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 search(String query) async { + Future 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>) { + 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>) { + 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>) { + 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> 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); //Used space List 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 getDirectory(String title) => + static Future getDirectory(String title) => platform.invokeMethod('getDirectory', {'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 jsonFromTrack(Track t, String path, - {private = true, AudioQuality quality}) async { + static Future 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 }; } } diff --git a/lib/api/importer.dart b/lib/api/importer.dart index 2afb72e..e4528c2 100644 --- a/lib/api/importer.dart +++ b/lib/api/importer.dart @@ -12,35 +12,42 @@ class Importer { bool download = false; //Preserve context - BuildContext context; - String title; - String description; - List tracks; - String playlistId; - Playlist playlist; + BuildContext? context; + String? title; + String? description; + late List 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 start(BuildContext context, String title, String description, List tracks) async { + Future start(BuildContext context, String? title, String? description, + List 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 _searchTrack(ImporterTrack track) async { + Future _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)) { 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 a, List b) { + bool _matchArtists(List a, List b) { //Clean List _a = a.map(_cleanArtist).toList(); List _b = b.map(_cleanArtist).toList(); @@ -139,33 +153,32 @@ class Importer { } return false; } - } class ImporterTrack { - String title; - List artists; - String isrc; + String? title; + List? 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); } } -} \ No newline at end of file +} diff --git a/lib/api/player.dart b/lib/api/player.dart index af68f51..254f333 100644 --- a/lib/api/player.dart +++ b/lib/api/player.dart @@ -10,6 +10,7 @@ import 'package:connectivity/connectivity.dart'; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; import 'package:freezer/translations.i18n.dart'; +import 'package:collection/collection.dart'; import 'package:scrobblenaut/scrobblenaut.dart'; import 'definitions.dart'; @@ -18,19 +19,19 @@ import '../settings.dart'; import 'dart:io'; import 'dart:async'; import 'dart:convert'; -import 'dart:math'; PlayerHelper playerHelper = PlayerHelper(); +late AudioHandler audioHandler; class PlayerHelper { - StreamSubscription _customEventSubscription; - StreamSubscription _mediaItemSubscription; - StreamSubscription _playbackStateStreamSubscription; - QueueSource queueSource; + late StreamSubscription _customEventSubscription; + late StreamSubscription _mediaItemSubscription; + late StreamSubscription _playbackStateStreamSubscription; + QueueSource? queueSource; LoopMode repeatType = LoopMode.off; - Timer _timer; - int audioSession; - int _prevAudioSession; + Timer? _timer; + int? audioSession; + int? _prevAudioSession; bool equalizerOpen = false; //Visualizer @@ -38,21 +39,18 @@ class PlayerHelper { Stream get visualizerStream => _visualizerController.stream; //Find queue index by id - int get queueIndex => AudioService.queue == null - ? 0 - : AudioService.queue - .indexWhere((mi) => mi.id == AudioService.currentMediaItem?.id); + int get queueIndex => audioHandler.queue.value + .indexWhere((mi) => mi.id == audioHandler.mediaItem.value?.id); Future start() async { //Subscribe to custom events - _customEventSubscription = - AudioService.customEventStream.listen((event) async { + _customEventSubscription = audioHandler.customEvent.listen((event) async { if (!(event is Map)) return; switch (event['action']) { case 'onLoad': //After audio_service is loaded, load queue, set quality await settings.updateAudioServiceQuality(); - await AudioService.customAction('load'); + await audioHandler.customAction('load', {}); await authorizeLastFM(); break; case 'onRestore': @@ -68,15 +66,15 @@ class PlayerHelper { case 'screenAndroidAuto': AndroidAuto androidAuto = AndroidAuto(); List data = await androidAuto.getScreen(event['id']); - await AudioService.customAction( - 'screenAndroidAuto', jsonEncode(data)); + await audioHandler + .customAction('screenAndroidAuto', {'val': jsonEncode(data)}); break; case 'tracksAndroidAuto': AndroidAuto androidAuto = AndroidAuto(); await androidAuto.playItem(event['id']); break; case 'audioSession': - if (!settings.enableEqualizer) break; + if (!settings.enableEqualizer!) break; //Save _prevAudioSession = audioSession; audioSession = event['id']; @@ -90,8 +88,8 @@ class PlayerHelper { //Change session id if (_prevAudioSession != audioSession) { if (_prevAudioSession != null) - Equalizer.removeAudioSessionId(_prevAudioSession); - Equalizer.setAudioSessionId(audioSession); + Equalizer.removeAudioSessionId(_prevAudioSession!); + Equalizer.setAudioSessionId(audioSession!); } break; //Visualizer data @@ -100,16 +98,14 @@ class PlayerHelper { break; } }); - _mediaItemSubscription = - AudioService.currentMediaItemStream.listen((event) { + _mediaItemSubscription = audioHandler.mediaItem.listen((event) { if (event == null) return; //Load more flow if index-1 song - if (queueIndex == AudioService.queue.length - 1) onQueueEnd(); + if (queueIndex == audioHandler.queue.value.length - 1) onQueueEnd(); //Save queue - AudioService.customAction('saveQueue'); + audioHandler.customAction('saveQueue', {}); //Add to history - if (cache.history == null) cache.history = []; if (cache.history.length > 0 && cache.history.last.id == event.id) return; cache.history.add(Track.fromMediaItem(event)); cache.save(); @@ -117,50 +113,36 @@ class PlayerHelper { //Logging listen timer _timer = Timer.periodic(Duration(seconds: 2), (timer) async { - if (AudioService.currentMediaItem == null || - !AudioService.playbackState.playing) return; - if (AudioService.playbackState.currentPosition.inSeconds > - (AudioService.currentMediaItem.duration.inSeconds * 0.75)) { - if (cache.loggedTrackId == AudioService.currentMediaItem.id) return; - cache.loggedTrackId = AudioService.currentMediaItem.id; + if (audioHandler.mediaItem.value == null || + !audioHandler.playbackState.value.playing) return; + if (audioHandler.playbackState.value.position.inSeconds > + (audioHandler.mediaItem.value!.duration!.inSeconds * 0.75)) { + if (cache.loggedTrackId == audioHandler.mediaItem.value!.id) return; + cache.loggedTrackId = audioHandler.mediaItem.value!.id; await cache.save(); //Log to Deezer - if (settings.logListen) { - deezerAPI.logListen(AudioService.currentMediaItem.id); + if (settings.logListen!) { + deezerAPI.logListen(audioHandler.mediaItem.value!.id); } } }); //Start audio_service - await startService(); - } - - Future startService() async { - if (AudioService.running && AudioService.connected) return; - if (!AudioService.connected) await AudioService.connect(); - if (!AudioService.running) - await AudioService.start( - backgroundTaskEntrypoint: backgroundTaskEntrypoint, - androidEnableQueue: true, - androidStopForegroundOnPause: false, - androidNotificationOngoing: false, - androidNotificationClickStartsActivity: true, - androidNotificationChannelDescription: 'Freezer', - androidNotificationChannelName: 'Freezer', - androidNotificationIcon: 'drawable/ic_logo', - params: {'ignoreInterruptions': settings.ignoreInterruptions}); + // await startService(); it is already ready, there is no need to start it } Future authorizeLastFM() async { if (settings.lastFMUsername == null || settings.lastFMPassword == null) return; - await AudioService.customAction( - "authorizeLastFM", [settings.lastFMUsername, settings.lastFMPassword]); + await audioHandler.customAction('authorizeLastFM', { + 'username': settings.lastFMUsername, + 'password': settings.lastFMPassword + }); } Future toggleShuffle() async { - await AudioService.customAction('shuffle'); + await audioHandler.customAction('shuffle'); } //Repeat toggle @@ -178,8 +160,7 @@ class PlayerHelper { break; } //Set repeat type - await AudioService.customAction( - "repeatType", LoopMode.values.indexOf(repeatType)); + await audioHandler.customAction('repeatType', {'type': repeatType.index}); } //Executed before exit @@ -190,15 +171,14 @@ class PlayerHelper { } //Replace queue, play specified track id - Future _loadQueuePlay(List queue, String trackId) async { - await startService(); + Future _loadQueuePlay(List queue, String? trackId) async { await settings.updateAudioServiceQuality(); - await AudioService.customAction( - 'setIndex', queue.indexWhere((m) => m.id == trackId)); - await AudioService.updateQueue(queue); + await audioHandler.customAction( + 'setIndex', {'index': queue.indexWhere((m) => m.id == trackId)}); + await audioHandler.updateQueue(queue); // if (queue[0].id != trackId) // await AudioService.skipToQueueItem(trackId); - if (!AudioService.playbackState.playing) AudioService.play(); + if (!audioHandler.playbackState.value.playing) audioHandler.play(); } //Called when queue ends to load more tracks @@ -206,24 +186,27 @@ class PlayerHelper { //Flow if (queueSource == null) return; - List tracks = []; - switch (queueSource.source) { + List? tracks = []; + switch (queueSource!.source) { case 'flow': - tracks = await deezerAPI.flow(); + tracks = await (deezerAPI.flow() as FutureOr>); break; //SmartRadio/Artist radio case 'smartradio': - tracks = await deezerAPI.smartRadio(queueSource.id); + tracks = await (deezerAPI.smartRadio(queueSource!.id!) + as FutureOr>); break; //Library shuffle case 'libraryshuffle': - tracks = - await deezerAPI.libraryShuffle(start: AudioService.queue.length); + tracks = await (deezerAPI.libraryShuffle( + start: audioHandler.queue.value.length) as FutureOr>); break; case 'mix': - tracks = await deezerAPI.playMix(queueSource.id); + tracks = + await (deezerAPI.playMix(queueSource!.id) as FutureOr>); // Deduplicate tracks with the same id - List queueIds = AudioService.queue.map((e) => e.id).toList(); + List queueIds = + audioHandler.queue.value.map((e) => e.id).toList(); tracks.removeWhere((track) => queueIds.contains(track.id)); break; default: @@ -232,19 +215,20 @@ class PlayerHelper { } List mi = tracks.map((t) => t.toMediaItem()).toList(); - await AudioService.addQueueItems(mi); + await audioHandler.addQueueItems(mi); // AudioService.skipToNext(); } //Play track from album Future playFromAlbum(Album album, String trackId) async { - await playFromTrackList(album.tracks, trackId, + await playFromTrackList(album.tracks!, trackId, QueueSource(id: album.id, text: album.title, source: 'album')); } //Play mix by track Future playMix(String trackId, String trackTitle) async { - List tracks = await deezerAPI.playMix(trackId); + List tracks = + await (deezerAPI.playMix(trackId) as FutureOr>); playFromTrackList( tracks, tracks[0].id, @@ -265,7 +249,7 @@ class PlayerHelper { } Future playFromPlaylist(Playlist playlist, String trackId) async { - await playFromTrackList(playlist.tracks, trackId, + await playFromTrackList(playlist.tracks!, trackId, QueueSource(id: playlist.id, text: playlist.title, source: 'playlist')); } @@ -279,21 +263,19 @@ class PlayerHelper { episodes.map((e) => e.toMediaItem(show)).toList(); //Load and play - await startService(); + // await startService(); // audioservice is ready await settings.updateAudioServiceQuality(); await setQueueSource(queueSource); - await AudioService.customAction('setIndex', index); - await AudioService.updateQueue(queue); - if (!AudioService.playbackState.playing) AudioService.play(); + await audioHandler.customAction('setIndex', {'index': index}); + await audioHandler.updateQueue(queue); + if (!audioHandler.playbackState.value.playing) audioHandler.play(); } //Load tracks as queue, play track id, set queue source Future playFromTrackList( - List tracks, String trackId, QueueSource queueSource) async { - await startService(); - + List tracks, String? trackId, QueueSource queueSource) async { List queue = - tracks.map((track) => track.toMediaItem()).toList(); + tracks.map((track) => track!.toMediaItem()).toList(); await setQueueSource(queueSource); await _loadQueuePlay(queue, trackId); } @@ -301,7 +283,7 @@ class PlayerHelper { //Load smart track list as queue, start from beginning Future playFromSmartTrackList(SmartTrackList stl) async { //Load from API if no tracks - if (stl.tracks == null || stl.tracks.length == 0) { + if (stl.tracks == null || stl.tracks!.length == 0) { if (settings.offlineMode) { Fluttertoast.showToast( msg: "Offline mode, can't play flow or smart track lists.".i18n, @@ -322,75 +304,73 @@ class PlayerHelper { source: (stl.id == 'flow') ? 'flow' : 'smarttracklist', text: stl.title ?? ((stl.id == 'flow') ? 'Flow'.i18n : 'Smart track list'.i18n)); - await playFromTrackList(stl.tracks, stl.tracks[0].id, queueSource); + await playFromTrackList(stl.tracks!, stl.tracks![0].id, queueSource); } Future setQueueSource(QueueSource queueSource) async { - await startService(); - this.queueSource = queueSource; - await AudioService.customAction('queueSource', queueSource.toJson()); + await audioHandler.customAction('queueSource', queueSource.toJson()); } //Reorder tracks in queue Future reorder(int oldIndex, int newIndex) async { - await AudioService.customAction('reorder', [oldIndex, newIndex]); + await audioHandler + .customAction('reorder', {'oldIndex': oldIndex, 'newIndex': newIndex}); } //Start visualizer // Future startVisualizer() async { - // await AudioService.customAction('startVisualizer'); + // await audioHandler.customAction('startVisualizer'); // } //Stop visualizer - Future stopVisualizer() async { - await AudioService.customAction('stopVisualizer'); - } + // Future stopVisualizer() async { + // await audioHandler.customAction('stopVisualizer'); + // } } -void backgroundTaskEntrypoint() async { - AudioServiceBackground.run(() => AudioPlayerTask()); -} - -class AudioPlayerTask extends BackgroundAudioTask { - AudioPlayer _player; +class AudioPlayerTask extends BaseAudioHandler { + late AudioPlayer _player; //Queue - List _queue = []; - List _originalQueue; + List? _queue = []; + List? _originalQueue; bool _shuffle = false; int _queueIndex = 0; - ConcatenatingAudioSource _audioSource; + bool _isInitialized = false; + late ConcatenatingAudioSource _audioSource; - AudioProcessingState _skipState; - Seeker _seeker; + Seeker? _seeker; //Stream subscriptions - StreamSubscription _eventSub; - StreamSubscription _audioSessionSub; - StreamSubscription _visualizerSubscription; + StreamSubscription? _eventSub; + StreamSubscription? _audioSessionSub; + StreamSubscription? _visualizerSubscription; //Loaded from file/frontend - int mobileQuality; - int wifiQuality; - QueueSource queueSource; - Duration _lastPosition; + int? mobileQuality; + int? wifiQuality; + QueueSource? queueSource; + Duration? _lastPosition; LoopMode _loopMode = LoopMode.off; - Completer _androidAutoCallback; - Scrobblenaut _scrobblenaut; + Completer>? _androidAutoCallback; + Scrobblenaut? _scrobblenaut; bool _scrobblenautReady = false; // Last logged track id - String _loggedTrackId; + String? _loggedTrackId; - MediaItem get mediaItem => _queue[_queueIndex]; + MediaItem get currentMediaItem => _queue![_queueIndex]; - @override - Future onStart(Map params) async { + AudioPlayerTask() { + onStart({}); // workaround i guess? + } + + Future onStart(Map? params) async { final session = await AudioSession.instance; session.configure(AudioSessionConfiguration.music()); - if (params['ignoreInterruptions'] == true) { + if (params?['ignoreInterruptions'] == true) { _player = AudioPlayer(handleInterruptions: false); session.interruptionEventStream.listen((_) {}); session.becomingNoisyEventStream.listen((_) {}); @@ -401,16 +381,17 @@ class AudioPlayerTask extends BackgroundAudioTask { _player.currentIndexStream.listen((index) { if (index != null) { _queueIndex = index; - AudioServiceBackground.setMediaItem(mediaItem); + mediaItem.add(currentMediaItem); } }); //Update state on all clients on change _eventSub = _player.playbackEventStream.listen((event) { //Quality string - if (_queueIndex != -1 && _queueIndex < _queue.length) { - Map extras = mediaItem.extras; + if (_queueIndex != -1 && _queueIndex < _queue!.length) { + Map extras = currentMediaItem.extras!; extras['qualityString'] = ''; - _queue[_queueIndex] = mediaItem.copyWith(extras: extras); + _queue![_queueIndex] = + currentMediaItem.copyWith(extras: extras as Map?); } //Update _broadcastState(); @@ -419,16 +400,12 @@ class AudioPlayerTask extends BackgroundAudioTask { switch (state) { case ProcessingState.completed: //Player ended, get more songs - if (_queueIndex == _queue.length - 1) - AudioServiceBackground.sendCustomEvent({ + if (_queueIndex == _queue!.length - 1) + customEvent.add({ 'action': 'queueEnd', 'queueSource': (queueSource ?? QueueSource()).toJson() }); break; - case ProcessingState.ready: - //Ready to play - _skipState = null; - break; default: break; } @@ -436,117 +413,110 @@ class AudioPlayerTask extends BackgroundAudioTask { //Audio session _audioSessionSub = _player.androidAudioSessionIdStream.listen((event) { - AudioServiceBackground.sendCustomEvent( - {"action": 'audioSession', "id": event}); + customEvent.add({'action': 'audioSession', 'id': event}); }); //Load queue - AudioServiceBackground.setQueue(_queue); - AudioServiceBackground.sendCustomEvent({'action': 'onLoad'}); + queue.add(_queue!); + customEvent.add({'action': 'onLoad'}); } @override - Future onSkipToQueueItem(String mediaId) async { + Future skipToQueueItem(int index) async { _lastPosition = null; - //Calculate new index - final newIndex = _queue.indexWhere((i) => i.id == mediaId); - if (newIndex == -1) return; - //Update buffering state - _skipState = newIndex > _queueIndex - ? AudioProcessingState.skippingToNext - : AudioProcessingState.skippingToPrevious; - //Skip in player - await _player.seek(Duration.zero, index: newIndex); - _queueIndex = newIndex; - _skipState = null; - onPlay(); + await _player.seek(Duration.zero, index: index); + _queueIndex = index; + play(); } @override - Future onPlay() async { + Future play() async { _player.play(); //Restore position on play if (_lastPosition != null) { - onSeekTo(_lastPosition); + seek(_lastPosition); _lastPosition = null; } //LastFM - if (_scrobblenautReady && mediaItem.id != _loggedTrackId) { - _loggedTrackId = mediaItem.id; - await _scrobblenaut.track.scrobble( - track: mediaItem.title, - artist: mediaItem.artist, - album: mediaItem.album, + if (_scrobblenautReady && currentMediaItem.id != _loggedTrackId) { + _loggedTrackId = currentMediaItem.id; + await _scrobblenaut!.track.scrobble( + track: currentMediaItem.title, + artist: currentMediaItem.artist!, + album: currentMediaItem.album, ); } } @override - Future onPause() => _player.pause(); + Future pause() => _player.pause(); @override - Future onSeekTo(Duration pos) => _player.seek(pos); + Future seek(Duration? pos) => _player.seek(pos); @override - Future onFastForward() => _seekRelative(fastForwardInterval); + Future fastForward() => + _seekRelative(AudioService.config.fastForwardInterval); @override - Future onRewind() => _seekRelative(-rewindInterval); + Future rewind() => _seekRelative(-AudioService.config.rewindInterval); @override - Future onSeekForward(bool begin) async => _seekContinuously(begin, 1); + Future seekForward(bool begin) async => _seekContinuously(begin, 1); @override - Future onSeekBackward(bool begin) async => _seekContinuously(begin, -1); + Future seekBackward(bool begin) async => _seekContinuously(begin, -1); //Remove item from queue @override - Future onRemoveQueueItem(MediaItem mediaItem) async { - int index = _queue.indexWhere((m) => m.id == mediaItem.id); - _queue.removeAt(index); + Future removeQueueItem(MediaItem mediaItem) async { + int index = _queue!.indexWhere((m) => m.id == mediaItem.id); + removeQueueItemAt(index); + } + + @override + Future removeQueueItemAt(int index) async { + _queue!.removeAt(index); if (index <= _queueIndex) { _queueIndex--; } - _audioSource.removeAt(index); + await _audioSource.removeAt(index); - AudioServiceBackground.setQueue(_queue); + queue.add(_queue!); } @override - Future onSkipToNext() async { + Future skipToNext() async { _lastPosition = null; - if (_queueIndex == _queue.length - 1) return; + if (_queueIndex == _queue!.length - 1) return; //Update buffering state - _skipState = AudioProcessingState.skippingToNext; _queueIndex++; await _player.seekToNext(); - _skipState = null; - await _broadcastState(); + _broadcastState(); } @override - Future onSkipToPrevious() async { + Future skipToPrevious() async { if (_queueIndex == 0) return; //Update buffering state - _skipState = AudioProcessingState.skippingToPrevious; + //_skipState = AudioProcessingState.skippingToPrevious; //Normal skip to previous _queueIndex--; await _player.seekToPrevious(); - _skipState = null; + //_skipState = null; } @override - Future> onLoadChildren(String parentMediaId) async { - AudioServiceBackground.sendCustomEvent( - {'action': 'screenAndroidAuto', 'id': parentMediaId}); + Future> getChildren(String parentMediaId, + [Map? options]) async { + customEvent.add({'action': 'screenAndroidAuto', 'id': parentMediaId}); //Wait for data from main thread - _androidAutoCallback = Completer(); - List data = - (await _androidAutoCallback.future) as List; + _androidAutoCallback = Completer>(); + final data = await _androidAutoCallback!.future; _androidAutoCallback = null; return data; } @@ -556,7 +526,7 @@ class AudioPlayerTask extends BackgroundAudioTask { _seeker?.stop(); if (begin) { _seeker = Seeker(_player, Duration(seconds: 10 * direction), - Duration(seconds: 1), mediaItem) + Duration(seconds: 1), currentMediaItem) ..start(); } } @@ -566,14 +536,15 @@ class AudioPlayerTask extends BackgroundAudioTask { Duration newPos = _player.position + offset; //Out of bounds check if (newPos < Duration.zero) newPos = Duration.zero; - if (newPos > mediaItem.duration) newPos = mediaItem.duration; + if (newPos > currentMediaItem.duration!) + newPos = currentMediaItem.duration!; await _player.seek(newPos); } //Update state on all clients - Future _broadcastState() async { - await AudioServiceBackground.setState( + void _broadcastState() { + playbackState.add(PlaybackState( controls: [ MediaControl.skipToPrevious, if (_player.playing) MediaControl.pause else MediaControl.play, @@ -584,28 +555,27 @@ class AudioPlayerTask extends BackgroundAudioTask { label: 'stop', action: MediaAction.stop), ], - systemActions: [ - MediaAction.seekTo, + systemActions: const { + MediaAction.seek, MediaAction.seekForward, MediaAction.seekBackward, MediaAction.stop - ], + }, processingState: _getProcessingState(), playing: _player.playing, - position: _player.position, + updatePosition: _player.position, bufferedPosition: _player.bufferedPosition, - speed: _player.speed); + speed: _player.speed)); } //just_audio state -> audio_service state. If skipping, use _skipState AudioProcessingState _getProcessingState() { - if (_skipState != null) return _skipState; //SRC: audio_service example switch (_player.processingState) { case ProcessingState.idle: - return AudioProcessingState.stopped; + return AudioProcessingState.idle; case ProcessingState.loading: - return AudioProcessingState.connecting; + return AudioProcessingState.loading; case ProcessingState.buffering: return AudioProcessingState.buffering; case ProcessingState.ready: @@ -619,34 +589,34 @@ class AudioPlayerTask extends BackgroundAudioTask { //Replace current queue @override - Future onUpdateQueue(List q) async { + Future updateQueue(List q) async { _lastPosition = null; //just_audio _shuffle = false; _originalQueue = null; _player.stop(); - if (_audioSource != null) _audioSource.clear(); + if (_isInitialized) _audioSource.clear(); //Filter duplicate IDs - List queue = []; - for (MediaItem mi in q) { - if (queue.indexWhere((m) => mi.id == m.id) == -1) queue.add(mi); - } - this._queue = queue; - AudioServiceBackground.setQueue(queue); + List newQueue = q.toSet().toList(); + + _queue = newQueue; + //Load await _loadQueue(); + // broadcast to ui + queue.add(newQueue); //await _player.seek(Duration.zero, index: 0); } //Load queue to just_audio Future _loadQueue() async { //Don't reset queue index by starting player - int qi = _queueIndex; + int? qi = _queueIndex; List sources = []; - for (int i = 0; i < _queue.length; i++) { - AudioSource s = await _mediaItemToAudioSource(_queue[i]); - if (s != null) sources.add(s); + for (int i = 0; i < _queue!.length; i++) { + AudioSource s = await _mediaItemToAudioSource(_queue![i]); + sources.add(s); } _audioSource = ConcatenatingAudioSource(children: sources); @@ -658,20 +628,21 @@ class AudioPlayerTask extends BackgroundAudioTask { //Error loading tracks } _queueIndex = qi; - AudioServiceBackground.setMediaItem(mediaItem); + mediaItem.add(currentMediaItem); } Future _mediaItemToAudioSource(MediaItem mi) async { - String url = await _getTrackUrl(mi); - if (url == null) return null; - if (url.startsWith('http')) return ProgressiveAudioSource(Uri.parse(url)); - return AudioSource.uri(Uri.parse(url), tag: mi.id); + String? url = await _getTrackUrl(mi); + final uri = Uri.parse(url); + if (uri.isScheme('http') || uri.isScheme('https')) + return ProgressiveAudioSource(uri); + return AudioSource.uri(uri, tag: mi.id); } - Future _getTrackUrl(MediaItem mediaItem, {int quality}) async { + Future _getTrackUrl(MediaItem mediaItem, {int? quality}) async { //Check if offline String _offlinePath = - p.join((await getExternalStorageDirectory()).path, 'offline/'); + p.join((await getExternalStorageDirectory())!.path, 'offline/'); File f = File(p.join(_offlinePath, mediaItem.id)); if (await f.exists()) { //return f.path; @@ -680,41 +651,42 @@ class AudioPlayerTask extends BackgroundAudioTask { } //Show episode direct link - if (mediaItem.extras['showUrl'] != null) return mediaItem.extras['showUrl']; + if (mediaItem.extras!['showUrl'] != null) + return mediaItem.extras!['showUrl']; //Due to current limitations of just_audio, quality fallback moved to DeezerDataSource in ExoPlayer //This just returns fake url that contains metadata - List playbackDetails = jsonDecode(mediaItem.extras['playbackDetails']); + List? playbackDetails = jsonDecode(mediaItem.extras!['playbackDetails']); //Quality ConnectivityResult conn = await Connectivity().checkConnectivity(); quality = mobileQuality; if (conn == ConnectivityResult.wifi) quality = wifiQuality; - if ((playbackDetails ?? []).length < 2) return null; + if ((playbackDetails ?? []).length < 2) + throw Exception('not enough playback details'); //String url = 'https://dzcdn.net/?md5=${playbackDetails[0]}&mv=${playbackDetails[1]}&q=${quality.toString()}#${mediaItem.id}'; String url = - 'http://localhost:36958/?q=$quality&mv=${playbackDetails[1]}&md5origin=${playbackDetails[0]}&id=${mediaItem.id}'; + 'http://localhost:36958/?q=$quality&mv=${playbackDetails![1]}&md5origin=${playbackDetails[0]}&id=${mediaItem.id}'; return url; } //Custom actions @override - Future onCustomAction(String name, dynamic args) async { + Future customAction(String name, [Map? args]) async { switch (name) { case 'updateQuality': //Pass wifi & mobile quality by custom action //Isolate can't access globals - this.wifiQuality = args['wifiQuality']; + this.wifiQuality = args!['wifiQuality']; this.mobileQuality = args['mobileQuality']; break; //Update queue source case 'queueSource': - this.queueSource = - QueueSource.fromJson(Map.from(args)); + this.queueSource = QueueSource.fromJson(args!); break; //Looping case 'repeatType': - _loopMode = LoopMode.values[args]; + _loopMode = LoopMode.values[args!['type']]; _player.setLoopMode(_loopMode); break; //Save queue @@ -726,22 +698,24 @@ class AudioPlayerTask extends BackgroundAudioTask { await this._loadQueueFile(); break; case 'shuffle': - String originalId = mediaItem.id; + + /// TODO: maybe use [_player.setShuffleModeEnabled] instead? + // why is this even a thing? + // String originalId = mediaItem.id; if (!_shuffle) { _shuffle = true; - _originalQueue = List.from(_queue); - _queue.shuffle(); + _originalQueue = List.from(_queue!); + _queue!.shuffle(); } else { _shuffle = false; _queue = _originalQueue; _originalQueue = null; } - //Broken // _queueIndex = _queue.indexWhere((mi) => mi.id == originalId); _queueIndex = 0; - AudioServiceBackground.setQueue(_queue); - AudioServiceBackground.setMediaItem(mediaItem); + queue.add(_queue!); + // AudioServiceBackground.setMediaItem(mediaItem); await _player.stop(); await _loadQueue(); await _player.play(); @@ -750,22 +724,25 @@ class AudioPlayerTask extends BackgroundAudioTask { //Android audio callback case 'screenAndroidAuto': if (_androidAutoCallback != null) - _androidAutoCallback.complete(jsonDecode(args) - .map((m) => MediaItem.fromJson(m)) - .toList()); + _androidAutoCallback!.complete( + (args!['value'] as List>) + .map(mediaItemFromJson) + .toList()); break; //Reorder tracks, args = [old, new] case 'reorder': - await _audioSource.move(args[0], args[1]); + final oldIndex = args!['oldIndex']! as int; + final newIndex = args['newIndex']! as int; + await _audioSource.move(oldIndex, newIndex); //Switch in queue - _queue.reorder(args[0], args[1]); + _queue!.reorder(oldIndex, newIndex); //Update UI - AudioServiceBackground.setQueue(_queue); + queue.add(_queue!); _broadcastState(); break; //Set index without affecting playback for loading - case 'setIndex': - this._queueIndex = args; + case 'setIndex': // i really don't get what this is for + this._queueIndex = args!['index']; break; //Start visualizer // case 'startVisualizer': @@ -796,10 +773,10 @@ class AudioPlayerTask extends BackgroundAudioTask { // break; //Authorize lastfm case 'authorizeLastFM': - String username = args[0]; - String password = args[1]; + final username = args!['username']! as String; + final password = args['password']! as String; try { - LastFM lastFM = await LastFM.authenticateWithPasswordHash( + final lastFM = await LastFM.authenticateWithPasswordHash( apiKey: 'b6ab5ae967bcd8b10b23f68f42493829', apiSecret: '861b0dff9a8a574bec747f9dab8b82bf', username: username, @@ -820,23 +797,19 @@ class AudioPlayerTask extends BackgroundAudioTask { } @override - Future onTaskRemoved() async { - await onStop(); - } + Future onTaskRemoved() => stop(); @override - Future onClose() async { - print('onClose'); - await onStop(); - } + Future onNotificationDeleted() => stop(); - Future onStop() async { + @override + Future stop() async { await _saveQueue(); _player.stop(); - if (_eventSub != null) _eventSub.cancel(); - if (_audioSessionSub != null) _audioSessionSub.cancel(); - - await super.onStop(); + _eventSub?.cancel(); + _audioSessionSub?.cancel(); + _visualizerSubscription?.cancel(); + await super.stop(); } //Get queue save file path @@ -846,8 +819,8 @@ class AudioPlayerTask extends BackgroundAudioTask { } //Export queue to JSON - Future _saveQueue() async { - if (_queueIndex == 0 && _queue.length == 0) return; + Future _saveQueue() async { + if (_queueIndex == 0 && _queue!.length == 0) return; String path = await _getQueuePath(); File f = File(path); @@ -857,35 +830,34 @@ class AudioPlayerTask extends BackgroundAudioTask { } Map data = { 'index': _queueIndex, - 'queue': _queue.map>((mi) => mi.toJson()).toList(), + 'queue': _queue!.map>(mediaItemToJson).toList(), 'position': _player.position.inMilliseconds, 'queueSource': (queueSource ?? QueueSource()).toJson(), - 'loopMode': LoopMode.values.indexOf(_loopMode ?? LoopMode.off) + 'loopMode': LoopMode.values.indexOf(_loopMode) }; await f.writeAsString(jsonEncode(data)); } //Restore queue & playback info from path - Future _loadQueueFile() async { + Future _loadQueueFile() async { File f = File(await _getQueuePath()); if (await f.exists()) { Map json = jsonDecode(await f.readAsString()); - this._queue = (json['queue'] ?? []) - .map((mi) => MediaItem.fromJson(mi)) - .toList(); + this._queue = + (json['queue'] ?? []).map(mediaItemFromJson).toList(); this._queueIndex = json['index'] ?? 0; this._lastPosition = Duration(milliseconds: json['position'] ?? 0); this.queueSource = QueueSource.fromJson(json['queueSource'] ?? {}); this._loopMode = LoopMode.values[(json['loopMode'] ?? 0)]; //Restore queue if (_queue != null) { - await AudioServiceBackground.setQueue(_queue); + queue.add(_queue!); await _loadQueue(); - await AudioServiceBackground.setMediaItem(mediaItem); + mediaItem.add(currentMediaItem); } } //Send restored queue source to ui - AudioServiceBackground.sendCustomEvent({ + customEvent.add({ 'action': 'onRestore', 'queueSource': (queueSource ?? QueueSource()).toJson(), 'loopMode': LoopMode.values.indexOf(_loopMode) @@ -894,35 +866,45 @@ class AudioPlayerTask extends BackgroundAudioTask { } @override - Future onAddQueueItemAt(MediaItem mi, int index) async { + Future insertQueueItem(int index, MediaItem mi) async { //-1 == play next if (index == -1) index = _queueIndex + 1; - _queue.insert(index, mi); - await AudioServiceBackground.setQueue(_queue); - AudioSource _newSource = await _mediaItemToAudioSource(mi); - if (_newSource != null) await _audioSource.insert(index, _newSource); + _queue!.insert(index, mi); + queue.add(_queue!); + AudioSource? _newSource = await _mediaItemToAudioSource(mi); + await _audioSource.insert(index, _newSource); _saveQueue(); } //Add at end of queue @override - Future onAddQueueItem(MediaItem mi) async { - if (_queue.indexWhere((m) => m.id == mi.id) != -1) return; + Future addQueueItem(MediaItem mediaItem, + {bool shouldSaveQueue = true}) async { + if (_queue!.indexWhere((m) => m.id == mediaItem.id) != -1) return; - _queue.add(mi); - await AudioServiceBackground.setQueue(_queue); - AudioSource _newSource = await _mediaItemToAudioSource(mi); - if (_newSource != null) await _audioSource.add(_newSource); + _queue!.add(mediaItem); + queue.add(_queue!); + AudioSource _newSource = await _mediaItemToAudioSource(mediaItem); + await _audioSource.add(_newSource); + if (shouldSaveQueue) _saveQueue(); + } + + @override + Future addQueueItems(List mediaItems) async { + for (final mediaItem in mediaItems) { + await addQueueItem(mediaItem, shouldSaveQueue: false); + } _saveQueue(); } @override - Future onPlayFromMediaId(String mediaId) async { + Future playFromMediaId(String mediaId, + [Map? args]) async { //Android auto load tracks if (mediaId.startsWith(AndroidAuto.prefix)) { - AudioServiceBackground.sendCustomEvent({ + customEvent.add({ 'action': 'tracksAndroidAuto', 'id': mediaId.replaceFirst(AndroidAuto.prefix, '') }); @@ -930,14 +912,32 @@ class AudioPlayerTask extends BackgroundAudioTask { } //Does the same thing - await this.onSkipToQueueItem(mediaId); + await this + .skipToQueueItem(_queue!.indexWhere((item) => item.id == mediaId)); } + + @override + Future getMediaItem(String mediaId) async => + _queue!.firstWhereOrNull((mediaItem) => mediaItem.id == mediaId); + + @override + Future playMediaItem(MediaItem mediaItem) => + playFromMediaId(mediaItem.id); + + // TODO: implement shuffle and repeat + @override + Future setRepeatMode(AudioServiceRepeatMode repeatMode) => + super.setRepeatMode(repeatMode); + + @override + Future setShuffleMode(AudioServiceShuffleMode shuffleMode) => + super.setShuffleMode(shuffleMode); } //Seeker from audio_service example (why reinvent the wheel?) //While holding seek button, will continuously seek class Seeker { - final AudioPlayer player; + final AudioPlayer? player; final Duration positionInterval; final Duration stepInterval; final MediaItem mediaItem; @@ -948,10 +948,10 @@ class Seeker { Future start() async { _running = true; while (_running) { - Duration newPosition = player.position + positionInterval; + Duration newPosition = player!.position + positionInterval; if (newPosition < Duration.zero) newPosition = Duration.zero; - if (newPosition > mediaItem.duration) newPosition = mediaItem.duration; - player.seek(newPosition); + if (newPosition > mediaItem.duration!) newPosition = mediaItem.duration!; + player!.seek(newPosition); await Future.delayed(stepInterval); } } diff --git a/lib/api/spotify.dart b/lib/api/spotify.dart index 7414bd2..351c46b 100644 --- a/lib/api/spotify.dart +++ b/lib/api/spotify.dart @@ -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 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 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 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 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 artists; - String isrc; + String? title; + List? artists; + String? isrc; SpotifyTrack({this.title, this.artists, this.isrc}); @@ -104,10 +104,10 @@ class SpotifyTrack { } class SpotifyPlaylist { - String name; - String description; - List tracks; - String image; + String? name; + String? description; + List? tracks; + String? image; SpotifyPlaylist({this.name, this.description, this.tracks, this.image}); @@ -122,12 +122,12 @@ class SpotifyPlaylist { //Convert to importer tracks List 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 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); } } diff --git a/lib/main.dart b/lib/main.dart index b02b13b..d616943 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -26,12 +26,10 @@ import 'settings.dart'; import 'ui/home_screen.dart'; import 'ui/player_bar.dart'; -Function updateTheme; -Function logOut; +late Function updateTheme; +late Function logOut; GlobalKey mainNavigatorKey = GlobalKey(); -GlobalKey navigatorKey; - -// TODO: migrate to null-safety +GlobalKey navigatorKey = GlobalKey(); 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( + 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 { 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 { 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 { //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 with SingleTickerProviderStateMixin, WidgetsBindingObserver { List _screens = [HomeScreen(), SearchScreen(), LibraryScreen()]; - int _selected = 0; - StreamSubscription _urlLinkStream; + final _selected = ValueNotifier(0); + StreamSubscription? _urlLinkStream; int _keyPressed = 0; bool textFieldVisited = false; @override void initState() { - navigatorKey = GlobalKey(); - //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 }); super.initState(); - WidgetsBinding.instance.addObserver(this); + WidgetsBinding.instance!.addObserver(this); } void _startStreamingServer() async { @@ -216,7 +225,7 @@ class _MainScreenState extends State 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 } 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 @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 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 ValueChanged _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 // 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 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 // 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 shortcuts = Shortcuts.of(context).shortcuts; - final BuildContext primaryContext = primaryFocus?.context; - Intent intent = shortcuts[LogicalKeySet(event.logicalKey)]; + Map shortcuts = + Shortcuts.of(context).shortcuts as Map; + 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 focusNode: FocusNode(), onKey: _handleKey(navigationBarFocusNode, screenFocusNode), child: Scaffold( - bottomNavigationBar: FocusScope( - node: navigationBarFocusNode, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - 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: [ + PlayerBar(), + ValueListenableBuilder( + 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( - 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( + 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( + 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 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 didPopRoute() async { - final NavigatorState navigator = navigatorKey?.currentState; + final NavigatorState? navigator = navigatorKey.currentState; if (navigator == null) return false; return await navigator.maybePop(); } @override Future 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 _onGenerateRoute(RouteSettings settings) { + Route? _onGenerateRoute(RouteSettings settings) { if (settings.name == Navigator.defaultRouteName) { return MaterialPageRoute(builder: (context) => home, settings: settings); } diff --git a/lib/settings.dart b/lib/settings.dart index 1e097b4..f34b176 100644 --- a/lib/settings.dart +++ b/lib/settings.dart @@ -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 tags; + List? 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 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 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 scopes; - DateTime expiration; + String? accessToken; + String? refreshToken; + List? scopes; + DateTime? expiration; SpotifyCredentialsSave( {this.accessToken, this.refreshToken, this.scopes, this.expiration}); diff --git a/lib/settings.g.dart b/lib/settings.g.dart index 275cd86..fa3afec 100644 --- a/lib/settings.g.dart +++ b/lib/settings.g.dart @@ -6,84 +6,84 @@ part of 'settings.dart'; // JsonSerializableGenerator // ************************************************************************** -Settings _$SettingsFromJson(Map 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); -} +Settings _$SettingsFromJson(Map 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?)?.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); Map _$SettingsToJson(Settings instance) => { 'language': instance.language, @@ -130,36 +130,41 @@ Map _$SettingsToJson(Settings instance) => { 'spotifyCredentials': instance.spotifyCredentials, }; -T _$enumDecode( - Map enumValues, - dynamic source, { - T unknownValue, +K _$enumDecode( + Map 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( - Map enumValues, +K? _$enumDecodeNullable( + Map enumValues, dynamic source, { - T unknownValue, + K? unknownValue, }) { if (source == null) { return null; } - return _$enumDecode(enumValues, source, unknownValue: unknownValue); + return _$enumDecode(enumValues, source, unknownValue: unknownValue); } const _$AudioQualityEnumMap = { @@ -177,16 +182,16 @@ const _$ThemesEnumMap = { }; SpotifyCredentialsSave _$SpotifyCredentialsSaveFromJson( - Map 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 json) => + 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 _$SpotifyCredentialsSaveToJson( SpotifyCredentialsSave instance) => diff --git a/lib/ui/android_auto.dart b/lib/ui/android_auto.dart index 32d8dd0..dd5fb89 100644 --- a/lib/ui/android_auto.dart +++ b/lib/ui/android_auto.dart @@ -9,7 +9,7 @@ class AndroidAuto { static const prefix = '_aa_'; //Get media items for parent id - Future> getScreen(String parentId) async { + Future> getScreen(String? parentId) async { print(parentId); //Homescreen @@ -24,9 +24,11 @@ class AndroidAuto { .map((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((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 artists = await deezerAPI.getArtists(); + List artists = (await deezerAPI.getArtists())!; List out = artists .map((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 albums = - await deezerAPI.discographyPage(parentId.replaceFirst('albums', '')); + List albums = (await deezerAPI + .discographyPage(parentId.replaceFirst('albums', '')))!; List out = albums .map((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 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 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, ), diff --git a/lib/ui/cached_image.dart b/lib/ui/cached_image.dart index 7110b64..3ba4f27 100644 --- a/lib/ui/cached_image.dart +++ b/lib/ui/cached_image.dart @@ -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 { 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 { } 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 { - PhotoViewController controller; + PhotoViewController? controller; bool photoViewOpened = false; @override @@ -132,7 +132,7 @@ class _ZoomableImageState extends State { // 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 { pageBuilder: (context, _, __) { photoViewOpened = true; return PhotoView( - imageProvider: CachedNetworkImageProvider(widget.url), + imageProvider: CachedNetworkImageProvider(widget.url!), maxScale: 8.0, minScale: 0.2, controller: controller, diff --git a/lib/ui/details_screens.dart b/lib/ui/details_screens.dart index c2bbd03..718d060 100644 --- a/lib/ui/details_screens.dart +++ b/lib/ui/details_screens.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:draggable_scrollbar/draggable_scrollbar.dart'; import 'package:flutter/material.dart'; import 'package:fluttericon/font_awesome5_icons.dart'; @@ -17,27 +19,25 @@ import 'tiles.dart'; import 'menu.dart'; class AlbumDetails extends StatefulWidget { - - final Album album; - AlbumDetails(this.album, {Key key}): super(key: key); + final Album? album; + AlbumDetails(this.album, {Key? key}) : super(key: key); @override _AlbumDetailsState createState() => _AlbumDetailsState(); } class _AlbumDetailsState extends State { - - Album album; + Album? album; bool _loading = true; bool _error = false; Future _loadAlbum() async { //Get album from API, if doesn't have tracks - if (this.album.tracks == null || this.album.tracks.length == 0) { + if (this.album!.tracks == null || this.album!.tracks!.length == 0) { try { - Album a = await deezerAPI.album(album.id); + Album a = await deezerAPI.album(album!.id); //Preserve library - a.library = album.library; + a.library = album!.library; setState(() => album = a); } catch (e) { setState(() => _error = true); @@ -47,10 +47,10 @@ class _AlbumDetailsState extends State { } //Get count of CDs in album - int get cdCount { - int c = 1; - for (Track t in album.tracks) { - if ((t.diskNumber??1) > c) c = t.diskNumber; + int? get cdCount { + int? c = 1; + for (Track? t in album!.tracks!) { + if ((t!.diskNumber ?? 1) > c!) c = t.diskNumber; } return c; } @@ -65,195 +65,225 @@ class _AlbumDetailsState extends State { @override Widget build(BuildContext context) { return Scaffold( - body: _error ? ErrorScreen() : _loading ? Center(child: CircularProgressIndicator()) : - ListView( - children: [ - //Album art, title, artists - Container( - color: Theme.of(context).scaffoldBackgroundColor, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container(height: 8.0,), - ZoomableImage( - url: album.art.full, - width: MediaQuery.of(context).size.width / 2, - rounded: true, - ), - Container(height: 8,), - Text( - album.title, - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - maxLines: 2, - style: TextStyle( - fontSize: 20.0, - fontWeight: FontWeight.bold - ), - ), - Text( - album.artistString, - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - maxLines: 2, - style: TextStyle( - fontSize: 16.0, - color: Theme.of(context).primaryColor - ), - ), - Container(height: 4.0), - if (album.releaseDate != null && album.releaseDate.length >= 4) - Text( - album.releaseDate, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 12.0, - color: Theme.of(context).disabledColor + body: _error + ? ErrorScreen() + : _loading + ? Center(child: CircularProgressIndicator()) + : ListView( + children: [ + //Album art, title, artists + Container( + color: Theme.of(context).scaffoldBackgroundColor, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + height: 8.0, + ), + ZoomableImage( + url: album!.art!.full, + width: MediaQuery.of(context).size.width / 2, + rounded: true, + ), + Container( + height: 8, + ), + Text( + album!.title!, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + maxLines: 2, + style: TextStyle( + fontSize: 20.0, fontWeight: FontWeight.bold), + ), + Text( + album!.artistString, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + maxLines: 2, + style: TextStyle( + fontSize: 16.0, + color: Theme.of(context).primaryColor), + ), + Container(height: 4.0), + if (album!.releaseDate != null && + album!.releaseDate!.length >= 4) + Text( + album!.releaseDate!, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12.0, + color: Theme.of(context).disabledColor), + ), + Container( + height: 8.0, + ), + ], + ), ), - ), - Container(height: 8.0,), - ], - ), - ), - FreezerDivider(), - //Details - Container( - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Row( - children: [ - Icon(Icons.audiotrack, size: 32.0, semanticLabel: "Tracks".i18n,), - Container(width: 8.0, height: 42.0,), //Height to adjust card height - Text( - album.tracks.length.toString(), - style: TextStyle(fontSize: 16.0), - ) - ], - ), - Row( - children: [ - Icon(Icons.timelapse, size: 32.0, semanticLabel: "Duration".i18n,), - Container(width: 8.0,), - Text( - album.durationString, - style: TextStyle(fontSize: 16.0), - ) - ], - ), - Row( - children: [ - Icon(Icons.people, size: 32.0, semanticLabel: "Fans".i18n), - Container(width: 8.0,), - Text( - album.fansString, - style: TextStyle(fontSize: 16.0), - ) - ], - ), - ], - ), - ), - FreezerDivider(), - //Options (offline, download...) - Container( - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - TextButton( - child: Row( - children: [ - Icon((album.library??false)? Icons.favorite : Icons.favorite_border, size: 32), - Container(width: 4,), - Text('Library'.i18n) - ], - ), - onPressed: () async { - //Add to library - if (!album.library) { - await deezerAPI.addFavoriteAlbum(album.id); - Fluttertoast.showToast( - msg: 'Added to library'.i18n, - toastLength: Toast.LENGTH_SHORT, - gravity: ToastGravity.BOTTOM + FreezerDivider(), + //Details + Container( + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Row( + children: [ + Icon( + Icons.audiotrack, + size: 32.0, + semanticLabel: "Tracks".i18n, + ), + Container( + width: 8.0, + height: 42.0, + ), //Height to adjust card height + Text( + album!.tracks!.length.toString(), + style: TextStyle(fontSize: 16.0), + ) + ], + ), + Row( + children: [ + Icon( + Icons.timelapse, + size: 32.0, + semanticLabel: "Duration".i18n, + ), + Container( + width: 8.0, + ), + Text( + album!.durationString, + style: TextStyle(fontSize: 16.0), + ) + ], + ), + Row( + children: [ + Icon(Icons.people, + size: 32.0, semanticLabel: "Fans".i18n), + Container( + width: 8.0, + ), + Text( + album!.fansString, + style: TextStyle(fontSize: 16.0), + ) + ], + ), + ], + ), + ), + FreezerDivider(), + //Options (offline, download...) + Container( + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + TextButton( + child: Row( + children: [ + Icon( + (album!.library ?? false) + ? Icons.favorite + : Icons.favorite_border, + size: 32), + Container( + width: 4, + ), + Text('Library'.i18n) + ], + ), + onPressed: () async { + //Add to library + if (!album!.library!) { + await deezerAPI.addFavoriteAlbum(album!.id); + Fluttertoast.showToast( + msg: 'Added to library'.i18n, + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM); + setState(() => album!.library = true); + return; + } + //Remove + await deezerAPI.removeAlbum(album!.id); + Fluttertoast.showToast( + msg: 'Album removed from library!'.i18n, + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM); + setState(() => album!.library = false); + }, + ), + MakeAlbumOffline(album: album), + TextButton( + child: Row( + children: [ + Icon( + Icons.file_download, + size: 32.0, + ), + Container( + width: 4, + ), + Text('Download'.i18n) + ], + ), + onPressed: () async { + if (await downloadManager.addOfflineAlbum(album, + private: false, context: context) != + false) + MenuSheet(context).showDownloadStartedToast(); + }, + ) + ], + ), + ), + FreezerDivider(), + ...List.generate(cdCount!, (cdi) { + List tracks = album!.tracks! + .where((t) => (t!.diskNumber ?? 1) == cdi + 1) + .toList(); + return Column( + children: [ + Padding( + padding: EdgeInsets.symmetric(vertical: 4.0), + child: Text( + 'Disk'.i18n.toUpperCase() + ' ${cdi + 1}', + style: TextStyle( + fontSize: 12.0, + fontWeight: FontWeight.w300), + ), + ), + ...List.generate( + tracks.length, + (i) => TrackTile(tracks[i], onTap: () { + playerHelper.playFromAlbum( + album!, tracks[i]!.id); + }, onHold: () { + MenuSheet m = MenuSheet(context); + m.defaultTrackMenu(tracks[i]!); + })) + ], ); - setState(() => album.library = true); - return; - } - //Remove - await deezerAPI.removeAlbum(album.id); - Fluttertoast.showToast( - msg: 'Album removed from library!'.i18n, - toastLength: Toast.LENGTH_SHORT, - gravity: ToastGravity.BOTTOM - ); - setState(() => album.library = false); - }, - ), - MakeAlbumOffline(album: album), - TextButton( - child: Row( - children: [ - Icon(Icons.file_download, size: 32.0,), - Container(width: 4,), - Text('Download'.i18n) - ], - ), - onPressed: () async { - if (await downloadManager.addOfflineAlbum(album, private: false, context: context) != false) - MenuSheet(context).showDownloadStartedToast(); - }, - ) - ], - ), - ), - FreezerDivider(), - ...List.generate(cdCount, (cdi) { - List tracks = album.tracks.where((t) => (t.diskNumber??1) == cdi + 1).toList(); - return Column( - children: [ - Padding( - padding: EdgeInsets.symmetric(vertical: 4.0), - child: Text( - 'Disk'.i18n.toUpperCase() + ' ${cdi + 1}', - style: TextStyle( - fontSize: 12.0, - fontWeight: FontWeight.w300 - ), - ), - ), - ...List.generate(tracks.length, (i) => TrackTile( - tracks[i], - onTap: () { - playerHelper.playFromAlbum(album, tracks[i].id); - }, - onHold: () { - MenuSheet m = MenuSheet(context); - m.defaultTrackMenu(tracks[i]); - } - )) - ], - ); - }), - ], - ) - ); + }), + ], + )); } } class MakeAlbumOffline extends StatefulWidget { - - final Album album; - MakeAlbumOffline({Key key, this.album}): super(key: key); + final Album? album; + MakeAlbumOffline({Key? key, this.album}) : super(key: key); @override _MakeAlbumOfflineState createState() => _MakeAlbumOfflineState(); } class _MakeAlbumOfflineState extends State { - bool _offline = false; @override @@ -275,7 +305,7 @@ class _MakeAlbumOfflineState extends State { onChanged: (v) async { if (v) { //Add to offline - await deezerAPI.addFavoriteAlbum(widget.album.id); + await deezerAPI.addFavoriteAlbum(widget.album!.id); downloadManager.addOfflineAlbum(widget.album, private: true); MenuSheet(context).showDownloadStartedToast(); setState(() { @@ -283,14 +313,19 @@ class _MakeAlbumOfflineState extends State { }); return; } - downloadManager.removeOfflineAlbum(widget.album.id); - Fluttertoast.showToast(msg: "Removed album from offline!".i18n, gravity: ToastGravity.BOTTOM, toastLength: Toast.LENGTH_SHORT); + downloadManager.removeOfflineAlbum(widget.album!.id); + Fluttertoast.showToast( + msg: "Removed album from offline!".i18n, + gravity: ToastGravity.BOTTOM, + toastLength: Toast.LENGTH_SHORT); setState(() { _offline = false; }); }, ), - Container(width: 4.0,), + Container( + width: 4.0, + ), Text( 'Offline'.i18n, style: TextStyle(fontSize: 16), @@ -300,28 +335,38 @@ class _MakeAlbumOfflineState extends State { } } - class ArtistDetails extends StatelessWidget { + late final Artist artist; + late final Future? _future; + ArtistDetails(Artist artist) { + FutureOr future = _loadArtist(artist); + if (future is Artist) { + this.artist = future; + _future = null; + } else if (future is Future) + _future = future.then((value) => this.artist = value); + } - Artist artist; - ArtistDetails(this.artist); - - Future _loadArtist() async { + FutureOr _loadArtist(Artist artist) { //Load artist from api if no albums - if ((this.artist.albums??[]).length == 0) { - this.artist = await deezerAPI.artist(artist.id); + if ((this.artist.albums ?? []).length == 0) { + return deezerAPI.artist(artist.id); } + return artist; } @override Widget build(BuildContext context) { return Scaffold( body: FutureBuilder( - future: _loadArtist(), + future: _future ?? Future.value(), builder: (BuildContext context, AsyncSnapshot snapshot) { //Error / not done if (snapshot.hasError) return ErrorScreen(); - if (snapshot.connectionState != ConnectionState.done) return Center(child: CircularProgressIndicator(),); + if (snapshot.connectionState != ConnectionState.done) + return Center( + child: CircularProgressIndicator(), + ); return ListView( children: [ @@ -331,7 +376,7 @@ class ArtistDetails extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ ZoomableImage( - url: artist.picture.full, + url: artist.picture!.full, width: MediaQuery.of(context).size.width / 2 - 8, rounded: true, ), @@ -343,16 +388,14 @@ class ArtistDetails extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.center, children: [ Text( - artist.name, + artist.name!, overflow: TextOverflow.ellipsis, maxLines: 4, textAlign: TextAlign.center, style: TextStyle( fontSize: 24.0, fontWeight: FontWeight.bold), ), - Container( - height: 8.0, - ), + const SizedBox(height: 8.0), Row( mainAxisSize: MainAxisSize.min, children: [ @@ -361,25 +404,23 @@ class ArtistDetails extends StatelessWidget { size: 32.0, semanticLabel: "Fans".i18n, ), - Container( - width: 8, - ), + const SizedBox(width: 8.0), Text( artist.fansString, style: TextStyle(fontSize: 16), ), ], ), - Container( - height: 4.0, - ), + const SizedBox(height: 4.0), Row( mainAxisSize: MainAxisSize.min, children: [ - Icon(Icons.album, size: 32.0, semanticLabel: "Albums".i18n,), - Container( - width: 8.0, + Icon( + Icons.album, + size: 32.0, + semanticLabel: "Albums".i18n, ), + const SizedBox(width: 8.0), Text( artist.albumCount.toString(), style: TextStyle(fontSize: 16), @@ -392,77 +433,78 @@ class ArtistDetails extends StatelessWidget { ], ), ), - Container(height: 4.0), + const SizedBox(height: 4.0), FreezerDivider(), - Container( - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ + Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + TextButton( + child: Row( + children: [ + Icon(Icons.favorite, size: 32), + const SizedBox(width: 4.0), + Text('Library'.i18n) + ], + ), + onPressed: () async { + await deezerAPI.addFavoriteArtist(artist.id); + Fluttertoast.showToast( + msg: 'Added to library'.i18n, + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM); + }, + ), + if ((artist.radio ?? false)) TextButton( child: Row( children: [ - Icon(Icons.favorite, size: 32), - Container(width: 4,), - Text('Library'.i18n) + Icon(Icons.radio, size: 32), + const SizedBox(width: 4.0), + Text('Radio'.i18n) ], ), onPressed: () async { - await deezerAPI.addFavoriteArtist(artist.id); - Fluttertoast.showToast( - msg: 'Added to library'.i18n, - toastLength: Toast.LENGTH_SHORT, - gravity: ToastGravity.BOTTOM - ); + List tracks = + (await deezerAPI.smartRadio(artist.id!))!; + playerHelper.playFromTrackList( + tracks, + tracks[0].id, + QueueSource( + id: artist.id, + text: 'Radio'.i18n + ' ${artist.name}', + source: 'smartradio')); }, - ), - if ((artist.radio??false)) - TextButton( - child: Row( - children: [ - Icon(Icons.radio, size: 32), - Container(width: 4,), - Text('Radio'.i18n) - ], - ), - onPressed: () async { - List tracks = await deezerAPI.smartRadio(artist.id); - playerHelper.playFromTrackList(tracks, tracks[0].id, QueueSource( - id: artist.id, - text: 'Radio'.i18n + ' ${artist.name}', - source: 'smartradio' - )); - }, - ) - ], - ), + ) + ], ), FreezerDivider(), - Container(height: 12.0,), + const SizedBox(height: 12.0), //Highlight if (artist.highlight != null) Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( - padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 2.0), + padding: + EdgeInsets.symmetric(horizontal: 16.0, vertical: 2.0), child: Text( - artist.highlight.title, + artist.highlight!.title!, textAlign: TextAlign.left, style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 20.0 - ), + fontWeight: FontWeight.bold, fontSize: 20.0), ), ), - if (artist.highlight.type == ArtistHighlightType.ALBUM) + if (artist.highlight!.type == ArtistHighlightType.ALBUM) AlbumTile( - artist.highlight.data, + artist.highlight!.data, onTap: () { - Navigator.of(context).push(MaterialPageRoute(builder: (context) => AlbumDetails(artist.highlight.data))); + Navigator.of(context).push(MaterialPageRoute( + builder: (context) => + AlbumDetails(artist.highlight!.data))); }, ), - Container(height: 8.0) + const SizedBox(height: 8.0) ], ), //Top tracks @@ -471,24 +513,19 @@ class ArtistDetails extends StatelessWidget { child: Text( 'Top Tracks'.i18n, textAlign: TextAlign.left, - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 20.0 - ), + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20.0), ), ), - Container(height: 4.0), + const SizedBox(height: 4.0), ...List.generate(5, (i) { - if (artist.topTracks.length <= i) return Container(height: 0, width: 0,); - Track t = artist.topTracks[i]; + if (artist.topTracks!.length <= i) + return const SizedBox(height: 0.0, width: 0.0); + Track t = artist.topTracks![i]; return TrackTile( t, onTap: () { playerHelper.playFromTopTracks( - artist.topTracks, - t.id, - artist - ); + artist.topTracks!, t.id, artist); }, onHold: () { MenuSheet mi = MenuSheet(context); @@ -497,17 +534,16 @@ class ArtistDetails extends StatelessWidget { ); }), ListTile( - title: Text('Show more tracks'.i18n), - onTap: () { - Navigator.of(context).push( - MaterialPageRoute(builder: (context) => TrackListScreen(artist.topTracks, QueueSource( - id: artist.id, - text: 'Top'.i18n + '${artist.name}', - source: 'topTracks' - ))) - ); - } - ), + title: Text('Show more tracks'.i18n), + onTap: () { + Navigator.of(context).push(MaterialPageRoute( + builder: (context) => TrackListScreen( + artist.topTracks, + QueueSource( + id: artist.id, + text: 'Top'.i18n + '${artist.name}', + source: 'topTracks')))); + }), FreezerDivider(), //Albums Padding( @@ -515,38 +551,34 @@ class ArtistDetails extends StatelessWidget { child: Text( 'Top Albums'.i18n, textAlign: TextAlign.left, - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 20.0 - ), + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20.0), ), ), - ...List.generate(artist.albums.length > 10 ? 11 : artist.albums.length + 1, (i) { + ...List.generate( + artist.albums!.length > 10 ? 11 : artist.albums!.length + 1, + (i) { //Show discography - if (i == 10 || i == artist.albums.length) { + if (i == 10 || i == artist.albums!.length) { return ListTile( - title: Text('Show all albums'.i18n), - onTap: () { - Navigator.of(context).push( - MaterialPageRoute(builder: (context) => DiscographyScreen(artist: artist,)) - ); - } - ); + title: Text('Show all albums'.i18n), + onTap: () { + Navigator.of(context).push(MaterialPageRoute( + builder: (context) => DiscographyScreen( + artist: artist, + ))); + }); } //Top albums - Album a = artist.albums[i]; + Album a = artist.albums![i]; return AlbumTile( a, onTap: () { - Navigator.of(context).push( - MaterialPageRoute(builder: (context) => AlbumDetails(a)) - ); + Navigator.of(context).push(MaterialPageRoute( + builder: (context) => AlbumDetails(a))); }, onHold: () { MenuSheet m = MenuSheet(context); - m.defaultAlbumMenu( - a - ); + m.defaultAlbumMenu(a); }, ); }) @@ -559,17 +591,15 @@ class ArtistDetails extends StatelessWidget { } class DiscographyScreen extends StatefulWidget { - - final Artist artist; - DiscographyScreen({@required this.artist, Key key}): super(key: key); + final Artist? artist; + DiscographyScreen({required this.artist, Key? key}) : super(key: key); @override _DiscographyScreenState createState() => _DiscographyScreenState(); } class _DiscographyScreenState extends State { - - Artist artist; + Artist? artist; bool _loading = false; bool _error = false; List _controllers = [ @@ -579,13 +609,14 @@ class _DiscographyScreenState extends State { ]; Future _load() async { - if (artist.albums.length >= artist.albumCount || _loading) return; + if (artist!.albums!.length >= artist!.albumCount! || _loading) return; setState(() => _loading = true); //Fetch data - List data; + List? data; try { - data = await deezerAPI.discographyPage(artist.id, start: artist.albums.length); + data = await deezerAPI.discographyPage(artist!.id!, + start: artist!.albums!.length); } catch (e) { setState(() { _error = true; @@ -596,21 +627,21 @@ class _DiscographyScreenState extends State { //Save setState(() { - artist.albums.addAll(data); + artist!.albums!.addAll(data!); _loading = false; }); - } //Get album tile Widget _tile(Album a) => AlbumTile( - a, - onTap: () => Navigator.of(context).push(MaterialPageRoute(builder: (context) => AlbumDetails(a))), - onHold: () { - MenuSheet m = MenuSheet(context); - m.defaultAlbumMenu(a); - }, - ); + a, + onTap: () => Navigator.of(context) + .push(MaterialPageRoute(builder: (context) => AlbumDetails(a))), + onHold: () { + MenuSheet m = MenuSheet(context); + m.defaultAlbumMenu(a); + }, + ); Widget get _loadingWidget { if (_loading) @@ -622,10 +653,9 @@ class _DiscographyScreenState extends State { ), ); //Error - if (_error) - return ErrorScreen(); + if (_error) return ErrorScreen(); //Success - return Container(width: 0, height: 0,); + return const SizedBox(width: 0.0, height: 0.0); } @override @@ -645,129 +675,144 @@ class _DiscographyScreenState extends State { @override Widget build(BuildContext context) { - return DefaultTabController( - length: 3, - child: Builder(builder: (BuildContext context) { + length: 3, + child: Builder(builder: (BuildContext context) { + final TabController tabController = DefaultTabController.of(context)!; + tabController.addListener(() { + if (!tabController.indexIsChanging) { + //Load data if empty tabs + int nSingles = artist!.albums! + .where((a) => a.type == AlbumType.SINGLE) + .length; + int nFeatures = artist!.albums! + .where((a) => a.type == AlbumType.FEATURED) + .length; + if ((nSingles == 0 || nFeatures == 0) && !_loading) _load(); + } + }); - final TabController tabController = DefaultTabController.of(context); - tabController.addListener(() { - if (!tabController.indexIsChanging) { - //Load data if empty tabs - int nSingles = artist.albums.where((a) => a.type == AlbumType.SINGLE).length; - int nFeatures = artist.albums.where((a) => a.type == AlbumType.FEATURED).length; - if ((nSingles == 0 || nFeatures == 0) && !_loading) _load(); - } - }); - - return Scaffold( - appBar: FreezerAppBar( - 'Discography'.i18n, - bottom: TabBar( - tabs: [ - Tab(icon: Icon(Icons.album, semanticLabel: "Albums".i18n,)), - Tab(icon: Icon(Icons.audiotrack, semanticLabel: "Singles".i18n)), - Tab(icon: Icon(Icons.recent_actors, semanticLabel: "Featured".i18n,)) + return Scaffold( + appBar: FreezerAppBar( + 'Discography'.i18n, + bottom: TabBar( + tabs: [ + Tab( + icon: Icon( + Icons.album, + semanticLabel: "Albums".i18n, + )), + Tab( + icon: Icon(Icons.audiotrack, + semanticLabel: "Singles".i18n)), + Tab( + icon: Icon( + Icons.recent_actors, + semanticLabel: "Featured".i18n, + )) + ], + ), + height: 100.0, + ), + body: TabBarView( + children: [ + //Albums + ListView.builder( + controller: _controllers[0], + itemCount: artist!.albums!.length + 1, + itemBuilder: (context, i) { + if (i == artist!.albums!.length) return _loadingWidget; + if (artist!.albums![i].type == AlbumType.ALBUM) + return _tile(artist!.albums![i]); + return const SizedBox(width: 0.0, height: 0.0); + }, + ), + //Singles + ListView.builder( + controller: _controllers[1], + itemCount: artist!.albums!.length + 1, + itemBuilder: (context, i) { + if (i == artist!.albums!.length) return _loadingWidget; + if (artist!.albums![i].type == AlbumType.SINGLE) + return _tile(artist!.albums![i]); + return const SizedBox(width: 0.0, height: 0.0); + }, + ), + //Featured + ListView.builder( + controller: _controllers[2], + itemCount: artist!.albums!.length + 1, + itemBuilder: (context, i) { + if (i == artist!.albums!.length) return _loadingWidget; + if (artist!.albums![i].type == AlbumType.FEATURED) + return _tile(artist!.albums![i]); + return const SizedBox(width: 0.0, height: 0.0); + }, + ), ], ), - height: 100.0, - ), - body: TabBarView( - children: [ - //Albums - ListView.builder( - controller: _controllers[0], - itemCount: artist.albums.length + 1, - itemBuilder: (context, i) { - if (i == artist.albums.length) return _loadingWidget; - if (artist.albums[i].type == AlbumType.ALBUM) return _tile(artist.albums[i]); - return Container(width: 0, height: 0,); - }, - ), - //Singles - ListView.builder( - controller: _controllers[1], - itemCount: artist.albums.length + 1, - itemBuilder: (context, i) { - if (i == artist.albums.length) return _loadingWidget; - if (artist.albums[i].type == AlbumType.SINGLE) return _tile(artist.albums[i]); - return Container(width: 0, height: 0,); - }, - ), - //Featured - ListView.builder( - controller: _controllers[2], - itemCount: artist.albums.length + 1, - itemBuilder: (context, i) { - if (i == artist.albums.length) return _loadingWidget; - if (artist.albums[i].type == AlbumType.FEATURED) return _tile(artist.albums[i]); - return Container(width: 0, height: 0,); - }, - ), - ], - ), - ); - }) - ); + ); + })); } } class PlaylistDetails extends StatefulWidget { - - final Playlist playlist; - PlaylistDetails(this.playlist, {Key key}): super(key: key); + final Playlist? playlist; + PlaylistDetails(this.playlist, {Key? key}) : super(key: key); @override _PlaylistDetailsState createState() => _PlaylistDetailsState(); } class _PlaylistDetailsState extends State { - - Playlist playlist; + Playlist? playlist; bool _loading = false; bool _error = false; - Sorting _sort; + Sorting? _sort; ScrollController _scrollController = ScrollController(); //Get sorted playlist List get sorted { - List tracks = new List.from(playlist.tracks??[]); - switch (_sort.type) { + List tracks = new List.from(playlist!.tracks ?? []); + switch (_sort!.type) { case SortType.ALPHABETIC: - tracks.sort((a, b) => a.title.compareTo(b.title)); + tracks.sort((a, b) => a.title!.compareTo(b.title!)); break; case SortType.ARTIST: - tracks.sort((a, b) => a.artists[0].name.toLowerCase().compareTo(b.artists[0].name.toLowerCase())); + tracks.sort((a, b) => a.artists![0].name! + .toLowerCase() + .compareTo(b.artists![0].name!.toLowerCase())); break; case SortType.DATE_ADDED: - tracks.sort((a, b) => (a.addedDate??0) - (b.addedDate??0)); + tracks.sort((a, b) => (a.addedDate ?? 0) - (b.addedDate ?? 0)); break; case SortType.DEFAULT: default: break; } //Reverse - if (_sort.reverse) - return tracks.reversed.toList(); + if (_sort!.reverse!) return tracks.reversed.toList(); return tracks; } //Load tracks from api void _load() async { - if (playlist.tracks.length < (playlist.trackCount??playlist.tracks.length) && !_loading) { + if (playlist!.tracks!.length < + (playlist!.trackCount ?? playlist!.tracks!.length) && + !_loading) { setState(() => _loading = true); - int pos = playlist.tracks.length; + int pos = playlist!.tracks!.length; //Get another page of tracks - List tracks; + List? tracks; try { - tracks = await deezerAPI.playlistTracksPage(playlist.id, pos); + tracks = await deezerAPI.playlistTracksPage(playlist!.id, pos); } catch (e) { setState(() => _error = true); return; } setState(() { - playlist.tracks.addAll(tracks); + playlist!.tracks!.addAll(tracks!); _loading = false; }); } @@ -776,22 +821,20 @@ class _PlaylistDetailsState extends State { //Load cached playlist sorting void _restoreSort() async { //Find index - int index = Sorting.index(SortSourceTypes.PLAYLIST, id: playlist.id); - if (index == null) - return; + int? index = Sorting.index(SortSourceTypes.PLAYLIST, id: playlist!.id); + if (index == null) return; //Preload tracks - if (playlist.tracks.length < playlist.trackCount) { - playlist = await deezerAPI.fullPlaylist(playlist.id); + if (playlist!.tracks!.length < playlist!.trackCount!) { + playlist = await deezerAPI.fullPlaylist(playlist!.id); } setState(() => _sort = cache.sorts[index]); } - 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 { @@ -800,15 +843,15 @@ class _PlaylistDetailsState extends State { await cache.save(); //Preload for sorting - if (playlist.tracks.length < playlist.trackCount) { - playlist = await deezerAPI.fullPlaylist(playlist.id); + if (playlist!.tracks!.length < playlist!.trackCount!) { + playlist = await deezerAPI.fullPlaylist(playlist!.id); } } @override void initState() { playlist = widget.playlist; - _sort = Sorting(sourceType: SortSourceTypes.PLAYLIST, id: playlist.id); + _sort = Sorting(sourceType: SortSourceTypes.PLAYLIST, id: playlist!.id); //If scrolled past 90% load next tracks _scrollController.addListener(() { double off = _scrollController.position.maxScrollExtent * 0.90; @@ -817,17 +860,15 @@ class _PlaylistDetailsState extends State { } }); //Load if no tracks - if (playlist.tracks.length == 0) { + if (playlist!.tracks!.length == 0) { //Get correct metadata - deezerAPI.playlist(playlist.id) - .then((Playlist p) { + deezerAPI.playlist(playlist!.id).then((Playlist p) { setState(() { playlist = p; }); //Load tracks _load(); - }) - .catchError((e) { + }).catchError((e) { setState(() => _error = true); }); } @@ -840,232 +881,255 @@ class _PlaylistDetailsState extends State { @override Widget build(BuildContext context) { return Scaffold( - body: DraggableScrollbar.rrect( + body: DraggableScrollbar.rrect( + controller: _scrollController, + backgroundColor: Theme.of(context).primaryColor, + child: ListView( controller: _scrollController, - backgroundColor: Theme.of(context).primaryColor, - child: ListView( - controller: _scrollController, - children: [ - Container(height: 4.0,), - Padding( - padding: EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - mainAxisSize: MainAxisSize.max, - children: [ - CachedImage( - url: playlist.image.full, - height: MediaQuery.of(context).size.width / 2 - 8, - rounded: true, - fullThumb: true, - ), - Container( - width: MediaQuery.of(context).size.width / 2 - 8, - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - playlist.title, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.center, - maxLines: 3, - style: TextStyle( - fontSize: 20.0, - fontWeight: FontWeight.bold + children: [ + Container( + height: 4.0, + ), + Padding( + padding: EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + mainAxisSize: MainAxisSize.max, + children: [ + CachedImage( + url: playlist!.image!.full, + height: MediaQuery.of(context).size.width / 2 - 8, + rounded: true, + fullThumb: true, + ), + Container( + width: MediaQuery.of(context).size.width / 2 - 8, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + playlist!.title!, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + maxLines: 3, + style: TextStyle( + fontSize: 20.0, fontWeight: FontWeight.bold), + ), + Container(height: 4.0), + Text( + playlist!.user!.name ?? '', + overflow: TextOverflow.ellipsis, + maxLines: 2, + textAlign: TextAlign.center, + style: TextStyle( + color: Theme.of(context).primaryColor, + fontSize: 17.0), + ), + Container(height: 10.0), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.audiotrack, + size: 32.0, + semanticLabel: "Tracks".i18n, ), - ), - Container(height: 4.0), - Text( - playlist.user.name??'', - overflow: TextOverflow.ellipsis, - maxLines: 2, - textAlign: TextAlign.center, - style: TextStyle( - color: Theme.of(context).primaryColor, - fontSize: 17.0 + Container( + width: 8.0, ), - ), - Container(height: 10.0), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.audiotrack, - size: 32.0, - semanticLabel: "Tracks".i18n, - ), - Container(width: 8.0,), - Text((playlist.trackCount??playlist.tracks.length).toString(), style: TextStyle(fontSize: 16),) - ], - ), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.timelapse, - size: 32.0, - semanticLabel: "Duration".i18n, - ), - Container(width: 8.0,), - Text(playlist.durationString, style: TextStyle(fontSize: 16),) - ], - ), - ], - ), - ) - ], - ), - ), - if (playlist.description != null && playlist.description.length > 0) - FreezerDivider(), - if (playlist.description != null && playlist.description.length > 0) - Container( - child: Padding( - padding: EdgeInsets.all(6.0), - child: Text( - playlist.description ?? '', - maxLines: 4, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 16.0 + Text( + (playlist!.trackCount ?? playlist!.tracks!.length) + .toString(), + style: TextStyle(fontSize: 16), + ) + ], ), - ), - ) - ), - FreezerDivider(), - Container( - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - MakePlaylistOffline(playlist), - - if (playlist.user.name != deezerAPI.userName) - IconButton( - icon: Icon(playlist.library ? Icons.favorite : Icons.favorite_outline, size: 32, - semanticLabel: playlist.library ? "Unlove".i18n : "Love".i18n,), - onPressed: () async { - //Add to library - if (!playlist.library) { - await deezerAPI.addPlaylist(playlist.id); - Fluttertoast.showToast( - msg: 'Added to library'.i18n, - toastLength: Toast.LENGTH_SHORT, - gravity: ToastGravity.BOTTOM - ); - setState(() => playlist.library = true); - return; - } - //Remove - await deezerAPI.removePlaylist(playlist.id); - Fluttertoast.showToast( - msg: 'Playlist removed from library!'.i18n, - toastLength: Toast.LENGTH_SHORT, - gravity: ToastGravity.BOTTOM - ); - setState(() => playlist.library = false); - - }, - ), - IconButton( - icon: Icon(Icons.file_download, size: 32.0, semanticLabel: "Download".i18n,), - onPressed: () async { - if (await downloadManager.addOfflinePlaylist(playlist, private: false, context: context) != false) - MenuSheet(context).showDownloadStartedToast(); - }, - ), - PopupMenuButton( - child: Icon(Icons.sort, size: 32.0, semanticLabel: "Sort playlist".i18n,), - color: Theme.of(context).scaffoldBackgroundColor, - onSelected: (SortType s) async { - if (playlist.tracks.length < playlist.trackCount) { - //Preload whole playlist - playlist = await deezerAPI.fullPlaylist(playlist.id); - } - setState(() => _sort.type = s); - - //Save sort type to cache - int index = Sorting.index(SortSourceTypes.PLAYLIST, id: playlist.id); - if (index == null) { - cache.sorts.add(_sort); - } else { - cache.sorts[index] = _sort; - } - await cache.save(); - }, - itemBuilder: (context) => >[ - PopupMenuItem( - value: SortType.DEFAULT, - child: Text('Default'.i18n, style: popupMenuTextStyle()), - ), - PopupMenuItem( - value: SortType.ALPHABETIC, - child: Text('Alphabetic'.i18n, style: popupMenuTextStyle()), - ), - PopupMenuItem( - value: SortType.ARTIST, - child: Text('Artist'.i18n, style: popupMenuTextStyle()), - ), - PopupMenuItem( - value: SortType.DATE_ADDED, - child: Text('Date added'.i18n, style: popupMenuTextStyle()), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.timelapse, + size: 32.0, + semanticLabel: "Duration".i18n, + ), + Container( + width: 8.0, + ), + Text( + playlist!.durationString, + style: TextStyle(fontSize: 16), + ) + ], ), ], ), - IconButton( - icon: Icon(_sort.reverse ? FontAwesome5.sort_alpha_up : FontAwesome5.sort_alpha_down, - semanticLabel: _sort.reverse ? "Sort descending".i18n : "Sort ascending".i18n,), - onPressed: () => _reverse(), - ), - Container(width: 4.0) - ], - ), + ) + ], ), + ), + if (playlist!.description != null && + playlist!.description!.length > 0) FreezerDivider(), - ...List.generate(playlist.tracks.length, (i) { - Track t = sorted[i]; - return TrackTile( - t, - onTap: () { - Playlist p = Playlist( - title: playlist.title, - id: playlist.id, - tracks: sorted - ); - playerHelper.playFromPlaylist(p, t.id); + if (playlist!.description != null && + playlist!.description!.length > 0) + Container( + child: Padding( + padding: EdgeInsets.all(6.0), + child: Text( + playlist!.description ?? '', + maxLines: 4, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + style: TextStyle(fontSize: 16.0), + ), + )), + FreezerDivider(), + Container( + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + MakePlaylistOffline(playlist), + if (playlist!.user!.name != deezerAPI.userName) + IconButton( + icon: Icon( + playlist!.library! + ? Icons.favorite + : Icons.favorite_outline, + size: 32, + semanticLabel: + playlist!.library! ? "Unlove".i18n : "Love".i18n, + ), + onPressed: () async { + //Add to library + if (!playlist!.library!) { + await deezerAPI.addPlaylist(playlist!.id!); + Fluttertoast.showToast( + msg: 'Added to library'.i18n, + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM); + setState(() => playlist!.library = true); + return; + } + //Remove + await deezerAPI.removePlaylist(playlist!.id!); + Fluttertoast.showToast( + msg: 'Playlist removed from library!'.i18n, + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM); + setState(() => playlist!.library = false); + }, + ), + IconButton( + icon: Icon( + Icons.file_download, + size: 32.0, + semanticLabel: "Download".i18n, + ), + onPressed: () async { + if (await downloadManager.addOfflinePlaylist(playlist, + private: false, context: context) != + false) MenuSheet(context).showDownloadStartedToast(); }, - onHold: () { - MenuSheet m = MenuSheet(context); - m.defaultTrackMenu(t, options: [ - (playlist.user.id == deezerAPI.userId) ? m.removeFromPlaylist(t, playlist) : Container(width: 0, height: 0,) - ]); - } - ); - }), - if (_loading) - Padding( - padding: EdgeInsets.symmetric(vertical: 8.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - CircularProgressIndicator() + ), + PopupMenuButton( + child: Icon( + Icons.sort, + size: 32.0, + semanticLabel: "Sort playlist".i18n, + ), + color: Theme.of(context).scaffoldBackgroundColor, + onSelected: (SortType s) async { + if (playlist!.tracks!.length < playlist!.trackCount!) { + //Preload whole playlist + playlist = await deezerAPI.fullPlaylist(playlist!.id); + } + setState(() => _sort!.type = s); + + //Save sort type to cache + int? index = Sorting.index(SortSourceTypes.PLAYLIST, + id: playlist!.id); + if (index == null) { + cache.sorts.add(_sort); + } else { + cache.sorts[index] = _sort; + } + await cache.save(); + }, + itemBuilder: (context) => >[ + PopupMenuItem( + value: SortType.DEFAULT, + child: Text('Default'.i18n, style: popupMenuTextStyle()), + ), + PopupMenuItem( + value: SortType.ALPHABETIC, + child: + Text('Alphabetic'.i18n, style: popupMenuTextStyle()), + ), + PopupMenuItem( + value: SortType.ARTIST, + child: Text('Artist'.i18n, style: popupMenuTextStyle()), + ), + PopupMenuItem( + value: SortType.DATE_ADDED, + child: + Text('Date added'.i18n, style: popupMenuTextStyle()), + ), ], ), + IconButton( + icon: Icon( + _sort!.reverse! + ? FontAwesome5.sort_alpha_up + : FontAwesome5.sort_alpha_down, + semanticLabel: _sort!.reverse! + ? "Sort descending".i18n + : "Sort ascending".i18n, + ), + onPressed: () => _reverse(), + ), + Container(width: 4.0) + ], + ), + ), + FreezerDivider(), + ...List.generate(playlist!.tracks!.length, (i) { + Track t = sorted[i]; + return TrackTile(t, onTap: () { + Playlist p = Playlist( + title: playlist!.title, id: playlist!.id, tracks: sorted); + playerHelper.playFromPlaylist(p, t.id); + }, onHold: () { + MenuSheet m = MenuSheet(context); + m.defaultTrackMenu(t, options: [ + (playlist!.user!.id == deezerAPI.userId) + ? m.removeFromPlaylist(t, playlist) + : Container( + width: 0, + height: 0, + ) + ]); + }); + }), + if (_loading) + Padding( + padding: EdgeInsets.symmetric(vertical: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [CircularProgressIndicator()], ), - if (_error) - ErrorScreen() - ], - ), - ) - ); + ), + if (_error) ErrorScreen() + ], + ), + )); } } class MakePlaylistOffline extends StatefulWidget { - final Playlist playlist; - MakePlaylistOffline(this.playlist, {Key key}): super(key: key); + final Playlist? playlist; + MakePlaylistOffline(this.playlist, {Key? key}) : super(key: key); @override _MakePlaylistOfflineState createState() => _MakePlaylistOfflineState(); @@ -1093,23 +1157,30 @@ class _MakePlaylistOfflineState extends State { onChanged: (v) async { if (v) { //Add to offline - if (widget.playlist.user != null && widget.playlist.user.id != deezerAPI.userId) - await deezerAPI.addPlaylist(widget.playlist.id); - downloadManager.addOfflinePlaylist(widget.playlist, private: true); + if (widget.playlist!.user != null && + widget.playlist!.user!.id != deezerAPI.userId) + await deezerAPI.addPlaylist(widget.playlist!.id!); + downloadManager.addOfflinePlaylist(widget.playlist, + private: true); MenuSheet(context).showDownloadStartedToast(); setState(() { _offline = true; }); return; } - downloadManager.removeOfflinePlaylist(widget.playlist.id); - Fluttertoast.showToast(msg: "Playlist removed from offline!".i18n, gravity: ToastGravity.BOTTOM, toastLength: Toast.LENGTH_SHORT); + downloadManager.removeOfflinePlaylist(widget.playlist!.id); + Fluttertoast.showToast( + msg: "Playlist removed from offline!".i18n, + gravity: ToastGravity.BOTTOM, + toastLength: Toast.LENGTH_SHORT); setState(() { _offline = false; }); }, ), - Container(width: 4.0,), + Container( + width: 4.0, + ), Text( 'Offline'.i18n, style: TextStyle(fontSize: 16), @@ -1120,26 +1191,24 @@ class _MakePlaylistOfflineState extends State { } class ShowScreen extends StatefulWidget { - - final Show show; - ShowScreen(this.show, {Key key}): super(key: key); + final Show? show; + ShowScreen(this.show, {Key? key}) : super(key: key); @override _ShowScreenState createState() => _ShowScreenState(); } class _ShowScreenState extends State { - - Show _show; + Show? _show; bool _loading = true; bool _error = false; - List _episodes; + List? _episodes; Future _load() async { //Fetch - List e; + List? e; try { - e = await deezerAPI.allShowEpisodes(_show.id); + e = await deezerAPI.allShowEpisodes(_show!.id); } catch (e) { setState(() { _loading = false; @@ -1163,7 +1232,7 @@ class _ShowScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: FreezerAppBar(_show.name), + appBar: FreezerAppBar(_show!.name), body: ListView( children: [ Padding( @@ -1173,7 +1242,7 @@ class _ShowScreenState extends State { mainAxisSize: MainAxisSize.max, children: [ CachedImage( - url: _show.art.full, + url: _show!.art!.full, rounded: true, width: MediaQuery.of(context).size.width / 2 - 16, ), @@ -1182,25 +1251,19 @@ class _ShowScreenState extends State { child: Column( mainAxisSize: MainAxisSize.max, children: [ - Text( - _show.name, - maxLines: 2, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 20.0, - fontWeight: FontWeight.bold - ) - ), + Text(_show!.name!, + maxLines: 2, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 20.0, fontWeight: FontWeight.bold)), Container(height: 8.0), Text( - _show.description, + _show!.description!, maxLines: 6, overflow: TextOverflow.ellipsis, textAlign: TextAlign.center, - style: TextStyle( - fontSize: 16.0 - ), + style: TextStyle(fontSize: 16.0), ) ], ), @@ -1210,42 +1273,42 @@ class _ShowScreenState extends State { ), Container(height: 4.0), FreezerDivider(), - + //Error - if (_error) - ErrorScreen(), - + if (_error) ErrorScreen(), + //Loading if (_loading) Padding( padding: EdgeInsets.all(8.0), child: Row( mainAxisAlignment: MainAxisAlignment.center, - children: [ - CircularProgressIndicator() - ], + children: [CircularProgressIndicator()], ), ), //Data if (!_loading && !_error) - ...List.generate(_episodes.length, (i) { - ShowEpisode e = _episodes[i]; + ...List.generate(_episodes!.length, (i) { + ShowEpisode e = _episodes![i]; return ShowEpisodeTile( e, trailing: IconButton( - icon: Icon(Icons.more_vert, semanticLabel: "Options".i18n,), + icon: Icon( + Icons.more_vert, + semanticLabel: "Options".i18n, + ), onPressed: () { MenuSheet m = MenuSheet(context); - m.defaultShowEpisodeMenu(_show, e); + m.defaultShowEpisodeMenu(_show!, e); }, ), onTap: () async { - await playerHelper.playShowEpisode(_show, _episodes, index: i); + await playerHelper.playShowEpisode(_show!, _episodes!, + index: i); }, ); }) - ], ), ); diff --git a/lib/ui/downloads_screen.dart b/lib/ui/downloads_screen.dart index 699316c..eb2c1f2 100644 --- a/lib/ui/downloads_screen.dart +++ b/lib/ui/downloads_screen.dart @@ -11,22 +11,29 @@ import 'cached_image.dart'; import 'dart:async'; - class DownloadsScreen extends StatefulWidget { @override _DownloadsScreenState createState() => _DownloadsScreenState(); } class _DownloadsScreenState extends State { - List downloads = []; - StreamSubscription _stateSubscription; + StreamSubscription? _stateSubscription; //Sublists - List get downloading => downloads.where((d) => d.state == DownloadState.DOWNLOADING || d.state == DownloadState.POST).toList(); - List get queued => downloads.where((d) => d.state == DownloadState.NONE).toList(); - List get failed => downloads.where((d) => d.state == DownloadState.ERROR || d.state == DownloadState.DEEZER_ERROR).toList(); - List get finished => downloads.where((d) => d.state == DownloadState.DONE).toList(); + List get downloading => downloads + .where((d) => + d.state == DownloadState.DOWNLOADING || d.state == DownloadState.POST) + .toList(); + List get queued => + downloads.where((d) => d.state == DownloadState.NONE).toList(); + List get failed => downloads + .where((d) => + d.state == DownloadState.ERROR || + d.state == DownloadState.DEEZER_ERROR) + .toList(); + List get finished => + downloads.where((d) => d.state == DownloadState.DONE).toList(); Future _load() async { //Load downloads @@ -50,7 +57,9 @@ class _DownloadsScreenState extends State { 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 { @override void dispose() { - if (_stateSubscription != null) - _stateSubscription.cancel(); + _stateSubscription?.cancel(); _stateSubscription = null; super.dispose(); } @@ -71,24 +79,30 @@ class _DownloadsScreenState extends State { 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 { 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 { 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 { 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 { 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 { 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 { 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 { - List 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 { } //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 { @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])), ), - ), - ); - }, - ) - ); + ); + }, + )); } } diff --git a/lib/ui/elements.dart b/lib/ui/elements.dart index 4c3ee4c..4cc122d 100644 --- a/lib/ui/elements.dart +++ b/lib/ui/elements.dart @@ -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 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 - ); -} \ No newline at end of file + return TextStyle(color: settings.isDark ? Colors.white : Colors.black); +} diff --git a/lib/ui/error.dart b/lib/ui/error.dart index 1130762..2442d26 100644 --- a/lib/ui/error.dart +++ b/lib/ui/error.dart @@ -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(); diff --git a/lib/ui/home_screen.dart b/lib/ui/home_screen.dart index e8cebb6..881a3a3 100644 --- a/lib/ui/home_screen.dart +++ b/lib/ui/home_screen.dart @@ -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 { - 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 { 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 { _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 { 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); } } diff --git a/lib/ui/importer_screen.dart b/lib/ui/importer_screen.dart index 8923407..9ff529b 100644 --- a/lib/ui/importer_screen.dart +++ b/lib/ui/importer_screen.dart @@ -18,11 +18,10 @@ class SpotifyImporterV1 extends StatefulWidget { } class _SpotifyImporterV1State extends State { - - 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 { _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 { 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 { //Start importing Future _start() async { - List tracks = _data.toImporter(); - await importer.start(context, _data.name, _data.description, tracks); + List tracks = _data!.toImporter(); + await importer.start(context, _data!.name, _data!.description, tracks); } @override @@ -65,7 +63,9 @@ class _SpotifyImporterV1State extends State { body: ListView( children: [ 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 { ), ), 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 { _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: [ - CircularProgressIndicator() - ], + children: [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 { - bool _done = false; - StreamSubscription _subscription; + StreamSubscription? _subscription; @override void initState() { @@ -195,22 +198,19 @@ class _ImporterStatusScreenState extends State { 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 { padding: EdgeInsets.symmetric(vertical: 8.0), child: Row( mainAxisAlignment: MainAxisAlignment.center, - children: [ - CircularProgressIndicator() - ], + children: [CircularProgressIndicator()], ), ), - // Progress indicator - Container( - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - Row( - mainAxisSize: MainAxisSize.min, - children: [ - 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: [ - Icon(Icons.done, size: 24.0,), - Container(width: 4.0,), - Text('${importer.ok}', style: TextStyle(fontSize: 24.0),) - ], - ), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - 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: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + 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: [ + Icon( + Icons.done, + size: 24.0, + ), + Container( + width: 4.0, + ), + Text( + '${importer.ok}', + style: TextStyle(fontSize: 24.0), + ) + ], + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + 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 { 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 { - 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 { 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 { _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 { 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 { ), ), 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 { ), 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 { 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 { 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 { 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 { } 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 { - - String _url; + late String _url; bool _urlLoading = false; - spotify.Playlist _urlPlaylist; + spotify.Playlist? _urlPlaylist; bool _playlistsLoading = true; - List _playlists; + List? _playlists; @override void initState() { @@ -486,7 +498,7 @@ class _SpotifyImporterV2MainState extends State { //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 { 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 tracks = all.map((t) => ImporterTrack(t.name, t.artists.map((a) => a.name).toList(), isrc: t.externalIds.isrc)).toList(); + List 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 { @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 { 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 { ), 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 { ], ), 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); }, ); }) - ], - ) - ); + ], + )); } } diff --git a/lib/ui/library.dart b/lib/ui/library.dart index 1bf4d01..30c9b43 100644 --- a/lib/ui/library.dart +++ b/lib/ui/library.dart @@ -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 tracks = await deezerAPI.libraryShuffle(); + List tracks = (await deezerAPI.libraryShuffle())!; playerHelper.playFromTrackList( tracks, tracks[0].id, @@ -197,7 +197,7 @@ class LibraryScreen extends StatelessWidget { children: [CircularProgressIndicator()], ), ); - List data = snapshot.data; + List data = snapshot.data! as List; return Column( children: [ ListTile( @@ -246,38 +246,38 @@ class _LibraryTracksState extends State { bool _loading = false; bool _loadingTracks = false; ScrollController _scrollController = ScrollController(); - List tracks = []; - List allTracks = []; - int trackCount; - Sorting _sort = Sorting(sourceType: SortSourceTypes.TRACKS); + List? tracks = []; + List allTracks = []; + int? trackCount; + Sorting? _sort = Sorting(sourceType: SortSourceTypes.TRACKS); Playlist get _playlist => Playlist(id: deezerAPI.favoritesPlaylistId); List get _sorted { - List tcopy = List.from(tracks); - tcopy.sort((a, b) => a.addedDate.compareTo(b.addedDate)); - switch (_sort.type) { + List 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 { 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 { 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 { } //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 { if (_loadingTracks) return; _loadingTracks = true; - List _t; + List? _t; try { _t = await deezerAPI.playlistTracksPage( deezerAPI.favoritesPlaylistId, pos); @@ -344,7 +344,7 @@ class _LibraryTracksState extends State { return; } setState(() { - tracks.addAll(_t); + tracks!.addAll(_t!); _makeFavorite(); _loading = false; _loadingTracks = false; @@ -354,14 +354,14 @@ class _LibraryTracksState extends State { //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 { } Future _loadOffline() async { - Playlist p = + Playlist? p = await downloadManager.getPlaylist(deezerAPI.favoritesPlaylistId); if (p != null) setState(() { @@ -381,13 +381,13 @@ class _LibraryTracksState extends State { Future _loadAllOffline() async { List tracks = await downloadManager.allOfflineTracks(); setState(() { - allTracks = tracks; + allTracks = tracks as List; }); } //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 { _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 { 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 { 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 { )), 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 { }, 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 { 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 { }, onHold: () { MenuSheet m = MenuSheet(context); - m.defaultTrackMenu(t); + m.defaultTrackMenu(t!); }, ); }) @@ -580,34 +580,34 @@ class LibraryAlbums extends StatefulWidget { } class _LibraryAlbumsState extends State { - List _albums; - Sorting _sort = Sorting(sourceType: SortSourceTypes.ALBUMS); + List? _albums; + Sorting? _sort = Sorting(sourceType: SortSourceTypes.ALBUMS); ScrollController _scrollController = ScrollController(); List get _sorted { - List albums = List.from(_albums); - albums.sort((a, b) => a.favoriteDate.compareTo(b.favoriteDate)); - switch (_sort.type) { + List 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 { 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 { 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 { 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 { children: [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 { 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 { 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 albums = snapshot.data; + List albums = snapshot.data as List; return Column( children: [ FreezerDivider(), @@ -759,7 +759,7 @@ class _LibraryAlbumsState extends State { 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 { - List _artists; - Sorting _sort = Sorting(sourceType: SortSourceTypes.ARTISTS); + late List _artists; + Sorting? _sort = Sorting(sourceType: SortSourceTypes.ARTISTS); bool _loading = true; bool _error = false; ScrollController _scrollController = ScrollController(); List get _sorted { List 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 { Future _load() async { setState(() => _loading = true); //Fetch - List data; + List? data; try { data = await deezerAPI.getArtists(); } catch (e) {} @@ -828,9 +828,9 @@ class _LibraryArtistsState extends State { } 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 { @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 { 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 { 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 { - List _playlists; - Sorting _sort = Sorting(sourceType: SortSourceTypes.PLAYLISTS); + List? _playlists; + Sorting? _sort = Sorting(sourceType: SortSourceTypes.PLAYLISTS); ScrollController _scrollController = ScrollController(); String _filter = ''; List get _sorted { - List playlists = List.from(_playlists - .where((p) => p.title.toLowerCase().contains(_filter.toLowerCase()))); - switch (_sort.type) { + List 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 playlists = await deezerAPI.getPlaylists(); + List? 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 { @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 { 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 { 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 { 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 { 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 { height: 0, width: 0, ); - if (snapshot.data.length == 0) + if ((snapshot.data! as List).length == 0) return Container( height: 0, width: 0, ); - List playlists = snapshot.data; + List playlists = snapshot.data! as List; return Column( children: [ FreezerDivider(), @@ -1183,7 +1183,7 @@ class _LibraryPlaylistsState extends State { m.defaultPlaylistMenu(p, onRemove: () { setState(() { playlists.remove(p); - _playlists.remove(p); + _playlists!.remove(p); }); }); }, @@ -1229,7 +1229,7 @@ class _HistoryScreenState extends State { 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( diff --git a/lib/ui/login_screen.dart b/lib/ui/login_screen.dart index 9411ec1..e4d23a7 100644 --- a/lib/ui/login_screen.dart +++ b/lib/ui/login_screen.dart @@ -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 { - String _arl; - String _error; + late String _arl; + String? _error; //Initialize deezer etc Future _init() async { @@ -40,14 +40,14 @@ class _LoginWidgetState extends State { 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 { 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 { } // 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 { ), ), )); - 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 { - 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); diff --git a/lib/ui/lyrics.dart b/lib/ui/lyrics.dart index 8a7465f..f884026 100644 --- a/lib/ui/lyrics.dart +++ b/lib/ui/lyrics.dart @@ -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 { - 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 _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 { @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 { 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 { @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 { @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( + 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 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 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), + ], + ), + )), + ); } } diff --git a/lib/ui/menu.dart b/lib/ui/menu.dart index b3648a1..b2f4eae 100644 --- a/lib/ui/menu.dart +++ b/lib/ui/menu.dart @@ -3,7 +3,6 @@ import 'dart:async'; import 'package:freezer/main.dart'; import 'package:wakelock/wakelock.dart'; import 'package:flutter/material.dart'; -import 'package:audio_service/audio_service.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:freezer/api/cache.dart'; import 'package:freezer/api/deezer.dart'; @@ -20,7 +19,7 @@ import 'package:url_launcher/url_launcher.dart'; class MenuSheet { BuildContext context; - Function navigateCallback; + Function? navigateCallback; MenuSheet(this.context, {this.navigateCallback}); @@ -68,7 +67,7 @@ class MenuSheet { children: [ 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: [ 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 options = const [], Function onRemove}) { + {List 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 options = const [], Function onRemove}) { + {List 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 options = const [], Function onRemove}) { + {List 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 options = const [], Function onRemove, Function onUpdate}) { + {List 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 { 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 { 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 { 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 { } 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 { ), ); - List playlists = snapshot.data; + List playlists = snapshot.data! as List; 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 { } class CreatePlaylistDialog extends StatefulWidget { - final List tracks; + final List? 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 { - 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 { 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 { ), DropdownButton( value: _playlistType, - onChanged: (int v) { + onChanged: (int? v) { setState(() => _playlistType = v); }, items: [ @@ -888,15 +889,15 @@ class _CreatePlaylistDialogState extends State { 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 tracks = []; if (widget.tracks != null) { - tracks = widget.tracks.map((t) => t.id).toList(); + tracks = widget.tracks!.map((t) => t!.id).toList(); } await deezerAPI.createPlaylist(_title, status: _playlistType, diff --git a/lib/ui/player_bar.dart b/lib/ui/player_bar.dart index b9938a0..bcd6bae 100644 --- a/lib/ui/player_bar.dart +++ b/lib/ui/player_bar.dart @@ -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 { + 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: [ - 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: [ + StreamBuilder( + 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: [ + 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: [ - 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( + 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>( + 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 with SingleTickerProviderStateMixin { - AnimationController _controller; - Animation _animation; + late AnimationController _controller; + late Animation _animation; @override void initState() { _controller = AnimationController(vsync: this, duration: Duration(milliseconds: 200)); - _animation = Tween(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 @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 ), 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(), ); } }, diff --git a/lib/ui/player_screen.dart b/lib/ui/player_screen.dart index 5be6643..d7d6094 100644 --- a/lib/ui/player_screen.dart +++ b/lib/ui/player_screen.dart @@ -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 { - 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 { 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 { } 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 { @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 { 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 { children: [ 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 { 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 { )), 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 { ), 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 { children: [ 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 { 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 { )), 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 { //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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { @override void dispose() { - if (_currentItemSub != null) _currentItemSub.cancel(); + _currentItemSub?.cancel(); super.dispose(); } @@ -666,12 +661,12 @@ class _BigAlbumArtState extends State { 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 { //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 { - double _pos; + bool _seeking = false; + late StreamSubscription _subscription; + final position = ValueNotifier(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: [ - Padding( - padding: EdgeInsets.symmetric(horizontal: 24.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - 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: [ + Padding( + padding: EdgeInsets.symmetric(horizontal: 24.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ValueListenableBuilder( + valueListenable: position, + builder: (context, value, _) => Text( + _timeString(value), + style: TextStyle(fontSize: ScreenUtil().setSp(35)), + )), + StreamBuilder( + stream: audioHandler.mediaItem, + builder: (context, snapshot) => Text( + _timeString(snapshot.data?.duration ?? Duration.zero), + style: TextStyle(fontSize: ScreenUtil().setSp(35)), + )), + ], + ), + ), + ValueListenableBuilder( + 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 { - 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 _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 { }, 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), ), ); }, diff --git a/lib/ui/search.dart b/lib/ui/search.dart index 47c142f..2ac7eb1 100644 --- a/lib/ui/search.dart +++ b/lib/ui/search.dart @@ -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 { - 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 { //Load search suggestions Future _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 { 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 { //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 queue = cache.searchHistory + List queue = cache.searchHistory! .where((h) => h.type == SearchHistoryItemType.TRACK) - .map((t) => t.data) + .map((t) => t.data) .toList(); playerHelper.playFromTrackList( queue, @@ -370,12 +372,13 @@ class _SearchScreenState extends State { }, 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 { ...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 { 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 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 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 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 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 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 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 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 tracks; + final List? 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 albums; + final List? 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 playlists; + final List? 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 shows; + final List? 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 episodes; + final List? 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 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)); }, ); diff --git a/lib/ui/settings_screen.dart b/lib/ui/settings_screen.dart index fe801fa..077e1ad 100644 --- a/lib/ui/settings_screen.dart +++ b/lib/ui/settings_screen.dart @@ -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 { ), 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 { 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 { 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 { 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 { '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 { ], 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 { 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 { 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 { - AudioQuality _quality; + AudioQuality? _quality; @override void initState() { @@ -484,7 +480,7 @@ class _QualityPickerState extends State { } //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 { 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 { 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 { } 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 { - 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 { 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 { - double _downloadThreads = settings.downloadThreads.toDouble(); + double _downloadThreads = settings.downloadThreads!.toDouble(); TextEditingController _artistSeparatorController = TextEditingController(text: settings.artistSeparator); @@ -802,7 +798,7 @@ class _DownloadsSettingsState extends State { 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 { _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 { ), 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 { ), 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { (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 { 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 { '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 { 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 { 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 { child: Text('Log out & Exit'.i18n), onPressed: () async { try { - AudioService.stop(); + audioHandler.stop(); } catch (e) {} await logOut(); await DownloadManager.platform diff --git a/lib/ui/tiles.dart b/lib/ui/tiles.dart index ef87a7d..493a19b 100644 --- a/lib/ui/tiles.dart +++ b/lib/ui/tiles.dart @@ -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 { - - 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 { 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 { 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 { 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 { } 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: [ - 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: [ + 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: [ - 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: [ + 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: [ 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: [ - Padding( - padding: EdgeInsets.all(8.0), - child: CachedImage( + child: InkWell( + onTap: onTap as void Function()?, + onLongPress: onHold as void Function()?, + child: Column( + children: [ + 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)), ), ], ), diff --git a/lib/ui/updater.dart b/lib/ui/updater.dart index 3d24b7e..2c0c90a 100644 --- a/lib/ui/updater.dart +++ b/lib/ui/updater.dart @@ -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 { 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 { _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 { 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 { 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 { Column(children: [ ElevatedButton( child: Text('Download'.i18n + - ' (${_versionDownload.version})'), + ' (${_versionDownload!.version})'), onPressed: _buttonEnabled ? () { setState(() => _buttonEnabled = false); @@ -184,8 +184,8 @@ class _UpdaterScreenState extends State { } class FreezerVersions { - String latest; - List versions; + String? latest; + List? 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 downloads; + String? version; + String? changelog; + List? 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}); diff --git a/pubspec.lock b/pubspec.lock index c97ea0e..bd22dff 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -9,7 +9,7 @@ packages: source: hosted version: "24.0.0" analyzer: - dependency: transitive + dependency: "direct overridden" description: name: analyzer url: "https://pub.dartlang.org" @@ -28,14 +28,28 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.7.0" + version: "2.8.1" audio_service: dependency: "direct main" description: name: audio_service url: "https://pub.dartlang.org" source: hosted - version: "0.17.1" + version: "0.18.0-beta.0" + audio_service_platform_interface: + dependency: transitive + description: + name: audio_service_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.0" + audio_service_web: + dependency: transitive + description: + name: audio_service_web + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.0" audio_session: dependency: "direct main" description: @@ -203,7 +217,7 @@ packages: name: connectivity_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.0.1" convert: dependency: transitive description: @@ -256,9 +270,11 @@ packages: disk_space: dependency: "direct main" description: - name: disk_space - url: "https://pub.dartlang.org" - source: hosted + path: "." + ref: HEAD + resolved-ref: "1cd2555ff2b78ea3cd2667e484bd4f1d35ff6a19" + url: "https://github.com/phipps980316/disk_space" + source: git version: "0.1.1" draggable_scrollbar: dependency: "direct main" @@ -276,6 +292,13 @@ packages: url: "https://github.com/gladson97/equalizer.git" source: git version: "0.0.2+2" + equatable: + dependency: transitive + description: + name: equatable + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.3" fading_edge_scrollview: dependency: transitive description: @@ -351,20 +374,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "5.3.2" - flutter_isolate: - dependency: transitive - description: - name: flutter_isolate - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.0" flutter_local_notifications: dependency: "direct main" description: name: flutter_local_notifications url: "https://pub.dartlang.org" source: hosted - version: "8.1.1+1" + version: "8.1.1+2" flutter_local_notifications_platform_interface: dependency: transitive description: @@ -484,7 +500,7 @@ packages: name: i18n_extension url: "https://pub.dartlang.org" source: hosted - version: "4.0.0" + version: "4.1.1" infinite_listview: dependency: transitive description: @@ -766,7 +782,14 @@ packages: name: quick_actions url: "https://pub.dartlang.org" source: hosted - version: "0.5.0+1" + version: "0.6.0+6" + quick_actions_platform_interface: + dependency: transitive + description: + name: quick_actions_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" random_string: dependency: "direct main" description: @@ -786,8 +809,8 @@ packages: description: path: "." ref: main - resolved-ref: a138aa57796cd1c1b3359d461b49515e58948baa - url: "https://github.com/furgoose/Scrobblenaut.git" + resolved-ref: d819904911782da678f499fbda300ed69c76e833 + url: "https://github.com/Pato05/Scrobblenaut.git" source: git version: "3.0.0" share: @@ -822,7 +845,7 @@ packages: name: source_gen url: "https://pub.dartlang.org" source: hosted - version: "1.0.5" + version: "1.1.0" source_helper: dependency: transitive description: @@ -913,14 +936,14 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.1" + version: "0.4.2" timezone: dependency: transitive description: name: timezone url: "https://pub.dartlang.org" source: hosted - version: "0.7.0" + version: "0.8.0" timing: dependency: transitive description: @@ -962,7 +985,7 @@ packages: name: url_launcher url: "https://pub.dartlang.org" source: hosted - version: "6.0.5" + version: "6.0.9" url_launcher_linux: dependency: transitive description: @@ -983,7 +1006,7 @@ packages: name: url_launcher_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "2.0.4" url_launcher_web: dependency: transitive description: @@ -1074,7 +1097,7 @@ packages: name: win32 url: "https://pub.dartlang.org" source: hosted - version: "2.2.7" + version: "2.2.8" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index aee55ad..87ff039 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -18,15 +18,13 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev version: 0.6.14+1 environment: - sdk: ">=2.8.0 <3.0.0" + sdk: '>=2.12.0 <3.0.0' dependencies: flutter: sdk: flutter - flutter_localizations: sdk: flutter - spotify: ^0.6.0 flutter_displaymode: ^0.3.2 crypto: ^3.0.0 @@ -48,27 +46,28 @@ dependencies: package_info: ^2.0.2 move_to_background: ^1.0.1 flutter_local_notifications: ^8.1.1+1 - collection: ^1.14.12 - disk_space: ^0.1.1 + collection: ^1.15.0-nullsafety.4 + disk_space: + git: https://github.com/phipps980316/disk_space random_string: ^2.0.1 - async: ^2.4.1 + async: ^2.8.1 html: ^0.15.0 flutter_screenutil: ^5.0.0+2 marquee: ^2.2.0 flutter_cache_manager: ^3.0.0 cached_network_image: ^3.1.0 - i18n_extension: ^4.0.0 + i18n_extension: ^4.1.1 fluttericon: ^2.0.0 url_launcher: ^6.0.5 uni_links: ^0.5.1 share: ^2.0.4 numberpicker: ^2.1.1 - quick_actions: ^0.5.0+1 + quick_actions: ^0.6.0+6 photo_view: ^0.12.0 draggable_scrollbar: ^0.1.0 scrobblenaut: git: - url: https://github.com/furgoose/Scrobblenaut.git + url: https://github.com/Pato05/Scrobblenaut.git ref: main open_file: ^3.0.3 version: ^2.0.0 @@ -76,21 +75,20 @@ dependencies: google_fonts: ^2.1.0 equalizer: git: https://github.com/gladson97/equalizer.git - audio_session: ^0.1.6 - audio_service: ^0.17.1 + audio_service: ^0.18.0-beta.0 just_audio: git: url: https://github.com/ryanheise/just_audio.git ref: dev path: just_audio/ - # cupertino_icons: ^0.1.3 +dependency_overrides: + analyzer: ^2.0.0 dev_dependencies: flutter_test: sdk: flutter - json_serializable: ^5.0.0 build_runner: ^2.1.1