diff --git a/src/renderer/components/item-list/item-table-list/cell-component-factory.tsx b/src/renderer/components/item-list/item-table-list/cell-component-factory.tsx
index a641edcf6..dc04a20d4 100644
--- a/src/renderer/components/item-list/item-table-list/cell-component-factory.tsx
+++ b/src/renderer/components/item-list/item-table-list/cell-component-factory.tsx
@@ -20,7 +20,8 @@ export const createColumnCellComponent = (
prevProps.columnIndex === nextProps.columnIndex &&
prevProps.data === nextProps.data &&
prevProps.style === nextProps.style &&
- prevProps.columns === nextProps.columns
+ prevProps.columns === nextProps.columns &&
+ prevProps.playlistId === nextProps.playlistId
);
},
);
diff --git a/src/renderer/components/item-list/item-table-list/columns/date-column.tsx b/src/renderer/components/item-list/item-table-list/columns/date-column.tsx
index 27a6c8cd1..22fb245be 100644
--- a/src/renderer/components/item-list/item-table-list/columns/date-column.tsx
+++ b/src/renderer/components/item-list/item-table-list/columns/date-column.tsx
@@ -18,10 +18,15 @@ const DateColumnBase = (props: ItemTableListInnerColumn) => {
const rowItem = props.getRowItem?.(props.rowIndex) ?? (props.data as any[])[props.rowIndex];
const row: string | undefined = (rowItem as any)?.[props.columns[props.columnIndex].id];
- if (typeof row === 'string' && row) {
+ const formattedAbsolute = useMemo(
+ () => (typeof row === 'string' && row ? formatDateAbsolute(row) : null),
+ [row],
+ );
+
+ if (formattedAbsolute) {
return (
- {formatDateAbsolute(row)}
+ {formattedAbsolute}
);
}
@@ -63,6 +68,11 @@ const AbsoluteDateColumnBase = (props: ItemTableListInnerColumn) => {
return null;
}, [props.type, rowItem]);
+ const formattedIsoFallback = useMemo(
+ () => (typeof row === 'string' && row ? formatPartialIsoDateUTC(row) : null),
+ [row],
+ );
+
if (props.type === TableColumn.RELEASE_DATE) {
if (releaseDateContent) {
return (
@@ -72,10 +82,10 @@ const AbsoluteDateColumnBase = (props: ItemTableListInnerColumn) => {
);
}
- if (typeof row === 'string' && row) {
+ if (formattedIsoFallback) {
return (
- {formatPartialIsoDateUTC(row)}
+ {formattedIsoFallback}
);
}
@@ -96,10 +106,15 @@ const RelativeDateColumnBase = (props: ItemTableListInnerColumn) => {
const rowItem = props.getRowItem?.(props.rowIndex) ?? (props.data as any[])[props.rowIndex];
const row: string | undefined = (rowItem as any)?.[props.columns[props.columnIndex].id];
- if (typeof row === 'string') {
+ const formattedRelative = useMemo(() => {
+ if (typeof row !== 'string') return null;
+ return formatDateRelative(row);
+ }, [row]);
+
+ if (formattedRelative !== null) {
return (
- {formatDateRelative(row)}
+ {formattedRelative}
);
}
diff --git a/src/renderer/components/item-list/item-table-list/columns/title-column.tsx b/src/renderer/components/item-list/item-table-list/columns/title-column.tsx
index 9af74f149..3c98e315b 100644
--- a/src/renderer/components/item-list/item-table-list/columns/title-column.tsx
+++ b/src/renderer/components/item-list/item-table-list/columns/title-column.tsx
@@ -1,4 +1,5 @@
import clsx from 'clsx';
+import { useMemo } from 'react';
import { Link } from 'react-router';
import styles from './title-column.module.css';
@@ -35,8 +36,12 @@ function DefaultTitleColumn(props: ItemTableListInnerColumn) {
const rowItem = props.getRowItem?.(props.rowIndex) ?? (props.data as any[])[props.rowIndex];
const row: string | undefined = rowItem?.[props.columns[props.columnIndex].id];
+ const path = useMemo(() => {
+ if (typeof row !== 'string' || !rowItem || !(rowItem as any).id) return undefined;
+ return getTitlePath(props.itemType, (rowItem as any).id as string);
+ }, [props.itemType, row, rowItem]);
+
if (typeof row === 'string') {
- const path = getTitlePath(props.itemType, (rowItem as any).id as string);
const item = rowItem as any;
const titleLinkProps = path
@@ -80,8 +85,12 @@ function QueueSongTitleColumn(props: ItemTableListInnerColumn) {
const song = rowItem as QueueSong;
const isActive = useIsActiveRow(song?.id, song?._uniqueId);
+ const path = useMemo(() => {
+ if (typeof row !== 'string' || !rowItem || !(rowItem as any).id) return undefined;
+ return getTitlePath(props.itemType, (rowItem as any).id as string);
+ }, [props.itemType, row, rowItem]);
+
if (typeof row === 'string') {
- const path = getTitlePath(props.itemType, (rowItem as any).id as string);
const item = rowItem as any;
const titleLinkProps = path
diff --git a/src/renderer/components/item-list/item-table-list/hooks/use-item-drag-drop-state.tsx b/src/renderer/components/item-list/item-table-list/hooks/use-item-drag-drop-state.tsx
index ee586cfb8..b8b89e73c 100644
--- a/src/renderer/components/item-list/item-table-list/hooks/use-item-drag-drop-state.tsx
+++ b/src/renderer/components/item-list/item-table-list/hooks/use-item-drag-drop-state.tsx
@@ -34,256 +34,268 @@ export const useItemDragDropState = => {
const shouldEnableDrag = enableDrag && isDataRow && !!item;
+ const needsDropRegistration =
+ shouldEnableDrag &&
+ (itemType === LibraryItem.QUEUE_SONG || itemType === LibraryItem.PLAYLIST_SONG);
+
const {
isDraggedOver,
isDragging: isDraggingLocal,
ref: dragRef,
} = useDragDrop({
- drag: {
- getId: () => {
- if (!item || !isDataRow) {
- return [];
- }
+ drag: shouldEnableDrag
+ ? {
+ getId: () => {
+ if (!item || !isDataRow) {
+ return [];
+ }
- const draggedItems = getDraggedItems(item as any, internalState);
+ const draggedItems = getDraggedItems(item as any, internalState);
- return draggedItems.map((draggedItem) => draggedItem.id);
- },
- getItem: () => {
- if (!item || !isDataRow) {
- return [];
- }
+ return draggedItems.map((draggedItem) => draggedItem.id);
+ },
+ getItem: () => {
+ if (!item || !isDataRow) {
+ return [];
+ }
- const draggedItems = getDraggedItems(item as any, internalState);
+ const draggedItems = getDraggedItems(item as any, internalState);
- return draggedItems;
- },
- itemType,
- onDragStart: () => {
- if (!item || !isDataRow) {
- return;
- }
+ return draggedItems;
+ },
+ itemType,
+ onDragStart: () => {
+ if (!item || !isDataRow) {
+ return;
+ }
- const draggedItems = getDraggedItems(item as any, internalState);
- if (internalState) {
- internalState.setDragging(draggedItems);
- }
- },
- onDrop: () => {
- if (internalState) {
- internalState.setDragging([]);
- }
- },
- operation:
- itemType === LibraryItem.QUEUE_SONG
- ? [DragOperation.REORDER, DragOperation.ADD]
- : itemType === LibraryItem.PLAYLIST_SONG
- ? [DragOperation.REORDER, DragOperation.ADD]
- : [DragOperation.ADD],
- target: DragTargetMap[itemType] || DragTarget.GENERIC,
- },
- drop: {
- canDrop: (args) => {
- if (args.source.type === DragTarget.TABLE_COLUMN) {
- return false;
- }
+ const draggedItems = getDraggedItems(item as any, internalState);
+ if (internalState) {
+ internalState.setDragging(draggedItems);
+ }
+ },
+ onDrop: () => {
+ if (internalState) {
+ internalState.setDragging([]);
+ }
+ },
+ operation:
+ itemType === LibraryItem.QUEUE_SONG
+ ? [DragOperation.REORDER, DragOperation.ADD]
+ : itemType === LibraryItem.PLAYLIST_SONG
+ ? [DragOperation.REORDER, DragOperation.ADD]
+ : [DragOperation.ADD],
+ target: DragTargetMap[itemType] || DragTarget.GENERIC,
+ }
+ : undefined,
+ drop: needsDropRegistration
+ ? {
+ canDrop: (args) => {
+ if (args.source.type === DragTarget.TABLE_COLUMN) {
+ return false;
+ }
- // Allow drops for QUEUE_SONG (queue reordering)
- if (itemType === LibraryItem.QUEUE_SONG) {
- return true;
- }
+ // Allow drops for QUEUE_SONG (queue reordering)
+ if (itemType === LibraryItem.QUEUE_SONG) {
+ return true;
+ }
- // Allow drops for PLAYLIST_SONG (playlist reordering)
- // Only allow drops when drag is started from the reorder handle
- if (
- itemType === LibraryItem.PLAYLIST_SONG &&
- args.source.itemType === LibraryItem.PLAYLIST_SONG &&
- args.source.metadata?.fromReorderHandle === true
- ) {
- return true;
- }
+ // Allow drops for PLAYLIST_SONG (playlist reordering)
+ // Only allow drops when drag is started from the reorder handle
+ if (
+ itemType === LibraryItem.PLAYLIST_SONG &&
+ args.source.itemType === LibraryItem.PLAYLIST_SONG &&
+ args.source.metadata?.fromReorderHandle === true
+ ) {
+ return true;
+ }
- return false;
- },
- getData: () => {
- return {
- id: [(item as unknown as { id: string }).id],
- item: [item as unknown as unknown[]],
- itemType,
- type: DragTargetMap[itemType] || DragTarget.GENERIC,
- };
- },
- onDrag: () => {
- return;
- },
- onDragLeave: () => {
- return;
- },
- onDrop: (args) => {
- if (args.self.type === DragTarget.QUEUE_SONG) {
- const sourceServerId = (
- args.source.item?.[0] as unknown as { _serverId: string }
- )._serverId;
+ return false;
+ },
+ getData: () => {
+ return {
+ id: [(item as unknown as { id: string }).id],
+ item: [item as unknown as unknown[]],
+ itemType,
+ type: DragTargetMap[itemType] || DragTarget.GENERIC,
+ };
+ },
+ onDrag: () => {
+ return;
+ },
+ onDragLeave: () => {
+ return;
+ },
+ onDrop: (args) => {
+ if (args.self.type === DragTarget.QUEUE_SONG) {
+ const sourceServerId = (
+ args.source.item?.[0] as unknown as { _serverId: string }
+ )._serverId;
- const sourceItemType = args.source.itemType as LibraryItem;
+ const sourceItemType = args.source.itemType as LibraryItem;
- const droppedOnUniqueId = (
- args.self.item?.[0] as unknown as { _uniqueId: string }
- )._uniqueId;
+ const droppedOnUniqueId = (
+ args.self.item?.[0] as unknown as { _uniqueId: string }
+ )._uniqueId;
- switch (args.source.type) {
- case DragTarget.ALBUM: {
- playerContext.addToQueueByFetch(
- sourceServerId,
- args.source.id,
- sourceItemType,
- { edge: args.edge, uniqueId: droppedOnUniqueId },
- );
- break;
- }
- case DragTarget.ALBUM_ARTIST: {
- playerContext.addToQueueByFetch(
- sourceServerId,
- args.source.id,
- sourceItemType,
- { edge: args.edge, uniqueId: droppedOnUniqueId },
- );
- break;
- }
- case DragTarget.ARTIST: {
- playerContext.addToQueueByFetch(
- sourceServerId,
- args.source.id,
- sourceItemType,
- { edge: args.edge, uniqueId: droppedOnUniqueId },
- );
- break;
- }
- case DragTarget.FOLDER: {
- const items = args.source.item;
+ switch (args.source.type) {
+ case DragTarget.ALBUM: {
+ playerContext.addToQueueByFetch(
+ sourceServerId,
+ args.source.id,
+ sourceItemType,
+ { edge: args.edge, uniqueId: droppedOnUniqueId },
+ );
+ break;
+ }
+ case DragTarget.ALBUM_ARTIST: {
+ playerContext.addToQueueByFetch(
+ sourceServerId,
+ args.source.id,
+ sourceItemType,
+ { edge: args.edge, uniqueId: droppedOnUniqueId },
+ );
+ break;
+ }
+ case DragTarget.ARTIST: {
+ playerContext.addToQueueByFetch(
+ sourceServerId,
+ args.source.id,
+ sourceItemType,
+ { edge: args.edge, uniqueId: droppedOnUniqueId },
+ );
+ break;
+ }
+ case DragTarget.FOLDER: {
+ const items = args.source.item;
- const { folders, songs } = (items || []).reduce<{
- folders: Folder[];
- songs: Song[];
- }>(
- (acc, item) => {
- if ((item as unknown as Song)._itemType === LibraryItem.SONG) {
- acc.songs.push(item as unknown as Song);
- } else if (
- (item as unknown as Folder)._itemType === LibraryItem.FOLDER
- ) {
- acc.folders.push(item as unknown as Folder);
- }
- return acc;
- },
- { folders: [], songs: [] },
- );
+ const { folders, songs } = (items || []).reduce<{
+ folders: Folder[];
+ songs: Song[];
+ }>(
+ (acc, item) => {
+ if (
+ (item as unknown as Song)._itemType ===
+ LibraryItem.SONG
+ ) {
+ acc.songs.push(item as unknown as Song);
+ } else if (
+ (item as unknown as Folder)._itemType ===
+ LibraryItem.FOLDER
+ ) {
+ acc.folders.push(item as unknown as Folder);
+ }
+ return acc;
+ },
+ { folders: [], songs: [] },
+ );
- const folderIds = folders.map((folder) => folder.id);
+ const folderIds = folders.map((folder) => folder.id);
- // Handle folders: fetch and add to queue
- if (folderIds.length > 0) {
- playerContext.addToQueueByFetch(
- sourceServerId,
- folderIds,
- LibraryItem.FOLDER,
- { edge: args.edge, uniqueId: droppedOnUniqueId },
- );
- }
+ // Handle folders: fetch and add to queue
+ if (folderIds.length > 0) {
+ playerContext.addToQueueByFetch(
+ sourceServerId,
+ folderIds,
+ LibraryItem.FOLDER,
+ { edge: args.edge, uniqueId: droppedOnUniqueId },
+ );
+ }
- // Handle songs: add directly to queue
- if (songs.length > 0) {
- playerContext.addToQueueByData(songs, {
- edge: args.edge,
- uniqueId: droppedOnUniqueId,
- });
- }
+ // Handle songs: add directly to queue
+ if (songs.length > 0) {
+ playerContext.addToQueueByData(songs, {
+ edge: args.edge,
+ uniqueId: droppedOnUniqueId,
+ });
+ }
- break;
- }
- case DragTarget.GENRE: {
- playerContext.addToQueueByFetch(
- sourceServerId,
- args.source.id,
- sourceItemType,
- { edge: args.edge, uniqueId: droppedOnUniqueId },
- );
- break;
- }
- case DragTarget.PLAYLIST: {
- playerContext.addToQueueByFetch(
- sourceServerId,
- args.source.id,
- sourceItemType,
- { edge: args.edge, uniqueId: droppedOnUniqueId },
- );
- break;
- }
- case DragTarget.QUEUE_SONG: {
- const sourceItems = (args.source.item || []) as QueueSong[];
- if (
- sourceItems.length > 0 &&
- args.edge &&
- (args.edge === 'top' || args.edge === 'bottom')
- ) {
- playerContext.moveSelectedTo(
- sourceItems,
- args.edge,
- droppedOnUniqueId,
- );
- }
- break;
- }
- case DragTarget.SONG: {
- const sourceItems = (args.source.item || []) as Song[];
- if (sourceItems.length > 0) {
- playerContext.addToQueueByData(sourceItems, {
- edge: args.edge,
- uniqueId: droppedOnUniqueId,
- });
- }
- break;
- }
- default: {
- break;
- }
- }
- }
+ break;
+ }
+ case DragTarget.GENRE: {
+ playerContext.addToQueueByFetch(
+ sourceServerId,
+ args.source.id,
+ sourceItemType,
+ { edge: args.edge, uniqueId: droppedOnUniqueId },
+ );
+ break;
+ }
+ case DragTarget.PLAYLIST: {
+ playerContext.addToQueueByFetch(
+ sourceServerId,
+ args.source.id,
+ sourceItemType,
+ { edge: args.edge, uniqueId: droppedOnUniqueId },
+ );
+ break;
+ }
+ case DragTarget.QUEUE_SONG: {
+ const sourceItems = (args.source.item || []) as QueueSong[];
+ if (
+ sourceItems.length > 0 &&
+ args.edge &&
+ (args.edge === 'top' || args.edge === 'bottom')
+ ) {
+ playerContext.moveSelectedTo(
+ sourceItems,
+ args.edge,
+ droppedOnUniqueId,
+ );
+ }
+ break;
+ }
+ case DragTarget.SONG: {
+ const sourceItems = (args.source.item || []) as Song[];
+ if (sourceItems.length > 0) {
+ playerContext.addToQueueByData(sourceItems, {
+ edge: args.edge,
+ uniqueId: droppedOnUniqueId,
+ });
+ }
+ break;
+ }
+ default: {
+ break;
+ }
+ }
+ }
- // Handle PLAYLIST_SONG reordering
- // Only allow drops when drag is started from the reorder handle
- if (
- args.self.itemType === LibraryItem.PLAYLIST_SONG &&
- args.source.itemType === LibraryItem.PLAYLIST_SONG &&
- args.source.metadata?.fromReorderHandle === true &&
- playlistId
- ) {
- const sourceItems = (args.source.item || []) as any[];
- const targetItem = item as any;
+ // Handle PLAYLIST_SONG reordering
+ // Only allow drops when drag is started from the reorder handle
+ if (
+ args.self.itemType === LibraryItem.PLAYLIST_SONG &&
+ args.source.itemType === LibraryItem.PLAYLIST_SONG &&
+ args.source.metadata?.fromReorderHandle === true &&
+ playlistId
+ ) {
+ const sourceItems = (args.source.item || []) as any[];
+ const targetItem = item as any;
- if (
- sourceItems.length > 0 &&
- args.edge &&
- (args.edge === 'top' || args.edge === 'bottom') &&
- targetItem
- ) {
- // Emit event to reorder playlist songs
- eventEmitter.emit('PLAYLIST_REORDER', {
- edge: args.edge,
- playlistId,
- sourceIds: args.source.id,
- targetId: targetItem.id,
- });
- }
- }
+ if (
+ sourceItems.length > 0 &&
+ args.edge &&
+ (args.edge === 'top' || args.edge === 'bottom') &&
+ targetItem
+ ) {
+ // Emit event to reorder playlist songs
+ eventEmitter.emit('PLAYLIST_REORDER', {
+ edge: args.edge,
+ playlistId,
+ sourceIds: args.source.id,
+ targetId: targetItem.id,
+ });
+ }
+ }
- if (internalState) {
- internalState.setDragging([]);
- }
+ if (internalState) {
+ internalState.setDragging([]);
+ }
- return;
- },
- },
+ return;
+ },
+ }
+ : undefined,
isEnabled: shouldEnableDrag,
});
diff --git a/src/renderer/components/item-list/item-table-list/item-table-list-column.tsx b/src/renderer/components/item-list/item-table-list/item-table-list-column.tsx
index fe3968480..ea2a5cebf 100644
--- a/src/renderer/components/item-list/item-table-list/item-table-list-column.tsx
+++ b/src/renderer/components/item-list/item-table-list/item-table-list-column.tsx
@@ -19,7 +19,6 @@ import React, {
useRef,
useState,
} from 'react';
-import { useParams } from 'react-router';
import { CellComponentProps } from 'react-window-v2';
import styles from './item-table-list-column.module.css';
@@ -82,7 +81,6 @@ export interface ItemTableListInnerColumn extends ItemTableListColumn {
}
const ItemTableListColumnBase = (props: ItemTableListColumn) => {
- const { playlistId } = useParams() as { playlistId?: string };
const type = props.columnType ?? (props.columns[props.columnIndex].id as TableColumn);
const isHeaderEnabled = !!props.enableHeader;
@@ -135,7 +133,7 @@ const ItemTableListColumnBase = (props: ItemTableListColumn) => {
item,
itemType: props.itemType,
playerContext: props.playerContext,
- playlistId,
+ playlistId: props.playlistId,
});
const controls = props.controls;
@@ -362,6 +360,7 @@ export const ItemTableListColumn = memo(ItemTableListColumnBase, (prevProps, nex
prevProps.enableColumnResize === nextProps.enableColumnResize &&
prevProps.enableColumnReorder === nextProps.enableColumnReorder &&
prevProps.cellPadding === nextProps.cellPadding &&
+ prevProps.playlistId === nextProps.playlistId &&
prevItem === nextItem
);
});
diff --git a/src/renderer/components/item-list/item-table-list/item-table-list-context.tsx b/src/renderer/components/item-list/item-table-list/item-table-list-context.tsx
index 4cf4db435..eca4ebc92 100644
--- a/src/renderer/components/item-list/item-table-list/item-table-list-context.tsx
+++ b/src/renderer/components/item-list/item-table-list/item-table-list-context.tsx
@@ -1,31 +1,51 @@
+import type { ReactElement } from 'react';
+
import React, { createContext, useContext, useEffect, useMemo, useRef } from 'react';
import { useSyncExternalStore } from 'react';
+import type { TableItemProps } from './item-table-list';
+
import { ItemListStateActions } from '/@/renderer/components/item-list/helpers/item-list-state';
import { ItemControls, ItemTableListColumnConfig } from '/@/renderer/components/item-list/types';
import { PlayerContext } from '/@/renderer/features/player/context/player-context';
import { LibraryItem } from '/@/shared/types/domain-types';
-/**
- * Stage A/B: Provide table-scoped config + external stores so churny values can update
- * without forcing `cellProps` identity changes (and therefore without rerendering every visible cell).
- */
-
export type ItemTableListConfig = {
cellPadding: 'lg' | 'md' | 'sm' | 'xl' | 'xs';
columns: ItemTableListColumnConfig[];
controls: ItemControls;
+ enableAlternateRowColors: boolean;
+ enableColumnReorder: boolean;
+ enableColumnResize: boolean;
+ enableDrag: boolean;
+ enableExpansion: boolean;
enableHeader: boolean;
+ enableHorizontalBorders: boolean;
enableRowHoverHighlight: boolean;
enableSelection: boolean;
+ enableVerticalBorders: boolean;
+ getRowHeight: (index: number, cellProps: TableItemProps) => number;
+ groups?: ItemTableListGroupHeader[];
internalState: ItemListStateActions;
itemType: LibraryItem;
playerContext: PlayerContext;
+ playlistId?: string;
size: 'compact' | 'default' | 'large';
startRowIndex?: number;
tableId: string;
};
+export type ItemTableListGroupHeader = {
+ itemCount: number;
+ render: (props: {
+ data: unknown[];
+ groupIndex: number;
+ index: number;
+ internalState: ItemListStateActions;
+ startDataIndex: number;
+ }) => ReactElement;
+};
+
const ItemTableListConfigContext = createContext(null);
export const ItemTableListConfigProvider = ({
diff --git a/src/renderer/components/item-list/item-table-list/item-table-list.tsx b/src/renderer/components/item-list/item-table-list/item-table-list.tsx
index 4bf34fea3..cdcf953c6 100644
--- a/src/renderer/components/item-list/item-table-list/item-table-list.tsx
+++ b/src/renderer/components/item-list/item-table-list/item-table-list.tsx
@@ -15,6 +15,7 @@ import React, {
useRef,
useState,
} from 'react';
+import { useParams } from 'react-router';
import { type CellComponentProps, Grid } from 'react-window-v2';
import styles from './item-table-list.module.css';
@@ -43,6 +44,7 @@ import { useTableRowModel } from '/@/renderer/components/item-list/item-table-li
import { useTableScrollToIndex } from '/@/renderer/components/item-list/item-table-list/hooks/use-table-scroll-to-index';
import { ItemTableListColumn } from '/@/renderer/components/item-list/item-table-list/item-table-list-column';
import {
+ type ItemTableListConfig,
ItemTableListConfigProvider,
ItemTableListStoreProvider,
} from '/@/renderer/components/item-list/item-table-list/item-table-list-context';
@@ -104,27 +106,11 @@ export enum TableItemSize {
interface VirtualizedTableGridProps {
calculatedColumnWidths: number[];
CellComponent: JSXElementConstructor>;
- cellPadding: 'lg' | 'md' | 'sm' | 'xl' | 'xs';
- controls: ItemControls;
data: unknown[];
dataWithGroups: (null | unknown)[];
- enableAlternateRowColors: boolean;
- enableColumnReorder: boolean;
- enableColumnResize: boolean;
- enableDrag?: boolean;
- enableExpansion: boolean;
- enableHeader: boolean;
- enableHorizontalBorders: boolean;
- enableRowHoverHighlight: boolean;
enableScrollShadow: boolean;
- enableSelection: boolean;
- enableVerticalBorders: boolean;
getItem?: (index: number) => undefined | unknown;
- getRowHeight: (index: number, cellProps: TableItemProps) => number;
- groups?: TableGroupHeader[];
headerHeight: number;
- internalState: ItemListStateActions;
- itemType: LibraryItem;
mergedRowRef: React.Ref;
onRangeChanged?: ItemTableListProps['onRangeChanged'];
parsedColumns: ReturnType;
@@ -134,13 +120,10 @@ interface VirtualizedTableGridProps {
pinnedRightColumnRef: React.RefObject;
pinnedRowCount: number;
pinnedRowRef: React.RefObject;
- playerContext: PlayerContext;
showLeftShadow: boolean;
showRightShadow: boolean;
showTopShadow: boolean;
- size: 'compact' | 'default' | 'large';
- startRowIndex?: number;
- tableId: string;
+ tableConfig: ItemTableListConfig;
totalColumnCount: number;
totalRowCount: number;
}
@@ -148,27 +131,11 @@ interface VirtualizedTableGridProps {
const VirtualizedTableGrid = ({
calculatedColumnWidths,
CellComponent,
- cellPadding,
- controls,
data,
dataWithGroups,
- enableAlternateRowColors,
- enableColumnReorder,
- enableColumnResize,
- enableDrag,
- enableExpansion,
- enableHeader,
- enableHorizontalBorders,
- enableRowHoverHighlight,
enableScrollShadow,
- enableSelection,
- enableVerticalBorders,
getItem,
- getRowHeight,
- groups,
headerHeight,
- internalState,
- itemType,
mergedRowRef,
onRangeChanged,
parsedColumns,
@@ -178,16 +145,14 @@ const VirtualizedTableGrid = ({
pinnedRightColumnRef,
pinnedRowCount,
pinnedRowRef,
- playerContext,
showLeftShadow,
showRightShadow,
showTopShadow,
- size,
- startRowIndex,
- tableId,
+ tableConfig,
totalColumnCount,
totalRowCount,
}: VirtualizedTableGridProps) => {
+ const { enableHeader, enableRowHoverHighlight, getRowHeight, groups } = tableConfig;
const hoverDelegateRef = useRef(null);
useRowInteractionDelegate({
@@ -345,35 +310,7 @@ const VirtualizedTableGrid = ({
],
);
- const stableConfigProps = useMemo(
- () => ({
- cellPadding,
- columns: parsedColumns,
- controls,
- enableHeader,
- getRowHeight,
- hasAlbumGroupColumn: parsedColumns.some((col) => col.id === TableColumn.ALBUM_GROUP),
- internalState,
- itemType,
- playerContext,
- size,
- tableId,
- }),
- [
- cellPadding,
- parsedColumns,
- controls,
- enableHeader,
- getRowHeight,
- internalState,
- itemType,
- playerContext,
- size,
- tableId,
- ],
- );
-
- const dynamicDataProps = useMemo(
+ const gridOnlyProps = useMemo(
() => ({
calculatedColumnWidths,
data: dataWithGroups,
@@ -381,11 +318,11 @@ const VirtualizedTableGrid = ({
getGroupRenderData,
getRowItem,
groupHeaderInfoByRowIndex,
+ hasAlbumGroupColumn: parsedColumns.some((col) => col.id === TableColumn.ALBUM_GROUP),
pinnedLeftColumnCount,
pinnedLeftColumnWidths,
pinnedRightColumnCount,
pinnedRightColumnWidths,
- startRowIndex,
}),
[
calculatedColumnWidths,
@@ -394,50 +331,68 @@ const VirtualizedTableGrid = ({
getAdjustedRowIndex,
getGroupRenderData,
groupHeaderInfoByRowIndex,
+ parsedColumns,
pinnedLeftColumnCount,
pinnedLeftColumnWidths,
pinnedRightColumnCount,
pinnedRightColumnWidths,
- startRowIndex,
- ],
- );
-
- const featureFlags = useMemo(
- () => ({
- enableAlternateRowColors,
- enableColumnReorder,
- enableColumnResize,
- enableDrag,
- enableExpansion,
- enableHorizontalBorders,
- enableRowHoverHighlight,
- enableSelection,
- enableVerticalBorders,
- groups,
- }),
- [
- enableAlternateRowColors,
- enableColumnReorder,
- enableColumnResize,
- enableDrag,
- enableExpansion,
- enableHorizontalBorders,
- enableRowHoverHighlight,
- enableSelection,
- enableVerticalBorders,
- groups,
],
);
const itemProps: TableItemProps = useMemo(
() => ({
- ...stableConfigProps,
- ...dynamicDataProps,
- ...featureFlags,
+ cellPadding: tableConfig.cellPadding,
+ columns: tableConfig.columns,
+ controls: tableConfig.controls,
+ enableAlternateRowColors: tableConfig.enableAlternateRowColors,
+ enableColumnReorder: tableConfig.enableColumnReorder,
+ enableColumnResize: tableConfig.enableColumnResize,
+ enableDrag: tableConfig.enableDrag,
+ enableExpansion: tableConfig.enableExpansion,
+ enableHeader: tableConfig.enableHeader,
+ enableHorizontalBorders: tableConfig.enableHorizontalBorders,
+ enableRowHoverHighlight: tableConfig.enableRowHoverHighlight,
+ enableSelection: tableConfig.enableSelection,
+ enableVerticalBorders: tableConfig.enableVerticalBorders,
+ getRowHeight: tableConfig.getRowHeight,
+ groups: tableConfig.groups,
+ internalState: tableConfig.internalState,
+ itemType: tableConfig.itemType,
+ playerContext: tableConfig.playerContext,
+ playlistId: tableConfig.playlistId,
+ size: tableConfig.size,
+ startRowIndex: tableConfig.startRowIndex,
+ tableId: tableConfig.tableId,
+ ...gridOnlyProps,
}),
- [stableConfigProps, dynamicDataProps, featureFlags],
+ [gridOnlyProps, tableConfig],
);
+ const pinnedLeftGridMinWidthPx = useMemo(() => {
+ let sum = 0;
+ for (let i = 0; i < pinnedLeftColumnCount; i++) {
+ sum += calculatedColumnWidths[i] ?? 0;
+ }
+ return sum;
+ }, [calculatedColumnWidths, pinnedLeftColumnCount]);
+
+ const pinnedRightGridMinWidthPx = useMemo(() => {
+ let sum = 0;
+ const start = pinnedLeftColumnCount + totalColumnCount;
+ for (let i = 0; i < pinnedRightColumnCount; i++) {
+ sum += calculatedColumnWidths[start + i] ?? 0;
+ }
+ return sum;
+ }, [calculatedColumnWidths, pinnedLeftColumnCount, pinnedRightColumnCount, totalColumnCount]);
+
+ const pinnedRowsMinHeightPx = useMemo(() => {
+ let sum = 0;
+ for (let i = 0; i < pinnedRowCount; i++) {
+ sum += getRowHeight(i, itemProps);
+ }
+ return sum;
+ }, [getRowHeight, itemProps, pinnedRowCount]);
+
const PinnedRowCell = useCallback(
(cellProps: CellComponentProps & TableItemProps) => {
return (
@@ -447,16 +402,14 @@ const VirtualizedTableGrid = ({
/>
);
},
- // eslint-disable-next-line react-hooks/exhaustive-deps
- [pinnedLeftColumnCount, CellComponent, featureFlags, calculatedColumnWidths],
+ [pinnedLeftColumnCount, CellComponent],
);
const PinnedColumnCell = useCallback(
(cellProps: CellComponentProps & TableItemProps) => {
return ;
},
- // eslint-disable-next-line react-hooks/exhaustive-deps
- [pinnedRowCount, CellComponent, featureFlags, calculatedColumnWidths],
+ [pinnedRowCount, CellComponent],
);
const PinnedRightColumnCell = useCallback(
@@ -469,15 +422,7 @@ const VirtualizedTableGrid = ({
/>
);
},
- // eslint-disable-next-line react-hooks/exhaustive-deps
- [
- pinnedLeftColumnCount,
- pinnedRowCount,
- totalColumnCount,
- CellComponent,
- featureFlags,
- calculatedColumnWidths,
- ],
+ [pinnedLeftColumnCount, pinnedRowCount, totalColumnCount, CellComponent],
);
const PinnedRightIntersectionCell = useCallback(
@@ -489,14 +434,7 @@ const VirtualizedTableGrid = ({
/>
);
},
- // eslint-disable-next-line react-hooks/exhaustive-deps
- [
- pinnedLeftColumnCount,
- totalColumnCount,
- CellComponent,
- featureFlags,
- calculatedColumnWidths,
- ],
+ [pinnedLeftColumnCount, totalColumnCount, CellComponent],
);
const RowCell = useCallback(
@@ -509,14 +447,7 @@ const VirtualizedTableGrid = ({
/>
);
},
- // eslint-disable-next-line react-hooks/exhaustive-deps
- [
- pinnedLeftColumnCount,
- pinnedRowCount,
- CellComponent,
- featureFlags,
- calculatedColumnWidths,
- ],
+ [pinnedLeftColumnCount, pinnedRowCount, CellComponent],
);
const handleOnCellsRendered = useCallback(
@@ -541,10 +472,7 @@ const VirtualizedTableGrid = ({
style={
{
'--header-height': `${headerHeight}px`,
- minWidth: `${Array.from({ length: pinnedLeftColumnCount }, () => 0).reduce(
- (a, _, i) => a + columnWidth(i),
- 0,
- )}px`,
+ minWidth: `${pinnedLeftGridMinWidthPx}px`,
} as React.CSSProperties
}
>
@@ -554,10 +482,7 @@ const VirtualizedTableGrid = ({
[styles.withHeader]: enableHeader,
})}
style={{
- minHeight: `${Array.from({ length: pinnedRowCount }, () => 0).reduce(
- (a, _, i) => a + getRowHeight(i, itemProps),
- 0,
- )}px`,
+ minHeight: `${pinnedRowsMinHeightPx}px`,
overflow: 'hidden',
}}
>
@@ -611,10 +536,7 @@ const VirtualizedTableGrid = ({
style={
{
'--header-height': `${headerHeight}px`,
- minHeight: `${Array.from(
- { length: pinnedRowCount },
- () => 0,
- ).reduce((a, _, i) => a + getRowHeight(i, itemProps), 0)}px`,
+ minHeight: `${pinnedRowsMinHeightPx}px`,
overflow: 'hidden',
} as React.CSSProperties
}
@@ -627,7 +549,7 @@ const VirtualizedTableGrid = ({
columnWidth={(index) => {
return columnWidth(index + pinnedLeftColumnCount);
}}
- rowCount={Array.from({ length: pinnedRowCount }, () => 0).length}
+ rowCount={pinnedRowCount}
rowHeight={getRowHeight}
/>
@@ -660,14 +582,7 @@ const VirtualizedTableGrid = ({
style={
{
'--header-height': `${headerHeight}px`,
- minWidth: `${Array.from(
- { length: pinnedRightColumnCount },
- () => 0,
- ).reduce(
- (a, _, i) =>
- a + columnWidth(i + pinnedLeftColumnCount + totalColumnCount),
- 0,
- )}px`,
+ minWidth: `${pinnedRightGridMinWidthPx}px`,
} as React.CSSProperties
}
>
@@ -677,10 +592,7 @@ const VirtualizedTableGrid = ({
[styles.withHeader]: enableHeader,
})}
style={{
- minHeight: `${Array.from(
- { length: pinnedRowCount },
- () => 0,
- ).reduce((a, _, i) => a + getRowHeight(i, itemProps), 0)}px`,
+ minHeight: `${pinnedRowsMinHeightPx}px`,
overflow: 'hidden',
}}
>
@@ -739,27 +651,12 @@ const MemoizedVirtualizedTableGrid = memo(VirtualizedTableGrid, (prevProps, next
prevProps.calculatedColumnWidths,
nextProps.calculatedColumnWidths,
) &&
- prevProps.cellPadding === nextProps.cellPadding &&
- prevProps.controls === nextProps.controls &&
+ prevProps.tableConfig === nextProps.tableConfig &&
prevProps.data === nextProps.data &&
prevProps.dataWithGroups === nextProps.dataWithGroups &&
- prevProps.enableAlternateRowColors === nextProps.enableAlternateRowColors &&
- prevProps.enableColumnReorder === nextProps.enableColumnReorder &&
- prevProps.enableColumnResize === nextProps.enableColumnResize &&
- prevProps.enableDrag === nextProps.enableDrag &&
- prevProps.enableExpansion === nextProps.enableExpansion &&
- prevProps.enableHeader === nextProps.enableHeader &&
- prevProps.enableHorizontalBorders === nextProps.enableHorizontalBorders &&
- prevProps.enableRowHoverHighlight === nextProps.enableRowHoverHighlight &&
prevProps.enableScrollShadow === nextProps.enableScrollShadow &&
- prevProps.enableSelection === nextProps.enableSelection &&
- prevProps.enableVerticalBorders === nextProps.enableVerticalBorders &&
prevProps.getItem === nextProps.getItem &&
- prevProps.getRowHeight === nextProps.getRowHeight &&
- prevProps.groups === nextProps.groups &&
prevProps.headerHeight === nextProps.headerHeight &&
- prevProps.internalState === nextProps.internalState &&
- prevProps.itemType === nextProps.itemType &&
prevProps.mergedRowRef === nextProps.mergedRowRef &&
prevProps.onRangeChanged === nextProps.onRangeChanged &&
prevProps.parsedColumns === nextProps.parsedColumns &&
@@ -769,13 +666,9 @@ const MemoizedVirtualizedTableGrid = memo(VirtualizedTableGrid, (prevProps, next
prevProps.pinnedRightColumnRef === nextProps.pinnedRightColumnRef &&
prevProps.pinnedRowCount === nextProps.pinnedRowCount &&
prevProps.pinnedRowRef === nextProps.pinnedRowRef &&
- prevProps.playerContext === nextProps.playerContext &&
prevProps.showLeftShadow === nextProps.showLeftShadow &&
prevProps.showRightShadow === nextProps.showRightShadow &&
prevProps.showTopShadow === nextProps.showTopShadow &&
- prevProps.size === nextProps.size &&
- prevProps.startRowIndex === nextProps.startRowIndex &&
- prevProps.tableId === nextProps.tableId &&
prevProps.totalColumnCount === nextProps.totalColumnCount &&
prevProps.totalRowCount === nextProps.totalRowCount &&
prevProps.CellComponent === nextProps.CellComponent
@@ -828,6 +721,7 @@ export interface TableItemProps {
pinnedRightColumnCount?: number;
pinnedRightColumnWidths?: number[];
playerContext: PlayerContext;
+ playlistId?: string;
size?: ItemTableListProps['size'];
startRowIndex?: number;
tableId: string;
@@ -1309,6 +1203,7 @@ const BaseItemTableList = ({
size = 'default',
startRowIndex,
}: ItemTableListProps) => {
+ const { playlistId: routePlaylistId } = useParams() as { playlistId?: string };
const tableId = useId();
const baseItemCount = itemCount ?? data.length;
const totalItemCount = enableHeader ? baseItemCount + 1 : baseItemCount;
@@ -1574,6 +1469,7 @@ const BaseItemTableList = ({
pinnedLeftColumnCount + totalColumnCount,
),
playerContext,
+ playlistId: routePlaylistId,
size,
tableId,
}),
@@ -1599,6 +1495,7 @@ const BaseItemTableList = ({
pinnedLeftColumnCount,
pinnedRightColumnCount,
playerContext,
+ routePlaylistId,
size,
tableId,
totalColumnCount,
@@ -1612,17 +1509,27 @@ const BaseItemTableList = ({
itemType,
});
- const tableConfigValue = useMemo(
+ const tableConfigValue = useMemo(
() => ({
cellPadding,
columns: parsedColumns,
controls,
+ enableAlternateRowColors,
+ enableColumnReorder: !!onColumnReordered,
+ enableColumnResize: !!onColumnResized,
+ enableDrag,
+ enableExpansion,
enableHeader,
+ enableHorizontalBorders,
enableRowHoverHighlight,
enableSelection,
+ enableVerticalBorders,
+ getRowHeight,
+ groups,
internalState,
itemType,
playerContext,
+ playlistId: routePlaylistId,
size,
startRowIndex,
tableId,
@@ -1631,12 +1538,22 @@ const BaseItemTableList = ({
cellPadding,
parsedColumns,
controls,
+ enableAlternateRowColors,
+ onColumnReordered,
+ onColumnResized,
+ enableDrag,
+ enableExpansion,
enableHeader,
+ enableHorizontalBorders,
enableRowHoverHighlight,
enableSelection,
+ enableVerticalBorders,
+ getRowHeight,
+ groups,
internalState,
itemType,
playerContext,
+ routePlaylistId,
size,
startRowIndex,
tableId,
@@ -1707,27 +1624,11 @@ const BaseItemTableList = ({
diff --git a/src/renderer/components/item-list/item-table-list/memoized-cell-router.tsx b/src/renderer/components/item-list/item-table-list/memoized-cell-router.tsx
index 61a2aa154..9431c5353 100644
--- a/src/renderer/components/item-list/item-table-list/memoized-cell-router.tsx
+++ b/src/renderer/components/item-list/item-table-list/memoized-cell-router.tsx
@@ -1,4 +1,4 @@
-import React, { memo, useMemo } from 'react';
+import React, { useMemo } from 'react';
import { CellComponentProps } from 'react-window-v2';
import { createColumnCellComponents } from './cell-component-factory';
@@ -24,24 +24,7 @@ const MemoizedCellRouterBase = (props: MemoizedCellRouterProps) => {
return ;
};
-export const MemoizedCellRouter = memo(MemoizedCellRouterBase, (prevProps, nextProps) => {
- return (
- prevProps.rowIndex === nextProps.rowIndex &&
- prevProps.columnIndex === nextProps.columnIndex &&
- prevProps.data === nextProps.data &&
- prevProps.columns === nextProps.columns &&
- prevProps.columnCellComponents === nextProps.columnCellComponents &&
- prevProps.size === nextProps.size &&
- prevProps.enableAlternateRowColors === nextProps.enableAlternateRowColors &&
- prevProps.enableHorizontalBorders === nextProps.enableHorizontalBorders &&
- prevProps.enableVerticalBorders === nextProps.enableVerticalBorders &&
- prevProps.enableRowHoverHighlight === nextProps.enableRowHoverHighlight &&
- prevProps.enableSelection === nextProps.enableSelection &&
- prevProps.enableColumnResize === nextProps.enableColumnResize &&
- prevProps.enableColumnReorder === nextProps.enableColumnReorder &&
- prevProps.cellPadding === nextProps.cellPadding
- );
-});
+export const MemoizedCellRouter = MemoizedCellRouterBase;
export const useColumnCellComponents = (
columns: TableColumn[],