Add dynamic grid sizing

This commit is contained in:
jeffvli 2023-08-07 14:42:47 -07:00
parent 1ab75f7187
commit f09ad1da89
15 changed files with 127 additions and 52 deletions

View file

@ -20,6 +20,7 @@ interface BaseGridCardProps {
itemType: LibraryItem; itemType: LibraryItem;
}) => void; }) => void;
handlePlayQueueAdd: (options: PlayQueueAddOptions) => void; handlePlayQueueAdd: (options: PlayQueueAddOptions) => void;
itemGap: number;
itemType: LibraryItem; itemType: LibraryItem;
playButtonBehavior: Play; playButtonBehavior: Play;
resetInfiniteLoaderCache: () => void; resetInfiniteLoaderCache: () => void;
@ -30,12 +31,12 @@ interface BaseGridCardProps {
listChildProps: Omit<ListChildComponentProps, 'data' | 'style'>; listChildProps: Omit<ListChildComponentProps, 'data' | 'style'>;
} }
const DefaultCardContainer = styled.div<{ $isHidden?: boolean }>` const DefaultCardContainer = styled.div<{ $isHidden?: boolean; $itemGap: number }>`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 100%; width: 100%;
height: calc(100% - 2rem); height: calc(100% - 2rem);
margin: 0.5rem; margin: ${({ $itemGap }) => $itemGap}px;
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);
@ -172,6 +173,7 @@ export const DefaultCard = ({
return ( return (
<DefaultCardContainer <DefaultCardContainer
key={`card-${columnIndex}-${listChildProps.index}`} key={`card-${columnIndex}-${listChildProps.index}`}
$itemGap={controls.itemGap}
onClick={() => navigate(path)} onClick={() => navigate(path)}
> >
<InnerCardContainer> <InnerCardContainer>
@ -221,6 +223,7 @@ export const DefaultCard = ({
<DefaultCardContainer <DefaultCardContainer
key={`card-${columnIndex}-${listChildProps.index}`} key={`card-${columnIndex}-${listChildProps.index}`}
$isHidden={isHidden} $isHidden={isHidden}
$itemGap={controls.itemGap}
> >
<InnerCardContainer> <InnerCardContainer>
<ImageContainer> <ImageContainer>

View file

@ -123,7 +123,7 @@ export const GridCardControls = ({
handlePlayQueueAdd?: (options: PlayQueueAddOptions) => void; handlePlayQueueAdd?: (options: PlayQueueAddOptions) => void;
itemData: any; itemData: any;
itemType: LibraryItem; itemType: LibraryItem;
resetInfiniteLoaderCache: () => void; resetInfiniteLoaderCache?: () => void;
}) => { }) => {
const [isFavorite, setIsFavorite] = useState(itemData?.userFavorite); const [isFavorite, setIsFavorite] = useState(itemData?.userFavorite);
const playButtonBehavior = usePlayButtonBehavior(); const playButtonBehavior = usePlayButtonBehavior();

View file

@ -12,6 +12,7 @@ export const GridCard = memo(({ data, index, style }: ListChildComponentProps) =
cardRows, cardRows,
itemData, itemData,
itemType, itemType,
itemGap,
playButtonBehavior, playButtonBehavior,
handlePlayQueueAdd, handlePlayQueueAdd,
handleFavorite, handleFavorite,
@ -40,6 +41,7 @@ export const GridCard = memo(({ data, index, style }: ListChildComponentProps) =
cardRows, cardRows,
handleFavorite, handleFavorite,
handlePlayQueueAdd, handlePlayQueueAdd,
itemGap,
itemType, itemType,
playButtonBehavior, playButtonBehavior,
resetInfiniteLoaderCache, resetInfiniteLoaderCache,

View file

@ -20,6 +20,7 @@ interface BaseGridCardProps {
itemType: LibraryItem; itemType: LibraryItem;
}) => void; }) => void;
handlePlayQueueAdd: (options: PlayQueueAddOptions) => void; handlePlayQueueAdd: (options: PlayQueueAddOptions) => void;
itemGap: number;
itemType: LibraryItem; itemType: LibraryItem;
playButtonBehavior: Play; playButtonBehavior: Play;
resetInfiniteLoaderCache: () => void; resetInfiniteLoaderCache: () => void;
@ -30,12 +31,12 @@ interface BaseGridCardProps {
listChildProps: Omit<ListChildComponentProps, 'data' | 'style'>; listChildProps: Omit<ListChildComponentProps, 'data' | 'style'>;
} }
const PosterCardContainer = styled.div<{ $isHidden?: boolean }>` const PosterCardContainer = styled.div<{ $isHidden?: boolean; $itemGap: number }>`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 100%; width: 100%;
height: 100%; height: 100%;
margin: 1rem; margin: ${({ $itemGap }) => $itemGap}px;
overflow: hidden; overflow: hidden;
opacity: ${({ $isHidden }) => ($isHidden ? 0 : 1)}; opacity: ${({ $isHidden }) => ($isHidden ? 0 : 1)};
pointer-events: auto; pointer-events: auto;
@ -158,7 +159,10 @@ export const PosterCard = ({
} }
return ( return (
<PosterCardContainer key={`card-${columnIndex}-${listChildProps.index}`}> <PosterCardContainer
key={`card-${columnIndex}-${listChildProps.index}`}
$itemGap={controls.itemGap}
>
<LinkContainer onClick={() => navigate(path)}> <LinkContainer onClick={() => navigate(path)}>
<ImageContainer $isFavorite={data?.userFavorite}> <ImageContainer $isFavorite={data?.userFavorite}>
{data?.imageUrl ? ( {data?.imageUrl ? (
@ -205,6 +209,7 @@ export const PosterCard = ({
<PosterCardContainer <PosterCardContainer
key={`card-${columnIndex}-${listChildProps.index}`} key={`card-${columnIndex}-${listChildProps.index}`}
$isHidden={isHidden} $isHidden={isHidden}
$itemGap={controls.itemGap}
> >
<Skeleton <Skeleton
visible visible

View file

@ -72,7 +72,7 @@ export const VirtualInfiniteGrid = forwardRef(
const [itemData, setItemData] = useState<any[]>(fetchInitialData?.() || []); const [itemData, setItemData] = useState<any[]>(fetchInitialData?.() || []);
const { itemHeight, rowCount, columnCount } = useMemo(() => { const { itemHeight, rowCount, columnCount } = useMemo(() => {
const itemsPerRow = itemSize; const itemsPerRow = width ? Math.floor(width / itemSize) : 5;
const widthPerItem = Number(width) / itemsPerRow; const widthPerItem = Number(width) / itemsPerRow;
const itemHeight = widthPerItem + cardRows.length * 26; const itemHeight = widthPerItem + cardRows.length * 26;

View file

@ -224,8 +224,8 @@ export const AlbumListGridView = ({ gridRef, itemCount }: any) => {
height={height} height={height}
initialScrollOffset={initialScrollOffset} initialScrollOffset={initialScrollOffset}
itemCount={itemCount || 0} itemCount={itemCount || 0}
itemGap={20} itemGap={grid?.itemGap ?? 10}
itemSize={grid?.itemsPerRow || 5} itemSize={grid?.itemSize || 200}
itemType={LibraryItem.ALBUM} itemType={LibraryItem.ALBUM}
loading={itemCount === undefined || itemCount === null} loading={itemCount === undefined || itemCount === null}
minimumBatchSize={40} minimumBatchSize={40}

View file

@ -207,12 +207,16 @@ export const AlbumListHeaderFilters = ({ gridRef, tableRef }: AlbumListHeaderFil
const handleItemSize = (e: number) => { const handleItemSize = (e: number) => {
if (isGrid) { if (isGrid) {
setGrid({ data: { itemsPerRow: e }, key: pageKey }); setGrid({ data: { itemSize: e }, key: pageKey });
} else { } else {
setTable({ data: { rowHeight: e }, key: pageKey }); setTable({ data: { rowHeight: e }, key: pageKey });
} }
}; };
const handleItemGap = (e: number) => {
setGrid({ data: { itemGap: e }, key: pageKey });
};
const handleSetViewType = useCallback( const handleSetViewType = useCallback(
(e: MouseEvent<HTMLButtonElement>) => { (e: MouseEvent<HTMLButtonElement>) => {
if (!e.currentTarget?.value) return; if (!e.currentTarget?.value) return;
@ -449,17 +453,28 @@ export const AlbumListHeaderFilters = ({ gridRef, tableRef }: AlbumListHeaderFil
Table (paginated) Table (paginated)
</DropdownMenu.Item> */} </DropdownMenu.Item> */}
<DropdownMenu.Divider /> <DropdownMenu.Divider />
<DropdownMenu.Label> <DropdownMenu.Label>Item size</DropdownMenu.Label>
{isGrid ? 'Items per row' : 'Item size'}
</DropdownMenu.Label>
<DropdownMenu.Item closeMenuOnClick={false}> <DropdownMenu.Item closeMenuOnClick={false}>
<Slider <Slider
defaultValue={isGrid ? grid?.itemsPerRow || 0 : table.rowHeight} defaultValue={isGrid ? grid?.itemSize || 0 : table.rowHeight}
max={isGrid ? 14 : 100} max={isGrid ? 300 : 100}
min={isGrid ? 2 : 25} min={isGrid ? 150 : 25}
onChangeEnd={handleItemSize} onChangeEnd={handleItemSize}
/> />
</DropdownMenu.Item> </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 ||
display === ListDisplayType.TABLE_PAGINATED) && ( display === ListDisplayType.TABLE_PAGINATED) && (
<> <>

View file

@ -158,8 +158,8 @@ export const AlbumArtistListGridView = ({ itemCount, gridRef }: AlbumArtistListG
height={height} height={height}
initialScrollOffset={grid?.scrollOffset || 0} initialScrollOffset={grid?.scrollOffset || 0}
itemCount={itemCount || 0} itemCount={itemCount || 0}
itemGap={20} itemGap={grid?.itemGap ?? 10}
itemSize={grid?.itemsPerRow || 5} itemSize={grid?.itemSize || 200}
itemType={LibraryItem.ALBUM_ARTIST} itemType={LibraryItem.ALBUM_ARTIST}
loading={itemCount === undefined || itemCount === null} loading={itemCount === undefined || itemCount === null}
minimumBatchSize={40} minimumBatchSize={40}

View file

@ -83,10 +83,14 @@ 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: { 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 debouncedHandleItemSize = debounce(handleItemSize, 20);
const fetch = useCallback( const fetch = useCallback(
@ -422,22 +426,33 @@ export const AlbumArtistListHeaderFilters = ({
{display === ListDisplayType.CARD || {display === ListDisplayType.CARD ||
display === ListDisplayType.POSTER ? ( display === ListDisplayType.POSTER ? (
<Slider <Slider
defaultValue={grid?.itemsPerRow} defaultValue={grid?.itemSize}
label={null} max={300}
max={10} min={150}
min={2}
onChange={debouncedHandleItemSize} onChange={debouncedHandleItemSize}
/> />
) : ( ) : (
<Slider <Slider
defaultValue={table.rowHeight} defaultValue={table.rowHeight}
label={null}
max={100} max={100}
min={30} min={30}
onChange={debouncedHandleItemSize} onChange={debouncedHandleItemSize}
/> />
)} )}
</DropdownMenu.Item> </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 && ( {!isGrid && (
<> <>
<DropdownMenu.Label>Table Columns</DropdownMenu.Label> <DropdownMenu.Label>Table Columns</DropdownMenu.Label>

View file

@ -87,7 +87,7 @@ export const useHandleGeneralContextMenu = (
export const useHandleGridContextMenu = ( export const useHandleGridContextMenu = (
itemType: LibraryItem, itemType: LibraryItem,
contextMenuItems: SetContextMenuItems, contextMenuItems: SetContextMenuItems,
resetGridCache: () => void, resetGridCache?: () => void,
context?: any, context?: any,
) => { ) => {
const handleContextMenu = ( const handleContextMenu = (

View file

@ -97,8 +97,8 @@ export const GenreListGridView = ({ gridRef, itemCount }: any) => {
height={height} height={height}
initialScrollOffset={initialScrollOffset} initialScrollOffset={initialScrollOffset}
itemCount={itemCount || 0} itemCount={itemCount || 0}
itemGap={20} itemGap={grid?.itemGap ?? 10}
itemSize={grid?.itemsPerRow || 5} itemSize={grid?.itemSize || 200}
itemType={LibraryItem.GENRE} itemType={LibraryItem.GENRE}
loading={itemCount === undefined || itemCount === null} loading={itemCount === undefined || itemCount === null}
minimumBatchSize={40} minimumBatchSize={40}

View file

@ -109,12 +109,16 @@ export const GenreListHeaderFilters = ({ gridRef, tableRef }: GenreListHeaderFil
const handleItemSize = (e: number) => { const handleItemSize = (e: number) => {
if (isGrid) { if (isGrid) {
setGrid({ data: { itemsPerRow: e }, key: pageKey }); setGrid({ data: { itemSize: e }, key: pageKey });
} else { } else {
setTable({ data: { rowHeight: e }, key: pageKey }); setTable({ data: { rowHeight: e }, key: pageKey });
} }
}; };
const handleItemGap = (e: number) => {
setGrid({ data: { itemGap: e }, key: pageKey });
};
const handleSetViewType = useCallback( const handleSetViewType = useCallback(
(e: MouseEvent<HTMLButtonElement>) => { (e: MouseEvent<HTMLButtonElement>) => {
if (!e.currentTarget?.value) return; if (!e.currentTarget?.value) return;
@ -256,17 +260,28 @@ export const GenreListHeaderFilters = ({ gridRef, tableRef }: GenreListHeaderFil
Table Table
</DropdownMenu.Item> </DropdownMenu.Item>
<DropdownMenu.Divider /> <DropdownMenu.Divider />
<DropdownMenu.Label> <DropdownMenu.Label>Item size</DropdownMenu.Label>
{isGrid ? 'Items per row' : 'Item size'}
</DropdownMenu.Label>
<DropdownMenu.Item closeMenuOnClick={false}> <DropdownMenu.Item closeMenuOnClick={false}>
<Slider <Slider
defaultValue={isGrid ? grid?.itemsPerRow || 0 : table.rowHeight} defaultValue={isGrid ? grid?.itemSize || 0 : table.rowHeight}
max={isGrid ? 14 : 100} max={isGrid ? 300 : 100}
min={isGrid ? 2 : 25} min={isGrid ? 150 : 25}
onChangeEnd={handleItemSize} onChangeEnd={handleItemSize}
/> />
</DropdownMenu.Item> </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 ||
display === ListDisplayType.TABLE_PAGINATED) && ( display === ListDisplayType.TABLE_PAGINATED) && (
<> <>

View file

@ -141,8 +141,8 @@ export const PlaylistListGridView = ({ gridRef, itemCount }: PlaylistListGridVie
height={height} height={height}
initialScrollOffset={grid?.scrollOffset || 0} initialScrollOffset={grid?.scrollOffset || 0}
itemCount={itemCount || 0} itemCount={itemCount || 0}
itemGap={20} itemGap={grid?.itemGap ?? 10}
itemSize={grid?.itemsPerRow || 5} itemSize={grid?.itemSize || 200}
itemType={LibraryItem.PLAYLIST} itemType={LibraryItem.PLAYLIST}
loading={itemCount === undefined || itemCount === null} loading={itemCount === undefined || itemCount === null}
minimumBatchSize={40} minimumBatchSize={40}

View file

@ -230,12 +230,16 @@ export const PlaylistListHeaderFilters = ({
const handleItemSize = (e: number) => { const handleItemSize = (e: number) => {
if (isGrid) { if (isGrid) {
setGrid({ data: { itemsPerRow: e }, key: pageKey }); setGrid({ data: { itemSize: e }, key: pageKey });
} else { } else {
setTable({ data: { rowHeight: e }, key: pageKey }); setTable({ data: { rowHeight: e }, key: pageKey });
} }
}; };
const handleItemGap = (e: number) => {
setGrid({ data: { itemGap: e }, key: pageKey });
};
const handleRefresh = () => { const handleRefresh = () => {
queryClient.invalidateQueries(queryKeys.playlists.list(server?.id || '', filter)); queryClient.invalidateQueries(queryKeys.playlists.list(server?.id || '', filter));
handleFilterChange(filter); handleFilterChange(filter);
@ -344,16 +348,27 @@ export const PlaylistListHeaderFilters = ({
Table (paginated) Table (paginated)
</DropdownMenu.Item> */} </DropdownMenu.Item> */}
<DropdownMenu.Divider /> <DropdownMenu.Divider />
<DropdownMenu.Label> <DropdownMenu.Label>Item size</DropdownMenu.Label>
{isGrid ? 'Items per row' : 'Item size'}
</DropdownMenu.Label>
<DropdownMenu.Item closeMenuOnClick={false}> <DropdownMenu.Item closeMenuOnClick={false}>
<Slider <Slider
defaultValue={isGrid ? grid?.itemsPerRow || 0 : table.rowHeight} defaultValue={isGrid ? grid?.itemSize || 0 : table.rowHeight}
max={isGrid ? 14 : 100} max={isGrid ? 300 : 100}
min={isGrid ? 2 : 25} min={isGrid ? 150 : 25}
onChangeEnd={handleItemSize} 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> </DropdownMenu.Item>
{!isGrid && ( {!isGrid && (
<> <>

View file

@ -43,6 +43,8 @@ export type ListTableProps = {
} & DataTableProps; } & DataTableProps;
export type ListGridProps = { export type ListGridProps = {
itemGap?: number;
itemSize?: number;
itemsPerRow?: number; itemsPerRow?: number;
scrollOffset?: number; scrollOffset?: number;
}; };
@ -222,9 +224,12 @@ export const useListStore = create<ListSlice>()(
state.detail[args.key] = { state.detail[args.key] = {
filter: {} as FilterType, filter: {} as FilterType,
grid: { grid: {
itemsPerRow: itemGap:
state.item[page as keyof ListState['item']].grid 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, scrollOffset: 0,
}, },
table: { table: {
@ -342,7 +347,7 @@ export const useListStore = create<ListSlice>()(
sortBy: AlbumListSort.RECENTLY_ADDED, sortBy: AlbumListSort.RECENTLY_ADDED,
sortOrder: SortOrder.DESC, sortOrder: SortOrder.DESC,
}, },
grid: { itemsPerRow: 5, scrollOffset: 0 }, grid: { itemGap: 10, itemSize: 200, scrollOffset: 0 },
table: { table: {
autoFit: true, autoFit: true,
columns: [ columns: [
@ -383,7 +388,7 @@ export const useListStore = create<ListSlice>()(
sortBy: AlbumArtistListSort.NAME, sortBy: AlbumArtistListSort.NAME,
sortOrder: SortOrder.DESC, sortOrder: SortOrder.DESC,
}, },
grid: { itemsPerRow: 5, scrollOffset: 0 }, grid: { itemGap: 10, itemSize: 200, scrollOffset: 0 },
table: { table: {
autoFit: true, autoFit: true,
columns: [ columns: [
@ -412,7 +417,7 @@ export const useListStore = create<ListSlice>()(
sortBy: AlbumListSort.RECENTLY_ADDED, sortBy: AlbumListSort.RECENTLY_ADDED,
sortOrder: SortOrder.DESC, sortOrder: SortOrder.DESC,
}, },
grid: { itemsPerRow: 5, scrollOffset: 0 }, grid: { itemGap: 10, itemSize: 200, scrollOffset: 0 },
table: { table: {
autoFit: true, autoFit: true,
columns: [ columns: [
@ -453,7 +458,7 @@ export const useListStore = create<ListSlice>()(
sortBy: SongListSort.RECENTLY_ADDED, sortBy: SongListSort.RECENTLY_ADDED,
sortOrder: SortOrder.DESC, sortOrder: SortOrder.DESC,
}, },
grid: { itemsPerRow: 5, scrollOffset: 0 }, grid: { itemGap: 10, itemSize: 200, scrollOffset: 0 },
table: { table: {
autoFit: true, autoFit: true,
columns: [ columns: [
@ -510,7 +515,7 @@ export const useListStore = create<ListSlice>()(
sortBy: GenreListSort.NAME, sortBy: GenreListSort.NAME,
sortOrder: SortOrder.ASC, sortOrder: SortOrder.ASC,
}, },
grid: { itemsPerRow: 5, scrollOffset: 0 }, grid: { itemGap: 10, itemSize: 200, scrollOffset: 0 },
table: { table: {
autoFit: true, autoFit: true,
columns: [ columns: [
@ -539,7 +544,7 @@ export const useListStore = create<ListSlice>()(
sortBy: PlaylistListSort.NAME, sortBy: PlaylistListSort.NAME,
sortOrder: SortOrder.DESC, sortOrder: SortOrder.DESC,
}, },
grid: { scrollOffset: 0, size: 0 }, grid: { itemGap: 10, itemSize: 200, scrollOffset: 0 },
table: { table: {
autoFit: true, autoFit: true,
columns: [ columns: [
@ -572,7 +577,7 @@ export const useListStore = create<ListSlice>()(
sortBy: SongListSort.RECENTLY_ADDED, sortBy: SongListSort.RECENTLY_ADDED,
sortOrder: SortOrder.DESC, sortOrder: SortOrder.DESC,
}, },
grid: { itemsPerRow: 5, scrollOffset: 0 }, grid: { itemGap: 10, itemSize: 200, scrollOffset: 0 },
table: { table: {
autoFit: true, autoFit: true,
columns: [ columns: [