refactor: horizontal playbutton layout to use ListView and breakdown search page into sections

This commit is contained in:
Kingkor Roy Tirtho 2023-11-08 17:07:20 +06:00
parent 487c2ed6bd
commit 6b8ae88db4
12 changed files with 374 additions and 511 deletions

View File

@ -4,7 +4,6 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/components/shared/playbutton_card.dart';
import 'package:spotube/hooks/use_breakpoint_value.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
import 'package:spotube/provider/spotify_provider.dart';
import 'package:spotube/services/audio_player/audio_player.dart';
@ -34,13 +33,6 @@ class AlbumCard extends HookConsumerWidget {
[playlist, album.id],
);
final marginH = useBreakpointValue<int>(
xs: 10,
sm: 10,
md: 15,
others: 20,
);
final updating = useState(false);
final spotify = ref.watch(spotifyProvider);
@ -49,7 +41,7 @@ class AlbumCard extends HookConsumerWidget {
album.images,
placeholder: ImagePlaceholder.collection,
),
margin: EdgeInsets.symmetric(horizontal: marginH.toDouble()),
margin: const EdgeInsets.symmetric(horizontal: 10),
isPlaying: isPlaylistPlaying,
isLoading: (isPlaylistPlaying && playlist.isFetching == true) ||
updating.value,

View File

