Lint all files

This commit is contained in:
jeffvli 2023-07-01 19:10:05 -07:00
parent 22af76b4d6
commit 30e52ebb54
334 changed files with 76519 additions and 75932 deletions

View file

@ -18,249 +18,261 @@ import { LibraryItem } from '/@/renderer/api/types';
import { usePlayQueueAdd } from '/@/renderer/features/player';
interface CommandPaletteProps {
modalProps: typeof useDisclosure['arguments'];
modalProps: typeof useDisclosure['arguments'];
}
const CustomModal = styled(Modal)`
& .mantine-Modal-header {
display: none;
}
& .mantine-Modal-header {
display: none;
}
`;
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;
const searchInputRef = useRef<HTMLInputElement>(null);
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;
const searchInputRef = useRef<HTMLInputElement>(null);
const popPage = useCallback(() => {
setPages((pages) => {
const x = [...pages];
x.splice(-1, 1);
return x;
const popPage = useCallback(() => {
setPages((pages) => {
const x = [...pages];
x.splice(-1, 1);
return x;
});
}, []);
const { data, isLoading } = useSearch({
options: { enabled: isHome && debouncedQuery !== '' && query !== '' },
query: {
albumArtistLimit: 4,
albumArtistStartIndex: 0,
albumLimit: 4,
albumStartIndex: 0,
query: debouncedQuery,
songLimit: 4,
songStartIndex: 0,
},
serverId: server?.id,
});
}, []);
const { data, isLoading } = useSearch({
options: { enabled: isHome && debouncedQuery !== '' && query !== '' },
query: {
albumArtistLimit: 4,
albumArtistStartIndex: 0,
albumLimit: 4,
albumStartIndex: 0,
query: debouncedQuery,
songLimit: 4,
songStartIndex: 0,
},
serverId: server?.id,
});
const showAlbumGroup = isHome && Boolean(query && data && data?.albums?.length > 0);
const showArtistGroup = isHome && Boolean(query && data && data?.albumArtists?.length > 0);
const showTrackGroup = isHome && Boolean(query && data && data?.songs?.length > 0);
const showAlbumGroup = isHome && Boolean(query && data && data?.albums?.length > 0);
const showArtistGroup = isHome && Boolean(query && data && data?.albumArtists?.length > 0);
const showTrackGroup = isHome && Boolean(query && data && data?.songs?.length > 0);
const handlePlayQueueAdd = usePlayQueueAdd();
const handlePlayQueueAdd = usePlayQueueAdd();
return (
<CustomModal
{...modalProps}
centered
handlers={{
...modalProps.handlers,
close: () => {
if (isHome) {
modalProps.handlers.close();
setQuery('');
} else {
popPage();
}
},
toggle: () => {
if (isHome) {
modalProps.handlers.toggle();
setQuery('');
} else {
popPage();
}
},
}}
scrollAreaComponent={ScrollArea.Autosize}
size="lg"
>
<Group
mb="1rem"
spacing="sm"
>
{pages.map((page, index) => (
<Fragment key={page}>
{index > 0 && ' > '}
<Button
compact
disabled
variant="default"
>
{page?.toLocaleUpperCase()}
</Button>
</Fragment>
))}
</Group>
<Command
filter={(value, search) => {
if (value.includes(search)) return 1;
if (value.includes('search')) return 1;
return 0;
}}
label="Global Command Menu"
value={value}
onValueChange={setValue}
>
<TextInput
ref={searchInputRef}
data-autofocus
icon={<RiSearchLine />}
rightSection={
<ActionIcon
onClick={() => {
setQuery('');
searchInputRef.current?.focus();
}}
>
<RiCloseFill />
</ActionIcon>
}
size="lg"
value={query}
onChange={(e) => setQuery(e.currentTarget.value)}
/>
<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 }));
modalProps.handlers.close();
setQuery('');
}}
>
<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,
}),
);
modalProps.handlers.close();
setQuery('');
}}
>
<LibraryCommandItem
handlePlayQueueAdd={handlePlayQueueAdd}
id={artist.id}
imageUrl={artist.imageUrl}
itemType={LibraryItem.ALBUM_ARTIST}
subtitle={
(artist?.albumCount || 0) > 0 ? `${artist.albumCount} albums` : undefined
return (
<CustomModal
{...modalProps}
centered
handlers={{
...modalProps.handlers,
close: () => {
if (isHome) {
modalProps.handlers.close();
setQuery('');
} else {
popPage();
}
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,
}),
);
modalProps.handlers.close();
setQuery('');
}}
>
<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}
pages={pages}
query={query}
setPages={setPages}
setQuery={setQuery}
/>
)}
{activePage === CommandPalettePages.GO_TO && (
<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>{isHome && 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>
);
},
toggle: () => {
if (isHome) {
modalProps.handlers.toggle();
setQuery('');
} else {
popPage();
}
},
}}
scrollAreaComponent={ScrollArea.Autosize}
size="lg"
>
<Group
mb="1rem"
spacing="sm"
>
{pages.map((page, index) => (
<Fragment key={page}>
{index > 0 && ' > '}
<Button
compact
disabled
variant="default"
>
{page?.toLocaleUpperCase()}
</Button>
</Fragment>
))}
</Group>
<Command
filter={(value, search) => {
if (value.includes(search)) return 1;
if (value.includes('search')) return 1;
return 0;
}}
label="Global Command Menu"
value={value}
onValueChange={setValue}
>
<TextInput
ref={searchInputRef}
data-autofocus
icon={<RiSearchLine />}
rightSection={
<ActionIcon
onClick={() => {
setQuery('');
searchInputRef.current?.focus();
}}
>
<RiCloseFill />
</ActionIcon>
}
size="lg"
value={query}
onChange={(e) => setQuery(e.currentTarget.value)}
/>
<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,
}),
);
modalProps.handlers.close();
setQuery('');
}}
>
<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,
}),
);
modalProps.handlers.close();
setQuery('');
}}
>
<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,
}),
);
modalProps.handlers.close();
setQuery('');
}}
>
<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}
pages={pages}
query={query}
setPages={setPages}
setQuery={setQuery}
/>
)}
{activePage === CommandPalettePages.GO_TO && (
<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>
{isHome && 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,69 +2,69 @@ import { Command as Cmdk } from 'cmdk';
import styled from 'styled-components';
export enum CommandPalettePages {
GO_TO = 'go',
HOME = 'home',
MANAGE_SERVERS = 'servers',
GO_TO = 'go',
HOME = 'home',
MANAGE_SERVERS = 'servers',
}
export const Command = styled(Cmdk)`
[cmdk-root] {
background-color: var(--background-color);
}
input[cmdk-input] {
width: 100%;
height: 1.5rem;
margin-bottom: 1rem;
padding: 1.3rem 0.5rem;
color: var(--input-fg);
font-family: var(--content-font-family);
background: var(--input-bg);
border: none;
border-radius: 5px;
&::placeholder {
color: var(--input-placeholder-fg);
}
}
[cmdk-group-heading] {
margin: 1rem 0;
font-size: 0.9rem;
opacity: 0.8;
}
[cmdk-group-items] {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
[cmdk-item] {
display: flex;
gap: 0.5rem;
align-items: center;
padding: 0.5rem;
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;
height: 1.2rem;
[cmdk-root] {
background-color: var(--background-color);
}
&[data-selected] {
color: var(--btn-default-fg-hover);
background: var(--btn-default-bg-hover);
}
}
input[cmdk-input] {
width: 100%;
height: 1.5rem;
margin-bottom: 1rem;
padding: 1.3rem 0.5rem;
color: var(--input-fg);
font-family: var(--content-font-family);
background: var(--input-bg);
border: none;
border-radius: 5px;
[cmdk-separator] {
height: 1px;
margin: 0 0 0.5rem;
background: var(--generic-border-color);
}
&::placeholder {
color: var(--input-placeholder-fg);
}
}
[cmdk-group-heading] {
margin: 1rem 0;
font-size: 0.9rem;
opacity: 0.8;
}
[cmdk-group-items] {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
[cmdk-item] {
display: flex;
gap: 0.5rem;
align-items: center;
padding: 0.5rem;
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;
height: 1.2rem;
}
&[data-selected] {
color: var(--btn-default-fg-hover);
background: var(--btn-default-bg-hover);
}
}
[cmdk-separator] {
height: 1px;
margin: 0 0 0.5rem;
background: var(--generic-border-color);
}
`;

View file

@ -4,42 +4,42 @@ import { Command, CommandPalettePages } from '/@/renderer/features/search/compon
import { AppRoute } from '/@/renderer/router/routes';
interface GoToCommandsProps {
handleClose: () => void;
setPages: (pages: CommandPalettePages[]) => void;
setQuery: Dispatch<string>;
handleClose: () => void;
setPages: (pages: CommandPalettePages[]) => void;
setQuery: Dispatch<string>;
}
export const GoToCommands = ({ setQuery, setPages, handleClose }: GoToCommandsProps) => {
const navigate = useNavigate();
const navigate = useNavigate();
const goTo = useCallback(
(route: string) => {
navigate(route);
handleClose();
setPages([CommandPalettePages.HOME]);
setQuery('');
},
[handleClose, navigate, setPages, setQuery],
);
const goTo = useCallback(
(route: string) => {
navigate(route);
handleClose();
setPages([CommandPalettePages.HOME]);
setQuery('');
},
[handleClose, navigate, setPages, setQuery],
);
return (
<>
<Command.Group>
<Command.Item onSelect={() => goTo(AppRoute.HOME)}>Home</Command.Item>
<Command.Item onSelect={() => goTo(AppRoute.SEARCH)}>Search</Command.Item>
<Command.Item onSelect={() => goTo(AppRoute.SETTINGS)}>Settings</Command.Item>
</Command.Group>
<Command.Group heading="Library">
<Command.Item onSelect={() => goTo(AppRoute.LIBRARY_ALBUMS)}>Albums</Command.Item>
<Command.Item onSelect={() => goTo(AppRoute.LIBRARY_SONGS)}>Tracks</Command.Item>
<Command.Item onSelect={() => goTo(AppRoute.LIBRARY_ALBUM_ARTISTS)}>
Album artists
</Command.Item>
<Command.Item onSelect={() => goTo(AppRoute.LIBRARY_GENRES)}>Genres</Command.Item>
<Command.Item onSelect={() => goTo(AppRoute.LIBRARY_FOLDERS)}>Folders</Command.Item>
<Command.Item onSelect={() => goTo(AppRoute.PLAYLISTS)}>Playlists</Command.Item>
</Command.Group>
<Command.Separator />
</>
);
return (
<>
<Command.Group>
<Command.Item onSelect={() => goTo(AppRoute.HOME)}>Home</Command.Item>
<Command.Item onSelect={() => goTo(AppRoute.SEARCH)}>Search</Command.Item>
<Command.Item onSelect={() => goTo(AppRoute.SETTINGS)}>Settings</Command.Item>
</Command.Group>
<Command.Group heading="Library">
<Command.Item onSelect={() => goTo(AppRoute.LIBRARY_ALBUMS)}>Albums</Command.Item>
<Command.Item onSelect={() => goTo(AppRoute.LIBRARY_SONGS)}>Tracks</Command.Item>
<Command.Item onSelect={() => goTo(AppRoute.LIBRARY_ALBUM_ARTISTS)}>
Album artists
</Command.Item>
<Command.Item onSelect={() => goTo(AppRoute.LIBRARY_GENRES)}>Genres</Command.Item>
<Command.Item onSelect={() => goTo(AppRoute.LIBRARY_FOLDERS)}>Folders</Command.Item>
<Command.Item onSelect={() => goTo(AppRoute.PLAYLISTS)}>Playlists</Command.Item>
</Command.Group>
<Command.Separator />
</>
);
};

View file

@ -11,68 +11,70 @@ import { useCurrentServer } from '/@/renderer/store';
import { ServerType } from '/@/renderer/types';
interface HomeCommandsProps {
handleClose: () => void;
pages: CommandPalettePages[];
query: string;
setPages: Dispatch<CommandPalettePages[]>;
setQuery: Dispatch<string>;
handleClose: () => void;
pages: CommandPalettePages[];
query: string;
setPages: Dispatch<CommandPalettePages[]>;
setQuery: Dispatch<string>;
}
export const HomeCommands = ({
query,
setQuery,
pages,
setPages,
handleClose,
query,
setQuery,
pages,
setPages,
handleClose,
}: HomeCommandsProps) => {
const navigate = useNavigate();
const server = useCurrentServer();
const navigate = useNavigate();
const server = useCurrentServer();
const handleCreatePlaylistModal = useCallback(() => {
handleClose();
const handleCreatePlaylistModal = useCallback(() => {
handleClose();
openModal({
children: <CreatePlaylistForm onCancel={() => closeAllModals()} />,
size: server?.type === ServerType?.NAVIDROME ? 'xl' : 'sm',
title: 'Create Playlist',
});
}, [handleClose, server?.type]);
openModal({
children: <CreatePlaylistForm onCancel={() => closeAllModals()} />,
size: server?.type === ServerType?.NAVIDROME ? 'xl' : 'sm',
title: 'Create Playlist',
});
}, [handleClose, server?.type]);
const handleSearch = () => {
navigate(
{
pathname: generatePath(AppRoute.SEARCH, { itemType: LibraryItem.SONG }),
search: createSearchParams({
query,
}).toString(),
},
{
state: {
navigationId: nanoid(),
},
},
const handleSearch = () => {
navigate(
{
pathname: generatePath(AppRoute.SEARCH, { itemType: LibraryItem.SONG }),
search: createSearchParams({
query,
}).toString(),
},
{
state: {
navigationId: nanoid(),
},
},
);
handleClose();
setQuery('');
};
return (
<>
<Command.Group heading="Commands">
<Command.Item
value="Search"
onSelect={handleSearch}
>
{query ? `Search for "${query}"...` : 'Search...'}
</Command.Item>
<Command.Item onSelect={handleCreatePlaylistModal}>Create playlist...</Command.Item>
<Command.Item onSelect={() => setPages([...pages, CommandPalettePages.GO_TO])}>
Go to page...
</Command.Item>
<Command.Item
onSelect={() => setPages([...pages, CommandPalettePages.MANAGE_SERVERS])}
>
Server commands...
</Command.Item>
</Command.Group>
</>
);
handleClose();
setQuery('');
};
return (
<>
<Command.Group heading="Commands">
<Command.Item
value="Search"
onSelect={handleSearch}
>
{query ? `Search for "${query}"...` : 'Search...'}
</Command.Item>
<Command.Item onSelect={handleCreatePlaylistModal}>Create playlist...</Command.Item>
<Command.Item onSelect={() => setPages([...pages, CommandPalettePages.GO_TO])}>
Go to page...
</Command.Item>
<Command.Item onSelect={() => setPages([...pages, CommandPalettePages.MANAGE_SERVERS])}>
Server commands...
</Command.Item>
</Command.Group>
</>
);
};

View file

@ -1,12 +1,12 @@
import { Center, Flex } from '@mantine/core';
import { useCallback, MouseEvent } from 'react';
import {
RiAddBoxFill,
RiAddCircleFill,
RiAlbumFill,
RiPlayFill,
RiPlayListFill,
RiUserVoiceFill,
RiAddBoxFill,
RiAddCircleFill,
RiAlbumFill,
RiPlayFill,
RiPlayListFill,
RiUserVoiceFill,
} from 'react-icons/ri';
import styled from 'styled-components';
import { LibraryItem } from '/@/renderer/api/types';
@ -16,168 +16,168 @@ import { Play, PlayQueueAddOptions } from '/@/renderer/types';
const Item = styled(Flex)``;
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;
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%;
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%;
display: flex;
flex-direction: column;
grid-area: info;
justify-content: center;
width: 100%;
`;
const StyledImage = styled.img`
object-fit: cover;
border-radius: 4px;
object-fit: cover;
border-radius: 4px;
`;
const ActionsContainer = styled(Flex)``;
interface LibraryCommandItemProps {
handlePlayQueueAdd?: (options: PlayQueueAddOptions) => void;
id: string;
imageUrl: string | null;
itemType: LibraryItem;
subtitle?: string;
title?: string;
handlePlayQueueAdd?: (options: PlayQueueAddOptions) => void;
id: string;
imageUrl: string | null;
itemType: LibraryItem;
subtitle?: string;
title?: string;
}
export const LibraryCommandItem = ({
id,
imageUrl,
subtitle,
title,
itemType,
handlePlayQueueAdd,
id,
imageUrl,
subtitle,
title,
itemType,
handlePlayQueueAdd,
}: LibraryCommandItemProps) => {
let Placeholder = RiAlbumFill;
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;
}
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, playType: Play) => {
e.stopPropagation();
handlePlayQueueAdd?.({
byItemType: {
id: [id],
type: itemType,
const handlePlay = useCallback(
(e: MouseEvent, id: string, playType: Play) => {
e.stopPropagation();
handlePlayQueueAdd?.({
byItemType: {
id: [id],
type: itemType,
},
playType,
});
},
playType,
});
},
[handlePlayQueueAdd, itemType],
);
[handlePlayQueueAdd, itemType],
);
return (
<Item
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`,
}}
return (
<Item
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>
<ActionsContainer
align="center"
gap="sm"
justify="flex-end"
>
<Placeholder
color="var(--placeholder-fg)"
size={35}
/>
</Center>
)}
</ImageWrapper>
<MetadataWrapper>
<Text overflow="hidden">{title}</Text>
<Text
$secondary
overflow="hidden"
>
{subtitle}
</Text>
</MetadataWrapper>
</ItemGrid>
<ActionsContainer
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>
</ActionsContainer>
</Item>
);
<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>
</ActionsContainer>
</Item>
);
};

