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
This commit is contained in:
parent
b629998416
commit
f126ffef46
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -51,7 +51,7 @@ android/app/.cxx
|
||||||
.pub/
|
.pub/
|
||||||
/build/
|
/build/
|
||||||
.gradle/
|
.gradle/
|
||||||
|
*.g.dart
|
||||||
# Web related
|
# Web related
|
||||||
lib/generated_plugin_registrant.dart
|
lib/generated_plugin_registrant.dart
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,11 @@ class Cache {
|
||||||
@HiveField(7)
|
@HiveField(7)
|
||||||
String? favoritesPlaylistId;
|
String? favoritesPlaylistId;
|
||||||
|
|
||||||
|
@HiveField(8, defaultValue: false)
|
||||||
|
bool canStreamHQ = false;
|
||||||
|
@HiveField(9, defaultValue: false)
|
||||||
|
bool canStreamLossless = false;
|
||||||
|
|
||||||
@JsonKey(includeToJson: false, includeFromJson: false)
|
@JsonKey(includeToJson: false, includeFromJson: false)
|
||||||
bool wakelock = false;
|
bool wakelock = false;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,198 +0,0 @@
|
||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
|
||||||
|
|
||||||
part of 'cache.dart';
|
|
||||||
|
|
||||||
// **************************************************************************
|
|
||||||
// TypeAdapterGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
class CacheAdapter extends TypeAdapter<Cache> {
|
|
||||||
@override
|
|
||||||
final int typeId = 22;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Cache read(BinaryReader reader) {
|
|
||||||
final numOfFields = reader.readByte();
|
|
||||||
final fields = <int, dynamic>{
|
|
||||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
|
||||||
};
|
|
||||||
return Cache()
|
|
||||||
..libraryTracks =
|
|
||||||
fields[0] == null ? [] : (fields[0] as List).cast<String>()
|
|
||||||
..loggedTrackId = fields[1] as String?
|
|
||||||
..history = (fields[2] as List).cast<Track>()
|
|
||||||
..sorts = (fields[3] as List).cast<Sorting?>()
|
|
||||||
..searchHistory = (fields[4] as List).cast<SearchHistoryItem>()
|
|
||||||
..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<SearchHistoryItem> {
|
|
||||||
@override
|
|
||||||
final int typeId = 20;
|
|
||||||
|
|
||||||
@override
|
|
||||||
SearchHistoryItem read(BinaryReader reader) {
|
|
||||||
final numOfFields = reader.readByte();
|
|
||||||
final fields = <int, dynamic>{
|
|
||||||
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<SearchHistoryItemType> {
|
|
||||||
@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<String, dynamic> json) => Cache()
|
|
||||||
..libraryTracks = (json['libraryTracks'] as List<dynamic>?)
|
|
||||||
?.map((e) => e as String)
|
|
||||||
.toList() ??
|
|
||||||
[]
|
|
||||||
..history = (json['history'] as List<dynamic>?)
|
|
||||||
?.map((e) => Track.fromJson(e as Map<String, dynamic>))
|
|
||||||
.toList() ??
|
|
||||||
[]
|
|
||||||
..sorts = (json['sorts'] as List<dynamic>?)
|
|
||||||
?.map((e) =>
|
|
||||||
e == null ? null : Sorting.fromJson(e as Map<String, dynamic>))
|
|
||||||
.toList() ??
|
|
||||||
[]
|
|
||||||
..searchHistory = (json['searchHistory2'] as List<dynamic>?)
|
|
||||||
?.map((e) => SearchHistoryItem.fromJson(e as Map<String, dynamic>))
|
|
||||||
.toList() ??
|
|
||||||
[]
|
|
||||||
..threadsWarning = json['threadsWarning'] as bool? ?? false
|
|
||||||
..lastUpdateCheck = json['lastUpdateCheck'] == null
|
|
||||||
? null
|
|
||||||
: DateTime.parse(json['lastUpdateCheck'] as String)
|
|
||||||
..favoritesPlaylistId = json['favoritesPlaylistId'] as String?;
|
|
||||||
|
|
||||||
Map<String, dynamic> _$CacheToJson(Cache instance) => <String, dynamic>{
|
|
||||||
'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<String, dynamic> json) =>
|
|
||||||
SearchHistoryItem(
|
|
||||||
json['data'],
|
|
||||||
SearchHistoryItem._searchHistoryItemTypeFromJson(json['type'] as int),
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$SearchHistoryItemToJson(SearchHistoryItem instance) =>
|
|
||||||
<String, dynamic>{
|
|
||||||
'type': SearchHistoryItem._searchHistoryItemTypeToJson(instance.type),
|
|
||||||
'data': instance.data,
|
|
||||||
};
|
|
||||||
104
lib/api/cache_provider.dart
Normal file
104
lib/api/cache_provider.dart
Normal file
|
|
@ -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<CacheObject> _box;
|
||||||
|
bool _isOpen;
|
||||||
|
FreezerCacheInfoRepository(this.boxName, this.path);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> exists() => Hive.boxExists(boxName, path: path);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> open() async {
|
||||||
|
if (_isOpen) return true;
|
||||||
|
|
||||||
|
_box = await Hive.openLazyBox<CacheObject>(boxName, path: path);
|
||||||
|
_isOpen = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<dynamic> updateOrInsert(CacheObject cacheObject) {
|
||||||
|
if (cacheObject.id == null) {
|
||||||
|
return insert(cacheObject);
|
||||||
|
} else {
|
||||||
|
return update(cacheObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<CacheObject> insert(CacheObject cacheObject,
|
||||||
|
{bool setTouchedToNow = true}) {
|
||||||
|
final id = await _box.add(cacheObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets a [CacheObject] by [key]
|
||||||
|
@override
|
||||||
|
Future<CacheObject?> get(String key);
|
||||||
|
|
||||||
|
/// Deletes a cache object by [id]
|
||||||
|
@override
|
||||||
|
Future<int> delete(int id);
|
||||||
|
|
||||||
|
/// Deletes items with [ids] from the repository
|
||||||
|
@override
|
||||||
|
Future<int> deleteAll(Iterable<int> ids);
|
||||||
|
|
||||||
|
/// Updates an existing [cacheObject]
|
||||||
|
@override
|
||||||
|
Future<int> update(CacheObject cacheObject, {bool setTouchedToNow = true});
|
||||||
|
|
||||||
|
/// Gets the list of all objects in the cache
|
||||||
|
@override
|
||||||
|
Future<List<CacheObject>> 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<List<CacheObject>> getObjectsOverCapacity(int capacity);
|
||||||
|
|
||||||
|
/// Returns a list of [CacheObject] that are older than [maxAge]
|
||||||
|
@override
|
||||||
|
Future<List<CacheObject>> 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<bool> close();
|
||||||
|
|
||||||
|
/// Deletes the cache data file including all cache data.
|
||||||
|
@override
|
||||||
|
Future<void> deleteDataFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
class CacheObjectAdapter extends TypeAdapter<CacheObject> {
|
||||||
|
@override
|
||||||
|
// TODO: implement typeId
|
||||||
|
int get typeId => throw UnimplementedError();
|
||||||
|
}
|
||||||
|
|
@ -26,6 +26,9 @@ class DeezerAPI {
|
||||||
String? userName;
|
String? userName;
|
||||||
String? favoritesPlaylistId;
|
String? favoritesPlaylistId;
|
||||||
String? sid;
|
String? sid;
|
||||||
|
late String licenseToken;
|
||||||
|
late bool canStreamLossless;
|
||||||
|
late bool canStreamHQ;
|
||||||
|
|
||||||
Future<bool>? _authorizing;
|
Future<bool>? _authorizing;
|
||||||
|
|
||||||
|
|
@ -125,10 +128,21 @@ class DeezerAPI {
|
||||||
userId = data['results']['USER']['USER_ID'].toString();
|
userId = data['results']['USER']['USER_ID'].toString();
|
||||||
userName = data['results']['USER']['BLOG_NAME'];
|
userName = data['results']['USER']['BLOG_NAME'];
|
||||||
favoritesPlaylistId = data['results']['USER']['LOVEDTRACKS_ID'];
|
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) {
|
if (cache.favoritesPlaylistId != favoritesPlaylistId) {
|
||||||
cache.favoritesPlaylistId = favoritesPlaylistId;
|
cache.favoritesPlaylistId = favoritesPlaylistId;
|
||||||
await cache.save();
|
|
||||||
}
|
}
|
||||||
|
await cache.save();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -191,6 +205,48 @@ class DeezerAPI {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<GetTrackUrlResponse> getTrackUrl(
|
||||||
|
String trackToken, String format) async =>
|
||||||
|
(await getTracksUrl([trackToken], format))[0];
|
||||||
|
|
||||||
|
Future<List<GetTrackUrlResponse>> getTracksUrl(
|
||||||
|
List<String> 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<TrackUrlSource>((e) => TrackUrlSource.fromPrivateJson(
|
||||||
|
(e as Map).cast<String, dynamic>()))
|
||||||
|
.toList(growable: false));
|
||||||
|
}).toList(growable: false);
|
||||||
|
}
|
||||||
|
|
||||||
//Search
|
//Search
|
||||||
Future<SearchResults> search(String? query) async {
|
Future<SearchResults> search(String? query) async {
|
||||||
Map<dynamic, dynamic> data = await callApi('deezer.pageSearch',
|
Map<dynamic, dynamic> data = await callApi('deezer.pageSearch',
|
||||||
|
|
@ -360,9 +416,6 @@ class DeezerAPI {
|
||||||
Map data = await callApi('deezer.pageSmartTracklist',
|
Map data = await callApi('deezer.pageSmartTracklist',
|
||||||
params: {'smarttracklist_id': id});
|
params: {'smarttracklist_id': id});
|
||||||
|
|
||||||
getExternalStorageDirectory().then((value) =>
|
|
||||||
File(join(value!.path, 'test.json')).writeAsString(jsonEncode(data)));
|
|
||||||
|
|
||||||
return SmartTrackList.fromPrivateJson(data['results'],
|
return SmartTrackList.fromPrivateJson(data['results'],
|
||||||
songsJson: data['results']['SONGS']);
|
songsJson: data['results']['SONGS']);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import 'package:crypto/crypto.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:dart_blowfish/dart_blowfish.dart';
|
import 'package:dart_blowfish/dart_blowfish.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:scrobblenaut/lastfm.dart';
|
||||||
|
|
||||||
typedef _IsolateMessage = (
|
typedef _IsolateMessage = (
|
||||||
Stream<List<int>> source,
|
Stream<List<int>> source,
|
||||||
|
|
@ -32,6 +33,8 @@ class DeezerAudioSource extends StreamAudioSource {
|
||||||
late String _trackId;
|
late String _trackId;
|
||||||
late String _md5origin;
|
late String _md5origin;
|
||||||
late String _mediaVersion;
|
late String _mediaVersion;
|
||||||
|
final String trackToken;
|
||||||
|
final int trackTokenExpiration;
|
||||||
final StreamInfoCallback? onStreamObtained;
|
final StreamInfoCallback? onStreamObtained;
|
||||||
|
|
||||||
// some cache
|
// some cache
|
||||||
|
|
@ -45,6 +48,8 @@ class DeezerAudioSource extends StreamAudioSource {
|
||||||
required String trackId,
|
required String trackId,
|
||||||
required String md5origin,
|
required String md5origin,
|
||||||
required String mediaVersion,
|
required String mediaVersion,
|
||||||
|
required this.trackToken,
|
||||||
|
required this.trackTokenExpiration,
|
||||||
this.onStreamObtained,
|
this.onStreamObtained,
|
||||||
}) {
|
}) {
|
||||||
_getQuality = getQuality;
|
_getQuality = getQuality;
|
||||||
|
|
@ -119,6 +124,8 @@ class DeezerAudioSource extends StreamAudioSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Uri> _qualityFallback() async {
|
Future<Uri> _qualityFallback() async {
|
||||||
|
// only use url generation with MP3_128
|
||||||
|
_currentQuality = AudioQuality.MP3_128;
|
||||||
final genUri = _generateTrackUri();
|
final genUri = _generateTrackUri();
|
||||||
|
|
||||||
final req = await http.head(genUri, headers: {
|
final req = await http.head(genUri, headers: {
|
||||||
|
|
@ -267,6 +274,32 @@ class DeezerAudioSource extends StreamAudioSource {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<Uri?> _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
|
@override
|
||||||
Future<StreamAudioResponse> request([int? start, int? end]) async {
|
Future<StreamAudioResponse> request([int? start, int? end]) async {
|
||||||
start ??= 0;
|
start ??= 0;
|
||||||
|
|
@ -289,21 +322,34 @@ class DeezerAudioSource extends StreamAudioSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
// determine quality to use
|
// determine quality to use
|
||||||
_currentQuality = _getQuality.call();
|
final newQuality = _getQuality.call();
|
||||||
|
|
||||||
final Uri uri;
|
if (_downloadUrl != null && _currentQuality != newQuality) {
|
||||||
if (_downloadUrl != null) {
|
// update currentUrl to get tracks with new quality
|
||||||
uri = _downloadUrl!;
|
_downloadUrl = null;
|
||||||
} else {
|
}
|
||||||
try {
|
|
||||||
_downloadUrl = uri = await _fallbackUrl();
|
_currentQuality = newQuality;
|
||||||
} on QualityException {
|
|
||||||
rethrow;
|
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 int deezerStart = start - (start % 2048);
|
||||||
final req = http.Request('GET', uri)
|
final req = http.Request('GET', _downloadUrl!)
|
||||||
..headers.addAll({
|
..headers.addAll({
|
||||||
'User-Agent': deezerAPI.headers['User-Agent']!,
|
'User-Agent': deezerAPI.headers['User-Agent']!,
|
||||||
'Accept-Language': '*',
|
'Accept-Language': '*',
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:audio_service/audio_service.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/api/cache.dart';
|
||||||
import 'package:freezer/page_routes/blur_slide.dart';
|
import 'package:freezer/page_routes/blur_slide.dart';
|
||||||
import 'package:freezer/page_routes/fade.dart';
|
import 'package:freezer/page_routes/fade.dart';
|
||||||
|
|
@ -57,6 +58,10 @@ class Track extends DeezerMediaItem {
|
||||||
//Date added to playlist / favorites
|
//Date added to playlist / favorites
|
||||||
int? addedDate;
|
int? addedDate;
|
||||||
|
|
||||||
|
// information for playback
|
||||||
|
String? trackToken;
|
||||||
|
int? trackTokenExpiration;
|
||||||
|
|
||||||
@HiveField(13)
|
@HiveField(13)
|
||||||
List<dynamic>? playbackDetails;
|
List<dynamic>? playbackDetails;
|
||||||
|
|
||||||
|
|
@ -75,11 +80,13 @@ class Track extends DeezerMediaItem {
|
||||||
this.diskNumber,
|
this.diskNumber,
|
||||||
this.explicit,
|
this.explicit,
|
||||||
this.addedDate,
|
this.addedDate,
|
||||||
|
this.trackToken,
|
||||||
|
this.trackTokenExpiration,
|
||||||
});
|
});
|
||||||
|
|
||||||
String get artistString => artists == null
|
String get artistString => artists == null
|
||||||
? ""
|
? ""
|
||||||
: artists!.map<String?>((art) => art.name).join(', ');
|
: artists!.map<String>((art) => art.name!).join(', ');
|
||||||
String get durationString => durationAsString(duration!);
|
String get durationString => durationAsString(duration!);
|
||||||
|
|
||||||
static String durationAsString(Duration duration) {
|
static String durationAsString(Duration duration) {
|
||||||
|
|
@ -88,6 +95,9 @@ class Track extends DeezerMediaItem {
|
||||||
|
|
||||||
//MediaItem
|
//MediaItem
|
||||||
Future<MediaItem> toMediaItem() async {
|
Future<MediaItem> toMediaItem() async {
|
||||||
|
DefaultCacheManager()
|
||||||
|
.getFileFromCache(albumArt!.full)
|
||||||
|
.then((i) => print('file: ${i?.file.uri}'));
|
||||||
return MediaItem(
|
return MediaItem(
|
||||||
title: title!,
|
title: title!,
|
||||||
album: album!.title!,
|
album: album!.title!,
|
||||||
|
|
@ -104,6 +114,8 @@ class Track extends DeezerMediaItem {
|
||||||
"thumb": albumArt!.thumb,
|
"thumb": albumArt!.thumb,
|
||||||
"lyrics": jsonEncode(lyrics!.toJson()),
|
"lyrics": jsonEncode(lyrics!.toJson()),
|
||||||
"albumId": album!.id,
|
"albumId": album!.id,
|
||||||
|
"trackToken": trackToken,
|
||||||
|
"trackTokenExpiration": trackTokenExpiration,
|
||||||
"artists":
|
"artists":
|
||||||
jsonEncode(artists!.map<Map>((art) => art.toJson()).toList())
|
jsonEncode(artists!.map<Map>((art) => art.toJson()).toList())
|
||||||
});
|
});
|
||||||
|
|
@ -151,21 +163,24 @@ class Track extends DeezerMediaItem {
|
||||||
title = "${json['SNG_TITLE']} ${json['VERSION']}";
|
title = "${json['SNG_TITLE']} ${json['VERSION']}";
|
||||||
}
|
}
|
||||||
return Track(
|
return Track(
|
||||||
id: json['SNG_ID'].toString(),
|
id: json['SNG_ID'].toString(),
|
||||||
title: title!,
|
title: title!,
|
||||||
duration: Duration(seconds: int.parse(json['DURATION'])),
|
duration: Duration(seconds: int.parse(json['DURATION'])),
|
||||||
albumArt: DeezerImageDetails(json['ALB_PICTURE']),
|
albumArt: DeezerImageDetails(json['ALB_PICTURE']),
|
||||||
album: Album.fromPrivateJson(json),
|
album: Album.fromPrivateJson(json),
|
||||||
artists: (json['ARTISTS'] ?? [json])
|
artists: (json['ARTISTS'] ?? [json])
|
||||||
.map<Artist>((dynamic art) => Artist.fromPrivateJson(art))
|
.map<Artist>((dynamic art) => Artist.fromPrivateJson(art))
|
||||||
.toList(),
|
.toList(),
|
||||||
trackNumber: int.parse((json['TRACK_NUMBER'] ?? '0').toString()),
|
trackNumber: int.parse((json['TRACK_NUMBER'] ?? '0').toString()),
|
||||||
playbackDetails: [json['MD5_ORIGIN'], json['MEDIA_VERSION']],
|
playbackDetails: [json['MD5_ORIGIN'], json['MEDIA_VERSION']],
|
||||||
lyrics: Lyrics(id: json['LYRICS_ID'].toString()),
|
lyrics: Lyrics(id: json['LYRICS_ID'].toString()),
|
||||||
favorite: favorite,
|
favorite: favorite,
|
||||||
diskNumber: int.parse(json['DISK_NUMBER'] ?? '1'),
|
diskNumber: int.parse(json['DISK_NUMBER'] ?? '1'),
|
||||||
explicit: (json['EXPLICIT_LYRICS'].toString() == '1') ? true : false,
|
explicit: (json['EXPLICIT_LYRICS'].toString() == '1') ? true : false,
|
||||||
addedDate: json['DATE_ADD']);
|
addedDate: json['DATE_ADD'],
|
||||||
|
trackToken: json['TRACK_TOKEN'],
|
||||||
|
trackTokenExpiration: json['TRACK_TOKEN_EXPIRE'],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
Map<String, dynamic> toSQL({off = false}) => {
|
Map<String, dynamic> toSQL({off = false}) => {
|
||||||
'id': id,
|
'id': id,
|
||||||
|
|
@ -413,7 +428,7 @@ class Artist extends DeezerMediaItem {
|
||||||
picture: json.containsKey('ART_PICTURE')
|
picture: json.containsKey('ART_PICTURE')
|
||||||
? DeezerImageDetails(json['ART_PICTURE'], type: 'artist')
|
? DeezerImageDetails(json['ART_PICTURE'], type: 'artist')
|
||||||
: null,
|
: null,
|
||||||
albumCount: albumsJson['total'],
|
albumCount: albumsJson['total'] as int?,
|
||||||
albums: (albumsJson['data'] ?? [])
|
albums: (albumsJson['data'] ?? [])
|
||||||
.map<Album>((dynamic data) => Album.fromPrivateJson(data))
|
.map<Album>((dynamic data) => Album.fromPrivateJson(data))
|
||||||
.toList(),
|
.toList(),
|
||||||
|
|
@ -1540,3 +1555,18 @@ extension GetId on FlowConfig {
|
||||||
}[this]!;
|
}[this]!;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class TrackUrlSource {
|
||||||
|
final String provider;
|
||||||
|
final String url;
|
||||||
|
TrackUrlSource({required this.provider, required this.url});
|
||||||
|
|
||||||
|
factory TrackUrlSource.fromPrivateJson(Map<String, dynamic> json) =>
|
||||||
|
TrackUrlSource(provider: json['provider'], url: json['url']);
|
||||||
|
}
|
||||||
|
|
||||||
|
class GetTrackUrlResponse {
|
||||||
|
final List<TrackUrlSource>? sources;
|
||||||
|
final String? error;
|
||||||
|
GetTrackUrlResponse({this.sources, this.error});
|
||||||
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
49
lib/api/paths.dart
Normal file
49
lib/api/paths.dart
Normal file
|
|
@ -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<String> 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<String> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,8 @@ import 'package:freezer/api/cache.dart';
|
||||||
import 'package:freezer/api/deezer.dart';
|
import 'package:freezer/api/deezer.dart';
|
||||||
import 'package:freezer/api/deezer_audio_source.dart';
|
import 'package:freezer/api/deezer_audio_source.dart';
|
||||||
import 'package:freezer/api/offline_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/api/url_audio_source.dart';
|
||||||
import 'package:freezer/ui/android_auto.dart';
|
import 'package:freezer/ui/android_auto.dart';
|
||||||
import 'package:hive_flutter/hive_flutter.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:collection/collection.dart';
|
||||||
import 'package:scrobblenaut/scrobblenaut.dart';
|
import 'package:scrobblenaut/scrobblenaut.dart';
|
||||||
import 'package:rxdart/rxdart.dart';
|
import 'package:rxdart/rxdart.dart';
|
||||||
|
import 'package:media_kit/media_kit.dart' show MPVLogLevel;
|
||||||
|
|
||||||
import 'definitions.dart';
|
import '../definitions.dart';
|
||||||
import '../settings.dart';
|
import '../../settings.dart';
|
||||||
|
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
@ -31,291 +34,6 @@ import 'dart:convert';
|
||||||
PlayerHelper playerHelper = PlayerHelper();
|
PlayerHelper playerHelper = PlayerHelper();
|
||||||
late AudioHandler audioHandler;
|
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<StreamQualityInfo>();
|
|
||||||
ValueStream<StreamQualityInfo> get streamInfo => _streamInfoSubject.stream;
|
|
||||||
|
|
||||||
final _bufferPositionSubject = BehaviorSubject<Duration>();
|
|
||||||
ValueStream<Duration> 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<void> 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<void> 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<void> authorizeLastFM() async {
|
|
||||||
if (settings.lastFMUsername == null || settings.lastFMPassword == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await audioHandler.customAction('authorizeLastFM', {
|
|
||||||
'username': settings.lastFMUsername,
|
|
||||||
'password': settings.lastFMPassword
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> 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<void> _loadQueuePlay(List<MediaItem> 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<Track> 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<Track> 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<void> playShowEpisode(Show show, List<ShowEpisode> episodes,
|
|
||||||
{int index = 0}) async {
|
|
||||||
QueueSource queueSource =
|
|
||||||
QueueSource(id: show.id, text: show.name, source: 'show');
|
|
||||||
//Generate media items
|
|
||||||
List<MediaItem> queue =
|
|
||||||
episodes.map<MediaItem>((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<Track?> tracks, String? trackId, QueueSource queueSource) async {
|
|
||||||
final queue = await Future.wait(tracks
|
|
||||||
.map<Future<MediaItem>>((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 {
|
class AudioPlayerTaskInitArguments {
|
||||||
final bool ignoreInterruptions;
|
final bool ignoreInterruptions;
|
||||||
final bool seekAsSkip;
|
final bool seekAsSkip;
|
||||||
|
|
@ -380,6 +98,7 @@ class AudioPlayerTask extends BaseAudioHandler {
|
||||||
StreamSubscription? _audioSessionSubscription;
|
StreamSubscription? _audioSessionSubscription;
|
||||||
StreamSubscription? _visualizerSubscription;
|
StreamSubscription? _visualizerSubscription;
|
||||||
StreamSubscription? _connectivitySubscription;
|
StreamSubscription? _connectivitySubscription;
|
||||||
|
bool _isConnectivityPluginAvailable = true;
|
||||||
|
|
||||||
/// Android Auto helper class for navigation
|
/// Android Auto helper class for navigation
|
||||||
late final AndroidAuto _androidAuto;
|
late final AndroidAuto _androidAuto;
|
||||||
|
|
@ -436,6 +155,7 @@ class AudioPlayerTask extends BaseAudioHandler {
|
||||||
// Linux/Windows specific options
|
// Linux/Windows specific options
|
||||||
JustAudioMediaKit.title = 'Freezer';
|
JustAudioMediaKit.title = 'Freezer';
|
||||||
JustAudioMediaKit.protocolWhitelist = const ['http'];
|
JustAudioMediaKit.protocolWhitelist = const ['http'];
|
||||||
|
JustAudioMediaKit.mpvLogLevel = MPVLogLevel.debug;
|
||||||
|
|
||||||
_deezerAPI = initArgs.deezerAPI;
|
_deezerAPI = initArgs.deezerAPI;
|
||||||
_androidAuto = AndroidAuto(deezerAPI: _deezerAPI);
|
_androidAuto = AndroidAuto(deezerAPI: _deezerAPI);
|
||||||
|
|
@ -445,8 +165,7 @@ class AudioPlayerTask extends BaseAudioHandler {
|
||||||
final session = await AudioSession.instance;
|
final session = await AudioSession.instance;
|
||||||
session.configure(const AudioSessionConfiguration.music());
|
session.configure(const AudioSessionConfiguration.music());
|
||||||
|
|
||||||
_box = await Hive.openLazyBox('playback',
|
_box = await Hive.openLazyBox('playback', path: await Paths.cacheDir());
|
||||||
path: (await getTemporaryDirectory()).path);
|
|
||||||
|
|
||||||
_player = AudioPlayer(
|
_player = AudioPlayer(
|
||||||
handleInterruptions: !initArgs.ignoreInterruptions,
|
handleInterruptions: !initArgs.ignoreInterruptions,
|
||||||
|
|
@ -461,6 +180,8 @@ class AudioPlayerTask extends BaseAudioHandler {
|
||||||
|
|
||||||
//Update track index
|
//Update track index
|
||||||
_player.currentIndexStream.listen((index) {
|
_player.currentIndexStream.listen((index) {
|
||||||
|
_amountSeeked = 0;
|
||||||
|
_amountPaused = 0;
|
||||||
_timestamp = DateTime.now().millisecondsSinceEpoch;
|
_timestamp = DateTime.now().millisecondsSinceEpoch;
|
||||||
if (index != null && queue.value.isNotEmpty) {
|
if (index != null && queue.value.isNotEmpty) {
|
||||||
_queueIndex = index;
|
_queueIndex = index;
|
||||||
|
|
@ -507,17 +228,11 @@ class AudioPlayerTask extends BaseAudioHandler {
|
||||||
// queue.add(_queue);
|
// queue.add(_queue);
|
||||||
|
|
||||||
// Determine audio quality to use
|
// Determine audio quality to use
|
||||||
try {
|
if (await _determineAudioQuality()) {
|
||||||
await Connectivity().checkConnectivity().then(_determineAudioQuality);
|
|
||||||
|
|
||||||
// listen for connectivity changes
|
// listen for connectivity changes
|
||||||
_connectivitySubscription =
|
_connectivitySubscription = Connectivity()
|
||||||
Connectivity().onConnectivityChanged.listen(_determineAudioQuality);
|
.onConnectivityChanged
|
||||||
} catch (e) {
|
.listen(_determineAudioQualityByResult);
|
||||||
_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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await _loadQueueFile();
|
await _loadQueueFile();
|
||||||
|
|
@ -530,7 +245,31 @@ class AudioPlayerTask extends BaseAudioHandler {
|
||||||
customEvent.add({'action': 'onLoad'});
|
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<bool> _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) {
|
switch (result) {
|
||||||
case ConnectivityResult.mobile:
|
case ConnectivityResult.mobile:
|
||||||
case ConnectivityResult.bluetooth:
|
case ConnectivityResult.bluetooth:
|
||||||
|
|
@ -543,6 +282,8 @@ class AudioPlayerTask extends BaseAudioHandler {
|
||||||
default:
|
default:
|
||||||
_currentQuality = wifiQuality;
|
_currentQuality = wifiQuality;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
print('quality: $_currentQuality');
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -674,6 +415,8 @@ class AudioPlayerTask extends BaseAudioHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _logListenedTrack(MediaItem mediaItem) async {
|
Future<void> _logListenedTrack(MediaItem mediaItem) async {
|
||||||
|
print(
|
||||||
|
'logging: seek: $_amountSeeked, pause: $_amountPaused, timestamp: $_timestamp (elapsed: ${DateTime.now().millisecondsSinceEpoch - _timestamp!}ms)');
|
||||||
if (!_shouldLogTracks) return;
|
if (!_shouldLogTracks) return;
|
||||||
|
|
||||||
//Log to Deezer
|
//Log to Deezer
|
||||||
|
|
@ -887,6 +630,8 @@ class AudioPlayerTask extends BaseAudioHandler {
|
||||||
return DeezerAudioSource(
|
return DeezerAudioSource(
|
||||||
getQuality: () => _currentQuality,
|
getQuality: () => _currentQuality,
|
||||||
trackId: mediaItem.id,
|
trackId: mediaItem.id,
|
||||||
|
trackToken: mediaItem.extras!['trackToken'] ?? '',
|
||||||
|
trackTokenExpiration: mediaItem.extras!['trackTokenExpiration'] ?? 0,
|
||||||
md5origin: playbackDetails![0],
|
md5origin: playbackDetails![0],
|
||||||
mediaVersion: playbackDetails[1],
|
mediaVersion: playbackDetails[1],
|
||||||
onStreamObtained: (qualityInfo) =>
|
onStreamObtained: (qualityInfo) =>
|
||||||
|
|
@ -903,6 +648,7 @@ class AudioPlayerTask extends BaseAudioHandler {
|
||||||
//Isolate can't access globals
|
//Isolate can't access globals
|
||||||
wifiQuality = extras!['wifiQuality'] as AudioQuality;
|
wifiQuality = extras!['wifiQuality'] as AudioQuality;
|
||||||
mobileQuality = extras['mobileQuality'] as AudioQuality;
|
mobileQuality = extras['mobileQuality'] as AudioQuality;
|
||||||
|
_determineAudioQuality();
|
||||||
break;
|
break;
|
||||||
//Update queue source
|
//Update queue source
|
||||||
case 'queueSource':
|
case 'queueSource':
|
||||||
|
|
@ -985,6 +731,7 @@ class AudioPlayerTask extends BaseAudioHandler {
|
||||||
_audioSessionSubscription?.cancel();
|
_audioSessionSubscription?.cancel();
|
||||||
_visualizerSubscription?.cancel();
|
_visualizerSubscription?.cancel();
|
||||||
_bufferPositionSubscription?.cancel();
|
_bufferPositionSubscription?.cancel();
|
||||||
|
_connectivitySubscription?.cancel();
|
||||||
await super.stop();
|
await super.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
304
lib/api/player/player_helper.dart
Normal file
304
lib/api/player/player_helper.dart
Normal file
|
|
@ -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<StreamQualityInfo>();
|
||||||
|
ValueStream<StreamQualityInfo> get streamInfo => _streamInfoSubject.stream;
|
||||||
|
|
||||||
|
final _bufferPositionSubject = BehaviorSubject<Duration>();
|
||||||
|
ValueStream<Duration> 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<void> 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<void> 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<void> authorizeLastFM() async {
|
||||||
|
if (settings.lastFMUsername == null || settings.lastFMPassword == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await audioHandler.customAction('authorizeLastFM', {
|
||||||
|
'username': settings.lastFMUsername,
|
||||||
|
'password': settings.lastFMPassword
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> 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<void> _loadQueuePlay(List<MediaItem> 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<Track> 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<Track> 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<void> playShowEpisode(Show show, List<ShowEpisode> episodes,
|
||||||
|
{int index = 0}) async {
|
||||||
|
QueueSource queueSource =
|
||||||
|
QueueSource(id: show.id, text: show.name, source: 'show');
|
||||||
|
//Generate media items
|
||||||
|
List<MediaItem> queue =
|
||||||
|
episodes.map<MediaItem>((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<Track?> tracks, String? trackId, QueueSource queueSource) async {
|
||||||
|
final queue = await Future.wait(tracks
|
||||||
|
.map<Future<MediaItem>>((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');
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
@ -12,11 +12,13 @@ import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
import 'package:freezer/api/cache.dart';
|
import 'package:freezer/api/cache.dart';
|
||||||
import 'package:freezer/api/definitions.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/blur_slide.dart';
|
||||||
import 'package:freezer/page_routes/fade.dart';
|
import 'package:freezer/page_routes/fade.dart';
|
||||||
import 'package:freezer/page_routes/scale_fade.dart';
|
import 'package:freezer/page_routes/scale_fade.dart';
|
||||||
import 'package:freezer/type_adapters/uri.dart';
|
import 'package:freezer/type_adapters/uri.dart';
|
||||||
import 'package:freezer/ui/downloads_screen.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/library.dart';
|
||||||
import 'package:freezer/ui/login_screen.dart';
|
import 'package:freezer/ui/login_screen.dart';
|
||||||
import 'package:freezer/ui/player_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/deezer.dart';
|
||||||
import 'api/download.dart';
|
import 'api/download.dart';
|
||||||
import 'api/player.dart';
|
import 'api/player/audio_handler.dart';
|
||||||
import 'settings.dart';
|
import 'settings.dart';
|
||||||
import 'ui/home_screen.dart';
|
import 'ui/home_screen.dart';
|
||||||
import 'ui/player_bar.dart';
|
import 'ui/player_bar.dart';
|
||||||
|
|
@ -54,19 +56,18 @@ void main() async {
|
||||||
..registerAdapter(HomePageItemAdapter())
|
..registerAdapter(HomePageItemAdapter())
|
||||||
..registerAdapter(HomePageItemTypeAdapter())
|
..registerAdapter(HomePageItemTypeAdapter())
|
||||||
..registerAdapter(HomePageSectionLayoutAdapter())
|
..registerAdapter(HomePageSectionLayoutAdapter())
|
||||||
|
..registerAdapter(SmartTrackListAdapter())
|
||||||
..registerAdapter(TrackAdapter())
|
..registerAdapter(TrackAdapter())
|
||||||
..registerAdapter(AlbumAdapter())
|
|
||||||
..registerAdapter(ArtistAdapter())
|
|
||||||
..registerAdapter(PlaylistAdapter())
|
|
||||||
..registerAdapter(UserAdapter())
|
|
||||||
..registerAdapter(DeezerImageDetailsAdapter())
|
..registerAdapter(DeezerImageDetailsAdapter())
|
||||||
..registerAdapter(LyricsAdapter())
|
..registerAdapter(LyricsAdapter())
|
||||||
..registerAdapter(LyricAdapter())
|
..registerAdapter(LyricAdapter())
|
||||||
..registerAdapter(SmartTrackListAdapter())
|
..registerAdapter(PlaylistAdapter())
|
||||||
|
..registerAdapter(ArtistAdapter())
|
||||||
|
..registerAdapter(AlbumAdapter())
|
||||||
|
..registerAdapter(UserAdapter())
|
||||||
|
..registerAdapter(AlbumTypeAdapter())
|
||||||
..registerAdapter(DeezerChannelAdapter())
|
..registerAdapter(DeezerChannelAdapter())
|
||||||
..registerAdapter(ShowAdapter())
|
..registerAdapter(ShowAdapter())
|
||||||
..registerAdapter(AlbumTypeAdapter())
|
|
||||||
..registerAdapter(ColorAdapter())
|
|
||||||
..registerAdapter(DurationAdapter())
|
..registerAdapter(DurationAdapter())
|
||||||
..registerAdapter(SortingAdapter())
|
..registerAdapter(SortingAdapter())
|
||||||
..registerAdapter(SortTypeAdapter())
|
..registerAdapter(SortTypeAdapter())
|
||||||
|
|
@ -74,6 +75,7 @@ void main() async {
|
||||||
..registerAdapter(SearchHistoryItemAdapter())
|
..registerAdapter(SearchHistoryItemAdapter())
|
||||||
..registerAdapter(SearchHistoryItemTypeAdapter())
|
..registerAdapter(SearchHistoryItemTypeAdapter())
|
||||||
..registerAdapter(CacheAdapter())
|
..registerAdapter(CacheAdapter())
|
||||||
|
..registerAdapter(ColorAdapter())
|
||||||
..registerAdapter(DateTimeAdapter())
|
..registerAdapter(DateTimeAdapter())
|
||||||
..registerAdapter(MediaItemAdapter())
|
..registerAdapter(MediaItemAdapter())
|
||||||
..registerAdapter(AudioServiceRepeatModeAdapter())
|
..registerAdapter(AudioServiceRepeatModeAdapter())
|
||||||
|
|
@ -84,8 +86,10 @@ void main() async {
|
||||||
..registerAdapter(NavigatorRouteTypeAdapter())
|
..registerAdapter(NavigatorRouteTypeAdapter())
|
||||||
..registerAdapter(UriAdapter())
|
..registerAdapter(UriAdapter())
|
||||||
..registerAdapter(QueueSourceAdapter())
|
..registerAdapter(QueueSourceAdapter())
|
||||||
..registerAdapter(HomePageAdapter());
|
..registerAdapter(HomePageAdapter())
|
||||||
await Hive.initFlutter();
|
..registerAdapter(NavigationRailAppearanceAdapter());
|
||||||
|
|
||||||
|
Hive.init(await Paths.dataDirectory());
|
||||||
|
|
||||||
//Initialize globals
|
//Initialize globals
|
||||||
settings = await Settings.load();
|
settings = await Settings.load();
|
||||||
|
|
@ -647,7 +651,7 @@ class MainScreenState extends State<MainScreen>
|
||||||
isDesktop = isLandscape && constraints.maxWidth > 1024;
|
isDesktop = isLandscape && constraints.maxWidth > 1024;
|
||||||
return FancyScaffold(
|
return FancyScaffold(
|
||||||
key: _fancyScaffoldKey,
|
key: _fancyScaffoldKey,
|
||||||
bodyDrawer: _buildNavigationRail(isDesktop),
|
navigationRail: _buildNavigationRail(isDesktop),
|
||||||
bottomNavigationBar: buildBottomBar(isDesktop),
|
bottomNavigationBar: buildBottomBar(isDesktop),
|
||||||
bottomPanel: PlayerBar(
|
bottomPanel: PlayerBar(
|
||||||
focusNode: playerBarFocusNode,
|
focusNode: playerBarFocusNode,
|
||||||
|
|
|
||||||
|
|
@ -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/page_routes/basic_page_route.dart';
|
||||||
import 'package:freezer/ui/animated_blur.dart';
|
import 'package:freezer/ui/animated_blur.dart';
|
||||||
|
|
||||||
|
|
@ -6,7 +6,7 @@ class FadePageRoute<T> extends BasicPageRoute<T> {
|
||||||
@override
|
@override
|
||||||
final bool barrierDismissible;
|
final bool barrierDismissible;
|
||||||
@override
|
@override
|
||||||
final Color? barrierColor;
|
final Color barrierColor;
|
||||||
@override
|
@override
|
||||||
final bool opaque;
|
final bool opaque;
|
||||||
|
|
||||||
|
|
@ -18,7 +18,7 @@ class FadePageRoute<T> extends BasicPageRoute<T> {
|
||||||
super.transitionDuration,
|
super.transitionDuration,
|
||||||
super.maintainState,
|
super.maintainState,
|
||||||
super.settings,
|
super.settings,
|
||||||
this.barrierColor,
|
this.barrierColor = Colors.black38,
|
||||||
this.barrierDismissible = false,
|
this.barrierDismissible = false,
|
||||||
this.opaque = true,
|
this.opaque = true,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:freezer/api/definitions.dart';
|
import 'package:freezer/api/definitions.dart';
|
||||||
import 'package:freezer/api/download.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:google_fonts/google_fonts.dart';
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
import 'package:json_annotation/json_annotation.dart';
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
@ -176,6 +178,20 @@ class Settings {
|
||||||
|
|
||||||
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 {
|
ThemeData? get themeData {
|
||||||
//System theme
|
//System theme
|
||||||
if (useSystemTheme) {
|
if (useSystemTheme) {
|
||||||
|
|
@ -376,14 +392,38 @@ enum AudioQuality {
|
||||||
ASK
|
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() {
|
int toDeezerQualityInt() {
|
||||||
return const {
|
return const {
|
||||||
AudioQuality.MP3_128: 1,
|
AudioQuality.MP3_128: 1,
|
||||||
AudioQuality.MP3_320: 3,
|
AudioQuality.MP3_320: 3,
|
||||||
AudioQuality.FLAC: 9,
|
AudioQuality.FLAC: 9,
|
||||||
}[this] ??
|
}[this]!;
|
||||||
8;
|
}
|
||||||
|
|
||||||
|
String toDeezerQualityString() {
|
||||||
|
return const {
|
||||||
|
AudioQuality.MP3_128: 'MP3_128',
|
||||||
|
AudioQuality.MP3_320: 'MP3_320',
|
||||||
|
AudioQuality.FLAC: 'FLAC',
|
||||||
|
}[this]!;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,482 +0,0 @@
|
||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
|
||||||
|
|
||||||
part of 'settings.dart';
|
|
||||||
|
|
||||||
// **************************************************************************
|
|
||||||
// TypeAdapterGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
class SettingsAdapter extends TypeAdapter<Settings> {
|
|
||||||
@override
|
|
||||||
final int typeId = 24;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Settings read(BinaryReader reader) {
|
|
||||||
final numOfFields = reader.readByte();
|
|
||||||
final fields = <int, dynamic>{
|
|
||||||
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<String>()
|
|
||||||
..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<SpotifyCredentialsSave> {
|
|
||||||
@override
|
|
||||||
final int typeId = 25;
|
|
||||||
|
|
||||||
@override
|
|
||||||
SpotifyCredentialsSave read(BinaryReader reader) {
|
|
||||||
final numOfFields = reader.readByte();
|
|
||||||
final fields = <int, dynamic>{
|
|
||||||
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<String>(),
|
|
||||||
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<AudioQuality> {
|
|
||||||
@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<Themes> {
|
|
||||||
@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<String, dynamic> 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<dynamic>).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<String, dynamic>)
|
|
||||||
..materialYouAccent = json['materialYouAccent'] as bool
|
|
||||||
..playerAlbumArtDropShadow = json['playerAlbumArtDropShadow'] as bool
|
|
||||||
..seekAsSkip = json['seekAsSkip'] as bool;
|
|
||||||
|
|
||||||
Map<String, dynamic> _$SettingsToJson(Settings instance) => <String, dynamic>{
|
|
||||||
'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<String, dynamic> json) =>
|
|
||||||
SpotifyCredentialsSave(
|
|
||||||
accessToken: json['accessToken'] as String?,
|
|
||||||
refreshToken: json['refreshToken'] as String?,
|
|
||||||
scopes:
|
|
||||||
(json['scopes'] as List<dynamic>?)?.map((e) => e as String).toList(),
|
|
||||||
expiration: json['expiration'] == null
|
|
||||||
? null
|
|
||||||
: DateTime.parse(json['expiration'] as String),
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$SpotifyCredentialsSaveToJson(
|
|
||||||
SpotifyCredentialsSave instance) =>
|
|
||||||
<String, dynamic>{
|
|
||||||
'accessToken': instance.accessToken,
|
|
||||||
'refreshToken': instance.refreshToken,
|
|
||||||
'scopes': instance.scopes,
|
|
||||||
'expiration': instance.expiration?.toIso8601String(),
|
|
||||||
};
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import 'package:audio_service/audio_service.dart';
|
import 'package:audio_service/audio_service.dart';
|
||||||
import 'package:freezer/api/deezer.dart';
|
import 'package:freezer/api/deezer.dart';
|
||||||
import 'package:freezer/api/definitions.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/translations.i18n.dart';
|
||||||
|
|
||||||
class AndroidAuto {
|
class AndroidAuto {
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import 'package:fluttericon/font_awesome5_icons.dart';
|
||||||
import 'package:freezer/api/cache.dart';
|
import 'package:freezer/api/cache.dart';
|
||||||
import 'package:freezer/api/deezer.dart';
|
import 'package:freezer/api/deezer.dart';
|
||||||
import 'package:freezer/api/download.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/elements.dart';
|
||||||
import 'package:freezer/ui/error.dart';
|
import 'package:freezer/ui/error.dart';
|
||||||
import 'package:freezer/ui/search.dart';
|
import 'package:freezer/ui/search.dart';
|
||||||
|
|
@ -66,6 +66,9 @@ class _AlbumDetailsState extends State<AlbumDetails> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(album!.title ?? ''),
|
||||||
|
),
|
||||||
body: _error
|
body: _error
|
||||||
? const ErrorScreen()
|
? const ErrorScreen()
|
||||||
: _loading
|
: _loading
|
||||||
|
|
@ -314,20 +317,17 @@ class ArtistDetails extends StatefulWidget {
|
||||||
|
|
||||||
class _ArtistDetailsState extends State<ArtistDetails> {
|
class _ArtistDetailsState extends State<ArtistDetails> {
|
||||||
late final Future<Artist> _future;
|
late final Future<Artist> _future;
|
||||||
|
|
||||||
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
FutureOr<Artist> future = _loadArtist(widget.artist);
|
_future = _loadArtist(widget.artist);
|
||||||
if (future is Artist) {
|
|
||||||
_future = Future.value(widget.artist);
|
|
||||||
} else {
|
|
||||||
_future = future;
|
|
||||||
}
|
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
FutureOr<Artist> _loadArtist(Artist artist) {
|
Future<Artist> _loadArtist(Artist artist) async {
|
||||||
//Load artist from api if no albums
|
//Load artist from api if no albums
|
||||||
if ((artist.albums ?? []).isEmpty) {
|
if ((artist.albums ?? []).isEmpty) {
|
||||||
return deezerAPI.artist(artist.id);
|
return await deezerAPI.artist(artist.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
return artist;
|
return artist;
|
||||||
|
|
@ -336,6 +336,7 @@ class _ArtistDetailsState extends State<ArtistDetails> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: Text(widget.artist.name ?? '')),
|
||||||
body: FutureBuilder<Artist>(
|
body: FutureBuilder<Artist>(
|
||||||
future: _future,
|
future: _future,
|
||||||
builder: (BuildContext context, snapshot) {
|
builder: (BuildContext context, snapshot) {
|
||||||
|
|
@ -424,14 +425,9 @@ class _ArtistDetailsState extends State<ArtistDetails> {
|
||||||
mainAxisSize: MainAxisSize.max,
|
mainAxisSize: MainAxisSize.max,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
TextButton(
|
TextButton.icon(
|
||||||
child: Row(
|
icon: const Icon(Icons.favorite),
|
||||||
children: <Widget>[
|
label: Text('Library'.i18n),
|
||||||
const Icon(Icons.favorite, size: 32),
|
|
||||||
const SizedBox(width: 4.0),
|
|
||||||
Text('Library'.i18n)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await deezerAPI.addFavoriteArtist(widget.artist.id);
|
await deezerAPI.addFavoriteArtist(widget.artist.id);
|
||||||
ScaffoldMessenger.of(context)
|
ScaffoldMessenger.of(context)
|
||||||
|
|
@ -439,14 +435,9 @@ class _ArtistDetailsState extends State<ArtistDetails> {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if ((artist.radio ?? false))
|
if ((artist.radio ?? false))
|
||||||
TextButton(
|
TextButton.icon(
|
||||||
child: Row(
|
icon: const Icon(Icons.radio),
|
||||||
children: <Widget>[
|
label: Text('Radio'.i18n),
|
||||||
const Icon(Icons.radio, size: 32),
|
|
||||||
const SizedBox(width: 4.0),
|
|
||||||
Text('Radio'.i18n)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
List<Track> tracks =
|
List<Track> tracks =
|
||||||
(await deezerAPI.smartRadio(artist.id))!;
|
(await deezerAPI.smartRadio(artist.id))!;
|
||||||
|
|
@ -873,238 +864,246 @@ class _PlaylistDetailsState extends State<PlaylistDetails> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: Text(playlist!.title ?? '')),
|
||||||
body: DraggableScrollbar.rrect(
|
body: DraggableScrollbar.rrect(
|
||||||
controller: _scrollController,
|
controller: _scrollController,
|
||||||
backgroundColor: Theme.of(context).primaryColor,
|
backgroundColor: Theme.of(context).primaryColor,
|
||||||
child: ListView(
|
child: ListView(
|
||||||
controller: _scrollController,
|
controller: _scrollController,
|
||||||
children: <Widget>[
|
|
||||||
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: <Widget>[
|
|
||||||
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: <Widget>[
|
|
||||||
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: <Widget>[
|
|
||||||
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: <Widget>[
|
|
||||||
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,
|
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
MakePlaylistOffline(playlist),
|
const SizedBox(height: 4.0),
|
||||||
if (playlist!.user!.name != deezerAPI.userName)
|
ConstrainedBox(
|
||||||
IconButton(
|
constraints: BoxConstraints.tight(
|
||||||
icon: Icon(
|
Size.fromHeight(MediaQuery.of(context).size.height / 3)),
|
||||||
playlist!.library!
|
child: Padding(
|
||||||
? Icons.favorite
|
padding: const EdgeInsets.symmetric(
|
||||||
: Icons.favorite_outline,
|
vertical: 4.0, horizontal: 8.0),
|
||||||
size: 32,
|
child: Row(
|
||||||
semanticLabel:
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
playlist!.library! ? "Unlove".i18n : "Love".i18n,
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
children: <Widget>[
|
||||||
|
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: <Widget>[
|
||||||
|
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: <Widget>[
|
||||||
|
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: <Widget>[
|
||||||
|
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(
|
if (playlist!.description != null &&
|
||||||
color: Theme.of(context).scaffoldBackgroundColor,
|
playlist!.description!.isNotEmpty)
|
||||||
onSelected: (SortType s) async {
|
const FreezerDivider(),
|
||||||
if (playlist!.tracks!.length < playlist!.trackCount!) {
|
if (playlist!.description != null &&
|
||||||
//Preload whole playlist
|
playlist!.description!.isNotEmpty)
|
||||||
playlist = await deezerAPI.fullPlaylist(playlist!.id);
|
Padding(
|
||||||
}
|
padding: const EdgeInsets.all(6.0),
|
||||||
setState(() => _sort!.type = s);
|
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: <Widget>[
|
||||||
|
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
|
//Save sort type to cache
|
||||||
int? index =
|
int? index = Sorting.index(SortSourceTypes.PLAYLIST,
|
||||||
Sorting.index(SortSourceTypes.PLAYLIST, id: playlist!.id);
|
id: playlist!.id);
|
||||||
if (index == null) {
|
if (index == null) {
|
||||||
cache.sorts.add(_sort);
|
cache.sorts.add(_sort);
|
||||||
} else {
|
} else {
|
||||||
cache.sorts[index] = _sort;
|
cache.sorts[index] = _sort;
|
||||||
}
|
}
|
||||||
await cache.save();
|
await cache.save();
|
||||||
},
|
},
|
||||||
itemBuilder: (context) => <PopupMenuEntry<SortType>>[
|
itemBuilder: (context) => <PopupMenuEntry<SortType>>[
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: SortType.DEFAULT,
|
value: SortType.DEFAULT,
|
||||||
child: Text('Default'.i18n, style: popupMenuTextStyle()),
|
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(
|
IconButton(
|
||||||
value: SortType.ALPHABETIC,
|
icon: Icon(
|
||||||
child: Text('Alphabetic'.i18n, style: popupMenuTextStyle()),
|
_sort!.reverse!
|
||||||
),
|
? FontAwesome5.sort_alpha_up
|
||||||
PopupMenuItem(
|
: FontAwesome5.sort_alpha_down,
|
||||||
value: SortType.ARTIST,
|
semanticLabel: _sort!.reverse!
|
||||||
child: Text('Artist'.i18n, style: popupMenuTextStyle()),
|
? "Sort descending".i18n
|
||||||
),
|
: "Sort ascending".i18n,
|
||||||
PopupMenuItem(
|
),
|
||||||
value: SortType.DATE_ADDED,
|
onPressed: () => _reverse(),
|
||||||
child: Text('Date added'.i18n, style: popupMenuTextStyle()),
|
|
||||||
),
|
),
|
||||||
|
Container(width: 4.0)
|
||||||
],
|
],
|
||||||
child: Icon(
|
|
||||||
Icons.sort,
|
|
||||||
size: 32.0,
|
|
||||||
semanticLabel: "Sort playlist".i18n,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
IconButton(
|
const FreezerDivider(),
|
||||||
icon: Icon(
|
if (playlist!.tracks!.isEmpty)
|
||||||
_sort!.reverse!
|
const Center(child: CircularProgressIndicator()),
|
||||||
? FontAwesome5.sort_alpha_up
|
...List.generate(playlist!.tracks!.length, (i) {
|
||||||
: FontAwesome5.sort_alpha_down,
|
Track t = sorted[i];
|
||||||
semanticLabel: _sort!.reverse!
|
return TrackTile.fromTrack(t, onTap: () {
|
||||||
? "Sort descending".i18n
|
Playlist p = Playlist(
|
||||||
: "Sort ascending".i18n,
|
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: <Widget>[CircularProgressIndicator()],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
onPressed: () => _reverse(),
|
if (_error) const ErrorScreen()
|
||||||
),
|
|
||||||
Container(width: 4.0)
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
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: <Widget>[CircularProgressIndicator()],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (_error) const ErrorScreen()
|
|
||||||
],
|
|
||||||
),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
195
lib/ui/fancy_scaffold.dart
Normal file
195
lib/ui/fancy_scaffold.dart
Normal file
|
|
@ -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<FancyScaffoldState>();
|
||||||
|
|
||||||
|
@override
|
||||||
|
FancyScaffoldState createState() => FancyScaffoldState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class FancyScaffoldState extends State<FancyScaffold>
|
||||||
|
with TickerProviderStateMixin {
|
||||||
|
// goes from 0 to 1 (double)
|
||||||
|
// 0 = preview, 1 = expanded
|
||||||
|
late final AnimationController dragController;
|
||||||
|
final statusNotifier =
|
||||||
|
ValueNotifier<AnimationStatus>(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<double>(
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:freezer/api/deezer.dart';
|
import 'package:freezer/api/deezer.dart';
|
||||||
import 'package:freezer/api/definitions.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/error.dart';
|
||||||
import 'package:freezer/ui/menu.dart';
|
import 'package:freezer/ui/menu.dart';
|
||||||
import 'package:freezer/translations.i18n.dart';
|
import 'package:freezer/translations.i18n.dart';
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import 'package:freezer/api/cache.dart';
|
||||||
import 'package:freezer/api/deezer.dart';
|
import 'package:freezer/api/deezer.dart';
|
||||||
import 'package:freezer/api/definitions.dart';
|
import 'package:freezer/api/definitions.dart';
|
||||||
import 'package:freezer/api/importer.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/settings.dart';
|
||||||
import 'package:freezer/ui/details_screens.dart';
|
import 'package:freezer/ui/details_screens.dart';
|
||||||
import 'package:freezer/ui/elements.dart';
|
import 'package:freezer/ui/elements.dart';
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
import 'package:freezer/api/deezer.dart';
|
import 'package:freezer/api/deezer.dart';
|
||||||
import 'package:freezer/api/definitions.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/settings.dart';
|
||||||
import 'package:freezer/translations.i18n.dart';
|
import 'package:freezer/translations.i18n.dart';
|
||||||
import 'package:freezer/ui/error.dart';
|
import 'package:freezer/ui/error.dart';
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:freezer/main.dart';
|
import 'package:freezer/main.dart';
|
||||||
|
import 'package:freezer/ui/fancy_scaffold.dart';
|
||||||
import 'package:freezer/ui/player_bar.dart';
|
import 'package:freezer/ui/player_bar.dart';
|
||||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:freezer/api/cache.dart';
|
import 'package:freezer/api/cache.dart';
|
||||||
import 'package:freezer/api/deezer.dart';
|
import 'package:freezer/api/deezer.dart';
|
||||||
import 'package:freezer/api/download.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/details_screens.dart';
|
||||||
import 'package:freezer/ui/error.dart';
|
import 'package:freezer/ui/error.dart';
|
||||||
import 'package:freezer/translations.i18n.dart';
|
import 'package:freezer/translations.i18n.dart';
|
||||||
|
|
@ -117,7 +118,7 @@ class MenuSheet {
|
||||||
showMenu(
|
showMenu(
|
||||||
elevation: 4.0,
|
elevation: 4.0,
|
||||||
context: context,
|
context: context,
|
||||||
constraints: const BoxConstraints(maxWidth: 300),
|
constraints: const BoxConstraints(maxWidth: 300.0),
|
||||||
position:
|
position:
|
||||||
RelativeRect.fromSize(actualPosition & Size.zero, overlay.size),
|
RelativeRect.fromSize(actualPosition & Size.zero, overlay.size),
|
||||||
items: options
|
items: options
|
||||||
|
|
@ -128,7 +129,7 @@ class MenuSheet {
|
||||||
: Row(mainAxisSize: MainAxisSize.min, children: [
|
: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||||
option.icon!,
|
option.icon!,
|
||||||
const SizedBox(width: 8.0),
|
const SizedBox(width: 8.0),
|
||||||
option.label,
|
Flexible(child: option.label),
|
||||||
])))
|
])))
|
||||||
.toList(growable: false));
|
.toList(growable: false));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,200 +4,12 @@ import 'package:audio_service/audio_service.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:freezer/settings.dart';
|
import 'package:freezer/settings.dart';
|
||||||
import 'package:freezer/translations.i18n.dart';
|
import 'package:freezer/translations.i18n.dart';
|
||||||
|
import 'package:freezer/ui/fancy_scaffold.dart';
|
||||||
import 'package:rxdart/rxdart.dart';
|
import 'package:rxdart/rxdart.dart';
|
||||||
|
|
||||||
import '../api/player.dart';
|
import '../api/player/audio_handler.dart';
|
||||||
import 'cached_image.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<FancyScaffoldState>();
|
|
||||||
|
|
||||||
@override
|
|
||||||
FancyScaffoldState createState() => FancyScaffoldState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class FancyScaffoldState extends State<FancyScaffold>
|
|
||||||
with TickerProviderStateMixin {
|
|
||||||
// goes from 0 to 1 (double)
|
|
||||||
// 0 = preview, 1 = expanded
|
|
||||||
late final AnimationController dragController;
|
|
||||||
final statusNotifier =
|
|
||||||
ValueNotifier<AnimationStatus>(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<double>(
|
|
||||||
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 {
|
class PlayerBar extends StatelessWidget {
|
||||||
final VoidCallback? onTap;
|
final VoidCallback? onTap;
|
||||||
final bool shouldHaveHero;
|
final bool shouldHaveHero;
|
||||||
|
|
|
||||||
|
|
@ -11,12 +11,13 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
import 'package:freezer/api/cache.dart';
|
import 'package:freezer/api/cache.dart';
|
||||||
import 'package:freezer/api/deezer.dart';
|
import 'package:freezer/api/deezer.dart';
|
||||||
import 'package:freezer/api/definitions.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/main.dart';
|
||||||
import 'package:freezer/page_routes/fade.dart';
|
import 'package:freezer/page_routes/fade.dart';
|
||||||
import 'package:freezer/settings.dart';
|
import 'package:freezer/settings.dart';
|
||||||
import 'package:freezer/translations.i18n.dart';
|
import 'package:freezer/translations.i18n.dart';
|
||||||
import 'package:freezer/ui/cached_image.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/lyrics_screen.dart';
|
||||||
import 'package:freezer/ui/menu.dart';
|
import 'package:freezer/ui/menu.dart';
|
||||||
import 'package:freezer/ui/player_bar.dart';
|
import 'package:freezer/ui/player_bar.dart';
|
||||||
|
|
@ -1151,7 +1152,8 @@ class BottomBarControls extends StatelessWidget {
|
||||||
),
|
),
|
||||||
iconSize: size * 0.85,
|
iconSize: size * 0.85,
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await deezerAPI.dislikeTrack(audioHandler.mediaItem.value!.id);
|
unawaited(
|
||||||
|
deezerAPI.dislikeTrack(audioHandler.mediaItem.value!.id));
|
||||||
if (playerHelper.queueIndex <
|
if (playerHelper.queueIndex <
|
||||||
audioHandler.queue.value.length - 1) {
|
audioHandler.queue.value.length - 1) {
|
||||||
audioHandler.skipToNext();
|
audioHandler.skipToNext();
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import 'package:audio_service/audio_service.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:freezer/api/definitions.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/translations.i18n.dart';
|
||||||
import 'package:freezer/ui/menu.dart';
|
import 'package:freezer/ui/menu.dart';
|
||||||
import 'package:freezer/ui/tiles.dart';
|
import 'package:freezer/ui/tiles.dart';
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
import 'package:fluttericon/typicons_icons.dart';
|
import 'package:fluttericon/typicons_icons.dart';
|
||||||
import 'package:freezer/api/cache.dart';
|
import 'package:freezer/api/cache.dart';
|
||||||
import 'package:freezer/api/download.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/notifiers/list_notifier.dart';
|
||||||
import 'package:freezer/ui/details_screens.dart';
|
import 'package:freezer/ui/details_screens.dart';
|
||||||
import 'package:freezer/ui/elements.dart';
|
import 'package:freezer/ui/elements.dart';
|
||||||
|
|
@ -283,7 +283,7 @@ class _SearchScreenState extends State<SearchScreen> {
|
||||||
List<Track?> queue = cache.searchHistory
|
List<Track?> queue = cache.searchHistory
|
||||||
.where((h) =>
|
.where((h) =>
|
||||||
h.type == SearchHistoryItemType.track)
|
h.type == SearchHistoryItemType.track)
|
||||||
.map<Track>((t) => Track.fromJson(t.data))
|
.map<Track>((t) => t.data)
|
||||||
.toList();
|
.toList();
|
||||||
playerHelper.playFromTrackList(
|
playerHelper.playFromTrackList(
|
||||||
queue,
|
queue,
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ import 'package:url_launcher/url_launcher.dart';
|
||||||
import 'package:freezer/api/cache.dart';
|
import 'package:freezer/api/cache.dart';
|
||||||
import 'package:freezer/api/deezer.dart';
|
import 'package:freezer/api/deezer.dart';
|
||||||
import 'package:freezer/api/download.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/downloads_screen.dart';
|
||||||
import 'package:freezer/ui/elements.dart';
|
import 'package:freezer/ui/elements.dart';
|
||||||
import 'package:freezer/ui/home_screen.dart';
|
import 'package:freezer/ui/home_screen.dart';
|
||||||
|
|
@ -579,19 +579,29 @@ class _QualitySettingsState extends State<QualitySettings> {
|
||||||
appBar: AppBar(title: Text('Quality'.i18n)),
|
appBar: AppBar(title: Text('Quality'.i18n)),
|
||||||
body: ListView(
|
body: ListView(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
ListTile(
|
...(playerHelper.isConnectivityPluginAvailable
|
||||||
title: Text('Mobile streaming'.i18n),
|
? [
|
||||||
leading:
|
ListTile(
|
||||||
const LeadingIcon(Icons.network_cell, color: Color(0xff384697)),
|
title: Text('Mobile streaming'.i18n),
|
||||||
),
|
leading: const LeadingIcon(Icons.network_cell,
|
||||||
const QualityPicker('mobile'),
|
color: Color(0xff384697)),
|
||||||
const FreezerDivider(),
|
),
|
||||||
ListTile(
|
const QualityPicker('mobile'),
|
||||||
title: Text('Wifi streaming'.i18n),
|
const FreezerDivider(),
|
||||||
leading:
|
ListTile(
|
||||||
const LeadingIcon(Icons.network_wifi, color: Color(0xff0880b5)),
|
title: Text('Wifi streaming'.i18n),
|
||||||
),
|
leading: const LeadingIcon(Icons.network_wifi,
|
||||||
const QualityPicker('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(),
|
const FreezerDivider(),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text('Offline'.i18n),
|
title: Text('Offline'.i18n),
|
||||||
|
|
@ -622,6 +632,8 @@ class QualityPicker extends StatefulWidget {
|
||||||
|
|
||||||
class _QualityPickerState extends State<QualityPicker> {
|
class _QualityPickerState extends State<QualityPicker> {
|
||||||
late AudioQuality _quality;
|
late AudioQuality _quality;
|
||||||
|
bool flacDisabled = !cache.canStreamLossless;
|
||||||
|
bool hqDisabled = !cache.canStreamHQ;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
|
@ -632,6 +644,7 @@ class _QualityPickerState extends State<QualityPicker> {
|
||||||
//Get current quality
|
//Get current quality
|
||||||
void _getQuality() {
|
void _getQuality() {
|
||||||
switch (widget.field) {
|
switch (widget.field) {
|
||||||
|
case 'mobile_wifi':
|
||||||
case 'mobile':
|
case 'mobile':
|
||||||
_quality = settings.mobileQuality;
|
_quality = settings.mobileQuality;
|
||||||
break;
|
break;
|
||||||
|
|
@ -655,6 +668,10 @@ class _QualityPickerState extends State<QualityPicker> {
|
||||||
_quality = q;
|
_quality = q;
|
||||||
});
|
});
|
||||||
switch (widget.field) {
|
switch (widget.field) {
|
||||||
|
case 'mobile_wifi':
|
||||||
|
settings.mobileQuality = settings.wifiQuality = _quality;
|
||||||
|
settings.updateAudioServiceQuality();
|
||||||
|
break;
|
||||||
case 'mobile':
|
case 'mobile':
|
||||||
settings.mobileQuality = _quality;
|
settings.mobileQuality = _quality;
|
||||||
settings.updateAudioServiceQuality();
|
settings.updateAudioServiceQuality();
|
||||||
|
|
@ -671,7 +688,6 @@ class _QualityPickerState extends State<QualityPicker> {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
await settings.save();
|
await settings.save();
|
||||||
await settings.updateAudioServiceQuality();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -682,26 +698,27 @@ class _QualityPickerState extends State<QualityPicker> {
|
||||||
title: const Text('MP3 128kbps'),
|
title: const Text('MP3 128kbps'),
|
||||||
groupValue: _quality,
|
groupValue: _quality,
|
||||||
value: AudioQuality.MP3_128,
|
value: AudioQuality.MP3_128,
|
||||||
onChanged: (q) => _updateQuality(q),
|
onChanged: (AudioQuality? q) => _updateQuality(q),
|
||||||
),
|
),
|
||||||
RadioListTile(
|
RadioListTile(
|
||||||
title: const Text('MP3 320kbps'),
|
title: const Text('MP3 320kbps'),
|
||||||
groupValue: _quality,
|
groupValue: _quality,
|
||||||
value: AudioQuality.MP3_320,
|
value: AudioQuality.MP3_320,
|
||||||
onChanged: (q) => _updateQuality(q),
|
onChanged: hqDisabled ? null : (AudioQuality? q) => _updateQuality(q),
|
||||||
),
|
),
|
||||||
RadioListTile(
|
RadioListTile(
|
||||||
title: const Text('FLAC'),
|
title: const Text('FLAC'),
|
||||||
groupValue: _quality,
|
groupValue: _quality,
|
||||||
value: AudioQuality.FLAC,
|
value: AudioQuality.FLAC,
|
||||||
onChanged: (q) => _updateQuality(q),
|
onChanged:
|
||||||
|
flacDisabled ? null : (AudioQuality? q) => _updateQuality(q),
|
||||||
),
|
),
|
||||||
if (widget.field == 'download')
|
if (widget.field == 'download')
|
||||||
RadioListTile(
|
RadioListTile(
|
||||||
title: Text('Ask before downloading'.i18n),
|
title: Text('Ask before downloading'.i18n),
|
||||||
groupValue: _quality,
|
groupValue: _quality,
|
||||||
value: AudioQuality.ASK,
|
value: AudioQuality.ASK,
|
||||||
onChanged: (q) => _updateQuality(q),
|
onChanged: (AudioQuality? q) => _updateQuality(q),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:fluttericon/octicons_icons.dart';
|
import 'package:fluttericon/octicons_icons.dart';
|
||||||
import 'package:freezer/api/deezer.dart';
|
import 'package:freezer/api/deezer.dart';
|
||||||
import 'package:freezer/api/download.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 'package:freezer/translations.i18n.dart';
|
||||||
|
|
||||||
import '../api/definitions.dart';
|
import '../api/definitions.dart';
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue