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/