View file

@ -1,9 +1,9 @@
import { MutableRefObject, useMemo, useCallback } from 'react';
import {
ColDef,
GridReadyEvent,
RowDoubleClickedEvent,
IDatasource,
ColDef,
GridReadyEvent,
RowDoubleClickedEvent,
IDatasource,
} from '@ag-grid-community/core';
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { Stack } from '@mantine/core';
@ -14,139 +14,143 @@ import { VirtualGridAutoSizerContainer } from '/@/renderer/components/virtual-gr
import { VirtualTable, getColumnDefs } from '/@/renderer/components/virtual-table';
import { useHandleTableContextMenu } from '/@/renderer/features/context-menu';
import {
ALBUM_CONTEXT_MENU_ITEMS,
ARTIST_CONTEXT_MENU_ITEMS,
SONG_CONTEXT_MENU_ITEMS,
ALBUM_CONTEXT_MENU_ITEMS,
ARTIST_CONTEXT_MENU_ITEMS,
SONG_CONTEXT_MENU_ITEMS,
} from '/@/renderer/features/context-menu/context-menu-items';
import { usePlayQueueAdd } from '/@/renderer/features/player';
import { AppRoute } from '../../../router/routes';
import {
useCurrentServer,
useSongListStore,
usePlayButtonBehavior,
useAlbumListStore,
useAlbumArtistListStore,
useCurrentServer,
useSongListStore,
usePlayButtonBehavior,
useAlbumListStore,
useAlbumArtistListStore,
} from '/@/renderer/store';
interface SearchContentProps {
getDatasource: (searchQuery: string, itemType: LibraryItem) => IDatasource | undefined;
tableRef: MutableRefObject<AgGridReactType | null>;
getDatasource: (searchQuery: string, itemType: LibraryItem) => IDatasource | undefined;
tableRef: MutableRefObject<AgGridReactType | null>;
}
export const SearchContent = ({ tableRef, getDatasource }: SearchContentProps) => {
const navigate = useNavigate();
const server = useCurrentServer();
const { itemType } = useParams() as { itemType: LibraryItem };
const [searchParams] = useSearchParams();
const songListStore = useSongListStore();
const albumListStore = useAlbumListStore();
const albumArtistListStore = useAlbumArtistListStore();
const handlePlayQueueAdd = usePlayQueueAdd();
const playButtonBehavior = usePlayButtonBehavior();
const navigate = useNavigate();
const server = useCurrentServer();
const { itemType } = useParams() as { itemType: LibraryItem };
const [searchParams] = useSearchParams();
const songListStore = useSongListStore();
const albumListStore = useAlbumListStore();
const albumArtistListStore = useAlbumArtistListStore();
const handlePlayQueueAdd = usePlayQueueAdd();
const playButtonBehavior = usePlayButtonBehavior();
const getTable = useCallback(
(itemType: string) => {
switch (itemType) {
case LibraryItem.SONG:
return songListStore.table;
case LibraryItem.ALBUM:
return albumListStore.table;
case LibraryItem.ALBUM_ARTIST:
return albumArtistListStore.table;
default:
return undefined;
}
},
[albumArtistListStore.table, albumListStore.table, songListStore.table],
);
const getTable = useCallback(
(itemType: string) => {
switch (itemType) {
case LibraryItem.SONG:
return songListStore.table;
case LibraryItem.ALBUM:
return albumListStore.table;
case LibraryItem.ALBUM_ARTIST:
return albumArtistListStore.table;
default:
return undefined;
}
},
[albumArtistListStore.table, albumListStore.table, songListStore.table],
);
const table = getTable(itemType)!;
const table = getTable(itemType)!;
const columnDefs: ColDef[] = useMemo(() => getColumnDefs(table.columns), [table.columns]);
const columnDefs: ColDef[] = useMemo(() => getColumnDefs(table.columns), [table.columns]);
const onGridReady = useCallback(
(params: GridReadyEvent) => {
const datasource = getDatasource(searchParams.get('query') || '', itemType);
if (!datasource) return;
const onGridReady = useCallback(
(params: GridReadyEvent) => {
const datasource = getDatasource(searchParams.get('query') || '', itemType);
if (!datasource) return;
params.api.setDatasource(datasource);
params.api.ensureIndexVisible(table.scrollOffset, 'top');
},
[getDatasource, itemType, searchParams, table.scrollOffset],
);
params.api.setDatasource(datasource);
params.api.ensureIndexVisible(table.scrollOffset, 'top');
},
[getDatasource, itemType, searchParams, table.scrollOffset],
);
const handleGridSizeChange = () => {
if (table.autoFit) {
tableRef?.current?.api.sizeColumnsToFit();
}
};
const handleGridSizeChange = () => {
if (table.autoFit) {
tableRef?.current?.api.sizeColumnsToFit();
}
};
const contextMenuItems = () => {
switch (itemType) {
case LibraryItem.ALBUM:
return ALBUM_CONTEXT_MENU_ITEMS;
case LibraryItem.ALBUM_ARTIST:
return ARTIST_CONTEXT_MENU_ITEMS;
case LibraryItem.SONG:
return SONG_CONTEXT_MENU_ITEMS;
default:
return [];
}
};
const contextMenuItems = () => {
switch (itemType) {
case LibraryItem.ALBUM:
return ALBUM_CONTEXT_MENU_ITEMS;
case LibraryItem.ALBUM_ARTIST:
return ARTIST_CONTEXT_MENU_ITEMS;
case LibraryItem.SONG:
return SONG_CONTEXT_MENU_ITEMS;
default:
return [];
}
};
const handleContextMenu = useHandleTableContextMenu(itemType, contextMenuItems());
const handleContextMenu = useHandleTableContextMenu(itemType, contextMenuItems());
const handleRowDoubleClick = (e: RowDoubleClickedEvent<QueueSong>) => {
if (!e.data) return;
switch (itemType) {
case LibraryItem.ALBUM:
navigate(generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, { albumId: e.data.id }));
break;
case LibraryItem.ALBUM_ARTIST:
navigate(generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, { albumArtistId: e.data.id }));
break;
case LibraryItem.SONG:
handlePlayQueueAdd?.({
byData: [e.data],
playType: playButtonBehavior,
});
break;
}
};
const handleRowDoubleClick = (e: RowDoubleClickedEvent<QueueSong>) => {
if (!e.data) return;
switch (itemType) {
case LibraryItem.ALBUM:
navigate(generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, { albumId: e.data.id }));
break;
case LibraryItem.ALBUM_ARTIST:
navigate(
generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, {
albumArtistId: e.data.id,
}),
);
break;
case LibraryItem.SONG:
handlePlayQueueAdd?.({
byData: [e.data],
playType: playButtonBehavior,
});
break;
}
};
return (
<Stack
h="100%"
spacing={0}
>
<VirtualGridAutoSizerContainer>
<VirtualTable
// https://github.com/ag-grid/ag-grid/issues/5284
// Key is used to force remount of table when display, rowHeight, or server changes
key={`table-${itemType}-${table.rowHeight}-${server?.id}`}
ref={tableRef}
alwaysShowHorizontalScroll
suppressRowDrag
autoFitColumns={table.autoFit}
blockLoadDebounceMillis={200}
cacheBlockSize={25}
cacheOverflowSize={1}
columnDefs={columnDefs}
context={{
query: searchParams.get('query'),
}}
getRowId={(data) => data.data.id}
infiniteInitialRowCount={25}
rowBuffer={20}
rowHeight={table.rowHeight || 40}
rowModelType="infinite"
rowSelection="multiple"
onCellContextMenu={handleContextMenu}
onGridReady={onGridReady}
onGridSizeChanged={handleGridSizeChange}
onRowDoubleClicked={handleRowDoubleClick}
/>
</VirtualGridAutoSizerContainer>
</Stack>
);
return (
<Stack
h="100%"
spacing={0}
>
<VirtualGridAutoSizerContainer>
<VirtualTable
// https://github.com/ag-grid/ag-grid/issues/5284
// Key is used to force remount of table when display, rowHeight, or server changes
key={`table-${itemType}-${table.rowHeight}-${server?.id}`}
ref={tableRef}
alwaysShowHorizontalScroll
suppressRowDrag
autoFitColumns={table.autoFit}
blockLoadDebounceMillis={200}
cacheBlockSize={25}
cacheOverflowSize={1}
columnDefs={columnDefs}
context={{
query: searchParams.get('query'),
}}
getRowId={(data) => data.data.id}
infiniteInitialRowCount={25}
rowBuffer={20}
rowHeight={table.rowHeight || 40}
rowModelType="infinite"
rowSelection="multiple"
onCellContextMenu={handleContextMenu}
onGridReady={onGridReady}
onGridSizeChanged={handleGridSizeChange}
onRowDoubleClicked={handleRowDoubleClick}
/>
</VirtualGridAutoSizerContainer>
</Stack>
);
};

