implement item list grid card row customization

This commit is contained in:
jeffvli
2025-11-14 15:18:25 -08:00
parent 56d0669510
commit b6c3200419
17 changed files with 559 additions and 149 deletions
@@ -0,0 +1,111 @@
import { useMemo } from 'react';
import { type DataRow, getDataRows } from '/@/renderer/components/item-card/item-card';
import { useSettingsStore } from '/@/renderer/store';
import { LibraryItem } from '/@/shared/types/domain-types';
import { TableColumn } from '/@/shared/types/types';
import { ItemListKey } from '/@/shared/types/types';
const getDefaultRowsForItemType = (itemType: LibraryItem): DataRow[] => {
const allRows = getDataRows();
const rowMap = new Map(allRows.map((row) => [row.id, row]));
switch (itemType) {
case LibraryItem.ALBUM:
return [rowMap.get('name'), rowMap.get('albumArtists')].filter(
(row): row is NonNullable<typeof row> => row !== undefined,
);
case LibraryItem.ALBUM_ARTIST:
return [rowMap.get('name')].filter(
(row): row is NonNullable<typeof row> => row !== undefined,
);
case LibraryItem.ARTIST:
return [rowMap.get('name')].filter(
(row): row is NonNullable<typeof row> => row !== undefined,
);
case LibraryItem.PLAYLIST:
return [rowMap.get('name')].filter(
(row): row is NonNullable<typeof row> => row !== undefined,
);
case LibraryItem.SONG:
return [rowMap.get('name')].filter(
(row): row is NonNullable<typeof row> => row !== undefined,
);
default:
return [];
}
};
// Map TableColumn enum values to row IDs used in getDataRows
const getRowIdFromTableColumn = (tableColumn: TableColumn): null | string => {
// Map TableColumn enum values to the row IDs used in getDataRows
const columnToRowIdMap: Record<TableColumn, null | string> = {
[TableColumn.ACTIONS]: null,
[TableColumn.ALBUM]: 'album',
[TableColumn.ALBUM_ARTIST]: 'albumArtists',
[TableColumn.ALBUM_COUNT]: 'albumCount',
[TableColumn.ARTIST]: 'artists',
[TableColumn.BIOGRAPHY]: null,
[TableColumn.BIT_RATE]: null,
[TableColumn.BPM]: null,
[TableColumn.CHANNELS]: null,
[TableColumn.CODEC]: null,
[TableColumn.COMMENT]: null,
[TableColumn.DATE_ADDED]: 'createdAt',
[TableColumn.DISC_NUMBER]: null,
[TableColumn.DURATION]: 'duration',
[TableColumn.GENRE]: 'genres',
[TableColumn.GENRE_BADGE]: null,
[TableColumn.ID]: null,
[TableColumn.IMAGE]: null,
[TableColumn.LAST_PLAYED]: 'lastPlayedAt',
[TableColumn.OWNER]: null,
[TableColumn.PATH]: null,
[TableColumn.PLAY_COUNT]: 'playCount',
[TableColumn.RELEASE_DATE]: 'releaseDate',
[TableColumn.ROW_INDEX]: null,
[TableColumn.SIZE]: null,
[TableColumn.SKIP]: null,
[TableColumn.SONG_COUNT]: 'songCount',
[TableColumn.TITLE]: 'name',
[TableColumn.TITLE_COMBINED]: null,
[TableColumn.TRACK_NUMBER]: null,
[TableColumn.USER_FAVORITE]: 'userFavorite',
[TableColumn.USER_RATING]: 'rating',
[TableColumn.YEAR]: 'releaseYear',
};
return columnToRowIdMap[tableColumn] || null;
};
export const useGridRows = (itemType: LibraryItem, listKey?: ItemListKey) => {
const gridRowsConfig = useSettingsStore((state) =>
listKey ? state.lists[listKey]?.grid?.rows : undefined,
);
return useMemo(() => {
const allRows = getDataRows();
if (!listKey || !gridRowsConfig || gridRowsConfig.length === 0) {
const defaultRows = getDefaultRowsForItemType(itemType);
return defaultRows.length > 0 ? defaultRows : allRows;
}
const rowMap = new Map(allRows.map((row) => [row.id, row]));
const configuredRows = gridRowsConfig
.filter((config) => config.isEnabled)
.map((config) => {
const rowId = getRowIdFromTableColumn(config.id);
const baseRow = rowId ? rowMap.get(rowId) : null;
if (!baseRow) return null;
return {
...baseRow,
align: config.align,
};
})
.filter((row): row is NonNullable<typeof row> => row !== null && row !== undefined);
return configuredRows.length > 0 ? configuredRows : allRows;
}, [itemType, listKey, gridRowsConfig]);
};
@@ -60,6 +60,7 @@ interface VirtualizedGridListProps {
onScrollEnd?: ItemGridListProps['onScrollEnd'];
outerRef: RefObject<any>;
ref: RefObject<FixedSizeList<GridItemProps>>;
rows?: ItemCardProps['rows'];
tableMeta: null | {
columnCount: number;
itemHeight: number;
@@ -85,6 +86,7 @@ const VirtualizedGridList = React.memo(
onScrollEnd,
outerRef,
ref,
rows,
tableMeta,
width,
}: VirtualizedGridListProps) => {
@@ -99,11 +101,13 @@ const VirtualizedGridList = React.memo(
gap,
internalState,
itemType,
rows,
tableMeta,
};
}, [
tableMeta,
controls,
rows,
data,
enableDrag,
enableExpansion,
@@ -167,59 +171,51 @@ const VirtualizedGridList = React.memo(
VirtualizedGridList.displayName = 'VirtualizedGridList';
const createThrottledSetTableMeta = (itemsPerRow?: number) => {
return throttle(
(
width: number,
dataLength: number,
type: LibraryItem,
setTableMeta: (meta: any) => void,
) => {
const isSm = width >= 600;
const isMd = width >= 768;
const isLg = width >= 960;
const isXl = width >= 1200;
const is2xl = width >= 1440;
const is3xl = width >= 1920;
const is4xl = width >= 2560;
const createThrottledSetTableMeta = (itemsPerRow?: number, rowsCount?: number) => {
return throttle((width: number, dataLength: number, setTableMeta: (meta: any) => void) => {
const isSm = width >= 600;
const isMd = width >= 768;
const isLg = width >= 960;
const isXl = width >= 1200;
const is2xl = width >= 1440;
const is3xl = width >= 1920;
const is4xl = width >= 2560;
let dynamicItemsPerRow = 2;
let dynamicItemsPerRow = 2;
if (is4xl) {
dynamicItemsPerRow = 12;
} else if (is3xl) {
dynamicItemsPerRow = 10;
} else if (is2xl) {
dynamicItemsPerRow = 8;
} else if (isXl) {
dynamicItemsPerRow = 6;
} else if (isLg) {
dynamicItemsPerRow = 5;
} else if (isMd) {
dynamicItemsPerRow = 4;
} else if (isSm) {
dynamicItemsPerRow = 3;
} else {
dynamicItemsPerRow = 2;
}
if (is4xl) {
dynamicItemsPerRow = 12;
} else if (is3xl) {
dynamicItemsPerRow = 10;
} else if (is2xl) {
dynamicItemsPerRow = 8;
} else if (isXl) {
dynamicItemsPerRow = 6;
} else if (isLg) {
dynamicItemsPerRow = 5;
} else if (isMd) {
dynamicItemsPerRow = 4;
} else if (isSm) {
dynamicItemsPerRow = 3;
} else {
dynamicItemsPerRow = 2;
}
const setItemsPerRow = itemsPerRow || dynamicItemsPerRow;
const setItemsPerRow = itemsPerRow || dynamicItemsPerRow;
const widthPerItem = Number(width) / setItemsPerRow;
const itemHeight = widthPerItem + getDataRowsCount(type) * 26;
const widthPerItem = Number(width) / setItemsPerRow;
const itemHeight = widthPerItem + (rowsCount || getDataRowsCount()) * 26;
if (widthPerItem === 0) {
return;
}
if (widthPerItem === 0) {
return;
}
setTableMeta({
columnCount: setItemsPerRow,
itemHeight,
rowCount: Math.ceil(dataLength / setItemsPerRow),
});
},
200,
);
setTableMeta({
columnCount: setItemsPerRow,
itemHeight,
rowCount: Math.ceil(dataLength / setItemsPerRow),
});
}, 200);
};
export interface GridItemProps {
@@ -232,6 +228,7 @@ export interface GridItemProps {
gap: 'lg' | 'md' | 'sm' | 'xl' | 'xs';
internalState: ItemListStateActions;
itemType: LibraryItem;
rows?: ItemCardProps['rows'];
tableMeta: null | {
columnCount: number;
itemHeight: number;
@@ -257,6 +254,7 @@ export interface ItemGridListProps {
onScroll?: (offset: number, direction: 'down' | 'up') => void;
onScrollEnd?: (offset: number, direction: 'down' | 'up') => void;
ref?: Ref<ItemListHandle>;
rows?: ItemCardProps['rows'];
}
export const ItemGridList = ({
@@ -273,6 +271,7 @@ export const ItemGridList = ({
onScroll,
onScrollEnd,
ref,
rows,
}: ItemGridListProps) => {
const rootRef = useRef(null);
const outerRef = useRef(null);
@@ -334,12 +333,12 @@ export const ItemGridList = ({
}, [initialize, tableMeta]);
const throttledSetTableMeta = useMemo(() => {
return createThrottledSetTableMeta(itemsPerRow);
}, [itemsPerRow]);
return createThrottledSetTableMeta(itemsPerRow, rows?.length);
}, [itemsPerRow, rows?.length]);
useLayoutEffect(() => {
throttledSetTableMeta(containerWidth, data.length, itemType, setTableMeta);
}, [containerWidth, data.length, itemType, throttledSetTableMeta]);
throttledSetTableMeta(containerWidth, data.length, setTableMeta);
}, [containerWidth, data.length, throttledSetTableMeta]);
const controls = useDefaultItemListControls();
@@ -620,6 +619,7 @@ export const ItemGridList = ({
onScrollEnd={onScrollEnd ?? (() => {})}
outerRef={outerRef}
ref={listRef}
rows={rows}
tableMeta={tableMeta}
width={width}
/>
@@ -638,7 +638,7 @@ export const ItemGridList = ({
const ListComponent = memo((props: ListChildComponentProps<GridItemProps>) => {
const { index, style } = props;
const { columns, controls, data, enableDrag, gap, itemType } = props.data;
const { columns, controls, data, enableDrag, gap, itemType, rows } = props.data;
const items: ReactNode[] = [];
const itemCount = data.length;
@@ -667,6 +667,7 @@ const ListComponent = memo((props: ListChildComponentProps<GridItemProps>) => {
enableDrag={enableDrag}
internalState={props.data.internalState}
itemType={itemType}
rows={rows}
withControls
/>
</div>,