mirror of
https://github.com/jeffvli/feishin.git
synced 2026-06-16 16:34:24 +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 }}
|
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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
Reference in New Issue
Block a user