Change grid size to items per row
This commit is contained in:
parent
e47fcfc62e
commit
219a9ed613
8 changed files with 125 additions and 97 deletions
|
@ -1,4 +1,4 @@
|
||||||
import { generatePath, Link } from 'react-router-dom';
|
import { generatePath, useNavigate } from 'react-router-dom';
|
||||||
import { ListChildComponentProps } from 'react-window';
|
import { ListChildComponentProps } from 'react-window';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { Album, AlbumArtist, Artist, LibraryItem } from '/@/renderer/api/types';
|
import { Album, AlbumArtist, Artist, LibraryItem } from '/@/renderer/api/types';
|
||||||
|
@ -31,6 +31,7 @@ const DefaultCardContainer = styled.div<{ $isHidden?: boolean }>`
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: var(--card-default-bg);
|
background: var(--card-default-bg);
|
||||||
border-radius: var(--card-default-radius);
|
border-radius: var(--card-default-radius);
|
||||||
|
cursor: pointer;
|
||||||
opacity: ${({ $isHidden }) => ($isHidden ? 0 : 1)};
|
opacity: ${({ $isHidden }) => ($isHidden ? 0 : 1)};
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
|
|
||||||
|
@ -106,42 +107,44 @@ export const DefaultCard = ({
|
||||||
controls,
|
controls,
|
||||||
isHidden,
|
isHidden,
|
||||||
}: BaseGridCardProps) => {
|
}: BaseGridCardProps) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
|
const path = generatePath(
|
||||||
|
controls.route.route,
|
||||||
|
controls.route.slugs?.reduce((acc, slug) => {
|
||||||
|
return {
|
||||||
|
...acc,
|
||||||
|
[slug.slugProperty]: data[slug.idProperty],
|
||||||
|
};
|
||||||
|
}, {}),
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DefaultCardContainer>
|
<DefaultCardContainer
|
||||||
<Link
|
key={`card-${columnIndex}-${listChildProps.index}`}
|
||||||
tabIndex={0}
|
onClick={() => navigate(path)}
|
||||||
to={generatePath(
|
>
|
||||||
controls.route.route,
|
<InnerCardContainer>
|
||||||
controls.route.slugs?.reduce((acc, slug) => {
|
<ImageContainer>
|
||||||
return {
|
<Image
|
||||||
...acc,
|
placeholder={data?.imagePlaceholderUrl || 'var(--placeholder-bg)'}
|
||||||
[slug.slugProperty]: data[slug.idProperty],
|
src={data?.imageUrl}
|
||||||
};
|
/>
|
||||||
}, {}),
|
<GridCardControls
|
||||||
)}
|
handleFavorite={controls.handleFavorite}
|
||||||
>
|
handlePlayQueueAdd={controls.handlePlayQueueAdd}
|
||||||
<InnerCardContainer key={`card-${columnIndex}-${listChildProps.index}`}>
|
itemData={data}
|
||||||
<ImageContainer>
|
itemType={controls.itemType}
|
||||||
<Image
|
/>
|
||||||
placeholder={data?.imagePlaceholderUrl || 'var(--placeholder-bg)'}
|
</ImageContainer>
|
||||||
src={data?.imageUrl}
|
<DetailContainer>
|
||||||
/>
|
<CardRows
|
||||||
<GridCardControls
|
data={data}
|
||||||
handleFavorite={controls.handleFavorite}
|
rows={controls.cardRows}
|
||||||
handlePlayQueueAdd={controls.handlePlayQueueAdd}
|
/>
|
||||||
itemData={data}
|
</DetailContainer>
|
||||||
itemType={controls.itemType}
|
</InnerCardContainer>
|
||||||
/>
|
|
||||||
</ImageContainer>
|
|
||||||
<DetailContainer>
|
|
||||||
<CardRows
|
|
||||||
data={data}
|
|
||||||
rows={controls.cardRows}
|
|
||||||
/>
|
|
||||||
</DetailContainer>
|
|
||||||
</InnerCardContainer>
|
|
||||||
</Link>
|
|
||||||
</DefaultCardContainer>
|
</DefaultCardContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { generatePath, Link } from 'react-router-dom';
|
import { Stack } from '@mantine/core';
|
||||||
|
import { generatePath, useNavigate } from 'react-router-dom';
|
||||||
import { ListChildComponentProps } from 'react-window';
|
import { ListChildComponentProps } from 'react-window';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { Album, AlbumArtist, Artist, LibraryItem } from '/@/renderer/api/types';
|
import { Album, AlbumArtist, Artist, LibraryItem } from '/@/renderer/api/types';
|
||||||
|
@ -27,7 +28,7 @@ const PosterCardContainer = styled.div<{ $isHidden?: boolean }>`
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
margin: 0.5rem;
|
margin: 1rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
opacity: ${({ $isHidden }) => ($isHidden ? 0 : 1)};
|
opacity: ${({ $isHidden }) => ($isHidden ? 0 : 1)};
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
|
@ -35,16 +36,10 @@ const PosterCardContainer = styled.div<{ $isHidden?: boolean }>`
|
||||||
.card-controls {
|
.card-controls {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
&:hover .card-controls {
|
const LinkContainer = styled.div`
|
||||||
opacity: 1;
|
cursor: pointer;
|
||||||
}
|
|
||||||
|
|
||||||
&:hover * {
|
|
||||||
&::before {
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ImageContainer = styled.div`
|
const ImageContainer = styled.div`
|
||||||
|
@ -70,6 +65,16 @@ const ImageContainer = styled.div`
|
||||||
content: '';
|
content: '';
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
&::before {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .card-controls {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Image = styled.img`
|
const Image = styled.img`
|
||||||
|
@ -91,21 +96,22 @@ export const PosterCard = ({
|
||||||
controls,
|
controls,
|
||||||
isHidden,
|
isHidden,
|
||||||
}: BaseGridCardProps) => {
|
}: BaseGridCardProps) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
|
const path = generatePath(
|
||||||
|
controls.route.route,
|
||||||
|
controls.route.slugs?.reduce((acc, slug) => {
|
||||||
|
return {
|
||||||
|
...acc,
|
||||||
|
[slug.slugProperty]: data[slug.idProperty],
|
||||||
|
};
|
||||||
|
}, {}),
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PosterCardContainer key={`card-${columnIndex}-${listChildProps.index}`}>
|
<PosterCardContainer key={`card-${columnIndex}-${listChildProps.index}`}>
|
||||||
<Link
|
<LinkContainer onClick={() => navigate(path)}>
|
||||||
tabIndex={0}
|
|
||||||
to={generatePath(
|
|
||||||
controls.route.route,
|
|
||||||
controls.route.slugs?.reduce((acc, slug) => {
|
|
||||||
return {
|
|
||||||
...acc,
|
|
||||||
[slug.slugProperty]: data[slug.idProperty],
|
|
||||||
};
|
|
||||||
}, {}),
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<ImageContainer>
|
<ImageContainer>
|
||||||
<Image
|
<Image
|
||||||
placeholder={data?.imagePlaceholderUrl || 'var(--card-default-bg)'}
|
placeholder={data?.imagePlaceholderUrl || 'var(--card-default-bg)'}
|
||||||
|
@ -118,7 +124,7 @@ export const PosterCard = ({
|
||||||
itemType={controls.itemType}
|
itemType={controls.itemType}
|
||||||
/>
|
/>
|
||||||
</ImageContainer>
|
</ImageContainer>
|
||||||
</Link>
|
</LinkContainer>
|
||||||
<DetailContainer>
|
<DetailContainer>
|
||||||
<CardRows
|
<CardRows
|
||||||
data={data}
|
data={data}
|
||||||
|
@ -134,13 +140,24 @@ export const PosterCard = ({
|
||||||
key={`card-${columnIndex}-${listChildProps.index}`}
|
key={`card-${columnIndex}-${listChildProps.index}`}
|
||||||
$isHidden={isHidden}
|
$isHidden={isHidden}
|
||||||
>
|
>
|
||||||
<ImageContainer>
|
<Skeleton
|
||||||
<Skeleton
|
visible
|
||||||
visible
|
radius="sm"
|
||||||
radius="sm"
|
>
|
||||||
/>
|
<ImageContainer />
|
||||||
</ImageContainer>
|
</Skeleton>
|
||||||
<DetailContainer />
|
<DetailContainer>
|
||||||
|
<Stack spacing="sm">
|
||||||
|
{controls.cardRows.map((row) => (
|
||||||
|
<Skeleton
|
||||||
|
key={row.arrayProperty}
|
||||||
|
visible
|
||||||
|
height={14}
|
||||||
|
radius="sm"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</DetailContainer>
|
||||||
</PosterCardContainer>
|
</PosterCardContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -56,20 +56,17 @@ export const VirtualInfiniteGrid = forwardRef(
|
||||||
const listRef = useRef<any>(null);
|
const listRef = useRef<any>(null);
|
||||||
const loader = useRef<InfiniteLoader>(null);
|
const loader = useRef<InfiniteLoader>(null);
|
||||||
|
|
||||||
const sz = itemSize / 2;
|
|
||||||
|
|
||||||
const { itemHeight, rowCount, columnCount } = useMemo(() => {
|
const { itemHeight, rowCount, columnCount } = useMemo(() => {
|
||||||
const itemsPerRow = Math.floor(Number(width) / sz!) - 1;
|
const itemsPerRow = itemSize;
|
||||||
const widthPerItem = Number(width) / itemsPerRow - 10;
|
const widthPerItem = Number(width) / itemsPerRow;
|
||||||
const itemHeight = widthPerItem + cardRows.length * 26;
|
const itemHeight = widthPerItem + cardRows.length * 26;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
columnCount: itemsPerRow,
|
columnCount: itemsPerRow,
|
||||||
itemHeight,
|
itemHeight,
|
||||||
itemWidth: sz,
|
|
||||||
rowCount: Math.ceil(itemCount / itemsPerRow),
|
rowCount: Math.ceil(itemCount / itemsPerRow),
|
||||||
};
|
};
|
||||||
}, [cardRows.length, itemCount, sz, width]);
|
}, [cardRows.length, itemCount, itemSize, width]);
|
||||||
|
|
||||||
const isItemLoaded = useCallback(
|
const isItemLoaded = useCallback(
|
||||||
(index: number) => {
|
(index: number) => {
|
||||||
|
|
|
@ -318,7 +318,7 @@ export const AlbumListContent = ({ itemCount, gridRef, tableRef }: AlbumListCont
|
||||||
itemCount={itemCount || 0}
|
itemCount={itemCount || 0}
|
||||||
itemData={itemData}
|
itemData={itemData}
|
||||||
itemGap={20}
|
itemGap={20}
|
||||||
itemSize={150 + (grid?.size || 0)}
|
itemSize={grid?.itemsPerRow || 5}
|
||||||
itemType={LibraryItem.ALBUM}
|
itemType={LibraryItem.ALBUM}
|
||||||
loading={itemCount === undefined || itemCount === null}
|
loading={itemCount === undefined || itemCount === null}
|
||||||
minimumBatchSize={40}
|
minimumBatchSize={40}
|
||||||
|
|
|
@ -331,7 +331,7 @@ export const AlbumListHeaderFilters = ({
|
||||||
if (display === ListDisplayType.TABLE || display === ListDisplayType.TABLE_PAGINATED) {
|
if (display === ListDisplayType.TABLE || display === ListDisplayType.TABLE_PAGINATED) {
|
||||||
setTable({ data: { rowHeight: e }, key: 'album' });
|
setTable({ data: { rowHeight: e }, key: 'album' });
|
||||||
} else {
|
} else {
|
||||||
setGrid({ data: { size: e }, key: 'album' });
|
setGrid({ data: { itemsPerRow: e }, key: 'album' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -564,17 +564,21 @@ export const AlbumListHeaderFilters = ({
|
||||||
Table (paginated)
|
Table (paginated)
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
<DropdownMenu.Divider />
|
<DropdownMenu.Divider />
|
||||||
<DropdownMenu.Label>Item size</DropdownMenu.Label>
|
<DropdownMenu.Label>
|
||||||
|
{display === ListDisplayType.CARD || display === ListDisplayType.POSTER
|
||||||
|
? 'Items per row'
|
||||||
|
: 'Item size'}
|
||||||
|
</DropdownMenu.Label>
|
||||||
<DropdownMenu.Item closeMenuOnClick={false}>
|
<DropdownMenu.Item closeMenuOnClick={false}>
|
||||||
<Slider
|
<Slider
|
||||||
defaultValue={
|
defaultValue={
|
||||||
display === ListDisplayType.CARD || display === ListDisplayType.POSTER
|
display === ListDisplayType.CARD || display === ListDisplayType.POSTER
|
||||||
? grid?.size || 0
|
? grid?.itemsPerRow || 0
|
||||||
: table.rowHeight
|
: table.rowHeight
|
||||||
}
|
}
|
||||||
label={null}
|
label={null}
|
||||||
max={400}
|
max={14}
|
||||||
min={-25}
|
min={2}
|
||||||
onChange={debouncedHandleItemSize}
|
onChange={debouncedHandleItemSize}
|
||||||
/>
|
/>
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
|
|
|
@ -276,7 +276,7 @@ export const AlbumArtistListContent = ({ gridRef, tableRef }: AlbumArtistListCon
|
||||||
itemCount={checkAlbumArtistList?.data?.totalRecordCount || 0}
|
itemCount={checkAlbumArtistList?.data?.totalRecordCount || 0}
|
||||||
itemData={itemData}
|
itemData={itemData}
|
||||||
itemGap={20}
|
itemGap={20}
|
||||||
itemSize={150 + (grid?.size || 0)}
|
itemSize={grid?.itemsPerRow || 5}
|
||||||
itemType={LibraryItem.ALBUM_ARTIST}
|
itemType={LibraryItem.ALBUM_ARTIST}
|
||||||
loading={checkAlbumArtistList.isLoading}
|
loading={checkAlbumArtistList.isLoading}
|
||||||
minimumBatchSize={40}
|
minimumBatchSize={40}
|
||||||
|
|
|
@ -96,7 +96,7 @@ export const AlbumArtistListHeaderFilters = ({
|
||||||
if (display === ListDisplayType.TABLE || display === ListDisplayType.TABLE_PAGINATED) {
|
if (display === ListDisplayType.TABLE || display === ListDisplayType.TABLE_PAGINATED) {
|
||||||
setTable({ data: { rowHeight: e }, key: pageKey });
|
setTable({ data: { rowHeight: e }, key: pageKey });
|
||||||
} else {
|
} else {
|
||||||
setGrid({ data: { size: e }, key: pageKey });
|
setGrid({ data: { itemsPerRow: e }, key: pageKey });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -432,17 +432,23 @@ export const AlbumArtistListHeaderFilters = ({
|
||||||
<DropdownMenu.Divider />
|
<DropdownMenu.Divider />
|
||||||
<DropdownMenu.Label>Item size</DropdownMenu.Label>
|
<DropdownMenu.Label>Item size</DropdownMenu.Label>
|
||||||
<DropdownMenu.Item closeMenuOnClick={false}>
|
<DropdownMenu.Item closeMenuOnClick={false}>
|
||||||
<Slider
|
{display === ListDisplayType.CARD || display === ListDisplayType.POSTER ? (
|
||||||
defaultValue={
|
<Slider
|
||||||
display === ListDisplayType.CARD || display === ListDisplayType.POSTER
|
defaultValue={grid?.itemsPerRow}
|
||||||
? grid?.size
|
label={null}
|
||||||
: table.rowHeight
|
max={10}
|
||||||
}
|
min={2}
|
||||||
label={null}
|
onChange={debouncedHandleItemSize}
|
||||||
max={400}
|
/>
|
||||||
min={-50}
|
) : (
|
||||||
onChange={debouncedHandleItemSize}
|
<Slider
|
||||||
/>
|
defaultValue={table.rowHeight}
|
||||||
|
label={null}
|
||||||
|
max={100}
|
||||||
|
min={30}
|
||||||
|
onChange={debouncedHandleItemSize}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
{(display === ListDisplayType.TABLE || display === ListDisplayType.TABLE_PAGINATED) && (
|
{(display === ListDisplayType.TABLE || display === ListDisplayType.TABLE_PAGINATED) && (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -34,8 +34,8 @@ type ListTableProps = {
|
||||||
} & DataTableProps;
|
} & DataTableProps;
|
||||||
|
|
||||||
type ListGridProps = {
|
type ListGridProps = {
|
||||||
|
itemsPerRow?: number;
|
||||||
scrollOffset?: number;
|
scrollOffset?: number;
|
||||||
size?: number;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type ItemProps<TFilter = any> = {
|
type ItemProps<TFilter = any> = {
|
||||||
|
@ -165,8 +165,9 @@ export const useListStore = create<ListSlice>()(
|
||||||
state.detail[args.key] = {
|
state.detail[args.key] = {
|
||||||
filter: {} as FilterType,
|
filter: {} as FilterType,
|
||||||
grid: {
|
grid: {
|
||||||
|
itemsPerRow:
|
||||||
|
state.item[page as keyof ListState['item']].grid?.itemsPerRow || 5,
|
||||||
scrollOffset: 0,
|
scrollOffset: 0,
|
||||||
size: state.item[page as keyof ListState['item']].grid?.size || 200,
|
|
||||||
},
|
},
|
||||||
table: {
|
table: {
|
||||||
pagination: {
|
pagination: {
|
||||||
|
@ -282,7 +283,7 @@ export const useListStore = create<ListSlice>()(
|
||||||
sortBy: AlbumListSort.RECENTLY_ADDED,
|
sortBy: AlbumListSort.RECENTLY_ADDED,
|
||||||
sortOrder: SortOrder.DESC,
|
sortOrder: SortOrder.DESC,
|
||||||
},
|
},
|
||||||
grid: { scrollOffset: 0, size: 200 },
|
grid: { itemsPerRow: 5, scrollOffset: 0 },
|
||||||
table: {
|
table: {
|
||||||
autoFit: true,
|
autoFit: true,
|
||||||
columns: [
|
columns: [
|
||||||
|
@ -323,7 +324,7 @@ export const useListStore = create<ListSlice>()(
|
||||||
sortBy: AlbumArtistListSort.NAME,
|
sortBy: AlbumArtistListSort.NAME,
|
||||||
sortOrder: SortOrder.DESC,
|
sortOrder: SortOrder.DESC,
|
||||||
},
|
},
|
||||||
grid: { scrollOffset: 0, size: 200 },
|
grid: { itemsPerRow: 5, scrollOffset: 0 },
|
||||||
table: {
|
table: {
|
||||||
autoFit: true,
|
autoFit: true,
|
||||||
columns: [
|
columns: [
|
||||||
|
@ -409,7 +410,7 @@ export const useListStore = create<ListSlice>()(
|
||||||
sortBy: SongListSort.RECENTLY_ADDED,
|
sortBy: SongListSort.RECENTLY_ADDED,
|
||||||
sortOrder: SortOrder.DESC,
|
sortOrder: SortOrder.DESC,
|
||||||
},
|
},
|
||||||
grid: { scrollOffset: 0, size: 0 },
|
grid: { itemsPerRow: 5, scrollOffset: 0 },
|
||||||
table: {
|
table: {
|
||||||
autoFit: true,
|
autoFit: true,
|
||||||
columns: [
|
columns: [
|
||||||
|
@ -470,7 +471,7 @@ export const useListStore = create<ListSlice>()(
|
||||||
Object.entries(state).filter(([key]) => !['detail'].includes(key)),
|
Object.entries(state).filter(([key]) => !['detail'].includes(key)),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
version: 1,
|
version: 2,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
Reference in a new issue