mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-09 20:29:36 +02:00
remove deprecated virtual grid
This commit is contained in:
@@ -1,98 +0,0 @@
|
|||||||
.container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
width: 100%;
|
|
||||||
height: calc(100% - 2rem);
|
|
||||||
overflow: hidden;
|
|
||||||
pointer-events: auto;
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: var(--theme-radius-md);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: var(--theme-colors-surface-hover);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.container.is-hidden {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inner-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
padding: 1rem;
|
|
||||||
overflow: hidden;
|
|
||||||
background: lighten(var(--theme-colors-surface), 3%);
|
|
||||||
|
|
||||||
.card-controls {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover .card-controls {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover * {
|
|
||||||
&::before {
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-container {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
height: 100%;
|
|
||||||
aspect-ratio: 1/1;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
z-index: 1;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
user-select: none;
|
|
||||||
content: '';
|
|
||||||
background: linear-gradient(0deg, rgb(0 0 0 / 100%) 35%, rgb(0 0 0 / 0%) 100%);
|
|
||||||
opacity: 0;
|
|
||||||
transition: all 0.2s ease-in-out;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-container.is-favorite {
|
|
||||||
&::after {
|
|
||||||
position: absolute;
|
|
||||||
top: -50px;
|
|
||||||
left: -50px;
|
|
||||||
width: 80px;
|
|
||||||
height: 80px;
|
|
||||||
pointer-events: none;
|
|
||||||
content: '';
|
|
||||||
background-color: var(--theme-colors-primary-filled);
|
|
||||||
box-shadow: 0 0 10px 8px rgb(0 0 0 / 80%);
|
|
||||||
transform: rotate(-45deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.image {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 100%;
|
|
||||||
height: 100% !important;
|
|
||||||
max-height: 100%;
|
|
||||||
border: 0;
|
|
||||||
border-radius: var(--theme-radius-md);
|
|
||||||
|
|
||||||
img {
|
|
||||||
height: 100%;
|
|
||||||
object-fit: var(--theme-image-fit);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-container {
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
}
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
import clsx from 'clsx';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { generatePath, useNavigate } from 'react-router-dom';
|
|
||||||
import { ListChildComponentProps } from 'react-window';
|
|
||||||
|
|
||||||
import styles from './default-card.module.css';
|
|
||||||
|
|
||||||
import { CardRows } from '/@/renderer/components/card/card-rows';
|
|
||||||
import { GridCardControls } from '/@/renderer/components/virtual-grid/grid-card/grid-card-controls';
|
|
||||||
import { Image } from '/@/shared/components/image/image';
|
|
||||||
import { Skeleton } from '/@/shared/components/skeleton/skeleton';
|
|
||||||
import { Stack } from '/@/shared/components/stack/stack';
|
|
||||||
import {
|
|
||||||
Album,
|
|
||||||
AlbumArtist,
|
|
||||||
Artist,
|
|
||||||
LibraryItem,
|
|
||||||
Playlist,
|
|
||||||
Song,
|
|
||||||
} from '/@/shared/types/domain-types';
|
|
||||||
import { CardRoute, CardRow, Play, PlayQueueAddOptions } from '/@/shared/types/types';
|
|
||||||
|
|
||||||
interface BaseGridCardProps {
|
|
||||||
columnIndex: number;
|
|
||||||
controls: {
|
|
||||||
cardRows: CardRow<Album | AlbumArtist | Artist | Playlist | Song>[];
|
|
||||||
handleFavorite: (options: {
|
|
||||||
id: string[];
|
|
||||||
isFavorite: boolean;
|
|
||||||
itemType: LibraryItem;
|
|
||||||
}) => void;
|
|
||||||
handlePlayQueueAdd: (options: PlayQueueAddOptions) => void;
|
|
||||||
itemGap: number;
|
|
||||||
itemType: LibraryItem;
|
|
||||||
playButtonBehavior: Play;
|
|
||||||
resetInfiniteLoaderCache: () => void;
|
|
||||||
route: CardRoute;
|
|
||||||
};
|
|
||||||
data: any;
|
|
||||||
isHidden?: boolean;
|
|
||||||
listChildProps: Omit<ListChildComponentProps, 'data' | 'style'>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DefaultCard = ({
|
|
||||||
columnIndex,
|
|
||||||
controls,
|
|
||||||
data,
|
|
||||||
isHidden,
|
|
||||||
listChildProps,
|
|
||||||
}: BaseGridCardProps) => {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
|
||||||
|
|
||||||
if (data) {
|
|
||||||
const path = generatePath(
|
|
||||||
controls.route.route as string,
|
|
||||||
controls.route.slugs?.reduce((acc, slug) => {
|
|
||||||
return {
|
|
||||||
...acc,
|
|
||||||
[slug.slugProperty]: data[slug.idProperty],
|
|
||||||
};
|
|
||||||
}, {}),
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={clsx(styles.container, isHidden && styles.isHidden)}
|
|
||||||
key={`card-${columnIndex}-${listChildProps.index}`}
|
|
||||||
onClick={() => navigate(path)}
|
|
||||||
onMouseEnter={() => setIsHovered(true)}
|
|
||||||
onMouseLeave={() => setIsHovered(false)}
|
|
||||||
style={{
|
|
||||||
margin: controls.itemGap,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className={styles.innerContainer}>
|
|
||||||
<div
|
|
||||||
className={clsx(
|
|
||||||
styles.imageContainer,
|
|
||||||
data?.userFavorite && styles.isFavorite,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Image className={styles.image} src={data?.imageUrl} />
|
|
||||||
<GridCardControls
|
|
||||||
handleFavorite={controls.handleFavorite}
|
|
||||||
handlePlayQueueAdd={controls.handlePlayQueueAdd}
|
|
||||||
isHovered={isHovered}
|
|
||||||
itemData={data}
|
|
||||||
itemType={controls.itemType}
|
|
||||||
resetInfiniteLoaderCache={controls.resetInfiniteLoaderCache}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className={styles.detailContainer}>
|
|
||||||
<CardRows data={data} rows={controls.cardRows} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={clsx(styles.container, isHidden && styles.isHidden)}
|
|
||||||
key={`card-${columnIndex}-${listChildProps.index}`}
|
|
||||||
style={{
|
|
||||||
margin: controls.itemGap,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className={styles.innerContainer}>
|
|
||||||
<div className={styles.imageContainer}>
|
|
||||||
<Skeleton className={styles.image} />
|
|
||||||
</div>
|
|
||||||
<div className={styles.detailContainer}>
|
|
||||||
<Stack gap="xs">
|
|
||||||
{(controls?.cardRows || []).map((row, index) => (
|
|
||||||
<Skeleton key={`${index}-${columnIndex}-${row.arrayProperty}`} />
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
.play-button {
|
|
||||||
position: absolute;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 50px;
|
|
||||||
height: 50px;
|
|
||||||
background-color: var(--theme-colors-white);
|
|
||||||
border: none;
|
|
||||||
border-radius: 50%;
|
|
||||||
opacity: 0.8;
|
|
||||||
transition: opacity 0.2s ease-in-out;
|
|
||||||
transition: scale 0.1s ease-in-out;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--theme-colors-white);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
svg {
|
|
||||||
fill: var(--theme-colors-black);
|
|
||||||
stroke: var(--theme-colors-black);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.secondary-button {
|
|
||||||
opacity: 0.8;
|
|
||||||
transition: opacity 0.2s ease-in-out;
|
|
||||||
transition: scale 0.2s linear;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-card-controls-container {
|
|
||||||
position: absolute;
|
|
||||||
z-index: 100;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.favorite-banner {
|
|
||||||
position: absolute;
|
|
||||||
top: -50px;
|
|
||||||
left: -50px;
|
|
||||||
width: 80px;
|
|
||||||
height: 80px;
|
|
||||||
pointer-events: none;
|
|
||||||
content: '';
|
|
||||||
background-color: var(--theme-colors-primary-filled);
|
|
||||||
box-shadow: 0 0 10px 8px rgb(0 0 0 / 80%);
|
|
||||||
transform: rotate(-45deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.favorite-wrapper {
|
|
||||||
svg {
|
|
||||||
fill: var(--theme-colors-primary-filled);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.bottom-controls {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
display: flex;
|
|
||||||
gap: var(--theme-spacing-md);
|
|
||||||
align-items: flex-end;
|
|
||||||
justify-content: flex-end;
|
|
||||||
width: 100%;
|
|
||||||
height: calc(100% / 3);
|
|
||||||
padding: 1rem 0.5rem;
|
|
||||||
}
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
import clsx from 'clsx';
|
|
||||||
import { MouseEvent, useState } from 'react';
|
|
||||||
|
|
||||||
import styles from './grid-card-controls.module.css';
|
|
||||||
|
|
||||||
import {
|
|
||||||
ALBUM_CONTEXT_MENU_ITEMS,
|
|
||||||
ARTIST_CONTEXT_MENU_ITEMS,
|
|
||||||
PLAYLIST_CONTEXT_MENU_ITEMS,
|
|
||||||
} from '/@/renderer/features/context-menu/context-menu-items';
|
|
||||||
import { useHandleGridContextMenu } from '/@/renderer/features/context-menu/hooks/use-handle-context-menu';
|
|
||||||
import { usePlayerContext } from '/@/renderer/features/player/context/player-context';
|
|
||||||
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
|
|
||||||
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
|
||||||
import { Button } from '/@/shared/components/button/button';
|
|
||||||
import { Icon } from '/@/shared/components/icon/icon';
|
|
||||||
import { LibraryItem } from '/@/shared/types/domain-types';
|
|
||||||
import { Play } from '/@/shared/types/types';
|
|
||||||
|
|
||||||
export const GridCardControls = ({
|
|
||||||
handleFavorite,
|
|
||||||
isHovered,
|
|
||||||
itemData,
|
|
||||||
itemType,
|
|
||||||
resetInfiniteLoaderCache,
|
|
||||||
}: {
|
|
||||||
handleFavorite: (options: {
|
|
||||||
id: string[];
|
|
||||||
isFavorite: boolean;
|
|
||||||
itemType: LibraryItem;
|
|
||||||
serverId: string;
|
|
||||||
}) => void;
|
|
||||||
isHovered?: boolean;
|
|
||||||
itemData: any;
|
|
||||||
itemType: LibraryItem;
|
|
||||||
resetInfiniteLoaderCache?: () => void;
|
|
||||||
}) => {
|
|
||||||
const [isFavorite, setIsFavorite] = useState(itemData?.userFavorite);
|
|
||||||
const playButtonBehavior = usePlayButtonBehavior();
|
|
||||||
|
|
||||||
const player = usePlayerContext();
|
|
||||||
|
|
||||||
const handlePlay = async (e: MouseEvent<HTMLButtonElement>, playType?: Play) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
player.addToQueueByFetch(
|
|
||||||
itemData._serverId,
|
|
||||||
[itemData.id],
|
|
||||||
itemType,
|
|
||||||
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 = useHandleGridContextMenu(
|
|
||||||
itemType,
|
|
||||||
itemType === LibraryItem.ALBUM
|
|
||||||
? ALBUM_CONTEXT_MENU_ITEMS
|
|
||||||
: itemType === LibraryItem.PLAYLIST
|
|
||||||
? PLAYLIST_CONTEXT_MENU_ITEMS
|
|
||||||
: ARTIST_CONTEXT_MENU_ITEMS,
|
|
||||||
resetInfiniteLoaderCache,
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{isFavorite ? <div className={styles.favoriteBanner} /> : null}
|
|
||||||
{isHovered && (
|
|
||||||
<div className={clsx(styles.gridCardControlsContainer)}>
|
|
||||||
<Button
|
|
||||||
classNames={{ root: styles.playButton }}
|
|
||||||
onClick={handlePlay}
|
|
||||||
variant="filled"
|
|
||||||
>
|
|
||||||
<Icon icon="mediaPlay" size="xl" />
|
|
||||||
</Button>
|
|
||||||
<div className={styles.bottomControls}>
|
|
||||||
{itemType !== LibraryItem.PLAYLIST && (
|
|
||||||
<ActionIcon
|
|
||||||
classNames={{ root: styles.secondaryButton }}
|
|
||||||
icon={isFavorite ? 'favorite' : 'favorite'}
|
|
||||||
iconProps={{
|
|
||||||
fill: isFavorite ? 'primary' : undefined,
|
|
||||||
}}
|
|
||||||
onClick={(e) => handleFavorites(e, itemData?.serverId)}
|
|
||||||
size="sm"
|
|
||||||
variant="transparent"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<ActionIcon
|
|
||||||
classNames={{ root: styles.secondaryButton }}
|
|
||||||
icon="ellipsisHorizontal"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
handleContextMenu(e, [itemData]);
|
|
||||||
}}
|
|
||||||
size="sm"
|
|
||||||
variant="transparent"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
import type { ListChildComponentProps } from 'react-window';
|
|
||||||
|
|
||||||
import { memo } from 'react';
|
|
||||||
import { areEqual } 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 { CardRow, GridCardData, ListDisplayType } from '/@/shared/types/types';
|
|
||||||
|
|
||||||
export const GridCard = memo(({ data, index, style }: ListChildComponentProps) => {
|
|
||||||
const {
|
|
||||||
cardRows,
|
|
||||||
columnCount,
|
|
||||||
display,
|
|
||||||
handleFavorite,
|
|
||||||
handlePlayQueueAdd,
|
|
||||||
itemCount,
|
|
||||||
itemData,
|
|
||||||
itemGap,
|
|
||||||
itemType,
|
|
||||||
playButtonBehavior,
|
|
||||||
resetInfiniteLoaderCache,
|
|
||||||
route,
|
|
||||||
} = data as GridCardData;
|
|
||||||
|
|
||||||
const cards: React.ReactNode[] = [];
|
|
||||||
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;
|
|
||||||
|
|
||||||
for (let i = startIndex; i <= stopIndex + columnCountToAdd; i += 1) {
|
|
||||||
cards.push(
|
|
||||||
<View
|
|
||||||
columnIndex={i}
|
|
||||||
controls={{
|
|
||||||
cardRows: cardRows as CardRow<any>[],
|
|
||||||
handleFavorite,
|
|
||||||
handlePlayQueueAdd,
|
|
||||||
itemGap,
|
|
||||||
itemType,
|
|
||||||
playButtonBehavior,
|
|
||||||
resetInfiniteLoaderCache,
|
|
||||||
route,
|
|
||||||
}}
|
|
||||||
data={itemData[i]}
|
|
||||||
isHidden={i > stopIndex}
|
|
||||||
key={`card-${i}-${index}`}
|
|
||||||
listChildProps={{ index }}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
...style,
|
|
||||||
display: 'flex',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{cards}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}, areEqual);
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
.container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
pointer-events: auto;
|
|
||||||
|
|
||||||
&:global(.card-controls) {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.container.hidden {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.link-container {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-container {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
aspect-ratio: 1/1;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
z-index: 1;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
user-select: none;
|
|
||||||
content: '';
|
|
||||||
background: linear-gradient(0deg, rgb(0 0 0 / 100%) 35%, rgb(0 0 0 / 0%) 100%);
|
|
||||||
opacity: 0;
|
|
||||||
transition: all 0.2s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
&::before {
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover .card-controls {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-container.is-favorite {
|
|
||||||
&::after {
|
|
||||||
position: absolute;
|
|
||||||
top: -50px;
|
|
||||||
left: -50px;
|
|
||||||
width: 80px;
|
|
||||||
height: 80px;
|
|
||||||
pointer-events: none;
|
|
||||||
content: '';
|
|
||||||
background-color: var(--theme-colors-primary-filled);
|
|
||||||
box-shadow: 0 0 10px 8px rgb(0 0 0 / 80%);
|
|
||||||
transform: rotate(-45deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.image {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 100%;
|
|
||||||
height: 100% !important;
|
|
||||||
max-height: 100%;
|
|
||||||
border: 0;
|
|
||||||
border-radius: var(--theme-radius-md);
|
|
||||||
|
|
||||||
img {
|
|
||||||
height: 100%;
|
|
||||||
object-fit: var(--theme-image-fit);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-container {
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.placeholder-wrapper {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
import clsx from 'clsx';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { generatePath, useNavigate } from 'react-router-dom';
|
|
||||||
import { ListChildComponentProps } from 'react-window';
|
|
||||||
|
|
||||||
import styles from './poster-card.module.css';
|
|
||||||
|
|
||||||
import { CardRows } from '/@/renderer/components/card/card-rows';
|
|
||||||
import { GridCardControls } from '/@/renderer/components/virtual-grid/grid-card/grid-card-controls';
|
|
||||||
import { Image } from '/@/shared/components/image/image';
|
|
||||||
import { Skeleton } from '/@/shared/components/skeleton/skeleton';
|
|
||||||
import { Stack } from '/@/shared/components/stack/stack';
|
|
||||||
import {
|
|
||||||
Album,
|
|
||||||
AlbumArtist,
|
|
||||||
Artist,
|
|
||||||
LibraryItem,
|
|
||||||
Playlist,
|
|
||||||
Song,
|
|
||||||
} from '/@/shared/types/domain-types';
|
|
||||||
import { CardRoute, CardRow, Play, PlayQueueAddOptions } from '/@/shared/types/types';
|
|
||||||
|
|
||||||
interface BaseGridCardProps {
|
|
||||||
columnIndex: number;
|
|
||||||
controls: {
|
|
||||||
cardRows: CardRow<Album | AlbumArtist | Artist | Playlist | Song>[];
|
|
||||||
handleFavorite: (options: {
|
|
||||||
id: string[];
|
|
||||||
isFavorite: boolean;
|
|
||||||
itemType: LibraryItem;
|
|
||||||
}) => void;
|
|
||||||
handlePlayQueueAdd: (options: PlayQueueAddOptions) => void;
|
|
||||||
itemGap: number;
|
|
||||||
itemType: LibraryItem;
|
|
||||||
playButtonBehavior: Play;
|
|
||||||
resetInfiniteLoaderCache: () => void;
|
|
||||||
route: CardRoute;
|
|
||||||
};
|
|
||||||
data: any;
|
|
||||||
isHidden?: boolean;
|
|
||||||
listChildProps: Omit<ListChildComponentProps, 'data' | 'style'>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PosterCard = ({
|
|
||||||
columnIndex,
|
|
||||||
controls,
|
|
||||||
data,
|
|
||||||
isHidden,
|
|
||||||
listChildProps,
|
|
||||||
}: BaseGridCardProps) => {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
|
||||||
|
|
||||||
if (data) {
|
|
||||||
const path = generatePath(
|
|
||||||
controls.route.route as string,
|
|
||||||
controls.route.slugs?.reduce((acc, slug) => {
|
|
||||||
return {
|
|
||||||
...acc,
|
|
||||||
[slug.slugProperty]: data[slug.idProperty],
|
|
||||||
};
|
|
||||||
}, {}),
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={styles.container}
|
|
||||||
key={`card-${columnIndex}-${listChildProps.index}`}
|
|
||||||
onMouseEnter={() => setIsHovered(true)}
|
|
||||||
onMouseLeave={() => setIsHovered(false)}
|
|
||||||
style={{
|
|
||||||
margin: controls.itemGap,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className={styles.linkContainer} onClick={() => navigate(path)}>
|
|
||||||
<div
|
|
||||||
className={`${styles.imageContainer} ${data?.userFavorite ? styles.isFavorite : ''}`}
|
|
||||||
>
|
|
||||||
<Image className={styles.image} src={data?.imageUrl} />
|
|
||||||
<GridCardControls
|
|
||||||
handleFavorite={controls.handleFavorite}
|
|
||||||
handlePlayQueueAdd={controls.handlePlayQueueAdd}
|
|
||||||
isHovered={isHovered}
|
|
||||||
itemData={data}
|
|
||||||
itemType={controls.itemType}
|
|
||||||
resetInfiniteLoaderCache={controls.resetInfiniteLoaderCache}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={styles.detailContainer}>
|
|
||||||
<CardRows data={data} rows={controls.cardRows} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={clsx(styles.container, isHidden && styles.hidden)}
|
|
||||||
key={`card-${columnIndex}-${listChildProps.index}`}
|
|
||||||
style={{
|
|
||||||
margin: controls.itemGap,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className={styles.imageContainer}>
|
|
||||||
<Skeleton className={styles.image} />
|
|
||||||
</div>
|
|
||||||
<div className={styles.detailContainer}>
|
|
||||||
<Stack gap="xs">
|
|
||||||
{(controls?.cardRows || []).map((row, index) => (
|
|
||||||
<Skeleton
|
|
||||||
className={styles.row}
|
|
||||||
key={`${index}-${columnIndex}-${row.arrayProperty}`}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
.virtual-grid-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.virtual-grid-auto-sizer-container {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
import type {
|
|
||||||
CardRoute,
|
|
||||||
CardRow,
|
|
||||||
ListDisplayType,
|
|
||||||
PlayQueueAddOptions,
|
|
||||||
} from '/@/shared/types/types';
|
|
||||||
import type { Ref } from 'react';
|
|
||||||
import type { FixedSizeListProps } from 'react-window';
|
|
||||||
|
|
||||||
import debounce from 'lodash/debounce';
|
|
||||||
import memoize from 'memoize-one';
|
|
||||||
import { FixedSizeList } from 'react-window';
|
|
||||||
|
|
||||||
import styles from './virtual-grid-wrapper.module.css';
|
|
||||||
|
|
||||||
import { GridCard } from '/@/renderer/components/virtual-grid/grid-card';
|
|
||||||
import { Album, AlbumArtist, Artist, LibraryItem } from '/@/shared/types/domain-types';
|
|
||||||
|
|
||||||
const createItemData = memoize(
|
|
||||||
(
|
|
||||||
cardRows,
|
|
||||||
columnCount,
|
|
||||||
display,
|
|
||||||
itemCount,
|
|
||||||
itemData,
|
|
||||||
itemGap,
|
|
||||||
itemHeight,
|
|
||||||
itemType,
|
|
||||||
itemWidth,
|
|
||||||
route,
|
|
||||||
handlePlayQueueAdd,
|
|
||||||
handleFavorite,
|
|
||||||
resetInfiniteLoaderCache,
|
|
||||||
) => ({
|
|
||||||
cardRows,
|
|
||||||
columnCount,
|
|
||||||
display,
|
|
||||||
handleFavorite,
|
|
||||||
handlePlayQueueAdd,
|
|
||||||
itemCount,
|
|
||||||
itemData,
|
|
||||||
itemGap,
|
|
||||||
itemHeight,
|
|
||||||
itemType,
|
|
||||||
itemWidth,
|
|
||||||
resetInfiniteLoaderCache,
|
|
||||||
route,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const createScrollHandler = memoize((onScroll) => debounce(onScroll, 250));
|
|
||||||
|
|
||||||
export const VirtualGridWrapper = ({
|
|
||||||
cardRows,
|
|
||||||
columnCount,
|
|
||||||
display,
|
|
||||||
handleFavorite,
|
|
||||||
handlePlayQueueAdd,
|
|
||||||
height,
|
|
||||||
initialScrollOffset,
|
|
||||||
itemCount,
|
|
||||||
itemData,
|
|
||||||
itemGap,
|
|
||||||
itemHeight,
|
|
||||||
itemType,
|
|
||||||
itemWidth,
|
|
||||||
onScroll,
|
|
||||||
refInstance,
|
|
||||||
resetInfiniteLoaderCache,
|
|
||||||
route,
|
|
||||||
rowCount,
|
|
||||||
width,
|
|
||||||
...rest
|
|
||||||
}: Omit<FixedSizeListProps, 'children' | 'height' | 'itemSize' | 'ref' | '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>;
|
|
||||||
resetInfiniteLoaderCache: () => void;
|
|
||||||
route?: CardRoute;
|
|
||||||
rowCount: number;
|
|
||||||
width?: number;
|
|
||||||
}) => {
|
|
||||||
const memoizedItemData = createItemData(
|
|
||||||
cardRows,
|
|
||||||
columnCount,
|
|
||||||
display,
|
|
||||||
itemCount,
|
|
||||||
itemData,
|
|
||||||
itemGap,
|
|
||||||
itemHeight,
|
|
||||||
itemType,
|
|
||||||
itemWidth,
|
|
||||||
route,
|
|
||||||
handlePlayQueueAdd,
|
|
||||||
handleFavorite,
|
|
||||||
resetInfiniteLoaderCache,
|
|
||||||
);
|
|
||||||
|
|
||||||
const memoizedOnScroll = createScrollHandler(onScroll);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FixedSizeList
|
|
||||||
ref={refInstance}
|
|
||||||
{...rest}
|
|
||||||
height={(height && Number(height)) || 0}
|
|
||||||
initialScrollOffset={initialScrollOffset}
|
|
||||||
itemCount={rowCount}
|
|
||||||
itemData={memoizedItemData}
|
|
||||||
itemSize={itemHeight}
|
|
||||||
onScroll={memoizedOnScroll}
|
|
||||||
overscanCount={5}
|
|
||||||
width={(width && Number(width)) || 0}
|
|
||||||
>
|
|
||||||
{GridCard}
|
|
||||||
</FixedSizeList>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface VirtualGridContainerProps {
|
|
||||||
children: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const VirtualGridContainer = ({ children }: VirtualGridContainerProps) => {
|
|
||||||
return <div className={styles.virtualGridContainer}>{children}</div>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const VirtualGridAutoSizerContainer = ({ children }: VirtualGridContainerProps) => {
|
|
||||||
return <div className={styles.virtualGridAutoSizerContainer}>{children}</div>;
|
|
||||||
};
|
|
||||||
@@ -1,213 +0,0 @@
|
|||||||
import type { CardRoute, CardRow, PlayQueueAddOptions } from '/@/shared/types/types';
|
|
||||||
import type { FixedSizeListProps } from 'react-window';
|
|
||||||
|
|
||||||
import debounce from 'lodash/debounce';
|
|
||||||
import {
|
|
||||||
forwardRef,
|
|
||||||
Ref,
|
|
||||||
useCallback,
|
|
||||||
useImperativeHandle,
|
|
||||||
useMemo,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
import InfiniteLoader from 'react-window-infinite-loader';
|
|
||||||
|
|
||||||
import { VirtualGridWrapper } from '/@/renderer/components/virtual-grid/virtual-grid-wrapper';
|
|
||||||
import { AnyLibraryItem, Genre, LibraryItem } from '/@/shared/types/domain-types';
|
|
||||||
import { ListDisplayType } from '/@/shared/types/types';
|
|
||||||
|
|
||||||
export type VirtualInfiniteGridRef = {
|
|
||||||
resetLoadMoreItemsCache: () => void;
|
|
||||||
scrollTo: (index: number) => void;
|
|
||||||
setItemData: (data: LibraryItemOrGenre[]) => void;
|
|
||||||
updateItemData: (rule: (item: LibraryItemOrGenre) => LibraryItemOrGenre) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
type LibraryItemOrGenre = AnyLibraryItem | Genre;
|
|
||||||
|
|
||||||
interface VirtualGridProps
|
|
||||||
extends Omit<FixedSizeListProps, 'children' | 'height' | 'itemSize' | 'width'> {
|
|
||||||
cardRows: CardRow<any>[];
|
|
||||||
display?: ListDisplayType;
|
|
||||||
fetchFn: (options: { columnCount: number; skip: number; take: number }) => Promise<any>;
|
|
||||||
fetchInitialData?: () => LibraryItemOrGenre[];
|
|
||||||
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(
|
|
||||||
(
|
|
||||||
{
|
|
||||||
cardRows,
|
|
||||||
display,
|
|
||||||
fetchFn,
|
|
||||||
fetchInitialData,
|
|
||||||
handleFavorite,
|
|
||||||
handlePlayQueueAdd,
|
|
||||||
height,
|
|
||||||
initialScrollOffset,
|
|
||||||
itemCount,
|
|
||||||
itemGap,
|
|
||||||
itemSize,
|
|
||||||
itemType,
|
|
||||||
loading,
|
|
||||||
minimumBatchSize,
|
|
||||||
onScroll,
|
|
||||||
route,
|
|
||||||
width,
|
|
||||||
}: VirtualGridProps,
|
|
||||||
ref: Ref<VirtualInfiniteGridRef>,
|
|
||||||
) => {
|
|
||||||
const listRef = useRef<any>(null);
|
|
||||||
const loader = useRef<InfiniteLoader>(null);
|
|
||||||
const minItemCount = useRef(0);
|
|
||||||
|
|
||||||
// itemData can be a sparse array. Treat the intermediate elements as being undefined
|
|
||||||
const [itemData, setItemData] = useState<Array<LibraryItemOrGenre | undefined>>(
|
|
||||||
fetchInitialData?.() || [],
|
|
||||||
);
|
|
||||||
|
|
||||||
const { columnCount, itemHeight, rowCount } = useMemo(() => {
|
|
||||||
const itemsPerRow = width ? Math.floor(width / (itemSize + itemGap * 2)) : 5;
|
|
||||||
const widthPerItem = Number(width) / itemsPerRow;
|
|
||||||
const itemHeight = widthPerItem + cardRows.length * 26;
|
|
||||||
|
|
||||||
return {
|
|
||||||
columnCount: itemsPerRow,
|
|
||||||
itemHeight,
|
|
||||||
rowCount: Math.ceil(itemCount / itemsPerRow),
|
|
||||||
};
|
|
||||||
}, [cardRows.length, itemCount, itemGap, itemSize, width]);
|
|
||||||
|
|
||||||
const isItemLoaded = useCallback(
|
|
||||||
(index: number) => {
|
|
||||||
const itemIndex = index * columnCount;
|
|
||||||
|
|
||||||
return itemData[itemIndex] !== undefined;
|
|
||||||
},
|
|
||||||
[columnCount, itemData],
|
|
||||||
);
|
|
||||||
|
|
||||||
const loadMoreItems = useCallback(
|
|
||||||
async (startIndex: number, stopIndex: number) => {
|
|
||||||
if (
|
|
||||||
// Fixes a caching bug(?) when switching between filters and the itemCount increases
|
|
||||||
startIndex === 1 ||
|
|
||||||
// Fixes a caching bug when refreshing items. Prevents a second
|
|
||||||
// refetch from happening if:
|
|
||||||
// 1: we are already in a refresh (-1)
|
|
||||||
// 2: we just had a refresh, and we are index 0
|
|
||||||
minItemCount.current === -1 ||
|
|
||||||
(minItemCount.current > 0 && startIndex === 0)
|
|
||||||
)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// 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,
|
|
||||||
});
|
|
||||||
|
|
||||||
setItemData((itemData) => {
|
|
||||||
const newData = [...itemData];
|
|
||||||
|
|
||||||
let itemIndex = 0;
|
|
||||||
for (let rowIndex = start; rowIndex < itemCount; rowIndex += 1) {
|
|
||||||
newData[rowIndex] = data.items[itemIndex];
|
|
||||||
itemIndex += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return newData;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[columnCount, fetchFn, itemCount],
|
|
||||||
);
|
|
||||||
|
|
||||||
const debouncedLoadMoreItems = debounce(loadMoreItems, 500);
|
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
|
||||||
resetLoadMoreItemsCache: () => {
|
|
||||||
if (loader.current) {
|
|
||||||
loader.current.resetloadMoreItemsCache(false);
|
|
||||||
minItemCount.current = -1;
|
|
||||||
setItemData([]);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
scrollTo: (index: number) => {
|
|
||||||
listRef?.current?.scrollToItem(index);
|
|
||||||
},
|
|
||||||
setItemData: (data: LibraryItemOrGenre[]) => {
|
|
||||||
setItemData(data);
|
|
||||||
minItemCount.current = data.length;
|
|
||||||
},
|
|
||||||
updateItemData: (rule) => {
|
|
||||||
setItemData((data) => data.map((item) => item && rule(item)));
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (loading) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<InfiniteLoader
|
|
||||||
isItemLoaded={(index) => isItemLoaded(index)}
|
|
||||||
itemCount={itemCount || 0}
|
|
||||||
loadMoreItems={debouncedLoadMoreItems}
|
|
||||||
minimumBatchSize={minimumBatchSize}
|
|
||||||
ref={loader}
|
|
||||||
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}
|
|
||||||
onItemsRendered={onItemsRendered}
|
|
||||||
onScroll={onScroll}
|
|
||||||
refInstance={(list) => {
|
|
||||||
infiniteLoaderRef(list);
|
|
||||||
listRef.current = list;
|
|
||||||
}}
|
|
||||||
resetInfiniteLoaderCache={() => {
|
|
||||||
if (loader.current) {
|
|
||||||
loader.current.resetloadMoreItemsCache(false);
|
|
||||||
setItemData([]);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
route={route}
|
|
||||||
rowCount={rowCount}
|
|
||||||
width={width}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</InfiniteLoader>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
Reference in New Issue
Block a user