From 4b5d0bd09cc354bed35b21a3216028fe79fb5332 Mon Sep 17 00:00:00 2001 From: Pato05 Date: Mon, 29 Apr 2024 16:23:22 +0200 Subject: [PATCH] improve player screen with blurred album art ui improvements in lyrics screen animated bars when track is playing fix back button when player screen is open instantly pop when track is changed in queue list --- lib/ui/animated_bars.dart | 51 ---------- lib/ui/fancy_scaffold.dart | 178 +++++++++++++++++------------------ lib/ui/home_screen.dart | 72 +++++++++------ lib/ui/lyrics_screen.dart | 184 +++++++++++++++++++++---------------- lib/ui/player_screen.dart | 10 +- lib/ui/queue_screen.dart | 9 +- lib/ui/tiles.dart | 72 +++++++-------- pubspec.lock | 2 +- pubspec.yaml | 1 + 9 files changed, 287 insertions(+), 292 deletions(-) delete mode 100644 lib/ui/animated_bars.dart diff --git a/lib/ui/animated_bars.dart b/lib/ui/animated_bars.dart deleted file mode 100644 index 6671db3..0000000 --- a/lib/ui/animated_bars.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'dart:math'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; - -class AnimatedBars extends StatefulWidget { - final double size; - final Color? color; - const AnimatedBars({ - super.key, - this.size = 24.0, - this.color, - }); - - @override - State createState() => _AnimatedBarsState(); -} - -class _AnimatedBarsState extends State - with TickerProviderStateMixin { - late final _controller = AnimationController( - vsync: this, duration: const Duration(milliseconds: 1000)) - ..repeat(reverse: true); - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final color = widget.color ?? Theme.of(context).colorScheme.onSurface; - const count = 3; - AnimatedIcons.search_ellipsis; - return SizedBox.square( - dimension: widget.size, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: List.generate( - count, - (index) => SizedBox( - width: widget.size / count, - child: Align( - alignment: Alignment.bottomCenter, - child: Container(color: color), - ), - )), - )); - } -} diff --git a/lib/ui/fancy_scaffold.dart b/lib/ui/fancy_scaffold.dart index 93a8d95..fbb6f17 100644 --- a/lib/ui/fancy_scaffold.dart +++ b/lib/ui/fancy_scaffold.dart @@ -71,106 +71,110 @@ class FancyScaffoldState extends State begin: widget.bottomPanelHeight / MediaQuery.of(context).size.height, end: 1.0, ).animate(dragController); - return Stack( - children: [ - Positioned.fill( - child: Scaffold( - body: widget.navigationRail != null - ? Row(children: [ - widget.navigationRail!, - const VerticalDivider( - indent: 0.0, - endIndent: 0.0, - width: 2.0, + return ValueListenableBuilder( + valueListenable: statusNotifier, + builder: (context, state, child) => PopScope( + canPop: state != AnimationStatus.dismissed, + onPopInvoked: state == AnimationStatus.dismissed + ? null + : (_) => dragController.fling(velocity: -1.0), + child: child!), + child: Stack( + children: [ + Positioned.fill( + child: Scaffold( + body: widget.navigationRail != null + ? Row(children: [ + widget.navigationRail!, + const VerticalDivider( + indent: 0.0, + endIndent: 0.0, + width: 2.0, + ), + Expanded(child: widget.body) + ]) + : widget.body, + drawer: widget.drawer, + bottomNavigationBar: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(height: widget.bottomPanelHeight), + if (widget.bottomNavigationBar != null) + SizeTransition( + axisAlignment: -1.0, + sizeFactor: + Tween(begin: 1.0, end: 0.0).animate(sizeAnimation), + child: widget.bottomNavigationBar, ), - Expanded(child: widget.body) - ]) - : widget.body, - drawer: widget.drawer, - bottomNavigationBar: Column( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox(height: widget.bottomPanelHeight), - if (widget.bottomNavigationBar != null) - SizeTransition( - axisAlignment: -1.0, - sizeFactor: - Tween(begin: 1.0, end: 0.0).animate(sizeAnimation), - child: widget.bottomNavigationBar, - ), - ], + ], + ), ), ), - ), - Positioned( - bottom: 0, - left: 0, - right: 0, - child: AnimatedBuilder( - animation: sizeAnimation, - builder: (context, child) { - final x = 1.0 - sizeAnimation.value; - return Padding( - padding: EdgeInsets.only( - bottom: (defaultBottomPadding /*+ 8.0*/) * x, - //right: 8.0 * x, - //left: 8.0 * x, - ), - child: child, - ); - }, - child: ValueListenableBuilder( - valueListenable: statusNotifier, - builder: (context, state, child) { - return GestureDetector( - onVerticalDragEnd: _onVerticalDragEnd, - onVerticalDragUpdate: _onVerticalDragUpdate, + Positioned( + bottom: 0, + left: 0, + right: 0, + child: AnimatedBuilder( + animation: sizeAnimation, + builder: (context, child) { + final x = 1.0 - sizeAnimation.value; + return Padding( + padding: EdgeInsets.only( + bottom: (defaultBottomPadding /*+ 8.0*/) * x, + //right: 8.0 * x, + //left: 8.0 * x, + ), child: child, ); }, - child: SizeTransition( - sizeFactor: sizeAnimation, - axisAlignment: -1.0, - axis: Axis.vertical, - child: SizedBox( - height: screenHeight, - width: MediaQuery.of(context).size.width, - child: ValueListenableBuilder( - valueListenable: statusNotifier, - builder: (context, state, _) => Stack( - children: [ - if (state != AnimationStatus.dismissed) - PopScope( - canPop: false, - onPopInvoked: (_) => - dragController.fling(velocity: -1.0), - child: Positioned.fill( + child: ValueListenableBuilder( + valueListenable: statusNotifier, + builder: (context, state, child) { + return GestureDetector( + onVerticalDragEnd: _onVerticalDragEnd, + onVerticalDragUpdate: _onVerticalDragUpdate, + child: child, + ); + }, + child: SizeTransition( + sizeFactor: sizeAnimation, + axisAlignment: -1.0, + axis: Axis.vertical, + child: SizedBox( + height: screenHeight, + width: MediaQuery.of(context).size.width, + child: ValueListenableBuilder( + valueListenable: statusNotifier, + builder: (context, state, _) => Stack( + children: [ + if (state != AnimationStatus.dismissed) + Positioned.fill( key: const Key('player_screen'), child: widget.expandedPanel, ), - ), - if (state != AnimationStatus.completed) - Positioned( - top: 0, - right: 0, - left: 0, - key: const Key('player_bar'), - child: FadeTransition( - opacity: Tween(begin: 1.0, end: 0.0) - .animate(dragController), - child: SizedBox( - height: widget.bottomPanelHeight, - child: widget.bottomPanel), + if (state != AnimationStatus.completed) + Positioned( + top: 0, + right: 0, + left: 0, + key: const Key('player_bar'), + child: FadeTransition( + opacity: Tween(begin: 1.0, end: 0.0) + .animate(dragController), + child: SizedBox( + height: widget.bottomPanelHeight, + child: widget.bottomPanel), + ), ), - ), - ], + ], + ), ), - ), - )), + )), + ), ), ), - ), - ], + ], + ), ); } diff --git a/lib/ui/home_screen.dart b/lib/ui/home_screen.dart index 6010e9b..9f597e0 100644 --- a/lib/ui/home_screen.dart +++ b/lib/ui/home_screen.dart @@ -248,14 +248,23 @@ class HomepageRowSection extends StatelessWidget { final HomePageSection section; const HomepageRowSection(this.section, {super.key}); - Widget buildChild(BuildContext context, List items) { - return Row( - children: items - .map((item) => Padding( - padding: const EdgeInsets.symmetric(horizontal: 6.0), - child: HomePageItemWidget(item), - )) - .toList(growable: false)); + Widget buildChild(BuildContext context, List items, + {bool hasMore = false}) { + return Row(children: [ + ...items.map((item) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 6.0), + child: HomePageItemWidget(item), + )), + if (hasMore) + TextButton( + onPressed: () => Navigator.of(context).pushRoute( + builder: (context) => HomePageScreen( + title: section.title!, + channel: DeezerChannel(target: section.pagePath), + ), + ), + child: Text('Show more'.i18n)) + ]); } List> _sliceInNLists(List source, int n) { @@ -279,15 +288,37 @@ class HomepageRowSection extends StatelessWidget { children: _sliceInNLists(section.items!, 3) .map((e) => buildChild(context, e)) .toList(growable: false)), - _ => buildChild(context, section.items!) + _ => + buildChild(context, section.items!, hasMore: section.hasMore ?? false) }; return ListTile( - title: Text( - section.title ?? '', - textAlign: TextAlign.left, - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: const TextStyle(fontSize: 20.0, fontWeight: FontWeight.w900), + title: InkWell( + onTap: section.hasMore == true + ? () => Navigator.of(context).pushRoute( + builder: (context) => HomePageScreen( + title: section.title!, + channel: DeezerChannel(target: section.pagePath), + ), + ) + : null, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + section.title ?? '', + textAlign: TextAlign.left, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: + const TextStyle(fontSize: 20.0, fontWeight: FontWeight.w900), + )), + if (section.hasMore == true) ...[ + const SizedBox(width: 16.0), + const Icon(Icons.keyboard_arrow_right), + ], + ], + ), ), subtitle: Scrollbar( thickness: MainScreen.of(context).isDesktop ? null : 1.0, @@ -296,17 +327,6 @@ class HomepageRowSection extends StatelessWidget { child: child, ), ), - trailing: section.hasMore == true - ? const Icon(Icons.keyboard_arrow_right) - : null, - onTap: section.hasMore == true - ? () => Navigator.of(context).pushRoute( - builder: (context) => HomePageScreen( - title: section.title!, - channel: DeezerChannel(target: section.pagePath), - ), - ) - : null, ); } } diff --git a/lib/ui/lyrics_screen.dart b/lib/ui/lyrics_screen.dart index 8fdf6c8..c5c2da6 100644 --- a/lib/ui/lyrics_screen.dart +++ b/lib/ui/lyrics_screen.dart @@ -1,7 +1,9 @@ import 'dart:async'; +import 'dart:collection'; import 'package:audio_service/audio_service.dart'; import 'package:dio/dio.dart'; +import 'package:fading_edge_scrollview/fading_edge_scrollview.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:freezer/api/definitions.dart'; @@ -12,6 +14,7 @@ import 'package:freezer/translations.i18n.dart'; import 'package:freezer/ui/error.dart'; import 'package:freezer/ui/player_bar.dart'; import 'package:freezer/ui/player_screen.dart'; +import 'package:mini_music_visualizer/mini_music_visualizer.dart'; class LyricsScreen extends StatelessWidget { const LyricsScreen({super.key}); @@ -21,11 +24,10 @@ class LyricsScreen extends StatelessWidget { return PlayerScreenBackground( enabled: settings.playerBackgroundOnLyrics, appBar: AppBar( - title: Text('Lyrics'.i18n), systemOverlayStyle: PlayerScreenBackground.getSystemUiOverlayStyle( context, enabled: settings.playerBackgroundOnLyrics), - backgroundColor: Colors.transparent, + forceMaterialTransparency: true, ), child: const Column( children: [ @@ -48,7 +50,7 @@ class _LyricsWidgetState extends State with WidgetsBindingObserver { StreamSubscription? _mediaItemSub; StreamSubscription? _positionSub; - int? _currentIndex = -1; + int _currentIndex = -1; Duration _nextOffset = Duration.zero; Duration _currentOffset = Duration.zero; String? _currentTrackId; @@ -68,6 +70,9 @@ class _LyricsWidgetState extends State bool _showTranslation = false; bool _availableTranslation = false; + // each individual lyric widget's height, either computed or cached + final _lyricHeights = HashMap(); + Future _loadForId(String trackId) async { if (_currentTrackId == trackId) return; _currentTrackId = trackId; @@ -137,7 +142,7 @@ class _LyricsWidgetState extends State } else { final widgetHeight = _widgetConstraints!.maxHeight; final minScroll = actualHeight * _currentIndex!; - scrollTo = minScroll - widgetHeight / 2 + height / 2; + scrollTo = minScroll + height / 2; } if (scrollTo < 0.0) scrollTo = 0.0; @@ -157,10 +162,10 @@ class _LyricsWidgetState extends State if (position < _nextOffset && position > _currentOffset) return; _currentIndex = - _lyrics?.lyrics?.lastIndexWhere((l) => l.offset! <= position); - if (_currentIndex! < 0) return; + _lyrics?.lyrics?.lastIndexWhere((l) => l.offset! <= position) ?? -1; + if (_currentIndex < 0) return; - if (_currentIndex! < _lyrics!.lyrics!.length - 1) { + if (_currentIndex < _lyrics!.lyrics!.length - 1) { // update nextOffset _nextOffset = _lyrics!.lyrics![_currentIndex! + 1].offset!; } else { @@ -208,7 +213,6 @@ class _LyricsWidgetState extends State @override void didChangeAppLifecycleState(AppLifecycleState state) { - print('fuck? $state'); switch (state) { case AppLifecycleState.paused: _cancelSubscriptions(); @@ -240,59 +244,49 @@ class _LyricsWidgetState extends State @override Widget build(BuildContext context) { + final textColor = + settings.playerBackgroundOnLyrics && settings.blurPlayerBackground + ? Theme.of(context).brightness == Brightness.light + ? Colors.black87 + : Colors.white70 + : Theme.of(context).colorScheme.onBackground; return Stack( children: [ - Column(children: [ - if (_freeScroll && !_loading) - Center( - child: TextButton( - onPressed: () { - setState(() => _freeScroll = false); - _scrollToLyric(); - }, - style: ButtonStyle( - foregroundColor: MaterialStateProperty.all(Colors.white)), - child: Text( - _currentIndex! >= 0 - ? (_lyrics?.lyrics?[_currentIndex!].text ?? '...') - : '...', - textAlign: TextAlign.center, - )), - ), - Expanded( - child: _error != null - ? - //Shouldn't really happen, empty lyrics have own text - ErrorScreen(message: _error.toString()) - : - // Loading lyrics - _loading - ? const Center(child: CircularProgressIndicator()) - : LayoutBuilder(builder: (context, constraints) { - _widgetConstraints = constraints; - return NotificationListener( - onNotification: - (ScrollStartNotification notification) { - if (!_syncedLyrics) return false; - final extentDiff = - (notification.metrics.extentBefore - - notification.metrics.extentAfter) - .abs(); - // avoid accidental clicks - const extentThreshold = 10.0; - if (extentDiff >= extentThreshold && - !_animatedScroll && - !_loading && - !_freeScroll) { - setState(() => _freeScroll = true); - } - return false; - }, - child: ScrollConfiguration( - behavior: _scrollBehavior, + _error != null + ? ErrorScreen(message: _error.toString()) + : + // Loading lyrics + _loading + ? const Center(child: CircularProgressIndicator()) + : LayoutBuilder(builder: (context, constraints) { + _widgetConstraints = constraints; + return NotificationListener( + onNotification: (notification) { + if (!_syncedLyrics) return false; + final extentDiff = + (notification.metrics.extentBefore - + notification.metrics.extentAfter) + .abs(); + // avoid accidental clicks + const extentThreshold = 10.0; + if (extentDiff >= extentThreshold && + !_animatedScroll && + !_loading && + !_freeScroll) { + setState(() => _freeScroll = true); + } + return false; + }, + child: ScrollConfiguration( + behavior: _scrollBehavior, + child: FadingEdgeScrollView.fromScrollView( + gradientFractionOnStart: 0.25, + gradientFractionOnEnd: 0.25, child: ListView.builder( - padding: const EdgeInsets.symmetric( - horizontal: 8.0), + padding: EdgeInsets.symmetric( + horizontal: 8.0, + vertical: constraints.maxHeight / 2 - + height / 2), controller: _controller, itemExtent: !_syncedLyrics ? null @@ -300,8 +294,22 @@ class _LyricsWidgetState extends State (_showTranslation ? additionalTranslationHeight : 0.0), - itemCount: _lyrics!.lyrics!.length, + itemCount: _lyrics!.lyrics!.length + 1, itemBuilder: (BuildContext context, int i) { + if (i-- == 0) { + return SizedBox( + height: height, + child: Center( + child: SizedBox( + width: 8.0 * 3 + 6.0, + child: MiniMusicVisualizer( + color: textColor, + width: 8.0, + height: 16.0, + animate: _currentIndex == -1, + ), + ))); + } return DecoratedBox( decoration: BoxDecoration( borderRadius: @@ -312,15 +320,22 @@ class _LyricsWidgetState extends State ), child: InkWell( borderRadius: - BorderRadius.circular(8.0), + BorderRadius.circular(12.0), onTap: _syncedLyrics && _lyrics!.id != null ? () => audioHandler.seek( _lyrics!.lyrics![i].offset!) : null, - child: Center( + child: Padding( + padding: + const EdgeInsets.symmetric( + horizontal: 4.0, + //vertical: 24.0, + ), child: Column( mainAxisSize: MainAxisSize.min, + mainAxisAlignment: + MainAxisAlignment.center, children: [ Text( _lyrics!.lyrics![i].text!, @@ -328,6 +343,7 @@ class _LyricsWidgetState extends State ? TextAlign.center : TextAlign.start, style: TextStyle( + color: textColor, fontSize: _syncedLyrics ? 26.0 : 20.0, @@ -357,25 +373,35 @@ class _LyricsWidgetState extends State ), ))); }, - ))); - }), - ), - ]), + )))); + }), if (_availableTranslation) - Align( - alignment: Alignment.bottomCenter, - child: Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: ElevatedButton( - onPressed: () { - setState(() => _showTranslation = !_showTranslation); - SchedulerBinding.instance - .addPostFrameCallback((_) => _scrollToLyric()); - }, - child: Text(_showTranslation - ? 'Without translation'.i18n - : 'With translation'.i18n)), - )), + Positioned( + bottom: 16.0, + left: 0, + right: 0, + child: Center( + child: ElevatedButton( + onPressed: () { + setState(() => _showTranslation = !_showTranslation); + SchedulerBinding.instance + .addPostFrameCallback((_) => _scrollToLyric()); + }, + child: Text(_showTranslation + ? 'Without translation'.i18n + : 'With translation'.i18n)), + ), + ), + if (_freeScroll) + Positioned( + bottom: 16.0, + right: 16.0, + child: FloatingActionButton( + child: const Icon(Icons.sync), + onPressed: () => setState(() { + _freeScroll = false; + _scrollToLyric(); + }))) ], ); } diff --git a/lib/ui/player_screen.dart b/lib/ui/player_screen.dart index b32b53e..9206e7c 100644 --- a/lib/ui/player_screen.dart +++ b/lib/ui/player_screen.dart @@ -163,12 +163,15 @@ class PlayerScreenBackground extends StatelessWidget { Widget _buildChild( BuildContext context, BackgroundProvider provider, Widget child) { + final isLightMode = Theme.of(context).brightness == Brightness.light; return Stack(children: [ if (provider.imageProvider != null || settings.colorGradientBackground) Positioned.fill( child: provider.imageProvider != null ? DecoratedBox( - decoration: const BoxDecoration(color: Colors.black), + decoration: BoxDecoration( + color: Color.lerp(provider.dominantColor, + isLightMode ? Colors.white : Colors.black, 0.75)), child: ImageFiltered( imageFilter: ImageFilter.blur( tileMode: TileMode.decal, @@ -180,10 +183,7 @@ class PlayerScreenBackground extends StatelessWidget { image: DecorationImage( image: provider.imageProvider!, fit: BoxFit.cover, - colorFilter: ColorFilter.mode( - Colors.white - .withOpacity(settings.isDark ? 0.55 : 0.75), - BlendMode.dstATop), + opacity: 0.35, )), ), ), diff --git a/lib/ui/queue_screen.dart b/lib/ui/queue_screen.dart index de72b13..1b3e0a4 100644 --- a/lib/ui/queue_screen.dart +++ b/lib/ui/queue_screen.dart @@ -170,11 +170,10 @@ class _QueueListWidgetState extends State { trailing: ReorderableDragStartListener( index: index, child: const Icon(Icons.drag_handle)), onTap: () { - audioHandler.skipToQueueItem(index).then((value) { - if (widget.shouldPopOnTap) { - Navigator.of(context).pop(); - } - }); + if (widget.shouldPopOnTap) { + Navigator.pop(context); + } + audioHandler.skipToQueueItem(index); }, onSecondary: (details) => menuSheet.defaultTrackMenu( Track.fromMediaItem(mediaItem), diff --git a/lib/ui/tiles.dart b/lib/ui/tiles.dart index 6456c11..b30dccd 100644 --- a/lib/ui/tiles.dart +++ b/lib/ui/tiles.dart @@ -8,7 +8,7 @@ import 'package:freezer/api/player/player_helper.dart'; import 'package:freezer/icons.dart'; import 'package:freezer/main.dart'; import 'package:freezer/translations.i18n.dart'; -import 'package:freezer/ui/animated_bars.dart'; +import 'package:mini_music_visualizer/mini_music_visualizer.dart'; import '../api/definitions.dart'; import 'cached_image.dart'; @@ -107,15 +107,6 @@ class TrackCardTile extends StatelessWidget { ), ), ); - ListTile( - title: Text(title), - subtitle: Text(artist), - leading: Stack( - children: [CachedImage(url: artUri), PlayItemButton(onTap: onTap)], - ), - contentPadding: const EdgeInsets.only(left: 16.0, right: 124.0), - onTap: onTap, - ); } } @@ -209,34 +200,39 @@ class TrackTile extends StatelessWidget { artist, maxLines: 1, ), - leading: CachedImage( - url: artUri, - width: 48.0, - height: 48.0, - ), - // StreamBuilder( - // initialData: audioHandler.mediaItem.value, - // stream: audioHandler.mediaItem, - // builder: (context, snapshot) { - // final child = CachedImage( - // url: artUri, - // width: 48.0, - // height: 48.0, - // ); -// - // if (snapshot.data?.id == trackId) { - // return Stack(children: [ - // child, - // const Positioned.fill( - // child: DecoratedBox( - // decoration: BoxDecoration(color: Colors.black26), - // child: AnimatedBars()), - // ), - // ]); - // } -// - // return child; - // }), + leading: StreamBuilder( + initialData: audioHandler.mediaItem.value, + stream: audioHandler.mediaItem, + builder: (context, snapshot) { + final child = CachedImage( + url: artUri, + width: 48.0, + height: 48.0, + ); + + if (snapshot.data?.id == trackId) { + return Stack(children: [ + child, + Positioned.fill( + child: DecoratedBox( + decoration: const BoxDecoration(color: Colors.black26), + child: Center( + child: SizedBox( + width: 18.0, + height: 16.0, + child: StreamBuilder( + stream: playerHelper.playing, + builder: (context, snapshot) { + return MiniMusicVisualizer( + color: Colors.white70, + animate: snapshot.data ?? false); + })), + )), + ), + ]); + } + return child; + }), onTap: onTap, onLongPress: normalizeSecondary(onSecondary), trailing: Row( diff --git a/pubspec.lock b/pubspec.lock index 210089b..db0ed2b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -371,7 +371,7 @@ packages: source: hosted version: "2.0.5" fading_edge_scrollview: - dependency: transitive + dependency: "direct main" description: name: fading_edge_scrollview sha256: c25c2231652ce774cc31824d0112f11f653881f43d7f5302c05af11942052031 diff --git a/pubspec.yaml b/pubspec.yaml index 2f21ec7..dc7de1b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -115,6 +115,7 @@ dependencies: freezed_annotation: ^2.4.1 mini_music_visualizer: ^1.1.0 + fading_edge_scrollview: ^3.0.0 #deezcryptor: #path: deezcryptor/