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:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/components/shared/playbutton_card.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/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
import 'package:spotube/provider/spotify_provider.dart';
|
import 'package:spotube/provider/spotify_provider.dart';
|
||||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
@ -34,13 +33,6 @@ class AlbumCard extends HookConsumerWidget {
|
|||||||
[playlist, album.id],
|
[playlist, album.id],
|
||||||
);
|
);
|
||||||
|
|
||||||
final marginH = useBreakpointValue<int>(
|
|
||||||
xs: 10,
|
|
||||||
sm: 10,
|
|
||||||
md: 15,
|
|
||||||
others: 20,
|
|
||||||
);
|
|
||||||
|
|
||||||
final updating = useState(false);
|
final updating = useState(false);
|
||||||
final spotify = ref.watch(spotifyProvider);
|
final spotify = ref.watch(spotifyProvider);
|
||||||
|
|
||||||
@ -49,7 +41,7 @@ class AlbumCard extends HookConsumerWidget {
|
|||||||
album.images,
|
album.images,
|
||||||
placeholder: ImagePlaceholder.collection,
|
placeholder: ImagePlaceholder.collection,
|
||||||
),
|
),
|
||||||
margin: EdgeInsets.symmetric(horizontal: marginH.toDouble()),
|
margin: const EdgeInsets.symmetric(horizontal: 10),
|
||||||
isPlaying: isPlaylistPlaying,
|
isPlaying: isPlaylistPlaying,
|
||||||
isLoading: (isPlaylistPlaying && playlist.isFetching == true) ||
|
isLoading: (isPlaylistPlaying && playlist.isFetching == true) ||
|
||||||
updating.value,
|
updating.value,
|
||||||
|
@ -1,11 +1,9 @@
|
|||||||
import 'package:flutter/gestures.dart';
|
|
||||||
import 'package:flutter/material.dart' hide Page;
|
import 'package:flutter/material.dart' hide Page;
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_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/components/album/album_card.dart';
|
import 'package:spotube/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
|
||||||
import 'package:spotube/components/shared/shimmers/shimmer_playbutton_card.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/components/shared/waypoint.dart';
|
|
||||||
import 'package:spotube/models/logger.dart';
|
import 'package:spotube/models/logger.dart';
|
||||||
import 'package:spotube/services/queries/queries.dart';
|
import 'package:spotube/services/queries/queries.dart';
|
||||||
|
|
||||||
@ -20,7 +18,6 @@ class ArtistAlbumList extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final scrollController = useScrollController();
|
|
||||||
final albumsQuery = useQueries.artist.albumsOf(ref, artistId);
|
final albumsQuery = useQueries.artist.albumsOf(ref, artistId);
|
||||||
|
|
||||||
final albums = useMemoized(() {
|
final albums = useMemoized(() {
|
||||||
@ -29,40 +26,16 @@ class ArtistAlbumList extends HookConsumerWidget {
|
|||||||
.toList();
|
.toList();
|
||||||
}, [albumsQuery.pages]);
|
}, [albumsQuery.pages]);
|
||||||
|
|
||||||
final hasNextPage = albumsQuery.pages.isEmpty
|
final theme = Theme.of(context);
|
||||||
? false
|
|
||||||
: (albumsQuery.pages.last.items?.length ?? 0) == 5;
|
|
||||||
|
|
||||||
return Column(
|
return HorizontalPlaybuttonCardView<Album>(
|
||||||
children: [
|
hasNextPage: albumsQuery.hasNextPage,
|
||||||
ScrollConfiguration(
|
items: albums,
|
||||||
behavior: ScrollConfiguration.of(context).copyWith(
|
onFetchMore: albumsQuery.fetchNext,
|
||||||
dragDevices: {
|
title: Text(
|
||||||
PointerDeviceKind.touch,
|
context.l10n.albums,
|
||||||
PointerDeviceKind.mouse,
|
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),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,10 @@
|
|||||||
import 'dart:ui';
|
import 'package:collection/collection.dart';
|
||||||
|
|
||||||
import 'package:flutter/material.dart' hide Page;
|
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:flutter_hooks/flutter_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/components/playlist/playlist_card.dart';
|
import 'package:spotube/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.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/models/logger.dart';
|
import 'package:spotube/models/logger.dart';
|
||||||
import 'package:spotube/services/queries/queries.dart';
|
import 'package:spotube/services/queries/queries.dart';
|
||||||
|
|
||||||
@ -24,7 +19,6 @@ class CategoryCard extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final scrollController = useScrollController();
|
|
||||||
final playlistQuery = useQueries.category.playlistsOf(
|
final playlistQuery = useQueries.category.playlistsOf(
|
||||||
ref,
|
ref,
|
||||||
category.id!,
|
category.id!,
|
||||||
@ -33,7 +27,8 @@ class CategoryCard extends HookConsumerWidget {
|
|||||||
final playlists = useMemoized(
|
final playlists = useMemoized(
|
||||||
() => playlistQuery.pages.expand(
|
() => playlistQuery.pages.expand(
|
||||||
(page) {
|
(page) {
|
||||||
return page.items?.where((i) => i != null) ?? const Iterable.empty();
|
return page.items?.whereNotNull() ??
|
||||||
|
const Iterable<PlaylistSimple>.empty();
|
||||||
},
|
},
|
||||||
).toList(),
|
).toList(),
|
||||||
[playlistQuery.pages],
|
[playlistQuery.pages],
|
||||||
@ -45,51 +40,11 @@ class CategoryCard extends HookConsumerWidget {
|
|||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
|
||||||
final mediaQuery = MediaQuery.of(context);
|
return HorizontalPlaybuttonCardView<PlaylistSimple>(
|
||||||
|
title: Text(category.name!),
|
||||||
return Padding(
|
hasNextPage: playlistQuery.hasNextPage,
|
||||||
padding: const EdgeInsets.all(8.0),
|
items: playlists,
|
||||||
child: Column(
|
onFetchMore: playlistQuery.fetchNext,
|
||||||
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);
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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),
|
const SizedBox(height: 50),
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(8.0),
|
|
||||||
child: Text(
|
|
||||||
context.l10n.albums,
|
|
||||||
style: theme.textTheme.headlineSmall,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
ArtistAlbumList(artistId),
|
ArtistAlbumList(artistId),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
Padding(
|
Padding(
|
||||||
|
@ -85,15 +85,19 @@ class GenrePage extends HookConsumerWidget {
|
|||||||
controller: scrollController,
|
controller: scrollController,
|
||||||
itemCount: categories.length,
|
itemCount: categories.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
return AnimatedCrossFade(
|
return AnimatedSwitcher(
|
||||||
crossFadeState: searchController.text.isEmpty &&
|
transitionBuilder: (child, animation) {
|
||||||
|
return FadeTransition(
|
||||||
|
opacity: animation,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
child: searchController.text.isEmpty &&
|
||||||
index == categories.length - 1 &&
|
index == categories.length - 1 &&
|
||||||
categoriesQuery.hasNextPage
|
categoriesQuery.hasNextPage
|
||||||
? CrossFadeState.showFirst
|
? const ShimmerCategories()
|
||||||
: CrossFadeState.showSecond,
|
: CategoryCard(categories[index]),
|
||||||
duration: const Duration(milliseconds: 300),
|
|
||||||
firstChild: const ShimmerCategories(),
|
|
||||||
secondChild: CategoryCard(categories[index]),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -1,111 +1,16 @@
|
|||||||
import 'package:flutter/gestures.dart';
|
|
||||||
import 'package:flutter/material.dart' hide Page;
|
import 'package:flutter/material.dart' hide Page;
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_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/components/album/album_card.dart';
|
import 'package:spotube/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
|
||||||
import 'package:spotube/components/playlist/playlist_card.dart';
|
|
||||||
import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.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_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/extensions/context.dart';
|
||||||
import 'package:spotube/models/logger.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';
|
||||||
import 'package:spotube/utils/type_conversion_utils.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 {
|
class PersonalizedPage extends HookConsumerWidget {
|
||||||
const PersonalizedPage({Key? key}) : super(key: key);
|
const PersonalizedPage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@ -133,10 +38,12 @@ class PersonalizedPage extends HookConsumerWidget {
|
|||||||
.whereType<Page<AlbumSimple>>()
|
.whereType<Page<AlbumSimple>>()
|
||||||
.expand((page) => page.items ?? const <AlbumSimple>[])
|
.expand((page) => page.items ?? const <AlbumSimple>[])
|
||||||
.where((album) {
|
.where((album) {
|
||||||
return album.artists
|
return album.artists
|
||||||
?.any((artist) => userArtists.contains(artist.id!)) ==
|
?.any((artist) => userArtists.contains(artist.id!)) ==
|
||||||
true;
|
true;
|
||||||
}),
|
})
|
||||||
|
.map((album) => TypeConversionUtils.simpleAlbum_X_Album(album))
|
||||||
|
.toList(),
|
||||||
[newReleases.pages],
|
[newReleases.pages],
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -149,18 +56,18 @@ class PersonalizedPage extends HookConsumerWidget {
|
|||||||
!featuredPlaylistsQuery.isLoadingNextPage)
|
!featuredPlaylistsQuery.isLoadingNextPage)
|
||||||
const ShimmerCategories()
|
const ShimmerCategories()
|
||||||
else
|
else
|
||||||
PersonalizedItemCard(
|
HorizontalPlaybuttonCardView<PlaylistSimple>(
|
||||||
playlists: playlists,
|
items: playlists.toList(),
|
||||||
title: context.l10n.featured,
|
title: Text(context.l10n.featured),
|
||||||
hasNextPage: featuredPlaylistsQuery.hasNextPage,
|
hasNextPage: featuredPlaylistsQuery.hasNextPage,
|
||||||
onFetchMore: featuredPlaylistsQuery.fetchNext,
|
onFetchMore: featuredPlaylistsQuery.fetchNext,
|
||||||
),
|
),
|
||||||
if (auth != null &&
|
if (auth != null &&
|
||||||
newReleases.hasPageData &&
|
newReleases.hasPageData &&
|
||||||
userArtistsQuery.hasData)
|
userArtistsQuery.hasData)
|
||||||
PersonalizedItemCard(
|
HorizontalPlaybuttonCardView<Album>(
|
||||||
albums: albums,
|
items: albums,
|
||||||
title: context.l10n.new_releases,
|
title: Text(context.l10n.new_releases),
|
||||||
hasNextPage: newReleases.hasNextPage,
|
hasNextPage: newReleases.hasNextPage,
|
||||||
onFetchMore: newReleases.fetchNext,
|
onFetchMore: newReleases.fetchNext,
|
||||||
),
|
),
|
||||||
@ -172,9 +79,9 @@ class PersonalizedPage extends HookConsumerWidget {
|
|||||||
.cast<PlaylistSimple>() ??
|
.cast<PlaylistSimple>() ??
|
||||||
<PlaylistSimple>[];
|
<PlaylistSimple>[];
|
||||||
if (playlists.isEmpty) return const SizedBox.shrink();
|
if (playlists.isEmpty) return const SizedBox.shrink();
|
||||||
return PersonalizedItemCard(
|
return HorizontalPlaybuttonCardView<PlaylistSimple>(
|
||||||
playlists: playlists,
|
items: playlists,
|
||||||
title: item["name"] ?? "",
|
title: Text(item["name"] ?? ""),
|
||||||
hasNextPage: false,
|
hasNextPage: false,
|
||||||
onFetchMore: () {},
|
onFetchMore: () {},
|
||||||
);
|
);
|
||||||
|
@ -1,30 +1,24 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/gestures.dart';
|
|
||||||
import 'package:flutter/material.dart' hide Page;
|
import 'package:flutter/material.dart' hide Page;
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_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/collections/spotube_icons.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/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/fallbacks/anonymous_fallback.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_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/constrains.dart';
|
||||||
import 'package:spotube/extensions/context.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/authentication_provider.dart';
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
|
||||||
import 'package:spotube/services/queries/queries.dart';
|
import 'package:spotube/services/queries/queries.dart';
|
||||||
|
|
||||||
import 'package:spotube/utils/platform.dart';
|
import 'package:spotube/utils/platform.dart';
|
||||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
|
||||||
final searchTermStateProvider = StateProvider<String>((ref) => "");
|
final searchTermStateProvider = StateProvider<String>((ref) => "");
|
||||||
@ -38,9 +32,6 @@ class SearchPage extends HookConsumerWidget {
|
|||||||
ref.watch(AuthenticationNotifier.provider);
|
ref.watch(AuthenticationNotifier.provider);
|
||||||
final authenticationNotifier =
|
final authenticationNotifier =
|
||||||
ref.watch(AuthenticationNotifier.provider.notifier);
|
ref.watch(AuthenticationNotifier.provider.notifier);
|
||||||
final albumController = useScrollController();
|
|
||||||
final playlistController = useScrollController();
|
|
||||||
final artistController = useScrollController();
|
|
||||||
final mediaQuery = MediaQuery.of(context);
|
final mediaQuery = MediaQuery.of(context);
|
||||||
|
|
||||||
final searchTerm = ref.watch(searchTermStateProvider);
|
final searchTerm = ref.watch(searchTermStateProvider);
|
||||||
@ -80,283 +71,26 @@ class SearchPage extends HookConsumerWidget {
|
|||||||
searchTerm.isNotEmpty;
|
searchTerm.isNotEmpty;
|
||||||
|
|
||||||
final resultWidget = HookBuilder(
|
final resultWidget = HookBuilder(
|
||||||
builder: (context) {
|
builder: (context) => InterScrollbar(
|
||||||
final playlist = ref.watch(ProxyPlaylistNotifier.provider);
|
child: SingleChildScrollView(
|
||||||
final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier);
|
child: Padding(
|
||||||
List<AlbumSimple> albums = [];
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
List<Artist> artists = [];
|
child: SafeArea(
|
||||||
List<Track> tracks = [];
|
child: Column(
|
||||||
List<PlaylistSimple> playlists = [];
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
final pages = [
|
children: [
|
||||||
...searchTrack.pages,
|
SearchTracksSection(query: searchTrack),
|
||||||
...searchAlbum.pages,
|
SearchPlaylistsSection(query: searchPlaylist),
|
||||||
...searchPlaylist.pages,
|
const SizedBox(height: 20),
|
||||||
...searchArtist.pages,
|
SearchArtistsSection(query: searchArtist),
|
||||||
].expand<Page>((page) => page).toList();
|
const SizedBox(height: 20),
|
||||||
for (MapEntry<int, Page> page in pages.asMap().entries) {
|
SearchAlbumsSection(query: searchAlbum),
|
||||||
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() ?? "",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
},
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
return SafeArea(
|
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