@ -1,11 +1,9 @@
import 'package:flutter/gestures.dart';
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/album/album_card.dart';
import 'package:spotube/components/shared/shimmers/shimmer_playbutton_card.dart';
import 'package:spotube/components/shared/waypoint.dart';
import 'package:spotube/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/models/logger.dart';
import 'package:spotube/services/queries/queries.dart';
@ -20,7 +18,6 @@ class ArtistAlbumList extends HookConsumerWidget {
@override
Widget build(BuildContext context, ref) {
final scrollController = useScrollController();
final albumsQuery = useQueries.artist.albumsOf(ref, artistId);
final albums = useMemoized(() {
@ -29,40 +26,16 @@ class ArtistAlbumList extends HookConsumerWidget {
.toList();
}, [albumsQuery.pages]);
final hasNextPage = albumsQuery.pages.isEmpty
? false
: (albumsQuery.pages.last.items?.length ?? 0) == 5;
final theme = Theme.of(context);
return Column(
children: [
ScrollConfiguration(
behavior: ScrollConfiguration.of(context).copyWith(
dragDevices: {
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
},
return HorizontalPlaybuttonCardView<Album>(
hasNextPage: albumsQuery.hasNextPage,
items: albums,
onFetchMore: albumsQuery.fetchNext,
title: Text(
context.l10n.albums,
style: theme.textTheme.headlineSmall,
),
child: Scrollbar(
interactive: false,
controller: scrollController,
child: Waypoint(
controller: scrollController,
onTouchEdge: albumsQuery.fetchNext,
child: SingleChildScrollView(
controller: scrollController,
scrollDirection: Axis.horizontal,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
...albums.map((album) => AlbumCard(album)),
if (hasNextPage) const ShimmerPlaybuttonCard(count: 1),
],
),
),
),
),
),
],
);
}
}

View File

@ -1,15 +1,10 @@
import 'dart:ui';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart' hide Page;
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
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/extensions/constrains.dart';
import 'package:spotube/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
import 'package:spotube/models/logger.dart';
import 'package:spotube/services/queries/queries.dart';
@ -24,7 +19,6 @@ class CategoryCard extends HookConsumerWidget {
@override
Widget build(BuildContext context, ref) {
final scrollController = useScrollController();
final playlistQuery = useQueries.category.playlistsOf(
ref,
category.id!,
@ -33,7 +27,8 @@ class CategoryCard extends HookConsumerWidget {
final playlists = useMemoized(
() => playlistQuery.pages.expand(
(page) {
return page.items?.where((i) => i != null) ?? const Iterable.empty();
return page.items?.whereNotNull() ??
const Iterable<PlaylistSimple>.empty();
},
).toList(),
[playlistQuery.pages],
@ -45,51 +40,11 @@ class CategoryCard extends HookConsumerWidget {
return const SizedBox.shrink();
}
final mediaQuery = MediaQuery.of(context);
return Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
category.name!,
style: Theme.of(context).textTheme.titleMedium,
),
SizedBox(
height: mediaQuery.smAndDown ? 226 : 266,
child: ScrollConfiguration(
behavior: ScrollConfiguration.of(context).copyWith(
dragDevices: {
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
},
),
child: ListView.builder(
controller: scrollController,
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(vertical: 8.0),
itemCount: playlists.length + 1,
itemBuilder: (context, index) {
if (index == playlists.length) {
if (!playlistQuery.hasNextPage) {
return const SizedBox.shrink();
}
return Waypoint(
controller: scrollController,
onTouchEdge: playlistQuery.fetchNext,
isGrid: true,
child: const ShimmerPlaybuttonCard(),
);
}
final playlist = playlists[index];
return PlaylistCard(playlist);
}),
),
),
],
),
return HorizontalPlaybuttonCardView<PlaylistSimple>(
title: Text(category.name!),
hasNextPage: playlistQuery.hasNextPage,
items: playlists,
onFetchMore: playlistQuery.fetchNext,
);
}
}

View File

@ -0,0 +1,96 @@
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/components/album/album_card.dart';
import 'package:spotube/components/artist/artist_card.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/hooks/use_breakpoint_value.dart';
class HorizontalPlaybuttonCardView<T> extends HookWidget {
final Widget title;
final List<T> items;
final VoidCallback onFetchMore;
final bool hasNextPage;
const HorizontalPlaybuttonCardView({
required this.title,
required this.items,
required this.hasNextPage,
required this.onFetchMore,
Key? key,
}) : assert(
items is List<PlaylistSimple> ||
items is List<Album> ||
items is List<Artist>,
),
super(key: key);
@override
Widget build(BuildContext context) {
final ThemeData(:textTheme) = Theme.of(context);
final scrollController = useScrollController();
final height = useBreakpointValue<double>(
xs: 226,
sm: 226,
md: 236,
others: 266,
);
return Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DefaultTextStyle(
style: textTheme.titleMedium!,
child: title,
),
SizedBox(
height: height,
child: ScrollConfiguration(
behavior: ScrollConfiguration.of(context).copyWith(
dragDevices: {
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
},
),
child: ListView.builder(
controller: scrollController,
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(vertical: 8.0),
itemCount: items.length + 1,
itemBuilder: (context, index) {
if (index == items.length) {
if (!hasNextPage) {
return const SizedBox.shrink();
}
return Waypoint(
controller: scrollController,
onTouchEdge: onFetchMore,
isGrid: true,
child: const ShimmerPlaybuttonCard(),
);
}
final item = items[index];
return switch (item.runtimeType) {
PlaylistSimple => PlaylistCard(item as PlaylistSimple),
Album => AlbumCard(item as Album),
Artist => Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: ArtistCard(item as Artist),
),
_ => const SizedBox.shrink(),
};
}),
),
),
],
),
);
}
}

View File

