mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-12 15:35:17 +00:00
feat: smoother list using fl_query and waypoint
fix(theme): remove splash effect feat(artists-albums): horizontal paginated list instead of grid view page
This commit is contained in:
parent
7eea968bcf
commit
c77b0e198b
73
lib/components/Artist/ArtistAlbumList.dart
Normal file
73
lib/components/Artist/ArtistAlbumList.dart
Normal file
@ -0,0 +1,73 @@
|
||||
import 'package:fl_query_hooks/fl_query_hooks.dart';
|
||||
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/AlbumCard.dart';
|
||||
import 'package:spotube/components/LoaderShimmers/ShimmerPlaybuttonCard.dart';
|
||||
import 'package:spotube/components/Shared/Waypoint.dart';
|
||||
import 'package:spotube/models/Logger.dart';
|
||||
import 'package:spotube/provider/SpotifyDI.dart';
|
||||
import 'package:spotube/provider/SpotifyRequests.dart';
|
||||
|
||||
class ArtistAlbumList extends HookConsumerWidget {
|
||||
final String artistId;
|
||||
ArtistAlbumList(
|
||||
this.artistId, {
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
final logger = getLogger(ArtistAlbumList);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final scrollController = useScrollController();
|
||||
final albumsQuery = useInfiniteQuery(
|
||||
job: artistAlbumsQueryJob(artistId),
|
||||
externalData: ref.watch(spotifyProvider),
|
||||
);
|
||||
|
||||
final albums = useMemoized(() {
|
||||
return albumsQuery.pages
|
||||
.expand<Album>((page) => page?.items ?? const Iterable.empty())
|
||||
.toList();
|
||||
}, [albumsQuery.pages]);
|
||||
|
||||
final hasNextPage = albumsQuery.pages.isEmpty
|
||||
? false
|
||||
: (albumsQuery.pages.last?.items?.length ?? 0) == 5;
|
||||
|
||||
return SizedBox(
|
||||
height: 300,
|
||||
child: ScrollConfiguration(
|
||||
behavior: ScrollConfiguration.of(context).copyWith(
|
||||
dragDevices: {
|
||||
PointerDeviceKind.touch,
|
||||
PointerDeviceKind.mouse,
|
||||
},
|
||||
),
|
||||
child: Scrollbar(
|
||||
interactive: false,
|
||||
controller: scrollController,
|
||||
child: ListView.builder(
|
||||
itemCount: albums.length,
|
||||
controller: scrollController,
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemBuilder: (context, index) {
|
||||
if (index == albums.length - 1 && hasNextPage) {
|
||||
return Waypoint(
|
||||
onEnter: () {
|
||||
albumsQuery.fetchNextPage();
|
||||
},
|
||||
child: const ShimmerPlaybuttonCard(count: 1),
|
||||
);
|
||||
}
|
||||
return AlbumCard(albums[index]);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,95 +0,0 @@
|
||||
import 'package:flutter/material.dart' hide Page;
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/Album/AlbumCard.dart';
|
||||
import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
|
||||
import 'package:spotube/models/Logger.dart';
|
||||
import 'package:spotube/provider/SpotifyDI.dart';
|
||||
|
||||
class ArtistAlbumView extends ConsumerStatefulWidget {
|
||||
final String artistId;
|
||||
final String artistName;
|
||||
const ArtistAlbumView(
|
||||
this.artistId,
|
||||
this.artistName, {
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
ConsumerState<ArtistAlbumView> createState() => _ArtistAlbumViewState();
|
||||
}
|
||||
|
||||
class _ArtistAlbumViewState extends ConsumerState<ArtistAlbumView> {
|
||||
final PagingController<int, Album> _pagingController =
|
||||
PagingController<int, Album>(firstPageKey: 0);
|
||||
|
||||
final logger = getLogger(ArtistAlbumView);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_pagingController.addPageRequestListener((pageKey) {
|
||||
_fetchPage(pageKey);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pagingController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
_fetchPage(int pageKey) async {
|
||||
try {
|
||||
SpotifyApi spotifyApi = ref.watch(spotifyProvider);
|
||||
Page<Album> albums =
|
||||
await spotifyApi.artists.albums(widget.artistId).getPage(8, pageKey);
|
||||
|
||||
var items = albums.items!.toList();
|
||||
|
||||
if (albums.isLast && albums.items != null) {
|
||||
_pagingController.appendLastPage(items);
|
||||
} else if (albums.items != null) {
|
||||
_pagingController.appendPage(items, albums.nextOffset);
|
||||
}
|
||||
} catch (e, stack) {
|
||||
logger.e(e, null, stack);
|
||||
_pagingController.error = e;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SafeArea(
|
||||
child: Scaffold(
|
||||
appBar: const PageWindowTitleBar(leading: BackButton()),
|
||||
body: Column(
|
||||
children: [
|
||||
Text(
|
||||
widget.artistName,
|
||||
style: Theme.of(context).textTheme.headline4,
|
||||
),
|
||||
Expanded(
|
||||
child: PagedGridView(
|
||||
pagingController: _pagingController,
|
||||
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: 260,
|
||||
childAspectRatio: 9 / 13,
|
||||
crossAxisSpacing: 20,
|
||||
mainAxisSpacing: 20,
|
||||
),
|
||||
padding: const EdgeInsets.all(10),
|
||||
builderDelegate: PagedChildBuilderDelegate<Album>(
|
||||
itemBuilder: (context, item, index) {
|
||||
return AlbumCard(item);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,10 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/Album/AlbumCard.dart';
|
||||
import 'package:spotube/components/Artist/ArtistAlbumList.dart';
|
||||
import 'package:spotube/components/Artist/ArtistCard.dart';
|
||||
import 'package:spotube/components/LoaderShimmers/ShimmerArtistProfile.dart';
|
||||
import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
|
||||
@ -28,7 +27,6 @@ class ArtistProfile extends HookConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
SpotifyApi spotify = ref.watch(spotifyProvider);
|
||||
final scrollController = useScrollController();
|
||||
final parentScrollController = useScrollController();
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
final chipTextVariant = useBreakpointValue(
|
||||
@ -55,7 +53,7 @@ class ArtistProfile extends HookConsumerWidget {
|
||||
final isFollowingSnapshot =
|
||||
ref.watch(currentUserFollowsArtistQuery(artistId));
|
||||
final topTracksSnapshot = ref.watch(artistTopTracksQuery(artistId));
|
||||
final albums = ref.watch(artistAlbumsQuery(artistId));
|
||||
|
||||
final relatedArtists = ref.watch(artistRelatedArtistsQuery(artistId));
|
||||
|
||||
return SafeArea(
|
||||
@ -263,46 +261,12 @@ class ArtistProfile extends HookConsumerWidget {
|
||||
child: CircularProgressIndicator.adaptive()),
|
||||
),
|
||||
const SizedBox(height: 50),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"Albums",
|
||||
style: Theme.of(context).textTheme.headline4,
|
||||
),
|
||||
TextButton(
|
||||
child: const Text("See All"),
|
||||
onPressed: () {
|
||||
GoRouter.of(context).push(
|
||||
"/artist-album/$artistId",
|
||||
extra: data.name ?? "KRTX",
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
Text(
|
||||
"Albums",
|
||||
style: Theme.of(context).textTheme.headline4,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
albums.when(
|
||||
data: (albums) {
|
||||
return Scrollbar(
|
||||
controller: scrollController,
|
||||
child: SingleChildScrollView(
|
||||
controller: scrollController,
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: albums.items
|
||||
?.map((album) => AlbumCard(album))
|
||||
.toList() ??
|
||||
[],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
error: (error, stackTrack) =>
|
||||
Text("Failed to get Artist albums $error"),
|
||||
loading: () => const CircularProgressIndicator.adaptive(),
|
||||
),
|
||||
ArtistAlbumList(artistId),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
"Fans also likes",
|
||||
|
@ -1,14 +1,14 @@
|
||||
import 'package:fl_query_hooks/fl_query_hooks.dart';
|
||||
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:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/LoaderShimmers/ShimmerPlaybuttonCard.dart';
|
||||
import 'package:spotube/components/Playlist/PlaylistCard.dart';
|
||||
import 'package:spotube/components/Shared/NotFound.dart';
|
||||
import 'package:spotube/hooks/usePaginatedFutureProvider.dart';
|
||||
import 'package:spotube/components/Shared/Waypoint.dart';
|
||||
import 'package:spotube/models/Logger.dart';
|
||||
import 'package:spotube/provider/SpotifyDI.dart';
|
||||
import 'package:spotube/provider/SpotifyRequests.dart';
|
||||
|
||||
class CategoryCard extends HookConsumerWidget {
|
||||
@ -25,29 +25,20 @@ class CategoryCard extends HookConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final scrollController = useScrollController();
|
||||
final mounted = useIsMounted();
|
||||
|
||||
final pagingController =
|
||||
usePaginatedFutureProvider<Page<PlaylistSimple>, int, PlaylistSimple>(
|
||||
(pageKey) => categoryPlaylistsQuery(
|
||||
[
|
||||
category.id,
|
||||
pageKey,
|
||||
].join("/"),
|
||||
),
|
||||
ref: ref,
|
||||
firstPageKey: 0,
|
||||
onData: (page, pagingController, pageKey) {
|
||||
if (playlists != null && playlists?.isNotEmpty == true && mounted()) {
|
||||
return pagingController.appendLastPage(playlists!.toList());
|
||||
}
|
||||
if (page.isLast && page.items != null) {
|
||||
pagingController.appendLastPage(page.items!.toList());
|
||||
} else if (page.items != null) {
|
||||
pagingController.appendPage(page.items!.toList(), page.nextOffset);
|
||||
}
|
||||
},
|
||||
final spotify = ref.watch(spotifyProvider);
|
||||
final playlistQuery = useInfiniteQuery(
|
||||
job: categoryPlaylistsQueryJob(category.id!),
|
||||
externalData: spotify,
|
||||
);
|
||||
final hasNextPage = playlistQuery.pages.isEmpty
|
||||
? false
|
||||
: (playlistQuery.pages.last?.items?.length ?? 0) == 5;
|
||||
|
||||
final playlists = playlistQuery.pages
|
||||
.expand(
|
||||
(page) => page?.items ?? const Iterable.empty(),
|
||||
)
|
||||
.toList();
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
@ -62,8 +53,8 @@ class CategoryCard extends HookConsumerWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
pagingController.error != null
|
||||
? const Text("Something Went Wrong")
|
||||
playlistQuery.hasError
|
||||
? Text("Something Went Wrong\n${playlistQuery.errors.first}")
|
||||
: SizedBox(
|
||||
height: 245,
|
||||
child: ScrollConfiguration(
|
||||
@ -76,26 +67,21 @@ class CategoryCard extends HookConsumerWidget {
|
||||
child: Scrollbar(
|
||||
controller: scrollController,
|
||||
interactive: false,
|
||||
child: PagedListView<int, PlaylistSimple>(
|
||||
shrinkWrap: true,
|
||||
pagingController: pagingController,
|
||||
scrollController: scrollController,
|
||||
child: ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
builderDelegate:
|
||||
PagedChildBuilderDelegate<PlaylistSimple>(
|
||||
noItemsFoundIndicatorBuilder: (context) {
|
||||
return const NotFound();
|
||||
},
|
||||
firstPageProgressIndicatorBuilder: (context) {
|
||||
return const ShimmerPlaybuttonCard();
|
||||
},
|
||||
newPageProgressIndicatorBuilder: (context) {
|
||||
return const ShimmerPlaybuttonCard();
|
||||
},
|
||||
itemBuilder: (context, playlist, index) {
|
||||
return PlaylistCard(playlist);
|
||||
},
|
||||
),
|
||||
shrinkWrap: true,
|
||||
itemCount: playlists.length,
|
||||
itemBuilder: (context, index) {
|
||||
if (index == playlists.length - 1 && hasNextPage) {
|
||||
return Waypoint(
|
||||
onEnter: () {
|
||||
playlistQuery.fetchNextPage();
|
||||
},
|
||||
child: const ShimmerPlaybuttonCard(count: 1),
|
||||
);
|
||||
}
|
||||
return PlaylistCard(playlists[index]);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -1,9 +1,9 @@
|
||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||
import 'package:fl_query_hooks/fl_query_hooks.dart';
|
||||
import 'package:flutter/material.dart' hide Page;
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
import 'package:spotify/spotify.dart' hide Image, Player, Search;
|
||||
|
||||
import 'package:spotube/components/Category/CategoryCard.dart';
|
||||
@ -16,12 +16,14 @@ import 'package:spotube/components/Shared/ReplaceDownloadedFileDialog.dart';
|
||||
import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
|
||||
import 'package:spotube/components/Player/Player.dart';
|
||||
import 'package:spotube/components/Library/UserLibrary.dart';
|
||||
import 'package:spotube/components/Shared/Waypoint.dart';
|
||||
import 'package:spotube/hooks/useBreakpointValue.dart';
|
||||
import 'package:spotube/hooks/usePaginatedFutureProvider.dart';
|
||||
import 'package:spotube/hooks/useUpdateChecker.dart';
|
||||
import 'package:spotube/models/Logger.dart';
|
||||
import 'package:spotube/provider/Downloader.dart';
|
||||
import 'package:spotube/provider/SpotifyDI.dart';
|
||||
import 'package:spotube/provider/SpotifyRequests.dart';
|
||||
import 'package:spotube/provider/UserPreferences.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
|
||||
List<String> spotifyScopes = [
|
||||
@ -144,38 +146,43 @@ class Home extends HookConsumerWidget {
|
||||
left: 8.0,
|
||||
),
|
||||
child: HookBuilder(builder: (context) {
|
||||
final pagingController = usePaginatedFutureProvider<
|
||||
Page<Category>, int, Category>(
|
||||
(pageKey) => categoriesQuery(pageKey),
|
||||
ref: ref,
|
||||
firstPageKey: 0,
|
||||
onData: (categories, pagingController, pageKey) {
|
||||
final items = categories.items?.toList();
|
||||
if (pageKey == 0) {
|
||||
Category category = Category();
|
||||
category.id = "user-featured-playlists";
|
||||
category.name = "Featured";
|
||||
items?.insert(0, category);
|
||||
}
|
||||
if (categories.isLast && items != null) {
|
||||
pagingController.appendLastPage(items);
|
||||
} else if (categories.items != null) {
|
||||
pagingController.appendPage(
|
||||
items!, categories.nextOffset);
|
||||
}
|
||||
final spotify = ref.watch(spotifyProvider);
|
||||
final recommendationMarket = ref.watch(
|
||||
userPreferencesProvider
|
||||
.select((s) => s.recommendationMarket),
|
||||
);
|
||||
|
||||
final categoriesQuery = useInfiniteQuery(
|
||||
job: categoriesQueryJob,
|
||||
externalData: {
|
||||
"spotify": spotify,
|
||||
"recommendationMarket": recommendationMarket,
|
||||
},
|
||||
);
|
||||
return PagedListView(
|
||||
pagingController: pagingController,
|
||||
builderDelegate: PagedChildBuilderDelegate<Category>(
|
||||
firstPageProgressIndicatorBuilder: (_) =>
|
||||
const ShimmerCategories(),
|
||||
newPageProgressIndicatorBuilder: (_) =>
|
||||
const ShimmerCategories(),
|
||||
itemBuilder: (context, item, index) {
|
||||
return CategoryCard(item);
|
||||
},
|
||||
),
|
||||
|
||||
final categories = categoriesQuery.pages
|
||||
.expand<Category?>(
|
||||
(page) => page?.items ?? const Iterable.empty(),
|
||||
)
|
||||
.toList();
|
||||
|
||||
return ListView.builder(
|
||||
itemCount: categories.length,
|
||||
itemBuilder: (context, index) {
|
||||
final category = categories[index];
|
||||
if (category == null) return Container();
|
||||
if (index == categories.length - 1) {
|
||||
return Waypoint(
|
||||
onEnter: () {
|
||||
if (categoriesQuery.hasNextPage) {
|
||||
categoriesQuery.fetchNextPage();
|
||||
}
|
||||
},
|
||||
child: const ShimmerCategories(),
|
||||
);
|
||||
}
|
||||
return CategoryCard(category);
|
||||
},
|
||||
);
|
||||
}),
|
||||
),
|
||||
|
@ -1,35 +1,35 @@
|
||||
import 'package:fl_query_hooks/fl_query_hooks.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/Artist/ArtistCard.dart';
|
||||
import 'package:spotube/hooks/usePaginatedFutureProvider.dart';
|
||||
import 'package:spotube/models/Logger.dart';
|
||||
import 'package:spotube/components/Shared/Waypoint.dart';
|
||||
import 'package:spotube/provider/SpotifyDI.dart';
|
||||
import 'package:spotube/provider/SpotifyRequests.dart';
|
||||
|
||||
class UserArtists extends HookConsumerWidget {
|
||||
UserArtists({Key? key}) : super(key: key);
|
||||
final logger = getLogger(UserArtists);
|
||||
const UserArtists({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final pagingController =
|
||||
usePaginatedFutureProvider<CursorPage<Artist>, String, Artist>(
|
||||
(pageKey) => currentUserFollowingArtistsQuery(pageKey),
|
||||
ref: ref,
|
||||
firstPageKey: "",
|
||||
onData: (artists, pagingController, pageKey) {
|
||||
final items = artists.items!.toList();
|
||||
|
||||
if (artists.items != null && items.length < 15) {
|
||||
pagingController.appendLastPage(items);
|
||||
} else if (artists.items != null) {
|
||||
pagingController.appendPage(items, items.last.id);
|
||||
}
|
||||
},
|
||||
final artistQuery = useInfiniteQuery(
|
||||
job: currentUserFollowingArtistsQueryJob,
|
||||
externalData: ref.watch(spotifyProvider),
|
||||
);
|
||||
|
||||
return PagedGridView(
|
||||
final artists = useMemoized(
|
||||
() => artistQuery.pages
|
||||
.expand<Artist>((page) => page?.items ?? const Iterable.empty())
|
||||
.toList(),
|
||||
[artistQuery.pages]);
|
||||
|
||||
final hasNextPage = artistQuery.pages.isEmpty
|
||||
? false
|
||||
: (artistQuery.pages.last?.items?.length ?? 0) == 15;
|
||||
|
||||
return GridView.builder(
|
||||
itemCount: artists.length,
|
||||
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: 200,
|
||||
mainAxisExtent: 250,
|
||||
@ -37,12 +37,17 @@ class UserArtists extends HookConsumerWidget {
|
||||
mainAxisSpacing: 20,
|
||||
),
|
||||
padding: const EdgeInsets.all(10),
|
||||
pagingController: pagingController,
|
||||
builderDelegate: PagedChildBuilderDelegate<Artist>(
|
||||
itemBuilder: (context, item, index) {
|
||||
return ArtistCard(item);
|
||||
},
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
if (index == artists.length - 1 && hasNextPage) {
|
||||
return Waypoint(
|
||||
onEnter: () {
|
||||
artistQuery.fetchNextPage();
|
||||
},
|
||||
child: ArtistCard(artists[index]),
|
||||
);
|
||||
}
|
||||
return ArtistCard(artists[index]);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import 'package:spotube/components/Library/UserDownloads.dart';
|
||||
import 'package:spotube/components/Library/UserLocalTracks.dart';
|
||||
import 'package:spotube/components/Library/UserPlaylists.dart';
|
||||
import 'package:spotube/components/Shared/AnonymousFallback.dart';
|
||||
import 'package:spotube/components/Shared/ColoredTabBar.dart';
|
||||
|
||||
class UserLibrary extends ConsumerWidget {
|
||||
const UserLibrary({Key? key}) : super(key: key);
|
||||
@ -16,22 +17,25 @@ class UserLibrary extends ConsumerWidget {
|
||||
length: 5,
|
||||
child: SafeArea(
|
||||
child: Scaffold(
|
||||
appBar: const TabBar(
|
||||
isScrollable: true,
|
||||
tabs: [
|
||||
Tab(text: "Playlist"),
|
||||
Tab(text: "Downloads"),
|
||||
Tab(text: "Local"),
|
||||
Tab(text: "Artists"),
|
||||
Tab(text: "Album"),
|
||||
],
|
||||
appBar: ColoredTabBar(
|
||||
color: Theme.of(context).backgroundColor,
|
||||
child: const TabBar(
|
||||
isScrollable: true,
|
||||
tabs: [
|
||||
Tab(text: "Playlist"),
|
||||
Tab(text: "Downloads"),
|
||||
Tab(text: "Local"),
|
||||
Tab(text: "Artists"),
|
||||
Tab(text: "Album"),
|
||||
],
|
||||
),
|
||||
),
|
||||
body: TabBarView(children: [
|
||||
const AnonymousFallback(child: UserPlaylists()),
|
||||
const UserDownloads(),
|
||||
const UserLocalTracks(),
|
||||
body: const TabBarView(children: [
|
||||
AnonymousFallback(child: UserPlaylists()),
|
||||
UserDownloads(),
|
||||
UserLocalTracks(),
|
||||
AnonymousFallback(child: UserArtists()),
|
||||
const AnonymousFallback(child: UserAlbums()),
|
||||
AnonymousFallback(child: UserAlbums()),
|
||||
]),
|
||||
),
|
||||
),
|
||||
|
@ -60,6 +60,10 @@ class Search extends HookConsumerWidget {
|
||||
controller.value.text;
|
||||
},
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 7,
|
||||
),
|
||||
hintStyle: const TextStyle(height: 2),
|
||||
hintText: "Search...",
|
||||
),
|
||||
@ -93,7 +97,9 @@ class Search extends HookConsumerWidget {
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8, horizontal: 20),
|
||||
vertical: 8,
|
||||
horizontal: 20,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
|
14
lib/components/Shared/ColoredTabBar.dart
Normal file
14
lib/components/Shared/ColoredTabBar.dart
Normal file
@ -0,0 +1,14 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ColoredTabBar extends ColoredBox implements PreferredSizeWidget {
|
||||
final TabBar child;
|
||||
|
||||
const ColoredTabBar({
|
||||
required super.color,
|
||||
required this.child,
|
||||
super.key,
|
||||
}) : super(child: child);
|
||||
|
||||
@override
|
||||
Size get preferredSize => child.preferredSize;
|
||||
}
|
29
lib/components/Shared/Waypoint.dart
Normal file
29
lib/components/Shared/Waypoint.dart
Normal file
@ -0,0 +1,29 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:visibility_detector/visibility_detector.dart';
|
||||
|
||||
class Waypoint extends StatelessWidget {
|
||||
final void Function()? onEnter;
|
||||
final void Function()? onLeave;
|
||||
final Widget? child;
|
||||
const Waypoint({
|
||||
Key? key,
|
||||
this.onEnter,
|
||||
this.onLeave,
|
||||
this.child,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return VisibilityDetector(
|
||||
key: const Key("waypoint"),
|
||||
onVisibilityChanged: (info) {
|
||||
if (info.visibleFraction == 0) {
|
||||
onLeave?.call();
|
||||
} else if (info.visibleFraction > 0) {
|
||||
onEnter?.call();
|
||||
}
|
||||
},
|
||||
child: child ?? Container(),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:hookified_infinite_scroll_pagination/hookified_infinite_scroll_pagination.dart';
|
||||
|
||||
PagingController<P, ItemType> usePaginatedFutureProvider<T, P, ItemType>(
|
||||
FutureProvider<T> Function(P pageKey) createSnapshot, {
|
||||
required P firstPageKey,
|
||||
required WidgetRef ref,
|
||||
void Function(
|
||||
T,
|
||||
PagingController<P, ItemType> pagingController,
|
||||
P pageKey,
|
||||
)?
|
||||
onData,
|
||||
void Function(Object)? onError,
|
||||
void Function()? onLoading,
|
||||
}) {
|
||||
final currentPageKey = useState(firstPageKey);
|
||||
final snapshot = ref.watch(createSnapshot(currentPageKey.value));
|
||||
final pagingController = usePagingController<P, ItemType>(
|
||||
firstPageKey: firstPageKey,
|
||||
onPageRequest: (pageKey, pagingController) {
|
||||
if (currentPageKey.value != pageKey) {
|
||||
currentPageKey.value = pageKey;
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() {
|
||||
snapshot.whenOrNull(
|
||||
data: (data) =>
|
||||
onData?.call(data, pagingController, currentPageKey.value),
|
||||
error: (error, _) {
|
||||
pagingController.error = error;
|
||||
return onError?.call(error);
|
||||
},
|
||||
loading: onLoading,
|
||||
);
|
||||
return null;
|
||||
}, [currentPageKey, snapshot]);
|
||||
|
||||
return pagingController;
|
||||
}
|
@ -2,6 +2,7 @@ import 'dart:convert';
|
||||
|
||||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||
import 'package:fl_query/fl_query.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
@ -25,6 +26,7 @@ import 'package:spotube/themes/dark-theme.dart';
|
||||
import 'package:spotube/themes/light-theme.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
|
||||
final bowl = QueryBowl();
|
||||
void main() async {
|
||||
await Hive.initFlutter();
|
||||
Hive.registerAdapter(CacheTrackAdapter());
|
||||
@ -124,7 +126,10 @@ void main() async {
|
||||
),
|
||||
)
|
||||
],
|
||||
child: const Spotube(),
|
||||
child: QueryBowlScope(
|
||||
bowl: bowl,
|
||||
child: const Spotube(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
@ -1,7 +1,6 @@
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/Album/AlbumView.dart';
|
||||
import 'package:spotube/components/Artist/ArtistAlbumView.dart';
|
||||
import 'package:spotube/components/Artist/ArtistProfile.dart';
|
||||
import 'package:spotube/components/Home/Home.dart';
|
||||
import 'package:spotube/components/Login/Login.dart';
|
||||
@ -49,19 +48,6 @@ GoRouter createGoRouter() => GoRouter(
|
||||
return SpotubePage(child: ArtistProfile(state.params["id"]!));
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: "/artist-album/:id",
|
||||
pageBuilder: (context, state) {
|
||||
assert(state.params["id"] != null);
|
||||
assert(state.extra is String);
|
||||
return SpotubePage(
|
||||
child: ArtistAlbumView(
|
||||
state.params["id"]!,
|
||||
state.extra as String,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: "/playlist/:id",
|
||||
pageBuilder: (context, state) {
|
||||
|
@ -1,5 +1,6 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:fl_query/fl_query.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:spotube/models/Logger.dart';
|
||||
import 'package:spotube/models/LyricsModels.dart';
|
||||
@ -11,29 +12,35 @@ import 'package:collection/collection.dart';
|
||||
import 'package:spotube/utils/service_utils.dart';
|
||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||
|
||||
final categoriesQuery = FutureProvider.family<Page<Category>, int>(
|
||||
(ref, pageKey) {
|
||||
final spotify = ref.watch(spotifyProvider);
|
||||
final recommendationMarket = ref.watch(
|
||||
userPreferencesProvider.select((s) => s.recommendationMarket),
|
||||
);
|
||||
return spotify.categories
|
||||
final categoriesQueryJob =
|
||||
InfiniteQueryJob<Page<Category>, Map<String, dynamic>, int>(
|
||||
queryKey: "categories-query",
|
||||
initialParam: 0,
|
||||
getNextPageParam: (lastPage, lastParam) => lastPage.nextOffset,
|
||||
getPreviousPageParam: (lastPage, lastParam) => lastPage.nextOffset - 16,
|
||||
task: (queryKey, pageParam, data) async {
|
||||
final SpotifyApi spotify = data["spotify"] as SpotifyApi;
|
||||
final String recommendationMarket = data["recommendationMarket"];
|
||||
final categories = await spotify.categories
|
||||
.list(country: recommendationMarket)
|
||||
.getPage(15, pageKey);
|
||||
.getPage(15, pageParam);
|
||||
|
||||
return categories;
|
||||
},
|
||||
);
|
||||
|
||||
final categoryPlaylistsQuery =
|
||||
FutureProvider.family<Page<PlaylistSimple>, String>(
|
||||
(ref, value) {
|
||||
final spotify = ref.watch(spotifyProvider);
|
||||
final List data = value.split("/");
|
||||
final id = data.first;
|
||||
final pageKey = data.last;
|
||||
final categoryPlaylistsQueryJob =
|
||||
InfiniteQueryJob.withVariableKey<Page<PlaylistSimple>, SpotifyApi, int>(
|
||||
preQueryKey: "category-playlists",
|
||||
initialParam: 0,
|
||||
getNextPageParam: (lastPage, lastParam) => lastPage.nextOffset,
|
||||
getPreviousPageParam: (lastPage, lastParam) => lastPage.nextOffset - 6,
|
||||
task: (queryKey, pageKey, spotify) {
|
||||
final id = getVariable(queryKey);
|
||||
return (id != "user-featured-playlists"
|
||||
? spotify.playlists.getByCategoryId(id)
|
||||
: spotify.playlists.featured)
|
||||
.getPage(3, int.parse(pageKey));
|
||||
.getPage(5, pageKey);
|
||||
},
|
||||
);
|
||||
|
||||
@ -59,6 +66,18 @@ final currentUserFollowingArtistsQuery =
|
||||
},
|
||||
);
|
||||
|
||||
final currentUserFollowingArtistsQueryJob =
|
||||
InfiniteQueryJob<CursorPage<Artist>, SpotifyApi, String>(
|
||||
queryKey: "user-following-artists",
|
||||
initialParam: "",
|
||||
getNextPageParam: (lastPage, lastParam) => lastPage.after,
|
||||
getPreviousPageParam: (lastPage, lastParam) =>
|
||||
lastPage.metadata.previous ?? "",
|
||||
task: (queryKey, pageKey, spotify) {
|
||||
return spotify.me.following(FollowingType.artist).getPage(15, pageKey);
|
||||
},
|
||||
);
|
||||
|
||||
final artistProfileQuery = FutureProvider.family<Artist, String>(
|
||||
(ref, id) {
|
||||
final spotify = ref.watch(spotifyProvider);
|
||||
@ -90,6 +109,18 @@ final artistAlbumsQuery = FutureProvider.family<Page<Album>, String>(
|
||||
},
|
||||
);
|
||||
|
||||
final artistAlbumsQueryJob =
|
||||
InfiniteQueryJob.withVariableKey<Page<Album>, SpotifyApi, int>(
|
||||
preQueryKey: "artist-albums",
|
||||
initialParam: 0,
|
||||
getNextPageParam: (lastPage, lastParam) => lastPage.nextOffset,
|
||||
getPreviousPageParam: (lastPage, lastParam) => lastPage.nextOffset - 6,
|
||||
task: (queryKey, pageKey, spotify) {
|
||||
final id = getVariable(queryKey);
|
||||
return spotify.artists.albums(id).getPage(5, pageKey);
|
||||
},
|
||||
);
|
||||
|
||||
final artistRelatedArtistsQuery =
|
||||
FutureProvider.family<Iterable<Artist>, String>(
|
||||
(ref, id) {
|
||||
|
@ -15,6 +15,7 @@ ThemeData darkTheme({
|
||||
)
|
||||
],
|
||||
primaryColor: accentMaterialColor,
|
||||
splashFactory: NoSplash.splashFactory,
|
||||
primarySwatch: accentMaterialColor,
|
||||
backgroundColor: backgroundMaterialColor[900],
|
||||
scaffoldBackgroundColor: backgroundMaterialColor[900],
|
||||
@ -56,7 +57,7 @@ ThemeData darkTheme({
|
||||
),
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
onPrimary: accentMaterialColor[300],
|
||||
foregroundColor: accentMaterialColor[300],
|
||||
textStyle: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
|
@ -80,9 +80,10 @@ ThemeData lightTheme({
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
color: backgroundMaterialColor[50],
|
||||
),
|
||||
splashFactory: NoSplash.splashFactory,
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
onPrimary: accentMaterialColor[800],
|
||||
foregroundColor: accentMaterialColor[800],
|
||||
textStyle: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
|
@ -9,6 +9,7 @@ import audio_service
|
||||
import audio_session
|
||||
import audioplayers_darwin
|
||||
import bitsdojo_window_macos
|
||||
import connectivity_plus_macos
|
||||
import package_info_plus_macos
|
||||
import path_provider_macos
|
||||
import shared_preferences_macos
|
||||
@ -20,6 +21,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin"))
|
||||
AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin"))
|
||||
BitsdojoWindowPlugin.register(with: registry.registrar(forPlugin: "BitsdojoWindowPlugin"))
|
||||
ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin"))
|
||||
FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin"))
|
||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
|
70
pubspec.lock
70
pubspec.lock
@ -346,6 +346,48 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.16.0"
|
||||
connectivity_plus:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: connectivity_plus
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.3.7"
|
||||
connectivity_plus_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: connectivity_plus_linux
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.3.1"
|
||||
connectivity_plus_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: connectivity_plus_macos
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.4"
|
||||
connectivity_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: connectivity_plus_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
connectivity_plus_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: connectivity_plus_web
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.4"
|
||||
connectivity_plus_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: connectivity_plus_windows
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.2"
|
||||
convert:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -437,6 +479,20 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
fl_query:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: fl_query
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.3.0"
|
||||
fl_query_hooks:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: fl_query_hooks
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.3.0"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
@ -732,6 +788,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
nm:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: nm
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.5.0"
|
||||
oauth2:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1262,6 +1325,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
visibility_detector:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: visibility_detector
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.3.3"
|
||||
watcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -54,6 +54,9 @@ dependencies:
|
||||
badges: ^2.0.3
|
||||
mime: ^1.0.2
|
||||
metadata_god: ^0.2.0
|
||||
visibility_detector: ^0.3.3
|
||||
fl_query: ^0.3.0
|
||||
fl_query_hooks: ^0.3.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
@ -8,6 +8,7 @@
|
||||
|
||||
#include <audioplayers_windows/audioplayers_windows_plugin.h>
|
||||
#include <bitsdojo_window_windows/bitsdojo_window_plugin.h>
|
||||
#include <connectivity_plus_windows/connectivity_plus_windows_plugin.h>
|
||||
#include <metadata_god/metadata_god_plugin_c_api.h>
|
||||
#include <permission_handler_windows/permission_handler_windows_plugin.h>
|
||||
#include <url_launcher_windows/url_launcher_windows.h>
|
||||
@ -17,6 +18,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
registry->GetRegistrarForPlugin("AudioplayersWindowsPlugin"));
|
||||
BitsdojoWindowPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("BitsdojoWindowPlugin"));
|
||||
ConnectivityPlusWindowsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
|
||||
MetadataGodPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("MetadataGodPluginCApi"));
|
||||
PermissionHandlerWindowsPluginRegisterWithRegistrar(
|
||||
|
@ -5,6 +5,7 @@
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
audioplayers_windows
|
||||
bitsdojo_window_windows
|
||||
connectivity_plus_windows
|
||||
metadata_god
|
||||
permission_handler_windows
|
||||
url_launcher_windows
|
||||
|
Loading…
Reference in New Issue
Block a user