Album list updates

This commit is contained in:
jeffvli
2022-11-06 00:08:22 -07:00
parent 07123615ca
commit 633c6416df
9 changed files with 710 additions and 99 deletions
@@ -0,0 +1,451 @@
import styled from '@emotion/styled';
import { Center, Skeleton } from '@mantine/core';
import { RiAlbumFill } from 'react-icons/ri';
import { generatePath, useNavigate } from 'react-router';
import { Link } from 'react-router-dom';
import { ListChildComponentProps } from 'react-window';
import { Text } from '@/renderer/components/text';
import { GridCardControls } from '@/renderer/components/virtual-grid/grid-card/grid-card-controls';
import { fadeIn } from '@/renderer/styles';
import {
PlayQueueAddOptions,
LibraryItem,
CardRow,
CardRoute,
} from '@/renderer/types';
const CardWrapper = styled.div<{
itemGap: number;
itemHeight: number;
itemWidth: number;
link?: boolean;
}>`
flex: ${({ itemWidth }) => `0 0 ${itemWidth - 12}px`};
width: ${({ itemWidth }) => `${itemWidth}px`};
height: ${({ itemHeight, itemGap }) => `${itemHeight - 12 - itemGap}px`};
margin: ${({ itemGap }) => `0 ${itemGap / 2}px`};
padding: 12px 12px 0;
background: var(--card-default-bg);
border-radius: var(--card-default-radius);
cursor: ${({ link }) => link && 'pointer'};
transition: border 0.2s ease-in-out, background 0.2s ease-in-out;
user-select: none;
pointer-events: auto; // https://github.com/bvaughn/react-window/issues/128#issuecomment-460166682
&:hover {
background: var(--card-default-bg-hover);
}
&:hover div {
opacity: 1;
}
&:hover * {
&::before {
opacity: 0.5;
}
}
&:focus-visible {
outline: 1px solid #fff;
}
`;
const StyledCard = styled.div`
display: flex;
flex-direction: column;
gap: 0.5rem;
width: 100%;
height: 100%;
padding: 0;
border-radius: var(--card-default-radius);
`;
const ImageSection = styled.div<{ size?: number }>`
position: relative;
width: ${({ size }) => size && `${size - 24}px`};
height: ${({ size }) => size && `${size - 24}px`};
border-radius: var(--card-default-radius);
&::before {
position: absolute;
top: 0;
left: 0;
z-index: 1;
width: 100%;
height: 100%;
background: linear-gradient(
0deg,
rgba(0, 0, 0, 100%) 35%,
rgba(0, 0, 0, 0%) 100%
);
opacity: 0;
transition: all 0.2s ease-in-out;
content: '';
user-select: none;
}
`;
interface ImageProps {
height: number;
isLoading?: boolean;
}
const Image = styled.img<ImageProps>`
width: ${({ height }) => `${height - 24}px`};
height: ${({ height }) => `${height - 24}px`};
object-fit: cover;
border: 0;
border-radius: var(--card-default-radius);
${fadeIn}
animation: fadein 0.3s ease-in-out;
`;
const ControlsContainer = styled.div`
position: absolute;
bottom: 0;
z-index: 50;
width: 100%;
opacity: 0;
transition: all 0.2s ease-in-out;
`;
const DetailSection = styled.div`
display: flex;
flex-direction: column;
`;
const Row = styled.div<{ secondary?: boolean }>`
width: 100%;
max-width: 100%;
height: 25px;
padding: 0 0.2rem;
overflow: hidden;
color: ${({ secondary }) =>
secondary ? 'var(--main-fg-secondary)' : 'var(--main-fg)'};
white-space: nowrap;
text-overflow: ellipsis;
`;
interface BaseGridCardProps {
columnIndex: number;
controls: {
cardControls: any[];
cardRows: CardRow[];
handlePlayQueueAdd: (options: PlayQueueAddOptions) => void;
itemType: LibraryItem;
route?: CardRoute;
};
data: any;
listChildProps: Omit<ListChildComponentProps, 'data' | 'style'>;
sizes: {
itemGap: number;
itemHeight: number;
itemWidth: number;
};
}
export const DefaultCard = ({
listChildProps,
data,
columnIndex,
controls,
sizes,
}: BaseGridCardProps) => {
const navigate = useNavigate();
const { isScrolling, index } = listChildProps;
const { itemGap, itemHeight, itemWidth } = sizes;
const { cardControls, handlePlayQueueAdd, itemType, cardRows, route } =
controls;
if (data) {
if (route) {
return (
<CardWrapper
key={`card-${columnIndex}-${index}`}
link
itemGap={itemGap}
itemHeight={itemHeight}
itemWidth={itemWidth}
onClick={() =>
navigate(
generatePath(
route.route,
route.slugs?.reduce((acc, slug) => {
return {
...acc,
[slug.slugProperty]: data[slug.idProperty],
};
}, {})
)
)
}
>
<StyledCard>
<ImageSection size={itemWidth}>
{data?.imageUrl ? (
<Image height={itemWidth} src={data?.imageUrl} />
) : (
<Center
sx={{
background: 'var(--placeholder-bg)',
borderRadius: 'var(--card-default-radius)',
height: '100%',
width: '100%',
}}
>
<RiAlbumFill color="var(--placeholder-fg)" size={35} />
</Center>
)}
<ControlsContainer>
{!isScrolling && (
<GridCardControls
cardControls={cardControls}
handlePlayQueueAdd={handlePlayQueueAdd}
itemData={data}
itemType={itemType}
/>
)}
</ControlsContainer>
</ImageSection>
<DetailSection>
{cardRows.map((row: CardRow, index: number) => {
if (row.arrayProperty) {
if (row.route) {
return (
<Row secondary={index > 0}>
{data[row.property].map(
(item: any, itemIndex: number) => (
<>
{itemIndex > 0 && (
<Text
sx={{
display: 'inline-block',
padding: '0 2px 0 1px',
}}
>
,
</Text>
)}{' '}
<Text
link
component={Link}
overflow="hidden"
secondary={index > 0}
to={generatePath(
row.route!.route,
row.route!.slugs?.reduce((acc, slug) => {
return {
...acc,
[slug.slugProperty]:
data[slug.idProperty],
};
}, {})
)}
onClick={(e) => e.stopPropagation()}
>
{row.arrayProperty && item[row.arrayProperty]}
</Text>
</>
)
)}
</Row>
);
}
return (
<Row>
{data[row.property].map((item: any) => (
<Text overflow="hidden" secondary={index > 0}>
{row.arrayProperty && item[row.arrayProperty]}
</Text>
))}
</Row>
);
}
return (
<Row key={row.property}>
{row.route ? (
<Text
link
component={Link}
overflow="hidden"
to={generatePath(
row.route.route,
row.route.slugs?.reduce((acc, slug) => {
return {
...acc,
[slug.slugProperty]: data[slug.idProperty],
};
}, {})
)}
onClick={(e) => e.stopPropagation()}
>
{data && data[row.property]}
</Text>
) : (
<Text overflow="hidden" secondary={index > 0}>
{data && data[row.property]}
</Text>
)}
</Row>
);
})}
</DetailSection>
</StyledCard>
</CardWrapper>
);
}
return (
<CardWrapper
key={`card-${columnIndex}-${index}`}
itemGap={itemGap}
itemHeight={itemHeight}
itemWidth={itemWidth}
>
<StyledCard>
<ImageSection size={itemWidth}>
{data?.imageUrl ? (
<Image height={itemWidth} src={data?.imageUrl} />
) : (
<Center
sx={{
background: 'var(--placeholder-bg)',
borderRadius: 'var(--card-default-radius)',
height: '100%',
width: '100%',
}}
>
<RiAlbumFill color="var(--placeholder-fg)" size={35} />
</Center>
)}
<ControlsContainer>
{!isScrolling && (
<GridCardControls
cardControls={cardControls}
handlePlayQueueAdd={handlePlayQueueAdd}
itemData={data}
itemType={itemType}
/>
)}
</ControlsContainer>
</ImageSection>
<DetailSection>
{cardRows.map((row: CardRow, index: number) => {
if (row.arrayProperty) {
if (row.route) {
return (
<Row secondary={index > 0}>
{data[row.property].map(
(item: any, itemIndex: number) => (
<>
{itemIndex > 0 && (
<Text
sx={{
display: 'inline-block',
padding: '0 2px 0 1px',
}}
>
,
</Text>
)}{' '}
<Text
link
component={Link}
overflow="hidden"
secondary={index > 0}
to={generatePath(
row.route!.route,
row.route!.slugs?.reduce((acc, slug) => {
return {
...acc,
[slug.slugProperty]: data[slug.idProperty],
};
}, {})
)}
onClick={(e) => e.stopPropagation()}
>
{row.arrayProperty && item[row.arrayProperty]}
</Text>
</>
)
)}
</Row>
);
}
return (
<Row>
{data[row.property].map((item: any) => (
<Text overflow="hidden" secondary={index > 0}>
{row.arrayProperty && item[row.arrayProperty]}
</Text>
))}
</Row>
);
}
return (
<Row key={row.property}>
{row.route ? (
<Text
link
component={Link}
overflow="hidden"
to={generatePath(
row.route.route,
row.route.slugs?.reduce((acc, slug) => {
return {
...acc,
[slug.slugProperty]: data[slug.idProperty],
};
}, {})
)}
onClick={(e) => e.stopPropagation()}
>
{data && data[row.property]}
</Text>
) : (
<Text overflow="hidden" secondary={index > 0}>
{data && data[row.property]}
</Text>
)}
</Row>
);
})}
</DetailSection>
</StyledCard>
</CardWrapper>
);
}
return (
<CardWrapper
key={`card-${columnIndex}-${index}`}
itemGap={itemGap}
itemHeight={itemHeight}
itemWidth={itemWidth + 12}
>
<StyledCard>
<Skeleton visible radius="sm">
<ImageSection size={itemWidth} />
</Skeleton>
<DetailSection>
{cardRows.map((row: CardRow, index: number) => (
<Skeleton
key={`row-${row.property}`}
my={2}
radius="md"
visible={!data}
width={!data ? `${90 - index * 20}%` : '100%'}
>
<Row />
</Skeleton>
))}
</DetailSection>
</StyledCard>
</CardWrapper>
);
};
@@ -90,6 +90,7 @@ export const GridCardControls = ({
whileTap={{ scale: 1 }}
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]);