Add library search

This commit is contained in:
jeffvli 2023-05-19 00:21:36 -07:00 committed by Jeff
parent cf9ed31dfd
commit c12c1bad73
3 changed files with 337 additions and 18 deletions

View File

@ -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[]>([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 (
<CustomModal
{...modalProps}
@ -47,7 +79,6 @@ export const CommandPalette = ({ modalProps }: CommandPaletteProps) => {
}
},
toggle: () => {
console.log('toggle');
if (isHome) {
modalProps.handlers.toggle();
setQuery('');
@ -56,11 +87,13 @@ export const CommandPalette = ({ modalProps }: CommandPaletteProps) => {
}
},
}}
scrollAreaComponent={ScrollArea.Autosize}
size="lg"
>
<Command
filter={(value, search) => {
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) => {
>
<Command.Input
autoFocus
placeholder="Enter your search..."
placeholder="Enter search..."
value={query}
onValueChange={setQuery}
/>
<Command.Separator />
<Command.List>
<Command.Empty>No results found.</Command.Empty>
{showAlbumGroup && (
<Command.Group heading="Albums">
{data?.albums?.map((album) => (
<Command.Item
key={`search-album-${album.id}`}
value={`search-${album.id}`}
onSelect={() =>
navigate(generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, { albumId: album.id }))
}
>
<LibraryCommandItem
handlePlayQueueAdd={handlePlayQueueAdd}
id={album.id}
imageUrl={album.imageUrl}
itemType={LibraryItem.ALBUM}
subtitle={album.albumArtists.map((artist) => artist.name).join(', ')}
title={album.name}
/>
</Command.Item>
))}
</Command.Group>
)}
{showArtistGroup && (
<Command.Group heading="Artists">
{data?.albumArtists.map((artist) => (
<Command.Item
key={`artist-${artist.id}`}
value={`search-${artist.id}`}
onSelect={() =>
navigate(
generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, {
albumArtistId: artist.id,
}),
)
}
>
<LibraryCommandItem
handlePlayQueueAdd={handlePlayQueueAdd}
id={artist.id}
imageUrl={artist.imageUrl}
itemType={LibraryItem.ALBUM_ARTIST}
subtitle={
(artist?.albumCount || 0) > 0 ? `${artist.albumCount} albums` : undefined
}
title={artist.name}
/>
</Command.Item>
))}
</Command.Group>
)}
{showTrackGroup && (
<Command.Group heading="Tracks">
{data?.songs.map((song) => (
<Command.Item
key={`artist-${song.id}`}
value={`search-${song.id}`}
onSelect={() =>
navigate(
generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, {
albumId: song.albumId,
}),
)
}
>
<LibraryCommandItem
handlePlayQueueAdd={handlePlayQueueAdd}
id={song.id}
imageUrl={song.imageUrl}
itemType={LibraryItem.SONG}
subtitle={song.artists.map((artist) => artist.name).join(', ')}
title={song.name}
/>
</Command.Item>
))}
</Command.Group>
)}
{activePage === CommandPalettePages.HOME && (
<HomeCommands
handleClose={modalProps.handlers.close}
@ -90,10 +198,32 @@ export const CommandPalette = ({ modalProps }: CommandPaletteProps) => {
<GoToCommands
handleClose={modalProps.handlers.close}
setPages={setPages}
setQuery={setQuery}
/>
)}
{activePage === CommandPalettePages.MANAGE_SERVERS && (
<ServerCommands
handleClose={modalProps.handlers.close}
setPages={setPages}
setQuery={setQuery}
/>
)}
</Command.List>
</Command>
<Paper
mt="0.5rem"
p="0.5rem"
>
<Group position="apart">
<Command.Loading>{isLoading && query !== '' && <Spinner />}</Command.Loading>
<Group spacing="sm">
<Kbd size="md">ESC</Kbd>
<Kbd size="md"></Kbd>
<Kbd size="md"></Kbd>
<Kbd size="md"></Kbd>
</Group>
</Group>
</Paper>
</CustomModal>
);
};

View File

@ -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);

View File

@ -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 (
<Flex
gap="xl"
justify="space-between"
style={{ height: '40px', width: '100%' }}
>
<ItemGrid height={40}>
<ImageWrapper>
{imageUrl ? (
<StyledImage
alt="cover"
height={40}
placeholder="var(--placeholder-bg)"
src={imageUrl}
style={{}}
width={40}
/>
) : (
<Center
style={{
background: 'var(--placeholder-bg)',
borderRadius: 'var(--card-default-radius)',
height: `${40}px`,
width: `${40}px`,
}}
>
<Placeholder
color="var(--placeholder-fg)"
size={35}
/>
</Center>
)}
</ImageWrapper>
<MetadataWrapper>
<Text overflow="hidden">{title}</Text>
<Text
$secondary
overflow="hidden"
>
{subtitle}
</Text>
</MetadataWrapper>
</ItemGrid>
<MotionFlex
align="center"
gap="sm"
justify="flex-end"
>
<Button
compact
size="md"
tooltip={{ label: 'Play', openDelay: 500 }}
variant="default"
onClick={(e) => handlePlay(e, id, Play.NOW)}
>
<RiPlayFill />
</Button>
<Button
compact
size="md"
tooltip={{ label: 'Add to queue', openDelay: 500 }}
variant="default"
onClick={(e) => handlePlay(e, id, Play.LAST)}
>
<RiAddBoxFill />
</Button>
<Button
compact
size="md"
tooltip={{ label: 'Play next', openDelay: 500 }}
variant="default"
onClick={(e) => handlePlay(e, id, Play.NEXT)}
>
<RiAddCircleFill />
</Button>
</MotionFlex>
</Flex>
);
};