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 {
//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'
}

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 '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, 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? 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<bool>? _authorizing;
//Get headers
Map<String, String> 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<void> logout() async {
// delete all cookies
await cookieJar.deleteAll();
updateHeaders();
}
void updateHeaders() {
dio.options.headers = headers;
}
//Call private API
Future<Map<dynamic, dynamic>> callApi(String method,
{Map<dynamic, dynamic>? 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<Map>('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<Map> 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<bool> authorize() async => _authorizing ??= rawAuthorize();
//Login with email
static Future<String?> getArlByEmail(String? email, String password) async {
//Login with email FROM DEEMIX-JS
Future<String> 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<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
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

View File

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

View File

@ -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<MediaItem> 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<String, dynamic> toJson() {
String type = describeEnum(this.type!);
String type = describeEnum(this.type);
return {'type': type, 'value': value.toJson()};
}
}

View File

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

View File

@ -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<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: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<bool> startService() {
if (Platform.isAndroid) {
//implements dl.DownloadManager {
SendPort? _sendPort;
Isolate? _isolate;
Future<bool> 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<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();
}

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 {
static const NOTIFICATION_ID = 6969;
static const NOTIFICATION_CHANNEL_ID = "freezerdownloads";
final ServiceInstance? service;
final ServiceInterface? service;
DownloadService(this.service);
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 {
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);

View File

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

View File

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

View File

@ -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<NavigatorState> mainNavigatorKey = GlobalKey<NavigatorState>();
GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
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<FreezerApp> {
@override
void initState() {
_initStateAsync();
//Make update theme global
updateTheme = _updateTheme;
super.initState();
}
@ -153,10 +160,6 @@ class _FreezerAppState extends State<FreezerApp> {
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<FreezerApp> {
@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: <ShortcutActivator, Intent>{
...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<UpdateThemeNotification>(
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: <ShortcutActivator, Intent>{
...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<LoginMainWrapper> {
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<LoginMainWrapper> {
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<MainScreen>
final playerBarFocusNode = FocusNode();
final _fancyScaffoldKey = GlobalKey<FancyScaffoldState>();
late bool isDesktop;
bool isDesktop = false;
@override
void initState() {
@ -549,6 +558,7 @@ class MainScreenState extends State<MainScreen>
6: '/library/history',
7: '/downloads',
8: '/settings',
9: '/spotify-importer'
};
final _navigationRailDestinations = <NavigationRailDestination>[
NavigationRailDestination(
@ -593,6 +603,10 @@ class MainScreenState extends State<MainScreen>
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<MainScreen>
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<MainScreen>
'/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<ExtensibleNavigationRail> {
}
}
class UpdateThemeNotification extends Notification {}
void updateTheme(BuildContext context) {
UpdateThemeNotification().dispatch(context);
}
// class FreezerDrawer extends StatelessWidget {
// final double? width;
// const FreezerDrawer({super.key, this.width});

View File

@ -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<Themes, ThemeData> _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<Themes, ThemeData> 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<String, dynamic> json) =>

View File

@ -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<PaletteGenerator> getPaletteGenerator(String url) {
return PaletteGenerator.fromImageProvider(CachedNetworkImageProvider(url));
return PaletteGenerator.fromImageProvider(
CachedNetworkImageProvider(url, cacheManager: cacheManager));
}
Future<Color> getPrimaryColor(String url) async {
@ -72,6 +74,7 @@ class _CachedImageState extends State<CachedImage> {
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<ZoomableImageRoute> {
}
},
child: PhotoView(
imageProvider: CachedNetworkImageProvider(widget.imageUrl),
imageProvider: CachedNetworkImageProvider(widget.imageUrl,
cacheManager: cacheManager),
maxScale: 8.0,
minScale: 0.2,
controller: controller,

View File

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

View File

@ -37,6 +37,14 @@ class FancyScaffoldState extends State<FancyScaffold>
final statusNotifier =
ValueNotifier<AnimationStatus>(AnimationStatus.dismissed);
void openPanel() {
dragController.fling(velocity: 1.0);
}
void closePanel() {
dragController.fling(velocity: -1.0);
}
@override
void initState() {
dragController = AnimationController(

View File

@ -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<SpotifyImporterV1> {
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<SpotifyImporterV2> {
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<SpotifyImporterV2Main> {
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);
},

View File

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

View File

@ -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<LoginWidget> {
_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<LoginWidget> {
},
),
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<LoginWidget> {
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<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 {
final Function updateParent;
const LoginBrowser(this.updateParent, {super.key});
@ -340,8 +437,8 @@ class EmailLogin extends StatefulWidget {
}
class _EmailLoginState extends State<EmailLogin> {
String? _email;
String? _password;
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
bool _loading = false;
Future _login() async {
@ -350,7 +447,8 @@ class _EmailLoginState extends State<EmailLogin> {
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<EmailLogin> {
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<EmailLogin> {
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)

View File

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

View File

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

View File

@ -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<QueueListWidget> createState() => _QueueListWidgetState();
@ -107,6 +117,13 @@ class _QueueListWidgetState extends State<QueueListWidget> {
@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<QueueListWidget> {
}
});
},
onSecondary: (details) => MenuSheet(context).defaultTrackMenu(
onSecondary: (details) => menuSheet.defaultTrackMenu(
Track.fromMediaItem(mediaItem),
details: details),
checkTrackOffline: false,

View File

@ -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<AppearanceSettings> {
onPressed: () {
settings.theme = Themes.Light;
settings.save();
updateTheme();
updateTheme(context);
Navigator.of(context).pop();
},
),
@ -193,7 +196,7 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
onPressed: () {
settings.theme = Themes.Dark;
settings.save();
updateTheme();
updateTheme(context);
Navigator.of(context).pop();
},
),
@ -202,7 +205,7 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
onPressed: () {
settings.theme = Themes.Black;
settings.save();
updateTheme();
updateTheme(context);
Navigator.of(context).pop();
},
),
@ -211,7 +214,7 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
onPressed: () {
settings.theme = Themes.Deezer;
settings.save();
updateTheme();
updateTheme(context);
Navigator.of(context).pop();
},
),
@ -229,7 +232,7 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
settings.useSystemTheme = v;
settings.save();
updateTheme();
updateTheme(context);
},
secondary: const Icon(Icons.android)),
SwitchListTile(
@ -239,7 +242,7 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
settings.materialYouAccent = v;
settings.save();
updateTheme();
updateTheme(context);
}),
ListTile(
title: Text('Font'.i18n),
@ -382,42 +385,48 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
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<Color>(
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<AppearanceSettings> {
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<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 {
final Function callback;
@ -518,7 +572,7 @@ class _FontSelectorState extends State<FontSelector> {
Navigator.of(context).pop();
widget.callback();
//Global setState
updateTheme();
updateTheme(context);
},
child: Text('Apply'.i18n),
),
@ -549,8 +603,11 @@ class _FontSelectorState extends State<FontSelector> {
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<DeezerSettings> {
setState(() => settings.deezerLanguage =
ContentLanguage.all[i].code);
await settings.save();
deezerAPI.updateHeaders();
Navigator.of(context).pop();
},
)),
@ -822,6 +880,7 @@ class _DeezerSettingsState extends State<DeezerSettings> {
onValuePicked: (Country country) {
setState(
() => settings.deezerCountry = country.isoCode);
deezerAPI.updateHeaders();
settings.save();
},
));
@ -1744,25 +1803,26 @@ class _CreditsScreenState extends State<CreditsScreen> {
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<CreditsScreen> {
setState(() {
settings.primaryColor = const Color(0xff333333);
});
updateTheme();
updateTheme(context);
settings.save();
},
),

View File

@ -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<SmartTrackListTile> {
// 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<SmartTrackListTile> {
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<bool>(
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<bool>(
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,

View File

@ -6,12 +6,16 @@
#include "generated_plugin_registrant.h"
#include <desktop_webview_window/desktop_webview_window_plugin.h>
#include <dynamic_color/dynamic_color_plugin.h>
#include <isar_flutter_libs/isar_flutter_libs_plugin.h>
#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
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);

View File

@ -3,6 +3,7 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
desktop_webview_window
dynamic_color
isar_flutter_libs
media_kit_libs_linux

View File

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

View File

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

View File

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

View File

@ -7,6 +7,7 @@
#include "generated_plugin_registrant.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 <isar_flutter_libs/isar_flutter_libs_plugin.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) {
ConnectivityPlusWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
DesktopWebviewWindowPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("DesktopWebviewWindowPlugin"));
DynamicColorPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("DynamicColorPluginCApi"));
IsarFlutterLibsPluginRegisterWithRegistrar(

View File

@ -4,6 +4,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
connectivity_plus
desktop_webview_window
dynamic_color
isar_flutter_libs
media_kit_libs_windows_audio