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;
}) => 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>

View file

@ -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();

View file

@ -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,

View file

@ -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

View file

@ -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;

View file

@ -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}

View file

@ -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) && (
<>

View file

@ -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}

View file

@ -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>

View file

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

View file

@ -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}

View file

@ -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) && (
<>

View file

@ -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}

View file

@ -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 && (
<>

View file

@ -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: [