save more battery now, few other changes idr

This commit is contained in:
Pato05 2024-01-28 12:02:59 +01:00
parent faec2af805
commit 8679cf844f
No known key found for this signature in database
GPG key ID: ED4C6F9C3D574FB6
15 changed files with 447 additions and 289 deletions

View file

@ -146,37 +146,40 @@ class DeezerAPI {
//Wrapper so it can be globally awaited //Wrapper so it can be globally awaited
Future<bool> authorize() async => _authorizing ??= rawAuthorize(); Future<bool> authorize() async => _authorizing ??= rawAuthorize();
//Login with email FROM DEEMIX-JS // NOT WORKING ANYMORE.
Future<String> getArlByEmail(String email, String password) async { // this didn't last very long now, did it?
//Get MD5 of password
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"];
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));
print(res.data);
final accessToken = res.data['access_token'] as String?;
if (accessToken == null) {
throw Exception('login failed, access token is null');
}
print(accessToken); // //Login with email FROM DEEMIX-JS
// Future<String> getArlByEmail(String email, String password) async {
return getArlByAccessToken(accessToken); // //Get MD5 of password
} // 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"];
// 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));
// print(res.data);
// final accessToken = res.data['access_token'] as String?;
// if (accessToken == null) {
// throw Exception('login failed, access token is null');
// }
//
// print(accessToken);
//
// return getArlByAccessToken(accessToken);
// }
// FROM DEEMIX-JS // FROM DEEMIX-JS
Future<String> getArlByAccessToken(String accessToken) async { Future<String> getArlByAccessToken(String accessToken) async {

View file

@ -14,13 +14,18 @@ class Paths {
final target = final target =
await Directory(path.join(home, '.local', 'share', 'freezer')) await Directory(path.join(home, '.local', 'share', 'freezer'))
.create(); .create();
if (kDebugMode) {
return (await Directory(path.join(target.path, 'debug')).create())
.path;
}
return target.path; return target.path;
} }
return path.dirname(Platform.resolvedExecutable); return path.dirname(Platform.resolvedExecutable);
case TargetPlatform.windows: case TargetPlatform.windows:
final String? localAppData = Platform.environment['LOCALAPPDATA']; final String? localAppData = Platform.environment['LOCALAPPDATA'];
if (localAppData != null) { if (localAppData != null) {
final target = await Directory(path.join(localAppData, 'Freezer')).create(); final target =
await Directory(path.join(localAppData, 'Freezer')).create();
return target.path; return target.path;
} }
String? home = Platform.environment['USERPROFILE']; String? home = Platform.environment['USERPROFILE'];
@ -35,7 +40,12 @@ class Paths {
} }
final target = final target =
await Directory(path.join(home, 'AppData', 'Local', 'Freezer')).create(); await Directory(path.join(home, 'AppData', 'Local', 'Freezer'))
.create();
if (kDebugMode) {
return (await Directory(path.join(target.path, 'debug')).create())
.path;
}
return target.path; return target.path;
default: default:
return (await getApplicationDocumentsDirectory()).path; return (await getApplicationDocumentsDirectory()).path;

View file

@ -171,8 +171,8 @@ class AudioPlayerTask extends BaseAudioHandler {
); );
if (initArgs.ignoreInterruptions) { if (initArgs.ignoreInterruptions) {
session.interruptionEventStream.listen((_) {}); //session.interruptionEventStream.listen((_) {});
session.becomingNoisyEventStream.listen((_) {}); //session.becomingNoisyEventStream.listen((_) {});
} }
//Update track index //Update track index

View file

