From c047239ccf1406756d5f53017849c24a01c2918e Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Sat, 23 Sep 2023 00:10:57 +0200 Subject: [PATCH] design and interaction improvements --- lib/components/PlayerScreen/queue_list.dart | 73 +++-- .../PlayerScreen/queue_list_item.dart | 270 +++++++++--------- lib/components/PlayerScreen/song_info.dart | 1 + .../blurred_player_screen_background.dart | 13 +- 4 files changed, 201 insertions(+), 156 deletions(-) diff --git a/lib/components/PlayerScreen/queue_list.dart b/lib/components/PlayerScreen/queue_list.dart index acd71cfaf..f8f18c9e4 100644 --- a/lib/components/PlayerScreen/queue_list.dart +++ b/lib/components/PlayerScreen/queue_list.dart @@ -38,11 +38,18 @@ class _QueueListStreamState { } class QueueList extends StatefulWidget { - const QueueList( - {Key? key, required this.scrollController, required this.nextUpHeaderKey}) + const QueueList({ + Key? key, + required this.scrollController, + required this.previousTracksHeaderKey, + required this.currentTrackKey, + required this.nextUpHeaderKey, + }) : super(key: key); final ScrollController scrollController; + final GlobalKey previousTracksHeaderKey; + final Key currentTrackKey; final GlobalKey nextUpHeaderKey; @override @@ -141,9 +148,9 @@ class _QueueListState extends State { // duration: Duration(seconds: 2), // curve: Curves.fastOutSlowIn, // ); - if (widget.nextUpHeaderKey.currentContext != null) { + if (widget.previousTracksHeaderKey.currentContext != null) { Scrollable.ensureVisible( - widget.nextUpHeaderKey.currentContext!, + widget.previousTracksHeaderKey.currentContext!, // duration: const Duration(milliseconds: 200), // curve: Curves.decelerate, ); @@ -155,15 +162,20 @@ class _QueueListState extends State { _contents = [ // Previous Tracks if (isRecentTracksExpanded) - const PreviousTracksList() + PreviousTracksList(previousTracksHeaderKey: widget.previousTracksHeaderKey) , + //TODO replace this with a SliverPersistentHeader and add an `onTap` callback to the delegate SliverToBoxAdapter( + key: widget.previousTracksHeaderKey, child: GestureDetector( onTap:() { setState(() => isRecentTracksExpanded = !isRecentTracksExpanded); if (!isRecentTracksExpanded) { Future.delayed(const Duration(milliseconds: 200), () => scrollToCurrentTrack()); } + // else { + // Future.delayed(const Duration(milliseconds: 300), () => scrollToCurrentTrack()); + // } }, child: Padding( padding: const EdgeInsets.only(left: 14.0, right: 14.0, bottom: 12.0, top: 8.0), @@ -187,13 +199,12 @@ class _QueueListState extends State { ) ), CurrentTrack( - key: UniqueKey(), + // key: UniqueKey(), + key: widget.currentTrackKey, ), // next up - SliverToBoxAdapter( - key: widget.nextUpHeaderKey, - ), StreamBuilder( + key: widget.nextUpHeaderKey, stream: _queueService.getQueueStream(), builder: (context, snapshot) { if (snapshot.data != null && snapshot.data!.nextUp.isNotEmpty) { @@ -201,6 +212,7 @@ class _QueueListState extends State { // key: widget.nextUpHeaderKey, padding: const EdgeInsets.only(top: 20.0, bottom: 0.0), sliver: SliverPersistentHeader( + pinned: false, //TODO use https://stackoverflow.com/a/69372976 to only ever have one of the headers pinned delegate: SectionHeaderDelegate( title: Text(AppLocalizations.of(context)!.nextUp), height: 30.0, @@ -213,10 +225,11 @@ class _QueueListState extends State { } }, ), - const NextUpTracksList(), + NextUpTracksList(previousTracksHeaderKey: widget.previousTracksHeaderKey), SliverPadding( padding: const EdgeInsets.only(top: 20.0, bottom: 0.0), sliver: SliverPersistentHeader( + pinned: true, delegate: SectionHeaderDelegate( title: Row( children: [ @@ -232,7 +245,7 @@ class _QueueListState extends State { ), ), // Queue - const QueueTracksList(), + QueueTracksList(previousTracksHeaderKey: widget.previousTracksHeaderKey), ]; return CustomScrollView( @@ -243,6 +256,8 @@ class _QueueListState extends State { } Future showQueueBottomSheet(BuildContext context) { + GlobalKey previousTracksHeaderKey = GlobalKey(); + Key currentTrackKey = UniqueKey(); GlobalKey nextUpHeaderKey = GlobalKey(); return showModalBottomSheet( @@ -282,7 +297,7 @@ Future showQueueBottomSheet(BuildContext context) { children: [ if (FinampSettingsHelper .finampSettings.showCoverAsPlayerBackground) - const BlurredPlayerScreenBackground(), + BlurredPlayerScreenBackground(brightnessFactor: Theme.of(context).brightness == Brightness.dark ? 1.0 : 1.0), Column( mainAxisSize: MainAxisSize.min, children: [ @@ -306,6 +321,8 @@ Future showQueueBottomSheet(BuildContext context) { Expanded( child: QueueList( scrollController: scrollController, + previousTracksHeaderKey: previousTracksHeaderKey, + currentTrackKey: currentTrackKey, nextUpHeaderKey: nextUpHeaderKey, ), ), @@ -316,7 +333,7 @@ Future showQueueBottomSheet(BuildContext context) { //TODO fade this out if the key is visible floatingActionButton: FloatingActionButton( onPressed: () => scrollToKey( - key: nextUpHeaderKey, + key: previousTracksHeaderKey, duration: const Duration(milliseconds: 500)), backgroundColor: IconTheme.of(context).color!.withOpacity(0.70), shape: const RoundedRectangleBorder( @@ -344,8 +361,12 @@ Future showQueueBottomSheet(BuildContext context) { } class PreviousTracksList extends StatefulWidget { + + final GlobalKey previousTracksHeaderKey; + const PreviousTracksList({ Key? key, + required this.previousTracksHeaderKey, }) : super(key: key); @override @@ -406,8 +427,10 @@ class _PreviousTracksListState extends State _queueService.playbackOrder == PlaybackOrder.linear, onTap: () async { await _queueService.skipByOffset(indexOffset); + scrollToKey(key: widget.previousTracksHeaderKey, duration: const Duration(milliseconds: 500)); }, isCurrentTrack: false, + isPreviousTrack: true, ); }, ); @@ -420,8 +443,12 @@ class _PreviousTracksListState extends State } class NextUpTracksList extends StatefulWidget { + + final GlobalKey previousTracksHeaderKey; + const NextUpTracksList({ Key? key, + required this.previousTracksHeaderKey, }) : super(key: key); @override @@ -478,6 +505,7 @@ class _NextUpTracksListState extends State { subqueue: _nextUp!, onTap: () async { await _queueService.skipByOffset(indexOffset); + scrollToKey(key: widget.previousTracksHeaderKey, duration: const Duration(milliseconds: 500)); }, isCurrentTrack: false, ); @@ -492,8 +520,12 @@ class _NextUpTracksListState extends State { } class QueueTracksList extends StatefulWidget { + + final GlobalKey previousTracksHeaderKey; + const QueueTracksList({ Key? key, + required this.previousTracksHeaderKey, }) : super(key: key); @override @@ -552,6 +584,7 @@ class _QueueTracksListState extends State { _queueService.playbackOrder == PlaybackOrder.linear, onTap: () async { await _queueService.skipByOffset(indexOffset); + scrollToKey(key: widget.previousTracksHeaderKey, duration: const Duration(milliseconds: 500)); }, isCurrentTrack: false, ); @@ -620,18 +653,15 @@ class _CurrentTrackState extends State { ), backgroundColor: const Color.fromRGBO(0, 0, 0, 0.0), flexibleSpace: Container( - // width: 328, + // width: 58, height: 70.0, padding: const EdgeInsets.symmetric(horizontal: 12), child: Container( + clipBehavior: Clip.antiAlias, decoration: ShapeDecoration( - // color: Color.fromRGBO(188, 136, 86, 0.20), - color: Color.alphaBlend(IconTheme.of(context).color!.withOpacity(0.20), Colors.black), + color: Color.alphaBlend(IconTheme.of(context).color!.withOpacity(0.35), Colors.black), shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topRight: Radius.circular(8), - bottomRight: Radius.circular(8), - ), + borderRadius: BorderRadius.all(Radius.circular(8.0)), ), ), child: Row( @@ -1086,7 +1116,8 @@ class SectionHeaderDelegate extends SliverPersistentHeaderDelegate { builder: (context, snapshot) { PlaybackBehaviorInfo? info = snapshot.data as PlaybackBehaviorInfo?; - return Padding( + return Container( + // color: Colors.black.withOpacity(0.5), padding: const EdgeInsets.symmetric(horizontal: 14.0), child: Row( crossAxisAlignment: CrossAxisAlignment.center, diff --git a/lib/components/PlayerScreen/queue_list_item.dart b/lib/components/PlayerScreen/queue_list_item.dart index c7d969783..7f84ee512 100644 --- a/lib/components/PlayerScreen/queue_list_item.dart +++ b/lib/components/PlayerScreen/queue_list_item.dart @@ -24,6 +24,7 @@ class QueueListItem extends StatefulWidget { late int indexOffset; late List subqueue; late bool isCurrentTrack; + late bool isPreviousTrack; late bool allowReorder; late void Function() onTap; @@ -37,6 +38,7 @@ class QueueListItem extends StatefulWidget { required this.onTap, this.allowReorder = true, this.isCurrentTrack = false, + this.isPreviousTrack = false, }) : super(key: key); @override State createState() => _QueueListItemState(); @@ -60,145 +62,149 @@ class _QueueListItemState extends State { }, child: GestureDetector( onLongPressStart: (details) => showSongMenu(details), - child: Card( - color: const Color.fromRGBO(255, 255, 255, 0.05), - elevation: 0, - margin: - const EdgeInsets.symmetric(horizontal: 12.0, vertical: 5.0), - clipBehavior: Clip.antiAlias, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), - ), - child: ListTile( - visualDensity: VisualDensity.standard, - minVerticalPadding: 0.0, - horizontalTitleGap: 10.0, - contentPadding: - const EdgeInsets.symmetric(vertical: 0.0, horizontal: 0.0), - tileColor: widget.isCurrentTrack - ? Theme.of(context).colorScheme.secondary.withOpacity(0.1) - : null, - leading: AlbumImage( - item: widget.item.item.extras?["itemJson"] == null - ? null - : jellyfin_models.BaseItemDto.fromJson( - widget.item.item.extras?["itemJson"]), + child: Opacity( + opacity: widget.isPreviousTrack ? 0.8 : 1.0, + child: Card( + color: const Color.fromRGBO(255, 255, 255, 0.075), + elevation: 0, + margin: + const EdgeInsets.symmetric(horizontal: 12.0, vertical: 5.0), + clipBehavior: Clip.antiAlias, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), ), - // leading: Container( - // height: 60.0, - // width: 60.0, - // color: Colors.white, - // ), - title: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.all(0.0), - child: Text( - widget.item.item.title, - style: this.widget.isCurrentTrack - ? TextStyle( - color: Theme.of(context).colorScheme.secondary, - fontSize: 16, - fontFamily: 'Lexend Deca', - fontWeight: FontWeight.w400, - overflow: TextOverflow.ellipsis) - : null, - overflow: TextOverflow.ellipsis, - ), - ), - Padding( - padding: const EdgeInsets.only(top: 6.0), - child: Text( - processArtist(widget.item.item.artist, context), - style: const TextStyle( - color: Colors.white70, - fontSize: 13, - fontFamily: 'Lexend Deca', - fontWeight: FontWeight.w300, - overflow: TextOverflow.ellipsis), - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - // subtitle: Container( - // alignment: Alignment.centerLeft, - // height: 40.5, // has to be above a certain value to get rid of vertical padding - // child: Padding( - // padding: const EdgeInsets.only(bottom: 2.0), - // child: Text( - // processArtist(widget.item.item.artist, context), - // style: const TextStyle( - // color: Colors.white70, - // fontSize: 13, - // fontFamily: 'Lexend Deca', - // fontWeight: FontWeight.w300, - // overflow: TextOverflow.ellipsis), - // overflow: TextOverflow.ellipsis, - // ), - // ), - // ), - trailing: Container( - alignment: Alignment.centerRight, - margin: const EdgeInsets.only(right: 8.0), - padding: const EdgeInsets.only(right: 6.0), - // width: widget.allowReorder ? 145.0 : 115.0, - width: widget.allowReorder ? 70.0 : 35.0, - height: 50.0, - child: Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.end, - crossAxisAlignment: CrossAxisAlignment.center, + child: ListTile( + visualDensity: VisualDensity.standard, + minVerticalPadding: 0.0, + horizontalTitleGap: 10.0, + contentPadding: + const EdgeInsets.symmetric(vertical: 0.0, horizontal: 0.0), + tileColor: widget.isCurrentTrack + ? Theme.of(context).colorScheme.secondary.withOpacity(0.1) + : null, + leading: AlbumImage( + item: widget.item.item.extras?["itemJson"] == null + ? null + : jellyfin_models.BaseItemDto.fromJson( + widget.item.item.extras?["itemJson"]), + borderRadius: BorderRadius.zero, + ), + // leading: Container( + // height: 60.0, + // width: 60.0, + // color: Colors.white, + // ), + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - "${widget.item.item.duration?.inMinutes.toString()}:${((widget.item.item.duration?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}", - textAlign: TextAlign.end, - style: TextStyle( - color: Theme.of(context).textTheme.bodySmall?.color, + Padding( + padding: const EdgeInsets.all(0.0), + child: Text( + widget.item.item.title, + style: this.widget.isCurrentTrack + ? TextStyle( + color: Theme.of(context).colorScheme.secondary, + fontSize: 16, + fontFamily: 'Lexend Deca', + fontWeight: FontWeight.w400, + overflow: TextOverflow.ellipsis) + : null, + overflow: TextOverflow.ellipsis, ), ), - // IconButton( - // padding: const EdgeInsets.all(0.0), - // visualDensity: VisualDensity.compact, - // icon: const Icon( - // TablerIcons.dots_vertical, - // color: Colors.white, - // weight: 1.5, - // ), - // iconSize: 24.0, - // onPressed: () => showSongMenu(), - // ), - // IconButton( - // padding: const EdgeInsets.only(right: 14.0), - // visualDensity: VisualDensity.compact, - // icon: const Icon( - // TablerIcons.x, - // color: Colors.white, - // weight: 1.5, - // ), - // iconSize: 24.0, - // onPressed: () async => - // await _queueService.removeAtOffset(widget.indexOffset), - // ), - if (widget.allowReorder) - ReorderableDragStartListener( - index: widget.listIndex, - child: Padding( - padding: EdgeInsets.only(bottom: 5.0, left: 6.0), - child: const Icon( - TablerIcons.grip_horizontal, - color: Colors.white, - size: 28.0, - weight: 1.5, - ), - ), + Padding( + padding: const EdgeInsets.only(top: 6.0), + child: Text( + processArtist(widget.item.item.artist, context), + style: const TextStyle( + color: Colors.white70, + fontSize: 13, + fontFamily: 'Lexend Deca', + fontWeight: FontWeight.w300, + overflow: TextOverflow.ellipsis), + overflow: TextOverflow.ellipsis, ), + ), ], ), - ), - onTap: widget.onTap, - ))), + // subtitle: Container( + // alignment: Alignment.centerLeft, + // height: 40.5, // has to be above a certain value to get rid of vertical padding + // child: Padding( + // padding: const EdgeInsets.only(bottom: 2.0), + // child: Text( + // processArtist(widget.item.item.artist, context), + // style: const TextStyle( + // color: Colors.white70, + // fontSize: 13, + // fontFamily: 'Lexend Deca', + // fontWeight: FontWeight.w300, + // overflow: TextOverflow.ellipsis), + // overflow: TextOverflow.ellipsis, + // ), + // ), + // ), + trailing: Container( + alignment: Alignment.centerRight, + margin: const EdgeInsets.only(right: 8.0), + padding: const EdgeInsets.only(right: 6.0), + // width: widget.allowReorder ? 145.0 : 115.0, + width: widget.allowReorder ? 70.0 : 35.0, + height: 50.0, + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + "${widget.item.item.duration?.inMinutes.toString()}:${((widget.item.item.duration?.inSeconds ?? 0) % 60).toString().padLeft(2, '0')}", + textAlign: TextAlign.end, + style: TextStyle( + color: Theme.of(context).textTheme.bodySmall?.color, + ), + ), + // IconButton( + // padding: const EdgeInsets.all(0.0), + // visualDensity: VisualDensity.compact, + // icon: const Icon( + // TablerIcons.dots_vertical, + // color: Colors.white, + // weight: 1.5, + // ), + // iconSize: 24.0, + // onPressed: () => showSongMenu(), + // ), + // IconButton( + // padding: const EdgeInsets.only(right: 14.0), + // visualDensity: VisualDensity.compact, + // icon: const Icon( + // TablerIcons.x, + // color: Colors.white, + // weight: 1.5, + // ), + // iconSize: 24.0, + // onPressed: () async => + // await _queueService.removeAtOffset(widget.indexOffset), + // ), + if (widget.allowReorder) + ReorderableDragStartListener( + index: widget.listIndex, + child: Padding( + padding: EdgeInsets.only(bottom: 5.0, left: 6.0), + child: const Icon( + TablerIcons.grip_horizontal, + color: Colors.white, + size: 28.0, + weight: 1.5, + ), + ), + ), + ], + ), + ), + onTap: widget.onTap, + )), + )), ); } diff --git a/lib/components/PlayerScreen/song_info.dart b/lib/components/PlayerScreen/song_info.dart index 4c0a28fe2..067df28a1 100644 --- a/lib/components/PlayerScreen/song_info.dart +++ b/lib/components/PlayerScreen/song_info.dart @@ -197,6 +197,7 @@ class _PlayerScreenAlbumImage extends ConsumerWidget { accentColor: newColour, brightness: theme.brightness, ); + } }), ), diff --git a/lib/screens/blurred_player_screen_background.dart b/lib/screens/blurred_player_screen_background.dart index 747199710..65427f622 100644 --- a/lib/screens/blurred_player_screen_background.dart +++ b/lib/screens/blurred_player_screen_background.dart @@ -9,7 +9,14 @@ import '../services/current_album_image_provider.dart'; /// Same as [_PlayerScreenAlbumImage], but with a BlurHash instead. We also /// filter the BlurHash so that it works as a background image. class BlurredPlayerScreenBackground extends ConsumerWidget { - const BlurredPlayerScreenBackground({Key? key}) : super(key: key); + + /// should never be less than 1.0 + final double brightnessFactor; + + const BlurredPlayerScreenBackground({ + Key? key, + this.brightnessFactor = 1.0, + }) : super(key: key); @override Widget build(BuildContext context, WidgetRef ref) { @@ -26,8 +33,8 @@ class BlurredPlayerScreenBackground extends ConsumerWidget { imageBuilder: (context, child) => ColorFiltered( colorFilter: ColorFilter.mode( Theme.of(context).brightness == Brightness.dark - ? Colors.black.withOpacity(0.75) - : Colors.white.withOpacity(0.50), + ? Colors.black.withOpacity(0.65 / brightnessFactor) + : Colors.white.withOpacity(0.50 / brightnessFactor), BlendMode.srcOver), child: ImageFiltered( imageFilter: ImageFilter.blur(