diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 4b82974..00948ef 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip diff --git a/lib/api/download.dart b/lib/api/download.dart index 6a64654..ad33e49 100644 --- a/lib/api/download.dart +++ b/lib/api/download.dart @@ -578,7 +578,8 @@ class DownloadManager { int? playlistCount = (await db .rawQuery('SELECT COUNT(*) FROM Playlists'))[0]['COUNT(*)'] as int?; //Free space - double diskSpace = await (DiskSpace.getFreeDiskSpace as FutureOr); + double diskSpace = + await DiskSpace.getFreeDiskSpace.then((value) => value ?? 0.0); //Used space List offlineStat = await Directory(offlinePath).list().toList(); diff --git a/lib/api/player.dart b/lib/api/player.dart index 254f333..6ccbce7 100644 --- a/lib/api/player.dart +++ b/lib/api/player.dart @@ -189,7 +189,7 @@ class PlayerHelper { List? tracks = []; switch (queueSource!.source) { case 'flow': - tracks = await (deezerAPI.flow() as FutureOr>); + tracks = await deezerAPI.flow(); break; //SmartRadio/Artist radio case 'smartradio': @@ -198,22 +198,26 @@ class PlayerHelper { break; //Library shuffle case 'libraryshuffle': - tracks = await (deezerAPI.libraryShuffle( - start: audioHandler.queue.value.length) as FutureOr>); + tracks = await deezerAPI.libraryShuffle( + start: audioHandler.queue.value.length); break; case 'mix': - tracks = - await (deezerAPI.playMix(queueSource!.id) as FutureOr>); + tracks = await deezerAPI.playMix(queueSource!.id); // Deduplicate tracks with the same id List queueIds = audioHandler.queue.value.map((e) => e.id).toList(); - tracks.removeWhere((track) => queueIds.contains(track.id)); + tracks?.removeWhere((track) => queueIds.contains(track.id)); break; default: // print(queueSource.toJson()); break; } + if (tracks == null) { + // try again i guess? + return await onQueueEnd(); + } + List mi = tracks.map((t) => t.toMediaItem()).toList(); await audioHandler.addQueueItems(mi); // AudioService.skipToNext(); @@ -546,9 +550,9 @@ class AudioPlayerTask extends BaseAudioHandler { void _broadcastState() { playbackState.add(PlaybackState( controls: [ - MediaControl.skipToPrevious, - if (_player.playing) MediaControl.pause else MediaControl.play, - MediaControl.skipToNext, + if (_queueIndex != 0) MediaControl.skipToPrevious, + _player.playing ? MediaControl.pause : MediaControl.play, + if (_queueIndex != _queue!.length - 1) MediaControl.skipToNext, //Stop MediaControl( androidIcon: 'drawable/ic_action_stop', @@ -725,8 +729,8 @@ class AudioPlayerTask extends BaseAudioHandler { case 'screenAndroidAuto': if (_androidAutoCallback != null) _androidAutoCallback!.complete( - (args!['value'] as List>) - .map(mediaItemFromJson) + (args!['value'] as List>?) + ?.map(mediaItemFromJson) .toList()); break; //Reorder tracks, args = [old, new] diff --git a/lib/main.dart b/lib/main.dart index d616943..247631a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -46,13 +46,14 @@ void main() async { audioHandler = await AudioService.init( builder: () => AudioPlayerTask(), config: AudioServiceConfig( + notificationColor: settings.primaryColor, androidStopForegroundOnPause: false, androidNotificationOngoing: false, androidNotificationClickStartsActivity: true, androidNotificationChannelDescription: 'Freezer', androidNotificationChannelName: 'Freezer', androidNotificationIcon: 'drawable/ic_logo', - preloadArtwork: true, + preloadArtwork: false, ), ); @@ -69,7 +70,6 @@ class _FreezerAppState extends State { void initState() { //Make update theme global updateTheme = _updateTheme; - _updateTheme(); super.initState(); } @@ -82,11 +82,6 @@ class _FreezerAppState extends State { setState(() { settings.themeData; }); - SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle( - systemNavigationBarColor: settings.themeData!.bottomAppBarColor, - systemNavigationBarIconBrightness: - settings.isDark ? Brightness.light : Brightness.dark, - )); } Locale? _locale() { @@ -171,8 +166,18 @@ class _LoginMainWrapperState extends State { @override Widget build(BuildContext context) { if (settings.arl == null) - return LoginWidget( - callback: () => setState(() => {}), + return AnnotatedRegion( + value: (Theme.of(context).brightness == Brightness.dark + ? SystemUiOverlayStyle.dark + : SystemUiOverlayStyle.light) + .copyWith( + statusBarColor: Colors.transparent, + systemNavigationBarColor: Theme.of(context).scaffoldBackgroundColor, + statusBarIconBrightness: Brightness.light, + ), + child: LoginWidget( + callback: () => setState(() => {}), + ), ); return MainScreen(); } @@ -190,6 +195,9 @@ class _MainScreenState extends State StreamSubscription? _urlLinkStream; int _keyPressed = 0; bool textFieldVisited = false; + final _slideTween = Tween( + begin: const Offset(0.0, 0.025), end: const Offset(0.0, 0.0)); + final _scaleTween = Tween(begin: 0.975, end: 1.0); @override void initState() { @@ -394,7 +402,7 @@ class _MainScreenState extends State } await navigatorKey.currentState!.maybePop(); - _selected.value = s; + if (_selected.value != s) _selected.value = s; }, selectedItemColor: Theme.of(context).primaryColor, items: [ @@ -420,7 +428,22 @@ class _MainScreenState extends State canRequestFocus: false, child: ValueListenableBuilder( valueListenable: _selected, - builder: (context, value, _) => _screens[value]))), + builder: (context, value, _) => AnimatedSwitcher( + duration: Duration(milliseconds: 250), + transitionBuilder: (child, animation) => + SlideTransition( + position: _slideTween.animate(animation), + child: ScaleTransition( + scale: _scaleTween.animate(animation), + child: FadeTransition( + opacity: animation, + child: child, + ), + )), + layoutBuilder: (currentChild, previousChildren) => + currentChild!, + child: _screens[value], + )))), )); } } diff --git a/lib/ui/elements.dart b/lib/ui/elements.dart index 4cc122d..ebe8116 100644 --- a/lib/ui/elements.dart +++ b/lib/ui/elements.dart @@ -37,9 +37,13 @@ class FreezerAppBar extends StatelessWidget implements PreferredSizeWidget { final Widget? bottom; //Should be specified if bottom is specified final double height; + final SystemUiOverlayStyle? systemUiOverlayStyle; - FreezerAppBar(this.title, - {this.actions = const [], this.bottom, this.height = 56.0}); + const FreezerAppBar(this.title, + {this.actions = const [], + this.bottom, + this.height = 56.0, + this.systemUiOverlayStyle}); Size get preferredSize => Size.fromHeight(this.height); @@ -51,9 +55,7 @@ class FreezerAppBar extends StatelessWidget implements PreferredSizeWidget { ? Colors.white : Colors.black), child: AppBar( - systemOverlayStyle: Theme.of(context).brightness == Brightness.dark - ? SystemUiOverlayStyle.dark - : SystemUiOverlayStyle.light, + systemOverlayStyle: systemUiOverlayStyle, elevation: 0.0, backgroundColor: Theme.of(context).scaffoldBackgroundColor, title: Text( diff --git a/lib/ui/home_screen.dart b/lib/ui/home_screen.dart index 881a3a3..9811cf0 100644 --- a/lib/ui/home_screen.dart +++ b/lib/ui/home_screen.dart @@ -124,12 +124,7 @@ class _HomePageScreenState extends State { @override Widget build(BuildContext context) { - if (_homePage == null) - return Center( - child: Padding( - padding: EdgeInsets.all(8.0), - child: CircularProgressIndicator(), - )); + if (_homePage == null) return Center(child: CircularProgressIndicator()); if (_error) return ErrorScreen(); return Column( children: List.generate( diff --git a/lib/ui/login_screen.dart b/lib/ui/login_screen.dart index e4d23a7..5fce6d8 100644 --- a/lib/ui/login_screen.dart +++ b/lib/ui/login_screen.dart @@ -351,7 +351,7 @@ class _EmailLoginState extends State { setState(() => _loading = true); //Try logging in String? arl; - late String exception; + String? exception; try { arl = await DeezerAPI.getArlByEmail(_email, _password!); } catch (e, st) { @@ -376,7 +376,7 @@ class _EmailLoginState extends State { title: Text("Error logging in!".i18n), content: Text( "Error logging in using email, please check your credentials.\nError: " + - exception), + (exception ?? 'Unknown')), actions: [ TextButton( child: Text('Dismiss'.i18n), diff --git a/lib/ui/lyrics.dart b/lib/ui/lyrics.dart index f884026..41ee275 100644 --- a/lib/ui/lyrics.dart +++ b/lib/ui/lyrics.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:audio_service/audio_service.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; import 'package:freezer/api/deezer.dart'; import 'package:freezer/api/definitions.dart'; @@ -35,17 +36,20 @@ class _LyricsScreenState extends State { Future _loadForId(String trackId) async { //Fetch - if (_loading == false && lyrics != null) + if (_loading == false && lyrics != null) { setState(() { + _freeScroll = false; _loading = true; lyrics = null; }); + } try { Lyrics l = await deezerAPI.lyrics(trackId); setState(() { _loading = false; lyrics = l; }); + _scrollToLyric(); } catch (e) { setState(() { _error = e; @@ -68,20 +72,22 @@ class _LyricsScreenState extends State { @override void initState() { - //Enable visualizer - // if (settings.lyricsVisualizer) playerHelper.startVisualizer(); - _playbackStateSub = AudioService.position.listen((position) { - if (_loading) 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(() => null); - _prevIndex = _currentIndex; - if (_freeScroll) return; - _scrollToLyric(); + SchedulerBinding.instance!.addPostFrameCallback((_) { + //Enable visualizer + // if (settings.lyricsVisualizer) playerHelper.startVisualizer(); + _playbackStateSub = AudioService.position.listen((position) { + if (_loading) 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(() => null); + _prevIndex = _currentIndex; + if (_freeScroll) return; + _scrollToLyric(); + }); }); if (audioHandler.mediaItem.value != null) _loadForId(audioHandler.mediaItem.value!.id); @@ -89,7 +95,7 @@ class _LyricsScreenState extends State { /// Track change = ~exit~ reload lyrics _mediaItemSub = audioHandler.mediaItem.listen((mediaItem) { if (mediaItem == null) return; - _controller.jumpTo(0.0); + if (_controller.hasClients) _controller.jumpTo(0.0); _loadForId(mediaItem.id); }); @@ -107,144 +113,145 @@ class _LyricsScreenState extends State { @override Widget build(BuildContext context) { - return AnnotatedRegion( - value: Theme.of(context).brightness == Brightness.dark - ? SystemUiOverlayStyle.dark - : SystemUiOverlayStyle.light, - child: Scaffold( - appBar: FreezerAppBar('Lyrics'.i18n), - body: SafeArea( - child: Column( - children: [ - Theme( - data: settings.themeData!.copyWith( - textButtonTheme: TextButtonThemeData( - style: ButtonStyle( - foregroundColor: - MaterialStateProperty.all(Colors.white)))), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (_freeScroll && !_loading) - TextButton( - onPressed: () { - setState(() => _freeScroll = false); - _scrollToLyric(); - }, - child: Text( - _currentIndex! >= 0 - ? (lyrics?.lyrics?[_currentIndex!].text ?? - '...') - : '...', - textAlign: TextAlign.center, - ), - style: ButtonStyle( - foregroundColor: - MaterialStateProperty.all(Colors.white))) - ], - ), - ), - Expanded( - child: Stack(children: [ - //Lyrics - _error != null - ? - //Shouldn't really happen, empty lyrics have own text - ErrorScreen(message: _error.toString()) - : - // Loading lyrics - _loading - ? Center(child: CircularProgressIndicator()) - : NotificationListener( - onNotification: (Notification notification) { - if (_freeScroll || - notification is! ScrollStartNotification) - return false; - if (!_animatedScroll && !_loading) - setState(() => _freeScroll = true); - return false; - }, - child: ListView.builder( - controller: _controller, - padding: EdgeInsets.fromLTRB( - 0, - 0, - 0, - settings.lyricsVisualizer! && false - ? 100 - : 0), - itemCount: lyrics!.lyrics!.length, - itemBuilder: (BuildContext context, int i) { - return Padding( - padding: EdgeInsets.symmetric( - horizontal: 8.0), - child: Container( - decoration: BoxDecoration( - borderRadius: - BorderRadius.circular(8.0), - color: _currentIndex == i - ? Colors.grey - .withOpacity(0.25) - : Colors.transparent, - ), - height: height, - child: InkWell( - borderRadius: - BorderRadius.circular(8.0), - onTap: lyrics!.id != null - ? () => audioHandler.seek( - lyrics! - .lyrics![i].offset!) - : null, - child: Center( - child: Text( - lyrics!.lyrics![i].text!, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 26.0, - fontWeight: - (_currentIndex == i) - ? FontWeight - .bold - : FontWeight - .normal), - ), - )))); - }, - )), - - //Visualizer - //if (settings.lyricsVisualizer) - // Positioned( - // bottom: 0, - // left: 0, - // right: 0, - // child: StreamBuilder( - // stream: playerHelper.visualizerStream, - // builder: (BuildContext context, AsyncSnapshot snapshot) { - // List data = snapshot.data ?? []; - // double width = MediaQuery.of(context).size.width / - // data.length; //- 0.25; - // return Row( - // crossAxisAlignment: CrossAxisAlignment.end, - // children: List.generate( - // data.length, - // (i) => AnimatedContainer( - // duration: Duration(milliseconds: 130), - // color: settings.primaryColor, - // height: data[i] * 100, - // width: width, - // )), - // ); - // }), - // ), - ]), - ), - PlayerBar(shouldHandleClicks: false), - ], - ), + return Scaffold( + appBar: FreezerAppBar('Lyrics'.i18n, + systemUiOverlayStyle: 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, )), + body: SafeArea( + child: Column( + children: [ + Theme( + data: settings.themeData!.copyWith( + textButtonTheme: TextButtonThemeData( + style: ButtonStyle( + foregroundColor: + MaterialStateProperty.all(Colors.white)))), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (_freeScroll && !_loading) + TextButton( + onPressed: () { + setState(() => _freeScroll = false); + _scrollToLyric(); + }, + child: Text( + _currentIndex! >= 0 + ? (lyrics?.lyrics?[_currentIndex!].text ?? '...') + : '...', + textAlign: TextAlign.center, + ), + style: ButtonStyle( + foregroundColor: + MaterialStateProperty.all(Colors.white))) + ], + ), + ), + Expanded( + child: Stack(children: [ + //Lyrics + _error != null + ? + //Shouldn't really happen, empty lyrics have own text + ErrorScreen(message: _error.toString()) + : + // Loading lyrics + _loading + ? Center(child: CircularProgressIndicator()) + : NotificationListener( + onNotification: (Notification notification) { + if (_freeScroll || + notification is! ScrollStartNotification) + return false; + if (!_animatedScroll && !_loading) + setState(() => _freeScroll = true); + return false; + }, + child: ListView.builder( + controller: _controller, + padding: EdgeInsets.fromLTRB( + 0, + 0, + 0, + settings.lyricsVisualizer! && false + ? 100 + : 0), + itemCount: lyrics!.lyrics!.length, + itemBuilder: (BuildContext context, int i) { + return Padding( + padding: + EdgeInsets.symmetric(horizontal: 8.0), + child: Container( + decoration: BoxDecoration( + borderRadius: + BorderRadius.circular(8.0), + color: _currentIndex == i + ? Colors.grey.withOpacity(0.25) + : Colors.transparent, + ), + height: height, + child: InkWell( + borderRadius: + BorderRadius.circular(8.0), + onTap: lyrics!.id != null + ? () => audioHandler.seek( + lyrics!.lyrics![i].offset!) + : null, + child: Center( + child: Text( + lyrics!.lyrics![i].text!, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 26.0, + fontWeight: + (_currentIndex == i) + ? FontWeight.bold + : FontWeight + .normal), + ), + )))); + }, + )), + + //Visualizer + //if (settings.lyricsVisualizer) + // Positioned( + // bottom: 0, + // left: 0, + // right: 0, + // child: StreamBuilder( + // stream: playerHelper.visualizerStream, + // builder: (BuildContext context, AsyncSnapshot snapshot) { + // List data = snapshot.data ?? []; + // double width = MediaQuery.of(context).size.width / + // data.length; //- 0.25; + // return Row( + // crossAxisAlignment: CrossAxisAlignment.end, + // children: List.generate( + // data.length, + // (i) => AnimatedContainer( + // duration: Duration(milliseconds: 130), + // color: settings.primaryColor, + // height: data[i] * 100, + // width: width, + // )), + // ); + // }), + // ), + ]), + ), + PlayerBar(shouldHandleClicks: false), + ], + ), + ), ); } } diff --git a/lib/ui/menu.dart b/lib/ui/menu.dart index b2f4eae..f6c911a 100644 --- a/lib/ui/menu.dart +++ b/lib/ui/menu.dart @@ -32,15 +32,17 @@ class MenuSheet { isScrollControlled: true, context: context, builder: (BuildContext context) { - return ConstrainedBox( - constraints: BoxConstraints( - maxHeight: - (MediaQuery.of(context).orientation == Orientation.landscape) - ? 220 - : 350, - ), - child: SingleChildScrollView( - child: Column(children: options), + return SafeArea( + child: ConstrainedBox( + constraints: BoxConstraints( + maxHeight: (MediaQuery.of(context).orientation == + Orientation.landscape) + ? 220 + : 350, + ), + child: SingleChildScrollView( + child: Column(children: options), + ), ), ); }); @@ -55,75 +57,73 @@ class MenuSheet { isScrollControlled: true, context: context, builder: (BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - height: 16.0, - ), - Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Semantics( - child: CachedImage( - url: track.albumArt!.full, - height: 128, - width: 128, + return SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 16.0), + Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Semantics( + child: CachedImage( + url: track.albumArt!.full, + height: 128, + width: 128, + ), + label: "Album art".i18n, + image: true, ), - label: "Album art".i18n, - image: true, - ), - Container( - width: 240.0, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - track.title!, - maxLines: 1, - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 22.0, fontWeight: FontWeight.bold), - ), - Text( - track.artistString, - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - maxLines: 1, - style: TextStyle(fontSize: 20.0), - ), - Container( - height: 8.0, - ), - Text( - track.album!.title!, - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - maxLines: 1, - ), - Text(track.durationString) - ], + SizedBox( + width: 240.0, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + track.title!, + maxLines: 1, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 22.0, fontWeight: FontWeight.bold), + ), + Text( + track.artistString, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + maxLines: 1, + style: TextStyle(fontSize: 20.0), + ), + Container( + height: 8.0, + ), + Text( + track.album!.title!, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + Text(track.durationString) + ], + ), ), + ], + ), + const SizedBox(height: 16.0), + ConstrainedBox( + constraints: BoxConstraints( + maxHeight: (MediaQuery.of(context).orientation == + Orientation.landscape) + ? 220 + : 350, ), - ], - ), - Container( - height: 16.0, - ), - ConstrainedBox( - constraints: BoxConstraints( - maxHeight: (MediaQuery.of(context).orientation == - Orientation.landscape) - ? 220 - : 350, - ), - child: SingleChildScrollView( - child: Column(children: options), - ), - ) - ], + child: SingleChildScrollView( + child: Column(children: options), + ), + ) + ], + ), ); }); } diff --git a/lib/ui/player_bar.dart b/lib/ui/player_bar.dart index bcd6bae..4e5ebeb 100644 --- a/lib/ui/player_bar.dart +++ b/lib/ui/player_bar.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:audio_service/audio_service.dart'; import 'package:flutter/material.dart'; import 'package:freezer/settings.dart'; @@ -17,6 +19,8 @@ class PlayerBar extends StatefulWidget { class _PlayerBarState extends State { final double iconSize = 28; + late StreamSubscription mediaItemSub; + late bool _isNothingPlaying = audioHandler.mediaItem.value == null; double parsePosition(Duration position) { if (audioHandler.mediaItem.value == null) return 0.0; @@ -26,98 +30,119 @@ class _PlayerBarState extends State { audioHandler.mediaItem.value!.duration!.inSeconds; } + @override + void initState() { + mediaItemSub = audioHandler.mediaItem.listen((playingItem) { + if ((playingItem == null && !_isNothingPlaying) || + (playingItem != null && _isNothingPlaying)) + setState(() => _isNothingPlaying = playingItem == null); + }); + super.initState(); + } + + @override + void dispose() { + mediaItemSub.cancel(); + super.dispose(); + } + bool _gestureRegistered = false; @override Widget build(BuildContext context) { var focusNode = FocusNode(); - return GestureDetector( - onHorizontalDragUpdate: (details) async { - if (_gestureRegistered) return; - final double sensitivity = 12.69; - //Right swipe - _gestureRegistered = true; - if (details.delta.dx > sensitivity) { - await audioHandler.skipToPrevious(); - } - //Left - if (details.delta.dx < -sensitivity) { - await audioHandler.skipToNext(); - } - _gestureRegistered = false; - return; - }, - child: Column(mainAxisSize: MainAxisSize.min, children: [ - StreamBuilder( - stream: audioHandler.mediaItem, - builder: (context, snapshot) { - if (!snapshot.hasData) return const SizedBox(); - final currentMediaItem = snapshot.data!; - return DecoratedBox( - // For Android TV: indicate focus by grey - decoration: BoxDecoration( - color: focusNode.hasFocus - ? Colors.black26 - : Theme.of(context).bottomAppBarColor), - child: ListTile( - dense: true, - focusNode: focusNode, - contentPadding: EdgeInsets.symmetric(horizontal: 8.0), - onTap: widget.shouldHandleClicks - ? () { - Navigator.of(context).push(MaterialPageRoute( - builder: (BuildContext context) => - PlayerScreen())); - } - : null, - leading: CachedImage( - width: 50, - height: 50, - url: currentMediaItem.extras!['thumb'] ?? - audioHandler.mediaItem.value!.artUri as String?, - ), - title: Text( - currentMediaItem.displayTitle!, - overflow: TextOverflow.clip, - maxLines: 1, - ), - subtitle: Text( - currentMediaItem.displaySubtitle ?? '', - overflow: TextOverflow.clip, - maxLines: 1, - ), - trailing: IconTheme( - data: IconThemeData( - color: settings.isDark - ? Colors.white - : Colors.grey[600]), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - PrevNextButton( - iconSize, - prev: true, + return _isNothingPlaying + ? const SizedBox() + : GestureDetector( + onHorizontalDragUpdate: (details) async { + if (_gestureRegistered) return; + final double sensitivity = 12.69; + //Right swipe + _gestureRegistered = true; + if (details.delta.dx > sensitivity) { + await audioHandler.skipToPrevious(); + } + //Left + if (details.delta.dx < -sensitivity) { + await audioHandler.skipToNext(); + } + _gestureRegistered = false; + return; + }, + child: Column(mainAxisSize: MainAxisSize.min, children: [ + StreamBuilder( + stream: audioHandler.mediaItem, + builder: (context, snapshot) { + if (!snapshot.hasData) return const SizedBox(); + final currentMediaItem = snapshot.data!; + return DecoratedBox( + // For Android TV: indicate focus by grey + decoration: BoxDecoration( + color: focusNode.hasFocus + ? Colors.black26 + : Theme.of(context).bottomAppBarColor), + child: ListTile( + dense: true, + focusNode: focusNode, + contentPadding: + EdgeInsets.symmetric(horizontal: 8.0), + onTap: widget.shouldHandleClicks + ? () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (BuildContext context) => + PlayerScreen())); + } + : null, + leading: CachedImage( + width: 50, + height: 50, + url: currentMediaItem.extras!['thumb'] ?? + audioHandler.mediaItem.value!.artUri + as String?, ), - PlayPauseButton(iconSize), - PrevNextButton(iconSize) - ], - ), - ))); - }), - SizedBox( - height: 3.0, - child: StreamBuilder( - stream: AudioService.position, - builder: (context, snapshot) { - return LinearProgressIndicator( - backgroundColor: - Theme.of(context).primaryColor.withOpacity(0.1), - value: parsePosition(snapshot.data ?? Duration.zero), - ); - }), - ), - ]), - ); + title: Text( + currentMediaItem.displayTitle!, + overflow: TextOverflow.clip, + maxLines: 1, + ), + subtitle: Text( + currentMediaItem.displaySubtitle ?? '', + overflow: TextOverflow.clip, + maxLines: 1, + ), + trailing: IconTheme( + data: IconThemeData( + color: settings.isDark + ? Colors.white + : Colors.grey[600]), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + PrevNextButton( + iconSize, + prev: true, + ), + PlayPauseButton(iconSize), + PrevNextButton(iconSize) + ], + ), + ))); + }), + SizedBox( + height: 3.0, + child: StreamBuilder( + stream: AudioService.position, + builder: (context, snapshot) { + return LinearProgressIndicator( + backgroundColor: + Theme.of(context).primaryColor.withOpacity(0.1), + value: parsePosition(snapshot.data ?? Duration.zero), + ); + }), + ), + ]), + ); } } @@ -205,7 +230,6 @@ class _PlayPauseButtonState extends State _controller.reverse(); return IconButton( - splashRadius: widget.size, icon: AnimatedIcon( icon: AnimatedIcons.play_pause, progress: _animation, diff --git a/lib/ui/player_screen.dart b/lib/ui/player_screen.dart index d7d6094..85b5a0f 100644 --- a/lib/ui/player_screen.dart +++ b/lib/ui/player_screen.dart @@ -4,6 +4,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:audio_service/audio_service.dart'; +import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:fluttertoast/fluttertoast.dart'; @@ -21,6 +22,7 @@ import 'package:freezer/ui/tiles.dart'; import 'package:just_audio/just_audio.dart'; import 'package:marquee/marquee.dart'; import 'package:palette_generator/palette_generator.dart'; +import 'package:photo_view/photo_view.dart'; import 'cached_image.dart'; import '../api/definitions.dart'; @@ -176,110 +178,85 @@ class PlayerScreenHorizontal extends StatefulWidget { class _PlayerScreenHorizontalState extends State { @override Widget build(BuildContext context) { - return Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - Padding( - padding: EdgeInsets.fromLTRB(16, 0, 16, 8), - child: Container( - width: ScreenUtil().setWidth(500), - child: Stack( + return Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + Expanded( + flex: 5, + child: BigAlbumArt(), + ), + //Right side + Expanded( + flex: 5, + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - BigAlbumArt(), + Padding( + padding: EdgeInsets.fromLTRB(8, 0, 8, 0), + child: Container( + child: PlayerScreenTopRow( + textSize: ScreenUtil().setSp(24), + iconSize: ScreenUtil().setSp(36), + textWidth: ScreenUtil().setWidth(350), + short: true), + )), + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + height: ScreenUtil().setSp(50), + child: audioHandler + .mediaItem.value!.displayTitle!.length >= + 22 + ? Marquee( + text: + audioHandler.mediaItem.value!.displayTitle!, + style: TextStyle( + fontSize: 40.sp, + fontWeight: FontWeight.bold), + blankSpace: 32.0, + startPadding: 10.0, + accelerationDuration: Duration(seconds: 1), + pauseAfterRound: Duration(seconds: 2), + ) + : Text( + audioHandler.mediaItem.value!.displayTitle!, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 40.sp, + fontWeight: FontWeight.bold), + )), + const SizedBox(height: 4.0), + Text( + audioHandler.mediaItem.value!.displaySubtitle ?? '', + maxLines: 1, + textAlign: TextAlign.center, + overflow: TextOverflow.clip, + style: TextStyle( + fontSize: 32.sp, + color: Theme.of(context).primaryColor, + ), + ), + ], + ), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 16.0), + child: SeekBar(), + ), + PlaybackControls(56.sp), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10.0), + child: BottomBarControls(size: 42.sp), + ) ], ), - ), - ), - //Right side - SizedBox( - width: ScreenUtil().setWidth(500), - child: Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: EdgeInsets.fromLTRB(8, 16, 8, 0), - child: Container( - child: PlayerScreenTopRow( - textSize: ScreenUtil().setSp(24), - iconSize: ScreenUtil().setSp(36), - textWidth: ScreenUtil().setWidth(350), - short: true), - )), - Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - height: ScreenUtil().setSp(50), - child: audioHandler - .mediaItem.value!.displayTitle!.length >= - 22 - ? Marquee( - text: audioHandler.mediaItem.value!.displayTitle!, - style: TextStyle( - fontSize: ScreenUtil().setSp(40), - fontWeight: FontWeight.bold), - blankSpace: 32.0, - startPadding: 10.0, - accelerationDuration: Duration(seconds: 1), - pauseAfterRound: Duration(seconds: 2), - ) - : Text( - audioHandler.mediaItem.value!.displayTitle!, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: ScreenUtil().setSp(40), - fontWeight: FontWeight.bold), - )), - const SizedBox(height: 4.0), - Text( - audioHandler.mediaItem.value!.displaySubtitle ?? '', - maxLines: 1, - textAlign: TextAlign.center, - overflow: TextOverflow.clip, - style: TextStyle( - fontSize: ScreenUtil().setSp(32), - color: Theme.of(context).primaryColor, - ), - ), - ], - ), - Container( - padding: EdgeInsets.symmetric(horizontal: 16.0), - child: SeekBar(), - ), - PlaybackControls(ScreenUtil().setSp(60)), - Padding( - padding: EdgeInsets.fromLTRB(8, 0, 8, 16), - child: Container( - padding: EdgeInsets.symmetric(horizontal: 2.0), - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - IconButton( - icon: Icon( - Icons.subtitles, - size: ScreenUtil().setWidth(32), - semanticLabel: "Lyrics".i18n, - ), - onPressed: () { - Navigator.of(context).push(MaterialPageRoute( - builder: (context) => LyricsScreen())); - }, - ), - QualityInfoWidget(), - RepeatButton(ScreenUtil().setWidth(32)), - PlayerMenuButton() - ], - ), - )) - ], - ), - ) - ], + ) + ], + ), ); } } @@ -293,116 +270,68 @@ class PlayerScreenVertical extends StatefulWidget { class _PlayerScreenVerticalState extends State { @override Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: EdgeInsets.fromLTRB(30, 4, 16, 0), - child: PlayerScreenTopRow()), - Padding( - padding: EdgeInsets.symmetric(horizontal: 16.0), - child: SizedBox( - child: BigAlbumArt(), - width: min(MediaQuery.of(context).size.width, - MediaQuery.of(context).size.height) * - 0.9, - height: min(MediaQuery.of(context).size.width, - MediaQuery.of(context).size.height) * - 0.9, - ), - ), - const SizedBox(height: 4.0), - Column( - mainAxisSize: MainAxisSize.min, + return Padding( + padding: EdgeInsets.fromLTRB(30, 4, 16, 0), + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Container( - height: ScreenUtil().setSp(80), - child: audioHandler.mediaItem.value!.displayTitle!.length >= 26 - ? Marquee( - text: audioHandler.mediaItem.value!.displayTitle!, - style: TextStyle( - fontSize: ScreenUtil().setSp(64), - fontWeight: FontWeight.bold), - blankSpace: 32.0, - startPadding: 10.0, - accelerationDuration: Duration(seconds: 1), - pauseAfterRound: Duration(seconds: 2), - ) - : Text( - audioHandler.mediaItem.value!.displayTitle!, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: ScreenUtil().setSp(64), - fontWeight: FontWeight.bold), - )), - const SizedBox(height: 4), - Text( - audioHandler.mediaItem.value!.displaySubtitle ?? '', - maxLines: 1, - textAlign: TextAlign.center, - overflow: TextOverflow.clip, - style: TextStyle( - fontSize: ScreenUtil().setSp(52), - color: Theme.of(context).primaryColor, + PlayerScreenTopRow(), + Padding( + padding: EdgeInsets.symmetric(horizontal: 16.0), + child: SizedBox( + child: BigAlbumArt(), + width: 1000.w, + height: 1000.w, ), ), + const SizedBox(height: 4.0), + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + height: ScreenUtil().setSp(80), + child: audioHandler.mediaItem.value!.displayTitle!.length >= + 26 + ? Marquee( + text: audioHandler.mediaItem.value!.displayTitle!, + style: TextStyle( + fontSize: ScreenUtil().setSp(64), + fontWeight: FontWeight.bold), + blankSpace: 32.0, + startPadding: 10.0, + accelerationDuration: Duration(seconds: 1), + pauseAfterRound: Duration(seconds: 2), + ) + : Text( + audioHandler.mediaItem.value!.displayTitle!, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: ScreenUtil().setSp(64), + fontWeight: FontWeight.bold), + )), + const SizedBox(height: 4), + Text( + audioHandler.mediaItem.value!.displaySubtitle ?? '', + maxLines: 1, + textAlign: TextAlign.center, + overflow: TextOverflow.clip, + style: TextStyle( + fontSize: ScreenUtil().setSp(52), + color: Theme.of(context).primaryColor, + ), + ), + ], + ), + SeekBar(), + PlaybackControls(86.sp), + Padding( + padding: EdgeInsets.symmetric(vertical: 0, horizontal: 16.0), + child: BottomBarControls(size: 56.sp), + ) ], - ), - SeekBar(), - PlaybackControls(ScreenUtil().setWidth(100)), - Padding( - padding: EdgeInsets.symmetric(vertical: 0, horizontal: 16.0), - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - IconButton( - icon: Icon( - Icons.subtitles, - size: ScreenUtil().setWidth(46), - semanticLabel: "Lyrics".i18n, - ), - onPressed: () async { - //Fix bottom buttons - SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle( - systemNavigationBarColor: - settings.themeData!.bottomAppBarColor, - statusBarColor: Colors.transparent)); - - await Navigator.of(context).push( - MaterialPageRoute(builder: (context) => LyricsScreen())); - - updateColor(); - }, - ), - IconButton( - icon: Icon( - Icons.file_download, - semanticLabel: "Download".i18n, - ), - onPressed: () async { - Track t = Track.fromMediaItem(audioHandler.mediaItem.value!); - if (await downloadManager.addOfflineTrack(t, - private: false, - context: context, - isSingleton: true) != - false) - Fluttertoast.showToast( - msg: 'Downloads added!'.i18n, - gravity: ToastGravity.BOTTOM, - toastLength: Toast.LENGTH_SHORT); - }, - ), - QualityInfoWidget(), - RepeatButton(ScreenUtil().setWidth(46)), - PlayerMenuButton() - ], - ), - ) - ], - ); + )); } } @@ -422,7 +351,7 @@ class _QualityInfoWidgetState extends State { "getStreamInfo", {"id": audioHandler.mediaItem.value!.id}); //N/A if (data == null) { - setState(() => value = ''); + if (mounted) setState(() => value = ''); //If not show, try again later if (audioHandler.mediaItem.value!.extras!['show'] == null) Future.delayed(Duration(milliseconds: 200), _load); @@ -439,10 +368,8 @@ class _QualityInfoWidgetState extends State { @override void initState() { - _load(); - streamSubscription = audioHandler.mediaItem.listen((event) async { - _load(); - }); + SchedulerBinding.instance!.addPostFrameCallback((_) => _load()); + streamSubscription = audioHandler.mediaItem.listen((_) => _load()); super.initState(); } @@ -465,12 +392,15 @@ class _QualityInfoWidgetState extends State { } class PlayerMenuButton extends StatelessWidget { + final double size; + const PlayerMenuButton({required this.size}); + @override Widget build(BuildContext context) { return IconButton( icon: Icon( Icons.more_vert, - size: ScreenUtil().setWidth(46), + size: size, semanticLabel: "Options".i18n, ), onPressed: () { @@ -552,13 +482,11 @@ class _PlaybackControlsState extends State { Track.fromMediaItem(audioHandler.mediaItem.value!))) { return Icon( Icons.favorite, - size: widget.iconSize * 0.64, semanticLabel: "Unlove".i18n, ); } return Icon( Icons.favorite_border, - size: widget.iconSize * 0.64, semanticLabel: "Love".i18n, ); } @@ -574,9 +502,9 @@ class _PlaybackControlsState extends State { IconButton( icon: Icon( Icons.sentiment_very_dissatisfied, - size: ScreenUtil().setWidth(46), semanticLabel: "Dislike".i18n, ), + iconSize: widget.iconSize * 0.75, onPressed: () async { await deezerAPI.dislikeTrack(audioHandler.mediaItem.value!.id); if (playerHelper.queueIndex < @@ -589,6 +517,7 @@ class _PlaybackControlsState extends State { PrevNextButton(widget.iconSize), IconButton( icon: libraryIcon, + iconSize: widget.iconSize * 0.75, onPressed: () async { if (cache.libraryTracks == null) cache.libraryTracks = []; @@ -624,6 +553,7 @@ class BigAlbumArt extends StatefulWidget { class _BigAlbumArtState extends State { PageController _pageController = PageController( initialPage: playerHelper.queueIndex, + viewportFraction: 1.0, ); StreamSubscription? _currentItemSub; bool _animationLock = true; @@ -632,6 +562,7 @@ class _BigAlbumArtState extends State { void initState() { _currentItemSub = audioHandler.mediaItem.listen((event) async { _animationLock = true; + // TODO: a lookup in the entire queue isn't that good, this can definitely be improved in some way await _pageController.animateToPage(playerHelper.queueIndex, duration: Duration(milliseconds: 300), curve: Curves.easeInOut); _animationLock = false; @@ -653,6 +584,22 @@ class _BigAlbumArtState extends State { Navigator.of(context).pop(); } }, + onTap: () => Navigator.push( + context, + PageRouteBuilder( + opaque: false, // transparent background + barrierDismissible: true, + pageBuilder: (context, _, __) { + return PhotoView( + imageProvider: CachedNetworkImageProvider( + audioHandler.mediaItem.value!.artUri.toString()), + maxScale: 8.0, + minScale: 0.2, + heroAttributes: PhotoViewHeroAttributes( + tag: audioHandler.mediaItem.value!.id), + backgroundDecoration: + BoxDecoration(color: Color.fromARGB(0x90, 0, 0, 0))); + })), child: PageView( controller: _pageController, onPageChanged: (int index) { @@ -665,8 +612,14 @@ class _BigAlbumArtState extends State { }, children: List.generate( audioHandler.queue.value.length, - (i) => ZoomableImage( - url: audioHandler.queue.value[i].artUri.toString(), + (i) => Padding( + padding: const EdgeInsets.all(8.0), + child: Hero( + tag: audioHandler.queue.value[i].id, + child: CachedImage( + url: audioHandler.queue.value[i].artUri.toString(), + ), + ), )), ), ); @@ -719,6 +672,8 @@ class PlayerScreenTopRow extends StatelessWidget { } class SeekBar extends StatefulWidget { + const SeekBar(); + @override _SeekBarState createState() => _SeekBarState(); } @@ -763,26 +718,6 @@ class _SeekBarState extends State { return Column( mainAxisSize: MainAxisSize.min, children: [ - Padding( - padding: EdgeInsets.symmetric(horizontal: 24.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - ValueListenableBuilder( - valueListenable: position, - builder: (context, value, _) => Text( - _timeString(value), - style: TextStyle(fontSize: ScreenUtil().setSp(35)), - )), - StreamBuilder( - stream: audioHandler.mediaItem, - builder: (context, snapshot) => Text( - _timeString(snapshot.data?.duration ?? Duration.zero), - style: TextStyle(fontSize: ScreenUtil().setSp(35)), - )), - ], - ), - ), ValueListenableBuilder( valueListenable: position, builder: (context, value, _) => Slider( @@ -804,6 +739,38 @@ class _SeekBarState extends State { audioHandler.seek(Duration(milliseconds: d.toInt())); }, )), + Padding( + padding: EdgeInsets.symmetric(horizontal: 24.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ValueListenableBuilder( + valueListenable: position, + builder: (context, value, _) => Text( + _timeString(value), + style: TextStyle( + fontSize: ScreenUtil().setSp(35), + color: Theme.of(context) + .textTheme + .bodyText1! + .color! + .withOpacity(.75)), + )), + StreamBuilder( + stream: audioHandler.mediaItem, + builder: (context, snapshot) => Text( + _timeString(snapshot.data?.duration ?? Duration.zero), + style: TextStyle( + fontSize: ScreenUtil().setSp(35), + color: Theme.of(context) + .textTheme + .bodyText1! + .color! + .withOpacity(.75)), + )), + ], + ), + ), ], ); } @@ -816,6 +783,20 @@ class QueueScreen extends StatefulWidget { class _QueueScreenState extends State { late StreamSubscription _queueSub; + static const _dismissibleBackground = DecoratedBox( + decoration: BoxDecoration(color: Colors.red), + child: Align( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 24.0), + child: Icon(Icons.delete)), + alignment: Alignment.centerLeft)); + static const _dismissibleSecondaryBackground = DecoratedBox( + decoration: BoxDecoration(color: Colors.red), + child: Align( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 24.0), + child: Icon(Icons.delete)), + alignment: Alignment.centerRight)); /// Basically a simple list that keeps itself synchronized with [AudioHandler.queue], /// so that the [ReorderableListView] is updated instanly (as it should be) @@ -827,7 +808,10 @@ class _QueueScreenState extends State { _queueSub = audioHandler.queue.listen((newQueue) { print('got queue $newQueue'); // avoid rebuilding if the cache has got the right update - if (listEquals(_queueCache, newQueue)) return; + if (listEquals(_queueCache, newQueue)) { + print('avoiding rebuilding queue since they are the same'); + return; + } setState(() => _queueCache = newQueue); }); super.initState(); @@ -842,22 +826,32 @@ class _QueueScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: FreezerAppBar( - 'Queue'.i18n, - actions: [ - IconButton( - icon: Icon( - Icons.shuffle, - semanticLabel: "Shuffle".i18n, - ), - onPressed: () async { - await playerHelper.toggleShuffle(); - setState(() {}); - }, - ) - ], + appBar: FreezerAppBar( + 'Queue'.i18n, + systemUiOverlayStyle: 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, ), - body: ReorderableListView.builder( + actions: [ + IconButton( + icon: Icon( + Icons.shuffle, + semanticLabel: "Shuffle".i18n, + ), + onPressed: () async { + await playerHelper.toggleShuffle(); + setState(() {}); + }, + ) + ], + ), + body: SafeArea( + child: ReorderableListView.builder( onReorder: (int oldIndex, int newIndex) { if (oldIndex == playerHelper.queueIndex) return; setState(() => _queueCache.reorder(oldIndex, newIndex)); @@ -868,20 +862,8 @@ class _QueueScreenState extends State { Track track = Track.fromMediaItem(audioHandler.queue.value[i]); return Dismissible( key: Key(track.id), - background: DecoratedBox( - decoration: BoxDecoration(color: Colors.red), - child: Align( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Icon(Icons.delete)), - alignment: Alignment.centerLeft)), - secondaryBackground: DecoratedBox( - decoration: BoxDecoration(color: Colors.red), - child: Align( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Icon(Icons.delete)), - alignment: Alignment.centerRight)), + background: _dismissibleBackground, + secondaryBackground: _dismissibleSecondaryBackground, onDismissed: (_) { audioHandler.removeQueueItemAt(i); setState(() => _queueCache.removeAt(i)); @@ -898,6 +880,56 @@ class _QueueScreenState extends State { ), ); }, - )); + ), + ), + ); + } +} + +class BottomBarControls extends StatelessWidget { + final double size; + const BottomBarControls({Key? key, required this.size}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + IconButton( + icon: Icon( + Icons.subtitles, + size: size, + semanticLabel: "Lyrics".i18n, + ), + onPressed: () async { + await Navigator.of(context) + .push(MaterialPageRoute(builder: (context) => LyricsScreen())); + + updateColor(); + }, + ), + IconButton( + iconSize: size, + icon: Icon( + Icons.file_download, + semanticLabel: "Download".i18n, + ), + onPressed: () async { + Track t = Track.fromMediaItem(audioHandler.mediaItem.value!); + if (await downloadManager.addOfflineTrack(t, + private: false, context: context, isSingleton: true) != + false) + Fluttertoast.showToast( + msg: 'Downloads added!'.i18n, + gravity: ToastGravity.BOTTOM, + toastLength: Toast.LENGTH_SHORT); + }, + ), + QualityInfoWidget(), + RepeatButton(size), + PlayerMenuButton(size: size) + ], + ); } } diff --git a/lib/ui/settings_screen.dart b/lib/ui/settings_screen.dart index 077e1ad..c39415c 100644 --- a/lib/ui/settings_screen.dart +++ b/lib/ui/settings_screen.dart @@ -240,19 +240,20 @@ class _AppearanceSettingsState extends State { .i18n), secondary: Icon(Icons.equalizer), value: settings.lyricsVisualizer!, - onChanged: (bool v) async { - if (await Permission.microphone.request().isGranted) { - setState(() => settings.lyricsVisualizer = v); - await settings.save(); - return; - } - }, + onChanged: null, // TODO: visualizer + //(bool v) async { + // if (await Permission.microphone.request().isGranted) { + // setState(() => settings.lyricsVisualizer = v); + // await settings.save(); + // return; + // } + //}, ), ListTile( title: Text('Primary color'.i18n), leading: Icon(Icons.format_paint), trailing: Padding( - padding: EdgeInsets.only(left: 8.0), + padding: EdgeInsets.only(right: 8.0), child: CircleAvatar( backgroundColor: settings.primaryColor, )), diff --git a/pubspec.lock b/pubspec.lock index bd22dff..1f11fe0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -548,10 +548,10 @@ packages: description: path: just_audio ref: dev - resolved-ref: "7ac783939a758be2799faefc8877c34a84fe1554" + resolved-ref: "27a777fbb5e0fca2c9db6bdf1cdc9672b3fe9971" url: "https://github.com/ryanheise/just_audio.git" source: git - version: "0.9.7" + version: "0.9.8" just_audio_platform_interface: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 87ff039..0f0f1e7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -50,7 +50,7 @@ dependencies: disk_space: git: https://github.com/phipps980316/disk_space random_string: ^2.0.1 - async: ^2.8.1 + async: ^2.6.1 html: ^0.15.0 flutter_screenutil: ^5.0.0+2 marquee: ^2.2.0