feat: compact and adaptive playbutton card design

This commit is contained in:
Kingkor Roy Tirtho 2023-03-11 22:41:46 +06:00
parent 1bdce9fe96
commit eeb8cabf49
15 changed files with 393 additions and 575 deletions

View File

@ -33,11 +33,9 @@ enum AlbumType {
class AlbumCard extends HookConsumerWidget { class AlbumCard extends HookConsumerWidget {
final Album album; final Album album;
final PlaybuttonCardViewType viewType;
const AlbumCard( const AlbumCard(
this.album, { this.album, {
Key? key, Key? key,
this.viewType = PlaybuttonCardViewType.square,
}) : super(key: key); }) : super(key: key);
@override @override
@ -65,7 +63,6 @@ class AlbumCard extends HookConsumerWidget {
album.images, album.images,
placeholder: ImagePlaceholder.collection, placeholder: ImagePlaceholder.collection,
), ),
viewType: viewType,
margin: EdgeInsets.symmetric(horizontal: marginH.toDouble()), margin: EdgeInsets.symmetric(horizontal: marginH.toDouble()),
isPlaying: isPlaylistPlaying, isPlaying: isPlaylistPlaying,
isLoading: isPlaylistPlaying && playlist?.isLoading == true, isLoading: isPlaylistPlaying && playlist?.isLoading == true,

View File

@ -1,12 +1,13 @@
import 'package:flutter/gestures.dart'; import 'dart:ui';
import 'package:flutter/material.dart' hide Page; import 'package:flutter/material.dart' hide Page;
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.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/shimmers/shimmer_playbutton_card.dart';
import 'package:spotube/components/shared/waypoint.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/models/logger.dart';
import 'package:spotube/services/queries/queries.dart'; import 'package:spotube/services/queries/queries.dart';
@ -27,51 +28,42 @@ class CategoryCard extends HookConsumerWidget {
category.id!, category.id!,
); );
final playlists = playlistQuery.pages if (playlistQuery.hasErrors && !playlistQuery.hasPageData) {
.expand( return const SizedBox.shrink();
(page) => page.items ?? const Iterable.empty(), }
) final playlists = playlistQuery.pages.expand(
.toList(); (page) {
return page.items?.where((i) => i != null) ?? const Iterable.empty();
return Column( },
children: [ ).toList();
Padding( return Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: Row( child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
category.name ?? "Unknown", category.name!,
style: Theme.of(context).textTheme.titleLarge, style: Theme.of(context).textTheme.titleLarge,
), ),
], ScrollConfiguration(
),
),
playlistQuery.hasPageError && !playlistQuery.hasPageData
? Text("Something Went Wrong\n${playlistQuery.errors.first}")
: SizedBox(
height: 245,
child: ScrollConfiguration(
behavior: ScrollConfiguration.of(context).copyWith( behavior: ScrollConfiguration.of(context).copyWith(
dragDevices: { dragDevices: {
PointerDeviceKind.touch, PointerDeviceKind.touch,
PointerDeviceKind.mouse, PointerDeviceKind.mouse,
}, },
), ),
child: Scrollbar(
controller: scrollController,
interactive: false,
child: Waypoint( child: Waypoint(
controller: scrollController, controller: scrollController,
onTouchEdge: () { onTouchEdge: playlistQuery.fetchNext,
playlistQuery.fetchNext(); child: SingleChildScrollView(
},
child: ListView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
shrinkWrap: true,
controller: scrollController, controller: scrollController,
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
...playlists ...playlists.map((playlist) => PlaylistCard(playlist)),
.map((playlist) => PlaylistCard(playlist)),
if (playlistQuery.hasNextPage) if (playlistQuery.hasNextPage)
const ShimmerPlaybuttonCard(count: 1), const ShimmerPlaybuttonCard(count: 1),
], ],
@ -79,8 +71,8 @@ class CategoryCard extends HookConsumerWidget {
), ),
), ),
), ),
),
], ],
),
); );
} }
} }

View File

