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:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart'; import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
@ -30,7 +30,7 @@ class ArtistAlbumList extends HookConsumerWidget {
onFetchMore: albumsQueryNotifier.fetchMore, onFetchMore: albumsQueryNotifier.fetchMore,
title: Text( title: Text(
context.l10n.albums, 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:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:skeletonizer/skeletonizer.dart'; import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotube/components/button/back_button.dart';
import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/components/titlebar/titlebar.dart';
import 'package:spotube/modules/artist/artist_album_list.dart'; import 'package:spotube/modules/artist/artist_album_list.dart';
@ -30,12 +30,14 @@ class ArtistPage extends HookConsumerWidget {
return SafeArea( return SafeArea(
bottom: false, bottom: false,
child: Scaffold( child: Scaffold(
appBar: const TitleBar( headers: const [
leading: [BackButton()], TitleBar(
backgroundColor: Colors.transparent, leading: [BackButton()],
), backgroundColor: Colors.transparent,
extendBodyBehindAppBar: true, )
body: Builder(builder: (context) { ],
floatingHeader: true,
child: Builder(builder: (context) {
if (artistQuery.hasError && artistQuery.asData?.value == null) { if (artistQuery.hasError && artistQuery.asData?.value == null) {
return Center(child: Text(artistQuery.error.toString())); return Center(child: Text(artistQuery.error.toString()));
} }
@ -50,31 +52,26 @@ class ArtistPage extends HookConsumerWidget {
child: ArtistPageHeader(artistId: artistId), child: ArtistPageHeader(artistId: artistId),
), ),
), ),
const SliverGap(50),
ArtistPageTopTracks(artistId: artistId),
const SliverGap(50),
SliverToBoxAdapter(child: ArtistAlbumList(artistId)),
const SliverGap(20), const SliverGap(20),
ArtistPageTopTracks(artistId: artistId),
const SliverGap(20),
SliverToBoxAdapter(child: ArtistAlbumList(artistId)),
SliverPadding( SliverPadding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
sliver: SliverToBoxAdapter( sliver: SliverToBoxAdapter(
child: Text( child: Text(
context.l10n.fans_also_like, context.l10n.fans_also_like,
style: theme.textTheme.headlineSmall, style: theme.typography.h4,
), ),
), ),
), ),
SliverSafeArea( ArtistPageRelatedArtists(artistId: artistId),
sliver: ArtistPageRelatedArtists(artistId: artistId), const SliverGap(20),
),
if (artistQuery.asData?.value != null) if (artistQuery.asData?.value != null)
SliverSafeArea( SliverToBoxAdapter(
top: false, child: ArtistPageFooter(artist: artistQuery.asData!.value),
sliver: 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(); if (summary.asData?.value == null) return const SizedBox.shrink();
return Container( return Container(
margin: const EdgeInsets.all(16), margin: const EdgeInsets.all(8),
padding: mediaQuery.smAndDown padding: mediaQuery.smAndDown
? const EdgeInsets.all(20) ? const EdgeInsets.all(20)
: const EdgeInsets.all(30), : 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:flutter/services.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:skeletonizer/skeletonizer.dart'; import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotube/collections/fake.dart'; import 'package:spotube/collections/fake.dart';
import 'package:spotube/collections/spotube_icons.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/constrains.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/image.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/models/database/database.dart';
import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/blacklist_provider.dart'; import 'package:spotube/provider/blacklist_provider.dart';
@ -25,19 +24,8 @@ class ArtistPageHeader extends HookConsumerWidget {
final artistQuery = ref.watch(artistProvider(artistId)); final artistQuery = ref.watch(artistProvider(artistId));
final artist = artistQuery.asData?.value ?? FakeData.artist; final artist = artistQuery.asData?.value ?? FakeData.artist;
final scaffoldMessenger = ScaffoldMessenger.of(context);
final mediaQuery = MediaQuery.of(context);
final theme = Theme.of(context); final theme = Theme.of(context);
final ThemeData(:textTheme) = theme; final ThemeData(:typography) = theme;
final chipTextVariant = useBreakpointValue(
xs: textTheme.bodySmall,
sm: textTheme.bodySmall,
md: textTheme.bodyMedium,
lg: textTheme.bodyLarge,
xl: textTheme.titleSmall,
xxl: textTheme.titleMedium,
);
final auth = ref.watch(authenticationProvider); final auth = ref.watch(authenticationProvider);
ref.watch(blacklistProvider); ref.watch(blacklistProvider);
@ -48,190 +36,192 @@ class ArtistPageHeader extends HookConsumerWidget {
placeholder: ImagePlaceholder.artist, placeholder: ImagePlaceholder.artist,
); );
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,
);
return switch (isFollowingQuery) {
AsyncData(value: final following) => Builder(
builder: (context) {
if (following) {
return Button.outline(
onPressed: () async {
await followingArtistNotifier
.removeArtists([artist.id!]);
},
child: Text(context.l10n.following),
);
}
return Button.primary(
onPressed: () async {
await followingArtistNotifier
.saveArtists([artist.id!]);
},
child: Text(context.l10n.follow),
);
},
),
AsyncError() => const SizedBox(),
_ => const SizedBox.square(
dimension: 20,
child: CircularProgressIndicator(),
)
};
},
),
const SizedBox(width: 5),
Tooltip(
tooltip: TooltipContainer(
child: Text(context.l10n.add_artist_to_blacklist),
),
child: IconButton(
icon: Icon(
SpotubeIcons.userRemove,
color: !isBlackListed ? Colors.red[400] : null,
),
variance: isBlackListed
? ButtonVariance.destructive
: ButtonVariance.ghost,
onPressed: () async {
if (isBlackListed) {
await ref.read(blacklistProvider.notifier).remove(artist.id!);
} else {
await ref.read(blacklistProvider.notifier).add(
BlacklistTableCompanion.insert(
name: artist.name!,
elementId: artist.id!,
elementType: BlacklistedType.artist,
),
);
}
},
),
),
IconButton.ghost(
icon: const Icon(SpotubeIcons.share),
onPressed: () async {
if (artist.externalUrls?.spotify != null) {
await Clipboard.setData(
ClipboardData(
text: artist.externalUrls!.spotify!,
),
);
}
if (!context.mounted) return;
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( return LayoutBuilder(
builder: (context, constrains) { builder: (context, constrains) {
return Center( return Padding(
child: Flex( padding: const EdgeInsets.symmetric(horizontal: 8.0),
mainAxisAlignment: MainAxisAlignment.center, child: Card(
crossAxisAlignment: constrains.smAndDown child: Column(
? CrossAxisAlignment.start crossAxisAlignment: CrossAxisAlignment.start,
: CrossAxisAlignment.center, mainAxisSize: MainAxisSize.min,
direction: constrains.smAndDown ? Axis.vertical : Axis.horizontal, children: [
children: [ Row(
DecoratedBox( crossAxisAlignment: CrossAxisAlignment.start,
decoration: BoxDecoration( children: [
boxShadow: kElevationToShadow[2], ClipRRect(
borderRadius: BorderRadius.circular(35), borderRadius: theme.borderRadiusXl,
), child: UniversalImage(
child: ClipRRect( path: image,
borderRadius: BorderRadius.circular(35), width: constrains.mdAndUp ? 200 : 120,
child: UniversalImage( height: constrains.mdAndUp ? 200 : 120,
path: image, fit: BoxFit.cover,
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( const Gap(20),
fontWeight: mediaQuery.mdAndUp ? FontWeight.bold : null, Flexible(
), child: Column(
), mainAxisSize: MainAxisSize.min,
const Gap(20), crossAxisAlignment: CrossAxisAlignment.start,
Skeleton.keep( children: [
child: Row( Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
if (auth.asData?.value != null) OutlineBadge(
Consumer( child:
builder: (context, ref, _) { Text(context.l10n.artist).small().muted(),
final isFollowingQuery = ref ),
.watch(artistIsFollowingProvider(artist.id!)); if (isBlackListed) ...[
final followingArtistNotifier = const Gap(5),
ref.watch(followedArtistsProvider.notifier); DestructiveBadge(
child: Text(context.l10n.blacklisted).small(),
return switch (isFollowingQuery) {
AsyncData(value: final following) => Builder(
builder: (context) {
if (following) {
return OutlinedButton(
onPressed: () async {
await followingArtistNotifier
.removeArtists([artist.id!]);
},
child: Text(context.l10n.following),
);
}
return FilledButton(
onPressed: () async {
await followingArtistNotifier
.saveArtists([artist.id!]);
},
child: Text(context.l10n.follow),
);
},
),
AsyncError() => const SizedBox(),
_ => const SizedBox.square(
dimension: 20,
child: CircularProgressIndicator(),
)
};
},
),
const SizedBox(width: 5),
IconButton(
tooltip: context.l10n.add_artist_to_blacklist,
icon: Icon(
SpotubeIcons.userRemove,
color:
!isBlackListed ? Colors.red[400] : Colors.white,
),
style: IconButton.styleFrom(
backgroundColor:
isBlackListed ? Colors.red[400] : null,
),
onPressed: () async {
if (isBlackListed) {
await ref
.read(blacklistProvider.notifier)
.remove(artist.id!);
} else {
await ref.read(blacklistProvider.notifier).add(
BlacklistTableCompanion.insert(
name: artist.name!,
elementId: artist.id!,
elementType: BlacklistedType.artist,
),
);
}
},
),
IconButton(
icon: const Icon(SpotubeIcons.share),
onPressed: () async {
if (artist.externalUrls?.spotify != null) {
await Clipboard.setData(
ClipboardData(
text: artist.externalUrls!.spotify!,
), ),
); ]
} ],
),
if (!context.mounted) return; const Gap(10),
Flexible(
scaffoldMessenger.showSnackBar( child: AutoSizeText(
SnackBar( artist.name!,
width: 300, style: constrains.smAndDown
behavior: SnackBarBehavior.floating, ? typography.h4
content: Text( : typography.h3,
context.l10n.artist_url_copied, maxLines: 2,
textAlign: TextAlign.center, 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:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:skeletonizer/skeletonizer.dart'; import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/collections/fake.dart'; import 'package:spotube/collections/fake.dart';
@ -19,7 +19,6 @@ class ArtistPageTopTracks extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final theme = Theme.of(context); final theme = Theme.of(context);
final scaffoldMessenger = ScaffoldMessenger.of(context);
final playlist = ref.watch(audioPlayerProvider); final playlist = ref.watch(audioPlayerProvider);
final playlistNotifier = ref.watch(audioPlayerProvider.notifier); final playlistNotifier = ref.watch(audioPlayerProvider.notifier);
@ -93,46 +92,46 @@ class ArtistPageTopTracks extends HookConsumerWidget {
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: Text( child: Text(
context.l10n.top_tracks, context.l10n.top_tracks,
style: theme.textTheme.headlineSmall, style: theme.typography.h4,
), ),
), ),
if (!isPlaylistPlaying) if (!isPlaylistPlaying)
IconButton( IconButton.outline(
icon: const Icon( icon: const Icon(
SpotubeIcons.queueAdd, SpotubeIcons.queueAdd,
), ),
onPressed: () { onPressed: () {
playlistNotifier.addTracks(topTracks.toList()); playlistNotifier.addTracks(topTracks.toList());
scaffoldMessenger.showSnackBar( showToast(
SnackBar( context: context,
width: 300, location: ToastLocation.topRight,
behavior: SnackBarBehavior.floating, builder: (context, overlay) {
content: Text( return SurfaceCard(
context.l10n.added_to_queue( child: Text(
topTracks.length, context.l10n.added_to_queue(
topTracks.length,
),
), ),
textAlign: TextAlign.center, );
), },
),
); );
}, },
), ),
const SizedBox(width: 5), const SizedBox(width: 5),
IconButton( IconButton.primary(
shape: ButtonShape.circle,
enabled: !isPlaylistPlaying,
icon: Skeleton.keep( icon: Skeleton.keep(
child: Icon( child: Icon(
isPlaylistPlaying ? SpotubeIcons.stop : SpotubeIcons.play, isPlaylistPlaying ? SpotubeIcons.pause : SpotubeIcons.play,
color: Colors.white,
), ),
), ),
style: IconButton.styleFrom(
backgroundColor: theme.colorScheme.primary,
),
onPressed: () => playPlaylist(topTracks.toList()), onPressed: () => playPlaylist(topTracks.toList()),
) )
], ],
), ),
), ),
const SliverGap(10),
SliverList.builder( SliverList.builder(
itemCount: topTracks.length, itemCount: topTracks.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {