mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-14 08:25:16 +00:00
feat(artist): modularize page and add wikipedia section
This commit is contained in:
parent
4511a0bd00
commit
2a69886556
@ -107,4 +107,5 @@ abstract class SpotubeIcons {
|
|||||||
static const eye = FeatherIcons.eye;
|
static const eye = FeatherIcons.eye;
|
||||||
static const noEye = FeatherIcons.eyeOff;
|
static const noEye = FeatherIcons.eyeOff;
|
||||||
static const normalize = FeatherIcons.barChart2;
|
static const normalize = FeatherIcons.barChart2;
|
||||||
|
static const wikipedia = SimpleIcons.wikipedia;
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@ 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';
|
||||||
import 'package:spotube/components/shared/waypoint.dart';
|
import 'package:spotube/components/shared/waypoint.dart';
|
||||||
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/provider/authentication_provider.dart';
|
import 'package:spotube/provider/authentication_provider.dart';
|
||||||
import 'package:spotube/services/queries/queries.dart';
|
import 'package:spotube/services/queries/queries.dart';
|
||||||
@ -120,31 +121,33 @@ class UserPlaylists extends HookConsumerWidget {
|
|||||||
const SliverToBoxAdapter(
|
const SliverToBoxAdapter(
|
||||||
child: SizedBox(height: 10),
|
child: SizedBox(height: 10),
|
||||||
),
|
),
|
||||||
SliverGrid.builder(
|
SliverLayoutBuilder(builder: (context, constrains) {
|
||||||
itemCount: playlists.length + 1,
|
return SliverGrid.builder(
|
||||||
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
|
itemCount: playlists.length + 1,
|
||||||
maxCrossAxisExtent: 200,
|
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
|
||||||
mainAxisExtent: DesktopTools.platform.isMobile ? 225 : 250,
|
maxCrossAxisExtent: 200,
|
||||||
crossAxisSpacing: 8,
|
mainAxisExtent: constrains.smAndDown ? 225 : 250,
|
||||||
mainAxisSpacing: 8,
|
crossAxisSpacing: 8,
|
||||||
),
|
mainAxisSpacing: 8,
|
||||||
itemBuilder: (context, index) {
|
),
|
||||||
if (index == playlists.length) {
|
itemBuilder: (context, index) {
|
||||||
if (!playlistsQuery.hasNextPage) {
|
if (index == playlists.length) {
|
||||||
return const SizedBox.shrink();
|
if (!playlistsQuery.hasNextPage) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Waypoint(
|
||||||
|
controller: controller,
|
||||||
|
isGrid: true,
|
||||||
|
onTouchEdge: playlistsQuery.fetchNext,
|
||||||
|
child: const ShimmerPlaybuttonCard(count: 1),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Waypoint(
|
return PlaylistCard(playlists[index]);
|
||||||
controller: controller,
|
},
|
||||||
isGrid: true,
|
);
|
||||||
onTouchEdge: playlistsQuery.fetchNext,
|
})
|
||||||
child: const ShimmerPlaybuttonCard(count: 1),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return PlaylistCard(playlists[index]);
|
|
||||||
},
|
|
||||||
)
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
28
lib/extensions/color.dart
Normal file
28
lib/extensions/color.dart
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
extension ColorAlterer on Color {
|
||||||
|
Color darken(double amount) {
|
||||||
|
assert(amount >= 0 && amount <= 1);
|
||||||
|
final hsl = HSLColor.fromColor(this);
|
||||||
|
final hslDark = hsl.withLightness((hsl.lightness - amount).clamp(0.0, 1.0));
|
||||||
|
return hslDark.toColor();
|
||||||
|
}
|
||||||
|
|
||||||
|
Color lighten(double amount) {
|
||||||
|
assert(amount >= 0 && amount <= 1);
|
||||||
|
final hsl = HSLColor.fromColor(this);
|
||||||
|
final hslLight =
|
||||||
|
hsl.withLightness((hsl.lightness + amount).clamp(0.0, 1.0));
|
||||||
|
return hslLight.toColor();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isLight() {
|
||||||
|
final luminance = computeLuminance();
|
||||||
|
return luminance > 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isDark() {
|
||||||
|
final luminance = computeLuminance();
|
||||||
|
return luminance <= 0.5;
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
// ignore: constant_identifier_names
|
// ignore: constant_identifier_names
|
||||||
@ -9,6 +10,29 @@ const Breakpoints = (
|
|||||||
xl: 1280.0,
|
xl: 1280.0,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
extension SliverBreakpoints on SliverConstraints {
|
||||||
|
bool get isXs => crossAxisExtent <= Breakpoints.xs;
|
||||||
|
bool get isSm =>
|
||||||
|
crossAxisExtent > Breakpoints.xs && crossAxisExtent <= Breakpoints.sm;
|
||||||
|
bool get isMd =>
|
||||||
|
crossAxisExtent > Breakpoints.sm && crossAxisExtent <= Breakpoints.md;
|
||||||
|
bool get isLg =>
|
||||||
|
crossAxisExtent > Breakpoints.md && crossAxisExtent <= Breakpoints.lg;
|
||||||
|
bool get isXl =>
|
||||||
|
crossAxisExtent > Breakpoints.lg && crossAxisExtent <= Breakpoints.xl;
|
||||||
|
bool get is2Xl => crossAxisExtent > Breakpoints.xl;
|
||||||
|
|
||||||
|
bool get smAndUp => isSm || isMd || isLg || isXl || is2Xl;
|
||||||
|
bool get mdAndUp => isMd || isLg || isXl || is2Xl;
|
||||||
|
bool get lgAndUp => isLg || isXl || is2Xl;
|
||||||
|
bool get xlAndUp => isXl || is2Xl;
|
||||||
|
|
||||||
|
bool get smAndDown => isXs || isSm;
|
||||||
|
bool get mdAndDown => isXs || isSm || isMd;
|
||||||
|
bool get lgAndDown => isXs || isSm || isMd || isLg;
|
||||||
|
bool get xlAndDown => isXs || isSm || isMd || isLg || isXl;
|
||||||
|
}
|
||||||
|
|
||||||
extension ContainerBreakpoints on BoxConstraints {
|
extension ContainerBreakpoints on BoxConstraints {
|
||||||
bool get isXs => biggest.width <= Breakpoints.xs;
|
bool get isXs => biggest.width <= Breakpoints.xs;
|
||||||
bool get isSm =>
|
bool get isSm =>
|
||||||
|
@ -1,32 +1,19 @@
|
|||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:fl_query_hooks/fl_query_hooks.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.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:spotify/spotify.dart';
|
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
|
||||||
import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart';
|
|
||||||
import 'package:spotube/components/shared/shimmers/shimmer_artist_profile.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/track_tile/track_tile.dart';
|
|
||||||
import 'package:spotube/components/shared/image/universal_image.dart';
|
|
||||||
import 'package:spotube/components/artist/artist_album_list.dart';
|
import 'package:spotube/components/artist/artist_album_list.dart';
|
||||||
import 'package:spotube/components/artist/artist_card.dart';
|
import 'package:spotube/components/shared/shimmers/shimmer_artist_profile.dart';
|
||||||
import 'package:spotube/extensions/constrains.dart';
|
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/hooks/utils/use_breakpoint_value.dart';
|
|
||||||
import 'package:spotube/models/logger.dart';
|
import 'package:spotube/models/logger.dart';
|
||||||
import 'package:spotube/provider/authentication_provider.dart';
|
import 'package:spotube/pages/artist/section/footer.dart';
|
||||||
import 'package:spotube/provider/blacklist_provider.dart';
|
import 'package:spotube/pages/artist/section/header.dart';
|
||||||
import 'package:spotube/provider/spotify_provider.dart';
|
import 'package:spotube/pages/artist/section/related_artists.dart';
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/pages/artist/section/top_tracks.dart';
|
||||||
import 'package:spotube/services/queries/queries.dart';
|
import 'package:spotube/services/queries/queries.dart';
|
||||||
|
|
||||||
import 'package:spotube/utils/primitive_utils.dart';
|
|
||||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
|
||||||
|
|
||||||
class ArtistPage extends HookConsumerWidget {
|
class ArtistPage extends HookConsumerWidget {
|
||||||
final String artistId;
|
final String artistId;
|
||||||
final logger = getLogger(ArtistPage);
|
final logger = getLogger(ArtistPage);
|
||||||
@ -34,427 +21,61 @@ class ArtistPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
SpotifyApi spotify = ref.watch(spotifyProvider);
|
final scrollController = useScrollController();
|
||||||
final parentScrollController = useScrollController();
|
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final scaffoldMessenger = ScaffoldMessenger.of(context);
|
|
||||||
final textTheme = theme.textTheme;
|
|
||||||
final chipTextVariant = useBreakpointValue(
|
|
||||||
xs: textTheme.bodySmall,
|
|
||||||
sm: textTheme.bodySmall,
|
|
||||||
md: textTheme.bodyMedium,
|
|
||||||
lg: textTheme.bodyLarge,
|
|
||||||
xl: textTheme.titleSmall,
|
|
||||||
xxl: textTheme.titleMedium,
|
|
||||||
);
|
|
||||||
|
|
||||||
final mediaQuery = MediaQuery.of(context);
|
final artistQuery = useQueries.artist.get(ref, artistId);
|
||||||
|
|
||||||
final avatarWidth = useBreakpointValue(
|
|
||||||
xs: mediaQuery.size.width * 0.50,
|
|
||||||
sm: mediaQuery.size.width * 0.50,
|
|
||||||
md: mediaQuery.size.width * 0.40,
|
|
||||||
lg: mediaQuery.size.width * 0.18,
|
|
||||||
xl: mediaQuery.size.width * 0.18,
|
|
||||||
xxl: mediaQuery.size.width * 0.18,
|
|
||||||
);
|
|
||||||
|
|
||||||
final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier);
|
|
||||||
final playlist = ref.watch(ProxyPlaylistNotifier.provider);
|
|
||||||
|
|
||||||
final auth = ref.watch(AuthenticationNotifier.provider);
|
|
||||||
|
|
||||||
final queryClient = useQueryClient();
|
|
||||||
|
|
||||||
return SafeArea(
|
return SafeArea(
|
||||||
bottom: false,
|
bottom: false,
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
appBar: const PageWindowTitleBar(
|
appBar: const PageWindowTitleBar(
|
||||||
leading: BackButton(),
|
leading: BackButton(),
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
),
|
),
|
||||||
body: HookBuilder(
|
extendBodyBehindAppBar: true,
|
||||||
builder: (context) {
|
body: Builder(builder: (context) {
|
||||||
final artistsQuery = useQueries.artist.get(ref, artistId);
|
if (artistQuery.isLoading || !artistQuery.hasData) {
|
||||||
|
const ShimmerArtistProfile();
|
||||||
if (artistsQuery.isLoading || !artistsQuery.hasData) {
|
} else if (artistQuery.hasError) {
|
||||||
return const ShimmerArtistProfile();
|
return Center(child: Text(artistQuery.error.toString()));
|
||||||
} else if (artistsQuery.hasError) {
|
}
|
||||||
return Center(
|
return CustomScrollView(
|
||||||
child: Text(artistsQuery.error.toString()),
|
controller: scrollController,
|
||||||
);
|
slivers: [
|
||||||
}
|
SliverToBoxAdapter(
|
||||||
|
|
||||||
final data = artistsQuery.data!;
|
|
||||||
|
|
||||||
final blacklist = ref.watch(BlackListNotifier.provider);
|
|
||||||
final isBlackListed = blacklist.contains(
|
|
||||||
BlacklistedElement.artist(artistId, data.name!),
|
|
||||||
);
|
|
||||||
|
|
||||||
return InterScrollbar(
|
|
||||||
controller: parentScrollController,
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
controller: parentScrollController,
|
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
child: Column(
|
bottom: false,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
child: ArtistPageHeader(artistId: artistId),
|
||||||
children: [
|
),
|
||||||
Wrap(
|
),
|
||||||
crossAxisAlignment: WrapCrossAlignment.center,
|
const SliverGap(50),
|
||||||
runAlignment: WrapAlignment.center,
|
ArtistPageTopTracks(artistId: artistId),
|
||||||
children: [
|
const SliverGap(50),
|
||||||
const SizedBox(width: 50),
|
SliverToBoxAdapter(child: ArtistAlbumList(artistId)),
|
||||||
Padding(
|
const SliverGap(20),
|
||||||
padding: const EdgeInsets.all(16),
|
SliverPadding(
|
||||||
child: CircleAvatar(
|
padding: const EdgeInsets.all(8.0),
|
||||||
radius: avatarWidth,
|
sliver: SliverToBoxAdapter(
|
||||||
backgroundImage: UniversalImage.imageProvider(
|
child: Text(
|
||||||
TypeConversionUtils.image_X_UrlString(
|
context.l10n.fans_also_like,
|
||||||
data.images,
|
style: theme.textTheme.headlineSmall,
|
||||||
placeholder: ImagePlaceholder.artist,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(20),
|
|
||||||
child: 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: Text(
|
|
||||||
data.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(
|
|
||||||
data.name!,
|
|
||||||
style: mediaQuery.smAndDown
|
|
||||||
? textTheme.headlineSmall
|
|
||||||
: textTheme.headlineMedium,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
context.l10n.followers(
|
|
||||||
PrimitiveUtils.toReadableNumber(
|
|
||||||
data.followers!.total!.toDouble(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
style: textTheme.bodyMedium?.copyWith(
|
|
||||||
fontWeight: mediaQuery.mdAndUp
|
|
||||||
? FontWeight.bold
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
if (auth != null)
|
|
||||||
HookBuilder(
|
|
||||||
builder: (context) {
|
|
||||||
final isFollowingQuery = useQueries
|
|
||||||
.artist
|
|
||||||
.doIFollow(ref, artistId);
|
|
||||||
|
|
||||||
final followUnfollow =
|
|
||||||
useCallback(() async {
|
|
||||||
try {
|
|
||||||
isFollowingQuery.data!
|
|
||||||
? await spotify.me.unfollow(
|
|
||||||
FollowingType.artist,
|
|
||||||
[artistId],
|
|
||||||
)
|
|
||||||
: await spotify.me.follow(
|
|
||||||
FollowingType.artist,
|
|
||||||
[artistId],
|
|
||||||
);
|
|
||||||
await isFollowingQuery.refresh();
|
|
||||||
|
|
||||||
queryClient
|
|
||||||
.refreshInfiniteQueryAllPages(
|
|
||||||
"user-following-artists");
|
|
||||||
} finally {
|
|
||||||
queryClient.refreshQuery(
|
|
||||||
"user-follows-artists-query/$artistId",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}, [isFollowingQuery]);
|
|
||||||
|
|
||||||
if (isFollowingQuery.isLoading ||
|
|
||||||
!isFollowingQuery.hasData) {
|
|
||||||
return const SizedBox(
|
|
||||||
height: 20,
|
|
||||||
width: 20,
|
|
||||||
child:
|
|
||||||
CircularProgressIndicator(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isFollowingQuery.data!) {
|
|
||||||
return OutlinedButton(
|
|
||||||
onPressed: followUnfollow,
|
|
||||||
child:
|
|
||||||
Text(context.l10n.following),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return FilledButton(
|
|
||||||
onPressed: followUnfollow,
|
|
||||||
child: Text(context.l10n.follow),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
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) {
|
|
||||||
ref
|
|
||||||
.read(BlackListNotifier
|
|
||||||
.provider.notifier)
|
|
||||||
.remove(
|
|
||||||
BlacklistedElement.artist(
|
|
||||||
data.id!, data.name!),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
ref
|
|
||||||
.read(BlackListNotifier
|
|
||||||
.provider.notifier)
|
|
||||||
.add(
|
|
||||||
BlacklistedElement.artist(
|
|
||||||
data.id!, data.name!),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(SpotubeIcons.share),
|
|
||||||
onPressed: () async {
|
|
||||||
if (data.externalUrls?.spotify !=
|
|
||||||
null) {
|
|
||||||
await Clipboard.setData(
|
|
||||||
ClipboardData(
|
|
||||||
text: data.externalUrls!.spotify!,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!context.mounted) return;
|
|
||||||
|
|
||||||
scaffoldMessenger.showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
width: 300,
|
|
||||||
behavior: SnackBarBehavior.floating,
|
|
||||||
content: Text(
|
|
||||||
context.l10n.artist_url_copied,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 50),
|
|
||||||
HookBuilder(
|
|
||||||
builder: (context) {
|
|
||||||
final topTracksQuery = useQueries.artist.topTracksOf(
|
|
||||||
ref,
|
|
||||||
artistId,
|
|
||||||
);
|
|
||||||
|
|
||||||
final isPlaylistPlaying = playlist.containsTracks(
|
|
||||||
topTracksQuery.data ?? <Track>[],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (topTracksQuery.isLoading ||
|
|
||||||
!topTracksQuery.hasData) {
|
|
||||||
return const CircularProgressIndicator();
|
|
||||||
} else if (topTracksQuery.hasError) {
|
|
||||||
return Center(
|
|
||||||
child: Text(topTracksQuery.error.toString()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final topTracks = topTracksQuery.data!;
|
|
||||||
|
|
||||||
void playPlaylist(List<Track> tracks,
|
|
||||||
{Track? currentTrack}) async {
|
|
||||||
currentTrack ??= tracks.first;
|
|
||||||
if (!isPlaylistPlaying) {
|
|
||||||
playlistNotifier.load(
|
|
||||||
tracks,
|
|
||||||
initialIndex: tracks.indexWhere(
|
|
||||||
(s) => s.id == currentTrack?.id),
|
|
||||||
autoPlay: true,
|
|
||||||
);
|
|
||||||
} else if (isPlaylistPlaying &&
|
|
||||||
currentTrack.id != null &&
|
|
||||||
currentTrack.id != playlist.activeTrack?.id) {
|
|
||||||
await playlistNotifier.jumpToTrack(currentTrack);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(8.0),
|
|
||||||
child: Text(
|
|
||||||
context.l10n.top_tracks,
|
|
||||||
style: theme.textTheme.headlineSmall,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (!isPlaylistPlaying)
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(
|
|
||||||
SpotubeIcons.queueAdd,
|
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
playlistNotifier
|
|
||||||
.addTracks(topTracks.toList());
|
|
||||||
scaffoldMessenger.showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
width: 300,
|
|
||||||
behavior: SnackBarBehavior.floating,
|
|
||||||
content: Text(
|
|
||||||
context.l10n.added_to_queue(
|
|
||||||
topTracks.length,
|
|
||||||
),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(width: 5),
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(
|
|
||||||
isPlaylistPlaying
|
|
||||||
? SpotubeIcons.stop
|
|
||||||
: SpotubeIcons.play,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
style: IconButton.styleFrom(
|
|
||||||
backgroundColor:
|
|
||||||
theme.colorScheme.primary,
|
|
||||||
),
|
|
||||||
onPressed: () =>
|
|
||||||
playPlaylist(topTracks.toList()),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
...topTracks.mapIndexed((i, track) {
|
|
||||||
return TrackTile(
|
|
||||||
index: i,
|
|
||||||
track: track,
|
|
||||||
onTap: () async {
|
|
||||||
playPlaylist(
|
|
||||||
topTracks.toList(),
|
|
||||||
currentTrack: track,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(height: 50),
|
|
||||||
ArtistAlbumList(artistId),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(8.0),
|
|
||||||
child: Text(
|
|
||||||
context.l10n.fans_also_like,
|
|
||||||
style: theme.textTheme.headlineSmall,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
HookBuilder(
|
|
||||||
builder: (context) {
|
|
||||||
final relatedArtists =
|
|
||||||
useQueries.artist.relatedArtistsOf(
|
|
||||||
ref,
|
|
||||||
artistId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (relatedArtists.isLoading ||
|
|
||||||
!relatedArtists.hasData) {
|
|
||||||
return const CircularProgressIndicator();
|
|
||||||
} else if (relatedArtists.hasError) {
|
|
||||||
return Center(
|
|
||||||
child: Text(relatedArtists.error.toString()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Center(
|
|
||||||
child: Wrap(
|
|
||||||
spacing: 20,
|
|
||||||
runSpacing: 20,
|
|
||||||
children: relatedArtists.data!
|
|
||||||
.map((artist) => ArtistCard(artist))
|
|
||||||
.toList(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
SliverSafeArea(
|
||||||
},
|
sliver: ArtistPageRelatedArtists(artistId: artistId),
|
||||||
),
|
),
|
||||||
|
if (artistQuery.data != null)
|
||||||
|
SliverSafeArea(
|
||||||
|
top: false,
|
||||||
|
sliver: SliverToBoxAdapter(
|
||||||
|
child: ArtistPageFooter(artist: artistQuery.data!),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
93
lib/pages/artist/section/footer.dart
Normal file
93
lib/pages/artist/section/footer.dart
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import 'package:flutter/gestures.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:spotify/spotify.dart';
|
||||||
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
|
import 'package:spotube/components/shared/image/universal_image.dart';
|
||||||
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
|
import 'package:spotube/services/queries/queries.dart';
|
||||||
|
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||||
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
|
class ArtistPageFooter extends HookConsumerWidget {
|
||||||
|
final Artist artist;
|
||||||
|
const ArtistPageFooter({Key? key, required this.artist}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, ref) {
|
||||||
|
final ThemeData(:textTheme) = Theme.of(context);
|
||||||
|
final mediaQuery = MediaQuery.of(context);
|
||||||
|
|
||||||
|
final artistImage = TypeConversionUtils.image_X_UrlString(
|
||||||
|
artist.images,
|
||||||
|
placeholder: ImagePlaceholder.artist,
|
||||||
|
);
|
||||||
|
final summary = useQueries.artist.wikipediaSummary(artist);
|
||||||
|
if (summary.hasError || !summary.hasData) return const SizedBox.shrink();
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.all(16),
|
||||||
|
padding: mediaQuery.smAndDown
|
||||||
|
? const EdgeInsets.all(20)
|
||||||
|
: const EdgeInsets.all(30),
|
||||||
|
constraints: const BoxConstraints(minHeight: 300),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
image: DecorationImage(
|
||||||
|
colorFilter: ColorFilter.mode(
|
||||||
|
Colors.black.withOpacity(0.5),
|
||||||
|
BlendMode.darken,
|
||||||
|
),
|
||||||
|
image: UniversalImage.imageProvider(
|
||||||
|
summary.data!.originalimage?.source_ ?? artistImage,
|
||||||
|
height: summary.data!.originalimage?.height.toDouble(),
|
||||||
|
width: summary.data!.originalimage?.width.toDouble(),
|
||||||
|
),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: RichText(
|
||||||
|
text: TextSpan(
|
||||||
|
style: textTheme.bodyLarge?.copyWith(
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
children: [
|
||||||
|
// icon
|
||||||
|
const WidgetSpan(
|
||||||
|
child: Icon(
|
||||||
|
SpotubeIcons.wikipedia,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 30,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
text: " Wikipedia",
|
||||||
|
style: textTheme.titleLarge?.copyWith(
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const TextSpan(text: '\n\n'),
|
||||||
|
TextSpan(
|
||||||
|
text: summary.data!.extract,
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
text: '\n...read more at wikipedia',
|
||||||
|
style: textTheme.bodyLarge?.copyWith(
|
||||||
|
color: Colors.lightBlue[300],
|
||||||
|
decoration: TextDecoration.underline,
|
||||||
|
decorationColor: Colors.lightBlue[300],
|
||||||
|
),
|
||||||
|
recognizer: TapGestureRecognizer()
|
||||||
|
..onTap = () async {
|
||||||
|
await launchUrlString(
|
||||||
|
"http://en.wikipedia.org/wiki?curid=${summary.data?.pageid}",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
257
lib/pages/artist/section/header.dart
Normal file
257
lib/pages/artist/section/header.dart
Normal file
@ -0,0 +1,257 @@
|
|||||||
|
import 'package:fl_query_hooks/fl_query_hooks.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:spotify/spotify.dart';
|
||||||
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
|
import 'package:spotube/components/shared/image/universal_image.dart';
|
||||||
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
|
import 'package:spotube/extensions/context.dart';
|
||||||
|
import 'package:spotube/hooks/utils/use_breakpoint_value.dart';
|
||||||
|
import 'package:spotube/provider/authentication_provider.dart';
|
||||||
|
import 'package:spotube/provider/blacklist_provider.dart';
|
||||||
|
import 'package:spotube/provider/spotify_provider.dart';
|
||||||
|
import 'package:spotube/services/queries/queries.dart';
|
||||||
|
import 'package:spotube/utils/primitive_utils.dart';
|
||||||
|
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||||
|
|
||||||
|
class ArtistPageHeader extends HookConsumerWidget {
|
||||||
|
final String artistId;
|
||||||
|
const ArtistPageHeader({Key? key, required this.artistId}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, ref) {
|
||||||
|
final queryClient = useQueryClient();
|
||||||
|
final artistQuery = useQueries.artist.get(ref, artistId);
|
||||||
|
final artist = artistQuery.data;
|
||||||
|
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (artist == null) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
final spotify = ref.read(spotifyProvider);
|
||||||
|
final auth = ref.watch(AuthenticationNotifier.provider);
|
||||||
|
final blacklist = ref.watch(BlackListNotifier.provider);
|
||||||
|
final isBlackListed = blacklist.contains(
|
||||||
|
BlacklistedElement.artist(artistId, artist.name!),
|
||||||
|
);
|
||||||
|
|
||||||
|
final image = TypeConversionUtils.image_X_UrlString(
|
||||||
|
artist.images,
|
||||||
|
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: 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),
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (auth != null)
|
||||||
|
HookBuilder(
|
||||||
|
builder: (context) {
|
||||||
|
final isFollowingQuery =
|
||||||
|
useQueries.artist.doIFollow(ref, artistId);
|
||||||
|
|
||||||
|
final followUnfollow = useCallback(() async {
|
||||||
|
try {
|
||||||
|
isFollowingQuery.data!
|
||||||
|
? await spotify.me.unfollow(
|
||||||
|
FollowingType.artist,
|
||||||
|
[artistId],
|
||||||
|
)
|
||||||
|
: await spotify.me.follow(
|
||||||
|
FollowingType.artist,
|
||||||
|
[artistId],
|
||||||
|
);
|
||||||
|
await isFollowingQuery.refresh();
|
||||||
|
|
||||||
|
queryClient.refreshInfiniteQueryAllPages(
|
||||||
|
"user-following-artists");
|
||||||
|
} finally {
|
||||||
|
queryClient.refreshQuery(
|
||||||
|
"user-follows-artists-query/$artistId",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [isFollowingQuery]);
|
||||||
|
|
||||||
|
if (isFollowingQuery.isLoading ||
|
||||||
|
!isFollowingQuery.hasData) {
|
||||||
|
return const SizedBox(
|
||||||
|
height: 20,
|
||||||
|
width: 20,
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFollowingQuery.data!) {
|
||||||
|
return OutlinedButton(
|
||||||
|
onPressed: followUnfollow,
|
||||||
|
child: Text(context.l10n.following),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return FilledButton(
|
||||||
|
onPressed: followUnfollow,
|
||||||
|
child: Text(context.l10n.follow),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
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) {
|
||||||
|
ref
|
||||||
|
.read(BlackListNotifier.provider.notifier)
|
||||||
|
.remove(
|
||||||
|
BlacklistedElement.artist(
|
||||||
|
artist.id!, artist.name!),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
ref.read(BlackListNotifier.provider.notifier).add(
|
||||||
|
BlacklistedElement.artist(
|
||||||
|
artist.id!, artist.name!),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
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;
|
||||||
|
|
||||||
|
scaffoldMessenger.showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
width: 300,
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
content: Text(
|
||||||
|
context.l10n.artist_url_copied,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
49
lib/pages/artist/section/related_artists.dart
Normal file
49
lib/pages/artist/section/related_artists.dart
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:spotube/components/artist/artist_card.dart';
|
||||||
|
import 'package:spotube/services/queries/queries.dart';
|
||||||
|
|
||||||
|
class ArtistPageRelatedArtists extends HookConsumerWidget {
|
||||||
|
final String artistId;
|
||||||
|
const ArtistPageRelatedArtists({
|
||||||
|
Key? key,
|
||||||
|
required this.artistId,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, ref) {
|
||||||
|
final relatedArtists = useQueries.artist.relatedArtistsOf(
|
||||||
|
ref,
|
||||||
|
artistId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (relatedArtists.isLoading || !relatedArtists.hasData) {
|
||||||
|
return const SliverToBoxAdapter(
|
||||||
|
child: Center(child: CircularProgressIndicator()));
|
||||||
|
} else if (relatedArtists.hasError) {
|
||||||
|
return SliverToBoxAdapter(
|
||||||
|
child: Center(
|
||||||
|
child: Text(relatedArtists.error.toString()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SliverPadding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||||
|
sliver: SliverGrid.builder(
|
||||||
|
itemCount: relatedArtists.data!.length,
|
||||||
|
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||||
|
maxCrossAxisExtent: 200,
|
||||||
|
mainAxisExtent: 250,
|
||||||
|
mainAxisSpacing: 10,
|
||||||
|
crossAxisSpacing: 10,
|
||||||
|
childAspectRatio: 0.8,
|
||||||
|
),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final artist = relatedArtists.data!.elementAt(index);
|
||||||
|
return ArtistCard(artist);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
126
lib/pages/artist/section/top_tracks.dart
Normal file
126
lib/pages/artist/section/top_tracks.dart
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:spotify/spotify.dart';
|
||||||
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
|
import 'package:spotube/components/shared/track_tile/track_tile.dart';
|
||||||
|
import 'package:spotube/extensions/context.dart';
|
||||||
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
|
import 'package:spotube/services/queries/queries.dart';
|
||||||
|
|
||||||
|
class ArtistPageTopTracks extends HookConsumerWidget {
|
||||||
|
final String artistId;
|
||||||
|
const ArtistPageTopTracks({Key? key, required this.artistId})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, ref) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final scaffoldMessenger = ScaffoldMessenger.of(context);
|
||||||
|
|
||||||
|
final playlist = ref.watch(ProxyPlaylistNotifier.provider);
|
||||||
|
final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier);
|
||||||
|
final topTracksQuery = useQueries.artist.topTracksOf(
|
||||||
|
ref,
|
||||||
|
artistId,
|
||||||
|
);
|
||||||
|
|
||||||
|
final isPlaylistPlaying = playlist.containsTracks(
|
||||||
|
topTracksQuery.data ?? <Track>[],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (topTracksQuery.isLoading || !topTracksQuery.hasData) {
|
||||||
|
return const SliverToBoxAdapter(
|
||||||
|
child: Center(child: CircularProgressIndicator()),
|
||||||
|
);
|
||||||
|
} else if (topTracksQuery.hasError) {
|
||||||
|
return SliverToBoxAdapter(
|
||||||
|
child: Center(
|
||||||
|
child: Text(topTracksQuery.error.toString()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final topTracks = topTracksQuery.data!;
|
||||||
|
|
||||||
|
void playPlaylist(List<Track> tracks, {Track? currentTrack}) async {
|
||||||
|
currentTrack ??= tracks.first;
|
||||||
|
if (!isPlaylistPlaying) {
|
||||||
|
playlistNotifier.load(
|
||||||
|
tracks,
|
||||||
|
initialIndex: tracks.indexWhere((s) => s.id == currentTrack?.id),
|
||||||
|
autoPlay: true,
|
||||||
|
);
|
||||||
|
} else if (isPlaylistPlaying &&
|
||||||
|
currentTrack.id != null &&
|
||||||
|
currentTrack.id != playlist.activeTrack?.id) {
|
||||||
|
await playlistNotifier.jumpToTrack(currentTrack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return SliverMainAxisGroup(
|
||||||
|
slivers: [
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Text(
|
||||||
|
context.l10n.top_tracks,
|
||||||
|
style: theme.textTheme.headlineSmall,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (!isPlaylistPlaying)
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(
|
||||||
|
SpotubeIcons.queueAdd,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
playlistNotifier.addTracks(topTracks.toList());
|
||||||
|
scaffoldMessenger.showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
width: 300,
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
content: Text(
|
||||||
|
context.l10n.added_to_queue(
|
||||||
|
topTracks.length,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(width: 5),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
isPlaylistPlaying ? SpotubeIcons.stop : SpotubeIcons.play,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
style: IconButton.styleFrom(
|
||||||
|
backgroundColor: theme.colorScheme.primary,
|
||||||
|
),
|
||||||
|
onPressed: () => playPlaylist(topTracks.toList()),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SliverList.builder(
|
||||||
|
itemCount: topTracks.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final track = topTracks.elementAt(index);
|
||||||
|
return TrackTile(
|
||||||
|
index: index,
|
||||||
|
track: track,
|
||||||
|
onTap: () async {
|
||||||
|
playPlaylist(
|
||||||
|
topTracks.toList(),
|
||||||
|
currentTrack: track,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,12 @@
|
|||||||
import 'package:fl_query/fl_query.dart';
|
import 'package:fl_query/fl_query.dart';
|
||||||
|
import 'package:fl_query_hooks/fl_query_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/hooks/spotify/use_spotify_infinite_query.dart';
|
import 'package:spotube/hooks/spotify/use_spotify_infinite_query.dart';
|
||||||
import 'package:spotube/hooks/spotify/use_spotify_query.dart';
|
import 'package:spotube/hooks/spotify/use_spotify_query.dart';
|
||||||
|
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||||
|
import 'package:spotube/services/wikipedia/wikipedia.dart';
|
||||||
|
import 'package:wikipedia_api/wikipedia_api.dart';
|
||||||
|
|
||||||
class ArtistQueries {
|
class ArtistQueries {
|
||||||
const ArtistQueries();
|
const ArtistQueries();
|
||||||
@ -72,11 +76,11 @@ class ArtistQueries {
|
|||||||
return useSpotifyQuery<bool, dynamic>(
|
return useSpotifyQuery<bool, dynamic>(
|
||||||
"user-follows-artists-query/$artist",
|
"user-follows-artists-query/$artist",
|
||||||
(spotify) async {
|
(spotify) async {
|
||||||
final result = await spotify.me.isFollowing(
|
final result = await spotify.me.checkFollowing(
|
||||||
FollowingType.artist,
|
FollowingType.artist,
|
||||||
[artist],
|
[artist],
|
||||||
);
|
);
|
||||||
return result.first;
|
return result[artist];
|
||||||
},
|
},
|
||||||
ref: ref,
|
ref: ref,
|
||||||
);
|
);
|
||||||
@ -86,10 +90,12 @@ class ArtistQueries {
|
|||||||
WidgetRef ref,
|
WidgetRef ref,
|
||||||
String artist,
|
String artist,
|
||||||
) {
|
) {
|
||||||
|
final preferences = ref.watch(userPreferencesProvider);
|
||||||
return useSpotifyQuery<Iterable<Track>, dynamic>(
|
return useSpotifyQuery<Iterable<Track>, dynamic>(
|
||||||
"artist-top-track-query/$artist",
|
"artist-top-track-query/$artist",
|
||||||
(spotify) {
|
(spotify) {
|
||||||
return spotify.artists.getTopTracks(artist, "US");
|
return spotify.artists
|
||||||
|
.topTracks(artist, preferences.recommendationMarket);
|
||||||
},
|
},
|
||||||
ref: ref,
|
ref: ref,
|
||||||
);
|
);
|
||||||
@ -122,9 +128,24 @@ class ArtistQueries {
|
|||||||
return useSpotifyQuery<Iterable<Artist>, dynamic>(
|
return useSpotifyQuery<Iterable<Artist>, dynamic>(
|
||||||
"artist-related-artist-query/$artist",
|
"artist-related-artist-query/$artist",
|
||||||
(spotify) {
|
(spotify) {
|
||||||
return spotify.artists.getRelatedArtists(artist);
|
return spotify.artists.relatedArtists(artist);
|
||||||
},
|
},
|
||||||
ref: ref,
|
ref: ref,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Query<Summary?, dynamic> wikipediaSummary(ArtistSimple artist) {
|
||||||
|
return useQuery<Summary?, dynamic>(
|
||||||
|
"artist-wikipedia-query/${artist.id}",
|
||||||
|
() async {
|
||||||
|
final query = artist.name!.replaceAll(" ", "_");
|
||||||
|
final res = await wikipedia.pageContent.pageSummaryTitleGet(query);
|
||||||
|
if (res?.type != "standard") {
|
||||||
|
return await wikipedia.pageContent
|
||||||
|
.pageSummaryTitleGet("${query}_(singer)");
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
3
lib/services/wikipedia/wikipedia.dart
Normal file
3
lib/services/wikipedia/wikipedia.dart
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import 'package:wikipedia_api/wikipedia_api.dart';
|
||||||
|
|
||||||
|
final wikipedia = WikipediaApi();
|
11
pubspec.lock
11
pubspec.lock
@ -2222,6 +2222,15 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.2"
|
version: "3.0.2"
|
||||||
|
wikipedia_api:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
path: "."
|
||||||
|
ref: HEAD
|
||||||
|
resolved-ref: cb7590a3d76b25f16ad3f7147ae6603350777a00
|
||||||
|
url: "https://github.com/KRTirtho/wikipedia_api.git"
|
||||||
|
source: git
|
||||||
|
version: "0.1.0"
|
||||||
win32:
|
win32:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -2288,5 +2297,5 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.2"
|
version: "2.0.2"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.2.0-194.0.dev <4.0.0"
|
dart: ">=3.2.0 <4.0.0"
|
||||||
flutter: ">=3.13.0"
|
flutter: ">=3.13.0"
|
||||||
|
@ -119,6 +119,9 @@ dependencies:
|
|||||||
git:
|
git:
|
||||||
url: https://github.com/Tommypop2/dart_discord_rpc.git
|
url: https://github.com/Tommypop2/dart_discord_rpc.git
|
||||||
html_unescape: ^2.0.0
|
html_unescape: ^2.0.0
|
||||||
|
wikipedia_api:
|
||||||
|
git:
|
||||||
|
url: https://github.com/KRTirtho/wikipedia_api.git
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
build_runner: ^2.3.2
|
build_runner: ^2.3.2
|
||||||
|
Loading…
Reference in New Issue
Block a user