@ -6,7 +6,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/album/album_card.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/shimmers/shimmer_playbutton_card.dart';
import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart'; import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart';
import 'package:spotube/hooks/use_breakpoint_value.dart'; import 'package:spotube/hooks/use_breakpoint_value.dart';
@ -28,9 +27,6 @@ class UserAlbums extends HookConsumerWidget {
sm: 0, sm: 0,
others: 20, others: 20,
); );
final viewType = MediaQuery.of(context).size.width < 480
? PlaybuttonCardViewType.list
: PlaybuttonCardViewType.square;
final searchText = useState(''); final searchText = useState('');
@ -82,7 +78,6 @@ class UserAlbums extends HookConsumerWidget {
alignment: WrapAlignment.center, alignment: WrapAlignment.center,
children: albums children: albums
.map((album) => AlbumCard( .map((album) => AlbumCard(
viewType: viewType,
TypeConversionUtils.simpleAlbum_X_Album(album), TypeConversionUtils.simpleAlbum_X_Album(album),
)) ))
.toList(), .toList(),

View File

@ -7,7 +7,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/playlist/playlist_create_dialog.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/shimmers/shimmer_playbutton_card.dart';
import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart'; import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart';
import 'package:spotube/components/playlist/playlist_card.dart'; import 'package:spotube/components/playlist/playlist_card.dart';
@ -28,9 +27,6 @@ class UserPlaylists extends HookConsumerWidget {
sm: 0, sm: 0,
others: 20, others: 20,
); );
final viewType = MediaQuery.of(context).size.width < 480
? PlaybuttonCardViewType.list
: PlaybuttonCardViewType.square;
final auth = ref.watch(AuthenticationNotifier.provider); final auth = ref.watch(AuthenticationNotifier.provider);
final playlistsQuery = useQueries.playlist.ofMine(ref); final playlistsQuery = useQueries.playlist.ofMine(ref);
@ -81,12 +77,7 @@ class UserPlaylists extends HookConsumerWidget {
final children = [ final children = [
const PlaylistCreateDialog(), const PlaylistCreateDialog(),
...playlists ...playlists.map((playlist) => PlaylistCard(playlist)).toList(),
.map((playlist) => PlaylistCard(
playlist,
viewType: viewType,
))
.toList(),
]; ];
return RefreshIndicator( return RefreshIndicator(
onRefresh: playlistsQuery.refresh, onRefresh: playlistsQuery.refresh,

View File

@ -13,11 +13,9 @@ import 'package:spotube/utils/type_conversion_utils.dart';
class PlaylistCard extends HookConsumerWidget { class PlaylistCard extends HookConsumerWidget {
final PlaylistSimple playlist; final PlaylistSimple playlist;
final PlaybuttonCardViewType viewType;
const PlaylistCard( const PlaylistCard(
this.playlist, { this.playlist, {
Key? key, Key? key,
this.viewType = PlaybuttonCardViewType.square,
}) : super(key: key); }) : super(key: key);
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
@ -43,9 +41,9 @@ class PlaylistCard extends HookConsumerWidget {
final spotify = ref.watch(spotifyProvider); final spotify = ref.watch(spotifyProvider);
return PlaybuttonCard( return PlaybuttonCard(
viewType: viewType,
margin: EdgeInsets.symmetric(horizontal: marginH.toDouble()), margin: EdgeInsets.symmetric(horizontal: marginH.toDouble()),
title: playlist.name!, title: playlist.name!,
description: playlist.description,
imageUrl: TypeConversionUtils.image_X_UrlString( imageUrl: TypeConversionUtils.image_X_UrlString(
playlist.images, playlist.images,
placeholder: ImagePlaceholder.collection, placeholder: ImagePlaceholder.collection,

View File

@ -1,13 +1,12 @@
import 'package:auto_size_text/auto_size_text.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/collections/assets.gen.dart';
import 'package:spotube/collections/spotube_icons.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'; import 'package:spotube/components/shared/image/universal_image.dart';
import 'package:spotube/hooks/use_breakpoint_value.dart';
enum PlaybuttonCardViewType { square, list } import 'package:spotube/hooks/use_brightness_value.dart';
class PlaybuttonCard extends HookWidget { class PlaybuttonCard extends HookWidget {
final void Function()? onTap; final void Function()? onTap;
@ -19,7 +18,6 @@ class PlaybuttonCard extends HookWidget {
final bool isPlaying; final bool isPlaying;
final bool isLoading; final bool isLoading;
final String title; final String title;
final PlaybuttonCardViewType viewType;
const PlaybuttonCard({ const PlaybuttonCard({
required this.imageUrl, required this.imageUrl,
@ -31,190 +29,144 @@ class PlaybuttonCard extends HookWidget {
this.onPlaybuttonPressed, this.onPlaybuttonPressed,
this.onAddToQueuePressed, this.onAddToQueuePressed,
this.onTap, this.onTap,
this.viewType = PlaybuttonCardViewType.square,
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
@override @override
Widget build(BuildContext context) { 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<double>(
sm: 130,
md: 150,
others: 170,
);
final end = useBreakpointValue<double>(
sm: 5,
md: 7,
others: 10,
);
return Container( return Container(
constraints: BoxConstraints(maxWidth: size),
margin: margin, margin: margin,
child: Material(
color: Color.lerp(
theme.colorScheme.surfaceVariant,
theme.colorScheme.surface,
useBrightnessValue(.9, .7),
),
borderRadius: radius,
shadowColor: shadowColor,
elevation: 3,
child: InkWell( child: InkWell(
mouseCursor: SystemMouseCursors.click,
onTap: onTap, onTap: onTap,
borderRadius: BorderRadius.circular(8), borderRadius: radius,
highlightColor: Colors.black12, splashFactory: theme.splashFactory,
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,
),
);
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( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (!isPlaying) addToQueueButton, Stack(
const SizedBox(height: 5), clipBehavior: Clip.none,
playButton, children: [
], Container(
margin: const EdgeInsets.only(
left: 8,
right: 8,
top: 8,
), ),
) 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: Positioned.directional(
const EdgeInsets.symmetric(horizontal: 16, vertical: 10), textDirection: TextDirection.ltr,
end: end,
bottom: -5,
child: Column( child: Column(
mainAxisSize: MainAxisSize.min,
children: [ children: [
Tooltip( if (!isPlaying)
message: title, IconButton(
child: SizedBox( style: IconButton.styleFrom(
height: 20, backgroundColor: theme.colorScheme.background,
child: SpotubeMarqueeText( foregroundColor: theme.colorScheme.primary,
text: title, minimumSize: const Size.square(10),
style: const TextStyle(fontWeight: FontWeight.bold),
isHovering: isHovering,
), ),
icon: const Icon(SpotubeIcons.queueAdd),
onPressed: isLoading ? null : onAddToQueuePressed,
), ),
const SizedBox(height: 5),
IconButton(
style: IconButton.styleFrom(
backgroundColor: theme.colorScheme.primaryContainer,
foregroundColor: theme.colorScheme.primary,
minimumSize: const Size.square(10),
), ),
if (description != null) ...[ icon: isLoading
const SizedBox(height: 10), ? SizedBox.fromSize(
SizedBox( size: const Size.square(15),
height: 30, child: const CircularProgressIndicator(
child: SpotubeMarqueeText( strokeWidth: 2),
text: description!, )
style: Theme.of(context).textTheme.bodySmall, : isPlaying
isHovering: isHovering, ? const Icon(SpotubeIcons.pause)
: const Icon(SpotubeIcons.play),
onPressed: isLoading ? null : onPlaybuttonPressed,
), ),
),
]
], ],
), ),
), ),
], ],
); ),
const SizedBox(height: 15),
final list = Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// thumbnail of the playlist
Flexible( Flexible(
child: Row( child: Padding(
children: [ padding: const EdgeInsets.symmetric(horizontal: 12.0),
image, child: AutoSizeText(
const SizedBox(width: 10), title,
Flexible( maxLines: 1,
child: RichText( minFontSize: theme.textTheme.bodyMedium!.fontSize!,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
text: TextSpan( ),
children: [ ),
TextSpan(
text: title,
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(fontWeight: FontWeight.bold),
), ),
if (description != null) if (description != null)
TextSpan( Flexible(
text: '\n$description', child: Padding(
style: Theme.of(context).textTheme.bodySmall, 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,
),
),
),
const SizedBox(height: 10),
], ],
), ),
), ),
), ),
],
),
),
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,
);
}),
),
),
); );
} }
} }

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; 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 { class ShimmerPlaybuttonCardPainter extends CustomPainter {
final Color background; final Color background;
@ -12,29 +13,59 @@ class ShimmerPlaybuttonCardPainter extends CustomPainter {
@override @override
void paint(Canvas canvas, Size size) { void paint(Canvas canvas, Size size) {
const radius = Radius.circular(15);
canvas.drawRRect( canvas.drawRRect(
RRect.fromRectAndRadius( RRect.fromRectAndRadius(
Rect.fromLTWH(0, 0, size.width, size.height), Rect.fromLTWH(0, 0, size.width, size.height),
const Radius.circular(10), radius,
), ),
Paint()..color = background, Paint()..color = background,
); );
canvas.drawRRect( canvas.drawRRect(
RRect.fromRectAndRadius( RRect.fromRectAndRadius(
Rect.fromLTWH(0, 0, size.width, size.height - 45), Rect.fromLTWH(8, 8, size.width - 16, size.height - 90),
const Radius.circular(10), radius,
), ),
Paint()..color = foreground, Paint()..color = foreground,
); );
canvas.drawRRect( canvas.drawRRect(
RRect.fromRectAndRadius( RRect.fromRectAndRadius(
Rect.fromLTWH(size.width / 4, size.height - 27, size.width / 2, 10), Rect.fromLTWH(12, size.height - 67, size.width / 2, 10),
const Radius.circular(10), radius,
), ),
Paint()..color = foreground, 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(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 @override
@ -43,7 +74,7 @@ class ShimmerPlaybuttonCardPainter extends CustomPainter {
} }
} }
class ShimmerPlaybuttonCard extends StatelessWidget { class ShimmerPlaybuttonCard extends HookWidget {
final int count; final int count;
const ShimmerPlaybuttonCard({ const ShimmerPlaybuttonCard({
@ -53,10 +84,19 @@ class ShimmerPlaybuttonCard extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context);
final Size size = useBreakpointValue<Size>(
sm: const Size(130, 200),
md: const Size(150, 220),
others: const Size(170, 240),
);
final isDark = Theme.of(context).brightness == Brightness.dark; final isDark = Theme.of(context).brightness == Brightness.dark;
final shimmerTheme = ShimmerColorTheme( final bgColor = theme.colorScheme.surfaceVariant.withOpacity(.2);
shimmerBackgroundColor: isDark ? Colors.grey[700] : Colors.grey[200], final fgColor = Color.lerp(
shimmerColor: isDark ? Colors.grey[800] : Colors.grey[300], theme.colorScheme.surfaceVariant,
isDark ? Colors.black : Colors.white,
.4,
); );
return Row( return Row(
@ -64,12 +104,10 @@ class ShimmerPlaybuttonCard extends StatelessWidget {
children: [ children: [
for (var i = 0; i < count; i++) ...[ for (var i = 0; i < count; i++) ...[
CustomPaint( CustomPaint(
size: const Size(200, 250), size: size,
painter: ShimmerPlaybuttonCardPainter( painter: ShimmerPlaybuttonCardPainter(
background: shimmerTheme.shimmerBackgroundColor ?? background: bgColor,
Theme.of(context).scaffoldBackgroundColor, foreground: fgColor!,
foreground:
shimmerTheme.shimmerColor ?? Theme.of(context).cardColor,
), ),
), ),
const SizedBox(width: 10), 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<ShimmerColorTheme>()?.shimmerColor ??
// Colors.white;
// final shimmerBackgroundColor = Theme.of(context)
// .extension<ShimmerColorTheme>()
// ?.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,
// ),
// ),
// ),
// ],
// ),
// );
// }
// }

View File

@ -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,
),
);
}
}

View File

@ -68,17 +68,21 @@ class GenrePage extends HookConsumerWidget {
} }
}, },
controller: scrollController, controller: scrollController,
child: ListView.builder( child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(), child: SafeArea(
controller: scrollController, child: Column(
itemCount: categories.length, crossAxisAlignment: CrossAxisAlignment.start,
itemBuilder: (context, index) { children: [
final category = categories[index]; ...categories.mapIndexed((index, category) {
if (searchText.value.isEmpty && index == categories.length - 1) { if (searchText.value.isEmpty &&
index == categories.length - 1) {
return const ShimmerCategories(); return const ShimmerCategories();
} }
return SafeArea(child: CategoryCard(category)); return CategoryCard(category);
}, })
],
),
),
), ),
), ),
); );

View File

@ -47,22 +47,20 @@ class PersonalizedItemCard extends HookWidget {
) )
.toList(); .toList();
return Column( return Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: Row( child: Text(
children: [
Text(
title, title,
style: Theme.of(context).textTheme.titleLarge, style: Theme.of(context).textTheme.titleLarge,
), ),
],
), ),
), ScrollConfiguration(
SizedBox(
height: playlists != null ? 245 : 285,
child: ScrollConfiguration(
behavior: ScrollConfiguration.of(context).copyWith( behavior: ScrollConfiguration.of(context).copyWith(
dragDevices: { dragDevices: {
PointerDeviceKind.touch, PointerDeviceKind.touch,
@ -75,12 +73,13 @@ class PersonalizedItemCard extends HookWidget {
child: Waypoint( child: Waypoint(
controller: scrollController, controller: scrollController,
onTouchEdge: hasNextPage ? onFetchMore : null, onTouchEdge: hasNextPage ? onFetchMore : null,
child: SafeArea( child: SingleChildScrollView(
child: ListView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
shrinkWrap: true,
controller: scrollController, controller: scrollController,
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [ children: [
...?playlistItems ...?playlistItems
?.map((playlist) => PlaylistCard(playlist)), ?.map((playlist) => PlaylistCard(playlist)),
@ -96,8 +95,8 @@ class PersonalizedItemCard extends HookWidget {
), ),
), ),
), ),
),
], ],
),
); );
} }
} }
@ -111,11 +110,14 @@ class PersonalizedPage extends HookConsumerWidget {
final newReleases = useQueries.album.newReleases(ref); final newReleases = useQueries.album.newReleases(ref);
return ListView( return SingleChildScrollView(
child: SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
PersonalizedItemCard( PersonalizedItemCard(
playlists: playlists: featuredPlaylistsQuery.pages
featuredPlaylistsQuery.pages.whereType<Page<PlaylistSimple>>(), .whereType<Page<PlaylistSimple>>(),
title: 'Featured', title: 'Featured',
hasNextPage: featuredPlaylistsQuery.hasNextPage, hasNextPage: featuredPlaylistsQuery.hasNextPage,
onFetchMore: featuredPlaylistsQuery.fetchNext, onFetchMore: featuredPlaylistsQuery.fetchNext,
@ -127,6 +129,8 @@ class PersonalizedPage extends HookConsumerWidget {
onFetchMore: newReleases.fetchNext, onFetchMore: newReleases.fetchNext,
), ),
], ],
),
),
); );
} }
} }

