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 f1dbc636e..470aab803 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 @@ -9,20 +9,24 @@ import { SetFavoriteAction } from '/@/renderer/features/context-menu/actions/set import { SetRatingAction } from '/@/renderer/features/context-menu/actions/set-rating-action'; import { ShareAction } from '/@/renderer/features/context-menu/actions/share-action'; import { ContextMenu } from '/@/shared/components/context-menu/context-menu'; +import { ContextMenuPreview } from '/@/shared/components/context-menu/context-menu-preview'; import { AlbumArtist, LibraryItem } from '/@/shared/types/domain-types'; interface AlbumArtistContextMenuProps { items: AlbumArtist[]; + type: LibraryItem.ALBUM_ARTIST; } -export const AlbumArtistContextMenu = ({ items }: AlbumArtistContextMenuProps) => { +export const AlbumArtistContextMenu = ({ items, type }: AlbumArtistContextMenuProps) => { const { ids } = useMemo(() => { const ids = items.map((item) => item.id); return { ids }; }, [items]); return ( - + } + > 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 3c1c3cfeb..21a094c5d 100644 --- a/src/renderer/features/context-menu/menus/album-context-menu.tsx +++ b/src/renderer/features/context-menu/menus/album-context-menu.tsx @@ -9,20 +9,24 @@ import { SetFavoriteAction } from '/@/renderer/features/context-menu/actions/set import { SetRatingAction } from '/@/renderer/features/context-menu/actions/set-rating-action'; import { ShareAction } from '/@/renderer/features/context-menu/actions/share-action'; import { ContextMenu } from '/@/shared/components/context-menu/context-menu'; +import { ContextMenuPreview } from '/@/shared/components/context-menu/context-menu-preview'; import { Album, LibraryItem } from '/@/shared/types/domain-types'; interface AlbumContextMenuProps { items: Album[]; + type: LibraryItem.ALBUM; } -export const AlbumContextMenu = ({ items }: AlbumContextMenuProps) => { +export const AlbumContextMenu = ({ items, type }: AlbumContextMenuProps) => { const { ids } = useMemo(() => { const ids = items.map((item) => item.id); return { ids }; }, [items]); return ( - + } + > 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 77a8780d6..766f0f91d 100644 --- a/src/renderer/features/context-menu/menus/artist-context-menu.tsx +++ b/src/renderer/features/context-menu/menus/artist-context-menu.tsx @@ -9,20 +9,24 @@ import { SetFavoriteAction } from '/@/renderer/features/context-menu/actions/set import { SetRatingAction } from '/@/renderer/features/context-menu/actions/set-rating-action'; import { ShareAction } from '/@/renderer/features/context-menu/actions/share-action'; import { ContextMenu } from '/@/shared/components/context-menu/context-menu'; +import { ContextMenuPreview } from '/@/shared/components/context-menu/context-menu-preview'; import { Artist, LibraryItem } from '/@/shared/types/domain-types'; interface ArtistContextMenuProps { items: Artist[]; + type: LibraryItem.ARTIST; } -export const ArtistContextMenu = ({ items }: ArtistContextMenuProps) => { +export const ArtistContextMenu = ({ items, type }: ArtistContextMenuProps) => { const { ids } = useMemo(() => { const ids = items.map((item) => item.id); return { ids }; }, [items]); return ( - + } + > diff --git a/src/renderer/features/context-menu/menus/genre-context-menu.tsx b/src/renderer/features/context-menu/menus/genre-context-menu.tsx index d2e636fee..5de3705dd 100644 --- a/src/renderer/features/context-menu/menus/genre-context-menu.tsx +++ b/src/renderer/features/context-menu/menus/genre-context-menu.tsx @@ -3,20 +3,24 @@ import { useMemo } from 'react'; import { AddToPlaylistAction } from '/@/renderer/features/context-menu/actions/add-to-playlist-action'; import { PlayAction } from '/@/renderer/features/context-menu/actions/play-action'; import { ContextMenu } from '/@/shared/components/context-menu/context-menu'; +import { ContextMenuPreview } from '/@/shared/components/context-menu/context-menu-preview'; import { Genre, LibraryItem } from '/@/shared/types/domain-types'; interface GenreContextMenuProps { items: Genre[]; + type: LibraryItem.GENRE; } -export const GenreContextMenu = ({ items }: GenreContextMenuProps) => { +export const GenreContextMenu = ({ items, type }: GenreContextMenuProps) => { const { ids } = useMemo(() => { const ids = items.map((item) => item.id); return { ids }; }, [items]); return ( - + } + > 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 af0c2f334..03728d977 100644 --- a/src/renderer/features/context-menu/menus/playlist-context-menu.tsx +++ b/src/renderer/features/context-menu/menus/playlist-context-menu.tsx @@ -5,20 +5,24 @@ import { DeletePlaylistAction } from '/@/renderer/features/context-menu/actions/ import { GetInfoAction } from '/@/renderer/features/context-menu/actions/get-info-action'; import { PlayAction } from '/@/renderer/features/context-menu/actions/play-action'; import { ContextMenu } from '/@/shared/components/context-menu/context-menu'; +import { ContextMenuPreview } from '/@/shared/components/context-menu/context-menu-preview'; import { LibraryItem, Playlist } from '/@/shared/types/domain-types'; interface PlaylistContextMenuProps { items: Playlist[]; + type: LibraryItem.PLAYLIST; } -export const PlaylistContextMenu = ({ items }: PlaylistContextMenuProps) => { +export const PlaylistContextMenu = ({ items, type }: PlaylistContextMenuProps) => { const { ids } = useMemo(() => { const ids = items.map((item) => item.id); return { ids }; }, [items]); return ( - + } + > 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 38030ee0c..2647b95a9 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 @@ -10,20 +10,24 @@ import { SetFavoriteAction } from '/@/renderer/features/context-menu/actions/set import { SetRatingAction } from '/@/renderer/features/context-menu/actions/set-rating-action'; import { ShareAction } from '/@/renderer/features/context-menu/actions/share-action'; import { ContextMenu } from '/@/shared/components/context-menu/context-menu'; +import { ContextMenuPreview } from '/@/shared/components/context-menu/context-menu-preview'; import { LibraryItem, Song } from '/@/shared/types/domain-types'; interface PlaylistSongContextMenuProps { items: Song[]; + type: LibraryItem.PLAYLIST_SONG; } -export const PlaylistSongContextMenu = ({ items }: PlaylistSongContextMenuProps) => { +export const PlaylistSongContextMenu = ({ items, type }: PlaylistSongContextMenuProps) => { const { ids } = useMemo(() => { const ids = items.map((item) => item.id); return { ids }; }, [items]); return ( - + } + > 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 52d678784..b0b7707df 100644 --- a/src/renderer/features/context-menu/menus/queue-context-menu.tsx +++ b/src/renderer/features/context-menu/menus/queue-context-menu.tsx @@ -11,20 +11,26 @@ import { SetRatingAction } from '/@/renderer/features/context-menu/actions/set-r import { ShareAction } from '/@/renderer/features/context-menu/actions/share-action'; import { ShuffleItemsAction } from '/@/renderer/features/context-menu/actions/shuffle-items-action'; import { ContextMenu } from '/@/shared/components/context-menu/context-menu'; +import { ContextMenuPreview } from '/@/shared/components/context-menu/context-menu-preview'; import { LibraryItem, QueueSong } from '/@/shared/types/domain-types'; interface QueueContextMenuProps { items: QueueSong[]; + type: LibraryItem.QUEUE_SONG; } -export const QueueContextMenu = ({ items }: QueueContextMenuProps) => { +export const QueueContextMenu = ({ items, type }: QueueContextMenuProps) => { const { ids } = useMemo(() => { const ids = items.map((item) => item.id); return { ids }; }, [items]); return ( - + + } + > 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 b55522045..bbda6f0ec 100644 --- a/src/renderer/features/context-menu/menus/song-context-menu.tsx +++ b/src/renderer/features/context-menu/menus/song-context-menu.tsx @@ -9,20 +9,24 @@ import { SetFavoriteAction } from '/@/renderer/features/context-menu/actions/set import { SetRatingAction } from '/@/renderer/features/context-menu/actions/set-rating-action'; import { ShareAction } from '/@/renderer/features/context-menu/actions/share-action'; import { ContextMenu } from '/@/shared/components/context-menu/context-menu'; +import { ContextMenuPreview } from '/@/shared/components/context-menu/context-menu-preview'; import { LibraryItem, Song } from '/@/shared/types/domain-types'; interface SongContextMenuProps { items: Song[]; + type: LibraryItem.SONG; } -export const SongContextMenu = ({ items }: SongContextMenuProps) => { +export const SongContextMenu = ({ items, type }: SongContextMenuProps) => { const { ids } = useMemo(() => { const ids = items.map((item) => item.id); return { ids }; }, [items]); return ( - + } + > diff --git a/src/shared/components/context-menu/context-menu-preview.module.css b/src/shared/components/context-menu/context-menu-preview.module.css new file mode 100644 index 000000000..d0a6b984b --- /dev/null +++ b/src/shared/components/context-menu/context-menu-preview.module.css @@ -0,0 +1,91 @@ +.container { + position: relative; + width: 100%; +} + +.divider { + height: 1px; + margin: var(--theme-spacing-xs) 0; + background: none; + border: none; + border-top: 1px solid var(--theme-colors-border); +} + +.preview { + display: flex; + align-items: center; + padding: var(--theme-spacing-sm) var(--theme-spacing-md); + border-radius: var(--theme-radius-sm); +} + +.content { + display: flex; + gap: var(--theme-spacing-sm); + align-items: center; + width: 100%; +} + +.image-container { + position: relative; + flex-shrink: 0; + width: 40px; + height: 40px; + overflow: hidden; + border-radius: var(--theme-radius-sm); + box-shadow: 0 2px 8px rgb(0 0 0 / 20%); +} + +.image { + width: 100%; + height: 100%; + object-fit: cover; +} + +.image-overlay { + position: absolute; + inset: 0; + pointer-events: none; + background: linear-gradient(135deg, rgb(255 255 255 / 10%) 0%, rgb(0 0 0 / 10%) 100%); +} + +.icon-container { + display: flex; + flex-shrink: 0; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + background: linear-gradient( + 135deg, + var(--theme-colors-surface) 0%, + var(--theme-colors-background) 100% + ); + border: 1px solid var(--theme-colors-border); + border-radius: var(--theme-radius-sm); + box-shadow: 0 2px 8px rgb(0 0 0 / 20%); +} + +.text-container { + display: flex; + flex: 1; + flex-direction: column; + gap: 2px; + min-width: 0; +} + +.name { + overflow: hidden; + text-overflow: ellipsis; + font-size: var(--theme-font-size-sm); + font-weight: 600; + line-height: 1.4; + color: var(--theme-colors-foreground); + white-space: nowrap; +} + +.count { + font-size: var(--theme-font-size-xs); + font-weight: 500; + line-height: 1.2; + color: var(--theme-colors-foreground-muted); +} diff --git a/src/shared/components/context-menu/context-menu-preview.tsx b/src/shared/components/context-menu/context-menu-preview.tsx new file mode 100644 index 000000000..1434f1544 --- /dev/null +++ b/src/shared/components/context-menu/context-menu-preview.tsx @@ -0,0 +1,80 @@ +import { memo } from 'react'; + +import styles from './context-menu-preview.module.css'; + +import { Icon } from '/@/shared/components/icon/icon'; +import { LibraryItem } from '/@/shared/types/domain-types'; + +interface ContextMenuPreviewProps { + items: unknown[]; + itemType?: LibraryItem; +} + +const getItemName = (item: unknown): string => { + if (item && typeof item === 'object') { + if ('name' in item && typeof item.name === 'string') { + return item.name; + } + if ('title' in item && typeof item.title === 'string') { + return item.title; + } + } + return 'Item'; +}; + +const getItemImage = (item: unknown): null | string => { + if (item && typeof item === 'object') { + if ('imageUrl' in item && typeof item.imageUrl === 'string') { + return item.imageUrl; + } + } + return null; +}; + +export const ContextMenuPreview = memo(({ items, itemType }: ContextMenuPreviewProps) => { + const itemCount = items.length; + const firstItem = items[0]; + const itemName = firstItem ? getItemName(firstItem) : 'Item'; + const itemImage = firstItem ? getItemImage(firstItem) : null; + const isMultiple = itemCount > 1; + + if (itemCount === 0) { + return null; + } + + return ( +
+
+
+
+ {itemImage ? ( +
+ {itemName} +
+
+ ) : ( +
+ {itemType === LibraryItem.ALBUM && } + {itemType === LibraryItem.SONG && } + {itemType === LibraryItem.ALBUM_ARTIST && ( + + )} + {itemType === LibraryItem.ARTIST && } + {itemType === LibraryItem.PLAYLIST && ( + + )} + {itemType === LibraryItem.GENRE && } + {!itemType && } +
+ )} +
+
{itemName}
+ {isMultiple &&
+{itemCount - 1} more
} +
+
+
+
+ ); +}); + +ContextMenuPreview.displayName = 'ContextMenuPreview'; diff --git a/src/shared/components/context-menu/context-menu.tsx b/src/shared/components/context-menu/context-menu.tsx index 0dd274858..19ca8577b 100644 --- a/src/shared/components/context-menu/context-menu.tsx +++ b/src/shared/components/context-menu/context-menu.tsx @@ -19,6 +19,7 @@ interface ContextMenuContext { export const ContextMenuContext = createContext(null); interface ContentProps { + bottomStickyContent?: ReactNode; children: ReactNode; onCloseAutoFocus?: (event: FocusEvent) => void; onEscapeKeyDown?: (event: KeyboardEvent) => void; @@ -72,7 +73,7 @@ export function ContextMenu(props: ContextMenuProps) { } function Content(props: ContentProps) { - const { children, stickyContent } = props; + const { bottomStickyContent, children, stickyContent } = props; const { open } = useContext(ContextMenuContext) as ContextMenuContext; return ( @@ -88,6 +89,7 @@ function Content(props: ContentProps) { > {stickyContent} {children} + {bottomStickyContent}