mirror of
https://github.com/jeffvli/feishin.git
synced 2024-11-20 14:37:06 +01:00
Auto scale grid items (#30)
This commit is contained in:
parent
69292a083d
commit
3153cdd6c4
@ -1,37 +1,56 @@
|
|||||||
import { Center } from '@mantine/core';
|
import { generatePath, Link } from 'react-router-dom';
|
||||||
import { RiAlbumFill } from 'react-icons/ri';
|
import { ListChildComponentProps } from 'react-window';
|
||||||
import { generatePath, useNavigate } from 'react-router';
|
|
||||||
import type { ListChildComponentProps } from 'react-window';
|
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import type { CardRow, CardRoute, Play, PlayQueueAddOptions } from '/@/renderer/types';
|
|
||||||
import { Skeleton } from '/@/renderer/components/skeleton';
|
|
||||||
import { GridCardControls } from '/@/renderer/components/virtual-grid/grid-card/grid-card-controls';
|
|
||||||
import { Album, AlbumArtist, Artist, LibraryItem } from '/@/renderer/api/types';
|
import { Album, AlbumArtist, Artist, LibraryItem } from '/@/renderer/api/types';
|
||||||
import { CardRows } from '/@/renderer/components/card';
|
import { CardRows } from '/@/renderer/components/card';
|
||||||
|
import { Skeleton } from '/@/renderer/components/skeleton';
|
||||||
|
import { GridCardControls } from '/@/renderer/components/virtual-grid/grid-card/grid-card-controls';
|
||||||
|
import { CardRow, PlayQueueAddOptions, Play, CardRoute } from '/@/renderer/types';
|
||||||
|
|
||||||
const CardWrapper = styled.div<{
|
interface BaseGridCardProps {
|
||||||
itemGap: number;
|
columnIndex: number;
|
||||||
itemHeight: number;
|
controls: {
|
||||||
itemWidth: number;
|
cardRows: CardRow<Album | AlbumArtist | Artist>[];
|
||||||
link?: boolean;
|
handleFavorite: (options: { id: string[]; isFavorite: boolean; itemType: LibraryItem }) => void;
|
||||||
}>`
|
handlePlayQueueAdd: (options: PlayQueueAddOptions) => void;
|
||||||
flex: ${({ itemWidth }) => `0 0 ${itemWidth - 12}px`};
|
itemType: LibraryItem;
|
||||||
width: ${({ itemWidth }) => `${itemWidth}px`};
|
playButtonBehavior: Play;
|
||||||
height: ${({ itemHeight, itemGap }) => `${itemHeight - 12 - itemGap}px`};
|
route: CardRoute;
|
||||||
margin: ${({ itemGap }) => `0 ${itemGap / 2}px`};
|
};
|
||||||
padding: 12px 12px 0;
|
data: any;
|
||||||
|
isHidden?: boolean;
|
||||||
|
listChildProps: Omit<ListChildComponentProps, 'data' | 'style'>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DefaultCardContainer = styled.div<{ $isHidden?: boolean }>`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100% - 2rem);
|
||||||
|
margin: 1rem;
|
||||||
|
overflow: hidden;
|
||||||
background: var(--card-default-bg);
|
background: var(--card-default-bg);
|
||||||
border-radius: var(--card-default-radius);
|
border-radius: var(--card-default-radius);
|
||||||
cursor: ${({ link }) => link && 'pointer'};
|
opacity: ${({ $isHidden }) => ($isHidden ? 0 : 1)};
|
||||||
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 {
|
&:hover {
|
||||||
background: var(--card-default-bg-hover);
|
background: var(--card-default-bg-hover);
|
||||||
}
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
&:hover div {
|
const InnerCardContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 1rem;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.card-controls {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .card-controls {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,26 +59,16 @@ const CardWrapper = styled.div<{
|
|||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus-visible {
|
|
||||||
outline: 1px solid #fff;
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledCard = styled.div`
|
const ImageContainer = 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;
|
position: relative;
|
||||||
width: ${({ size }) => size && `${size - 24}px`};
|
display: flex;
|
||||||
height: ${({ size }) => size && `${size - 24}px`};
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
aspect-ratio: 1/1;
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--placeholder-bg);
|
||||||
border-radius: var(--card-default-radius);
|
border-radius: var(--card-default-radius);
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
@ -78,151 +87,77 @@ const ImageSection = styled.div<{ size?: number }>`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const Image = styled.img`
|
const Image = styled.img`
|
||||||
object-fit: cover;
|
|
||||||
border-radius: var(--card-default-radius);
|
|
||||||
box-shadow: 2px 2px 10px 2px rgba(0, 0, 0, 20%);
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ControlsContainer = styled.div`
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
z-index: 50;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
opacity: 0;
|
max-width: 100%;
|
||||||
transition: all 0.2s ease-in-out;
|
height: auto;
|
||||||
|
object-fit: contain;
|
||||||
|
border: 0;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const DetailSection = styled.div`
|
const DetailContainer = styled.div`
|
||||||
display: flex;
|
margin-top: 0.5rem;
|
||||||
flex-direction: column;
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
interface BaseGridCardProps {
|
|
||||||
columnIndex: number;
|
|
||||||
controls: {
|
|
||||||
cardRows: CardRow<Album | AlbumArtist | Artist>[];
|
|
||||||
handleFavorite: (options: { id: string[]; isFavorite: boolean; itemType: LibraryItem }) => void;
|
|
||||||
handlePlayQueueAdd: (options: PlayQueueAddOptions) => void;
|
|
||||||
itemType: LibraryItem;
|
|
||||||
playButtonBehavior: Play;
|
|
||||||
route: CardRoute;
|
|
||||||
};
|
|
||||||
data: any;
|
|
||||||
listChildProps: Omit<ListChildComponentProps, 'data' | 'style'>;
|
|
||||||
sizes: {
|
|
||||||
itemGap: number;
|
|
||||||
itemHeight: number;
|
|
||||||
itemWidth: number;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DefaultCard = ({
|
export const DefaultCard = ({
|
||||||
listChildProps,
|
listChildProps,
|
||||||
data,
|
data,
|
||||||
columnIndex,
|
columnIndex,
|
||||||
controls,
|
controls,
|
||||||
sizes,
|
isHidden,
|
||||||
}: BaseGridCardProps) => {
|
}: BaseGridCardProps) => {
|
||||||
const navigate = useNavigate();
|
|
||||||
const { index } = listChildProps;
|
|
||||||
const { itemGap, itemHeight, itemWidth } = sizes;
|
|
||||||
const { itemType, cardRows, route, handlePlayQueueAdd } = controls;
|
|
||||||
|
|
||||||
const cardSize = itemWidth - 24;
|
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
return (
|
return (
|
||||||
<CardWrapper
|
<DefaultCardContainer>
|
||||||
key={`card-${columnIndex}-${index}`}
|
<Link
|
||||||
link
|
tabIndex={0}
|
||||||
itemGap={itemGap}
|
to={generatePath(
|
||||||
itemHeight={itemHeight}
|
controls.route.route,
|
||||||
itemWidth={itemWidth}
|
controls.route.slugs?.reduce((acc, slug) => {
|
||||||
onClick={() =>
|
return {
|
||||||
navigate(
|
...acc,
|
||||||
generatePath(
|
[slug.slugProperty]: data[slug.idProperty],
|
||||||
route.route,
|
};
|
||||||
route.slugs?.reduce((acc, slug) => {
|
}, {}),
|
||||||
return {
|
)}
|
||||||
...acc,
|
>
|
||||||
[slug.slugProperty]: data[slug.idProperty],
|
<InnerCardContainer key={`card-${columnIndex}-${listChildProps.index}`}>
|
||||||
};
|
<ImageContainer>
|
||||||
}, {}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<StyledCard>
|
|
||||||
<ImageSection size={itemWidth}>
|
|
||||||
{data?.imageUrl ? (
|
|
||||||
<Image
|
<Image
|
||||||
height={cardSize}
|
placeholder={data?.imagePlaceholderUrl || 'var(--placeholder-bg)'}
|
||||||
placeholder={data?.imagePlaceholderUrl || 'var(--card-default-bg)'}
|
|
||||||
src={data?.imageUrl}
|
src={data?.imageUrl}
|
||||||
width={cardSize}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
|
||||||
<Center
|
|
||||||
sx={{
|
|
||||||
background: 'var(--placeholder-bg)',
|
|
||||||
borderRadius: 'var(--card-default-radius)',
|
|
||||||
height: '100%',
|
|
||||||
width: '100%',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<RiAlbumFill
|
|
||||||
color="var(--placeholder-fg)"
|
|
||||||
size={35}
|
|
||||||
/>
|
|
||||||
</Center>
|
|
||||||
)}
|
|
||||||
<ControlsContainer>
|
|
||||||
<GridCardControls
|
<GridCardControls
|
||||||
handleFavorite={controls.handleFavorite}
|
handleFavorite={controls.handleFavorite}
|
||||||
handlePlayQueueAdd={handlePlayQueueAdd}
|
handlePlayQueueAdd={controls.handlePlayQueueAdd}
|
||||||
itemData={data}
|
itemData={data}
|
||||||
itemType={itemType}
|
itemType={controls.itemType}
|
||||||
/>
|
/>
|
||||||
</ControlsContainer>
|
</ImageContainer>
|
||||||
</ImageSection>
|
<DetailContainer>
|
||||||
<DetailSection>
|
<CardRows
|
||||||
<CardRows
|
data={data}
|
||||||
data={data}
|
rows={controls.cardRows}
|
||||||
rows={cardRows}
|
/>
|
||||||
/>
|
</DetailContainer>
|
||||||
</DetailSection>
|
</InnerCardContainer>
|
||||||
</StyledCard>
|
</Link>
|
||||||
</CardWrapper>
|
</DefaultCardContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CardWrapper
|
<DefaultCardContainer
|
||||||
key={`card-${columnIndex}-${index}`}
|
key={`card-${columnIndex}-${listChildProps.index}`}
|
||||||
itemGap={itemGap}
|
$isHidden={isHidden}
|
||||||
itemHeight={itemHeight}
|
|
||||||
itemWidth={itemWidth + 12}
|
|
||||||
>
|
>
|
||||||
<StyledCard>
|
<InnerCardContainer>
|
||||||
<Skeleton
|
<ImageContainer>
|
||||||
visible
|
<Skeleton
|
||||||
radius="sm"
|
visible
|
||||||
>
|
radius="sm"
|
||||||
<ImageSection size={itemWidth} />
|
/>
|
||||||
</Skeleton>
|
</ImageContainer>
|
||||||
<DetailSection>
|
</InnerCardContainer>
|
||||||
{cardRows.map((row: CardRow<Album | Artist | AlbumArtist>, index: number) => (
|
</DefaultCardContainer>
|
||||||
<Skeleton
|
|
||||||
key={`row-${row.property}-${columnIndex}`}
|
|
||||||
height={20}
|
|
||||||
my={2}
|
|
||||||
radius="md"
|
|
||||||
visible={!data}
|
|
||||||
width={!data ? (index > 0 ? '50%' : '90%') : '100%'}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</DetailSection>
|
|
||||||
</StyledCard>
|
|
||||||
</CardWrapper>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import type { MouseEvent } from 'react';
|
import type { MouseEvent } from 'react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import type { UnstyledButtonProps } from '@mantine/core';
|
import type { UnstyledButtonProps } from '@mantine/core';
|
||||||
import { Group } from '@mantine/core';
|
import { RiPlayFill, RiHeartFill, RiHeartLine, RiMoreFill } from 'react-icons/ri';
|
||||||
import { RiPlayFill, RiMore2Fill, RiHeartFill, RiHeartLine } from 'react-icons/ri';
|
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { _Button } from '/@/renderer/components/button';
|
import { _Button } from '/@/renderer/components/button';
|
||||||
import type { PlayQueueAddOptions } from '/@/renderer/types';
|
import type { PlayQueueAddOptions } from '/@/renderer/types';
|
||||||
@ -18,6 +17,7 @@ import {
|
|||||||
type PlayButtonType = UnstyledButtonProps & React.ComponentPropsWithoutRef<'button'>;
|
type PlayButtonType = UnstyledButtonProps & React.ComponentPropsWithoutRef<'button'>;
|
||||||
|
|
||||||
const PlayButton = styled.button<PlayButtonType>`
|
const PlayButton = styled.button<PlayButtonType>`
|
||||||
|
position: absolute;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@ -28,7 +28,7 @@ const PlayButton = styled.button<PlayButtonType>`
|
|||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
transition: opacity 0.2s ease-in-out;
|
transition: opacity 0.2s ease-in-out;
|
||||||
transition: scale 0.2s linear;
|
transition: scale 0.1s ease-in-out;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
@ -63,6 +63,8 @@ const SecondaryButton = styled(_Button)`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const GridCardControlsContainer = styled.div`
|
const GridCardControlsContainer = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
z-index: 100;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -76,24 +78,13 @@ const ControlsRow = styled.div`
|
|||||||
height: calc(100% / 3);
|
height: calc(100% / 3);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// const TopControls = styled(ControlsRow)`
|
|
||||||
// display: flex;
|
|
||||||
// align-items: flex-start;
|
|
||||||
// justify-content: space-between;
|
|
||||||
// padding: 0.5rem;
|
|
||||||
// `;
|
|
||||||
|
|
||||||
// const CenterControls = styled(ControlsRow)`
|
|
||||||
// display: flex;
|
|
||||||
// align-items: center;
|
|
||||||
// justify-content: center;
|
|
||||||
// padding: 0.5rem;
|
|
||||||
// `;
|
|
||||||
|
|
||||||
const BottomControls = styled(ControlsRow)`
|
const BottomControls = styled(ControlsRow)`
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
justify-content: space-between;
|
justify-content: flex-end;
|
||||||
padding: 1rem 0.5rem;
|
padding: 1rem 0.5rem;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -146,45 +137,43 @@ export const GridCardControls = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GridCardControlsContainer>
|
<GridCardControlsContainer className="card-controls">
|
||||||
|
<PlayButton onClick={handlePlay}>
|
||||||
|
<RiPlayFill size={25} />
|
||||||
|
</PlayButton>
|
||||||
<BottomControls>
|
<BottomControls>
|
||||||
<PlayButton onClick={handlePlay}>
|
<SecondaryButton
|
||||||
<RiPlayFill size={25} />
|
p={5}
|
||||||
</PlayButton>
|
sx={{ svg: { fill: 'white !important' } }}
|
||||||
<Group spacing="xs">
|
variant="subtle"
|
||||||
<SecondaryButton
|
onClick={handleFavorites}
|
||||||
p={5}
|
>
|
||||||
sx={{ svg: { fill: 'white !important' } }}
|
<FavoriteWrapper isFavorite={itemData?.isFavorite}>
|
||||||
variant="subtle"
|
{itemData?.userFavorite ? (
|
||||||
onClick={handleFavorites}
|
<RiHeartFill size={20} />
|
||||||
>
|
) : (
|
||||||
<FavoriteWrapper isFavorite={itemData?.isFavorite}>
|
<RiHeartLine
|
||||||
{itemData?.userFavorite ? (
|
color="white"
|
||||||
<RiHeartFill size={20} />
|
size={20}
|
||||||
) : (
|
/>
|
||||||
<RiHeartLine
|
)}
|
||||||
color="white"
|
</FavoriteWrapper>
|
||||||
size={20}
|
</SecondaryButton>
|
||||||
/>
|
<SecondaryButton
|
||||||
)}
|
p={5}
|
||||||
</FavoriteWrapper>
|
sx={{ svg: { fill: 'white !important' } }}
|
||||||
</SecondaryButton>
|
variant="subtle"
|
||||||
<SecondaryButton
|
onClick={(e) => {
|
||||||
p={5}
|
e.preventDefault();
|
||||||
sx={{ svg: { fill: 'white !important' } }}
|
e.stopPropagation();
|
||||||
variant="subtle"
|
handleContextMenu(e, [itemData]);
|
||||||
onClick={(e) => {
|
}}
|
||||||
e.preventDefault();
|
>
|
||||||
e.stopPropagation();
|
<RiMoreFill
|
||||||
handleContextMenu(e, [itemData]);
|
color="white"
|
||||||
}}
|
size={20}
|
||||||
>
|
/>
|
||||||
<RiMore2Fill
|
</SecondaryButton>
|
||||||
color="white"
|
|
||||||
size={20}
|
|
||||||
/>
|
|
||||||
</SecondaryButton>
|
|
||||||
</Group>
|
|
||||||
</BottomControls>
|
</BottomControls>
|
||||||
</GridCardControlsContainer>
|
</GridCardControlsContainer>
|
||||||
);
|
);
|
||||||
|
@ -3,15 +3,11 @@ import type { ListChildComponentProps } from 'react-window';
|
|||||||
import { areEqual } from 'react-window';
|
import { areEqual } from 'react-window';
|
||||||
import { DefaultCard } from '/@/renderer/components/virtual-grid/grid-card/default-card';
|
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 type { GridCardData } from '/@/renderer/types';
|
import { GridCardData, ListDisplayType } from '/@/renderer/types';
|
||||||
import { ListDisplayType } from '/@/renderer/types';
|
|
||||||
|
|
||||||
export const GridCard = memo(({ data, index, style }: ListChildComponentProps) => {
|
export const GridCard = memo(({ data, index, style }: ListChildComponentProps) => {
|
||||||
const {
|
const {
|
||||||
itemHeight,
|
|
||||||
itemWidth,
|
|
||||||
columnCount,
|
columnCount,
|
||||||
itemGap,
|
|
||||||
itemCount,
|
itemCount,
|
||||||
cardRows,
|
cardRows,
|
||||||
itemData,
|
itemData,
|
||||||
@ -27,9 +23,14 @@ export const GridCard = memo(({ data, index, style }: ListChildComponentProps) =
|
|||||||
const startIndex = index * columnCount;
|
const startIndex = index * columnCount;
|
||||||
const stopIndex = Math.min(itemCount - 1, startIndex + columnCount - 1);
|
const stopIndex = Math.min(itemCount - 1, startIndex + columnCount - 1);
|
||||||
|
|
||||||
|
const columnCountInRow = stopIndex - startIndex + 1;
|
||||||
|
let columnCountToAdd = 0;
|
||||||
|
if (columnCountInRow !== columnCount) {
|
||||||
|
columnCountToAdd = columnCount - columnCountInRow;
|
||||||
|
}
|
||||||
const View = display === ListDisplayType.CARD ? DefaultCard : PosterCard;
|
const View = display === ListDisplayType.CARD ? DefaultCard : PosterCard;
|
||||||
|
|
||||||
for (let i = startIndex; i <= stopIndex; i += 1) {
|
for (let i = startIndex; i <= stopIndex + columnCountToAdd; i += 1) {
|
||||||
cards.push(
|
cards.push(
|
||||||
<View
|
<View
|
||||||
key={`card-${i}-${index}`}
|
key={`card-${i}-${index}`}
|
||||||
@ -43,24 +44,20 @@ export const GridCard = memo(({ data, index, style }: ListChildComponentProps) =
|
|||||||
route,
|
route,
|
||||||
}}
|
}}
|
||||||
data={itemData[i]}
|
data={itemData[i]}
|
||||||
|
isHidden={i > stopIndex}
|
||||||
listChildProps={{ index }}
|
listChildProps={{ index }}
|
||||||
sizes={{ itemGap, itemHeight, itemWidth }}
|
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div
|
||||||
<div
|
style={{
|
||||||
style={{
|
...style,
|
||||||
...style,
|
display: 'flex',
|
||||||
alignItems: 'center',
|
}}
|
||||||
display: 'flex',
|
>
|
||||||
justifyContent: 'start',
|
{cards}
|
||||||
}}
|
</div>
|
||||||
>
|
|
||||||
{cards}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}, areEqual);
|
}, areEqual);
|
||||||
|
@ -1,28 +1,41 @@
|
|||||||
import { Center } from '@mantine/core';
|
import { generatePath, Link } from 'react-router-dom';
|
||||||
import { RiAlbumFill } from 'react-icons/ri';
|
import { ListChildComponentProps } from 'react-window';
|
||||||
import { generatePath } from 'react-router';
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
import type { ListChildComponentProps } from 'react-window';
|
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { Skeleton } from '/@/renderer/components/skeleton';
|
import { Album, AlbumArtist, Artist, LibraryItem } from '/@/renderer/api/types';
|
||||||
import type { CardRow, CardRoute, Play, PlayQueueAddOptions } from '/@/renderer/types';
|
|
||||||
import { GridCardControls } from '/@/renderer/components/virtual-grid/grid-card/grid-card-controls';
|
|
||||||
import { Album, Artist, AlbumArtist, LibraryItem } from '/@/renderer/api/types';
|
|
||||||
import { CardRows } from '/@/renderer/components/card';
|
import { CardRows } from '/@/renderer/components/card';
|
||||||
|
import { Skeleton } from '/@/renderer/components/skeleton';
|
||||||
|
import { GridCardControls } from '/@/renderer/components/virtual-grid/grid-card/grid-card-controls';
|
||||||
|
import { CardRow, PlayQueueAddOptions, Play, CardRoute } from '/@/renderer/types';
|
||||||
|
|
||||||
const CardWrapper = styled.div<{
|
interface BaseGridCardProps {
|
||||||
itemGap: number;
|
columnIndex: number;
|
||||||
itemHeight: number;
|
controls: {
|
||||||
itemWidth: number;
|
cardRows: CardRow<Album | AlbumArtist | Artist>[];
|
||||||
}>`
|
handleFavorite: (options: { id: string[]; isFavorite: boolean; itemType: LibraryItem }) => void;
|
||||||
flex: ${({ itemWidth }) => `0 0 ${itemWidth}px`};
|
handlePlayQueueAdd: (options: PlayQueueAddOptions) => void;
|
||||||
width: ${({ itemWidth }) => `${itemWidth}px`};
|
itemType: LibraryItem;
|
||||||
height: ${({ itemHeight, itemGap }) => `${itemHeight - itemGap}px`};
|
playButtonBehavior: Play;
|
||||||
margin: ${({ itemGap }) => `0 ${itemGap / 2}px`};
|
route: CardRoute;
|
||||||
user-select: none;
|
};
|
||||||
pointer-events: auto; // https://github.com/bvaughn/react-window/issues/128#issuecomment-460166682
|
data: any;
|
||||||
|
isHidden?: boolean;
|
||||||
|
listChildProps: Omit<ListChildComponentProps, 'data' | 'style'>;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover div {
|
const PosterCardContainer = styled.div<{ $isHidden?: boolean }>`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 1rem;
|
||||||
|
overflow: hidden;
|
||||||
|
opacity: ${({ $isHidden }) => ($isHidden ? 0 : 1)};
|
||||||
|
|
||||||
|
.card-controls {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .card-controls {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -31,30 +44,16 @@ const CardWrapper = styled.div<{
|
|||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus-visible {
|
|
||||||
outline: 1px solid #fff;
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledCard = styled.div`
|
const ImageContainer = styled.div`
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.5rem;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
padding: 0;
|
|
||||||
background: var(--card-poster-bg);
|
|
||||||
border-radius: var(--card-poster-radius);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: var(--card-poster-bg-hover);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ImageSection = styled.div`
|
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
aspect-ratio: 1/1;
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--card-default-bg);
|
||||||
border-radius: var(--card-poster-radius);
|
border-radius: var(--card-poster-radius);
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
@ -72,148 +71,74 @@ const ImageSection = styled.div`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
interface ImageProps {
|
const Image = styled.img`
|
||||||
height: number;
|
width: 100%;
|
||||||
isLoading?: boolean;
|
max-width: 100%;
|
||||||
}
|
height: auto;
|
||||||
|
|
||||||
const Image = styled.img<ImageProps>`
|
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: var(--card-poster-radius);
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ControlsContainer = styled.div`
|
const DetailContainer = styled.div`
|
||||||
position: absolute;
|
margin-top: 0.5rem;
|
||||||
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;
|
|
||||||
`;
|
|
||||||
|
|
||||||
interface BaseGridCardProps {
|
|
||||||
columnIndex: number;
|
|
||||||
controls: {
|
|
||||||
cardRows: CardRow<Album | AlbumArtist | Artist>[];
|
|
||||||
handleFavorite: (options: { id: string[]; isFavorite: boolean; itemType: LibraryItem }) => void;
|
|
||||||
handlePlayQueueAdd: (options: PlayQueueAddOptions) => void;
|
|
||||||
itemType: LibraryItem;
|
|
||||||
playButtonBehavior: Play;
|
|
||||||
route: CardRoute;
|
|
||||||
};
|
|
||||||
data: any;
|
|
||||||
listChildProps: Omit<ListChildComponentProps, 'data' | 'style'>;
|
|
||||||
sizes: {
|
|
||||||
itemGap: number;
|
|
||||||
itemHeight: number;
|
|
||||||
itemWidth: number;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PosterCard = ({
|
export const PosterCard = ({
|
||||||
listChildProps,
|
listChildProps,
|
||||||
data,
|
data,
|
||||||
columnIndex,
|
columnIndex,
|
||||||
controls,
|
controls,
|
||||||
sizes,
|
isHidden,
|
||||||
}: BaseGridCardProps) => {
|
}: BaseGridCardProps) => {
|
||||||
if (data) {
|
if (data) {
|
||||||
return (
|
return (
|
||||||
<CardWrapper
|
<PosterCardContainer key={`card-${columnIndex}-${listChildProps.index}`}>
|
||||||
key={`card-${columnIndex}-${listChildProps.index}`}
|
<Link
|
||||||
itemGap={sizes.itemGap}
|
tabIndex={0}
|
||||||
itemHeight={sizes.itemHeight}
|
to={generatePath(
|
||||||
itemWidth={sizes.itemWidth}
|
controls.route.route,
|
||||||
>
|
controls.route.slugs?.reduce((acc, slug) => {
|
||||||
<StyledCard>
|
return {
|
||||||
<Link
|
...acc,
|
||||||
tabIndex={0}
|
[slug.slugProperty]: data[slug.idProperty],
|
||||||
to={generatePath(
|
};
|
||||||
controls.route.route,
|
}, {}),
|
||||||
controls.route.slugs?.reduce((acc, slug) => {
|
)}
|
||||||
return {
|
>
|
||||||
...acc,
|
<ImageContainer>
|
||||||
[slug.slugProperty]: data[slug.idProperty],
|
<Image
|
||||||
};
|
placeholder={data?.imagePlaceholderUrl || 'var(--card-default-bg)'}
|
||||||
}, {}),
|
src={data?.imageUrl}
|
||||||
)}
|
|
||||||
>
|
|
||||||
<ImageSection style={{ height: `${sizes.itemWidth}px` }}>
|
|
||||||
{data?.imageUrl ? (
|
|
||||||
<Image
|
|
||||||
height={sizes.itemWidth}
|
|
||||||
placeholder={data?.imagePlaceholderUrl || 'var(--card-default-bg)'}
|
|
||||||
src={data?.imageUrl}
|
|
||||||
width={sizes.itemWidth}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Center
|
|
||||||
sx={{
|
|
||||||
background: 'var(--placeholder-bg)',
|
|
||||||
borderRadius: 'var(--card-poster-radius)',
|
|
||||||
height: '100%',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<RiAlbumFill
|
|
||||||
color="var(--placeholder-fg)"
|
|
||||||
size={35}
|
|
||||||
/>
|
|
||||||
</Center>
|
|
||||||
)}
|
|
||||||
<ControlsContainer>
|
|
||||||
<GridCardControls
|
|
||||||
handleFavorite={controls.handleFavorite}
|
|
||||||
handlePlayQueueAdd={controls.handlePlayQueueAdd}
|
|
||||||
itemData={data}
|
|
||||||
itemType={controls.itemType}
|
|
||||||
/>
|
|
||||||
</ControlsContainer>
|
|
||||||
</ImageSection>
|
|
||||||
</Link>
|
|
||||||
<DetailSection>
|
|
||||||
<CardRows
|
|
||||||
data={data}
|
|
||||||
rows={controls.cardRows}
|
|
||||||
/>
|
/>
|
||||||
</DetailSection>
|
<GridCardControls
|
||||||
</StyledCard>
|
handleFavorite={controls.handleFavorite}
|
||||||
</CardWrapper>
|
handlePlayQueueAdd={controls.handlePlayQueueAdd}
|
||||||
|
itemData={data}
|
||||||
|
itemType={controls.itemType}
|
||||||
|
/>
|
||||||
|
</ImageContainer>
|
||||||
|
</Link>
|
||||||
|
<DetailContainer>
|
||||||
|
<CardRows
|
||||||
|
data={data}
|
||||||
|
rows={controls.cardRows}
|
||||||
|
/>
|
||||||
|
</DetailContainer>
|
||||||
|
</PosterCardContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CardWrapper
|
<PosterCardContainer
|
||||||
key={`card-${columnIndex}-${listChildProps.index}`}
|
key={`card-${columnIndex}-${listChildProps.index}`}
|
||||||
itemGap={sizes.itemGap}
|
$isHidden={isHidden}
|
||||||
itemHeight={sizes.itemHeight}
|
|
||||||
itemWidth={sizes.itemWidth}
|
|
||||||
>
|
>
|
||||||
<StyledCard>
|
<ImageContainer>
|
||||||
<Skeleton
|
<Skeleton
|
||||||
visible
|
visible
|
||||||
radius="sm"
|
radius="sm"
|
||||||
>
|
/>
|
||||||
<ImageSection style={{ height: `${sizes.itemWidth}px` }} />
|
</ImageContainer>
|
||||||
</Skeleton>
|
</PosterCardContainer>
|
||||||
<DetailSection>
|
|
||||||
{controls.cardRows.map((row: CardRow<Album | Artist | AlbumArtist>, index: number) => (
|
|
||||||
<Skeleton
|
|
||||||
key={`row-${row.property}-${columnIndex}`}
|
|
||||||
height={20}
|
|
||||||
my={2}
|
|
||||||
radius="md"
|
|
||||||
visible={!data}
|
|
||||||
width={!data ? (index > 0 ? '50%' : '90%') : '100%'}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</DetailSection>
|
|
||||||
</StyledCard>
|
|
||||||
</CardWrapper>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -29,14 +29,6 @@ interface VirtualGridProps extends Omit<FixedSizeListProps, 'children' | 'itemSi
|
|||||||
setItemData: (data: any[]) => void;
|
setItemData: (data: any[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// const constrainWidth = (width: number) => {
|
|
||||||
// if (width < 1920) {
|
|
||||||
// return width;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return 1920;
|
|
||||||
// };
|
|
||||||
|
|
||||||
export const VirtualInfiniteGrid = forwardRef(
|
export const VirtualInfiniteGrid = forwardRef(
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
@ -64,16 +56,20 @@ export const VirtualInfiniteGrid = forwardRef(
|
|||||||
const listRef = useRef<any>(null);
|
const listRef = useRef<any>(null);
|
||||||
const loader = useRef<InfiniteLoader>(null);
|
const loader = useRef<InfiniteLoader>(null);
|
||||||
|
|
||||||
|
const sz = itemSize / 2;
|
||||||
|
|
||||||
const { itemHeight, rowCount, columnCount } = useMemo(() => {
|
const { itemHeight, rowCount, columnCount } = useMemo(() => {
|
||||||
const itemsPerRow = Math.floor((Number(width) - itemGap + 3) / (itemSize! + itemGap + 2));
|
const itemsPerRow = Math.floor(Number(width) / sz!) - 1;
|
||||||
|
const widthPerItem = Number(width) / itemsPerRow - 10;
|
||||||
|
const itemHeight = widthPerItem + cardRows.length * 26;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
columnCount: itemsPerRow,
|
columnCount: itemsPerRow,
|
||||||
itemHeight: itemSize! + cardRows.length * 22 + itemGap,
|
itemHeight,
|
||||||
itemWidth: itemSize! + itemGap,
|
itemWidth: sz,
|
||||||
rowCount: Math.ceil(itemCount / itemsPerRow),
|
rowCount: Math.ceil(itemCount / itemsPerRow),
|
||||||
};
|
};
|
||||||
}, [cardRows.length, itemCount, itemGap, itemSize, width]);
|
}, [cardRows.length, itemCount, sz, width]);
|
||||||
|
|
||||||
const isItemLoaded = useCallback(
|
const isItemLoaded = useCallback(
|
||||||
(index: number) => {
|
(index: number) => {
|
||||||
@ -153,7 +149,7 @@ export const VirtualInfiniteGrid = forwardRef(
|
|||||||
itemCount={itemCount || 0}
|
itemCount={itemCount || 0}
|
||||||
itemData={itemData}
|
itemData={itemData}
|
||||||
itemGap={itemGap}
|
itemGap={itemGap}
|
||||||
itemHeight={itemHeight + itemGap / 2}
|
itemHeight={itemHeight}
|
||||||
itemType={itemType}
|
itemType={itemType}
|
||||||
itemWidth={itemSize}
|
itemWidth={itemSize}
|
||||||
refInstance={(list) => {
|
refInstance={(list) => {
|
||||||
|
Loading…
Reference in New Issue
Block a user