refactor: artist page

This commit is contained in:
Kingkor Roy Tirtho 2025-01-05 09:47:32 +06:00
parent bbad701c07
commit 4afe0cca68
5 changed files with 222 additions and 236 deletions

View File

@ -1,5 +1,5 @@
import 'package:flutter/material.dart' hide Page;
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
import 'package:spotube/extensions/context.dart';
@ -30,7 +30,7 @@ class ArtistAlbumList extends HookConsumerWidget {
onFetchMore: albumsQueryNotifier.fetchMore,
title: Text(
context.l10n.albums,
style: theme.textTheme.headlineSmall,
style: theme.typography.h4,
),
);
}

View File

@ -1,8 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotube/components/button/back_button.dart';
import 'package:spotube/components/titlebar/titlebar.dart';
import 'package:spotube/modules/artist/artist_album_list.dart';
@ -30,12 +30,14 @@ class ArtistPage extends HookConsumerWidget {
return SafeArea(
bottom: false,
child: Scaffold(
appBar: const TitleBar(
headers: const [
TitleBar(
leading: [BackButton()],
backgroundColor: Colors.transparent,
),
extendBodyBehindAppBar: true,
body: Builder(builder: (context) {
)
],
floatingHeader: true,
child: Builder(builder: (context) {
if (artistQuery.hasError && artistQuery.asData?.value == null) {
return Center(child: Text(artistQuery.error.toString()));
}
@ -50,31 +52,26 @@ class ArtistPage extends HookConsumerWidget {
child: ArtistPageHeader(artistId: artistId),
),
),
const SliverGap(50),
ArtistPageTopTracks(artistId: artistId),
const SliverGap(50),
SliverToBoxAdapter(child: ArtistAlbumList(artistId)),
const SliverGap(20),
ArtistPageTopTracks(artistId: artistId),
const SliverGap(20),
SliverToBoxAdapter(child: ArtistAlbumList(artistId)),
SliverPadding(
padding: const EdgeInsets.all(8.0),
sliver: SliverToBoxAdapter(
child: Text(
context.l10n.fans_also_like,
style: theme.textTheme.headlineSmall,
style: theme.typography.h4,
),
),
),
SliverSafeArea(
sliver: ArtistPageRelatedArtists(artistId: artistId),
),
ArtistPageRelatedArtists(artistId: artistId),
const SliverGap(20),
if (artistQuery.asData?.value != null)
SliverSafeArea(
top: false,
sliver: SliverToBoxAdapter(
child:
ArtistPageFooter(artist: artistQuery.asData!.value),
),
SliverToBoxAdapter(
child: ArtistPageFooter(artist: artistQuery.asData!.value),
),
const SliverSafeArea(sliver: SliverGap(10)),
],
),
);

View File

@ -26,7 +26,7 @@ class ArtistPageFooter extends ConsumerWidget {
if (summary.asData?.value == null) return const SizedBox.shrink();
return Container(
margin: const EdgeInsets.all(16),
margin: const EdgeInsets.all(8),
padding: mediaQuery.smAndDown
? const EdgeInsets.all(20)
: const EdgeInsets.all(30),

View File

@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:auto_size_text/auto_size_text.dart';
import 'package:flutter/services.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotube/collections/fake.dart';
import 'package:spotube/collections/spotube_icons.dart';
@ -9,7 +9,6 @@ import 'package:spotube/components/image/universal_image.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/image.dart';
import 'package:spotube/hooks/utils/use_breakpoint_value.dart';
import 'package:spotube/models/database/database.dart';
import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/blacklist_provider.dart';
@ -25,19 +24,8 @@ class ArtistPageHeader extends HookConsumerWidget {
final artistQuery = ref.watch(artistProvider(artistId));
final artist = artistQuery.asData?.value ?? FakeData.artist;
final scaffoldMessenger = ScaffoldMessenger.of(context);
final mediaQuery = MediaQuery.of(context);
final theme = Theme.of(context);
final ThemeData(:textTheme) = theme;
final chipTextVariant = useBreakpointValue(
xs: textTheme.bodySmall,
sm: textTheme.bodySmall,
md: textTheme.bodyMedium,
lg: textTheme.bodyLarge,
xl: textTheme.titleSmall,
xxl: textTheme.titleMedium,
);
final ThemeData(:typography) = theme;
final auth = ref.watch(authenticationProvider);
ref.watch(blacklistProvider);
@ -48,106 +36,25 @@ class ArtistPageHeader extends HookConsumerWidget {
placeholder: ImagePlaceholder.artist,
);
return LayoutBuilder(
builder: (context, constrains) {
return Center(
child: Flex(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: constrains.smAndDown
? CrossAxisAlignment.start
: CrossAxisAlignment.center,
direction: constrains.smAndDown ? Axis.vertical : Axis.horizontal,
children: [
DecoratedBox(
decoration: BoxDecoration(
boxShadow: kElevationToShadow[2],
borderRadius: BorderRadius.circular(35),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(35),
child: UniversalImage(
path: image,
width: 250,
height: 250,
fit: BoxFit.cover,
),
),
),
const Gap(20),
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(50)),
child: Skeleton.keep(
child: Text(
artist.type!.toUpperCase(),
style: chipTextVariant.copyWith(
color: Colors.white,
),
),
),
),
if (isBlackListed) ...[
const SizedBox(width: 5),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: Colors.red[400],
borderRadius: BorderRadius.circular(50)),
child: Text(
context.l10n.blacklisted,
style: chipTextVariant.copyWith(
color: Colors.white,
),
),
),
]
],
),
Text(
artist.name!,
style: mediaQuery.smAndDown
? textTheme.headlineSmall
: textTheme.headlineMedium,
),
Text(
context.l10n.followers(
PrimitiveUtils.toReadableNumber(
artist.followers!.total!.toDouble(),
),
),
style: textTheme.bodyMedium?.copyWith(
fontWeight: mediaQuery.mdAndUp ? FontWeight.bold : null,
),
),
const Gap(20),
Skeleton.keep(
final actions = Skeleton.keep(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (auth.asData?.value != null)
Consumer(
builder: (context, ref, _) {
final isFollowingQuery = ref
.watch(artistIsFollowingProvider(artist.id!));
final followingArtistNotifier =
ref.watch(followedArtistsProvider.notifier);
final isFollowingQuery = ref.watch(
artistIsFollowingProvider(artist.id!),
);
final followingArtistNotifier = ref.watch(
followedArtistsProvider.notifier,
);
return switch (isFollowingQuery) {
AsyncData(value: final following) => Builder(
builder: (context) {
if (following) {
return OutlinedButton(
return Button.outline(
onPressed: () async {
await followingArtistNotifier
.removeArtists([artist.id!]);
@ -156,7 +63,7 @@ class ArtistPageHeader extends HookConsumerWidget {
);
}
return FilledButton(
return Button.primary(
onPressed: () async {
await followingArtistNotifier
.saveArtists([artist.id!]);
@ -174,22 +81,21 @@ class ArtistPageHeader extends HookConsumerWidget {
},
),
const SizedBox(width: 5),
IconButton(
tooltip: context.l10n.add_artist_to_blacklist,
Tooltip(
tooltip: TooltipContainer(
child: Text(context.l10n.add_artist_to_blacklist),
),
child: IconButton(
icon: Icon(
SpotubeIcons.userRemove,
color:
!isBlackListed ? Colors.red[400] : Colors.white,
),
style: IconButton.styleFrom(
backgroundColor:
isBlackListed ? Colors.red[400] : null,
color: !isBlackListed ? Colors.red[400] : null,
),
variance: isBlackListed
? ButtonVariance.destructive
: ButtonVariance.ghost,
onPressed: () async {
if (isBlackListed) {
await ref
.read(blacklistProvider.notifier)
.remove(artist.id!);
await ref.read(blacklistProvider.notifier).remove(artist.id!);
} else {
await ref.read(blacklistProvider.notifier).add(
BlacklistTableCompanion.insert(
@ -201,7 +107,8 @@ class ArtistPageHeader extends HookConsumerWidget {
}
},
),
IconButton(
),
IconButton.ghost(
icon: const Icon(SpotubeIcons.share),
onPressed: () async {
if (artist.externalUrls?.spotify != null) {
@ -214,25 +121,108 @@ class ArtistPageHeader extends HookConsumerWidget {
if (!context.mounted) return;
scaffoldMessenger.showSnackBar(
SnackBar(
width: 300,
behavior: SnackBarBehavior.floating,
content: Text(
showToast(
context: context,
location: ToastLocation.topRight,
dismissible: true,
builder: (context, overlay) {
return SurfaceCard(
child: Text(
context.l10n.artist_url_copied,
textAlign: TextAlign.center,
),
),
);
},
);
},
)
],
),
)
);
return LayoutBuilder(
builder: (context, constrains) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Card(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ClipRRect(
borderRadius: theme.borderRadiusXl,
child: UniversalImage(
path: image,
width: constrains.mdAndUp ? 200 : 120,
height: constrains.mdAndUp ? 200 : 120,
fit: BoxFit.cover,
),
),
const Gap(20),
Flexible(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
OutlineBadge(
child:
Text(context.l10n.artist).small().muted(),
),
if (isBlackListed) ...[
const Gap(5),
DestructiveBadge(
child: Text(context.l10n.blacklisted).small(),
),
]
],
),
const Gap(10),
Flexible(
child: AutoSizeText(
artist.name!,
style: constrains.smAndDown
? typography.h4
: typography.h3,
maxLines: 2,
overflow: TextOverflow.ellipsis,
minFontSize: 14,
),
),
const Gap(5),
Flexible(
child: AutoSizeText(
context.l10n.followers(
PrimitiveUtils.toReadableNumber(
artist.followers!.total!.toDouble(),
),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
minFontSize: 12,
).muted(),
),
if (constrains.mdAndUp) ...[
const Gap(20),
actions,
]
],
),
),
],
),
if (constrains.smAndDown) ...[
const Gap(20),
actions,
]
],
),
),
);
},
);

View File

@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/collections/fake.dart';
@ -19,7 +19,6 @@ class ArtistPageTopTracks extends HookConsumerWidget {
@override
Widget build(BuildContext context, ref) {
final theme = Theme.of(context);
final scaffoldMessenger = ScaffoldMessenger.of(context);
final playlist = ref.watch(audioPlayerProvider);
final playlistNotifier = ref.watch(audioPlayerProvider.notifier);
@ -93,46 +92,46 @@ class ArtistPageTopTracks extends HookConsumerWidget {
padding: const EdgeInsets.all(8.0),
child: Text(
context.l10n.top_tracks,
style: theme.textTheme.headlineSmall,
style: theme.typography.h4,
),
),
if (!isPlaylistPlaying)
IconButton(
IconButton.outline(
icon: const Icon(
SpotubeIcons.queueAdd,
),
onPressed: () {
playlistNotifier.addTracks(topTracks.toList());
scaffoldMessenger.showSnackBar(
SnackBar(
width: 300,
behavior: SnackBarBehavior.floating,
content: Text(
showToast(
context: context,
location: ToastLocation.topRight,
builder: (context, overlay) {
return SurfaceCard(
child: Text(
context.l10n.added_to_queue(
topTracks.length,
),
textAlign: TextAlign.center,
),
),
);
},
);
},
),
const SizedBox(width: 5),
IconButton(
IconButton.primary(
shape: ButtonShape.circle,
enabled: !isPlaylistPlaying,
icon: Skeleton.keep(
child: Icon(
isPlaylistPlaying ? SpotubeIcons.stop : SpotubeIcons.play,
color: Colors.white,
isPlaylistPlaying ? SpotubeIcons.pause : SpotubeIcons.play,
),
),
style: IconButton.styleFrom(
backgroundColor: theme.colorScheme.primary,
),
onPressed: () => playPlaylist(topTracks.toList()),
)
],
),
),
const SliverGap(10),
SliverList.builder(
itemCount: topTracks.length,
itemBuilder: (context, index) {