diff --git a/lib/components/genre/category_card.dart b/lib/components/genre/category_card.dart index 1aa33cd65..a8d677716 100644 --- a/lib/components/genre/category_card.dart +++ b/lib/components/genre/category_card.dart @@ -1,6 +1,7 @@ import 'dart:ui'; 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'; @@ -8,6 +9,7 @@ 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/models/logger.dart'; import 'package:spotube/services/queries/queries.dart'; @@ -28,16 +30,23 @@ class CategoryCard extends HookConsumerWidget { category.id!, ); + final playlists = useMemoized( + () => playlistQuery.pages.expand( + (page) { + return page.items?.where((i) => i != null) ?? const Iterable.empty(); + }, + ).toList(), + [playlistQuery.pages], + ); + if (playlistQuery.hasErrors && !playlistQuery.hasPageData && !playlistQuery.isLoadingNextPage) { return const SizedBox.shrink(); } - final playlists = playlistQuery.pages.expand( - (page) { - return page.items?.where((i) => i != null) ?? const Iterable.empty(); - }, - ).toList(); + + final mediaQuery = MediaQuery.of(context); + return Padding( padding: const EdgeInsets.all(8.0), child: Column( @@ -48,29 +57,35 @@ class CategoryCard extends HookConsumerWidget { category.name!, style: Theme.of(context).textTheme.titleMedium, ), - ScrollConfiguration( - behavior: ScrollConfiguration.of(context).copyWith( - dragDevices: { - PointerDeviceKind.touch, - PointerDeviceKind.mouse, - }, - ), - child: Waypoint( - controller: scrollController, - onTouchEdge: playlistQuery.fetchNext, - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - controller: scrollController, - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ...playlists.map((playlist) => PlaylistCard(playlist)), - if (playlistQuery.hasNextPage) - const ShimmerPlaybuttonCard(count: 1), - ], - ), + 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); + }), ), ), ], diff --git a/lib/components/library/user_playlists.dart b/lib/components/library/user_playlists.dart index 8ed3e73d6..ecf4fa124 100644 --- a/lib/components/library/user_playlists.dart +++ b/lib/components/library/user_playlists.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart' hide Image; +import 'package:flutter_desktop_tools/flutter_desktop_tools.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:fuzzywuzzy/fuzzywuzzy.dart'; import 'package:collection/collection.dart'; @@ -8,7 +9,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/playlist/playlist_create_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/playlist/playlist_card.dart'; @@ -80,20 +80,13 @@ class UserPlaylists extends HookConsumerWidget { return RefreshIndicator( onRefresh: playlistsQuery.refresh, - child: InterScrollbar( - controller: controller, - child: SingleChildScrollView( + child: SafeArea( + child: CustomScrollView( controller: controller, - physics: const AlwaysScrollableScrollPhysics(), - child: Waypoint( - controller: controller, - onTouchEdge: () { - if (playlistsQuery.hasNextPage) { - playlistsQuery.fetchNext(); - } - }, - child: SafeArea( + slivers: [ + SliverToBoxAdapter( child: Column( + mainAxisSize: MainAxisSize.min, children: [ Padding( padding: const EdgeInsets.all(10), @@ -103,42 +96,53 @@ class UserPlaylists extends HookConsumerWidget { leading: const Icon(SpotubeIcons.filter), ), ), - AnimatedCrossFade( - duration: const Duration(milliseconds: 300), - crossFadeState: !playlistsQuery.hasPageData && - !playlistsQuery.hasPageError && - !playlistsQuery.isLoadingNextPage - ? CrossFadeState.showFirst - : CrossFadeState.showSecond, - firstChild: - const Center(child: ShimmerPlaybuttonCard(count: 7)), - secondChild: Wrap( - runSpacing: 10, - alignment: WrapAlignment.center, - children: [ - Row( - children: [ - const SizedBox(width: 10), - const PlaylistCreateDialogButton(), - const SizedBox(width: 10), - ElevatedButton.icon( - icon: const Icon(SpotubeIcons.magic), - label: Text(context.l10n.generate_playlist), - onPressed: () { - GoRouter.of(context).push("/library/generate"); - }, - ), - const SizedBox(width: 10), - ], - ), - ...playlists.map((playlist) => PlaylistCard(playlist)) - ], - ), + Row( + children: [ + const SizedBox(width: 10), + const PlaylistCreateDialogButton(), + const SizedBox(width: 10), + ElevatedButton.icon( + icon: const Icon(SpotubeIcons.magic), + label: Text(context.l10n.generate_playlist), + onPressed: () { + GoRouter.of(context).push("/library/generate"); + }, + ), + const SizedBox(width: 10), + ], ), ], ), ), - ), + const SliverToBoxAdapter( + child: SizedBox(height: 10), + ), + SliverGrid.builder( + itemCount: playlists.length + 1, + gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 200, + mainAxisExtent: DesktopTools.platform.isMobile ? 225 : 250, + crossAxisSpacing: 8, + mainAxisSpacing: 8, + ), + itemBuilder: (context, index) { + if (index == playlists.length) { + if (!playlistsQuery.hasNextPage) { + return const SizedBox.shrink(); + } + + return Waypoint( + controller: controller, + isGrid: true, + onTouchEdge: playlistsQuery.fetchNext, + child: const ShimmerPlaybuttonCard(count: 1), + ); + } + + return PlaylistCard(playlists[index]); + }, + ) + ], ), ), ); diff --git a/lib/components/shared/track_table/track_tile.dart b/lib/components/shared/track_table/track_tile.dart index ff1b314b0..4980f96b7 100644 --- a/lib/components/shared/track_table/track_tile.dart +++ b/lib/components/shared/track_table/track_tile.dart @@ -113,7 +113,7 @@ class TrackTile extends HookConsumerWidget { child: Padding( padding: const EdgeInsets.symmetric(horizontal: 6), child: Text( - '$index', + '${(index ?? 0) + 1}', maxLines: 1, style: theme.textTheme.bodySmall, textAlign: TextAlign.center, diff --git a/lib/pages/home/personalized.dart b/lib/pages/home/personalized.dart index f7e942bec..4f0b655f6 100644 --- a/lib/pages/home/personalized.dart +++ b/lib/pages/home/personalized.dart @@ -10,6 +10,7 @@ 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'; @@ -38,6 +39,7 @@ class PersonalizedItemCard extends HookWidget { @override Widget build(BuildContext context) { final scrollController = useScrollController(); + final mediaQuery = MediaQuery.of(context); return Padding( padding: const EdgeInsets.all(8.0), @@ -52,36 +54,48 @@ class PersonalizedItemCard extends HookWidget { style: Theme.of(context).textTheme.titleLarge, ), ), - ScrollConfiguration( - behavior: ScrollConfiguration.of(context).copyWith( - dragDevices: { - PointerDeviceKind.touch, - PointerDeviceKind.mouse, - }, - ), - child: Scrollbar( - controller: scrollController, - interactive: false, - child: Waypoint( + SizedBox( + height: mediaQuery.smAndDown ? 226 : 266, + child: ScrollConfiguration( + behavior: ScrollConfiguration.of(context).copyWith( + dragDevices: { + PointerDeviceKind.touch, + PointerDeviceKind.mouse, + }, + ), + child: Scrollbar( controller: scrollController, - onTouchEdge: hasNextPage ? onFetchMore : null, - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - controller: scrollController, - physics: const AlwaysScrollableScrollPhysics(), + interactive: false, + child: ListView.builder( + itemCount: (playlists?.length ?? albums?.length)! + 1, padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - ...?playlists?.map((playlist) => PlaylistCard(playlist)), - ...?albums?.map( - (album) => AlbumCard( - TypeConversionUtils.simpleAlbum_X_Album(album), + 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, ), - ), - if (hasNextPage) const ShimmerPlaybuttonCard(count: 1), - ], - ), + ); + } + + return PlaylistCard(item as PlaylistSimple); + }, ), ), ),