mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
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:
parent
35e9920b51
commit
6673e5a8a8
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@ -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
170
.vscode/snippets.code-snippets
vendored
Normal 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(),",
|
||||||
|
");"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
@ -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
|
||||||
|
@ -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,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -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(
|
||||||
|
@ -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,
|
||||||
|
@ -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) {
|
||||||
|
@ -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) {
|
||||||
|
@ -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,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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(),
|
||||||
);
|
);
|
||||||
|
@ -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}",
|
||||||
|
@ -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(
|
||||||
);
|
() =>
|
||||||
final categoriesQuery =
|
categoriesQuery.value
|
||||||
useQueries.category.listAll(ref, recommendationMarket);
|
|
||||||
|
|
||||||
final categories = categoriesQuery.data
|
|
||||||
?.where((c) => (c.icons?.length ?? 0) > 0)
|
?.where((c) => (c.icons?.length ?? 0) > 0)
|
||||||
.take(mediaQuery.mdAndDown ? 6 : 10)
|
.take(mediaQuery.mdAndDown ? 6 : 10)
|
||||||
.toList() ??
|
.toList() ??
|
||||||
<Category>[];
|
<Category>[],
|
||||||
|
[mediaQuery.mdAndDown, categoriesQuery.value],
|
||||||
|
);
|
||||||
|
|
||||||
return SliverMainAxisGroup(
|
return SliverMainAxisGroup(
|
||||||
slivers: [
|
slivers: [
|
||||||
|
@ -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))
|
||||||
|
@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
@ -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) {
|
||||||
|
@ -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) {
|
||||||
|
@ -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) {
|
||||||
|
@ -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) {
|
||||||
|
@ -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,33 +14,28 @@ 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,
|
||||||
@ -49,8 +43,9 @@ class UserAlbums extends HookConsumerWidget {
|
|||||||
.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)
|
||||||
|
Skeletonizer(
|
||||||
|
enabled: true,
|
||||||
|
child: Waypoint(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
isGrid: true,
|
isGrid: true,
|
||||||
onTouchEdge: albumsQuery.fetchNext,
|
onTouchEdge: albumsQueryNotifier.fetchMore,
|
||||||
child: AlbumCard(FakeData.album),
|
child: AlbumCard(FakeData.album),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -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(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -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) {
|
||||||
|
@ -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) {
|
||||||
|
@ -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,
|
||||||
|
@ -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),
|
||||||
|
@ -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) {
|
||||||
|
@ -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) {
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
|
@ -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) {
|
||||||
|
@ -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) {
|
||||||
|
@ -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) {
|
||||||
|
@ -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) {
|
||||||
|
@ -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) {
|
||||||
|
@ -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: () {
|
||||||
|
@ -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(
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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) {
|
||||||
|
@ -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,
|
||||||
|
@ -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) {
|
||||||
|
@ -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;
|
||||||
|
@ -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) {
|
||||||
|
@ -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) {
|
||||||
|
@ -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) {
|
||||||
|
@ -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(
|
||||||
|
@ -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) {
|
||||||
|
@ -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) {
|
||||||
|
@ -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) {
|
||||||
|
@ -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) {
|
||||||
|
@ -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) {
|
||||||
|
@ -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,
|
||||||
);
|
);
|
||||||
|
@ -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),
|
||||||
|
@ -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) {
|
||||||
|
@ -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, {
|
||||||
|
@ -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) {
|
||||||
|
@ -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) {
|
||||||
|
@ -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) {
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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(
|
||||||
|
@ -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();
|
||||||
|
@ -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) {
|
||||||
|
@ -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) {
|
||||||
|
@ -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) {
|
||||||
|
@ -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) {
|
||||||
|
@ -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(
|
||||||
|
@ -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) {
|
||||||
|
@ -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) {
|
||||||
|
@ -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) {
|
||||||
|
@ -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) {
|
||||||
|
@ -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],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
@ -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) {
|
||||||
|
@ -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) {
|
||||||
|
@ -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) {
|
||||||
|
@ -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 &&
|
||||||
|
@ -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) {
|
||||||
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
@ -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]);
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
||||||
|
@ -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() =>
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
@ -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 {
|
||||||
|
@ -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(),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
40
lib/models/spotify/recommendation_seeds.dart
Normal file
40
lib/models/spotify/recommendation_seeds.dart
Normal 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);
|
||||||
|
}
|
756
lib/models/spotify/recommendation_seeds.freezed.dart
Normal file
756
lib/models/spotify/recommendation_seeds.freezed.dart
Normal 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;
|
||||||
|
}
|
45
lib/models/spotify/recommendation_seeds.g.dart
Normal file
45
lib/models/spotify/recommendation_seeds.g.dart
Normal 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,
|
||||||
|
};
|
@ -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 {
|
||||||
return null;
|
if (isSavedAlbum.value!) {
|
||||||
|
await favoriteAlbumsNotifier.removeFavorites([album.id!]);
|
||||||
|
} else {
|
||||||
|
await favoriteAlbumsNotifier.addFavorites([album.id!]);
|
||||||
}
|
}
|
||||||
: null,
|
return null;
|
||||||
|
},
|
||||||
child: const TrackView(),
|
child: const TrackView(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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!),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -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}",
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -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,55 +137,43 @@ class ArtistPageHeader extends HookConsumerWidget {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
if (auth != null)
|
if (auth != null)
|
||||||
HookBuilder(
|
Consumer(
|
||||||
|
builder: (context, ref, _) {
|
||||||
|
final isFollowingQuery = ref
|
||||||
|
.watch(artistIsFollowingProvider(artist.id!));
|
||||||
|
final followingArtistNotifier =
|
||||||
|
ref.watch(followedArtistsProvider.notifier);
|
||||||
|
|
||||||
|
return switch (isFollowingQuery) {
|
||||||
|
AsyncData(value: final following) => Builder(
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
final isFollowingQuery =
|
if (following) {
|
||||||
useQueries.artist.doIFollow(ref, artistId);
|
|
||||||
|
|
||||||
final followUnfollow = useCallback(() async {
|
|
||||||
try {
|
|
||||||
isFollowingQuery.data!
|
|
||||||
? await spotify.me.unfollow(
|
|
||||||
FollowingType.artist,
|
|
||||||
[artistId],
|
|
||||||
)
|
|
||||||
: await spotify.me.follow(
|
|
||||||
FollowingType.artist,
|
|
||||||
[artistId],
|
|
||||||
);
|
|
||||||
await isFollowingQuery.refresh();
|
|
||||||
|
|
||||||
queryClient.refreshInfiniteQueryAllPages(
|
|
||||||
"user-following-artists");
|
|
||||||
} finally {
|
|
||||||
queryClient.refreshQuery(
|
|
||||||
"user-follows-artists-query/$artistId",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}, [isFollowingQuery]);
|
|
||||||
|
|
||||||
if (isFollowingQuery.isLoading ||
|
|
||||||
!isFollowingQuery.hasData) {
|
|
||||||
return const SizedBox(
|
|
||||||
height: 20,
|
|
||||||
width: 20,
|
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isFollowingQuery.data!) {
|
|
||||||
return OutlinedButton(
|
return OutlinedButton(
|
||||||
onPressed: followUnfollow,
|
onPressed: () async {
|
||||||
|
await followingArtistNotifier
|
||||||
|
.removeArtists([artist.id!]);
|
||||||
|
},
|
||||||
child: Text(context.l10n.following),
|
child: Text(context.l10n.following),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return FilledButton(
|
return FilledButton(
|
||||||
onPressed: followUnfollow,
|
onPressed: () async {
|
||||||
|
await followingArtistNotifier
|
||||||
|
.saveArtists([artist.id!]);
|
||||||
|
},
|
||||||
child: Text(context.l10n.follow),
|
child: Text(context.l10n.follow),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
AsyncError() => const SizedBox(),
|
||||||
|
_ => const SizedBox.square(
|
||||||
|
dimension: 20,
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
},
|
||||||
|
),
|
||||||
const SizedBox(width: 5),
|
const SizedBox(width: 5),
|
||||||
IconButton(
|
IconButton(
|
||||||
tooltip: context.l10n.add_artist_to_blacklist,
|
tooltip: context.l10n.add_artist_to_blacklist,
|
||||||
|
@ -1,37 +1,24 @@
|
|||||||
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()));
|
|
||||||
} else if (relatedArtists.hasError) {
|
|
||||||
return SliverToBoxAdapter(
|
|
||||||
child: Center(
|
|
||||||
child: Text(relatedArtists.error.toString()),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return SliverPadding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||||
sliver: SliverGrid.builder(
|
sliver: SliverGrid.builder(
|
||||||
itemCount: relatedArtists.data!.length,
|
itemCount: artists.length,
|
||||||
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||||
maxCrossAxisExtent: 200,
|
maxCrossAxisExtent: 200,
|
||||||
mainAxisExtent: 250,
|
mainAxisExtent: 250,
|
||||||
@ -40,10 +27,19 @@ class ArtistPageRelatedArtists extends HookConsumerWidget {
|
|||||||
childAspectRatio: 0.8,
|
childAspectRatio: 0.8,
|
||||||
),
|
),
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final artist = relatedArtists.data!.elementAt(index);
|
final artist = artists.elementAt(index);
|
||||||
return ArtistCard(artist);
|
return ArtistCard(artist);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
|
AsyncError(:final error) => SliverToBoxAdapter(
|
||||||
|
child: Center(
|
||||||
|
child: Text(error.toString()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_ => const SliverToBoxAdapter(
|
||||||
|
child: Center(child: CircularProgressIndicator()),
|
||||||
|
),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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) {
|
||||||
|
@ -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) {
|
||||||
|
@ -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
Loading…
Reference in New Issue
Block a user