diff --git a/lib/api/deezer.dart b/lib/api/deezer.dart index 18451f3..e3449bd 100644 --- a/lib/api/deezer.dart +++ b/lib/api/deezer.dart @@ -146,37 +146,40 @@ class DeezerAPI { //Wrapper so it can be globally awaited Future authorize() async => _authorizing ??= rawAuthorize(); - //Login with email FROM DEEMIX-JS - Future getArlByEmail(String email, String password) async { - //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'); - } + // NOT WORKING ANYMORE. + // this didn't last very long now, did it? - print(accessToken); - - return getArlByAccessToken(accessToken); - } + // //Login with email FROM DEEMIX-JS + // Future getArlByEmail(String email, String password) async { + // //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 Future getArlByAccessToken(String accessToken) async { diff --git a/lib/api/paths.dart b/lib/api/paths.dart index 96a470c..136fce8 100644 --- a/lib/api/paths.dart +++ b/lib/api/paths.dart @@ -14,15 +14,20 @@ class Paths { final target = await Directory(path.join(home, '.local', 'share', 'freezer')) .create(); + if (kDebugMode) { + return (await Directory(path.join(target.path, 'debug')).create()) + .path; + } return target.path; } return path.dirname(Platform.resolvedExecutable); case TargetPlatform.windows: - final String? localAppData = Platform.environment['LOCALAPPDATA']; - if (localAppData != null) { - final target = await Directory(path.join(localAppData, 'Freezer')).create(); - return target.path; - } + final String? localAppData = Platform.environment['LOCALAPPDATA']; + if (localAppData != null) { + final target = + await Directory(path.join(localAppData, 'Freezer')).create(); + return target.path; + } String? home = Platform.environment['USERPROFILE']; if (home == null) { final drive = Platform.environment['HOMEDRIVE']; @@ -35,7 +40,12 @@ class Paths { } 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; default: return (await getApplicationDocumentsDirectory()).path; diff --git a/lib/api/player/audio_handler.dart b/lib/api/player/audio_handler.dart index c13bd6b..9acf696 100644 --- a/lib/api/player/audio_handler.dart +++ b/lib/api/player/audio_handler.dart @@ -171,8 +171,8 @@ class AudioPlayerTask extends BaseAudioHandler { ); if (initArgs.ignoreInterruptions) { - session.interruptionEventStream.listen((_) {}); - session.becomingNoisyEventStream.listen((_) {}); + //session.interruptionEventStream.listen((_) {}); + //session.becomingNoisyEventStream.listen((_) {}); } //Update track index diff --git a/lib/api/player/player_helper.dart b/lib/api/player/player_helper.dart index d5382a9..0e6dad2 100644 --- a/lib/api/player/player_helper.dart +++ b/lib/api/player/player_helper.dart @@ -39,6 +39,13 @@ class PlayerHelper { final _bufferPositionSubject = BehaviorSubject(); ValueStream get bufferPosition => _bufferPositionSubject.stream; + final _playingSubject = BehaviorSubject(); + ValueStream get playing => _playingSubject.stream; + + final _processingStateSubject = BehaviorSubject(); + ValueStream get processingState => + _processingStateSubject.stream; + /// Find queue index by id /// /// 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), config: AudioServiceConfig( notificationColor: settings.primaryColor, - androidStopForegroundOnPause: false, - androidNotificationOngoing: false, + androidStopForegroundOnPause: true, + androidNotificationOngoing: true, androidNotificationClickStartsActivity: true, androidNotificationChannelDescription: 'Freezer', androidNotificationChannelName: 'Freezer', androidNotificationIcon: 'drawable/ic_logo', - preloadArtwork: false, + preloadArtwork: true, ), cacheManager: cacheManager, ); @@ -81,8 +88,6 @@ class PlayerHelper { Logger('PlayerHelper').fine("event received: ${event['action']}"); switch (event['action']) { case 'onLoad': - //After audio_service is loaded, load queue, set quality - await settings.updateAudioServiceQuality(); break; case 'onRestore': //Load queueSource from isolate @@ -140,6 +145,21 @@ class PlayerHelper { cache.history.add(Track.fromMediaItem(mediaItem)); 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 // await startService(); it is already ready, there is no need to start it @@ -178,16 +198,15 @@ class PlayerHelper { } //Executed before exit - Future onExit() async { + Future stop() async { _customEventSubscription.cancel(); _playbackStateStreamSubscription.cancel(); _mediaItemSubscription.cancel(); + _started = false; } //Replace queue, play specified track id Future _loadQueuePlay(List queue, int? index) async { - await settings.updateAudioServiceQuality(); - if (index != null) { await audioHandler.customAction('setIndex', {'index': index}); } @@ -270,7 +289,6 @@ class PlayerHelper { //Load and play // await startService(); // audioservice is ready - await settings.updateAudioServiceQuality(); await setQueueSource(queueSource); await audioHandler.customAction('setIndex', {'index': index}); await audioHandler.updateQueue(queue); diff --git a/lib/main.dart b/lib/main.dart index 3c7eac7..ae8d5df 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -113,7 +113,7 @@ void main() async { DefaultCacheManager.key, // cache aggressively stalePeriod: const Duration(days: 30), - maxNrOfCacheObjects: 1000, + maxNrOfCacheObjects: 5000, )); // cacheManager = HiveCacheManager( // boxName: 'freezer-images', boxPath: await Paths.cacheDir()); @@ -142,32 +142,32 @@ class FreezerApp extends StatefulWidget { State createState() => _FreezerAppState(); } -class _FreezerAppState extends State { - late StreamSubscription _playbackStateSub; - +class _FreezerAppState extends State with WidgetsBindingObserver { @override void initState() { - _initStateAsync(); + WidgetsBinding.instance.addObserver(this); super.initState(); } - Future _initStateAsync() async { - _playbackStateChanged(audioHandler.playbackState.value); - _playbackStateSub = - audioHandler.playbackState.listen(_playbackStateChanged); - } + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + switch (state) { + case AppLifecycleState.paused: + playerHelper.stop(); + break; + case AppLifecycleState.resumed: + playerHelper.start(); + break; - Future _playbackStateChanged(PlaybackState playbackState) async { - if (playbackState.processingState == AudioProcessingState.idle || - playbackState.processingState == AudioProcessingState.error) { - // TODO: reconnect maybe? - return; + default: + print('lifecycle: $state'); } } @override void dispose() { - _playbackStateSub.cancel(); + WidgetsBinding.instance.removeObserver(this); + playerHelper.stop(); super.dispose(); } @@ -191,13 +191,35 @@ class _FreezerAppState extends State { builder: (context, child) => DynamicColorBuilder(builder: (lightScheme, darkScheme) { 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; final darkTheme = settings.materialYouAccent ? ThemeData( colorScheme: darkScheme, 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; return MaterialApp( title: 'Freezer', @@ -269,13 +291,13 @@ class _LoginMainWrapperState extends State { } Future _logOut() async { + await deezerAPI.logout(); + await settings.save(); + await Cache.wipe(); setState(() { settings.arl = null; settings.offlineMode = false; }); - await deezerAPI.logout(); - await settings.save(); - await Cache.wipe(); } @override @@ -672,18 +694,17 @@ class MainScreenState extends State onKey: _handleKey, child: LayoutBuilder(builder: (context, constraints) { // check if we're able to display the desktop layout - if (Platform.isLinux || Platform.isWindows || Platform.isMacOS) { - final isLandscape = constraints.maxWidth > constraints.maxHeight; - isDesktop = isLandscape && - constraints.maxWidth >= 1100 && - constraints.maxHeight >= 600; - } + final isLandscape = constraints.maxWidth > constraints.maxHeight; + isDesktop = isLandscape && + constraints.maxWidth >= 1100 && + constraints.maxHeight >= 600; return FancyScaffold( key: _fancyScaffoldKey, navigationRail: _buildNavigationRail(isDesktop), bottomNavigationBar: buildBottomBar(isDesktop), bottomPanel: Builder( builder: (context) => PlayerBar( + backgroundColor: Theme.of(context).cardColor, focusNode: playerBarFocusNode, onTap: FancyScaffold.of(context)!.openPanel, shouldHaveHero: false, diff --git a/lib/settings.dart b/lib/settings.dart index e0b4728..3fe9999 100644 --- a/lib/settings.dart +++ b/lib/settings.dart @@ -1,4 +1,5 @@ import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; import 'package:freezer/api/definitions.dart'; import 'package:freezer/api/download.dart'; import 'package:freezer/api/player/audio_handler.dart'; @@ -170,6 +171,9 @@ class Settings { NavigationRailAppearance navigationRailAppearance = NavigationRailAppearance.expand_on_hover; + @HiveField(49, defaultValue: true) + bool enableMaterial3PlayButton = true; + static LazyBox? __box; static Future> get _box async => __box ??= await Hive.openLazyBox('settings'); @@ -267,12 +271,6 @@ class Settings { downloadManager.updateServiceSettings(); } - Future updateAudioServiceQuality() async { - //Send wifi & mobile quality to audio service isolate - await audioHandler.customAction('updateQuality', - {'mobileQuality': mobileQuality, 'wifiQuality': wifiQuality}); - } - // MaterialColor get _primarySwatch => // MaterialColor(primaryColor.value, { // 50: primaryColor.withOpacity(.1), @@ -319,6 +317,15 @@ class Settings { sliderTheme: _sliderTheme, bottomAppBarTheme: const BottomAppBarTheme(color: Color(0xfff5f5f5)), 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( textTheme: textTheme, @@ -331,6 +338,15 @@ class Settings { ), sliderTheme: _sliderTheme, 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( textTheme: textTheme, @@ -349,6 +365,15 @@ class Settings { const BottomSheetThemeData(backgroundColor: deezerBottom), cardColor: deezerBg, 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( textTheme: textTheme, @@ -370,6 +395,15 @@ class Settings { BottomSheetThemeData(backgroundColor: _elevation1Black), cardColor: _elevation1Black, 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, + )), ) }; diff --git a/lib/ui/home_screen.dart b/lib/ui/home_screen.dart index fa2ab78..064a47d 100644 --- a/lib/ui/home_screen.dart +++ b/lib/ui/home_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:freezer/api/deezer.dart'; import 'package:freezer/api/definitions.dart'; import 'package:freezer/api/player/audio_handler.dart'; @@ -54,15 +55,19 @@ class HomeScreen extends StatelessWidget { PointerDeviceKind.trackpad }, ), - child: SafeArea( - child: Scaffold( - body: NestedScrollView( - floatHeaderSlivers: true, - headerSliverBuilder: (context, _) => [ - SliverPersistentHeader( - delegate: _SearchHeaderDelegate(), floating: true) - ], - body: const HomePageWidget(cacheable: true), + child: AnnotatedRegion( + value: Theme.of(context).appBarTheme.systemOverlayStyle ?? + const SystemUiOverlayStyle(), + child: SafeArea( + child: Scaffold( + body: NestedScrollView( + floatHeaderSlivers: true, + headerSliverBuilder: (context, _) => [ + SliverPersistentHeader( + delegate: _SearchHeaderDelegate(), floating: true) + ], + body: const HomePageWidget(cacheable: true), + ), ), ), ), diff --git a/lib/ui/login_screen.dart b/lib/ui/login_screen.dart index 5744bb4..7bc42f5 100644 --- a/lib/ui/login_screen.dart +++ b/lib/ui/login_screen.dart @@ -209,19 +209,19 @@ class _LoginWidgetState extends State { 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), + //Email login dialog (Not working anymore) + // 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) @@ -345,7 +345,7 @@ class LoginBrowser extends StatelessWidget { controller.evaluateJavascript( source: 'window.location.href = "/open_app"'); } - print('scheme ${uri.scheme}, host: ${uri.host}'); + print('uri $uri'); //Parse arl from url if (uri.scheme == 'intent' && uri.host == 'deezer.page.link') { try { @@ -367,107 +367,109 @@ class LoginBrowser extends StatelessWidget { } } -class EmailLogin extends StatefulWidget { - final Function callback; - const EmailLogin(this.callback, {Key? key}) : super(key: key); +// email login is removed cuz not working = USELESS - @override - State createState() => _EmailLoginState(); -} - -class _EmailLoginState extends State { - final _emailController = TextEditingController(); - final _passwordController = TextEditingController(); - bool _loading = false; - - Future _login() async { - setState(() => _loading = true); - //Try logging in - String? arl; - String? exception; - try { - arl = await deezerAPI.getArlByEmail( - _emailController.text, _passwordController.text); - } catch (e, st) { - exception = e.toString(); - print(e); - print(st); - } - setState(() => _loading = false); - - //Success - if (arl != null) { - settings.arl = arl; - Navigator.of(context).pop(); - widget.callback(); - return; - } - - //Error - showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text("Error logging in!".i18n), - content: Text( - "Error logging in using email, please check your credentials.\nError: ${exception ?? 'Unknown'}"), - 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( - 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: [ - 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'), - ) - ], - ); - } -} +//class EmailLogin extends StatefulWidget { +// final Function callback; +// const EmailLogin(this.callback, {Key? key}) : super(key: key); +// +// @override +// State createState() => _EmailLoginState(); +//} +// +//class _EmailLoginState extends State { +// final _emailController = TextEditingController(); +// final _passwordController = TextEditingController(); +// bool _loading = false; +// +// Future _login() async { +// setState(() => _loading = true); +// //Try logging in +// String? arl; +// String? exception; +// try { +// arl = await deezerAPI.getArlByEmail( +// _emailController.text, _passwordController.text); +// } catch (e, st) { +// exception = e.toString(); +// print(e); +// print(st); +// } +// setState(() => _loading = false); +// +// //Success +// if (arl != null) { +// settings.arl = arl; +// Navigator.of(context).pop(); +// widget.callback(); +// return; +// } +// +// //Error +// showDialog( +// context: context, +// builder: (context) => AlertDialog( +// title: Text("Error logging in!".i18n), +// content: Text( +// "Error logging in using email, please check your credentials.\nError: ${exception ?? 'Unknown'}"), +// 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( +// 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: [ +// 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'), +// ) +// ], +// ); +// } +// } diff --git a/lib/ui/lyrics_screen.dart b/lib/ui/lyrics_screen.dart index ca9f0dc..332b860 100644 --- a/lib/ui/lyrics_screen.dart +++ b/lib/ui/lyrics_screen.dart @@ -45,12 +45,14 @@ class LyricsWidget extends StatefulWidget { State createState() => _LyricsWidgetState(); } -class _LyricsWidgetState extends State { +class _LyricsWidgetState extends State + with WidgetsBindingObserver { late StreamSubscription _mediaItemSub; late StreamSubscription _playbackStateSub; int? _currentIndex = -1; Duration _nextOffset = Duration.zero; Duration _currentOffset = Duration.zero; + String? _currentTrackId; final ScrollController _controller = ScrollController(); final double height = 90; BoxConstraints? _widgetConstraints; @@ -64,8 +66,12 @@ class _LyricsWidgetState extends State { bool _syncedLyrics = false; Future _loadForId(String trackId) async { + if (_currentTrackId == trackId) return; + _currentTrackId = trackId; + print('cancelling req?'); // cancel current request, if applicable _lyricsCancelToken?.cancel(); + _currentIndex = -1; _currentOffset = Duration.zero; _nextOffset = Duration.zero; @@ -83,6 +89,7 @@ class _LyricsWidgetState extends State { _lyricsCancelToken = CancelToken(); final lyrics = await deezerAPI.lyrics(trackId, cancelToken: _lyricsCancelToken); + _syncedLyrics = lyrics.sync; if (!mounted) return; setState(() { @@ -95,10 +102,14 @@ class _LyricsWidgetState extends State { } on DioException catch (e) { if (e.type != DioExceptionType.cancel) rethrow; } catch (e) { + _currentTrackId = null; if (!mounted) return; setState(() { _error = e; }); + } finally { + _lyricsCancelToken = + null; // dispose of cancel token after lyrics are fetched. } } @@ -115,8 +126,6 @@ class _LyricsWidgetState extends State { scrollTo = minScroll - widgetHeight / 2 + height / 2; } - print( - '${height * _currentIndex!}, ${MediaQuery.of(context).size.height / 2}'); if (scrollTo < 0.0) scrollTo = 0.0; if (scrollTo > _controller.position.maxScrollExtent) { scrollTo = _controller.position.maxScrollExtent; @@ -152,28 +161,49 @@ class _LyricsWidgetState extends State { _scrollToLyric(); } - @override - void initState() { - SchedulerBinding.instance.addPostFrameCallback((_) { - //Enable visualizer - // if (settings.lyricsVisualizer) playerHelper.startVisualizer(); - _playbackStateSub = AudioService.position.listen(_updatePosition); - }); + void _makeSubscriptions() { + _playbackStateSub = AudioService.position.listen(_updatePosition); - /// Track change = ~exit~ reload lyrics + /// Track change = reload new lyrics _mediaItemSub = audioHandler.mediaItem.listen((mediaItem) { if (mediaItem == null) return; if (_controller.hasClients) _controller.jumpTo(0.0); _loadForId(mediaItem.id); }); + } + @override + void initState() { + SchedulerBinding.instance.addPostFrameCallback((_) { + //Enable visualizer + // if (settings.lyricsVisualizer) playerHelper.startVisualizer(); + _makeSubscriptions(); + }); + + WidgetsBinding.instance.addObserver(this); 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 void dispose() { _mediaItemSub.cancel(); _playbackStateSub.cancel(); + WidgetsBinding.instance.removeObserver(this); //Stop visualizer // if (settings.lyricsVisualizer) playerHelper.stopVisualizer(); super.dispose(); diff --git a/lib/ui/menu.dart b/lib/ui/menu.dart index 39700a8..e3db986 100644 --- a/lib/ui/menu.dart +++ b/lib/ui/menu.dart @@ -148,28 +148,27 @@ class MenuSheet { showModalBottomSheet( isScrollControlled: false, // true, context: context, + useSafeArea: true, builder: (BuildContext context) { - return SafeArea( - child: ConstrainedBox( - constraints: BoxConstraints( - maxHeight: (MediaQuery.of(context).orientation == - Orientation.landscape) - ? 220 - : 350, - ), - child: SingleChildScrollView( - child: Column( - children: options - .map((option) => ListTile( - title: option.label, - leading: option.icon, - onTap: () { - option.onTap.call(); - Navigator.pop(context); - }, - )) - .toList(growable: false)), - ), + return ConstrainedBox( + constraints: BoxConstraints( + maxHeight: + (MediaQuery.of(context).orientation == Orientation.landscape) + ? 220 + : 350, + ), + child: SingleChildScrollView( + child: Column( + children: options + .map((option) => ListTile( + title: option.label, + leading: option.icon, + onTap: () { + option.onTap.call(); + Navigator.pop(context); + }, + )) + .toList(growable: false)), ), ); }); @@ -197,13 +196,13 @@ class MenuSheet { return DraggableScrollableSheet( initialChildSize: 0.5, minChildSize: 0.45, - maxChildSize: 0.75, - builder: (context, scrollController) => SafeArea( - child: Material( - type: MaterialType.card, - clipBehavior: Clip.antiAlias, - borderRadius: - const BorderRadius.vertical(top: Radius.circular(20.0)), + maxChildSize: 0.95, + builder: (context, scrollController) => Material( + type: MaterialType.card, + clipBehavior: Clip.antiAlias, + borderRadius: + const BorderRadius.vertical(top: Radius.circular(20.0)), + child: SafeArea( child: CustomScrollView( controller: scrollController, slivers: [ diff --git a/lib/ui/player_bar.dart b/lib/ui/player_bar.dart index e0e1757..5a751fb 100644 --- a/lib/ui/player_bar.dart +++ b/lib/ui/player_bar.dart @@ -69,6 +69,7 @@ class PlayerBar extends StatelessWidget { } final currentMediaItem = snapshot.data!; final image = CachedImage( + rounded: true, width: 50, height: 50, url: currentMediaItem.extras!['thumb'] ?? @@ -185,6 +186,7 @@ class PrevNextButton extends StatelessWidget { class PlayPauseButton extends StatefulWidget { final double size; final bool filled; + final bool material3; final Color? iconColor; /// The color of the card if [filled] is true @@ -193,6 +195,7 @@ class PlayPauseButton extends StatefulWidget { this.size, { Key? key, this.filled = false, + this.material3 = true, this.color, this.iconColor, }) : super(key: key); @@ -205,7 +208,8 @@ class _PlayPauseButtonState extends State with SingleTickerProviderStateMixin { late final AnimationController _controller; late final Animation _animation; - late StreamSubscription _subscription; + late StreamSubscription _stateSubscription; + late StreamSubscription _playingSubscription; late bool _canPlay = audioHandler.playbackState.value.processingState == AudioProcessingState.ready || audioHandler.playbackState.value.processingState == @@ -217,26 +221,30 @@ class _PlayPauseButtonState extends State vsync: this, duration: const Duration(milliseconds: 200)); _animation = CurvedAnimation(parent: _controller, curve: Curves.easeInOut); - _subscription = audioHandler.playbackState.listen((playbackState) { - if (playbackState.processingState == AudioProcessingState.ready || + _stateSubscription = playerHelper.processingState.listen((processingState) { + print('yes!'); + if (processingState == AudioProcessingState.ready || audioHandler.playbackState.value.processingState == AudioProcessingState.idle) { - if (playbackState.playing) { - _controller.forward(); - } else { - _controller.reverse(); - } if (!_canPlay) setState(() => _canPlay = true); return; } setState(() => _canPlay = false); }); + _playingSubscription = playerHelper.playing.listen((playing) { + if (playing) { + _controller.forward(); + } else { + _controller.reverse(); + } + }); super.initState(); } @override void dispose() { - _subscription.cancel(); + _stateSubscription.cancel(); + _playingSubscription.cancel(); _controller.dispose(); super.dispose(); } @@ -261,7 +269,7 @@ class _PlayPauseButtonState extends State : 'Play'.i18n, ); if (!widget.filled) { - return IconButton( + child = IconButton( color: widget.iconColor, icon: icon, iconSize: widget.size, @@ -270,7 +278,7 @@ class _PlayPauseButtonState extends State child = Material( type: MaterialType.transparency, child: InkWell( - customBorder: const CircleBorder(), + customBorder: widget.material3 ? null : const CircleBorder(), onTap: _playPause, child: IconTheme.merge( child: Center(child: icon), @@ -290,16 +298,33 @@ class _PlayPauseButtonState extends State break; } } - if (widget.filled) { - return SizedBox.square( - dimension: widget.size, - child: AnimatedContainer( - duration: const Duration(seconds: 1), - decoration: - BoxDecoration(shape: BoxShape.circle, color: widget.color), - child: child)); - } else { - return SizedBox.square(dimension: widget.size, child: child); + if (widget.material3 && widget.filled) { + return StreamBuilder( + 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) { + return AnimatedContainer( + width: widget.size, + height: widget.size, + duration: const Duration(seconds: 1), + decoration: + BoxDecoration(shape: BoxShape.circle, color: widget.color), + child: child); + } + + return SizedBox.square(dimension: widget.size * 2, child: child); } } diff --git a/lib/ui/player_screen.dart b/lib/ui/player_screen.dart index a349e32..8c7bcf5 100644 --- a/lib/ui/player_screen.dart +++ b/lib/ui/player_screen.dart @@ -61,6 +61,7 @@ class BackgroundProvider extends ChangeNotifier { @override void addListener(VoidCallback listener) { + print('[PROVIDER] listener added $hasListeners'); _mediaItemSub ??= audioHandler.mediaItem.listen((mediaItem) { if (mediaItem == null) return; _updateColor(mediaItem); @@ -71,14 +72,12 @@ class BackgroundProvider extends ChangeNotifier { @override void removeListener(VoidCallback listener) { super.removeListener(listener); - if (!hasListeners && _mediaItemSub != null) { - _mediaItemSub!.cancel(); - _mediaItemSub = null; - } + print('[PROVIDER] listener removed! hasListeners? $hasListeners'); } @override void dispose() { + print('[PROVIDER] DISPOSED'); _isDisposed = true; _mediaItemSub?.cancel(); super.dispose(); @@ -243,7 +242,7 @@ class PlayerScreenHorizontal extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Padding( - padding: const EdgeInsets.fromLTRB(8, 0, 8, 0), + padding: const EdgeInsets.fromLTRB(0, 0, 8, 0), child: PlayerScreenTopRow( textSize: 24.sp, iconSize: 36.sp, @@ -251,19 +250,19 @@ class PlayerScreenHorizontal extends StatelessWidget { short: true), ), Padding( - padding: const EdgeInsets.symmetric(horizontal: 32.0), + padding: const EdgeInsets.symmetric(horizontal: 24.0), child: PlayerTextSubtext(textSize: 35.sp), ), Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), + padding: const EdgeInsets.symmetric(horizontal: 8.0), child: SeekBar(textSize: 24.sp), ), Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), + padding: const EdgeInsets.symmetric(horizontal: 24.0), child: PlaybackControls(46.sp), ), Padding( - padding: const EdgeInsets.symmetric(horizontal: 10.0), + padding: const EdgeInsets.symmetric(horizontal: 24.0), child: BottomBarControls(size: 30.sp), ) ], @@ -457,6 +456,7 @@ class _FitOrScrollTextState extends State { style: widget.style, blankSpace: 32.0, startPadding: 0.0, + numberOfRounds: 2, accelerationDuration: const Duration(seconds: 1), pauseAfterRound: const Duration(seconds: 2), crossAxisAlignment: CrossAxisAlignment.end, @@ -491,7 +491,9 @@ class PlayerTextSubtext extends StatelessWidget { text: currentMediaItem.displayTitle!, maxLines: 1, style: TextStyle( - fontSize: textSize, fontWeight: FontWeight.bold)), + fontSize: textSize, + fontWeight: FontWeight.bold, + overflow: TextOverflow.ellipsis)), ), // child: currentMediaItem.displayTitle!.length >= 26 // ? Marquee( @@ -552,6 +554,12 @@ class QualityInfoWidget extends StatelessWidget { stream: playerHelper.streamInfo.map(_getQualityStringFromInfo), builder: (context, snapshot) { 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 ?? '', style: textSize == null ? null : TextStyle(fontSize: textSize)), onPressed: () => Navigator.of(context).push(MaterialPageRoute( @@ -795,6 +803,7 @@ class PlaybackControls extends StatelessWidget { : darken(provider.dominantColor!); return PlayPauseButton(size * 2.25, filled: true, + material3: settings.enableMaterial3PlayButton, color: color, iconColor: Color.lerp( (ThemeData.estimateBrightnessForColor(color) == @@ -1172,6 +1181,7 @@ class BottomBarControls extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.center, children: [ QualityInfoWidget(textSize: size * 0.75), + const Expanded(child: SizedBox()), if (!desktopMode) IconButton( iconSize: size, diff --git a/lib/ui/queue_screen.dart b/lib/ui/queue_screen.dart index 1562ddb..fd7063b 100644 --- a/lib/ui/queue_screen.dart +++ b/lib/ui/queue_screen.dart @@ -18,15 +18,6 @@ class QueueScreen extends StatelessWidget { return Scaffold( appBar: AppBar( 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: [ // IconButton( // icon: Icon( diff --git a/lib/ui/settings_screen.dart b/lib/ui/settings_screen.dart index 598c408..c9fa551 100644 --- a/lib/ui/settings_screen.dart +++ b/lib/ui/settings_screen.dart @@ -349,13 +349,21 @@ class _AppearanceSettingsState extends State { }), ), SwitchListTile( - title: const Text('Enable filled play button'), + title: Text('Enable filled play button'.i18n), secondary: const Icon(Icons.play_circle), value: settings.enableFilledPlayButton, onChanged: (bool v) { setState(() => settings.enableFilledPlayButton = v); 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( title: Text('Visualizer'.i18n), subtitle: Text( @@ -724,15 +732,12 @@ class _QualityPickerState extends State { switch (widget.field) { case 'mobile_wifi': settings.mobileQuality = settings.wifiQuality = _quality; - settings.updateAudioServiceQuality(); break; case 'mobile': settings.mobileQuality = _quality; - settings.updateAudioServiceQuality(); break; case 'wifi': settings.wifiQuality = _quality; - settings.updateAudioServiceQuality(); break; case 'download': settings.downloadQuality = _quality; @@ -1818,6 +1823,10 @@ class _CreditsScreenState extends State { title: Text('Pato05'), subtitle: Text('Current Developer - best of all'), ), + const ListTile( + title: Text('iDrinkCoffee'), + subtitle: Text('idk, he\'s romanian'), + ), const ListTile( title: Text('exttex'), subtitle: Text('Ex-Developer'), diff --git a/lib/ui/tiles.dart b/lib/ui/tiles.dart index 1e96218..701f74d 100644 --- a/lib/ui/tiles.dart +++ b/lib/ui/tiles.dart @@ -269,6 +269,7 @@ class PlaylistTile extends StatelessWidget { leading: CachedImage( url: playlist!.image!.thumb, width: 48, + rounded: true, ), onTap: onTap, onLongPress: normalizeSecondary(onSecondary),