From f126ffef46a6dca70b3fb25cf376a4df2cec3cbc Mon Sep 17 00:00:00 2001 From: Pato05 Date: Wed, 18 Oct 2023 17:08:05 +0200 Subject: [PATCH] check if user can stream hq or flac use get_url api by default, and fall back to old generation if get_url failed start to write a better cachemanager to implement in all systems write in more appropriate directories on windows and linux improve check for Connectivity by adding a fallback (needed for example on linux systems without NetworkManager) allow to dynamically change track quality without rebuilding the object --- .gitignore | 2 +- lib/api/cache.dart | 5 + lib/api/cache.g.dart | 198 --- lib/api/cache_provider.dart | 104 ++ lib/api/deezer.dart | 61 +- lib/api/deezer_audio_source.dart | 68 +- lib/api/definitions.dart | 64 +- lib/api/definitions.g.dart | 1579 ----------------- lib/api/paths.dart | 49 + .../audio_handler.dart} | 347 +--- lib/api/player/player_helper.dart | 304 ++++ lib/main.dart | 26 +- lib/page_routes/fade.dart | 6 +- lib/settings.dart | 54 +- lib/settings.g.dart | 482 ----- lib/ui/android_auto.dart | 2 +- lib/ui/details_screens.dart | 487 +++-- lib/ui/fancy_scaffold.dart | 195 ++ lib/ui/home_screen.dart | 2 +- lib/ui/library.dart | 2 +- lib/ui/lyrics_screen.dart | 2 +- lib/ui/menu.dart | 7 +- lib/ui/player_bar.dart | 192 +- lib/ui/player_screen.dart | 6 +- lib/ui/queue_screen.dart | 2 +- lib/ui/search.dart | 4 +- lib/ui/settings_screen.dart | 55 +- lib/ui/tiles.dart | 2 +- 28 files changed, 1228 insertions(+), 3079 deletions(-) delete mode 100644 lib/api/cache.g.dart create mode 100644 lib/api/cache_provider.dart delete mode 100644 lib/api/definitions.g.dart create mode 100644 lib/api/paths.dart rename lib/api/{player.dart => player/audio_handler.dart} (69%) create mode 100644 lib/api/player/player_helper.dart delete mode 100644 lib/settings.g.dart create mode 100644 lib/ui/fancy_scaffold.dart diff --git a/.gitignore b/.gitignore index 27957d1..da6b2d5 100644 --- a/.gitignore +++ b/.gitignore @@ -51,7 +51,7 @@ android/app/.cxx .pub/ /build/ .gradle/ - +*.g.dart # Web related lib/generated_plugin_registrant.dart diff --git a/lib/api/cache.dart b/lib/api/cache.dart index 32e8817..964fd0f 100644 --- a/lib/api/cache.dart +++ b/lib/api/cache.dart @@ -57,6 +57,11 @@ class Cache { @HiveField(7) String? favoritesPlaylistId; + @HiveField(8, defaultValue: false) + bool canStreamHQ = false; + @HiveField(9, defaultValue: false) + bool canStreamLossless = false; + @JsonKey(includeToJson: false, includeFromJson: false) bool wakelock = false; diff --git a/lib/api/cache.g.dart b/lib/api/cache.g.dart deleted file mode 100644 index f7a001f..0000000 --- a/lib/api/cache.g.dart +++ /dev/null @@ -1,198 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'cache.dart'; - -// ************************************************************************** -// TypeAdapterGenerator -// ************************************************************************** - -class CacheAdapter extends TypeAdapter { - @override - final int typeId = 22; - - @override - Cache read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return Cache() - ..libraryTracks = - fields[0] == null ? [] : (fields[0] as List).cast() - ..loggedTrackId = fields[1] as String? - ..history = (fields[2] as List).cast() - ..sorts = (fields[3] as List).cast() - ..searchHistory = (fields[4] as List).cast() - ..threadsWarning = fields[5] as bool - ..lastUpdateCheck = fields[6] as DateTime? - ..favoritesPlaylistId = fields[7] as String?; - } - - @override - void write(BinaryWriter writer, Cache obj) { - writer - ..writeByte(8) - ..writeByte(0) - ..write(obj.libraryTracks) - ..writeByte(1) - ..write(obj.loggedTrackId) - ..writeByte(2) - ..write(obj.history) - ..writeByte(3) - ..write(obj.sorts) - ..writeByte(4) - ..write(obj.searchHistory) - ..writeByte(5) - ..write(obj.threadsWarning) - ..writeByte(6) - ..write(obj.lastUpdateCheck) - ..writeByte(7) - ..write(obj.favoritesPlaylistId); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is CacheAdapter && - runtimeType == other.runtimeType && - typeId == other.typeId; -} - -class SearchHistoryItemAdapter extends TypeAdapter { - @override - final int typeId = 20; - - @override - SearchHistoryItem read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return SearchHistoryItem( - fields[1] as dynamic, - fields[0] as SearchHistoryItemType, - ); - } - - @override - void write(BinaryWriter writer, SearchHistoryItem obj) { - writer - ..writeByte(2) - ..writeByte(0) - ..write(obj.type) - ..writeByte(1) - ..write(obj.data); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is SearchHistoryItemAdapter && - runtimeType == other.runtimeType && - typeId == other.typeId; -} - -class SearchHistoryItemTypeAdapter extends TypeAdapter { - @override - final int typeId = 21; - - @override - SearchHistoryItemType read(BinaryReader reader) { - switch (reader.readByte()) { - case 0: - return SearchHistoryItemType.track; - case 1: - return SearchHistoryItemType.album; - case 2: - return SearchHistoryItemType.artist; - case 3: - return SearchHistoryItemType.playlist; - default: - return SearchHistoryItemType.track; - } - } - - @override - void write(BinaryWriter writer, SearchHistoryItemType obj) { - switch (obj) { - case SearchHistoryItemType.track: - writer.writeByte(0); - break; - case SearchHistoryItemType.album: - writer.writeByte(1); - break; - case SearchHistoryItemType.artist: - writer.writeByte(2); - break; - case SearchHistoryItemType.playlist: - writer.writeByte(3); - break; - } - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is SearchHistoryItemTypeAdapter && - runtimeType == other.runtimeType && - typeId == other.typeId; -} - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -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) => - e == null ? null : 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'] == null - ? null - : DateTime.parse(json['lastUpdateCheck'] as String) - ..favoritesPlaylistId = json['favoritesPlaylistId'] as String?; - -Map _$CacheToJson(Cache instance) => { - 'libraryTracks': instance.libraryTracks, - 'history': instance.history.map((e) => e.toJson()).toList(), - 'sorts': instance.sorts.map((e) => e?.toJson()).toList(), - 'searchHistory2': instance.searchHistory.map((e) => e.toJson()).toList(), - 'threadsWarning': instance.threadsWarning, - 'lastUpdateCheck': instance.lastUpdateCheck?.toIso8601String(), - 'favoritesPlaylistId': instance.favoritesPlaylistId, - }; - -SearchHistoryItem _$SearchHistoryItemFromJson(Map json) => - SearchHistoryItem( - json['data'], - SearchHistoryItem._searchHistoryItemTypeFromJson(json['type'] as int), - ); - -Map _$SearchHistoryItemToJson(SearchHistoryItem instance) => - { - 'type': SearchHistoryItem._searchHistoryItemTypeToJson(instance.type), - 'data': instance.data, - }; diff --git a/lib/api/cache_provider.dart b/lib/api/cache_provider.dart new file mode 100644 index 0000000..2edf422 --- /dev/null +++ b/lib/api/cache_provider.dart @@ -0,0 +1,104 @@ +// ignore_for_file: implementation_imports + +import 'package:dart_blowfish/dart_blowfish.dart'; +import 'package:flutter_cache_manager/flutter_cache_manager.dart'; +import 'package:flutter_cache_manager/src/storage/cache_object.dart'; +import 'package:freezer/type_adapters/uri.dart'; +import 'package:hive_flutter/hive_flutter.dart'; + +class FreezerCacheManager extends CacheManager { + static const key = 'freezerImageCache'; + + void init(String path, {String? boxName}) { + _instance = + FreezerCacheManager._(FreezerCacheInfoRepository(boxName ?? key, path)); + } + + static late final FreezerCacheManager _instance; + factory FreezerCacheManager() => _instance; + + FreezerCacheManager._(FreezerCacheInfoRepository repo) + : super(Config(key, repo: repo)); +} + +class FreezerCacheInfoRepository extends CacheInfoRepository { + final String boxName; + final String path; + late final LazyBox _box; + bool _isOpen; + FreezerCacheInfoRepository(this.boxName, this.path); + + @override + Future exists() => Hive.boxExists(boxName, path: path); + + @override + Future open() async { + if (_isOpen) return true; + + _box = await Hive.openLazyBox(boxName, path: path); + _isOpen = true; + return true; + } + + @override + Future updateOrInsert(CacheObject cacheObject) { + if (cacheObject.id == null) { + return insert(cacheObject); + } else { + return update(cacheObject); + } + } + + @override + Future insert(CacheObject cacheObject, + {bool setTouchedToNow = true}) { + final id = await _box.add(cacheObject); + } + + /// Gets a [CacheObject] by [key] + @override + Future get(String key); + + /// Deletes a cache object by [id] + @override + Future delete(int id); + + /// Deletes items with [ids] from the repository + @override + Future deleteAll(Iterable ids); + + /// Updates an existing [cacheObject] + @override + Future update(CacheObject cacheObject, {bool setTouchedToNow = true}); + + /// Gets the list of all objects in the cache + @override + Future> getAllObjects(); + + /// Gets the list of [CacheObject] that can be removed if the repository is over capacity. + /// + /// The exact implementation is up to the repository, but implementations should + /// return a preferred list of items. For example, the least recently accessed + @override + Future> getObjectsOverCapacity(int capacity); + + /// Returns a list of [CacheObject] that are older than [maxAge] + @override + Future> getOldObjects(Duration maxAge); + + /// Close the connection to the repository. If this is the last connection + /// to the repository it will return true and the repository is truly + /// closed. If there are still open connections it will return false; + @override + Future close(); + + /// Deletes the cache data file including all cache data. + @override + Future deleteDataFile(); +} + +class CacheObjectAdapter extends TypeAdapter { + @override + // TODO: implement typeId + int get typeId => throw UnimplementedError(); +} \ No newline at end of file diff --git a/lib/api/deezer.dart b/lib/api/deezer.dart index c8c4bca..742f2d9 100644 --- a/lib/api/deezer.dart +++ b/lib/api/deezer.dart @@ -26,6 +26,9 @@ class DeezerAPI { String? userName; String? favoritesPlaylistId; String? sid; + late String licenseToken; + late bool canStreamLossless; + late bool canStreamHQ; Future? _authorizing; @@ -125,10 +128,21 @@ class DeezerAPI { userId = data['results']['USER']['USER_ID'].toString(); userName = data['results']['USER']['BLOG_NAME']; favoritesPlaylistId = data['results']['USER']['LOVEDTRACKS_ID']; + canStreamHQ = data['results']['USER']['OPTIONS']['web_hq'] || + data['results']['USER']['OPTIONS']['mobile_hq']; + canStreamLossless = data['results']['USER']['OPTIONS'] + ['web_lossless'] || + data['results']['USER']['OPTIONS']['mobile_lossless']; + licenseToken = + data['results']['USER']['OPTIONS']['license_token'] as String; + + settings.checkQuality(canStreamHQ, canStreamLossless); + cache.canStreamHQ = canStreamHQ; + cache.canStreamLossless = canStreamLossless; if (cache.favoritesPlaylistId != favoritesPlaylistId) { cache.favoritesPlaylistId = favoritesPlaylistId; - await cache.save(); } + await cache.save(); return true; } } catch (e) { @@ -191,6 +205,48 @@ class DeezerAPI { } } + Future getTrackUrl( + String trackToken, String format) async => + (await getTracksUrl([trackToken], format))[0]; + + Future> getTracksUrl( + List trackTokens, String format) async { + final response = await http.post( + Uri.https('media.deezer.com', '/v1/get_url'), + body: jsonEncode({ + "license_token": licenseToken, + "media": [ + { + "type": "FULL", + "formats": [ + {"cipher": "BF_CBC_STRIPE", "format": format} + ], + } + ], + "track_tokens": trackTokens, + }), + headers: headers, + ); + final data = (jsonDecode(response.body) as Map)['data'] as List; + return data.map((data) { + if (data['errors'] != null) { + if (data['errors'][0]['code'] == 2002) { + return GetTrackUrlResponse(error: 'Wrong geolocation'); + } + + return GetTrackUrlResponse( + error: (data['errors'][0] as Map).toString()); + } + + if (data['media'] == null) return GetTrackUrlResponse(); + return GetTrackUrlResponse( + sources: (data['media'][0]['sources'] as List) + .map((e) => TrackUrlSource.fromPrivateJson( + (e as Map).cast())) + .toList(growable: false)); + }).toList(growable: false); + } + //Search Future search(String? query) async { Map data = await callApi('deezer.pageSearch', @@ -360,9 +416,6 @@ class DeezerAPI { Map data = await callApi('deezer.pageSmartTracklist', params: {'smarttracklist_id': id}); - getExternalStorageDirectory().then((value) => - File(join(value!.path, 'test.json')).writeAsString(jsonEncode(data))); - return SmartTrackList.fromPrivateJson(data['results'], songsJson: data['results']['SONGS']); } diff --git a/lib/api/deezer_audio_source.dart b/lib/api/deezer_audio_source.dart index 00cbe35..598c985 100644 --- a/lib/api/deezer_audio_source.dart +++ b/lib/api/deezer_audio_source.dart @@ -14,6 +14,7 @@ import 'package:crypto/crypto.dart'; import 'package:http/http.dart' as http; import 'package:dart_blowfish/dart_blowfish.dart'; import 'package:logging/logging.dart'; +import 'package:scrobblenaut/lastfm.dart'; typedef _IsolateMessage = ( Stream> source, @@ -32,6 +33,8 @@ class DeezerAudioSource extends StreamAudioSource { late String _trackId; late String _md5origin; late String _mediaVersion; + final String trackToken; + final int trackTokenExpiration; final StreamInfoCallback? onStreamObtained; // some cache @@ -45,6 +48,8 @@ class DeezerAudioSource extends StreamAudioSource { required String trackId, required String md5origin, required String mediaVersion, + required this.trackToken, + required this.trackTokenExpiration, this.onStreamObtained, }) { _getQuality = getQuality; @@ -119,6 +124,8 @@ class DeezerAudioSource extends StreamAudioSource { } Future _qualityFallback() async { + // only use url generation with MP3_128 + _currentQuality = AudioQuality.MP3_128; final genUri = _generateTrackUri(); final req = await http.head(genUri, headers: { @@ -267,6 +274,32 @@ class DeezerAudioSource extends StreamAudioSource { } } + Future _getUrl() async { + final String actualTrackToken; + if (DateTime.now().millisecondsSinceEpoch ~/ 1000 > trackTokenExpiration) { + if (!deezerAPI.canStreamHQ && !deezerAPI.canStreamLossless) { + _logger.fine('using old url generation, token expired.'); + } + final newTrack = await deezerAPI.track(trackId); + actualTrackToken = newTrack.trackToken!; + } else { + actualTrackToken = trackToken; + } + + final res = await deezerAPI.getTrackUrl( + actualTrackToken, _currentQuality!.toDeezerQualityString()); + if (res.error != null) { + _logger.warning('Error while getting track url: ${res.error!}'); + return null; + } + if (res.sources == null) { + _logger.warning('Error while getting track url: No sources!'); + return null; + } + + return Uri.parse(res.sources![0].url); + } + @override Future request([int? start, int? end]) async { start ??= 0; @@ -289,21 +322,34 @@ class DeezerAudioSource extends StreamAudioSource { } // determine quality to use - _currentQuality = _getQuality.call(); + final newQuality = _getQuality.call(); - final Uri uri; - if (_downloadUrl != null) { - uri = _downloadUrl!; - } else { - try { - _downloadUrl = uri = await _fallbackUrl(); - } on QualityException { - rethrow; + if (_downloadUrl != null && _currentQuality != newQuality) { + // update currentUrl to get tracks with new quality + _downloadUrl = null; + } + + _currentQuality = newQuality; + + if (_downloadUrl == null) { + final gottenUrl = await _getUrl(); + if (gottenUrl != null) { + _downloadUrl = gottenUrl; + } else { + _logger.warning('falling back to old url generation!'); + + // OLD URL GENERATION + try { + _downloadUrl = await _fallbackUrl(); + } on QualityException { + rethrow; + } } } - _logger.fine("Downloading track from ${uri.toString()}"); + + _logger.fine("Downloading track from ${_downloadUrl!.toString()}"); final int deezerStart = start - (start % 2048); - final req = http.Request('GET', uri) + final req = http.Request('GET', _downloadUrl!) ..headers.addAll({ 'User-Agent': deezerAPI.headers['User-Agent']!, 'Accept-Language': '*', diff --git a/lib/api/definitions.dart b/lib/api/definitions.dart index a57d0fb..ba47670 100644 --- a/lib/api/definitions.dart +++ b/lib/api/definitions.dart @@ -5,6 +5,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:audio_service/audio_service.dart'; +import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:freezer/api/cache.dart'; import 'package:freezer/page_routes/blur_slide.dart'; import 'package:freezer/page_routes/fade.dart'; @@ -57,6 +58,10 @@ class Track extends DeezerMediaItem { //Date added to playlist / favorites int? addedDate; + // information for playback + String? trackToken; + int? trackTokenExpiration; + @HiveField(13) List? playbackDetails; @@ -75,11 +80,13 @@ class Track extends DeezerMediaItem { this.diskNumber, this.explicit, this.addedDate, + this.trackToken, + this.trackTokenExpiration, }); String get artistString => artists == null ? "" - : artists!.map((art) => art.name).join(', '); + : artists!.map((art) => art.name!).join(', '); String get durationString => durationAsString(duration!); static String durationAsString(Duration duration) { @@ -88,6 +95,9 @@ class Track extends DeezerMediaItem { //MediaItem Future toMediaItem() async { + DefaultCacheManager() + .getFileFromCache(albumArt!.full) + .then((i) => print('file: ${i?.file.uri}')); return MediaItem( title: title!, album: album!.title!, @@ -104,6 +114,8 @@ class Track extends DeezerMediaItem { "thumb": albumArt!.thumb, "lyrics": jsonEncode(lyrics!.toJson()), "albumId": album!.id, + "trackToken": trackToken, + "trackTokenExpiration": trackTokenExpiration, "artists": jsonEncode(artists!.map((art) => art.toJson()).toList()) }); @@ -151,21 +163,24 @@ class Track extends DeezerMediaItem { title = "${json['SNG_TITLE']} ${json['VERSION']}"; } return Track( - id: json['SNG_ID'].toString(), - title: title!, - duration: Duration(seconds: int.parse(json['DURATION'])), - albumArt: DeezerImageDetails(json['ALB_PICTURE']), - album: Album.fromPrivateJson(json), - artists: (json['ARTISTS'] ?? [json]) - .map((dynamic art) => Artist.fromPrivateJson(art)) - .toList(), - trackNumber: int.parse((json['TRACK_NUMBER'] ?? '0').toString()), - playbackDetails: [json['MD5_ORIGIN'], json['MEDIA_VERSION']], - lyrics: Lyrics(id: json['LYRICS_ID'].toString()), - favorite: favorite, - diskNumber: int.parse(json['DISK_NUMBER'] ?? '1'), - explicit: (json['EXPLICIT_LYRICS'].toString() == '1') ? true : false, - addedDate: json['DATE_ADD']); + id: json['SNG_ID'].toString(), + title: title!, + duration: Duration(seconds: int.parse(json['DURATION'])), + albumArt: DeezerImageDetails(json['ALB_PICTURE']), + album: Album.fromPrivateJson(json), + artists: (json['ARTISTS'] ?? [json]) + .map((dynamic art) => Artist.fromPrivateJson(art)) + .toList(), + trackNumber: int.parse((json['TRACK_NUMBER'] ?? '0').toString()), + playbackDetails: [json['MD5_ORIGIN'], json['MEDIA_VERSION']], + lyrics: Lyrics(id: json['LYRICS_ID'].toString()), + favorite: favorite, + diskNumber: int.parse(json['DISK_NUMBER'] ?? '1'), + explicit: (json['EXPLICIT_LYRICS'].toString() == '1') ? true : false, + addedDate: json['DATE_ADD'], + trackToken: json['TRACK_TOKEN'], + trackTokenExpiration: json['TRACK_TOKEN_EXPIRE'], + ); } Map toSQL({off = false}) => { 'id': id, @@ -413,7 +428,7 @@ class Artist extends DeezerMediaItem { picture: json.containsKey('ART_PICTURE') ? DeezerImageDetails(json['ART_PICTURE'], type: 'artist') : null, - albumCount: albumsJson['total'], + albumCount: albumsJson['total'] as int?, albums: (albumsJson['data'] ?? []) .map((dynamic data) => Album.fromPrivateJson(data)) .toList(), @@ -1540,3 +1555,18 @@ extension GetId on FlowConfig { }[this]!; } } + +class TrackUrlSource { + final String provider; + final String url; + TrackUrlSource({required this.provider, required this.url}); + + factory TrackUrlSource.fromPrivateJson(Map json) => + TrackUrlSource(provider: json['provider'], url: json['url']); +} + +class GetTrackUrlResponse { + final List? sources; + final String? error; + GetTrackUrlResponse({this.sources, this.error}); +} diff --git a/lib/api/definitions.g.dart b/lib/api/definitions.g.dart deleted file mode 100644 index 9cc5e8d..0000000 --- a/lib/api/definitions.g.dart +++ /dev/null @@ -1,1579 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'definitions.dart'; - -// ************************************************************************** -// TypeAdapterGenerator -// ************************************************************************** - -class TrackAdapter extends TypeAdapter { - @override - final int typeId = 5; - - @override - Track read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return Track( - id: fields[0] as String, - title: fields[1] as String?, - duration: fields[4] as Duration?, - album: fields[2] as Album?, - playbackDetails: (fields[13] as List?)?.cast(), - albumArt: fields[5] as DeezerImageDetails?, - artists: (fields[3] as List?)?.cast(), - trackNumber: fields[6] as int?, - offline: fields[7] as bool?, - lyrics: fields[8] as Lyrics?, - favorite: fields[9] as bool?, - diskNumber: fields[10] as int?, - explicit: fields[11] as bool?, - addedDate: fields[12] as int?, - ); - } - - @override - void write(BinaryWriter writer, Track obj) { - writer - ..writeByte(14) - ..writeByte(0) - ..write(obj.id) - ..writeByte(1) - ..write(obj.title) - ..writeByte(2) - ..write(obj.album) - ..writeByte(3) - ..write(obj.artists) - ..writeByte(4) - ..write(obj.duration) - ..writeByte(5) - ..write(obj.albumArt) - ..writeByte(6) - ..write(obj.trackNumber) - ..writeByte(7) - ..write(obj.offline) - ..writeByte(8) - ..write(obj.lyrics) - ..writeByte(9) - ..write(obj.favorite) - ..writeByte(10) - ..write(obj.diskNumber) - ..writeByte(11) - ..write(obj.explicit) - ..writeByte(12) - ..write(obj.addedDate) - ..writeByte(13) - ..write(obj.playbackDetails); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is TrackAdapter && - runtimeType == other.runtimeType && - typeId == other.typeId; -} - -class AlbumAdapter extends TypeAdapter { - @override - final int typeId = 12; - - @override - Album read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return Album( - id: fields[0] as String?, - title: fields[1] as String?, - art: fields[4] as DeezerImageDetails?, - artists: (fields[2] as List?)?.cast(), - tracks: (fields[3] as List?)?.cast(), - fans: fields[5] as int?, - offline: fields[6] as bool?, - library: fields[7] as bool?, - type: fields[8] as AlbumType?, - releaseDate: fields[9] as String?, - favoriteDate: fields[10] as String?, - ); - } - - @override - void write(BinaryWriter writer, Album obj) { - writer - ..writeByte(11) - ..writeByte(0) - ..write(obj.id) - ..writeByte(1) - ..write(obj.title) - ..writeByte(2) - ..write(obj.artists) - ..writeByte(3) - ..write(obj.tracks) - ..writeByte(4) - ..write(obj.art) - ..writeByte(5) - ..write(obj.fans) - ..writeByte(6) - ..write(obj.offline) - ..writeByte(7) - ..write(obj.library) - ..writeByte(8) - ..write(obj.type) - ..writeByte(9) - ..write(obj.releaseDate) - ..writeByte(10) - ..write(obj.favoriteDate); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is AlbumAdapter && - runtimeType == other.runtimeType && - typeId == other.typeId; -} - -class ArtistAdapter extends TypeAdapter { - @override - final int typeId = 11; - - @override - Artist read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return Artist( - id: fields[0] as String, - name: fields[1] as String?, - albums: (fields[2] as List?)?.cast(), - albumCount: fields[3] as int?, - topTracks: (fields[4] as List?)?.cast(), - picture: fields[5] as DeezerImageDetails?, - fans: fields[6] as int?, - offline: fields[7] as bool?, - library: fields[8] as bool?, - radio: fields[9] as bool?, - favoriteDate: fields[10] as String?, - highlight: fields[11] as ArtistHighlight?, - ); - } - - @override - void write(BinaryWriter writer, Artist obj) { - writer - ..writeByte(12) - ..writeByte(0) - ..write(obj.id) - ..writeByte(1) - ..write(obj.name) - ..writeByte(2) - ..write(obj.albums) - ..writeByte(3) - ..write(obj.albumCount) - ..writeByte(4) - ..write(obj.topTracks) - ..writeByte(5) - ..write(obj.picture) - ..writeByte(6) - ..write(obj.fans) - ..writeByte(7) - ..write(obj.offline) - ..writeByte(8) - ..write(obj.library) - ..writeByte(9) - ..write(obj.radio) - ..writeByte(10) - ..write(obj.favoriteDate) - ..writeByte(11) - ..write(obj.highlight); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is ArtistAdapter && - runtimeType == other.runtimeType && - typeId == other.typeId; -} - -class PlaylistAdapter extends TypeAdapter { - @override - final int typeId = 9; - - @override - Playlist read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return Playlist( - id: fields[0] as String, - title: fields[1] as String?, - tracks: (fields[2] as List?)?.cast(), - image: fields[3] as ImageDetails?, - trackCount: fields[5] as int?, - duration: fields[4] as Duration?, - user: fields[6] as User?, - fans: fields[7] as int?, - library: fields[8] as bool?, - description: fields[9] as String?, - ); - } - - @override - void write(BinaryWriter writer, Playlist obj) { - writer - ..writeByte(10) - ..writeByte(0) - ..write(obj.id) - ..writeByte(1) - ..write(obj.title) - ..writeByte(2) - ..write(obj.tracks) - ..writeByte(3) - ..write(obj.image) - ..writeByte(4) - ..write(obj.duration) - ..writeByte(5) - ..write(obj.trackCount) - ..writeByte(6) - ..write(obj.user) - ..writeByte(7) - ..write(obj.fans) - ..writeByte(8) - ..write(obj.library) - ..writeByte(9) - ..write(obj.description); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is PlaylistAdapter && - runtimeType == other.runtimeType && - typeId == other.typeId; -} - -class UserAdapter extends TypeAdapter { - @override - final int typeId = 10; - - @override - User read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return User( - id: fields[0] as String?, - name: fields[1] as String?, - picture: fields[2] as DeezerImageDetails?, - ); - } - - @override - void write(BinaryWriter writer, User obj) { - writer - ..writeByte(3) - ..writeByte(0) - ..write(obj.id) - ..writeByte(1) - ..write(obj.name) - ..writeByte(2) - ..write(obj.picture); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is UserAdapter && - runtimeType == other.runtimeType && - typeId == other.typeId; -} - -class DeezerImageDetailsAdapter extends TypeAdapter { - @override - final int typeId = 6; - - @override - DeezerImageDetails read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return DeezerImageDetails( - fields[1] as String, - type: fields[0] as String, - ); - } - - @override - void write(BinaryWriter writer, DeezerImageDetails obj) { - writer - ..writeByte(2) - ..writeByte(0) - ..write(obj.type) - ..writeByte(1) - ..write(obj.md5); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is DeezerImageDetailsAdapter && - runtimeType == other.runtimeType && - typeId == other.typeId; -} - -class LyricsAdapter extends TypeAdapter { - @override - final int typeId = 7; - - @override - Lyrics read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return Lyrics( - id: fields[0] as String?, - writers: fields[1] as String?, - lyrics: (fields[2] as List?)?.cast(), - ); - } - - @override - void write(BinaryWriter writer, Lyrics obj) { - writer - ..writeByte(3) - ..writeByte(0) - ..write(obj.id) - ..writeByte(1) - ..write(obj.writers) - ..writeByte(2) - ..write(obj.lyrics); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is LyricsAdapter && - runtimeType == other.runtimeType && - typeId == other.typeId; -} - -class LyricAdapter extends TypeAdapter { - @override - final int typeId = 8; - - @override - Lyric read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return Lyric( - offset: fields[0] as Duration?, - text: fields[1] as String?, - lrcTimestamp: fields[2] as String?, - ); - } - - @override - void write(BinaryWriter writer, Lyric obj) { - writer - ..writeByte(3) - ..writeByte(0) - ..write(obj.offset) - ..writeByte(1) - ..write(obj.text) - ..writeByte(2) - ..write(obj.lrcTimestamp); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is LyricAdapter && - runtimeType == other.runtimeType && - typeId == other.typeId; -} - -class QueueSourceAdapter extends TypeAdapter { - @override - final int typeId = 32; - - @override - QueueSource read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return QueueSource( - id: fields[0] as String?, - text: fields[1] as String?, - source: fields[2] as String?, - ); - } - - @override - void write(BinaryWriter writer, QueueSource obj) { - writer - ..writeByte(3) - ..writeByte(0) - ..write(obj.id) - ..writeByte(1) - ..write(obj.text) - ..writeByte(2) - ..write(obj.source); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is QueueSourceAdapter && - runtimeType == other.runtimeType && - typeId == other.typeId; -} - -class SmartTrackListAdapter extends TypeAdapter { - @override - final int typeId = 4; - - @override - SmartTrackList read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return SmartTrackList( - id: fields[0] as String?, - title: fields[1] as String?, - description: fields[3] as String?, - trackCount: fields[4] as int?, - tracks: (fields[5] as List?)?.cast(), - cover: (fields[6] as List?)?.cast(), - subtitle: fields[2] as String?, - flowConfig: fields[7] as String?, - ); - } - - @override - void write(BinaryWriter writer, SmartTrackList obj) { - writer - ..writeByte(8) - ..writeByte(0) - ..write(obj.id) - ..writeByte(1) - ..write(obj.title) - ..writeByte(2) - ..write(obj.subtitle) - ..writeByte(3) - ..write(obj.description) - ..writeByte(4) - ..write(obj.trackCount) - ..writeByte(5) - ..write(obj.tracks) - ..writeByte(6) - ..write(obj.cover) - ..writeByte(7) - ..write(obj.flowConfig); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is SmartTrackListAdapter && - runtimeType == other.runtimeType && - typeId == other.typeId; -} - -class HomePageAdapter extends TypeAdapter { - @override - final int typeId = 33; - - @override - HomePage read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return HomePage( - sections: (fields[0] as List).cast(), - lastUpdated: fields[1] as DateTime?, - ); - } - - @override - void write(BinaryWriter writer, HomePage obj) { - writer - ..writeByte(2) - ..writeByte(0) - ..write(obj.sections) - ..writeByte(1) - ..write(obj.lastUpdated); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is HomePageAdapter && - runtimeType == other.runtimeType && - typeId == other.typeId; -} - -class HomePageSectionAdapter extends TypeAdapter { - @override - final int typeId = 0; - - @override - HomePageSection read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return HomePageSection( - layout: fields[1] as HomePageSectionLayout, - items: (fields[4] as List?)?.cast(), - title: fields[0] as String?, - pagePath: fields[2] as String?, - hasMore: fields[3] as bool?, - ); - } - - @override - void write(BinaryWriter writer, HomePageSection obj) { - writer - ..writeByte(5) - ..writeByte(0) - ..write(obj.title) - ..writeByte(1) - ..write(obj.layout) - ..writeByte(2) - ..write(obj.pagePath) - ..writeByte(3) - ..write(obj.hasMore) - ..writeByte(4) - ..write(obj.items); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is HomePageSectionAdapter && - runtimeType == other.runtimeType && - typeId == other.typeId; -} - -class HomePageItemAdapter extends TypeAdapter { - @override - final int typeId = 1; - - @override - HomePageItem read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return HomePageItem( - type: fields[0] as HomePageItemType, - value: fields[1] as dynamic, - ); - } - - @override - void write(BinaryWriter writer, HomePageItem obj) { - writer - ..writeByte(2) - ..writeByte(0) - ..write(obj.type) - ..writeByte(1) - ..write(obj.value); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is HomePageItemAdapter && - runtimeType == other.runtimeType && - typeId == other.typeId; -} - -class DeezerChannelAdapter extends TypeAdapter { - @override - final int typeId = 14; - - @override - DeezerChannel read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return DeezerChannel( - id: fields[0] as String?, - title: fields[2] as String?, - backgroundColor: fields[3] as Color, - target: fields[1] as String?, - logo: fields[4] as DeezerImageDetails?, - picture: fields[5] as DeezerImageDetails?, - ); - } - - @override - void write(BinaryWriter writer, DeezerChannel obj) { - writer - ..writeByte(6) - ..writeByte(0) - ..write(obj.id) - ..writeByte(1) - ..write(obj.target) - ..writeByte(2) - ..write(obj.title) - ..writeByte(3) - ..write(obj.backgroundColor) - ..writeByte(4) - ..write(obj.logo) - ..writeByte(5) - ..write(obj.picture); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is DeezerChannelAdapter && - runtimeType == other.runtimeType && - typeId == other.typeId; -} - -class SortingAdapter extends TypeAdapter { - @override - final int typeId = 17; - - @override - Sorting read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return Sorting( - type: fields[0] as SortType?, - reverse: fields[1] as bool?, - id: fields[2] as String?, - sourceType: fields[3] as SortSourceTypes?, - ); - } - - @override - void write(BinaryWriter writer, Sorting obj) { - writer - ..writeByte(4) - ..writeByte(0) - ..write(obj.type) - ..writeByte(1) - ..write(obj.reverse) - ..writeByte(2) - ..write(obj.id) - ..writeByte(3) - ..write(obj.sourceType); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is SortingAdapter && - runtimeType == other.runtimeType && - typeId == other.typeId; -} - -class ShowAdapter extends TypeAdapter { - @override - final int typeId = 15; - - @override - Show read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return Show( - name: fields[0] as String?, - description: fields[1] as String?, - art: fields[2] as DeezerImageDetails?, - id: fields[3] as String?, - ); - } - - @override - void write(BinaryWriter writer, Show obj) { - writer - ..writeByte(4) - ..writeByte(0) - ..write(obj.name) - ..writeByte(1) - ..write(obj.description) - ..writeByte(2) - ..write(obj.art) - ..writeByte(3) - ..write(obj.id); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is ShowAdapter && - runtimeType == other.runtimeType && - typeId == other.typeId; -} - -class AlbumTypeAdapter extends TypeAdapter { - @override - final int typeId = 13; - - @override - AlbumType read(BinaryReader reader) { - switch (reader.readByte()) { - case 0: - return AlbumType.ALBUM; - case 1: - return AlbumType.SINGLE; - case 2: - return AlbumType.FEATURED; - default: - return AlbumType.ALBUM; - } - } - - @override - void write(BinaryWriter writer, AlbumType obj) { - switch (obj) { - case AlbumType.ALBUM: - writer.writeByte(0); - break; - case AlbumType.SINGLE: - writer.writeByte(1); - break; - case AlbumType.FEATURED: - writer.writeByte(2); - break; - } - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is AlbumTypeAdapter && - runtimeType == other.runtimeType && - typeId == other.typeId; -} - -class HomePageItemTypeAdapter extends TypeAdapter { - @override - final int typeId = 2; - - @override - HomePageItemType read(BinaryReader reader) { - switch (reader.readByte()) { - case 0: - return HomePageItemType.SMARTTRACKLIST; - case 1: - return HomePageItemType.PLAYLIST; - case 2: - return HomePageItemType.ARTIST; - case 3: - return HomePageItemType.CHANNEL; - case 4: - return HomePageItemType.ALBUM; - case 5: - return HomePageItemType.SHOW; - default: - return HomePageItemType.SMARTTRACKLIST; - } - } - - @override - void write(BinaryWriter writer, HomePageItemType obj) { - switch (obj) { - case HomePageItemType.SMARTTRACKLIST: - writer.writeByte(0); - break; - case HomePageItemType.PLAYLIST: - writer.writeByte(1); - break; - case HomePageItemType.ARTIST: - writer.writeByte(2); - break; - case HomePageItemType.CHANNEL: - writer.writeByte(3); - break; - case HomePageItemType.ALBUM: - writer.writeByte(4); - break; - case HomePageItemType.SHOW: - writer.writeByte(5); - break; - } - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is HomePageItemTypeAdapter && - runtimeType == other.runtimeType && - typeId == other.typeId; -} - -class HomePageSectionLayoutAdapter extends TypeAdapter { - @override - final int typeId = 3; - - @override - HomePageSectionLayout read(BinaryReader reader) { - switch (reader.readByte()) { - case 0: - return HomePageSectionLayout.ROW; - case 1: - return HomePageSectionLayout.GRID; - default: - return HomePageSectionLayout.ROW; - } - } - - @override - void write(BinaryWriter writer, HomePageSectionLayout obj) { - switch (obj) { - case HomePageSectionLayout.ROW: - writer.writeByte(0); - break; - case HomePageSectionLayout.GRID: - writer.writeByte(1); - break; - } - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is HomePageSectionLayoutAdapter && - runtimeType == other.runtimeType && - typeId == other.typeId; -} - -class SortTypeAdapter extends TypeAdapter { - @override - final int typeId = 18; - - @override - SortType read(BinaryReader reader) { - switch (reader.readByte()) { - case 0: - return SortType.DEFAULT; - case 1: - return SortType.ALPHABETIC; - case 2: - return SortType.ARTIST; - case 3: - return SortType.ALBUM; - case 4: - return SortType.RELEASE_DATE; - case 5: - return SortType.POPULARITY; - case 6: - return SortType.USER; - case 7: - return SortType.TRACK_COUNT; - case 8: - return SortType.DATE_ADDED; - default: - return SortType.DEFAULT; - } - } - - @override - void write(BinaryWriter writer, SortType obj) { - switch (obj) { - case SortType.DEFAULT: - writer.writeByte(0); - break; - case SortType.ALPHABETIC: - writer.writeByte(1); - break; - case SortType.ARTIST: - writer.writeByte(2); - break; - case SortType.ALBUM: - writer.writeByte(3); - break; - case SortType.RELEASE_DATE: - writer.writeByte(4); - break; - case SortType.POPULARITY: - writer.writeByte(5); - break; - case SortType.USER: - writer.writeByte(6); - break; - case SortType.TRACK_COUNT: - writer.writeByte(7); - break; - case SortType.DATE_ADDED: - writer.writeByte(8); - break; - } - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is SortTypeAdapter && - runtimeType == other.runtimeType && - typeId == other.typeId; -} - -class SortSourceTypesAdapter extends TypeAdapter { - @override - final int typeId = 19; - - @override - SortSourceTypes read(BinaryReader reader) { - switch (reader.readByte()) { - case 0: - return SortSourceTypes.TRACKS; - case 1: - return SortSourceTypes.PLAYLISTS; - case 2: - return SortSourceTypes.ALBUMS; - case 3: - return SortSourceTypes.ARTISTS; - case 4: - return SortSourceTypes.PLAYLIST; - default: - return SortSourceTypes.TRACKS; - } - } - - @override - void write(BinaryWriter writer, SortSourceTypes obj) { - switch (obj) { - case SortSourceTypes.TRACKS: - writer.writeByte(0); - break; - case SortSourceTypes.PLAYLISTS: - writer.writeByte(1); - break; - case SortSourceTypes.ALBUMS: - writer.writeByte(2); - break; - case SortSourceTypes.ARTISTS: - writer.writeByte(3); - break; - case SortSourceTypes.PLAYLIST: - writer.writeByte(4); - break; - } - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is SortSourceTypesAdapter && - runtimeType == other.runtimeType && - typeId == other.typeId; -} - -class NavigatorRouteTypeAdapter extends TypeAdapter { - @override - final int typeId = 30; - - @override - NavigatorRouteType read(BinaryReader reader) { - switch (reader.readByte()) { - case 0: - return NavigatorRouteType.blur_slide; - case 1: - return NavigatorRouteType.fade; - case 2: - return NavigatorRouteType.fade_blur; - case 3: - return NavigatorRouteType.material; - case 4: - return NavigatorRouteType.cupertino; - default: - return NavigatorRouteType.blur_slide; - } - } - - @override - void write(BinaryWriter writer, NavigatorRouteType obj) { - switch (obj) { - case NavigatorRouteType.blur_slide: - writer.writeByte(0); - break; - case NavigatorRouteType.fade: - writer.writeByte(1); - break; - case NavigatorRouteType.fade_blur: - writer.writeByte(2); - break; - case NavigatorRouteType.material: - writer.writeByte(3); - break; - case NavigatorRouteType.cupertino: - writer.writeByte(4); - break; - } - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is NavigatorRouteTypeAdapter && - runtimeType == other.runtimeType && - typeId == other.typeId; -} - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -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 - : DeezerImageDetails.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, - 'title': instance.title, - 'album': instance.album?.toJson(), - 'artists': instance.artists?.map((e) => e.toJson()).toList(), - 'duration': instance.duration?.inMicroseconds, - 'albumArt': instance.albumArt?.toJson(), - 'trackNumber': instance.trackNumber, - 'offline': instance.offline, - 'lyrics': instance.lyrics?.toJson(), - 'favorite': instance.favorite, - 'diskNumber': instance.diskNumber, - 'explicit': instance.explicit, - 'addedDate': instance.addedDate, - 'playbackDetails': instance.playbackDetails, - }; - -Album _$AlbumFromJson(Map json) => Album( - id: json['id'] as String?, - title: json['title'] as String?, - art: json['art'] == null - ? null - : DeezerImageDetails.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, - 'title': instance.title, - 'artists': instance.artists?.map((e) => e.toJson()).toList(), - 'tracks': instance.tracks?.map((e) => e.toJson()).toList(), - 'art': instance.art?.toJson(), - 'fans': instance.fans, - 'offline': instance.offline, - 'library': instance.library, - 'type': _$AlbumTypeEnumMap[instance.type], - 'releaseDate': instance.releaseDate, - 'favoriteDate': instance.favoriteDate, - }; - -const _$AlbumTypeEnumMap = { - AlbumType.ALBUM: 'ALBUM', - AlbumType.SINGLE: 'SINGLE', - AlbumType.FEATURED: 'FEATURED', -}; - -ArtistHighlight _$ArtistHighlightFromJson(Map json) => - ArtistHighlight( - data: json['data'], - type: $enumDecodeNullable(_$ArtistHighlightTypeEnumMap, json['type']), - title: json['title'] as String?, - ); - -Map _$ArtistHighlightToJson(ArtistHighlight instance) => - { - 'data': instance.data, - 'type': _$ArtistHighlightTypeEnumMap[instance.type], - 'title': instance.title, - }; - -const _$ArtistHighlightTypeEnumMap = { - ArtistHighlightType.ALBUM: 'ALBUM', -}; - -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 - : DeezerImageDetails.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, - 'name': instance.name, - 'albums': instance.albums?.map((e) => e.toJson()).toList(), - 'albumCount': instance.albumCount, - 'topTracks': instance.topTracks?.map((e) => e.toJson()).toList(), - 'picture': instance.picture?.toJson(), - 'fans': instance.fans, - 'offline': instance.offline, - 'library': instance.library, - 'radio': instance.radio, - 'favoriteDate': instance.favoriteDate, - 'highlight': instance.highlight?.toJson(), - }; - -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: _$JsonConverterFromJson, ImageDetails>( - json['image'], const JsonImageDetailsConverter().fromJson), - 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, - 'title': instance.title, - 'tracks': instance.tracks?.map((e) => e.toJson()).toList(), - 'image': _$JsonConverterToJson, ImageDetails>( - instance.image, const JsonImageDetailsConverter().toJson), - 'duration': instance.duration?.inMicroseconds, - 'trackCount': instance.trackCount, - 'user': instance.user?.toJson(), - 'fans': instance.fans, - 'library': instance.library, - 'description': instance.description, - }; - -Value? _$JsonConverterFromJson( - Object? json, - Value? Function(Json json) fromJson, -) => - json == null ? null : fromJson(json as Json); - -Json? _$JsonConverterToJson( - Value? value, - Json? Function(Value value) toJson, -) => - value == null ? null : toJson(value); - -User _$UserFromJson(Map json) => User( - id: json['id'] as String?, - name: json['name'] as String?, - picture: json['picture'] == null - ? null - : DeezerImageDetails.fromJson( - json['picture'] as Map), - ); - -Map _$UserToJson(User instance) => { - 'id': instance.id, - 'name': instance.name, - 'picture': instance.picture?.toJson(), - }; - -UrlImageDetails _$UrlImageDetailsFromJson(Map json) => - UrlImageDetails( - full: json['full'] as String, - thumb: json['thumb'] as String, - ); - -Map _$UrlImageDetailsToJson(UrlImageDetails instance) => - { - 'full': instance.full, - 'thumb': instance.thumb, - }; - -DeezerImageDetails _$DeezerImageDetailsFromJson(Map json) => - DeezerImageDetails( - json['md5'] as String, - type: json['type'] as String? ?? 'cover', - ); - -Map _$DeezerImageDetailsToJson(DeezerImageDetails instance) => - { - 'type': instance.type, - 'md5': instance.md5, - }; - -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(), - sync: json['sync'] as bool? ?? true, - ); - -Map _$LyricsToJson(Lyrics instance) => { - 'id': instance.id, - 'writers': instance.writers, - 'lyrics': instance.lyrics?.map((e) => e.toJson()).toList(), - 'sync': instance.sync, - }; - -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, - 'text': instance.text, - 'lrcTimestamp': instance.lrcTimestamp, - }; - -QueueSource _$QueueSourceFromJson(Map json) => QueueSource( - id: json['id'] as String?, - text: json['text'] as String?, - source: json['source'] as String?, - ); - -Map _$QueueSourceToJson(QueueSource instance) => - { - 'id': instance.id, - 'text': instance.text, - 'source': instance.source, - }; - -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'] as List?) - ?.map((e) => DeezerImageDetails.fromJson(e as Map)) - .toList(), - subtitle: json['subtitle'] as String?, - flowConfig: SmartTrackList._configFromJson(json['flowConfig'] as String), - ); - -Map _$SmartTrackListToJson(SmartTrackList instance) => - { - 'id': instance.id, - 'title': instance.title, - 'subtitle': instance.subtitle, - 'description': instance.description, - 'trackCount': instance.trackCount, - 'tracks': instance.tracks?.map((e) => e.toJson()).toList(), - 'cover': instance.cover?.map((e) => e.toJson()).toList(), - 'flowConfig': instance.flowConfig, - }; - -HomePage _$HomePageFromJson(Map json) => HomePage( - sections: (json['sections'] as List) - .map((e) => HomePageSection.fromJson(e as Map)) - .toList(), - lastUpdated: json['lastUpdated'] == null - ? null - : DateTime.parse(json['lastUpdated'] as String), - ); - -Map _$HomePageToJson(HomePage instance) => { - 'sections': instance.sections.map((e) => e.toJson()).toList(), - 'lastUpdated': instance.lastUpdated.toIso8601String(), - }; - -HomePageSection _$HomePageSectionFromJson(Map json) => - HomePageSection( - layout: $enumDecode(_$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) => - { - 'title': instance.title, - 'layout': _$HomePageSectionLayoutEnumMap[instance.layout]!, - 'pagePath': instance.pagePath, - 'hasMore': instance.hasMore, - 'items': HomePageSection._homePageItemToJson(instance.items), - }; - -const _$HomePageSectionLayoutEnumMap = { - HomePageSectionLayout.ROW: 'ROW', - HomePageSectionLayout.GRID: 'GRID', -}; - -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?, - logo: json['logo'] == null - ? null - : DeezerImageDetails.fromJson(json['logo'] as Map), - picture: json['picture'] == null - ? null - : DeezerImageDetails.fromJson( - json['picture'] as Map), - ); - -Map _$DeezerChannelToJson(DeezerChannel instance) => - { - 'id': instance.id, - 'target': instance.target, - 'title': instance.title, - 'backgroundColor': DeezerChannel._colorToJson(instance.backgroundColor), - 'logo': instance.logo?.toJson(), - 'picture': instance.picture?.toJson(), - }; - -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], - 'reverse': instance.reverse, - 'id': instance.id, - 'sourceType': _$SortSourceTypesEnumMap[instance.sourceType], - }; - -const _$SortTypeEnumMap = { - SortType.DEFAULT: 'DEFAULT', - SortType.ALPHABETIC: 'ALPHABETIC', - SortType.ARTIST: 'ARTIST', - SortType.ALBUM: 'ALBUM', - SortType.RELEASE_DATE: 'RELEASE_DATE', - SortType.POPULARITY: 'POPULARITY', - SortType.USER: 'USER', - SortType.TRACK_COUNT: 'TRACK_COUNT', - SortType.DATE_ADDED: 'DATE_ADDED', -}; - -const _$SortSourceTypesEnumMap = { - SortSourceTypes.TRACKS: 'TRACKS', - SortSourceTypes.PLAYLISTS: 'PLAYLISTS', - SortSourceTypes.ALBUMS: 'ALBUMS', - SortSourceTypes.ARTISTS: 'ARTISTS', - SortSourceTypes.PLAYLIST: 'PLAYLIST', -}; - -Show _$ShowFromJson(Map json) => Show( - name: json['name'] as String?, - description: json['description'] as String?, - art: json['art'] == null - ? null - : DeezerImageDetails.fromJson(json['art'] as Map), - id: json['id'] as String?, - ); - -Map _$ShowToJson(Show instance) => { - 'name': instance.name, - 'description': instance.description, - 'art': instance.art?.toJson(), - 'id': instance.id, - }; - -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) => - { - 'id': instance.id, - 'title': instance.title, - 'description': instance.description, - 'url': instance.url, - 'duration': instance.duration?.inMicroseconds, - 'publishedDate': instance.publishedDate, - 'show': instance.show?.toJson(), - }; - -StreamQualityInfo _$StreamQualityInfoFromJson(Map json) => - StreamQualityInfo( - format: $enumDecode(_$FormatEnumMap, json['format']), - source: $enumDecode(_$SourceEnumMap, json['source']), - quality: $enumDecodeNullable(_$AudioQualityEnumMap, json['quality']), - size: json['size'] as int?, - ); - -Map _$StreamQualityInfoToJson(StreamQualityInfo instance) => - { - 'format': _$FormatEnumMap[instance.format]!, - 'size': instance.size, - 'quality': _$AudioQualityEnumMap[instance.quality], - 'source': _$SourceEnumMap[instance.source]!, - }; - -const _$FormatEnumMap = { - Format.MP3: 'MP3', - Format.FLAC: 'FLAC', -}; - -const _$SourceEnumMap = { - Source.offline: 'offline', - Source.stream: 'stream', -}; - -const _$AudioQualityEnumMap = { - AudioQuality.MP3_128: 'MP3_128', - AudioQuality.MP3_320: 'MP3_320', - AudioQuality.FLAC: 'FLAC', - AudioQuality.ASK: 'ASK', -}; diff --git a/lib/api/paths.dart b/lib/api/paths.dart new file mode 100644 index 0000000..85e9725 --- /dev/null +++ b/lib/api/paths.dart @@ -0,0 +1,49 @@ +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:path/path.dart' as path; +import 'package:path_provider/path_provider.dart'; + +class Paths { + static Future dataDirectory() async { + switch (defaultTargetPlatform) { + case TargetPlatform.linux: + final home = Platform.environment['HOME']; + if (home == null) return path.dirname(Platform.resolvedExecutable); + if (await Directory(path.join(home, '.local', 'share')).exists()) { + final target = + await Directory(path.join(home, '.local', 'share', 'freezer')) + .create(); + return target.path; + } + return path.dirname(Platform.resolvedExecutable); + case TargetPlatform.windows: + String? home = Platform.environment['USERPROFILE']; + if (home == null) { + final drive = Platform.environment['HOMEDRIVE']; + final homepath = Platform.environment['HOMEPATH']; + if (drive == null || homepath == null) { + return path.dirname(Platform.resolvedExecutable); + } + + home = drive + homepath; + } + + final target = + await Directory(path.join(home, 'AppData', 'Freezer')).create(); + return target.path; + default: + return (await getApplicationDocumentsDirectory()).path; + } + } + + static Future cacheDir() async { + if (Platform.isLinux || Platform.isWindows) { + final dataDir = await dataDirectory(); + final target = await Directory(path.join(dataDir, 'cache')).create(); + return target.path; + } + + return (await getTemporaryDirectory()).path; + } +} diff --git a/lib/api/player.dart b/lib/api/player/audio_handler.dart similarity index 69% rename from lib/api/player.dart rename to lib/api/player/audio_handler.dart index e091d40..5feddea 100644 --- a/lib/api/player.dart +++ b/lib/api/player/audio_handler.dart @@ -7,6 +7,8 @@ import 'package:freezer/api/cache.dart'; import 'package:freezer/api/deezer.dart'; import 'package:freezer/api/deezer_audio_source.dart'; import 'package:freezer/api/offline_audio_source.dart'; +import 'package:freezer/api/paths.dart'; +import 'package:freezer/api/player/player_helper.dart'; import 'package:freezer/api/url_audio_source.dart'; import 'package:freezer/ui/android_auto.dart'; import 'package:hive_flutter/hive_flutter.dart'; @@ -20,9 +22,10 @@ import 'package:freezer/translations.i18n.dart'; import 'package:collection/collection.dart'; import 'package:scrobblenaut/scrobblenaut.dart'; import 'package:rxdart/rxdart.dart'; +import 'package:media_kit/media_kit.dart' show MPVLogLevel; -import 'definitions.dart'; -import '../settings.dart'; +import '../definitions.dart'; +import '../../settings.dart'; import 'dart:io'; import 'dart:async'; @@ -31,291 +34,6 @@ import 'dart:convert'; PlayerHelper playerHelper = PlayerHelper(); late AudioHandler audioHandler; -class PlayerHelper { - late StreamSubscription _customEventSubscription; - late StreamSubscription _mediaItemSubscription; - late StreamSubscription _playbackStateStreamSubscription; - QueueSource? queueSource; - AudioServiceRepeatMode repeatType = AudioServiceRepeatMode.none; - int? audioSession; - int? _prevAudioSession; - bool equalizerOpen = false; - bool _shuffleEnabled = false; - int _queueIndex = 0; - bool _started = false; - - //Visualizer - // StreamController _visualizerController = StreamController.broadcast(); - // Stream get visualizerStream => _visualizerController.stream; - - final _streamInfoSubject = BehaviorSubject(); - ValueStream get streamInfo => _streamInfoSubject.stream; - - final _bufferPositionSubject = BehaviorSubject(); - ValueStream get bufferPosition => _bufferPositionSubject.stream; - - /// Find queue index by id - /// - /// The function gets more expensive the longer the queue is and the further the element is from the beginning. - int getQueueIndexFromId() => audioHandler.mediaItem.value == null - ? -1 - : audioHandler.queue.value - .indexWhere((mi) => mi.id == audioHandler.mediaItem.value!.id); - - int getQueueIndex() => - audioHandler.playbackState.value.queueIndex ?? getQueueIndexFromId(); - - int get queueIndex => _queueIndex; - - Future initAudioHandler() async { - final initArgs = AudioPlayerTaskInitArguments.from( - settings: settings, deezerAPI: deezerAPI); - // initialize our audiohandler instance - audioHandler = await AudioService.init( - builder: () => AudioPlayerTask(initArgs), - config: AudioServiceConfig( - notificationColor: settings.primaryColor, - androidStopForegroundOnPause: false, - androidNotificationOngoing: false, - androidNotificationClickStartsActivity: true, - androidNotificationChannelDescription: 'Freezer', - androidNotificationChannelName: 'Freezer', - androidNotificationIcon: 'drawable/ic_logo', - preloadArtwork: false, - ), - ); - } - - Future start() async { - if (_started) return; - _started = true; - //Subscribe to custom events - _customEventSubscription = audioHandler.customEvent.listen((event) async { - if (event is! Map) return; - Logger('PlayerHelper').fine("event received: ${event['action']}"); - switch (event['action']) { - case 'onLoad': - //After audio_service is loaded, load queue, set quality - await settings.updateAudioServiceQuality(); - break; - case 'onRestore': - //Load queueSource from isolate - queueSource = event['queueSource'] as QueueSource; - repeatType = event['repeatMode'] as AudioServiceRepeatMode; - _queueIndex = getQueueIndex(); - break; - case 'audioSession': - if (!settings.enableEqualizer) break; - //Save - _prevAudioSession = audioSession; - audioSession = event['id']; - if (audioSession == null) break; - //Open EQ - if (!equalizerOpen) { - Equalizer.open(event['id']); - equalizerOpen = true; - break; - } - //Change session id - if (_prevAudioSession != audioSession) { - if (_prevAudioSession != null) { - Equalizer.removeAudioSessionId(_prevAudioSession!); - } - Equalizer.setAudioSessionId(audioSession!); - } - break; - //Visualizer data - // case 'visualizer': - // _visualizerController.add(event['data']); - // break; - case 'streamInfo': - Logger('PlayerHelper').fine("streamInfo received"); - _streamInfoSubject.add(event['data'] as StreamQualityInfo); - break; - case 'bufferPosition': - _bufferPositionSubject.add(event['data'] as Duration); - break; - } - }); - _mediaItemSubscription = audioHandler.mediaItem.listen((mediaItem) async { - if (mediaItem == null) return; - _queueIndex = getQueueIndex(); - //Load more flow if last song (not using .last since it iterates through previous elements first) - - //Save queue - await audioHandler.customAction('saveQueue', {}); - //Add to history - if (cache.history.isNotEmpty && cache.history.last.id == mediaItem.id) { - return; - } - cache.history.add(Track.fromMediaItem(mediaItem)); - cache.save(); - }); - - //Start audio_service - // 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 audioHandler.customAction('authorizeLastFM', { - 'username': settings.lastFMUsername, - 'password': settings.lastFMPassword - }); - } - - Future toggleShuffle() async { - _shuffleEnabled = !_shuffleEnabled; - await audioHandler.setShuffleMode(_shuffleEnabled - ? AudioServiceShuffleMode.all - : AudioServiceShuffleMode.none); - return _shuffleEnabled; - } - - bool get shuffleEnabled => _shuffleEnabled; - - //Repeat toggle - Future changeRepeat() async { - //Change to next repeat type - repeatType = repeatType == AudioServiceRepeatMode.all - ? AudioServiceRepeatMode.none - : repeatType == AudioServiceRepeatMode.none - ? AudioServiceRepeatMode.one - : AudioServiceRepeatMode.all; - //Set repeat type - await audioHandler.setRepeatMode(repeatType); - } - - //Executed before exit - Future onExit() async { - _customEventSubscription.cancel(); - _playbackStateStreamSubscription.cancel(); - _mediaItemSubscription.cancel(); - } - - //Replace queue, play specified track id - Future _loadQueuePlay(List queue, String? trackId) async { - await settings.updateAudioServiceQuality(); - - await audioHandler.customAction('setIndex', { - 'index': trackId == null ? 0 : queue.indexWhere((m) => m.id == trackId) - }); - await audioHandler.updateQueue(queue); -// if (queue[0].id != trackId) -// await AudioService.skipToQueueItem(trackId); - if (!audioHandler.playbackState.value.playing) audioHandler.play(); - } - - //Play track from album - Future playFromAlbum(Album album, [String? trackId]) async { - 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))!; - playFromTrackList( - tracks, - tracks[0].id, - QueueSource( - id: trackId, - text: '${'Mix based on'.i18n} $trackTitle', - source: 'mix')); - } - - //Play from artist top tracks - Future playFromTopTracks( - List tracks, String trackId, Artist artist) async { - await playFromTrackList( - tracks, - trackId, - QueueSource( - id: artist.id, text: 'Top ${artist.name}', source: 'topTracks')); - } - - Future playFromPlaylist(Playlist playlist, [String? trackId]) async { - await playFromTrackList(playlist.tracks!, trackId, - QueueSource(id: playlist.id, text: playlist.title, source: 'playlist')); - } - - //Play episode from show, load whole show as queue - Future playShowEpisode(Show show, List episodes, - {int index = 0}) async { - QueueSource queueSource = - QueueSource(id: show.id, text: show.name, source: 'show'); - //Generate media items - List queue = - episodes.map((e) => e.toMediaItem(show)).toList(); - - //Load and play - // await startService(); // audioservice is ready - await settings.updateAudioServiceQuality(); - await setQueueSource(queueSource); - 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 { - final queue = await Future.wait(tracks - .map>((track) => track!.toMediaItem()) - .toList()); - await setQueueSource(queueSource); - await _loadQueuePlay(queue, trackId); - } - - //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!.isEmpty) { - if (settings.offlineMode) { - Fluttertoast.showToast( - msg: "Offline mode, can't play flow or smart track lists.".i18n, - gravity: ToastGravity.BOTTOM, - toastLength: Toast.LENGTH_SHORT); - return; - } - - //Flow songs cannot be accessed by smart track list call - if (stl.id! == 'flow') { - stl.tracks = await deezerAPI.flow(stl.flowConfig); - } else { - stl = await deezerAPI.smartTrackList(stl.id); - } - } - QueueSource queueSource = QueueSource( - id: stl.flowConfig ?? stl.id, - 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); - } - - Future setQueueSource(QueueSource queueSource) async { - this.queueSource = queueSource; - await audioHandler.customAction('queueSource', queueSource.toJson()); - } - - //Reorder tracks in queue - Future reorder(int oldIndex, int newIndex) => audioHandler - .customAction('reorder', {'oldIndex': oldIndex, 'newIndex': newIndex}); - - //Start visualizer - // Future startVisualizer() async { - // await audioHandler.customAction('startVisualizer'); - // } - - //Stop visualizer - // Future stopVisualizer() async { - // await audioHandler.customAction('stopVisualizer'); - // } -} - class AudioPlayerTaskInitArguments { final bool ignoreInterruptions; final bool seekAsSkip; @@ -380,6 +98,7 @@ class AudioPlayerTask extends BaseAudioHandler { StreamSubscription? _audioSessionSubscription; StreamSubscription? _visualizerSubscription; StreamSubscription? _connectivitySubscription; + bool _isConnectivityPluginAvailable = true; /// Android Auto helper class for navigation late final AndroidAuto _androidAuto; @@ -436,6 +155,7 @@ class AudioPlayerTask extends BaseAudioHandler { // Linux/Windows specific options JustAudioMediaKit.title = 'Freezer'; JustAudioMediaKit.protocolWhitelist = const ['http']; + JustAudioMediaKit.mpvLogLevel = MPVLogLevel.debug; _deezerAPI = initArgs.deezerAPI; _androidAuto = AndroidAuto(deezerAPI: _deezerAPI); @@ -445,8 +165,7 @@ class AudioPlayerTask extends BaseAudioHandler { final session = await AudioSession.instance; session.configure(const AudioSessionConfiguration.music()); - _box = await Hive.openLazyBox('playback', - path: (await getTemporaryDirectory()).path); + _box = await Hive.openLazyBox('playback', path: await Paths.cacheDir()); _player = AudioPlayer( handleInterruptions: !initArgs.ignoreInterruptions, @@ -461,6 +180,8 @@ class AudioPlayerTask extends BaseAudioHandler { //Update track index _player.currentIndexStream.listen((index) { + _amountSeeked = 0; + _amountPaused = 0; _timestamp = DateTime.now().millisecondsSinceEpoch; if (index != null && queue.value.isNotEmpty) { _queueIndex = index; @@ -507,17 +228,11 @@ class AudioPlayerTask extends BaseAudioHandler { // queue.add(_queue); // Determine audio quality to use - try { - await Connectivity().checkConnectivity().then(_determineAudioQuality); - + if (await _determineAudioQuality()) { // listen for connectivity changes - _connectivitySubscription = - Connectivity().onConnectivityChanged.listen(_determineAudioQuality); - } catch (e) { - _logger.warning( - 'Couldn\'t determine connection! Falling back to other (which may use wifi quality)'); - // on error, return dummy value -- error can happen on linux if not using NetworkManager, for example - _determineAudioQuality(ConnectivityResult.other); + _connectivitySubscription = Connectivity() + .onConnectivityChanged + .listen(_determineAudioQualityByResult); } await _loadQueueFile(); @@ -530,7 +245,31 @@ class AudioPlayerTask extends BaseAudioHandler { customEvent.add({'action': 'onLoad'}); } - void _determineAudioQuality(ConnectivityResult result) { + /// Determine the [AudioQuality] to use according to current connection + /// + /// Returns whether the [Connectivity] plugin is available on this system or not + Future _determineAudioQuality() async { + if (_isConnectivityPluginAvailable) { + try { + await Connectivity() + .checkConnectivity() + .then(_determineAudioQualityByResult); + return true; + } catch (e) { + _isConnectivityPluginAvailable = false; + customEvent.add({'action': 'connectivityPlugin', 'available': false}); + } + } + + _logger.warning( + 'Couldn\'t determine connection! Falling back to other (which may use wifi quality)'); + // on error, return dummy value -- error can happen on linux if not using NetworkManager, for example + _determineAudioQualityByResult(ConnectivityResult.other); + return false; + } + + /// Determines the [AudioQuality] to use according to [result] + void _determineAudioQualityByResult(ConnectivityResult result) { switch (result) { case ConnectivityResult.mobile: case ConnectivityResult.bluetooth: @@ -543,6 +282,8 @@ class AudioPlayerTask extends BaseAudioHandler { default: _currentQuality = wifiQuality; } + + print('quality: $_currentQuality'); } @override @@ -674,6 +415,8 @@ class AudioPlayerTask extends BaseAudioHandler { } Future _logListenedTrack(MediaItem mediaItem) async { + print( + 'logging: seek: $_amountSeeked, pause: $_amountPaused, timestamp: $_timestamp (elapsed: ${DateTime.now().millisecondsSinceEpoch - _timestamp!}ms)'); if (!_shouldLogTracks) return; //Log to Deezer @@ -887,6 +630,8 @@ class AudioPlayerTask extends BaseAudioHandler { return DeezerAudioSource( getQuality: () => _currentQuality, trackId: mediaItem.id, + trackToken: mediaItem.extras!['trackToken'] ?? '', + trackTokenExpiration: mediaItem.extras!['trackTokenExpiration'] ?? 0, md5origin: playbackDetails![0], mediaVersion: playbackDetails[1], onStreamObtained: (qualityInfo) => @@ -903,6 +648,7 @@ class AudioPlayerTask extends BaseAudioHandler { //Isolate can't access globals wifiQuality = extras!['wifiQuality'] as AudioQuality; mobileQuality = extras['mobileQuality'] as AudioQuality; + _determineAudioQuality(); break; //Update queue source case 'queueSource': @@ -985,6 +731,7 @@ class AudioPlayerTask extends BaseAudioHandler { _audioSessionSubscription?.cancel(); _visualizerSubscription?.cancel(); _bufferPositionSubscription?.cancel(); + _connectivitySubscription?.cancel(); await super.stop(); } diff --git a/lib/api/player/player_helper.dart b/lib/api/player/player_helper.dart new file mode 100644 index 0000000..4ab2afa --- /dev/null +++ b/lib/api/player/player_helper.dart @@ -0,0 +1,304 @@ +import 'dart:async'; +import 'package:audio_service/audio_service.dart'; +import 'package:equalizer/equalizer.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:freezer/api/cache.dart'; +import 'package:freezer/api/deezer.dart'; +import 'package:freezer/api/definitions.dart'; +import 'package:freezer/api/player/audio_handler.dart'; +import 'package:freezer/settings.dart'; +import 'package:freezer/translations.i18n.dart'; +import 'package:logging/logging.dart'; +import 'package:rxdart/rxdart.dart'; + +class PlayerHelper { + late StreamSubscription _customEventSubscription; + late StreamSubscription _mediaItemSubscription; + late StreamSubscription _playbackStateStreamSubscription; + QueueSource? queueSource; + AudioServiceRepeatMode repeatType = AudioServiceRepeatMode.none; + int? audioSession; + int? _prevAudioSession; + bool equalizerOpen = false; + bool _shuffleEnabled = false; + int _queueIndex = 0; + bool _started = false; + + /// Whether this system supports the [Connectivity] plugin or not + bool _isConnectivityPluginAvailable = true; + bool get isConnectivityPluginAvailable => _isConnectivityPluginAvailable; + + //Visualizer + // StreamController _visualizerController = StreamController.broadcast(); + // Stream get visualizerStream => _visualizerController.stream; + + final _streamInfoSubject = BehaviorSubject(); + ValueStream get streamInfo => _streamInfoSubject.stream; + + final _bufferPositionSubject = BehaviorSubject(); + ValueStream get bufferPosition => _bufferPositionSubject.stream; + + /// Find queue index by id + /// + /// The function gets more expensive the longer the queue is and the further the element is from the beginning. + int getQueueIndexFromId() => audioHandler.mediaItem.value == null + ? -1 + : audioHandler.queue.value + .indexWhere((mi) => mi.id == audioHandler.mediaItem.value!.id); + + int getQueueIndex() => + audioHandler.playbackState.value.queueIndex ?? getQueueIndexFromId(); + + int get queueIndex => _queueIndex; + + Future initAudioHandler() async { + final initArgs = AudioPlayerTaskInitArguments.from( + settings: settings, deezerAPI: deezerAPI); + // initialize our audiohandler instance + audioHandler = await AudioService.init( + builder: () => AudioPlayerTask(initArgs), + config: AudioServiceConfig( + notificationColor: settings.primaryColor, + androidStopForegroundOnPause: false, + androidNotificationOngoing: false, + androidNotificationClickStartsActivity: true, + androidNotificationChannelDescription: 'Freezer', + androidNotificationChannelName: 'Freezer', + androidNotificationIcon: 'drawable/ic_logo', + preloadArtwork: false, + ), + ); + } + + Future start() async { + if (_started) return; + _started = true; + //Subscribe to custom events + _customEventSubscription = audioHandler.customEvent.listen((event) async { + if (event is! Map) return; + Logger('PlayerHelper').fine("event received: ${event['action']}"); + switch (event['action']) { + case 'onLoad': + //After audio_service is loaded, load queue, set quality + await settings.updateAudioServiceQuality(); + break; + case 'onRestore': + //Load queueSource from isolate + queueSource = event['queueSource'] as QueueSource; + repeatType = event['repeatMode'] as AudioServiceRepeatMode; + _queueIndex = getQueueIndex(); + break; + case 'audioSession': + if (!settings.enableEqualizer) break; + //Save + _prevAudioSession = audioSession; + audioSession = event['id']; + if (audioSession == null) break; + //Open EQ + if (!equalizerOpen) { + Equalizer.open(event['id']); + equalizerOpen = true; + break; + } + //Change session id + if (_prevAudioSession != audioSession) { + if (_prevAudioSession != null) { + Equalizer.removeAudioSessionId(_prevAudioSession!); + } + Equalizer.setAudioSessionId(audioSession!); + } + break; + //Visualizer data + // case 'visualizer': + // _visualizerController.add(event['data']); + // break; + case 'streamInfo': + Logger('PlayerHelper').fine("streamInfo received"); + _streamInfoSubject.add(event['data'] as StreamQualityInfo); + break; + case 'bufferPosition': + _bufferPositionSubject.add(event['data'] as Duration); + break; + case 'connectivityPlugin': + _isConnectivityPluginAvailable = event['available'] as bool; + break; + } + }); + _mediaItemSubscription = audioHandler.mediaItem.listen((mediaItem) async { + if (mediaItem == null) return; + _queueIndex = getQueueIndex(); + //Load more flow if last song (not using .last since it iterates through previous elements first) + + //Save queue + await audioHandler.customAction('saveQueue', {}); + //Add to history + if (cache.history.isNotEmpty && cache.history.last.id == mediaItem.id) { + return; + } + cache.history.add(Track.fromMediaItem(mediaItem)); + cache.save(); + }); + + //Start audio_service + // 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 audioHandler.customAction('authorizeLastFM', { + 'username': settings.lastFMUsername, + 'password': settings.lastFMPassword + }); + } + + Future toggleShuffle() async { + _shuffleEnabled = !_shuffleEnabled; + await audioHandler.setShuffleMode(_shuffleEnabled + ? AudioServiceShuffleMode.all + : AudioServiceShuffleMode.none); + return _shuffleEnabled; + } + + bool get shuffleEnabled => _shuffleEnabled; + + //Repeat toggle + Future changeRepeat() async { + //Change to next repeat type + repeatType = repeatType == AudioServiceRepeatMode.all + ? AudioServiceRepeatMode.none + : repeatType == AudioServiceRepeatMode.none + ? AudioServiceRepeatMode.one + : AudioServiceRepeatMode.all; + //Set repeat type + await audioHandler.setRepeatMode(repeatType); + } + + //Executed before exit + Future onExit() async { + _customEventSubscription.cancel(); + _playbackStateStreamSubscription.cancel(); + _mediaItemSubscription.cancel(); + } + + //Replace queue, play specified track id + Future _loadQueuePlay(List queue, String? trackId) async { + await settings.updateAudioServiceQuality(); + + await audioHandler.customAction('setIndex', { + 'index': trackId == null ? 0 : queue.indexWhere((m) => m.id == trackId) + }); + await audioHandler.updateQueue(queue); +// if (queue[0].id != trackId) +// await AudioService.skipToQueueItem(trackId); + if (!audioHandler.playbackState.value.playing) audioHandler.play(); + } + + //Play track from album + Future playFromAlbum(Album album, [String? trackId]) async { + 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))!; + playFromTrackList( + tracks, + tracks[0].id, + QueueSource( + id: trackId, + text: '${'Mix based on'.i18n} $trackTitle', + source: 'mix')); + } + + //Play from artist top tracks + Future playFromTopTracks( + List tracks, String trackId, Artist artist) async { + await playFromTrackList( + tracks, + trackId, + QueueSource( + id: artist.id, text: 'Top ${artist.name}', source: 'topTracks')); + } + + Future playFromPlaylist(Playlist playlist, [String? trackId]) async { + await playFromTrackList(playlist.tracks!, trackId, + QueueSource(id: playlist.id, text: playlist.title, source: 'playlist')); + } + + //Play episode from show, load whole show as queue + Future playShowEpisode(Show show, List episodes, + {int index = 0}) async { + QueueSource queueSource = + QueueSource(id: show.id, text: show.name, source: 'show'); + //Generate media items + List queue = + episodes.map((e) => e.toMediaItem(show)).toList(); + + //Load and play + // await startService(); // audioservice is ready + await settings.updateAudioServiceQuality(); + await setQueueSource(queueSource); + 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 { + final queue = await Future.wait(tracks + .map>((track) => track!.toMediaItem()) + .toList()); + await setQueueSource(queueSource); + await _loadQueuePlay(queue, trackId); + } + + //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!.isEmpty) { + if (settings.offlineMode) { + Fluttertoast.showToast( + msg: "Offline mode, can't play flow or smart track lists.".i18n, + gravity: ToastGravity.BOTTOM, + toastLength: Toast.LENGTH_SHORT); + return; + } + + //Flow songs cannot be accessed by smart track list call + if (stl.id! == 'flow') { + stl.tracks = await deezerAPI.flow(stl.flowConfig); + } else { + stl = await deezerAPI.smartTrackList(stl.id); + } + } + QueueSource queueSource = QueueSource( + id: stl.flowConfig ?? stl.id, + 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); + } + + Future setQueueSource(QueueSource queueSource) async { + this.queueSource = queueSource; + await audioHandler.customAction('queueSource', queueSource.toJson()); + } + + //Reorder tracks in queue + Future reorder(int oldIndex, int newIndex) => audioHandler + .customAction('reorder', {'oldIndex': oldIndex, 'newIndex': newIndex}); + + //Start visualizer + // Future startVisualizer() async { + // await audioHandler.customAction('startVisualizer'); + // } + + //Stop visualizer + // Future stopVisualizer() async { + // await audioHandler.customAction('stopVisualizer'); + // } +} diff --git a/lib/main.dart b/lib/main.dart index 9d899a0..df9153a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -12,11 +12,13 @@ import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:freezer/api/cache.dart'; import 'package:freezer/api/definitions.dart'; +import 'package:freezer/api/paths.dart'; import 'package:freezer/page_routes/blur_slide.dart'; import 'package:freezer/page_routes/fade.dart'; import 'package:freezer/page_routes/scale_fade.dart'; import 'package:freezer/type_adapters/uri.dart'; import 'package:freezer/ui/downloads_screen.dart'; +import 'package:freezer/ui/fancy_scaffold.dart'; import 'package:freezer/ui/library.dart'; import 'package:freezer/ui/login_screen.dart'; import 'package:freezer/ui/player_screen.dart'; @@ -35,7 +37,7 @@ import 'package:freezer/type_adapters/mediaitem.dart'; import 'api/deezer.dart'; import 'api/download.dart'; -import 'api/player.dart'; +import 'api/player/audio_handler.dart'; import 'settings.dart'; import 'ui/home_screen.dart'; import 'ui/player_bar.dart'; @@ -54,19 +56,18 @@ void main() async { ..registerAdapter(HomePageItemAdapter()) ..registerAdapter(HomePageItemTypeAdapter()) ..registerAdapter(HomePageSectionLayoutAdapter()) + ..registerAdapter(SmartTrackListAdapter()) ..registerAdapter(TrackAdapter()) - ..registerAdapter(AlbumAdapter()) - ..registerAdapter(ArtistAdapter()) - ..registerAdapter(PlaylistAdapter()) - ..registerAdapter(UserAdapter()) ..registerAdapter(DeezerImageDetailsAdapter()) ..registerAdapter(LyricsAdapter()) ..registerAdapter(LyricAdapter()) - ..registerAdapter(SmartTrackListAdapter()) + ..registerAdapter(PlaylistAdapter()) + ..registerAdapter(ArtistAdapter()) + ..registerAdapter(AlbumAdapter()) + ..registerAdapter(UserAdapter()) + ..registerAdapter(AlbumTypeAdapter()) ..registerAdapter(DeezerChannelAdapter()) ..registerAdapter(ShowAdapter()) - ..registerAdapter(AlbumTypeAdapter()) - ..registerAdapter(ColorAdapter()) ..registerAdapter(DurationAdapter()) ..registerAdapter(SortingAdapter()) ..registerAdapter(SortTypeAdapter()) @@ -74,6 +75,7 @@ void main() async { ..registerAdapter(SearchHistoryItemAdapter()) ..registerAdapter(SearchHistoryItemTypeAdapter()) ..registerAdapter(CacheAdapter()) + ..registerAdapter(ColorAdapter()) ..registerAdapter(DateTimeAdapter()) ..registerAdapter(MediaItemAdapter()) ..registerAdapter(AudioServiceRepeatModeAdapter()) @@ -84,8 +86,10 @@ void main() async { ..registerAdapter(NavigatorRouteTypeAdapter()) ..registerAdapter(UriAdapter()) ..registerAdapter(QueueSourceAdapter()) - ..registerAdapter(HomePageAdapter()); - await Hive.initFlutter(); + ..registerAdapter(HomePageAdapter()) + ..registerAdapter(NavigationRailAppearanceAdapter()); + + Hive.init(await Paths.dataDirectory()); //Initialize globals settings = await Settings.load(); @@ -647,7 +651,7 @@ class MainScreenState extends State isDesktop = isLandscape && constraints.maxWidth > 1024; return FancyScaffold( key: _fancyScaffoldKey, - bodyDrawer: _buildNavigationRail(isDesktop), + navigationRail: _buildNavigationRail(isDesktop), bottomNavigationBar: buildBottomBar(isDesktop), bottomPanel: PlayerBar( focusNode: playerBarFocusNode, diff --git a/lib/page_routes/fade.dart b/lib/page_routes/fade.dart index 54a060d..1c80104 100644 --- a/lib/page_routes/fade.dart +++ b/lib/page_routes/fade.dart @@ -1,4 +1,4 @@ -import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; import 'package:freezer/page_routes/basic_page_route.dart'; import 'package:freezer/ui/animated_blur.dart'; @@ -6,7 +6,7 @@ class FadePageRoute extends BasicPageRoute { @override final bool barrierDismissible; @override - final Color? barrierColor; + final Color barrierColor; @override final bool opaque; @@ -18,7 +18,7 @@ class FadePageRoute extends BasicPageRoute { super.transitionDuration, super.maintainState, super.settings, - this.barrierColor, + this.barrierColor = Colors.black38, this.barrierDismissible = false, this.opaque = true, }); diff --git a/lib/settings.dart b/lib/settings.dart index d1b123b..00f75bf 100644 --- a/lib/settings.dart +++ b/lib/settings.dart @@ -1,7 +1,9 @@ +import 'dart:math'; + import 'package:flutter/foundation.dart'; import 'package:freezer/api/definitions.dart'; import 'package:freezer/api/download.dart'; -import 'package:freezer/api/player.dart'; +import 'package:freezer/api/player/audio_handler.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:json_annotation/json_annotation.dart'; @@ -176,6 +178,20 @@ class Settings { Settings(); + void checkQuality(bool canStreamHQ, bool canStreamLossless) { + if (canStreamLossless) return; + + final maxQuality = + canStreamHQ ? AudioQuality.MP3_320 : AudioQuality.MP3_128; + + wifiQuality = _minQuality(wifiQuality, maxQuality); + mobileQuality = _minQuality(mobileQuality, maxQuality); + offlineQuality = _minQuality(offlineQuality, maxQuality); + downloadQuality = _minQuality(downloadQuality, maxQuality); + } + + AudioQuality _minQuality(AudioQuality a, AudioQuality b) => a < b ? a : b; + ThemeData? get themeData { //System theme if (useSystemTheme) { @@ -376,14 +392,38 @@ enum AudioQuality { ASK } -extension ToDeezerInt on AudioQuality { +extension Deezer on AudioQuality { + static AudioQuality fromDeezerQualityInt(int quality) { + return const { + 1: AudioQuality.MP3_128, + 3: AudioQuality.MP3_320, + 9: AudioQuality.FLAC, + }[quality]!; + } + + bool operator <(AudioQuality other) => + toDeezerQualityInt() < other.toDeezerQualityInt(); + bool operator >(AudioQuality other) => + toDeezerQualityInt() > other.toDeezerQualityInt(); + bool operator <=(AudioQuality other) => + toDeezerQualityInt() <= other.toDeezerQualityInt(); + bool operator >=(AudioQuality other) => + toDeezerQualityInt() >= other.toDeezerQualityInt(); + int toDeezerQualityInt() { return const { - AudioQuality.MP3_128: 1, - AudioQuality.MP3_320: 3, - AudioQuality.FLAC: 9, - }[this] ?? - 8; + AudioQuality.MP3_128: 1, + AudioQuality.MP3_320: 3, + AudioQuality.FLAC: 9, + }[this]!; + } + + String toDeezerQualityString() { + return const { + AudioQuality.MP3_128: 'MP3_128', + AudioQuality.MP3_320: 'MP3_320', + AudioQuality.FLAC: 'FLAC', + }[this]!; } } diff --git a/lib/settings.g.dart b/lib/settings.g.dart deleted file mode 100644 index 94412e4..0000000 --- a/lib/settings.g.dart +++ /dev/null @@ -1,482 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'settings.dart'; - -// ************************************************************************** -// TypeAdapterGenerator -// ************************************************************************** - -class SettingsAdapter extends TypeAdapter { - @override - final int typeId = 24; - - @override - Settings read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return Settings() - ..language = fields[0] as String? - ..ignoreInterruptions = fields[1] as bool - ..enableEqualizer = fields[2] as bool - ..arl = fields[3] as String? - ..wifiQuality = fields[4] as AudioQuality - ..mobileQuality = fields[5] as AudioQuality - ..offlineQuality = fields[6] as AudioQuality - ..downloadQuality = fields[7] as AudioQuality - ..downloadPath = fields[8] as String? - ..downloadFilename = fields[9] as String - ..albumFolder = fields[10] as bool - ..artistFolder = fields[11] as bool - ..albumDiscFolder = fields[12] as bool - ..overwriteDownload = fields[13] as bool - ..downloadThreads = fields[14] as int - ..playlistFolder = fields[15] as bool - ..downloadLyrics = fields[16] as bool - ..trackCover = fields[17] as bool - ..albumCover = fields[18] as bool - ..nomediaFiles = fields[19] as bool - ..artistSeparator = fields[20] as String - ..singletonFilename = fields[21] as String - ..albumArtResolution = fields[22] as int - ..tags = (fields[23] as List).cast() - ..theme = fields[24] as Themes - ..useSystemTheme = fields[25] as bool - ..colorGradientBackground = fields[26] as bool - ..blurPlayerBackground = fields[27] as bool - ..font = fields[28] as String - ..lyricsVisualizer = fields[29] as bool - ..displayMode = fields[30] as int? - ..enableFilledPlayButton = fields[31] == null ? true : fields[31] as bool - ..playerBackgroundOnLyrics = - fields[32] == null ? false : fields[32] as bool - ..navigatorRouteType = fields[33] == null - ? NavigatorRouteType.material - : fields[33] as NavigatorRouteType - ..primaryColor = fields[34] == null ? Colors.blue : fields[34] as Color - ..useArtColor = fields[35] as bool - ..deezerLanguage = fields[36] as String - ..deezerCountry = fields[37] as String - ..logListen = fields[38] as bool - ..proxyAddress = fields[39] as String? - ..lastFMUsername = fields[40] as String? - ..lastFMPassword = fields[41] as String? - ..spotifyClientId = fields[42] as String? - ..spotifyClientSecret = fields[43] as String? - ..spotifyCredentials = fields[44] as SpotifyCredentialsSave? - ..materialYouAccent = fields[45] == null ? false : fields[45] as bool - ..playerAlbumArtDropShadow = - fields[46] == null ? true : fields[46] as bool - ..seekAsSkip = fields[47] == null ? false : fields[47] as bool; - } - - @override - void write(BinaryWriter writer, Settings obj) { - writer - ..writeByte(48) - ..writeByte(0) - ..write(obj.language) - ..writeByte(1) - ..write(obj.ignoreInterruptions) - ..writeByte(2) - ..write(obj.enableEqualizer) - ..writeByte(3) - ..write(obj.arl) - ..writeByte(4) - ..write(obj.wifiQuality) - ..writeByte(5) - ..write(obj.mobileQuality) - ..writeByte(6) - ..write(obj.offlineQuality) - ..writeByte(7) - ..write(obj.downloadQuality) - ..writeByte(8) - ..write(obj.downloadPath) - ..writeByte(9) - ..write(obj.downloadFilename) - ..writeByte(10) - ..write(obj.albumFolder) - ..writeByte(11) - ..write(obj.artistFolder) - ..writeByte(12) - ..write(obj.albumDiscFolder) - ..writeByte(13) - ..write(obj.overwriteDownload) - ..writeByte(14) - ..write(obj.downloadThreads) - ..writeByte(15) - ..write(obj.playlistFolder) - ..writeByte(16) - ..write(obj.downloadLyrics) - ..writeByte(17) - ..write(obj.trackCover) - ..writeByte(18) - ..write(obj.albumCover) - ..writeByte(19) - ..write(obj.nomediaFiles) - ..writeByte(20) - ..write(obj.artistSeparator) - ..writeByte(21) - ..write(obj.singletonFilename) - ..writeByte(22) - ..write(obj.albumArtResolution) - ..writeByte(23) - ..write(obj.tags) - ..writeByte(24) - ..write(obj.theme) - ..writeByte(25) - ..write(obj.useSystemTheme) - ..writeByte(26) - ..write(obj.colorGradientBackground) - ..writeByte(27) - ..write(obj.blurPlayerBackground) - ..writeByte(28) - ..write(obj.font) - ..writeByte(29) - ..write(obj.lyricsVisualizer) - ..writeByte(30) - ..write(obj.displayMode) - ..writeByte(31) - ..write(obj.enableFilledPlayButton) - ..writeByte(32) - ..write(obj.playerBackgroundOnLyrics) - ..writeByte(33) - ..write(obj.navigatorRouteType) - ..writeByte(34) - ..write(obj.primaryColor) - ..writeByte(35) - ..write(obj.useArtColor) - ..writeByte(36) - ..write(obj.deezerLanguage) - ..writeByte(37) - ..write(obj.deezerCountry) - ..writeByte(38) - ..write(obj.logListen) - ..writeByte(39) - ..write(obj.proxyAddress) - ..writeByte(40) - ..write(obj.lastFMUsername) - ..writeByte(41) - ..write(obj.lastFMPassword) - ..writeByte(42) - ..write(obj.spotifyClientId) - ..writeByte(43) - ..write(obj.spotifyClientSecret) - ..writeByte(44) - ..write(obj.spotifyCredentials) - ..writeByte(45) - ..write(obj.materialYouAccent) - ..writeByte(46) - ..write(obj.playerAlbumArtDropShadow) - ..writeByte(47) - ..write(obj.seekAsSkip); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is SettingsAdapter && - runtimeType == other.runtimeType && - typeId == other.typeId; -} - -class SpotifyCredentialsSaveAdapter - extends TypeAdapter { - @override - final int typeId = 25; - - @override - SpotifyCredentialsSave read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return SpotifyCredentialsSave( - accessToken: fields[0] as String?, - refreshToken: fields[1] as String?, - scopes: (fields[2] as List?)?.cast(), - expiration: fields[3] as DateTime?, - ); - } - - @override - void write(BinaryWriter writer, SpotifyCredentialsSave obj) { - writer - ..writeByte(4) - ..writeByte(0) - ..write(obj.accessToken) - ..writeByte(1) - ..write(obj.refreshToken) - ..writeByte(2) - ..write(obj.scopes) - ..writeByte(3) - ..write(obj.expiration); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is SpotifyCredentialsSaveAdapter && - runtimeType == other.runtimeType && - typeId == other.typeId; -} - -class AudioQualityAdapter extends TypeAdapter { - @override - final int typeId = 29; - - @override - AudioQuality read(BinaryReader reader) { - switch (reader.readByte()) { - case 0: - return AudioQuality.MP3_128; - case 1: - return AudioQuality.MP3_320; - case 2: - return AudioQuality.FLAC; - case 3: - return AudioQuality.ASK; - default: - return AudioQuality.MP3_128; - } - } - - @override - void write(BinaryWriter writer, AudioQuality obj) { - switch (obj) { - case AudioQuality.MP3_128: - writer.writeByte(0); - break; - case AudioQuality.MP3_320: - writer.writeByte(1); - break; - case AudioQuality.FLAC: - writer.writeByte(2); - break; - case AudioQuality.ASK: - writer.writeByte(3); - break; - } - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is AudioQualityAdapter && - runtimeType == other.runtimeType && - typeId == other.typeId; -} - -class ThemesAdapter extends TypeAdapter { - @override - final int typeId = 28; - - @override - Themes read(BinaryReader reader) { - switch (reader.readByte()) { - case 0: - return Themes.Light; - case 1: - return Themes.Dark; - case 2: - return Themes.Deezer; - case 3: - return Themes.Black; - default: - return Themes.Light; - } - } - - @override - void write(BinaryWriter writer, Themes obj) { - switch (obj) { - case Themes.Light: - writer.writeByte(0); - break; - case Themes.Dark: - writer.writeByte(1); - break; - case Themes.Deezer: - writer.writeByte(2); - break; - case Themes.Black: - writer.writeByte(3); - break; - } - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is ThemesAdapter && - runtimeType == other.runtimeType && - typeId == other.typeId; -} - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -Settings _$SettingsFromJson(Map json) => Settings() - ..language = json['language'] as String? - ..ignoreInterruptions = json['ignoreInterruptions'] as bool - ..enableEqualizer = json['enableEqualizer'] as bool - ..arl = json['arl'] as String? - ..offlineMode = json['offlineMode'] as bool - ..wifiQuality = $enumDecode(_$AudioQualityEnumMap, json['wifiQuality']) - ..mobileQuality = $enumDecode(_$AudioQualityEnumMap, json['mobileQuality']) - ..offlineQuality = $enumDecode(_$AudioQualityEnumMap, json['offlineQuality']) - ..downloadQuality = - $enumDecode(_$AudioQualityEnumMap, json['downloadQuality']) - ..downloadPath = json['downloadPath'] as String? - ..downloadFilename = json['downloadFilename'] as String - ..albumFolder = json['albumFolder'] as bool - ..artistFolder = json['artistFolder'] as bool - ..albumDiscFolder = json['albumDiscFolder'] as bool - ..overwriteDownload = json['overwriteDownload'] as bool - ..downloadThreads = json['downloadThreads'] as int - ..playlistFolder = json['playlistFolder'] as bool - ..downloadLyrics = json['downloadLyrics'] as bool - ..trackCover = json['trackCover'] as bool - ..albumCover = json['albumCover'] as bool - ..nomediaFiles = json['nomediaFiles'] as bool - ..artistSeparator = json['artistSeparator'] as String - ..singletonFilename = json['singletonFilename'] as String - ..albumArtResolution = json['albumArtResolution'] as int - ..tags = (json['tags'] as List).map((e) => e as String).toList() - ..theme = $enumDecode(_$ThemesEnumMap, json['theme']) - ..useSystemTheme = json['useSystemTheme'] as bool - ..colorGradientBackground = json['colorGradientBackground'] as bool - ..blurPlayerBackground = json['blurPlayerBackground'] as bool - ..font = json['font'] as String - ..lyricsVisualizer = json['lyricsVisualizer'] as bool - ..displayMode = json['displayMode'] as int? - ..enableFilledPlayButton = json['enableFilledPlayButton'] as bool - ..playerBackgroundOnLyrics = json['playerBackgroundOnLyrics'] as bool - ..navigatorRouteType = - $enumDecode(_$NavigatorRouteTypeEnumMap, json['navigatorRouteType']) - ..primaryColor = Settings._colorFromJson(json['primaryColor'] as int?) - ..useArtColor = json['useArtColor'] as bool - ..deezerLanguage = json['deezerLanguage'] as String - ..deezerCountry = json['deezerCountry'] as String - ..logListen = json['logListen'] as bool - ..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) - ..materialYouAccent = json['materialYouAccent'] as bool - ..playerAlbumArtDropShadow = json['playerAlbumArtDropShadow'] as bool - ..seekAsSkip = json['seekAsSkip'] as bool; - -Map _$SettingsToJson(Settings instance) => { - 'language': instance.language, - 'ignoreInterruptions': instance.ignoreInterruptions, - 'enableEqualizer': instance.enableEqualizer, - 'arl': instance.arl, - 'wifiQuality': _$AudioQualityEnumMap[instance.wifiQuality]!, - 'mobileQuality': _$AudioQualityEnumMap[instance.mobileQuality]!, - 'offlineQuality': _$AudioQualityEnumMap[instance.offlineQuality]!, - 'downloadQuality': _$AudioQualityEnumMap[instance.downloadQuality]!, - 'downloadPath': instance.downloadPath, - 'downloadFilename': instance.downloadFilename, - 'albumFolder': instance.albumFolder, - 'artistFolder': instance.artistFolder, - 'albumDiscFolder': instance.albumDiscFolder, - 'overwriteDownload': instance.overwriteDownload, - 'downloadThreads': instance.downloadThreads, - 'playlistFolder': instance.playlistFolder, - 'downloadLyrics': instance.downloadLyrics, - 'trackCover': instance.trackCover, - 'albumCover': instance.albumCover, - 'nomediaFiles': instance.nomediaFiles, - 'artistSeparator': instance.artistSeparator, - 'singletonFilename': instance.singletonFilename, - 'albumArtResolution': instance.albumArtResolution, - 'tags': instance.tags, - 'theme': _$ThemesEnumMap[instance.theme]!, - 'useSystemTheme': instance.useSystemTheme, - 'colorGradientBackground': instance.colorGradientBackground, - 'blurPlayerBackground': instance.blurPlayerBackground, - 'font': instance.font, - 'lyricsVisualizer': instance.lyricsVisualizer, - 'displayMode': instance.displayMode, - 'enableFilledPlayButton': instance.enableFilledPlayButton, - 'playerBackgroundOnLyrics': instance.playerBackgroundOnLyrics, - 'navigatorRouteType': - _$NavigatorRouteTypeEnumMap[instance.navigatorRouteType]!, - 'primaryColor': Settings._colorToJson(instance.primaryColor), - 'useArtColor': instance.useArtColor, - 'deezerLanguage': instance.deezerLanguage, - 'deezerCountry': instance.deezerCountry, - 'logListen': instance.logListen, - 'proxyAddress': instance.proxyAddress, - 'lastFMUsername': instance.lastFMUsername, - 'lastFMPassword': instance.lastFMPassword, - 'spotifyClientId': instance.spotifyClientId, - 'spotifyClientSecret': instance.spotifyClientSecret, - 'spotifyCredentials': instance.spotifyCredentials?.toJson(), - 'materialYouAccent': instance.materialYouAccent, - 'playerAlbumArtDropShadow': instance.playerAlbumArtDropShadow, - 'seekAsSkip': instance.seekAsSkip, - }; - -const _$AudioQualityEnumMap = { - AudioQuality.MP3_128: 'MP3_128', - AudioQuality.MP3_320: 'MP3_320', - AudioQuality.FLAC: 'FLAC', - AudioQuality.ASK: 'ASK', -}; - -const _$ThemesEnumMap = { - Themes.Light: 'Light', - Themes.Dark: 'Dark', - Themes.Deezer: 'Deezer', - Themes.Black: 'Black', -}; - -const _$NavigatorRouteTypeEnumMap = { - NavigatorRouteType.blur_slide: 'blur_slide', - NavigatorRouteType.fade: 'fade', - NavigatorRouteType.fade_blur: 'fade_blur', - NavigatorRouteType.material: 'material', - NavigatorRouteType.cupertino: 'cupertino', -}; - -SpotifyCredentialsSave _$SpotifyCredentialsSaveFromJson( - 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) => - { - 'accessToken': instance.accessToken, - 'refreshToken': instance.refreshToken, - 'scopes': instance.scopes, - 'expiration': instance.expiration?.toIso8601String(), - }; diff --git a/lib/ui/android_auto.dart b/lib/ui/android_auto.dart index aaa3571..1c2e1b8 100644 --- a/lib/ui/android_auto.dart +++ b/lib/ui/android_auto.dart @@ -1,7 +1,7 @@ import 'package:audio_service/audio_service.dart'; import 'package:freezer/api/deezer.dart'; import 'package:freezer/api/definitions.dart'; -import 'package:freezer/api/player.dart'; +import 'package:freezer/api/player/audio_handler.dart'; import 'package:freezer/translations.i18n.dart'; class AndroidAuto { diff --git a/lib/ui/details_screens.dart b/lib/ui/details_screens.dart index 3c89961..b6c75bf 100644 --- a/lib/ui/details_screens.dart +++ b/lib/ui/details_screens.dart @@ -8,7 +8,7 @@ import 'package:fluttericon/font_awesome5_icons.dart'; import 'package:freezer/api/cache.dart'; import 'package:freezer/api/deezer.dart'; import 'package:freezer/api/download.dart'; -import 'package:freezer/api/player.dart'; +import 'package:freezer/api/player/audio_handler.dart'; import 'package:freezer/ui/elements.dart'; import 'package:freezer/ui/error.dart'; import 'package:freezer/ui/search.dart'; @@ -66,6 +66,9 @@ class _AlbumDetailsState extends State { @override Widget build(BuildContext context) { return Scaffold( + appBar: AppBar( + title: Text(album!.title ?? ''), + ), body: _error ? const ErrorScreen() : _loading @@ -314,20 +317,17 @@ class ArtistDetails extends StatefulWidget { class _ArtistDetailsState extends State { late final Future _future; + + @override void initState() { - FutureOr future = _loadArtist(widget.artist); - if (future is Artist) { - _future = Future.value(widget.artist); - } else { - _future = future; - } + _future = _loadArtist(widget.artist); super.initState(); } - FutureOr _loadArtist(Artist artist) { + Future _loadArtist(Artist artist) async { //Load artist from api if no albums if ((artist.albums ?? []).isEmpty) { - return deezerAPI.artist(artist.id); + return await deezerAPI.artist(artist.id); } return artist; @@ -336,6 +336,7 @@ class _ArtistDetailsState extends State { @override Widget build(BuildContext context) { return Scaffold( + appBar: AppBar(title: Text(widget.artist.name ?? '')), body: FutureBuilder( future: _future, builder: (BuildContext context, snapshot) { @@ -424,14 +425,9 @@ class _ArtistDetailsState extends State { mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ - TextButton( - child: Row( - children: [ - const Icon(Icons.favorite, size: 32), - const SizedBox(width: 4.0), - Text('Library'.i18n) - ], - ), + TextButton.icon( + icon: const Icon(Icons.favorite), + label: Text('Library'.i18n), onPressed: () async { await deezerAPI.addFavoriteArtist(widget.artist.id); ScaffoldMessenger.of(context) @@ -439,14 +435,9 @@ class _ArtistDetailsState extends State { }, ), if ((artist.radio ?? false)) - TextButton( - child: Row( - children: [ - const Icon(Icons.radio, size: 32), - const SizedBox(width: 4.0), - Text('Radio'.i18n) - ], - ), + TextButton.icon( + icon: const Icon(Icons.radio), + label: Text('Radio'.i18n), onPressed: () async { List tracks = (await deezerAPI.smartRadio(artist.id))!; @@ -873,238 +864,246 @@ class _PlaylistDetailsState extends State { @override Widget build(BuildContext context) { return Scaffold( + appBar: AppBar(title: Text(playlist!.title ?? '')), body: DraggableScrollbar.rrect( - controller: _scrollController, - backgroundColor: Theme.of(context).primaryColor, - child: ListView( - controller: _scrollController, - children: [ - const SizedBox(height: 4.0), - ConstrainedBox( - constraints: BoxConstraints.tight( - Size.fromHeight(MediaQuery.of(context).size.height / 3)), - child: Padding( - padding: - const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.max, - children: [ - Flexible( - child: CachedImage( - url: playlist!.image!.full, - rounded: true, - fullThumb: true, - ), - ), - SizedBox( - width: min(MediaQuery.of(context).size.width / 16, 60.0)), - Expanded( - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - playlist!.title!, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.start, - maxLines: 3, - style: const TextStyle( - fontSize: 20.0, fontWeight: FontWeight.bold), - ), - Text( - playlist!.user!.name ?? '', - overflow: TextOverflow.ellipsis, - maxLines: 2, - textAlign: TextAlign.start, - style: TextStyle( - color: Theme.of(context).primaryColor, - fontSize: 17.0), - ), - const SizedBox(height: 16.0), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.audiotrack, - size: 20.0, - semanticLabel: "Tracks".i18n, - ), - const SizedBox(width: 8.0), - Text( - (playlist!.trackCount ?? playlist!.tracks!.length) - .toString(), - style: const TextStyle(fontSize: 16), - ) - ], - ), - const SizedBox(height: 6.0), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.timelapse, - size: 32.0, - semanticLabel: "Duration".i18n, - ), - const SizedBox(width: 8.0), - Text( - playlist!.durationString, - style: const TextStyle(fontSize: 16), - ) - ], - ), - ], - ), - ), - ], - ), - ), - ), - if (playlist!.description != null && - playlist!.description!.isNotEmpty) - const FreezerDivider(), - if (playlist!.description != null && - playlist!.description!.isNotEmpty) - Padding( - padding: const EdgeInsets.all(6.0), - child: Text( - playlist!.description ?? '', - maxLines: 4, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.center, - style: const TextStyle(fontSize: 16.0), - ), - ), - const FreezerDivider(), - Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceAround, + controller: _scrollController, + backgroundColor: Theme.of(context).primaryColor, + child: ListView( + controller: _scrollController, 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, + const SizedBox(height: 4.0), + ConstrainedBox( + constraints: BoxConstraints.tight( + Size.fromHeight(MediaQuery.of(context).size.height / 3)), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 4.0, horizontal: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: [ + Flexible( + child: CachedImage( + url: playlist!.image!.full, + rounded: true, + fullThumb: true, + ), + ), + SizedBox( + width: min( + MediaQuery.of(context).size.width / 16, 60.0)), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + playlist!.title!, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.start, + maxLines: 3, + style: const TextStyle( + fontSize: 20.0, fontWeight: FontWeight.bold), + ), + Text( + playlist!.user!.name ?? '', + overflow: TextOverflow.ellipsis, + maxLines: 2, + textAlign: TextAlign.start, + style: TextStyle( + color: Theme.of(context).primaryColor, + fontSize: 17.0), + ), + const SizedBox(height: 16.0), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.audiotrack, + size: 20.0, + semanticLabel: "Tracks".i18n, + ), + const SizedBox(width: 8.0), + Text( + (playlist!.trackCount ?? + playlist!.tracks!.length) + .toString(), + style: const TextStyle(fontSize: 16), + ) + ], + ), + const SizedBox(height: 6.0), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.timelapse, + size: 32.0, + semanticLabel: "Duration".i18n, + ), + const SizedBox(width: 8.0), + Text( + playlist!.durationString, + style: const TextStyle(fontSize: 16), + ) + ], + ), + ], + ), + ), + ], ), - onPressed: () async { - //Add to library - if (!playlist!.library!) { - await deezerAPI.addPlaylist(playlist!.id); - ScaffoldMessenger.of(context) - .snack('Added to library'.i18n); - setState(() => playlist!.library = true); - return; - } - //Remove - await deezerAPI.removePlaylist(playlist!.id); - ScaffoldMessenger.of(context) - .snack('Playlist removed from library!'.i18n); - 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( - 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); + if (playlist!.description != null && + playlist!.description!.isNotEmpty) + const FreezerDivider(), + if (playlist!.description != null && + playlist!.description!.isNotEmpty) + Padding( + padding: const EdgeInsets.all(6.0), + child: Text( + playlist!.description ?? '', + maxLines: 4, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 16.0), + ), + ), + const FreezerDivider(), + 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); + ScaffoldMessenger.of(context) + .snack('Added to library'.i18n); + setState(() => playlist!.library = true); + return; + } + //Remove + await deezerAPI.removePlaylist(playlist!.id); + ScaffoldMessenger.of(context) + .snack('Playlist removed from library!'.i18n); + 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( + 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()), + //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()), + ), + ], + child: Icon( + Icons.sort, + size: 32.0, + semanticLabel: "Sort playlist".i18n, + ), ), - 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) ], - child: Icon( - Icons.sort, - size: 32.0, - semanticLabel: "Sort playlist".i18n, - ), ), - IconButton( - icon: Icon( - _sort!.reverse! - ? FontAwesome5.sort_alpha_up - : FontAwesome5.sort_alpha_down, - semanticLabel: _sort!.reverse! - ? "Sort descending".i18n - : "Sort ascending".i18n, + const FreezerDivider(), + if (playlist!.tracks!.isEmpty) + const Center(child: CircularProgressIndicator()), + ...List.generate(playlist!.tracks!.length, (i) { + Track t = sorted[i]; + return TrackTile.fromTrack(t, onTap: () { + Playlist p = Playlist( + title: playlist!.title, id: playlist!.id, tracks: sorted); + playerHelper.playFromPlaylist(p, t.id); + }, onSecondary: (details) { + MenuSheet m = MenuSheet(context); + m.defaultTrackMenu(t, details: details, options: [ + if (playlist!.user!.id == deezerAPI.userId) + m.removeFromPlaylist(t, playlist) + ]); + }); + }), + if (_loading) + const Padding( + padding: EdgeInsets.symmetric(vertical: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [CircularProgressIndicator()], + ), ), - onPressed: () => _reverse(), - ), - Container(width: 4.0) + if (_error) const ErrorScreen() ], ), - const FreezerDivider(), - ...List.generate(playlist!.tracks!.length, (i) { - Track t = sorted[i]; - return TrackTile.fromTrack(t, onTap: () { - Playlist p = Playlist( - title: playlist!.title, id: playlist!.id, tracks: sorted); - playerHelper.playFromPlaylist(p, t.id); - }, onSecondary: (details) { - MenuSheet m = MenuSheet(context); - m.defaultTrackMenu(t, details: details, options: [ - if (playlist!.user!.id == deezerAPI.userId) - m.removeFromPlaylist(t, playlist) - ]); - }); - }), - if (_loading) - const Padding( - padding: EdgeInsets.symmetric(vertical: 8.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [CircularProgressIndicator()], - ), - ), - if (_error) const ErrorScreen() - ], - ), - )); + )); } } diff --git a/lib/ui/fancy_scaffold.dart b/lib/ui/fancy_scaffold.dart new file mode 100644 index 0000000..dbec2a9 --- /dev/null +++ b/lib/ui/fancy_scaffold.dart @@ -0,0 +1,195 @@ +import 'package:flutter/material.dart'; + +class FancyScaffold extends StatefulWidget { + final Widget bottomPanel; + final double bottomPanelHeight; + final Widget expandedPanel; + final Widget? bottomNavigationBar; + final Widget? drawer; + final Widget? navigationRail; + final Widget body; + final void Function(AnimationStatus)? onAnimationStatusChange; + + const FancyScaffold({ + required this.bottomPanel, + required this.bottomPanelHeight, + required this.expandedPanel, + required this.body, + this.onAnimationStatusChange, + this.bottomNavigationBar, + this.navigationRail, + this.drawer, + super.key, + }); + + static FancyScaffoldState? of(BuildContext context) => + context.findAncestorStateOfType(); + + @override + FancyScaffoldState createState() => FancyScaffoldState(); +} + +class FancyScaffoldState extends State + with TickerProviderStateMixin { + // goes from 0 to 1 (double) + // 0 = preview, 1 = expanded + late final AnimationController dragController; + final statusNotifier = + ValueNotifier(AnimationStatus.dismissed); + + @override + void initState() { + dragController = AnimationController( + vsync: this, duration: const Duration(milliseconds: 500)); + dragController.addStatusListener((status) => statusNotifier.value = status); + statusNotifier.addListener( + () => widget.onAnimationStatusChange?.call(statusNotifier.value)); + super.initState(); + } + + @override + void dispose() { + dragController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final systemPadding = MediaQuery.of(context).viewPadding; + final defaultBottomPadding = + (widget.bottomNavigationBar == null ? 0 : 80.0) + systemPadding.bottom; + final screenHeight = MediaQuery.of(context).size.height; + final sizeAnimation = Tween( + begin: widget.bottomPanelHeight / MediaQuery.of(context).size.height, + end: 1.0, + ).animate(dragController); + return WillPopScope( + onWillPop: () { + if (statusNotifier.value == AnimationStatus.completed || + statusNotifier.value == AnimationStatus.reverse) { + dragController.fling(velocity: -1.0); + return Future.value(false); + } + + return Future.value(true); + }, + child: Stack( + children: [ + Positioned.fill( + child: Scaffold( + body: widget.navigationRail != null + ? Row(children: [ + widget.navigationRail!, + const VerticalDivider( + indent: 0.0, + endIndent: 0.0, + width: 2.0, + ), + Expanded(child: widget.body) + ]) + : widget.body, + drawer: widget.drawer, + bottomNavigationBar: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(height: widget.bottomPanelHeight), + if (widget.bottomNavigationBar != null) + SizeTransition( + axisAlignment: -1.0, + sizeFactor: + Tween(begin: 1.0, end: 0.0).animate(sizeAnimation), + child: widget.bottomNavigationBar, + ), + ], + ), + ), + ), + Positioned( + bottom: 0, + left: 0, + right: 0, + child: AnimatedBuilder( + animation: sizeAnimation, + builder: (context, child) { + final x = 1.0 - sizeAnimation.value; + return Padding( + padding: EdgeInsets.only( + bottom: (defaultBottomPadding /*+ 8.0*/) * x, + //right: 8.0 * x, + //left: 8.0 * x, + ), + child: child, + ); + }, + child: ValueListenableBuilder( + valueListenable: statusNotifier, + builder: (context, state, child) { + return GestureDetector( + onVerticalDragEnd: _onVerticalDragEnd, + onVerticalDragUpdate: _onVerticalDragUpdate, + child: child, + ); + }, + child: SizeTransition( + sizeFactor: sizeAnimation, + axisAlignment: -1.0, + axis: Axis.vertical, + child: SizedBox( + height: screenHeight, + width: MediaQuery.of(context).size.width, + child: ValueListenableBuilder( + valueListenable: statusNotifier, + builder: (context, state, _) => Stack( + children: [ + if (state != AnimationStatus.dismissed) + Positioned.fill( + key: const Key('player_screen'), + child: widget.expandedPanel, + ), + if (state != AnimationStatus.completed) + Positioned( + top: 0, + right: 0, + left: 0, + key: const Key('player_bar'), + child: FadeTransition( + opacity: Tween(begin: 1.0, end: 0.0) + .animate(dragController), + child: SizedBox( + height: widget.bottomPanelHeight, + child: widget.bottomPanel), + ), + ), + ], + ), + ), + )), + ), + ), + ), + ], + ), + ); + } + + void _onVerticalDragUpdate(DragUpdateDetails details) { + dragController.value -= + details.delta.dy / MediaQuery.of(context).size.height; + } + + void _onVerticalDragEnd(DragEndDetails details) { + // snap widget to size + // this should be also handled by drag velocity and not only with bare size. + + const double minFlingVelocity = 365.0; + + if (details.velocity.pixelsPerSecond.dy.abs() > minFlingVelocity) { + dragController.fling( + velocity: -details.velocity.pixelsPerSecond.dy / + MediaQuery.of(context).size.height); + return; + } + + dragController.fling(velocity: dragController.value > 0.5 ? 1.0 : -1.0); + } +} diff --git a/lib/ui/home_screen.dart b/lib/ui/home_screen.dart index 4ad1170..3a081aa 100644 --- a/lib/ui/home_screen.dart +++ b/lib/ui/home_screen.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:freezer/api/deezer.dart'; import 'package:freezer/api/definitions.dart'; -import 'package:freezer/api/player.dart'; +import 'package:freezer/api/player/audio_handler.dart'; import 'package:freezer/ui/error.dart'; import 'package:freezer/ui/menu.dart'; import 'package:freezer/translations.i18n.dart'; diff --git a/lib/ui/library.dart b/lib/ui/library.dart index 89f21d0..15ed4bd 100644 --- a/lib/ui/library.dart +++ b/lib/ui/library.dart @@ -5,7 +5,7 @@ import 'package:freezer/api/cache.dart'; import 'package:freezer/api/deezer.dart'; import 'package:freezer/api/definitions.dart'; import 'package:freezer/api/importer.dart'; -import 'package:freezer/api/player.dart'; +import 'package:freezer/api/player/audio_handler.dart'; import 'package:freezer/settings.dart'; import 'package:freezer/ui/details_screens.dart'; import 'package:freezer/ui/elements.dart'; diff --git a/lib/ui/lyrics_screen.dart b/lib/ui/lyrics_screen.dart index e19e4ff..47f4bae 100644 --- a/lib/ui/lyrics_screen.dart +++ b/lib/ui/lyrics_screen.dart @@ -6,7 +6,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:freezer/api/deezer.dart'; import 'package:freezer/api/definitions.dart'; -import 'package:freezer/api/player.dart'; +import 'package:freezer/api/player/audio_handler.dart'; import 'package:freezer/settings.dart'; import 'package:freezer/translations.i18n.dart'; import 'package:freezer/ui/error.dart'; diff --git a/lib/ui/menu.dart b/lib/ui/menu.dart index 5683d38..76a30ed 100644 --- a/lib/ui/menu.dart +++ b/lib/ui/menu.dart @@ -1,13 +1,14 @@ import 'dart:async'; import 'package:freezer/main.dart'; +import 'package:freezer/ui/fancy_scaffold.dart'; import 'package:freezer/ui/player_bar.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; import 'package:flutter/material.dart'; import 'package:freezer/api/cache.dart'; import 'package:freezer/api/deezer.dart'; import 'package:freezer/api/download.dart'; -import 'package:freezer/api/player.dart'; +import 'package:freezer/api/player/audio_handler.dart'; import 'package:freezer/ui/details_screens.dart'; import 'package:freezer/ui/error.dart'; import 'package:freezer/translations.i18n.dart'; @@ -117,7 +118,7 @@ class MenuSheet { showMenu( elevation: 4.0, context: context, - constraints: const BoxConstraints(maxWidth: 300), + constraints: const BoxConstraints(maxWidth: 300.0), position: RelativeRect.fromSize(actualPosition & Size.zero, overlay.size), items: options @@ -128,7 +129,7 @@ class MenuSheet { : Row(mainAxisSize: MainAxisSize.min, children: [ option.icon!, const SizedBox(width: 8.0), - option.label, + Flexible(child: option.label), ]))) .toList(growable: false)); } diff --git a/lib/ui/player_bar.dart b/lib/ui/player_bar.dart index 7fd570d..dc6d0b7 100644 --- a/lib/ui/player_bar.dart +++ b/lib/ui/player_bar.dart @@ -4,200 +4,12 @@ import 'package:audio_service/audio_service.dart'; import 'package:flutter/material.dart'; import 'package:freezer/settings.dart'; import 'package:freezer/translations.i18n.dart'; +import 'package:freezer/ui/fancy_scaffold.dart'; import 'package:rxdart/rxdart.dart'; -import '../api/player.dart'; +import '../api/player/audio_handler.dart'; import 'cached_image.dart'; -class FancyScaffold extends StatefulWidget { - final Widget bottomPanel; - final double bottomPanelHeight; - final Widget expandedPanel; - final Widget? bottomNavigationBar; - final Widget? drawer; - final Widget? bodyDrawer; - final Widget body; - final void Function(AnimationStatus)? onAnimationStatusChange; - - const FancyScaffold({ - required this.bottomPanel, - required this.bottomPanelHeight, - required this.expandedPanel, - required this.body, - this.onAnimationStatusChange, - this.bottomNavigationBar, - this.bodyDrawer, - this.drawer, - super.key, - }); - - static FancyScaffoldState? of(BuildContext context) => - context.findAncestorStateOfType(); - - @override - FancyScaffoldState createState() => FancyScaffoldState(); -} - -class FancyScaffoldState extends State - with TickerProviderStateMixin { - // goes from 0 to 1 (double) - // 0 = preview, 1 = expanded - late final AnimationController dragController; - final statusNotifier = - ValueNotifier(AnimationStatus.dismissed); - - @override - void initState() { - dragController = AnimationController( - vsync: this, duration: const Duration(milliseconds: 500)); - dragController.addStatusListener((status) => statusNotifier.value = status); - statusNotifier.addListener( - () => widget.onAnimationStatusChange?.call(statusNotifier.value)); - super.initState(); - } - - @override - void dispose() { - dragController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final systemPadding = MediaQuery.of(context).viewPadding; - final defaultBottomPadding = - (widget.bottomNavigationBar == null ? 0 : 80.0) + systemPadding.bottom; - final screenHeight = MediaQuery.of(context).size.height; - final sizeAnimation = Tween( - begin: widget.bottomPanelHeight / MediaQuery.of(context).size.height, - end: 1.0, - ).animate(dragController); - return WillPopScope( - onWillPop: () { - if (statusNotifier.value == AnimationStatus.completed || - statusNotifier.value == AnimationStatus.reverse) { - dragController.fling(velocity: -1.0); - return Future.value(false); - } - - return Future.value(true); - }, - child: Stack( - children: [ - Positioned.fill( - child: Scaffold( - body: widget.bodyDrawer != null - ? Row(children: [ - widget.bodyDrawer!, - Expanded(child: widget.body) - ]) - : widget.body, - drawer: widget.drawer, - bottomNavigationBar: Column( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox(height: widget.bottomPanelHeight), - if (widget.bottomNavigationBar != null) - SizeTransition( - axisAlignment: -1.0, - sizeFactor: - Tween(begin: 1.0, end: 0.0).animate(sizeAnimation), - child: widget.bottomNavigationBar, - ), - ], - ), - ), - ), - Positioned( - bottom: 0, - left: 0, - right: 0, - child: AnimatedBuilder( - animation: sizeAnimation, - builder: (context, child) { - final x = 1.0 - sizeAnimation.value; - return Padding( - padding: EdgeInsets.only( - bottom: (defaultBottomPadding /*+ 8.0*/) * x, - //right: 8.0 * x, - //left: 8.0 * x, - ), - child: child, - ); - }, - child: ValueListenableBuilder( - valueListenable: statusNotifier, - builder: (context, state, child) { - return GestureDetector( - onVerticalDragEnd: _onVerticalDragEnd, - onVerticalDragUpdate: _onVerticalDragUpdate, - child: child, - ); - }, - child: SizeTransition( - sizeFactor: sizeAnimation, - axisAlignment: -1.0, - axis: Axis.vertical, - child: SizedBox( - height: screenHeight, - width: MediaQuery.of(context).size.width, - child: ValueListenableBuilder( - valueListenable: statusNotifier, - builder: (context, state, _) => Stack( - children: [ - if (state != AnimationStatus.dismissed) - Positioned.fill( - key: const Key('player_screen'), - child: widget.expandedPanel, - ), - if (state != AnimationStatus.completed) - Positioned( - top: 0, - right: 0, - left: 0, - key: const Key('player_bar'), - child: FadeTransition( - opacity: Tween(begin: 1.0, end: 0.0) - .animate(dragController), - child: SizedBox( - height: widget.bottomPanelHeight, - child: widget.bottomPanel), - ), - ), - ], - ), - ), - )), - ), - ), - ), - ], - ), - ); - } - - void _onVerticalDragUpdate(DragUpdateDetails details) { - dragController.value -= - details.delta.dy / MediaQuery.of(context).size.height; - } - - void _onVerticalDragEnd(DragEndDetails details) { - // snap widget to size - // this should be also handled by drag velocity and not only with bare size. - - const double minFlingVelocity = 365.0; - - if (details.velocity.pixelsPerSecond.dy.abs() > minFlingVelocity) { - dragController.fling( - velocity: -details.velocity.pixelsPerSecond.dy / - MediaQuery.of(context).size.height); - return; - } - - dragController.fling(velocity: dragController.value > 0.5 ? 1.0 : -1.0); - } -} - class PlayerBar extends StatelessWidget { final VoidCallback? onTap; final bool shouldHaveHero; diff --git a/lib/ui/player_screen.dart b/lib/ui/player_screen.dart index 02a1960..d9e5843 100644 --- a/lib/ui/player_screen.dart +++ b/lib/ui/player_screen.dart @@ -11,12 +11,13 @@ import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:freezer/api/cache.dart'; import 'package:freezer/api/deezer.dart'; import 'package:freezer/api/definitions.dart'; -import 'package:freezer/api/player.dart'; +import 'package:freezer/api/player/audio_handler.dart'; import 'package:freezer/main.dart'; import 'package:freezer/page_routes/fade.dart'; import 'package:freezer/settings.dart'; import 'package:freezer/translations.i18n.dart'; import 'package:freezer/ui/cached_image.dart'; +import 'package:freezer/ui/fancy_scaffold.dart'; import 'package:freezer/ui/lyrics_screen.dart'; import 'package:freezer/ui/menu.dart'; import 'package:freezer/ui/player_bar.dart'; @@ -1151,7 +1152,8 @@ class BottomBarControls extends StatelessWidget { ), iconSize: size * 0.85, onPressed: () async { - await deezerAPI.dislikeTrack(audioHandler.mediaItem.value!.id); + unawaited( + deezerAPI.dislikeTrack(audioHandler.mediaItem.value!.id)); if (playerHelper.queueIndex < audioHandler.queue.value.length - 1) { audioHandler.skipToNext(); diff --git a/lib/ui/queue_screen.dart b/lib/ui/queue_screen.dart index c5da696..96c3fcc 100644 --- a/lib/ui/queue_screen.dart +++ b/lib/ui/queue_screen.dart @@ -4,7 +4,7 @@ import 'package:audio_service/audio_service.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:freezer/api/definitions.dart'; -import 'package:freezer/api/player.dart'; +import 'package:freezer/api/player/audio_handler.dart'; import 'package:freezer/translations.i18n.dart'; import 'package:freezer/ui/menu.dart'; import 'package:freezer/ui/tiles.dart'; diff --git a/lib/ui/search.dart b/lib/ui/search.dart index 57b2d44..d25f70e 100644 --- a/lib/ui/search.dart +++ b/lib/ui/search.dart @@ -8,7 +8,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:fluttericon/typicons_icons.dart'; import 'package:freezer/api/cache.dart'; import 'package:freezer/api/download.dart'; -import 'package:freezer/api/player.dart'; +import 'package:freezer/api/player/audio_handler.dart'; import 'package:freezer/notifiers/list_notifier.dart'; import 'package:freezer/ui/details_screens.dart'; import 'package:freezer/ui/elements.dart'; @@ -283,7 +283,7 @@ class _SearchScreenState extends State { List queue = cache.searchHistory .where((h) => h.type == SearchHistoryItemType.track) - .map((t) => Track.fromJson(t.data)) + .map((t) => t.data) .toList(); playerHelper.playFromTrackList( queue, diff --git a/lib/ui/settings_screen.dart b/lib/ui/settings_screen.dart index 246ef5f..c33ceb1 100644 --- a/lib/ui/settings_screen.dart +++ b/lib/ui/settings_screen.dart @@ -19,7 +19,7 @@ import 'package:url_launcher/url_launcher.dart'; import 'package:freezer/api/cache.dart'; import 'package:freezer/api/deezer.dart'; import 'package:freezer/api/download.dart'; -import 'package:freezer/api/player.dart'; +import 'package:freezer/api/player/audio_handler.dart'; import 'package:freezer/ui/downloads_screen.dart'; import 'package:freezer/ui/elements.dart'; import 'package:freezer/ui/home_screen.dart'; @@ -579,19 +579,29 @@ class _QualitySettingsState extends State { appBar: AppBar(title: Text('Quality'.i18n)), body: ListView( children: [ - ListTile( - title: Text('Mobile streaming'.i18n), - leading: - const LeadingIcon(Icons.network_cell, color: Color(0xff384697)), - ), - const QualityPicker('mobile'), - const FreezerDivider(), - ListTile( - title: Text('Wifi streaming'.i18n), - leading: - const LeadingIcon(Icons.network_wifi, color: Color(0xff0880b5)), - ), - const QualityPicker('wifi'), + ...(playerHelper.isConnectivityPluginAvailable + ? [ + ListTile( + title: Text('Mobile streaming'.i18n), + leading: const LeadingIcon(Icons.network_cell, + color: Color(0xff384697)), + ), + const QualityPicker('mobile'), + const FreezerDivider(), + ListTile( + title: Text('Wifi streaming'.i18n), + leading: const LeadingIcon(Icons.network_wifi, + color: Color(0xff0880b5)), + ), + const QualityPicker('wifi'), + ] + : [ + ListTile( + title: Text('Streaming'.i18n), + leading: const LeadingIcon(Icons.cloud, + color: Color(0xff384697))), + const QualityPicker('mobile_wifi'), + ]), const FreezerDivider(), ListTile( title: Text('Offline'.i18n), @@ -622,6 +632,8 @@ class QualityPicker extends StatefulWidget { class _QualityPickerState extends State { late AudioQuality _quality; + bool flacDisabled = !cache.canStreamLossless; + bool hqDisabled = !cache.canStreamHQ; @override void initState() { @@ -632,6 +644,7 @@ class _QualityPickerState extends State { //Get current quality void _getQuality() { switch (widget.field) { + case 'mobile_wifi': case 'mobile': _quality = settings.mobileQuality; break; @@ -655,6 +668,10 @@ class _QualityPickerState extends State { _quality = q; }); switch (widget.field) { + case 'mobile_wifi': + settings.mobileQuality = settings.wifiQuality = _quality; + settings.updateAudioServiceQuality(); + break; case 'mobile': settings.mobileQuality = _quality; settings.updateAudioServiceQuality(); @@ -671,7 +688,6 @@ class _QualityPickerState extends State { break; } await settings.save(); - await settings.updateAudioServiceQuality(); } @override @@ -682,26 +698,27 @@ class _QualityPickerState extends State { title: const Text('MP3 128kbps'), groupValue: _quality, value: AudioQuality.MP3_128, - onChanged: (q) => _updateQuality(q), + onChanged: (AudioQuality? q) => _updateQuality(q), ), RadioListTile( title: const Text('MP3 320kbps'), groupValue: _quality, value: AudioQuality.MP3_320, - onChanged: (q) => _updateQuality(q), + onChanged: hqDisabled ? null : (AudioQuality? q) => _updateQuality(q), ), RadioListTile( title: const Text('FLAC'), groupValue: _quality, value: AudioQuality.FLAC, - onChanged: (q) => _updateQuality(q), + onChanged: + flacDisabled ? null : (AudioQuality? q) => _updateQuality(q), ), if (widget.field == 'download') RadioListTile( title: Text('Ask before downloading'.i18n), groupValue: _quality, value: AudioQuality.ASK, - onChanged: (q) => _updateQuality(q), + onChanged: (AudioQuality? q) => _updateQuality(q), ) ], ); diff --git a/lib/ui/tiles.dart b/lib/ui/tiles.dart index e6555a0..5d811cb 100644 --- a/lib/ui/tiles.dart +++ b/lib/ui/tiles.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.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/api/player/audio_handler.dart'; import 'package:freezer/translations.i18n.dart'; import '../api/definitions.dart';