View file

@ -11,96 +11,100 @@ import { useContainerQuery } from '/@/renderer/hooks';
import { AppRoute } from '/@/renderer/router/routes';
interface SearchHeaderProps {
getDatasource: (searchQuery: string, itemType: LibraryItem) => IDatasource | undefined;
navigationId: string;
tableRef: MutableRefObject<AgGridReactType | null>;
getDatasource: (searchQuery: string, itemType: LibraryItem) => IDatasource | undefined;
navigationId: string;
tableRef: MutableRefObject<AgGridReactType | null>;
}
export const SearchHeader = ({ tableRef, getDatasource, navigationId }: SearchHeaderProps) => {
const { itemType } = useParams() as { itemType: LibraryItem };
const [searchParams, setSearchParams] = useSearchParams();
const cq = useContainerQuery();
const { itemType } = useParams() as { itemType: LibraryItem };
const [searchParams, setSearchParams] = useSearchParams();
const cq = useContainerQuery();
const handleSearch = debounce((e: ChangeEvent<HTMLInputElement>) => {
if (!e.target.value) return;
setSearchParams({ query: e.target.value }, { replace: true, state: { navigationId } });
const datasource = getDatasource(e.target.value, itemType);
if (!datasource) return;
tableRef.current?.api.setDatasource(datasource);
}, 200);
const handleSearch = debounce((e: ChangeEvent<HTMLInputElement>) => {
if (!e.target.value) return;
setSearchParams({ query: e.target.value }, { replace: true, state: { navigationId } });
const datasource = getDatasource(e.target.value, itemType);
if (!datasource) return;
tableRef.current?.api.setDatasource(datasource);
}, 200);
return (
<Stack
ref={cq.ref}
spacing={0}
>
<PageHeader>
<Flex
justify="space-between"
w="100%"
return (
<Stack
ref={cq.ref}
spacing={0}
>
<LibraryHeaderBar>
<LibraryHeaderBar.Title>Search</LibraryHeaderBar.Title>
</LibraryHeaderBar>
<Group>
<SearchInput
// key={`search-input-${initialQuery}`}
defaultValue={searchParams.get('query') || ''}
openedWidth={cq.isMd ? 250 : cq.isSm ? 200 : 150}
onChange={handleSearch}
/>
</Group>
</Flex>
</PageHeader>
<FilterBar>
<Group>
<Button
compact
replace
component={Link}
fw={600}
size="md"
state={{ navigationId }}
to={{
pathname: generatePath(AppRoute.SEARCH, { itemType: LibraryItem.SONG }),
search: searchParams.toString(),
}}
variant={itemType === LibraryItem.SONG ? 'filled' : 'subtle'}
>
Tracks
</Button>
<Button
compact
replace
component={Link}
fw={600}
size="md"
state={{ navigationId }}
to={{
pathname: generatePath(AppRoute.SEARCH, { itemType: LibraryItem.ALBUM }),
search: searchParams.toString(),
}}
variant={itemType === LibraryItem.ALBUM ? 'filled' : 'subtle'}
>
Albums
</Button>
<Button
compact
replace
component={Link}
fw={600}
size="md"
state={{ navigationId }}
to={{
pathname: generatePath(AppRoute.SEARCH, { itemType: LibraryItem.ALBUM_ARTIST }),
search: searchParams.toString(),
}}
variant={itemType === LibraryItem.ALBUM_ARTIST ? 'filled' : 'subtle'}
>
Artists
</Button>
</Group>
</FilterBar>
</Stack>
);
<PageHeader>
<Flex
justify="space-between"
w="100%"
>
<LibraryHeaderBar>
<LibraryHeaderBar.Title>Search</LibraryHeaderBar.Title>
</LibraryHeaderBar>
<Group>
<SearchInput
// key={`search-input-${initialQuery}`}
defaultValue={searchParams.get('query') || ''}
openedWidth={cq.isMd ? 250 : cq.isSm ? 200 : 150}
onChange={handleSearch}
/>
</Group>
</Flex>
</PageHeader>
<FilterBar>
<Group>
<Button
compact
replace
component={Link}
fw={600}
size="md"
state={{ navigationId }}
to={{
pathname: generatePath(AppRoute.SEARCH, { itemType: LibraryItem.SONG }),
search: searchParams.toString(),
}}
variant={itemType === LibraryItem.SONG ? 'filled' : 'subtle'}
>
Tracks
</Button>
<Button
compact
replace
component={Link}
fw={600}
size="md"
state={{ navigationId }}
to={{
pathname: generatePath(AppRoute.SEARCH, {
itemType: LibraryItem.ALBUM,
}),
search: searchParams.toString(),
}}
variant={itemType === LibraryItem.ALBUM ? 'filled' : 'subtle'}
>
Albums
</Button>
<Button
compact
replace
component={Link}
fw={600}
size="md"
state={{ navigationId }}
to={{
pathname: generatePath(AppRoute.SEARCH, {
itemType: LibraryItem.ALBUM_ARTIST,
}),
search: searchParams.toString(),
}}
variant={itemType === LibraryItem.ALBUM_ARTIST ? 'filled' : 'subtle'}
>
Artists
</Button>
</Group>
</FilterBar>
</Stack>
);
};

