feat: improved caching based on riverpod (#1343)

* feat: add riverpod based favorite album provider

* feat: add album is saved, new releases and tracks providers

* feat: add artist related providers

* feat: add all categories providers

* feat: add lyrics provider

* feat: add playlist related providers

* feat: add search provider

* feat: add view and spotify friends provider

* feat: add playlist create and update and favorite handlers

* feat: use providers in home screen

* chore: fix dart lint issues

* feat: use new providers for playlist and albums screen

* feat: use providers in artist page

* feat: use providers on library page

* feat: use provider for playlist and album card and heart button

* feat: use provider in search page

* feat: use providers in generate playlist

* feat: use provider in lyrics screen

* feat: use provider for create playlist

* feat: use provider in add track dialog

* feat: use providers in remaining pages and remove fl_query

* fix: remove direct access to provider.value

* fix: glitching when loading

* fix: user album loading next page indicator

* feat: make many provider autoDispose after 5 minutes of no usage

* fix: ignore episodes in tracks
This commit is contained in:
Kingkor Roy Tirtho 2024-03-20 23:38:39 +06:00 committed by GitHub
parent 35e9920b51
commit 6673e5a8a8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
193 changed files with 3862 additions and 2954 deletions

View File

@ -2,6 +2,7 @@
"cmake.configureOnOpen": false, "cmake.configureOnOpen": false,
"cSpell.words": [ "cSpell.words": [
"acousticness", "acousticness",
"Buildless",
"danceability", "danceability",
"instrumentalness", "instrumentalness",
"Mpris", "Mpris",

170
.vscode/snippets.code-snippets vendored Normal file
View File

@ -0,0 +1,170 @@
{
"PaginatedState": {
"scope": "dart",
"prefix": "paginatedState",
"description": "Generate a PaginatedState",
"body": [
"class ${1:Model}State extends PaginatedState<${2:Model}> {",
" ${1:Model}State({",
" required super.items,",
" required super.offset,",
" required super.limit,",
" required super.hasMore,",
" });",
" ",
" @override",
" ${1:Model}State copyWith({",
" List<${2:Model}>? items,",
" int? offset,",
" int? limit,",
" bool? hasMore,",
" }) {",
" return ${1:Model}State(",
" items: items ?? this.items,",
" offset: offset ?? this.offset,",
" limit: limit ?? this.limit,",
" hasMore: hasMore ?? this.hasMore,",
" );",
" }",
"}"
]
},
"PaginatedAsyncNotifier": {
"scope": "dart",
"prefix": "paginatedAsyncNotifier",
"description": "Generate a PaginatedAsyncNotifier",
"body": [
"class ${1:NotifierName}Notifier extends PaginatedAsyncNotifier<${3:Item}, ${2:Model}State> {",
" ${1:NotifierName}Notifier() : super();",
" ",
" @override",
" fetch(int offset, int limit) async {",
" throw UnimplementedError();",
" }",
" ",
" @override",
" build() async {",
" throw UnimplementedError();",
" }",
"}"
]
},
"PaginaitedNotifierWithState": {
"scope": "dart",
"prefix": "paginatedNotifierWithState",
"description": "Generate a PaginatedNotifier with PaginatedState",
"body": [
"class $1State extends PaginatedState<$2> {",
" $1State({",
" required super.items,",
" required super.offset,",
" required super.limit,",
" required super.hasMore,",
" });",
" ",
" @override",
" $1State copyWith({",
" List<$2>? items,",
" int? offset,",
" int? limit,",
" bool? hasMore,",
" }) {",
" return $1State(",
" items: items ?? this.items,",
" offset: offset ?? this.offset,",
" limit: limit ?? this.limit,",
" hasMore: hasMore ?? this.hasMore,",
" );",
" }",
"}",
" ",
"class $1Notifier",
" extends PaginatedAsyncNotifier<$2, $1State> {",
" $1Notifier() : super();",
" ",
" @override",
" fetch(int offset, int limit) async {",
" throw UnimplementedError();",
" }",
" ",
" @override",
" build() async {",
" throw UnimplementedError();",
" }",
"}",
" ",
"final ${1/(.*)/${1:/camelcase}/}Provider = AsyncNotifierProvider<$1Notifier, $1State>(",
" ()=> $1Notifier(),",
");"
]
},
"FamilyPaginatedAsyncNotifier": {
"scope": "dart",
"prefix": "familyPaginatedAsyncNotifier",
"description": "Generate a FamilyPaginatedAsyncNotifier",
"body": [
"class ${1:NotifierName}Notifier extends FamilyPaginatedAsyncNotifier<${3:Item}, ${2:Model}State, {$4:Arg}> {",
" ${1:NotifierName}Notifier() : super();",
" ",
" @override",
" fetch(arg, offset, limit) async {",
" throw UnimplementedError();",
" }",
" ",
" @override",
" build(arg) async {",
" throw UnimplementedError();",
" }",
"}"
]
},
"FamilyPaginaitedNotifierWithState": {
"scope": "dart",
"prefix": "familyPaginatedNotifierWithState",
"description": "Generate a FamilyPaginatedAsyncNotifier with PaginatedState",
"body": [
"class $1State extends PaginatedState<$2> {",
" $1State({",
" required super.items,",
" required super.offset,",
" required super.limit,",
" required super.hasMore,",
" });",
" ",
" @override",
" $1State copyWith({",
" List<$2>? items,",
" int? offset,",
" int? limit,",
" bool? hasMore,",
" }) {",
" return $1State(",
" items: items ?? this.items,",
" offset: offset ?? this.offset,",
" limit: limit ?? this.limit,",
" hasMore: hasMore ?? this.hasMore,",
" );",
" }",
"}",
" ",
"class $1Notifier",
" extends FamilyPaginatedAsyncNotifier<$2, $1State, $3> {",
" $1Notifier() : super();",
" ",
" @override",
" fetch(arg, offset, limit) async {",
" throw UnimplementedError();",
" }",
" ",
" @override",
" build(arg) async {",
" throw UnimplementedError();",
" }",
"}",
" ",
"final ${1/(.*)/${1:/camelcase}/}Provider = AsyncNotifierProviderFamily<$1Notifier, $1State, $3>(",
" ()=> $1Notifier(),",
");"
]
},
}

View File

@ -25,6 +25,7 @@ linter:
# avoid_print: false # Uncomment to disable the `avoid_print` rule # avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
file_names: false file_names: false
avoid_renaming_method_parameters: false
# Additional information about this file can be found at # Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options # https://dart.dev/guides/language/analysis-options

View File

@ -4,6 +4,7 @@ import 'package:flutter/widgets.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart' hide Search; import 'package:spotify/spotify.dart' hide Search;
import 'package:spotube/models/spotify/recommendation_seeds.dart';
import 'package:spotube/pages/album/album.dart'; import 'package:spotube/pages/album/album.dart';
import 'package:spotube/pages/getting_started/getting_started.dart'; import 'package:spotube/pages/getting_started/getting_started.dart';
import 'package:spotube/pages/home/genres/genre_playlists.dart'; import 'package:spotube/pages/home/genres/genre_playlists.dart';
@ -96,8 +97,7 @@ final routerProvider = Provider((ref) {
path: "result", path: "result",
pageBuilder: (context, state) => SpotubePage( pageBuilder: (context, state) => SpotubePage(
child: PlaylistGenerateResultPage( child: PlaylistGenerateResultPage(
state: state: state.extra as GeneratePlaylistProviderInput,
state.extra as PlaylistGenerateResultRouteState,
), ),
), ),
), ),

View File

@ -1,15 +1,12 @@
import 'package:fl_query_hooks/fl_query_hooks.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.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/shared/playbutton_card.dart'; import 'package:spotube/components/shared/playbutton_card.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/infinite_query.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/spotify.dart';
import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/audio_player.dart';
import 'package:spotube/services/queries/album.dart';
import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/service_utils.dart';
import 'package:spotube/utils/type_conversion_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart';
@ -31,15 +28,12 @@ class AlbumCard extends HookConsumerWidget {
useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying; useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying;
final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier); final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier);
final queryClient = useQueryClient();
bool isPlaylistPlaying = useMemoized( bool isPlaylistPlaying = useMemoized(
() => playlist.containsCollection(album.id!), () => playlist.containsCollection(album.id!),
[playlist, album.id], [playlist, album.id],
); );
final updating = useState(false); final updating = useState(false);
final spotify = ref.watch(spotifyProvider);
final scaffoldMessenger = ScaffoldMessenger.maybeOf(context); final scaffoldMessenger = ScaffoldMessenger.maybeOf(context);
@ -50,23 +44,8 @@ class AlbumCard extends HookConsumerWidget {
TypeConversionUtils.simpleTrack_X_Track(track, album)) TypeConversionUtils.simpleTrack_X_Track(track, album))
.toList(); .toList();
} }
final job = AlbumQueries.tracksOfJob(album.id!); await ref.read(albumTracksProvider(album).future);
return ref.read(albumTracksProvider(album).notifier).fetchAll();
final query = queryClient.createInfiniteQuery(
job.queryKey,
(page) => job.task(page, (spotify: spotify, album: album)),
initialPage: 0,
nextPage: job.nextPage,
);
return await query.fetchAllTracks(
getAllTracks: () async {
final res = await spotify.albums.tracks(album.id!).all();
return res
.map((e) => TypeConversionUtils.simpleTrack_X_Track(e, album))
.toList();
},
);
} }
return PlaybuttonCard( return PlaybuttonCard(

View File

@ -1,38 +1,35 @@
import 'package:flutter/material.dart' hide Page; import 'package:flutter/material.dart' hide Page;
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/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart'; import 'package:spotube/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/models/logger.dart'; import 'package:spotube/models/logger.dart';
import 'package:spotube/services/queries/queries.dart'; import 'package:spotube/provider/spotify/spotify.dart';
class ArtistAlbumList extends HookConsumerWidget { class ArtistAlbumList extends HookConsumerWidget {
final String artistId; final String artistId;
ArtistAlbumList( ArtistAlbumList(
this.artistId, { this.artistId, {
Key? key, super.key,
}) : super(key: key); });
final logger = getLogger(ArtistAlbumList); final logger = getLogger(ArtistAlbumList);
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final albumsQuery = useQueries.artist.albumsOf(ref, artistId); final albumsQuery = ref.watch(artistAlbumsProvider(artistId));
final albumsQueryNotifier =
ref.watch(artistAlbumsProvider(artistId).notifier);
final albums = useMemoized(() { final albums = albumsQuery.asData?.value.items ?? [];
return albumsQuery.pages
.expand<Album>((page) => page.items ?? const Iterable.empty())
.toList();
}, [albumsQuery.pages]);
final theme = Theme.of(context); final theme = Theme.of(context);
return HorizontalPlaybuttonCardView<Album>( return HorizontalPlaybuttonCardView<Album>(
isLoadingNextPage: albumsQuery.isLoadingNextPage, isLoadingNextPage: albumsQuery.isLoadingNextPage,
hasNextPage: albumsQuery.hasNextPage, hasNextPage: albumsQuery.asData?.value.hasMore ?? false,
items: albums, items: albums,
onFetchMore: albumsQuery.fetchNext, onFetchMore: albumsQueryNotifier.fetchMore,
title: Text( title: Text(
context.l10n.albums, context.l10n.albums,
style: theme.textTheme.headlineSmall, style: theme.textTheme.headlineSmall,

View File

@ -14,7 +14,7 @@ import 'package:spotube/utils/type_conversion_utils.dart';
class ArtistCard extends HookConsumerWidget { class ArtistCard extends HookConsumerWidget {
final Artist artist; final Artist artist;
const ArtistCard(this.artist, {Key? key}) : super(key: key); const ArtistCard(this.artist, {super.key});
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {

View File

@ -8,9 +8,9 @@ import 'package:spotube/provider/authentication_provider.dart';
class TokenLoginForm extends HookConsumerWidget { class TokenLoginForm extends HookConsumerWidget {
final void Function()? onDone; final void Function()? onDone;
const TokenLoginForm({ const TokenLoginForm({
Key? key, super.key,
this.onDone, this.onDone,
}) : super(key: key); });
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {

View File

@ -1,35 +1,28 @@
import 'package:flutter/material.dart' hide Page; import 'package:flutter/material.dart' hide Page;
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:skeletonizer/skeletonizer.dart'; import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart'; import 'package:spotube/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/services/queries/queries.dart'; import 'package:spotube/provider/spotify/spotify.dart';
class HomeFeaturedSection extends HookConsumerWidget { class HomeFeaturedSection extends HookConsumerWidget {
const HomeFeaturedSection({Key? key}) : super(key: key); const HomeFeaturedSection({super.key});
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final featuredPlaylistsQuery = useQueries.playlist.featured(ref); final featuredPlaylists = ref.watch(featuredPlaylistsProvider);
final playlists = useMemoized( final featuredPlaylistsNotifier =
() => featuredPlaylistsQuery.pages ref.watch(featuredPlaylistsProvider.notifier);
.whereType<Page<PlaylistSimple>>()
.expand((page) => page.items ?? const <PlaylistSimple>[]),
[featuredPlaylistsQuery.pages],
);
final isLoadingFeaturedPlaylists = !featuredPlaylistsQuery.hasPageData &&
!featuredPlaylistsQuery.isLoadingNextPage;
return Skeletonizer( return Skeletonizer(
enabled: isLoadingFeaturedPlaylists, enabled: featuredPlaylists.isLoading,
child: HorizontalPlaybuttonCardView<PlaylistSimple>( child: HorizontalPlaybuttonCardView<PlaylistSimple>(
items: playlists.toList(), items: featuredPlaylists.asData?.value.items ?? [],
title: Text(context.l10n.featured), title: Text(context.l10n.featured),
isLoadingNextPage: featuredPlaylistsQuery.isLoadingNextPage, isLoadingNextPage: featuredPlaylists.isLoadingNextPage,
hasNextPage: featuredPlaylistsQuery.hasNextPage, hasNextPage: featuredPlaylists.asData?.value.hasMore ?? false,
onFetchMore: featuredPlaylistsQuery.fetchNext, onFetchMore: featuredPlaylistsNotifier.fetchMore,
), ),
); );
} }

View File

@ -1,4 +1,3 @@
import 'dart:ffi';
import 'dart:ui'; import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -8,15 +7,16 @@ import 'package:spotube/collections/fake.dart';
import 'package:spotube/components/home/sections/friends/friend_item.dart'; import 'package:spotube/components/home/sections/friends/friend_item.dart';
import 'package:spotube/hooks/utils/use_breakpoint_value.dart'; import 'package:spotube/hooks/utils/use_breakpoint_value.dart';
import 'package:spotube/models/spotify_friends.dart'; import 'package:spotube/models/spotify_friends.dart';
import 'package:spotube/services/queries/queries.dart'; import 'package:spotube/provider/spotify/spotify.dart';
class HomePageFriendsSection extends HookConsumerWidget { class HomePageFriendsSection extends HookConsumerWidget {
const HomePageFriendsSection({Key? key}) : super(key: key); const HomePageFriendsSection({super.key});
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final friendsQuery = useQueries.user.friendActivity(ref); final friendsQuery = ref.watch(friendsProvider);
final friends = friendsQuery.data?.friends ?? FakeData.friends.friends; final friends =
friendsQuery.asData?.value.friends ?? FakeData.friends.friends;
final groupCount = useBreakpointValue( final groupCount = useBreakpointValue(
sm: 3, sm: 3,
@ -51,8 +51,8 @@ class HomePageFriendsSection extends HookConsumerWidget {
}, },
); );
if (!friendsQuery.isLoading && if (friendsQuery.isLoading ||
(!friendsQuery.hasData || friendsQuery.data!.friends.isEmpty)) { friendsQuery.asData?.value.friends.isEmpty == true) {
return const SliverToBoxAdapter( return const SliverToBoxAdapter(
child: SizedBox.shrink(), child: SizedBox.shrink(),
); );

View File

@ -1,10 +1,8 @@
import 'package:fl_query_hooks/fl_query_hooks.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/shared/image/universal_image.dart'; import 'package:spotube/components/shared/image/universal_image.dart';
import 'package:spotube/models/spotify_friends.dart'; import 'package:spotube/models/spotify_friends.dart';
@ -13,9 +11,9 @@ import 'package:spotube/provider/spotify_provider.dart';
class FriendItem extends HookConsumerWidget { class FriendItem extends HookConsumerWidget {
final SpotifyFriendActivity friend; final SpotifyFriendActivity friend;
const FriendItem({ const FriendItem({
Key? key, super.key,
required this.friend, required this.friend,
}) : super(key: key); });
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
@ -24,7 +22,6 @@ class FriendItem extends HookConsumerWidget {
colorScheme: colorScheme, colorScheme: colorScheme,
) = Theme.of(context); ) = Theme.of(context);
final queryClient = useQueryClient();
final spotify = ref.watch(spotifyProvider); final spotify = ref.watch(spotifyProvider);
return Container( return Container(
@ -86,15 +83,11 @@ class FriendItem extends HookConsumerWidget {
..onTap = () async { ..onTap = () async {
context.push( context.push(
"/${friend.track.context.path}", "/${friend.track.context.path}",
extra: !friend.track.context.path extra:
.startsWith("album") !friend.track.context.path.startsWith("album")
? null ? null
: await queryClient.fetchQuery<Album, dynamic>( : await spotify.albums
"album/${friend.track.album.id}", .get(friend.track.context.id),
() => spotify.albums.get(
friend.track.album.id,
),
),
); );
}, },
), ),
@ -110,12 +103,7 @@ class FriendItem extends HookConsumerWidget {
recognizer: TapGestureRecognizer() recognizer: TapGestureRecognizer()
..onTap = () async { ..onTap = () async {
final album = final album =
await queryClient.fetchQuery<Album, dynamic>( await spotify.albums.get(friend.track.album.id);
"album/${friend.track.album.id}",
() => spotify.albums.get(
friend.track.album.id,
),
);
if (context.mounted) { if (context.mounted) {
context.push( context.push(
"/album/${friend.track.album.id}", "/album/${friend.track.album.id}",

View File

@ -13,28 +13,26 @@ import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/shared/image/universal_image.dart'; import 'package:spotube/components/shared/image/universal_image.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/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/services/queries/queries.dart';
class HomeGenresSection extends HookConsumerWidget { class HomeGenresSection extends HookConsumerWidget {
const HomeGenresSection({Key? key}) : super(key: key); const HomeGenresSection({super.key});
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final ThemeData(:textTheme, :colorScheme) = Theme.of(context); final ThemeData(:textTheme, :colorScheme) = Theme.of(context);
final mediaQuery = MediaQuery.of(context); final mediaQuery = MediaQuery.of(context);
final recommendationMarket = ref.watch( final categoriesQuery = ref.watch(categoriesProvider);
userPreferencesProvider.select((s) => s.recommendationMarket), final categories = useMemoized(
() =>
categoriesQuery.value
?.where((c) => (c.icons?.length ?? 0) > 0)
.take(mediaQuery.mdAndDown ? 6 : 10)
.toList() ??
<Category>[],
[mediaQuery.mdAndDown, categoriesQuery.value],
); );
final categoriesQuery =
useQueries.category.listAll(ref, recommendationMarket);
final categories = categoriesQuery.data
?.where((c) => (c.icons?.length ?? 0) > 0)
.take(mediaQuery.mdAndDown ? 6 : 10)
.toList() ??
<Category>[];
return SliverMainAxisGroup( return SliverMainAxisGroup(
slivers: [ slivers: [

View File

@ -2,19 +2,19 @@ import 'package:flutter/widgets.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/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart'; import 'package:spotube/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
import 'package:spotube/services/queries/queries.dart'; import 'package:spotube/provider/spotify/spotify.dart';
class HomeMadeForUserSection extends HookConsumerWidget { class HomeMadeForUserSection extends HookConsumerWidget {
const HomeMadeForUserSection({Key? key}) : super(key: key); const HomeMadeForUserSection({super.key});
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final madeForUser = useQueries.views.get(ref, "made-for-x-hub"); final madeForUser = ref.watch(viewProvider("made-for-x-hub"));
return SliverList.builder( return SliverList.builder(
itemCount: madeForUser.data?["content"]?["items"]?.length ?? 0, itemCount: madeForUser.value?["content"]?["items"]?.length ?? 0,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final item = madeForUser.data?["content"]?["items"]?[index]; final item = madeForUser.value?["content"]?["items"]?[index];
final playlists = item["content"]?["items"] final playlists = item["content"]?["items"]
?.where((itemL2) => itemL2["type"] == "playlist") ?.where((itemL2) => itemL2["type"] == "playlist")
.map((itemL2) => PlaylistSimple.fromJson(itemL2)) .map((itemL2) => PlaylistSimple.fromJson(itemL2))

View File

@ -1,56 +1,35 @@
import 'package:flutter/material.dart' hide Page; import 'package:flutter/material.dart' hide Page;
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/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart'; import 'package:spotube/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.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/provider/spotify/spotify.dart';
import 'package:spotube/utils/type_conversion_utils.dart';
class HomeNewReleasesSection extends HookConsumerWidget { class HomeNewReleasesSection extends HookConsumerWidget {
const HomeNewReleasesSection({Key? key}) : super(key: key); const HomeNewReleasesSection({super.key});
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final auth = ref.watch(AuthenticationNotifier.provider); final auth = ref.watch(AuthenticationNotifier.provider);
final newReleases = useQueries.album.newReleases(ref); final newReleases = ref.watch(albumReleasesProvider);
final userArtistsQuery = useQueries.artist.followedByMeAll(ref); final newReleasesNotifier = ref.read(albumReleasesProvider.notifier);
final userArtists =
userArtistsQuery.data?.map((s) => s.id!).toList() ?? const [];
final albums = useMemoized( final albums = ref.watch(userArtistAlbumReleasesProvider);
() {
final allReleases = newReleases.pages
.whereType<Page<AlbumSimple>>()
.expand((page) => page.items ?? const <AlbumSimple>[])
.map((album) => TypeConversionUtils.simpleAlbum_X_Album(album));
final userArtistReleases = allReleases.where((album) { if (auth == null ||
return album.artists newReleases.isLoading ||
?.any((artist) => userArtists.contains(artist.id!)) == newReleases.asData?.value.items.isEmpty == true) {
true; return const SizedBox.shrink();
}).toList(); }
if (userArtistReleases.isEmpty) return allReleases.toList();
return userArtistReleases;
},
[newReleases.pages],
);
final hasNewReleases = newReleases.hasPageData &&
userArtistsQuery.hasData &&
!newReleases.isLoadingNextPage;
if (auth == null || !hasNewReleases) return const SizedBox.shrink();
return HorizontalPlaybuttonCardView<Album>( return HorizontalPlaybuttonCardView<Album>(
items: albums, items: albums,
title: Text(context.l10n.new_releases), title: Text(context.l10n.new_releases),
isLoadingNextPage: newReleases.isLoadingNextPage, isLoadingNextPage: newReleases.isLoadingNextPage,
hasNextPage: newReleases.hasNextPage, hasNextPage: newReleases.asData?.value.hasMore ?? false,
onFetchMore: newReleases.fetchNext, onFetchMore: newReleasesNotifier.fetchMore,
); );
} }
} }

View File

@ -25,7 +25,7 @@ class MultiSelectField<T> extends HookWidget {
final bool enabled; final bool enabled;
const MultiSelectField({ const MultiSelectField({
Key? key, super.key,
required this.options, required this.options,
required this.selectedOptions, required this.selectedOptions,
required this.getValueForOption, required this.getValueForOption,
@ -36,7 +36,7 @@ class MultiSelectField<T> extends HookWidget {
this.dialogTitle, this.dialogTitle,
this.helperText, this.helperText,
this.enabled = true, this.enabled = true,
}) : super(key: key); });
Widget defaultSelectedOptionBuilder(T option) { Widget defaultSelectedOptionBuilder(T option) {
return Chip( return Chip(
@ -134,14 +134,14 @@ class _MultiSelectDialog<T> extends HookWidget {
final String? helperText; final String? helperText;
const _MultiSelectDialog({ const _MultiSelectDialog({
Key? key, super.key,
required this.dialogTitle, required this.dialogTitle,
required this.options, required this.options,
required this.getValueForOption, required this.getValueForOption,
this.optionBuilder, this.optionBuilder,
this.initialSelection = const [], this.initialSelection = const [],
this.helperText, this.helperText,
}) : super(key: key); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@ -20,12 +20,12 @@ class RecommendationAttributeDials extends HookWidget {
final double base; final double base;
const RecommendationAttributeDials({ const RecommendationAttributeDials({
Key? key, super.key,
required this.values, required this.values,
required this.onChanged, required this.onChanged,
required this.title, required this.title,
this.base = 1, this.base = 1,
}) : super(key: key); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@ -12,12 +12,12 @@ class RecommendationAttributeFields extends HookWidget {
final Map<String, RecommendationAttribute>? presets; final Map<String, RecommendationAttribute>? presets;
const RecommendationAttributeFields({ const RecommendationAttributeFields({
Key? key, super.key,
required this.values, required this.values,
required this.onChanged, required this.onChanged,
required this.title, required this.title,
this.presets, this.presets,
}) : super(key: key); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@ -26,7 +26,7 @@ class SeedsMultiAutocomplete<T extends Object> extends HookWidget {
final SelectedItemDisplayType selectedItemDisplayType; final SelectedItemDisplayType selectedItemDisplayType;
const SeedsMultiAutocomplete({ const SeedsMultiAutocomplete({
Key? key, super.key,
required this.seeds, required this.seeds,
required this.fetchSeeds, required this.fetchSeeds,
required this.autocompleteOptionBuilder, required this.autocompleteOptionBuilder,
@ -35,7 +35,7 @@ class SeedsMultiAutocomplete<T extends Object> extends HookWidget {
this.inputDecoration, this.inputDecoration,
this.enabled = true, this.enabled = true,
this.selectedItemDisplayType = SelectedItemDisplayType.wrap, this.selectedItemDisplayType = SelectedItemDisplayType.wrap,
}) : super(key: key); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@ -10,10 +10,10 @@ class SimpleTrackTile extends HookWidget {
final Track track; final Track track;
final VoidCallback? onDelete; final VoidCallback? onDelete;
const SimpleTrackTile({ const SimpleTrackTile({
Key? key, super.key,
required this.track, required this.track,
this.onDelete, this.onDelete,
}) : super(key: key); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@ -4,7 +4,6 @@ import 'package:collection/collection.dart';
import 'package:fuzzywuzzy/fuzzywuzzy.dart'; import 'package:fuzzywuzzy/fuzzywuzzy.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:skeletonizer/skeletonizer.dart'; import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/collections/fake.dart'; import 'package:spotube/collections/fake.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
@ -15,42 +14,38 @@ import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart';
import 'package:spotube/components/shared/waypoint.dart'; import 'package:spotube/components/shared/waypoint.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.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/provider/spotify/spotify.dart';
import 'package:spotube/utils/type_conversion_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart';
class UserAlbums extends HookConsumerWidget { class UserAlbums extends HookConsumerWidget {
const UserAlbums({Key? key}) : super(key: key); const UserAlbums({super.key});
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final auth = ref.watch(AuthenticationNotifier.provider); final auth = ref.watch(AuthenticationNotifier.provider);
final albumsQuery = useQueries.album.ofMine(ref); final albumsQuery = ref.watch(favoriteAlbumsProvider);
final albumsQueryNotifier = ref.watch(favoriteAlbumsProvider.notifier);
final controller = useScrollController(); final controller = useScrollController();
final searchText = useState(''); final searchText = useState('');
final allAlbums = useMemoized(
() => albumsQuery.pages
.expand((element) => element.items ?? <AlbumSimple>[]),
[albumsQuery.pages],
);
final albums = useMemoized(() { final albums = useMemoized(() {
if (searchText.value.isEmpty) { if (searchText.value.isEmpty) {
return allAlbums; return albumsQuery.asData?.value.items ?? [];
} }
return allAlbums return albumsQuery.asData?.value.items
.map((e) => ( .map((e) => (
weightedRatio(e.name!, searchText.value), weightedRatio(e.name!, searchText.value),
e, e,
)) ))
.sorted((a, b) => b.$1.compareTo(a.$1)) .sorted((a, b) => b.$1.compareTo(a.$1))
.where((e) => e.$1 > 50) .where((e) => e.$1 > 50)
.map((e) => e.$2) .map((e) => e.$2)
.toList(); .toList() ??
}, [allAlbums, searchText.value]); [];
}, [albumsQuery.value, searchText.value]);
if (auth == null) { if (auth == null) {
return const AnonymousFallback(); return const AnonymousFallback();
@ -60,7 +55,7 @@ class UserAlbums extends HookConsumerWidget {
return RefreshIndicator( return RefreshIndicator(
onRefresh: () async { onRefresh: () async {
await albumsQuery.refresh(); ref.invalidate(favoriteAlbumsProvider);
}, },
child: SafeArea( child: SafeArea(
child: Scaffold( child: Scaffold(
@ -85,7 +80,7 @@ class UserAlbums extends HookConsumerWidget {
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
controller: controller, controller: controller,
child: Skeletonizer( child: Skeletonizer(
enabled: albumsQuery.pages.isEmpty, enabled: albumsQuery.isLoading,
child: Center( child: Center(
child: Wrap( child: Wrap(
runSpacing: 20, runSpacing: 20,
@ -93,7 +88,8 @@ class UserAlbums extends HookConsumerWidget {
runAlignment: WrapAlignment.center, runAlignment: WrapAlignment.center,
crossAxisAlignment: WrapCrossAlignment.center, crossAxisAlignment: WrapCrossAlignment.center,
children: [ children: [
if (albumsQuery.pages.isEmpty) if (albumsQuery.value == null ||
albumsQuery.value!.items.isEmpty)
...List.generate( ...List.generate(
10, 10,
(index) => AlbumCard(FakeData.album), (index) => AlbumCard(FakeData.album),
@ -107,12 +103,16 @@ class UserAlbums extends HookConsumerWidget {
AlbumCard( AlbumCard(
TypeConversionUtils.simpleAlbum_X_Album(album), TypeConversionUtils.simpleAlbum_X_Album(album),
), ),
if (albums.isNotEmpty && albumsQuery.hasNextPage) if (albums.isNotEmpty &&
Waypoint( albumsQuery.asData?.value.hasMore == true)
controller: controller, Skeletonizer(
isGrid: true, enabled: true,
onTouchEdge: albumsQuery.fetchNext, child: Waypoint(
child: AlbumCard(FakeData.album), controller: controller,
isGrid: true,
onTouchEdge: albumsQueryNotifier.fetchMore,
child: AlbumCard(FakeData.album),
),
) )
], ],
), ),

View File

@ -13,22 +13,22 @@ import 'package:spotube/components/shared/fallbacks/not_found.dart';
import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart'; import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.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/provider/spotify/spotify.dart';
class UserArtists extends HookConsumerWidget { class UserArtists extends HookConsumerWidget {
const UserArtists({Key? key}) : super(key: key); const UserArtists({super.key});
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final theme = Theme.of(context); final theme = Theme.of(context);
final auth = ref.watch(AuthenticationNotifier.provider); final auth = ref.watch(AuthenticationNotifier.provider);
final artistQuery = useQueries.artist.followedByMeAll(ref); final artistQuery = ref.watch(followedArtistsProvider);
final searchText = useState(''); final searchText = useState('');
final filteredArtists = useMemoized(() { final filteredArtists = useMemoized(() {
final artists = artistQuery.data ?? []; final artists = artistQuery.asData?.value.items ?? [];
if (searchText.value.isEmpty) { if (searchText.value.isEmpty) {
return artists.toList(); return artists.toList();
@ -42,7 +42,7 @@ class UserArtists extends HookConsumerWidget {
.where((e) => e.$1 > 50) .where((e) => e.$1 > 50)
.map((e) => e.$2) .map((e) => e.$2)
.toList(); .toList();
}, [artistQuery.data, searchText.value]); }, [artistQuery.asData?.value.items, searchText.value]);
final controller = useScrollController(); final controller = useScrollController();
@ -66,7 +66,7 @@ class UserArtists extends HookConsumerWidget {
), ),
), ),
backgroundColor: theme.scaffoldBackgroundColor, backgroundColor: theme.scaffoldBackgroundColor,
body: artistQuery.data?.isEmpty == true body: artistQuery.asData?.value.items.isEmpty == true
? Padding( ? Padding(
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
child: Row( child: Row(
@ -80,7 +80,7 @@ class UserArtists extends HookConsumerWidget {
) )
: RefreshIndicator( : RefreshIndicator(
onRefresh: () async { onRefresh: () async {
await artistQuery.refresh(); ref.invalidate(followedArtistsProvider);
}, },
child: InterScrollbar( child: InterScrollbar(
controller: controller, controller: controller,
@ -109,8 +109,9 @@ class UserArtists extends HookConsumerWidget {
) )
] ]
: filteredArtists : filteredArtists
.mapIndexed((index, artist) => .mapIndexed(
ArtistCard(artist)) (index, artist) => ArtistCard(artist),
)
.toList(), .toList(),
), ),
), ),

View File

@ -7,7 +7,7 @@ import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/download_manager_provider.dart'; import 'package:spotube/provider/download_manager_provider.dart';
class UserDownloads extends HookConsumerWidget { class UserDownloads extends HookConsumerWidget {
const UserDownloads({Key? key}) : super(key: key); const UserDownloads({super.key});
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {

View File

@ -13,9 +13,9 @@ import 'package:spotube/utils/type_conversion_utils.dart';
class DownloadItem extends HookConsumerWidget { class DownloadItem extends HookConsumerWidget {
final Track track; final Track track;
const DownloadItem({ const DownloadItem({
Key? key, super.key,
required this.track, required this.track,
}) : super(key: key); });
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {

View File

@ -129,7 +129,7 @@ final localTracksProvider = FutureProvider<List<LocalTrack>>((ref) async {
}); });
class UserLocalTracks extends HookConsumerWidget { class UserLocalTracks extends HookConsumerWidget {
const UserLocalTracks({Key? key}) : super(key: key); const UserLocalTracks({super.key});
Future<void> playLocalTracks( Future<void> playLocalTracks(
WidgetRef ref, WidgetRef ref,
@ -178,7 +178,7 @@ class UserLocalTracks extends HookConsumerWidget {
FilledButton( FilledButton(
onPressed: trackSnapshot.value != null onPressed: trackSnapshot.value != null
? () async { ? () async {
if (trackSnapshot.value?.isNotEmpty == true) { if (trackSnapshot.asData?.value.isNotEmpty == true) {
if (!isPlaylistPlaying) { if (!isPlaylistPlaying) {
await playLocalTracks( await playLocalTracks(
ref, ref,
@ -217,7 +217,7 @@ class UserLocalTracks extends HookConsumerWidget {
FilledButton( FilledButton(
child: const Icon(SpotubeIcons.refresh), child: const Icon(SpotubeIcons.refresh),
onPressed: () { onPressed: () {
ref.refresh(localTracksProvider); ref.invalidate(localTracksProvider);
}, },
) )
], ],
@ -269,7 +269,7 @@ class UserLocalTracks extends HookConsumerWidget {
return Expanded( return Expanded(
child: RefreshIndicator( child: RefreshIndicator(
onRefresh: () async { onRefresh: () async {
ref.refresh(localTracksProvider); ref.invalidate(localTracksProvider);
}, },
child: InterScrollbar( child: InterScrollbar(
controller: controller, controller: controller,

View File

@ -17,10 +17,10 @@ import 'package:spotube/components/shared/waypoint.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/provider/authentication_provider.dart'; import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/services/queries/queries.dart'; import 'package:spotube/provider/spotify/spotify.dart';
class UserPlaylists extends HookConsumerWidget { class UserPlaylists extends HookConsumerWidget {
const UserPlaylists({Key? key}) : super(key: key); const UserPlaylists({super.key});
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
@ -28,13 +28,9 @@ class UserPlaylists extends HookConsumerWidget {
final auth = ref.watch(AuthenticationNotifier.provider); final auth = ref.watch(AuthenticationNotifier.provider);
final playlistsQuery = useQueries.playlist.ofMine(ref); final playlistsQuery = ref.watch(favoritePlaylistsProvider);
final playlistsQueryNotifier =
final pagePlaylists = useMemoized( ref.watch(favoritePlaylistsProvider.notifier);
() => playlistsQuery.pages
.expand((page) => page.items?.toList() ?? <PlaylistSimple>[]),
[playlistsQuery.pages],
);
final likedTracksPlaylist = useMemoized( final likedTracksPlaylist = useMemoized(
() => PlaylistSimple() () => PlaylistSimple()
@ -58,12 +54,12 @@ class UserPlaylists extends HookConsumerWidget {
if (searchText.value.isEmpty) { if (searchText.value.isEmpty) {
return [ return [
likedTracksPlaylist, likedTracksPlaylist,
...pagePlaylists, ...?playlistsQuery.asData?.value.items,
]; ];
} }
return [ return [
likedTracksPlaylist, likedTracksPlaylist,
...pagePlaylists, ...?playlistsQuery.asData?.value.items,
] ]
.map((e) => (weightedRatio(e.name!, searchText.value), e)) .map((e) => (weightedRatio(e.name!, searchText.value), e))
.sorted((a, b) => b.$1.compareTo(a.$1)) .sorted((a, b) => b.$1.compareTo(a.$1))
@ -71,7 +67,7 @@ class UserPlaylists extends HookConsumerWidget {
.map((e) => e.$2) .map((e) => e.$2)
.toList(); .toList();
}, },
[pagePlaylists, searchText.value], [playlistsQuery, searchText.value],
); );
final controller = useScrollController(); final controller = useScrollController();
@ -81,7 +77,9 @@ class UserPlaylists extends HookConsumerWidget {
} }
return RefreshIndicator( return RefreshIndicator(
onRefresh: playlistsQuery.refresh, onRefresh: () async {
ref.invalidate(favoritePlaylistsProvider);
},
child: SafeArea( child: SafeArea(
child: InterScrollbar( child: InterScrollbar(
controller: controller, controller: controller,
@ -132,14 +130,14 @@ class UserPlaylists extends HookConsumerWidget {
), ),
itemBuilder: (context, index) { itemBuilder: (context, index) {
if (playlists.isNotEmpty && index == playlists.length) { if (playlists.isNotEmpty && index == playlists.length) {
if (!playlistsQuery.hasNextPage) { if (playlistsQuery.asData?.value.hasMore != true) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
return Waypoint( return Waypoint(
controller: controller, controller: controller,
isGrid: true, isGrid: true,
onTouchEdge: playlistsQuery.fetchNext, onTouchEdge: playlistsQueryNotifier.fetchMore,
child: Skeletonizer( child: Skeletonizer(
enabled: true, enabled: true,
child: PlaylistCard(FakeData.playlistSimple), child: PlaylistCard(FakeData.playlistSimple),

View File

@ -17,7 +17,7 @@ class ZoomControls extends HookWidget {
final String unit; final String unit;
const ZoomControls({ const ZoomControls({
Key? key, super.key,
required this.value, required this.value,
required this.onChanged, required this.onChanged,
this.min, this.min,
@ -27,7 +27,7 @@ class ZoomControls extends HookWidget {
this.decreaseIcon = const Icon(SpotubeIcons.zoomOut), this.decreaseIcon = const Icon(SpotubeIcons.zoomOut),
this.direction = Axis.horizontal, this.direction = Axis.horizontal,
this.unit = "%", this.unit = "%",
}) : super(key: key); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@ -32,10 +32,10 @@ class PlayerView extends HookConsumerWidget {
final PanelController panelController; final PanelController panelController;
final ScrollController scrollController; final ScrollController scrollController;
const PlayerView({ const PlayerView({
Key? key, super.key,
required this.panelController, required this.panelController,
required this.scrollController, required this.scrollController,
}) : super(key: key); });
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {

View File

@ -8,7 +8,6 @@ import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/player/sibling_tracks_sheet.dart'; import 'package:spotube/components/player/sibling_tracks_sheet.dart';
import 'package:spotube/components/shared/adaptive/adaptive_pop_sheet_list.dart'; import 'package:spotube/components/shared/adaptive/adaptive_pop_sheet_list.dart';
import 'package:spotube/components/shared/heart_button.dart'; import 'package:spotube/components/shared/heart_button.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/duration.dart'; import 'package:spotube/extensions/duration.dart';
import 'package:spotube/models/local_track.dart'; import 'package:spotube/models/local_track.dart';
@ -29,13 +28,12 @@ class PlayerActions extends HookConsumerWidget {
this.floatingQueue = true, this.floatingQueue = true,
this.showQueue = true, this.showQueue = true,
this.extraActions, this.extraActions,
Key? key, super.key,
}) : super(key: key); });
final logger = getLogger(PlayerActions); final logger = getLogger(PlayerActions);
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final mediaQuery = MediaQuery.of(context);
final playlist = ref.watch(ProxyPlaylistNotifier.provider); final playlist = ref.watch(ProxyPlaylistNotifier.provider);
final isLocalTrack = playlist.activeTrack is LocalTrack; final isLocalTrack = playlist.activeTrack is LocalTrack;
ref.watch(downloadManagerProvider); ref.watch(downloadManagerProvider);

View File

@ -21,8 +21,8 @@ class PlayerControls extends HookConsumerWidget {
PlayerControls({ PlayerControls({
this.palette, this.palette,
this.compact = false, this.compact = false,
Key? key, super.key,
}) : super(key: key); });
final logger = getLogger(PlayerControls); final logger = getLogger(PlayerControls);
@ -256,7 +256,7 @@ class PlayerControls extends HookConsumerWidget {
onPressed: playlist.isFetching == true onPressed: playlist.isFetching == true
? null ? null
: () async { : () async {
switch (await audioPlayer.loopMode) { switch (audioPlayer.loopMode) {
case PlaybackLoopMode.all: case PlaybackLoopMode.all:
audioPlayer audioPlayer
.setLoopMode(PlaybackLoopMode.one); .setLoopMode(PlaybackLoopMode.one);

View File

@ -19,8 +19,8 @@ class PlayerOverlay extends HookConsumerWidget {
const PlayerOverlay({ const PlayerOverlay({
required this.albumArt, required this.albumArt,
Key? key, super.key,
}) : super(key: key); });
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {

View File

@ -22,8 +22,8 @@ class PlayerQueue extends HookConsumerWidget {
final bool floating; final bool floating;
const PlayerQueue({ const PlayerQueue({
this.floating = true, this.floating = true,
Key? key, super.key,
}) : super(key: key); });
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {

View File

@ -13,8 +13,7 @@ import 'package:spotube/utils/type_conversion_utils.dart';
class PlayerTrackDetails extends HookConsumerWidget { class PlayerTrackDetails extends HookConsumerWidget {
final String? albumArt; final String? albumArt;
final Color? color; final Color? color;
const PlayerTrackDetails({Key? key, this.albumArt, this.color}) const PlayerTrackDetails({super.key, this.albumArt, this.color});
: super(key: key);
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {

View File

@ -45,9 +45,9 @@ final sourceInfoToIconMap = {
class SiblingTracksSheet extends HookConsumerWidget { class SiblingTracksSheet extends HookConsumerWidget {
final bool floating; final bool floating;
const SiblingTracksSheet({ const SiblingTracksSheet({
Key? key, super.key,
this.floating = true, this.floating = true,
}) : super(key: key); });
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {

View File

@ -8,9 +8,9 @@ import 'package:spotube/provider/volume_provider.dart';
class VolumeSlider extends HookConsumerWidget { class VolumeSlider extends HookConsumerWidget {
final bool fullWidth; final bool fullWidth;
const VolumeSlider({ const VolumeSlider({
Key? key, super.key,
this.fullWidth = false, this.fullWidth = false,
}) : super(key: key); });
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {

View File

@ -1,14 +1,11 @@
import 'package:fl_query/fl_query.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.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/shared/playbutton_card.dart'; import 'package:spotube/components/shared/playbutton_card.dart';
import 'package:spotube/extensions/infinite_query.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/spotify.dart';
import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/audio_player.dart';
import 'package:spotube/services/queries/queries.dart';
import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/service_utils.dart';
import 'package:spotube/utils/type_conversion_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart';
@ -16,48 +13,30 @@ class PlaylistCard extends HookConsumerWidget {
final PlaylistSimple playlist; final PlaylistSimple playlist;
const PlaylistCard( const PlaylistCard(
this.playlist, { this.playlist, {
Key? key, super.key,
}) : super(key: key); });
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final playlistQueue = ref.watch(ProxyPlaylistNotifier.provider); final playlistQueue = ref.watch(ProxyPlaylistNotifier.provider);
final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier); final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier);
final playing = final playing =
useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying; useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying;
final queryClient = QueryClient.of(context);
final tracks = useState<List<TrackSimple>?>(null);
bool isPlaylistPlaying = useMemoized( bool isPlaylistPlaying = useMemoized(
() => playlistQueue.containsCollection(playlist.id!), () => playlistQueue.containsCollection(playlist.id!),
[playlistQueue, playlist.id], [playlistQueue, playlist.id],
); );
final updating = useState(false); final updating = useState(false);
final spotify = ref.watch(spotifyProvider); final me = ref.watch(meProvider);
final me = useQueries.user.me(ref);
Future<List<Track>> fetchAllTracks() async { Future<List<Track>> fetchAllTracks() async {
if (playlist.id == 'user-liked-tracks') { if (playlist.id == 'user-liked-tracks') {
return await queryClient.fetchQuery( return await ref.read(likedTracksProvider.future);
"user-liked-tracks",
() => useQueries.playlist.likedTracks(spotify),
) ??
[];
} }
final query = queryClient.createInfiniteQuery<List<Track>, dynamic, int>( await ref.read(playlistTracksProvider(playlist.id!).future);
"playlist-tracks/${playlist.id}",
(page) => useQueries.playlist.tracksOf(page, spotify, playlist.id!),
initialPage: 0,
nextPage: useQueries.playlist.tracksOfQueryNextPage,
);
return await query.fetchAllTracks( return ref.read(playlistTracksProvider(playlist.id!).notifier).fetchAll();
getAllTracks: () async {
final res =
await spotify.playlists.getTracksByPlaylistId(playlist.id!).all();
return res.toList();
},
);
} }
return PlaybuttonCard( return PlaybuttonCard(
@ -71,7 +50,8 @@ class PlaylistCard extends HookConsumerWidget {
isPlaying: isPlaylistPlaying, isPlaying: isPlaylistPlaying,
isLoading: isLoading:
(isPlaylistPlaying && playlistQueue.isFetching) || updating.value, (isPlaylistPlaying && playlistQueue.isFetching) || updating.value,
isOwner: playlist.owner?.id == me.data?.id && me.data?.id != null, isOwner: playlist.owner?.id == me.asData?.value.id &&
me.asData?.value.id != null,
onTap: () { onTap: () {
ServiceUtils.push( ServiceUtils.push(
context, context,
@ -94,7 +74,6 @@ class PlaylistCard extends HookConsumerWidget {
await playlistNotifier.load(fetchedTracks, autoPlay: true); await playlistNotifier.load(fetchedTracks, autoPlay: true);
playlistNotifier.addCollection(playlist.id!); playlistNotifier.addCollection(playlist.id!);
tracks.value = fetchedTracks;
} finally { } finally {
if (context.mounted) { if (context.mounted) {
updating.value = false; updating.value = false;
@ -112,10 +91,9 @@ class PlaylistCard extends HookConsumerWidget {
playlistNotifier.addTracks(fetchedTracks); playlistNotifier.addTracks(fetchedTracks);
playlistNotifier.addCollection(playlist.id!); playlistNotifier.addCollection(playlist.id!);
tracks.value = fetchedTracks;
if (context.mounted) { if (context.mounted) {
final snackbar = SnackBar( final snackbar = SnackBar(
content: Text("Added ${tracks.value?.length} tracks to queue"), content: Text("Added ${fetchedTracks.length} tracks to queue"),
action: SnackBarAction( action: SnackBarAction(
label: "Undo", label: "Undo",
onPressed: () { onPressed: () {

View File

@ -5,6 +5,7 @@ import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:form_validator/form_validator.dart'; import 'package:form_validator/form_validator.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
@ -13,10 +14,8 @@ import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/shared/image/universal_image.dart'; import 'package:spotube/components/shared/image/universal_image.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/provider/spotify/spotify.dart';
import 'package:spotube/provider/spotify_provider.dart'; import 'package:spotube/provider/spotify_provider.dart';
import 'package:spotube/services/mutations/mutations.dart';
import 'package:spotube/services/mutations/playlist.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 PlaylistCreateDialog extends HookConsumerWidget { class PlaylistCreateDialog extends HookConsumerWidget {
@ -24,10 +23,10 @@ class PlaylistCreateDialog extends HookConsumerWidget {
final List<String> trackIds; final List<String> trackIds;
final String? playlistId; final String? playlistId;
PlaylistCreateDialog({ PlaylistCreateDialog({
Key? key, super.key,
this.trackIds = const [], this.trackIds = const [],
this.playlistId, this.playlistId,
}) : super(key: key); });
final formKey = GlobalKey<FormState>(); final formKey = GlobalKey<FormState>();
@ -37,13 +36,16 @@ class PlaylistCreateDialog extends HookConsumerWidget {
child: Scaffold( child: Scaffold(
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
body: HookBuilder(builder: (context) { body: HookBuilder(builder: (context) {
final userPlaylists = useQueries.playlist.ofMine(ref); final userPlaylists = ref.watch(favoritePlaylistsProvider);
final playlist = ref.watch(playlistProvider(playlistId ?? ""));
final playlistNotifier =
ref.watch(playlistProvider(playlistId ?? "").notifier);
final updatingPlaylist = useMemoized( final updatingPlaylist = useMemoized(
() => userPlaylists.pages () => userPlaylists.asData?.value.items
.expand((p) => p.items ?? <PlaylistSimple>[])
.firstWhereOrNull((playlist) => playlist.id == playlistId), .firstWhereOrNull((playlist) => playlist.id == playlistId),
[ [
userPlaylists.pages, userPlaylists.asData?.value.items,
playlistId, playlistId,
], ],
); );
@ -84,28 +86,10 @@ class PlaylistCreateDialog extends HookConsumerWidget {
} }
}, [scaffold, l10n, theme]); }, [scaffold, l10n, theme]);
final playlistCreateMutation = useMutations.playlist.create(
ref,
trackIds: trackIds,
onData: (value) {
Navigator.pop(context);
},
onError: onError,
);
final playlistUpdateMutation = useMutations.playlist.update(
ref,
playlistId: playlistId,
onData: (value) {
Navigator.pop(context);
},
onError: onError,
);
Future<void> onCreate() async { Future<void> onCreate() async {
if (!formKey.currentState!.validate()) return; if (!formKey.currentState!.validate()) return;
final PlaylistCRUDVariables payload = ( final PlaylistInput payload = (
playlistName: playlistName.text, playlistName: playlistName.text,
collaborative: collaborative.value, collaborative: collaborative.value,
public: public.value, public: public.value,
@ -118,9 +102,14 @@ class PlaylistCreateDialog extends HookConsumerWidget {
); );
if (isUpdatingPlaylist) { if (isUpdatingPlaylist) {
await playlistUpdateMutation.mutate(payload); await playlistNotifier.modify(payload, onError);
} else { } else {
await playlistCreateMutation.mutate(payload); await playlistNotifier.create(payload, onError);
}
if (context.mounted &&
!ref.read(playlistProvider(playlistId ?? "")).hasError) {
context.pop();
} }
} }
@ -138,7 +127,7 @@ class PlaylistCreateDialog extends HookConsumerWidget {
}, },
), ),
FilledButton( FilledButton(
onPressed: onCreate, onPressed: playlist.isLoading ? null : onCreate,
child: Text( child: Text(
isUpdatingPlaylist isUpdatingPlaylist
? context.l10n.update ? context.l10n.update
@ -275,7 +264,7 @@ class PlaylistCreateDialog extends HookConsumerWidget {
} }
class PlaylistCreateDialogButton extends HookConsumerWidget { class PlaylistCreateDialogButton extends HookConsumerWidget {
const PlaylistCreateDialogButton({Key? key}) : super(key: key); const PlaylistCreateDialogButton({super.key});
showPlaylistDialog(BuildContext context, SpotifyApi spotify) { showPlaylistDialog(BuildContext context, SpotifyApi spotify) {
showDialog( showDialog(

View File

@ -25,7 +25,7 @@ import 'package:spotube/utils/platform.dart';
import 'package:spotube/utils/type_conversion_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart';
class BottomPlayer extends HookConsumerWidget { class BottomPlayer extends HookConsumerWidget {
BottomPlayer({Key? key}) : super(key: key); BottomPlayer({super.key});
final logger = getLogger(BottomPlayer); final logger = getLogger(BottomPlayer);
@override @override

View File

@ -15,10 +15,10 @@ import 'package:spotube/hooks/utils/use_brightness_value.dart';
import 'package:spotube/hooks/controllers/use_sidebarx_controller.dart'; import 'package:spotube/hooks/controllers/use_sidebarx_controller.dart';
import 'package:spotube/provider/download_manager_provider.dart'; import 'package:spotube/provider/download_manager_provider.dart';
import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; import 'package:spotube/provider/user_preferences/user_preferences_state.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:spotube/utils/type_conversion_utils.dart';
@ -31,8 +31,8 @@ class Sidebar extends HookConsumerWidget {
required this.selectedIndex, required this.selectedIndex,
required this.onSelectedIndexChanged, required this.onSelectedIndexChanged,
required this.child, required this.child,
Key? key, super.key,
}) : super(key: key); });
static Widget brandLogo() { static Widget brandLogo() {
return Container( return Container(
@ -195,7 +195,7 @@ class Sidebar extends HookConsumerWidget {
} }
class SidebarHeader extends HookWidget { class SidebarHeader extends HookWidget {
const SidebarHeader({Key? key}) : super(key: key); const SidebarHeader({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -234,15 +234,15 @@ class SidebarHeader extends HookWidget {
class SidebarFooter extends HookConsumerWidget { class SidebarFooter extends HookConsumerWidget {
const SidebarFooter({ const SidebarFooter({
Key? key, super.key,
}) : super(key: key); });
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final theme = Theme.of(context); final theme = Theme.of(context);
final mediaQuery = MediaQuery.of(context); final mediaQuery = MediaQuery.of(context);
final me = useQueries.user.me(ref); final me = ref.watch(meProvider);
final data = me.data; final data = me.asData?.value;
final avatarImg = TypeConversionUtils.image_X_UrlString( final avatarImg = TypeConversionUtils.image_X_UrlString(
data?.images, data?.images,

View File

@ -23,8 +23,8 @@ class SpotubeNavigationBar extends HookConsumerWidget {
const SpotubeNavigationBar({ const SpotubeNavigationBar({
required this.selectedIndex, required this.selectedIndex,
required this.onSelectedIndexChanged, required this.onSelectedIndexChanged,
Key? key, super.key,
}) : super(key: key); });
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {

View File

@ -8,9 +8,9 @@ import 'package:system_theme/system_theme.dart';
class SpotubeColor extends Color { class SpotubeColor extends Color {
final String name; final String name;
const SpotubeColor(int color, {required this.name}) : super(color); const SpotubeColor(super.color, {required this.name});
const SpotubeColor.from(int value, {required this.name}) : super(value); const SpotubeColor.from(super.value, {required this.name});
factory SpotubeColor.fromString(String string) { factory SpotubeColor.fromString(String string) {
final slices = string.split(":"); final slices = string.split(":");
@ -44,7 +44,7 @@ final Set<SpotubeColor> colorsMap = {
}; };
class ColorSchemePickerDialog extends HookConsumerWidget { class ColorSchemePickerDialog extends HookConsumerWidget {
const ColorSchemePickerDialog({Key? key}) : super(key: key); const ColorSchemePickerDialog({super.key});
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
@ -119,8 +119,8 @@ class ColorTile extends StatelessWidget {
this.onPressed, this.onPressed,
this.tooltip = "", this.tooltip = "",
this.isCompact = false, this.isCompact = false,
Key? key, super.key,
}) : super(key: key); });
factory ColorTile.compact({ factory ColorTile.compact({
required Color color, required Color color,

View File

@ -12,13 +12,13 @@ class Action extends StatelessWidget {
final bool isExpanded; final bool isExpanded;
final Color? backgroundColor; final Color? backgroundColor;
const Action({ const Action({
Key? key, super.key,
required this.icon, required this.icon,
required this.text, required this.text,
required this.onPressed, required this.onPressed,
this.isExpanded = true, this.isExpanded = true,
this.backgroundColor, this.backgroundColor,
}) : super(key: key); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@ -3,7 +3,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
class AnimateGradient extends HookWidget { class AnimateGradient extends HookWidget {
const AnimateGradient({ const AnimateGradient({
Key? key, super.key,
required this.primaryColors, required this.primaryColors,
required this.secondaryColors, required this.secondaryColors,
this.child, this.child,
@ -17,8 +17,7 @@ class AnimateGradient extends HookWidget {
this.reverse = true, this.reverse = true,
}) : assert(primaryColors.length >= 2), }) : assert(primaryColors.length >= 2),
assert(primaryColors.length == secondaryColors.length), assert(primaryColors.length == secondaryColors.length),
_controller = controller, _controller = controller;
super(key: key);
/// [controller]: pass this to have a fine control over the [Animation] /// [controller]: pass this to have a fine control over the [Animation]
final AnimationController? _controller; final AnimationController? _controller;

View File

@ -11,12 +11,12 @@ class CompactSearch extends HookWidget {
final Color? iconColor; final Color? iconColor;
const CompactSearch({ const CompactSearch({
Key? key, super.key,
this.onChanged, this.onChanged,
this.placeholder = "Search...", this.placeholder = "Search...",
this.icon = SpotubeIcons.search, this.icon = SpotubeIcons.search,
this.iconColor, this.iconColor,
}) : super(key: key); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@ -5,7 +5,7 @@ import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
class ConfirmDownloadDialog extends StatelessWidget { class ConfirmDownloadDialog extends StatelessWidget {
const ConfirmDownloadDialog({Key? key}) : super(key: key); const ConfirmDownloadDialog({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -82,7 +82,7 @@ class ConfirmDownloadDialog extends StatelessWidget {
class BulletPoint extends StatelessWidget { class BulletPoint extends StatelessWidget {
final String text; final String text;
const BulletPoint(this.text, {Key? key}) : super(key: key); const BulletPoint(this.text, {super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@ -5,7 +5,7 @@ import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
class PipedDownDialog extends HookConsumerWidget { class PipedDownDialog extends HookConsumerWidget {
const PipedDownDialog({Key? key}) : super(key: key); const PipedDownDialog({super.key});
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {

View File

@ -1,4 +1,3 @@
import 'package:fl_query_hooks/fl_query_hooks.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
@ -8,8 +7,7 @@ import 'package:spotify/spotify.dart';
import 'package:spotube/components/playlist/playlist_create_dialog.dart'; import 'package:spotube/components/playlist/playlist_create_dialog.dart';
import 'package:spotube/components/shared/image/universal_image.dart'; import 'package:spotube/components/shared/image/universal_image.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/spotify_provider.dart'; import 'package:spotube/provider/spotify/spotify.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 PlaylistAddTrackDialog extends HookConsumerWidget { class PlaylistAddTrackDialog extends HookConsumerWidget {
@ -19,33 +17,40 @@ class PlaylistAddTrackDialog extends HookConsumerWidget {
const PlaylistAddTrackDialog({ const PlaylistAddTrackDialog({
required this.tracks, required this.tracks,
required this.openFromPlaylist, required this.openFromPlaylist,
Key? key, super.key,
}) : super(key: key); });
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final ThemeData(:textTheme) = Theme.of(context); final ThemeData(:textTheme) = Theme.of(context);
final spotify = ref.watch(spotifyProvider); final userPlaylists = ref.watch(favoritePlaylistsProvider);
final userPlaylists = useQueries.playlist.ofMineAll(ref); final favoritePlaylistsNotifier =
ref.watch(favoritePlaylistsProvider.notifier);
final me = useQueries.user.me(ref); final me = ref.watch(meProvider);
final filteredPlaylists = useMemoized( final filteredPlaylists = useMemoized(
() => () =>
userPlaylists.data userPlaylists.asData?.value.items
?.where( .where(
(playlist) => (playlist) =>
playlist.owner?.id != null && playlist.owner?.id != null &&
playlist.owner!.id == me.data?.id && playlist.owner!.id == me.asData?.value.id &&
playlist.id != openFromPlaylist, playlist.id != openFromPlaylist,
) )
.toList() ?? .toList() ??
[], [],
[userPlaylists.data, me.data?.id, openFromPlaylist], [userPlaylists.asData?.value, me.asData?.value.id, openFromPlaylist],
); );
final playlistsCheck = useState(<String, bool>{}); final playlistsCheck = useState(<String, bool>{});
final queryClient = useQueryClient();
useEffect(() {
if (userPlaylists.asData?.value != null) {
favoritePlaylistsNotifier.fetchAll();
}
return null;
}, [userPlaylists.asData?.value]);
Future<void> onAdd() async { Future<void> onAdd() async {
final selectedPlaylists = playlistsCheck.value.entries final selectedPlaylists = playlistsCheck.value.entries
@ -54,21 +59,12 @@ class PlaylistAddTrackDialog extends HookConsumerWidget {
await Future.wait( await Future.wait(
selectedPlaylists.map( selectedPlaylists.map(
(playlistId) => spotify.playlists.addTracks( (playlistId) => favoritePlaylistsNotifier.addTracks(
tracks playlistId,
.map( tracks.map((e) => e.id!).toList(),
(track) => track.uri!, ),
)
.toList(),
playlistId),
), ),
).then((_) => Navigator.pop(context, true)); ).then((_) => Navigator.pop(context, true));
await queryClient.refreshQueries(
selectedPlaylists
.map((playlistId) => "playlist-tracks/$playlistId")
.toList(),
);
} }
return AlertDialog( return AlertDialog(

View File

@ -8,8 +8,7 @@ final replaceDownloadedFileState = StateProvider<bool?>((ref) => null);
class ReplaceDownloadedDialog extends ConsumerWidget { class ReplaceDownloadedDialog extends ConsumerWidget {
final Track track; final Track track;
const ReplaceDownloadedDialog({required this.track, Key? key}) const ReplaceDownloadedDialog({required this.track, super.key});
: super(key: key);
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {

View File

@ -13,9 +13,9 @@ import 'package:spotube/extensions/duration.dart';
class TrackDetailsDialog extends HookWidget { class TrackDetailsDialog extends HookWidget {
final Track track; final Track track;
const TrackDetailsDialog({ const TrackDetailsDialog({
Key? key, super.key,
required this.track, required this.track,
}) : super(key: key); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@ -10,12 +10,12 @@ class ExpandableSearchField extends StatelessWidget {
final FocusNode searchFocus; final FocusNode searchFocus;
const ExpandableSearchField({ const ExpandableSearchField({
Key? key, super.key,
required this.isFiltering, required this.isFiltering,
required this.onChangeFiltering, required this.onChangeFiltering,
required this.searchController, required this.searchController,
required this.searchFocus, required this.searchFocus,
}) : super(key: key); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -60,12 +60,12 @@ class ExpandableSearchButton extends StatelessWidget {
final ValueChanged<bool>? onPressed; final ValueChanged<bool>? onPressed;
const ExpandableSearchButton({ const ExpandableSearchButton({
Key? key, super.key,
required this.isFiltering, required this.isFiltering,
required this.searchFocus, required this.searchFocus,
this.icon = const Icon(SpotubeIcons.filter), this.icon = const Icon(SpotubeIcons.filter),
this.onPressed, this.onPressed,
}) : super(key: key); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@ -8,9 +8,9 @@ import 'package:spotube/utils/service_utils.dart';
class AnonymousFallback extends ConsumerWidget { class AnonymousFallback extends ConsumerWidget {
final Widget? child; final Widget? child;
const AnonymousFallback({ const AnonymousFallback({
Key? key, super.key,
this.child, this.child,
}) : super(key: key); });
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {

View File

@ -3,7 +3,7 @@ import 'package:spotube/collections/assets.gen.dart';
class NotFound extends StatelessWidget { class NotFound extends StatelessWidget {
final bool vertical; final bool vertical;
const NotFound({Key? key, this.vertical = false}) : super(key: key); const NotFound({super.key, this.vertical = false});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@ -1,5 +1,3 @@
import 'package:fl_query/fl_query.dart';
import 'package:fl_query_hooks/fl_query_hooks.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.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';
@ -8,8 +6,7 @@ import 'package:spotify/spotify.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/scrobbler_provider.dart'; import 'package:spotube/provider/scrobbler_provider.dart';
import 'package:spotube/services/mutations/mutations.dart'; import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/services/queries/queries.dart';
class HeartButton extends HookConsumerWidget { class HeartButton extends HookConsumerWidget {
final bool isLiked; final bool isLiked;
@ -23,8 +20,8 @@ class HeartButton extends HookConsumerWidget {
this.color, this.color,
this.tooltip, this.tooltip,
this.icon, this.icon,
Key? key, super.key,
}) : super(key: key); });
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
@ -60,90 +57,50 @@ class HeartButton extends HookConsumerWidget {
typedef UseTrackToggleLike = ({ typedef UseTrackToggleLike = ({
bool isLiked, bool isLiked,
Mutation<bool, dynamic, bool> toggleTrackLike, Future<void> Function(Track track) toggleTrackLike,
Query<User?, dynamic> me,
}); });
UseTrackToggleLike useTrackToggleLike(Track track, WidgetRef ref) { UseTrackToggleLike useTrackToggleLike(Track track, WidgetRef ref) {
final me = useQueries.user.me(ref); final savedTracks = ref.watch(likedTracksProvider);
final savedTracksNotifier = ref.watch(likedTracksProvider.notifier);
final savedTracks = useQueries.playlist.likedTracksQuery(ref);
final isLiked = useMemoized( final isLiked = useMemoized(
() => savedTracks.data?.any((element) => element.id == track.id) ?? false, () =>
[savedTracks.data, track.id], savedTracks.asData?.value.any((element) => element.id == track.id) ??
false,
[savedTracks.value, track.id],
); );
final mounted = useIsMounted();
final scrobblerNotifier = ref.read(scrobblerProvider.notifier); final scrobblerNotifier = ref.read(scrobblerProvider.notifier);
final toggleTrackLike = useMutations.track.toggleFavorite( return (
ref, isLiked: isLiked,
track.id!, toggleTrackLike: (track) async {
onMutate: (isLiked) { await savedTracksNotifier.toggleFavorite(track);
if (isLiked) {
savedTracks.setData( if (!isLiked) {
savedTracks.data
?.where((element) => element.id != track.id)
.toList() ??
[],
);
} else {
savedTracks.setData(
[
...?savedTracks.data,
track,
],
);
}
return isLiked;
},
onData: (isLiked, recoveryData) async {
await savedTracks.refresh();
if (isLiked) {
await scrobblerNotifier.love(track); await scrobblerNotifier.love(track);
} else { } else {
await scrobblerNotifier.unlove(track); await scrobblerNotifier.unlove(track);
} }
}, },
onError: (payload, isLiked) {
if (!mounted()) return;
if (isLiked != true) {
savedTracks.setData(
savedTracks.data
?.where((element) => element.id != track.id)
.toList() ??
[],
);
} else {
savedTracks.setData(
[
...?savedTracks.data,
track,
],
);
}
},
); );
return (isLiked: isLiked, toggleTrackLike: toggleTrackLike, me: me);
} }
class TrackHeartButton extends HookConsumerWidget { class TrackHeartButton extends HookConsumerWidget {
final Track track; final Track track;
const TrackHeartButton({ const TrackHeartButton({
Key? key, super.key,
required this.track, required this.track,
}) : super(key: key); });
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final savedTracks = useQueries.playlist.likedTracksQuery(ref); final savedTracks = ref.watch(likedTracksProvider);
final (:me, :isLiked, :toggleTrackLike) = useTrackToggleLike(track, ref); final me = ref.watch(meProvider);
final (:isLiked, :toggleTrackLike) = useTrackToggleLike(track, ref);
if (me.isLoading || !me.hasData) { if (me.isLoading) {
return const CircularProgressIndicator(); return const CircularProgressIndicator();
} }
@ -152,104 +109,9 @@ class TrackHeartButton extends HookConsumerWidget {
? context.l10n.remove_from_favorites ? context.l10n.remove_from_favorites
: context.l10n.save_as_favorite, : context.l10n.save_as_favorite,
isLiked: isLiked, isLiked: isLiked,
onPressed: savedTracks.hasData onPressed: savedTracks.value != null
? () { ? () {
toggleTrackLike.mutate(isLiked); toggleTrackLike(track);
}
: null,
);
}
}
class PlaylistHeartButton extends HookConsumerWidget {
final PlaylistSimple playlist;
final IconData? icon;
final ValueChanged<bool>? onData;
const PlaylistHeartButton({
required this.playlist,
Key? key,
this.icon,
this.onData,
}) : super(key: key);
@override
Widget build(BuildContext context, ref) {
final me = useQueries.user.me(ref);
final isLikedQuery = useQueries.playlist.doesUserFollow(
ref,
playlist.id!,
me.data?.id ?? '',
);
final togglePlaylistLike = useMutations.playlist.toggleFavorite(
ref,
playlist.id!,
refreshQueries: [
isLikedQuery.key,
],
onData: onData,
);
if (me.isLoading || !me.hasData) {
return const CircularProgressIndicator();
}
return HeartButton(
isLiked: isLikedQuery.data ?? false,
tooltip: isLikedQuery.data ?? false
? context.l10n.remove_from_favorites
: context.l10n.save_as_favorite,
color: Colors.white,
icon: icon,
onPressed: isLikedQuery.hasData
? () {
togglePlaylistLike.mutate(isLikedQuery.data!);
}
: null,
);
}
}
class AlbumHeartButton extends HookConsumerWidget {
final AlbumSimple album;
const AlbumHeartButton({
required this.album,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context, ref) {
final client = useQueryClient();
final me = useQueries.user.me(ref);
final albumIsSaved = useQueries.album.isSavedForMe(ref, album.id!);
final isLiked = albumIsSaved.data ?? false;
final toggleAlbumLike = useMutations.album.toggleFavorite(
ref,
album.id!,
refreshQueries: [albumIsSaved.key],
onData: (_, __) async {
await client.refreshInfiniteQueryAllPages("current-user-albums");
},
);
if (me.isLoading || !me.hasData) {
return const CircularProgressIndicator();
}
return HeartButton(
isLiked: isLiked,
tooltip: isLiked
? context.l10n.remove_from_favorites
: context.l10n.save_as_favorite,
color: Colors.white,
onPressed: albumIsSaved.hasData
? () {
toggleAlbumLike.mutate(isLiked);
} }
: null, : null,
); );

View File

@ -24,13 +24,12 @@ class HorizontalPlaybuttonCardView<T> extends HookWidget {
required this.hasNextPage, required this.hasNextPage,
required this.onFetchMore, required this.onFetchMore,
required this.isLoadingNextPage, required this.isLoadingNextPage,
Key? key, super.key,
}) : assert( }) : assert(
items is List<PlaylistSimple> || items is List<PlaylistSimple> ||
items is List<Album> || items is List<Album> ||
items is List<Artist>, items is List<Artist>,
), );
super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -85,11 +84,11 @@ class HorizontalPlaybuttonCardView<T> extends HookWidget {
itemBuilder: (context, index) { itemBuilder: (context, index) {
final item = items[index]; final item = items[index];
return switch (item.runtimeType) { return switch (item) {
PlaylistSimple => PlaylistSimple() =>
PlaylistCard(item as PlaylistSimple), PlaylistCard(item as PlaylistSimple),
Album => AlbumCard(item as Album), Album() => AlbumCard(item as Album),
Artist => Padding( Artist() => Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 12.0), horizontal: 12.0),
child: ArtistCard(item as Artist), child: ArtistCard(item as Artist),

View File

@ -7,8 +7,8 @@ class HoverBuilder extends HookWidget {
const HoverBuilder({ const HoverBuilder({
required this.builder, required this.builder,
this.permanentState, this.permanentState,
Key? key, super.key,
}) : super(key: key); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@ -20,8 +20,8 @@ class UniversalImage extends HookWidget {
this.placeholder, this.placeholder,
this.fit, this.fit,
this.scale = 1, this.scale = 1,
Key? key, super.key,
}) : super(key: key); });
static ImageProvider imageProvider( static ImageProvider imageProvider(
String path, { String path, {

View File

@ -11,13 +11,13 @@ class AnchorButton<T> extends HookWidget {
const AnchorButton( const AnchorButton(
this.text, { this.text, {
Key? key, super.key,
this.onTap, this.onTap,
this.textAlign, this.textAlign,
this.overflow, this.overflow,
this.maxLines, this.maxLines,
this.style = const TextStyle(), this.style = const TextStyle(),
}) : super(key: key); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@ -13,12 +13,12 @@ class Hyperlink extends StatelessWidget {
const Hyperlink( const Hyperlink(
this.text, this.text,
this.url, { this.url, {
Key? key, super.key,
this.textAlign, this.textAlign,
this.overflow, this.overflow,
this.style = const TextStyle(), this.style = const TextStyle(),
this.maxLines, this.maxLines,
}) : super(key: key); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@ -15,14 +15,14 @@ class LinkText<T> extends StatelessWidget {
const LinkText( const LinkText(
this.text, this.text,
this.route, { this.route, {
Key? key, super.key,
this.textAlign, this.textAlign,
this.extra, this.extra,
this.overflow, this.overflow,
this.style = const TextStyle(), this.style = const TextStyle(),
this.maxLines, this.maxLines,
this.push = false, this.push = false,
}) : super(key: key); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@ -27,7 +27,7 @@ class PageWindowTitleBar extends StatefulHookConsumerWidget
final Widget? title; final Widget? title;
const PageWindowTitleBar({ const PageWindowTitleBar({
Key? key, super.key,
this.actions, this.actions,
this.title, this.title,
this.toolbarOpacity = 1, this.toolbarOpacity = 1,
@ -42,7 +42,7 @@ class PageWindowTitleBar extends StatefulHookConsumerWidget
this.titleTextStyle, this.titleTextStyle,
this.titleWidth, this.titleWidth,
this.toolbarTextStyle, this.toolbarTextStyle,
}) : super(key: key); });
@override @override
Size get preferredSize => const Size.fromHeight(kToolbarHeight); Size get preferredSize => const Size.fromHeight(kToolbarHeight);
@ -107,9 +107,9 @@ class _PageWindowTitleBarState extends ConsumerState<PageWindowTitleBar> {
class WindowTitleBarButtons extends HookConsumerWidget { class WindowTitleBarButtons extends HookConsumerWidget {
final Color? foregroundColor; final Color? foregroundColor;
const WindowTitleBarButtons({ const WindowTitleBarButtons({
Key? key, super.key,
this.foregroundColor, this.foregroundColor,
}) : super(key: key); });
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
@ -277,14 +277,13 @@ class WindowButton extends StatelessWidget {
final VoidCallback? onPressed; final VoidCallback? onPressed;
WindowButton( WindowButton(
{Key? key, {super.key,
WindowButtonColors? colors, WindowButtonColors? colors,
this.builder, this.builder,
@required this.iconBuilder, @required this.iconBuilder,
this.padding, this.padding,
this.onPressed, this.onPressed,
this.animate = false}) this.animate = false}) {
: super(key: key) {
this.colors = colors ?? _defaultButtonColors; this.colors = colors ?? _defaultButtonColors;
} }
@ -350,49 +349,40 @@ class WindowButton extends StatelessWidget {
class MinimizeWindowButton extends WindowButton { class MinimizeWindowButton extends WindowButton {
MinimizeWindowButton( MinimizeWindowButton(
{Key? key, {super.key,
WindowButtonColors? colors, super.colors,
VoidCallback? onPressed, super.onPressed,
bool? animate}) bool? animate})
: super( : super(
key: key,
colors: colors,
animate: animate ?? false, animate: animate ?? false,
iconBuilder: (buttonContext) => iconBuilder: (buttonContext) =>
MinimizeIcon(color: buttonContext.iconColor), MinimizeIcon(color: buttonContext.iconColor),
onPressed: onPressed,
); );
} }
class MaximizeWindowButton extends WindowButton { class MaximizeWindowButton extends WindowButton {
MaximizeWindowButton( MaximizeWindowButton(
{Key? key, {super.key,
WindowButtonColors? colors, super.colors,
VoidCallback? onPressed, super.onPressed,
bool? animate}) bool? animate})
: super( : super(
key: key,
colors: colors,
animate: animate ?? false, animate: animate ?? false,
iconBuilder: (buttonContext) => iconBuilder: (buttonContext) =>
MaximizeIcon(color: buttonContext.iconColor), MaximizeIcon(color: buttonContext.iconColor),
onPressed: onPressed,
); );
} }
class RestoreWindowButton extends WindowButton { class RestoreWindowButton extends WindowButton {
RestoreWindowButton( RestoreWindowButton(
{Key? key, {super.key,
WindowButtonColors? colors, super.colors,
VoidCallback? onPressed, super.onPressed,
bool? animate}) bool? animate})
: super( : super(
key: key,
colors: colors,
animate: animate ?? false, animate: animate ?? false,
iconBuilder: (buttonContext) => iconBuilder: (buttonContext) =>
RestoreIcon(color: buttonContext.iconColor), RestoreIcon(color: buttonContext.iconColor),
onPressed: onPressed,
); );
} }
@ -404,17 +394,15 @@ final _defaultCloseButtonColors = WindowButtonColors(
class CloseWindowButton extends WindowButton { class CloseWindowButton extends WindowButton {
CloseWindowButton( CloseWindowButton(
{Key? key, {super.key,
WindowButtonColors? colors, WindowButtonColors? colors,
VoidCallback? onPressed, super.onPressed,
bool? animate}) bool? animate})
: super( : super(
key: key,
colors: colors ?? _defaultCloseButtonColors, colors: colors ?? _defaultCloseButtonColors,
animate: animate ?? false, animate: animate ?? false,
iconBuilder: (buttonContext) => iconBuilder: (buttonContext) =>
CloseIcon(color: buttonContext.iconColor), CloseIcon(color: buttonContext.iconColor),
onPressed: onPressed,
); );
} }
@ -423,7 +411,7 @@ class CloseWindowButton extends WindowButton {
/// Close /// Close
class CloseIcon extends StatelessWidget { class CloseIcon extends StatelessWidget {
final Color color; final Color color;
const CloseIcon({Key? key, required this.color}) : super(key: key); const CloseIcon({super.key, required this.color});
@override @override
Widget build(BuildContext context) => Align( Widget build(BuildContext context) => Align(
alignment: Alignment.topLeft, alignment: Alignment.topLeft,
@ -444,13 +432,13 @@ class CloseIcon extends StatelessWidget {
/// Maximize /// Maximize
class MaximizeIcon extends StatelessWidget { class MaximizeIcon extends StatelessWidget {
final Color color; final Color color;
const MaximizeIcon({Key? key, required this.color}) : super(key: key); const MaximizeIcon({super.key, required this.color});
@override @override
Widget build(BuildContext context) => _AlignedPaint(_MaximizePainter(color)); Widget build(BuildContext context) => _AlignedPaint(_MaximizePainter(color));
} }
class _MaximizePainter extends _IconPainter { class _MaximizePainter extends _IconPainter {
_MaximizePainter(Color color) : super(color); _MaximizePainter(super.color);
@override @override
void paint(Canvas canvas, Size size) { void paint(Canvas canvas, Size size) {
Paint p = getPaint(color); Paint p = getPaint(color);
@ -462,15 +450,15 @@ class _MaximizePainter extends _IconPainter {
class RestoreIcon extends StatelessWidget { class RestoreIcon extends StatelessWidget {
final Color color; final Color color;
const RestoreIcon({ const RestoreIcon({
Key? key, super.key,
required this.color, required this.color,
}) : super(key: key); });
@override @override
Widget build(BuildContext context) => _AlignedPaint(_RestorePainter(color)); Widget build(BuildContext context) => _AlignedPaint(_RestorePainter(color));
} }
class _RestorePainter extends _IconPainter { class _RestorePainter extends _IconPainter {
_RestorePainter(Color color) : super(color); _RestorePainter(super.color);
@override @override
void paint(Canvas canvas, Size size) { void paint(Canvas canvas, Size size) {
Paint p = getPaint(color); Paint p = getPaint(color);
@ -487,13 +475,13 @@ class _RestorePainter extends _IconPainter {
/// Minimize /// Minimize
class MinimizeIcon extends StatelessWidget { class MinimizeIcon extends StatelessWidget {
final Color color; final Color color;
const MinimizeIcon({Key? key, required this.color}) : super(key: key); const MinimizeIcon({super.key, required this.color});
@override @override
Widget build(BuildContext context) => _AlignedPaint(_MinimizePainter(color)); Widget build(BuildContext context) => _AlignedPaint(_MinimizePainter(color));
} }
class _MinimizePainter extends _IconPainter { class _MinimizePainter extends _IconPainter {
_MinimizePainter(Color color) : super(color); _MinimizePainter(super.color);
@override @override
void paint(Canvas canvas, Size size) { void paint(Canvas canvas, Size size) {
Paint p = getPaint(color); Paint p = getPaint(color);
@ -512,7 +500,7 @@ abstract class _IconPainter extends CustomPainter {
} }
class _AlignedPaint extends StatelessWidget { class _AlignedPaint extends StatelessWidget {
const _AlignedPaint(this.painter, {Key? key}) : super(key: key); const _AlignedPaint(this.painter);
final CustomPainter painter; final CustomPainter painter;
@override @override
@ -547,8 +535,7 @@ T? _ambiguate<T>(T? value) => value;
class MouseStateBuilder extends StatefulWidget { class MouseStateBuilder extends StatefulWidget {
final MouseStateBuilderCB builder; final MouseStateBuilderCB builder;
final VoidCallback? onPressed; final VoidCallback? onPressed;
const MouseStateBuilder({Key? key, required this.builder, this.onPressed}) const MouseStateBuilder({super.key, required this.builder, this.onPressed});
: super(key: key);
@override @override
_MouseStateBuilderState createState() => _MouseStateBuilderState(); _MouseStateBuilderState createState() => _MouseStateBuilderState();
} }

View File

@ -47,8 +47,7 @@ class ForceDraggableWidgetRenderBox extends RenderPointerListener {
/// To make [ForceDraggableWidget] work in [Scrollable] widgets /// To make [ForceDraggableWidget] work in [Scrollable] widgets
class PanelScrollPhysics extends ScrollPhysics { class PanelScrollPhysics extends ScrollPhysics {
final PanelController controller; final PanelController controller;
const PanelScrollPhysics({required this.controller, ScrollPhysics? parent}) const PanelScrollPhysics({required this.controller, super.parent});
: super(parent: parent);
@override @override
PanelScrollPhysics applyTo(ScrollPhysics? ancestor) { PanelScrollPhysics applyTo(ScrollPhysics? ancestor) {
return PanelScrollPhysics( return PanelScrollPhysics(

View File

@ -146,7 +146,7 @@ class SlidingUpPanel extends StatefulWidget {
final BoxDecoration? panelDecoration; final BoxDecoration? panelDecoration;
const SlidingUpPanel( const SlidingUpPanel(
{Key? key, {super.key,
this.body, this.body,
this.collapsed, this.collapsed,
this.minHeight = 100.0, this.minHeight = 100.0,
@ -176,8 +176,7 @@ class SlidingUpPanel extends StatefulWidget {
this.panelBuilder}) this.panelBuilder})
: assert(panelBuilder != null), : assert(panelBuilder != null),
assert(0 <= backdropOpacity && backdropOpacity <= 1.0), assert(0 <= backdropOpacity && backdropOpacity <= 1.0),
assert(snapPoint == null || 0 < snapPoint && snapPoint < 1.0), assert(snapPoint == null || 0 < snapPoint && snapPoint < 1.0);
super(key: key);
@override @override
SlidingUpPanelState createState() => SlidingUpPanelState(); SlidingUpPanelState createState() => SlidingUpPanelState();

View File

@ -43,8 +43,8 @@ class PlaybuttonCard extends HookWidget {
this.onAddToQueuePressed, this.onAddToQueuePressed,
this.onTap, this.onTap,
this.isOwner = false, this.isOwner = false,
Key? key, super.key,
}) : super(key: key); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@ -5,7 +5,7 @@ import 'package:gap/gap.dart';
import 'package:skeletonizer/skeletonizer.dart'; import 'package:skeletonizer/skeletonizer.dart';
class ShimmerLyrics extends HookWidget { class ShimmerLyrics extends HookWidget {
const ShimmerLyrics({Key? key}) : super(key: key); const ShimmerLyrics({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@ -11,8 +11,8 @@ class SortTracksDropdown extends StatelessWidget {
const SortTracksDropdown({ const SortTracksDropdown({
this.onChanged, this.onChanged,
this.value, this.value,
Key? key, super.key,
}) : super(key: key); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@ -5,7 +5,7 @@ import 'package:spotube/hooks/utils/use_brightness_value.dart';
class ThemedButtonsTabBar extends HookWidget implements PreferredSizeWidget { class ThemedButtonsTabBar extends HookWidget implements PreferredSizeWidget {
final List<Widget> tabs; final List<Widget> tabs;
const ThemedButtonsTabBar({Key? key, required this.tabs}) : super(key: key); const ThemedButtonsTabBar({super.key, required this.tabs});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@ -1,6 +1,5 @@
import 'dart:io'; import 'dart:io';
import 'package:fl_query/fl_query.dart';
import 'package:flutter/material.dart' hide Page; import 'package:flutter/material.dart' hide Page;
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
@ -23,9 +22,8 @@ import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/blacklist_provider.dart'; import 'package:spotube/provider/blacklist_provider.dart';
import 'package:spotube/provider/download_manager_provider.dart'; import 'package:spotube/provider/download_manager_provider.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/spotify.dart';
import 'package:spotube/provider/spotify_provider.dart'; import 'package:spotube/provider/spotify_provider.dart';
import 'package:spotube/services/mutations/mutations.dart';
import 'package:spotube/services/queries/search.dart';
import 'package:spotube/utils/type_conversion_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
@ -53,13 +51,13 @@ class TrackOptions extends HookConsumerWidget {
final ObjectRef<ValueChanged<RelativeRect>?>? showMenuCbRef; final ObjectRef<ValueChanged<RelativeRect>?>? showMenuCbRef;
final Widget? icon; final Widget? icon;
const TrackOptions({ const TrackOptions({
Key? key, super.key,
required this.track, required this.track,
this.showMenuCbRef, this.showMenuCbRef,
this.userPlaylist = false, this.userPlaylist = false,
this.playlistId, this.playlistId,
this.icon, this.icon,
}) : super(key: key); });
void actionShare(BuildContext context, Track track) { void actionShare(BuildContext context, Track track) {
final data = "https://open.spotify.com/track/${track.id}"; final data = "https://open.spotify.com/track/${track.id}";
@ -99,21 +97,10 @@ class TrackOptions extends HookConsumerWidget {
final playlist = ref.read(ProxyPlaylistNotifier.provider); final playlist = ref.read(ProxyPlaylistNotifier.provider);
final spotify = ref.read(spotifyProvider); final spotify = ref.read(spotifyProvider);
final query = "${track.name} Radio"; final query = "${track.name} Radio";
final pages = await QueryClient.of(context) final pages =
.fetchInfiniteQueryJob<List<Page>, dynamic, int, SearchParams>( await spotify.search.get(query, types: [SearchType.playlist]).first();
job: SearchQueries.queryJob(query),
args: (
spotify: spotify,
searchType: SearchType.playlist,
query: query,
),
) ??
[];
final radios = pages final radios = pages.map((e) => e.items).toList().cast<PlaylistSimple>();
.expand((e) => e.items?.toList() ?? <PlaylistSimple>[])
.toList()
.cast<PlaylistSimple>();
final artists = track.artists!.map((e) => e.name); final artists = track.artists!.map((e) => e.name);
@ -176,6 +163,7 @@ class TrackOptions extends HookConsumerWidget {
ref.watch(downloadManagerProvider); ref.watch(downloadManagerProvider);
final downloadManager = ref.watch(downloadManagerProvider.notifier); final downloadManager = ref.watch(downloadManagerProvider.notifier);
final blacklist = ref.watch(BlackListNotifier.provider); final blacklist = ref.watch(BlackListNotifier.provider);
final me = ref.watch(meProvider);
final favorites = useTrackToggleLike(track, ref); final favorites = useTrackToggleLike(track, ref);
@ -190,10 +178,8 @@ class TrackOptions extends HookConsumerWidget {
); );
final removingTrack = useState<String?>(null); final removingTrack = useState<String?>(null);
final removeTrack = useMutations.playlist.removeTrackOf( final favoritePlaylistsNotifier =
ref, ref.watch(favoritePlaylistsProvider.notifier);
playlistId ?? "",
);
final isInQueue = useMemoized(() { final isInQueue = useMemoized(() {
if (playlist.activeTrack == null) return false; if (playlist.activeTrack == null) return false;
@ -220,7 +206,7 @@ class TrackOptions extends HookConsumerWidget {
break; break;
case TrackOptionValue.delete: case TrackOptionValue.delete:
await File((track as LocalTrack).path).delete(); await File((track as LocalTrack).path).delete();
ref.refresh(localTracksProvider); ref.invalidate(localTracksProvider);
break; break;
case TrackOptionValue.addToQueue: case TrackOptionValue.addToQueue:
await playback.addTrack(track); await playback.addTrack(track);
@ -257,14 +243,15 @@ class TrackOptions extends HookConsumerWidget {
); );
break; break;
case TrackOptionValue.favorite: case TrackOptionValue.favorite:
favorites.toggleTrackLike.mutate(favorites.isLiked); favorites.toggleTrackLike(track);
break; break;
case TrackOptionValue.addToPlaylist: case TrackOptionValue.addToPlaylist:
actionAddToPlaylist(context, track); actionAddToPlaylist(context, track);
break; break;
case TrackOptionValue.removeFromPlaylist: case TrackOptionValue.removeFromPlaylist:
removingTrack.value = track.uri; removingTrack.value = track.uri;
removeTrack.mutate(track.uri!); favoritePlaylistsNotifier
.removeTracks(playlistId ?? "", [track.id!]);
break; break;
case TrackOptionValue.blacklist: case TrackOptionValue.blacklist:
if (isBlackListed) { if (isBlackListed) {
@ -328,7 +315,7 @@ class TrackOptions extends HookConsumerWidget {
), ),
], ],
children: switch (track.runtimeType) { children: switch (track.runtimeType) {
LocalTrack => [ LocalTrack() => [
PopSheetEntry( PopSheetEntry(
value: TrackOptionValue.delete, value: TrackOptionValue.delete,
leading: const Icon(SpotubeIcons.trash), leading: const Icon(SpotubeIcons.trash),
@ -361,7 +348,7 @@ class TrackOptions extends HookConsumerWidget {
leading: const Icon(SpotubeIcons.queueRemove), leading: const Icon(SpotubeIcons.queueRemove),
title: Text(context.l10n.remove_from_queue), title: Text(context.l10n.remove_from_queue),
), ),
if (favorites.me.hasData) if (me.value != null)
PopSheetEntry( PopSheetEntry(
value: TrackOptionValue.favorite, value: TrackOptionValue.favorite,
leading: favorites.isLiked leading: favorites.isLiked
@ -391,10 +378,7 @@ class TrackOptions extends HookConsumerWidget {
if (userPlaylist && auth != null) if (userPlaylist && auth != null)
PopSheetEntry( PopSheetEntry(
value: TrackOptionValue.removeFromPlaylist, value: TrackOptionValue.removeFromPlaylist,
leading: (removeTrack.isMutating || !removeTrack.hasData) && leading: const Icon(SpotubeIcons.removeFilled),
removingTrack.value == track.uri
? const CircularProgressIndicator()
: const Icon(SpotubeIcons.removeFilled),
title: Text(context.l10n.remove_from_playlist), title: Text(context.l10n.remove_from_playlist),
), ),
PopSheetEntry( PopSheetEntry(

View File

@ -32,7 +32,7 @@ class TrackTile extends HookConsumerWidget {
final List<Widget>? leadingActions; final List<Widget>? leadingActions;
const TrackTile({ const TrackTile({
Key? key, super.key,
this.index, this.index,
required this.track, required this.track,
this.selected = false, this.selected = false,
@ -42,7 +42,7 @@ class TrackTile extends HookConsumerWidget {
this.userPlaylist = false, this.userPlaylist = false,
this.playlistId, this.playlistId,
this.leadingActions, this.leadingActions,
}) : super(key: key); });
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {

View File

@ -19,7 +19,7 @@ import 'package:spotube/utils/service_utils.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart'; import 'package:very_good_infinite_list/very_good_infinite_list.dart';
class TrackViewBodySection extends HookConsumerWidget { class TrackViewBodySection extends HookConsumerWidget {
const TrackViewBodySection({Key? key}) : super(key: key); const TrackViewBodySection({super.key});
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {

View File

@ -13,10 +13,10 @@ class TrackViewBodyHeaders extends HookConsumerWidget {
final FocusNode searchFocus; final FocusNode searchFocus;
const TrackViewBodyHeaders({ const TrackViewBodyHeaders({
Key? key, super.key,
required this.isFiltering, required this.isFiltering,
required this.searchFocus, required this.searchFocus,
}) : super(key: key); });
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {

View File

@ -13,7 +13,7 @@ import 'package:spotube/provider/user_preferences/user_preferences_provider.dart
import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; import 'package:spotube/provider/user_preferences/user_preferences_state.dart';
class TrackViewBodyOptions extends HookConsumerWidget { class TrackViewBodyOptions extends HookConsumerWidget {
const TrackViewBodyOptions({Key? key}) : super(key: key); const TrackViewBodyOptions({super.key});
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {

View File

@ -1,18 +1,18 @@
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:spotube/services/queries/queries.dart'; import 'package:spotube/provider/spotify/spotify.dart';
bool useIsUserPlaylist(WidgetRef ref, String playlistId) { bool useIsUserPlaylist(WidgetRef ref, String playlistId) {
final userPlaylistsQuery = useQueries.playlist.ofMineAll(ref); final userPlaylistsQuery = ref.watch(favoritePlaylistsProvider);
final me = useQueries.user.me(ref); final me = ref.watch(meProvider);
return useMemoized( return useMemoized(
() => () =>
userPlaylistsQuery.data?.any((e) => userPlaylistsQuery.asData?.value.items.any((e) =>
e.id == playlistId && e.id == playlistId &&
me.data != null && me.value != null &&
e.owner?.id == me.data?.id) ?? e.owner?.id == me.asData?.value.id) ??
false, false,
[userPlaylistsQuery.data, playlistId, me.data], [userPlaylistsQuery.value, playlistId, me.value],
); );
} }

View File

@ -14,7 +14,7 @@ import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/hooks/utils/use_palette_color.dart'; import 'package:spotube/hooks/utils/use_palette_color.dart';
class TrackViewFlexHeader extends HookConsumerWidget { class TrackViewFlexHeader extends HookConsumerWidget {
const TrackViewFlexHeader({Key? key}) : super(key: key); const TrackViewFlexHeader({super.key});
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {

View File

@ -12,7 +12,7 @@ import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
class TrackViewHeaderActions extends HookConsumerWidget { class TrackViewHeaderActions extends HookConsumerWidget {
const TrackViewHeaderActions({Key? key}) : super(key: key); const TrackViewHeaderActions({super.key});
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {

View File

@ -15,10 +15,10 @@ class TrackViewHeaderButtons extends HookConsumerWidget {
final PaletteColor color; final PaletteColor color;
final bool compact; final bool compact;
const TrackViewHeaderButtons({ const TrackViewHeaderButtons({
Key? key, super.key,
required this.color, required this.color,
this.compact = false, this.compact = false,
}) : super(key: key); });
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {

View File

@ -10,7 +10,7 @@ import 'package:spotube/components/shared/tracks_view/sections/body/track_view_b
import 'package:spotube/components/shared/tracks_view/track_view_props.dart'; import 'package:spotube/components/shared/tracks_view/track_view_props.dart';
class TrackView extends HookConsumerWidget { class TrackView extends HookConsumerWidget {
const TrackView({Key? key}) : super(key: key); const TrackView({super.key});
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {

View File

@ -1,6 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'package:fl_query/fl_query.dart';
import 'package:flutter/material.dart' hide Page; import 'package:flutter/material.dart' hide Page;
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
@ -19,19 +18,6 @@ class PaginationProps {
required this.onRefresh, required this.onRefresh,
}); });
factory PaginationProps.fromQuery(
InfiniteQuery<List<Track>, dynamic, int> query, {
required Future<List<Track>> Function() onFetchAll,
}) {
return PaginationProps(
hasNextPage: query.hasNextPage,
isLoading: query.isLoadingNextPage,
onFetchMore: query.fetchNext,
onFetchAll: onFetchAll,
onRefresh: query.refreshAll,
);
}
@override @override
operator ==(Object other) { operator ==(Object other) {
return other is PaginationProps && return other is PaginationProps &&

View File

@ -11,12 +11,12 @@ class Waypoint extends HookWidget {
final bool isGrid; final bool isGrid;
const Waypoint({ const Waypoint({
Key? key, super.key,
required this.controller, required this.controller,
this.isGrid = false, this.isGrid = false,
this.onTouchEdge, this.onTouchEdge,
this.child, this.child,
}) : super(key: key); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@ -1,34 +0,0 @@
import 'package:fl_query/fl_query.dart';
import 'package:spotify/spotify.dart';
extension FetchAllTracks on InfiniteQuery<List<Track>, dynamic, int> {
Future<List<Track>> fetchAllTracks({
required Future<List<Track>> Function() getAllTracks,
}) async {
if (pages.isNotEmpty && !hasNextPage) {
return pages.expand((page) => page).toList();
}
final tracks = await getAllTracks();
final numOfPages = (tracks.length / 20).round();
final Map<int, List<Track>> pagedTracks = {};
for (var i = 0; i < numOfPages; i++) {
if (i == numOfPages - 1) {
final pageTracks = tracks.sublist(i * 20);
pagedTracks[i] = pageTracks;
break;
}
final pageTracks = tracks.sublist(i * 20, (i + 1) * 20);
pagedTracks[i] = pageTracks;
}
for (final group in pagedTracks.entries) {
setPageData(group.key, group.value);
}
return tracks.toList();
}
}

View File

@ -1,10 +1,8 @@
import 'dart:async'; import 'dart:async';
import 'package:app_links/app_links.dart'; import 'package:app_links/app_links.dart';
import 'package:fl_query_hooks/fl_query_hooks.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:spotube/collections/routes.dart'; import 'package:spotube/collections/routes.dart';
import 'package:spotube/provider/spotify_provider.dart'; import 'package:spotube/provider/spotify_provider.dart';
import 'package:flutter_sharing_intent/flutter_sharing_intent.dart'; import 'package:flutter_sharing_intent/flutter_sharing_intent.dart';
@ -17,8 +15,6 @@ final linkStream = appLinks.allStringLinkStream.asBroadcastStream();
void useDeepLinking(WidgetRef ref) { void useDeepLinking(WidgetRef ref) {
// single instance no worries // single instance no worries
final spotify = ref.watch(spotifyProvider); final spotify = ref.watch(spotifyProvider);
final queryClient = useQueryClient();
final router = ref.watch(routerProvider); final router = ref.watch(routerProvider);
useEffect(() { useEffect(() {
@ -32,10 +28,7 @@ void useDeepLinking(WidgetRef ref) {
case "album": case "album":
router.push( router.push(
"/album/${url.pathSegments.last}", "/album/${url.pathSegments.last}",
extra: await queryClient.fetchQuery<Album, dynamic>( extra: await spotify.albums.get(url.pathSegments.last),
"album/${url.pathSegments.last}",
() => spotify.albums.get(url.pathSegments.last),
),
); );
break; break;
case "artist": case "artist":
@ -44,10 +37,7 @@ void useDeepLinking(WidgetRef ref) {
case "playlist": case "playlist":
router.push( router.push(
"/playlist/${url.pathSegments.last}", "/playlist/${url.pathSegments.last}",
extra: await queryClient.fetchQuery<Playlist, dynamic>( extra: await spotify.playlists.get(url.pathSegments.last),
"playlist/${url.pathSegments.last}",
() => spotify.playlists.get(url.pathSegments.last),
),
); );
break; break;
case "track": case "track":
@ -78,10 +68,7 @@ void useDeepLinking(WidgetRef ref) {
case "spotify:album": case "spotify:album":
await router.push( await router.push(
"/album/$endSegment", "/album/$endSegment",
extra: await queryClient.fetchQuery<Album, dynamic>( extra: await spotify.albums.get(endSegment),
"album/$endSegment",
() => spotify.albums.get(endSegment),
),
); );
break; break;
case "spotify:artist": case "spotify:artist":
@ -93,10 +80,7 @@ void useDeepLinking(WidgetRef ref) {
case "spotify:playlist": case "spotify:playlist":
await router.push( await router.push(
"/playlist/$endSegment", "/playlist/$endSegment",
extra: await queryClient.fetchQuery<Playlist, dynamic>( extra: await spotify.playlists.get(endSegment),
"playlist/$endSegment",
() => spotify.playlists.get(endSegment),
),
); );
break; break;
default: default:
@ -108,5 +92,5 @@ void useDeepLinking(WidgetRef ref) {
mediaStream?.cancel(); mediaStream?.cancel();
subscription.cancel(); subscription.cancel();
}; };
}, [spotify, queryClient]); }, [spotify]);
} }

View File

@ -1,5 +1,4 @@
import 'package:catcher_2/catcher_2.dart'; import 'package:catcher_2/catcher_2.dart';
import 'package:fl_query_hooks/fl_query_hooks.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
@ -8,7 +7,6 @@ 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/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/audio_player.dart';
import 'package:spotube/services/queries/search.dart';
void useEndlessPlayback(WidgetRef ref) { void useEndlessPlayback(WidgetRef ref) {
final auth = ref.watch(AuthenticationNotifier.provider); final auth = ref.watch(AuthenticationNotifier.provider);
@ -18,7 +16,6 @@ void useEndlessPlayback(WidgetRef ref) {
final endlessPlayback = final endlessPlayback =
ref.watch(userPreferencesProvider.select((s) => s.endlessPlayback)); ref.watch(userPreferencesProvider.select((s) => s.endlessPlayback));
final queryClient = useQueryClient();
useEffect( useEffect(
() { () {
@ -32,16 +29,8 @@ void useEndlessPlayback(WidgetRef ref) {
final track = playlist.tracks.last; final track = playlist.tracks.last;
final query = "${track.name} Radio"; final query = "${track.name} Radio";
final pages = await queryClient.fetchInfiniteQueryJob<List<Page>, final pages = await spotify.search
dynamic, int, SearchParams>( .get(query, types: [SearchType.playlist]).first();
job: SearchQueries.queryJob(query),
args: (
spotify: spotify,
searchType: SearchType.playlist,
query: query
),
) ??
[];
final radios = pages final radios = pages
.expand((e) => e.items?.toList() ?? <PlaylistSimple>[]) .expand((e) => e.items?.toList() ?? <PlaylistSimple>[])
@ -94,7 +83,6 @@ void useEndlessPlayback(WidgetRef ref) {
[ [
spotify, spotify,
playback, playback,
queryClient,
playlist.tracks, playlist.tracks,
endlessPlayback, endlessPlayback,
auth, auth,

View File

@ -39,8 +39,8 @@ class _AutoScrollControllerHook extends Hook<AutoScrollController> {
this.copyTagsFrom, this.copyTagsFrom,
this.suggestedRowHeight, this.suggestedRowHeight,
this.debugLabel, this.debugLabel,
List<Object?>? keys, super.keys,
}) : super(keys: keys); });
final double initialScrollOffset; final double initialScrollOffset;
final bool keepScrollOffset; final bool keepScrollOffset;

View File

@ -44,8 +44,8 @@ class _PackageInfoHook<PageKeyType, ItemType> extends Hook<PackageInfo> {
required this.version, required this.version,
required this.buildNumber, required this.buildNumber,
this.buildSignature = '', this.buildSignature = '',
List<Object?>? keys, super.keys,
}) : super(keys: keys); });
@override @override
HookState<PackageInfo, Hook<PackageInfo>> createState() => HookState<PackageInfo, Hook<PackageInfo>> createState() =>

View File

@ -24,8 +24,8 @@ class _SidebarXControllerHook extends Hook<SidebarXController> {
const _SidebarXControllerHook({ const _SidebarXControllerHook({
required this.selectedIndex, required this.selectedIndex,
this.extended, this.extended,
List<Object?>? keys, super.keys,
}) : super(keys: keys); });
final int selectedIndex; final int selectedIndex;
final bool? extended; final bool? extended;

View File

@ -1,53 +0,0 @@
import 'dart:async';
import 'package:fl_query/fl_query.dart';
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:spotify/spotify.dart';
import 'package:spotube/provider/spotify_provider.dart';
InfiniteQuery<DataType, ErrorType, PageType>
useSpotifyInfiniteQuery<DataType, ErrorType, PageType>(
String queryKey,
FutureOr<DataType?> Function(PageType page, SpotifyApi spotify) queryFn, {
required WidgetRef ref,
required InfiniteQueryNextPage<DataType, PageType> nextPage,
required PageType initialPage,
RetryConfig? retryConfig,
RefreshConfig? refreshConfig,
JsonConfig<DataType>? jsonConfig,
ValueChanged<PageEvent<DataType, PageType>>? onData,
ValueChanged<PageEvent<ErrorType, PageType>>? onError,
bool enabled = true,
List<Object?>? keys,
}) {
final spotify = ref.watch(spotifyProvider);
final query = useInfiniteQuery<DataType, ErrorType, PageType>(
queryKey,
(page) => queryFn(page, spotify),
nextPage: nextPage,
initialPage: initialPage,
retryConfig: retryConfig,
refreshConfig: refreshConfig,
jsonConfig: jsonConfig,
onData: onData,
onError: onError,
enabled: enabled,
keys: keys,
);
useEffect(() {
return ref.listenManual(
spotifyProvider,
(previous, next) {
if (previous != next) {
query.refreshAll();
}
},
).close;
}, [query]);
return query;
}

View File

@ -1,36 +0,0 @@
import 'package:fl_query/fl_query.dart';
import 'package:fl_query_hooks/fl_query_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/provider/spotify_provider.dart';
Mutation<DataType, ErrorType, VariablesType>
useSpotifyMutation<DataType, ErrorType, VariablesType, RecoveryType>(
String mutationKey,
Future<DataType> Function(VariablesType variables, SpotifyApi spotify)
mutationFn, {
required WidgetRef ref,
RetryConfig? retryConfig,
MutationOnDataFn<DataType, RecoveryType>? onData,
MutationOnErrorFn<ErrorType, RecoveryType>? onError,
MutationOnMutationFn<VariablesType, RecoveryType>? onMutate,
List<String>? refreshQueries,
List<String>? refreshInfiniteQueries,
List<Object?>? keys,
}) {
final spotify = ref.watch(spotifyProvider);
final mutation =
useMutation<DataType, ErrorType, VariablesType, RecoveryType>(
mutationKey,
(variables) => mutationFn(variables, spotify),
retryConfig: retryConfig,
onData: onData,
onError: onError,
onMutate: onMutate,
refreshQueries: refreshQueries,
refreshInfiniteQueries: refreshInfiniteQueries,
keys: keys,
);
return mutation;
}

View File

@ -1,52 +0,0 @@
import 'dart:async';
import 'package:fl_query/fl_query.dart';
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:spotify/spotify.dart';
import 'package:spotube/provider/spotify_provider.dart';
typedef SpotifyQueryFn<DataType> = FutureOr<DataType?> Function(
SpotifyApi spotify);
Query<DataType, ErrorType> useSpotifyQuery<DataType, ErrorType>(
final String queryKey,
final SpotifyQueryFn<DataType> queryFn, {
required WidgetRef ref,
final DataType? initial,
final RetryConfig? retryConfig,
final RefreshConfig? refreshConfig,
final JsonConfig<DataType>? jsonConfig,
final ValueChanged<DataType>? onData,
final ValueChanged<ErrorType>? onError,
final bool enabled = true,
}) {
final spotify = ref.watch(spotifyProvider);
final query = useQuery<DataType, ErrorType>(
queryKey,
() => queryFn(spotify),
initial: initial,
retryConfig: retryConfig,
refreshConfig: refreshConfig,
jsonConfig: jsonConfig,
onData: onData,
onError: onError,
enabled: enabled,
);
useEffect(() {
return ref.listenManual(
spotifyProvider,
(previous, next) {
if (previous != next) {
query.refresh();
}
},
).close;
}, [query]);
return query;
}

View File

@ -11,6 +11,7 @@
/// Stephan-P@github, SecularSteve@github => Dutch /// Stephan-P@github, SecularSteve@github => Dutch
/// doannc2212@github => Vietnamese /// doannc2212@github => Vietnamese
/// sappho192@github => Korean /// sappho192@github => Korean
library;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class L10n { class L10n {

View File

@ -1,14 +1,12 @@
import 'package:catcher_2/catcher_2.dart'; import 'package:catcher_2/catcher_2.dart';
import 'package:dart_discord_rpc/dart_discord_rpc.dart'; import 'package:dart_discord_rpc/dart_discord_rpc.dart';
import 'package:device_preview/device_preview.dart'; import 'package:device_preview/device_preview.dart';
import 'package:fl_query/fl_query.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart'; 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:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:media_kit/media_kit.dart'; import 'package:media_kit/media_kit.dart';
@ -29,7 +27,6 @@ import 'package:spotube/provider/palette_provider.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/audio_player.dart';
import 'package:spotube/services/cli/cli.dart'; import 'package:spotube/services/cli/cli.dart';
import 'package:spotube/services/connectivity_adapter.dart';
import 'package:spotube/services/kv_store/kv_store.dart'; import 'package:spotube/services/kv_store/kv_store.dart';
import 'package:spotube/themes/theme.dart'; import 'package:spotube/themes/theme.dart';
import 'package:spotube/utils/persisted_state_notifier.dart'; import 'package:spotube/utils/persisted_state_notifier.dart';
@ -75,11 +72,7 @@ Future<void> main(List<String> rawArgs) async {
final hiveCacheDir = final hiveCacheDir =
kIsWeb ? null : (await getApplicationSupportDirectory()).path; kIsWeb ? null : (await getApplicationSupportDirectory()).path;
await QueryClient.initialize( Hive.init(hiveCacheDir);
cachePrefix: "oss.krtirtho.spotube",
cacheDir: hiveCacheDir,
connectivity: FlQueryInternetConnectionCheckerAdapter(),
);
Hive.registerAdapter(SkipSegmentAdapter()); Hive.registerAdapter(SkipSegmentAdapter());
@ -145,10 +138,7 @@ Future<void> main(List<String> rawArgs) async {
orientation: Orientation.portrait, orientation: Orientation.portrait,
), ),
builder: (context) { builder: (context) {
return QueryClientProvider( return const Spotube();
staleDuration: const Duration(minutes: 30),
child: const Spotube(),
);
}, },
), ),
), ),

View File

@ -0,0 +1,40 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'recommendation_seeds.freezed.dart';
part 'recommendation_seeds.g.dart';
@freezed
class GeneratePlaylistProviderInput with _$GeneratePlaylistProviderInput {
factory GeneratePlaylistProviderInput({
Iterable<String>? seedArtists,
Iterable<String>? seedGenres,
Iterable<String>? seedTracks,
required int limit,
RecommendationSeeds? max,
RecommendationSeeds? min,
RecommendationSeeds? target,
}) = _GeneratePlaylistProviderInput;
}
@freezed
class RecommendationSeeds with _$RecommendationSeeds {
factory RecommendationSeeds({
num? acousticness,
num? danceability,
@JsonKey(name: "duration_ms") num? durationMs,
num? energy,
num? instrumentalness,
num? key,
num? liveness,
num? loudness,
num? mode,
num? popularity,
num? speechiness,
num? tempo,
@JsonKey(name: "time_signature") num? timeSignature,
num? valence,
}) = _RecommendationSeeds;
factory RecommendationSeeds.fromJson(Map<String, dynamic> json) =>
_$RecommendationSeedsFromJson(json);
}

View File

@ -0,0 +1,756 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'recommendation_seeds.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
/// @nodoc
mixin _$GeneratePlaylistProviderInput {
Iterable<String>? get seedArtists => throw _privateConstructorUsedError;
Iterable<String>? get seedGenres => throw _privateConstructorUsedError;
Iterable<String>? get seedTracks => throw _privateConstructorUsedError;
int get limit => throw _privateConstructorUsedError;
RecommendationSeeds? get max => throw _privateConstructorUsedError;
RecommendationSeeds? get min => throw _privateConstructorUsedError;
RecommendationSeeds? get target => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$GeneratePlaylistProviderInputCopyWith<GeneratePlaylistProviderInput>
get copyWith => throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $GeneratePlaylistProviderInputCopyWith<$Res> {
factory $GeneratePlaylistProviderInputCopyWith(
GeneratePlaylistProviderInput value,
$Res Function(GeneratePlaylistProviderInput) then) =
_$GeneratePlaylistProviderInputCopyWithImpl<$Res,
GeneratePlaylistProviderInput>;
@useResult
$Res call(
{Iterable<String>? seedArtists,
Iterable<String>? seedGenres,
Iterable<String>? seedTracks,
int limit,
RecommendationSeeds? max,
RecommendationSeeds? min,
RecommendationSeeds? target});
$RecommendationSeedsCopyWith<$Res>? get max;
$RecommendationSeedsCopyWith<$Res>? get min;
$RecommendationSeedsCopyWith<$Res>? get target;
}
/// @nodoc
class _$GeneratePlaylistProviderInputCopyWithImpl<$Res,
$Val extends GeneratePlaylistProviderInput>
implements $GeneratePlaylistProviderInputCopyWith<$Res> {
_$GeneratePlaylistProviderInputCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? seedArtists = freezed,
Object? seedGenres = freezed,
Object? seedTracks = freezed,
Object? limit = null,
Object? max = freezed,
Object? min = freezed,
Object? target = freezed,
}) {
return _then(_value.copyWith(
seedArtists: freezed == seedArtists
? _value.seedArtists
: seedArtists // ignore: cast_nullable_to_non_nullable
as Iterable<String>?,
seedGenres: freezed == seedGenres
? _value.seedGenres
: seedGenres // ignore: cast_nullable_to_non_nullable
as Iterable<String>?,
seedTracks: freezed == seedTracks
? _value.seedTracks
: seedTracks // ignore: cast_nullable_to_non_nullable
as Iterable<String>?,
limit: null == limit
? _value.limit
: limit // ignore: cast_nullable_to_non_nullable
as int,
max: freezed == max
? _value.max
: max // ignore: cast_nullable_to_non_nullable
as RecommendationSeeds?,
min: freezed == min
? _value.min
: min // ignore: cast_nullable_to_non_nullable
as RecommendationSeeds?,
target: freezed == target
? _value.target
: target // ignore: cast_nullable_to_non_nullable
as RecommendationSeeds?,
) as $Val);
}
@override
@pragma('vm:prefer-inline')
$RecommendationSeedsCopyWith<$Res>? get max {
if (_value.max == null) {
return null;
}
return $RecommendationSeedsCopyWith<$Res>(_value.max!, (value) {
return _then(_value.copyWith(max: value) as $Val);
});
}
@override
@pragma('vm:prefer-inline')
$RecommendationSeedsCopyWith<$Res>? get min {
if (_value.min == null) {
return null;
}
return $RecommendationSeedsCopyWith<$Res>(_value.min!, (value) {
return _then(_value.copyWith(min: value) as $Val);
});
}
@override
@pragma('vm:prefer-inline')
$RecommendationSeedsCopyWith<$Res>? get target {
if (_value.target == null) {
return null;
}
return $RecommendationSeedsCopyWith<$Res>(_value.target!, (value) {
return _then(_value.copyWith(target: value) as $Val);
});
}
}
/// @nodoc
abstract class _$$GeneratePlaylistProviderInputImplCopyWith<$Res>
implements $GeneratePlaylistProviderInputCopyWith<$Res> {
factory _$$GeneratePlaylistProviderInputImplCopyWith(
_$GeneratePlaylistProviderInputImpl value,
$Res Function(_$GeneratePlaylistProviderInputImpl) then) =
__$$GeneratePlaylistProviderInputImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{Iterable<String>? seedArtists,
Iterable<String>? seedGenres,
Iterable<String>? seedTracks,
int limit,
RecommendationSeeds? max,
RecommendationSeeds? min,
RecommendationSeeds? target});
@override
$RecommendationSeedsCopyWith<$Res>? get max;
@override
$RecommendationSeedsCopyWith<$Res>? get min;
@override
$RecommendationSeedsCopyWith<$Res>? get target;
}
/// @nodoc
class __$$GeneratePlaylistProviderInputImplCopyWithImpl<$Res>
extends _$GeneratePlaylistProviderInputCopyWithImpl<$Res,
_$GeneratePlaylistProviderInputImpl>
implements _$$GeneratePlaylistProviderInputImplCopyWith<$Res> {
__$$GeneratePlaylistProviderInputImplCopyWithImpl(
_$GeneratePlaylistProviderInputImpl _value,
$Res Function(_$GeneratePlaylistProviderInputImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? seedArtists = freezed,
Object? seedGenres = freezed,
Object? seedTracks = freezed,
Object? limit = null,
Object? max = freezed,
Object? min = freezed,
Object? target = freezed,
}) {
return _then(_$GeneratePlaylistProviderInputImpl(
seedArtists: freezed == seedArtists
? _value.seedArtists
: seedArtists // ignore: cast_nullable_to_non_nullable
as Iterable<String>?,
seedGenres: freezed == seedGenres
? _value.seedGenres
: seedGenres // ignore: cast_nullable_to_non_nullable
as Iterable<String>?,
seedTracks: freezed == seedTracks
? _value.seedTracks
: seedTracks // ignore: cast_nullable_to_non_nullable
as Iterable<String>?,
limit: null == limit
? _value.limit
: limit // ignore: cast_nullable_to_non_nullable
as int,
max: freezed == max
? _value.max
: max // ignore: cast_nullable_to_non_nullable
as RecommendationSeeds?,
min: freezed == min
? _value.min
: min // ignore: cast_nullable_to_non_nullable
as RecommendationSeeds?,
target: freezed == target
? _value.target
: target // ignore: cast_nullable_to_non_nullable
as RecommendationSeeds?,
));
}
}
/// @nodoc
class _$GeneratePlaylistProviderInputImpl
implements _GeneratePlaylistProviderInput {
_$GeneratePlaylistProviderInputImpl(
{this.seedArtists,
this.seedGenres,
this.seedTracks,
required this.limit,
this.max,
this.min,
this.target});
@override
final Iterable<String>? seedArtists;
@override
final Iterable<String>? seedGenres;
@override
final Iterable<String>? seedTracks;
@override
final int limit;
@override
final RecommendationSeeds? max;
@override
final RecommendationSeeds? min;
@override
final RecommendationSeeds? target;
@override
String toString() {
return 'GeneratePlaylistProviderInput(seedArtists: $seedArtists, seedGenres: $seedGenres, seedTracks: $seedTracks, limit: $limit, max: $max, min: $min, target: $target)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$GeneratePlaylistProviderInputImpl &&
const DeepCollectionEquality()
.equals(other.seedArtists, seedArtists) &&
const DeepCollectionEquality()
.equals(other.seedGenres, seedGenres) &&
const DeepCollectionEquality()
.equals(other.seedTracks, seedTracks) &&
(identical(other.limit, limit) || other.limit == limit) &&
(identical(other.max, max) || other.max == max) &&
(identical(other.min, min) || other.min == min) &&
(identical(other.target, target) || other.target == target));
}
@override
int get hashCode => Object.hash(
runtimeType,
const DeepCollectionEquality().hash(seedArtists),
const DeepCollectionEquality().hash(seedGenres),
const DeepCollectionEquality().hash(seedTracks),
limit,
max,
min,
target);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$GeneratePlaylistProviderInputImplCopyWith<
_$GeneratePlaylistProviderInputImpl>
get copyWith => __$$GeneratePlaylistProviderInputImplCopyWithImpl<
_$GeneratePlaylistProviderInputImpl>(this, _$identity);
}
abstract class _GeneratePlaylistProviderInput
implements GeneratePlaylistProviderInput {
factory _GeneratePlaylistProviderInput(
{final Iterable<String>? seedArtists,
final Iterable<String>? seedGenres,
final Iterable<String>? seedTracks,
required final int limit,
final RecommendationSeeds? max,
final RecommendationSeeds? min,
final RecommendationSeeds? target}) = _$GeneratePlaylistProviderInputImpl;
@override
Iterable<String>? get seedArtists;
@override
Iterable<String>? get seedGenres;
@override
Iterable<String>? get seedTracks;
@override
int get limit;
@override
RecommendationSeeds? get max;
@override
RecommendationSeeds? get min;
@override
RecommendationSeeds? get target;
@override
@JsonKey(ignore: true)
_$$GeneratePlaylistProviderInputImplCopyWith<
_$GeneratePlaylistProviderInputImpl>
get copyWith => throw _privateConstructorUsedError;
}
RecommendationSeeds _$RecommendationSeedsFromJson(Map<String, dynamic> json) {
return _RecommendationSeeds.fromJson(json);
}
/// @nodoc
mixin _$RecommendationSeeds {
num? get acousticness => throw _privateConstructorUsedError;
num? get danceability => throw _privateConstructorUsedError;
@JsonKey(name: "duration_ms")
num? get durationMs => throw _privateConstructorUsedError;
num? get energy => throw _privateConstructorUsedError;
num? get instrumentalness => throw _privateConstructorUsedError;
num? get key => throw _privateConstructorUsedError;
num? get liveness => throw _privateConstructorUsedError;
num? get loudness => throw _privateConstructorUsedError;
num? get mode => throw _privateConstructorUsedError;
num? get popularity => throw _privateConstructorUsedError;
num? get speechiness => throw _privateConstructorUsedError;
num? get tempo => throw _privateConstructorUsedError;
@JsonKey(name: "time_signature")
num? get timeSignature => throw _privateConstructorUsedError;
num? get valence => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$RecommendationSeedsCopyWith<RecommendationSeeds> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $RecommendationSeedsCopyWith<$Res> {
factory $RecommendationSeedsCopyWith(
RecommendationSeeds value, $Res Function(RecommendationSeeds) then) =
_$RecommendationSeedsCopyWithImpl<$Res, RecommendationSeeds>;
@useResult
$Res call(
{num? acousticness,
num? danceability,
@JsonKey(name: "duration_ms") num? durationMs,
num? energy,
num? instrumentalness,
num? key,
num? liveness,
num? loudness,
num? mode,
num? popularity,
num? speechiness,
num? tempo,
@JsonKey(name: "time_signature") num? timeSignature,
num? valence});
}
/// @nodoc
class _$RecommendationSeedsCopyWithImpl<$Res, $Val extends RecommendationSeeds>
implements $RecommendationSeedsCopyWith<$Res> {
_$RecommendationSeedsCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? acousticness = freezed,
Object? danceability = freezed,
Object? durationMs = freezed,
Object? energy = freezed,
Object? instrumentalness = freezed,
Object? key = freezed,
Object? liveness = freezed,
Object? loudness = freezed,
Object? mode = freezed,
Object? popularity = freezed,
Object? speechiness = freezed,
Object? tempo = freezed,
Object? timeSignature = freezed,
Object? valence = freezed,
}) {
return _then(_value.copyWith(
acousticness: freezed == acousticness
? _value.acousticness
: acousticness // ignore: cast_nullable_to_non_nullable
as num?,
danceability: freezed == danceability
? _value.danceability
: danceability // ignore: cast_nullable_to_non_nullable
as num?,
durationMs: freezed == durationMs
? _value.durationMs
: durationMs // ignore: cast_nullable_to_non_nullable
as num?,
energy: freezed == energy
? _value.energy
: energy // ignore: cast_nullable_to_non_nullable
as num?,
instrumentalness: freezed == instrumentalness
? _value.instrumentalness
: instrumentalness // ignore: cast_nullable_to_non_nullable
as num?,
key: freezed == key
? _value.key
: key // ignore: cast_nullable_to_non_nullable
as num?,
liveness: freezed == liveness
? _value.liveness
: liveness // ignore: cast_nullable_to_non_nullable
as num?,
loudness: freezed == loudness
? _value.loudness
: loudness // ignore: cast_nullable_to_non_nullable
as num?,
mode: freezed == mode
? _value.mode
: mode // ignore: cast_nullable_to_non_nullable
as num?,
popularity: freezed == popularity
? _value.popularity
: popularity // ignore: cast_nullable_to_non_nullable
as num?,
speechiness: freezed == speechiness
? _value.speechiness
: speechiness // ignore: cast_nullable_to_non_nullable
as num?,
tempo: freezed == tempo
? _value.tempo
: tempo // ignore: cast_nullable_to_non_nullable
as num?,
timeSignature: freezed == timeSignature
? _value.timeSignature
: timeSignature // ignore: cast_nullable_to_non_nullable
as num?,
valence: freezed == valence
? _value.valence
: valence // ignore: cast_nullable_to_non_nullable
as num?,
) as $Val);
}
}
/// @nodoc
abstract class _$$RecommendationSeedsImplCopyWith<$Res>
implements $RecommendationSeedsCopyWith<$Res> {
factory _$$RecommendationSeedsImplCopyWith(_$RecommendationSeedsImpl value,
$Res Function(_$RecommendationSeedsImpl) then) =
__$$RecommendationSeedsImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{num? acousticness,
num? danceability,
@JsonKey(name: "duration_ms") num? durationMs,
num? energy,
num? instrumentalness,
num? key,
num? liveness,
num? loudness,
num? mode,
num? popularity,
num? speechiness,
num? tempo,
@JsonKey(name: "time_signature") num? timeSignature,
num? valence});
}
/// @nodoc
class __$$RecommendationSeedsImplCopyWithImpl<$Res>
extends _$RecommendationSeedsCopyWithImpl<$Res, _$RecommendationSeedsImpl>
implements _$$RecommendationSeedsImplCopyWith<$Res> {
__$$RecommendationSeedsImplCopyWithImpl(_$RecommendationSeedsImpl _value,
$Res Function(_$RecommendationSeedsImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? acousticness = freezed,
Object? danceability = freezed,
Object? durationMs = freezed,
Object? energy = freezed,
Object? instrumentalness = freezed,
Object? key = freezed,
Object? liveness = freezed,
Object? loudness = freezed,
Object? mode = freezed,
Object? popularity = freezed,
Object? speechiness = freezed,
Object? tempo = freezed,
Object? timeSignature = freezed,
Object? valence = freezed,
}) {
return _then(_$RecommendationSeedsImpl(
acousticness: freezed == acousticness
? _value.acousticness
: acousticness // ignore: cast_nullable_to_non_nullable
as num?,
danceability: freezed == danceability
? _value.danceability
: danceability // ignore: cast_nullable_to_non_nullable
as num?,
durationMs: freezed == durationMs
? _value.durationMs
: durationMs // ignore: cast_nullable_to_non_nullable
as num?,
energy: freezed == energy
? _value.energy
: energy // ignore: cast_nullable_to_non_nullable
as num?,
instrumentalness: freezed == instrumentalness
? _value.instrumentalness
: instrumentalness // ignore: cast_nullable_to_non_nullable
as num?,
key: freezed == key
? _value.key
: key // ignore: cast_nullable_to_non_nullable
as num?,
liveness: freezed == liveness
? _value.liveness
: liveness // ignore: cast_nullable_to_non_nullable
as num?,
loudness: freezed == loudness
? _value.loudness
: loudness // ignore: cast_nullable_to_non_nullable
as num?,
mode: freezed == mode
? _value.mode
: mode // ignore: cast_nullable_to_non_nullable
as num?,
popularity: freezed == popularity
? _value.popularity
: popularity // ignore: cast_nullable_to_non_nullable
as num?,
speechiness: freezed == speechiness
? _value.speechiness
: speechiness // ignore: cast_nullable_to_non_nullable
as num?,
tempo: freezed == tempo
? _value.tempo
: tempo // ignore: cast_nullable_to_non_nullable
as num?,
timeSignature: freezed == timeSignature
? _value.timeSignature
: timeSignature // ignore: cast_nullable_to_non_nullable
as num?,
valence: freezed == valence
? _value.valence
: valence // ignore: cast_nullable_to_non_nullable
as num?,
));
}
}
/// @nodoc
@JsonSerializable()
class _$RecommendationSeedsImpl implements _RecommendationSeeds {
_$RecommendationSeedsImpl(
{this.acousticness,
this.danceability,
@JsonKey(name: "duration_ms") this.durationMs,
this.energy,
this.instrumentalness,
this.key,
this.liveness,
this.loudness,
this.mode,
this.popularity,
this.speechiness,
this.tempo,
@JsonKey(name: "time_signature") this.timeSignature,
this.valence});
factory _$RecommendationSeedsImpl.fromJson(Map<String, dynamic> json) =>
_$$RecommendationSeedsImplFromJson(json);
@override
final num? acousticness;
@override
final num? danceability;
@override
@JsonKey(name: "duration_ms")
final num? durationMs;
@override
final num? energy;
@override
final num? instrumentalness;
@override
final num? key;
@override
final num? liveness;
@override
final num? loudness;
@override
final num? mode;
@override
final num? popularity;
@override
final num? speechiness;
@override
final num? tempo;
@override
@JsonKey(name: "time_signature")
final num? timeSignature;
@override
final num? valence;
@override
String toString() {
return 'RecommendationSeeds(acousticness: $acousticness, danceability: $danceability, durationMs: $durationMs, energy: $energy, instrumentalness: $instrumentalness, key: $key, liveness: $liveness, loudness: $loudness, mode: $mode, popularity: $popularity, speechiness: $speechiness, tempo: $tempo, timeSignature: $timeSignature, valence: $valence)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$RecommendationSeedsImpl &&
(identical(other.acousticness, acousticness) ||
other.acousticness == acousticness) &&
(identical(other.danceability, danceability) ||
other.danceability == danceability) &&
(identical(other.durationMs, durationMs) ||
other.durationMs == durationMs) &&
(identical(other.energy, energy) || other.energy == energy) &&
(identical(other.instrumentalness, instrumentalness) ||
other.instrumentalness == instrumentalness) &&
(identical(other.key, key) || other.key == key) &&
(identical(other.liveness, liveness) ||
other.liveness == liveness) &&
(identical(other.loudness, loudness) ||
other.loudness == loudness) &&
(identical(other.mode, mode) || other.mode == mode) &&
(identical(other.popularity, popularity) ||
other.popularity == popularity) &&
(identical(other.speechiness, speechiness) ||
other.speechiness == speechiness) &&
(identical(other.tempo, tempo) || other.tempo == tempo) &&
(identical(other.timeSignature, timeSignature) ||
other.timeSignature == timeSignature) &&
(identical(other.valence, valence) || other.valence == valence));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(
runtimeType,
acousticness,
danceability,
durationMs,
energy,
instrumentalness,
key,
liveness,
loudness,
mode,
popularity,
speechiness,
tempo,
timeSignature,
valence);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$RecommendationSeedsImplCopyWith<_$RecommendationSeedsImpl> get copyWith =>
__$$RecommendationSeedsImplCopyWithImpl<_$RecommendationSeedsImpl>(
this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$RecommendationSeedsImplToJson(
this,
);
}
}
abstract class _RecommendationSeeds implements RecommendationSeeds {
factory _RecommendationSeeds(
{final num? acousticness,
final num? danceability,
@JsonKey(name: "duration_ms") final num? durationMs,
final num? energy,
final num? instrumentalness,
final num? key,
final num? liveness,
final num? loudness,
final num? mode,
final num? popularity,
final num? speechiness,
final num? tempo,
@JsonKey(name: "time_signature") final num? timeSignature,
final num? valence}) = _$RecommendationSeedsImpl;
factory _RecommendationSeeds.fromJson(Map<String, dynamic> json) =
_$RecommendationSeedsImpl.fromJson;
@override
num? get acousticness;
@override
num? get danceability;
@override
@JsonKey(name: "duration_ms")
num? get durationMs;
@override
num? get energy;
@override
num? get instrumentalness;
@override
num? get key;
@override
num? get liveness;
@override
num? get loudness;
@override
num? get mode;
@override
num? get popularity;
@override
num? get speechiness;
@override
num? get tempo;
@override
@JsonKey(name: "time_signature")
num? get timeSignature;
@override
num? get valence;
@override
@JsonKey(ignore: true)
_$$RecommendationSeedsImplCopyWith<_$RecommendationSeedsImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@ -0,0 +1,45 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'recommendation_seeds.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$RecommendationSeedsImpl _$$RecommendationSeedsImplFromJson(
Map<String, dynamic> json) =>
_$RecommendationSeedsImpl(
acousticness: json['acousticness'] as num?,
danceability: json['danceability'] as num?,
durationMs: json['duration_ms'] as num?,
energy: json['energy'] as num?,
instrumentalness: json['instrumentalness'] as num?,
key: json['key'] as num?,
liveness: json['liveness'] as num?,
loudness: json['loudness'] as num?,
mode: json['mode'] as num?,
popularity: json['popularity'] as num?,
speechiness: json['speechiness'] as num?,
tempo: json['tempo'] as num?,
timeSignature: json['time_signature'] as num?,
valence: json['valence'] as num?,
);
Map<String, dynamic> _$$RecommendationSeedsImplToJson(
_$RecommendationSeedsImpl instance) =>
<String, dynamic>{
'acousticness': instance.acousticness,
'danceability': instance.danceability,
'duration_ms': instance.durationMs,
'energy': instance.energy,
'instrumentalness': instance.instrumentalness,
'key': instance.key,
'liveness': instance.liveness,
'loudness': instance.loudness,
'mode': instance.mode,
'popularity': instance.popularity,
'speechiness': instance.speechiness,
'tempo': instance.tempo,
'time_signature': instance.timeSignature,
'valence': instance.valence,
};

View File

@ -1,15 +1,10 @@
import 'package:fl_query_hooks/fl_query_hooks.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.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/shared/tracks_view/track_view.dart'; import 'package:spotube/components/shared/tracks_view/track_view.dart';
import 'package:spotube/components/shared/tracks_view/track_view_props.dart'; import 'package:spotube/components/shared/tracks_view/track_view_props.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/infinite_query.dart'; import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/provider/spotify_provider.dart';
import 'package:spotube/services/mutations/mutations.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 AlbumPage extends HookConsumerWidget { class AlbumPage extends HookConsumerWidget {
@ -21,26 +16,10 @@ class AlbumPage extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final spotify = ref.watch(spotifyProvider); final tracks = ref.watch(albumTracksProvider(album));
final tracksQuery = useQueries.album.tracksOf(ref, album); final tracksNotifier = ref.watch(albumTracksProvider(album).notifier);
final favoriteAlbumsNotifier = ref.watch(favoriteAlbumsProvider.notifier);
final tracks = useMemoized(() { final isSavedAlbum = ref.watch(albumsIsSavedProvider(album.id!));
return tracksQuery.pages.expand((element) => element).toList();
}, [tracksQuery.pages]);
final client = useQueryClient();
final albumIsSaved = useQueries.album.isSavedForMe(ref, album.id!);
final isLiked = albumIsSaved.data ?? false;
final toggleAlbumLike = useMutations.album.toggleFavorite(
ref,
album.id!,
refreshQueries: [albumIsSaved.key],
onData: (_, __) async {
await client.refreshInfiniteQueryAllPages("current-user-albums");
},
);
return InheritedTrackView( return InheritedTrackView(
collectionId: album.id!, collectionId: album.id!,
@ -51,29 +30,33 @@ class AlbumPage extends HookConsumerWidget {
title: album.name!, title: album.name!,
description: description:
"${context.l10n.released}${album.releaseDate}${album.artists!.first.name}", "${context.l10n.released}${album.releaseDate}${album.artists!.first.name}",
tracks: tracks, tracks: tracks.asData?.value.items ?? [],
pagination: PaginationProps.fromQuery( pagination: PaginationProps(
tracksQuery, hasNextPage: tracks.asData?.value.hasMore ?? false,
onFetchAll: () { isLoading: tracks.isLoadingNextPage,
return tracksQuery.fetchAllTracks(getAllTracks: () async { onFetchMore: () async {
final res = await spotify.albums.tracks(album.id!).all(); await tracksNotifier.fetchMore();
},
return res onFetchAll: () async {
.map((track) => return tracksNotifier.fetchAll();
TypeConversionUtils.simpleTrack_X_Track(track, album)) },
.toList(); onRefresh: () async {
}); ref.invalidate(albumTracksProvider(album));
}, },
), ),
routePath: "/album/${album.id}", routePath: "/album/${album.id}",
shareUrl: album.externalUrls!.spotify!, shareUrl: album.externalUrls!.spotify!,
isLiked: isLiked, isLiked: isSavedAlbum.value ?? false,
onHeart: albumIsSaved.hasData onHeart: isSavedAlbum.value == null
? () async { ? null
await toggleAlbumLike.mutate(isLiked); : () async {
if (isSavedAlbum.value!) {
await favoriteAlbumsNotifier.removeFavorites([album.id!]);
} else {
await favoriteAlbumsNotifier.addFavorites([album.id!]);
}
return null; return null;
} },
: null,
child: const TrackView(), child: const TrackView(),
); );
} }

View File

@ -12,19 +12,19 @@ import 'package:spotube/pages/artist/section/footer.dart';
import 'package:spotube/pages/artist/section/header.dart'; import 'package:spotube/pages/artist/section/header.dart';
import 'package:spotube/pages/artist/section/related_artists.dart'; import 'package:spotube/pages/artist/section/related_artists.dart';
import 'package:spotube/pages/artist/section/top_tracks.dart'; import 'package:spotube/pages/artist/section/top_tracks.dart';
import 'package:spotube/services/queries/queries.dart'; import 'package:spotube/provider/spotify/spotify.dart';
class ArtistPage extends HookConsumerWidget { class ArtistPage extends HookConsumerWidget {
final String artistId; final String artistId;
final logger = getLogger(ArtistPage); final logger = getLogger(ArtistPage);
ArtistPage(this.artistId, {Key? key}) : super(key: key); ArtistPage(this.artistId, {super.key});
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final scrollController = useScrollController(); final scrollController = useScrollController();
final theme = Theme.of(context); final theme = Theme.of(context);
final artistQuery = useQueries.artist.get(ref, artistId); final artistQuery = ref.watch(artistProvider(artistId));
return SafeArea( return SafeArea(
bottom: false, bottom: false,
@ -35,7 +35,7 @@ class ArtistPage extends HookConsumerWidget {
), ),
extendBodyBehindAppBar: true, extendBodyBehindAppBar: true,
body: Builder(builder: (context) { body: Builder(builder: (context) {
if (artistQuery.hasError && artistQuery.data == null) { if (artistQuery.hasError && artistQuery.value == null) {
return Center(child: Text(artistQuery.error.toString())); return Center(child: Text(artistQuery.error.toString()));
} }
return Skeletonizer( return Skeletonizer(
@ -66,11 +66,11 @@ class ArtistPage extends HookConsumerWidget {
SliverSafeArea( SliverSafeArea(
sliver: ArtistPageRelatedArtists(artistId: artistId), sliver: ArtistPageRelatedArtists(artistId: artistId),
), ),
if (artistQuery.data != null) if (artistQuery.value != null)
SliverSafeArea( SliverSafeArea(
top: false, top: false,
sliver: SliverToBoxAdapter( sliver: SliverToBoxAdapter(
child: ArtistPageFooter(artist: artistQuery.data!), child: ArtistPageFooter(artist: artistQuery.value!),
), ),
), ),
], ],

View File

@ -5,13 +5,13 @@ import 'package:spotify/spotify.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/shared/image/universal_image.dart'; import 'package:spotube/components/shared/image/universal_image.dart';
import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/services/queries/queries.dart'; import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/utils/type_conversion_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
class ArtistPageFooter extends HookConsumerWidget { class ArtistPageFooter extends ConsumerWidget {
final Artist artist; final Artist artist;
const ArtistPageFooter({Key? key, required this.artist}) : super(key: key); const ArtistPageFooter({super.key, required this.artist});
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
@ -22,8 +22,9 @@ class ArtistPageFooter extends HookConsumerWidget {
artist.images, artist.images,
placeholder: ImagePlaceholder.artist, placeholder: ImagePlaceholder.artist,
); );
final summary = useQueries.artist.wikipediaSummary(artist); final summary = ref.watch(artistWikipediaSummaryProvider(artist));
if (summary.hasError || !summary.hasData) return const SizedBox.shrink(); if (summary.value == null) return const SizedBox.shrink();
return Container( return Container(
margin: const EdgeInsets.all(16), margin: const EdgeInsets.all(16),
padding: mediaQuery.smAndDown padding: mediaQuery.smAndDown
@ -38,9 +39,9 @@ class ArtistPageFooter extends HookConsumerWidget {
BlendMode.darken, BlendMode.darken,
), ),
image: UniversalImage.imageProvider( image: UniversalImage.imageProvider(
summary.data!.thumbnail?.source_ ?? artistImage, summary.value!.thumbnail?.source_ ?? artistImage,
height: summary.data!.thumbnail?.height.toDouble(), height: summary.value!.thumbnail?.height.toDouble(),
width: summary.data!.thumbnail?.width.toDouble(), width: summary.value!.thumbnail?.width.toDouble(),
), ),
fit: BoxFit.cover, fit: BoxFit.cover,
alignment: Alignment.center, alignment: Alignment.center,
@ -69,7 +70,7 @@ class ArtistPageFooter extends HookConsumerWidget {
), ),
const TextSpan(text: '\n\n'), const TextSpan(text: '\n\n'),
TextSpan( TextSpan(
text: summary.data!.extract, text: summary.value!.extract,
), ),
TextSpan( TextSpan(
text: '\n...read more at wikipedia', text: '\n...read more at wikipedia',
@ -81,7 +82,7 @@ class ArtistPageFooter extends HookConsumerWidget {
recognizer: TapGestureRecognizer() recognizer: TapGestureRecognizer()
..onTap = () async { ..onTap = () async {
await launchUrlString( await launchUrlString(
"http://en.wikipedia.org/wiki?curid=${summary.data?.pageid}", "http://en.wikipedia.org/wiki?curid=${summary.asData?.value?.pageid}",
); );
}, },
), ),

View File

@ -1,11 +1,8 @@
import 'package:fl_query_hooks/fl_query_hooks.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:skeletonizer/skeletonizer.dart'; import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/collections/fake.dart'; import 'package:spotube/collections/fake.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/shared/image/universal_image.dart'; import 'package:spotube/components/shared/image/universal_image.dart';
@ -14,20 +11,18 @@ import 'package:spotube/extensions/context.dart';
import 'package:spotube/hooks/utils/use_breakpoint_value.dart'; import 'package:spotube/hooks/utils/use_breakpoint_value.dart';
import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/blacklist_provider.dart'; import 'package:spotube/provider/blacklist_provider.dart';
import 'package:spotube/provider/spotify_provider.dart'; import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/services/queries/queries.dart';
import 'package:spotube/utils/primitive_utils.dart'; import 'package:spotube/utils/primitive_utils.dart';
import 'package:spotube/utils/type_conversion_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart';
class ArtistPageHeader extends HookConsumerWidget { class ArtistPageHeader extends HookConsumerWidget {
final String artistId; final String artistId;
const ArtistPageHeader({Key? key, required this.artistId}) : super(key: key); const ArtistPageHeader({super.key, required this.artistId});
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final queryClient = useQueryClient(); final artistQuery = ref.watch(artistProvider(artistId));
final artistQuery = useQueries.artist.get(ref, artistId); final artist = artistQuery.value ?? FakeData.artist;
final artist = artistQuery.data ?? FakeData.artist;
final scaffoldMessenger = ScaffoldMessenger.of(context); final scaffoldMessenger = ScaffoldMessenger.of(context);
final mediaQuery = MediaQuery.of(context); final mediaQuery = MediaQuery.of(context);
@ -43,7 +38,6 @@ class ArtistPageHeader extends HookConsumerWidget {
xxl: textTheme.titleMedium, xxl: textTheme.titleMedium,
); );
final spotify = ref.read(spotifyProvider);
final auth = ref.watch(AuthenticationNotifier.provider); final auth = ref.watch(AuthenticationNotifier.provider);
final blacklist = ref.watch(BlackListNotifier.provider); final blacklist = ref.watch(BlackListNotifier.provider);
final isBlackListed = blacklist.contains( final isBlackListed = blacklist.contains(
@ -143,53 +137,41 @@ class ArtistPageHeader extends HookConsumerWidget {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
if (auth != null) if (auth != null)
HookBuilder( Consumer(
builder: (context) { builder: (context, ref, _) {
final isFollowingQuery = final isFollowingQuery = ref
useQueries.artist.doIFollow(ref, artistId); .watch(artistIsFollowingProvider(artist.id!));
final followingArtistNotifier =
ref.watch(followedArtistsProvider.notifier);
final followUnfollow = useCallback(() async { return switch (isFollowingQuery) {
try { AsyncData(value: final following) => Builder(
isFollowingQuery.data! builder: (context) {
? await spotify.me.unfollow( if (following) {
FollowingType.artist, return OutlinedButton(
[artistId], onPressed: () async {
) await followingArtistNotifier
: await spotify.me.follow( .removeArtists([artist.id!]);
FollowingType.artist, },
[artistId], child: Text(context.l10n.following),
); );
await isFollowingQuery.refresh(); }
queryClient.refreshInfiniteQueryAllPages( return FilledButton(
"user-following-artists"); onPressed: () async {
} finally { await followingArtistNotifier
queryClient.refreshQuery( .saveArtists([artist.id!]);
"user-follows-artists-query/$artistId", },
); child: Text(context.l10n.follow),
} );
}, [isFollowingQuery]); },
),
if (isFollowingQuery.isLoading || AsyncError() => const SizedBox(),
!isFollowingQuery.hasData) { _ => const SizedBox.square(
return const SizedBox( dimension: 20,
height: 20, child: CircularProgressIndicator(),
width: 20, )
child: CircularProgressIndicator(), };
);
}
if (isFollowingQuery.data!) {
return OutlinedButton(
onPressed: followUnfollow,
child: Text(context.l10n.following),
);
}
return FilledButton(
onPressed: followUnfollow,
child: Text(context.l10n.follow),
);
}, },
), ),
const SizedBox(width: 5), const SizedBox(width: 5),

View File

@ -1,49 +1,45 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/components/artist/artist_card.dart'; import 'package:spotube/components/artist/artist_card.dart';
import 'package:spotube/services/queries/queries.dart'; import 'package:spotube/provider/spotify/spotify.dart';
class ArtistPageRelatedArtists extends HookConsumerWidget { class ArtistPageRelatedArtists extends ConsumerWidget {
final String artistId; final String artistId;
const ArtistPageRelatedArtists({ const ArtistPageRelatedArtists({
Key? key, super.key,
required this.artistId, required this.artistId,
}) : super(key: key); });
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final relatedArtists = useQueries.artist.relatedArtistsOf( final relatedArtists = ref.watch(relatedArtistsProvider(artistId));
ref,
artistId,
);
if (relatedArtists.isLoading || !relatedArtists.hasData) { return switch (relatedArtists) {
return const SliverToBoxAdapter( AsyncData(value: final artists) => SliverPadding(
child: Center(child: CircularProgressIndicator())); padding: const EdgeInsets.symmetric(horizontal: 8.0),
} else if (relatedArtists.hasError) { sliver: SliverGrid.builder(
return SliverToBoxAdapter( itemCount: artists.length,
child: Center( gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
child: Text(relatedArtists.error.toString()), maxCrossAxisExtent: 200,
mainAxisExtent: 250,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
childAspectRatio: 0.8,
),
itemBuilder: (context, index) {
final artist = artists.elementAt(index);
return ArtistCard(artist);
},
),
), ),
); AsyncError(:final error) => SliverToBoxAdapter(
} child: Center(
child: Text(error.toString()),
return SliverPadding( ),
padding: const EdgeInsets.symmetric(horizontal: 8.0),
sliver: SliverGrid.builder(
itemCount: relatedArtists.data!.length,
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 200,
mainAxisExtent: 250,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
childAspectRatio: 0.8,
), ),
itemBuilder: (context, index) { _ => const SliverToBoxAdapter(
final artist = relatedArtists.data!.elementAt(index); child: Center(child: CircularProgressIndicator()),
return ArtistCard(artist); ),
}, };
),
);
} }
} }

View File

@ -7,12 +7,11 @@ import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/shared/track_tile/track_tile.dart'; import 'package:spotube/components/shared/track_tile/track_tile.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
import 'package:spotube/services/queries/queries.dart'; import 'package:spotube/provider/spotify/spotify.dart';
class ArtistPageTopTracks extends HookConsumerWidget { class ArtistPageTopTracks extends HookConsumerWidget {
final String artistId; final String artistId;
const ArtistPageTopTracks({Key? key, required this.artistId}) const ArtistPageTopTracks({super.key, required this.artistId});
: super(key: key);
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
@ -21,13 +20,10 @@ class ArtistPageTopTracks extends HookConsumerWidget {
final playlist = ref.watch(ProxyPlaylistNotifier.provider); final playlist = ref.watch(ProxyPlaylistNotifier.provider);
final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier); final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier);
final topTracksQuery = useQueries.artist.topTracksOf( final topTracksQuery = ref.watch(artistTopTracksProvider(artistId));
ref,
artistId,
);
final isPlaylistPlaying = playlist.containsTracks( final isPlaylistPlaying = playlist.containsTracks(
topTracksQuery.data ?? <Track>[], topTracksQuery.value ?? <Track>[],
); );
if (topTracksQuery.hasError) { if (topTracksQuery.hasError) {
@ -39,7 +35,7 @@ class ArtistPageTopTracks extends HookConsumerWidget {
} }
final topTracks = final topTracks =
topTracksQuery.data ?? List.generate(10, (index) => FakeData.track); topTracksQuery.value ?? List.generate(10, (index) => FakeData.track);
void playPlaylist(List<Track> tracks, {Track? currentTrack}) async { void playPlaylist(List<Track> tracks, {Track? currentTrack}) async {
currentTrack ??= tracks.first; currentTrack ??= tracks.first;

View File

@ -9,7 +9,7 @@ import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
class DesktopLoginPage extends HookConsumerWidget { class DesktopLoginPage extends HookConsumerWidget {
const DesktopLoginPage({Key? key}) : super(key: key); const DesktopLoginPage({super.key});
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {

View File

@ -12,7 +12,7 @@ import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/service_utils.dart';
class LoginTutorial extends ConsumerWidget { class LoginTutorial extends ConsumerWidget {
const LoginTutorial({Key? key}) : super(key: key); const LoginTutorial({super.key});
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {

View File

@ -1,5 +1,3 @@
import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
@ -12,7 +10,7 @@ import 'package:spotube/components/shared/image/universal_image.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/waypoint.dart'; import 'package:spotube/components/shared/waypoint.dart';
import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/services/queries/queries.dart'; import 'package:spotube/provider/spotify/spotify.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart'; import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
@ -22,23 +20,10 @@ class GenrePlaylistsPage extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final playlistsQuery = useQueries.category.playlistsOf(
ref,
category.id!,
);
final playlists = useMemoized(
() => playlistsQuery.pages.expand(
(page) {
return page.items?.whereNotNull() ??
const Iterable<PlaylistSimple>.empty();
},
).toList(),
[playlistsQuery.pages],
);
final mediaQuery = MediaQuery.of(context); final mediaQuery = MediaQuery.of(context);
final playlists = ref.watch(categoryPlaylistsProvider(category.id!));
final playlistsNotifier =
ref.read(categoryPlaylistsProvider(category.id!).notifier);
final scrollController = useScrollController(); final scrollController = useScrollController();
return Scaffold( return Scaffold(
@ -109,7 +94,7 @@ class GenrePlaylistsPage extends HookConsumerWidget {
padding: EdgeInsets.symmetric( padding: EdgeInsets.symmetric(
horizontal: mediaQuery.mdAndDown ? 12 : 24, horizontal: mediaQuery.mdAndDown ? 12 : 24,
), ),
sliver: playlists.isEmpty sliver: playlists.asData?.value.items.isNotEmpty != true
? Skeletonizer.sliver( ? Skeletonizer.sliver(
child: SliverToBoxAdapter( child: SliverToBoxAdapter(
child: Wrap( child: Wrap(
@ -129,12 +114,14 @@ class GenrePlaylistsPage extends HookConsumerWidget {
crossAxisSpacing: 12, crossAxisSpacing: 12,
mainAxisSpacing: 12, mainAxisSpacing: 12,
), ),
itemCount: playlists.length + 1, itemCount:
(playlists.asData?.value.items.length ?? 0) + 1,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final playlist = playlists.elementAtOrNull(index); final playlist = playlists.asData?.value.items
.elementAtOrNull(index);
if (playlist == null) { if (playlist == null) {
if (!playlistsQuery.hasNextPage) { if (playlists.asData?.value.hasMore == false) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
return Skeletonizer( return Skeletonizer(
@ -142,11 +129,7 @@ class GenrePlaylistsPage extends HookConsumerWidget {
child: Waypoint( child: Waypoint(
controller: scrollController, controller: scrollController,
isGrid: true, isGrid: true,
onTouchEdge: () async { onTouchEdge: playlistsNotifier.fetchMore,
if (playlistsQuery.hasNextPage) {
await playlistsQuery.fetchNext();
}
},
child: PlaylistCard(FakeData.playlist), child: PlaylistCard(FakeData.playlist),
), ),
); );

Some files were not shown because too many files have changed in this diff Show More