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,60 +28,51 @@ 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(
children: [ mainAxisSize: MainAxisSize.min,
Text( crossAxisAlignment: CrossAxisAlignment.start,
category.name ?? "Unknown", children: [
style: Theme.of(context).textTheme.titleLarge, Text(
), category.name!,
], style: Theme.of(context).textTheme.titleLarge,
), ),
), ScrollConfiguration(
playlistQuery.hasPageError && !playlistQuery.hasPageData behavior: ScrollConfiguration.of(context).copyWith(
? Text("Something Went Wrong\n${playlistQuery.errors.first}") dragDevices: {
: SizedBox( PointerDeviceKind.touch,
height: 245, PointerDeviceKind.mouse,
child: ScrollConfiguration( },
behavior: ScrollConfiguration.of(context).copyWith( ),
dragDevices: { child: Waypoint(
PointerDeviceKind.touch, controller: scrollController,
PointerDeviceKind.mouse, onTouchEdge: playlistQuery.fetchNext,
}, child: SingleChildScrollView(
), scrollDirection: Axis.horizontal,
child: Scrollbar( controller: scrollController,
controller: scrollController, padding: const EdgeInsets.symmetric(vertical: 8.0),
interactive: false, child: Row(
child: Waypoint( crossAxisAlignment: CrossAxisAlignment.start,
controller: scrollController, children: [
onTouchEdge: () { ...playlists.map((playlist) => PlaylistCard(playlist)),
playlistQuery.fetchNext(); if (playlistQuery.hasNextPage)
}, const ShimmerPlaybuttonCard(count: 1),
child: ListView( ],
scrollDirection: Axis.horizontal,
shrinkWrap: true,
controller: scrollController,
children: [
...playlists
.map((playlist) => PlaylistCard(playlist)),
if (playlistQuery.hasNextPage)
const ShimmerPlaybuttonCard(count: 1),
],
),
),
),
), ),
), ),
], ),
),
],
),
); );
} }
} }

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,188 +29,142 @@ 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: InkWell( child: Material(
onTap: onTap, color: Color.lerp(
borderRadius: BorderRadius.circular(8), theme.colorScheme.surfaceVariant,
highlightColor: Colors.black12, theme.colorScheme.surface,
child: ConstrainedBox( useBrightnessValue(.9, .7),
constraints: BoxConstraints( ),
maxWidth: isSquare ? 200 : double.infinity, borderRadius: radius,
maxHeight: !isSquare ? 60 : double.infinity, shadowColor: shadowColor,
), elevation: 3,
child: HoverBuilder(builder: (context, isHovering) { child: InkWell(
final playButton = IconButton( mouseCursor: SystemMouseCursors.click,
onPressed: onPlaybuttonPressed, onTap: onTap,
style: IconButton.styleFrom( borderRadius: radius,
backgroundColor: Theme.of(context).primaryColor, splashFactory: theme.splashFactory,
hoverColor: Theme.of(context).primaryColor.withOpacity(0.5), child: Column(
), mainAxisSize: MainAxisSize.min,
icon: isLoading crossAxisAlignment: CrossAxisAlignment.start,
? SizedBox( children: [
height: 23, Stack(
width: 23, clipBehavior: Clip.none,
child: CircularProgressIndicator( children: [
color: ThemeData.estimateBrightnessForColor( Container(
Theme.of(context).primaryColor, margin: const EdgeInsets.only(
) == left: 8,
Brightness.dark right: 8,
? Colors.white top: 8,
: Colors.grey[900],
),
)
: Icon(
isPlaying ? SpotubeIcons.pause : SpotubeIcons.play,
color: Colors.white,
), ),
); constraints: BoxConstraints(maxHeight: size),
final addToQueueButton = IconButton( child: ClipRRect(
onPressed: isLoading ? null : onAddToQueuePressed, borderRadius: radius,
style: IconButton.styleFrom( child: UniversalImage(
backgroundColor: Theme.of(context).cardColor, path: imageUrl,
hoverColor: Theme.of(context) placeholder: (context, url) {
.cardColor return Assets.albumPlaceholder
.withOpacity(isLoading ? 1 : 0.5), .image(fit: BoxFit.cover);
), },
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,
],
), ),
) ),
],
),
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,
),
),
),
if (description != null) ...[
const SizedBox(height: 10),
SizedBox(
height: 30,
child: SpotubeMarqueeText(
text: description!,
style: Theme.of(context).textTheme.bodySmall,
isHovering: isHovering,
),
),
]
],
), ),
), Positioned.directional(
], textDirection: TextDirection.ltr,
); end: end,
bottom: -5,
final list = Row( child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisSize: MainAxisSize.min,
children: [ children: [
// thumbnail of the playlist if (!isPlaying)
Flexible( IconButton(
child: Row( style: IconButton.styleFrom(
children: [ backgroundColor: theme.colorScheme.background,
image, foregroundColor: theme.colorScheme.primary,
const SizedBox(width: 10), minimumSize: const Size.square(10),
Flexible( ),
child: RichText( icon: const Icon(SpotubeIcons.queueAdd),
overflow: TextOverflow.ellipsis, onPressed: isLoading ? null : onAddToQueuePressed,
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,
),
],
), ),
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,
), ),
), ],
], ),
),
),
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: 15),
); Flexible(
}), child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: AutoSizeText(
title,
maxLines: 1,
minFontSize: theme.textTheme.bodyMedium!.fontSize!,
overflow: TextOverflow.ellipsis,
),
),
),
if (description != null)
Flexible(
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,
),
),
),
const SizedBox(height: 10),
],
),
), ),
), ),
); );

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 &&
return const ShimmerCategories(); index == categories.length - 1) {
} 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(
children: [ padding: const EdgeInsets.all(8.0),
Padding( child: Column(
padding: const EdgeInsets.all(8.0), crossAxisAlignment: CrossAxisAlignment.start,
child: Row( mainAxisSize: MainAxisSize.min,
children: [ children: [
Text( Padding(
title, padding: const EdgeInsets.all(8.0),
style: Theme.of(context).textTheme.titleLarge, child: Text(
), title,
], 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, controller: scrollController,
shrinkWrap: true, physics: const AlwaysScrollableScrollPhysics(),
controller: scrollController, padding: const EdgeInsets.symmetric(vertical: 8.0),
physics: const AlwaysScrollableScrollPhysics(), 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,22 +110,27 @@ class PersonalizedPage extends HookConsumerWidget {
final newReleases = useQueries.album.newReleases(ref); final newReleases = useQueries.album.newReleases(ref);
return ListView( return SingleChildScrollView(
children: [ child: SafeArea(
PersonalizedItemCard( child: Column(
playlists: crossAxisAlignment: CrossAxisAlignment.start,
featuredPlaylistsQuery.pages.whereType<Page<PlaylistSimple>>(), children: [
title: 'Featured', PersonalizedItemCard(
hasNextPage: featuredPlaylistsQuery.hasNextPage, playlists: featuredPlaylistsQuery.pages
onFetchMore: featuredPlaylistsQuery.fetchNext, .whereType<Page<PlaylistSimple>>(),
title: 'Featured',
hasNextPage: featuredPlaylistsQuery.hasNextPage,
onFetchMore: featuredPlaylistsQuery.fetchNext,
),
PersonalizedItemCard(
albums: newReleases.pages.whereType<Page<AlbumSimple>>(),
title: 'New Releases',
hasNextPage: newReleases.hasNextPage,
onFetchMore: newReleases.fetchNext,
),
],
), ),
PersonalizedItemCard( ),
albums: newReleases.pages.whereType<Page<AlbumSimple>>(),
title: 'New Releases',
hasNextPage: newReleases.hasNextPage,
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,118 +68,115 @@ 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: Text(
child: SpotubeMarqueeText( playlist?.activeTrack.name ?? "Not Playing",
text: playlist?.activeTrack.name ?? "Not Playing", style: headlineTextStyle,
style: headlineTextStyle,
isHovering: true,
),
), ),
if (isModal != true) ),
Center( if (isModal != true)
child: Text( Center(
TypeConversionUtils.artists_X_String<Artist>( child: Text(
playlist?.activeTrack.artists ?? []), TypeConversionUtils.artists_X_String<Artist>(
style: breakpoint >= Breakpoints.md playlist?.activeTrack.artists ?? []),
? textTheme.headlineSmall style: breakpoint >= Breakpoints.md
: textTheme.titleLarge, ? textTheme.headlineSmall
), : textTheme.titleLarge,
), ),
if (lyricValue != null && lyricValue.lyrics.isNotEmpty) ),
Expanded( if (lyricValue != null && lyricValue.lyrics.isNotEmpty)
child: ListView.builder( Expanded(
controller: controller, child: ListView.builder(
itemCount: lyricValue.lyrics.length, controller: controller,
itemBuilder: (context, index) { itemCount: lyricValue.lyrics.length,
final lyricSlice = lyricValue.lyrics[index]; itemBuilder: (context, index) {
final isActive = lyricSlice.time.inSeconds == currentTime; final lyricSlice = lyricValue.lyrics[index];
final isActive = lyricSlice.time.inSeconds == currentTime;
if (isActive) { if (isActive) {
controller.scrollToIndex( controller.scrollToIndex(
index, index,
preferPosition: AutoScrollPosition.middle, preferPosition: AutoScrollPosition.middle,
); );
} }
return AutoScrollTag( return AutoScrollTag(
key: ValueKey(index), key: ValueKey(index),
index: index, index: index,
controller: controller, controller: controller,
child: lyricSlice.text.isEmpty child: lyricSlice.text.isEmpty
? Container() ? Container()
: Center( : Center(
child: Padding( child: Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: AnimatedDefaultTextStyle( child: AnimatedDefaultTextStyle(
duration: const Duration(milliseconds: 250), duration: const Duration(milliseconds: 250),
style: TextStyle( style: TextStyle(
color: isActive color: isActive
? Colors.white ? Colors.white
: palette.bodyTextColor, : palette.bodyTextColor,
fontWeight: isActive fontWeight: isActive
? FontWeight.bold ? FontWeight.bold
: FontWeight.normal, : FontWeight.normal,
fontSize: (isActive ? 30 : 26) * fontSize: (isActive ? 30 : 26) *
(textZoomLevel.value / 100), (textZoomLevel.value / 100),
), ),
child: Text( child: Text(
lyricSlice.text, lyricSlice.text,
maxLines: 2, maxLines: 2,
textAlign: TextAlign.center, 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,
);
}),
),
],
);
} }
} }

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