From 69f7f5c236b88d4fe32097b6502aa69dc105b738 Mon Sep 17 00:00:00 2001 From: jeffvli Date: Tue, 18 Nov 2025 14:07:46 -0800 Subject: [PATCH] handle favorite/rating events for all tables --- .../item-card/item-card-controls.tsx | 18 ++++++++-- .../item-list/helpers/item-list-controls.ts | 26 +++++++++++++-- .../helpers/item-list-infinite-loader.ts | 18 ++++++++-- .../helpers/item-list-paginated-loader.ts | 18 ++++++++-- .../components/play-queue.module.css | 3 +- .../now-playing/components/play-queue.tsx | 33 +++++++++++++++++++ .../mutations/create-favorite-mutation.ts | 2 ++ .../mutations/delete-favorite-mutation.ts | 2 ++ src/renderer/store/player.store.ts | 11 ++++++- 9 files changed, 118 insertions(+), 13 deletions(-) diff --git a/src/renderer/components/item-card/item-card-controls.tsx b/src/renderer/components/item-card/item-card-controls.tsx index fdcce6094..4deaf5e0a 100644 --- a/src/renderer/components/item-card/item-card-controls.tsx +++ b/src/renderer/components/item-card/item-card-controls.tsx @@ -8,7 +8,7 @@ import { ItemListStateActions } from '/@/renderer/components/item-list/helpers/i import { ItemControls } from '/@/renderer/components/item-list/types'; import { useIsPlayerFetching } from '/@/renderer/features/player/context/player-context'; import { animationVariants } from '/@/shared/components/animations/animation-variants'; -import { AppIcon, Icon } from '/@/shared/components/icon/icon'; +import { AppIcon, Icon, IconProps } from '/@/shared/components/icon/icon'; import { Rating } from '/@/shared/components/rating/rating'; import { Album, @@ -124,6 +124,14 @@ export const ItemCardControls = ({ { e.stopPropagation(); @@ -268,9 +276,13 @@ interface SecondaryButtonProps { const SecondaryButton = ({ className, icon, + iconProps, onClick, onDoubleClick, -}: SecondaryButtonProps & { onDoubleClick?: (e: MouseEvent) => void }) => { +}: SecondaryButtonProps & { + iconProps?: Partial; + onDoubleClick?: (e: MouseEvent) => void; +}) => { return ( ); }; diff --git a/src/renderer/components/item-list/helpers/item-list-controls.ts b/src/renderer/components/item-list/helpers/item-list-controls.ts index f267e7f99..2643d2631 100644 --- a/src/renderer/components/item-list/helpers/item-list-controls.ts +++ b/src/renderer/components/item-list/helpers/item-list-controls.ts @@ -18,6 +18,16 @@ interface UseDefaultItemListControlsArgs { onColumnResized?: (columnId: TableColumn, width: number) => void; } +const itemTypeMapping = { + [LibraryItem.ALBUM]: LibraryItem.ALBUM, + [LibraryItem.ALBUM_ARTIST]: LibraryItem.ALBUM_ARTIST, + [LibraryItem.ARTIST]: LibraryItem.ARTIST, + [LibraryItem.GENRE]: LibraryItem.GENRE, + [LibraryItem.PLAYLIST]: LibraryItem.PLAYLIST, + [LibraryItem.PLAYLIST_SONG]: LibraryItem.SONG, + [LibraryItem.QUEUE_SONG]: LibraryItem.SONG, +}; + export const useDefaultItemListControls = (args?: UseDefaultItemListControlsArgs) => { const player = usePlayer(); const navigate = useNavigate(); @@ -228,7 +238,13 @@ export const useDefaultItemListControls = (args?: UseDefaultItemListControlsArgs return; } - player.setFavorite(item._serverId, [item.id], itemType, favorite); + const apiItemType = itemTypeMapping[itemType] || itemType; + + if (!item.id || !item._serverId) { + return; + } + + player.setFavorite(item._serverId, [item.id], apiItemType, favorite); }, onMore: ({ event, internalState, item, itemType }: DefaultItemControlProps) => { @@ -294,6 +310,12 @@ export const useDefaultItemListControls = (args?: UseDefaultItemListControlsArgs return; } + const apiItemType = itemTypeMapping[itemType] || itemType; + + if (!item.id || !item._serverId) { + return; + } + const previousRating = (item as { userRating: number }).userRating || 0; let newRating = rating; @@ -302,7 +324,7 @@ export const useDefaultItemListControls = (args?: UseDefaultItemListControlsArgs newRating = 0; } - player.setRating(item._serverId, [item.id], itemType, newRating); + player.setRating(item._serverId, [item.id], apiItemType, newRating); }, }; }, [onColumnReordered, onColumnResized, navigate, player]); diff --git a/src/renderer/components/item-list/helpers/item-list-infinite-loader.ts b/src/renderer/components/item-list/helpers/item-list-infinite-loader.ts index aeee59d40..7029a7d53 100644 --- a/src/renderer/components/item-list/helpers/item-list-infinite-loader.ts +++ b/src/renderer/components/item-list/helpers/item-list-infinite-loader.ts @@ -264,6 +264,10 @@ export const useItemListInfiniteLoader = ({ useEffect(() => { const handleFavorite = (payload: UserFavoriteEventPayload) => { + if (payload.itemType !== itemType || payload.serverId !== serverId) { + return; + } + const idToIndexMap = data.data .filter(Boolean) .reduce((acc: Record, item: any, index: number) => { @@ -271,7 +275,9 @@ export const useItemListInfiniteLoader = ({ return acc; }, {}); - const dataIndexes = payload.id.map((id: string) => idToIndexMap[id]); + const dataIndexes = payload.id + .map((id: string) => idToIndexMap[id]) + .filter((idx) => idx !== undefined); if (dataIndexes.length === 0) { return; @@ -281,6 +287,10 @@ export const useItemListInfiniteLoader = ({ }; const handleRating = (payload: UserRatingEventPayload) => { + if (payload.itemType !== itemType || payload.serverId !== serverId) { + return; + } + const idToIndexMap = data.data .filter(Boolean) .reduce((acc: Record, item: any, index: number) => { @@ -288,7 +298,9 @@ export const useItemListInfiniteLoader = ({ return acc; }, {}); - const dataIndexes = payload.id.map((id: string) => idToIndexMap[id]); + const dataIndexes = payload.id + .map((id: string) => idToIndexMap[id]) + .filter((idx) => idx !== undefined); if (dataIndexes.length === 0) { return; @@ -304,7 +316,7 @@ export const useItemListInfiniteLoader = ({ eventEmitter.off('USER_FAVORITE', handleFavorite); eventEmitter.off('USER_RATING', handleRating); }; - }, [data, eventKey, updateItems]); + }, [data, eventKey, itemType, serverId, updateItems]); return { data: data.data, onRangeChanged, refresh, updateItems }; }; diff --git a/src/renderer/components/item-list/helpers/item-list-paginated-loader.ts b/src/renderer/components/item-list/helpers/item-list-paginated-loader.ts index dae9ece90..105e38366 100644 --- a/src/renderer/components/item-list/helpers/item-list-paginated-loader.ts +++ b/src/renderer/components/item-list/helpers/item-list-paginated-loader.ts @@ -145,6 +145,10 @@ export const useItemListPaginatedLoader = ({ return; } + if (payload.itemType !== itemType || payload.serverId !== serverId) { + return; + } + const idToIndexMap = data .filter(Boolean) .reduce((acc: Record, item: any, index: number) => { @@ -152,7 +156,9 @@ export const useItemListPaginatedLoader = ({ return acc; }, {}); - const dataIndexes = payload.id.map((id: string) => idToIndexMap[id]); + const dataIndexes = payload.id + .map((id: string) => idToIndexMap[id]) + .filter((idx) => idx !== undefined); if (dataIndexes.length === 0) { return; @@ -166,6 +172,10 @@ export const useItemListPaginatedLoader = ({ return; } + if (payload.itemType !== itemType || payload.serverId !== serverId) { + return; + } + const idToIndexMap = data.reduce( (acc: Record, item: any, index: number) => { acc[item.id] = index; @@ -174,7 +184,9 @@ export const useItemListPaginatedLoader = ({ {}, ); - const dataIndexes = payload.id.map((id: string) => idToIndexMap[id]); + const dataIndexes = payload.id + .map((id: string) => idToIndexMap[id]) + .filter((idx) => idx !== undefined); if (dataIndexes.length === 0) { return; @@ -192,7 +204,7 @@ export const useItemListPaginatedLoader = ({ eventEmitter.off('USER_FAVORITE', handleFavorite); eventEmitter.off('USER_RATING', handleRating); }; - }, [data, eventKey, refresh, updateItems]); + }, [data, eventKey, itemType, serverId, refresh, updateItems]); return { data, pageCount, totalItemCount }; }; diff --git a/src/renderer/features/now-playing/components/play-queue.module.css b/src/renderer/features/now-playing/components/play-queue.module.css index 93d2e621f..ebe3d934c 100644 --- a/src/renderer/features/now-playing/components/play-queue.module.css +++ b/src/renderer/features/now-playing/components/play-queue.module.css @@ -8,9 +8,10 @@ .group-row { display: flex; align-items: center; - justify-content: center; width: 100%; height: 100%; + padding: 0 var(--theme-spacing-md); + text-transform: uppercase; user-select: none; } diff --git a/src/renderer/features/now-playing/components/play-queue.tsx b/src/renderer/features/now-playing/components/play-queue.tsx index c816addb1..9411a5b41 100644 --- a/src/renderer/features/now-playing/components/play-queue.tsx +++ b/src/renderer/features/now-playing/components/play-queue.tsx @@ -11,11 +11,16 @@ import { } from '/@/renderer/components/item-list/item-table-list/item-table-list'; import { ItemTableListColumn } from '/@/renderer/components/item-list/item-table-list/item-table-list-column'; import { ItemListHandle } from '/@/renderer/components/item-list/types'; +import { eventEmitter } from '/@/renderer/events/event-emitter'; +import { UserFavoriteEventPayload, UserRatingEventPayload } from '/@/renderer/events/events'; import { useIsPlayerFetching, usePlayer } from '/@/renderer/features/player/context/player-context'; import { useDragDrop } from '/@/renderer/hooks/use-drag-drop'; import { subscribeCurrentTrack, subscribePlayerQueue, + updateQueueFavorites, + updateQueueRatings, + useCurrentServerId, useListSettings, usePlayerActions, usePlayerQueueType, @@ -43,6 +48,7 @@ export const PlayQueue = forwardRef(({ listKey, sear const mergedRef = useMergedRef(ref, tableRef); const { getQueue } = usePlayerActions(); const queueType = usePlayerQueueType(); + const serverId = useCurrentServerId(); const [debouncedSearchTerm] = useDebouncedValue(searchTerm, 200); @@ -104,6 +110,33 @@ export const PlayQueue = forwardRef(({ listKey, sear }; }, [getQueue, queueType, tableRef]); + // Listen to favorite and rating events to update queue songs + useEffect(() => { + const handleFavorite = (payload: UserFavoriteEventPayload) => { + if (payload.itemType !== LibraryItem.SONG || payload.serverId !== serverId) { + return; + } + + updateQueueFavorites(payload.id, payload.favorite); + }; + + const handleRating = (payload: UserRatingEventPayload) => { + if (payload.itemType !== LibraryItem.SONG || payload.serverId !== serverId) { + return; + } + + updateQueueRatings(payload.id, payload.rating); + }; + + eventEmitter.on('USER_FAVORITE', handleFavorite); + eventEmitter.on('USER_RATING', handleRating); + + return () => { + eventEmitter.off('USER_FAVORITE', handleFavorite); + eventEmitter.off('USER_RATING', handleRating); + }; + }, [serverId]); + const filteredData: QueueSong[] = useMemo(() => { if (debouncedSearchTerm) { const searched = searchSongs(data, debouncedSearchTerm); diff --git a/src/renderer/features/shared/mutations/create-favorite-mutation.ts b/src/renderer/features/shared/mutations/create-favorite-mutation.ts index e0823f44e..c0128ad84 100644 --- a/src/renderer/features/shared/mutations/create-favorite-mutation.ts +++ b/src/renderer/features/shared/mutations/create-favorite-mutation.ts @@ -37,6 +37,7 @@ export const useCreateFavorite = (args: MutationHookArgs) => { favorite: false, id: variables.query.id, itemType: variables.query.type, + serverId: variables.apiClientProps.serverId, }); }, onMutate: (variables) => { @@ -44,6 +45,7 @@ export const useCreateFavorite = (args: MutationHookArgs) => { favorite: true, id: variables.query.id, itemType: variables.query.type, + serverId: variables.apiClientProps.serverId, }); return null; diff --git a/src/renderer/features/shared/mutations/delete-favorite-mutation.ts b/src/renderer/features/shared/mutations/delete-favorite-mutation.ts index 9f1280329..02cc80ee0 100644 --- a/src/renderer/features/shared/mutations/delete-favorite-mutation.ts +++ b/src/renderer/features/shared/mutations/delete-favorite-mutation.ts @@ -37,6 +37,7 @@ export const useDeleteFavorite = (args: MutationHookArgs) => { favorite: true, id: variables.query.id, itemType: variables.query.type, + serverId: variables.apiClientProps.serverId, }); }, onMutate: (variables) => { @@ -44,6 +45,7 @@ export const useDeleteFavorite = (args: MutationHookArgs) => { favorite: false, id: variables.query.id, itemType: variables.query.type, + serverId: variables.apiClientProps.serverId, }); return null; diff --git a/src/renderer/store/player.store.ts b/src/renderer/store/player.store.ts index 6d29a2aa3..d0b0967ae 100644 --- a/src/renderer/store/player.store.ts +++ b/src/renderer/store/player.store.ts @@ -1448,7 +1448,6 @@ export const usePlayerData = (): PlayerData => { export const updateQueueFavorites = (ids: string[], favorite: boolean) => { usePlayerStoreBase.setState((state) => { - // Update songs in the songs object Object.values(state.queue.songs).forEach((song) => { if (ids.includes(song.id)) { song.userFavorite = favorite; @@ -1457,6 +1456,16 @@ export const updateQueueFavorites = (ids: string[], favorite: boolean) => { }); }; +export const updateQueueRatings = (ids: string[], rating: null | number) => { + usePlayerStoreBase.setState((state) => { + Object.values(state.queue.songs).forEach((song) => { + if (ids.includes(song.id)) { + song.userRating = rating; + } + }); + }); +}; + export const usePlayerMuted = () => { return usePlayerStoreBase((state) => state.player.muted); };