@ -410,13 +410,6 @@ class ArtistPage extends HookConsumerWidget {
},
),
const SizedBox(height: 50),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
context.l10n.albums,
style: theme.textTheme.headlineSmall,
),
),
ArtistAlbumList(artistId),
const SizedBox(height: 20),
Padding(

View File

@ -85,15 +85,19 @@ class GenrePage extends HookConsumerWidget {
controller: scrollController,
itemCount: categories.length,
itemBuilder: (context, index) {
return AnimatedCrossFade(
crossFadeState: searchController.text.isEmpty &&
return AnimatedSwitcher(
transitionBuilder: (child, animation) {
return FadeTransition(
opacity: animation,
child: child,
);
},
duration: const Duration(milliseconds: 300),
child: searchController.text.isEmpty &&
index == categories.length - 1 &&
categoriesQuery.hasNextPage
? CrossFadeState.showFirst
: CrossFadeState.showSecond,
duration: const Duration(milliseconds: 300),
firstChild: const ShimmerCategories(),
secondChild: CategoryCard(categories[index]),
? const ShimmerCategories()
: CategoryCard(categories[index]),
);
},
),

View File

@ -1,111 +1,16 @@
import 'package:flutter/gestures.dart';
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/album/album_card.dart';
import 'package:spotube/components/playlist/playlist_card.dart';
import 'package:spotube/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart';
import 'package:spotube/components/shared/shimmers/shimmer_categories.dart';
import 'package:spotube/components/shared/shimmers/shimmer_playbutton_card.dart';
import 'package:spotube/components/shared/waypoint.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/models/logger.dart';
import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/services/queries/queries.dart';
import 'package:spotube/utils/type_conversion_utils.dart';
class PersonalizedItemCard extends HookWidget {
final Iterable<PlaylistSimple>? playlists;
final Iterable<AlbumSimple>? albums;
final String title;
final bool hasNextPage;
final void Function() onFetchMore;
PersonalizedItemCard({
this.playlists,
this.albums,
required this.title,
required this.hasNextPage,
required this.onFetchMore,
Key? key,
}) : assert(playlists == null || albums == null),
super(key: key);
final logger = getLogger(PersonalizedItemCard);
@override
Widget build(BuildContext context) {
final scrollController = useScrollController();
final mediaQuery = MediaQuery.of(context);
return Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
title,
style: Theme.of(context).textTheme.titleLarge,
),
),
SizedBox(
height: mediaQuery.smAndDown ? 226 : 266,
child: ScrollConfiguration(
behavior: ScrollConfiguration.of(context).copyWith(
dragDevices: {
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
},
),
child: Scrollbar(
controller: scrollController,
interactive: false,
child: ListView.builder(
itemCount: (playlists?.length ?? albums?.length)! + 1,
padding: const EdgeInsets.symmetric(vertical: 8.0),
scrollDirection: Axis.horizontal,
itemBuilder: (context, index) {
if (index == (playlists?.length ?? albums?.length)!) {
if (!hasNextPage) return const SizedBox.shrink();
return Waypoint(
controller: scrollController,
onTouchEdge: onFetchMore,
isGrid: true,
child: const ShimmerPlaybuttonCard(count: 1),
);
}
final item = playlists == null
? albums!.elementAt(index)
: playlists!.elementAt(index);
if (playlists == null) {
return AlbumCard(
TypeConversionUtils.simpleAlbum_X_Album(
item as AlbumSimple,
),
);
}
return PlaylistCard(item as PlaylistSimple);
},
),
),
),
),
],
),
);
}
}
class PersonalizedPage extends HookConsumerWidget {
const PersonalizedPage({Key? key}) : super(key: key);
@ -136,7 +41,9 @@ class PersonalizedPage extends HookConsumerWidget {
return album.artists
?.any((artist) => userArtists.contains(artist.id!)) ==
true;
}),
})
.map((album) => TypeConversionUtils.simpleAlbum_X_Album(album))
.toList(),
[newReleases.pages],
);
@ -149,18 +56,18 @@ class PersonalizedPage extends HookConsumerWidget {
!featuredPlaylistsQuery.isLoadingNextPage)
const ShimmerCategories()
else
PersonalizedItemCard(
playlists: playlists,
title: context.l10n.featured,
HorizontalPlaybuttonCardView<PlaylistSimple>(
items: playlists.toList(),
title: Text(context.l10n.featured),
hasNextPage: featuredPlaylistsQuery.hasNextPage,
onFetchMore: featuredPlaylistsQuery.fetchNext,
),
if (auth != null &&
newReleases.hasPageData &&
userArtistsQuery.hasData)
PersonalizedItemCard(
albums: albums,
title: context.l10n.new_releases,
HorizontalPlaybuttonCardView<Album>(
items: albums,
title: Text(context.l10n.new_releases),
hasNextPage: newReleases.hasNextPage,
onFetchMore: newReleases.fetchNext,
),
@ -172,9 +79,9 @@ class PersonalizedPage extends HookConsumerWidget {
.cast<PlaylistSimple>() ??
<PlaylistSimple>[];
if (playlists.isEmpty) return const SizedBox.shrink();
return PersonalizedItemCard(
playlists: playlists,
title: item["name"] ?? "",
return HorizontalPlaybuttonCardView<PlaylistSimple>(
items: playlists,
title: Text(item["name"] ?? ""),
hasNextPage: false,
onFetchMore: () {},
);

View File

@ -1,30 +1,24 @@
import 'dart:async';
import 'package:flutter/gestures.dart';
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/collections/spotube_icons.dart';
import 'package:spotube/components/album/album_card.dart';
import 'package:spotube/components/shared/dialogs/prompt_dialog.dart';
import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart';
import 'package:spotube/components/shared/shimmers/shimmer_playbutton_card.dart';
import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart';
import 'package:spotube/components/shared/page_window_title_bar.dart';
import 'package:spotube/components/shared/track_table/track_tile.dart';
import 'package:spotube/components/shared/waypoint.dart';
import 'package:spotube/components/artist/artist_card.dart';
import 'package:spotube/components/playlist/playlist_card.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/pages/search/sections/albums.dart';
import 'package:spotube/pages/search/sections/artists.dart';
import 'package:spotube/pages/search/sections/playlists.dart';
import 'package:spotube/pages/search/sections/tracks.dart';
import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
import 'package:spotube/services/queries/queries.dart';
import 'package:spotube/utils/platform.dart';
import 'package:spotube/utils/type_conversion_utils.dart';
import 'package:collection/collection.dart';
final searchTermStateProvider = StateProvider<String>((ref) => "");
@ -38,9 +32,6 @@ class SearchPage extends HookConsumerWidget {
ref.watch(AuthenticationNotifier.provider);
final authenticationNotifier =
ref.watch(AuthenticationNotifier.provider.notifier);
final albumController = useScrollController();
final playlistController = useScrollController();
final artistController = useScrollController();
final mediaQuery = MediaQuery.of(context);
final searchTerm = ref.watch(searchTermStateProvider);
@ -80,34 +71,7 @@ class SearchPage extends HookConsumerWidget {
searchTerm.isNotEmpty;
final resultWidget = HookBuilder(
builder: (context) {
final playlist = ref.watch(ProxyPlaylistNotifier.provider);
final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier);
List<AlbumSimple> albums = [];
List<Artist> artists = [];
List<Track> tracks = [];
List<PlaylistSimple> playlists = [];
final pages = [
...searchTrack.pages,
...searchAlbum.pages,
...searchPlaylist.pages,
...searchArtist.pages,
].expand<Page>((page) => page).toList();
for (MapEntry<int, Page> page in pages.asMap().entries) {
for (var item in page.value.items ?? []) {
if (item is AlbumSimple) {
albums.add(item);
} else if (item is PlaylistSimple) {
playlists.add(item);
} else if (item is Artist) {
artists.add(item);
} else if (item is Track) {
tracks.add(item);
}
}
}
return InterScrollbar(
builder: (context) => InterScrollbar(
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
@ -115,248 +79,18 @@ class SearchPage extends HookConsumerWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (tracks.isNotEmpty)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Text(
context.l10n.songs,
style: theme.textTheme.titleLarge!,
),
),
if (!searchTrack.hasPageData &&
!searchTrack.hasPageError &&
!searchTrack.isLoadingNextPage)
const CircularProgressIndicator()
else if (searchTrack.hasPageError)
Text(
searchTrack.errors.lastOrNull?.toString() ?? "",
)
else
...tracks.mapIndexed((i, track) {
return TrackTile(
index: i,
track: track,
onTap: () async {
final isTrackPlaying =
playlist.activeTrack?.id == track.id;
if (!isTrackPlaying && context.mounted) {
final shouldPlay = (playlist.tracks.length) > 20
? await showPromptDialog(
context: context,
title: context.l10n.playing_track(
track.name!,
),
message: context.l10n.queue_clear_alert(
playlist.tracks.length,
),
)
: true;
if (shouldPlay) {
await playlistNotifier.load(
[track],
autoPlay: true,
);
}
}
},
);
}),
if (searchTrack.hasNextPage && tracks.isNotEmpty)
Center(
child: TextButton(
onPressed: searchTrack.isLoadingNextPage
? null
: () => searchTrack.fetchNext(),
child: searchTrack.isLoadingNextPage
? const CircularProgressIndicator()
: Text(context.l10n.load_more),
),
),
if (playlists.isNotEmpty)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Text(
context.l10n.playlists,
style: theme.textTheme.titleLarge!,
),
),
const SizedBox(height: 10),
ScrollConfiguration(
behavior: ScrollConfiguration.of(context).copyWith(
dragDevices: {
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
},
),
child: Scrollbar(
scrollbarOrientation: mediaQuery.lgAndUp
? ScrollbarOrientation.bottom
: ScrollbarOrientation.top,
controller: playlistController,
child: Waypoint(
onTouchEdge: () {
searchPlaylist.fetchNext();
},
controller: playlistController,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
controller: playlistController,
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
children: [
...playlists.mapIndexed(
(i, playlist) {
if (i == playlists.length - 1 &&
searchPlaylist.hasNextPage) {
return const ShimmerPlaybuttonCard(
count: 1);
}
return PlaylistCard(playlist);
},
),
],
),
),
),
),
),
if (!searchPlaylist.hasPageData &&
!searchPlaylist.hasPageError &&
!searchPlaylist.isLoadingNextPage)
const CircularProgressIndicator(),
if (searchPlaylist.hasPageError)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Text(
searchPlaylist.errors.lastOrNull?.toString() ?? "",
),
),
SearchTracksSection(query: searchTrack),
SearchPlaylistsSection(query: searchPlaylist),
const SizedBox(height: 20),
if (artists.isNotEmpty)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Text(
context.l10n.artists,
style: theme.textTheme.titleLarge!,
),
),
const SizedBox(height: 10),
ScrollConfiguration(
behavior: ScrollConfiguration.of(context).copyWith(
dragDevices: {
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
},
),
child: Scrollbar(
controller: artistController,
child: Waypoint(
controller: artistController,
onTouchEdge: () {
searchArtist.fetchNext();
},
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
controller: artistController,
child: Row(
children: [
...artists.mapIndexed(
(i, artist) {
if (i == artists.length - 1 &&
searchArtist.hasNextPage) {
return const ShimmerPlaybuttonCard(
count: 1);
}
return Container(
margin: const EdgeInsets.symmetric(
horizontal: 15),
child: ArtistCard(artist),
);
},
),
],
),
),
),
),
),
if (!searchArtist.hasPageData &&
!searchArtist.hasPageError &&
!searchArtist.isLoadingNextPage)
const CircularProgressIndicator(),
if (searchArtist.hasPageError)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Text(
searchArtist.errors.lastOrNull?.toString() ?? "",
),
),
SearchArtistsSection(query: searchArtist),
const SizedBox(height: 20),
if (albums.isNotEmpty)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Text(
context.l10n.albums,
style: theme.textTheme.titleLarge!,
),
),
const SizedBox(height: 10),
ScrollConfiguration(
behavior: ScrollConfiguration.of(context).copyWith(
dragDevices: {
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
},
),
child: Scrollbar(
controller: albumController,
child: Waypoint(
controller: albumController,
onTouchEdge: () {
searchAlbum.fetchNext();
},
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
controller: albumController,
child: Row(
children: [
...albums.mapIndexed((i, album) {
if (i == albums.length - 1 &&
searchAlbum.hasNextPage) {
return const ShimmerPlaybuttonCard(
count: 1);
}
return AlbumCard(
TypeConversionUtils.simpleAlbum_X_Album(
album,
),
);
}),
SearchAlbumsSection(query: searchAlbum),
],
),
),
),
),
),
if (!searchAlbum.hasPageData &&
!searchAlbum.hasPageError &&
!searchAlbum.isLoadingNextPage)
const CircularProgressIndicator(),
if (searchAlbum.hasPageError)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Text(
searchAlbum.errors.lastOrNull?.toString() ?? "",
),
),
],
),
),
),
),
);
},
);
return SafeArea(

View File

@ -0,0 +1,39 @@
import 'package:fl_query/fl_query.dart';
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/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/utils/type_conversion_utils.dart';
class SearchAlbumsSection extends HookConsumerWidget {
final InfiniteQuery<List<Page<dynamic>>, dynamic, int> query;
const SearchAlbumsSection({
required this.query,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context, ref) {
final albums = useMemoized(
() => query.pages
.expand(
(page) => page.map((p) => p.items!).expand((element) => element),
)
.whereType<AlbumSimple>()
.map((e) => TypeConversionUtils.simpleAlbum_X_Album(e))
.toList(),
[query.pages],
);
return HorizontalPlaybuttonCardView(
hasNextPage: query.hasNextPage,
items: albums,
onFetchMore: query.fetchNext,
title: Text(context.l10n.albums),
);
}
}

View File

@ -0,0 +1,37 @@
import 'package:fl_query/fl_query.dart';
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/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
import 'package:spotube/extensions/context.dart';
class SearchArtistsSection extends HookConsumerWidget {
final InfiniteQuery<List<Page<dynamic>>, dynamic, int> query;
const SearchArtistsSection({
Key? key,
required this.query,
}) : super(key: key);
@override
Widget build(BuildContext context, ref) {
final artists = useMemoized(
() => query.pages
.expand(
(page) => page.map((p) => p.items!).expand((element) => element),
)
.whereType<Artist>()
.toList(),
[query.pages],
);
return HorizontalPlaybuttonCardView<Artist>(
hasNextPage: query.hasNextPage,
items: artists,
onFetchMore: query.fetchNext,
title: Text(context.l10n.albums),
);
}
}

View File

@ -0,0 +1,35 @@
import 'package:fl_query/fl_query.dart';
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/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
import 'package:spotube/extensions/context.dart';
class SearchPlaylistsSection extends HookConsumerWidget {
final InfiniteQuery<List<Page<dynamic>>, dynamic, int> query;
const SearchPlaylistsSection({
required this.query,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context, ref) {
final playlists = useMemoized(
() => query.pages
.expand(
(page) => page.map((p) => p.items!).expand((element) => element),
)
.whereType<PlaylistSimple>()
.toList(),
[query.pages],
);
return HorizontalPlaybuttonCardView(
hasNextPage: query.hasNextPage,
items: playlists,
onFetchMore: query.fetchNext,
title: Text(context.l10n.playlists),
);
}
}

View File

@ -0,0 +1,98 @@
import 'package:collection/collection.dart';
import 'package:fl_query/fl_query.dart';
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/shared/dialogs/prompt_dialog.dart';
import 'package:spotube/components/shared/track_table/track_tile.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
class SearchTracksSection extends HookConsumerWidget {
final InfiniteQuery<List<Page<dynamic>>, dynamic, int> query;
const SearchTracksSection({
Key? key,
required this.query,
}) : super(key: key);
@override
Widget build(BuildContext context, ref) {
final searchTrack = query;
final tracks = useMemoized(
() => searchTrack.pages
.expand(
(page) => page.map((p) => p.items!).expand((element) => element),
)
.whereType<Track>(),
[searchTrack.pages],
);
final playlistNotifier = ref.watch(ProxyPlaylistNotifier.provider.notifier);
final playlist = ref.watch(ProxyPlaylistNotifier.provider);
final theme = Theme.of(context);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
if (tracks.isNotEmpty)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Text(
context.l10n.songs,
style: theme.textTheme.titleLarge!,
),
),
if (!searchTrack.hasPageData &&
!searchTrack.hasPageError &&
!searchTrack.isLoadingNextPage)
const CircularProgressIndicator()
else if (searchTrack.hasPageError)
Text(
searchTrack.errors.lastOrNull?.toString() ?? "",
)
else
...tracks.mapIndexed((i, track) {
return TrackTile(
index: i,
track: track,
onTap: () async {
final isTrackPlaying = playlist.activeTrack?.id == track.id;
if (!isTrackPlaying && context.mounted) {
final shouldPlay = (playlist.tracks.length) > 20
? await showPromptDialog(
context: context,
title: context.l10n.playing_track(
track.name!,
),
message: context.l10n.queue_clear_alert(
playlist.tracks.length,
),
)
: true;
if (shouldPlay) {
await playlistNotifier.load(
[track],
autoPlay: true,
);
}
}
},
);
}),
if (searchTrack.hasNextPage && tracks.isNotEmpty)
Center(
child: TextButton(
onPressed: searchTrack.isLoadingNextPage
? null
: () => searchTrack.fetchNext(),
child: searchTrack.isLoadingNextPage
? const CircularProgressIndicator()
: Text(context.l10n.load_more),
),
)
],
);
}
}