Album list updates

This commit is contained in:
jeffvli
2022-11-06 00:08:22 -07:00
parent 07123615ca
commit 633c6416df
9 changed files with 710 additions and 99 deletions
@@ -0,0 +1,451 @@
import styled from '@emotion/styled';
import { Center, Skeleton } from '@mantine/core';
import { RiAlbumFill } from 'react-icons/ri';
import { generatePath, useNavigate } from 'react-router';
import { Link } from 'react-router-dom';
import { ListChildComponentProps } from 'react-window';
import { Text } from '@/renderer/components/text';
import { GridCardControls } from '@/renderer/components/virtual-grid/grid-card/grid-card-controls';
import { fadeIn } from '@/renderer/styles';
import {
PlayQueueAddOptions,
LibraryItem,
CardRow,
CardRoute,
} from '@/renderer/types';
const CardWrapper = styled.div<{
itemGap: number;
itemHeight: number;
itemWidth: number;
link?: boolean;
}>`
flex: ${({ itemWidth }) => `0 0 ${itemWidth - 12}px`};
width: ${({ itemWidth }) => `${itemWidth}px`};
height: ${({ itemHeight, itemGap }) => `${itemHeight - 12 - itemGap}px`};
margin: ${({ itemGap }) => `0 ${itemGap / 2}px`};
padding: 12px 12px 0;
background: var(--card-default-bg);
border-radius: var(--card-default-radius);
cursor: ${({ link }) => link && 'pointer'};
transition: border 0.2s ease-in-out, background 0.2s ease-in-out;
user-select: none;
pointer-events: auto; // https://github.com/bvaughn/react-window/issues/128#issuecomment-460166682
&:hover {
background: var(--card-default-bg-hover);
}
&:hover div {
opacity: 1;
}
&:hover * {
&::before {
opacity: 0.5;
}
}
&:focus-visible {
outline: 1px solid #fff;
}
`;
const StyledCard = styled.div`
display: flex;
flex-direction: column;
gap: 0.5rem;
width: 100%;
height: 100%;
padding: 0;
border-radius: var(--card-default-radius);
`;
const ImageSection = styled.div<{ size?: number }>`
position: relative;
width: ${({ size }) => size && `${size - 24}px`};
height: ${({ size }) => size && `${size - 24}px`};
border-radius: var(--card-default-radius);
&::before {
position: absolute;
top: 0;
left: 0;
z-index: 1;
width: 100%;
height: 100%;
background: linear-gradient(
0deg,
rgba(0, 0, 0, 100%) 35%,
rgba(0, 0, 0, 0%) 100%
);
opacity: 0;
transition: all 0.2s ease-in-out;
content: '';
user-select: none;
}
`;
interface ImageProps {
height: number;
isLoading?: boolean;
}
const Image = styled.img<ImageProps>`
width: ${({ height }) => `${height - 24}px`};
height: ${({ height }) => `${height - 24}px`};
object-fit: cover;
border: 0;
border-radius: var(--card-default-radius);
${fadeIn}
animation: fadein 0.3s ease-in-out;
`;
const ControlsContainer = styled.div`
position: absolute;
bottom: 0;
z-index: 50;
width: 100%;
opacity: 0;
transition: all 0.2s ease-in-out;
`;
const DetailSection = styled.div`
display: flex;
flex-direction: column;
`;
const Row = styled.div<{ secondary?: boolean }>`
width: 100%;
max-width: 100%;
height: 25px;
padding: 0 0.2rem;
overflow: hidden;
color: ${({ secondary }) =>
secondary ? 'var(--main-fg-secondary)' : 'var(--main-fg)'};
white-space: nowrap;
text-overflow: ellipsis;
`;
interface BaseGridCardProps {
columnIndex: number;
controls: {
cardControls: any[];
cardRows: CardRow[];
handlePlayQueueAdd: (options: PlayQueueAddOptions) => void;
itemType: LibraryItem;
route?: CardRoute;
};
data: any;
listChildProps: Omit<ListChildComponentProps, 'data' | 'style'>;
sizes: {
itemGap: number;
itemHeight: number;
itemWidth: number;
};
}
export const DefaultCard = ({
listChildProps,
data,
columnIndex,
controls,
sizes,
}: BaseGridCardProps) => {
const navigate = useNavigate();
const { isScrolling, index } = listChildProps;
const { itemGap, itemHeight, itemWidth } = sizes;
const { cardControls, handlePlayQueueAdd, itemType, cardRows, route } =
controls;
if (data) {
if (route) {
return (
<CardWrapper
key={`card-${columnIndex}-${index}`}
link
itemGap={itemGap}
itemHeight={itemHeight}
itemWidth={itemWidth}
onClick={() =>
navigate(
generatePath(
route.route,
route.slugs?.reduce((acc, slug) => {
return {
...acc,
[slug.slugProperty]: data[slug.idProperty],
};
}, {})
)
)
}
>
<StyledCard>
<ImageSection size={itemWidth}>
{data?.imageUrl ? (
<Image height={itemWidth} src={data?.imageUrl} />
) : (
<Center
sx={{
background: 'var(--placeholder-bg)',
borderRadius: 'var(--card-default-radius)',
height: '100%',
width: '100%',
}}
>
<RiAlbumFill color="var(--placeholder-fg)" size={35} />
</Center>
)}
<ControlsContainer>
{!isScrolling && (
<GridCardControls
cardControls={cardControls}
handlePlayQueueAdd={handlePlayQueueAdd}
itemData={data}
itemType={itemType}
/>
)}
</ControlsContainer>
</ImageSection>
<DetailSection>
{cardRows.map((row: CardRow, index: number) => {
if (row.arrayProperty) {
if (row.route) {
return (
<Row secondary={index > 0}>
{data[row.property].map(
(item: any, itemIndex: number) => (
<>
{itemIndex > 0 && (
<Text
sx={{
display: 'inline-block',
padding: '0 2px 0 1px',
}}
>
,
</Text>
)}{' '}
<Text
link
component={Link}
overflow="hidden"
secondary={index > 0}
to={generatePath(
row.route!.route,
row.route!.slugs?.reduce((acc, slug) => {
return {
...acc,
[slug.slugProperty]:
data[slug.idProperty],
};
}, {})
)}
onClick={(e) => e.stopPropagation()}
>
{row.arrayProperty && item[row.arrayProperty]}
</Text>
</>
)
)}
</Row>
);
}
return (
<Row>
{data[row.property].map((item: any) => (
<Text overflow="hidden" secondary={index > 0}>
{row.arrayProperty && item[row.arrayProperty]}
</Text>
))}
</Row>
);
}
return (
<Row key={row.property}>
{row.route ? (
<Text
link
component={Link}
overflow="hidden"
to={generatePath(
row.route.route,
row.route.slugs?.reduce((acc, slug) => {
return {
...acc,
[slug.slugProperty]: data[slug.idProperty],
};
}, {})
)}
onClick={(e) => e.stopPropagation()}
>
{data && data[row.property]}
</Text>
) : (
<Text overflow="hidden" secondary={index > 0}>
{data && data[row.property]}
</Text>
)}
</Row>
);
})}
</DetailSection>
</StyledCard>
</CardWrapper>
);
}
return (
<CardWrapper
key={`card-${columnIndex}-${index}`}
itemGap={itemGap}
itemHeight={itemHeight}
itemWidth={itemWidth}
>
<StyledCard>
<ImageSection size={itemWidth}>
{data?.imageUrl ? (
<Image height={itemWidth} src={data?.imageUrl} />
) : (
<Center
sx={{
background: 'var(--placeholder-bg)',
borderRadius: 'var(--card-default-radius)',
height: '100%',
width: '100%',
}}
>
<RiAlbumFill color="var(--placeholder-fg)" size={35} />
</Center>
)}
<ControlsContainer>
{!isScrolling && (
<GridCardControls
cardControls={cardControls}
handlePlayQueueAdd={handlePlayQueueAdd}
itemData={data}
itemType={itemType}
/>
)}
</ControlsContainer>
</ImageSection>
<DetailSection>
{cardRows.map((row: CardRow, index: number) => {
if (row.arrayProperty) {
if (row.route) {
return (
<Row secondary={index > 0}>
{data[row.property].map(
(item: any, itemIndex: number) => (
<>
{itemIndex > 0 && (
<Text
sx={{
display: 'inline-block',
padding: '0 2px 0 1px',
}}
>
,
</Text>
)}{' '}
<Text
link
component={Link}
overflow="hidden"
secondary={index > 0}
to={generatePath(
row.route!.route,
row.route!.slugs?.reduce((acc, slug) => {
return {
...acc,
[slug.slugProperty]: data[slug.idProperty],
};
}, {})
)}
onClick={(e) => e.stopPropagation()}
>
{row.arrayProperty && item[row.arrayProperty]}
</Text>
</>
)
)}
</Row>
);
}
return (
<Row>
{data[row.property].map((item: any) => (
<Text overflow="hidden" secondary={index > 0}>
{row.arrayProperty && item[row.arrayProperty]}
</Text>
))}
</Row>
);
}
return (
<Row key={row.property}>
{row.route ? (
<Text
link
component={Link}
overflow="hidden"
to={generatePath(
row.route.route,
row.route.slugs?.reduce((acc, slug) => {
return {
...acc,
[slug.slugProperty]: data[slug.idProperty],
};
}, {})
)}
onClick={(e) => e.stopPropagation()}
>
{data && data[row.property]}
</Text>
) : (
<Text overflow="hidden" secondary={index > 0}>
{data && data[row.property]}
</Text>
)}
</Row>
);
})}
</DetailSection>
</StyledCard>
</CardWrapper>
);
}
return (
<CardWrapper
key={`card-${columnIndex}-${index}`}
itemGap={itemGap}
itemHeight={itemHeight}
itemWidth={itemWidth + 12}
>
<StyledCard>
<Skeleton visible radius="sm">
<ImageSection size={itemWidth} />
</Skeleton>
<DetailSection>
{cardRows.map((row: CardRow, index: number) => (
<Skeleton
key={`row-${row.property}`}
my={2}
radius="md"
visible={!data}
width={!data ? `${90 - index * 20}%` : '100%'}
>
<Row />
</Skeleton>
))}
</DetailSection>
</StyledCard>
</CardWrapper>
);
};
@@ -90,6 +90,7 @@ export const GridCardControls = ({
whileTap={{ scale: 1 }} whileTap={{ scale: 1 }}
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation();
handlePlayQueueAdd({ handlePlayQueueAdd({
byItemType: { byItemType: {
id: itemData.id, id: itemData.id,
@@ -116,7 +117,10 @@ export const GridCardControls = ({
<Button <Button
p={5} p={5}
variant="subtle" variant="subtle"
onClick={(e) => e.preventDefault()} onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
> >
<RiMore2Fill color="white" size={20} /> <RiMore2Fill color="white" size={20} />
</Button> </Button>
@@ -125,6 +129,7 @@ export const GridCardControls = ({
<DropdownMenu.Item <DropdownMenu.Item
onClick={(e: MouseEvent) => { onClick={(e: MouseEvent) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation();
handlePlayQueueAdd({ handlePlayQueueAdd({
byItemType: { byItemType: {
id: itemData.id, id: itemData.id,
@@ -139,6 +144,7 @@ export const GridCardControls = ({
<DropdownMenu.Item <DropdownMenu.Item
onClick={(e: MouseEvent) => { onClick={(e: MouseEvent) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation();
handlePlayQueueAdd({ handlePlayQueueAdd({
byItemType: { byItemType: {
id: itemData.id, id: itemData.id,
@@ -1,6 +1,7 @@
import { ListChildComponentProps } from 'react-window'; import { ListChildComponentProps } from 'react-window';
import { DefaultCard } from '@/renderer/components/virtual-grid/grid-card/default-card';
import { PosterCard } from '@/renderer/components/virtual-grid/grid-card/poster-card'; import { PosterCard } from '@/renderer/components/virtual-grid/grid-card/poster-card';
import { GridCardData } from '@/renderer/types'; import { CardDisplayType, GridCardData } from '@/renderer/types';
export const GridCard = ({ export const GridCard = ({
data, data,
@@ -27,9 +28,11 @@ export const GridCard = ({
const stopIndex = Math.min(itemCount - 1, startIndex + columnCount - 1); const stopIndex = Math.min(itemCount - 1, startIndex + columnCount - 1);
const cards = []; const cards = [];
const View = display === CardDisplayType.CARD ? DefaultCard : PosterCard;
for (let i = startIndex; i <= stopIndex; i += 1) { for (let i = startIndex; i <= stopIndex; i += 1) {
cards.push( cards.push(
<PosterCard <View
key={`card-${i}-${index}`} key={`card-${i}-${index}`}
columnIndex={i} columnIndex={i}
controls={{ controls={{
@@ -21,7 +21,7 @@ const CardWrapper = styled.div<{
}>` }>`
flex: ${({ itemWidth }) => `0 0 ${itemWidth}px`}; flex: ${({ itemWidth }) => `0 0 ${itemWidth}px`};
width: ${({ itemWidth }) => `${itemWidth}px`}; width: ${({ itemWidth }) => `${itemWidth}px`};
height: ${({ itemHeight }) => `${itemHeight}px`}; height: ${({ itemHeight, itemGap }) => `${itemHeight - itemGap}px`};
margin: ${({ itemGap }) => `0 ${itemGap / 2}px`}; margin: ${({ itemGap }) => `0 ${itemGap / 2}px`};
transition: border 0.2s ease-in-out; transition: border 0.2s ease-in-out;
user-select: none; user-select: none;
@@ -49,13 +49,13 @@ const StyledCard = styled.div`
width: 100%; width: 100%;
height: 100%; height: 100%;
padding: 0; padding: 0;
border-radius: 5px; border-radius: var(--card-poster-radius);
`; `;
const ImageSection = styled.div` const ImageSection = styled.div`
position: relative; position: relative;
width: 100%; width: 100%;
border-radius: 5px; border-radius: var(--card-poster-radius);
&::before { &::before {
position: absolute; position: absolute;
@@ -86,7 +86,7 @@ const Image = styled.img<ImageProps>`
height: ${({ height }) => `${height}px`}; height: ${({ height }) => `${height}px`};
object-fit: cover; object-fit: cover;
border: 0; border: 0;
border-radius: 2px; border-radius: var(--card-poster-radius);
${fadeIn} ${fadeIn}
animation: fadein 0.3s ease-in-out; animation: fadein 0.3s ease-in-out;
@@ -177,7 +177,7 @@ export const PosterCard = ({
<Center <Center
sx={{ sx={{
background: 'var(--placeholder-bg)', background: 'var(--placeholder-bg)',
borderRadius: '5px', borderRadius: 'var(--card-poster-radius)',
height: '100%', height: '100%',
}} }}
> >
@@ -204,7 +204,7 @@ export const PosterCard = ({
<Center <Center
sx={{ sx={{
background: 'var(--placeholder-bg)', background: 'var(--placeholder-bg)',
borderRadius: '5px', borderRadius: 'var(--card-poster-radius)',
height: '100%', height: '100%',
}} }}
> >
@@ -16,6 +16,7 @@ export const VirtualGridWrapper = ({
itemGap, itemGap,
itemType, itemType,
itemWidth, itemWidth,
display,
itemHeight, itemHeight,
itemCount, itemCount,
columnCount, columnCount,
@@ -42,6 +43,7 @@ export const VirtualGridWrapper = ({
() => ({ () => ({
cardRows, cardRows,
columnCount, columnCount,
display,
handlePlayQueueAdd, handlePlayQueueAdd,
itemCount, itemCount,
itemData, itemData,
@@ -58,6 +60,7 @@ export const VirtualGridWrapper = ({
handlePlayQueueAdd, handlePlayQueueAdd,
itemCount, itemCount,
itemData, itemData,
display,
itemGap, itemGap,
itemHeight, itemHeight,
route, route,
@@ -72,7 +75,7 @@ export const VirtualGridWrapper = ({
useIsScrolling useIsScrolling
itemCount={rowCount} itemCount={rowCount}
itemData={memo} itemData={memo}
itemSize={itemHeight + itemGap} itemSize={itemHeight}
overscanCount={5} overscanCount={5}
> >
{GridCard} {GridCard}
@@ -52,7 +52,8 @@ export const VirtualInfiniteGrid = ({
return { return {
columnCount: itemsPerRow, columnCount: itemsPerRow,
itemHeight: itemSize! + cardRows.length * 25, itemHeight: itemSize! + cardRows.length * 25 + itemGap,
itemWidth: itemSize! + itemGap,
rowCount: Math.ceil(itemCount / itemsPerRow), rowCount: Math.ceil(itemCount / itemsPerRow),
}; };
}, [cardRows.length, itemCount, itemGap, itemSize, width]); }, [cardRows.length, itemCount, itemGap, itemSize, width]);
@@ -79,7 +79,7 @@ const FILTER_GROUP_OPTIONS_DATA = [
const FILTER_OPTIONS_DATA = [ const FILTER_OPTIONS_DATA = [
{ {
default: '~', default: '~',
label: 'Artist Title', label: 'Artist Name',
value: 'artists.name', value: 'artists.name',
}, },
{ {
@@ -94,7 +94,7 @@ const FILTER_OPTIONS_DATA = [
}, },
{ {
default: '~', default: '~',
label: 'Album Artist Title', label: 'Album Artist Name',
value: 'albumArtists.name', value: 'albumArtists.name',
}, },
{ {
@@ -109,7 +109,7 @@ const FILTER_OPTIONS_DATA = [
}, },
{ {
default: '~', default: '~',
label: 'Album Title', label: 'Album Name',
value: 'albums.name', value: 'albums.name',
}, },
{ {
@@ -145,7 +145,7 @@ const FILTER_OPTIONS_DATA = [
}, },
{ {
default: '~', default: '~',
label: 'Track Title', label: 'Track Name',
value: 'songs.name', value: 'songs.name',
}, },
{ {
@@ -1,10 +1,16 @@
/* eslint-disable no-plusplus */ /* eslint-disable no-plusplus */
import { useState, useCallback, useMemo } from 'react'; import { useState, useCallback, useMemo } from 'react';
import { Group, Checkbox } from '@mantine/core'; import { Group, Checkbox, Box, Slider } from '@mantine/core';
import { useDebouncedValue, useSetState, useToggle } from '@mantine/hooks'; import { useDebouncedValue, useSetState, useToggle } from '@mantine/hooks';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import { AnimatePresence, motion } from 'framer-motion';
import throttle from 'lodash/throttle';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import { RiArrowDownSLine, RiArrowLeftLine } from 'react-icons/ri'; import {
RiArrowDownSLine,
RiDeleteBack2Fill,
RiSettings2Fill,
} from 'react-icons/ri';
import AutoSizer from 'react-virtualized-auto-sizer'; import AutoSizer from 'react-virtualized-auto-sizer';
import { api } from '@/renderer/api'; import { api } from '@/renderer/api';
import { AlbumSort } from '@/renderer/api/albums.api'; import { AlbumSort } from '@/renderer/api/albums.api';
@@ -25,18 +31,14 @@ import {
AdvancedFilterGroup, AdvancedFilterGroup,
AdvancedFilters, AdvancedFilters,
FilterGroupType, FilterGroupType,
formatAdvancedFiltersQuery, encodeAdvancedFiltersQuery,
} from '@/renderer/features/albums/components/advanced-filters'; } from '@/renderer/features/albums/components/advanced-filters';
import { useAlbumList } from '@/renderer/features/albums/queries/use-album-list'; import { useAlbumList } from '@/renderer/features/albums/queries/use-album-list';
import { useServerList } from '@/renderer/features/servers'; import { useServerList } from '@/renderer/features/servers';
import { AnimatedPage, useServerCredential } from '@/renderer/features/shared'; import { AnimatedPage, useServerCredential } from '@/renderer/features/shared';
import { AppRoute } from '@/renderer/router/routes'; import { AppRoute } from '@/renderer/router/routes';
import { useAuthStore } from '@/renderer/store'; import { useAppStore, useAuthStore } from '@/renderer/store';
import { LibraryItem } from '@/renderer/types'; import { LibraryItem, CardDisplayType } from '@/renderer/types';
import {
ViewType,
ViewTypeButton,
} from '../../library/components/ViewTypeButton';
const FILTERS = [ const FILTERS = [
{ name: 'Title', value: AlbumSort.NAME }, { name: 'Title', value: AlbumSort.NAME },
@@ -57,12 +59,27 @@ const ORDER = [
{ name: 'Descending', value: SortOrder.DESC }, { name: 'Descending', value: SortOrder.DESC },
]; ];
const DEFAULT_ADVANCED_FILTERS = {
group: [],
rules: [
{
field: '',
operator: '',
uniqueId: nanoid(),
value: '',
},
],
type: FilterGroupType.AND,
uniqueId: nanoid(),
};
export const AlbumListRoute = () => { export const AlbumListRoute = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { serverToken, isImageTokenRequired } = useServerCredential(); const { serverToken, isImageTokenRequired } = useServerCredential();
const page = useAppStore((state) => state.albums);
const setPage = useAppStore((state) => state.setPage);
const serverId = useAuthStore((state) => state.currentServer?.id) || ''; const serverId = useAuthStore((state) => state.currentServer?.id) || '';
const { data: servers } = useServerList({ enabled: true }); const { data: servers } = useServerList({ enabled: true });
const [viewType, setViewType] = useState(ViewType.Grid);
const [filters, setFilters] = useSetState({ const [filters, setFilters] = useSetState({
orderBy: SortOrder.ASC, orderBy: SortOrder.ASC,
serverFolderId: [] as string[], serverFolderId: [] as string[],
@@ -70,19 +87,23 @@ export const AlbumListRoute = () => {
}); });
const [isAdvFilter, toggleAdvFilter] = useToggle(); const [isAdvFilter, toggleAdvFilter] = useToggle();
const [rawAdvFilters, setRawAdvFilters] = useState<AdvancedFilterGroup>({ const [rawAdvFilters, setRawAdvFilters] = useState<AdvancedFilterGroup>(
group: [], DEFAULT_ADVANCED_FILTERS
rules: [{ field: null, operator: null, uniqueId: nanoid(), value: null }], );
type: FilterGroupType.AND,
uniqueId: nanoid(),
});
const [debouncedAdvFilters] = useDebouncedValue(rawAdvFilters, 300); const [debouncedAdvFilters] = useDebouncedValue(rawAdvFilters, 500);
const advancedFilters = useMemo(() => { const advancedFilters = useMemo(() => {
const value = formatAdvancedFiltersQuery(debouncedAdvFilters); if (!isAdvFilter) {
return encodeURI(JSON.stringify(value)); return encodeAdvancedFiltersQuery(DEFAULT_ADVANCED_FILTERS);
}, [debouncedAdvFilters]); }
return encodeAdvancedFiltersQuery(debouncedAdvFilters);
}, [debouncedAdvFilters, isAdvFilter]);
const handleResetAdvancedFilters = () => {
setRawAdvFilters(DEFAULT_ADVANCED_FILTERS);
};
const serverFolders = useMemo(() => { const serverFolders = useMemo(() => {
const server = servers?.data.find((server) => server.id === serverId); const server = servers?.data.find((server) => server.id === serverId);
@@ -99,7 +120,7 @@ export const AlbumListRoute = () => {
}); });
const fetch = useCallback( const fetch = useCallback(
async ({ skip, take }) => { async ({ skip, take }: { skip: number; take: number }) => {
const albums = await queryClient.fetchQuery( const albums = await queryClient.fetchQuery(
queryKeys.albums.list(serverId, { queryKeys.albums.list(serverId, {
skip, skip,
@@ -138,6 +159,15 @@ export const AlbumListRoute = () => {
] ]
); );
const setSize = throttle(
(e: number) =>
setPage('albums', {
...page,
list: { ...page.list, size: e },
}),
200
);
return ( return (
<AnimatedPage> <AnimatedPage>
<VirtualGridContainer> <VirtualGridContainer>
@@ -159,16 +189,7 @@ export const AlbumListRoute = () => {
{FILTERS.map((filter) => ( {FILTERS.map((filter) => (
<DropdownMenu.Item <DropdownMenu.Item
key={`filter-${filter.value}`} key={`filter-${filter.value}`}
color={ isActive={filter.value === filters.sortBy}
filter.value === filters.sortBy
? 'var(--primary-color)'
: undefined
}
rightSection={
filter.value === filters.sortBy ? (
<RiArrowLeftLine />
) : undefined
}
onClick={() => setFilters({ sortBy: filter.value })} onClick={() => setFilters({ sortBy: filter.value })}
> >
{filter.name} {filter.name}
@@ -176,8 +197,7 @@ export const AlbumListRoute = () => {
))} ))}
<DropdownMenu.Divider /> <DropdownMenu.Divider />
<DropdownMenu.Item <DropdownMenu.Item
color={isAdvFilter ? 'var(--primary-color)' : undefined} isActive={isAdvFilter}
rightSection={isAdvFilter ? <RiArrowLeftLine /> : undefined}
onClick={() => toggleAdvFilter()} onClick={() => toggleAdvFilter()}
> >
Advanced Filters Advanced Filters
@@ -197,16 +217,7 @@ export const AlbumListRoute = () => {
{ORDER.map((sort) => ( {ORDER.map((sort) => (
<DropdownMenu.Item <DropdownMenu.Item
key={`sort-${sort.value}`} key={`sort-${sort.value}`}
color={ isActive={sort.value === filters.orderBy}
sort.value === filters.orderBy
? 'var(--primary-color)'
: undefined
}
rightSection={
sort.value === filters.orderBy ? (
<RiArrowLeftLine />
) : undefined
}
onClick={() => setFilters({ orderBy: sort.value })} onClick={() => setFilters({ orderBy: sort.value })}
> >
{sort.name} {sort.name}
@@ -240,69 +251,181 @@ export const AlbumListRoute = () => {
</DropdownMenu> </DropdownMenu>
</Group> </Group>
<Group position="right"> <Group position="right">
<ViewTypeButton <DropdownMenu position="bottom-end" width={100}>
handler={setViewType} <DropdownMenu.Target>
menuProps={{ position: 'bottom-end' }} <Button compact variant="subtle">
type={viewType} <RiSettings2Fill size={15} />
/> </Button>
</DropdownMenu.Target>
<DropdownMenu.Dropdown>
<DropdownMenu.Item>
<Slider
defaultValue={page.list?.size || 0}
label={null}
onChange={setSize}
/>
</DropdownMenu.Item>
<DropdownMenu.Divider />
<DropdownMenu.Item
isActive={
page.list.type === 'grid' &&
page.list.display === CardDisplayType.CARD
}
onClick={() =>
setPage('albums', {
...page,
list: {
...page.list,
display: CardDisplayType.CARD,
type: 'grid',
},
})
}
>
Card
</DropdownMenu.Item>
<DropdownMenu.Item
isActive={
page.list.type === 'grid' &&
page.list.display === CardDisplayType.POSTER
}
onClick={() =>
setPage('albums', {
...page,
list: {
...page.list,
display: CardDisplayType.POSTER,
type: 'grid',
},
})
}
>
Poster
</DropdownMenu.Item>
<DropdownMenu.Item
disabled
isActive={page.list.type === 'list'}
onClick={() =>
setPage('albums', {
...page,
list: {
...page.list,
type: 'list',
},
})
}
>
List
</DropdownMenu.Item>
</DropdownMenu.Dropdown>
</DropdownMenu>
</Group> </Group>
</Group> </Group>
{isAdvFilter && ( <AnimatePresence
<> key="album-list-advanced-filter"
<Paper sx={{ maxHeight: '20vh' }}> exitBeforeEnter
<ScrollArea initial={false}
my={10} >
px={10} {isAdvFilter && (
sx={{ height: '100%', width: '100%' }} <motion.div
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -25 }}
initial={{ opacity: 0, y: -25 }}
style={{ maxHeight: '20vh', zIndex: 100 }}
transition={{ duration: 0.2, ease: 'easeInOut' }}
>
<Paper
sx={{
boxShadow: ' 0 10px 5px -2px rgb(0, 0, 0, .2)',
height: '100%',
position: 'relative',
}}
> >
<Group noWrap my={10} position="apart"> <ScrollArea sx={{ height: '100%', width: '100%' }}>
<Group> <Group
<Text>Advanced Filters</Text> noWrap
<NumberInput p={10}
disabled position="apart"
min={1} sx={{
placeholder="Limit" background: 'var(--paper-bg)',
size="xs" position: 'sticky',
width={75} top: 0,
/> zIndex: 50,
}}
>
<Group>
<Text noSelect>Advanced Filters</Text>
<NumberInput
disabled
min={1}
placeholder="Limit"
size="xs"
width={75}
/>
<Button
px={10}
size="xs"
tooltip={{ label: 'Reset' }}
variant="default"
onClick={handleResetAdvancedFilters}
>
<RiDeleteBack2Fill size={15} />
</Button>
</Group>
<Button disabled uppercase variant="default">
Save as...
</Button>
</Group> </Group>
<Button disabled uppercase> <Box p={10}>
Save as... <AdvancedFilters
</Button> filters={rawAdvFilters}
</Group> setFilters={setRawAdvFilters}
<AdvancedFilters />
filters={rawAdvFilters} </Box>
setFilters={setRawAdvFilters} </ScrollArea>
/> </Paper>
</ScrollArea> </motion.div>
</Paper> )}
</> </AnimatePresence>
)}
<VirtualGridAutoSizerContainer> <VirtualGridAutoSizerContainer>
<AutoSizer> <AutoSizer>
{({ height, width }) => ( {({ height, width }) => (
<VirtualInfiniteGrid <VirtualInfiniteGrid
cardRows={[ cardRows={[
{ {
align: 'center', property: 'name',
prop: 'name',
route: { route: {
prop: 'id',
route: AppRoute.LIBRARY_ALBUMS_DETAIL, route: AppRoute.LIBRARY_ALBUMS_DETAIL,
slugs: [{ idProperty: 'id', slugProperty: 'albumId' }],
}, },
}, },
{ {
align: 'center', arrayProperty: 'name',
prop: 'releaseYear', property: 'albumArtists',
route: {
route: AppRoute.LIBRARY_ALBUMARTISTS_DETAIL,
slugs: [
{ idProperty: 'id', slugProperty: 'albumArtistId' },
],
},
},
{
property: 'releaseYear',
}, },
]} ]}
display={page.list?.display || CardDisplayType.CARD}
fetchFn={fetch} fetchFn={fetch}
height={height} height={height}
itemCount={albums?.pagination.totalEntries || 0} itemCount={albums?.pagination.totalEntries || 0}
itemGap={20} itemGap={20}
itemSize={200} itemSize={150 + page.list?.size}
itemType={LibraryItem.ALBUM} itemType={LibraryItem.ALBUM}
minimumBatchSize={40} minimumBatchSize={40}
refresh={advancedFilters}
route={{
route: AppRoute.LIBRARY_ALBUMS_DETAIL,
slugs: [{ idProperty: 'id', slugProperty: 'albumId' }],
}}
width={width} width={width}
/> />
)} )}
+25 -1
View File
@@ -1,7 +1,7 @@
import create from 'zustand'; import create from 'zustand';
import { devtools, persist } from 'zustand/middleware'; import { devtools, persist } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer'; import { immer } from 'zustand/middleware/immer';
import { Platform } from '@/renderer/types'; import { CardDisplayType, Platform } from '@/renderer/types';
type SidebarProps = { type SidebarProps = {
expanded: string[]; expanded: string[];
@@ -11,7 +11,18 @@ type SidebarProps = {
rightWidth: string; rightWidth: string;
}; };
type LibraryPageProps = {
list: ListProps;
};
type ListProps = {
display: CardDisplayType;
size: number;
type: 'list' | 'grid';
};
export interface AppState { export interface AppState {
albums: LibraryPageProps;
platform: Platform; platform: Platform;
sidebar: { sidebar: {
expanded: string[]; expanded: string[];
@@ -24,6 +35,7 @@ export interface AppState {
export interface AppSlice extends AppState { export interface AppSlice extends AppState {
setAppStore: (data: Partial<AppSlice>) => void; setAppStore: (data: Partial<AppSlice>) => void;
setPage: (page: 'albums', options: Partial<LibraryPageProps>) => void;
setSidebar: (options: Partial<SidebarProps>) => void; setSidebar: (options: Partial<SidebarProps>) => void;
} }
@@ -31,10 +43,22 @@ export const useAppStore = create<AppSlice>()(
persist( persist(
devtools( devtools(
immer((set, get) => ({ immer((set, get) => ({
albums: {
list: {
display: CardDisplayType.CARD,
size: 50,
type: 'list',
},
},
platform: Platform.WINDOWS, platform: Platform.WINDOWS,
setAppStore: (data) => { setAppStore: (data) => {
set({ ...get(), ...data }); set({ ...get(), ...data });
}, },
setPage: (page: 'albums', data: any) => {
set((state) => {
state[page] = { ...state[page], ...data };
});
},
setSidebar: (options) => { setSidebar: (options) => {
set((state) => { set((state) => {
state.sidebar = { ...state.sidebar, ...options }; state.sidebar = { ...state.sidebar, ...options };