View file

@ -8,51 +8,51 @@ import { useNavigate } from 'react-router';
import { AppRoute } from '/@/renderer/router/routes';
interface ServerCommandsProps {
handleClose: () => void;
setPages: (pages: CommandPalettePages[]) => void;
setQuery: Dispatch<string>;
handleClose: () => void;
setPages: (pages: CommandPalettePages[]) => void;
setQuery: Dispatch<string>;
}
export const ServerCommands = ({ setQuery, setPages, handleClose }: ServerCommandsProps) => {
const serverList = useServerList();
const navigate = useNavigate();
const { setCurrentServer } = useAuthStoreActions();
const serverList = useServerList();
const navigate = useNavigate();
const { setCurrentServer } = useAuthStoreActions();
const handleManageServersModal = useCallback(() => {
openModal({
children: <ServerList />,
title: 'Manage Servers',
});
handleClose();
setQuery('');
setPages([CommandPalettePages.HOME]);
}, [handleClose, setPages, setQuery]);
const handleManageServersModal = useCallback(() => {
openModal({
children: <ServerList />,
title: 'Manage Servers',
});
handleClose();
setQuery('');
setPages([CommandPalettePages.HOME]);
}, [handleClose, setPages, setQuery]);
const handleSelectServer = useCallback(
(server: ServerListItem) => {
navigate(AppRoute.HOME);
setCurrentServer(server);
handleClose();
setQuery('');
setPages([CommandPalettePages.HOME]);
},
[handleClose, navigate, setCurrentServer, setPages, setQuery],
);
const handleSelectServer = useCallback(
(server: ServerListItem) => {
navigate(AppRoute.HOME);
setCurrentServer(server);
handleClose();
setQuery('');
setPages([CommandPalettePages.HOME]);
},
[handleClose, navigate, setCurrentServer, setPages, setQuery],
);
return (
<>
<Command.Group heading="Select a server">
{Object.keys(serverList).map((key) => (
<Command.Item
key={key}
onSelect={() => handleSelectServer(serverList[key])}
>{`Switch to ${serverList[key].name}...`}</Command.Item>
))}
</Command.Group>
<Command.Group heading="Manage">
<Command.Item onSelect={handleManageServersModal}>Manage servers...</Command.Item>
</Command.Group>
<Command.Separator />
</>
);
return (
<>
<Command.Group heading="Select a server">
{Object.keys(serverList).map((key) => (
<Command.Item
key={key}
onSelect={() => handleSelectServer(serverList[key])}
>{`Switch to ${serverList[key].name}...`}</Command.Item>
))}
</Command.Group>
<Command.Group heading="Manage">
<Command.Item onSelect={handleManageServersModal}>Manage servers...</Command.Item>
</Command.Group>
<Command.Separator />
</>
);
};