fix favorite/rating handlers on detail page

This commit is contained in:
jeffvli
2025-11-21 20:25:35 -08:00
parent 35d7d380ec
commit 6ba15ae0a0
5 changed files with 186 additions and 105 deletions
@@ -10,6 +10,9 @@ import {
WidePlayButton, WidePlayButton,
WideShuffleButton, WideShuffleButton,
} from '/@/renderer/features/shared/components/play-button'; } from '/@/renderer/features/shared/components/play-button';
import { useIsMutatingCreateFavorite } from '/@/renderer/features/shared/mutations/create-favorite-mutation';
import { useIsMutatingDeleteFavorite } from '/@/renderer/features/shared/mutations/delete-favorite-mutation';
import { useIsMutatingRating } from '/@/renderer/features/shared/mutations/set-rating-mutation';
import { ActionIcon } from '/@/shared/components/action-icon/action-icon'; import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
import { Center } from '/@/shared/components/center/center'; import { Center } from '/@/shared/components/center/center';
import { Group } from '/@/shared/components/group/group'; import { Group } from '/@/shared/components/group/group';
@@ -149,6 +152,10 @@ export const LibraryHeaderMenu = ({
onShuffle, onShuffle,
rating, rating,
}: LibraryHeaderMenuProps) => { }: LibraryHeaderMenuProps) => {
const isMutatingRating = useIsMutatingRating();
const isMutatingCreateFavorite = useIsMutatingCreateFavorite();
const isMutatingDeleteFavorite = useIsMutatingDeleteFavorite();
return ( return (
<div className={styles.libraryHeaderMenu}> <div className={styles.libraryHeaderMenu}>
<Group wrap="nowrap"> <Group wrap="nowrap">
@@ -156,12 +163,20 @@ export const LibraryHeaderMenu = ({
{onShuffle && <WideShuffleButton onClick={onShuffle} />} {onShuffle && <WideShuffleButton onClick={onShuffle} />}
</Group> </Group>
<Group gap="sm" wrap="nowrap"> <Group gap="sm" wrap="nowrap">
{onRating && <Rating onChange={onRating} size="lg" value={rating || 0} />} {onRating && (
<Rating
onChange={onRating}
readOnly={isMutatingRating}
size="lg"
value={rating || 0}
/>
)}
{onFavorite && ( {onFavorite && (
<ActionIcon <ActionIcon
disabled={isMutatingCreateFavorite || isMutatingDeleteFavorite}
icon="favorite" icon="favorite"
iconProps={{ iconProps={{
fill: favorite ? 'primary' : undefined, fill: favorite ? 'favorite' : undefined,
}} }}
onClick={onFavorite} onClick={onFavorite}
size="lg" size="lg"
@@ -1,4 +1,4 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useIsMutating, useMutation, useQueryClient } from '@tanstack/react-query';
import { AxiosError } from 'axios'; import { AxiosError } from 'axios';
import isElectron from 'is-electron'; import isElectron from 'is-electron';
@@ -6,23 +6,17 @@ import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys'; import { queryKeys } from '/@/renderer/api/query-keys';
import { eventEmitter } from '/@/renderer/events/event-emitter'; import { eventEmitter } from '/@/renderer/events/event-emitter';
import { MutationHookArgs } from '/@/renderer/lib/react-query'; import { MutationHookArgs } from '/@/renderer/lib/react-query';
import { useSetAlbumListItemDataById } from '/@/renderer/store';
import { useFavoriteEvent } from '/@/renderer/store/event.store'; import { useFavoriteEvent } from '/@/renderer/store/event.store';
import { import { FavoriteArgs, FavoriteResponse, LibraryItem } from '/@/shared/types/domain-types';
AlbumArtistDetailResponse,
AlbumDetailResponse,
FavoriteArgs,
FavoriteResponse,
LibraryItem,
} from '/@/shared/types/domain-types';
const remote = isElectron() ? window.api.remote : null; const remote = isElectron() ? window.api.remote : null;
const createFavoriteQueryKey = ['set-favorite', true];
export const useCreateFavorite = (args: MutationHookArgs) => { export const useCreateFavorite = (args: MutationHookArgs) => {
const { options } = args || {}; const { options } = args || {};
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const setAlbumListData = useSetAlbumListItemDataById();
// const setQueueFavorite = useSetQueueFavorite();
const setFavoriteEvent = useFavoriteEvent(); const setFavoriteEvent = useFavoriteEvent();
return useMutation<FavoriteResponse, AxiosError, FavoriteArgs, null>({ return useMutation<FavoriteResponse, AxiosError, FavoriteArgs, null>({
@@ -32,6 +26,7 @@ export const useCreateFavorite = (args: MutationHookArgs) => {
apiClientProps: { serverId: args.apiClientProps.serverId }, apiClientProps: { serverId: args.apiClientProps.serverId },
}); });
}, },
mutationKey: createFavoriteQueryKey,
onError: (_error, variables) => { onError: (_error, variables) => {
eventEmitter.emit('USER_FAVORITE', { eventEmitter.emit('USER_FAVORITE', {
favorite: false, favorite: false,
@@ -51,56 +46,61 @@ export const useCreateFavorite = (args: MutationHookArgs) => {
return null; return null;
}, },
onSuccess: (_data, variables) => { onSuccess: (_data, variables) => {
const { apiClientProps } = variables;
const serverId = apiClientProps.serverId;
if (!serverId) return;
for (const id of variables.query.id) {
// Set the userFavorite property to true for the album in the album list data store
if (variables.query.type === LibraryItem.ALBUM) {
setAlbumListData(id, { userFavorite: true });
}
}
if (variables.query.type === LibraryItem.SONG) { if (variables.query.type === LibraryItem.SONG) {
remote?.updateFavorite(true, serverId, variables.query.id); remote?.updateFavorite(true, variables.apiClientProps.serverId, variables.query.id);
// setQueueFavorite(variables.query.id, true); // setQueueFavorite(variables.query.id, true);
setFavoriteEvent(variables.query.id, true); setFavoriteEvent(variables.query.id, true);
} }
// We only need to set if we're already on the album detail page switch (variables.query.type) {
if (variables.query.type === LibraryItem.ALBUM && variables.query.id.length === 1) { case LibraryItem.ALBUM: {
const queryKey = queryKeys.albums.detail(serverId, { id: variables.query.id[0] }); const queryKey = queryKeys.albums.detail(variables.apiClientProps.serverId);
const previous = queryClient.getQueryData<AlbumDetailResponse>(queryKey); queryClient.invalidateQueries({
exact: false,
if (previous) { queryKey,
queryClient.setQueryData<AlbumDetailResponse>(queryKey, {
...previous,
userFavorite: true,
}); });
break;
} }
} case LibraryItem.ALBUM_ARTIST: {
const queryKey = queryKeys.albumArtists.detail(
variables.apiClientProps.serverId,
);
// We only need to set if we're already on the album detail page queryClient.invalidateQueries({
if ( exact: false,
variables.query.type === LibraryItem.ALBUM_ARTIST && queryKey,
variables.query.id.length === 1
) {
const queryKey = queryKeys.albumArtists.detail(serverId, {
id: variables.query.id[0],
});
const previous = queryClient.getQueryData<AlbumArtistDetailResponse>(queryKey);
if (previous) {
queryClient.setQueryData<AlbumArtistDetailResponse>(queryKey, {
...previous,
userFavorite: true,
}); });
break;
}
case LibraryItem.ARTIST: {
const queryKey = queryKeys.artists.detail(variables.apiClientProps.serverId);
queryClient.invalidateQueries({
exact: false,
queryKey,
});
break;
}
case LibraryItem.SONG: {
const queryKey = queryKeys.songs.detail(variables.apiClientProps.serverId);
queryClient.invalidateQueries({
exact: false,
queryKey,
});
break;
} }
} }
}, },
...options, ...options,
}); });
}; };
export const useIsMutatingCreateFavorite = () => {
const mutatingCount = useIsMutating({ mutationKey: createFavoriteQueryKey });
return mutatingCount > 0;
};
@@ -1,4 +1,4 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useIsMutating, useMutation, useQueryClient } from '@tanstack/react-query';
import { AxiosError } from 'axios'; import { AxiosError } from 'axios';
import isElectron from 'is-electron'; import isElectron from 'is-electron';
@@ -6,25 +6,17 @@ import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys'; import { queryKeys } from '/@/renderer/api/query-keys';
import { eventEmitter } from '/@/renderer/events/event-emitter'; import { eventEmitter } from '/@/renderer/events/event-emitter';
import { MutationHookArgs } from '/@/renderer/lib/react-query'; import { MutationHookArgs } from '/@/renderer/lib/react-query';
import { useSetAlbumListItemDataById } from '/@/renderer/store';
import { useFavoriteEvent } from '/@/renderer/store/event.store'; import { useFavoriteEvent } from '/@/renderer/store/event.store';
import { import { FavoriteArgs, FavoriteResponse, LibraryItem } from '/@/shared/types/domain-types';
AlbumArtistDetailResponse,
AlbumDetailResponse,
FavoriteArgs,
FavoriteResponse,
LibraryItem,
} from '/@/shared/types/domain-types';
const remote = isElectron() ? window.api.remote : null; const remote = isElectron() ? window.api.remote : null;
const deleteFavoriteQueryKey = ['set-favorite', false];
export const useDeleteFavorite = (args: MutationHookArgs) => { export const useDeleteFavorite = (args: MutationHookArgs) => {
const { options } = args || {}; const { options } = args || {};
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const setAlbumListData = useSetAlbumListItemDataById();
// const setQueueFavorite = useSetQueueFavorite();
const setFavoriteEvent = useFavoriteEvent(); const setFavoriteEvent = useFavoriteEvent();
return useMutation<FavoriteResponse, AxiosError, FavoriteArgs, null>({ return useMutation<FavoriteResponse, AxiosError, FavoriteArgs, null>({
mutationFn: (args) => { mutationFn: (args) => {
return api.controller.deleteFavorite({ return api.controller.deleteFavorite({
@@ -32,6 +24,7 @@ export const useDeleteFavorite = (args: MutationHookArgs) => {
apiClientProps: { serverId: args.apiClientProps.serverId }, apiClientProps: { serverId: args.apiClientProps.serverId },
}); });
}, },
mutationKey: deleteFavoriteQueryKey,
onError: (_error, variables) => { onError: (_error, variables) => {
eventEmitter.emit('USER_FAVORITE', { eventEmitter.emit('USER_FAVORITE', {
favorite: true, favorite: true,
@@ -51,55 +44,64 @@ export const useDeleteFavorite = (args: MutationHookArgs) => {
return null; return null;
}, },
onSuccess: (_data, variables) => { onSuccess: (_data, variables) => {
const { apiClientProps } = variables;
const serverId = apiClientProps.serverId;
for (const id of variables.query.id) {
// Set the userFavorite property to false for the album in the album list data store
if (variables.query.type === LibraryItem.ALBUM) {
setAlbumListData(id, { userFavorite: false });
}
}
if (variables.query.type === LibraryItem.SONG) { if (variables.query.type === LibraryItem.SONG) {
remote?.updateFavorite(false, serverId, variables.query.id); remote?.updateFavorite(
// setQueueFavorite(variables.query.id, false); false,
variables.apiClientProps.serverId,
variables.query.id,
);
setFavoriteEvent(variables.query.id, false); setFavoriteEvent(variables.query.id, false);
} }
// We only need to set if we're already on the album detail page switch (variables.query.type) {
if (variables.query.type === LibraryItem.ALBUM && variables.query.id.length === 1) { case LibraryItem.ALBUM: {
const queryKey = queryKeys.albums.detail(serverId, { const queryKey = queryKeys.albums.detail(variables.apiClientProps.serverId);
id: variables.query.id[0], queryClient.invalidateQueries({
}); exact: false,
const previous = queryClient.getQueryData<AlbumDetailResponse>(queryKey); queryKey,
if (previous) {
queryClient.setQueryData<AlbumDetailResponse>(queryKey, {
...previous,
userFavorite: false,
}); });
break;
} }
} case LibraryItem.ALBUM_ARTIST: {
const queryKey = queryKeys.albumArtists.detail(
variables.apiClientProps.serverId,
);
// We only need to set if we're already on the album artist detail page queryClient.invalidateQueries({
if ( exact: false,
variables.query.type === LibraryItem.ALBUM_ARTIST && queryKey,
variables.query.id.length === 1
) {
const queryKey = queryKeys.albumArtists.detail(serverId, {
id: variables.query.id[0],
});
const previous = queryClient.getQueryData<AlbumArtistDetailResponse>(queryKey);
if (previous) {
queryClient.setQueryData<AlbumArtistDetailResponse>(queryKey, {
...previous,
userFavorite: false,
}); });
break;
}
case LibraryItem.ARTIST: {
const queryKey = queryKeys.artists.detail(variables.apiClientProps.serverId);
queryClient.invalidateQueries({
exact: false,
queryKey,
});
break;
}
case LibraryItem.SONG: {
const queryKey = queryKeys.songs.detail(variables.apiClientProps.serverId);
queryClient.invalidateQueries({
exact: false,
queryKey,
});
break;
} }
} }
}, },
...options, ...options,
}); });
}; };
export const useIsMutatingDeleteFavorite = () => {
const mutatingCount = useIsMutating({ mutationKey: deleteFavoriteQueryKey });
return mutatingCount > 0;
};
@@ -1,13 +1,17 @@
import { useMutation } from '@tanstack/react-query'; import { useIsMutating, useMutation, useQueryClient } from '@tanstack/react-query';
import { AxiosError } from 'axios'; import { AxiosError } from 'axios';
import { api } from '/@/renderer/api'; import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { eventEmitter } from '/@/renderer/events/event-emitter'; import { eventEmitter } from '/@/renderer/events/event-emitter';
import { MutationHookArgs } from '/@/renderer/lib/react-query'; import { MutationHookArgs } from '/@/renderer/lib/react-query';
import { LibraryItem, RatingResponse, SetRatingArgs } from '/@/shared/types/domain-types'; import { LibraryItem, RatingResponse, SetRatingArgs } from '/@/shared/types/domain-types';
const setRatingQueryKey = ['set-rating'];
export const useSetRating = (args: MutationHookArgs) => { export const useSetRating = (args: MutationHookArgs) => {
const { options } = args || {}; const { options } = args || {};
const queryClient = useQueryClient();
return useMutation< return useMutation<
RatingResponse, RatingResponse,
@@ -21,7 +25,8 @@ export const useSetRating = (args: MutationHookArgs) => {
apiClientProps: { serverId: args.apiClientProps.serverId }, apiClientProps: { serverId: args.apiClientProps.serverId },
}); });
}, },
onSuccess: (_data, variables) => { mutationKey: setRatingQueryKey,
onError: (_error, variables) => {
eventEmitter.emit('USER_RATING', { eventEmitter.emit('USER_RATING', {
id: variables.query.id, id: variables.query.id,
itemType: variables.query.type, itemType: variables.query.type,
@@ -29,6 +34,66 @@ export const useSetRating = (args: MutationHookArgs) => {
serverId: variables.apiClientProps.serverId, serverId: variables.apiClientProps.serverId,
}); });
}, },
onMutate: (variables) => {
eventEmitter.emit('USER_RATING', {
id: variables.query.id,
itemType: variables.query.type,
rating: variables.query.rating,
serverId: variables.apiClientProps.serverId,
});
return { previous: undefined };
},
onSuccess: (_data, variables) => {
switch (variables.query.type) {
case LibraryItem.ALBUM: {
const queryKey = queryKeys.albums.detail(variables.apiClientProps.serverId);
queryClient.invalidateQueries({
exact: false,
queryKey,
});
break;
}
case LibraryItem.ALBUM_ARTIST: {
const queryKey = queryKeys.albumArtists.detail(
variables.apiClientProps.serverId,
);
queryClient.invalidateQueries({
exact: false,
queryKey,
});
break;
}
case LibraryItem.ARTIST: {
const queryKey = queryKeys.artists.detail(variables.apiClientProps.serverId);
queryClient.invalidateQueries({
exact: false,
queryKey,
});
break;
}
case LibraryItem.SONG: {
const queryKey = queryKeys.songs.detail(variables.apiClientProps.serverId);
queryClient.invalidateQueries({
exact: false,
queryKey,
});
break;
}
}
},
...options, ...options,
}); });
}; };
export const useIsMutatingRating = () => {
const mutatingCount = useIsMutating({ mutationKey: setRatingQueryKey });
return mutatingCount > 0;
};
+1 -2
View File
@@ -20,14 +20,13 @@ const queryCache = new QueryCache({
const queryConfig: DefaultOptions = { const queryConfig: DefaultOptions = {
mutations: { mutations: {
gcTime: 1000 * 20, // 20 seconds
retry: process.env.NODE_ENV === 'production', retry: process.env.NODE_ENV === 'production',
}, },
queries: { queries: {
gcTime: 1000 * 5, // 5 seconds gcTime: 1000 * 5, // 5 seconds
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
retry: process.env.NODE_ENV === 'production', retry: process.env.NODE_ENV === 'production',
staleTime: 1000 * 5, // 5 seconds staleTime: 0, // 5 seconds
throwOnError: (error: any) => { throwOnError: (error: any) => {
return error?.response?.status >= 500; return error?.response?.status >= 500;
}, },