View File

@ -6,7 +6,6 @@ import 'package:spotify/spotify.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/lyrics/zoom_controls.dart'; import 'package:spotube/components/lyrics/zoom_controls.dart';
import 'package:spotube/components/shared/shimmers/shimmer_lyrics.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_auto_scroll_controller.dart';
import 'package:spotube/hooks/use_breakpoints.dart'; import 'package:spotube/hooks/use_breakpoints.dart';
import 'package:spotube/hooks/use_synced_lyrics.dart'; import 'package:spotube/hooks/use_synced_lyrics.dart';
@ -69,17 +68,15 @@ class SyncedLyrics extends HookConsumerWidget {
: textTheme.headlineMedium?.copyWith(fontSize: 25)) : textTheme.headlineMedium?.copyWith(fontSize: 25))
?.copyWith(color: palette.titleTextColor); ?.copyWith(color: palette.titleTextColor);
return HookBuilder(builder: (context) {
return Stack( return Stack(
children: [ children: [
Column( Column(
children: [ children: [
if (isModal != true) if (isModal != true)
Center( Center(
child: SpotubeMarqueeText( child: Text(
text: playlist?.activeTrack.name ?? "Not Playing", playlist?.activeTrack.name ?? "Not Playing",
style: headlineTextStyle, style: headlineTextStyle,
isHovering: true,
), ),
), ),
if (isModal != true) if (isModal != true)
@ -181,6 +178,5 @@ class SyncedLyrics extends HookConsumerWidget {
), ),
], ],
); );
});
} }
} }

