fix email login, fix spotify importer
migrate to dio instead of http, with unique cookiejar
This commit is contained in:
parent
276d0ad4bf
commit
7a119d281c
|
@ -80,7 +80,7 @@ android {
|
||||||
dependencies {
|
dependencies {
|
||||||
//implementation group: 'org', name: 'jaudiotagger', version: '2.0.3'
|
//implementation group: 'org', name: 'jaudiotagger', version: '2.0.3'
|
||||||
implementation files('libs/jaudiotagger-2.2.3.jar')
|
implementation files('libs/jaudiotagger-2.2.3.jar')
|
||||||
// implementation files('libs/extension-flac.aar')
|
implementation files('libs/extension-flac.aar')
|
||||||
implementation group: 'org.nanohttpd', name: 'nanohttpd', version: '2.3.1'
|
implementation group: 'org.nanohttpd', name: 'nanohttpd', version: '2.3.1'
|
||||||
implementation group: 'androidx.core', name: 'core', version: '1.6.0'
|
implementation group: 'androidx.core', name: 'core', version: '1.6.0'
|
||||||
}
|
}
|
||||||
|
|
Binary file not shown.
|
@ -1,104 +0,0 @@
|
||||||
// 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();
|
|
||||||
}
|
|
|
@ -1,26 +1,55 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:cookie_jar/cookie_jar.dart';
|
||||||
import 'package:crypto/crypto.dart';
|
import 'package:crypto/crypto.dart';
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:dio_cookie_manager/dio_cookie_manager.dart';
|
||||||
|
import 'package:flutter/foundation.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/spotify.dart';
|
import 'package:freezer/api/spotify.dart';
|
||||||
import 'package:freezer/settings.dart';
|
import 'package:freezer/settings.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:path/path.dart';
|
|
||||||
|
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:path_provider/path_provider.dart';
|
|
||||||
|
|
||||||
final deezerAPI = DeezerAPI();
|
final deezerAPI = DeezerAPI();
|
||||||
|
|
||||||
class DeezerAPI {
|
class DeezerAPI {
|
||||||
static final _logger = Logger('DeezerAPI');
|
// from deemix: https://gitlab.com/RemixDev/deemix-js/-/blob/main/deemix/utils/deezer.js?ref_type=heads#L6
|
||||||
DeezerAPI({this.arl});
|
static const CLIENT_ID = "172365";
|
||||||
|
static const CLIENT_SECRET = "fb0bec7ccc063dab0417eb7b0d847f34";
|
||||||
|
|
||||||
|
static const USER_AGENT_SUFFIX =
|
||||||
|
'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36';
|
||||||
|
static const WINDOWS_USER_AGENT =
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) $USER_AGENT_SUFFIX";
|
||||||
|
static const MACOS_USER_AGENT =
|
||||||
|
'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_0) $USER_AGENT_SUFFIX';
|
||||||
|
static const USER_AGENTS = <TargetPlatform, String>{
|
||||||
|
TargetPlatform.android: WINDOWS_USER_AGENT,
|
||||||
|
TargetPlatform.windows: WINDOWS_USER_AGENT,
|
||||||
|
TargetPlatform.iOS: MACOS_USER_AGENT,
|
||||||
|
TargetPlatform.macOS: MACOS_USER_AGENT,
|
||||||
|
TargetPlatform.linux: 'Mozilla/5.0 (X11; Linux x86_64) $USER_AGENT_SUFFIX',
|
||||||
|
};
|
||||||
|
|
||||||
|
static String get userAgent => USER_AGENTS[defaultTargetPlatform]!;
|
||||||
|
|
||||||
|
static final _logger = Logger('DeezerAPI');
|
||||||
|
DeezerAPI();
|
||||||
|
|
||||||
|
set arl(String? arl) {
|
||||||
|
if (arl == null) {
|
||||||
|
cookieJar.delete(Uri.https('www.deezer.com'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cookieJar
|
||||||
|
.saveFromResponse(Uri.https('www.deezer.com'), [Cookie('arl', arl)]);
|
||||||
|
}
|
||||||
|
|
||||||
String? arl;
|
|
||||||
String? token;
|
String? token;
|
||||||
String? userId;
|
String? userId;
|
||||||
String? userName;
|
String? userName;
|
||||||
|
@ -30,12 +59,16 @@ class DeezerAPI {
|
||||||
late bool canStreamLossless;
|
late bool canStreamLossless;
|
||||||
late bool canStreamHQ;
|
late bool canStreamHQ;
|
||||||
|
|
||||||
|
final cookieJar = DefaultCookieJar();
|
||||||
|
late final dio =
|
||||||
|
Dio(BaseOptions(headers: headers, responseType: ResponseType.json))
|
||||||
|
..interceptors.add(CookieManager(cookieJar));
|
||||||
|
|
||||||
Future<bool>? _authorizing;
|
Future<bool>? _authorizing;
|
||||||
|
|
||||||
//Get headers
|
//Get headers
|
||||||
Map<String, String> get headers => {
|
Map<String, String> get headers => {
|
||||||
"User-Agent":
|
"User-Agent": userAgent,
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36",
|
|
||||||
"Content-Language":
|
"Content-Language":
|
||||||
'${settings.deezerLanguage}-${settings.deezerCountry}',
|
'${settings.deezerLanguage}-${settings.deezerCountry}',
|
||||||
"Cache-Control": "max-age=0",
|
"Cache-Control": "max-age=0",
|
||||||
|
@ -44,77 +77,102 @@ class DeezerAPI {
|
||||||
"Accept-Language":
|
"Accept-Language":
|
||||||
"${settings.deezerLanguage}-${settings.deezerCountry},${settings.deezerLanguage};q=0.9,en-US;q=0.8,en;q=0.7",
|
"${settings.deezerLanguage}-${settings.deezerCountry},${settings.deezerLanguage};q=0.9,en-US;q=0.8,en;q=0.7",
|
||||||
"Connection": "keep-alive",
|
"Connection": "keep-alive",
|
||||||
"Cookie": "arl=$arl${(sid == null) ? '' : '; sid=$sid'}"
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Future<void> logout() async {
|
||||||
|
// delete all cookies
|
||||||
|
await cookieJar.deleteAll();
|
||||||
|
updateHeaders();
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateHeaders() {
|
||||||
|
dio.options.headers = headers;
|
||||||
|
}
|
||||||
|
|
||||||
//Call private API
|
//Call private API
|
||||||
Future<Map<dynamic, dynamic>> callApi(String method,
|
Future<Map<dynamic, dynamic>> callApi(String method,
|
||||||
{Map<dynamic, dynamic>? params, String? gatewayInput}) async {
|
{Map<dynamic, dynamic>? params, String? gatewayInput}) async {
|
||||||
//Generate URL
|
//Post
|
||||||
Uri uri = Uri.https('www.deezer.com', '/ajax/gw-light.php', {
|
final res = await dio.post<Map>('https://www.deezer.com/ajax/gw-light.php',
|
||||||
|
queryParameters: {
|
||||||
'api_version': '1.0',
|
'api_version': '1.0',
|
||||||
'api_token': token,
|
'api_token': token,
|
||||||
'input': '3',
|
'input': '3',
|
||||||
'method': method,
|
'method': method,
|
||||||
//Used for homepage
|
//Used for homepage
|
||||||
if (gatewayInput != null) 'gateway_input': gatewayInput
|
if (gatewayInput != null) 'gateway_input': gatewayInput
|
||||||
});
|
},
|
||||||
//Post
|
data: jsonEncode(params));
|
||||||
http.Response res =
|
final body = res.data;
|
||||||
await http.post(uri, headers: headers, body: jsonEncode(params));
|
|
||||||
dynamic body = jsonDecode(res.body);
|
|
||||||
//Grab SID
|
|
||||||
if (method == 'deezer.getUserData') {
|
|
||||||
for (String cookieHeader in res.headers['set-cookie']!.split(';')) {
|
|
||||||
if (cookieHeader.startsWith('sid=')) {
|
|
||||||
sid = cookieHeader.split('=')[1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// In case of error "Invalid CSRF token" retrieve new one and retry the same call
|
// In case of error "Invalid CSRF token" retrieve new one and retry the same call
|
||||||
if (body['error'].isNotEmpty &&
|
if (body!['error'].isNotEmpty &&
|
||||||
body['error'].containsKey('VALID_TOKEN_REQUIRED') &&
|
body['error'].containsKey('VALID_TOKEN_REQUIRED') &&
|
||||||
await rawAuthorize()) {
|
await rawAuthorize()) {
|
||||||
return callApi(method, params: params, gatewayInput: gatewayInput);
|
return callApi(method, params: params, gatewayInput: gatewayInput);
|
||||||
}
|
}
|
||||||
|
|
||||||
return body;
|
return body;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Map> callPublicApi(String path) async {
|
Future<Map> callPublicApi(String path) async {
|
||||||
Uri uri = Uri.https('api.deezer.com', '/$path');
|
final res = await dio.get('https://api.deezer.com/$path');
|
||||||
http.Response res = await http.get(uri);
|
return res.data;
|
||||||
return jsonDecode(res.body);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//Wrapper so it can be globally awaited
|
//Wrapper so it can be globally awaited
|
||||||
Future<bool> authorize() async => _authorizing ??= rawAuthorize();
|
Future<bool> authorize() async => _authorizing ??= rawAuthorize();
|
||||||
|
|
||||||
//Login with email
|
//Login with email FROM DEEMIX-JS
|
||||||
static Future<String?> getArlByEmail(String? email, String password) async {
|
Future<String> getArlByEmail(String email, String password) async {
|
||||||
//Get MD5 of password
|
//Get MD5 of password
|
||||||
Digest digest = md5.convert(utf8.encode(password));
|
final md5Password = md5.convert(utf8.encode(password)).toString();
|
||||||
String md5password = '$digest';
|
final hash = md5
|
||||||
|
.convert(utf8
|
||||||
|
.encode([CLIENT_ID, email, md5Password, CLIENT_SECRET].join('')))
|
||||||
|
.toString();
|
||||||
//Get access token
|
//Get access token
|
||||||
String url =
|
// String url =
|
||||||
"https://tv.deezer.com/smarttv/8caf9315c1740316053348a24d25afc7/user_auth.php?login=$email&password=$md5password&device=panasonic&output=json";
|
// "https://tv.deezer.com/smarttv/8caf9315c1740316053348a24d25afc7/user_auth.php?login=$email&password=$md5password&device=panasonic&output=json";
|
||||||
http.Response response = await http.get(Uri.parse(url));
|
// http.Response response = await http.get(Uri.parse(url));
|
||||||
String? accessToken = jsonDecode(response.body)["access_token"];
|
// String? accessToken = jsonDecode(response.body)["access_token"];
|
||||||
//Get SID
|
final res = await dio.get('https://api.deezer.com/auth/token',
|
||||||
url = "https://api.deezer.com/platform/generic/track/42069";
|
queryParameters: {
|
||||||
response = await http
|
'app_id': CLIENT_ID,
|
||||||
.get(Uri.parse(url), headers: {"Authorization": "Bearer $accessToken"});
|
'login': email,
|
||||||
String? sid;
|
'password': md5Password,
|
||||||
for (String cookieHeader in response.headers['set-cookie']!.split(';')) {
|
'hash': hash
|
||||||
if (cookieHeader.startsWith('sid=')) {
|
},
|
||||||
sid = cookieHeader.split('=')[1];
|
options: Options(responseType: ResponseType.json));
|
||||||
|
final accessToken = res.data['access_token'] as String?;
|
||||||
|
print(res.data);
|
||||||
|
if (accessToken == null) {
|
||||||
|
throw Exception('login failed, access token is null');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return getArlByAccessToken(accessToken);
|
||||||
}
|
}
|
||||||
if (sid == null) return null;
|
|
||||||
|
// FROM DEEMIX-JS
|
||||||
|
Future<String> getArlByAccessToken(String accessToken) async {
|
||||||
|
//Get SID in cookieJar
|
||||||
|
await dio.get("https://api.deezer.com/platform/generic/track/3135556",
|
||||||
|
options: Options(headers: {
|
||||||
|
'Authorization': 'Bearer $accessToken',
|
||||||
|
}));
|
||||||
//Get ARL
|
//Get ARL
|
||||||
url =
|
final arlRes = await dio.get("https://www.deezer.com/ajax/gw-light.php",
|
||||||
"https://deezer.com/ajax/gw-light.php?api_version=1.0&api_token=null&input=3&method=user.getArl";
|
queryParameters: {
|
||||||
response = await http.get(Uri.parse(url), headers: {"Cookie": "sid=$sid"});
|
'method': 'user.getArl',
|
||||||
return jsonDecode(response.body)["results"];
|
'input': '3',
|
||||||
|
'api_version': '1.0',
|
||||||
|
'api_token': 'null',
|
||||||
|
},
|
||||||
|
options: Options(responseType: ResponseType.json));
|
||||||
|
final arl = arlRes.data["results"];
|
||||||
|
print(arlRes.data);
|
||||||
|
if (arl == null) throw Exception('couldn\'t obtain ARL');
|
||||||
|
return arl;
|
||||||
}
|
}
|
||||||
|
|
||||||
//Authorize, bool = success
|
//Authorize, bool = success
|
||||||
|
|
|
@ -5,7 +5,6 @@ import 'dart:isolate';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:encrypt/encrypt.dart';
|
import 'package:encrypt/encrypt.dart';
|
||||||
import 'package:flutter/foundation.dart' as flutter;
|
|
||||||
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/settings.dart';
|
import 'package:freezer/settings.dart';
|
||||||
|
@ -14,7 +13,6 @@ 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,
|
||||||
|
@ -26,7 +24,7 @@ typedef _IsolateMessage = (
|
||||||
// Maybe better implementation of Blowfish CBC instead of random-ass, unpublished library from github?
|
// Maybe better implementation of Blowfish CBC instead of random-ass, unpublished library from github?
|
||||||
// This class can be considered a rewrite in Dart of the Java backend (from the StreamServer.deezer() function and also from the Deezer class)
|
// This class can be considered a rewrite in Dart of the Java backend (from the StreamServer.deezer() function and also from the Deezer class)
|
||||||
class DeezerAudioSource extends StreamAudioSource {
|
class DeezerAudioSource extends StreamAudioSource {
|
||||||
final _logger = Logger("DeezerAudioSource");
|
static final _logger = Logger("DeezerAudioSource");
|
||||||
|
|
||||||
late AudioQuality Function() _getQuality;
|
late AudioQuality Function() _getQuality;
|
||||||
late AudioQuality? _initialQuality;
|
late AudioQuality? _initialQuality;
|
||||||
|
@ -129,7 +127,7 @@ class DeezerAudioSource extends StreamAudioSource {
|
||||||
final genUri = _generateTrackUri();
|
final genUri = _generateTrackUri();
|
||||||
|
|
||||||
final req = await http.head(genUri, headers: {
|
final req = await http.head(genUri, headers: {
|
||||||
'User-Agent': deezerAPI.headers['User-Agent']!,
|
'User-Agent': DeezerAPI.userAgent,
|
||||||
'Accept-Language': '*',
|
'Accept-Language': '*',
|
||||||
'Accept': '*/*'
|
'Accept': '*/*'
|
||||||
});
|
});
|
||||||
|
@ -351,7 +349,7 @@ class DeezerAudioSource extends StreamAudioSource {
|
||||||
final int deezerStart = start - (start % 2048);
|
final int deezerStart = start - (start % 2048);
|
||||||
final req = http.Request('GET', _downloadUrl!)
|
final req = http.Request('GET', _downloadUrl!)
|
||||||
..headers.addAll({
|
..headers.addAll({
|
||||||
'User-Agent': deezerAPI.headers['User-Agent']!,
|
'User-Agent': DeezerAPI.userAgent,
|
||||||
'Accept-Language': '*',
|
'Accept-Language': '*',
|
||||||
'Accept': '*/*',
|
'Accept': '*/*',
|
||||||
if (deezerStart > 0)
|
if (deezerStart > 0)
|
||||||
|
|
|
@ -5,17 +5,16 @@ 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';
|
||||||
import 'package:freezer/settings.dart';
|
import 'package:freezer/settings.dart';
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
|
import 'package:isar/isar.dart';
|
||||||
import 'package:json_annotation/json_annotation.dart';
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:just_audio/just_audio.dart';
|
import 'package:just_audio/just_audio.dart';
|
||||||
import 'package:freezer/translations.i18n.dart';
|
import 'package:freezer/translations.i18n.dart';
|
||||||
import 'package:isar/isar.dart';
|
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
@ -95,9 +94,6 @@ 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!,
|
||||||
|
@ -1069,7 +1065,7 @@ class HomePageItem {
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
String type = describeEnum(this.type!);
|
String type = describeEnum(this.type);
|
||||||
return {'type': type, 'value': value.toJson()};
|
return {'type': type, 'value': value.toJson()};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:freezer/main.dart';
|
||||||
import 'package:sqflite/sqflite.dart';
|
import 'package:sqflite/sqflite.dart';
|
||||||
import 'package:disk_space_plus/disk_space_plus.dart';
|
import 'package:disk_space_plus/disk_space_plus.dart';
|
||||||
import 'package:filesize/filesize.dart';
|
import 'package:filesize/filesize.dart';
|
||||||
|
@ -157,7 +158,9 @@ class DownloadManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> addOfflineTrack(Track track,
|
Future<bool> addOfflineTrack(Track track,
|
||||||
{private = true, BuildContext? context, isSingleton = false}) async {
|
{bool private = true,
|
||||||
|
BuildContext? context,
|
||||||
|
bool isSingleton = false}) async {
|
||||||
if (!isSupported) return false;
|
if (!isSupported) return false;
|
||||||
//Permission
|
//Permission
|
||||||
if (!private && !(await checkPermission())) return false;
|
if (!private && !(await checkPermission())) return false;
|
||||||
|
@ -183,8 +186,8 @@ class DownloadManager {
|
||||||
await b.commit();
|
await b.commit();
|
||||||
|
|
||||||
//Cache art
|
//Cache art
|
||||||
DefaultCacheManager().getSingleFile(track.albumArt!.thumb);
|
cacheManager.getSingleFile(track.albumArt!.thumb);
|
||||||
DefaultCacheManager().getSingleFile(track.albumArt!.full);
|
cacheManager.getSingleFile(track.albumArt!.full);
|
||||||
}
|
}
|
||||||
|
|
||||||
//Get path
|
//Get path
|
||||||
|
@ -217,8 +220,8 @@ class DownloadManager {
|
||||||
//Add to DB
|
//Add to DB
|
||||||
if (private) {
|
if (private) {
|
||||||
//Cache art
|
//Cache art
|
||||||
DefaultCacheManager().getSingleFile(album.art!.thumb);
|
cacheManager.getSingleFile(album.art!.thumb);
|
||||||
DefaultCacheManager().getSingleFile(album.art!.full);
|
cacheManager.getSingleFile(album.art!.full);
|
||||||
|
|
||||||
Batch b = db.batch();
|
Batch b = db.batch();
|
||||||
b.insert('Albums', album.toSQL(off: true),
|
b.insert('Albums', album.toSQL(off: true),
|
||||||
|
@ -268,8 +271,8 @@ class DownloadManager {
|
||||||
for (Track? t in playlist.tracks!) {
|
for (Track? t in playlist.tracks!) {
|
||||||
b = await _addTrackToDB(b, t!, false);
|
b = await _addTrackToDB(b, t!, false);
|
||||||
//Cache art
|
//Cache art
|
||||||
DefaultCacheManager().getSingleFile(t.albumArt!.thumb);
|
cacheManager.getSingleFile(t.albumArt!.thumb);
|
||||||
DefaultCacheManager().getSingleFile(t.albumArt!.full);
|
cacheManager.getSingleFile(t.albumArt!.full);
|
||||||
}
|
}
|
||||||
await b.commit();
|
await b.commit();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,32 @@
|
||||||
|
import 'package:freezer/api/definitions.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
|
|
||||||
class T {}
|
@collection
|
||||||
|
class Track {
|
||||||
|
Id id = Isar.autoIncrement;
|
||||||
|
final String trackId;
|
||||||
|
final String title;
|
||||||
|
final String albumId;
|
||||||
|
final List<String> artistIds;
|
||||||
|
//final DeezerImageDetails albumArt;
|
||||||
|
final int? trackNumber;
|
||||||
|
final bool offline;
|
||||||
|
//final Lyrics lyrics;
|
||||||
|
final bool favorite;
|
||||||
|
final int? diskNumber;
|
||||||
|
final bool explicit;
|
||||||
|
|
||||||
|
Track({
|
||||||
|
required this.trackId,
|
||||||
|
required this.title,
|
||||||
|
required this.albumId,
|
||||||
|
required this.artistIds,
|
||||||
|
//required this.albumArt,
|
||||||
|
required this.trackNumber,
|
||||||
|
//required this.lyrics,
|
||||||
|
required this.favorite,
|
||||||
|
required this.diskNumber,
|
||||||
|
required this.explicit,
|
||||||
|
this.offline = true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -1,16 +1,25 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'dart:isolate';
|
||||||
|
|
||||||
|
import 'package:flutter/src/widgets/framework.dart';
|
||||||
import 'package:flutter_background_service/flutter_background_service.dart';
|
import 'package:flutter_background_service/flutter_background_service.dart';
|
||||||
|
import 'package:freezer/api/definitions.dart';
|
||||||
import 'package:freezer/api/download_manager/download_service.dart';
|
import 'package:freezer/api/download_manager/download_service.dart';
|
||||||
|
import 'package:freezer/api/download_manager/service_interface.dart';
|
||||||
import 'package:freezer/translations.i18n.dart';
|
import 'package:freezer/translations.i18n.dart';
|
||||||
|
import '../download.dart' as dl;
|
||||||
|
|
||||||
class DownloadManager {
|
class DownloadManager {
|
||||||
Future<bool> startService() {
|
//implements dl.DownloadManager {
|
||||||
if (Platform.isAndroid) {
|
SendPort? _sendPort;
|
||||||
|
Isolate? _isolate;
|
||||||
|
|
||||||
|
Future<bool> configure() {
|
||||||
|
if (Platform.isAndroid || Platform.isIOS) {
|
||||||
return FlutterBackgroundService().configure(
|
return FlutterBackgroundService().configure(
|
||||||
iosConfiguration: IosConfiguration(), // fuck ios
|
iosConfiguration: IosConfiguration(), // fuck ios
|
||||||
androidConfiguration: AndroidConfiguration(
|
androidConfiguration: AndroidConfiguration(
|
||||||
onStart: _startService,
|
onStart: _startNative,
|
||||||
isForegroundMode: false,
|
isForegroundMode: false,
|
||||||
autoStart: false,
|
autoStart: false,
|
||||||
autoStartOnBoot: false,
|
autoStartOnBoot: false,
|
||||||
|
@ -21,10 +30,48 @@ class DownloadManager {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
_startService(null);
|
// will run in foreground instead, in a separate isolate
|
||||||
return Future.value(true);
|
return Future.value(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void _startService(ServiceInstance? service) =>
|
Future<bool> startService() async {
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
return FlutterBackgroundService().startService();
|
||||||
|
}
|
||||||
|
|
||||||
|
final receivePort = ReceivePort();
|
||||||
|
_sendPort = receivePort.sendPort;
|
||||||
|
_isolate = await Isolate.spawn(
|
||||||
|
_startService, ServiceInterface(receivePort: receivePort));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void kill() {
|
||||||
|
_isolate?.kill();
|
||||||
|
}
|
||||||
|
|
||||||
|
void invoke(String method, [Map<String, dynamic>? args]) {
|
||||||
|
if (_sendPort != null) {
|
||||||
|
_sendPort!.send({
|
||||||
|
'method': method,
|
||||||
|
if (args != null) ...args,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
FlutterBackgroundService().invoke(method, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> addOfflineTrack(Track track,
|
||||||
|
{bool private = true, BuildContext? context, isSingleton = false}) {
|
||||||
|
// TODO: implement addOfflineTrack
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void _startNative(ServiceInstance service) =>
|
||||||
|
_startService(ServiceInterface(service: service));
|
||||||
|
|
||||||
|
static void _startService(ServiceInterface? service) =>
|
||||||
DownloadService(service).run();
|
DownloadService(service).run();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import 'package:flutter_background_service/flutter_background_service.dart';
|
import 'package:freezer/api/download_manager/service_interface.dart';
|
||||||
|
|
||||||
class DownloadService {
|
class DownloadService {
|
||||||
static const NOTIFICATION_ID = 6969;
|
static const NOTIFICATION_ID = 6969;
|
||||||
static const NOTIFICATION_CHANNEL_ID = "freezerdownloads";
|
static const NOTIFICATION_CHANNEL_ID = "freezerdownloads";
|
||||||
|
|
||||||
final ServiceInstance? service;
|
final ServiceInterface? service;
|
||||||
DownloadService(this.service);
|
DownloadService(this.service);
|
||||||
|
|
||||||
void run() {}
|
void run() {}
|
||||||
|
|
21
lib/api/download_manager/service_interface.dart
Normal file
21
lib/api/download_manager/service_interface.dart
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import 'dart:isolate';
|
||||||
|
|
||||||
|
import 'package:flutter_background_service/flutter_background_service.dart';
|
||||||
|
|
||||||
|
class ServiceInterface {
|
||||||
|
final ReceivePort? receivePort;
|
||||||
|
final ServiceInstance? service;
|
||||||
|
|
||||||
|
ServiceInterface({this.receivePort, this.service})
|
||||||
|
: assert(receivePort != null || service != null);
|
||||||
|
|
||||||
|
Stream<Map<String, dynamic>?> on(String method) {
|
||||||
|
if (service != null) {
|
||||||
|
return service!.on(method);
|
||||||
|
}
|
||||||
|
|
||||||
|
return receivePort!
|
||||||
|
.where((event) => event['method'] == method)
|
||||||
|
.map((event) => (event as Map?)?.cast<String, dynamic>());
|
||||||
|
}
|
||||||
|
}
|
|
@ -65,7 +65,7 @@ class AudioPlayerTaskInitArguments {
|
||||||
static Future<AudioPlayerTaskInitArguments> loadSettings() async {
|
static Future<AudioPlayerTaskInitArguments> loadSettings() async {
|
||||||
final settings = await Settings.load();
|
final settings = await Settings.load();
|
||||||
|
|
||||||
final deezerAPI = DeezerAPI(arl: settings.arl);
|
final deezerAPI = DeezerAPI()..arl = settings.arl;
|
||||||
await deezerAPI.authorize();
|
await deezerAPI.authorize();
|
||||||
|
|
||||||
return from(settings: settings, deezerAPI: deezerAPI);
|
return from(settings: settings, deezerAPI: deezerAPI);
|
||||||
|
|
|
@ -6,6 +6,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/player/audio_handler.dart';
|
import 'package:freezer/api/player/audio_handler.dart';
|
||||||
|
import 'package:freezer/main.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:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
@ -67,6 +68,7 @@ class PlayerHelper {
|
||||||
androidNotificationIcon: 'drawable/ic_logo',
|
androidNotificationIcon: 'drawable/ic_logo',
|
||||||
preloadArtwork: false,
|
preloadArtwork: false,
|
||||||
),
|
),
|
||||||
|
cacheManager: cacheManager,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -141,7 +141,7 @@ class SpotifyAlbum {
|
||||||
}
|
}
|
||||||
|
|
||||||
class SpotifyAPIWrapper {
|
class SpotifyAPIWrapper {
|
||||||
late HttpServer _server;
|
HttpServer? _server;
|
||||||
late SpotifyApi spotify;
|
late SpotifyApi spotify;
|
||||||
late User me;
|
late User me;
|
||||||
|
|
||||||
|
@ -183,7 +183,8 @@ class SpotifyAPIWrapper {
|
||||||
grant.getAuthorizationUrl(Uri.parse(redirectUri), scopes: scopes);
|
grant.getAuthorizationUrl(Uri.parse(redirectUri), scopes: scopes);
|
||||||
launchUrl(authUri);
|
launchUrl(authUri);
|
||||||
//Wait for code
|
//Wait for code
|
||||||
await for (HttpRequest request in _server) {
|
|
||||||
|
await for (HttpRequest request in _server!) {
|
||||||
//Exit window
|
//Exit window
|
||||||
request.response.headers.set("Content-Type", "text/html; charset=UTF-8");
|
request.response.headers.set("Content-Type", "text/html; charset=UTF-8");
|
||||||
request.response.write(
|
request.response.write(
|
||||||
|
@ -191,7 +192,7 @@ class SpotifyAPIWrapper {
|
||||||
request.response.close();
|
request.response.close();
|
||||||
//Get token
|
//Get token
|
||||||
if (request.uri.queryParameters["code"] != null) {
|
if (request.uri.queryParameters["code"] != null) {
|
||||||
_server.close();
|
_server!.close();
|
||||||
responseUri = request.uri.toString();
|
responseUri = request.uri.toString();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -221,6 +222,6 @@ class SpotifyAPIWrapper {
|
||||||
|
|
||||||
//Cancel authorization
|
//Cancel authorization
|
||||||
void cancelAuthorize() {
|
void cancelAuthorize() {
|
||||||
_server.close(force: true);
|
_server?.close(force: true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,9 +7,12 @@ 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:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||||
|
import 'package:flutter_cache_manager_hive/flutter_cache_manager_hive.dart';
|
||||||
import 'package:flutter_displaymode/flutter_displaymode.dart';
|
import 'package:flutter_displaymode/flutter_displaymode.dart';
|
||||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
|
import 'package:fluttericon/font_awesome5_icons.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/api/paths.dart';
|
||||||
|
@ -19,6 +22,7 @@ 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/fancy_scaffold.dart';
|
||||||
|
import 'package:freezer/ui/importer_screen.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';
|
||||||
|
@ -42,10 +46,10 @@ import 'settings.dart';
|
||||||
import 'ui/home_screen.dart';
|
import 'ui/home_screen.dart';
|
||||||
import 'ui/player_bar.dart';
|
import 'ui/player_bar.dart';
|
||||||
|
|
||||||
late Function updateTheme;
|
|
||||||
late Function logOut;
|
late Function logOut;
|
||||||
GlobalKey<NavigatorState> mainNavigatorKey = GlobalKey<NavigatorState>();
|
GlobalKey<NavigatorState> mainNavigatorKey = GlobalKey<NavigatorState>();
|
||||||
GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||||
|
late final CacheManager cacheManager;
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
@ -87,7 +91,8 @@ void main() async {
|
||||||
..registerAdapter(UriAdapter())
|
..registerAdapter(UriAdapter())
|
||||||
..registerAdapter(QueueSourceAdapter())
|
..registerAdapter(QueueSourceAdapter())
|
||||||
..registerAdapter(HomePageAdapter())
|
..registerAdapter(HomePageAdapter())
|
||||||
..registerAdapter(NavigationRailAppearanceAdapter());
|
..registerAdapter(NavigationRailAppearanceAdapter())
|
||||||
|
..registerAdapter(HiveCacheObjectAdapter(typeId: 35));
|
||||||
|
|
||||||
Hive.init(await Paths.dataDirectory());
|
Hive.init(await Paths.dataDirectory());
|
||||||
|
|
||||||
|
@ -96,6 +101,10 @@ void main() async {
|
||||||
settings.save();
|
settings.save();
|
||||||
downloadManager.init();
|
downloadManager.init();
|
||||||
cache = await Cache.load();
|
cache = await Cache.load();
|
||||||
|
// photos
|
||||||
|
cacheManager = DefaultCacheManager();
|
||||||
|
// cacheManager = HiveCacheManager(
|
||||||
|
// boxName: 'freezer-images', boxPath: await Paths.cacheDir());
|
||||||
// TODO: WA
|
// TODO: WA
|
||||||
deezerAPI.favoritesPlaylistId = cache.favoritesPlaylistId;
|
deezerAPI.favoritesPlaylistId = cache.favoritesPlaylistId;
|
||||||
|
|
||||||
|
@ -128,8 +137,6 @@ class _FreezerAppState extends State<FreezerApp> {
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
_initStateAsync();
|
_initStateAsync();
|
||||||
//Make update theme global
|
|
||||||
updateTheme = _updateTheme;
|
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -153,10 +160,6 @@ class _FreezerAppState extends State<FreezerApp> {
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _updateTheme() {
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
|
|
||||||
Locale? _locale() {
|
Locale? _locale() {
|
||||||
if (settings.language == null || settings.language!.split('_').length < 2) {
|
if (settings.language == null || settings.language!.split('_').length < 2) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -167,7 +170,12 @@ class _FreezerAppState extends State<FreezerApp> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ScreenUtilInit(
|
return NotificationListener<UpdateThemeNotification>(
|
||||||
|
onNotification: (notification) {
|
||||||
|
setState(() => settings.themeData);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
child: ScreenUtilInit(
|
||||||
designSize: const Size(1080, 720),
|
designSize: const Size(1080, 720),
|
||||||
builder: (context, child) =>
|
builder: (context, child) =>
|
||||||
DynamicColorBuilder(builder: (lightScheme, darkScheme) {
|
DynamicColorBuilder(builder: (lightScheme, darkScheme) {
|
||||||
|
@ -212,6 +220,7 @@ class _FreezerAppState extends State<FreezerApp> {
|
||||||
navigatorKey: mainNavigatorKey,
|
navigatorKey: mainNavigatorKey,
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -229,7 +238,7 @@ class _LoginMainWrapperState extends State<LoginMainWrapper> {
|
||||||
void initState() {
|
void initState() {
|
||||||
if (settings.arl != null) {
|
if (settings.arl != null) {
|
||||||
//Load token on background
|
//Load token on background
|
||||||
deezerAPI.arl = settings.arl;
|
deezerAPI.arl = settings.arl!;
|
||||||
settings.offlineMode = true;
|
settings.offlineMode = true;
|
||||||
deezerAPI.authorize().then((b) async {
|
deezerAPI.authorize().then((b) async {
|
||||||
if (b) setState(() => settings.offlineMode = false);
|
if (b) setState(() => settings.offlineMode = false);
|
||||||
|
@ -252,8 +261,8 @@ class _LoginMainWrapperState extends State<LoginMainWrapper> {
|
||||||
setState(() {
|
setState(() {
|
||||||
settings.arl = null;
|
settings.arl = null;
|
||||||
settings.offlineMode = false;
|
settings.offlineMode = false;
|
||||||
deezerAPI.arl = null;
|
|
||||||
});
|
});
|
||||||
|
await deezerAPI.logout();
|
||||||
await settings.save();
|
await settings.save();
|
||||||
await Cache.wipe();
|
await Cache.wipe();
|
||||||
}
|
}
|
||||||
|
@ -320,7 +329,7 @@ class MainScreenState extends State<MainScreen>
|
||||||
final playerBarFocusNode = FocusNode();
|
final playerBarFocusNode = FocusNode();
|
||||||
final _fancyScaffoldKey = GlobalKey<FancyScaffoldState>();
|
final _fancyScaffoldKey = GlobalKey<FancyScaffoldState>();
|
||||||
|
|
||||||
late bool isDesktop;
|
bool isDesktop = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
@ -549,6 +558,7 @@ class MainScreenState extends State<MainScreen>
|
||||||
6: '/library/history',
|
6: '/library/history',
|
||||||
7: '/downloads',
|
7: '/downloads',
|
||||||
8: '/settings',
|
8: '/settings',
|
||||||
|
9: '/spotify-importer'
|
||||||
};
|
};
|
||||||
final _navigationRailDestinations = <NavigationRailDestination>[
|
final _navigationRailDestinations = <NavigationRailDestination>[
|
||||||
NavigationRailDestination(
|
NavigationRailDestination(
|
||||||
|
@ -593,6 +603,10 @@ class MainScreenState extends State<MainScreen>
|
||||||
selectedIcon: const Icon(Icons.settings),
|
selectedIcon: const Icon(Icons.settings),
|
||||||
label: Text('Settings'.i18n),
|
label: Text('Settings'.i18n),
|
||||||
),
|
),
|
||||||
|
NavigationRailDestination(
|
||||||
|
icon: const Icon(FontAwesome5.spotify),
|
||||||
|
label: Text('Importer'.i18n),
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
void _onDestinationSelected(int s,
|
void _onDestinationSelected(int s,
|
||||||
|
@ -646,19 +660,23 @@ class MainScreenState extends State<MainScreen>
|
||||||
focusNode: FocusNode(),
|
focusNode: FocusNode(),
|
||||||
onKey: _handleKey,
|
onKey: _handleKey,
|
||||||
child: LayoutBuilder(builder: (context, constraints) {
|
child: LayoutBuilder(builder: (context, constraints) {
|
||||||
// check if we're running on a desktop platform
|
// check if we're able to display the desktop layout
|
||||||
|
if (Platform.isLinux || Platform.isWindows || Platform.isMacOS) {
|
||||||
final isLandscape = constraints.maxWidth > constraints.maxHeight;
|
final isLandscape = constraints.maxWidth > constraints.maxHeight;
|
||||||
isDesktop = isLandscape && constraints.maxWidth > 1024;
|
isDesktop = isLandscape &&
|
||||||
|
constraints.maxWidth >= 1100 &&
|
||||||
|
constraints.maxHeight >= 600;
|
||||||
|
}
|
||||||
return FancyScaffold(
|
return FancyScaffold(
|
||||||
key: _fancyScaffoldKey,
|
key: _fancyScaffoldKey,
|
||||||
navigationRail: _buildNavigationRail(isDesktop),
|
navigationRail: _buildNavigationRail(isDesktop),
|
||||||
bottomNavigationBar: buildBottomBar(isDesktop),
|
bottomNavigationBar: buildBottomBar(isDesktop),
|
||||||
bottomPanel: PlayerBar(
|
bottomPanel: Builder(
|
||||||
|
builder: (context) => PlayerBar(
|
||||||
focusNode: playerBarFocusNode,
|
focusNode: playerBarFocusNode,
|
||||||
onTap: () =>
|
onTap: FancyScaffold.of(context)!.openPanel,
|
||||||
_fancyScaffoldKey.currentState!.dragController.fling(),
|
|
||||||
shouldHaveHero: false,
|
shouldHaveHero: false,
|
||||||
),
|
)),
|
||||||
bottomPanelHeight: 68.0,
|
bottomPanelHeight: 68.0,
|
||||||
expandedPanel: FocusScope(
|
expandedPanel: FocusScope(
|
||||||
node: playerScreenFocusNode,
|
node: playerScreenFocusNode,
|
||||||
|
@ -699,6 +717,8 @@ class MainScreenState extends State<MainScreen>
|
||||||
'/search': (context) => const SearchScreen(),
|
'/search': (context) => const SearchScreen(),
|
||||||
'/settings': (context) => const SettingsScreen(),
|
'/settings': (context) => const SettingsScreen(),
|
||||||
'/downloads': (context) => const DownloadsScreen(),
|
'/downloads': (context) => const DownloadsScreen(),
|
||||||
|
'/spotify-importer': (context) =>
|
||||||
|
const SpotifyImporterV2(),
|
||||||
},
|
},
|
||||||
)));
|
)));
|
||||||
}));
|
}));
|
||||||
|
@ -826,6 +846,12 @@ class _ExtensibleNavigationRailState extends State<ExtensibleNavigationRail> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class UpdateThemeNotification extends Notification {}
|
||||||
|
|
||||||
|
void updateTheme(BuildContext context) {
|
||||||
|
UpdateThemeNotification().dispatch(context);
|
||||||
|
}
|
||||||
|
|
||||||
// class FreezerDrawer extends StatelessWidget {
|
// class FreezerDrawer extends StatelessWidget {
|
||||||
// final double? width;
|
// final double? width;
|
||||||
// const FreezerDrawer({super.key, this.width});
|
// const FreezerDrawer({super.key, this.width});
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
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';
|
||||||
|
@ -303,7 +301,7 @@ class Settings {
|
||||||
|
|
||||||
static const deezerBg = Color(0xFF1F1A16);
|
static const deezerBg = Color(0xFF1F1A16);
|
||||||
static const deezerBottom = Color(0xFF1b1714);
|
static const deezerBottom = Color(0xFF1b1714);
|
||||||
TextTheme? get textTheme => (font == 'Deezer')
|
TextTheme? get textTheme => (font == 'Deezer' || font == 'System')
|
||||||
? null
|
? null
|
||||||
: GoogleFonts.getTextTheme(font,
|
: GoogleFonts.getTextTheme(font,
|
||||||
isDark ? ThemeData.dark().textTheme : ThemeData.light().textTheme);
|
isDark ? ThemeData.dark().textTheme : ThemeData.light().textTheme);
|
||||||
|
@ -311,7 +309,7 @@ class Settings {
|
||||||
|
|
||||||
final _elevation1Black = Color.alphaBlend(Colors.white12, Colors.black);
|
final _elevation1Black = Color.alphaBlend(Colors.white12, Colors.black);
|
||||||
|
|
||||||
late final Map<Themes, ThemeData> _themeData = {
|
Map<Themes, ThemeData> get _themeData => {
|
||||||
Themes.Light: ThemeData(
|
Themes.Light: ThemeData(
|
||||||
textTheme: textTheme,
|
textTheme: textTheme,
|
||||||
fontFamily: fontFamily,
|
fontFamily: fontFamily,
|
||||||
|
@ -368,7 +366,8 @@ class Settings {
|
||||||
bottomAppBarTheme: const BottomAppBarTheme(color: Colors.black),
|
bottomAppBarTheme: const BottomAppBarTheme(color: Colors.black),
|
||||||
dialogBackgroundColor: _elevation1Black,
|
dialogBackgroundColor: _elevation1Black,
|
||||||
sliderTheme: _sliderTheme,
|
sliderTheme: _sliderTheme,
|
||||||
bottomSheetTheme: BottomSheetThemeData(backgroundColor: _elevation1Black),
|
bottomSheetTheme:
|
||||||
|
BottomSheetThemeData(backgroundColor: _elevation1Black),
|
||||||
cardColor: _elevation1Black,
|
cardColor: _elevation1Black,
|
||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:freezer/main.dart';
|
||||||
import 'package:freezer/page_routes/fade.dart';
|
import 'package:freezer/page_routes/fade.dart';
|
||||||
import 'package:palette_generator/palette_generator.dart';
|
import 'package:palette_generator/palette_generator.dart';
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
@ -14,11 +15,12 @@ class ImagesDatabase {
|
||||||
*/
|
*/
|
||||||
|
|
||||||
void saveImage(String url) {
|
void saveImage(String url) {
|
||||||
CachedNetworkImageProvider(url);
|
CachedNetworkImageProvider(url, cacheManager: cacheManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<PaletteGenerator> getPaletteGenerator(String url) {
|
Future<PaletteGenerator> getPaletteGenerator(String url) {
|
||||||
return PaletteGenerator.fromImageProvider(CachedNetworkImageProvider(url));
|
return PaletteGenerator.fromImageProvider(
|
||||||
|
CachedNetworkImageProvider(url, cacheManager: cacheManager));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Color> getPrimaryColor(String url) async {
|
Future<Color> getPrimaryColor(String url) async {
|
||||||
|
@ -72,6 +74,7 @@ class _CachedImageState extends State<CachedImage> {
|
||||||
imageUrl: widget.url,
|
imageUrl: widget.url,
|
||||||
width: widget.width,
|
width: widget.width,
|
||||||
height: widget.height,
|
height: widget.height,
|
||||||
|
cacheManager: cacheManager,
|
||||||
placeholder: (context, url) {
|
placeholder: (context, url) {
|
||||||
if (widget.fullThumb) {
|
if (widget.fullThumb) {
|
||||||
return Image.asset(
|
return Image.asset(
|
||||||
|
@ -202,7 +205,8 @@ class _ZoomableImageRouteState extends State<ZoomableImageRoute> {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: PhotoView(
|
child: PhotoView(
|
||||||
imageProvider: CachedNetworkImageProvider(widget.imageUrl),
|
imageProvider: CachedNetworkImageProvider(widget.imageUrl,
|
||||||
|
cacheManager: cacheManager),
|
||||||
maxScale: 8.0,
|
maxScale: 8.0,
|
||||||
minScale: 0.2,
|
minScale: 0.2,
|
||||||
controller: controller,
|
controller: controller,
|
||||||
|
|
|
@ -3,7 +3,6 @@ import 'dart:math';
|
||||||
|
|
||||||
import 'package:draggable_scrollbar/draggable_scrollbar.dart';
|
import 'package:draggable_scrollbar/draggable_scrollbar.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
|
||||||
import 'package:fluttericon/font_awesome5_icons.dart';
|
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';
|
||||||
|
|
|
@ -37,6 +37,14 @@ class FancyScaffoldState extends State<FancyScaffold>
|
||||||
final statusNotifier =
|
final statusNotifier =
|
||||||
ValueNotifier<AnimationStatus>(AnimationStatus.dismissed);
|
ValueNotifier<AnimationStatus>(AnimationStatus.dismissed);
|
||||||
|
|
||||||
|
void openPanel() {
|
||||||
|
dragController.fling(velocity: 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void closePanel() {
|
||||||
|
dragController.fling(velocity: -1.0);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
dragController = AnimationController(
|
dragController = AnimationController(
|
||||||
|
|
|
@ -13,6 +13,8 @@ import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
class SpotifyImporterV1 extends StatefulWidget {
|
class SpotifyImporterV1 extends StatefulWidget {
|
||||||
const SpotifyImporterV1({super.key});
|
const SpotifyImporterV1({super.key});
|
||||||
|
|
||||||
|
@ -74,6 +76,14 @@ class _SpotifyImporterV1State extends State<SpotifyImporterV1> {
|
||||||
color: Colors.deepOrangeAccent,
|
color: Colors.deepOrangeAccent,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const ListTile(
|
||||||
|
title: Text('It\'s broken.'),
|
||||||
|
subtitle: Text('Use importer V2'),
|
||||||
|
leading: Icon(
|
||||||
|
Icons.warning,
|
||||||
|
color: Colors.deepOrangeAccent,
|
||||||
|
),
|
||||||
|
),
|
||||||
const FreezerDivider(),
|
const FreezerDivider(),
|
||||||
Container(
|
Container(
|
||||||
height: 16.0,
|
height: 16.0,
|
||||||
|
@ -410,7 +420,7 @@ class _SpotifyImporterV2State extends State<SpotifyImporterV2> {
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
child: Text("Open in Browser".i18n),
|
child: Text("Open in Browser".i18n),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
launchUrl(Uri.parse("https://developer.spotify.com/dashboard"));
|
launchUrlString("https://developer.spotify.com/dashboard");
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -676,8 +686,9 @@ class _SpotifyImporterV2MainState extends State<SpotifyImporterV2Main> {
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text(p.name!, maxLines: 1),
|
title: Text(p.name!, maxLines: 1),
|
||||||
subtitle: Text(p.owner!.displayName!, maxLines: 1),
|
subtitle: Text(p.owner!.displayName!, maxLines: 1),
|
||||||
leading: Image.network(p.images!.first.url ??
|
leading: Image.network((p.images?.isEmpty ?? true)
|
||||||
"http://cdn-images.deezer.com/images/cover//256x256-000000-80-0-0.jpg"),
|
? "http://cdn-images.deezer.com/images/cover//256x256-000000-80-0-0.jpg"
|
||||||
|
: p.images!.first.url!),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
_startImport(p.name, "", p.id);
|
_startImport(p.name, "", p.id);
|
||||||
},
|
},
|
||||||
|
|
|
@ -129,40 +129,42 @@ class LibraryScreen extends StatelessWidget {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
//Pick importer dialog
|
Navigator.of(context).pushNamed('/spotify-importer');
|
||||||
showDialog(
|
|
||||||
context: context,
|
//Pick importer dialog (removed as ImporterV1 is broken)
|
||||||
builder: (context) => SimpleDialog(
|
// showDialog(
|
||||||
title: Text('Importer'.i18n),
|
// context: context,
|
||||||
children: [
|
// builder: (context) => SimpleDialog(
|
||||||
ListTile(
|
// title: Text('Importer'.i18n),
|
||||||
leading: const Icon(FontAwesome5.spotify),
|
// children: [
|
||||||
title: Text('Spotify v1'.i18n),
|
// ListTile(
|
||||||
subtitle: Text(
|
// leading: const Icon(FontAwesome5.spotify),
|
||||||
'Import Spotify playlists up to 100 tracks without any login.'
|
// title: Text('Spotify v1'.i18n),
|
||||||
.i18n),
|
// subtitle: Text(
|
||||||
onTap: () {
|
// 'Import Spotify playlists up to 100 tracks without any login.'
|
||||||
Navigator.of(context).pop();
|
// .i18n),
|
||||||
Navigator.of(context).pushRoute(
|
// onTap: () {
|
||||||
builder: (context) =>
|
// Navigator.of(context).pop();
|
||||||
const SpotifyImporterV1());
|
// Navigator.of(context).pushRoute(
|
||||||
},
|
// builder: (context) =>
|
||||||
),
|
// const SpotifyImporterV1());
|
||||||
ListTile(
|
// },
|
||||||
leading: const Icon(FontAwesome5.spotify),
|
// ),
|
||||||
title: Text('Spotify v2'.i18n),
|
// ListTile(
|
||||||
subtitle: Text(
|
// leading: const Icon(FontAwesome5.spotify),
|
||||||
'Import any Spotify playlist, import from own Spotify library. Requires free account.'
|
// title: Text('Spotify v2'.i18n),
|
||||||
.i18n),
|
// subtitle: Text(
|
||||||
onTap: () {
|
// 'Import any Spotify playlist, import from own Spotify library. Requires free account.'
|
||||||
Navigator.of(context).pop();
|
// .i18n),
|
||||||
Navigator.of(context).pushRoute(
|
// onTap: () {
|
||||||
builder: (context) =>
|
// Navigator.of(context).pop();
|
||||||
const SpotifyImporterV2());
|
// Navigator.of(context).pushRoute(
|
||||||
},
|
// builder: (context) =>
|
||||||
)
|
// const SpotifyImporterV2());
|
||||||
],
|
// },
|
||||||
));
|
// )
|
||||||
|
// ],
|
||||||
|
// ));
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (DownloadManager.isSupported)
|
if (DownloadManager.isSupported)
|
||||||
|
|
|
@ -1,8 +1,13 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/gestures.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/deezer.dart';
|
import 'package:freezer/api/deezer.dart';
|
||||||
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||||
import 'package:freezer/translations.i18n.dart';
|
import 'package:freezer/translations.i18n.dart';
|
||||||
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
import 'package:desktop_webview_window/desktop_webview_window.dart';
|
||||||
|
|
||||||
import '../settings.dart';
|
import '../settings.dart';
|
||||||
import '../api/definitions.dart';
|
import '../api/definitions.dart';
|
||||||
|
@ -143,6 +148,78 @@ class _LoginWidgetState extends State<LoginWidget> {
|
||||||
_update();
|
_update();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _loginBrowser() async {
|
||||||
|
if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) {
|
||||||
|
// TODO: find a way to read arl from webview
|
||||||
|
if (!await WebviewWindow.isWebviewAvailable()) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (ctx) => AlertDialog(
|
||||||
|
title: Text('Can\'t login via browser'.i18n),
|
||||||
|
content: RichText(
|
||||||
|
text: TextSpan(children: [
|
||||||
|
TextSpan(
|
||||||
|
text:
|
||||||
|
'Your system doesn\'t have a WebView implementation.\n\u2022 If you are on Windows,'
|
||||||
|
.i18n),
|
||||||
|
TextSpan(
|
||||||
|
text:
|
||||||
|
'make sure it is available on your system.\n'.i18n,
|
||||||
|
style: const TextStyle(color: Colors.blue),
|
||||||
|
recognizer: TapGestureRecognizer()
|
||||||
|
..onTap = () => launchUrlString(
|
||||||
|
'https://developer.microsoft.com/en-us/microsoft-edge/webview2')),
|
||||||
|
TextSpan(
|
||||||
|
text:
|
||||||
|
'\u2022 If you are on Linux, make sure webkit2gtk is installed.\n'
|
||||||
|
.i18n),
|
||||||
|
TextSpan(
|
||||||
|
text:
|
||||||
|
'\nAlternatively, you can login in your browser on deezer.com, open your developer console with F12 and logging in with the ARL token'
|
||||||
|
.i18n),
|
||||||
|
])),
|
||||||
|
));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Navigator.of(context)
|
||||||
|
.pushRoute(builder: (context) => LoadingWindowWait(update: _update));
|
||||||
|
final webview =
|
||||||
|
await WebviewWindow.create(configuration: CreateConfiguration());
|
||||||
|
webview.launch('https://deezer.com/login');
|
||||||
|
webview.onClose.then((_) {
|
||||||
|
Navigator.pop(context);
|
||||||
|
});
|
||||||
|
|
||||||
|
webview.addOnUrlRequestCallback((url) {
|
||||||
|
if (!url.contains('/login') && url.contains('/register')) {
|
||||||
|
webview.evaluateJavaScript('window.location.href = "/open_app"');
|
||||||
|
}
|
||||||
|
|
||||||
|
final uri = Uri.parse(url);
|
||||||
|
//Parse arl from url
|
||||||
|
if (uri.scheme == 'intent' && uri.host == 'deezer.page.link') {
|
||||||
|
try {
|
||||||
|
//Actual url is in `link` query parameter
|
||||||
|
Uri linkUri = Uri.parse(uri.queryParameters['link']!);
|
||||||
|
String? arl = linkUri.queryParameters['arl'];
|
||||||
|
if (arl != null) {
|
||||||
|
settings.arl = arl;
|
||||||
|
webview.close();
|
||||||
|
_update();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Navigator.of(context)
|
||||||
|
.pushRoute(builder: (context) => LoginBrowser(_update));
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
//If arl non null, show loading
|
//If arl non null, show loading
|
||||||
|
@ -211,12 +288,8 @@ class _LoginWidgetState extends State<LoginWidget> {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
|
onPressed: _loginBrowser,
|
||||||
child: Text('Login using browser'.i18n),
|
child: Text('Login using browser'.i18n),
|
||||||
onPressed: () {
|
|
||||||
Navigator.of(context).pushRoute(
|
|
||||||
builder: (context) =>
|
|
||||||
LoginBrowser(_update));
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
child: Text('Login using token'.i18n),
|
child: Text('Login using token'.i18n),
|
||||||
|
@ -266,8 +339,7 @@ class _LoginWidgetState extends State<LoginWidget> {
|
||||||
child: OutlinedButton(
|
child: OutlinedButton(
|
||||||
child: Text('Open in browser'.i18n),
|
child: Text('Open in browser'.i18n),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
InAppBrowser.openWithSystemBrowser(
|
launchUrlString('https://deezer.com/register');
|
||||||
url: Uri.parse('https://deezer.com/register'));
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -292,6 +364,31 @@ class _LoginWidgetState extends State<LoginWidget> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class LoadingWindowWait extends StatelessWidget {
|
||||||
|
final VoidCallback update;
|
||||||
|
const LoadingWindowWait({super.key, required this.update});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
body: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Please login by using the floating window'.i18n,
|
||||||
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16.0),
|
||||||
|
const CircularProgressIndicator(),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class LoginBrowser extends StatelessWidget {
|
class LoginBrowser extends StatelessWidget {
|
||||||
final Function updateParent;
|
final Function updateParent;
|
||||||
const LoginBrowser(this.updateParent, {super.key});
|
const LoginBrowser(this.updateParent, {super.key});
|
||||||
|
@ -340,8 +437,8 @@ class EmailLogin extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _EmailLoginState extends State<EmailLogin> {
|
class _EmailLoginState extends State<EmailLogin> {
|
||||||
String? _email;
|
final _emailController = TextEditingController();
|
||||||
String? _password;
|
final _passwordController = TextEditingController();
|
||||||
bool _loading = false;
|
bool _loading = false;
|
||||||
|
|
||||||
Future _login() async {
|
Future _login() async {
|
||||||
|
@ -350,7 +447,8 @@ class _EmailLoginState extends State<EmailLogin> {
|
||||||
String? arl;
|
String? arl;
|
||||||
String? exception;
|
String? exception;
|
||||||
try {
|
try {
|
||||||
arl = await DeezerAPI.getArlByEmail(_email, _password!);
|
arl = await deezerAPI.getArlByEmail(
|
||||||
|
_emailController.text, _passwordController.text);
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
exception = e.toString();
|
exception = e.toString();
|
||||||
print(e);
|
print(e);
|
||||||
|
@ -393,17 +491,15 @@ class _EmailLoginState extends State<EmailLogin> {
|
||||||
children: _loading
|
children: _loading
|
||||||
? [const CircularProgressIndicator()]
|
? [const CircularProgressIndicator()]
|
||||||
: [
|
: [
|
||||||
TextField(
|
TextFormField(
|
||||||
decoration: InputDecoration(labelText: 'Email'.i18n),
|
decoration: InputDecoration(labelText: 'Email'.i18n),
|
||||||
onChanged: (s) => _email = s,
|
controller: _emailController,
|
||||||
),
|
),
|
||||||
Container(
|
const SizedBox(height: 8.0),
|
||||||
height: 8.0,
|
TextFormField(
|
||||||
),
|
|
||||||
TextField(
|
|
||||||
obscureText: true,
|
obscureText: true,
|
||||||
decoration: InputDecoration(labelText: "Password".i18n),
|
decoration: InputDecoration(labelText: "Password".i18n),
|
||||||
onChanged: (s) => _password = s,
|
controller: _passwordController,
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -412,7 +508,8 @@ class _EmailLoginState extends State<EmailLogin> {
|
||||||
TextButton(
|
TextButton(
|
||||||
child: const Text('Login'),
|
child: const Text('Login'),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
if (_email != null && _password != null) {
|
if (_emailController.text.isNotEmpty &&
|
||||||
|
_passwordController.text.isNotEmpty) {
|
||||||
await _login();
|
await _login();
|
||||||
} else {
|
} else {
|
||||||
ScaffoldMessenger.of(context)
|
ScaffoldMessenger.of(context)
|
||||||
|
|
|
@ -106,8 +106,9 @@ class MenuSheetOption {
|
||||||
}
|
}
|
||||||
|
|
||||||
class MenuSheet {
|
class MenuSheet {
|
||||||
BuildContext context;
|
final BuildContext context;
|
||||||
Function? navigateCallback;
|
final Function? navigateCallback;
|
||||||
|
bool _contextMenuOpen = false;
|
||||||
|
|
||||||
MenuSheet(this.context, {this.navigateCallback});
|
MenuSheet(this.context, {this.navigateCallback});
|
||||||
|
|
||||||
|
@ -115,12 +116,16 @@ class MenuSheet {
|
||||||
{required TapDownDetails details}) {
|
{required TapDownDetails details}) {
|
||||||
final overlay = Overlay.of(context).context.findRenderObject() as RenderBox;
|
final overlay = Overlay.of(context).context.findRenderObject() as RenderBox;
|
||||||
final actualPosition = overlay.globalToLocal(details.globalPosition);
|
final actualPosition = overlay.globalToLocal(details.globalPosition);
|
||||||
|
_contextMenuOpen = true;
|
||||||
showMenu(
|
showMenu(
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
elevation: 4.0,
|
elevation: 4.0,
|
||||||
context: context,
|
context: context,
|
||||||
constraints: const BoxConstraints(maxWidth: 300.0),
|
constraints: const BoxConstraints(maxWidth: 300.0),
|
||||||
position:
|
position:
|
||||||
RelativeRect.fromSize(actualPosition & Size.zero, overlay.size),
|
RelativeRect.fromSize(actualPosition & Size.zero, overlay.size),
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(28.0))),
|
||||||
items: options
|
items: options
|
||||||
.map((option) => PopupMenuItem(
|
.map((option) => PopupMenuItem(
|
||||||
onTap: option.onTap,
|
onTap: option.onTap,
|
||||||
|
@ -131,7 +136,8 @@ class MenuSheet {
|
||||||
const SizedBox(width: 8.0),
|
const SizedBox(width: 8.0),
|
||||||
Flexible(child: option.label),
|
Flexible(child: option.label),
|
||||||
])))
|
])))
|
||||||
.toList(growable: false));
|
.toList(growable: false))
|
||||||
|
.then((_) => _contextMenuOpen = false);
|
||||||
}
|
}
|
||||||
|
|
||||||
//===================
|
//===================
|
||||||
|
@ -162,7 +168,10 @@ class MenuSheet {
|
||||||
.map((option) => ListTile(
|
.map((option) => ListTile(
|
||||||
title: option.label,
|
title: option.label,
|
||||||
leading: option.icon,
|
leading: option.icon,
|
||||||
onTap: option.onTap,
|
onTap: () {
|
||||||
|
option.onTap.call();
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
))
|
))
|
||||||
.toList(growable: false)),
|
.toList(growable: false)),
|
||||||
),
|
),
|
||||||
|
@ -212,7 +221,10 @@ class MenuSheet {
|
||||||
.map((option) => ListTile(
|
.map((option) => ListTile(
|
||||||
title: option.label,
|
title: option.label,
|
||||||
leading: option.icon,
|
leading: option.icon,
|
||||||
onTap: option.onTap,
|
onTap: () {
|
||||||
|
option.onTap.call();
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
))
|
))
|
||||||
.toList(growable: false))),
|
.toList(growable: false))),
|
||||||
],
|
],
|
||||||
|
@ -259,14 +271,12 @@ class MenuSheet {
|
||||||
icon: const Icon(Icons.playlist_play), onTap: () async {
|
icon: const Icon(Icons.playlist_play), onTap: () async {
|
||||||
//-1 = next
|
//-1 = next
|
||||||
await audioHandler.insertQueueItem(-1, await t.toMediaItem());
|
await audioHandler.insertQueueItem(-1, await t.toMediaItem());
|
||||||
_close();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
MenuSheetOption addToQueue(Track t) =>
|
MenuSheetOption addToQueue(Track t) =>
|
||||||
MenuSheetOption(Text('Add to queue'.i18n),
|
MenuSheetOption(Text('Add to queue'.i18n),
|
||||||
icon: const Icon(Icons.playlist_add), onTap: () async {
|
icon: const Icon(Icons.playlist_add), onTap: () async {
|
||||||
await audioHandler.addQueueItem(await t.toMediaItem());
|
await audioHandler.addQueueItem(await t.toMediaItem());
|
||||||
_close();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
MenuSheetOption addTrackFavorite(Track t) =>
|
MenuSheetOption addTrackFavorite(Track t) =>
|
||||||
|
@ -281,8 +291,6 @@ class MenuSheet {
|
||||||
ScaffoldMessenger.of(context).snack('Added to library'.i18n);
|
ScaffoldMessenger.of(context).snack('Added to library'.i18n);
|
||||||
//Add to cache
|
//Add to cache
|
||||||
cache.libraryTracks.add(t.id);
|
cache.libraryTracks.add(t.id);
|
||||||
|
|
||||||
_close();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
MenuSheetOption downloadTrack(Track t) => MenuSheetOption(
|
MenuSheetOption downloadTrack(Track t) => MenuSheetOption(
|
||||||
|
@ -292,7 +300,6 @@ class MenuSheet {
|
||||||
if (await downloadManager.addOfflineTrack(t,
|
if (await downloadManager.addOfflineTrack(t,
|
||||||
private: false, context: context, isSingleton: true) !=
|
private: false, context: context, isSingleton: true) !=
|
||||||
false) showDownloadStartedToast();
|
false) showDownloadStartedToast();
|
||||||
_close();
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -316,7 +323,6 @@ class MenuSheet {
|
||||||
.snack("${"Track added to".i18n} ${p.title}");
|
.snack("${"Track added to".i18n} ${p.title}");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
_close();
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -327,7 +333,6 @@ class MenuSheet {
|
||||||
await deezerAPI.removeFromPlaylist(t.id, p!.id);
|
await deezerAPI.removeFromPlaylist(t.id, p!.id);
|
||||||
ScaffoldMessenger.of(context)
|
ScaffoldMessenger.of(context)
|
||||||
.snack('${'Track removed from'.i18n} ${p.title}');
|
.snack('${'Track removed from'.i18n} ${p.title}');
|
||||||
_close();
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -346,7 +351,6 @@ class MenuSheet {
|
||||||
ScaffoldMessenger.of(context)
|
ScaffoldMessenger.of(context)
|
||||||
.snack('Track removed from library'.i18n);
|
.snack('Track removed from library'.i18n);
|
||||||
if (onUpdate != null) onUpdate();
|
if (onUpdate != null) onUpdate();
|
||||||
_close();
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -359,7 +363,6 @@ class MenuSheet {
|
||||||
),
|
),
|
||||||
icon: const Icon(Icons.recent_actors),
|
icon: const Icon(Icons.recent_actors),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
_close();
|
|
||||||
navigatorKey.currentState!
|
navigatorKey.currentState!
|
||||||
.push(MaterialPageRoute(builder: (context) => ArtistDetails(a)));
|
.push(MaterialPageRoute(builder: (context) => ArtistDetails(a)));
|
||||||
|
|
||||||
|
@ -377,7 +380,6 @@ class MenuSheet {
|
||||||
),
|
),
|
||||||
icon: const Icon(Icons.album),
|
icon: const Icon(Icons.album),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
_close();
|
|
||||||
navigatorKey.currentState!
|
navigatorKey.currentState!
|
||||||
.push(MaterialPageRoute(builder: (context) => AlbumDetails(a)));
|
.push(MaterialPageRoute(builder: (context) => AlbumDetails(a)));
|
||||||
|
|
||||||
|
@ -392,7 +394,6 @@ class MenuSheet {
|
||||||
icon: const Icon(Icons.online_prediction),
|
icon: const Icon(Icons.online_prediction),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
playerHelper.playMix(track.id, track.title!);
|
playerHelper.playMix(track.id, track.title!);
|
||||||
_close();
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -412,7 +413,6 @@ class MenuSheet {
|
||||||
await downloadManager.addOfflineTrack(track,
|
await downloadManager.addOfflineTrack(track,
|
||||||
private: true, context: context);
|
private: true, context: context);
|
||||||
}
|
}
|
||||||
_close();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
//===================
|
//===================
|
||||||
|
@ -442,7 +442,6 @@ class MenuSheet {
|
||||||
MenuSheetOption downloadAlbum(Album a) =>
|
MenuSheetOption downloadAlbum(Album a) =>
|
||||||
MenuSheetOption(Text('Download'.i18n),
|
MenuSheetOption(Text('Download'.i18n),
|
||||||
icon: const Icon(Icons.file_download), onTap: () async {
|
icon: const Icon(Icons.file_download), onTap: () async {
|
||||||
_close();
|
|
||||||
if (await downloadManager.addOfflineAlbum(a,
|
if (await downloadManager.addOfflineAlbum(a,
|
||||||
private: false, context: context) !=
|
private: false, context: context) !=
|
||||||
false) showDownloadStartedToast();
|
false) showDownloadStartedToast();
|
||||||
|
@ -454,7 +453,7 @@ class MenuSheet {
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
await deezerAPI.addFavoriteAlbum(a.id);
|
await deezerAPI.addFavoriteAlbum(a.id);
|
||||||
await downloadManager.addOfflineAlbum(a, private: true);
|
await downloadManager.addOfflineAlbum(a, private: true);
|
||||||
_close();
|
|
||||||
showDownloadStartedToast();
|
showDownloadStartedToast();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -465,7 +464,6 @@ class MenuSheet {
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
await deezerAPI.addFavoriteAlbum(a.id);
|
await deezerAPI.addFavoriteAlbum(a.id);
|
||||||
ScaffoldMessenger.of(context).snack('Added to library'.i18n);
|
ScaffoldMessenger.of(context).snack('Added to library'.i18n);
|
||||||
_close();
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -478,7 +476,6 @@ class MenuSheet {
|
||||||
await downloadManager.removeOfflineAlbum(a.id);
|
await downloadManager.removeOfflineAlbum(a.id);
|
||||||
ScaffoldMessenger.of(context).snack('Album removed'.i18n);
|
ScaffoldMessenger.of(context).snack('Album removed'.i18n);
|
||||||
if (onRemove != null) onRemove();
|
if (onRemove != null) onRemove();
|
||||||
_close();
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -512,7 +509,6 @@ class MenuSheet {
|
||||||
ScaffoldMessenger.of(context)
|
ScaffoldMessenger.of(context)
|
||||||
.snack('Artist removed from library'.i18n);
|
.snack('Artist removed from library'.i18n);
|
||||||
if (onRemove != null) onRemove();
|
if (onRemove != null) onRemove();
|
||||||
_close();
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -522,7 +518,6 @@ class MenuSheet {
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
await deezerAPI.addFavoriteArtist(a.id);
|
await deezerAPI.addFavoriteArtist(a.id);
|
||||||
ScaffoldMessenger.of(context).snack('Added to library'.i18n);
|
ScaffoldMessenger.of(context).snack('Added to library'.i18n);
|
||||||
_close();
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -567,7 +562,6 @@ class MenuSheet {
|
||||||
}
|
}
|
||||||
downloadManager.removeOfflinePlaylist(p.id);
|
downloadManager.removeOfflinePlaylist(p.id);
|
||||||
if (onRemove != null) onRemove();
|
if (onRemove != null) onRemove();
|
||||||
_close();
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -577,7 +571,6 @@ class MenuSheet {
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
await deezerAPI.addPlaylist(p.id);
|
await deezerAPI.addPlaylist(p.id);
|
||||||
ScaffoldMessenger.of(context).snack('Added playlist to library'.i18n);
|
ScaffoldMessenger.of(context).snack('Added playlist to library'.i18n);
|
||||||
_close();
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -588,7 +581,7 @@ class MenuSheet {
|
||||||
//Add to library
|
//Add to library
|
||||||
await deezerAPI.addPlaylist(p.id);
|
await deezerAPI.addPlaylist(p.id);
|
||||||
downloadManager.addOfflinePlaylist(p, private: true);
|
downloadManager.addOfflinePlaylist(p, private: true);
|
||||||
_close();
|
|
||||||
showDownloadStartedToast();
|
showDownloadStartedToast();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -597,7 +590,6 @@ class MenuSheet {
|
||||||
Text('Download playlist'.i18n),
|
Text('Download playlist'.i18n),
|
||||||
icon: const Icon(Icons.file_download),
|
icon: const Icon(Icons.file_download),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
_close();
|
|
||||||
if (await downloadManager.addOfflinePlaylist(p,
|
if (await downloadManager.addOfflinePlaylist(p,
|
||||||
private: false, context: context) !=
|
private: false, context: context) !=
|
||||||
false) showDownloadStartedToast();
|
false) showDownloadStartedToast();
|
||||||
|
@ -612,7 +604,7 @@ class MenuSheet {
|
||||||
await showDialog(
|
await showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => CreatePlaylistDialog(playlist: p));
|
builder: (context) => CreatePlaylistDialog(playlist: p));
|
||||||
_close();
|
|
||||||
if (onUpdate != null) onUpdate();
|
if (onUpdate != null) onUpdate();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -689,7 +681,6 @@ class MenuSheet {
|
||||||
Text('Keep the screen on'.i18n),
|
Text('Keep the screen on'.i18n),
|
||||||
icon: const Icon(Icons.screen_lock_portrait),
|
icon: const Icon(Icons.screen_lock_portrait),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
_close();
|
|
||||||
//Enable
|
//Enable
|
||||||
if (!cache.wakelock) {
|
if (!cache.wakelock) {
|
||||||
WakelockPlus.enable();
|
WakelockPlus.enable();
|
||||||
|
@ -703,11 +694,6 @@ class MenuSheet {
|
||||||
cache.wakelock = false;
|
cache.wakelock = false;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
void _close() {
|
|
||||||
FancyScaffold.of(context)!.dragController.fling(velocity: -1.0);
|
|
||||||
// Navigator.of(context).pop();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class SleepTimerDialog extends StatefulWidget {
|
class SleepTimerDialog extends StatefulWidget {
|
||||||
|
|
|
@ -50,7 +50,8 @@ class BackgroundProvider extends ChangeNotifier {
|
||||||
!settings.enableFilledPlayButton &&
|
!settings.enableFilledPlayButton &&
|
||||||
!settings.playerAlbumArtDropShadow) return;
|
!settings.playerAlbumArtDropShadow) return;
|
||||||
final imageProvider = CachedNetworkImageProvider(
|
final imageProvider = CachedNetworkImageProvider(
|
||||||
mediaItem.extras!['thumb'] ?? mediaItem.artUri.toString());
|
mediaItem.extras!['thumb'] ?? mediaItem.artUri.toString(),
|
||||||
|
cacheManager: cacheManager);
|
||||||
//Run in isolate
|
//Run in isolate
|
||||||
_palette = await PaletteGenerator.fromImageProvider(imageProvider);
|
_palette = await PaletteGenerator.fromImageProvider(imageProvider);
|
||||||
_dominantColor = _palette!.dominantColor!.color;
|
_dominantColor = _palette!.dominantColor!.color;
|
||||||
|
@ -285,24 +286,27 @@ class PlayerScreenVertical extends StatelessWidget {
|
||||||
mainAxisSize: MainAxisSize.max,
|
mainAxisSize: MainAxisSize.max,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
const Padding(
|
Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 4.0),
|
padding: EdgeInsets.symmetric(horizontal: 4.0),
|
||||||
child: PlayerScreenTopRow(),
|
child: PlayerScreenTopRow(
|
||||||
|
textSize: 20.spMax,
|
||||||
|
iconSize: 24.spMax,
|
||||||
),
|
),
|
||||||
const BigAlbumArt(),
|
),
|
||||||
|
Flexible(child: const BigAlbumArt()),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
||||||
child: PlayerTextSubtext(textSize: 64.sp),
|
child: PlayerTextSubtext(textSize: 32.spMax),
|
||||||
),
|
),
|
||||||
SeekBar(textSize: 38.sp),
|
SeekBar(textSize: 20.spMax),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||||
child: PlaybackControls(86.sp),
|
child: PlaybackControls(40.spMax),
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding:
|
padding:
|
||||||
const EdgeInsets.symmetric(vertical: 0, horizontal: 16.0),
|
const EdgeInsets.symmetric(vertical: 0, horizontal: 16.0),
|
||||||
child: BottomBarControls(size: 56.sp),
|
child: BottomBarControls(size: 28.spMax),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
));
|
));
|
||||||
|
@ -384,13 +388,15 @@ class _DesktopTabView extends StatelessWidget {
|
||||||
.textTheme
|
.textTheme
|
||||||
.labelLarge!
|
.labelLarge!
|
||||||
.copyWith(fontSize: 18.0)),
|
.copyWith(fontSize: 18.0)),
|
||||||
const Expanded(
|
Expanded(
|
||||||
child: SizedBox.expand(
|
child: SizedBox.expand(
|
||||||
child: Material(
|
child: Material(
|
||||||
type: MaterialType.transparency,
|
type: MaterialType.transparency,
|
||||||
child: TabBarView(children: [
|
child: TabBarView(children: [
|
||||||
QueueListWidget(),
|
QueueListWidget(
|
||||||
LyricsWidget(),
|
closePlayer: FancyScaffold.of(context)!.closePanel,
|
||||||
|
),
|
||||||
|
const LyricsWidget(),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
)),
|
)),
|
||||||
|
@ -570,7 +576,8 @@ class PlayerMenuButton extends StatelessWidget {
|
||||||
final currentMediaItem = audioHandler.mediaItem.value!;
|
final currentMediaItem = audioHandler.mediaItem.value!;
|
||||||
Track t = Track.fromMediaItem(currentMediaItem);
|
Track t = Track.fromMediaItem(currentMediaItem);
|
||||||
MenuSheet m = MenuSheet(context, navigateCallback: () {
|
MenuSheet m = MenuSheet(context, navigateCallback: () {
|
||||||
Navigator.of(context).pop();
|
// close player
|
||||||
|
FancyScaffold.of(context)?.closePanel();
|
||||||
});
|
});
|
||||||
if (currentMediaItem.extras!['show'] == null) {
|
if (currentMediaItem.extras!['show'] == null) {
|
||||||
m.defaultTrackMenu(t, options: [m.sleepTimer(), m.wakelock()]);
|
m.defaultTrackMenu(t, options: [m.sleepTimer(), m.wakelock()]);
|
||||||
|
@ -952,8 +959,7 @@ class PlayerScreenTopRow extends StatelessWidget {
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () =>
|
onPressed: FancyScaffold.of(context)!.closePanel,
|
||||||
FancyScaffold.of(context)!.dragController.fling(velocity: -1.0),
|
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
Icons.keyboard_arrow_down,
|
Icons.keyboard_arrow_down,
|
||||||
semanticLabel: 'Close'.i18n,
|
semanticLabel: 'Close'.i18n,
|
||||||
|
@ -985,8 +991,10 @@ class PlayerScreenTopRow extends StatelessWidget {
|
||||||
),
|
),
|
||||||
iconSize: size,
|
iconSize: size,
|
||||||
splashRadius: size * 1.5,
|
splashRadius: size * 1.5,
|
||||||
onPressed: () => Navigator.of(context)
|
onPressed: () => Navigator.of(context).pushRoute(
|
||||||
.pushRoute(builder: (context) => const QueueScreen()),
|
builder: (ctx) => QueueScreen(
|
||||||
|
closePlayer: FancyScaffold.of(context)!.closePanel,
|
||||||
|
)),
|
||||||
)
|
)
|
||||||
: SizedBox.square(dimension: size + 16.0),
|
: SizedBox.square(dimension: size + 16.0),
|
||||||
],
|
],
|
||||||
|
|
|
@ -6,11 +6,13 @@ import 'package:flutter/services.dart';
|
||||||
import 'package:freezer/api/definitions.dart';
|
import 'package:freezer/api/definitions.dart';
|
||||||
import 'package:freezer/api/player/audio_handler.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/fancy_scaffold.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';
|
||||||
|
|
||||||
class QueueScreen extends StatelessWidget {
|
class QueueScreen extends StatelessWidget {
|
||||||
const QueueScreen({super.key});
|
final VoidCallback closePlayer;
|
||||||
|
const QueueScreen({super.key, required this.closePlayer});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
@ -39,13 +41,21 @@ class QueueScreen extends StatelessWidget {
|
||||||
// )
|
// )
|
||||||
// ],
|
// ],
|
||||||
),
|
),
|
||||||
body: const SafeArea(child: QueueListWidget(shouldPopOnTap: true)));
|
body: SafeArea(
|
||||||
|
child: QueueListWidget(
|
||||||
|
shouldPopOnTap: true, closePlayer: closePlayer)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class QueueListWidget extends StatefulWidget {
|
class QueueListWidget extends StatefulWidget {
|
||||||
|
final VoidCallback closePlayer;
|
||||||
final bool shouldPopOnTap;
|
final bool shouldPopOnTap;
|
||||||
const QueueListWidget({super.key, this.shouldPopOnTap = false});
|
final bool isInsidePlayer;
|
||||||
|
const QueueListWidget(
|
||||||
|
{super.key,
|
||||||
|
this.shouldPopOnTap = false,
|
||||||
|
this.isInsidePlayer = false,
|
||||||
|
required this.closePlayer});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<QueueListWidget> createState() => _QueueListWidgetState();
|
State<QueueListWidget> createState() => _QueueListWidgetState();
|
||||||
|
@ -107,6 +117,13 @@ class _QueueListWidgetState extends State<QueueListWidget> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final menuSheet = MenuSheet(context, navigateCallback: () {
|
||||||
|
if (!widget.isInsidePlayer) {
|
||||||
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
widget.closePlayer.call();
|
||||||
|
});
|
||||||
return ReorderableListView.builder(
|
return ReorderableListView.builder(
|
||||||
buildDefaultDragHandles: false,
|
buildDefaultDragHandles: false,
|
||||||
scrollController: _scrollController,
|
scrollController: _scrollController,
|
||||||
|
@ -168,7 +185,7 @@ class _QueueListWidgetState extends State<QueueListWidget> {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onSecondary: (details) => MenuSheet(context).defaultTrackMenu(
|
onSecondary: (details) => menuSheet.defaultTrackMenu(
|
||||||
Track.fromMediaItem(mediaItem),
|
Track.fromMediaItem(mediaItem),
|
||||||
details: details),
|
details: details),
|
||||||
checkTrackOffline: false,
|
checkTrackOffline: false,
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:country_pickers/country.dart';
|
import 'package:country_pickers/country.dart';
|
||||||
import 'package:country_pickers/country_picker_dialog.dart';
|
import 'package:country_pickers/country_picker_dialog.dart';
|
||||||
|
import 'package:flex_color_picker/flex_color_picker.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
@ -11,6 +13,7 @@ import 'package:fluttericon/font_awesome5_icons.dart';
|
||||||
import 'package:fluttericon/web_symbols_icons.dart';
|
import 'package:fluttericon/web_symbols_icons.dart';
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:freezer/api/definitions.dart';
|
import 'package:freezer/api/definitions.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
import 'package:scrobblenaut/scrobblenaut.dart';
|
import 'package:scrobblenaut/scrobblenaut.dart';
|
||||||
|
@ -184,7 +187,7 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
settings.theme = Themes.Light;
|
settings.theme = Themes.Light;
|
||||||
settings.save();
|
settings.save();
|
||||||
updateTheme();
|
updateTheme(context);
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -193,7 +196,7 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
settings.theme = Themes.Dark;
|
settings.theme = Themes.Dark;
|
||||||
settings.save();
|
settings.save();
|
||||||
updateTheme();
|
updateTheme(context);
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -202,7 +205,7 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
settings.theme = Themes.Black;
|
settings.theme = Themes.Black;
|
||||||
settings.save();
|
settings.save();
|
||||||
updateTheme();
|
updateTheme(context);
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -211,7 +214,7 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
settings.theme = Themes.Deezer;
|
settings.theme = Themes.Deezer;
|
||||||
settings.save();
|
settings.save();
|
||||||
updateTheme();
|
updateTheme(context);
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -229,7 +232,7 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
|
||||||
settings.useSystemTheme = v;
|
settings.useSystemTheme = v;
|
||||||
|
|
||||||
settings.save();
|
settings.save();
|
||||||
updateTheme();
|
updateTheme(context);
|
||||||
},
|
},
|
||||||
secondary: const Icon(Icons.android)),
|
secondary: const Icon(Icons.android)),
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
|
@ -239,7 +242,7 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
|
||||||
settings.materialYouAccent = v;
|
settings.materialYouAccent = v;
|
||||||
|
|
||||||
settings.save();
|
settings.save();
|
||||||
updateTheme();
|
updateTheme(context);
|
||||||
}),
|
}),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text('Font'.i18n),
|
title: Text('Font'.i18n),
|
||||||
|
@ -382,41 +385,47 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
|
||||||
child: CircleAvatar(
|
child: CircleAvatar(
|
||||||
backgroundColor: settings.primaryColor,
|
backgroundColor: settings.primaryColor,
|
||||||
)),
|
)),
|
||||||
onTap: settings.materialYouAccent
|
enabled: !settings.materialYouAccent,
|
||||||
? null
|
onTap: () async {
|
||||||
: () {
|
final color = await showDialog<Color>(
|
||||||
showDialog(
|
context: context, builder: (context) => const _ColorPicker());
|
||||||
context: context,
|
|
||||||
builder: (context) {
|
|
||||||
return AlertDialog(
|
|
||||||
title: Text('Primary color'.i18n),
|
|
||||||
content: SizedBox(
|
|
||||||
height: 240,
|
|
||||||
child: MaterialColorPicker(
|
|
||||||
colors: [
|
|
||||||
...Colors.primaries,
|
|
||||||
//Logo colors
|
|
||||||
_swatch(0xffeca704),
|
|
||||||
_swatch(0xffbe3266),
|
|
||||||
_swatch(0xff4b2e7e),
|
|
||||||
_swatch(0xff384697),
|
|
||||||
_swatch(0xff0880b5),
|
|
||||||
_swatch(0xff009a85),
|
|
||||||
_swatch(0xff2ba766)
|
|
||||||
],
|
|
||||||
allowShades: false,
|
|
||||||
selectedColor: settings.primaryColor,
|
|
||||||
onMainColorChange: (ColorSwatch? color) {
|
|
||||||
if (color == null) return;
|
if (color == null) return;
|
||||||
settings.primaryColor = color;
|
settings.primaryColor = color;
|
||||||
settings.save();
|
settings.save();
|
||||||
updateTheme();
|
updateTheme(context);
|
||||||
Navigator.of(context).pop();
|
|
||||||
},
|
//showDialog(
|
||||||
),
|
// context: context,
|
||||||
),
|
// builder: (context) {
|
||||||
);
|
// return AlertDialog(
|
||||||
});
|
// title: Text('Primary color'.i18n),
|
||||||
|
// content: SizedBox(
|
||||||
|
// height: 240,
|
||||||
|
// child: MaterialColorPicker(
|
||||||
|
// colors: [
|
||||||
|
// ...Colors.primaries,
|
||||||
|
// //Logo colors
|
||||||
|
// _swatch(0xffeca704),
|
||||||
|
// _swatch(0xffbe3266),
|
||||||
|
// _swatch(0xff4b2e7e),
|
||||||
|
// _swatch(0xff384697),
|
||||||
|
// _swatch(0xff0880b5),
|
||||||
|
// _swatch(0xff009a85),
|
||||||
|
// _swatch(0xff2ba766)
|
||||||
|
// ],
|
||||||
|
// allowShades: false,
|
||||||
|
// selectedColor: settings.primaryColor,
|
||||||
|
// onMainColorChange: (ColorSwatch? color) {
|
||||||
|
// if (color == null) return;
|
||||||
|
// settings.primaryColor = color;
|
||||||
|
// settings.save();
|
||||||
|
// updateTheme(context);
|
||||||
|
// Navigator.of(context).pop();
|
||||||
|
// },
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
// });
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
|
@ -443,7 +452,9 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
settings.navigationRailAppearance = value;
|
settings.navigationRailAppearance = value;
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
settings.save().then((_) => updateTheme());
|
settings
|
||||||
|
.save()
|
||||||
|
.then((_) => updateTheme(context));
|
||||||
}))
|
}))
|
||||||
.toList(growable: false),
|
.toList(growable: false),
|
||||||
)),
|
)),
|
||||||
|
@ -484,6 +495,49 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _ColorPicker extends StatefulWidget {
|
||||||
|
const _ColorPicker({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_ColorPicker> createState() => _ColorPickerState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ColorPickerState extends State<_ColorPicker> {
|
||||||
|
Color color = settings.primaryColor;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text('Primary color'.i18n),
|
||||||
|
content: SizedBox(
|
||||||
|
height: 600.0,
|
||||||
|
width: min(MediaQuery.of(context).size.width * 0.9, 600.0),
|
||||||
|
child: ColorPicker(
|
||||||
|
width: 56.0,
|
||||||
|
height: 56.0,
|
||||||
|
borderRadius: 50.0,
|
||||||
|
onColorChanged: (color) => setState(() => this.color = color),
|
||||||
|
color: color,
|
||||||
|
showColorCode: true,
|
||||||
|
pickersEnabled: const {
|
||||||
|
ColorPickerType.both: false,
|
||||||
|
ColorPickerType.primary: true,
|
||||||
|
ColorPickerType.accent: false,
|
||||||
|
ColorPickerType.bw: false,
|
||||||
|
ColorPickerType.custom: true,
|
||||||
|
ColorPickerType.wheel: true,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context, color),
|
||||||
|
child: Text('OK'.i18n)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class FontSelector extends StatefulWidget {
|
class FontSelector extends StatefulWidget {
|
||||||
final Function callback;
|
final Function callback;
|
||||||
|
|
||||||
|
@ -518,7 +572,7 @@ class _FontSelectorState extends State<FontSelector> {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
widget.callback();
|
widget.callback();
|
||||||
//Global setState
|
//Global setState
|
||||||
updateTheme();
|
updateTheme(context);
|
||||||
},
|
},
|
||||||
child: Text('Apply'.i18n),
|
child: Text('Apply'.i18n),
|
||||||
),
|
),
|
||||||
|
@ -549,8 +603,11 @@ class _FontSelectorState extends State<FontSelector> {
|
||||||
onChanged: (q) => setState(() => query = q),
|
onChanged: (q) => setState(() => query = q),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SingleChildScrollView(
|
SizedBox(
|
||||||
child: SizedBox(
|
height: MediaQuery.of(context).size.height - 300.0,
|
||||||
|
width: 400.0,
|
||||||
|
child: Material(
|
||||||
|
type: MaterialType.transparency,
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
itemExtent: 56.0,
|
itemExtent: 56.0,
|
||||||
|
@ -802,6 +859,7 @@ class _DeezerSettingsState extends State<DeezerSettings> {
|
||||||
setState(() => settings.deezerLanguage =
|
setState(() => settings.deezerLanguage =
|
||||||
ContentLanguage.all[i].code);
|
ContentLanguage.all[i].code);
|
||||||
await settings.save();
|
await settings.save();
|
||||||
|
deezerAPI.updateHeaders();
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
)),
|
)),
|
||||||
|
@ -822,6 +880,7 @@ class _DeezerSettingsState extends State<DeezerSettings> {
|
||||||
onValuePicked: (Country country) {
|
onValuePicked: (Country country) {
|
||||||
setState(
|
setState(
|
||||||
() => settings.deezerCountry = country.isoCode);
|
() => settings.deezerCountry = country.isoCode);
|
||||||
|
deezerAPI.updateHeaders();
|
||||||
settings.save();
|
settings.save();
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
|
@ -1744,25 +1803,26 @@ class _CreditsScreenState extends State<CreditsScreen> {
|
||||||
onTap: () => launchUrl(Uri.parse('https://discord.gg/qwJpa3r4dQ')),
|
onTap: () => launchUrl(Uri.parse('https://discord.gg/qwJpa3r4dQ')),
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text('Repository'.i18n),
|
title: Text('${'Repository'.i18n} (unavailable)'),
|
||||||
subtitle: Text('Source code, report issues there.'.i18n),
|
subtitle: Text('Source code, report issues there.'.i18n),
|
||||||
leading: const Icon(Icons.code, color: Colors.green, size: 36.0),
|
leading: const Icon(Icons.code, color: Colors.green, size: 36.0),
|
||||||
onTap: () {
|
enabled: false,
|
||||||
launchUrl(Uri.parse('https://git.freezer.life/exttex/freezer'));
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
ListTile(
|
const ListTile(
|
||||||
title: const Text('Donate'),
|
enabled: false,
|
||||||
subtitle: const Text(
|
title: Text('Don\'t Donate'),
|
||||||
|
subtitle: Text(
|
||||||
'You should rather support your favorite artists, instead of this app!'),
|
'You should rather support your favorite artists, instead of this app!'),
|
||||||
leading:
|
leading: Icon(FontAwesome5.paypal, color: Colors.blue, size: 36.0),
|
||||||
const Icon(FontAwesome5.paypal, color: Colors.blue, size: 36.0),
|
|
||||||
onTap: () => launchUrl(Uri.parse('https://paypal.me/exttex')),
|
|
||||||
),
|
),
|
||||||
const FreezerDivider(),
|
const FreezerDivider(),
|
||||||
|
const ListTile(
|
||||||
|
title: Text('Pato05'),
|
||||||
|
subtitle: Text('Current Developer - best of all'),
|
||||||
|
),
|
||||||
const ListTile(
|
const ListTile(
|
||||||
title: Text('exttex'),
|
title: Text('exttex'),
|
||||||
subtitle: Text('Developer'),
|
subtitle: Text('Ex-Developer'),
|
||||||
),
|
),
|
||||||
const ListTile(
|
const ListTile(
|
||||||
title: Text('Bas Curtiz'),
|
title: Text('Bas Curtiz'),
|
||||||
|
@ -1787,7 +1847,7 @@ class _CreditsScreenState extends State<CreditsScreen> {
|
||||||
setState(() {
|
setState(() {
|
||||||
settings.primaryColor = const Color(0xff333333);
|
settings.primaryColor = const Color(0xff333333);
|
||||||
});
|
});
|
||||||
updateTheme();
|
updateTheme(context);
|
||||||
settings.save();
|
settings.save();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
|
@ -5,6 +5,7 @@ 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/audio_handler.dart';
|
import 'package:freezer/api/player/audio_handler.dart';
|
||||||
|
import 'package:freezer/main.dart';
|
||||||
import 'package:freezer/translations.i18n.dart';
|
import 'package:freezer/translations.i18n.dart';
|
||||||
|
|
||||||
import '../api/definitions.dart';
|
import '../api/definitions.dart';
|
||||||
|
@ -296,8 +297,8 @@ class ArtistHorizontalTile extends StatelessWidget {
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
),
|
),
|
||||||
leading: CircleAvatar(
|
leading: CircleAvatar(
|
||||||
backgroundImage:
|
backgroundImage: CachedNetworkImageProvider(artist!.picture!.thumb,
|
||||||
CachedNetworkImageProvider(artist!.picture!.thumb)),
|
cacheManager: cacheManager)),
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
onLongPress: onHold,
|
onLongPress: onHold,
|
||||||
trailing: trailing,
|
trailing: trailing,
|
||||||
|
@ -491,12 +492,27 @@ class _SmartTrackListTileState extends State<SmartTrackListTile> {
|
||||||
// children: [...covers.map((e) => CachedImage(url: e.thumb))],
|
// children: [...covers.map((e) => CachedImage(url: e.thumb))],
|
||||||
// );
|
// );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (widget.smartTrackList?.id == 'flow') {
|
||||||
|
return Material(
|
||||||
|
elevation: 2.0,
|
||||||
|
shape: const CircleBorder(),
|
||||||
|
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||||
|
child: CachedImage(
|
||||||
|
width: widget.size,
|
||||||
|
height: widget.size,
|
||||||
|
url: covers[0].size(232, 232, id: 'none', num: 80, format: 'png'),
|
||||||
|
rounded: false,
|
||||||
|
circular: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
return CachedImage(
|
return CachedImage(
|
||||||
width: widget.size,
|
width: widget.size,
|
||||||
height: widget.size,
|
height: widget.size,
|
||||||
url: covers[0].full,
|
url: covers[0].full,
|
||||||
rounded: widget.smartTrackList?.id != 'flow',
|
rounded: true,
|
||||||
circular: widget.smartTrackList?.id == 'flow',
|
circular: false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -534,6 +550,7 @@ class _SmartTrackListTileState extends State<SmartTrackListTile> {
|
||||||
color: Colors.white),
|
color: Colors.white),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (widget.smartTrackList?.id != 'flow')
|
||||||
Center(
|
Center(
|
||||||
child: SizedBox.square(
|
child: SizedBox.square(
|
||||||
dimension: 32.0,
|
dimension: 32.0,
|
||||||
|
@ -667,6 +684,7 @@ class ChannelTile extends StatelessWidget {
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: CachedNetworkImage(
|
child: CachedNetworkImage(
|
||||||
cacheKey: channel.logo!.md5,
|
cacheKey: channel.logo!.md5,
|
||||||
|
cacheManager: cacheManager,
|
||||||
height: 52.0,
|
height: 52.0,
|
||||||
imageUrl:
|
imageUrl:
|
||||||
channel.logo!.size(52, 0, num: 100, id: 'none', format: 'png')),
|
channel.logo!.size(52, 0, num: 100, id: 'none', format: 'png')),
|
||||||
|
@ -696,6 +714,7 @@ class ChannelTile extends StatelessWidget {
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
image: CachedNetworkImageProvider(
|
image: CachedNetworkImageProvider(
|
||||||
channel.picture!.size(134, 264),
|
channel.picture!.size(134, 264),
|
||||||
|
cacheManager: cacheManager,
|
||||||
cacheKey: channel.picture!.md5))),
|
cacheKey: channel.picture!.md5))),
|
||||||
child: Material(
|
child: Material(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
|
|
|
@ -6,12 +6,16 @@
|
||||||
|
|
||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
#include <desktop_webview_window/desktop_webview_window_plugin.h>
|
||||||
#include <dynamic_color/dynamic_color_plugin.h>
|
#include <dynamic_color/dynamic_color_plugin.h>
|
||||||
#include <isar_flutter_libs/isar_flutter_libs_plugin.h>
|
#include <isar_flutter_libs/isar_flutter_libs_plugin.h>
|
||||||
#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
|
#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
|
||||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||||
|
|
||||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||||
|
g_autoptr(FlPluginRegistrar) desktop_webview_window_registrar =
|
||||||
|
fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopWebviewWindowPlugin");
|
||||||
|
desktop_webview_window_plugin_register_with_registrar(desktop_webview_window_registrar);
|
||||||
g_autoptr(FlPluginRegistrar) dynamic_color_registrar =
|
g_autoptr(FlPluginRegistrar) dynamic_color_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "DynamicColorPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "DynamicColorPlugin");
|
||||||
dynamic_color_plugin_register_with_registrar(dynamic_color_registrar);
|
dynamic_color_plugin_register_with_registrar(dynamic_color_registrar);
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
#
|
#
|
||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
|
desktop_webview_window
|
||||||
dynamic_color
|
dynamic_color
|
||||||
isar_flutter_libs
|
isar_flutter_libs
|
||||||
media_kit_libs_linux
|
media_kit_libs_linux
|
||||||
|
|
|
@ -8,6 +8,7 @@ import Foundation
|
||||||
import audio_service
|
import audio_service
|
||||||
import audio_session
|
import audio_session
|
||||||
import connectivity_plus
|
import connectivity_plus
|
||||||
|
import desktop_webview_window
|
||||||
import dynamic_color
|
import dynamic_color
|
||||||
import flutter_local_notifications
|
import flutter_local_notifications
|
||||||
import isar_flutter_libs
|
import isar_flutter_libs
|
||||||
|
@ -23,6 +24,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
AudioServicePlugin.register(with: registry.registrar(forPlugin: "AudioServicePlugin"))
|
AudioServicePlugin.register(with: registry.registrar(forPlugin: "AudioServicePlugin"))
|
||||||
AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin"))
|
AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin"))
|
||||||
ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin"))
|
ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin"))
|
||||||
|
DesktopWebviewWindowPlugin.register(with: registry.registrar(forPlugin: "DesktopWebviewWindowPlugin"))
|
||||||
DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin"))
|
DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin"))
|
||||||
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
|
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
|
||||||
IsarFlutterLibsPlugin.register(with: registry.registrar(forPlugin: "IsarFlutterLibsPlugin"))
|
IsarFlutterLibsPlugin.register(with: registry.registrar(forPlugin: "IsarFlutterLibsPlugin"))
|
||||||
|
|
67
pubspec.lock
67
pubspec.lock
|
@ -322,14 +322,30 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.8"
|
version: "0.7.8"
|
||||||
|
desktop_webview_window:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: desktop_webview_window
|
||||||
|
sha256: "57cf20d81689d5cbb1adfd0017e96b669398a669d927906073b0e42fc64111c0"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.3"
|
||||||
dio:
|
dio:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: dio
|
name: dio
|
||||||
sha256: "417e2a6f9d83ab396ec38ff4ea5da6c254da71e4db765ad737a42af6930140b7"
|
sha256: "417e2a6f9d83ab396ec38ff4ea5da6c254da71e4db765ad737a42af6930140b7"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.3.3"
|
version: "5.3.3"
|
||||||
|
dio_cookie_manager:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: dio_cookie_manager
|
||||||
|
sha256: e79498b0f632897ff0c28d6e8178b4bc6e9087412401f618c31fa0904ace050d
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.1"
|
||||||
disk_space_plus:
|
disk_space_plus:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -427,6 +443,22 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.0"
|
version: "1.1.0"
|
||||||
|
flex_color_picker:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flex_color_picker
|
||||||
|
sha256: f37476ab3e80dcaca94e428e159944d465dd16312fda9ff41e07e86f04bfa51c
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.3.0"
|
||||||
|
flex_seed_scheme:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flex_seed_scheme
|
||||||
|
sha256: "29c12aba221eb8a368a119685371381f8035011d18de5ba277ad11d7dfb8657f"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.4.0"
|
||||||
flutter:
|
flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
|
@ -472,6 +504,15 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.3.1"
|
version: "3.3.1"
|
||||||
|
flutter_cache_manager_hive:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
path: "."
|
||||||
|
ref: HEAD
|
||||||
|
resolved-ref: f7bb0375770844bfcd19547f9389df83811e60d8
|
||||||
|
url: "https://github.com/Pato05/flutter_cache_manager_hive.git"
|
||||||
|
source: git
|
||||||
|
version: "0.0.9"
|
||||||
flutter_displaymode:
|
flutter_displaymode:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -607,6 +648,22 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.1"
|
version: "2.3.1"
|
||||||
|
hashlib:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: hashlib
|
||||||
|
sha256: "71bf102329ddb8e50c8a995ee4645ae7f1728bb65e575c17196b4d8262121a96"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.12.0"
|
||||||
|
hashlib_codecs:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: hashlib_codecs
|
||||||
|
sha256: "49e2a471f74b15f1854263e58c2ac11f2b631b5b12c836f9708a35397d36d626"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.0"
|
||||||
hive:
|
hive:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -1016,6 +1073,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.1"
|
version: "2.2.1"
|
||||||
|
pedantic:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: pedantic
|
||||||
|
sha256: "67fc27ed9639506c856c840ccce7594d0bdcd91bc8d53d6e52359449a1d50602"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.11.1"
|
||||||
permission_handler:
|
permission_handler:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
|
@ -94,6 +94,13 @@ dependencies:
|
||||||
isar_flutter_libs: ^3.1.0+1
|
isar_flutter_libs: ^3.1.0+1
|
||||||
flutter_background_service: ^5.0.1
|
flutter_background_service: ^5.0.1
|
||||||
audio_service_mpris: ^0.1.0
|
audio_service_mpris: ^0.1.0
|
||||||
|
desktop_webview_window: ^0.2.3
|
||||||
|
dio: ^5.3.3
|
||||||
|
dio_cookie_manager: ^3.1.1
|
||||||
|
flutter_cache_manager_hive:
|
||||||
|
git: https://github.com/Pato05/flutter_cache_manager_hive.git
|
||||||
|
flex_color_picker: ^3.3.0
|
||||||
|
|
||||||
#deezcryptor:
|
#deezcryptor:
|
||||||
#path: deezcryptor/
|
#path: deezcryptor/
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
#include <connectivity_plus/connectivity_plus_windows_plugin.h>
|
#include <connectivity_plus/connectivity_plus_windows_plugin.h>
|
||||||
|
#include <desktop_webview_window/desktop_webview_window_plugin.h>
|
||||||
#include <dynamic_color/dynamic_color_plugin_c_api.h>
|
#include <dynamic_color/dynamic_color_plugin_c_api.h>
|
||||||
#include <isar_flutter_libs/isar_flutter_libs_plugin.h>
|
#include <isar_flutter_libs/isar_flutter_libs_plugin.h>
|
||||||
#include <media_kit_libs_windows_audio/media_kit_libs_windows_audio_plugin_c_api.h>
|
#include <media_kit_libs_windows_audio/media_kit_libs_windows_audio_plugin_c_api.h>
|
||||||
|
@ -17,6 +18,8 @@
|
||||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
ConnectivityPlusWindowsPluginRegisterWithRegistrar(
|
ConnectivityPlusWindowsPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
|
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
|
||||||
|
DesktopWebviewWindowPluginRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("DesktopWebviewWindowPlugin"));
|
||||||
DynamicColorPluginCApiRegisterWithRegistrar(
|
DynamicColorPluginCApiRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("DynamicColorPluginCApi"));
|
registry->GetRegistrarForPlugin("DynamicColorPluginCApi"));
|
||||||
IsarFlutterLibsPluginRegisterWithRegistrar(
|
IsarFlutterLibsPluginRegisterWithRegistrar(
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
connectivity_plus
|
connectivity_plus
|
||||||
|
desktop_webview_window
|
||||||
dynamic_color
|
dynamic_color
|
||||||
isar_flutter_libs
|
isar_flutter_libs
|
||||||
media_kit_libs_windows_audio
|
media_kit_libs_windows_audio
|
||||||
|
|
Loading…
Reference in a new issue