From 84419820b867f75d92fac8f2fd5d89b19b315f05 Mon Sep 17 00:00:00 2001 From: jeffvli Date: Sun, 23 Nov 2025 03:40:53 -0800 Subject: [PATCH] add optimistic update for favorite/ratings mutations --- src/renderer/api/query-keys.ts | 45 ++ .../item-card/item-card-controls.tsx | 99 +++-- .../helpers/item-list-infinite-loader.ts | 4 +- .../columns/favorite-column.tsx | 7 + .../item-table-list/columns/rating-column.tsx | 4 + .../components/album-detail-content.tsx | 4 +- .../components/album-infinite-carousel.tsx | 2 +- .../album-artist-infinite-carousel.tsx | 2 +- .../player/mutations/scrobble-mutation.ts | 4 +- .../shared/components/library-header.tsx | 3 +- .../mutations/create-favorite-mutation.ts | 92 ++--- .../mutations/delete-favorite-mutation.ts | 98 ++--- .../mutations/favorite-optimistic-updates.ts | 389 ++++++++++++++++++ .../mutations/rating-optimistic-updates.ts | 354 ++++++++++++++++ .../shared/mutations/set-rating-mutation.ts | 104 ++--- src/renderer/store/event.store.ts | 71 ---- src/renderer/themes/mantine-theme.tsx | 11 +- .../action-icon/action-icon.module.css | 4 + src/shared/types/domain-types.ts | 2 +- 19 files changed, 975 insertions(+), 324 deletions(-) create mode 100644 src/renderer/features/shared/mutations/favorite-optimistic-updates.ts create mode 100644 src/renderer/features/shared/mutations/rating-optimistic-updates.ts delete mode 100644 src/renderer/store/event.store.ts diff --git a/src/renderer/api/query-keys.ts b/src/renderer/api/query-keys.ts index e188c9980..98301f27d 100644 --- a/src/renderer/api/query-keys.ts +++ b/src/renderer/api/query-keys.ts @@ -71,6 +71,18 @@ export const queryKeys: Record< return [serverId, 'albumArtists', 'detail'] as const; }, + infiniteList: (serverId: string, query?: AlbumArtistListQuery) => { + const { filter, pagination } = splitPaginatedQuery(query); + if (query && pagination) { + return [serverId, 'albumArtists', 'infiniteList', filter, pagination] as const; + } + + if (query) { + return [serverId, 'albumArtists', 'infiniteList', filter] as const; + } + + return [serverId, 'albumArtists', 'infiniteList'] as const; + }, list: (serverId: string, query?: AlbumArtistListQuery) => { const { filter, pagination } = splitPaginatedQuery(query); if (query && pagination) { @@ -118,6 +130,27 @@ export const queryKeys: Record< return [serverId, 'albums', 'detail'] as const; }, + infiniteList: (serverId: string, query?: AlbumListQuery, artistId?: string) => { + const { filter, pagination } = splitPaginatedQuery(query); + + if (query && pagination && artistId) { + return [serverId, 'albums', 'infiniteList', artistId, filter, pagination] as const; + } + + if (query && pagination) { + return [serverId, 'albums', 'infiniteList', filter, pagination] as const; + } + + if (query && artistId) { + return [serverId, 'albums', 'infiniteList', artistId, filter] as const; + } + + if (query) { + return [serverId, 'albums', 'infiniteList', filter] as const; + } + + return [serverId, 'albums', 'infiniteList'] as const; + }, list: (serverId: string, query?: AlbumListQuery, artistId?: string) => { const { filter, pagination } = splitPaginatedQuery(query); @@ -165,6 +198,18 @@ export const queryKeys: Record< return [serverId, 'artists', 'count'] as const; }, + infiniteList: (serverId: string, query?: ArtistListQuery) => { + const { filter, pagination } = splitPaginatedQuery(query); + if (query && pagination) { + return [serverId, 'artists', 'infiniteList', filter, pagination] as const; + } + + if (query) { + return [serverId, 'artists', 'infiniteList', filter] as const; + } + + return [serverId, 'artists', 'infiniteList'] as const; + }, list: (serverId: string, query?: ArtistListQuery) => { const { filter, pagination } = splitPaginatedQuery(query); if (query && pagination) { diff --git a/src/renderer/components/item-card/item-card-controls.tsx b/src/renderer/components/item-card/item-card-controls.tsx index a7d345e87..4a32f10f8 100644 --- a/src/renderer/components/item-card/item-card-controls.tsx +++ b/src/renderer/components/item-card/item-card-controls.tsx @@ -7,6 +7,9 @@ import styles from './item-card-controls.module.css'; import { ItemListStateActions } from '/@/renderer/components/item-list/helpers/item-list-state'; import { ItemControls } from '/@/renderer/components/item-list/types'; import { useIsPlayerFetching } from '/@/renderer/features/player/context/player-context'; +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 { animationVariants } from '/@/shared/components/animations/animation-variants'; import { AppIcon, Icon, IconProps } from '/@/shared/components/icon/icon'; import { Rating } from '/@/shared/components/rating/rating'; @@ -127,16 +130,6 @@ const createRatingChangeHandler = }); }; -const ratingClickHandler = (e: MouseEvent) => { - e.stopPropagation(); - e.preventDefault(); -}; - -const ratingMouseDownHandler = (e: React.MouseEvent) => { - e.stopPropagation(); - e.preventDefault(); -}; - const moreDoubleClickHandler = (e: MouseEvent) => { e.stopPropagation(); e.preventDefault(); @@ -225,14 +218,6 @@ export const ItemCardControls = ({ const isFavorite = (item as { userFavorite?: boolean })?.userFavorite ?? false; - const favoriteIconProps = useMemo>( - () => ({ - color: isFavorite ? ('primary' as const) : ('default' as const), - fill: isFavorite ? ('primary' as const) : undefined, - }), - [isFavorite], - ); - return ( {controls?.onPlay && ( @@ -251,20 +236,12 @@ export const ItemCardControls = ({ )} {controls?.onFavorite && ( - + )} {controls?.onRating && ( - )} {controls?.onMore && ( @@ -286,6 +263,67 @@ export const ItemCardControls = ({ ); }; +const FavoriteButton = memo( + ({ + isFavorite, + onClick, + }: { + isFavorite: boolean; + onClick?: (e: MouseEvent) => void; + }) => { + const isMutatingCreate = useIsMutatingCreateFavorite(); + const isMutatingDelete = useIsMutatingDeleteFavorite(); + const isMutating = isMutatingCreate || isMutatingDelete; + + const favoriteIconProps = useMemo>( + () => ({ + color: isFavorite ? ('primary' as const) : ('default' as const), + fill: isFavorite ? ('primary' as const) : undefined, + }), + [isFavorite], + ); + + return ( + + ); + }, + (prev, next) => prev.isFavorite === next.isFavorite, +); + +const RatingButton = memo( + ({ onChange, rating }: { onChange: (rating: number) => void; rating: number }) => { + const ratingClickHandler = (e: MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + }; + + const ratingMouseDownHandler = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + }; + + const isMutatingRating = useIsMutatingRating(); + return ( + + ); + }, + (prev, next) => prev.rating === next.rating, +); + const PlayButton = memo( ({ disabled, @@ -360,6 +398,7 @@ const SecondaryPlayButton = memo( interface SecondaryButtonProps { className?: string; + disabled?: boolean; icon: keyof typeof AppIcon; onClick?: (e: MouseEvent) => void; } @@ -367,6 +406,7 @@ interface SecondaryButtonProps { const SecondaryButton = memo( ({ className, + disabled, icon, iconProps, onClick, @@ -395,6 +435,7 @@ const SecondaryButton = memo( return (