mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
refactor: horizontal playbutton layout to use ListView and breakdown search page into sections
This commit is contained in:
parent
487c2ed6bd
commit
6b8ae88db4
@ -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,
|
||||
|
@ -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,
|
||||
},
|
||||
),
|
||||
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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
return HorizontalPlaybuttonCardView<Album>(
|
||||
hasNextPage: albumsQuery.hasNextPage,
|
||||
items: albums,
|
||||
onFetchMore: albumsQuery.fetchNext,
|
||||
title: Text(
|
||||
context.l10n.albums,
|
||||
style: theme.textTheme.headlineSmall,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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(),
|
||||
};
|
||||
}),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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(
|
||||
|
@ -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]),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
@ -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);
|
||||
|
||||
@ -133,10 +38,12 @@ class PersonalizedPage extends HookConsumerWidget {
|
||||
.whereType<Page<AlbumSimple>>()
|
||||
.expand((page) => page.items ?? const <AlbumSimple>[])
|
||||
.where((album) {
|
||||
return album.artists
|
||||
?.any((artist) => userArtists.contains(artist.id!)) ==
|
||||
true;
|
||||
}),
|
||||
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: () {},
|
||||
);
|
||||
|
@ -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,283 +71,26 @@ 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(
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: SafeArea(
|
||||
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() ?? "",
|
||||
),
|
||||
),
|
||||
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() ?? "",
|
||||
),
|
||||
),
|
||||
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,
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
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() ?? "",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
builder: (context) => InterScrollbar(
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SearchTracksSection(query: searchTrack),
|
||||
SearchPlaylistsSection(query: searchPlaylist),
|
||||
const SizedBox(height: 20),
|
||||
SearchArtistsSection(query: searchArtist),
|
||||
const SizedBox(height: 20),
|
||||
SearchAlbumsSection(query: searchAlbum),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return SafeArea(
|
||||
|
39
lib/pages/search/sections/albums.dart
Normal file
39
lib/pages/search/sections/albums.dart
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
37
lib/pages/search/sections/artists.dart
Normal file
37
lib/pages/search/sections/artists.dart
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
35
lib/pages/search/sections/playlists.dart
Normal file
35
lib/pages/search/sections/playlists.dart
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
98
lib/pages/search/sections/tracks.dart
Normal file
98
lib/pages/search/sections/tracks.dart
Normal 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),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user