diff --git a/android/app/build.gradle b/android/app/build.gradle index c4564ae..4112a8f 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -80,7 +80,7 @@ android { dependencies { //implementation group: 'org', name: 'jaudiotagger', version: '2.0.3' 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: 'androidx.core', name: 'core', version: '1.6.0' } diff --git a/android/app/libs/extension-flac.aar b/android/app/libs/extension-flac.aar index 7620c97..32c52fe 100644 Binary files a/android/app/libs/extension-flac.aar and b/android/app/libs/extension-flac.aar differ diff --git a/lib/api/cache_provider.dart b/lib/api/cache_provider.dart deleted file mode 100644 index 23c8370..0000000 --- a/lib/api/cache_provider.dart +++ /dev/null @@ -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 _box; - bool _isOpen; - FreezerCacheInfoRepository(this.boxName, this.path); - - @override - Future exists() => Hive.boxExists(boxName, path: path); - - @override - Future open() async { - if (_isOpen) return true; - - _box = await Hive.openLazyBox(boxName, path: path); - _isOpen = true; - return true; - } - - @override - Future updateOrInsert(CacheObject cacheObject) { - if (cacheObject.id == null) { - return insert(cacheObject); - } else { - return update(cacheObject); - } - } - - @override - Future insert(CacheObject cacheObject, - {bool setTouchedToNow = true}) { - final id = await _box.add(cacheObject); - } - - /// Gets a [CacheObject] by [key] - @override - Future get(String key); - - /// Deletes a cache object by [id] - @override - Future delete(int id); - - /// Deletes items with [ids] from the repository - @override - Future deleteAll(Iterable ids); - - /// Updates an existing [cacheObject] - @override - Future update(CacheObject cacheObject, {bool setTouchedToNow = true}); - - /// Gets the list of all objects in the cache - @override - Future> getAllObjects(); - - /// Gets the list of [CacheObject] that can be removed if the repository is over capacity. - /// - /// The exact implementation is up to the repository, but implementations should - /// return a preferred list of items. For example, the least recently accessed - @override - Future> getObjectsOverCapacity(int capacity); - - /// Returns a list of [CacheObject] that are older than [maxAge] - @override - Future> getOldObjects(Duration maxAge); - - /// Close the connection to the repository. If this is the last connection - /// to the repository it will return true and the repository is truly - /// closed. If there are still open connections it will return false; - @override - Future close(); - - /// Deletes the cache data file including all cache data. - @override - Future deleteDataFile(); -} - -class CacheObjectAdapter extends TypeAdapter { - @override - // TODO: implement typeId - int get typeId => throw UnimplementedError(); -} \ No newline at end of file diff --git a/lib/api/deezer.dart b/lib/api/deezer.dart index 742f2d9..daa7850 100644 --- a/lib/api/deezer.dart +++ b/lib/api/deezer.dart @@ -1,26 +1,55 @@ import 'dart:io'; +import 'package:cookie_jar/cookie_jar.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/definitions.dart'; import 'package:freezer/api/spotify.dart'; import 'package:freezer/settings.dart'; import 'package:http/http.dart' as http; import 'package:logging/logging.dart'; -import 'package:path/path.dart'; import 'dart:convert'; import 'dart:async'; -import 'package:path_provider/path_provider.dart'; - final deezerAPI = DeezerAPI(); class DeezerAPI { - static final _logger = Logger('DeezerAPI'); - DeezerAPI({this.arl}); + // from deemix: https://gitlab.com/RemixDev/deemix-js/-/blob/main/deemix/utils/deezer.js?ref_type=heads#L6 + 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.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? userId; String? userName; @@ -30,12 +59,16 @@ class DeezerAPI { late bool canStreamLossless; late bool canStreamHQ; + final cookieJar = DefaultCookieJar(); + late final dio = + Dio(BaseOptions(headers: headers, responseType: ResponseType.json)) + ..interceptors.add(CookieManager(cookieJar)); + Future? _authorizing; //Get headers Map get headers => { - "User-Agent": - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36", + "User-Agent": userAgent, "Content-Language": '${settings.deezerLanguage}-${settings.deezerCountry}', "Cache-Control": "max-age=0", @@ -44,77 +77,102 @@ class DeezerAPI { "Accept-Language": "${settings.deezerLanguage}-${settings.deezerCountry},${settings.deezerLanguage};q=0.9,en-US;q=0.8,en;q=0.7", "Connection": "keep-alive", - "Cookie": "arl=$arl${(sid == null) ? '' : '; sid=$sid'}" }; + Future logout() async { + // delete all cookies + await cookieJar.deleteAll(); + updateHeaders(); + } + + void updateHeaders() { + dio.options.headers = headers; + } + //Call private API Future> callApi(String method, {Map? params, String? gatewayInput}) async { - //Generate URL - Uri uri = Uri.https('www.deezer.com', '/ajax/gw-light.php', { - 'api_version': '1.0', - 'api_token': token, - 'input': '3', - 'method': method, - //Used for homepage - if (gatewayInput != null) 'gateway_input': gatewayInput - }); //Post - http.Response res = - 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]; - } - } - } + final res = await dio.post('https://www.deezer.com/ajax/gw-light.php', + queryParameters: { + 'api_version': '1.0', + 'api_token': token, + 'input': '3', + 'method': method, + //Used for homepage + if (gatewayInput != null) 'gateway_input': gatewayInput + }, + data: jsonEncode(params)); + final body = res.data; + // 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') && await rawAuthorize()) { return callApi(method, params: params, gatewayInput: gatewayInput); } + return body; } Future callPublicApi(String path) async { - Uri uri = Uri.https('api.deezer.com', '/$path'); - http.Response res = await http.get(uri); - return jsonDecode(res.body); + final res = await dio.get('https://api.deezer.com/$path'); + return res.data; } //Wrapper so it can be globally awaited Future authorize() async => _authorizing ??= rawAuthorize(); - //Login with email - static Future getArlByEmail(String? email, String password) async { + //Login with email FROM DEEMIX-JS + Future getArlByEmail(String email, String password) async { //Get MD5 of password - Digest digest = md5.convert(utf8.encode(password)); - String md5password = '$digest'; + final md5Password = md5.convert(utf8.encode(password)).toString(); + final hash = md5 + .convert(utf8 + .encode([CLIENT_ID, email, md5Password, CLIENT_SECRET].join(''))) + .toString(); //Get access token - String url = - "https://tv.deezer.com/smarttv/8caf9315c1740316053348a24d25afc7/user_auth.php?login=$email&password=$md5password&device=panasonic&output=json"; - http.Response response = await http.get(Uri.parse(url)); - String? accessToken = jsonDecode(response.body)["access_token"]; - //Get SID - url = "https://api.deezer.com/platform/generic/track/42069"; - response = await http - .get(Uri.parse(url), headers: {"Authorization": "Bearer $accessToken"}); - String? sid; - for (String cookieHeader in response.headers['set-cookie']!.split(';')) { - if (cookieHeader.startsWith('sid=')) { - sid = cookieHeader.split('=')[1]; - } + // String url = + // "https://tv.deezer.com/smarttv/8caf9315c1740316053348a24d25afc7/user_auth.php?login=$email&password=$md5password&device=panasonic&output=json"; + // http.Response response = await http.get(Uri.parse(url)); + // String? accessToken = jsonDecode(response.body)["access_token"]; + final res = await dio.get('https://api.deezer.com/auth/token', + queryParameters: { + 'app_id': CLIENT_ID, + 'login': email, + 'password': md5Password, + 'hash': hash + }, + 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'); } - if (sid == null) return null; + + return getArlByAccessToken(accessToken); + } + + // FROM DEEMIX-JS + Future 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 - url = - "https://deezer.com/ajax/gw-light.php?api_version=1.0&api_token=null&input=3&method=user.getArl"; - response = await http.get(Uri.parse(url), headers: {"Cookie": "sid=$sid"}); - return jsonDecode(response.body)["results"]; + final arlRes = await dio.get("https://www.deezer.com/ajax/gw-light.php", + queryParameters: { + 'method': 'user.getArl', + '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 diff --git a/lib/api/deezer_audio_source.dart b/lib/api/deezer_audio_source.dart index 20d9576..12360f8 100644 --- a/lib/api/deezer_audio_source.dart +++ b/lib/api/deezer_audio_source.dart @@ -5,7 +5,6 @@ import 'dart:isolate'; import 'dart:typed_data'; import 'package:encrypt/encrypt.dart'; -import 'package:flutter/foundation.dart' as flutter; import 'package:freezer/api/deezer.dart'; import 'package:freezer/api/definitions.dart'; import 'package:freezer/settings.dart'; @@ -14,7 +13,6 @@ import 'package:crypto/crypto.dart'; import 'package:http/http.dart' as http; import 'package:dart_blowfish/dart_blowfish.dart'; import 'package:logging/logging.dart'; -import 'package:scrobblenaut/lastfm.dart'; typedef _IsolateMessage = ( Stream> source, @@ -26,7 +24,7 @@ typedef _IsolateMessage = ( // 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) class DeezerAudioSource extends StreamAudioSource { - final _logger = Logger("DeezerAudioSource"); + static final _logger = Logger("DeezerAudioSource"); late AudioQuality Function() _getQuality; late AudioQuality? _initialQuality; @@ -129,7 +127,7 @@ class DeezerAudioSource extends StreamAudioSource { final genUri = _generateTrackUri(); final req = await http.head(genUri, headers: { - 'User-Agent': deezerAPI.headers['User-Agent']!, + 'User-Agent': DeezerAPI.userAgent, 'Accept-Language': '*', 'Accept': '*/*' }); @@ -351,7 +349,7 @@ class DeezerAudioSource extends StreamAudioSource { final int deezerStart = start - (start % 2048); final req = http.Request('GET', _downloadUrl!) ..headers.addAll({ - 'User-Agent': deezerAPI.headers['User-Agent']!, + 'User-Agent': DeezerAPI.userAgent, 'Accept-Language': '*', 'Accept': '*/*', if (deezerStart > 0) diff --git a/lib/api/definitions.dart b/lib/api/definitions.dart index ba47670..160edb1 100644 --- a/lib/api/definitions.dart +++ b/lib/api/definitions.dart @@ -5,17 +5,16 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:audio_service/audio_service.dart'; -import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:freezer/api/cache.dart'; import 'package:freezer/page_routes/blur_slide.dart'; import 'package:freezer/page_routes/fade.dart'; import 'package:freezer/settings.dart'; import 'package:hive_flutter/hive_flutter.dart'; +import 'package:isar/isar.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:intl/intl.dart'; import 'package:just_audio/just_audio.dart'; import 'package:freezer/translations.i18n.dart'; -import 'package:isar/isar.dart'; import 'dart:convert'; import 'package:logging/logging.dart'; @@ -95,9 +94,6 @@ class Track extends DeezerMediaItem { //MediaItem Future toMediaItem() async { - DefaultCacheManager() - .getFileFromCache(albumArt!.full) - .then((i) => print('file: ${i?.file.uri}')); return MediaItem( title: title!, album: album!.title!, @@ -1069,7 +1065,7 @@ class HomePageItem { } Map toJson() { - String type = describeEnum(this.type!); + String type = describeEnum(this.type); return {'type': type, 'value': value.toJson()}; } } diff --git a/lib/api/download.dart b/lib/api/download.dart index fb83870..8007649 100644 --- a/lib/api/download.dart +++ b/lib/api/download.dart @@ -1,4 +1,5 @@ import 'package:flutter/foundation.dart'; +import 'package:freezer/main.dart'; import 'package:sqflite/sqflite.dart'; import 'package:disk_space_plus/disk_space_plus.dart'; import 'package:filesize/filesize.dart'; @@ -157,7 +158,9 @@ class DownloadManager { } Future addOfflineTrack(Track track, - {private = true, BuildContext? context, isSingleton = false}) async { + {bool private = true, + BuildContext? context, + bool isSingleton = false}) async { if (!isSupported) return false; //Permission if (!private && !(await checkPermission())) return false; @@ -183,8 +186,8 @@ class DownloadManager { await b.commit(); //Cache art - DefaultCacheManager().getSingleFile(track.albumArt!.thumb); - DefaultCacheManager().getSingleFile(track.albumArt!.full); + cacheManager.getSingleFile(track.albumArt!.thumb); + cacheManager.getSingleFile(track.albumArt!.full); } //Get path @@ -217,8 +220,8 @@ class DownloadManager { //Add to DB if (private) { //Cache art - DefaultCacheManager().getSingleFile(album.art!.thumb); - DefaultCacheManager().getSingleFile(album.art!.full); + cacheManager.getSingleFile(album.art!.thumb); + cacheManager.getSingleFile(album.art!.full); Batch b = db.batch(); b.insert('Albums', album.toSQL(off: true), @@ -268,8 +271,8 @@ class DownloadManager { for (Track? t in playlist.tracks!) { b = await _addTrackToDB(b, t!, false); //Cache art - DefaultCacheManager().getSingleFile(t.albumArt!.thumb); - DefaultCacheManager().getSingleFile(t.albumArt!.full); + cacheManager.getSingleFile(t.albumArt!.thumb); + cacheManager.getSingleFile(t.albumArt!.full); } await b.commit(); } diff --git a/lib/api/download_manager/database.dart b/lib/api/download_manager/database.dart index d6a74e8..d59d7a7 100644 --- a/lib/api/download_manager/database.dart +++ b/lib/api/download_manager/database.dart @@ -1,3 +1,32 @@ +import 'package:freezer/api/definitions.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 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, + }); +} diff --git a/lib/api/download_manager/download_manager.dart b/lib/api/download_manager/download_manager.dart index ae13c3b..abbc78e 100644 --- a/lib/api/download_manager/download_manager.dart +++ b/lib/api/download_manager/download_manager.dart @@ -1,16 +1,25 @@ import 'dart:io'; +import 'dart:isolate'; +import 'package:flutter/src/widgets/framework.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/service_interface.dart'; import 'package:freezer/translations.i18n.dart'; +import '../download.dart' as dl; class DownloadManager { - Future startService() { - if (Platform.isAndroid) { + //implements dl.DownloadManager { + SendPort? _sendPort; + Isolate? _isolate; + + Future configure() { + if (Platform.isAndroid || Platform.isIOS) { return FlutterBackgroundService().configure( iosConfiguration: IosConfiguration(), // fuck ios androidConfiguration: AndroidConfiguration( - onStart: _startService, + onStart: _startNative, isForegroundMode: false, autoStart: false, autoStartOnBoot: false, @@ -21,10 +30,48 @@ class DownloadManager { )); } - _startService(null); + // will run in foreground instead, in a separate isolate return Future.value(true); } - static void _startService(ServiceInstance? service) => + Future 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? args]) { + if (_sendPort != null) { + _sendPort!.send({ + 'method': method, + if (args != null) ...args, + }); + return; + } + FlutterBackgroundService().invoke(method, args); + } + + @override + Future 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(); } diff --git a/lib/api/download_manager/download_service.dart b/lib/api/download_manager/download_service.dart index da9cbf2..d89564c 100644 --- a/lib/api/download_manager/download_service.dart +++ b/lib/api/download_manager/download_service.dart @@ -1,10 +1,10 @@ -import 'package:flutter_background_service/flutter_background_service.dart'; +import 'package:freezer/api/download_manager/service_interface.dart'; class DownloadService { static const NOTIFICATION_ID = 6969; static const NOTIFICATION_CHANNEL_ID = "freezerdownloads"; - final ServiceInstance? service; + final ServiceInterface? service; DownloadService(this.service); void run() {} diff --git a/lib/api/download_manager/service_interface.dart b/lib/api/download_manager/service_interface.dart new file mode 100644 index 0000000..f2bf9ac --- /dev/null +++ b/lib/api/download_manager/service_interface.dart @@ -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?> on(String method) { + if (service != null) { + return service!.on(method); + } + + return receivePort! + .where((event) => event['method'] == method) + .map((event) => (event as Map?)?.cast()); + } +} diff --git a/lib/api/player/audio_handler.dart b/lib/api/player/audio_handler.dart index 5feddea..5b8c75c 100644 --- a/lib/api/player/audio_handler.dart +++ b/lib/api/player/audio_handler.dart @@ -65,7 +65,7 @@ class AudioPlayerTaskInitArguments { static Future loadSettings() async { final settings = await Settings.load(); - final deezerAPI = DeezerAPI(arl: settings.arl); + final deezerAPI = DeezerAPI()..arl = settings.arl; await deezerAPI.authorize(); return from(settings: settings, deezerAPI: deezerAPI); diff --git a/lib/api/player/player_helper.dart b/lib/api/player/player_helper.dart index eac20aa..be1e718 100644 --- a/lib/api/player/player_helper.dart +++ b/lib/api/player/player_helper.dart @@ -6,6 +6,7 @@ import 'package:freezer/api/cache.dart'; import 'package:freezer/api/deezer.dart'; import 'package:freezer/api/definitions.dart'; import 'package:freezer/api/player/audio_handler.dart'; +import 'package:freezer/main.dart'; import 'package:freezer/settings.dart'; import 'package:freezer/translations.i18n.dart'; import 'package:logging/logging.dart'; @@ -67,6 +68,7 @@ class PlayerHelper { androidNotificationIcon: 'drawable/ic_logo', preloadArtwork: false, ), + cacheManager: cacheManager, ); } diff --git a/lib/api/spotify.dart b/lib/api/spotify.dart index 6ccf8e4..c5c87c0 100644 --- a/lib/api/spotify.dart +++ b/lib/api/spotify.dart @@ -141,7 +141,7 @@ class SpotifyAlbum { } class SpotifyAPIWrapper { - late HttpServer _server; + HttpServer? _server; late SpotifyApi spotify; late User me; @@ -183,7 +183,8 @@ class SpotifyAPIWrapper { grant.getAuthorizationUrl(Uri.parse(redirectUri), scopes: scopes); launchUrl(authUri); //Wait for code - await for (HttpRequest request in _server) { + + await for (HttpRequest request in _server!) { //Exit window request.response.headers.set("Content-Type", "text/html; charset=UTF-8"); request.response.write( @@ -191,7 +192,7 @@ class SpotifyAPIWrapper { request.response.close(); //Get token if (request.uri.queryParameters["code"] != null) { - _server.close(); + _server!.close(); responseUri = request.uri.toString(); break; } @@ -221,6 +222,6 @@ class SpotifyAPIWrapper { //Cancel authorization void cancelAuthorize() { - _server.close(force: true); + _server?.close(force: true); } } diff --git a/lib/main.dart b/lib/main.dart index df9153a..74b76ea 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -7,9 +7,12 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.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_localizations/flutter_localizations.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/definitions.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/ui/downloads_screen.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/login_screen.dart'; import 'package:freezer/ui/player_screen.dart'; @@ -42,10 +46,10 @@ import 'settings.dart'; import 'ui/home_screen.dart'; import 'ui/player_bar.dart'; -late Function updateTheme; late Function logOut; GlobalKey mainNavigatorKey = GlobalKey(); GlobalKey navigatorKey = GlobalKey(); +late final CacheManager cacheManager; void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -87,7 +91,8 @@ void main() async { ..registerAdapter(UriAdapter()) ..registerAdapter(QueueSourceAdapter()) ..registerAdapter(HomePageAdapter()) - ..registerAdapter(NavigationRailAppearanceAdapter()); + ..registerAdapter(NavigationRailAppearanceAdapter()) + ..registerAdapter(HiveCacheObjectAdapter(typeId: 35)); Hive.init(await Paths.dataDirectory()); @@ -96,6 +101,10 @@ void main() async { settings.save(); downloadManager.init(); cache = await Cache.load(); + // photos + cacheManager = DefaultCacheManager(); + // cacheManager = HiveCacheManager( + // boxName: 'freezer-images', boxPath: await Paths.cacheDir()); // TODO: WA deezerAPI.favoritesPlaylistId = cache.favoritesPlaylistId; @@ -128,8 +137,6 @@ class _FreezerAppState extends State { @override void initState() { _initStateAsync(); - //Make update theme global - updateTheme = _updateTheme; super.initState(); } @@ -153,10 +160,6 @@ class _FreezerAppState extends State { super.dispose(); } - void _updateTheme() { - setState(() {}); - } - Locale? _locale() { if (settings.language == null || settings.language!.split('_').length < 2) { return null; @@ -167,51 +170,57 @@ class _FreezerAppState extends State { @override Widget build(BuildContext context) { - return ScreenUtilInit( - designSize: const Size(1080, 720), - builder: (context, child) => - DynamicColorBuilder(builder: (lightScheme, darkScheme) { - final lightTheme = settings.materialYouAccent - ? ThemeData(colorScheme: lightScheme, useMaterial3: true) - : settings.themeData; - final darkTheme = settings.materialYouAccent - ? ThemeData( - colorScheme: darkScheme, - useMaterial3: true, - brightness: Brightness.dark) - : null; - return MaterialApp( - title: 'Freezer', - shortcuts: { - ...WidgetsApp.defaultShortcuts, - LogicalKeySet(LogicalKeyboardKey.select): - const ActivateIntent(), // DPAD center key, for remote controls - }, - theme: lightTheme, - darkTheme: darkTheme, - localizationsDelegates: const [ - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - supportedLocales: supportedLocales, - home: WillPopScope( - onWillPop: () async { - if (navigatorKey.currentState!.canPop()) { - await navigatorKey.currentState!.maybePop(); - return false; - } - // await MoveToBackground.moveTaskToBack(); - return true; + return NotificationListener( + onNotification: (notification) { + setState(() => settings.themeData); + return true; + }, + child: ScreenUtilInit( + designSize: const Size(1080, 720), + builder: (context, child) => + DynamicColorBuilder(builder: (lightScheme, darkScheme) { + final lightTheme = settings.materialYouAccent + ? ThemeData(colorScheme: lightScheme, useMaterial3: true) + : settings.themeData; + final darkTheme = settings.materialYouAccent + ? ThemeData( + colorScheme: darkScheme, + useMaterial3: true, + brightness: Brightness.dark) + : null; + return MaterialApp( + title: 'Freezer', + shortcuts: { + ...WidgetsApp.defaultShortcuts, + LogicalKeySet(LogicalKeyboardKey.select): + const ActivateIntent(), // DPAD center key, for remote controls }, - child: I18n( - initialLocale: _locale(), - child: const LoginMainWrapper(), + theme: lightTheme, + darkTheme: darkTheme, + localizationsDelegates: const [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: supportedLocales, + home: WillPopScope( + onWillPop: () async { + if (navigatorKey.currentState!.canPop()) { + await navigatorKey.currentState!.maybePop(); + return false; + } + // await MoveToBackground.moveTaskToBack(); + return true; + }, + child: I18n( + initialLocale: _locale(), + child: const LoginMainWrapper(), + ), ), - ), - navigatorKey: mainNavigatorKey, - ); - }), + navigatorKey: mainNavigatorKey, + ); + }), + ), ); } } @@ -229,7 +238,7 @@ class _LoginMainWrapperState extends State { void initState() { if (settings.arl != null) { //Load token on background - deezerAPI.arl = settings.arl; + deezerAPI.arl = settings.arl!; settings.offlineMode = true; deezerAPI.authorize().then((b) async { if (b) setState(() => settings.offlineMode = false); @@ -252,8 +261,8 @@ class _LoginMainWrapperState extends State { setState(() { settings.arl = null; settings.offlineMode = false; - deezerAPI.arl = null; }); + await deezerAPI.logout(); await settings.save(); await Cache.wipe(); } @@ -320,7 +329,7 @@ class MainScreenState extends State final playerBarFocusNode = FocusNode(); final _fancyScaffoldKey = GlobalKey(); - late bool isDesktop; + bool isDesktop = false; @override void initState() { @@ -549,6 +558,7 @@ class MainScreenState extends State 6: '/library/history', 7: '/downloads', 8: '/settings', + 9: '/spotify-importer' }; final _navigationRailDestinations = [ NavigationRailDestination( @@ -593,6 +603,10 @@ class MainScreenState extends State selectedIcon: const Icon(Icons.settings), label: Text('Settings'.i18n), ), + NavigationRailDestination( + icon: const Icon(FontAwesome5.spotify), + label: Text('Importer'.i18n), + ), ]; void _onDestinationSelected(int s, @@ -646,19 +660,23 @@ class MainScreenState extends State focusNode: FocusNode(), onKey: _handleKey, child: LayoutBuilder(builder: (context, constraints) { - // check if we're running on a desktop platform - final isLandscape = constraints.maxWidth > constraints.maxHeight; - isDesktop = isLandscape && constraints.maxWidth > 1024; + // check if we're able to display the desktop layout + if (Platform.isLinux || Platform.isWindows || Platform.isMacOS) { + final isLandscape = constraints.maxWidth > constraints.maxHeight; + isDesktop = isLandscape && + constraints.maxWidth >= 1100 && + constraints.maxHeight >= 600; + } return FancyScaffold( key: _fancyScaffoldKey, navigationRail: _buildNavigationRail(isDesktop), bottomNavigationBar: buildBottomBar(isDesktop), - bottomPanel: PlayerBar( - focusNode: playerBarFocusNode, - onTap: () => - _fancyScaffoldKey.currentState!.dragController.fling(), - shouldHaveHero: false, - ), + bottomPanel: Builder( + builder: (context) => PlayerBar( + focusNode: playerBarFocusNode, + onTap: FancyScaffold.of(context)!.openPanel, + shouldHaveHero: false, + )), bottomPanelHeight: 68.0, expandedPanel: FocusScope( node: playerScreenFocusNode, @@ -699,6 +717,8 @@ class MainScreenState extends State '/search': (context) => const SearchScreen(), '/settings': (context) => const SettingsScreen(), '/downloads': (context) => const DownloadsScreen(), + '/spotify-importer': (context) => + const SpotifyImporterV2(), }, ))); })); @@ -826,6 +846,12 @@ class _ExtensibleNavigationRailState extends State { } } +class UpdateThemeNotification extends Notification {} + +void updateTheme(BuildContext context) { + UpdateThemeNotification().dispatch(context); +} + // class FreezerDrawer extends StatelessWidget { // final double? width; // const FreezerDrawer({super.key, this.width}); diff --git a/lib/settings.dart b/lib/settings.dart index 00f75bf..e0b4728 100644 --- a/lib/settings.dart +++ b/lib/settings.dart @@ -1,5 +1,3 @@ -import 'dart:math'; - import 'package:flutter/foundation.dart'; import 'package:freezer/api/definitions.dart'; import 'package:freezer/api/download.dart'; @@ -303,7 +301,7 @@ class Settings { static const deezerBg = Color(0xFF1F1A16); static const deezerBottom = Color(0xFF1b1714); - TextTheme? get textTheme => (font == 'Deezer') + TextTheme? get textTheme => (font == 'Deezer' || font == 'System') ? null : GoogleFonts.getTextTheme(font, isDark ? ThemeData.dark().textTheme : ThemeData.light().textTheme); @@ -311,68 +309,69 @@ class Settings { final _elevation1Black = Color.alphaBlend(Colors.white12, Colors.black); - late final Map _themeData = { - Themes.Light: ThemeData( - textTheme: textTheme, - fontFamily: fontFamily, - brightness: Brightness.light, - primaryColor: primaryColor, - colorScheme: ColorScheme.fromSeed(seedColor: primaryColor), - sliderTheme: _sliderTheme, - bottomAppBarTheme: const BottomAppBarTheme(color: Color(0xfff5f5f5)), - useMaterial3: true, - ), - Themes.Dark: ThemeData( - textTheme: textTheme, - fontFamily: fontFamily, - brightness: Brightness.dark, - primaryColor: primaryColor, - colorScheme: ColorScheme.fromSeed( - seedColor: primaryColor, - brightness: Brightness.dark, - ), - sliderTheme: _sliderTheme, - useMaterial3: true, - ), - Themes.Deezer: ThemeData( - textTheme: textTheme, - fontFamily: fontFamily, - brightness: Brightness.dark, - primaryColor: primaryColor, - colorScheme: ColorScheme.fromSeed( - primary: primaryColor, - seedColor: deezerBg, - brightness: Brightness.dark), - sliderTheme: _sliderTheme, - scaffoldBackgroundColor: deezerBg, - bottomAppBarTheme: const BottomAppBarTheme(color: deezerBottom), - dialogBackgroundColor: deezerBottom, - bottomSheetTheme: - const BottomSheetThemeData(backgroundColor: deezerBottom), - cardColor: deezerBg, - useMaterial3: true, - ), - Themes.Black: ThemeData( - textTheme: textTheme, - fontFamily: fontFamily, - brightness: Brightness.dark, - primaryColor: primaryColor, - colorScheme: ColorScheme.fromSeed( - seedColor: Colors.black, - primary: primaryColor, - background: Colors.black, - brightness: Brightness.dark), - scaffoldBackgroundColor: Colors.black, - navigationBarTheme: - const NavigationBarThemeData(backgroundColor: Colors.black), - bottomAppBarTheme: const BottomAppBarTheme(color: Colors.black), - dialogBackgroundColor: _elevation1Black, - sliderTheme: _sliderTheme, - bottomSheetTheme: BottomSheetThemeData(backgroundColor: _elevation1Black), - cardColor: _elevation1Black, - useMaterial3: true, - ) - }; + Map get _themeData => { + Themes.Light: ThemeData( + textTheme: textTheme, + fontFamily: fontFamily, + brightness: Brightness.light, + primaryColor: primaryColor, + colorScheme: ColorScheme.fromSeed(seedColor: primaryColor), + sliderTheme: _sliderTheme, + bottomAppBarTheme: const BottomAppBarTheme(color: Color(0xfff5f5f5)), + useMaterial3: true, + ), + Themes.Dark: ThemeData( + textTheme: textTheme, + fontFamily: fontFamily, + brightness: Brightness.dark, + primaryColor: primaryColor, + colorScheme: ColorScheme.fromSeed( + seedColor: primaryColor, + brightness: Brightness.dark, + ), + sliderTheme: _sliderTheme, + useMaterial3: true, + ), + Themes.Deezer: ThemeData( + textTheme: textTheme, + fontFamily: fontFamily, + brightness: Brightness.dark, + primaryColor: primaryColor, + colorScheme: ColorScheme.fromSeed( + primary: primaryColor, + seedColor: deezerBg, + brightness: Brightness.dark), + sliderTheme: _sliderTheme, + scaffoldBackgroundColor: deezerBg, + bottomAppBarTheme: const BottomAppBarTheme(color: deezerBottom), + dialogBackgroundColor: deezerBottom, + bottomSheetTheme: + const BottomSheetThemeData(backgroundColor: deezerBottom), + cardColor: deezerBg, + useMaterial3: true, + ), + Themes.Black: ThemeData( + textTheme: textTheme, + fontFamily: fontFamily, + brightness: Brightness.dark, + primaryColor: primaryColor, + colorScheme: ColorScheme.fromSeed( + seedColor: Colors.black, + primary: primaryColor, + background: Colors.black, + brightness: Brightness.dark), + scaffoldBackgroundColor: Colors.black, + navigationBarTheme: + const NavigationBarThemeData(backgroundColor: Colors.black), + bottomAppBarTheme: const BottomAppBarTheme(color: Colors.black), + dialogBackgroundColor: _elevation1Black, + sliderTheme: _sliderTheme, + bottomSheetTheme: + BottomSheetThemeData(backgroundColor: _elevation1Black), + cardColor: _elevation1Black, + useMaterial3: true, + ) + }; //JSON factory Settings.fromJson(Map json) => diff --git a/lib/ui/cached_image.dart b/lib/ui/cached_image.dart index bdf8f61..73413a4 100644 --- a/lib/ui/cached_image.dart +++ b/lib/ui/cached_image.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:freezer/main.dart'; import 'package:freezer/page_routes/fade.dart'; import 'package:palette_generator/palette_generator.dart'; import 'package:cached_network_image/cached_network_image.dart'; @@ -14,11 +15,12 @@ class ImagesDatabase { */ void saveImage(String url) { - CachedNetworkImageProvider(url); + CachedNetworkImageProvider(url, cacheManager: cacheManager); } Future getPaletteGenerator(String url) { - return PaletteGenerator.fromImageProvider(CachedNetworkImageProvider(url)); + return PaletteGenerator.fromImageProvider( + CachedNetworkImageProvider(url, cacheManager: cacheManager)); } Future getPrimaryColor(String url) async { @@ -72,6 +74,7 @@ class _CachedImageState extends State { imageUrl: widget.url, width: widget.width, height: widget.height, + cacheManager: cacheManager, placeholder: (context, url) { if (widget.fullThumb) { return Image.asset( @@ -202,7 +205,8 @@ class _ZoomableImageRouteState extends State { } }, child: PhotoView( - imageProvider: CachedNetworkImageProvider(widget.imageUrl), + imageProvider: CachedNetworkImageProvider(widget.imageUrl, + cacheManager: cacheManager), maxScale: 8.0, minScale: 0.2, controller: controller, diff --git a/lib/ui/details_screens.dart b/lib/ui/details_screens.dart index b6c75bf..432e16e 100644 --- a/lib/ui/details_screens.dart +++ b/lib/ui/details_screens.dart @@ -3,7 +3,6 @@ import 'dart:math'; import 'package:draggable_scrollbar/draggable_scrollbar.dart'; import 'package:flutter/material.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/deezer.dart'; diff --git a/lib/ui/fancy_scaffold.dart b/lib/ui/fancy_scaffold.dart index d6fa8f9..6c2bdc4 100644 --- a/lib/ui/fancy_scaffold.dart +++ b/lib/ui/fancy_scaffold.dart @@ -37,6 +37,14 @@ class FancyScaffoldState extends State final statusNotifier = ValueNotifier(AnimationStatus.dismissed); + void openPanel() { + dragController.fling(velocity: 1.0); + } + + void closePanel() { + dragController.fling(velocity: -1.0); + } + @override void initState() { dragController = AnimationController( diff --git a/lib/ui/importer_screen.dart b/lib/ui/importer_screen.dart index 64fcd17..0634c21 100644 --- a/lib/ui/importer_screen.dart +++ b/lib/ui/importer_screen.dart @@ -13,6 +13,8 @@ import 'package:url_launcher/url_launcher.dart'; import 'dart:async'; +import 'package:url_launcher/url_launcher_string.dart'; + class SpotifyImporterV1 extends StatefulWidget { const SpotifyImporterV1({super.key}); @@ -74,6 +76,14 @@ class _SpotifyImporterV1State extends State { color: Colors.deepOrangeAccent, ), ), + const ListTile( + title: Text('It\'s broken.'), + subtitle: Text('Use importer V2'), + leading: Icon( + Icons.warning, + color: Colors.deepOrangeAccent, + ), + ), const FreezerDivider(), Container( height: 16.0, @@ -410,7 +420,7 @@ class _SpotifyImporterV2State extends State { child: ElevatedButton( child: Text("Open in Browser".i18n), onPressed: () { - launchUrl(Uri.parse("https://developer.spotify.com/dashboard")); + launchUrlString("https://developer.spotify.com/dashboard"); }, ), ), @@ -676,8 +686,9 @@ class _SpotifyImporterV2MainState extends State { return ListTile( title: Text(p.name!, maxLines: 1), subtitle: Text(p.owner!.displayName!, maxLines: 1), - leading: Image.network(p.images!.first.url ?? - "http://cdn-images.deezer.com/images/cover//256x256-000000-80-0-0.jpg"), + leading: Image.network((p.images?.isEmpty ?? true) + ? "http://cdn-images.deezer.com/images/cover//256x256-000000-80-0-0.jpg" + : p.images!.first.url!), onTap: () { _startImport(p.name, "", p.id); }, diff --git a/lib/ui/library.dart b/lib/ui/library.dart index 15ed4bd..0d00751 100644 --- a/lib/ui/library.dart +++ b/lib/ui/library.dart @@ -129,40 +129,42 @@ class LibraryScreen extends StatelessWidget { return; } - //Pick importer dialog - showDialog( - context: context, - builder: (context) => SimpleDialog( - title: Text('Importer'.i18n), - children: [ - ListTile( - leading: const Icon(FontAwesome5.spotify), - title: Text('Spotify v1'.i18n), - subtitle: Text( - 'Import Spotify playlists up to 100 tracks without any login.' - .i18n), - onTap: () { - Navigator.of(context).pop(); - Navigator.of(context).pushRoute( - builder: (context) => - const SpotifyImporterV1()); - }, - ), - ListTile( - leading: const Icon(FontAwesome5.spotify), - title: Text('Spotify v2'.i18n), - subtitle: Text( - 'Import any Spotify playlist, import from own Spotify library. Requires free account.' - .i18n), - onTap: () { - Navigator.of(context).pop(); - Navigator.of(context).pushRoute( - builder: (context) => - const SpotifyImporterV2()); - }, - ) - ], - )); + Navigator.of(context).pushNamed('/spotify-importer'); + + //Pick importer dialog (removed as ImporterV1 is broken) + // showDialog( + // context: context, + // builder: (context) => SimpleDialog( + // title: Text('Importer'.i18n), + // children: [ + // ListTile( + // leading: const Icon(FontAwesome5.spotify), + // title: Text('Spotify v1'.i18n), + // subtitle: Text( + // 'Import Spotify playlists up to 100 tracks without any login.' + // .i18n), + // onTap: () { + // Navigator.of(context).pop(); + // Navigator.of(context).pushRoute( + // builder: (context) => + // const SpotifyImporterV1()); + // }, + // ), + // ListTile( + // leading: const Icon(FontAwesome5.spotify), + // title: Text('Spotify v2'.i18n), + // subtitle: Text( + // 'Import any Spotify playlist, import from own Spotify library. Requires free account.' + // .i18n), + // onTap: () { + // Navigator.of(context).pop(); + // Navigator.of(context).pushRoute( + // builder: (context) => + // const SpotifyImporterV2()); + // }, + // ) + // ], + // )); }, ), if (DownloadManager.isSupported) diff --git a/lib/ui/login_screen.dart b/lib/ui/login_screen.dart index 7a41f14..df12ab6 100644 --- a/lib/ui/login_screen.dart +++ b/lib/ui/login_screen.dart @@ -1,8 +1,13 @@ +import 'dart:io'; + +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:freezer/api/deezer.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.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 '../api/definitions.dart'; @@ -143,6 +148,78 @@ class _LoginWidgetState extends State { _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 Widget build(BuildContext context) { //If arl non null, show loading @@ -211,12 +288,8 @@ class _LoginWidgetState extends State { }, ), OutlinedButton( + onPressed: _loginBrowser, child: Text('Login using browser'.i18n), - onPressed: () { - Navigator.of(context).pushRoute( - builder: (context) => - LoginBrowser(_update)); - }, ), OutlinedButton( child: Text('Login using token'.i18n), @@ -266,8 +339,7 @@ class _LoginWidgetState extends State { child: OutlinedButton( child: Text('Open in browser'.i18n), onPressed: () { - InAppBrowser.openWithSystemBrowser( - url: Uri.parse('https://deezer.com/register')); + launchUrlString('https://deezer.com/register'); }, ), ), @@ -292,6 +364,31 @@ class _LoginWidgetState extends State { } } +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 { final Function updateParent; const LoginBrowser(this.updateParent, {super.key}); @@ -340,8 +437,8 @@ class EmailLogin extends StatefulWidget { } class _EmailLoginState extends State { - String? _email; - String? _password; + final _emailController = TextEditingController(); + final _passwordController = TextEditingController(); bool _loading = false; Future _login() async { @@ -350,7 +447,8 @@ class _EmailLoginState extends State { String? arl; String? exception; try { - arl = await DeezerAPI.getArlByEmail(_email, _password!); + arl = await deezerAPI.getArlByEmail( + _emailController.text, _passwordController.text); } catch (e, st) { exception = e.toString(); print(e); @@ -393,17 +491,15 @@ class _EmailLoginState extends State { children: _loading ? [const CircularProgressIndicator()] : [ - TextField( + TextFormField( decoration: InputDecoration(labelText: 'Email'.i18n), - onChanged: (s) => _email = s, + controller: _emailController, ), - Container( - height: 8.0, - ), - TextField( + const SizedBox(height: 8.0), + TextFormField( obscureText: true, decoration: InputDecoration(labelText: "Password".i18n), - onChanged: (s) => _password = s, + controller: _passwordController, ) ], ), @@ -412,7 +508,8 @@ class _EmailLoginState extends State { TextButton( child: const Text('Login'), onPressed: () async { - if (_email != null && _password != null) { + if (_emailController.text.isNotEmpty && + _passwordController.text.isNotEmpty) { await _login(); } else { ScaffoldMessenger.of(context) diff --git a/lib/ui/menu.dart b/lib/ui/menu.dart index 76a30ed..ebe5ec7 100644 --- a/lib/ui/menu.dart +++ b/lib/ui/menu.dart @@ -106,8 +106,9 @@ class MenuSheetOption { } class MenuSheet { - BuildContext context; - Function? navigateCallback; + final BuildContext context; + final Function? navigateCallback; + bool _contextMenuOpen = false; MenuSheet(this.context, {this.navigateCallback}); @@ -115,23 +116,28 @@ class MenuSheet { {required TapDownDetails details}) { final overlay = Overlay.of(context).context.findRenderObject() as RenderBox; final actualPosition = overlay.globalToLocal(details.globalPosition); + _contextMenuOpen = true; showMenu( - elevation: 4.0, - context: context, - constraints: const BoxConstraints(maxWidth: 300.0), - position: - RelativeRect.fromSize(actualPosition & Size.zero, overlay.size), - items: options - .map((option) => PopupMenuItem( - onTap: option.onTap, - child: option.icon == null - ? option.label - : Row(mainAxisSize: MainAxisSize.min, children: [ - option.icon!, - const SizedBox(width: 8.0), - Flexible(child: option.label), - ]))) - .toList(growable: false)); + clipBehavior: Clip.antiAlias, + elevation: 4.0, + context: context, + constraints: const BoxConstraints(maxWidth: 300.0), + position: + RelativeRect.fromSize(actualPosition & Size.zero, overlay.size), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(28.0))), + items: options + .map((option) => PopupMenuItem( + onTap: option.onTap, + child: option.icon == null + ? option.label + : Row(mainAxisSize: MainAxisSize.min, children: [ + option.icon!, + const SizedBox(width: 8.0), + Flexible(child: option.label), + ]))) + .toList(growable: false)) + .then((_) => _contextMenuOpen = false); } //=================== @@ -162,7 +168,10 @@ class MenuSheet { .map((option) => ListTile( title: option.label, leading: option.icon, - onTap: option.onTap, + onTap: () { + option.onTap.call(); + Navigator.pop(context); + }, )) .toList(growable: false)), ), @@ -212,7 +221,10 @@ class MenuSheet { .map((option) => ListTile( title: option.label, leading: option.icon, - onTap: option.onTap, + onTap: () { + option.onTap.call(); + Navigator.pop(context); + }, )) .toList(growable: false))), ], @@ -259,14 +271,12 @@ class MenuSheet { icon: const Icon(Icons.playlist_play), onTap: () async { //-1 = next await audioHandler.insertQueueItem(-1, await t.toMediaItem()); - _close(); }); MenuSheetOption addToQueue(Track t) => MenuSheetOption(Text('Add to queue'.i18n), icon: const Icon(Icons.playlist_add), onTap: () async { await audioHandler.addQueueItem(await t.toMediaItem()); - _close(); }); MenuSheetOption addTrackFavorite(Track t) => @@ -281,8 +291,6 @@ class MenuSheet { ScaffoldMessenger.of(context).snack('Added to library'.i18n); //Add to cache cache.libraryTracks.add(t.id); - - _close(); }); MenuSheetOption downloadTrack(Track t) => MenuSheetOption( @@ -292,7 +300,6 @@ class MenuSheet { if (await downloadManager.addOfflineTrack(t, private: false, context: context, isSingleton: true) != false) showDownloadStartedToast(); - _close(); }, ); @@ -316,7 +323,6 @@ class MenuSheet { .snack("${"Track added to".i18n} ${p.title}"); }); }); - _close(); }, ); @@ -327,7 +333,6 @@ class MenuSheet { await deezerAPI.removeFromPlaylist(t.id, p!.id); ScaffoldMessenger.of(context) .snack('${'Track removed from'.i18n} ${p.title}'); - _close(); }, ); @@ -346,7 +351,6 @@ class MenuSheet { ScaffoldMessenger.of(context) .snack('Track removed from library'.i18n); if (onUpdate != null) onUpdate(); - _close(); }, ); @@ -359,7 +363,6 @@ class MenuSheet { ), icon: const Icon(Icons.recent_actors), onTap: () { - _close(); navigatorKey.currentState! .push(MaterialPageRoute(builder: (context) => ArtistDetails(a))); @@ -377,7 +380,6 @@ class MenuSheet { ), icon: const Icon(Icons.album), onTap: () { - _close(); navigatorKey.currentState! .push(MaterialPageRoute(builder: (context) => AlbumDetails(a))); @@ -392,7 +394,6 @@ class MenuSheet { icon: const Icon(Icons.online_prediction), onTap: () async { playerHelper.playMix(track.id, track.title!); - _close(); }, ); @@ -412,7 +413,6 @@ class MenuSheet { await downloadManager.addOfflineTrack(track, private: true, context: context); } - _close(); }); //=================== @@ -442,7 +442,6 @@ class MenuSheet { MenuSheetOption downloadAlbum(Album a) => MenuSheetOption(Text('Download'.i18n), icon: const Icon(Icons.file_download), onTap: () async { - _close(); if (await downloadManager.addOfflineAlbum(a, private: false, context: context) != false) showDownloadStartedToast(); @@ -454,7 +453,7 @@ class MenuSheet { onTap: () async { await deezerAPI.addFavoriteAlbum(a.id); await downloadManager.addOfflineAlbum(a, private: true); - _close(); + showDownloadStartedToast(); }, ); @@ -465,7 +464,6 @@ class MenuSheet { onTap: () async { await deezerAPI.addFavoriteAlbum(a.id); ScaffoldMessenger.of(context).snack('Added to library'.i18n); - _close(); }, ); @@ -478,7 +476,6 @@ class MenuSheet { await downloadManager.removeOfflineAlbum(a.id); ScaffoldMessenger.of(context).snack('Album removed'.i18n); if (onRemove != null) onRemove(); - _close(); }, ); @@ -512,7 +509,6 @@ class MenuSheet { ScaffoldMessenger.of(context) .snack('Artist removed from library'.i18n); if (onRemove != null) onRemove(); - _close(); }, ); @@ -522,7 +518,6 @@ class MenuSheet { onTap: () async { await deezerAPI.addFavoriteArtist(a.id); ScaffoldMessenger.of(context).snack('Added to library'.i18n); - _close(); }, ); @@ -567,7 +562,6 @@ class MenuSheet { } downloadManager.removeOfflinePlaylist(p.id); if (onRemove != null) onRemove(); - _close(); }, ); @@ -577,7 +571,6 @@ class MenuSheet { onTap: () async { await deezerAPI.addPlaylist(p.id); ScaffoldMessenger.of(context).snack('Added playlist to library'.i18n); - _close(); }, ); @@ -588,7 +581,7 @@ class MenuSheet { //Add to library await deezerAPI.addPlaylist(p.id); downloadManager.addOfflinePlaylist(p, private: true); - _close(); + showDownloadStartedToast(); }, ); @@ -597,7 +590,6 @@ class MenuSheet { Text('Download playlist'.i18n), icon: const Icon(Icons.file_download), onTap: () async { - _close(); if (await downloadManager.addOfflinePlaylist(p, private: false, context: context) != false) showDownloadStartedToast(); @@ -612,7 +604,7 @@ class MenuSheet { await showDialog( context: context, builder: (context) => CreatePlaylistDialog(playlist: p)); - _close(); + if (onUpdate != null) onUpdate(); }, ); @@ -689,7 +681,6 @@ class MenuSheet { Text('Keep the screen on'.i18n), icon: const Icon(Icons.screen_lock_portrait), onTap: () async { - _close(); //Enable if (!cache.wakelock) { WakelockPlus.enable(); @@ -703,11 +694,6 @@ class MenuSheet { cache.wakelock = false; }, ); - - void _close() { - FancyScaffold.of(context)!.dragController.fling(velocity: -1.0); - // Navigator.of(context).pop(); - } } class SleepTimerDialog extends StatefulWidget { diff --git a/lib/ui/player_screen.dart b/lib/ui/player_screen.dart index d9e5843..3d3a2d2 100644 --- a/lib/ui/player_screen.dart +++ b/lib/ui/player_screen.dart @@ -50,7 +50,8 @@ class BackgroundProvider extends ChangeNotifier { !settings.enableFilledPlayButton && !settings.playerAlbumArtDropShadow) return; final imageProvider = CachedNetworkImageProvider( - mediaItem.extras!['thumb'] ?? mediaItem.artUri.toString()); + mediaItem.extras!['thumb'] ?? mediaItem.artUri.toString(), + cacheManager: cacheManager); //Run in isolate _palette = await PaletteGenerator.fromImageProvider(imageProvider); _dominantColor = _palette!.dominantColor!.color; @@ -285,24 +286,27 @@ class PlayerScreenVertical extends StatelessWidget { mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Padding( + Padding( padding: EdgeInsets.symmetric(horizontal: 4.0), - child: PlayerScreenTopRow(), + child: PlayerScreenTopRow( + textSize: 20.spMax, + iconSize: 24.spMax, + ), ), - const BigAlbumArt(), + Flexible(child: const BigAlbumArt()), Padding( 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: const EdgeInsets.symmetric(horizontal: 8.0), - child: PlaybackControls(86.sp), + child: PlaybackControls(40.spMax), ), Padding( padding: 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 .labelLarge! .copyWith(fontSize: 18.0)), - const Expanded( + Expanded( child: SizedBox.expand( child: Material( type: MaterialType.transparency, child: TabBarView(children: [ - QueueListWidget(), - LyricsWidget(), + QueueListWidget( + closePlayer: FancyScaffold.of(context)!.closePanel, + ), + const LyricsWidget(), ]), ), )), @@ -570,7 +576,8 @@ class PlayerMenuButton extends StatelessWidget { final currentMediaItem = audioHandler.mediaItem.value!; Track t = Track.fromMediaItem(currentMediaItem); MenuSheet m = MenuSheet(context, navigateCallback: () { - Navigator.of(context).pop(); + // close player + FancyScaffold.of(context)?.closePanel(); }); if (currentMediaItem.extras!['show'] == null) { m.defaultTrackMenu(t, options: [m.sleepTimer(), m.wakelock()]); @@ -952,8 +959,7 @@ class PlayerScreenTopRow extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.center, children: [ IconButton( - onPressed: () => - FancyScaffold.of(context)!.dragController.fling(velocity: -1.0), + onPressed: FancyScaffold.of(context)!.closePanel, icon: Icon( Icons.keyboard_arrow_down, semanticLabel: 'Close'.i18n, @@ -985,8 +991,10 @@ class PlayerScreenTopRow extends StatelessWidget { ), iconSize: size, splashRadius: size * 1.5, - onPressed: () => Navigator.of(context) - .pushRoute(builder: (context) => const QueueScreen()), + onPressed: () => Navigator.of(context).pushRoute( + builder: (ctx) => QueueScreen( + closePlayer: FancyScaffold.of(context)!.closePanel, + )), ) : SizedBox.square(dimension: size + 16.0), ], diff --git a/lib/ui/queue_screen.dart b/lib/ui/queue_screen.dart index 96c3fcc..b9a2c65 100644 --- a/lib/ui/queue_screen.dart +++ b/lib/ui/queue_screen.dart @@ -6,11 +6,13 @@ import 'package:flutter/services.dart'; import 'package:freezer/api/definitions.dart'; import 'package:freezer/api/player/audio_handler.dart'; import 'package:freezer/translations.i18n.dart'; +import 'package:freezer/ui/fancy_scaffold.dart'; import 'package:freezer/ui/menu.dart'; import 'package:freezer/ui/tiles.dart'; class QueueScreen extends StatelessWidget { - const QueueScreen({super.key}); + final VoidCallback closePlayer; + const QueueScreen({super.key, required this.closePlayer}); @override 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 { + final VoidCallback closePlayer; 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 State createState() => _QueueListWidgetState(); @@ -107,6 +117,13 @@ class _QueueListWidgetState extends State { @override Widget build(BuildContext context) { + final menuSheet = MenuSheet(context, navigateCallback: () { + if (!widget.isInsidePlayer) { + Navigator.pop(context); + } + + widget.closePlayer.call(); + }); return ReorderableListView.builder( buildDefaultDragHandles: false, scrollController: _scrollController, @@ -168,7 +185,7 @@ class _QueueListWidgetState extends State { } }); }, - onSecondary: (details) => MenuSheet(context).defaultTrackMenu( + onSecondary: (details) => menuSheet.defaultTrackMenu( Track.fromMediaItem(mediaItem), details: details), checkTrackOffline: false, diff --git a/lib/ui/settings_screen.dart b/lib/ui/settings_screen.dart index c33ceb1..c1c1995 100644 --- a/lib/ui/settings_screen.dart +++ b/lib/ui/settings_screen.dart @@ -1,7 +1,9 @@ import 'dart:io'; +import 'dart:math'; import 'package:country_pickers/country.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/material.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:fluttertoast/fluttertoast.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:permission_handler/permission_handler.dart'; import 'package:scrobblenaut/scrobblenaut.dart'; @@ -184,7 +187,7 @@ class _AppearanceSettingsState extends State { onPressed: () { settings.theme = Themes.Light; settings.save(); - updateTheme(); + updateTheme(context); Navigator.of(context).pop(); }, ), @@ -193,7 +196,7 @@ class _AppearanceSettingsState extends State { onPressed: () { settings.theme = Themes.Dark; settings.save(); - updateTheme(); + updateTheme(context); Navigator.of(context).pop(); }, ), @@ -202,7 +205,7 @@ class _AppearanceSettingsState extends State { onPressed: () { settings.theme = Themes.Black; settings.save(); - updateTheme(); + updateTheme(context); Navigator.of(context).pop(); }, ), @@ -211,7 +214,7 @@ class _AppearanceSettingsState extends State { onPressed: () { settings.theme = Themes.Deezer; settings.save(); - updateTheme(); + updateTheme(context); Navigator.of(context).pop(); }, ), @@ -229,7 +232,7 @@ class _AppearanceSettingsState extends State { settings.useSystemTheme = v; settings.save(); - updateTheme(); + updateTheme(context); }, secondary: const Icon(Icons.android)), SwitchListTile( @@ -239,7 +242,7 @@ class _AppearanceSettingsState extends State { settings.materialYouAccent = v; settings.save(); - updateTheme(); + updateTheme(context); }), ListTile( title: Text('Font'.i18n), @@ -382,42 +385,48 @@ class _AppearanceSettingsState extends State { child: CircleAvatar( backgroundColor: settings.primaryColor, )), - onTap: settings.materialYouAccent - ? null - : () { - 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(); - Navigator.of(context).pop(); - }, - ), - ), - ); - }); - }, + enabled: !settings.materialYouAccent, + onTap: () async { + final color = await showDialog( + context: context, builder: (context) => const _ColorPicker()); + if (color == null) return; + settings.primaryColor = color; + settings.save(); + updateTheme(context); + + //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( title: Text('Use album art primary color'.i18n), @@ -443,7 +452,9 @@ class _AppearanceSettingsState extends State { onPressed: () { settings.navigationRailAppearance = value; Navigator.pop(context); - settings.save().then((_) => updateTheme()); + settings + .save() + .then((_) => updateTheme(context)); })) .toList(growable: false), )), @@ -484,6 +495,49 @@ class _AppearanceSettingsState extends State { } } +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 { final Function callback; @@ -518,7 +572,7 @@ class _FontSelectorState extends State { Navigator.of(context).pop(); widget.callback(); //Global setState - updateTheme(); + updateTheme(context); }, child: Text('Apply'.i18n), ), @@ -549,8 +603,11 @@ class _FontSelectorState extends State { onChanged: (q) => setState(() => query = q), ), ), - SingleChildScrollView( - child: SizedBox( + SizedBox( + height: MediaQuery.of(context).size.height - 300.0, + width: 400.0, + child: Material( + type: MaterialType.transparency, child: ListView.builder( shrinkWrap: true, itemExtent: 56.0, @@ -802,6 +859,7 @@ class _DeezerSettingsState extends State { setState(() => settings.deezerLanguage = ContentLanguage.all[i].code); await settings.save(); + deezerAPI.updateHeaders(); Navigator.of(context).pop(); }, )), @@ -822,6 +880,7 @@ class _DeezerSettingsState extends State { onValuePicked: (Country country) { setState( () => settings.deezerCountry = country.isoCode); + deezerAPI.updateHeaders(); settings.save(); }, )); @@ -1744,25 +1803,26 @@ class _CreditsScreenState extends State { onTap: () => launchUrl(Uri.parse('https://discord.gg/qwJpa3r4dQ')), ), ListTile( - title: Text('Repository'.i18n), + title: Text('${'Repository'.i18n} (unavailable)'), subtitle: Text('Source code, report issues there.'.i18n), leading: const Icon(Icons.code, color: Colors.green, size: 36.0), - onTap: () { - launchUrl(Uri.parse('https://git.freezer.life/exttex/freezer')); - }, + enabled: false, ), - ListTile( - title: const Text('Donate'), - subtitle: const Text( + const ListTile( + enabled: false, + title: Text('Don\'t Donate'), + subtitle: Text( 'You should rather support your favorite artists, instead of this app!'), - leading: - const Icon(FontAwesome5.paypal, color: Colors.blue, size: 36.0), - onTap: () => launchUrl(Uri.parse('https://paypal.me/exttex')), + leading: Icon(FontAwesome5.paypal, color: Colors.blue, size: 36.0), ), const FreezerDivider(), + const ListTile( + title: Text('Pato05'), + subtitle: Text('Current Developer - best of all'), + ), const ListTile( title: Text('exttex'), - subtitle: Text('Developer'), + subtitle: Text('Ex-Developer'), ), const ListTile( title: Text('Bas Curtiz'), @@ -1787,7 +1847,7 @@ class _CreditsScreenState extends State { setState(() { settings.primaryColor = const Color(0xff333333); }); - updateTheme(); + updateTheme(context); settings.save(); }, ), diff --git a/lib/ui/tiles.dart b/lib/ui/tiles.dart index 5d811cb..c535c78 100644 --- a/lib/ui/tiles.dart +++ b/lib/ui/tiles.dart @@ -5,6 +5,7 @@ import 'package:fluttericon/octicons_icons.dart'; import 'package:freezer/api/deezer.dart'; import 'package:freezer/api/download.dart'; import 'package:freezer/api/player/audio_handler.dart'; +import 'package:freezer/main.dart'; import 'package:freezer/translations.i18n.dart'; import '../api/definitions.dart'; @@ -296,8 +297,8 @@ class ArtistHorizontalTile extends StatelessWidget { maxLines: 1, ), leading: CircleAvatar( - backgroundImage: - CachedNetworkImageProvider(artist!.picture!.thumb)), + backgroundImage: CachedNetworkImageProvider(artist!.picture!.thumb, + cacheManager: cacheManager)), onTap: onTap, onLongPress: onHold, trailing: trailing, @@ -491,12 +492,27 @@ class _SmartTrackListTileState extends State { // 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( width: widget.size, height: widget.size, url: covers[0].full, - rounded: widget.smartTrackList?.id != 'flow', - circular: widget.smartTrackList?.id == 'flow', + rounded: true, + circular: false, ); } @@ -534,32 +550,33 @@ class _SmartTrackListTileState extends State { color: Colors.white), ), ), - Center( - child: SizedBox.square( - dimension: 32.0, - child: DecoratedBox( - decoration: const BoxDecoration( - shape: BoxShape.circle, color: Colors.white), - child: Center( - child: ValueListenableBuilder( - valueListenable: _isLoading, - builder: (context, isLoading, _) { - if (isLoading) { - return const SizedBox.square( - dimension: 16.0, - child: CircularProgressIndicator( - color: Colors.black, - strokeWidth: 2.0, - )); - } - return const Icon( - Icons.play_arrow, - color: Colors.black, - size: 24.0, - ); - }), - ), - ))), + if (widget.smartTrackList?.id != 'flow') + Center( + child: SizedBox.square( + dimension: 32.0, + child: DecoratedBox( + decoration: const BoxDecoration( + shape: BoxShape.circle, color: Colors.white), + child: Center( + child: ValueListenableBuilder( + valueListenable: _isLoading, + builder: (context, isLoading, _) { + if (isLoading) { + return const SizedBox.square( + dimension: 16.0, + child: CircularProgressIndicator( + color: Colors.black, + strokeWidth: 2.0, + )); + } + return const Icon( + Icons.play_arrow, + color: Colors.black, + size: 24.0, + ); + }), + ), + ))), ], ), ), @@ -667,6 +684,7 @@ class ChannelTile extends StatelessWidget { padding: const EdgeInsets.all(8.0), child: CachedNetworkImage( cacheKey: channel.logo!.md5, + cacheManager: cacheManager, height: 52.0, imageUrl: channel.logo!.size(52, 0, num: 100, id: 'none', format: 'png')), @@ -696,6 +714,7 @@ class ChannelTile extends StatelessWidget { fit: BoxFit.cover, image: CachedNetworkImageProvider( channel.picture!.size(134, 264), + cacheManager: cacheManager, cacheKey: channel.picture!.md5))), child: Material( color: Colors.transparent, diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 606c5a6..f847d24 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,12 +6,16 @@ #include "generated_plugin_registrant.h" +#include #include #include #include #include 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 = fl_plugin_registry_get_registrar_for_plugin(registry, "DynamicColorPlugin"); dynamic_color_plugin_register_with_registrar(dynamic_color_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index f1b77ef..9f7f4e8 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + desktop_webview_window dynamic_color isar_flutter_libs media_kit_libs_linux diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index fcd3f4c..c9c340a 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -8,6 +8,7 @@ import Foundation import audio_service import audio_session import connectivity_plus +import desktop_webview_window import dynamic_color import flutter_local_notifications import isar_flutter_libs @@ -23,6 +24,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AudioServicePlugin.register(with: registry.registrar(forPlugin: "AudioServicePlugin")) AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin")) ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin")) + DesktopWebviewWindowPlugin.register(with: registry.registrar(forPlugin: "DesktopWebviewWindowPlugin")) DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin")) FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) IsarFlutterLibsPlugin.register(with: registry.registrar(forPlugin: "IsarFlutterLibsPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 00ea16b..44f27b8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -322,14 +322,30 @@ packages: url: "https://pub.dev" source: hosted 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: - dependency: transitive + dependency: "direct main" description: name: dio sha256: "417e2a6f9d83ab396ec38ff4ea5da6c254da71e4db765ad737a42af6930140b7" url: "https://pub.dev" source: hosted 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: dependency: "direct main" description: @@ -427,6 +443,22 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: "direct main" description: flutter @@ -472,6 +504,15 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: "direct main" description: @@ -607,6 +648,22 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: transitive description: @@ -1016,6 +1073,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.1" + pedantic: + dependency: transitive + description: + name: pedantic + sha256: "67fc27ed9639506c856c840ccce7594d0bdcd91bc8d53d6e52359449a1d50602" + url: "https://pub.dev" + source: hosted + version: "1.11.1" permission_handler: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 05467f4..fa5b1df 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -94,6 +94,13 @@ dependencies: isar_flutter_libs: ^3.1.0+1 flutter_background_service: ^5.0.1 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: #path: deezcryptor/ diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 1dc0921..7349940 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -7,6 +7,7 @@ #include "generated_plugin_registrant.h" #include +#include #include #include #include @@ -17,6 +18,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { ConnectivityPlusWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); + DesktopWebviewWindowPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("DesktopWebviewWindowPlugin")); DynamicColorPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("DynamicColorPluginCApi")); IsarFlutterLibsPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 79c3036..56ca898 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST connectivity_plus + desktop_webview_window dynamic_color isar_flutter_libs media_kit_libs_windows_audio