@ -39,6 +39,13 @@ class PlayerHelper {
final _bufferPositionSubject = BehaviorSubject<Duration>(); final _bufferPositionSubject = BehaviorSubject<Duration>();
ValueStream<Duration> get bufferPosition => _bufferPositionSubject.stream; ValueStream<Duration> get bufferPosition => _bufferPositionSubject.stream;
final _playingSubject = BehaviorSubject<bool>();
ValueStream<bool> get playing => _playingSubject.stream;
final _processingStateSubject = BehaviorSubject<AudioProcessingState>();
ValueStream<AudioProcessingState> get processingState =>
_processingStateSubject.stream;
/// Find queue index by id /// Find queue index by id
/// ///
/// The function gets more expensive the longer the queue is and the further the element is from the beginning. /// The function gets more expensive the longer the queue is and the further the element is from the beginning.
@ -60,13 +67,13 @@ class PlayerHelper {
builder: () => AudioPlayerTask(initArgs), builder: () => AudioPlayerTask(initArgs),
config: AudioServiceConfig( config: AudioServiceConfig(
notificationColor: settings.primaryColor, notificationColor: settings.primaryColor,
androidStopForegroundOnPause: false, androidStopForegroundOnPause: true,
androidNotificationOngoing: false, androidNotificationOngoing: true,
androidNotificationClickStartsActivity: true, androidNotificationClickStartsActivity: true,
androidNotificationChannelDescription: 'Freezer', androidNotificationChannelDescription: 'Freezer',
androidNotificationChannelName: 'Freezer', androidNotificationChannelName: 'Freezer',
androidNotificationIcon: 'drawable/ic_logo', androidNotificationIcon: 'drawable/ic_logo',
preloadArtwork: false, preloadArtwork: true,
), ),
cacheManager: cacheManager, cacheManager: cacheManager,
); );
@ -81,8 +88,6 @@ class PlayerHelper {
Logger('PlayerHelper').fine("event received: ${event['action']}"); Logger('PlayerHelper').fine("event received: ${event['action']}");
switch (event['action']) { switch (event['action']) {
case 'onLoad': case 'onLoad':
//After audio_service is loaded, load queue, set quality
await settings.updateAudioServiceQuality();
break; break;
case 'onRestore': case 'onRestore':
//Load queueSource from isolate //Load queueSource from isolate
@ -140,6 +145,21 @@ class PlayerHelper {
cache.history.add(Track.fromMediaItem(mediaItem)); cache.history.add(Track.fromMediaItem(mediaItem));
cache.save(); cache.save();
}); });
_playbackStateStreamSubscription =
audioHandler.playbackState.listen((playbackState) {
if (!_processingStateSubject.hasValue ||
_processingStateSubject.value != playbackState.processingState) {
_processingStateSubject.add(playbackState.processingState);
}
print(
'now ${playbackState.playing}, previous ${_playingSubject.valueOrNull}');
if (!_playingSubject.hasValue ||
_playingSubject.value != playbackState.playing) {
print('added!');
_playingSubject.add(playbackState.playing);
}
});
//Start audio_service //Start audio_service
// await startService(); it is already ready, there is no need to start it // await startService(); it is already ready, there is no need to start it
@ -178,16 +198,15 @@ class PlayerHelper {
} }
//Executed before exit //Executed before exit
Future onExit() async { Future stop() async {
_customEventSubscription.cancel(); _customEventSubscription.cancel();
_playbackStateStreamSubscription.cancel(); _playbackStateStreamSubscription.cancel();
_mediaItemSubscription.cancel(); _mediaItemSubscription.cancel();
_started = false;
} }
//Replace queue, play specified track id //Replace queue, play specified track id
Future<void> _loadQueuePlay(List<MediaItem> queue, int? index) async { Future<void> _loadQueuePlay(List<MediaItem> queue, int? index) async {
await settings.updateAudioServiceQuality();
if (index != null) { if (index != null) {
await audioHandler.customAction('setIndex', {'index': index}); await audioHandler.customAction('setIndex', {'index': index});
} }
@ -270,7 +289,6 @@ class PlayerHelper {
//Load and play //Load and play
// await startService(); // audioservice is ready // await startService(); // audioservice is ready
await settings.updateAudioServiceQuality();
await setQueueSource(queueSource); await setQueueSource(queueSource);
await audioHandler.customAction('setIndex', {'index': index}); await audioHandler.customAction('setIndex', {'index': index});
await audioHandler.updateQueue(queue); await audioHandler.updateQueue(queue);

View file

@ -113,7 +113,7 @@ void main() async {
DefaultCacheManager.key, DefaultCacheManager.key,
// cache aggressively // cache aggressively
stalePeriod: const Duration(days: 30), stalePeriod: const Duration(days: 30),
maxNrOfCacheObjects: 1000, maxNrOfCacheObjects: 5000,
)); ));
// cacheManager = HiveCacheManager( // cacheManager = HiveCacheManager(
// boxName: 'freezer-images', boxPath: await Paths.cacheDir()); // boxName: 'freezer-images', boxPath: await Paths.cacheDir());
@ -142,32 +142,32 @@ class FreezerApp extends StatefulWidget {
State<FreezerApp> createState() => _FreezerAppState(); State<FreezerApp> createState() => _FreezerAppState();
} }
class _FreezerAppState extends State<FreezerApp> { class _FreezerAppState extends State<FreezerApp> with WidgetsBindingObserver {
late StreamSubscription _playbackStateSub;
@override @override
void initState() { void initState() {
_initStateAsync(); WidgetsBinding.instance.addObserver(this);
super.initState(); super.initState();
} }
Future<void> _initStateAsync() async { @override
_playbackStateChanged(audioHandler.playbackState.value); void didChangeAppLifecycleState(AppLifecycleState state) {
_playbackStateSub = switch (state) {
audioHandler.playbackState.listen(_playbackStateChanged); case AppLifecycleState.paused:
} playerHelper.stop();
break;
case AppLifecycleState.resumed:
playerHelper.start();
break;
Future<void> _playbackStateChanged(PlaybackState playbackState) async { default:
if (playbackState.processingState == AudioProcessingState.idle || print('lifecycle: $state');
playbackState.processingState == AudioProcessingState.error) {
// TODO: reconnect maybe?
return;
} }
} }
@override @override
void dispose() { void dispose() {
_playbackStateSub.cancel(); WidgetsBinding.instance.removeObserver(this);
playerHelper.stop();
super.dispose(); super.dispose();
} }
@ -191,13 +191,35 @@ class _FreezerAppState extends State<FreezerApp> {
builder: (context, child) => builder: (context, child) =>
DynamicColorBuilder(builder: (lightScheme, darkScheme) { DynamicColorBuilder(builder: (lightScheme, darkScheme) {
final lightTheme = settings.materialYouAccent final lightTheme = settings.materialYouAccent
? ThemeData(colorScheme: lightScheme, useMaterial3: true) ? ThemeData(
colorScheme: lightScheme,
useMaterial3: true,
appBarTheme: const AppBarTheme(
systemOverlayStyle: SystemUiOverlayStyle(
statusBarBrightness: Brightness.dark,
statusBarIconBrightness: Brightness.dark,
systemNavigationBarIconBrightness: Brightness.dark,
statusBarColor: Colors.transparent,
systemNavigationBarColor: Colors.transparent,
systemNavigationBarDividerColor: Colors.transparent,
)),
)
: settings.themeData; : settings.themeData;
final darkTheme = settings.materialYouAccent final darkTheme = settings.materialYouAccent
? ThemeData( ? ThemeData(
colorScheme: darkScheme, colorScheme: darkScheme,
useMaterial3: true, useMaterial3: true,
brightness: Brightness.dark) brightness: Brightness.dark,
appBarTheme: const AppBarTheme(
systemOverlayStyle: SystemUiOverlayStyle(
statusBarBrightness: Brightness.light,
statusBarIconBrightness: Brightness.light,
systemNavigationBarIconBrightness: Brightness.light,
statusBarColor: Colors.transparent,
systemNavigationBarColor: Colors.transparent,
systemNavigationBarDividerColor: Colors.transparent,
)),
)
: null; : null;
return MaterialApp( return MaterialApp(
title: 'Freezer', title: 'Freezer',
@ -269,13 +291,13 @@ class _LoginMainWrapperState extends State<LoginMainWrapper> {
} }
Future _logOut() async { Future _logOut() async {
await deezerAPI.logout();
await settings.save();
await Cache.wipe();
setState(() { setState(() {
settings.arl = null; settings.arl = null;
settings.offlineMode = false; settings.offlineMode = false;
}); });
await deezerAPI.logout();
await settings.save();
await Cache.wipe();
} }
@override @override
@ -672,18 +694,17 @@ class MainScreenState extends State<MainScreen>
onKey: _handleKey, onKey: _handleKey,
child: LayoutBuilder(builder: (context, constraints) { child: LayoutBuilder(builder: (context, constraints) {
// check if we're able to display the desktop layout // check if we're able to display the desktop layout
if (Platform.isLinux || Platform.isWindows || Platform.isMacOS) {
final isLandscape = constraints.maxWidth > constraints.maxHeight; final isLandscape = constraints.maxWidth > constraints.maxHeight;
isDesktop = isLandscape && isDesktop = isLandscape &&
constraints.maxWidth >= 1100 && constraints.maxWidth >= 1100 &&
constraints.maxHeight >= 600; constraints.maxHeight >= 600;
}
return FancyScaffold( return FancyScaffold(
key: _fancyScaffoldKey, key: _fancyScaffoldKey,
navigationRail: _buildNavigationRail(isDesktop), navigationRail: _buildNavigationRail(isDesktop),
bottomNavigationBar: buildBottomBar(isDesktop), bottomNavigationBar: buildBottomBar(isDesktop),
bottomPanel: Builder( bottomPanel: Builder(
builder: (context) => PlayerBar( builder: (context) => PlayerBar(
backgroundColor: Theme.of(context).cardColor,
focusNode: playerBarFocusNode, focusNode: playerBarFocusNode,
onTap: FancyScaffold.of(context)!.openPanel, onTap: FancyScaffold.of(context)!.openPanel,
shouldHaveHero: false, shouldHaveHero: false,

View file

@ -1,4 +1,5 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:freezer/api/definitions.dart'; import 'package:freezer/api/definitions.dart';
import 'package:freezer/api/download.dart'; import 'package:freezer/api/download.dart';
import 'package:freezer/api/player/audio_handler.dart'; import 'package:freezer/api/player/audio_handler.dart';
@ -170,6 +171,9 @@ class Settings {
NavigationRailAppearance navigationRailAppearance = NavigationRailAppearance navigationRailAppearance =
NavigationRailAppearance.expand_on_hover; NavigationRailAppearance.expand_on_hover;
@HiveField(49, defaultValue: true)
bool enableMaterial3PlayButton = true;
static LazyBox<Settings>? __box; static LazyBox<Settings>? __box;
static Future<LazyBox<Settings>> get _box async => static Future<LazyBox<Settings>> get _box async =>
__box ??= await Hive.openLazyBox<Settings>('settings'); __box ??= await Hive.openLazyBox<Settings>('settings');
@ -267,12 +271,6 @@ class Settings {
downloadManager.updateServiceSettings(); downloadManager.updateServiceSettings();
} }
Future<void> updateAudioServiceQuality() async {
//Send wifi & mobile quality to audio service isolate
await audioHandler.customAction('updateQuality',
{'mobileQuality': mobileQuality, 'wifiQuality': wifiQuality});
}
// MaterialColor get _primarySwatch => // MaterialColor get _primarySwatch =>
// MaterialColor(primaryColor.value, <int, Color>{ // MaterialColor(primaryColor.value, <int, Color>{
// 50: primaryColor.withOpacity(.1), // 50: primaryColor.withOpacity(.1),
@ -319,6 +317,15 @@ class Settings {
sliderTheme: _sliderTheme, sliderTheme: _sliderTheme,
bottomAppBarTheme: const BottomAppBarTheme(color: Color(0xfff5f5f5)), bottomAppBarTheme: const BottomAppBarTheme(color: Color(0xfff5f5f5)),
useMaterial3: true, useMaterial3: true,
appBarTheme: const AppBarTheme(
systemOverlayStyle: SystemUiOverlayStyle(
statusBarBrightness: Brightness.dark,
statusBarIconBrightness: Brightness.dark,
systemNavigationBarIconBrightness: Brightness.dark,
statusBarColor: Colors.transparent,
systemNavigationBarColor: Colors.transparent,
systemNavigationBarDividerColor: Colors.transparent,
)),
), ),
Themes.Dark: ThemeData( Themes.Dark: ThemeData(
textTheme: textTheme, textTheme: textTheme,
@ -331,6 +338,15 @@ class Settings {
), ),
sliderTheme: _sliderTheme, sliderTheme: _sliderTheme,
useMaterial3: true, useMaterial3: true,
appBarTheme: const AppBarTheme(
systemOverlayStyle: SystemUiOverlayStyle(
statusBarBrightness: Brightness.light,
statusBarIconBrightness: Brightness.light,
systemNavigationBarIconBrightness: Brightness.light,
statusBarColor: Colors.transparent,
systemNavigationBarColor: Colors.transparent,
systemNavigationBarDividerColor: Colors.transparent,
)),
), ),
Themes.Deezer: ThemeData( Themes.Deezer: ThemeData(
textTheme: textTheme, textTheme: textTheme,
@ -349,6 +365,15 @@ class Settings {
const BottomSheetThemeData(backgroundColor: deezerBottom), const BottomSheetThemeData(backgroundColor: deezerBottom),
cardColor: deezerBg, cardColor: deezerBg,
useMaterial3: true, useMaterial3: true,
appBarTheme: const AppBarTheme(
systemOverlayStyle: SystemUiOverlayStyle(
statusBarBrightness: Brightness.light,
statusBarIconBrightness: Brightness.light,
systemNavigationBarIconBrightness: Brightness.light,
statusBarColor: Colors.transparent,
systemNavigationBarColor: Colors.transparent,
systemNavigationBarDividerColor: Colors.transparent,
)),
), ),
Themes.Black: ThemeData( Themes.Black: ThemeData(
textTheme: textTheme, textTheme: textTheme,
@ -370,6 +395,15 @@ class Settings {
BottomSheetThemeData(backgroundColor: _elevation1Black), BottomSheetThemeData(backgroundColor: _elevation1Black),
cardColor: _elevation1Black, cardColor: _elevation1Black,
useMaterial3: true, useMaterial3: true,
appBarTheme: const AppBarTheme(
systemOverlayStyle: SystemUiOverlayStyle(
statusBarBrightness: Brightness.light,
statusBarIconBrightness: Brightness.light,
systemNavigationBarIconBrightness: Brightness.light,
statusBarColor: Colors.transparent,
systemNavigationBarColor: Colors.transparent,
systemNavigationBarDividerColor: Colors.transparent,
)),
) )
}; };

View file

@ -1,5 +1,6 @@
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:freezer/api/deezer.dart'; import 'package:freezer/api/deezer.dart';
import 'package:freezer/api/definitions.dart'; import 'package:freezer/api/definitions.dart';
import 'package:freezer/api/player/audio_handler.dart'; import 'package:freezer/api/player/audio_handler.dart';
@ -54,6 +55,9 @@ class HomeScreen extends StatelessWidget {
PointerDeviceKind.trackpad PointerDeviceKind.trackpad
}, },
), ),
child: AnnotatedRegion<SystemUiOverlayStyle>(
value: Theme.of(context).appBarTheme.systemOverlayStyle ??
const SystemUiOverlayStyle(),
child: SafeArea( child: SafeArea(
child: Scaffold( child: Scaffold(
body: NestedScrollView( body: NestedScrollView(
@ -66,6 +70,7 @@ class HomeScreen extends StatelessWidget {
), ),
), ),
), ),
),
); );
} }
} }

View file

@ -209,19 +209,19 @@ class _LoginWidgetState extends State<LoginWidget> {
style: const TextStyle(fontSize: 16.0), style: const TextStyle(fontSize: 16.0),
), ),
const SizedBox(height: 16.0), const SizedBox(height: 16.0),
//Email login dialog //Email login dialog (Not working anymore)
ElevatedButton( // ElevatedButton(
child: Text( // child: Text(
'Login using email'.i18n, // 'Login using email'.i18n,
), // ),
onPressed: () { // onPressed: () {
showDialog( // showDialog(
context: context, // context: context,
builder: (context) => // builder: (context) =>
EmailLogin(_update)); // EmailLogin(_update));
}, // },
), // ),
const SizedBox(height: 2.0), // const SizedBox(height: 2.0),
// only supported on android // only supported on android
if (Platform.isAndroid) if (Platform.isAndroid)
@ -345,7 +345,7 @@ class LoginBrowser extends StatelessWidget {
controller.evaluateJavascript( controller.evaluateJavascript(
source: 'window.location.href = "/open_app"'); source: 'window.location.href = "/open_app"');
} }
print('scheme ${uri.scheme}, host: ${uri.host}'); print('uri $uri');
//Parse arl from url //Parse arl from url
if (uri.scheme == 'intent' && uri.host == 'deezer.page.link') { if (uri.scheme == 'intent' && uri.host == 'deezer.page.link') {
try { try {
@ -367,107 +367,109 @@ class LoginBrowser extends StatelessWidget {
} }
} }
class EmailLogin extends StatefulWidget { // email login is removed cuz not working = USELESS
final Function callback;
const EmailLogin(this.callback, {Key? key}) : super(key: key);
@override //class EmailLogin extends StatefulWidget {
State<EmailLogin> createState() => _EmailLoginState(); // final Function callback;
} // const EmailLogin(this.callback, {Key? key}) : super(key: key);
//
class _EmailLoginState extends State<EmailLogin> { // @override
final _emailController = TextEditingController(); // State<EmailLogin> createState() => _EmailLoginState();
final _passwordController = TextEditingController(); //}
bool _loading = false; //
//class _EmailLoginState extends State<EmailLogin> {
Future _login() async { // final _emailController = TextEditingController();
setState(() => _loading = true); // final _passwordController = TextEditingController();
//Try logging in // bool _loading = false;
String? arl; //
String? exception; // Future _login() async {
try { // setState(() => _loading = true);
arl = await deezerAPI.getArlByEmail( // //Try logging in
_emailController.text, _passwordController.text); // String? arl;
} catch (e, st) { // String? exception;
exception = e.toString(); // try {
print(e); // arl = await deezerAPI.getArlByEmail(
print(st); // _emailController.text, _passwordController.text);
} // } catch (e, st) {
setState(() => _loading = false); // exception = e.toString();
// print(e);
//Success // print(st);
if (arl != null) { // }
settings.arl = arl; // setState(() => _loading = false);
Navigator.of(context).pop(); //
widget.callback(); // //Success
return; // if (arl != null) {
} // settings.arl = arl;
// Navigator.of(context).pop();
//Error // widget.callback();
showDialog( // return;
context: context, // }
builder: (context) => AlertDialog( //
title: Text("Error logging in!".i18n), // //Error
content: Text( // showDialog(
"Error logging in using email, please check your credentials.\nError: ${exception ?? 'Unknown'}"), // context: context,
actions: [ // builder: (context) => AlertDialog(
TextButton( // title: Text("Error logging in!".i18n),
child: Text('Dismiss'.i18n), // content: Text(
onPressed: () { // "Error logging in using email, please check your credentials.\nError: ${exception ?? 'Unknown'}"),
Navigator.of(context).pop(); // actions: [
}, // TextButton(
) // child: Text('Dismiss'.i18n),
], // onPressed: () {
)); // Navigator.of(context).pop();
} // },
// )
@override // ],
Widget build(BuildContext context) { // ));
return AlertDialog( // }
title: Text('Email Login'.i18n), //
content: AutofillGroup( // @override
child: Column( // Widget build(BuildContext context) {
mainAxisSize: MainAxisSize.min, // return AlertDialog(
children: [ // title: Text('Email Login'.i18n),
TextFormField( // content: AutofillGroup(
enabled: !_loading, // child: Column(
decoration: InputDecoration(labelText: 'Email'.i18n), // mainAxisSize: MainAxisSize.min,
controller: _emailController, // children: [
autofillHints: const [ // TextFormField(
AutofillHints.email, // enabled: !_loading,
], // decoration: InputDecoration(labelText: 'Email'.i18n),
), // controller: _emailController,
const SizedBox(height: 8.0), // autofillHints: const [
TextFormField( // AutofillHints.email,
enabled: !_loading, // ],
obscureText: true, // ),
decoration: InputDecoration(labelText: "Password".i18n), // const SizedBox(height: 8.0),
controller: _passwordController, // TextFormField(
autofillHints: const [ // enabled: !_loading,
AutofillHints.password, // obscureText: true,
], // decoration: InputDecoration(labelText: "Password".i18n),
) // controller: _passwordController,
], // autofillHints: const [
), // AutofillHints.password,
), // ],
actions: [ // )
TextButton( // ],
onPressed: _loading // ),
? null // ),
: () async { // actions: [
if (_emailController.text.isNotEmpty && // TextButton(
_passwordController.text.isNotEmpty) { // onPressed: _loading
await _login(); // ? null
} else { // : () async {
ScaffoldMessenger.of(context) // if (_emailController.text.isNotEmpty &&
.snack("Missing email or password!".i18n); // _passwordController.text.isNotEmpty) {
} // await _login();
}, // } else {
child: _loading // ScaffoldMessenger.of(context)
? const CircularProgressIndicator() // .snack("Missing email or password!".i18n);
: const Text('Login'), // }
) // },
], // child: _loading
); // ? const CircularProgressIndicator()
} // : const Text('Login'),
} // )
// ],
// );
// }
// }

View file

@ -45,12 +45,14 @@ class LyricsWidget extends StatefulWidget {
State<LyricsWidget> createState() => _LyricsWidgetState(); State<LyricsWidget> createState() => _LyricsWidgetState();
} }
class _LyricsWidgetState extends State<LyricsWidget> { class _LyricsWidgetState extends State<LyricsWidget>
with WidgetsBindingObserver {
late StreamSubscription _mediaItemSub; late StreamSubscription _mediaItemSub;
late StreamSubscription _playbackStateSub; late StreamSubscription _playbackStateSub;
int? _currentIndex = -1; int? _currentIndex = -1;
Duration _nextOffset = Duration.zero; Duration _nextOffset = Duration.zero;
Duration _currentOffset = Duration.zero; Duration _currentOffset = Duration.zero;
String? _currentTrackId;
final ScrollController _controller = ScrollController(); final ScrollController _controller = ScrollController();
final double height = 90; final double height = 90;
BoxConstraints? _widgetConstraints; BoxConstraints? _widgetConstraints;
@ -64,8 +66,12 @@ class _LyricsWidgetState extends State<LyricsWidget> {
bool _syncedLyrics = false; bool _syncedLyrics = false;
Future<void> _loadForId(String trackId) async { Future<void> _loadForId(String trackId) async {
if (_currentTrackId == trackId) return;
_currentTrackId = trackId;
print('cancelling req?');
// cancel current request, if applicable // cancel current request, if applicable
_lyricsCancelToken?.cancel(); _lyricsCancelToken?.cancel();
_currentIndex = -1; _currentIndex = -1;
_currentOffset = Duration.zero; _currentOffset = Duration.zero;
_nextOffset = Duration.zero; _nextOffset = Duration.zero;
@ -83,6 +89,7 @@ class _LyricsWidgetState extends State<LyricsWidget> {
_lyricsCancelToken = CancelToken(); _lyricsCancelToken = CancelToken();
final lyrics = final lyrics =
await deezerAPI.lyrics(trackId, cancelToken: _lyricsCancelToken); await deezerAPI.lyrics(trackId, cancelToken: _lyricsCancelToken);
_syncedLyrics = lyrics.sync; _syncedLyrics = lyrics.sync;
if (!mounted) return; if (!mounted) return;
setState(() { setState(() {
@ -95,10 +102,14 @@ class _LyricsWidgetState extends State<LyricsWidget> {
} on DioException catch (e) { } on DioException catch (e) {
if (e.type != DioExceptionType.cancel) rethrow; if (e.type != DioExceptionType.cancel) rethrow;
} catch (e) { } catch (e) {
_currentTrackId = null;
if (!mounted) return; if (!mounted) return;
setState(() { setState(() {
_error = e; _error = e;
}); });
} finally {
_lyricsCancelToken =
null; // dispose of cancel token after lyrics are fetched.
} }
} }
@ -115,8 +126,6 @@ class _LyricsWidgetState extends State<LyricsWidget> {
scrollTo = minScroll - widgetHeight / 2 + height / 2; scrollTo = minScroll - widgetHeight / 2 + height / 2;
} }
print(
'${height * _currentIndex!}, ${MediaQuery.of(context).size.height / 2}');
if (scrollTo < 0.0) scrollTo = 0.0; if (scrollTo < 0.0) scrollTo = 0.0;
if (scrollTo > _controller.position.maxScrollExtent) { if (scrollTo > _controller.position.maxScrollExtent) {
scrollTo = _controller.position.maxScrollExtent; scrollTo = _controller.position.maxScrollExtent;
@ -152,28 +161,49 @@ class _LyricsWidgetState extends State<LyricsWidget> {
_scrollToLyric(); _scrollToLyric();
} }
@override void _makeSubscriptions() {
void initState() {
SchedulerBinding.instance.addPostFrameCallback((_) {
//Enable visualizer
// if (settings.lyricsVisualizer) playerHelper.startVisualizer();
_playbackStateSub = AudioService.position.listen(_updatePosition); _playbackStateSub = AudioService.position.listen(_updatePosition);
});
/// Track change = ~exit~ reload lyrics /// Track change = reload new lyrics
_mediaItemSub = audioHandler.mediaItem.listen((mediaItem) { _mediaItemSub = audioHandler.mediaItem.listen((mediaItem) {
if (mediaItem == null) return; if (mediaItem == null) return;
if (_controller.hasClients) _controller.jumpTo(0.0); if (_controller.hasClients) _controller.jumpTo(0.0);
_loadForId(mediaItem.id); _loadForId(mediaItem.id);
}); });
}
@override
void initState() {
SchedulerBinding.instance.addPostFrameCallback((_) {
//Enable visualizer
// if (settings.lyricsVisualizer) playerHelper.startVisualizer();
_makeSubscriptions();
});
WidgetsBinding.instance.addObserver(this);
super.initState(); super.initState();
} }
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
switch (state) {
case AppLifecycleState.paused:
_mediaItemSub.cancel();
_playbackStateSub.cancel();
break;
case AppLifecycleState.resumed:
_makeSubscriptions();
break;
default:
break;
}
}
@override @override
void dispose() { void dispose() {
_mediaItemSub.cancel(); _mediaItemSub.cancel();
_playbackStateSub.cancel(); _playbackStateSub.cancel();
WidgetsBinding.instance.removeObserver(this);
//Stop visualizer //Stop visualizer
// if (settings.lyricsVisualizer) playerHelper.stopVisualizer(); // if (settings.lyricsVisualizer) playerHelper.stopVisualizer();
super.dispose(); super.dispose();

View file

@ -148,12 +148,12 @@ class MenuSheet {
showModalBottomSheet( showModalBottomSheet(
isScrollControlled: false, // true, isScrollControlled: false, // true,
context: context, context: context,
useSafeArea: true,
builder: (BuildContext context) { builder: (BuildContext context) {
return SafeArea( return ConstrainedBox(
child: ConstrainedBox(
constraints: BoxConstraints( constraints: BoxConstraints(
maxHeight: (MediaQuery.of(context).orientation == maxHeight:
Orientation.landscape) (MediaQuery.of(context).orientation == Orientation.landscape)
? 220 ? 220
: 350, : 350,
), ),
@ -170,7 +170,6 @@ class MenuSheet {
)) ))
.toList(growable: false)), .toList(growable: false)),
), ),
),
); );
}); });
} }
@ -197,13 +196,13 @@ class MenuSheet {
return DraggableScrollableSheet( return DraggableScrollableSheet(
initialChildSize: 0.5, initialChildSize: 0.5,
minChildSize: 0.45, minChildSize: 0.45,
maxChildSize: 0.75, maxChildSize: 0.95,
builder: (context, scrollController) => SafeArea( builder: (context, scrollController) => Material(
child: Material(
type: MaterialType.card, type: MaterialType.card,
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
borderRadius: borderRadius:
const BorderRadius.vertical(top: Radius.circular(20.0)), const BorderRadius.vertical(top: Radius.circular(20.0)),
child: SafeArea(
child: CustomScrollView( child: CustomScrollView(
controller: scrollController, controller: scrollController,
slivers: [ slivers: [

View file

@ -69,6 +69,7 @@ class PlayerBar extends StatelessWidget {
} }
final currentMediaItem = snapshot.data!; final currentMediaItem = snapshot.data!;
final image = CachedImage( final image = CachedImage(
rounded: true,
width: 50, width: 50,
height: 50, height: 50,
url: currentMediaItem.extras!['thumb'] ?? url: currentMediaItem.extras!['thumb'] ??
@ -185,6 +186,7 @@ class PrevNextButton extends StatelessWidget {
class PlayPauseButton extends StatefulWidget { class PlayPauseButton extends StatefulWidget {
final double size; final double size;
final bool filled; final bool filled;
final bool material3;
final Color? iconColor; final Color? iconColor;
/// The color of the card if [filled] is true /// The color of the card if [filled] is true
@ -193,6 +195,7 @@ class PlayPauseButton extends StatefulWidget {
this.size, { this.size, {
Key? key, Key? key,
this.filled = false, this.filled = false,
this.material3 = true,
this.color, this.color,
this.iconColor, this.iconColor,
}) : super(key: key); }) : super(key: key);
@ -205,7 +208,8 @@ class _PlayPauseButtonState extends State<PlayPauseButton>
with SingleTickerProviderStateMixin { with SingleTickerProviderStateMixin {
late final AnimationController _controller; late final AnimationController _controller;
late final Animation<double> _animation; late final Animation<double> _animation;
late StreamSubscription _subscription; late StreamSubscription _stateSubscription;
late StreamSubscription _playingSubscription;
late bool _canPlay = audioHandler.playbackState.value.processingState == late bool _canPlay = audioHandler.playbackState.value.processingState ==
AudioProcessingState.ready || AudioProcessingState.ready ||
audioHandler.playbackState.value.processingState == audioHandler.playbackState.value.processingState ==
@ -217,26 +221,30 @@ class _PlayPauseButtonState extends State<PlayPauseButton>
vsync: this, duration: const Duration(milliseconds: 200)); vsync: this, duration: const Duration(milliseconds: 200));
_animation = CurvedAnimation(parent: _controller, curve: Curves.easeInOut); _animation = CurvedAnimation(parent: _controller, curve: Curves.easeInOut);
_subscription = audioHandler.playbackState.listen((playbackState) { _stateSubscription = playerHelper.processingState.listen((processingState) {
if (playbackState.processingState == AudioProcessingState.ready || print('yes!');
if (processingState == AudioProcessingState.ready ||
audioHandler.playbackState.value.processingState == audioHandler.playbackState.value.processingState ==
AudioProcessingState.idle) { AudioProcessingState.idle) {
if (playbackState.playing) {
_controller.forward();
} else {
_controller.reverse();
}
if (!_canPlay) setState(() => _canPlay = true); if (!_canPlay) setState(() => _canPlay = true);
return; return;
} }
setState(() => _canPlay = false); setState(() => _canPlay = false);
}); });
_playingSubscription = playerHelper.playing.listen((playing) {
if (playing) {
_controller.forward();
} else {
_controller.reverse();
}
});
super.initState(); super.initState();
} }
@override @override
void dispose() { void dispose() {
_subscription.cancel(); _stateSubscription.cancel();
_playingSubscription.cancel();
_controller.dispose(); _controller.dispose();
super.dispose(); super.dispose();
} }
@ -261,7 +269,7 @@ class _PlayPauseButtonState extends State<PlayPauseButton>
: 'Play'.i18n, : 'Play'.i18n,
); );
if (!widget.filled) { if (!widget.filled) {
return IconButton( child = IconButton(
color: widget.iconColor, color: widget.iconColor,
icon: icon, icon: icon,
iconSize: widget.size, iconSize: widget.size,
@ -270,7 +278,7 @@ class _PlayPauseButtonState extends State<PlayPauseButton>
child = Material( child = Material(
type: MaterialType.transparency, type: MaterialType.transparency,
child: InkWell( child: InkWell(
customBorder: const CircleBorder(), customBorder: widget.material3 ? null : const CircleBorder(),
onTap: _playPause, onTap: _playPause,
child: IconTheme.merge( child: IconTheme.merge(
child: Center(child: icon), child: Center(child: icon),
@ -290,16 +298,33 @@ class _PlayPauseButtonState extends State<PlayPauseButton>
break; break;
} }
} }
if (widget.material3 && widget.filled) {
return StreamBuilder<bool>(
stream: playerHelper.playing,
builder: (context, snapshot) {
return AnimatedContainer(
clipBehavior: Clip.antiAlias,
width: widget.size,
height: widget.size,
duration: const Duration(milliseconds: 500),
decoration: BoxDecoration(
borderRadius: snapshot.data == true
? BorderRadius.circular(24.0)
: BorderRadius.circular(36.0),
color: widget.color),
child: child);
});
}
if (widget.filled) { if (widget.filled) {
return SizedBox.square( return AnimatedContainer(
dimension: widget.size, width: widget.size,
child: AnimatedContainer( height: widget.size,
duration: const Duration(seconds: 1), duration: const Duration(seconds: 1),
decoration: decoration:
BoxDecoration(shape: BoxShape.circle, color: widget.color), BoxDecoration(shape: BoxShape.circle, color: widget.color),
child: child)); child: child);
} else {
return SizedBox.square(dimension: widget.size, child: child);
} }
return SizedBox.square(dimension: widget.size * 2, child: child);
} }
} }

View file

@ -61,6 +61,7 @@ class BackgroundProvider extends ChangeNotifier {
@override @override
void addListener(VoidCallback listener) { void addListener(VoidCallback listener) {
print('[PROVIDER] listener added $hasListeners');
_mediaItemSub ??= audioHandler.mediaItem.listen((mediaItem) { _mediaItemSub ??= audioHandler.mediaItem.listen((mediaItem) {
if (mediaItem == null) return; if (mediaItem == null) return;
_updateColor(mediaItem); _updateColor(mediaItem);
@ -71,14 +72,12 @@ class BackgroundProvider extends ChangeNotifier {
@override @override
void removeListener(VoidCallback listener) { void removeListener(VoidCallback listener) {
super.removeListener(listener); super.removeListener(listener);
if (!hasListeners && _mediaItemSub != null) { print('[PROVIDER] listener removed! hasListeners? $hasListeners');
_mediaItemSub!.cancel();
_mediaItemSub = null;
}
} }
@override @override
void dispose() { void dispose() {
print('[PROVIDER] DISPOSED');
_isDisposed = true; _isDisposed = true;
_mediaItemSub?.cancel(); _mediaItemSub?.cancel();
super.dispose(); super.dispose();
@ -243,7 +242,7 @@ class PlayerScreenHorizontal extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[ children: <Widget>[
Padding( Padding(
padding: const EdgeInsets.fromLTRB(8, 0, 8, 0), padding: const EdgeInsets.fromLTRB(0, 0, 8, 0),
child: PlayerScreenTopRow( child: PlayerScreenTopRow(
textSize: 24.sp, textSize: 24.sp,
iconSize: 36.sp, iconSize: 36.sp,
@ -251,19 +250,19 @@ class PlayerScreenHorizontal extends StatelessWidget {
short: true), short: true),
), ),
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 32.0), padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: PlayerTextSubtext(textSize: 35.sp), child: PlayerTextSubtext(textSize: 35.sp),
), ),
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0), padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: SeekBar(textSize: 24.sp), child: SeekBar(textSize: 24.sp),
), ),
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0), padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: PlaybackControls(46.sp), child: PlaybackControls(46.sp),
), ),
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 10.0), padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: BottomBarControls(size: 30.sp), child: BottomBarControls(size: 30.sp),
) )
], ],
@ -457,6 +456,7 @@ class _FitOrScrollTextState extends State<FitOrScrollText> {
style: widget.style, style: widget.style,
blankSpace: 32.0, blankSpace: 32.0,
startPadding: 0.0, startPadding: 0.0,
numberOfRounds: 2,
accelerationDuration: const Duration(seconds: 1), accelerationDuration: const Duration(seconds: 1),
pauseAfterRound: const Duration(seconds: 2), pauseAfterRound: const Duration(seconds: 2),
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
@ -491,7 +491,9 @@ class PlayerTextSubtext extends StatelessWidget {
text: currentMediaItem.displayTitle!, text: currentMediaItem.displayTitle!,
maxLines: 1, maxLines: 1,
style: TextStyle( style: TextStyle(
fontSize: textSize, fontWeight: FontWeight.bold)), fontSize: textSize,
fontWeight: FontWeight.bold,
overflow: TextOverflow.ellipsis)),
), ),
// child: currentMediaItem.displayTitle!.length >= 26 // child: currentMediaItem.displayTitle!.length >= 26
// ? Marquee( // ? Marquee(
@ -552,6 +554,12 @@ class QualityInfoWidget extends StatelessWidget {
stream: playerHelper.streamInfo.map<String>(_getQualityStringFromInfo), stream: playerHelper.streamInfo.map<String>(_getQualityStringFromInfo),
builder: (context, snapshot) { builder: (context, snapshot) {
return TextButton( return TextButton(
// style: ButtonStyle(
// elevation: MaterialStatePropertyAll(0.5),
// padding: MaterialStatePropertyAll(
// EdgeInsets.symmetric(horizontal: 16, vertical: 4)),
// foregroundColor: MaterialStatePropertyAll(
// Theme.of(context).colorScheme.onSurface)),
child: Text(snapshot.data ?? '', child: Text(snapshot.data ?? '',
style: textSize == null ? null : TextStyle(fontSize: textSize)), style: textSize == null ? null : TextStyle(fontSize: textSize)),
onPressed: () => Navigator.of(context).push(MaterialPageRoute( onPressed: () => Navigator.of(context).push(MaterialPageRoute(
@ -795,6 +803,7 @@ class PlaybackControls extends StatelessWidget {
: darken(provider.dominantColor!); : darken(provider.dominantColor!);
return PlayPauseButton(size * 2.25, return PlayPauseButton(size * 2.25,
filled: true, filled: true,
material3: settings.enableMaterial3PlayButton,
color: color, color: color,
iconColor: Color.lerp( iconColor: Color.lerp(
(ThemeData.estimateBrightnessForColor(color) == (ThemeData.estimateBrightnessForColor(color) ==
@ -1172,6 +1181,7 @@ class BottomBarControls extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[ children: <Widget>[
QualityInfoWidget(textSize: size * 0.75), QualityInfoWidget(textSize: size * 0.75),
const Expanded(child: SizedBox()),
if (!desktopMode) if (!desktopMode)
IconButton( IconButton(
iconSize: size, iconSize: size,

View file

@ -18,15 +18,6 @@ class QueueScreen extends StatelessWidget {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text('Queue'.i18n), title: Text('Queue'.i18n),
systemOverlayStyle: SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Brightness.light,
statusBarBrightness: Brightness.light,
systemNavigationBarColor: Theme.of(context).scaffoldBackgroundColor,
systemNavigationBarDividerColor: Color(
Theme.of(context).scaffoldBackgroundColor.value - 0x00111111),
systemNavigationBarIconBrightness: Brightness.light,
),
// actions: <Widget>[ // actions: <Widget>[
// IconButton( // IconButton(
// icon: Icon( // icon: Icon(

View file

@ -349,13 +349,21 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
}), }),
), ),
SwitchListTile( SwitchListTile(
title: const Text('Enable filled play button'), title: Text('Enable filled play button'.i18n),
secondary: const Icon(Icons.play_circle), secondary: const Icon(Icons.play_circle),
value: settings.enableFilledPlayButton, value: settings.enableFilledPlayButton,
onChanged: (bool v) { onChanged: (bool v) {
setState(() => settings.enableFilledPlayButton = v); setState(() => settings.enableFilledPlayButton = v);
settings.save(); settings.save();
}), }),
SwitchListTile(
title: Text('Material 3 play button'.i18n),
secondary: const Icon(Icons.play_circle_outline),
value: settings.enableMaterial3PlayButton,
onChanged: (bool v) {
setState(() => settings.enableMaterial3PlayButton = v);
settings.save();
}),
SwitchListTile( SwitchListTile(
title: Text('Visualizer'.i18n), title: Text('Visualizer'.i18n),
subtitle: Text( subtitle: Text(
@ -724,15 +732,12 @@ class _QualityPickerState extends State<QualityPicker> {
switch (widget.field) { switch (widget.field) {
case 'mobile_wifi': case 'mobile_wifi':
settings.mobileQuality = settings.wifiQuality = _quality; settings.mobileQuality = settings.wifiQuality = _quality;
settings.updateAudioServiceQuality();
break; break;
case 'mobile': case 'mobile':
settings.mobileQuality = _quality; settings.mobileQuality = _quality;
settings.updateAudioServiceQuality();
break; break;
case 'wifi': case 'wifi':
settings.wifiQuality = _quality; settings.wifiQuality = _quality;
settings.updateAudioServiceQuality();
break; break;
case 'download': case 'download':
settings.downloadQuality = _quality; settings.downloadQuality = _quality;
@ -1818,6 +1823,10 @@ class _CreditsScreenState extends State<CreditsScreen> {
title: Text('Pato05'), title: Text('Pato05'),
subtitle: Text('Current Developer - best of all'), subtitle: Text('Current Developer - best of all'),
), ),
const ListTile(
title: Text('iDrinkCoffee'),
subtitle: Text('idk, he\'s romanian'),
),
const ListTile( const ListTile(
title: Text('exttex'), title: Text('exttex'),
subtitle: Text('Ex-Developer'), subtitle: Text('Ex-Developer'),

View file

@ -269,6 +269,7 @@ class PlaylistTile extends StatelessWidget {
leading: CachedImage( leading: CachedImage(
url: playlist!.image!.thumb, url: playlist!.image!.thumb,
width: 48, width: 48,
rounded: true,
), ),
onTap: onTap, onTap: onTap,
onLongPress: normalizeSecondary(onSecondary), onLongPress: normalizeSecondary(onSecondary),