fix email login, fix spotify importer

migrate to dio instead of http, with unique cookiejar
This commit is contained in:
Pato05 2023-10-21 01:12:33 +02:00
parent 276d0ad4bf
commit 7a119d281c
No known key found for this signature in database
GPG key ID: F53CA394104BA0CB
34 changed files with 906 additions and 536 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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