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 {
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,

View File

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

View File

@ -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(),

View File

@ -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,

View File

@ -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,

View File

@ -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<double>(
sm: 130,
md: 150,
others: 170,
);
final end = useBreakpointValue<double>(
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,
),
),
),
if (description != null) ...[
const SizedBox(height: 10),
SizedBox(
height: 30,
child: SpotubeMarqueeText(
text: description!,
style: Theme.of(context).textTheme.bodySmall,
isHovering: isHovering,
),
),
]
],
),
),
),
],
);
final list = Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// thumbnail of the playlist
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,
),
],
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,
),
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_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(size.width / 4, size.height - 27, size.width / 2, 10),
const Radius.circular(10),
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(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<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 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<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,
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);
})
],
),
),
),
),
);

View File

@ -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<Page<PlaylistSimple>>(),
title: 'Featured',
hasNextPage: featuredPlaylistsQuery.hasNextPage,
onFetchMore: featuredPlaylistsQuery.fetchNext,
return SingleChildScrollView(
child: SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
PersonalizedItemCard(
playlists: featuredPlaylistsQuery.pages
.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/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<Artist>(
playlist?.activeTrack.artists ?? []),
style: breakpoint >= Breakpoints.md
? textTheme.headlineSmall
: textTheme.titleLarge,
),
),
if (isModal != true)
Center(
child: Text(
TypeConversionUtils.artists_X_String<Artist>(
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 (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 (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,
);
}),
),
],
);
}
}

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_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)

View File

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

View File

@ -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:

View File

@ -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