mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-07 12:30:12 +02:00
Album list updates
This commit is contained in:
@@ -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 }}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handlePlayQueueAdd({
|
||||
byItemType: {
|
||||
id: itemData.id,
|
||||
@@ -116,7 +117,10 @@ export const GridCardControls = ({
|
||||
<Button
|
||||
p={5}
|
||||
variant="subtle"
|
||||
onClick={(e) => e.preventDefault()}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<RiMore2Fill color="white" size={20} />
|
||||
</Button>
|
||||
@@ -125,6 +129,7 @@ export const GridCardControls = ({
|
||||
<DropdownMenu.Item
|
||||
onClick={(e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handlePlayQueueAdd({
|
||||
byItemType: {
|
||||
id: itemData.id,
|
||||
@@ -139,6 +144,7 @@ export const GridCardControls = ({
|
||||
<DropdownMenu.Item
|
||||
onClick={(e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handlePlayQueueAdd({
|
||||
byItemType: {
|
||||
id: itemData.id,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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 { GridCardData } from '@/renderer/types';
|
||||
import { CardDisplayType, GridCardData } from '@/renderer/types';
|
||||
|
||||
export const GridCard = ({
|
||||
data,
|
||||
@@ -27,9 +28,11 @@ export const GridCard = ({
|
||||
const stopIndex = Math.min(itemCount - 1, startIndex + columnCount - 1);
|
||||
const cards = [];
|
||||
|
||||
const View = display === CardDisplayType.CARD ? DefaultCard : PosterCard;
|
||||
|
||||
for (let i = startIndex; i <= stopIndex; i += 1) {
|
||||
cards.push(
|
||||
<PosterCard
|
||||
<View
|
||||
key={`card-${i}-${index}`}
|
||||
columnIndex={i}
|
||||
controls={{
|
||||
|
||||
@@ -21,7 +21,7 @@ const CardWrapper = styled.div<{
|
||||
}>`
|
||||
flex: ${({ itemWidth }) => `0 0 ${itemWidth}px`};
|
||||
width: ${({ itemWidth }) => `${itemWidth}px`};
|
||||
height: ${({ itemHeight }) => `${itemHeight}px`};
|
||||
height: ${({ itemHeight, itemGap }) => `${itemHeight - itemGap}px`};
|
||||
margin: ${({ itemGap }) => `0 ${itemGap / 2}px`};
|
||||
transition: border 0.2s ease-in-out;
|
||||
user-select: none;
|
||||
@@ -49,13 +49,13 @@ const StyledCard = styled.div`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
border-radius: 5px;
|
||||
border-radius: var(--card-poster-radius);
|
||||
`;
|
||||
|
||||
const ImageSection = styled.div`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
border-radius: 5px;
|
||||
border-radius: var(--card-poster-radius);
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
@@ -86,7 +86,7 @@ const Image = styled.img<ImageProps>`
|
||||
height: ${({ height }) => `${height}px`};
|
||||
object-fit: cover;
|
||||
border: 0;
|
||||
border-radius: 2px;
|
||||
border-radius: var(--card-poster-radius);
|
||||
|
||||
${fadeIn}
|
||||
animation: fadein 0.3s ease-in-out;
|
||||
@@ -177,7 +177,7 @@ export const PosterCard = ({
|
||||
<Center
|
||||
sx={{
|
||||
background: 'var(--placeholder-bg)',
|
||||
borderRadius: '5px',
|
||||
borderRadius: 'var(--card-poster-radius)',
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
@@ -204,7 +204,7 @@ export const PosterCard = ({
|
||||
<Center
|
||||
sx={{
|
||||
background: 'var(--placeholder-bg)',
|
||||
borderRadius: '5px',
|
||||
borderRadius: 'var(--card-poster-radius)',
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -16,6 +16,7 @@ export const VirtualGridWrapper = ({
|
||||
itemGap,
|
||||
itemType,
|
||||
itemWidth,
|
||||
display,
|
||||
itemHeight,
|
||||
itemCount,
|
||||
columnCount,
|
||||
@@ -42,6 +43,7 @@ export const VirtualGridWrapper = ({
|
||||
() => ({
|
||||
cardRows,
|
||||
columnCount,
|
||||
display,
|
||||
handlePlayQueueAdd,
|
||||
itemCount,
|
||||
itemData,
|
||||
@@ -58,6 +60,7 @@ export const VirtualGridWrapper = ({
|
||||
handlePlayQueueAdd,
|
||||
itemCount,
|
||||
itemData,
|
||||
display,
|
||||
itemGap,
|
||||
itemHeight,
|
||||
route,
|
||||
@@ -72,7 +75,7 @@ export const VirtualGridWrapper = ({
|
||||
useIsScrolling
|
||||
itemCount={rowCount}
|
||||
itemData={memo}
|
||||
itemSize={itemHeight + itemGap}
|
||||
itemSize={itemHeight}
|
||||
overscanCount={5}
|
||||
>
|
||||
{GridCard}
|
||||
|
||||
@@ -52,7 +52,8 @@ export const VirtualInfiniteGrid = ({
|
||||
|
||||
return {
|
||||
columnCount: itemsPerRow,
|
||||
itemHeight: itemSize! + cardRows.length * 25,
|
||||
itemHeight: itemSize! + cardRows.length * 25 + itemGap,
|
||||
itemWidth: itemSize! + itemGap,
|
||||
rowCount: Math.ceil(itemCount / itemsPerRow),
|
||||
};
|
||||
}, [cardRows.length, itemCount, itemGap, itemSize, width]);
|
||||
|
||||
@@ -79,7 +79,7 @@ const FILTER_GROUP_OPTIONS_DATA = [
|
||||
const FILTER_OPTIONS_DATA = [
|
||||
{
|
||||
default: '~',
|
||||
label: 'Artist Title',
|
||||
label: 'Artist Name',
|
||||
value: 'artists.name',
|
||||
},
|
||||
{
|
||||
@@ -94,7 +94,7 @@ const FILTER_OPTIONS_DATA = [
|
||||
},
|
||||
{
|
||||
default: '~',
|
||||
label: 'Album Artist Title',
|
||||
label: 'Album Artist Name',
|
||||
value: 'albumArtists.name',
|
||||
},
|
||||
{
|
||||
@@ -109,7 +109,7 @@ const FILTER_OPTIONS_DATA = [
|
||||
},
|
||||
{
|
||||
default: '~',
|
||||
label: 'Album Title',
|
||||
label: 'Album Name',
|
||||
value: 'albums.name',
|
||||
},
|
||||
{
|
||||
@@ -145,7 +145,7 @@ const FILTER_OPTIONS_DATA = [
|
||||
},
|
||||
{
|
||||
default: '~',
|
||||
label: 'Track Title',
|
||||
label: 'Track Name',
|
||||
value: 'songs.name',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
/* eslint-disable no-plusplus */
|
||||
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 { useQueryClient } from '@tanstack/react-query';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import throttle from 'lodash/throttle';
|
||||
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 { api } from '@/renderer/api';
|
||||
import { AlbumSort } from '@/renderer/api/albums.api';
|
||||
@@ -25,18 +31,14 @@ import {
|
||||
AdvancedFilterGroup,
|
||||
AdvancedFilters,
|
||||
FilterGroupType,
|
||||
formatAdvancedFiltersQuery,
|
||||
encodeAdvancedFiltersQuery,
|
||||
} from '@/renderer/features/albums/components/advanced-filters';
|
||||
import { useAlbumList } from '@/renderer/features/albums/queries/use-album-list';
|
||||
import { useServerList } from '@/renderer/features/servers';
|
||||
import { AnimatedPage, useServerCredential } from '@/renderer/features/shared';
|
||||
import { AppRoute } from '@/renderer/router/routes';
|
||||
import { useAuthStore } from '@/renderer/store';
|
||||
import { LibraryItem } from '@/renderer/types';
|
||||
import {
|
||||
ViewType,
|
||||
ViewTypeButton,
|
||||
} from '../../library/components/ViewTypeButton';
|
||||
import { useAppStore, useAuthStore } from '@/renderer/store';
|
||||
import { LibraryItem, CardDisplayType } from '@/renderer/types';
|
||||
|
||||
const FILTERS = [
|
||||
{ name: 'Title', value: AlbumSort.NAME },
|
||||
@@ -57,12 +59,27 @@ const ORDER = [
|
||||
{ name: 'Descending', value: SortOrder.DESC },
|
||||
];
|
||||
|
||||
const DEFAULT_ADVANCED_FILTERS = {
|
||||
group: [],
|
||||
rules: [
|
||||
{
|
||||
field: '',
|
||||
operator: '',
|
||||
uniqueId: nanoid(),
|
||||
value: '',
|
||||
},
|
||||
],
|
||||
type: FilterGroupType.AND,
|
||||
uniqueId: nanoid(),
|
||||
};
|
||||
|
||||
export const AlbumListRoute = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const { serverToken, isImageTokenRequired } = useServerCredential();
|
||||
const page = useAppStore((state) => state.albums);
|
||||
const setPage = useAppStore((state) => state.setPage);
|
||||
const serverId = useAuthStore((state) => state.currentServer?.id) || '';
|
||||
const { data: servers } = useServerList({ enabled: true });
|
||||
const [viewType, setViewType] = useState(ViewType.Grid);
|
||||
const [filters, setFilters] = useSetState({
|
||||
orderBy: SortOrder.ASC,
|
||||
serverFolderId: [] as string[],
|
||||
@@ -70,19 +87,23 @@ export const AlbumListRoute = () => {
|
||||
});
|
||||
|
||||
const [isAdvFilter, toggleAdvFilter] = useToggle();
|
||||
const [rawAdvFilters, setRawAdvFilters] = useState<AdvancedFilterGroup>({
|
||||
group: [],
|
||||
rules: [{ field: null, operator: null, uniqueId: nanoid(), value: null }],
|
||||
type: FilterGroupType.AND,
|
||||
uniqueId: nanoid(),
|
||||
});
|
||||
const [rawAdvFilters, setRawAdvFilters] = useState<AdvancedFilterGroup>(
|
||||
DEFAULT_ADVANCED_FILTERS
|
||||
);
|
||||
|
||||
const [debouncedAdvFilters] = useDebouncedValue(rawAdvFilters, 300);
|
||||
const [debouncedAdvFilters] = useDebouncedValue(rawAdvFilters, 500);
|
||||
|
||||
const advancedFilters = useMemo(() => {
|
||||
const value = formatAdvancedFiltersQuery(debouncedAdvFilters);
|
||||
return encodeURI(JSON.stringify(value));
|
||||
}, [debouncedAdvFilters]);
|
||||
if (!isAdvFilter) {
|
||||
return encodeAdvancedFiltersQuery(DEFAULT_ADVANCED_FILTERS);
|
||||
}
|
||||
|
||||
return encodeAdvancedFiltersQuery(debouncedAdvFilters);
|
||||
}, [debouncedAdvFilters, isAdvFilter]);
|
||||
|
||||
const handleResetAdvancedFilters = () => {
|
||||
setRawAdvFilters(DEFAULT_ADVANCED_FILTERS);
|
||||
};
|
||||
|
||||
const serverFolders = useMemo(() => {
|
||||
const server = servers?.data.find((server) => server.id === serverId);
|
||||
@@ -99,7 +120,7 @@ export const AlbumListRoute = () => {
|
||||
});
|
||||
|
||||
const fetch = useCallback(
|
||||
async ({ skip, take }) => {
|
||||
async ({ skip, take }: { skip: number; take: number }) => {
|
||||
const albums = await queryClient.fetchQuery(
|
||||
queryKeys.albums.list(serverId, {
|
||||
skip,
|
||||
@@ -138,6 +159,15 @@ export const AlbumListRoute = () => {
|
||||
]
|
||||
);
|
||||
|
||||
const setSize = throttle(
|
||||
(e: number) =>
|
||||
setPage('albums', {
|
||||
...page,
|
||||
list: { ...page.list, size: e },
|
||||
}),
|
||||
200
|
||||
);
|
||||
|
||||
return (
|
||||
<AnimatedPage>
|
||||
<VirtualGridContainer>
|
||||
@@ -159,16 +189,7 @@ export const AlbumListRoute = () => {
|
||||
{FILTERS.map((filter) => (
|
||||
<DropdownMenu.Item
|
||||
key={`filter-${filter.value}`}
|
||||
color={
|
||||
filter.value === filters.sortBy
|
||||
? 'var(--primary-color)'
|
||||
: undefined
|
||||
}
|
||||
rightSection={
|
||||
filter.value === filters.sortBy ? (
|
||||
<RiArrowLeftLine />
|
||||
) : undefined
|
||||
}
|
||||
isActive={filter.value === filters.sortBy}
|
||||
onClick={() => setFilters({ sortBy: filter.value })}
|
||||
>
|
||||
{filter.name}
|
||||
@@ -176,8 +197,7 @@ export const AlbumListRoute = () => {
|
||||
))}
|
||||
<DropdownMenu.Divider />
|
||||
<DropdownMenu.Item
|
||||
color={isAdvFilter ? 'var(--primary-color)' : undefined}
|
||||
rightSection={isAdvFilter ? <RiArrowLeftLine /> : undefined}
|
||||
isActive={isAdvFilter}
|
||||
onClick={() => toggleAdvFilter()}
|
||||
>
|
||||
Advanced Filters
|
||||
@@ -197,16 +217,7 @@ export const AlbumListRoute = () => {
|
||||
{ORDER.map((sort) => (
|
||||
<DropdownMenu.Item
|
||||
key={`sort-${sort.value}`}
|
||||
color={
|
||||
sort.value === filters.orderBy
|
||||
? 'var(--primary-color)'
|
||||
: undefined
|
||||
}
|
||||
rightSection={
|
||||
sort.value === filters.orderBy ? (
|
||||
<RiArrowLeftLine />
|
||||
) : undefined
|
||||
}
|
||||
isActive={sort.value === filters.orderBy}
|
||||
onClick={() => setFilters({ orderBy: sort.value })}
|
||||
>
|
||||
{sort.name}
|
||||
@@ -240,69 +251,181 @@ export const AlbumListRoute = () => {
|
||||
</DropdownMenu>
|
||||
</Group>
|
||||
<Group position="right">
|
||||
<ViewTypeButton
|
||||
handler={setViewType}
|
||||
menuProps={{ position: 'bottom-end' }}
|
||||
type={viewType}
|
||||
/>
|
||||
<DropdownMenu position="bottom-end" width={100}>
|
||||
<DropdownMenu.Target>
|
||||
<Button compact variant="subtle">
|
||||
<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>
|
||||
{isAdvFilter && (
|
||||
<>
|
||||
<Paper sx={{ maxHeight: '20vh' }}>
|
||||
<ScrollArea
|
||||
my={10}
|
||||
px={10}
|
||||
sx={{ height: '100%', width: '100%' }}
|
||||
<AnimatePresence
|
||||
key="album-list-advanced-filter"
|
||||
exitBeforeEnter
|
||||
initial={false}
|
||||
>
|
||||
{isAdvFilter && (
|
||||
<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">
|
||||
<Group>
|
||||
<Text>Advanced Filters</Text>
|
||||
<NumberInput
|
||||
disabled
|
||||
min={1}
|
||||
placeholder="Limit"
|
||||
size="xs"
|
||||
width={75}
|
||||
/>
|
||||
<ScrollArea sx={{ height: '100%', width: '100%' }}>
|
||||
<Group
|
||||
noWrap
|
||||
p={10}
|
||||
position="apart"
|
||||
sx={{
|
||||
background: 'var(--paper-bg)',
|
||||
position: 'sticky',
|
||||
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>
|
||||
<Button disabled uppercase>
|
||||
Save as...
|
||||
</Button>
|
||||
</Group>
|
||||
<AdvancedFilters
|
||||
filters={rawAdvFilters}
|
||||
setFilters={setRawAdvFilters}
|
||||
/>
|
||||
</ScrollArea>
|
||||
</Paper>
|
||||
</>
|
||||
)}
|
||||
<Box p={10}>
|
||||
<AdvancedFilters
|
||||
filters={rawAdvFilters}
|
||||
setFilters={setRawAdvFilters}
|
||||
/>
|
||||
</Box>
|
||||
</ScrollArea>
|
||||
</Paper>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<VirtualGridAutoSizerContainer>
|
||||
<AutoSizer>
|
||||
{({ height, width }) => (
|
||||
<VirtualInfiniteGrid
|
||||
cardRows={[
|
||||
{
|
||||
align: 'center',
|
||||
prop: 'name',
|
||||
property: 'name',
|
||||
route: {
|
||||
prop: 'id',
|
||||
route: AppRoute.LIBRARY_ALBUMS_DETAIL,
|
||||
slugs: [{ idProperty: 'id', slugProperty: 'albumId' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
align: 'center',
|
||||
prop: 'releaseYear',
|
||||
arrayProperty: 'name',
|
||||
property: 'albumArtists',
|
||||
route: {
|
||||
route: AppRoute.LIBRARY_ALBUMARTISTS_DETAIL,
|
||||
slugs: [
|
||||
{ idProperty: 'id', slugProperty: 'albumArtistId' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
property: 'releaseYear',
|
||||
},
|
||||
]}
|
||||
display={page.list?.display || CardDisplayType.CARD}
|
||||
fetchFn={fetch}
|
||||
height={height}
|
||||
itemCount={albums?.pagination.totalEntries || 0}
|
||||
itemGap={20}
|
||||
itemSize={200}
|
||||
itemSize={150 + page.list?.size}
|
||||
itemType={LibraryItem.ALBUM}
|
||||
minimumBatchSize={40}
|
||||
refresh={advancedFilters}
|
||||
route={{
|
||||
route: AppRoute.LIBRARY_ALBUMS_DETAIL,
|
||||
slugs: [{ idProperty: 'id', slugProperty: 'albumId' }],
|
||||
}}
|
||||
width={width}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import create from 'zustand';
|
||||
import { devtools, persist } from 'zustand/middleware';
|
||||
import { immer } from 'zustand/middleware/immer';
|
||||
import { Platform } from '@/renderer/types';
|
||||
import { CardDisplayType, Platform } from '@/renderer/types';
|
||||
|
||||
type SidebarProps = {
|
||||
expanded: string[];
|
||||
@@ -11,7 +11,18 @@ type SidebarProps = {
|
||||
rightWidth: string;
|
||||
};
|
||||
|
||||
type LibraryPageProps = {
|
||||
list: ListProps;
|
||||
};
|
||||
|
||||
type ListProps = {
|
||||
display: CardDisplayType;
|
||||
size: number;
|
||||
type: 'list' | 'grid';
|
||||
};
|
||||
|
||||
export interface AppState {
|
||||
albums: LibraryPageProps;
|
||||
platform: Platform;
|
||||
sidebar: {
|
||||
expanded: string[];
|
||||
@@ -24,6 +35,7 @@ export interface AppState {
|
||||
|
||||
export interface AppSlice extends AppState {
|
||||
setAppStore: (data: Partial<AppSlice>) => void;
|
||||
setPage: (page: 'albums', options: Partial<LibraryPageProps>) => void;
|
||||
setSidebar: (options: Partial<SidebarProps>) => void;
|
||||
}
|
||||
|
||||
@@ -31,10 +43,22 @@ export const useAppStore = create<AppSlice>()(
|
||||
persist(
|
||||
devtools(
|
||||
immer((set, get) => ({
|
||||
albums: {
|
||||
list: {
|
||||
display: CardDisplayType.CARD,
|
||||
size: 50,
|
||||
type: 'list',
|
||||
},
|
||||
},
|
||||
platform: Platform.WINDOWS,
|
||||
setAppStore: (data) => {
|
||||
set({ ...get(), ...data });
|
||||
},
|
||||
setPage: (page: 'albums', data: any) => {
|
||||
set((state) => {
|
||||
state[page] = { ...state[page], ...data };
|
||||
});
|
||||
},
|
||||
setSidebar: (options) => {
|
||||
set((state) => {
|
||||
state.sidebar = { ...state.sidebar, ...options };
|
||||
|
||||
Reference in New Issue
Block a user