Add dynamic grid sizing
This commit is contained in:
parent
1ab75f7187
commit
f09ad1da89
15 changed files with 127 additions and 52 deletions
|
@ -20,6 +20,7 @@ interface BaseGridCardProps {
|
|||
itemType: LibraryItem;
|
||||
}) => void;
|
||||
handlePlayQueueAdd: (options: PlayQueueAddOptions) => void;
|
||||
itemGap: number;
|
||||
itemType: LibraryItem;
|
||||
playButtonBehavior: Play;
|
||||
resetInfiniteLoaderCache: () => void;
|
||||
|
@ -30,12 +31,12 @@ interface BaseGridCardProps {
|
|||
listChildProps: Omit<ListChildComponentProps, 'data' | 'style'>;
|
||||
}
|
||||
|
||||
const DefaultCardContainer = styled.div<{ $isHidden?: boolean }>`
|
||||
const DefaultCardContainer = styled.div<{ $isHidden?: boolean; $itemGap: number }>`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: calc(100% - 2rem);
|
||||
margin: 0.5rem;
|
||||
margin: ${({ $itemGap }) => $itemGap}px;
|
||||
overflow: hidden;
|
||||
background: var(--card-default-bg);
|
||||
border-radius: var(--card-default-radius);
|
||||
|
@ -172,6 +173,7 @@ export const DefaultCard = ({
|
|||
return (
|
||||
<DefaultCardContainer
|
||||
key={`card-${columnIndex}-${listChildProps.index}`}
|
||||
$itemGap={controls.itemGap}
|
||||
onClick={() => navigate(path)}
|
||||
>
|
||||
<InnerCardContainer>
|
||||
|
@ -221,6 +223,7 @@ export const DefaultCard = ({
|
|||
<DefaultCardContainer
|
||||
key={`card-${columnIndex}-${listChildProps.index}`}
|
||||
$isHidden={isHidden}
|
||||
$itemGap={controls.itemGap}
|
||||
>
|
||||
<InnerCardContainer>
|
||||
<ImageContainer>
|
||||
|
|
|
@ -123,7 +123,7 @@ export const GridCardControls = ({
|
|||
handlePlayQueueAdd?: (options: PlayQueueAddOptions) => void;
|
||||
itemData: any;
|
||||
itemType: LibraryItem;
|
||||
resetInfiniteLoaderCache: () => void;
|
||||
resetInfiniteLoaderCache?: () => void;
|
||||
}) => {
|
||||
const [isFavorite, setIsFavorite] = useState(itemData?.userFavorite);
|
||||
const playButtonBehavior = usePlayButtonBehavior();
|
||||
|
|
|
@ -12,6 +12,7 @@ export const GridCard = memo(({ data, index, style }: ListChildComponentProps) =
|
|||
cardRows,
|
||||
itemData,
|
||||
itemType,
|
||||
itemGap,
|
||||
playButtonBehavior,
|
||||
handlePlayQueueAdd,
|
||||
handleFavorite,
|
||||
|
@ -40,6 +41,7 @@ export const GridCard = memo(({ data, index, style }: ListChildComponentProps) =
|
|||
cardRows,
|
||||
handleFavorite,
|
||||
handlePlayQueueAdd,
|
||||
itemGap,
|
||||
itemType,
|
||||
playButtonBehavior,
|
||||
resetInfiniteLoaderCache,
|
||||
|
|
|
@ -20,6 +20,7 @@ interface BaseGridCardProps {
|
|||
itemType: LibraryItem;
|
||||
}) => void;
|
||||
handlePlayQueueAdd: (options: PlayQueueAddOptions) => void;
|
||||
itemGap: number;
|
||||
itemType: LibraryItem;
|
||||
playButtonBehavior: Play;
|
||||
resetInfiniteLoaderCache: () => void;
|
||||
|
@ -30,12 +31,12 @@ interface BaseGridCardProps {
|
|||
listChildProps: Omit<ListChildComponentProps, 'data' | 'style'>;
|
||||
}
|
||||
|
||||
const PosterCardContainer = styled.div<{ $isHidden?: boolean }>`
|
||||
const PosterCardContainer = styled.div<{ $isHidden?: boolean; $itemGap: number }>`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 1rem;
|
||||
margin: ${({ $itemGap }) => $itemGap}px;
|
||||
overflow: hidden;
|
||||
opacity: ${({ $isHidden }) => ($isHidden ? 0 : 1)};
|
||||
pointer-events: auto;
|
||||
|
@ -158,7 +159,10 @@ export const PosterCard = ({
|
|||
}
|
||||
|
||||
return (
|
||||
<PosterCardContainer key={`card-${columnIndex}-${listChildProps.index}`}>
|
||||
<PosterCardContainer
|
||||
key={`card-${columnIndex}-${listChildProps.index}`}
|
||||
$itemGap={controls.itemGap}
|
||||
>
|
||||
<LinkContainer onClick={() => navigate(path)}>
|
||||
<ImageContainer $isFavorite={data?.userFavorite}>
|
||||
{data?.imageUrl ? (
|
||||
|
@ -205,6 +209,7 @@ export const PosterCard = ({
|
|||
<PosterCardContainer
|
||||
key={`card-${columnIndex}-${listChildProps.index}`}
|
||||
$isHidden={isHidden}
|
||||
$itemGap={controls.itemGap}
|
||||
>
|
||||
<Skeleton
|
||||
visible
|
||||
|
|
|
@ -72,7 +72,7 @@ export const VirtualInfiniteGrid = forwardRef(
|
|||
const [itemData, setItemData] = useState<any[]>(fetchInitialData?.() || []);
|
||||
|
||||
const { itemHeight, rowCount, columnCount } = useMemo(() => {
|
||||
const itemsPerRow = itemSize;
|
||||
const itemsPerRow = width ? Math.floor(width / itemSize) : 5;
|
||||
const widthPerItem = Number(width) / itemsPerRow;
|
||||
const itemHeight = widthPerItem + cardRows.length * 26;
|
||||
|
||||
|
|
|
@ -224,8 +224,8 @@ export const AlbumListGridView = ({ gridRef, itemCount }: any) => {
|
|||
height={height}
|
||||
initialScrollOffset={initialScrollOffset}
|
||||
itemCount={itemCount || 0}
|
||||
itemGap={20}
|
||||
itemSize={grid?.itemsPerRow || 5}
|
||||
itemGap={grid?.itemGap ?? 10}
|
||||
itemSize={grid?.itemSize || 200}
|
||||
itemType={LibraryItem.ALBUM}
|
||||
loading={itemCount === undefined || itemCount === null}
|
||||
minimumBatchSize={40}
|
||||
|
|
|
@ -207,12 +207,16 @@ export const AlbumListHeaderFilters = ({ gridRef, tableRef }: AlbumListHeaderFil
|
|||
|
||||
const handleItemSize = (e: number) => {
|
||||
if (isGrid) {
|
||||
setGrid({ data: { itemsPerRow: e }, key: pageKey });
|
||||
setGrid({ data: { itemSize: e }, key: pageKey });
|
||||
} else {
|
||||
setTable({ data: { rowHeight: e }, key: pageKey });
|
||||
}
|
||||
};
|
||||
|
||||
const handleItemGap = (e: number) => {
|
||||
setGrid({ data: { itemGap: e }, key: pageKey });
|
||||
};
|
||||
|
||||
const handleSetViewType = useCallback(
|
||||
(e: MouseEvent<HTMLButtonElement>) => {
|
||||
if (!e.currentTarget?.value) return;
|
||||
|
@ -449,17 +453,28 @@ export const AlbumListHeaderFilters = ({ gridRef, tableRef }: AlbumListHeaderFil
|
|||
Table (paginated)
|
||||
</DropdownMenu.Item> */}
|
||||
<DropdownMenu.Divider />
|
||||
<DropdownMenu.Label>
|
||||
{isGrid ? 'Items per row' : 'Item size'}
|
||||
</DropdownMenu.Label>
|
||||
<DropdownMenu.Label>Item size</DropdownMenu.Label>
|
||||
<DropdownMenu.Item closeMenuOnClick={false}>
|
||||
<Slider
|
||||
defaultValue={isGrid ? grid?.itemsPerRow || 0 : table.rowHeight}
|
||||
max={isGrid ? 14 : 100}
|
||||
min={isGrid ? 2 : 25}
|
||||
defaultValue={isGrid ? grid?.itemSize || 0 : table.rowHeight}
|
||||
max={isGrid ? 300 : 100}
|
||||
min={isGrid ? 150 : 25}
|
||||
onChangeEnd={handleItemSize}
|
||||
/>
|
||||
</DropdownMenu.Item>
|
||||
{isGrid && (
|
||||
<>
|
||||
<DropdownMenu.Label>Item gap</DropdownMenu.Label>
|
||||
<DropdownMenu.Item closeMenuOnClick={false}>
|
||||
<Slider
|
||||
defaultValue={grid?.itemGap || 0}
|
||||
max={30}
|
||||
min={0}
|
||||
onChangeEnd={handleItemGap}
|
||||
/>
|
||||
</DropdownMenu.Item>
|
||||
</>
|
||||
)}
|
||||
{(display === ListDisplayType.TABLE ||
|
||||
display === ListDisplayType.TABLE_PAGINATED) && (
|
||||
<>
|
||||
|
|
|
@ -158,8 +158,8 @@ export const AlbumArtistListGridView = ({ itemCount, gridRef }: AlbumArtistListG
|
|||
height={height}
|
||||
initialScrollOffset={grid?.scrollOffset || 0}
|
||||
itemCount={itemCount || 0}
|
||||
itemGap={20}
|
||||
itemSize={grid?.itemsPerRow || 5}
|
||||
itemGap={grid?.itemGap ?? 10}
|
||||
itemSize={grid?.itemSize || 200}
|
||||
itemType={LibraryItem.ALBUM_ARTIST}
|
||||
loading={itemCount === undefined || itemCount === null}
|
||||
minimumBatchSize={40}
|
||||
|
|
|
@ -83,10 +83,14 @@ export const AlbumArtistListHeaderFilters = ({
|
|||
if (display === ListDisplayType.TABLE || display === ListDisplayType.TABLE_PAGINATED) {
|
||||
setTable({ data: { rowHeight: e }, key: pageKey });
|
||||
} else {
|
||||
setGrid({ data: { itemsPerRow: e }, key: pageKey });
|
||||
setGrid({ data: { itemSize: e }, key: pageKey });
|
||||
}
|
||||
};
|
||||
|
||||
const handleItemGap = (e: number) => {
|
||||
setGrid({ data: { itemGap: e }, key: pageKey });
|
||||
};
|
||||
|
||||
const debouncedHandleItemSize = debounce(handleItemSize, 20);
|
||||
|
||||
const fetch = useCallback(
|
||||
|
@ -422,22 +426,33 @@ export const AlbumArtistListHeaderFilters = ({
|
|||
{display === ListDisplayType.CARD ||
|
||||
display === ListDisplayType.POSTER ? (
|
||||
<Slider
|
||||
defaultValue={grid?.itemsPerRow}
|
||||
label={null}
|
||||
max={10}
|
||||
min={2}
|
||||
defaultValue={grid?.itemSize}
|
||||
max={300}
|
||||
min={150}
|
||||
onChange={debouncedHandleItemSize}
|
||||
/>
|
||||
) : (
|
||||
<Slider
|
||||
defaultValue={table.rowHeight}
|
||||
label={null}
|
||||
max={100}
|
||||
min={30}
|
||||
onChange={debouncedHandleItemSize}
|
||||
/>
|
||||
)}
|
||||
</DropdownMenu.Item>
|
||||
{isGrid && (
|
||||
<>
|
||||
<DropdownMenu.Label>Item gap</DropdownMenu.Label>
|
||||
<DropdownMenu.Item closeMenuOnClick={false}>
|
||||
<Slider
|
||||
defaultValue={grid?.itemGap || 0}
|
||||
max={30}
|
||||
min={0}
|
||||
onChangeEnd={handleItemGap}
|
||||
/>
|
||||
</DropdownMenu.Item>
|
||||
</>
|
||||
)}
|
||||
{!isGrid && (
|
||||
<>
|
||||
<DropdownMenu.Label>Table Columns</DropdownMenu.Label>
|
||||
|
|
|
@ -87,7 +87,7 @@ export const useHandleGeneralContextMenu = (
|
|||
export const useHandleGridContextMenu = (
|
||||
itemType: LibraryItem,
|
||||
contextMenuItems: SetContextMenuItems,
|
||||
resetGridCache: () => void,
|
||||
resetGridCache?: () => void,
|
||||
context?: any,
|
||||
) => {
|
||||
const handleContextMenu = (
|
||||
|
|
|
@ -97,8 +97,8 @@ export const GenreListGridView = ({ gridRef, itemCount }: any) => {
|
|||
height={height}
|
||||
initialScrollOffset={initialScrollOffset}
|
||||
itemCount={itemCount || 0}
|
||||
itemGap={20}
|
||||
itemSize={grid?.itemsPerRow || 5}
|
||||
itemGap={grid?.itemGap ?? 10}
|
||||
itemSize={grid?.itemSize || 200}
|
||||
itemType={LibraryItem.GENRE}
|
||||
loading={itemCount === undefined || itemCount === null}
|
||||
minimumBatchSize={40}
|
||||
|
|
|
@ -109,12 +109,16 @@ export const GenreListHeaderFilters = ({ gridRef, tableRef }: GenreListHeaderFil
|
|||
|
||||
const handleItemSize = (e: number) => {
|
||||
if (isGrid) {
|
||||
setGrid({ data: { itemsPerRow: e }, key: pageKey });
|
||||
setGrid({ data: { itemSize: e }, key: pageKey });
|
||||
} else {
|
||||
setTable({ data: { rowHeight: e }, key: pageKey });
|
||||
}
|
||||
};
|
||||
|
||||
const handleItemGap = (e: number) => {
|
||||
setGrid({ data: { itemGap: e }, key: pageKey });
|
||||
};
|
||||
|
||||
const handleSetViewType = useCallback(
|
||||
(e: MouseEvent<HTMLButtonElement>) => {
|
||||
if (!e.currentTarget?.value) return;
|
||||
|
@ -256,17 +260,28 @@ export const GenreListHeaderFilters = ({ gridRef, tableRef }: GenreListHeaderFil
|
|||
Table
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Divider />
|
||||
<DropdownMenu.Label>
|
||||
{isGrid ? 'Items per row' : 'Item size'}
|
||||
</DropdownMenu.Label>
|
||||
<DropdownMenu.Label>Item size</DropdownMenu.Label>
|
||||
<DropdownMenu.Item closeMenuOnClick={false}>
|
||||
<Slider
|
||||
defaultValue={isGrid ? grid?.itemsPerRow || 0 : table.rowHeight}
|
||||
max={isGrid ? 14 : 100}
|
||||
min={isGrid ? 2 : 25}
|
||||
defaultValue={isGrid ? grid?.itemSize || 0 : table.rowHeight}
|
||||
max={isGrid ? 300 : 100}
|
||||
min={isGrid ? 150 : 25}
|
||||
onChangeEnd={handleItemSize}
|
||||
/>
|
||||
</DropdownMenu.Item>
|
||||
{isGrid && (
|
||||
<>
|
||||
<DropdownMenu.Label>Item gap</DropdownMenu.Label>
|
||||
<DropdownMenu.Item closeMenuOnClick={false}>
|
||||
<Slider
|
||||
defaultValue={grid?.itemGap || 0}
|
||||
max={30}
|
||||
min={0}
|
||||
onChangeEnd={handleItemGap}
|
||||
/>
|
||||
</DropdownMenu.Item>
|
||||
</>
|
||||
)}
|
||||
{(display === ListDisplayType.TABLE ||
|
||||
display === ListDisplayType.TABLE_PAGINATED) && (
|
||||
<>
|
||||
|
|
|
@ -141,8 +141,8 @@ export const PlaylistListGridView = ({ gridRef, itemCount }: PlaylistListGridVie
|
|||
height={height}
|
||||
initialScrollOffset={grid?.scrollOffset || 0}
|
||||
itemCount={itemCount || 0}
|
||||
itemGap={20}
|
||||
itemSize={grid?.itemsPerRow || 5}
|
||||
itemGap={grid?.itemGap ?? 10}
|
||||
itemSize={grid?.itemSize || 200}
|
||||
itemType={LibraryItem.PLAYLIST}
|
||||
loading={itemCount === undefined || itemCount === null}
|
||||
minimumBatchSize={40}
|
||||
|
|
|
@ -230,12 +230,16 @@ export const PlaylistListHeaderFilters = ({
|
|||
|
||||
const handleItemSize = (e: number) => {
|
||||
if (isGrid) {
|
||||
setGrid({ data: { itemsPerRow: e }, key: pageKey });
|
||||
setGrid({ data: { itemSize: e }, key: pageKey });
|
||||
} else {
|
||||
setTable({ data: { rowHeight: e }, key: pageKey });
|
||||
}
|
||||
};
|
||||
|
||||
const handleItemGap = (e: number) => {
|
||||
setGrid({ data: { itemGap: e }, key: pageKey });
|
||||
};
|
||||
|
||||
const handleRefresh = () => {
|
||||
queryClient.invalidateQueries(queryKeys.playlists.list(server?.id || '', filter));
|
||||
handleFilterChange(filter);
|
||||
|
@ -344,16 +348,27 @@ export const PlaylistListHeaderFilters = ({
|
|||
Table (paginated)
|
||||
</DropdownMenu.Item> */}
|
||||
<DropdownMenu.Divider />
|
||||
<DropdownMenu.Label>
|
||||
{isGrid ? 'Items per row' : 'Item size'}
|
||||
</DropdownMenu.Label>
|
||||
<DropdownMenu.Label>Item size</DropdownMenu.Label>
|
||||
<DropdownMenu.Item closeMenuOnClick={false}>
|
||||
<Slider
|
||||
defaultValue={isGrid ? grid?.itemsPerRow || 0 : table.rowHeight}
|
||||
max={isGrid ? 14 : 100}
|
||||
min={isGrid ? 2 : 25}
|
||||
defaultValue={isGrid ? grid?.itemSize || 0 : table.rowHeight}
|
||||
max={isGrid ? 300 : 100}
|
||||
min={isGrid ? 150 : 25}
|
||||
onChangeEnd={handleItemSize}
|
||||
/>
|
||||
{isGrid && (
|
||||
<>
|
||||
<DropdownMenu.Label>Item gap</DropdownMenu.Label>
|
||||
<DropdownMenu.Item closeMenuOnClick={false}>
|
||||
<Slider
|
||||
defaultValue={grid?.itemGap || 0}
|
||||
max={30}
|
||||
min={0}
|
||||
onChangeEnd={handleItemGap}
|
||||
/>
|
||||
</DropdownMenu.Item>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenu.Item>
|
||||
{!isGrid && (
|
||||
<>
|
||||
|
|
|
@ -43,6 +43,8 @@ export type ListTableProps = {
|
|||
} & DataTableProps;
|
||||
|
||||
export type ListGridProps = {
|
||||
itemGap?: number;
|
||||
itemSize?: number;
|
||||
itemsPerRow?: number;
|
||||
scrollOffset?: number;
|
||||
};
|
||||
|
@ -222,9 +224,12 @@ export const useListStore = create<ListSlice>()(
|
|||
state.detail[args.key] = {
|
||||
filter: {} as FilterType,
|
||||
grid: {
|
||||
itemsPerRow:
|
||||
itemGap:
|
||||
state.item[page as keyof ListState['item']].grid
|
||||
?.itemsPerRow || 5,
|
||||
?.itemGap || 10,
|
||||
itemSize:
|
||||
state.item[page as keyof ListState['item']].grid
|
||||
?.itemSize || 5,
|
||||
scrollOffset: 0,
|
||||
},
|
||||
table: {
|
||||
|
@ -342,7 +347,7 @@ export const useListStore = create<ListSlice>()(
|
|||
sortBy: AlbumListSort.RECENTLY_ADDED,
|
||||
sortOrder: SortOrder.DESC,
|
||||
},
|
||||
grid: { itemsPerRow: 5, scrollOffset: 0 },
|
||||
grid: { itemGap: 10, itemSize: 200, scrollOffset: 0 },
|
||||
table: {
|
||||
autoFit: true,
|
||||
columns: [
|
||||
|
@ -383,7 +388,7 @@ export const useListStore = create<ListSlice>()(
|
|||
sortBy: AlbumArtistListSort.NAME,
|
||||
sortOrder: SortOrder.DESC,
|
||||
},
|
||||
grid: { itemsPerRow: 5, scrollOffset: 0 },
|
||||
grid: { itemGap: 10, itemSize: 200, scrollOffset: 0 },
|
||||
table: {
|
||||
autoFit: true,
|
||||
columns: [
|
||||
|
@ -412,7 +417,7 @@ export const useListStore = create<ListSlice>()(
|
|||
sortBy: AlbumListSort.RECENTLY_ADDED,
|
||||
sortOrder: SortOrder.DESC,
|
||||
},
|
||||
grid: { itemsPerRow: 5, scrollOffset: 0 },
|
||||
grid: { itemGap: 10, itemSize: 200, scrollOffset: 0 },
|
||||
table: {
|
||||
autoFit: true,
|
||||
columns: [
|
||||
|
@ -453,7 +458,7 @@ export const useListStore = create<ListSlice>()(
|
|||
sortBy: SongListSort.RECENTLY_ADDED,
|
||||
sortOrder: SortOrder.DESC,
|
||||
},
|
||||
grid: { itemsPerRow: 5, scrollOffset: 0 },
|
||||
grid: { itemGap: 10, itemSize: 200, scrollOffset: 0 },
|
||||
table: {
|
||||
autoFit: true,
|
||||
columns: [
|
||||
|
@ -510,7 +515,7 @@ export const useListStore = create<ListSlice>()(
|
|||
sortBy: GenreListSort.NAME,
|
||||
sortOrder: SortOrder.ASC,
|
||||
},
|
||||
grid: { itemsPerRow: 5, scrollOffset: 0 },
|
||||
grid: { itemGap: 10, itemSize: 200, scrollOffset: 0 },
|
||||
table: {
|
||||
autoFit: true,
|
||||
columns: [
|
||||
|
@ -539,7 +544,7 @@ export const useListStore = create<ListSlice>()(
|
|||
sortBy: PlaylistListSort.NAME,
|
||||
sortOrder: SortOrder.DESC,
|
||||
},
|
||||
grid: { scrollOffset: 0, size: 0 },
|
||||
grid: { itemGap: 10, itemSize: 200, scrollOffset: 0 },
|
||||
table: {
|
||||
autoFit: true,
|
||||
columns: [
|
||||
|
@ -572,7 +577,7 @@ export const useListStore = create<ListSlice>()(
|
|||
sortBy: SongListSort.RECENTLY_ADDED,
|
||||
sortOrder: SortOrder.DESC,
|
||||
},
|
||||
grid: { itemsPerRow: 5, scrollOffset: 0 },
|
||||
grid: { itemGap: 10, itemSize: 200, scrollOffset: 0 },
|
||||
table: {
|
||||
autoFit: true,
|
||||
columns: [
|
||||
|
|
Reference in a new issue