From c12c1bad7321062598e3cd6b0a626104cdf0ee14 Mon Sep 17 00:00:00 2001 From: jeffvli Date: Fri, 19 May 2023 00:21:36 -0700 Subject: [PATCH] Add library search --- .../search/components/command-palette.tsx | 142 +++++++++++++- .../features/search/components/command.tsx | 34 ++-- .../components/library-command-item.tsx | 179 ++++++++++++++++++ 3 files changed, 337 insertions(+), 18 deletions(-) create mode 100644 src/renderer/features/search/components/library-command-item.tsx diff --git a/src/renderer/features/search/components/command-palette.tsx b/src/renderer/features/search/components/command-palette.tsx index 8e4aa275..ad71fb6a 100644 --- a/src/renderer/features/search/components/command-palette.tsx +++ b/src/renderer/features/search/components/command-palette.tsx @@ -1,11 +1,20 @@ /* eslint-disable react/no-unknown-property */ import { useCallback, useState } from 'react'; -import { useDisclosure } from '@mantine/hooks'; +import { Group, Kbd, ScrollArea } from '@mantine/core'; +import { useDisclosure, useDebouncedValue } from '@mantine/hooks'; +import { generatePath, useNavigate } from 'react-router'; import styled from 'styled-components'; import { GoToCommands } from './go-to-commands'; import { Command, CommandPalettePages } from '/@/renderer/features/search/components/command'; -import { Modal } from '/@/renderer/components'; +import { Modal, Paper, Spinner } from '/@/renderer/components'; import { HomeCommands } from './home-commands'; +import { ServerCommands } from '/@/renderer/features/search/components/server-commands'; +import { useSearch } from '/@/renderer/features/search/queries/search-query'; +import { useCurrentServer } from '/@/renderer/store'; +import { AppRoute } from '/@/renderer/router/routes'; +import { LibraryCommandItem } from '/@/renderer/features/search/components/library-command-item'; +import { LibraryItem } from '/@/renderer/api/types'; +import { usePlayQueueAdd } from '/@/renderer/features/player'; interface CommandPaletteProps { modalProps: typeof useDisclosure['arguments']; @@ -18,8 +27,11 @@ const CustomModal = styled(Modal)` `; export const CommandPalette = ({ modalProps }: CommandPaletteProps) => { + const navigate = useNavigate(); + const server = useCurrentServer(); const [value, setValue] = useState(''); const [query, setQuery] = useState(''); + const [debouncedQuery] = useDebouncedValue(query, 400); const [pages, setPages] = useState([CommandPalettePages.HOME]); const activePage = pages[pages.length - 1]; const isHome = activePage === CommandPalettePages.HOME; @@ -32,6 +44,26 @@ export const CommandPalette = ({ modalProps }: CommandPaletteProps) => { }); }, []); + const { data, isLoading } = useSearch({ + options: { enabled: debouncedQuery !== '' && query !== '' }, + query: { + albumArtistLimit: 4, + albumArtistStartIndex: 0, + albumLimit: 4, + albumStartIndex: 0, + query: debouncedQuery, + songLimit: 4, + songStartIndex: 0, + }, + serverId: server?.id, + }); + + const showAlbumGroup = Boolean(query && data && data?.albums?.length > 0); + const showArtistGroup = Boolean(query && data && data?.albumArtists?.length > 0); + const showTrackGroup = Boolean(query && data && data?.songs?.length > 0); + + const handlePlayQueueAdd = usePlayQueueAdd(); + return ( { } }, toggle: () => { - console.log('toggle'); if (isHome) { modalProps.handlers.toggle(); setQuery(''); @@ -56,11 +87,13 @@ export const CommandPalette = ({ modalProps }: CommandPaletteProps) => { } }, }} + scrollAreaComponent={ScrollArea.Autosize} + size="lg" > { if (value.includes(search)) return 1; - if (value === 'search') return 1; + if (value.includes('search')) return 1; return 0; }} label="Global Command Menu" @@ -69,14 +102,89 @@ export const CommandPalette = ({ modalProps }: CommandPaletteProps) => { > No results found. - + {showAlbumGroup && ( + + {data?.albums?.map((album) => ( + + navigate(generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, { albumId: album.id })) + } + > + artist.name).join(', ')} + title={album.name} + /> + + ))} + + )} + {showArtistGroup && ( + + {data?.albumArtists.map((artist) => ( + + navigate( + generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, { + albumArtistId: artist.id, + }), + ) + } + > + 0 ? `${artist.albumCount} albums` : undefined + } + title={artist.name} + /> + + ))} + + )} + {showTrackGroup && ( + + {data?.songs.map((song) => ( + + navigate( + generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, { + albumId: song.albumId, + }), + ) + } + > + artist.name).join(', ')} + title={song.name} + /> + + ))} + + )} {activePage === CommandPalettePages.HOME && ( { + )} + {activePage === CommandPalettePages.MANAGE_SERVERS && ( + )} + + + {isLoading && query !== '' && } + + ESC + + + + + + ); }; diff --git a/src/renderer/features/search/components/command.tsx b/src/renderer/features/search/components/command.tsx index 8bb56d44..9ea1059b 100644 --- a/src/renderer/features/search/components/command.tsx +++ b/src/renderer/features/search/components/command.tsx @@ -2,45 +2,55 @@ import { Command as Cmdk } from 'cmdk'; import styled from 'styled-components'; export enum CommandPalettePages { - GO_TO = 'go to', + GO_TO = 'go', HOME = 'home', + MANAGE_SERVERS = 'servers', } export const Command = styled(Cmdk)` [cmdk-root] { - font-family: var(--content-font-family); background-color: var(--background-color); } input[cmdk-input] { width: 100%; - height: 2rem; + height: 1.5rem; margin-bottom: 1rem; - padding: 0 0.5rem; + padding: 1.3rem 0.5rem; color: var(--input-fg); - font-size: 1.1rem; - background: transparent; + font-size: 1.2rem; + font-family: var(--content-font-family); + background: var(--input-bg); border: none; + border-radius: 5px; &::placeholder { color: var(--input-placeholder-fg); } } - div[cmdk-group-heading] { + [cmdk-group-heading] { margin: 1rem 0; font-size: 0.9rem; opacity: 0.8; } - div[cmdk-item] { + [cmdk-group-items] { + display: flex; + flex-direction: column; + gap: 0.4rem; + } + + [cmdk-item] { display: flex; gap: 0.5rem; align-items: center; padding: 1rem 0.5rem; - color: var(--btn-subtle-fg); - background: var(--btn-subtle-bg); + color: var(--btn-default-fg); + font-family: var(--content-font-family); + background: var(--btn-default-bg); border-radius: 5px; + cursor: pointer; svg { width: 1.2rem; @@ -48,12 +58,12 @@ export const Command = styled(Cmdk)` } &[data-selected] { - color: var(--btn-subtle-fg-hover); + color: var(--btn-default-fg-hover); background: rgba(255, 255, 255, 10%); } } - div[cmdk-separator] { + [cmdk-separator] { height: 1px; margin: 0 0 0.5rem; background: var(--generic-border-color); diff --git a/src/renderer/features/search/components/library-command-item.tsx b/src/renderer/features/search/components/library-command-item.tsx new file mode 100644 index 00000000..74fc0f43 --- /dev/null +++ b/src/renderer/features/search/components/library-command-item.tsx @@ -0,0 +1,179 @@ +import { Center, Flex } from '@mantine/core'; +import { useCallback, MouseEvent } from 'react'; +import { + RiAddBoxFill, + RiAddCircleFill, + RiAlbumFill, + RiPlayFill, + RiPlayListFill, + RiUserVoiceFill, +} from 'react-icons/ri'; +import styled from 'styled-components'; +import { LibraryItem } from '/@/renderer/api/types'; +import { Button, MotionFlex, Text } from '/@/renderer/components'; +import { Play, PlayQueueAddOptions } from '/@/renderer/types'; + +const ItemGrid = styled.div<{ height: number }>` + display: grid; + grid-auto-columns: 1fr; + grid-template-areas: 'image info'; + grid-template-rows: 1fr; + grid-template-columns: ${(props) => props.height}px minmax(0, 1fr); + gap: 0.5rem; + width: 100%; + max-width: 100%; + height: 100%; + letter-spacing: 0.5px; +`; + +const ImageWrapper = styled.div` + display: flex; + grid-area: image; + align-items: center; + justify-content: center; + height: 100%; +`; + +const MetadataWrapper = styled.div` + display: flex; + flex-direction: column; + grid-area: info; + justify-content: center; + width: 100%; +`; + +const StyledImage = styled.img` + object-fit: cover; + border-radius: 4px; +`; + +interface LibraryCommandItemProps { + handlePlayQueueAdd?: (options: PlayQueueAddOptions) => void; + id: string; + imageUrl: string | null; + itemType: LibraryItem; + subtitle?: string; + title?: string; +} + +export const LibraryCommandItem = ({ + id, + imageUrl, + subtitle, + title, + itemType, + handlePlayQueueAdd, +}: LibraryCommandItemProps) => { + let Placeholder = RiAlbumFill; + + switch (itemType) { + case LibraryItem.ALBUM: + Placeholder = RiAlbumFill; + break; + case LibraryItem.ARTIST: + Placeholder = RiUserVoiceFill; + break; + case LibraryItem.ALBUM_ARTIST: + Placeholder = RiUserVoiceFill; + break; + case LibraryItem.PLAYLIST: + Placeholder = RiPlayListFill; + break; + default: + Placeholder = RiAlbumFill; + break; + } + + const handlePlay = useCallback( + (e: MouseEvent, id: string, play: Play) => { + e.stopPropagation(); + handlePlayQueueAdd?.({ + byItemType: { + id, + type: itemType, + }, + play, + }); + }, + [handlePlayQueueAdd, itemType], + ); + + return ( + + + + {imageUrl ? ( + + ) : ( +
+ +
+ )} +
+ + {title} + + {subtitle} + + +
+ + + + + +
+ ); +};