View File

@ -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_actions.dart';
import 'package:spotube/components/player/player_controls.dart'; import 'package:spotube/components/player/player_controls.dart';
import 'package:spotube/components/shared/page_window_title_bar.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/components/shared/image/universal_image.dart';
import 'package:spotube/hooks/use_breakpoints.dart'; import 'package:spotube/hooks/use_breakpoints.dart';
import 'package:spotube/hooks/use_custom_status_bar_color.dart'; import 'package:spotube/hooks/use_custom_status_bar_color.dart';
@ -93,8 +92,8 @@ class PlayerView extends HookConsumerWidget {
children: [ children: [
SizedBox( SizedBox(
height: 30, height: 30,
child: SpotubeMarqueeText( child: Text(
text: currentTrack?.name ?? "Not playing", currentTrack?.name ?? "Not playing",
style: Theme.of(context) style: Theme.of(context)
.textTheme .textTheme
.headlineSmall .headlineSmall
@ -102,7 +101,6 @@ class PlayerView extends HookConsumerWidget {
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: paletteColor.titleTextColor, color: paletteColor.titleTextColor,
), ),
isHovering: true,
), ),
), ),
if (isLocalTrack) if (isLocalTrack)

View File

@ -35,16 +35,20 @@ class CategoryQueries {
); );
} }
InfiniteQuery<Page<PlaylistSimple>, dynamic, int> playlistsOf( InfiniteQuery<Page<PlaylistSimple?>, dynamic, int> playlistsOf(
WidgetRef ref, WidgetRef ref,
String category, String category,
) { ) {
return useSpotifyInfiniteQuery<Page<PlaylistSimple>, dynamic, int>( return useSpotifyInfiniteQuery<Page<PlaylistSimple?>, dynamic, int>(
"category-playlists/$category", "category-playlists/$category",
(pageParam, spotify) async { (pageParam, spotify) async {
final playlists = await spotify.playlists final playlists = await Pages<PlaylistSimple?>(
.getByCategoryId(category) spotify,
.getPage(5, pageParam); "v1/browse/categories/$category/playlists",
(json) => json == null ? null : PlaylistSimple.fromJson(json),
'playlists',
(json) => PlaylistsFeatured.fromJson(json),
).getPage(5, pageParam);
return playlists; return playlists;
}, },

View File

@ -490,14 +490,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.0" 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: fake_async:
dependency: transitive dependency: transitive
description: description:
@ -929,14 +921,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.0.0" version: "6.0.0"
marquee:
dependency: "direct main"
description:
name: marquee
sha256: "4b5243d2804373bdc25fc93d42c3b402d6ec1f4ee8d0bb72276edd04ae7addb8"
url: "https://pub.dev"
source: hosted
version: "2.2.3"
matcher: matcher:
dependency: transitive dependency: transitive
description: description:

View File

@ -51,7 +51,6 @@ dependencies:
json_annotation: ^4.8.0 json_annotation: ^4.8.0
json_serializable: ^6.6.0 json_serializable: ^6.6.0
logger: ^1.1.0 logger: ^1.1.0
marquee: ^2.2.3
metadata_god: ^0.3.2 metadata_god: ^0.3.2
mime: ^1.0.2 mime: ^1.0.2
package_info_plus: ^3.0.2 package_info_plus: ^3.0.2