Add swiper card / update virt cards
This commit is contained in:
parent
d8130f48e2
commit
58d912065b
5 changed files with 278 additions and 45 deletions
|
@ -21,7 +21,7 @@ const Row = styled.div<{ $secondary?: boolean }>`
|
||||||
|
|
||||||
interface CardRowsProps {
|
interface CardRowsProps {
|
||||||
data: any;
|
data: any;
|
||||||
rows: CardRow<Album | Artist | AlbumArtist>[];
|
rows: CardRow<Album>[] | CardRow<Artist>[] | CardRow<AlbumArtist>[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CardRows = ({ data, rows }: CardRowsProps) => {
|
export const CardRows = ({ data, rows }: CardRowsProps) => {
|
||||||
|
|
206
src/renderer/components/card/poster-card.tsx
Normal file
206
src/renderer/components/card/poster-card.tsx
Normal file
|
@ -0,0 +1,206 @@
|
||||||
|
import { Center, Stack } from '@mantine/core';
|
||||||
|
import { RiAlbumFill, RiPlayListFill, RiUserVoiceFill } from 'react-icons/ri';
|
||||||
|
import { generatePath, Link } from 'react-router-dom';
|
||||||
|
import { SimpleImg } from 'react-simple-img';
|
||||||
|
import styled, { css } from 'styled-components';
|
||||||
|
import { Album, AlbumArtist, Artist, LibraryItem } from '/@/renderer/api/types';
|
||||||
|
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';
|
||||||
|
|
||||||
|
interface BaseGridCardProps {
|
||||||
|
controls: {
|
||||||
|
cardRows: CardRow<Album>[] | CardRow<Artist>[] | CardRow<AlbumArtist>[];
|
||||||
|
handleFavorite: (options: {
|
||||||
|
id: string[];
|
||||||
|
isFavorite: boolean;
|
||||||
|
itemType: LibraryItem;
|
||||||
|
serverId: string;
|
||||||
|
}) => void;
|
||||||
|
handlePlayQueueAdd: ((options: PlayQueueAddOptions) => void) | undefined;
|
||||||
|
itemType: LibraryItem;
|
||||||
|
playButtonBehavior: Play;
|
||||||
|
route: CardRoute;
|
||||||
|
};
|
||||||
|
data: any;
|
||||||
|
isLoading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PosterCardContainer = styled.div<{ $isHidden?: boolean }>`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
opacity: ${({ $isHidden }) => ($isHidden ? 0 : 1)};
|
||||||
|
pointer-events: auto;
|
||||||
|
|
||||||
|
.card-controls {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ImageContainerStyles = css`
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
aspect-ratio: 1/1;
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--card-default-bg);
|
||||||
|
border-radius: var(--card-poster-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;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
&::before {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .card-controls {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ImageContainer = styled(Link)<{ $isFavorite?: boolean }>`
|
||||||
|
${ImageContainerStyles}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ImageContainerSkeleton = styled.div`
|
||||||
|
${ImageContainerStyles}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Image = styled(SimpleImg)`
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
height: 100% !important;
|
||||||
|
max-height: 100%;
|
||||||
|
border: 0;
|
||||||
|
|
||||||
|
img {
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const DetailContainer = styled.div`
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const PosterCard = ({
|
||||||
|
data,
|
||||||
|
controls,
|
||||||
|
isLoading,
|
||||||
|
uniqueId,
|
||||||
|
}: BaseGridCardProps & { uniqueId: string }) => {
|
||||||
|
if (!isLoading) {
|
||||||
|
const path = generatePath(
|
||||||
|
controls.route.route,
|
||||||
|
controls.route.slugs?.reduce((acc, slug) => {
|
||||||
|
return {
|
||||||
|
...acc,
|
||||||
|
[slug.slugProperty]: data[slug.idProperty],
|
||||||
|
};
|
||||||
|
}, {}),
|
||||||
|
);
|
||||||
|
|
||||||
|
let Placeholder = RiAlbumFill;
|
||||||
|
|
||||||
|
switch (controls.itemType) {
|
||||||
|
case LibraryItem.ALBUM:
|
||||||
|
Placeholder = RiAlbumFill;
|
||||||
|
break;
|
||||||
|
case LibraryItem.ARTIST:
|
||||||
|
Placeholder = RiUserVoiceFill;
|
||||||
|
break;
|
||||||
|
case LibraryItem.ALBUM_ARTIST:
|
||||||
|
Placeholder = RiUserVoiceFill;
|
||||||
|
break;
|
||||||
|
case LibraryItem.PLAYLIST:
|
||||||
|
Placeholder = RiPlayListFill;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
Placeholder = RiAlbumFill;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PosterCardContainer key={`${uniqueId}-${data.id}`}>
|
||||||
|
<ImageContainer
|
||||||
|
$isFavorite={data?.userFavorite}
|
||||||
|
to={path}
|
||||||
|
>
|
||||||
|
{data?.imageUrl ? (
|
||||||
|
<Image
|
||||||
|
importance="auto"
|
||||||
|
placeholder={data?.imagePlaceholderUrl || 'var(--card-default-bg)'}
|
||||||
|
src={data?.imageUrl}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Center
|
||||||
|
sx={{
|
||||||
|
background: 'var(--placeholder-bg)',
|
||||||
|
borderRadius: 'var(--card-default-radius)',
|
||||||
|
height: '100%',
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Placeholder
|
||||||
|
color="var(--placeholder-fg)"
|
||||||
|
size={35}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
)}
|
||||||
|
<GridCardControls
|
||||||
|
handleFavorite={controls.handleFavorite}
|
||||||
|
handlePlayQueueAdd={controls.handlePlayQueueAdd}
|
||||||
|
itemData={data}
|
||||||
|
itemType={controls.itemType}
|
||||||
|
/>
|
||||||
|
</ImageContainer>
|
||||||
|
<DetailContainer>
|
||||||
|
<CardRows
|
||||||
|
data={data}
|
||||||
|
rows={controls.cardRows}
|
||||||
|
/>
|
||||||
|
</DetailContainer>
|
||||||
|
</PosterCardContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PosterCardContainer key={`placeholder-${uniqueId}-${data.id}`}>
|
||||||
|
<Skeleton
|
||||||
|
visible
|
||||||
|
radius="sm"
|
||||||
|
>
|
||||||
|
<ImageContainerSkeleton />
|
||||||
|
</Skeleton>
|
||||||
|
<DetailContainer>
|
||||||
|
<Stack spacing="sm">
|
||||||
|
{controls.cardRows.map((row, index) => (
|
||||||
|
<Skeleton
|
||||||
|
key={`${index}-${row.arrayProperty}`}
|
||||||
|
visible
|
||||||
|
height={14}
|
||||||
|
radius="sm"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</DetailContainer>
|
||||||
|
</PosterCardContainer>
|
||||||
|
);
|
||||||
|
};
|
|
@ -110,7 +110,7 @@ const ImageContainer = styled.div<{ $isFavorite?: boolean }>`
|
||||||
const Image = styled(SimpleImg)`
|
const Image = styled(SimpleImg)`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
height: 100%;
|
height: 100% !important;
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
border: 0;
|
border: 0;
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import type { MouseEvent } from 'react';
|
import React, { MouseEvent, useState } from 'react';
|
||||||
import React from 'react';
|
|
||||||
import type { UnstyledButtonProps } from '@mantine/core';
|
import type { UnstyledButtonProps } from '@mantine/core';
|
||||||
import { RiPlayFill, RiHeartFill, RiHeartLine, RiMoreFill } from 'react-icons/ri';
|
import { RiPlayFill, RiHeartFill, RiHeartLine, RiMoreFill } from 'react-icons/ri';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
@ -62,7 +61,7 @@ const SecondaryButton = styled(_Button)`
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const GridCardControlsContainer = styled.div`
|
const GridCardControlsContainer = styled.div<{ $isFavorite?: boolean }>`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -73,6 +72,19 @@ const GridCardControlsContainer = styled.div`
|
||||||
height: 100%;
|
height: 100%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const FavoriteBanner = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
top: -50px;
|
||||||
|
left: -50px;
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
box-shadow: 0 0 10px 8px rgba(0, 0, 0, 80%);
|
||||||
|
transform: rotate(-45deg);
|
||||||
|
content: '';
|
||||||
|
pointer-events: none;
|
||||||
|
`;
|
||||||
|
|
||||||
const ControlsRow = styled.div`
|
const ControlsRow = styled.div`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: calc(100% / 3);
|
height: calc(100% / 3);
|
||||||
|
@ -100,11 +112,17 @@ export const GridCardControls = ({
|
||||||
handlePlayQueueAdd,
|
handlePlayQueueAdd,
|
||||||
handleFavorite,
|
handleFavorite,
|
||||||
}: {
|
}: {
|
||||||
handleFavorite: (options: { id: string[]; isFavorite: boolean; itemType: LibraryItem }) => void;
|
handleFavorite: (options: {
|
||||||
|
id: string[];
|
||||||
|
isFavorite: boolean;
|
||||||
|
itemType: LibraryItem;
|
||||||
|
serverId: string;
|
||||||
|
}) => void;
|
||||||
handlePlayQueueAdd?: (options: PlayQueueAddOptions) => void;
|
handlePlayQueueAdd?: (options: PlayQueueAddOptions) => void;
|
||||||
itemData: any;
|
itemData: any;
|
||||||
itemType: LibraryItem;
|
itemType: LibraryItem;
|
||||||
}) => {
|
}) => {
|
||||||
|
const [isFavorite, setIsFavorite] = useState(itemData?.userFavorite);
|
||||||
const playButtonBehavior = usePlayButtonBehavior();
|
const playButtonBehavior = usePlayButtonBehavior();
|
||||||
|
|
||||||
const handlePlay = async (e: MouseEvent<HTMLButtonElement>, playType?: Play) => {
|
const handlePlay = async (e: MouseEvent<HTMLButtonElement>, playType?: Play) => {
|
||||||
|
@ -120,7 +138,7 @@ export const GridCardControls = ({
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFavorites = async (e: MouseEvent<HTMLButtonElement>) => {
|
const handleFavorites = async (e: MouseEvent<HTMLButtonElement>, serverId: string) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
|
@ -128,7 +146,10 @@ export const GridCardControls = ({
|
||||||
id: [itemData.id],
|
id: [itemData.id],
|
||||||
isFavorite: itemData.userFavorite,
|
isFavorite: itemData.userFavorite,
|
||||||
itemType,
|
itemType,
|
||||||
|
serverId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setIsFavorite(!isFavorite);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleContextMenu = useHandleGeneralContextMenu(
|
const handleContextMenu = useHandleGeneralContextMenu(
|
||||||
|
@ -137,42 +158,48 @@ export const GridCardControls = ({
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GridCardControlsContainer className="card-controls">
|
<>
|
||||||
<PlayButton onClick={handlePlay}>
|
{isFavorite ? <FavoriteBanner /> : null}
|
||||||
<RiPlayFill size={25} />
|
<GridCardControlsContainer
|
||||||
</PlayButton>
|
$isFavorite
|
||||||
<BottomControls>
|
className="card-controls"
|
||||||
<SecondaryButton
|
>
|
||||||
p={5}
|
<PlayButton onClick={handlePlay}>
|
||||||
variant="subtle"
|
<RiPlayFill size={25} />
|
||||||
onClick={handleFavorites}
|
</PlayButton>
|
||||||
>
|
<BottomControls>
|
||||||
<FavoriteWrapper isFavorite={itemData?.isFavorite}>
|
<SecondaryButton
|
||||||
{itemData?.userFavorite ? (
|
p={5}
|
||||||
<RiHeartFill size={20} />
|
variant="subtle"
|
||||||
) : (
|
onClick={(e) => handleFavorites(e, itemData?.serverId)}
|
||||||
<RiHeartLine
|
>
|
||||||
color="white"
|
<FavoriteWrapper isFavorite={itemData?.isFavorite}>
|
||||||
size={20}
|
{isFavorite ? (
|
||||||
/>
|
<RiHeartFill size={20} />
|
||||||
)}
|
) : (
|
||||||
</FavoriteWrapper>
|
<RiHeartLine
|
||||||
</SecondaryButton>
|
color="white"
|
||||||
<SecondaryButton
|
size={20}
|
||||||
p={5}
|
/>
|
||||||
variant="subtle"
|
)}
|
||||||
onClick={(e) => {
|
</FavoriteWrapper>
|
||||||
e.preventDefault();
|
</SecondaryButton>
|
||||||
e.stopPropagation();
|
<SecondaryButton
|
||||||
handleContextMenu(e, [itemData]);
|
p={5}
|
||||||
}}
|
variant="subtle"
|
||||||
>
|
onClick={(e) => {
|
||||||
<RiMoreFill
|
e.preventDefault();
|
||||||
color="white"
|
e.stopPropagation();
|
||||||
size={20}
|
handleContextMenu(e, [itemData]);
|
||||||
/>
|
}}
|
||||||
</SecondaryButton>
|
>
|
||||||
</BottomControls>
|
<RiMoreFill
|
||||||
</GridCardControlsContainer>
|
color="white"
|
||||||
|
size={20}
|
||||||
|
/>
|
||||||
|
</SecondaryButton>
|
||||||
|
</BottomControls>
|
||||||
|
</GridCardControlsContainer>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -98,7 +98,7 @@ const ImageContainer = styled.div<{ $isFavorite?: boolean }>`
|
||||||
const Image = styled(SimpleImg)`
|
const Image = styled(SimpleImg)`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
height: 100%;
|
height: 100% !important;
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
border: 0;
|
border: 0;
|
||||||
|
|
||||||
|
|
Reference in a new issue