Lint all files

This commit is contained in:
jeffvli 2023-07-01 19:10:05 -07:00
parent 22af76b4d6
commit 30e52ebb54
334 changed files with 76519 additions and 75932 deletions

View file

@ -11,87 +11,91 @@ import { GridCardControls } from '/@/renderer/components/virtual-grid/grid-card/
import { CardRow, PlayQueueAddOptions, Play, CardRoute } from '/@/renderer/types';
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;
isHidden?: boolean;
listChildProps: Omit<ListChildComponentProps, 'data' | 'style'>;
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;
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: 0.5rem;
overflow: hidden;
background: var(--card-default-bg);
border-radius: var(--card-default-radius);
cursor: pointer;
opacity: ${({ $isHidden }) => ($isHidden ? 0 : 1)};
pointer-events: auto;
display: flex;
flex-direction: column;
width: 100%;
height: calc(100% - 2rem);
margin: 0.5rem;
overflow: hidden;
background: var(--card-default-bg);
border-radius: var(--card-default-radius);
cursor: pointer;
opacity: ${({ $isHidden }) => ($isHidden ? 0 : 1)};
pointer-events: auto;
&:hover {
background: var(--card-default-bg-hover);
}
&:hover {
background: var(--card-default-bg-hover);
}
`;
const InnerCardContainer = styled.div`
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
padding: 1rem;
overflow: hidden;
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
padding: 1rem;
overflow: hidden;
.card-controls {
opacity: 0;
}
&:hover .card-controls {
opacity: 1;
}
&:hover * {
&::before {
opacity: 0.5;
.card-controls {
opacity: 0;
}
&:hover .card-controls {
opacity: 1;
}
&:hover * {
&::before {
opacity: 0.5;
}
}
}
`;
const ImageContainer = styled.div<{ $isFavorite?: boolean }>`
position: relative;
display: flex;
align-items: center;
height: 100%;
aspect-ratio: 1/1;
overflow: hidden;
background: var(--placeholder-bg);
border-radius: var(--card-default-radius);
&::before {
position: absolute;
top: 0;
left: 0;
z-index: 1;
width: 100%;
position: relative;
display: flex;
align-items: center;
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;
}
${(props) =>
props.$isFavorite &&
`
aspect-ratio: 1/1;
overflow: hidden;
background: var(--placeholder-bg);
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;
}
${(props) =>
props.$isFavorite &&
`
&::after {
position: absolute;
top: -50px;
@ -108,134 +112,134 @@ const ImageContainer = styled.div<{ $isFavorite?: boolean }>`
`;
const Image = styled(SimpleImg)`
width: 100%;
max-width: 100%;
height: 100% !important;
max-height: 100%;
border: 0;
width: 100%;
max-width: 100%;
height: 100% !important;
max-height: 100%;
border: 0;
img {
height: 100%;
object-fit: cover;
}
img {
height: 100%;
object-fit: cover;
}
`;
const DetailContainer = styled.div`
margin-top: 0.5rem;
margin-top: 0.5rem;
`;
export const DefaultCard = ({
listChildProps,
data,
columnIndex,
controls,
isHidden,
listChildProps,
data,
columnIndex,
controls,
isHidden,
}: BaseGridCardProps) => {
const navigate = useNavigate();
const navigate = useNavigate();
if (data) {
const path = generatePath(
controls.route.route,
controls.route.slugs?.reduce((acc, slug) => {
return {
...acc,
[slug.slugProperty]: data[slug.idProperty],
};
}, {}),
);
if (data) {
const path = generatePath(
controls.route.route,
controls.route.slugs?.reduce((acc, slug) => {
return {
...acc,
[slug.slugProperty]: data[slug.idProperty],
};
}, {}),
);
let Placeholder = RiAlbumFill;
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;
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 (
<DefaultCardContainer
key={`card-${columnIndex}-${listChildProps.index}`}
onClick={() => navigate(path)}
>
<InnerCardContainer>
<ImageContainer $isFavorite={data?.userFavorite}>
{data?.imageUrl ? (
<Image
importance="auto"
placeholder={data?.imagePlaceholderUrl || 'var(--placeholder-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>
</InnerCardContainer>
</DefaultCardContainer>
);
}
return (
<DefaultCardContainer
key={`card-${columnIndex}-${listChildProps.index}`}
onClick={() => navigate(path)}
>
<InnerCardContainer>
<ImageContainer $isFavorite={data?.userFavorite}>
{data?.imageUrl ? (
<Image
importance="auto"
placeholder={data?.imagePlaceholderUrl || 'var(--placeholder-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>
</InnerCardContainer>
</DefaultCardContainer>
<DefaultCardContainer
key={`card-${columnIndex}-${listChildProps.index}`}
$isHidden={isHidden}
>
<InnerCardContainer>
<ImageContainer>
<Skeleton
visible
radius="sm"
/>
</ImageContainer>
<DetailContainer>
<Stack spacing="sm">
{controls.cardRows.map((row, index) => (
<Skeleton
key={`${index}-${columnIndex}-${row.arrayProperty}`}
visible
height={14}
radius="sm"
/>
))}
</Stack>
</DetailContainer>
</InnerCardContainer>
</DefaultCardContainer>
);
}
return (
<DefaultCardContainer
key={`card-${columnIndex}-${listChildProps.index}`}
$isHidden={isHidden}
>
<InnerCardContainer>
<ImageContainer>
<Skeleton
visible
radius="sm"
/>
</ImageContainer>
<DetailContainer>
<Stack spacing="sm">
{controls.cardRows.map((row, index) => (
<Skeleton
key={`${index}-${columnIndex}-${row.arrayProperty}`}
visible
height={14}
radius="sm"
/>
))}
</Stack>
</DetailContainer>
</InnerCardContainer>
</DefaultCardContainer>
);
};

View file

@ -9,197 +9,197 @@ import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
import { LibraryItem } from '/@/renderer/api/types';
import { useHandleGeneralContextMenu } from '/@/renderer/features/context-menu/hooks/use-handle-context-menu';
import {
ALBUM_CONTEXT_MENU_ITEMS,
ARTIST_CONTEXT_MENU_ITEMS,
ALBUM_CONTEXT_MENU_ITEMS,
ARTIST_CONTEXT_MENU_ITEMS,
} from '/@/renderer/features/context-menu/context-menu-items';
type PlayButtonType = UnstyledButtonProps & React.ComponentPropsWithoutRef<'button'>;
const PlayButton = styled.button<PlayButtonType>`
position: absolute;
display: flex;
align-items: center;
justify-content: center;
width: 50px;
height: 50px;
background-color: rgb(255, 255, 255);
border: none;
border-radius: 50%;
opacity: 0.8;
transition: opacity 0.2s ease-in-out;
transition: scale 0.1s ease-in-out;
position: absolute;
display: flex;
align-items: center;
justify-content: center;
width: 50px;
height: 50px;
background-color: rgb(255, 255, 255);
border: none;
border-radius: 50%;
opacity: 0.8;
transition: opacity 0.2s ease-in-out;
transition: scale 0.1s ease-in-out;
&:hover {
opacity: 1;
scale: 1.1;
}
&:hover {
opacity: 1;
scale: 1.1;
}
&:active {
opacity: 1;
scale: 1;
}
&:active {
opacity: 1;
scale: 1;
}
svg {
fill: rgb(0, 0, 0);
stroke: rgb(0, 0, 0);
}
svg {
fill: rgb(0, 0, 0);
stroke: rgb(0, 0, 0);
}
`;
const SecondaryButton = styled(_Button)`
opacity: 0.8;
transition: opacity 0.2s ease-in-out;
transition: scale 0.2s linear;
opacity: 0.8;
transition: opacity 0.2s ease-in-out;
transition: scale 0.2s linear;
&:hover {
opacity: 1;
scale: 1.1;
}
&:hover {
opacity: 1;
scale: 1.1;
}
&:active {
opacity: 1;
scale: 1;
}
&:active {
opacity: 1;
scale: 1;
}
`;
const GridCardControlsContainer = styled.div<{ $isFavorite?: boolean }>`
position: absolute;
z-index: 100;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
position: absolute;
z-index: 100;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 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;
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`
width: 100%;
height: calc(100% / 3);
width: 100%;
height: calc(100% / 3);
`;
const BottomControls = styled(ControlsRow)`
position: absolute;
bottom: 0;
display: flex;
gap: 0.5rem;
align-items: flex-end;
justify-content: flex-end;
padding: 1rem 0.5rem;
position: absolute;
bottom: 0;
display: flex;
gap: 0.5rem;
align-items: flex-end;
justify-content: flex-end;
padding: 1rem 0.5rem;
`;
const FavoriteWrapper = styled.span<{ isFavorite: boolean }>`
svg {
fill: ${(props) => props.isFavorite && 'var(--primary-color)'};
}
svg {
fill: ${(props) => props.isFavorite && 'var(--primary-color)'};
}
`;
export const GridCardControls = ({
itemData,
itemType,
handlePlayQueueAdd,
handleFavorite,
}: {
handleFavorite: (options: {
id: string[];
isFavorite: boolean;
itemType: LibraryItem;
serverId: string;
}) => void;
handlePlayQueueAdd?: (options: PlayQueueAddOptions) => void;
itemData: any;
itemType: LibraryItem;
}) => {
const [isFavorite, setIsFavorite] = useState(itemData?.userFavorite);
const playButtonBehavior = usePlayButtonBehavior();
const handlePlay = async (e: MouseEvent<HTMLButtonElement>, playType?: Play) => {
e.preventDefault();
e.stopPropagation();
handlePlayQueueAdd?.({
byItemType: {
id: [itemData.id],
type: itemType,
},
playType: playType || playButtonBehavior,
});
};
const handleFavorites = async (e: MouseEvent<HTMLButtonElement>, serverId: string) => {
e.preventDefault();
e.stopPropagation();
handleFavorite?.({
id: [itemData.id],
isFavorite: itemData.userFavorite,
itemType,
serverId,
});
setIsFavorite(!isFavorite);
};
const handleContextMenu = useHandleGeneralContextMenu(
itemData,
itemType,
itemType === LibraryItem.ALBUM ? ALBUM_CONTEXT_MENU_ITEMS : ARTIST_CONTEXT_MENU_ITEMS,
);
handlePlayQueueAdd,
handleFavorite,
}: {
handleFavorite: (options: {
id: string[];
isFavorite: boolean;
itemType: LibraryItem;
serverId: string;
}) => void;
handlePlayQueueAdd?: (options: PlayQueueAddOptions) => void;
itemData: any;
itemType: LibraryItem;
}) => {
const [isFavorite, setIsFavorite] = useState(itemData?.userFavorite);
const playButtonBehavior = usePlayButtonBehavior();
return (
<>
{isFavorite ? <FavoriteBanner /> : null}
<GridCardControlsContainer
$isFavorite
className="card-controls"
>
<PlayButton onClick={handlePlay}>
<RiPlayFill size={25} />
</PlayButton>
<BottomControls>
<SecondaryButton
p={5}
variant="subtle"
onClick={(e) => handleFavorites(e, itemData?.serverId)}
>
<FavoriteWrapper isFavorite={itemData?.isFavorite}>
{isFavorite ? (
<RiHeartFill size={20} />
) : (
<RiHeartLine
color="white"
size={20}
/>
)}
</FavoriteWrapper>
</SecondaryButton>
<SecondaryButton
p={5}
variant="subtle"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleContextMenu(e, [itemData]);
}}
>
<RiMoreFill
color="white"
size={20}
/>
</SecondaryButton>
</BottomControls>
</GridCardControlsContainer>
</>
);
const handlePlay = async (e: MouseEvent<HTMLButtonElement>, playType?: Play) => {
e.preventDefault();
e.stopPropagation();
handlePlayQueueAdd?.({
byItemType: {
id: [itemData.id],
type: itemType,
},
playType: playType || playButtonBehavior,
});
};
const handleFavorites = async (e: MouseEvent<HTMLButtonElement>, serverId: string) => {
e.preventDefault();
e.stopPropagation();
handleFavorite?.({
id: [itemData.id],
isFavorite: itemData.userFavorite,
itemType,
serverId,
});
setIsFavorite(!isFavorite);
};
const handleContextMenu = useHandleGeneralContextMenu(
itemType,
itemType === LibraryItem.ALBUM ? ALBUM_CONTEXT_MENU_ITEMS : ARTIST_CONTEXT_MENU_ITEMS,
);
return (
<>
{isFavorite ? <FavoriteBanner /> : null}
<GridCardControlsContainer
$isFavorite
className="card-controls"
>
<PlayButton onClick={handlePlay}>
<RiPlayFill size={25} />
</PlayButton>
<BottomControls>
<SecondaryButton
p={5}
variant="subtle"
onClick={(e) => handleFavorites(e, itemData?.serverId)}
>
<FavoriteWrapper isFavorite={itemData?.isFavorite}>
{isFavorite ? (
<RiHeartFill size={20} />
) : (
<RiHeartLine
color="white"
size={20}
/>
)}
</FavoriteWrapper>
</SecondaryButton>
<SecondaryButton
p={5}
variant="subtle"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleContextMenu(e, [itemData]);
}}
>
<RiMoreFill
color="white"
size={20}
/>
</SecondaryButton>
</BottomControls>
</GridCardControlsContainer>
</>
);
};

View file

@ -6,58 +6,58 @@ import { PosterCard } from '/@/renderer/components/virtual-grid/grid-card/poster
import { GridCardData, ListDisplayType } from '/@/renderer/types';
export const GridCard = memo(({ data, index, style }: ListChildComponentProps) => {
const {
columnCount,
itemCount,
cardRows,
itemData,
itemType,
playButtonBehavior,
handlePlayQueueAdd,
handleFavorite,
route,
display,
} = data as GridCardData;
const {
columnCount,
itemCount,
cardRows,
itemData,
itemType,
playButtonBehavior,
handlePlayQueueAdd,
handleFavorite,
route,
display,
} = data as GridCardData;
const cards = [];
const startIndex = index * columnCount;
const stopIndex = Math.min(itemCount - 1, startIndex + columnCount - 1);
const cards = [];
const startIndex = index * columnCount;
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 columnCountInRow = stopIndex - startIndex + 1;
let columnCountToAdd = 0;
if (columnCountInRow !== columnCount) {
columnCountToAdd = columnCount - columnCountInRow;
}
const View = display === ListDisplayType.CARD ? DefaultCard : PosterCard;
for (let i = startIndex; i <= stopIndex + columnCountToAdd; i += 1) {
cards.push(
<View
key={`card-${i}-${index}`}
columnIndex={i}
controls={{
cardRows,
handleFavorite,
handlePlayQueueAdd,
itemType,
playButtonBehavior,
route,
}}
data={itemData[i]}
isHidden={i > stopIndex}
listChildProps={{ index }}
/>,
for (let i = startIndex; i <= stopIndex + columnCountToAdd; i += 1) {
cards.push(
<View
key={`card-${i}-${index}`}
columnIndex={i}
controls={{
cardRows,
handleFavorite,
handlePlayQueueAdd,
itemType,
playButtonBehavior,
route,
}}
data={itemData[i]}
isHidden={i > stopIndex}
listChildProps={{ index }}
/>,
);
}
return (
<div
style={{
...style,
display: 'flex',
}}
>
{cards}
</div>
);
}
return (
<div
style={{
...style,
display: 'flex',
}}
>
{cards}
</div>
);
}, areEqual);

View file

@ -11,65 +11,69 @@ import { GridCardControls } from '/@/renderer/components/virtual-grid/grid-card/
import { CardRow, PlayQueueAddOptions, Play, CardRoute } from '/@/renderer/types';
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;
isHidden?: boolean;
listChildProps: Omit<ListChildComponentProps, 'data' | 'style'>;
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;
isHidden?: boolean;
listChildProps: Omit<ListChildComponentProps, 'data' | 'style'>;
}
const PosterCardContainer = styled.div<{ $isHidden?: boolean }>`
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
margin: 1rem;
overflow: hidden;
opacity: ${({ $isHidden }) => ($isHidden ? 0 : 1)};
pointer-events: auto;
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
margin: 1rem;
overflow: hidden;
opacity: ${({ $isHidden }) => ($isHidden ? 0 : 1)};
pointer-events: auto;
.card-controls {
opacity: 0;
}
.card-controls {
opacity: 0;
}
`;
const LinkContainer = styled.div`
cursor: pointer;
cursor: pointer;
`;
const ImageContainer = styled.div<{ $isFavorite?: boolean }>`
position: relative;
display: flex;
align-items: center;
aspect-ratio: 1/1;
overflow: hidden;
background: var(--card-default-bg);
border-radius: var(--card-poster-radius);
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;
}
&::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;
}
${(props) =>
props.$isFavorite &&
`
${(props) =>
props.$isFavorite &&
`
&::after {
position: absolute;
top: -50px;
@ -84,140 +88,140 @@ const ImageContainer = styled.div<{ $isFavorite?: boolean }>`
}
`}
&:hover {
&::before {
opacity: 0.5;
&:hover {
&::before {
opacity: 0.5;
}
}
}
&:hover .card-controls {
opacity: 1;
}
&:hover .card-controls {
opacity: 1;
}
`;
const Image = styled(SimpleImg)`
width: 100%;
max-width: 100%;
height: 100% !important;
max-height: 100%;
border: 0;
width: 100%;
max-width: 100%;
height: 100% !important;
max-height: 100%;
border: 0;
img {
height: 100%;
object-fit: cover;
}
img {
height: 100%;
object-fit: cover;
}
`;
const DetailContainer = styled.div`
margin-top: 0.5rem;
margin-top: 0.5rem;
`;
export const PosterCard = ({
listChildProps,
data,
columnIndex,
controls,
isHidden,
listChildProps,
data,
columnIndex,
controls,
isHidden,
}: BaseGridCardProps) => {
const navigate = useNavigate();
const navigate = useNavigate();
if (data) {
const path = generatePath(
controls.route.route,
controls.route.slugs?.reduce((acc, slug) => {
return {
...acc,
[slug.slugProperty]: data[slug.idProperty],
};
}, {}),
);
if (data) {
const path = generatePath(
controls.route.route,
controls.route.slugs?.reduce((acc, slug) => {
return {
...acc,
[slug.slugProperty]: data[slug.idProperty],
};
}, {}),
);
let Placeholder = RiAlbumFill;
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;
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={`card-${columnIndex}-${listChildProps.index}`}>
<LinkContainer onClick={() => navigate(path)}>
<ImageContainer $isFavorite={data?.userFavorite}>
{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>
</LinkContainer>
<DetailContainer>
<CardRows
data={data}
rows={controls.cardRows}
/>
</DetailContainer>
</PosterCardContainer>
);
}
return (
<PosterCardContainer key={`card-${columnIndex}-${listChildProps.index}`}>
<LinkContainer onClick={() => navigate(path)}>
<ImageContainer $isFavorite={data?.userFavorite}>
{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>
</LinkContainer>
<DetailContainer>
<CardRows
data={data}
rows={controls.cardRows}
/>
</DetailContainer>
</PosterCardContainer>
);
}
return (
<PosterCardContainer
key={`card-${columnIndex}-${listChildProps.index}`}
$isHidden={isHidden}
>
<Skeleton
visible
radius="sm"
>
<ImageContainer />
</Skeleton>
<DetailContainer>
<Stack spacing="sm">
{controls.cardRows.map((row, index) => (
<PosterCardContainer
key={`card-${columnIndex}-${listChildProps.index}`}
$isHidden={isHidden}
>
<Skeleton
key={`${index}-${columnIndex}-${row.arrayProperty}`}
visible
height={14}
radius="sm"
/>
))}
</Stack>
</DetailContainer>
</PosterCardContainer>
);
visible
radius="sm"
>
<ImageContainer />
</Skeleton>
<DetailContainer>
<Stack spacing="sm">
{controls.cardRows.map((row, index) => (
<Skeleton
key={`${index}-${columnIndex}-${row.arrayProperty}`}
visible
height={14}
radius="sm"
/>
))}
</Stack>
</DetailContainer>
</PosterCardContainer>
);
};

View file

@ -9,119 +9,123 @@ import type { CardRow, ListDisplayType, CardRoute, PlayQueueAddOptions } from '/
import { Album, AlbumArtist, Artist, LibraryItem } from '/@/renderer/api/types';
const createItemData = memoize(
(
cardRows,
columnCount,
display,
itemCount,
itemData,
itemGap,
itemHeight,
itemType,
itemWidth,
route,
handlePlayQueueAdd,
handleFavorite,
) => ({
cardRows,
columnCount,
display,
handleFavorite,
handlePlayQueueAdd,
itemCount,
itemData,
itemGap,
itemHeight,
itemType,
itemWidth,
route,
}),
(
cardRows,
columnCount,
display,
itemCount,
itemData,
itemGap,
itemHeight,
itemType,
itemWidth,
route,
handlePlayQueueAdd,
handleFavorite,
) => ({
cardRows,
columnCount,
display,
handleFavorite,
handlePlayQueueAdd,
itemCount,
itemData,
itemGap,
itemHeight,
itemType,
itemWidth,
route,
}),
);
const createScrollHandler = memoize((onScroll) => debounce(onScroll, 250));
export const VirtualGridWrapper = ({
refInstance,
cardRows,
itemGap,
itemType,
itemWidth,
display,
itemHeight,
itemCount,
columnCount,
rowCount,
initialScrollOffset,
handleFavorite,
handlePlayQueueAdd,
itemData,
route,
onScroll,
height,
width,
...rest
}: Omit<FixedSizeListProps, 'ref' | 'itemSize' | 'children' | 'height' | 'width'> & {
cardRows: CardRow<Album | AlbumArtist | Artist>[];
columnCount: number;
display: ListDisplayType;
handleFavorite?: (options: { id: string[]; isFavorite: boolean; itemType: LibraryItem }) => void;
handlePlayQueueAdd?: (options: PlayQueueAddOptions) => void;
height?: number;
itemData: any[];
itemGap: number;
itemHeight: number;
itemType: LibraryItem;
itemWidth: number;
refInstance: Ref<any>;
route?: CardRoute;
rowCount: number;
width?: number;
}) => {
const memoizedItemData = createItemData(
refInstance,
cardRows,
columnCount,
display,
itemCount,
itemData,
itemGap,
itemHeight,
itemType,
itemWidth,
route,
handlePlayQueueAdd,
display,
itemHeight,
itemCount,
columnCount,
rowCount,
initialScrollOffset,
handleFavorite,
);
handlePlayQueueAdd,
itemData,
route,
onScroll,
height,
width,
...rest
}: Omit<FixedSizeListProps, 'ref' | 'itemSize' | 'children' | 'height' | 'width'> & {
cardRows: CardRow<Album | AlbumArtist | Artist>[];
columnCount: number;
display: ListDisplayType;
handleFavorite?: (options: {
id: string[];
isFavorite: boolean;
itemType: LibraryItem;
}) => void;
handlePlayQueueAdd?: (options: PlayQueueAddOptions) => void;
height?: number;
itemData: any[];
itemGap: number;
itemHeight: number;
itemType: LibraryItem;
itemWidth: number;
refInstance: Ref<any>;
route?: CardRoute;
rowCount: number;
width?: number;
}) => {
const memoizedItemData = createItemData(
cardRows,
columnCount,
display,
itemCount,
itemData,
itemGap,
itemHeight,
itemType,
itemWidth,
route,
handlePlayQueueAdd,
handleFavorite,
);
const memoizedOnScroll = createScrollHandler(onScroll);
const memoizedOnScroll = createScrollHandler(onScroll);
return (
<FixedSizeList
ref={refInstance}
{...rest}
height={(height && Number(height)) || 0}
initialScrollOffset={initialScrollOffset}
itemCount={rowCount}
itemData={memoizedItemData}
itemSize={itemHeight}
overscanCount={5}
width={(width && Number(width)) || 0}
onScroll={memoizedOnScroll}
>
{GridCard}
</FixedSizeList>
);
return (
<FixedSizeList
ref={refInstance}
{...rest}
height={(height && Number(height)) || 0}
initialScrollOffset={initialScrollOffset}
itemCount={rowCount}
itemData={memoizedItemData}
itemSize={itemHeight}
overscanCount={5}
width={(width && Number(width)) || 0}
onScroll={memoizedOnScroll}
>
{GridCard}
</FixedSizeList>
);
};
VirtualGridWrapper.defaultProps = {
route: undefined,
route: undefined,
};
export const VirtualGridContainer = styled.div`
display: flex;
flex-direction: column;
height: 100%;
display: flex;
flex-direction: column;
height: 100%;
`;
export const VirtualGridAutoSizerContainer = styled.div`
flex: 1;
flex: 1;
`;

View file

@ -1,11 +1,11 @@
import {
useState,
useRef,
useMemo,
useCallback,
forwardRef,
Ref,
useImperativeHandle,
useState,
useRef,
useMemo,
useCallback,
forwardRef,
Ref,
useImperativeHandle,
} from 'react';
import debounce from 'lodash/debounce';
import type { FixedSizeListProps } from 'react-window';
@ -16,167 +16,171 @@ import { ListDisplayType } from '/@/renderer/types';
import { LibraryItem } from '/@/renderer/api/types';
export type VirtualInfiniteGridRef = {
resetLoadMoreItemsCache: () => void;
scrollTo: (index: number) => void;
setItemData: (data: any[]) => void;
resetLoadMoreItemsCache: () => void;
scrollTo: (index: number) => void;
setItemData: (data: any[]) => void;
};
interface VirtualGridProps
extends Omit<FixedSizeListProps, 'children' | 'itemSize' | 'height' | 'width'> {
cardRows: CardRow<any>[];
display?: ListDisplayType;
fetchFn: (options: { columnCount: number; skip: number; take: number }) => Promise<any>;
handleFavorite?: (options: { id: string[]; isFavorite: boolean; itemType: LibraryItem }) => void;
handlePlayQueueAdd?: (options: PlayQueueAddOptions) => void;
height?: number;
itemGap: number;
itemSize: number;
itemType: LibraryItem;
loading?: boolean;
minimumBatchSize?: number;
route?: CardRoute;
width?: number;
extends Omit<FixedSizeListProps, 'children' | 'itemSize' | 'height' | 'width'> {
cardRows: CardRow<any>[];
display?: ListDisplayType;
fetchFn: (options: { columnCount: number; skip: number; take: number }) => Promise<any>;
handleFavorite?: (options: {
id: string[];
isFavorite: boolean;
itemType: LibraryItem;
}) => void;
handlePlayQueueAdd?: (options: PlayQueueAddOptions) => void;
height?: number;
itemGap: number;
itemSize: number;
itemType: LibraryItem;
loading?: boolean;
minimumBatchSize?: number;
route?: CardRoute;
width?: number;
}
export const VirtualInfiniteGrid = forwardRef(
(
{
itemCount,
itemGap,
itemSize,
itemType,
cardRows,
route,
onScroll,
display,
handlePlayQueueAdd,
minimumBatchSize,
fetchFn,
loading,
initialScrollOffset,
handleFavorite,
height,
width,
}: VirtualGridProps,
ref: Ref<VirtualInfiniteGridRef>,
) => {
const listRef = useRef<any>(null);
const loader = useRef<InfiniteLoader>(null);
(
{
itemCount,
itemGap,
itemSize,
itemType,
cardRows,
route,
onScroll,
display,
handlePlayQueueAdd,
minimumBatchSize,
fetchFn,
loading,
initialScrollOffset,
handleFavorite,
height,
width,
}: VirtualGridProps,
ref: Ref<VirtualInfiniteGridRef>,
) => {
const listRef = useRef<any>(null);
const loader = useRef<InfiniteLoader>(null);
const [itemData, setItemData] = useState<any[]>([]);
const [itemData, setItemData] = useState<any[]>([]);
const { itemHeight, rowCount, columnCount } = useMemo(() => {
const itemsPerRow = itemSize;
const widthPerItem = Number(width) / itemsPerRow;
const itemHeight = widthPerItem + cardRows.length * 26;
const { itemHeight, rowCount, columnCount } = useMemo(() => {
const itemsPerRow = itemSize;
const widthPerItem = Number(width) / itemsPerRow;
const itemHeight = widthPerItem + cardRows.length * 26;
return {
columnCount: itemsPerRow,
itemHeight,
rowCount: Math.ceil(itemCount / itemsPerRow),
};
}, [cardRows.length, itemCount, itemSize, width]);
return {
columnCount: itemsPerRow,
itemHeight,
rowCount: Math.ceil(itemCount / itemsPerRow),
};
}, [cardRows.length, itemCount, itemSize, width]);
const isItemLoaded = useCallback(
(index: number) => {
const itemIndex = index * columnCount;
const isItemLoaded = useCallback(
(index: number) => {
const itemIndex = index * columnCount;
return itemData[itemIndex] !== undefined;
},
[columnCount, itemData],
);
return itemData[itemIndex] !== undefined;
},
[columnCount, itemData],
);
const loadMoreItems = useCallback(
async (startIndex: number, stopIndex: number) => {
// Fixes a caching bug(?) when switching between filters and the itemCount increases
if (startIndex === 1) return;
const loadMoreItems = useCallback(
async (startIndex: number, stopIndex: number) => {
// Fixes a caching bug(?) when switching between filters and the itemCount increases
if (startIndex === 1) return;
// Need to multiply by columnCount due to the grid layout
const start = startIndex * columnCount;
const end = stopIndex * columnCount + columnCount;
// Need to multiply by columnCount due to the grid layout
const start = startIndex * columnCount;
const end = stopIndex * columnCount + columnCount;
const data = await fetchFn({
columnCount,
skip: start,
take: end - start,
});
const data = await fetchFn({
columnCount,
skip: start,
take: end - start,
});
const newData: any[] = [...itemData];
const newData: any[] = [...itemData];
let itemIndex = 0;
for (let rowIndex = start; rowIndex < end; rowIndex += 1) {
newData[rowIndex] = data.items[itemIndex];
itemIndex += 1;
}
let itemIndex = 0;
for (let rowIndex = start; rowIndex < end; rowIndex += 1) {
newData[rowIndex] = data.items[itemIndex];
itemIndex += 1;
}
setItemData(newData);
},
[columnCount, fetchFn, itemData, setItemData],
);
setItemData(newData);
},
[columnCount, fetchFn, itemData, setItemData],
);
const debouncedLoadMoreItems = debounce(loadMoreItems, 500);
const debouncedLoadMoreItems = debounce(loadMoreItems, 500);
useImperativeHandle(ref, () => ({
resetLoadMoreItemsCache: () => {
if (loader.current) {
loader.current.resetloadMoreItemsCache(false);
setItemData([]);
}
},
scrollTo: (index: number) => {
listRef?.current?.scrollToItem(index);
},
setItemData: (data: any[]) => {
setItemData(data);
},
}));
useImperativeHandle(ref, () => ({
resetLoadMoreItemsCache: () => {
if (loader.current) {
loader.current.resetloadMoreItemsCache(false);
setItemData([]);
}
},
scrollTo: (index: number) => {
listRef?.current?.scrollToItem(index);
},
setItemData: (data: any[]) => {
setItemData(data);
},
}));
if (loading) return null;
if (loading) return null;
return (
<>
<InfiniteLoader
ref={loader}
isItemLoaded={(index) => isItemLoaded(index)}
itemCount={itemCount || 0}
loadMoreItems={debouncedLoadMoreItems}
minimumBatchSize={minimumBatchSize}
threshold={30}
>
{({ onItemsRendered, ref: infiniteLoaderRef }) => (
<VirtualGridWrapper
cardRows={cardRows}
columnCount={columnCount}
display={display || ListDisplayType.CARD}
handleFavorite={handleFavorite}
handlePlayQueueAdd={handlePlayQueueAdd}
height={height}
initialScrollOffset={initialScrollOffset}
itemCount={itemCount || 0}
itemData={itemData}
itemGap={itemGap}
itemHeight={itemHeight}
itemType={itemType}
itemWidth={itemSize}
refInstance={(list) => {
infiniteLoaderRef(list);
listRef.current = list;
}}
route={route}
rowCount={rowCount}
width={width}
onItemsRendered={onItemsRendered}
onScroll={onScroll}
/>
)}
</InfiniteLoader>
</>
);
},
return (
<>
<InfiniteLoader
ref={loader}
isItemLoaded={(index) => isItemLoaded(index)}
itemCount={itemCount || 0}
loadMoreItems={debouncedLoadMoreItems}
minimumBatchSize={minimumBatchSize}
threshold={30}
>
{({ onItemsRendered, ref: infiniteLoaderRef }) => (
<VirtualGridWrapper
cardRows={cardRows}
columnCount={columnCount}
display={display || ListDisplayType.CARD}
handleFavorite={handleFavorite}
handlePlayQueueAdd={handlePlayQueueAdd}
height={height}
initialScrollOffset={initialScrollOffset}
itemCount={itemCount || 0}
itemData={itemData}
itemGap={itemGap}
itemHeight={itemHeight}
itemType={itemType}
itemWidth={itemSize}
refInstance={(list) => {
infiniteLoaderRef(list);
listRef.current = list;
}}
route={route}
rowCount={rowCount}
width={width}
onItemsRendered={onItemsRendered}
onScroll={onScroll}
/>
)}
</InfiniteLoader>
</>
);
},
);
VirtualInfiniteGrid.defaultProps = {
display: ListDisplayType.CARD,
minimumBatchSize: 20,
route: undefined,
display: ListDisplayType.CARD,
minimumBatchSize: 20,
route: undefined,
};