mirror of
https://github.com/jeffvli/feishin.git
synced 2026-06-17 00:44:23 +02:00
Update grid card styles and props
This commit is contained in:
@@ -1,220 +0,0 @@
|
|||||||
import styled from '@emotion/styled';
|
|
||||||
import { Center, Skeleton } from '@mantine/core';
|
|
||||||
import { RiAlbumFill } from 'react-icons/ri';
|
|
||||||
import { Link, generatePath } from 'react-router-dom';
|
|
||||||
import { Text } from '@/renderer/components/text';
|
|
||||||
import { GridCardControls } from '@/renderer/components/virtual-grid/grid-card-controls';
|
|
||||||
import { AppRoute } from '@/renderer/router/routes';
|
|
||||||
import { fadeIn } from '@/renderer/styles';
|
|
||||||
import { CardRow } from '@/renderer/types';
|
|
||||||
|
|
||||||
const CardWrapper = styled.div<{
|
|
||||||
itemGap: number;
|
|
||||||
itemHeight: number;
|
|
||||||
itemWidth: number;
|
|
||||||
}>`
|
|
||||||
flex: ${({ itemWidth }) => `0 0 ${itemWidth}px`};
|
|
||||||
width: ${({ itemWidth }) => `${itemWidth}px`};
|
|
||||||
height: ${({ itemHeight }) => `${itemHeight}px`};
|
|
||||||
margin: ${({ itemGap }) => `0 ${itemGap / 2}px`};
|
|
||||||
transition: border 0.2s ease-in-out;
|
|
||||||
user-select: none;
|
|
||||||
pointer-events: auto; // https://github.com/bvaughn/react-window/issues/128#issuecomment-460166682
|
|
||||||
|
|
||||||
&: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: 3px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ImageSection = styled.div`
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
`;
|
|
||||||
|
|
||||||
interface ImageProps {
|
|
||||||
height: number;
|
|
||||||
src: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Image = styled.div<ImageProps>`
|
|
||||||
${fadeIn};
|
|
||||||
height: ${({ height }) => `${height}px`};
|
|
||||||
background-image: ${({ src }) => `url(${src})`};
|
|
||||||
background-position: center;
|
|
||||||
background-size: cover;
|
|
||||||
border: 0;
|
|
||||||
border-radius: 5px;
|
|
||||||
|
|
||||||
&::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;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
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`
|
|
||||||
height: 25px;
|
|
||||||
padding: 0 0.2rem;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const GridCard = ({ data, index, style }: any) => {
|
|
||||||
const {
|
|
||||||
itemHeight,
|
|
||||||
itemWidth,
|
|
||||||
columnCount,
|
|
||||||
itemGap,
|
|
||||||
itemCount,
|
|
||||||
cardControls,
|
|
||||||
handlePlayQueueAdd,
|
|
||||||
cardRows,
|
|
||||||
itemData,
|
|
||||||
itemType,
|
|
||||||
} = data;
|
|
||||||
|
|
||||||
const startIndex = index * columnCount;
|
|
||||||
const stopIndex = Math.min(itemCount - 1, startIndex + columnCount - 1);
|
|
||||||
const cards = [];
|
|
||||||
|
|
||||||
for (let i = startIndex; i <= stopIndex; i += 1) {
|
|
||||||
if (itemData[i]) {
|
|
||||||
cards.push(
|
|
||||||
<CardWrapper
|
|
||||||
key={`card-${i}-${index}`}
|
|
||||||
itemGap={itemGap}
|
|
||||||
itemHeight={itemHeight}
|
|
||||||
itemWidth={itemWidth}
|
|
||||||
>
|
|
||||||
<StyledCard>
|
|
||||||
<Link
|
|
||||||
tabIndex={0}
|
|
||||||
to={generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, {
|
|
||||||
albumId: itemData[i]?.id,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<ImageSection style={{ height: `${itemWidth}px` }}>
|
|
||||||
{itemData[i]?.imageUrl ? (
|
|
||||||
<Image height={itemWidth} src={itemData[i]?.imageUrl} />
|
|
||||||
) : (
|
|
||||||
<Center
|
|
||||||
sx={{
|
|
||||||
background: 'var(--placeholder-bg)',
|
|
||||||
borderRadius: '5px',
|
|
||||||
height: '100%',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<RiAlbumFill color="var(--placeholder-fg)" size={35} />
|
|
||||||
</Center>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<ControlsContainer>
|
|
||||||
<GridCardControls
|
|
||||||
cardControls={cardControls}
|
|
||||||
handlePlayQueueAdd={handlePlayQueueAdd}
|
|
||||||
itemData={itemData[i]}
|
|
||||||
itemType={itemType}
|
|
||||||
/>
|
|
||||||
</ControlsContainer>
|
|
||||||
</ImageSection>
|
|
||||||
</Link>
|
|
||||||
<DetailSection>
|
|
||||||
{cardRows.map((row: CardRow) => (
|
|
||||||
<Row key={row.prop}>
|
|
||||||
<Text overflow="hidden">
|
|
||||||
{itemData[i] && itemData[i][row.prop]}
|
|
||||||
</Text>
|
|
||||||
</Row>
|
|
||||||
))}
|
|
||||||
</DetailSection>
|
|
||||||
</StyledCard>
|
|
||||||
</CardWrapper>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
cards.push(
|
|
||||||
<CardWrapper
|
|
||||||
key={`card-${i}-${index}`}
|
|
||||||
itemGap={itemGap}
|
|
||||||
itemHeight={itemHeight}
|
|
||||||
itemWidth={itemWidth}
|
|
||||||
>
|
|
||||||
<StyledCard>
|
|
||||||
<Skeleton visible radius="sm">
|
|
||||||
<ImageSection style={{ height: `${itemWidth}px` }} />
|
|
||||||
</Skeleton>
|
|
||||||
<DetailSection>
|
|
||||||
{cardRows.map((row: CardRow, index: number) => (
|
|
||||||
<Skeleton
|
|
||||||
key={`row-${row.prop}`}
|
|
||||||
my={2}
|
|
||||||
radius="md"
|
|
||||||
visible={!itemData[i]}
|
|
||||||
width={!itemData[i] ? (index > 0 ? '50%' : '90%') : '100%'}
|
|
||||||
>
|
|
||||||
<Row />
|
|
||||||
</Skeleton>
|
|
||||||
))}
|
|
||||||
</DetailSection>
|
|
||||||
</StyledCard>
|
|
||||||
</CardWrapper>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
...style,
|
|
||||||
alignItems: 'center',
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'start',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{cards}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
+43
-14
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React, { MouseEvent } from 'react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { Box, UnstyledButton, Group, UnstyledButtonProps } from '@mantine/core';
|
import { Group, UnstyledButtonProps } from '@mantine/core';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import {
|
import {
|
||||||
RiPlayFill,
|
RiPlayFill,
|
||||||
@@ -15,30 +15,25 @@ import { Play } from '@/renderer/types';
|
|||||||
type PlayButtonType = UnstyledButtonProps &
|
type PlayButtonType = UnstyledButtonProps &
|
||||||
React.ComponentPropsWithoutRef<'button'>;
|
React.ComponentPropsWithoutRef<'button'>;
|
||||||
|
|
||||||
const PlayButton = styled(UnstyledButton)<PlayButtonType>`
|
const PlayButton = styled(motion.button)<PlayButtonType>`
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 50px;
|
width: 50px;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
background-color: rgb(255, 255, 255);
|
background-color: rgb(255, 255, 255);
|
||||||
|
border: none;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
opacity: 0.8;
|
|
||||||
transition: opacity 0.2s ease-in-out;
|
transition: opacity 0.2s ease-in-out;
|
||||||
transition: scale 0.2s ease-in;
|
transition: scale 0.2s ease-in;
|
||||||
|
|
||||||
&:hover {
|
|
||||||
opacity: 1;
|
|
||||||
scale: 1.1;
|
|
||||||
}
|
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
fill: rgb(0, 0, 0);
|
fill: rgb(0, 0, 0);
|
||||||
stroke: rgb(0, 0, 0);
|
stroke: rgb(0, 0, 0);
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const GridCardControlsContainer = styled(Box)`
|
const GridCardControlsContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -90,7 +85,11 @@ export const GridCardControls = ({
|
|||||||
<CenterControls animate={{ opacity: 1 }} initial={{ opacity: 0 }} />
|
<CenterControls animate={{ opacity: 1 }} initial={{ opacity: 0 }} />
|
||||||
<BottomControls>
|
<BottomControls>
|
||||||
<PlayButton
|
<PlayButton
|
||||||
onClick={() => {
|
animate={{ opacity: 0.6 }}
|
||||||
|
whileHover={{ opacity: 1, scale: 1.1 }}
|
||||||
|
whileTap={{ scale: 1 }}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
handlePlayQueueAdd({
|
handlePlayQueueAdd({
|
||||||
byItemType: {
|
byItemType: {
|
||||||
id: itemData.id,
|
id: itemData.id,
|
||||||
@@ -114,13 +113,43 @@ export const GridCardControls = ({
|
|||||||
</Button>
|
</Button>
|
||||||
<DropdownMenu withinPortal position="bottom-start">
|
<DropdownMenu withinPortal position="bottom-start">
|
||||||
<DropdownMenu.Target>
|
<DropdownMenu.Target>
|
||||||
<Button p={5} variant="subtle">
|
<Button
|
||||||
|
p={5}
|
||||||
|
variant="subtle"
|
||||||
|
onClick={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
<RiMore2Fill color="white" size={20} />
|
<RiMore2Fill color="white" size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenu.Target>
|
</DropdownMenu.Target>
|
||||||
<DropdownMenu.Dropdown>
|
<DropdownMenu.Dropdown>
|
||||||
<DropdownMenu.Item>Play next</DropdownMenu.Item>
|
<DropdownMenu.Item
|
||||||
<DropdownMenu.Item>Play later</DropdownMenu.Item>
|
onClick={(e: MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handlePlayQueueAdd({
|
||||||
|
byItemType: {
|
||||||
|
id: itemData.id,
|
||||||
|
type: itemType,
|
||||||
|
},
|
||||||
|
play: Play.LAST,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Play later
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
onClick={(e: MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handlePlayQueueAdd({
|
||||||
|
byItemType: {
|
||||||
|
id: itemData.id,
|
||||||
|
type: itemType,
|
||||||
|
},
|
||||||
|
play: Play.NEXT,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Play next
|
||||||
|
</DropdownMenu.Item>
|
||||||
<DropdownMenu.Divider />
|
<DropdownMenu.Divider />
|
||||||
<DropdownMenu.Item disabled>Add to playlist</DropdownMenu.Item>
|
<DropdownMenu.Item disabled>Add to playlist</DropdownMenu.Item>
|
||||||
<DropdownMenu.Divider />
|
<DropdownMenu.Divider />
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { ListChildComponentProps } from 'react-window';
|
||||||
|
import { PosterCard } from '@/renderer/components/virtual-grid/grid-card/poster-card';
|
||||||
|
import { GridCardData } from '@/renderer/types';
|
||||||
|
|
||||||
|
export const GridCard = ({
|
||||||
|
data,
|
||||||
|
index,
|
||||||
|
style,
|
||||||
|
isScrolling,
|
||||||
|
}: ListChildComponentProps) => {
|
||||||
|
const {
|
||||||
|
itemHeight,
|
||||||
|
itemWidth,
|
||||||
|
columnCount,
|
||||||
|
itemGap,
|
||||||
|
itemCount,
|
||||||
|
cardControls,
|
||||||
|
handlePlayQueueAdd,
|
||||||
|
cardRows,
|
||||||
|
itemData,
|
||||||
|
itemType,
|
||||||
|
route,
|
||||||
|
display,
|
||||||
|
} = data as GridCardData;
|
||||||
|
|
||||||
|
const startIndex = index * columnCount;
|
||||||
|
const stopIndex = Math.min(itemCount - 1, startIndex + columnCount - 1);
|
||||||
|
const cards = [];
|
||||||
|
|
||||||
|
for (let i = startIndex; i <= stopIndex; i += 1) {
|
||||||
|
cards.push(
|
||||||
|
<PosterCard
|
||||||
|
key={`card-${i}-${index}`}
|
||||||
|
columnIndex={i}
|
||||||
|
controls={{
|
||||||
|
cardControls,
|
||||||
|
cardRows,
|
||||||
|
handlePlayQueueAdd,
|
||||||
|
itemType,
|
||||||
|
route,
|
||||||
|
}}
|
||||||
|
data={itemData[i]}
|
||||||
|
listChildProps={{ index, isScrolling }}
|
||||||
|
sizes={{ itemGap, itemHeight, itemWidth }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
...style,
|
||||||
|
alignItems: 'center',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'start',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{cards}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,338 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { Center, Skeleton } from '@mantine/core';
|
||||||
|
import { RiAlbumFill } from 'react-icons/ri';
|
||||||
|
import { generatePath } 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;
|
||||||
|
}>`
|
||||||
|
flex: ${({ itemWidth }) => `0 0 ${itemWidth}px`};
|
||||||
|
width: ${({ itemWidth }) => `${itemWidth}px`};
|
||||||
|
height: ${({ itemHeight }) => `${itemHeight}px`};
|
||||||
|
margin: ${({ itemGap }) => `0 ${itemGap / 2}px`};
|
||||||
|
transition: border 0.2s ease-in-out;
|
||||||
|
user-select: none;
|
||||||
|
pointer-events: auto; // https://github.com/bvaughn/react-window/issues/128#issuecomment-460166682
|
||||||
|
|
||||||
|
&: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: 5px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ImageSection = styled.div`
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 5px;
|
||||||
|
|
||||||
|
&::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>`
|
||||||
|
${fadeIn};
|
||||||
|
width: ${({ height }) => `${height}px`};
|
||||||
|
height: ${({ height }) => `${height}px`};
|
||||||
|
object-fit: cover;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 2px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
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 PosterCard = ({
|
||||||
|
listChildProps,
|
||||||
|
data,
|
||||||
|
columnIndex,
|
||||||
|
controls,
|
||||||
|
sizes,
|
||||||
|
}: BaseGridCardProps) => {
|
||||||
|
const { isScrolling, index } = listChildProps;
|
||||||
|
const { itemGap, itemHeight, itemWidth } = sizes;
|
||||||
|
const { cardControls, handlePlayQueueAdd, itemType, cardRows, route } =
|
||||||
|
controls;
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
return (
|
||||||
|
<CardWrapper
|
||||||
|
key={`card-${columnIndex}-${index}`}
|
||||||
|
itemGap={itemGap}
|
||||||
|
itemHeight={itemHeight}
|
||||||
|
itemWidth={itemWidth}
|
||||||
|
>
|
||||||
|
<StyledCard>
|
||||||
|
{route ? (
|
||||||
|
<Link
|
||||||
|
tabIndex={0}
|
||||||
|
to={generatePath(
|
||||||
|
route.route,
|
||||||
|
route.slugs?.reduce((acc, slug) => {
|
||||||
|
return {
|
||||||
|
...acc,
|
||||||
|
[slug.slugProperty]: data[slug.idProperty],
|
||||||
|
};
|
||||||
|
}, {})
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<ImageSection style={{ height: `${itemWidth}px` }}>
|
||||||
|
{data?.imageUrl ? (
|
||||||
|
<Image height={itemWidth} src={data?.imageUrl} />
|
||||||
|
) : (
|
||||||
|
<Center
|
||||||
|
sx={{
|
||||||
|
background: 'var(--placeholder-bg)',
|
||||||
|
borderRadius: '5px',
|
||||||
|
height: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RiAlbumFill color="var(--placeholder-fg)" size={35} />
|
||||||
|
</Center>
|
||||||
|
)}
|
||||||
|
<ControlsContainer>
|
||||||
|
{!isScrolling && (
|
||||||
|
<GridCardControls
|
||||||
|
cardControls={cardControls}
|
||||||
|
handlePlayQueueAdd={handlePlayQueueAdd}
|
||||||
|
itemData={data}
|
||||||
|
itemType={itemType}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ControlsContainer>
|
||||||
|
</ImageSection>
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<ImageSection style={{ height: `${itemWidth}px` }}>
|
||||||
|
{data?.imageUrl ? (
|
||||||
|
<Image height={itemWidth} src={data?.imageUrl} />
|
||||||
|
) : (
|
||||||
|
<Center
|
||||||
|
sx={{
|
||||||
|
background: 'var(--placeholder-bg)',
|
||||||
|
borderRadius: '5px',
|
||||||
|
height: '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],
|
||||||
|
};
|
||||||
|
}, {})
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{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],
|
||||||
|
};
|
||||||
|
}, {})
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{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>
|
||||||
|
<Skeleton visible radius="sm">
|
||||||
|
<ImageSection style={{ height: `${itemWidth}px` }} />
|
||||||
|
</Skeleton>
|
||||||
|
<DetailSection>
|
||||||
|
{cardRows.map((row: CardRow, index: number) => (
|
||||||
|
<Skeleton
|
||||||
|
key={`row-${row.property}`}
|
||||||
|
my={2}
|
||||||
|
radius="md"
|
||||||
|
visible={!data}
|
||||||
|
width={!data ? (index > 0 ? '50%' : '90%') : '100%'}
|
||||||
|
>
|
||||||
|
<Row />
|
||||||
|
</Skeleton>
|
||||||
|
))}
|
||||||
|
</DetailSection>
|
||||||
|
</StyledCard>
|
||||||
|
</CardWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -3,7 +3,12 @@ import styled from '@emotion/styled';
|
|||||||
import { FixedSizeList, FixedSizeListProps } from 'react-window';
|
import { FixedSizeList, FixedSizeListProps } from 'react-window';
|
||||||
import { GridCard } from '@/renderer/components/virtual-grid/grid-card';
|
import { GridCard } from '@/renderer/components/virtual-grid/grid-card';
|
||||||
import { usePlayQueueHandler } from '@/renderer/features/player/hooks/use-playqueue-handler';
|
import { usePlayQueueHandler } from '@/renderer/features/player/hooks/use-playqueue-handler';
|
||||||
import { CardRow, LibraryItem } from '@/renderer/types';
|
import {
|
||||||
|
CardRow,
|
||||||
|
LibraryItem,
|
||||||
|
CardDisplayType,
|
||||||
|
CardRoute,
|
||||||
|
} from '@/renderer/types';
|
||||||
|
|
||||||
export const VirtualGridWrapper = ({
|
export const VirtualGridWrapper = ({
|
||||||
refInstance,
|
refInstance,
|
||||||
@@ -16,16 +21,19 @@ export const VirtualGridWrapper = ({
|
|||||||
columnCount,
|
columnCount,
|
||||||
rowCount,
|
rowCount,
|
||||||
itemData,
|
itemData,
|
||||||
|
route,
|
||||||
...rest
|
...rest
|
||||||
}: Omit<FixedSizeListProps, 'ref' | 'itemSize' | 'children'> & {
|
}: Omit<FixedSizeListProps, 'ref' | 'itemSize' | 'children'> & {
|
||||||
cardRows: CardRow[];
|
cardRows: CardRow[];
|
||||||
columnCount: number;
|
columnCount: number;
|
||||||
|
display: CardDisplayType;
|
||||||
itemData: any[];
|
itemData: any[];
|
||||||
itemGap: number;
|
itemGap: number;
|
||||||
itemHeight: number;
|
itemHeight: number;
|
||||||
itemType: LibraryItem;
|
itemType: LibraryItem;
|
||||||
itemWidth: number;
|
itemWidth: number;
|
||||||
refInstance: Ref<any>;
|
refInstance: Ref<any>;
|
||||||
|
route?: CardRoute;
|
||||||
rowCount: number;
|
rowCount: number;
|
||||||
}) => {
|
}) => {
|
||||||
const handlePlayQueueAdd = usePlayQueueHandler();
|
const handlePlayQueueAdd = usePlayQueueHandler();
|
||||||
@@ -41,6 +49,7 @@ export const VirtualGridWrapper = ({
|
|||||||
itemHeight,
|
itemHeight,
|
||||||
itemType,
|
itemType,
|
||||||
itemWidth,
|
itemWidth,
|
||||||
|
route,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
cardRows,
|
cardRows,
|
||||||
@@ -51,6 +60,7 @@ export const VirtualGridWrapper = ({
|
|||||||
itemData,
|
itemData,
|
||||||
itemGap,
|
itemGap,
|
||||||
itemHeight,
|
itemHeight,
|
||||||
|
route,
|
||||||
itemWidth,
|
itemWidth,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
@@ -59,16 +69,21 @@ export const VirtualGridWrapper = ({
|
|||||||
<FixedSizeList
|
<FixedSizeList
|
||||||
ref={refInstance}
|
ref={refInstance}
|
||||||
{...rest}
|
{...rest}
|
||||||
|
useIsScrolling
|
||||||
itemCount={rowCount}
|
itemCount={rowCount}
|
||||||
itemData={memo}
|
itemData={memo}
|
||||||
itemSize={itemHeight + itemGap}
|
itemSize={itemHeight + itemGap}
|
||||||
overscanCount={10}
|
overscanCount={5}
|
||||||
>
|
>
|
||||||
{GridCard}
|
{GridCard}
|
||||||
</FixedSizeList>
|
</FixedSizeList>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
VirtualGridWrapper.defaultProps = {
|
||||||
|
route: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
export const VirtualGridContainer = styled.div`
|
export const VirtualGridContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -3,11 +3,17 @@ import debounce from 'lodash/debounce';
|
|||||||
import { FixedSizeListProps } from 'react-window';
|
import { FixedSizeListProps } from 'react-window';
|
||||||
import InfiniteLoader from 'react-window-infinite-loader';
|
import InfiniteLoader from 'react-window-infinite-loader';
|
||||||
import { VirtualGridWrapper } from '@/renderer/components/virtual-grid/virtual-grid-wrapper';
|
import { VirtualGridWrapper } from '@/renderer/components/virtual-grid/virtual-grid-wrapper';
|
||||||
import { CardRow, LibraryItem } from '@/renderer/types';
|
import {
|
||||||
|
CardDisplayType,
|
||||||
|
CardRoute,
|
||||||
|
CardRow,
|
||||||
|
LibraryItem,
|
||||||
|
} from '@/renderer/types';
|
||||||
|
|
||||||
interface VirtualGridProps
|
interface VirtualGridProps
|
||||||
extends Omit<FixedSizeListProps, 'children' | 'itemSize'> {
|
extends Omit<FixedSizeListProps, 'children' | 'itemSize'> {
|
||||||
cardRows: CardRow[];
|
cardRows: CardRow[];
|
||||||
|
display?: CardDisplayType;
|
||||||
fetchFn: (options: {
|
fetchFn: (options: {
|
||||||
columnCount: number;
|
columnCount: number;
|
||||||
skip: number;
|
skip: number;
|
||||||
@@ -17,6 +23,8 @@ interface VirtualGridProps
|
|||||||
itemSize: number;
|
itemSize: number;
|
||||||
itemType: LibraryItem;
|
itemType: LibraryItem;
|
||||||
minimumBatchSize?: number;
|
minimumBatchSize?: number;
|
||||||
|
refresh?: any; // Pass in any value to refresh the grid when changed
|
||||||
|
route?: CardRoute;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const VirtualInfiniteGrid = ({
|
export const VirtualInfiniteGrid = ({
|
||||||
@@ -25,10 +33,13 @@ export const VirtualInfiniteGrid = ({
|
|||||||
itemSize,
|
itemSize,
|
||||||
itemType,
|
itemType,
|
||||||
cardRows,
|
cardRows,
|
||||||
|
route,
|
||||||
|
display,
|
||||||
minimumBatchSize,
|
minimumBatchSize,
|
||||||
fetchFn,
|
fetchFn,
|
||||||
height,
|
height,
|
||||||
width,
|
width,
|
||||||
|
refresh,
|
||||||
}: VirtualGridProps) => {
|
}: VirtualGridProps) => {
|
||||||
const [itemData, setItemData] = useState<any[]>([]);
|
const [itemData, setItemData] = useState<any[]>([]);
|
||||||
const listRef = useRef<any>(null);
|
const listRef = useRef<any>(null);
|
||||||
@@ -88,7 +99,7 @@ export const VirtualInfiniteGrid = ({
|
|||||||
loadMoreItems(0, minimumBatchSize! * 2);
|
loadMoreItems(0, minimumBatchSize! * 2);
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [minimumBatchSize, fetchFn]);
|
}, [minimumBatchSize, fetchFn, refresh]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InfiniteLoader
|
<InfiniteLoader
|
||||||
@@ -104,6 +115,7 @@ export const VirtualInfiniteGrid = ({
|
|||||||
useIsScrolling
|
useIsScrolling
|
||||||
cardRows={cardRows}
|
cardRows={cardRows}
|
||||||
columnCount={columnCount}
|
columnCount={columnCount}
|
||||||
|
display={display || CardDisplayType.CARD}
|
||||||
height={height}
|
height={height}
|
||||||
itemCount={itemCount || 0}
|
itemCount={itemCount || 0}
|
||||||
itemData={itemData}
|
itemData={itemData}
|
||||||
@@ -115,6 +127,7 @@ export const VirtualInfiniteGrid = ({
|
|||||||
infiniteLoaderRef(list);
|
infiniteLoaderRef(list);
|
||||||
listRef.current = list;
|
listRef.current = list;
|
||||||
}}
|
}}
|
||||||
|
route={route}
|
||||||
rowCount={rowCount}
|
rowCount={rowCount}
|
||||||
width={width}
|
width={width}
|
||||||
onItemsRendered={onItemsRendered}
|
onItemsRendered={onItemsRendered}
|
||||||
@@ -125,5 +138,8 @@ export const VirtualInfiniteGrid = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
VirtualInfiniteGrid.defaultProps = {
|
VirtualInfiniteGrid.defaultProps = {
|
||||||
|
display: CardDisplayType.CARD,
|
||||||
minimumBatchSize: 20,
|
minimumBatchSize: 20,
|
||||||
|
refresh: undefined,
|
||||||
|
route: undefined,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { api } from '@/renderer/api';
|
|||||||
import { queryKeys } from '@/renderer/api/query-keys';
|
import { queryKeys } from '@/renderer/api/query-keys';
|
||||||
import { useServerCredential } from '@/renderer/features/shared';
|
import { useServerCredential } from '@/renderer/features/shared';
|
||||||
import { useAuthStore, usePlayerStore } from '@/renderer/store';
|
import { useAuthStore, usePlayerStore } from '@/renderer/store';
|
||||||
import { LibraryItem, Play } from '@/renderer/types';
|
import { LibraryItem, Play, PlayQueueAddOptions } from '@/renderer/types';
|
||||||
import { mpvPlayer } from '../utils/mpvPlayer';
|
import { mpvPlayer } from '../utils/mpv-player';
|
||||||
|
|
||||||
export const usePlayQueueHandler = () => {
|
export const usePlayQueueHandler = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
@@ -12,14 +12,7 @@ export const usePlayQueueHandler = () => {
|
|||||||
const { serverToken, isImageTokenRequired } = useServerCredential();
|
const { serverToken, isImageTokenRequired } = useServerCredential();
|
||||||
const addToQueue = usePlayerStore((state) => state.addToQueue);
|
const addToQueue = usePlayerStore((state) => state.addToQueue);
|
||||||
|
|
||||||
const handlePlayQueueAdd = async (options: {
|
const handlePlayQueueAdd = async (options: PlayQueueAddOptions) => {
|
||||||
byData?: any[];
|
|
||||||
byItemType?: {
|
|
||||||
id: string;
|
|
||||||
type: LibraryItem;
|
|
||||||
};
|
|
||||||
play: Play;
|
|
||||||
}) => {
|
|
||||||
if (options.byData) {
|
if (options.byData) {
|
||||||
// dispatchSongsToQueue(options.byData, options.play);
|
// dispatchSongsToQueue(options.byData, options.play);
|
||||||
}
|
}
|
||||||
@@ -65,6 +58,7 @@ export const usePlayQueueHandler = () => {
|
|||||||
mpvPlayer.setQueueNext(playerData);
|
mpvPlayer.setQueueNext(playerData);
|
||||||
} else {
|
} else {
|
||||||
mpvPlayer.setQueue(playerData);
|
mpvPlayer.setQueue(playerData);
|
||||||
|
mpvPlayer.play();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
+43
-7
@@ -1,12 +1,24 @@
|
|||||||
import { AppRoute } from './router/routes';
|
import { AppRoute } from './router/routes';
|
||||||
|
|
||||||
export interface CardRow {
|
export type RouteSlug = {
|
||||||
align?: 'left' | 'center' | 'right';
|
idProperty: string;
|
||||||
prop: string;
|
slugProperty: string;
|
||||||
route?: {
|
};
|
||||||
prop: string;
|
|
||||||
route: AppRoute | string;
|
export type CardRoute = {
|
||||||
};
|
route: AppRoute | string;
|
||||||
|
slugs?: RouteSlug[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CardRow = {
|
||||||
|
arrayProperty?: string;
|
||||||
|
property: string;
|
||||||
|
route?: CardRoute;
|
||||||
|
};
|
||||||
|
|
||||||
|
export enum CardDisplayType {
|
||||||
|
CARD = 'card',
|
||||||
|
POSTER = 'poster',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum LibraryItem {
|
export enum LibraryItem {
|
||||||
@@ -74,3 +86,27 @@ export enum SortOrder {
|
|||||||
ASC = 'asc',
|
ASC = 'asc',
|
||||||
DESC = 'desc',
|
DESC = 'desc',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type PlayQueueAddOptions = {
|
||||||
|
byData?: any[];
|
||||||
|
byItemType?: {
|
||||||
|
id: string;
|
||||||
|
type: LibraryItem;
|
||||||
|
};
|
||||||
|
play: Play;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GridCardData = {
|
||||||
|
cardControls: any;
|
||||||
|
cardRows: CardRow[];
|
||||||
|
columnCount: number;
|
||||||
|
display: CardDisplayType;
|
||||||
|
handlePlayQueueAdd: (options: PlayQueueAddOptions) => void;
|
||||||
|
itemCount: number;
|
||||||
|
itemData: any[];
|
||||||
|
itemGap: number;
|
||||||
|
itemHeight: number;
|
||||||
|
itemType: LibraryItem;
|
||||||
|
itemWidth: number;
|
||||||
|
route?: CardRoute;
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user