mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
feat: compact and adaptive playbutton card design
This commit is contained in:
parent
1bdce9fe96
commit
eeb8cabf49
@ -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,
|
||||
|
@ -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,51 +28,42 @@ class CategoryCard extends HookConsumerWidget {
|
||||
category.id!,
|
||||
);
|
||||
|
||||
final playlists = playlistQuery.pages
|
||||
.expand(
|
||||
(page) => page.items ?? const Iterable.empty(),
|
||||
)
|
||||
.toList();
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
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: Row(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
category.name ?? "Unknown",
|
||||
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(
|
||||
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(
|
||||
onTouchEdge: playlistQuery.fetchNext,
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
shrinkWrap: true,
|
||||
controller: scrollController,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
...playlists
|
||||
.map((playlist) => PlaylistCard(playlist)),
|
||||
...playlists.map((playlist) => PlaylistCard(playlist)),
|
||||
if (playlistQuery.hasNextPage)
|
||||
const ShimmerPlaybuttonCard(count: 1),
|
||||
],
|
||||
@ -79,8 +71,8 @@ class CategoryCard extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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(),
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,190 +29,144 @@ 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: 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: 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,
|
||||
),
|
||||
);
|
||||
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,
|
||||
borderRadius: radius,
|
||||
splashFactory: theme.splashFactory,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (!isPlaying) addToQueueButton,
|
||||
const SizedBox(height: 5),
|
||||
playButton,
|
||||
],
|
||||
Stack(
|
||||
clipBehavior: Clip.none,
|
||||
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:
|
||||
const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||
),
|
||||
),
|
||||
Positioned.directional(
|
||||
textDirection: TextDirection.ltr,
|
||||
end: end,
|
||||
bottom: -5,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Tooltip(
|
||||
message: title,
|
||||
child: SizedBox(
|
||||
height: 20,
|
||||
child: SpotubeMarqueeText(
|
||||
text: title,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
isHovering: isHovering,
|
||||
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),
|
||||
),
|
||||
if (description != null) ...[
|
||||
const SizedBox(height: 10),
|
||||
SizedBox(
|
||||
height: 30,
|
||||
child: SpotubeMarqueeText(
|
||||
text: description!,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
isHovering: isHovering,
|
||||
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,
|
||||
),
|
||||
),
|
||||
]
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
final list = Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
// thumbnail of the playlist
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
Flexible(
|
||||
child: Row(
|
||||
children: [
|
||||
image,
|
||||
const SizedBox(width: 10),
|
||||
Flexible(
|
||||
child: RichText(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: AutoSizeText(
|
||||
title,
|
||||
maxLines: 1,
|
||||
minFontSize: theme.textTheme.bodyMedium!.fontSize!,
|
||||
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,
|
||||
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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
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,
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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) {
|
||||
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 SafeArea(child: CategoryCard(category));
|
||||
},
|
||||
return CategoryCard(category);
|
||||
})
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -47,22 +47,20 @@ class PersonalizedItemCard extends HookWidget {
|
||||
)
|
||||
.toList();
|
||||
|
||||
return Column(
|
||||
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: Row(
|
||||
children: [
|
||||
Text(
|
||||
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(
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
shrinkWrap: true,
|
||||
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,11 +110,14 @@ class PersonalizedPage extends HookConsumerWidget {
|
||||
|
||||
final newReleases = useQueries.album.newReleases(ref);
|
||||
|
||||
return ListView(
|
||||
return SingleChildScrollView(
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
PersonalizedItemCard(
|
||||
playlists:
|
||||
featuredPlaylistsQuery.pages.whereType<Page<PlaylistSimple>>(),
|
||||
playlists: featuredPlaylistsQuery.pages
|
||||
.whereType<Page<PlaylistSimple>>(),
|
||||
title: 'Featured',
|
||||
hasNextPage: featuredPlaylistsQuery.hasNextPage,
|
||||
onFetchMore: featuredPlaylistsQuery.fetchNext,
|
||||
@ -127,6 +129,8 @@ class PersonalizedPage extends HookConsumerWidget {
|
||||
onFetchMore: newReleases.fetchNext,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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,17 +68,15 @@ 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",
|
||||
child: Text(
|
||||
playlist?.activeTrack.name ?? "Not Playing",
|
||||
style: headlineTextStyle,
|
||||
isHovering: true,
|
||||
),
|
||||
),
|
||||
if (isModal != true)
|
||||
@ -181,6 +178,5 @@ class SyncedLyrics extends HookConsumerWidget {
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
},
|
||||
|
16
pubspec.lock
16
pubspec.lock
@ -490,14 +490,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
fading_edge_scrollview:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fading_edge_scrollview
|
||||
sha256: c25c2231652ce774cc31824d0112f11f653881f43d7f5302c05af11942052031
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -929,14 +921,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.0"
|
||||
marquee:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: marquee
|
||||
sha256: "4b5243d2804373bdc25fc93d42c3b402d6ec1f4ee8d0bb72276edd04ae7addb8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.3"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user