From 7701ea0a8c71958b8128433883434d6662fa7883 Mon Sep 17 00:00:00 2001 From: jeffvli Date: Tue, 2 Dec 2025 17:23:18 -0800 Subject: [PATCH] support multiple items in item details modal --- .../context-menu/actions/get-info-action.tsx | 22 +++-- .../menus/album-artist-context-menu.tsx | 2 +- .../context-menu/menus/album-context-menu.tsx | 2 +- .../menus/artist-context-menu.tsx | 2 +- .../menus/playlist-context-menu.tsx | 2 +- .../menus/playlist-song-context-menu.tsx | 2 +- .../context-menu/menus/queue-context-menu.tsx | 2 +- .../context-menu/menus/song-context-menu.tsx | 2 +- .../components/item-details-modal.tsx | 94 +++++++++++++------ 9 files changed, 88 insertions(+), 42 deletions(-) diff --git a/src/renderer/features/context-menu/actions/get-info-action.tsx b/src/renderer/features/context-menu/actions/get-info-action.tsx index bb3e651d5..2f4b041a6 100644 --- a/src/renderer/features/context-menu/actions/get-info-action.tsx +++ b/src/renderer/features/context-menu/actions/get-info-action.tsx @@ -11,25 +11,35 @@ import { ContextMenu } from '/@/shared/components/context-menu/context-menu'; interface GetInfoActionProps { disabled?: boolean; - item: ItemDetailsModalProps['item']; + items: ItemDetailsModalProps['item'][]; } -export const GetInfoAction = ({ disabled, item }: GetInfoActionProps) => { +export const GetInfoAction = ({ disabled, items }: GetInfoActionProps) => { const { t } = useTranslation(); const server = useCurrentServer(); const onSelect = useCallback(async () => { - if (!server) return; + if (!server || items.length === 0) return; + + const filteredItems = items.filter( + (item): item is NonNullable => item !== undefined, + ); + + if (filteredItems.length === 0) return; openModal({ - children: , + children: , size: 'lg', styles: { body: { paddingBottom: 'var(--theme-spacing-xl)' }, }, - title: item.name || t('page.contextMenu.showDetails', { postProcess: 'sentenceCase' }), + title: + filteredItems.length === 1 + ? filteredItems[0]?.name || + t('page.contextMenu.showDetails', { postProcess: 'sentenceCase' }) + : t('page.contextMenu.showDetails', { postProcess: 'sentenceCase' }), }); - }, [item, server, t]); + }, [items, server, t]); return ( diff --git a/src/renderer/features/context-menu/menus/album-artist-context-menu.tsx b/src/renderer/features/context-menu/menus/album-artist-context-menu.tsx index 470aab803..50e211ef6 100644 --- a/src/renderer/features/context-menu/menus/album-artist-context-menu.tsx +++ b/src/renderer/features/context-menu/menus/album-artist-context-menu.tsx @@ -39,7 +39,7 @@ export const AlbumArtistContextMenu = ({ items, type }: AlbumArtistContextMenuPr - + ); }; diff --git a/src/renderer/features/context-menu/menus/album-context-menu.tsx b/src/renderer/features/context-menu/menus/album-context-menu.tsx index 21a094c5d..d1f9ec846 100644 --- a/src/renderer/features/context-menu/menus/album-context-menu.tsx +++ b/src/renderer/features/context-menu/menus/album-context-menu.tsx @@ -39,7 +39,7 @@ export const AlbumContextMenu = ({ items, type }: AlbumContextMenuProps) => { - + ); }; diff --git a/src/renderer/features/context-menu/menus/artist-context-menu.tsx b/src/renderer/features/context-menu/menus/artist-context-menu.tsx index 766f0f91d..d8567a73e 100644 --- a/src/renderer/features/context-menu/menus/artist-context-menu.tsx +++ b/src/renderer/features/context-menu/menus/artist-context-menu.tsx @@ -39,7 +39,7 @@ export const ArtistContextMenu = ({ items, type }: ArtistContextMenuProps) => { - + ); }; diff --git a/src/renderer/features/context-menu/menus/playlist-context-menu.tsx b/src/renderer/features/context-menu/menus/playlist-context-menu.tsx index 4a2fb59fc..2a25ff555 100644 --- a/src/renderer/features/context-menu/menus/playlist-context-menu.tsx +++ b/src/renderer/features/context-menu/menus/playlist-context-menu.tsx @@ -28,7 +28,7 @@ export const PlaylistContextMenu = ({ items, type }: PlaylistContextMenuProps) = - + diff --git a/src/renderer/features/context-menu/menus/playlist-song-context-menu.tsx b/src/renderer/features/context-menu/menus/playlist-song-context-menu.tsx index 126534335..5fca1aa71 100644 --- a/src/renderer/features/context-menu/menus/playlist-song-context-menu.tsx +++ b/src/renderer/features/context-menu/menus/playlist-song-context-menu.tsx @@ -42,7 +42,7 @@ export const PlaylistSongContextMenu = ({ items, type }: PlaylistSongContextMenu - + ); }; diff --git a/src/renderer/features/context-menu/menus/queue-context-menu.tsx b/src/renderer/features/context-menu/menus/queue-context-menu.tsx index 54b8041a9..927d78685 100644 --- a/src/renderer/features/context-menu/menus/queue-context-menu.tsx +++ b/src/renderer/features/context-menu/menus/queue-context-menu.tsx @@ -43,7 +43,7 @@ export const QueueContextMenu = ({ items }: QueueContextMenuProps) => { - + ); }; diff --git a/src/renderer/features/context-menu/menus/song-context-menu.tsx b/src/renderer/features/context-menu/menus/song-context-menu.tsx index bbda6f0ec..b5e77218a 100644 --- a/src/renderer/features/context-menu/menus/song-context-menu.tsx +++ b/src/renderer/features/context-menu/menus/song-context-menu.tsx @@ -39,7 +39,7 @@ export const SongContextMenu = ({ items, type }: SongContextMenuProps) => { - + ); }; diff --git a/src/renderer/features/item-details/components/item-details-modal.tsx b/src/renderer/features/item-details/components/item-details-modal.tsx index fc732c6a2..6eebffdcb 100644 --- a/src/renderer/features/item-details/components/item-details-modal.tsx +++ b/src/renderer/features/item-details/components/item-details-modal.tsx @@ -1,5 +1,5 @@ import { TFunction } from 'i18next'; -import { ReactNode } from 'react'; +import { ReactNode, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { generatePath, Link } from 'react-router'; @@ -12,8 +12,10 @@ import { normalizeReleaseTypes } from '/@/renderer/utils/normalize-release-types import { sanitize } from '/@/renderer/utils/sanitize'; import { SEPARATOR_STRING } from '/@/shared/api/utils'; import { Icon } from '/@/shared/components/icon/icon'; +import { Select } from '/@/shared/components/select/select'; import { Separator } from '/@/shared/components/separator/separator'; import { Spoiler } from '/@/shared/components/spoiler/spoiler'; +import { Stack } from '/@/shared/components/stack/stack'; import { Table } from '/@/shared/components/table/table'; import { Text } from '/@/shared/components/text/text'; import { @@ -29,7 +31,8 @@ import { } from '/@/shared/types/domain-types'; export type ItemDetailsModalProps = { - item: Album | AlbumArtist | Artist | Playlist | Song; + item?: Album | AlbumArtist | Artist | Playlist | Song; + items?: (Album | AlbumArtist | Artist | Playlist | Song)[]; }; type ItemDetailRow = { @@ -404,48 +407,81 @@ const handleParticipants = (item: Album | Song, t: TFunction) => { return []; }; -export const ItemDetailsModal = ({ item }: ItemDetailsModalProps) => { +export const ItemDetailsModal = ({ item, items }: ItemDetailsModalProps) => { const { t } = useTranslation(); + const allItems = useMemo(() => items || (item ? [item] : []), [item, items]); + const [selectedIndex, setSelectedIndex] = useState(0); + + const selectedItem = useMemo(() => { + return allItems[selectedIndex] || null; + }, [allItems, selectedIndex]); + + const selectData = useMemo(() => { + return allItems.map((it, index) => ({ + label: + it.name || + `${t('common.item', { defaultValue: 'Item', postProcess: 'sentenceCase' })} ${index + 1}`, + value: String(index), + })); + }, [allItems, t]); + + if (!selectedItem) { + return null; + } + let body: ReactNode[] = []; - switch (item._itemType) { + switch (selectedItem._itemType) { case LibraryItem.ALBUM: - body = AlbumPropertyMapping.map((rule) => handleRow(t, item, rule)); - body.push(...handleParticipants(item, t)); - body.push(...handleTags(item, t)); + body = AlbumPropertyMapping.map((rule) => handleRow(t, selectedItem, rule)); + body.push(...handleParticipants(selectedItem, t)); + body.push(...handleTags(selectedItem, t)); break; case LibraryItem.ALBUM_ARTIST: - body = AlbumArtistPropertyMapping.map((rule) => handleRow(t, item, rule)); + body = AlbumArtistPropertyMapping.map((rule) => handleRow(t, selectedItem, rule)); break; case LibraryItem.PLAYLIST: - body = PlaylistPropertyMapping.map((rule) => handleRow(t, item, rule)); + body = PlaylistPropertyMapping.map((rule) => handleRow(t, selectedItem, rule)); break; case LibraryItem.SONG: - body = SongPropertyMapping.map((rule) => handleRow(t, item, rule)); - body.push(...handleParticipants(item, t)); - body.push(...handleTags(item, t)); + body = SongPropertyMapping.map((rule) => handleRow(t, selectedItem, rule)); + body.push(...handleParticipants(selectedItem, t)); + body.push(...handleTags(selectedItem, t)); break; default: body = []; } return ( - - {body} -
+ + {allItems.length > 1 && ( +