From eeb8cabf491d5242bd434b3c71c39363f24bdcf9 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 11 Mar 2023 22:41:46 +0600 Subject: [PATCH] feat: compact and adaptive playbutton card design --- lib/components/album/album_card.dart | 3 - lib/components/genre/category_card.dart | 98 +++--- lib/components/library/user_albums.dart | 5 - lib/components/library/user_playlists.dart | 11 +- lib/components/playlist/playlist_card.dart | 4 +- lib/components/shared/playbutton_card.dart | 286 ++++++++---------- .../shimmers/shimmer_playbutton_card.dart | 154 ++++------ .../shared/spotube_marquee_text.dart | 48 --- lib/pages/home/genres.dart | 26 +- lib/pages/home/personalized.dart | 80 ++--- lib/pages/lyrics/synced_lyrics.dart | 212 +++++++------ lib/pages/player/player.dart | 6 +- lib/services/queries/category.dart | 14 +- pubspec.lock | 16 - pubspec.yaml | 1 - 15 files changed, 391 insertions(+), 573 deletions(-) delete mode 100644 lib/components/shared/spotube_marquee_text.dart diff --git a/lib/components/album/album_card.dart b/lib/components/album/album_card.dart index e222aa7f9..29caf36ce 100644 --- a/lib/components/album/album_card.dart +++ b/lib/components/album/album_card.dart @@ -33,11 +33,9 @@ enum AlbumType { class AlbumCard extends HookConsumerWidget { final Album album; - final PlaybuttonCardViewType viewType; const AlbumCard( this.album, { Key? key, - this.viewType = PlaybuttonCardViewType.square, }) : super(key: key); @override @@ -65,7 +63,6 @@ class AlbumCard extends HookConsumerWidget { album.images, placeholder: ImagePlaceholder.collection, ), - viewType: viewType, margin: EdgeInsets.symmetric(horizontal: marginH.toDouble()), isPlaying: isPlaylistPlaying, isLoading: isPlaylistPlaying && playlist?.isLoading == true, diff --git a/lib/components/genre/category_card.dart b/lib/components/genre/category_card.dart index 264c5ef9c..bcb5afc41 100644 --- a/lib/components/genre/category_card.dart +++ b/lib/components/genre/category_card.dart @@ -1,12 +1,13 @@ -import 'package:flutter/gestures.dart'; +import 'dart:ui'; + import 'package:flutter/material.dart' hide Page; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; +import 'package:spotube/components/playlist/playlist_card.dart'; import 'package:spotube/components/shared/shimmers/shimmer_playbutton_card.dart'; import 'package:spotube/components/shared/waypoint.dart'; -import 'package:spotube/components/playlist/playlist_card.dart'; import 'package:spotube/models/logger.dart'; import 'package:spotube/services/queries/queries.dart'; @@ -27,60 +28,51 @@ class CategoryCard extends HookConsumerWidget { category.id!, ); - final playlists = playlistQuery.pages - .expand( - (page) => page.items ?? const Iterable.empty(), - ) - .toList(); - - return Column( - children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: Row( - children: [ - Text( - category.name ?? "Unknown", - style: Theme.of(context).textTheme.titleLarge, - ), - ], + if (playlistQuery.hasErrors && !playlistQuery.hasPageData) { + return const SizedBox.shrink(); + } + final playlists = playlistQuery.pages.expand( + (page) { + return page.items?.where((i) => i != null) ?? const Iterable.empty(); + }, + ).toList(); + return Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + category.name!, + style: Theme.of(context).textTheme.titleLarge, ), - ), - playlistQuery.hasPageError && !playlistQuery.hasPageData - ? Text("Something Went Wrong\n${playlistQuery.errors.first}") - : SizedBox( - height: 245, - child: ScrollConfiguration( - behavior: ScrollConfiguration.of(context).copyWith( - dragDevices: { - PointerDeviceKind.touch, - PointerDeviceKind.mouse, - }, - ), - child: Scrollbar( - controller: scrollController, - interactive: false, - child: Waypoint( - controller: scrollController, - onTouchEdge: () { - playlistQuery.fetchNext(); - }, - child: ListView( - scrollDirection: Axis.horizontal, - shrinkWrap: true, - controller: scrollController, - children: [ - ...playlists - .map((playlist) => PlaylistCard(playlist)), - if (playlistQuery.hasNextPage) - const ShimmerPlaybuttonCard(count: 1), - ], - ), - ), - ), + ScrollConfiguration( + behavior: ScrollConfiguration.of(context).copyWith( + dragDevices: { + PointerDeviceKind.touch, + PointerDeviceKind.mouse, + }, + ), + child: Waypoint( + controller: scrollController, + onTouchEdge: playlistQuery.fetchNext, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + controller: scrollController, + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ...playlists.map((playlist) => PlaylistCard(playlist)), + if (playlistQuery.hasNextPage) + const ShimmerPlaybuttonCard(count: 1), + ], ), ), - ], + ), + ), + ], + ), ); } } diff --git a/lib/components/library/user_albums.dart b/lib/components/library/user_albums.dart index 56dce0062..cfe213df1 100644 --- a/lib/components/library/user_albums.dart +++ b/lib/components/library/user_albums.dart @@ -6,7 +6,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/album/album_card.dart'; -import 'package:spotube/components/shared/playbutton_card.dart'; import 'package:spotube/components/shared/shimmers/shimmer_playbutton_card.dart'; import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart'; import 'package:spotube/hooks/use_breakpoint_value.dart'; @@ -28,9 +27,6 @@ class UserAlbums extends HookConsumerWidget { sm: 0, others: 20, ); - final viewType = MediaQuery.of(context).size.width < 480 - ? PlaybuttonCardViewType.list - : PlaybuttonCardViewType.square; final searchText = useState(''); @@ -82,7 +78,6 @@ class UserAlbums extends HookConsumerWidget { alignment: WrapAlignment.center, children: albums .map((album) => AlbumCard( - viewType: viewType, TypeConversionUtils.simpleAlbum_X_Album(album), )) .toList(), diff --git a/lib/components/library/user_playlists.dart b/lib/components/library/user_playlists.dart index 5e43e4e44..d87a2da9f 100644 --- a/lib/components/library/user_playlists.dart +++ b/lib/components/library/user_playlists.dart @@ -7,7 +7,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/playlist/playlist_create_dialog.dart'; -import 'package:spotube/components/shared/playbutton_card.dart'; import 'package:spotube/components/shared/shimmers/shimmer_playbutton_card.dart'; import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart'; import 'package:spotube/components/playlist/playlist_card.dart'; @@ -28,9 +27,6 @@ class UserPlaylists extends HookConsumerWidget { sm: 0, others: 20, ); - final viewType = MediaQuery.of(context).size.width < 480 - ? PlaybuttonCardViewType.list - : PlaybuttonCardViewType.square; final auth = ref.watch(AuthenticationNotifier.provider); final playlistsQuery = useQueries.playlist.ofMine(ref); @@ -81,12 +77,7 @@ class UserPlaylists extends HookConsumerWidget { final children = [ const PlaylistCreateDialog(), - ...playlists - .map((playlist) => PlaylistCard( - playlist, - viewType: viewType, - )) - .toList(), + ...playlists.map((playlist) => PlaylistCard(playlist)).toList(), ]; return RefreshIndicator( onRefresh: playlistsQuery.refresh, diff --git a/lib/components/playlist/playlist_card.dart b/lib/components/playlist/playlist_card.dart index b02b5f56d..487bb4018 100644 --- a/lib/components/playlist/playlist_card.dart +++ b/lib/components/playlist/playlist_card.dart @@ -13,11 +13,9 @@ import 'package:spotube/utils/type_conversion_utils.dart'; class PlaylistCard extends HookConsumerWidget { final PlaylistSimple playlist; - final PlaybuttonCardViewType viewType; const PlaylistCard( this.playlist, { Key? key, - this.viewType = PlaybuttonCardViewType.square, }) : super(key: key); @override Widget build(BuildContext context, ref) { @@ -43,9 +41,9 @@ class PlaylistCard extends HookConsumerWidget { final spotify = ref.watch(spotifyProvider); return PlaybuttonCard( - viewType: viewType, margin: EdgeInsets.symmetric(horizontal: marginH.toDouble()), title: playlist.name!, + description: playlist.description, imageUrl: TypeConversionUtils.image_X_UrlString( playlist.images, placeholder: ImagePlaceholder.collection, diff --git a/lib/components/shared/playbutton_card.dart b/lib/components/shared/playbutton_card.dart index 3f16bf7aa..aa6049708 100644 --- a/lib/components/shared/playbutton_card.dart +++ b/lib/components/shared/playbutton_card.dart @@ -1,13 +1,12 @@ +import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/collections/spotube_icons.dart'; -import 'package:spotube/components/shared/hover_builder.dart'; -import 'package:spotube/components/shared/spotube_marquee_text.dart'; import 'package:spotube/components/shared/image/universal_image.dart'; - -enum PlaybuttonCardViewType { square, list } +import 'package:spotube/hooks/use_breakpoint_value.dart'; +import 'package:spotube/hooks/use_brightness_value.dart'; class PlaybuttonCard extends HookWidget { final void Function()? onTap; @@ -19,7 +18,6 @@ class PlaybuttonCard extends HookWidget { final bool isPlaying; final bool isLoading; final String title; - final PlaybuttonCardViewType viewType; const PlaybuttonCard({ required this.imageUrl, @@ -31,188 +29,142 @@ class PlaybuttonCard extends HookWidget { this.onPlaybuttonPressed, this.onAddToQueuePressed, this.onTap, - this.viewType = PlaybuttonCardViewType.square, Key? key, }) : super(key: key); @override Widget build(BuildContext context) { - final backgroundColor = Theme.of(context).cardColor; + final theme = Theme.of(context); + final radius = BorderRadius.circular(15); - final isSquare = viewType == PlaybuttonCardViewType.square; + final shadowColor = useBrightnessValue( + theme.colorScheme.background, + theme.colorScheme.background, + ); + + final double size = useBreakpointValue( + sm: 130, + md: 150, + others: 170, + ); + + final end = useBreakpointValue( + sm: 5, + md: 7, + others: 10, + ); return Container( + constraints: BoxConstraints(maxWidth: size), margin: margin, - child: InkWell( - onTap: onTap, - borderRadius: BorderRadius.circular(8), - highlightColor: Colors.black12, - child: ConstrainedBox( - constraints: BoxConstraints( - maxWidth: isSquare ? 200 : double.infinity, - maxHeight: !isSquare ? 60 : double.infinity, - ), - child: HoverBuilder(builder: (context, isHovering) { - final playButton = IconButton( - onPressed: onPlaybuttonPressed, - style: IconButton.styleFrom( - backgroundColor: Theme.of(context).primaryColor, - hoverColor: Theme.of(context).primaryColor.withOpacity(0.5), - ), - icon: isLoading - ? SizedBox( - height: 23, - width: 23, - child: CircularProgressIndicator( - color: ThemeData.estimateBrightnessForColor( - Theme.of(context).primaryColor, - ) == - Brightness.dark - ? Colors.white - : Colors.grey[900], - ), - ) - : Icon( - isPlaying ? SpotubeIcons.pause : SpotubeIcons.play, - color: Colors.white, + child: Material( + color: Color.lerp( + theme.colorScheme.surfaceVariant, + theme.colorScheme.surface, + useBrightnessValue(.9, .7), + ), + borderRadius: radius, + shadowColor: shadowColor, + elevation: 3, + child: InkWell( + mouseCursor: SystemMouseCursors.click, + onTap: onTap, + borderRadius: radius, + splashFactory: theme.splashFactory, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( + clipBehavior: Clip.none, + children: [ + Container( + margin: const EdgeInsets.only( + left: 8, + right: 8, + top: 8, ), - ); - final addToQueueButton = IconButton( - onPressed: isLoading ? null : onAddToQueuePressed, - style: IconButton.styleFrom( - backgroundColor: Theme.of(context).cardColor, - hoverColor: Theme.of(context) - .cardColor - .withOpacity(isLoading ? 1 : 0.5), - ), - icon: const Icon(SpotubeIcons.queueAdd), - ); - final image = ClipRRect( - borderRadius: BorderRadius.circular(8), - child: UniversalImage( - path: imageUrl, - width: isSquare ? 200 : 60, - placeholder: (context, url) => Assets.placeholder.image(), - ), - ); - - final square = Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - // thumbnail of the playlist - Stack( - children: [ - image, - Positioned.directional( - textDirection: TextDirection.ltr, - bottom: 10, - end: 5, - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.end, - children: [ - if (!isPlaying) addToQueueButton, - const SizedBox(height: 5), - playButton, - ], + constraints: BoxConstraints(maxHeight: size), + child: ClipRRect( + borderRadius: radius, + child: UniversalImage( + path: imageUrl, + placeholder: (context, url) { + return Assets.albumPlaceholder + .image(fit: BoxFit.cover); + }, ), - ) - ], - ), - const SizedBox(height: 5), - Padding( - padding: - const EdgeInsets.symmetric(horizontal: 16, vertical: 10), - child: Column( - children: [ - Tooltip( - message: title, - child: SizedBox( - height: 20, - child: SpotubeMarqueeText( - text: title, - style: const TextStyle(fontWeight: FontWeight.bold), - isHovering: isHovering, + ), + ), + Positioned.directional( + textDirection: TextDirection.ltr, + end: end, + bottom: -5, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (!isPlaying) + IconButton( + style: IconButton.styleFrom( + backgroundColor: theme.colorScheme.background, + foregroundColor: theme.colorScheme.primary, + minimumSize: const Size.square(10), + ), + icon: const Icon(SpotubeIcons.queueAdd), + onPressed: isLoading ? null : onAddToQueuePressed, ), - ), - ), - if (description != null) ...[ - const SizedBox(height: 10), - SizedBox( - height: 30, - child: SpotubeMarqueeText( - text: description!, - style: Theme.of(context).textTheme.bodySmall, - isHovering: isHovering, + const SizedBox(height: 5), + IconButton( + style: IconButton.styleFrom( + backgroundColor: theme.colorScheme.primaryContainer, + foregroundColor: theme.colorScheme.primary, + minimumSize: const Size.square(10), ), + icon: isLoading + ? SizedBox.fromSize( + size: const Size.square(15), + child: const CircularProgressIndicator( + strokeWidth: 2), + ) + : isPlaying + ? const Icon(SpotubeIcons.pause) + : const Icon(SpotubeIcons.play), + onPressed: isLoading ? null : onPlaybuttonPressed, ), - ] - ], + ], + ), + ), + ], + ), + const SizedBox(height: 15), + Flexible( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: AutoSizeText( + title, + maxLines: 1, + minFontSize: theme.textTheme.bodyMedium!.fontSize!, + overflow: TextOverflow.ellipsis, ), ), - ], - ); - - final list = Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - // thumbnail of the playlist + ), + if (description != null) Flexible( - child: Row( - children: [ - image, - const SizedBox(width: 10), - Flexible( - child: RichText( - overflow: TextOverflow.ellipsis, - text: TextSpan( - children: [ - TextSpan( - text: title, - style: Theme.of(context) - .textTheme - .bodyMedium - ?.copyWith(fontWeight: FontWeight.bold), - ), - if (description != null) - TextSpan( - text: '\n$description', - style: Theme.of(context).textTheme.bodySmall, - ), - ], - ), - ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: AutoSizeText( + description!, + maxLines: 2, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurface.withOpacity(.5), ), - ], + overflow: TextOverflow.ellipsis, + ), ), ), - Row( - children: [ - addToQueueButton, - const SizedBox(width: 10), - playButton, - const SizedBox(width: 10), - ], - ), - ], - ); - - return Ink( - decoration: BoxDecoration( - color: backgroundColor, - borderRadius: BorderRadius.circular(8), - boxShadow: [ - BoxShadow( - blurRadius: 10, - offset: const Offset(0, 3), - spreadRadius: 5, - color: Theme.of(context).colorScheme.shadow, - ), - ], - ), - child: isSquare ? square : list, - ); - }), + const SizedBox(height: 10), + ], + ), ), ), ); diff --git a/lib/components/shared/shimmers/shimmer_playbutton_card.dart b/lib/components/shared/shimmers/shimmer_playbutton_card.dart index f584dfc28..23ffdc093 100644 --- a/lib/components/shared/shimmers/shimmer_playbutton_card.dart +++ b/lib/components/shared/shimmers/shimmer_playbutton_card.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:spotube/extensions/theme.dart'; +import 'package:spotube/hooks/use_breakpoint_value.dart'; class ShimmerPlaybuttonCardPainter extends CustomPainter { final Color background; @@ -12,29 +13,59 @@ class ShimmerPlaybuttonCardPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { + const radius = Radius.circular(15); + canvas.drawRRect( RRect.fromRectAndRadius( Rect.fromLTWH(0, 0, size.width, size.height), - const Radius.circular(10), + radius, ), Paint()..color = background, ); canvas.drawRRect( RRect.fromRectAndRadius( - Rect.fromLTWH(0, 0, size.width, size.height - 45), - const Radius.circular(10), + Rect.fromLTWH(8, 8, size.width - 16, size.height - 90), + radius, + ), + Paint()..color = foreground, + ); + + canvas.drawRRect( + RRect.fromRectAndRadius( + Rect.fromLTWH(12, size.height - 67, size.width / 2, 10), + radius, + ), + Paint()..color = foreground, + ); + + canvas.drawRRect( + RRect.fromRectAndRadius( + Rect.fromLTWH(12, size.height - 45, size.width - 24, 8), + radius, ), Paint()..color = foreground, ); canvas.drawRRect( RRect.fromRectAndRadius( - Rect.fromLTWH(size.width / 4, size.height - 27, size.width / 2, 10), - const Radius.circular(10), + Rect.fromLTWH(12, size.height - 30, size.width * .4, 8), + radius, ), Paint()..color = foreground, ); + + canvas.drawCircle( + Offset(size.width * .85, size.height * .50), + 17, + Paint()..color = background, + ); + + canvas.drawCircle( + Offset(size.width * .85, size.height * .67), + 17, + Paint()..color = background, + ); } @override @@ -43,7 +74,7 @@ class ShimmerPlaybuttonCardPainter extends CustomPainter { } } -class ShimmerPlaybuttonCard extends StatelessWidget { +class ShimmerPlaybuttonCard extends HookWidget { final int count; const ShimmerPlaybuttonCard({ @@ -53,10 +84,19 @@ class ShimmerPlaybuttonCard extends StatelessWidget { @override Widget build(BuildContext context) { + final theme = Theme.of(context); + final Size size = useBreakpointValue( + sm: const Size(130, 200), + md: const Size(150, 220), + others: const Size(170, 240), + ); + final isDark = Theme.of(context).brightness == Brightness.dark; - final shimmerTheme = ShimmerColorTheme( - shimmerBackgroundColor: isDark ? Colors.grey[700] : Colors.grey[200], - shimmerColor: isDark ? Colors.grey[800] : Colors.grey[300], + final bgColor = theme.colorScheme.surfaceVariant.withOpacity(.2); + final fgColor = Color.lerp( + theme.colorScheme.surfaceVariant, + isDark ? Colors.black : Colors.white, + .4, ); return Row( @@ -64,12 +104,10 @@ class ShimmerPlaybuttonCard extends StatelessWidget { children: [ for (var i = 0; i < count; i++) ...[ CustomPaint( - size: const Size(200, 250), + size: size, painter: ShimmerPlaybuttonCardPainter( - background: shimmerTheme.shimmerBackgroundColor ?? - Theme.of(context).scaffoldBackgroundColor, - foreground: - shimmerTheme.shimmerColor ?? Theme.of(context).cardColor, + background: bgColor, + foreground: fgColor!, ), ), const SizedBox(width: 10), @@ -78,89 +116,3 @@ class ShimmerPlaybuttonCard extends StatelessWidget { ); } } - -// class ShimmerPlaybuttonCard extends StatelessWidget { -// final int count; -// const ShimmerPlaybuttonCard({Key? key, this.count = 4}) : super(key: key); - -// @override -// Widget build(BuildContext context) { -// final shimmerColor = -// Theme.of(context).extension()?.shimmerColor ?? -// Colors.white; -// final shimmerBackgroundColor = Theme.of(context) -// .extension() -// ?.shimmerBackgroundColor ?? -// Colors.grey; - -// final card = Stack( -// children: [ -// SkeletonAnimation( -// shimmerColor: shimmerColor, -// borderRadius: BorderRadius.circular(20), -// shimmerDuration: 1000, -// child: Container( -// width: 200, -// height: 220, -// decoration: BoxDecoration( -// color: shimmerBackgroundColor, -// borderRadius: BorderRadius.circular(10), -// ), -// margin: const EdgeInsets.only(top: 10), -// ), -// ), -// Column( -// children: [ -// SkeletonAnimation( -// shimmerColor: shimmerBackgroundColor, -// borderRadius: BorderRadius.circular(20), -// shimmerDuration: 1000, -// child: Container( -// width: 200, -// height: 180, -// decoration: BoxDecoration( -// color: shimmerColor, -// borderRadius: BorderRadius.circular(10), -// ), -// margin: const EdgeInsets.only(top: 10), -// ), -// ), -// const SizedBox(height: 5), -// SkeletonAnimation( -// shimmerColor: shimmerBackgroundColor, -// borderRadius: BorderRadius.circular(20), -// shimmerDuration: 1000, -// child: Container( -// width: 150, -// height: 10, -// decoration: BoxDecoration( -// color: shimmerColor, -// borderRadius: BorderRadius.circular(10), -// ), -// margin: const EdgeInsets.only(top: 10), -// ), -// ), -// ], -// ), -// ], -// ); - -// return SingleChildScrollView( -// physics: const NeverScrollableScrollPhysics(), -// scrollDirection: Axis.horizontal, -// child: Row( -// children: [ -// Row( -// children: List.generate( -// count, -// (_) => Padding( -// padding: const EdgeInsets.symmetric(horizontal: 15), -// child: card, -// ), -// ), -// ), -// ], -// ), -// ); -// } -// } diff --git a/lib/components/shared/spotube_marquee_text.dart b/lib/components/shared/spotube_marquee_text.dart deleted file mode 100644 index 65583b321..000000000 --- a/lib/components/shared/spotube_marquee_text.dart +++ /dev/null @@ -1,48 +0,0 @@ -import 'package:auto_size_text/auto_size_text.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:marquee/marquee.dart'; - -class SpotubeMarqueeText extends HookWidget { - final bool? isHovering; - const SpotubeMarqueeText({ - Key? key, - required this.text, - this.style, - this.isHovering, - }) : super(key: key); - final TextStyle? style; - final String text; - - @override - Widget build(BuildContext context) { - final uKey = useState(UniqueKey()); - - useEffect(() { - uKey.value = UniqueKey(); - return; - }, [isHovering]); - - return AutoSizeText( - text, - minFontSize: 13, - style: DefaultTextStyle.of(context).style.merge(style), - maxLines: 1, - overflowReplacement: Marquee( - key: uKey.value, - text: text, - style: DefaultTextStyle.of(context).style.merge(style), - scrollAxis: Axis.horizontal, - crossAxisAlignment: CrossAxisAlignment.start, - blankSpace: 40.0, - velocity: 30.0, - accelerationDuration: const Duration(seconds: 1), - accelerationCurve: Curves.linear, - decelerationDuration: const Duration(milliseconds: 500), - decelerationCurve: Curves.easeOut, - showFadingOnlyWhenScrolling: true, - numberOfRounds: isHovering == true ? null : 1, - ), - ); - } -} diff --git a/lib/pages/home/genres.dart b/lib/pages/home/genres.dart index 7445b99e1..315684b9c 100644 --- a/lib/pages/home/genres.dart +++ b/lib/pages/home/genres.dart @@ -68,17 +68,21 @@ class GenrePage extends HookConsumerWidget { } }, controller: scrollController, - child: ListView.builder( - physics: const AlwaysScrollableScrollPhysics(), - controller: scrollController, - itemCount: categories.length, - itemBuilder: (context, index) { - final category = categories[index]; - if (searchText.value.isEmpty && index == categories.length - 1) { - return const ShimmerCategories(); - } - return SafeArea(child: CategoryCard(category)); - }, + child: SingleChildScrollView( + child: SafeArea( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ...categories.mapIndexed((index, category) { + if (searchText.value.isEmpty && + index == categories.length - 1) { + return const ShimmerCategories(); + } + return CategoryCard(category); + }) + ], + ), + ), ), ), ); diff --git a/lib/pages/home/personalized.dart b/lib/pages/home/personalized.dart index 9225db5b6..3752f18a6 100644 --- a/lib/pages/home/personalized.dart +++ b/lib/pages/home/personalized.dart @@ -47,22 +47,20 @@ class PersonalizedItemCard extends HookWidget { ) .toList(); - return Column( - children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: Row( - children: [ - Text( - title, - style: Theme.of(context).textTheme.titleLarge, - ), - ], + return Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + title, + style: Theme.of(context).textTheme.titleLarge, + ), ), - ), - SizedBox( - height: playlists != null ? 245 : 285, - child: ScrollConfiguration( + ScrollConfiguration( behavior: ScrollConfiguration.of(context).copyWith( dragDevices: { PointerDeviceKind.touch, @@ -75,12 +73,13 @@ class PersonalizedItemCard extends HookWidget { child: Waypoint( controller: scrollController, onTouchEdge: hasNextPage ? onFetchMore : null, - child: SafeArea( - child: ListView( - scrollDirection: Axis.horizontal, - shrinkWrap: true, - controller: scrollController, - physics: const AlwaysScrollableScrollPhysics(), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + controller: scrollController, + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Row( + mainAxisSize: MainAxisSize.min, children: [ ...?playlistItems ?.map((playlist) => PlaylistCard(playlist)), @@ -96,8 +95,8 @@ class PersonalizedItemCard extends HookWidget { ), ), ), - ), - ], + ], + ), ); } } @@ -111,22 +110,27 @@ class PersonalizedPage extends HookConsumerWidget { final newReleases = useQueries.album.newReleases(ref); - return ListView( - children: [ - PersonalizedItemCard( - playlists: - featuredPlaylistsQuery.pages.whereType>(), - title: 'Featured', - hasNextPage: featuredPlaylistsQuery.hasNextPage, - onFetchMore: featuredPlaylistsQuery.fetchNext, - ), - PersonalizedItemCard( - albums: newReleases.pages.whereType>(), - title: 'New Releases', - hasNextPage: newReleases.hasNextPage, - onFetchMore: newReleases.fetchNext, + return SingleChildScrollView( + child: SafeArea( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + PersonalizedItemCard( + playlists: featuredPlaylistsQuery.pages + .whereType>(), + title: 'Featured', + hasNextPage: featuredPlaylistsQuery.hasNextPage, + onFetchMore: featuredPlaylistsQuery.fetchNext, + ), + PersonalizedItemCard( + albums: newReleases.pages.whereType>(), + title: 'New Releases', + hasNextPage: newReleases.hasNextPage, + onFetchMore: newReleases.fetchNext, + ), + ], ), - ], + ), ); } } diff --git a/lib/pages/lyrics/synced_lyrics.dart b/lib/pages/lyrics/synced_lyrics.dart index b19b83ab4..6f39228be 100644 --- a/lib/pages/lyrics/synced_lyrics.dart +++ b/lib/pages/lyrics/synced_lyrics.dart @@ -6,7 +6,6 @@ import 'package:spotify/spotify.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/lyrics/zoom_controls.dart'; import 'package:spotube/components/shared/shimmers/shimmer_lyrics.dart'; -import 'package:spotube/components/shared/spotube_marquee_text.dart'; import 'package:spotube/hooks/use_auto_scroll_controller.dart'; import 'package:spotube/hooks/use_breakpoints.dart'; import 'package:spotube/hooks/use_synced_lyrics.dart'; @@ -69,118 +68,115 @@ class SyncedLyrics extends HookConsumerWidget { : textTheme.headlineMedium?.copyWith(fontSize: 25)) ?.copyWith(color: palette.titleTextColor); - return HookBuilder(builder: (context) { - return Stack( - children: [ - Column( - children: [ - if (isModal != true) - Center( - child: SpotubeMarqueeText( - text: playlist?.activeTrack.name ?? "Not Playing", - style: headlineTextStyle, - isHovering: true, - ), + return Stack( + children: [ + Column( + children: [ + if (isModal != true) + Center( + child: Text( + playlist?.activeTrack.name ?? "Not Playing", + style: headlineTextStyle, ), - if (isModal != true) - Center( - child: Text( - TypeConversionUtils.artists_X_String( - playlist?.activeTrack.artists ?? []), - style: breakpoint >= Breakpoints.md - ? textTheme.headlineSmall - : textTheme.titleLarge, - ), + ), + if (isModal != true) + Center( + child: Text( + TypeConversionUtils.artists_X_String( + playlist?.activeTrack.artists ?? []), + style: breakpoint >= Breakpoints.md + ? textTheme.headlineSmall + : textTheme.titleLarge, ), - if (lyricValue != null && lyricValue.lyrics.isNotEmpty) - Expanded( - child: ListView.builder( - controller: controller, - itemCount: lyricValue.lyrics.length, - itemBuilder: (context, index) { - final lyricSlice = lyricValue.lyrics[index]; - final isActive = lyricSlice.time.inSeconds == currentTime; - - if (isActive) { - controller.scrollToIndex( - index, - preferPosition: AutoScrollPosition.middle, - ); - } - return AutoScrollTag( - key: ValueKey(index), - index: index, - controller: controller, - child: lyricSlice.text.isEmpty - ? Container() - : Center( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: AnimatedDefaultTextStyle( - duration: const Duration(milliseconds: 250), - style: TextStyle( - color: isActive - ? Colors.white - : palette.bodyTextColor, - fontWeight: isActive - ? FontWeight.bold - : FontWeight.normal, - fontSize: (isActive ? 30 : 26) * - (textZoomLevel.value / 100), - ), - child: Text( - lyricSlice.text, - maxLines: 2, - textAlign: TextAlign.center, - ), + ), + if (lyricValue != null && lyricValue.lyrics.isNotEmpty) + Expanded( + child: ListView.builder( + controller: controller, + itemCount: lyricValue.lyrics.length, + itemBuilder: (context, index) { + final lyricSlice = lyricValue.lyrics[index]; + final isActive = lyricSlice.time.inSeconds == currentTime; + + if (isActive) { + controller.scrollToIndex( + index, + preferPosition: AutoScrollPosition.middle, + ); + } + return AutoScrollTag( + key: ValueKey(index), + index: index, + controller: controller, + child: lyricSlice.text.isEmpty + ? Container() + : Center( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: AnimatedDefaultTextStyle( + duration: const Duration(milliseconds: 250), + style: TextStyle( + color: isActive + ? Colors.white + : palette.bodyTextColor, + fontWeight: isActive + ? FontWeight.bold + : FontWeight.normal, + fontSize: (isActive ? 30 : 26) * + (textZoomLevel.value / 100), + ), + child: Text( + lyricSlice.text, + maxLines: 2, + textAlign: TextAlign.center, ), ), ), - ); - }, - ), - ), - if (playlist?.activeTrack != null && - (lyricValue == null || lyricValue.lyrics.isEmpty == true)) - const Expanded(child: ShimmerLyrics()), - ], - ), - Align( - alignment: Alignment.bottomRight, - child: Builder(builder: (context) { - final actions = [ - ZoomControls( - value: delay, - onChanged: (value) => ref.read(_delay.notifier).state = value, - interval: 1, - unit: "s", - increaseIcon: const Icon(SpotubeIcons.add), - decreaseIcon: const Icon(SpotubeIcons.remove), - direction: isModal == true ? Axis.horizontal : Axis.vertical, - ), - ZoomControls( - value: textZoomLevel.value, - onChanged: (value) => textZoomLevel.value = value, - min: 50, - max: 200, - ), - ]; - - return isModal == true - ? Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.end, - children: actions, - ) - : Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.end, - children: actions, + ), ); - }), - ), - ], - ); - }); + }, + ), + ), + if (playlist?.activeTrack != null && + (lyricValue == null || lyricValue.lyrics.isEmpty == true)) + const Expanded(child: ShimmerLyrics()), + ], + ), + Align( + alignment: Alignment.bottomRight, + child: Builder(builder: (context) { + final actions = [ + ZoomControls( + value: delay, + onChanged: (value) => ref.read(_delay.notifier).state = value, + interval: 1, + unit: "s", + increaseIcon: const Icon(SpotubeIcons.add), + decreaseIcon: const Icon(SpotubeIcons.remove), + direction: isModal == true ? Axis.horizontal : Axis.vertical, + ), + ZoomControls( + value: textZoomLevel.value, + onChanged: (value) => textZoomLevel.value = value, + min: 50, + max: 200, + ), + ]; + + return isModal == true + ? Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: actions, + ) + : Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: actions, + ); + }), + ), + ], + ); } } diff --git a/lib/pages/player/player.dart b/lib/pages/player/player.dart index 093f742ef..b45c80a12 100644 --- a/lib/pages/player/player.dart +++ b/lib/pages/player/player.dart @@ -11,7 +11,6 @@ import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/player/player_actions.dart'; import 'package:spotube/components/player/player_controls.dart'; import 'package:spotube/components/shared/page_window_title_bar.dart'; -import 'package:spotube/components/shared/spotube_marquee_text.dart'; import 'package:spotube/components/shared/image/universal_image.dart'; import 'package:spotube/hooks/use_breakpoints.dart'; import 'package:spotube/hooks/use_custom_status_bar_color.dart'; @@ -93,8 +92,8 @@ class PlayerView extends HookConsumerWidget { children: [ SizedBox( height: 30, - child: SpotubeMarqueeText( - text: currentTrack?.name ?? "Not playing", + child: Text( + currentTrack?.name ?? "Not playing", style: Theme.of(context) .textTheme .headlineSmall @@ -102,7 +101,6 @@ class PlayerView extends HookConsumerWidget { fontWeight: FontWeight.bold, color: paletteColor.titleTextColor, ), - isHovering: true, ), ), if (isLocalTrack) diff --git a/lib/services/queries/category.dart b/lib/services/queries/category.dart index 319a326ad..04f4c2d3c 100644 --- a/lib/services/queries/category.dart +++ b/lib/services/queries/category.dart @@ -35,16 +35,20 @@ class CategoryQueries { ); } - InfiniteQuery, dynamic, int> playlistsOf( + InfiniteQuery, dynamic, int> playlistsOf( WidgetRef ref, String category, ) { - return useSpotifyInfiniteQuery, dynamic, int>( + return useSpotifyInfiniteQuery, dynamic, int>( "category-playlists/$category", (pageParam, spotify) async { - final playlists = await spotify.playlists - .getByCategoryId(category) - .getPage(5, pageParam); + final playlists = await Pages( + spotify, + "v1/browse/categories/$category/playlists", + (json) => json == null ? null : PlaylistSimple.fromJson(json), + 'playlists', + (json) => PlaylistsFeatured.fromJson(json), + ).getPage(5, pageParam); return playlists; }, diff --git a/pubspec.lock b/pubspec.lock index 3304ba0e2..394427ecd 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -490,14 +490,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" - fading_edge_scrollview: - dependency: transitive - description: - name: fading_edge_scrollview - sha256: c25c2231652ce774cc31824d0112f11f653881f43d7f5302c05af11942052031 - url: "https://pub.dev" - source: hosted - version: "3.0.0" fake_async: dependency: transitive description: @@ -929,14 +921,6 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.0" - marquee: - dependency: "direct main" - description: - name: marquee - sha256: "4b5243d2804373bdc25fc93d42c3b402d6ec1f4ee8d0bb72276edd04ae7addb8" - url: "https://pub.dev" - source: hosted - version: "2.2.3" matcher: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 9f501bb3b..c65e0abbb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -51,7 +51,6 @@ dependencies: json_annotation: ^4.8.0 json_serializable: ^6.6.0 logger: ^1.1.0 - marquee: ^2.2.3 metadata_god: ^0.3.2 mime: ^1.0.2 package_info_plus: ^3.0.2