remove browser login for desktop
restore translations functionality make scrollViews handle mouse pointers like touch, so that pull to refresh functionality is available exit app if opening cache or settings fails (another instance running) remove draggable_scrollbar and use builtin widget instead fix email login better way to manage lyrics (less updates and lookups in the lyrics List) fix player_screen on mobile (too big -> just average :)) right click: use TapUp events instead desktop: show context menu on triple dots button also avoid showing connection error if the homepage is cached and available offline i'm probably forgetting something idk
This commit is contained in:
parent
7a119d281c
commit
2862c9ec05
|
@ -109,24 +109,24 @@ class Cache {
|
|||
static Future<Cache> load([int tryN = 0]) async {
|
||||
final box = await _box;
|
||||
|
||||
if (tryN > 2) {
|
||||
if (tryN > 1) {
|
||||
// recursion error, something's wrong
|
||||
throw Exception("can't load cache");
|
||||
}
|
||||
|
||||
//Doesn't exist, create new
|
||||
if (box.isEmpty) {
|
||||
final c = Cache();
|
||||
await c.save();
|
||||
return c;
|
||||
}
|
||||
try {
|
||||
return (await box.get(0))!;
|
||||
} catch (e) {
|
||||
// invalidate cache on error
|
||||
await box.clear();
|
||||
return await load(tryN + 1);
|
||||
if (box.isNotEmpty) {
|
||||
try {
|
||||
return (await box.get(0))!;
|
||||
} catch (e) {
|
||||
// invalidate cache on error
|
||||
await box.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// create cache if non-existent or error occurred
|
||||
final c = Cache();
|
||||
await c.save();
|
||||
return c;
|
||||
}
|
||||
|
||||
Future<void> save() async {
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:cookie_jar/cookie_jar.dart';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
|
@ -60,9 +58,11 @@ class DeezerAPI {
|
|||
late bool canStreamHQ;
|
||||
|
||||
final cookieJar = DefaultCookieJar();
|
||||
late final dio =
|
||||
Dio(BaseOptions(headers: headers, responseType: ResponseType.json))
|
||||
..interceptors.add(CookieManager(cookieJar));
|
||||
late final dio = Dio(BaseOptions(
|
||||
headers: headers,
|
||||
responseType: ResponseType.json,
|
||||
validateStatus: (status) => true))
|
||||
..interceptors.add(CookieManager(cookieJar));
|
||||
|
||||
Future<bool>? _authorizing;
|
||||
|
||||
|
@ -93,7 +93,7 @@ class DeezerAPI {
|
|||
Future<Map<dynamic, dynamic>> callApi(String method,
|
||||
{Map<dynamic, dynamic>? params, String? gatewayInput}) async {
|
||||
//Post
|
||||
final res = await dio.post<Map>('https://www.deezer.com/ajax/gw-light.php',
|
||||
final res = await dio.post('https://www.deezer.com/ajax/gw-light.php',
|
||||
queryParameters: {
|
||||
'api_version': '1.0',
|
||||
'api_token': token,
|
||||
|
@ -145,21 +145,25 @@ class DeezerAPI {
|
|||
},
|
||||
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');
|
||||
}
|
||||
|
||||
print(accessToken);
|
||||
|
||||
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',
|
||||
}));
|
||||
final res =
|
||||
await dio.get("https://api.deezer.com/platform/generic/track/3135556",
|
||||
options: Options(headers: {
|
||||
'Authorization': 'Bearer $accessToken',
|
||||
'User-Agent': userAgent,
|
||||
}));
|
||||
print(res.data);
|
||||
//Get ARL
|
||||
final arlRes = await dio.get("https://www.deezer.com/ajax/gw-light.php",
|
||||
queryParameters: {
|
||||
|
@ -170,7 +174,6 @@ class DeezerAPI {
|
|||
},
|
||||
options: Options(responseType: ResponseType.json));
|
||||
final arl = arlRes.data["results"];
|
||||
print(arlRes.data);
|
||||
if (arl == null) throw Exception('couldn\'t obtain ARL');
|
||||
return arl;
|
||||
}
|
||||
|
@ -178,19 +181,20 @@ class DeezerAPI {
|
|||
//Authorize, bool = success
|
||||
Future<bool> rawAuthorize({Function? onError}) async {
|
||||
try {
|
||||
Map<dynamic, dynamic> data = await callApi('deezer.getUserData');
|
||||
if (data['results']['USER']['USER_ID'] == 0) {
|
||||
final data = await callApi('deezer.getUserData');
|
||||
if (data['results']?['USER']?['USER_ID'] == null ||
|
||||
data['results']['USER']['USER_ID'] == 0) {
|
||||
return false;
|
||||
} else {
|
||||
token = data['results']['checkForm'];
|
||||
userId = data['results']['USER']['USER_ID'].toString();
|
||||
userName = data['results']['USER']['BLOG_NAME'];
|
||||
favoritesPlaylistId = data['results']['USER']['LOVEDTRACKS_ID'];
|
||||
canStreamHQ = data['results']['USER']['OPTIONS']['web_hq'] ||
|
||||
data['results']['USER']['OPTIONS']['mobile_hq'];
|
||||
canStreamLossless = data['results']['USER']['OPTIONS']
|
||||
['web_lossless'] ||
|
||||
data['results']['USER']['OPTIONS']['mobile_lossless'];
|
||||
canStreamHQ = data['results']['USER']['OPTIONS']['web_hq'] as bool ||
|
||||
data['results']['USER']['OPTIONS']['mobile_hq'] as bool;
|
||||
canStreamLossless =
|
||||
data['results']['USER']['OPTIONS']['web_lossless'] as bool ||
|
||||
data['results']['USER']['OPTIONS']['mobile_lossless'] as bool;
|
||||
licenseToken =
|
||||
data['results']['USER']['OPTIONS']['license_token'] as String;
|
||||
|
||||
|
@ -531,18 +535,39 @@ class DeezerAPI {
|
|||
return HomePage.fromPrivateJson(data['results']);
|
||||
}
|
||||
|
||||
//Log song listen to deezer
|
||||
Future logListen(String trackId,
|
||||
{int seek = 0, int pause = 0, int sync = 1, int? timestamp}) async {
|
||||
/// Log song listen to deezer
|
||||
Future logListen(
|
||||
String trackId, {
|
||||
/// Amount of times the track's been seeked
|
||||
int seek = 0,
|
||||
|
||||
/// Amount of times the track's been paused
|
||||
int pause = 0,
|
||||
|
||||
/// 1 if the track was listened in sync, 0 otherwise
|
||||
int sync = 1,
|
||||
|
||||
/// If the track is skipped to the next song
|
||||
bool next = false,
|
||||
|
||||
/// If the track is skipped to the previous song
|
||||
bool prev = false,
|
||||
|
||||
/// When the timestamp has begun as UTC int (in SECONDS)
|
||||
int? timestamp,
|
||||
}) async {
|
||||
await callApi('log.listen', params: {
|
||||
'params': {
|
||||
'timestamp': timestamp ?? DateTime.now().millisecondsSinceEpoch,
|
||||
'ts_listen': DateTime.now().millisecondsSinceEpoch,
|
||||
'timestamp':
|
||||
timestamp ?? (DateTime.now().millisecondsSinceEpoch) ~/ 1000,
|
||||
'ts_listen': DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||
'type': 1,
|
||||
'stat': {
|
||||
'seek': seek, // amount of times seeked
|
||||
'pause': pause, // amount of times paused
|
||||
'sync': sync
|
||||
'sync': sync,
|
||||
if (next) 'next': true,
|
||||
if (prev) 'prev': true,
|
||||
},
|
||||
'media': {'id': trackId, 'type': 'song', 'format': 'MP3_128'}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,6 @@ 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';
|
||||
|
@ -966,6 +965,8 @@ class HomePageSection {
|
|||
}[json['layout'] ?? ''];
|
||||
if (layout == null) {
|
||||
_logger.warning('UNKNOWN LAYOUT: ${json['layout']}');
|
||||
_logger.warning('LAYOUT DATA:');
|
||||
_logger.warning(json);
|
||||
return null;
|
||||
}
|
||||
final items = <HomePageItem>[];
|
||||
|
@ -1552,6 +1553,10 @@ extension GetId on FlowConfig {
|
|||
}
|
||||
}
|
||||
|
||||
extension LastChars on String {
|
||||
String withoutLast(int n) => substring(0, length - n);
|
||||
}
|
||||
|
||||
class TrackUrlSource {
|
||||
final String provider;
|
||||
final String url;
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
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';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:freezer/api/deezer.dart';
|
||||
import 'package:freezer/api/definitions.dart';
|
||||
|
@ -488,12 +486,10 @@ class DownloadManager {
|
|||
}
|
||||
|
||||
//Check if album, track or playlist is offline
|
||||
Future<bool> _checkOffline(
|
||||
(Album? album, Track? track, Playlist? playlist) message) async {
|
||||
Future<bool> checkOffline(
|
||||
{Album? album, Track? track, Playlist? playlist}) async {
|
||||
if (!isSupported) return false;
|
||||
|
||||
final (album, track, playlist) = message;
|
||||
|
||||
//Track
|
||||
if (track != null) {
|
||||
List res = await db.query('Tracks',
|
||||
|
@ -518,10 +514,6 @@ class DownloadManager {
|
|||
return false;
|
||||
}
|
||||
|
||||
Future<bool> checkOffline({Album? album, Track? track, Playlist? playlist}) {
|
||||
return compute(_checkOffline, (album, track, playlist));
|
||||
}
|
||||
|
||||
//Offline search
|
||||
Future<SearchResults> search(String? query) async {
|
||||
SearchResults results =
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import 'package:freezer/api/definitions.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
@collection
|
||||
|
|
|
@ -7,7 +7,6 @@ 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 {
|
||||
//implements dl.DownloadManager {
|
||||
|
@ -62,7 +61,6 @@ class DownloadManager {
|
|||
FlutterBackgroundService().invoke(method, args);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> addOfflineTrack(Track track,
|
||||
{bool private = true, BuildContext? context, isSingleton = false}) {
|
||||
// TODO: implement addOfflineTrack
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:audio_session/audio_session.dart';
|
||||
import 'package:equalizer/equalizer.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:freezer/api/cache.dart';
|
||||
import 'package:freezer/api/deezer.dart';
|
||||
import 'package:freezer/api/deezer_audio_source.dart';
|
||||
|
@ -18,11 +16,8 @@ import 'package:just_audio_media_kit/just_audio_media_kit.dart';
|
|||
import 'package:logging/logging.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:freezer/translations.i18n.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:scrobblenaut/scrobblenaut.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
import 'package:media_kit/media_kit.dart' show MPVLogLevel;
|
||||
|
||||
import '../definitions.dart';
|
||||
import '../../settings.dart';
|
||||
|
@ -122,6 +117,9 @@ class AudioPlayerTask extends BaseAudioHandler {
|
|||
/// Last logged track id (to avoid duplicates)
|
||||
String? _loggedTrackId;
|
||||
|
||||
/// Last track's id
|
||||
String? _lastTrackId;
|
||||
|
||||
// deezer track logging
|
||||
/// Whether we should send log requests to deezer
|
||||
late bool _shouldLogTracks;
|
||||
|
@ -132,7 +130,7 @@ class AudioPlayerTask extends BaseAudioHandler {
|
|||
/// Amount of times the song's been seeked
|
||||
int _amountSeeked = 0;
|
||||
|
||||
/// When playback begun
|
||||
/// When playback begun (in SECONDS)
|
||||
int? _timestamp;
|
||||
|
||||
MediaItem get currentMediaItem => queue.value[_queueIndex];
|
||||
|
@ -155,7 +153,6 @@ class AudioPlayerTask extends BaseAudioHandler {
|
|||
// Linux/Windows specific options
|
||||
JustAudioMediaKit.title = 'Freezer';
|
||||
JustAudioMediaKit.protocolWhitelist = const ['http'];
|
||||
JustAudioMediaKit.mpvLogLevel = MPVLogLevel.debug;
|
||||
|
||||
_deezerAPI = initArgs.deezerAPI;
|
||||
_androidAuto = AndroidAuto(deezerAPI: _deezerAPI);
|
||||
|
@ -180,12 +177,22 @@ class AudioPlayerTask extends BaseAudioHandler {
|
|||
|
||||
//Update track index
|
||||
_player.currentIndexStream.listen((index) {
|
||||
_amountSeeked = 0;
|
||||
_amountPaused = 0;
|
||||
_timestamp = DateTime.now().millisecondsSinceEpoch;
|
||||
if (index != null && queue.value.isNotEmpty) {
|
||||
_queueIndex = index;
|
||||
mediaItem.add(currentMediaItem);
|
||||
|
||||
// log previous track
|
||||
if (index != 0 &&
|
||||
_lastTrackId != null &&
|
||||
_lastTrackId! != currentMediaItem.id) {
|
||||
_logListenedTrack(_lastTrackId!,
|
||||
sync: _amountPaused == 0 && _amountSeeked == 0);
|
||||
}
|
||||
|
||||
_lastTrackId = currentMediaItem.id;
|
||||
_amountSeeked = 0;
|
||||
_amountPaused = 0;
|
||||
_timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||
}
|
||||
|
||||
if (index == queue.value.length - 1) {
|
||||
|
@ -290,7 +297,7 @@ class AudioPlayerTask extends BaseAudioHandler {
|
|||
Future skipToQueueItem(int index) async {
|
||||
_lastPosition = null;
|
||||
|
||||
unawaited(_logListenedTrack(currentMediaItem));
|
||||
unawaited(_logListenedTrack(currentMediaItem.id, sync: false));
|
||||
//Skip in player
|
||||
await _player.seek(Duration.zero, index: index);
|
||||
_queueIndex = index;
|
||||
|
@ -396,7 +403,7 @@ class AudioPlayerTask extends BaseAudioHandler {
|
|||
_lastPosition = null;
|
||||
if (_queueIndex == queue.value.length - 1) return;
|
||||
//Update buffering state
|
||||
unawaited(_logListenedTrack(currentMediaItem));
|
||||
unawaited(_logListenedTrack(currentMediaItem.id, sync: false, next: true));
|
||||
_queueIndex++;
|
||||
await _player.seekToNext();
|
||||
_broadcastState();
|
||||
|
@ -408,24 +415,30 @@ class AudioPlayerTask extends BaseAudioHandler {
|
|||
//Update buffering state
|
||||
//_skipState = AudioProcessingState.skippingToPrevious;
|
||||
//Normal skip to previous
|
||||
unawaited(_logListenedTrack(currentMediaItem));
|
||||
unawaited(_logListenedTrack(currentMediaItem.id, sync: false, prev: true));
|
||||
_queueIndex--;
|
||||
await _player.seekToPrevious();
|
||||
//_skipState = null;
|
||||
}
|
||||
|
||||
Future<void> _logListenedTrack(MediaItem mediaItem) async {
|
||||
Future<void> _logListenedTrack(String trackId,
|
||||
{required bool sync, bool next = false, bool prev = false}) async {
|
||||
if (_loggedTrackId == trackId) return;
|
||||
_loggedTrackId = trackId;
|
||||
|
||||
print(
|
||||
'logging: seek: $_amountSeeked, pause: $_amountPaused, timestamp: $_timestamp (elapsed: ${DateTime.now().millisecondsSinceEpoch - _timestamp!}ms)');
|
||||
'logging: seek: $_amountSeeked, pause: $_amountPaused, sync: $sync, next: $next, prev: $prev, timestamp: $_timestamp (elapsed: ${DateTime.now().millisecondsSinceEpoch ~/ 1000 - _timestamp!}s)');
|
||||
if (!_shouldLogTracks) return;
|
||||
|
||||
//Log to Deezer
|
||||
_deezerAPI.logListen(
|
||||
mediaItem.id,
|
||||
trackId,
|
||||
seek: _amountSeeked,
|
||||
pause: _amountPaused,
|
||||
sync: _amountSeeked == 0 && _amountPaused == 0 ? 1 : 0,
|
||||
sync: sync ? 1 : 0,
|
||||
timestamp: _timestamp,
|
||||
prev: prev,
|
||||
next: next,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -97,10 +97,17 @@ void main() async {
|
|||
Hive.init(await Paths.dataDirectory());
|
||||
|
||||
//Initialize globals
|
||||
settings = await Settings.load();
|
||||
settings.save();
|
||||
try {
|
||||
cache = await Cache.load();
|
||||
settings = await Settings.load();
|
||||
settings.save();
|
||||
} catch (e) {
|
||||
// ignore: avoid_print
|
||||
print(
|
||||
'Cannot load cache or settings box. Maybe another instance of the app is running?');
|
||||
exit(1);
|
||||
}
|
||||
downloadManager.init();
|
||||
cache = await Cache.load();
|
||||
// photos
|
||||
cacheManager = DefaultCacheManager();
|
||||
// cacheManager = HiveCacheManager(
|
||||
|
@ -109,9 +116,8 @@ void main() async {
|
|||
deezerAPI.favoritesPlaylistId = cache.favoritesPlaylistId;
|
||||
|
||||
Logger.root.onRecord.listen((record) {
|
||||
if (kDebugMode) {
|
||||
print('${record.level.name}: ${record.time}: ${record.message}');
|
||||
}
|
||||
// ignore: avoid_print
|
||||
print('${record.level.name}: ${record.time}: ${record.message}');
|
||||
});
|
||||
|
||||
if (kDebugMode) {
|
||||
|
@ -699,8 +705,7 @@ class MainScreenState extends State<MainScreen>
|
|||
child: _MainRouteNavigator(
|
||||
navigatorKey: navigatorKey,
|
||||
routes: {
|
||||
Navigator.defaultRouteName: (context) =>
|
||||
const HomeScreen(),
|
||||
Navigator.defaultRouteName: (context) => HomeScreen(),
|
||||
'/podcasts': (context) => HomePageScreen(
|
||||
cacheable: true,
|
||||
channel: const DeezerChannel(
|
||||
|
|
|
@ -39,7 +39,7 @@ const List<Language> languages = [
|
|||
List<Locale> get supportedLocales => languages.map((l) => l.getLocale).toList();
|
||||
|
||||
extension Localization on String {
|
||||
static const _t = Translations.from("en_US", {...language_en_us, ...crowdin});
|
||||
static final _t = Translations.byLocale("en_US") + language_en_us + crowdin;
|
||||
|
||||
String get i18n => localize(this, _t);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:draggable_scrollbar/draggable_scrollbar.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fluttericon/font_awesome5_icons.dart';
|
||||
import 'package:freezer/api/cache.dart';
|
||||
|
@ -864,9 +863,10 @@ class _PlaylistDetailsState extends State<PlaylistDetails> {
|
|||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text(playlist!.title ?? '')),
|
||||
body: DraggableScrollbar.rrect(
|
||||
body: Scrollbar(
|
||||
interactive: true,
|
||||
controller: _scrollController,
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
thickness: 8.0,
|
||||
child: ListView(
|
||||
controller: _scrollController,
|
||||
children: <Widget>[
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.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/ui/error.dart';
|
||||
import 'package:freezer/ui/menu.dart';
|
||||
import 'package:freezer/translations.i18n.dart';
|
||||
|
@ -138,8 +140,10 @@ class _HomePageWidgetState extends State<HomePageWidget> {
|
|||
}
|
||||
if (!mounted) return;
|
||||
if (homePage == null) {
|
||||
//On error
|
||||
setState(() => _error = true);
|
||||
if (_homePage == null) {
|
||||
//On error
|
||||
setState(() => _error = true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (homePage.sections.isEmpty) return;
|
||||
|
@ -183,6 +187,16 @@ class _HomePageWidgetState extends State<HomePageWidget> {
|
|||
});
|
||||
}
|
||||
|
||||
Widget getSectionChild(HomePageSection section) {
|
||||
switch (section.layout) {
|
||||
case HomePageSectionLayout.GRID:
|
||||
return HomePageGridSection(section);
|
||||
case HomePageSectionLayout.ROW:
|
||||
default:
|
||||
return HomepageRowSection(section);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_error) return const ErrorScreen();
|
||||
|
@ -190,74 +204,102 @@ class _HomePageWidgetState extends State<HomePageWidget> {
|
|||
if (_homePage != null) {
|
||||
sections = _homePage!.sections;
|
||||
}
|
||||
return RefreshIndicator(
|
||||
key: _indicatorKey,
|
||||
onRefresh: _load,
|
||||
child: _homePage == null
|
||||
? const SizedBox.expand(child: SingleChildScrollView())
|
||||
: ListView.builder(
|
||||
itemBuilder: (context, index) {
|
||||
final section = sections![index];
|
||||
switch (section.layout) {
|
||||
case HomePageSectionLayout.GRID:
|
||||
return HomePageGridSection(section);
|
||||
case HomePageSectionLayout.ROW:
|
||||
default:
|
||||
return HomepageRowSection(section);
|
||||
}
|
||||
},
|
||||
itemCount: sections!.length,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
),
|
||||
|
||||
final actualScrollConfiguration = ScrollConfiguration.of(context);
|
||||
return ScrollConfiguration(
|
||||
behavior: actualScrollConfiguration.copyWith(
|
||||
dragDevices: {
|
||||
PointerDeviceKind.mouse,
|
||||
PointerDeviceKind.stylus,
|
||||
PointerDeviceKind.touch,
|
||||
PointerDeviceKind.trackpad
|
||||
},
|
||||
),
|
||||
child: RefreshIndicator(
|
||||
key: _indicatorKey,
|
||||
onRefresh: _load,
|
||||
child: _homePage == null
|
||||
? const SizedBox.expand(child: SingleChildScrollView())
|
||||
: ListView.builder(
|
||||
itemBuilder: (context, index) {
|
||||
return Padding(
|
||||
padding: index == 0
|
||||
? EdgeInsets.zero
|
||||
: const EdgeInsets.only(top: 16.0),
|
||||
child: getSectionChild(sections![index]));
|
||||
},
|
||||
itemCount: sections!.length,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class HomepageRowSection extends StatelessWidget {
|
||||
class HomepageRowSection extends StatefulWidget {
|
||||
final HomePageSection section;
|
||||
const HomepageRowSection(this.section, {super.key});
|
||||
|
||||
@override
|
||||
State<HomepageRowSection> createState() => _HomepageRowSectionState();
|
||||
}
|
||||
|
||||
class _HomepageRowSectionState extends State<HomepageRowSection> {
|
||||
final _controller = ScrollController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
title: Text(
|
||||
section.title ?? '',
|
||||
widget.section.title ?? '',
|
||||
textAlign: TextAlign.left,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontSize: 20.0, fontWeight: FontWeight.w900),
|
||||
),
|
||||
subtitle: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: List.generate(section.items!.length + 1, (j) {
|
||||
//Has more items
|
||||
if (j == section.items!.length) {
|
||||
if (section.hasMore ?? false) {
|
||||
return TextButton(
|
||||
child: Text(
|
||||
'Show more'.i18n,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 20.0),
|
||||
),
|
||||
onPressed: () => Navigator.of(context).pushRoute(
|
||||
builder: (context) => HomePageScreen(
|
||||
title: section.title!,
|
||||
channel: DeezerChannel(target: section.pagePath),
|
||||
subtitle: Scrollbar(
|
||||
controller: _controller,
|
||||
thickness: MainScreen.of(context).isDesktop ? null : 1.0,
|
||||
child: SingleChildScrollView(
|
||||
controller: _controller,
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: List.generate(widget.section.items!.length + 1, (j) {
|
||||
//Has more items
|
||||
if (j == widget.section.items!.length) {
|
||||
if (widget.section.hasMore ?? false) {
|
||||
return TextButton(
|
||||
child: Text(
|
||||
'Show more'.i18n,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 20.0),
|
||||
),
|
||||
),
|
||||
);
|
||||
onPressed: () => Navigator.of(context).pushRoute(
|
||||
builder: (context) => HomePageScreen(
|
||||
title: widget.section.title!,
|
||||
channel:
|
||||
DeezerChannel(target: widget.section.pagePath),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return const SizedBox();
|
||||
}
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
//Show item
|
||||
HomePageItem item = section.items![j];
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6.0),
|
||||
child: HomePageItemWidget(item),
|
||||
);
|
||||
}),
|
||||
//Show item
|
||||
HomePageItem item = widget.section.items![j];
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6.0),
|
||||
child: HomePageItemWidget(item),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
@ -265,7 +307,14 @@ class HomepageRowSection extends StatelessWidget {
|
|||
|
||||
class HomePageGridSection extends StatelessWidget {
|
||||
final HomePageSection section;
|
||||
const HomePageGridSection(this.section, {super.key});
|
||||
final double horizontalSpacing;
|
||||
final double verticalSpacing;
|
||||
const HomePageGridSection(
|
||||
this.section, {
|
||||
this.horizontalSpacing = 6.0,
|
||||
this.verticalSpacing = 6.0,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -284,13 +333,22 @@ class HomePageGridSection extends StatelessWidget {
|
|||
const TextStyle(fontSize: 20.0, fontWeight: FontWeight.w900),
|
||||
),
|
||||
),
|
||||
Wrap(
|
||||
spacing: 4.0,
|
||||
runSpacing: 4.0,
|
||||
alignment: WrapAlignment.spaceEvenly,
|
||||
children: section.items!
|
||||
.map((e) => HomePageItemWidget(e))
|
||||
.toList(growable: false)),
|
||||
LayoutBuilder(builder: (context, constraints) {
|
||||
final amountThatCanFit =
|
||||
constraints.maxWidth ~/ (150.0 + horizontalSpacing);
|
||||
final widthOfEach =
|
||||
constraints.maxWidth / amountThatCanFit - horizontalSpacing;
|
||||
return Wrap(
|
||||
spacing: horizontalSpacing,
|
||||
runSpacing: verticalSpacing,
|
||||
alignment: WrapAlignment.start,
|
||||
children: section.items!
|
||||
.map((e) => SizedBox(
|
||||
width: widthOfEach,
|
||||
child: HomePageItemWidget(e),
|
||||
))
|
||||
.toList(growable: false));
|
||||
}),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
@ -9,7 +9,6 @@ import 'package:freezer/api/spotify.dart';
|
|||
import 'package:freezer/ui/elements.dart';
|
||||
import 'package:freezer/translations.i18n.dart';
|
||||
import 'package:spotify/spotify.dart' as spotify;
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
|
@ -576,15 +575,18 @@ class _SpotifyImporterV2MainState extends State<SpotifyImporterV2Main> {
|
|||
await importer.start(context, title, description, tracks);
|
||||
|
||||
if (!context.mounted) return;
|
||||
//Route
|
||||
Navigator.of(context).pop();
|
||||
Navigator.of(context).pushReplacement(MaterialPageRoute(
|
||||
builder: (context) => const ImporterStatusScreen()));
|
||||
} catch (e) {
|
||||
} catch (e, st) {
|
||||
print(e);
|
||||
print(st);
|
||||
ScaffoldMessenger.of(context).snack(e.toString());
|
||||
Navigator.of(context).pop();
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
return;
|
||||
}
|
||||
|
||||
//Route
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(builder: (context) => const ImporterStatusScreen()));
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
@ -13,7 +13,6 @@ import 'package:freezer/ui/error.dart';
|
|||
import 'package:freezer/ui/importer_screen.dart';
|
||||
import 'package:freezer/ui/tiles.dart';
|
||||
import 'package:freezer/translations.i18n.dart';
|
||||
import 'package:draggable_scrollbar/draggable_scrollbar.dart';
|
||||
|
||||
import 'menu.dart';
|
||||
import '../api/download.dart';
|
||||
|
@ -471,9 +470,10 @@ class _LibraryTracksState extends State<LibraryTracks> {
|
|||
),
|
||||
body: _loading && allTracks.isEmpty
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: DraggableScrollbar.rrect(
|
||||
: Scrollbar(
|
||||
interactive: true,
|
||||
controller: _scrollController,
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
thickness: 8.0,
|
||||
child: ListView(
|
||||
controller: _scrollController,
|
||||
children: <Widget>[
|
||||
|
@ -677,9 +677,10 @@ class _LibraryAlbumsState extends State<LibraryAlbums> {
|
|||
),
|
||||
body: !settings.offlineMode && _albums == null
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: DraggableScrollbar.rrect(
|
||||
: Scrollbar(
|
||||
interactive: true,
|
||||
controller: _scrollController,
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
thickness: 8.0,
|
||||
child: ListView(
|
||||
controller: _scrollController,
|
||||
children: <Widget>[
|
||||
|
@ -886,9 +887,10 @@ class _LibraryArtistsState extends State<LibraryArtists> {
|
|||
? const Center(child: CircularProgressIndicator())
|
||||
: _error
|
||||
? const Center(child: ErrorScreen())
|
||||
: DraggableScrollbar.rrect(
|
||||
: Scrollbar(
|
||||
interactive: true,
|
||||
controller: _scrollController,
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
thickness: 8.0,
|
||||
child: ListView(
|
||||
controller: _scrollController,
|
||||
children: <Widget>[
|
||||
|
@ -1047,9 +1049,10 @@ class _LibraryPlaylistsState extends State<LibraryPlaylists> {
|
|||
Container(width: 8.0),
|
||||
],
|
||||
),
|
||||
body: DraggableScrollbar.rrect(
|
||||
body: Scrollbar(
|
||||
interactive: true,
|
||||
controller: _scrollController,
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
thickness: 8.0,
|
||||
child: ListView(
|
||||
controller: _scrollController,
|
||||
children: <Widget>[
|
||||
|
@ -1198,9 +1201,10 @@ class _HistoryScreenState extends State<HistoryScreen> {
|
|||
)
|
||||
],
|
||||
),
|
||||
body: DraggableScrollbar.rrect(
|
||||
body: Scrollbar(
|
||||
interactive: true,
|
||||
controller: _scrollController,
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
thickness: 8.0,
|
||||
child: ListView.builder(
|
||||
controller: _scrollController,
|
||||
itemCount: cache.history.length,
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
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';
|
||||
|
@ -149,73 +147,6 @@ class _LoginWidgetState extends State<LoginWidget> {
|
|||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
@ -256,106 +187,114 @@ class _LoginWidgetState extends State<LoginWidget> {
|
|||
// style: ButtonStyle(
|
||||
// foregroundColor:
|
||||
// MaterialStateProperty.all(Colors.white)))),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const FreezerTitle(),
|
||||
Container(height: 16.0),
|
||||
Text(
|
||||
"Please login using your Deezer account.".i18n,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 16.0),
|
||||
),
|
||||
Container(
|
||||
height: 16.0,
|
||||
),
|
||||
//Email login dialog
|
||||
OutlinedButton(
|
||||
child: Text(
|
||||
'Login using email'.i18n,
|
||||
),
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => EmailLogin(_update));
|
||||
},
|
||||
),
|
||||
OutlinedButton(
|
||||
onPressed: _loginBrowser,
|
||||
child: Text('Login using browser'.i18n),
|
||||
),
|
||||
OutlinedButton(
|
||||
child: Text('Login using token'.i18n),
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
Future.delayed(
|
||||
const Duration(seconds: 1),
|
||||
() => {
|
||||
focusNode.requestFocus()
|
||||
}); // autofocus doesn't work - it's replacement
|
||||
return AlertDialog(
|
||||
title: Text('Enter ARL'.i18n),
|
||||
content: TextField(
|
||||
onChanged: (String s) => _arl = s,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Token (ARL)'.i18n),
|
||||
focusNode: focusNode,
|
||||
controller: controller,
|
||||
onSubmitted: (String s) {
|
||||
goARL(focusNode, controller);
|
||||
},
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
child: Text('Save'.i18n),
|
||||
onPressed: () =>
|
||||
goARL(null, controller),
|
||||
)
|
||||
],
|
||||
);
|
||||
});
|
||||
},
|
||||
),
|
||||
]))),
|
||||
Container(height: 16.0),
|
||||
Text(
|
||||
"If you don't have account, you can register on deezer.com for free."
|
||||
.i18n,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 16.0),
|
||||
child: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 700.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const FreezerTitle(),
|
||||
const SizedBox(height: 16.0),
|
||||
Text(
|
||||
"Please login using your Deezer account."
|
||||
.i18n,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 16.0),
|
||||
),
|
||||
const SizedBox(height: 16.0),
|
||||
//Email login dialog
|
||||
ElevatedButton(
|
||||
child: Text(
|
||||
'Login using email'.i18n,
|
||||
),
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) =>
|
||||
EmailLogin(_update));
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 2.0),
|
||||
|
||||
// only supported on android
|
||||
if (Platform.isAndroid)
|
||||
ElevatedButton(
|
||||
onPressed: _loginBrowser,
|
||||
child: Text('Login using browser'.i18n),
|
||||
),
|
||||
const SizedBox(height: 2.0),
|
||||
ElevatedButton(
|
||||
child: Text('Login using token'.i18n),
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
Future.delayed(
|
||||
const Duration(seconds: 1),
|
||||
() => {
|
||||
focusNode.requestFocus()
|
||||
}); // autofocus doesn't work - it's replacement
|
||||
return AlertDialog(
|
||||
title: Text('Enter ARL'.i18n),
|
||||
content: TextField(
|
||||
onChanged: (String s) => _arl = s,
|
||||
decoration: InputDecoration(
|
||||
labelText:
|
||||
'Token (ARL)'.i18n),
|
||||
focusNode: focusNode,
|
||||
controller: controller,
|
||||
onSubmitted: (String s) {
|
||||
goARL(focusNode, controller);
|
||||
},
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
child: Text('Save'.i18n),
|
||||
onPressed: () =>
|
||||
goARL(null, controller),
|
||||
)
|
||||
],
|
||||
);
|
||||
});
|
||||
},
|
||||
),
|
||||
]))),
|
||||
const SizedBox(height: 16.0),
|
||||
Text(
|
||||
"If you don't have account, you can register on deezer.com for free."
|
||||
.i18n,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 16.0),
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32.0),
|
||||
child: ElevatedButton(
|
||||
child: Text('Open in browser'.i18n),
|
||||
onPressed: () {
|
||||
launchUrlString('https://deezer.com/register');
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
const Divider(),
|
||||
const SizedBox(height: 8.0),
|
||||
Text(
|
||||
"By using this app, you don't agree with the Deezer ToS"
|
||||
.i18n,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 14.0),
|
||||
)
|
||||
],
|
||||
),
|
||||
Container(height: 8.0),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32.0),
|
||||
child: OutlinedButton(
|
||||
child: Text('Open in browser'.i18n),
|
||||
onPressed: () {
|
||||
launchUrlString('https://deezer.com/register');
|
||||
},
|
||||
),
|
||||
),
|
||||
Container(
|
||||
height: 8.0,
|
||||
),
|
||||
const Divider(),
|
||||
Container(
|
||||
height: 8.0,
|
||||
),
|
||||
Text(
|
||||
"By using this app, you don't agree with the Deezer ToS".i18n,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 14.0),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
));
|
||||
|
@ -486,37 +425,48 @@ class _EmailLoginState extends State<EmailLogin> {
|
|||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text('Email Login'.i18n),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: _loading
|
||||
? [const CircularProgressIndicator()]
|
||||
: [
|
||||
TextFormField(
|
||||
decoration: InputDecoration(labelText: 'Email'.i18n),
|
||||
controller: _emailController,
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
TextFormField(
|
||||
obscureText: true,
|
||||
decoration: InputDecoration(labelText: "Password".i18n),
|
||||
controller: _passwordController,
|
||||
)
|
||||
content: AutofillGroup(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextFormField(
|
||||
enabled: !_loading,
|
||||
decoration: InputDecoration(labelText: 'Email'.i18n),
|
||||
controller: _emailController,
|
||||
autofillHints: const [
|
||||
AutofillHints.email,
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
TextFormField(
|
||||
enabled: !_loading,
|
||||
obscureText: true,
|
||||
decoration: InputDecoration(labelText: "Password".i18n),
|
||||
controller: _passwordController,
|
||||
autofillHints: const [
|
||||
AutofillHints.password,
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
if (!_loading)
|
||||
TextButton(
|
||||
child: const Text('Login'),
|
||||
onPressed: () async {
|
||||
if (_emailController.text.isNotEmpty &&
|
||||
_passwordController.text.isNotEmpty) {
|
||||
await _login();
|
||||
} else {
|
||||
ScaffoldMessenger.of(context)
|
||||
.snack("Missing email or password!".i18n);
|
||||
}
|
||||
},
|
||||
)
|
||||
TextButton(
|
||||
onPressed: _loading
|
||||
? null
|
||||
: () async {
|
||||
if (_emailController.text.isNotEmpty &&
|
||||
_passwordController.text.isNotEmpty) {
|
||||
await _login();
|
||||
} else {
|
||||
ScaffoldMessenger.of(context)
|
||||
.snack("Missing email or password!".i18n);
|
||||
}
|
||||
},
|
||||
child: _loading
|
||||
? const CircularProgressIndicator()
|
||||
: const Text('Login'),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
@ -48,7 +48,7 @@ class _LyricsWidgetState extends State<LyricsWidget> {
|
|||
late StreamSubscription _mediaItemSub;
|
||||
late StreamSubscription _playbackStateSub;
|
||||
int? _currentIndex = -1;
|
||||
int? _prevIndex = -1;
|
||||
Duration _nextPosition = Duration.zero;
|
||||
final ScrollController _controller = ScrollController();
|
||||
final double height = 90;
|
||||
BoxConstraints? _widgetConstraints;
|
||||
|
@ -86,6 +86,7 @@ class _LyricsWidgetState extends State<LyricsWidget> {
|
|||
_lyrics = lyrics;
|
||||
});
|
||||
|
||||
_nextPosition = Duration.zero;
|
||||
SchedulerBinding.instance.addPostFrameCallback(
|
||||
(_) => _updatePosition(audioHandler.playbackState.value.position));
|
||||
} catch (e) {
|
||||
|
@ -96,7 +97,7 @@ class _LyricsWidgetState extends State<LyricsWidget> {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> _scrollToLyric() async {
|
||||
void _scrollToLyric() {
|
||||
if (!_controller.hasClients) return;
|
||||
//Lyric height, screen height, appbar height
|
||||
double scrollTo;
|
||||
|
@ -111,27 +112,36 @@ class _LyricsWidgetState extends State<LyricsWidget> {
|
|||
|
||||
print(
|
||||
'${height * _currentIndex!}, ${MediaQuery.of(context).size.height / 2}');
|
||||
if (0 > scrollTo) return;
|
||||
if (scrollTo < 0.0) scrollTo = 0.0;
|
||||
if (scrollTo > _controller.position.maxScrollExtent) {
|
||||
scrollTo = _controller.position.maxScrollExtent;
|
||||
}
|
||||
_animatedScroll = true;
|
||||
await _controller.animateTo(scrollTo,
|
||||
duration: const Duration(milliseconds: 250), curve: Curves.ease);
|
||||
_animatedScroll = false;
|
||||
_controller
|
||||
.animateTo(scrollTo,
|
||||
duration: const Duration(milliseconds: 250), curve: Curves.ease)
|
||||
.then((_) => _animatedScroll = false);
|
||||
}
|
||||
|
||||
void _updatePosition(Duration position) {
|
||||
if (_loading) return;
|
||||
if (!_syncedLyrics) return;
|
||||
if (position < _nextPosition) return;
|
||||
|
||||
_currentIndex =
|
||||
_lyrics?.lyrics?.lastIndexWhere((l) => l.offset! <= position);
|
||||
//Scroll to current lyric
|
||||
if (_currentIndex! < 0) return;
|
||||
if (_prevIndex == _currentIndex) return;
|
||||
//Update current lyric index
|
||||
setState(() {});
|
||||
_prevIndex = _currentIndex;
|
||||
if (_currentIndex! < _lyrics!.lyrics!.length) {
|
||||
// update nextPosition
|
||||
_nextPosition = _lyrics!.lyrics![_currentIndex! + 1].offset!;
|
||||
} else {
|
||||
// dummy position so that the before-hand condition always returns false
|
||||
_nextPosition = const Duration(days: 69);
|
||||
}
|
||||
|
||||
setState(() => _currentIndex);
|
||||
if (_freeScroll) return;
|
||||
_scrollToLyric();
|
||||
}
|
||||
|
|
110
lib/ui/menu.dart
110
lib/ui/menu.dart
|
@ -1,8 +1,6 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:freezer/main.dart';
|
||||
import 'package:freezer/ui/fancy_scaffold.dart';
|
||||
import 'package:freezer/ui/player_bar.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:freezer/api/cache.dart';
|
||||
|
@ -108,43 +106,40 @@ class MenuSheetOption {
|
|||
class MenuSheet {
|
||||
final BuildContext context;
|
||||
final Function? navigateCallback;
|
||||
bool _contextMenuOpen = false;
|
||||
|
||||
MenuSheet(this.context, {this.navigateCallback});
|
||||
|
||||
void _showContextMenu(List<MenuSheetOption> options,
|
||||
{required TapDownDetails details}) {
|
||||
{required TapUpDetails details}) {
|
||||
final overlay = Overlay.of(context).context.findRenderObject() as RenderBox;
|
||||
final actualPosition = overlay.globalToLocal(details.globalPosition);
|
||||
_contextMenuOpen = true;
|
||||
showMenu(
|
||||
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);
|
||||
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));
|
||||
}
|
||||
|
||||
//===================
|
||||
// DEFAULT
|
||||
//===================
|
||||
|
||||
void show(List<MenuSheetOption> options, {TapDownDetails? details}) {
|
||||
void show(List<MenuSheetOption> options, {TapUpDetails? details}) {
|
||||
if (details != null) {
|
||||
_showContextMenu(options, details: details);
|
||||
return;
|
||||
|
@ -185,7 +180,7 @@ class MenuSheet {
|
|||
//===================
|
||||
|
||||
void showWithTrack(Track track, List<MenuSheetOption> options,
|
||||
{TapDownDetails? details}) {
|
||||
{TapUpDetails? details}) {
|
||||
if (details != null) {
|
||||
_showContextMenu(options, details: details);
|
||||
return;
|
||||
|
@ -197,37 +192,38 @@ class MenuSheet {
|
|||
isScrollControlled: true,
|
||||
enableDrag: false,
|
||||
showDragHandle: false,
|
||||
useSafeArea: true,
|
||||
elevation: 0.0,
|
||||
builder: (BuildContext context) {
|
||||
return DraggableScrollableSheet(
|
||||
initialChildSize: 0.5,
|
||||
minChildSize: 0.45,
|
||||
maxChildSize: 0.75,
|
||||
builder: (context, scrollController) => Material(
|
||||
type: MaterialType.card,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
borderRadius:
|
||||
const BorderRadius.vertical(top: Radius.circular(20.0)),
|
||||
child: CustomScrollView(
|
||||
controller: scrollController,
|
||||
slivers: [
|
||||
SliverPersistentHeader(
|
||||
pinned: true,
|
||||
delegate: SliverTrackPersistentHeader(track,
|
||||
extent: 128.0 + 16.0 + 16.0)),
|
||||
SliverList(
|
||||
delegate: SliverChildListDelegate.fixed(options
|
||||
.map((option) => ListTile(
|
||||
title: option.label,
|
||||
leading: option.icon,
|
||||
onTap: () {
|
||||
option.onTap.call();
|
||||
Navigator.pop(context);
|
||||
},
|
||||
))
|
||||
.toList(growable: false))),
|
||||
],
|
||||
builder: (context, scrollController) => SafeArea(
|
||||
child: Material(
|
||||
type: MaterialType.card,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
borderRadius:
|
||||
const BorderRadius.vertical(top: Radius.circular(20.0)),
|
||||
child: CustomScrollView(
|
||||
controller: scrollController,
|
||||
slivers: [
|
||||
SliverPersistentHeader(
|
||||
pinned: true,
|
||||
delegate: SliverTrackPersistentHeader(track,
|
||||
extent: 128.0 + 16.0 + 16.0)),
|
||||
SliverList(
|
||||
delegate: SliverChildListDelegate.fixed(options
|
||||
.map((option) => ListTile(
|
||||
title: option.label,
|
||||
leading: option.icon,
|
||||
onTap: () {
|
||||
option.onTap.call();
|
||||
Navigator.pop(context);
|
||||
},
|
||||
))
|
||||
.toList(growable: false))),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -239,7 +235,7 @@ class MenuSheet {
|
|||
Track track, {
|
||||
List<MenuSheetOption> options = const [],
|
||||
Function? onRemove,
|
||||
TapDownDetails? details,
|
||||
TapUpDetails? details,
|
||||
}) {
|
||||
showWithTrack(
|
||||
track,
|
||||
|
@ -423,7 +419,7 @@ class MenuSheet {
|
|||
void defaultAlbumMenu(Album album,
|
||||
{List<MenuSheetOption> options = const [],
|
||||
Function? onRemove,
|
||||
TapDownDetails? details}) {
|
||||
TapUpDetails? details}) {
|
||||
show([
|
||||
album.library!
|
||||
? removeAlbum(album, onRemove: onRemove)
|
||||
|
@ -486,7 +482,7 @@ class MenuSheet {
|
|||
void defaultArtistMenu(Artist artist,
|
||||
{List<MenuSheetOption> options = const [],
|
||||
Function? onRemove,
|
||||
TapDownDetails? details}) {
|
||||
TapUpDetails? details}) {
|
||||
show(details: details, [
|
||||
artist.library!
|
||||
? removeArtist(artist, onRemove: onRemove)
|
||||
|
@ -529,7 +525,7 @@ class MenuSheet {
|
|||
{List<MenuSheetOption> options = const [],
|
||||
Function? onRemove,
|
||||
Function? onUpdate,
|
||||
TapDownDetails? details}) {
|
||||
TapUpDetails? details}) {
|
||||
show(details: details, [
|
||||
if (playlist.library != null)
|
||||
playlist.library!
|
||||
|
@ -614,7 +610,7 @@ class MenuSheet {
|
|||
//===================
|
||||
|
||||
defaultShowEpisodeMenu(Show s, ShowEpisode e,
|
||||
{List<MenuSheetOption> options = const [], TapDownDetails? details}) {
|
||||
{List<MenuSheetOption> options = const [], TapUpDetails? details}) {
|
||||
show(details: details, [
|
||||
shareTile('episode', e.id),
|
||||
shareShow(s.id),
|
||||
|
|
|
@ -4,7 +4,6 @@ import 'package:audio_service/audio_service.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:freezer/settings.dart';
|
||||
import 'package:freezer/translations.i18n.dart';
|
||||
import 'package:freezer/ui/fancy_scaffold.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
|
||||
import '../api/player/audio_handler.dart';
|
||||
|
|
|
@ -287,26 +287,26 @@ class PlayerScreenVertical extends StatelessWidget {
|
|||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 4.0),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
child: PlayerScreenTopRow(
|
||||
textSize: 20.spMax,
|
||||
iconSize: 24.spMax,
|
||||
textSize: 14.spMax,
|
||||
iconSize: 20.spMax,
|
||||
),
|
||||
),
|
||||
Flexible(child: const BigAlbumArt()),
|
||||
const Flexible(child: BigAlbumArt()),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
||||
child: PlayerTextSubtext(textSize: 32.spMax),
|
||||
child: PlayerTextSubtext(textSize: 24.spMax),
|
||||
),
|
||||
SeekBar(textSize: 20.spMax),
|
||||
SeekBar(textSize: 14.spMax),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: PlaybackControls(40.spMax),
|
||||
child: PlaybackControls(32.spMax),
|
||||
),
|
||||
Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 0, horizontal: 16.0),
|
||||
child: BottomBarControls(size: 28.spMax),
|
||||
child: BottomBarControls(size: 22.spMax),
|
||||
)
|
||||
],
|
||||
));
|
||||
|
@ -350,7 +350,7 @@ class PlayerScreenDesktop extends StatelessWidget {
|
|||
const EdgeInsets.symmetric(vertical: 0, horizontal: 16.0),
|
||||
child: BottomBarControls(
|
||||
size: 16.sp,
|
||||
showLyricsButton: false,
|
||||
desktopMode: true,
|
||||
),
|
||||
)
|
||||
]),
|
||||
|
@ -359,7 +359,7 @@ class PlayerScreenDesktop extends StatelessWidget {
|
|||
const Expanded(
|
||||
flex: 2,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(left: 16.0, right: 16.0, top: 24.0),
|
||||
padding: EdgeInsets.only(left: 16.0, right: 16.0, top: 16.0),
|
||||
child: _DesktopTabView(),
|
||||
)),
|
||||
]);
|
||||
|
@ -395,6 +395,7 @@ class _DesktopTabView extends StatelessWidget {
|
|||
child: TabBarView(children: [
|
||||
QueueListWidget(
|
||||
closePlayer: FancyScaffold.of(context)!.closePanel,
|
||||
isInsidePlayer: true,
|
||||
),
|
||||
const LyricsWidget(),
|
||||
]),
|
||||
|
@ -478,53 +479,50 @@ class PlayerTextSubtext extends StatelessWidget {
|
|||
return const SizedBox();
|
||||
}
|
||||
final currentMediaItem = snapshot.data!;
|
||||
return Column(mainAxisSize: MainAxisSize.min, children: <Widget>[
|
||||
SizedBox(
|
||||
height: textSize * 1.25,
|
||||
width: double.infinity,
|
||||
child: FitOrScrollText(
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
FitOrScrollText(
|
||||
key: Key(currentMediaItem.displayTitle!),
|
||||
text: currentMediaItem.displayTitle!,
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
fontSize: textSize, fontWeight: FontWeight.bold))),
|
||||
// child: currentMediaItem.displayTitle!.length >= 26
|
||||
// ? Marquee(
|
||||
// key: Key(currentMediaItem.displayTitle!),
|
||||
// text: currentMediaItem.displayTitle!,
|
||||
// style: TextStyle(
|
||||
// fontSize: textSize, fontWeight: FontWeight.bold),
|
||||
// blankSpace: 32.0,
|
||||
// startPadding: 0.0,
|
||||
// accelerationDuration: const Duration(seconds: 1),
|
||||
// pauseAfterRound: const Duration(seconds: 2),
|
||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
||||
// fadingEdgeEndFraction: 0.05,
|
||||
// fadingEdgeStartFraction: 0.05,
|
||||
// )
|
||||
// : Text(
|
||||
// currentMediaItem.displayTitle!,
|
||||
// maxLines: 1,
|
||||
// overflow: TextOverflow.ellipsis,
|
||||
// textAlign: TextAlign.start,
|
||||
// style: TextStyle(
|
||||
// fontSize: textSize, fontWeight: FontWeight.bold),
|
||||
// )),
|
||||
const SizedBox(height: 4),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: Text(
|
||||
currentMediaItem.displaySubtitle ?? '',
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.start,
|
||||
overflow: TextOverflow.clip,
|
||||
style: TextStyle(
|
||||
fontSize: textSize * 0.8, // 20% smaller
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontSize: textSize, fontWeight: FontWeight.bold)),
|
||||
// child: currentMediaItem.displayTitle!.length >= 26
|
||||
// ? Marquee(
|
||||
// key: Key(currentMediaItem.displayTitle!),
|
||||
// text: currentMediaItem.displayTitle!,
|
||||
// style: TextStyle(
|
||||
// fontSize: textSize, fontWeight: FontWeight.bold),
|
||||
// blankSpace: 32.0,
|
||||
// startPadding: 0.0,
|
||||
// accelerationDuration: const Duration(seconds: 1),
|
||||
// pauseAfterRound: const Duration(seconds: 2),
|
||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
||||
// fadingEdgeEndFraction: 0.05,
|
||||
// fadingEdgeStartFraction: 0.05,
|
||||
// )
|
||||
// : Text(
|
||||
// currentMediaItem.displayTitle!,
|
||||
// maxLines: 1,
|
||||
// overflow: TextOverflow.ellipsis,
|
||||
// textAlign: TextAlign.start,
|
||||
// style: TextStyle(
|
||||
// fontSize: textSize, fontWeight: FontWeight.bold),
|
||||
// )),
|
||||
const SizedBox(height: 2.0),
|
||||
Text(
|
||||
currentMediaItem.displaySubtitle ?? '',
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.start,
|
||||
overflow: TextOverflow.clip,
|
||||
style: TextStyle(
|
||||
fontSize: textSize * 0.8, // 20% smaller
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
]);
|
||||
]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -560,6 +558,47 @@ class QualityInfoWidget extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
void Function([TapUpDetails?]) _onMenuPressedCallback(BuildContext context) {
|
||||
return ([details]) {
|
||||
final currentMediaItem = audioHandler.mediaItem.value!;
|
||||
Track t = Track.fromMediaItem(currentMediaItem);
|
||||
MenuSheet m = MenuSheet(context, navigateCallback: () {
|
||||
// close player
|
||||
FancyScaffold.of(context)?.closePanel();
|
||||
});
|
||||
if (currentMediaItem.extras!['show'] == null) {
|
||||
m.defaultTrackMenu(t,
|
||||
options: [m.sleepTimer(), m.wakelock()], details: details);
|
||||
} else {
|
||||
m.defaultShowEpisodeMenu(Show.fromJson(currentMediaItem.extras!['show']),
|
||||
ShowEpisode.fromMediaItem(currentMediaItem),
|
||||
options: [m.sleepTimer(), m.wakelock()], details: details);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
class PlayerMenuButtonDesktop extends StatelessWidget {
|
||||
final double size;
|
||||
|
||||
const PlayerMenuButtonDesktop({super.key, required this.size});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
customBorder: const CircleBorder(),
|
||||
onTapUp: _onMenuPressedCallback(context),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Icon(
|
||||
Icons.more_vert,
|
||||
semanticLabel: "Options".i18n,
|
||||
size: size,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PlayerMenuButton extends StatelessWidget {
|
||||
final double size;
|
||||
const PlayerMenuButton({super.key, required this.size});
|
||||
|
@ -567,28 +606,12 @@ class PlayerMenuButton extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IconButton(
|
||||
iconSize: size,
|
||||
icon: Icon(
|
||||
Icons.more_vert,
|
||||
semanticLabel: "Options".i18n,
|
||||
),
|
||||
onPressed: () {
|
||||
final currentMediaItem = audioHandler.mediaItem.value!;
|
||||
Track t = Track.fromMediaItem(currentMediaItem);
|
||||
MenuSheet m = MenuSheet(context, navigateCallback: () {
|
||||
// close player
|
||||
FancyScaffold.of(context)?.closePanel();
|
||||
});
|
||||
if (currentMediaItem.extras!['show'] == null) {
|
||||
m.defaultTrackMenu(t, options: [m.sleepTimer(), m.wakelock()]);
|
||||
} else {
|
||||
m.defaultShowEpisodeMenu(
|
||||
Show.fromJson(currentMediaItem.extras!['show']),
|
||||
ShowEpisode.fromMediaItem(currentMediaItem),
|
||||
options: [m.sleepTimer(), m.wakelock()]);
|
||||
}
|
||||
},
|
||||
);
|
||||
iconSize: size,
|
||||
icon: Icon(
|
||||
Icons.more_vert,
|
||||
semanticLabel: "Options".i18n,
|
||||
),
|
||||
onPressed: _onMenuPressedCallback(context));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -767,7 +790,7 @@ class PlaybackControls extends StatelessWidget {
|
|||
: Theme.of(context).brightness == Brightness.light
|
||||
? provider.dominantColor!
|
||||
: darken(provider.dominantColor!);
|
||||
return PlayPauseButton(size * 2.0,
|
||||
return PlayPauseButton(size * 2.25,
|
||||
filled: true,
|
||||
color: color,
|
||||
iconColor: Color.lerp(
|
||||
|
@ -975,7 +998,8 @@ class PlayerScreenTopRow extends StatelessWidget {
|
|||
text: TextSpan(children: [
|
||||
if (!short)
|
||||
TextSpan(
|
||||
text: 'PLAYING FROM\n'.i18n,
|
||||
text:
|
||||
'${'Playing from:'.i18n.toUpperCase().withoutLast(1)}\n',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.5,
|
||||
|
@ -1118,11 +1142,11 @@ class _SeekBarState extends State<SeekBar> {
|
|||
class BottomBarControls extends StatelessWidget {
|
||||
final double size;
|
||||
final bool
|
||||
showLyricsButton; // removed in desktop mode, because there's a tabbed view which includes it
|
||||
desktopMode; // removed in desktop mode, because there's a tabbed view which includes it
|
||||
const BottomBarControls({
|
||||
super.key,
|
||||
required this.size,
|
||||
this.showLyricsButton = true,
|
||||
this.desktopMode = false,
|
||||
});
|
||||
|
||||
@override
|
||||
|
@ -1145,7 +1169,7 @@ class BottomBarControls extends StatelessWidget {
|
|||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
QualityInfoWidget(textSize: size * 0.75),
|
||||
if (showLyricsButton)
|
||||
if (!desktopMode)
|
||||
IconButton(
|
||||
iconSize: size,
|
||||
icon: Icon(
|
||||
|
@ -1185,7 +1209,9 @@ class BottomBarControls extends StatelessWidget {
|
|||
// },
|
||||
// ),
|
||||
FavoriteButton(size: size * 0.85),
|
||||
PlayerMenuButton(size: size)
|
||||
desktopMode
|
||||
? PlayerMenuButtonDesktop(size: size)
|
||||
: PlayerMenuButton(size: size)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@ 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';
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:country_pickers/country.dart';
|
||||
|
@ -8,12 +7,10 @@ import 'package:flutter/foundation.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_displaymode/flutter_displaymode.dart';
|
||||
import 'package:flutter_material_color_picker/flutter_material_color_picker.dart';
|
||||
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';
|
||||
|
@ -96,8 +93,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||
title: Text(l.name),
|
||||
subtitle: Text("${l.locale}-${l.country}"),
|
||||
onTap: () async {
|
||||
setState(() => settings.language =
|
||||
"${l.locale}_${l.country}");
|
||||
settings.language = "${l.locale}_${l.country}";
|
||||
await settings.save();
|
||||
showDialog(
|
||||
context: context,
|
||||
|
@ -173,55 +169,54 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
|
|||
subtitle: Text(
|
||||
'${'Currently'.i18n}: ${settings.theme.toString().split('.').lastItem}'),
|
||||
leading: const Icon(Icons.color_lens),
|
||||
onTap: settings.materialYouAccent
|
||||
? null
|
||||
: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return SimpleDialog(
|
||||
title: Text('Select theme'.i18n),
|
||||
children: <Widget>[
|
||||
SimpleDialogOption(
|
||||
child: Text('Light'.i18n),
|
||||
onPressed: () {
|
||||
settings.theme = Themes.Light;
|
||||
settings.save();
|
||||
updateTheme(context);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
SimpleDialogOption(
|
||||
child: Text('Dark'.i18n),
|
||||
onPressed: () {
|
||||
settings.theme = Themes.Dark;
|
||||
settings.save();
|
||||
updateTheme(context);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
SimpleDialogOption(
|
||||
child: Text('Black (AMOLED)'.i18n),
|
||||
onPressed: () {
|
||||
settings.theme = Themes.Black;
|
||||
settings.save();
|
||||
updateTheme(context);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
SimpleDialogOption(
|
||||
child: Text('Deezer (Dark)'.i18n),
|
||||
onPressed: () {
|
||||
settings.theme = Themes.Deezer;
|
||||
settings.save();
|
||||
updateTheme(context);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
},
|
||||
enabled: !settings.materialYouAccent,
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return SimpleDialog(
|
||||
title: Text('Select theme'.i18n),
|
||||
children: <Widget>[
|
||||
SimpleDialogOption(
|
||||
child: Text('Light'.i18n),
|
||||
onPressed: () {
|
||||
settings.theme = Themes.Light;
|
||||
settings.save();
|
||||
updateTheme(context);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
SimpleDialogOption(
|
||||
child: Text('Dark'.i18n),
|
||||
onPressed: () {
|
||||
settings.theme = Themes.Dark;
|
||||
settings.save();
|
||||
updateTheme(context);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
SimpleDialogOption(
|
||||
child: Text('Black (AMOLED)'.i18n),
|
||||
onPressed: () {
|
||||
settings.theme = Themes.Black;
|
||||
settings.save();
|
||||
updateTheme(context);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
SimpleDialogOption(
|
||||
child: Text('Deezer (Dark)'.i18n),
|
||||
onPressed: () {
|
||||
settings.theme = Themes.Deezer;
|
||||
settings.save();
|
||||
updateTheme(context);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
},
|
||||
),
|
||||
SwitchListTile(
|
||||
title: Text('Use system theme'.i18n),
|
||||
|
@ -512,21 +507,23 @@ class _ColorPickerState extends State<_ColorPicker> {
|
|||
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,
|
||||
},
|
||||
child: SingleChildScrollView(
|
||||
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: [
|
||||
|
@ -1476,11 +1473,12 @@ class _GeneralSettingsState extends State<GeneralSettings> {
|
|||
child: Text('Log out & Exit'.i18n),
|
||||
onPressed: () async {
|
||||
try {
|
||||
audioHandler.stop();
|
||||
await audioHandler.stop();
|
||||
await DownloadManager.platform
|
||||
.invokeMethod("kill");
|
||||
} catch (e) {}
|
||||
await logOut();
|
||||
await DownloadManager.platform
|
||||
.invokeMethod("kill");
|
||||
|
||||
SystemNavigator.pop();
|
||||
},
|
||||
)
|
||||
|
|
|
@ -13,7 +13,7 @@ import 'cached_image.dart';
|
|||
|
||||
import 'dart:async';
|
||||
|
||||
typedef SecondaryTapCallback = void Function(TapDownDetails?);
|
||||
typedef SecondaryTapCallback = void Function(TapUpDetails?);
|
||||
|
||||
VoidCallback? normalizeSecondary(SecondaryTapCallback? callback) {
|
||||
if (callback == null) return null;
|
||||
|
@ -90,7 +90,7 @@ class TrackTile extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onSecondaryTapDown: onSecondary,
|
||||
onSecondaryTapUp: onSecondary,
|
||||
child: ListTile(
|
||||
title: StreamBuilder<MediaItem?>(
|
||||
stream: audioHandler.mediaItem,
|
||||
|
@ -175,7 +175,7 @@ class AlbumTile extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onSecondaryTapDown: onSecondary,
|
||||
onSecondaryTapUp: onSecondary,
|
||||
child: ListTile(
|
||||
title: Text(
|
||||
album!.title!,
|
||||
|
@ -212,7 +212,7 @@ class ArtistTile extends StatelessWidget {
|
|||
borderRadius: const BorderRadius.all(Radius.circular(4.0)),
|
||||
onTap: onTap,
|
||||
onLongPress: normalizeSecondary(onSecondary),
|
||||
onSecondaryTapDown: onSecondary,
|
||||
onSecondaryTapUp: onSecondary,
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: <Widget>[
|
||||
const SizedBox(height: 4.0),
|
||||
CachedImage(
|
||||
|
@ -256,7 +256,7 @@ class PlaylistTile extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onSecondaryTapDown: onSecondary,
|
||||
onSecondaryTapUp: onSecondary,
|
||||
child: ListTile(
|
||||
title: Text(
|
||||
playlist!.title!,
|
||||
|
@ -317,7 +317,7 @@ class PlaylistCardTile extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onSecondaryTapDown: onSecondary,
|
||||
onSecondaryTapUp: onSecondary,
|
||||
child: SizedBox(
|
||||
height: 180.0,
|
||||
child: InkWell(
|
||||
|
@ -814,7 +814,7 @@ class ShowEpisodeTile extends StatelessWidget {
|
|||
return InkWell(
|
||||
onTap: onTap,
|
||||
onLongPress: normalizeSecondary(onSecondary),
|
||||
onSecondaryTapDown: onSecondary,
|
||||
onSecondaryTapUp: onSecondary,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
|
|
|
@ -6,16 +6,12 @@
|
|||
|
||||
#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);
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
desktop_webview_window
|
||||
dynamic_color
|
||||
isar_flutter_libs
|
||||
media_kit_libs_linux
|
||||
|
|
|
@ -8,7 +8,6 @@ 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
|
||||
|
@ -24,7 +23,6 @@ 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"))
|
||||
|
|
16
pubspec.lock
16
pubspec.lock
|
@ -322,14 +322,6 @@ 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: "direct main"
|
||||
description:
|
||||
|
@ -354,14 +346,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.3"
|
||||
draggable_scrollbar:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: draggable_scrollbar
|
||||
sha256: a906e27fc1ee056e2942d66989dd0cb5b70a361e7d44af566cfa1b584054eac3
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.0"
|
||||
dynamic_color:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
289
pubspec.yaml
289
pubspec.yaml
|
@ -3,7 +3,7 @@ description: Freezer
|
|||
|
||||
# The following line prevents the package from being accidentally published to
|
||||
# pub.dev using `pub publish`. This is preferred for private packages.
|
||||
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||
publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
||||
|
||||
# The following defines the version and build number for your application.
|
||||
# A version number is three numbers separated by dots, like 1.2.43
|
||||
|
@ -18,163 +18,162 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
|||
version: 0.6.14+1
|
||||
|
||||
environment:
|
||||
sdk: '>=3.0.0 <4.0.0'
|
||||
sdk: ">=3.0.0 <4.0.0"
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
flutter_localizations:
|
||||
sdk: flutter
|
||||
spotify: ^0.12.0
|
||||
flutter_displaymode: ^0.6.0
|
||||
crypto: ^3.0.3
|
||||
http: ^1.1.0
|
||||
cookie_jar: ^4.0.8
|
||||
json_annotation: ^4.8.1
|
||||
path_provider: ^2.0.15
|
||||
path: ^1.6.4
|
||||
sqflite: ^2.0.0+3
|
||||
permission_handler: ^10.4.3
|
||||
intl: ^0.18.0
|
||||
filesize: ^2.0.1
|
||||
fluttertoast: ^8.0.8
|
||||
palette_generator: ^0.3.0
|
||||
flutter_material_color_picker: ^1.0.5
|
||||
flutter_inappwebview: ^5.3.2
|
||||
country_pickers: ^2.0.0
|
||||
move_to_background: ^1.0.1
|
||||
flutter_local_notifications: ^15.1.0+1
|
||||
collection: ^1.17.1
|
||||
random_string: ^2.0.1
|
||||
async: ^2.6.1
|
||||
html: ^0.15.0
|
||||
flutter_screenutil: ^5.0.0+2
|
||||
marquee: ^2.2.0
|
||||
flutter_cache_manager: ^3.0.0
|
||||
cached_network_image: ^3.1.0
|
||||
i18n_extension: ^9.0.2
|
||||
fluttericon: ^2.0.0
|
||||
url_launcher: ^6.0.5
|
||||
uni_links: ^0.5.1
|
||||
numberpicker: ^2.1.1
|
||||
quick_actions: ^1.0.5
|
||||
photo_view: ^0.14.0
|
||||
draggable_scrollbar: ^0.1.0
|
||||
scrobblenaut:
|
||||
git:
|
||||
url: https://github.com/Pato05/Scrobblenaut.git
|
||||
ref: main
|
||||
open_file: ^3.0.3
|
||||
version: ^3.0.2
|
||||
wakelock_plus: ^1.1.1
|
||||
google_fonts: ^5.1.0
|
||||
equalizer:
|
||||
git: https://github.com/gladson97/equalizer.git
|
||||
audio_session: ^0.1.6
|
||||
audio_service: ^0.18.1
|
||||
provider: ^6.0.0
|
||||
hive_flutter: ^1.1.0
|
||||
connectivity_plus: ^4.0.1
|
||||
share_plus: ^7.0.2
|
||||
disk_space_plus: ^0.2.3
|
||||
dynamic_color: ^1.6.6
|
||||
package_info_plus: ^4.0.2
|
||||
encrypt: ^5.0.1
|
||||
dart_blowfish:
|
||||
git:
|
||||
url: https://github.com/Pato05/dart_blowfish.git
|
||||
ref: main
|
||||
logging: ^1.2.0
|
||||
just_audio: ^0.9.35
|
||||
just_audio_media_kit:
|
||||
git: https://github.com/Pato05/just_audio_media_kit.git
|
||||
rxdart: ^0.27.7
|
||||
flutter_isolate: ^2.0.4
|
||||
isar: ^3.1.0+1
|
||||
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/
|
||||
flutter:
|
||||
sdk: flutter
|
||||
flutter_localizations:
|
||||
sdk: flutter
|
||||
spotify: ^0.12.0
|
||||
flutter_displaymode: ^0.6.0
|
||||
crypto: ^3.0.3
|
||||
http: ^1.1.0
|
||||
cookie_jar: ^4.0.8
|
||||
json_annotation: ^4.8.1
|
||||
path_provider: ^2.0.15
|
||||
path: ^1.6.4
|
||||
sqflite: ^2.0.0+3
|
||||
permission_handler: ^10.4.3
|
||||
intl: ^0.18.0
|
||||
filesize: ^2.0.1
|
||||
fluttertoast: ^8.0.8
|
||||
palette_generator: ^0.3.0
|
||||
flutter_material_color_picker: ^1.0.5
|
||||
flutter_inappwebview: ^5.3.2
|
||||
country_pickers: ^2.0.0
|
||||
move_to_background: ^1.0.1
|
||||
flutter_local_notifications: ^15.1.0+1
|
||||
collection: ^1.17.1
|
||||
random_string: ^2.0.1
|
||||
async: ^2.6.1
|
||||
html: ^0.15.0
|
||||
flutter_screenutil: ^5.0.0+2
|
||||
marquee: ^2.2.0
|
||||
flutter_cache_manager: ^3.0.0
|
||||
cached_network_image: ^3.1.0
|
||||
i18n_extension: ^9.0.2
|
||||
fluttericon: ^2.0.0
|
||||
url_launcher: ^6.0.5
|
||||
uni_links: ^0.5.1
|
||||
numberpicker: ^2.1.1
|
||||
quick_actions: ^1.0.5
|
||||
photo_view: ^0.14.0
|
||||
scrobblenaut:
|
||||
git:
|
||||
url: https://github.com/Pato05/Scrobblenaut.git
|
||||
ref: main
|
||||
open_file: ^3.0.3
|
||||
version: ^3.0.2
|
||||
wakelock_plus: ^1.1.1
|
||||
google_fonts: ^5.1.0
|
||||
equalizer:
|
||||
git: https://github.com/gladson97/equalizer.git
|
||||
audio_session: ^0.1.6
|
||||
audio_service: ^0.18.1
|
||||
provider: ^6.0.0
|
||||
hive_flutter: ^1.1.0
|
||||
connectivity_plus: ^4.0.1
|
||||
share_plus: ^7.0.2
|
||||
disk_space_plus: ^0.2.3
|
||||
dynamic_color: ^1.6.6
|
||||
package_info_plus: ^4.0.2
|
||||
encrypt: ^5.0.1
|
||||
dart_blowfish:
|
||||
git:
|
||||
url: https://github.com/Pato05/dart_blowfish.git
|
||||
ref: main
|
||||
logging: ^1.2.0
|
||||
just_audio: ^0.9.35
|
||||
just_audio_media_kit:
|
||||
git: https://github.com/Pato05/just_audio_media_kit.git
|
||||
rxdart: ^0.27.7
|
||||
flutter_isolate: ^2.0.4
|
||||
isar: ^3.1.0+1
|
||||
isar_flutter_libs: ^3.1.0+1
|
||||
flutter_background_service: ^5.0.1
|
||||
audio_service_mpris: ^0.1.0
|
||||
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/
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
json_serializable: ^6.0.1
|
||||
build_runner: ^2.4.6
|
||||
hive_generator: ^2.0.0
|
||||
flutter_lints: ^2.0.3
|
||||
isar_generator: ^3.1.0+1
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
json_serializable: ^6.0.1
|
||||
build_runner: ^2.4.6
|
||||
hive_generator: ^2.0.0
|
||||
flutter_lints: ^2.0.3
|
||||
isar_generator: ^3.1.0+1
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
# following page: https://dart.dev/tools/pub/pubspec
|
||||
|
||||
# The following section is specific to Flutter.
|
||||
flutter:
|
||||
# The following line ensures that the Material Icons font is
|
||||
# included with your application, so that you can use the icons in
|
||||
# the material Icons class.
|
||||
uses-material-design: true
|
||||
# The following line ensures that the Material Icons font is
|
||||
# included with your application, so that you can use the icons in
|
||||
# the material Icons class.
|
||||
uses-material-design: true
|
||||
|
||||
# To add assets to your application, add an assets section, like this:
|
||||
# assets:
|
||||
# - images/a_dot_burr.jpeg
|
||||
# - images/a_dot_ham.jpeg
|
||||
# To add assets to your application, add an assets section, like this:
|
||||
# assets:
|
||||
# - images/a_dot_burr.jpeg
|
||||
# - images/a_dot_ham.jpeg
|
||||
|
||||
assets:
|
||||
- assets/cover.jpg
|
||||
- assets/cover_thumb.jpg
|
||||
- assets/icon.png
|
||||
- assets/favorites_thumb.jpg
|
||||
- assets/browse_icon.png
|
||||
assets:
|
||||
- assets/cover.jpg
|
||||
- assets/cover_thumb.jpg
|
||||
- assets/icon.png
|
||||
- assets/favorites_thumb.jpg
|
||||
- assets/browse_icon.png
|
||||
|
||||
fonts:
|
||||
# - family: Montserrat
|
||||
# fonts:
|
||||
# - asset: assets/fonts/Montserrat-Regular.ttf
|
||||
# - asset: assets/fonts/Montserrat-Bold.ttf
|
||||
# weight: 700
|
||||
# - asset: assets/fonts/Montserrat-Italic.ttf
|
||||
# style: italic
|
||||
- family: MabryPro
|
||||
fonts:
|
||||
- asset: assets/fonts/MabryPro.otf
|
||||
- asset: assets/fonts/MabryProItalic.otf
|
||||
style: italic
|
||||
- asset: assets/fonts/MabryProBold.otf
|
||||
weight: 700
|
||||
- asset: assets/fonts/MabryProBlack.otf
|
||||
weight: 900
|
||||
fonts:
|
||||
# - family: Montserrat
|
||||
# fonts:
|
||||
# - asset: assets/fonts/Montserrat-Regular.ttf
|
||||
# - asset: assets/fonts/Montserrat-Bold.ttf
|
||||
# weight: 700
|
||||
# - asset: assets/fonts/Montserrat-Italic.ttf
|
||||
# style: italic
|
||||
- family: MabryPro
|
||||
fonts:
|
||||
- asset: assets/fonts/MabryPro.otf
|
||||
- asset: assets/fonts/MabryProItalic.otf
|
||||
style: italic
|
||||
- asset: assets/fonts/MabryProBold.otf
|
||||
weight: 700
|
||||
- asset: assets/fonts/MabryProBlack.otf
|
||||
weight: 900
|
||||
|
||||
# An image asset can refer to one or more resolution-specific "variants", see
|
||||
# https://flutter.dev/assets-and-images/#resolution-aware.
|
||||
# An image asset can refer to one or more resolution-specific "variants", see
|
||||
# https://flutter.dev/assets-and-images/#resolution-aware.
|
||||
|
||||
# For details regarding adding assets from package dependencies, see
|
||||
# https://flutter.dev/assets-and-images/#from-packages
|
||||
# For details regarding adding assets from package dependencies, see
|
||||
# https://flutter.dev/assets-and-images/#from-packages
|
||||
|
||||
# To add custom fonts to your application, add a fonts section here,
|
||||
# in this "flutter" section. Each entry in this list should have a
|
||||
# "family" key with the font family name, and a "fonts" key with a
|
||||
# list giving the asset and other descriptors for the font. For
|
||||
# example:
|
||||
# fonts:
|
||||
# - family: Schyler
|
||||
# fonts:
|
||||
# - asset: fonts/Schyler-Regular.ttf
|
||||
# - asset: fonts/Schyler-Italic.ttf
|
||||
# style: italic
|
||||
# - family: Trajan Pro
|
||||
# fonts:
|
||||
# - asset: fonts/TrajanPro.ttf
|
||||
# - asset: fonts/TrajanPro_Bold.ttf
|
||||
# weight: 700
|
||||
#
|
||||
# For details regarding fonts from package dependencies,
|
||||
# see https://flutter.dev/custom-fonts/#from-packages
|
||||
# To add custom fonts to your application, add a fonts section here,
|
||||
# in this "flutter" section. Each entry in this list should have a
|
||||
# "family" key with the font family name, and a "fonts" key with a
|
||||
# list giving the asset and other descriptors for the font. For
|
||||
# example:
|
||||
# fonts:
|
||||
# - family: Schyler
|
||||
# fonts:
|
||||
# - asset: fonts/Schyler-Regular.ttf
|
||||
# - asset: fonts/Schyler-Italic.ttf
|
||||
# style: italic
|
||||
# - family: Trajan Pro
|
||||
# fonts:
|
||||
# - asset: fonts/TrajanPro.ttf
|
||||
# - asset: fonts/TrajanPro_Bold.ttf
|
||||
# weight: 700
|
||||
#
|
||||
# For details regarding fonts from package dependencies,
|
||||
# see https://flutter.dev/custom-fonts/#from-packages
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
#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>
|
||||
|
@ -18,8 +17,6 @@
|
|||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
ConnectivityPlusWindowsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
|
||||
DesktopWebviewWindowPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("DesktopWebviewWindowPlugin"));
|
||||
DynamicColorPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("DynamicColorPluginCApi"));
|
||||
IsarFlutterLibsPluginRegisterWithRegistrar(
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
connectivity_plus
|
||||
desktop_webview_window
|
||||
dynamic_color
|
||||
isar_flutter_libs
|
||||
media_kit_libs_windows_audio
|
||||
|
|
Loading…
Reference in New Issue