diff --git a/src/renderer/components/item-list/helpers/use-item-list-scroll-persist.ts b/src/renderer/components/item-list/helpers/use-item-list-scroll-persist.ts index 92453b359..76f829648 100644 --- a/src/renderer/components/item-list/helpers/use-item-list-scroll-persist.ts +++ b/src/renderer/components/item-list/helpers/use-item-list-scroll-persist.ts @@ -1,4 +1,4 @@ -import { useMemo } from 'react'; +import { useCallback, useMemo } from 'react'; import { useSearchParams } from 'react-router'; import { parseIntParam, setSearchParam } from '/@/renderer/utils/query-params'; @@ -12,11 +12,16 @@ export const useItemListScrollPersist = ({ enabled }: UseItemListScrollPersistPr const scrollOffset = useMemo(() => parseIntParam(searchParams, 'scrollOffset'), [searchParams]); - const handleOnScrollEnd = (offset: number) => { - if (!enabled) return; + const handleOnScrollEnd = useCallback( + (offset: number) => { + if (!enabled) return; - setSearchParams((prev) => setSearchParam(prev, 'scrollOffset', offset), { replace: true }); - }; + setSearchParams((prev) => setSearchParam(prev, 'scrollOffset', offset), { + replace: true, + }); + }, + [enabled, setSearchParams], + ); return { handleOnScrollEnd, scrollOffset }; }; 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 new file mode 100644 index 000000000..a641edcf6 --- /dev/null +++ b/src/renderer/components/item-list/item-table-list/cell-component-factory.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { CellComponentProps } from 'react-window-v2'; + +import { TableItemProps } from '/@/renderer/components/item-list/item-table-list/item-table-list'; +import { ItemTableListColumn } from '/@/renderer/components/item-list/item-table-list/item-table-list-column'; +import { LibraryItem } from '/@/shared/types/domain-types'; +import { TableColumn } from '/@/shared/types/types'; + +export const createColumnCellComponent = ( + columnType: TableColumn, + itemType: LibraryItem, +): React.ComponentType> => { + return React.memo( + (props: CellComponentProps) => { + return ; + }, + (prevProps, nextProps) => { + return ( + prevProps.rowIndex === nextProps.rowIndex && + prevProps.columnIndex === nextProps.columnIndex && + prevProps.data === nextProps.data && + prevProps.style === nextProps.style && + prevProps.columns === nextProps.columns + ); + }, + ); +}; + +export const createColumnCellComponents = ( + columns: TableColumn[], + itemType: LibraryItem, +): Map>> => { + const componentMap = new Map< + TableColumn, + React.ComponentType> + >(); + + columns.forEach((columnType) => { + componentMap.set(columnType, createColumnCellComponent(columnType, itemType)); + }); + + return componentMap; +}; diff --git a/src/renderer/components/item-list/item-table-list/columns/actions-column.tsx b/src/renderer/components/item-list/item-table-list/columns/actions-column.tsx index c6aa14fb8..29ddef595 100644 --- a/src/renderer/components/item-list/item-table-list/columns/actions-column.tsx +++ b/src/renderer/components/item-list/item-table-list/columns/actions-column.tsx @@ -1,3 +1,5 @@ +import { memo } from 'react'; + import { ItemTableListInnerColumn, TableColumnContainer, @@ -5,7 +7,7 @@ import { import { ItemListItem } from '/@/renderer/components/item-list/types'; import { ActionIcon } from '/@/shared/components/action-icon/action-icon'; -export const ActionsColumn = (props: ItemTableListInnerColumn) => { +const ActionsColumnBase = (props: ItemTableListInnerColumn) => { const row: any = props.getRowItem?.(props.rowIndex) ?? (props.data as any[])[props.rowIndex]; const handleActionClick = (event: React.MouseEvent) => { @@ -51,3 +53,16 @@ export const ActionsColumn = (props: ItemTableListInnerColumn) => { return  ; }; + +export const ActionsColumn = memo(ActionsColumnBase, (prevProps, nextProps) => { + const prevItem = prevProps.getRowItem?.(prevProps.rowIndex); + const nextItem = nextProps.getRowItem?.(nextProps.rowIndex); + + return ( + prevProps.rowIndex === nextProps.rowIndex && + prevProps.columnIndex === nextProps.columnIndex && + prevProps.data === nextProps.data && + prevProps.columns === nextProps.columns && + prevItem === nextItem + ); +}); diff --git a/src/renderer/components/item-list/item-table-list/columns/album-artists-column.tsx b/src/renderer/components/item-list/item-table-list/columns/album-artists-column.tsx index 1798788b8..527bbc93a 100644 --- a/src/renderer/components/item-list/item-table-list/columns/album-artists-column.tsx +++ b/src/renderer/components/item-list/item-table-list/columns/album-artists-column.tsx @@ -54,6 +54,14 @@ const AlbumArtistsColumn = (props: ItemTableListInnerColumn) => { return ; }; -export const AlbumArtistsColumnMemo = memo(AlbumArtistsColumn); +export const AlbumArtistsColumnMemo = memo(AlbumArtistsColumn, (prevProps, nextProps) => { + return ( + prevProps.rowIndex === nextProps.rowIndex && + prevProps.columnIndex === nextProps.columnIndex && + prevProps.data === nextProps.data && + prevProps.columns === nextProps.columns && + prevProps.size === nextProps.size + ); +}); export { AlbumArtistsColumnMemo as AlbumArtistsColumn }; diff --git a/src/renderer/components/item-list/item-table-list/columns/album-column.tsx b/src/renderer/components/item-list/item-table-list/columns/album-column.tsx index dcd727279..abdf4a36c 100644 --- a/src/renderer/components/item-list/item-table-list/columns/album-column.tsx +++ b/src/renderer/components/item-list/item-table-list/columns/album-column.tsx @@ -75,6 +75,18 @@ const AlbumColumn = (props: ItemTableListInnerColumn) => { return ; }; -export const AlbumColumnMemo = memo(AlbumColumn); +export const AlbumColumnMemo = memo(AlbumColumn, (prevProps, nextProps) => { + const prevItem = prevProps.getRowItem?.(prevProps.rowIndex); + const nextItem = nextProps.getRowItem?.(nextProps.rowIndex); + + return ( + prevProps.rowIndex === nextProps.rowIndex && + prevProps.columnIndex === nextProps.columnIndex && + prevProps.data === nextProps.data && + prevProps.columns === nextProps.columns && + prevProps.size === nextProps.size && + prevItem === nextItem + ); +}); export { AlbumColumnMemo as AlbumColumn }; diff --git a/src/renderer/components/item-list/item-table-list/columns/artists-column.tsx b/src/renderer/components/item-list/item-table-list/columns/artists-column.tsx index 2c7ea1d88..97df53460 100644 --- a/src/renderer/components/item-list/item-table-list/columns/artists-column.tsx +++ b/src/renderer/components/item-list/item-table-list/columns/artists-column.tsx @@ -109,6 +109,15 @@ const BaseArtistsColumn = (props: ItemTableListInnerColumn) => { } }; -const ArtistsColumnMemo = memo(BaseArtistsColumn); +const ArtistsColumnMemo = memo(BaseArtistsColumn, (prevProps, nextProps) => { + return ( + prevProps.rowIndex === nextProps.rowIndex && + prevProps.columnIndex === nextProps.columnIndex && + prevProps.data === nextProps.data && + prevProps.columns === nextProps.columns && + prevProps.itemType === nextProps.itemType && + prevProps.size === nextProps.size + ); +}); export { ArtistsColumnMemo as ArtistsColumn }; diff --git a/src/renderer/components/item-list/item-table-list/columns/composer-column.tsx b/src/renderer/components/item-list/item-table-list/columns/composer-column.tsx index c655bb1e2..9cf40577a 100644 --- a/src/renderer/components/item-list/item-table-list/columns/composer-column.tsx +++ b/src/renderer/components/item-list/item-table-list/columns/composer-column.tsx @@ -45,6 +45,18 @@ const ComposerColumn = (props: ItemTableListInnerColumn) => { return ; }; -export const ComposerColumnMemo = memo(ComposerColumn); +export const ComposerColumnMemo = memo(ComposerColumn, (prevProps, nextProps) => { + const prevItem = prevProps.getRowItem?.(prevProps.rowIndex); + const nextItem = nextProps.getRowItem?.(nextProps.rowIndex); + + return ( + prevProps.rowIndex === nextProps.rowIndex && + prevProps.columnIndex === nextProps.columnIndex && + prevProps.data === nextProps.data && + prevProps.columns === nextProps.columns && + prevProps.size === nextProps.size && + prevItem === nextItem + ); +}); export { ComposerColumnMemo as ComposerColumn }; 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 88d45d9c0..b57a90c87 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 @@ -1,3 +1,5 @@ +import { memo, useMemo } from 'react'; + import { ColumnNullFallback, ColumnSkeletonFixed, @@ -29,15 +31,25 @@ const getDateTooltipLabel = (utcString: string) => { ); }; -export const DateColumn = (props: ItemTableListInnerColumn) => { +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]; + const { formattedDate, tooltipLabel } = useMemo(() => { + if (typeof row === 'string' && row) { + return { + formattedDate: formatDateAbsolute(row), + tooltipLabel: getDateTooltipLabel(row), + }; + } + return { formattedDate: null, tooltipLabel: null }; + }, [row]); + if (typeof row === 'string' && row) { return ( - - {formatDateAbsolute(row)} + + {formattedDate} ); @@ -50,42 +62,70 @@ export const DateColumn = (props: ItemTableListInnerColumn) => { return ; }; -export const AbsoluteDateColumn = (props: ItemTableListInnerColumn) => { +export const DateColumn = memo(DateColumnBase, (prevProps, nextProps) => { + return ( + prevProps.rowIndex === nextProps.rowIndex && + prevProps.columnIndex === nextProps.columnIndex && + prevProps.data === nextProps.data && + prevProps.columns === nextProps.columns + ); +}); + +const AbsoluteDateColumnBase = (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]; + const releaseDateContent = useMemo(() => { + if (props.type === TableColumn.RELEASE_DATE) { + const item = rowItem as any; + if (item && 'releaseDate' in item && item.releaseDate) { + const releaseDate = item.releaseDate; + const originalDate = + 'originalDate' in item && item.originalDate && item.originalDate !== releaseDate + ? item.originalDate + : null; + + if (originalDate) { + const formattedOriginalDate = formatDateAbsoluteUTC(originalDate); + const formattedReleaseDate = formatDateAbsoluteUTC(releaseDate); + const displayText = `${formattedOriginalDate}${SEPARATOR_STRING}${formattedReleaseDate}`; + + return { + displayText, + tooltipLabel: getDateTooltipLabel(releaseDate), + }; + } + + if (typeof releaseDate === 'string' && releaseDate) { + return { + displayText: formatDateAbsoluteUTC(releaseDate), + tooltipLabel: getDateTooltipLabel(releaseDate), + }; + } + } + } + return null; + }, [props.type, rowItem]); + + const { formattedDate, tooltipLabel } = useMemo(() => { + if (typeof row === 'string' && row) { + return { + formattedDate: formatDateAbsoluteUTC(row), + tooltipLabel: getDateTooltipLabel(row), + }; + } + return { formattedDate: null, tooltipLabel: null }; + }, [row]); + if (props.type === TableColumn.RELEASE_DATE) { - const item = rowItem as any; - if (item && 'releaseDate' in item && item.releaseDate) { - const releaseDate = item.releaseDate; - const originalDate = - 'originalDate' in item && item.originalDate && item.originalDate !== releaseDate - ? item.originalDate - : null; - - if (originalDate) { - const formattedOriginalDate = formatDateAbsoluteUTC(originalDate); - const formattedReleaseDate = formatDateAbsoluteUTC(releaseDate); - const displayText = `${formattedOriginalDate}${SEPARATOR_STRING}${formattedReleaseDate}`; - - return ( - - - {displayText} - - - ); - } - - if (typeof releaseDate === 'string' && releaseDate) { - return ( - - - {formatDateAbsoluteUTC(releaseDate)} - - - ); - } + if (releaseDateContent) { + return ( + + + {releaseDateContent.displayText} + + + ); } if (row === null) { @@ -98,8 +138,8 @@ export const AbsoluteDateColumn = (props: ItemTableListInnerColumn) => { if (typeof row === 'string' && row) { return ( - - {formatDateAbsoluteUTC(row)} + + {formattedDate} ); @@ -112,15 +152,35 @@ export const AbsoluteDateColumn = (props: ItemTableListInnerColumn) => { return ; }; -export const RelativeDateColumn = (props: ItemTableListInnerColumn) => { +export const AbsoluteDateColumn = memo(AbsoluteDateColumnBase, (prevProps, nextProps) => { + return ( + prevProps.rowIndex === nextProps.rowIndex && + prevProps.columnIndex === nextProps.columnIndex && + prevProps.data === nextProps.data && + prevProps.columns === nextProps.columns && + prevProps.type === nextProps.type + ); +}); + +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]; + const { formattedDate, tooltipLabel } = useMemo(() => { + if (typeof row === 'string') { + return { + formattedDate: formatDateRelative(row), + tooltipLabel: getDateTooltipLabel(row), + }; + } + return { formattedDate: null, tooltipLabel: null }; + }, [row]); + if (typeof row === 'string') { return ( - - {formatDateRelative(row)} + + {formattedDate} ); @@ -132,3 +192,12 @@ export const RelativeDateColumn = (props: ItemTableListInnerColumn) => { return ; }; + +export const RelativeDateColumn = memo(RelativeDateColumnBase, (prevProps, nextProps) => { + return ( + prevProps.rowIndex === nextProps.rowIndex && + prevProps.columnIndex === nextProps.columnIndex && + prevProps.data === nextProps.data && + prevProps.columns === nextProps.columns + ); +}); diff --git a/src/renderer/components/item-list/item-table-list/columns/default-column.tsx b/src/renderer/components/item-list/item-table-list/columns/default-column.tsx index cf5cbcb62..d84a232d2 100644 --- a/src/renderer/components/item-list/item-table-list/columns/default-column.tsx +++ b/src/renderer/components/item-list/item-table-list/columns/default-column.tsx @@ -1,3 +1,5 @@ +import { memo } from 'react'; + import { ColumnNullFallback, ColumnSkeletonFixed, @@ -5,7 +7,7 @@ import { TableColumnTextContainer, } from '/@/renderer/components/item-list/item-table-list/item-table-list-column'; -export const DefaultColumn = (props: ItemTableListInnerColumn) => { +const DefaultColumnBase = (props: ItemTableListInnerColumn) => { const rowItem = props.getRowItem?.(props.rowIndex) ?? (props.data as any[])[props.rowIndex]; const row: any | undefined = (rowItem as any)?.[props.columns[props.columnIndex].id]; @@ -19,3 +21,12 @@ export const DefaultColumn = (props: ItemTableListInnerColumn) => { return ; }; + +export const DefaultColumn = memo(DefaultColumnBase, (prevProps, nextProps) => { + return ( + prevProps.rowIndex === nextProps.rowIndex && + prevProps.columnIndex === nextProps.columnIndex && + prevProps.data === nextProps.data && + prevProps.columns === nextProps.columns + ); +}); diff --git a/src/renderer/components/item-list/item-table-list/columns/duration-column.tsx b/src/renderer/components/item-list/item-table-list/columns/duration-column.tsx index 452ca3666..4e79f6941 100644 --- a/src/renderer/components/item-list/item-table-list/columns/duration-column.tsx +++ b/src/renderer/components/item-list/item-table-list/columns/duration-column.tsx @@ -1,4 +1,5 @@ import formatDuration from 'format-duration'; +import React, { useMemo } from 'react'; import { ColumnNullFallback, @@ -7,14 +8,16 @@ import { TableColumnTextContainer, } from '/@/renderer/components/item-list/item-table-list/item-table-list-column'; -export const DurationColumn = (props: ItemTableListInnerColumn) => { +const DurationColumnBase = (props: ItemTableListInnerColumn) => { const rowItem = props.getRowItem?.(props.rowIndex) ?? (props.data as any[])[props.rowIndex]; const row: number | undefined = (rowItem as any)?.[props.columns[props.columnIndex].id]; + const formattedDuration = useMemo(() => { + return typeof row === 'number' ? formatDuration(row) : null; + }, [row]); + if (typeof row === 'number') { - return ( - {formatDuration(row)} - ); + return {formattedDuration}; } if (row === null) { @@ -23,3 +26,12 @@ export const DurationColumn = (props: ItemTableListInnerColumn) => { return ; }; + +export const DurationColumn = React.memo(DurationColumnBase, (prevProps, nextProps) => { + return ( + prevProps.rowIndex === nextProps.rowIndex && + prevProps.columnIndex === nextProps.columnIndex && + prevProps.data === nextProps.data && + prevProps.columns === nextProps.columns + ); +}); diff --git a/src/renderer/components/item-list/item-table-list/columns/favorite-column.tsx b/src/renderer/components/item-list/item-table-list/columns/favorite-column.tsx index 783a96414..f8bcf5152 100644 --- a/src/renderer/components/item-list/item-table-list/columns/favorite-column.tsx +++ b/src/renderer/components/item-list/item-table-list/columns/favorite-column.tsx @@ -1,3 +1,5 @@ +import { memo } from 'react'; + import { ItemTableListInnerColumn, TableColumnContainer, @@ -7,7 +9,7 @@ import { useIsMutatingCreateFavorite } from '/@/renderer/features/shared/mutatio import { useIsMutatingDeleteFavorite } from '/@/renderer/features/shared/mutations/delete-favorite-mutation'; import { ActionIcon } from '/@/shared/components/action-icon/action-icon'; -export const FavoriteColumn = (props: ItemTableListInnerColumn) => { +const FavoriteColumnBase = (props: ItemTableListInnerColumn) => { const rowItem = props.getRowItem?.(props.rowIndex) ?? (props.data as any[])[props.rowIndex]; const row: boolean | undefined = rowItem?.[props.columns[props.columnIndex].id]; @@ -55,3 +57,19 @@ export const FavoriteColumn = (props: ItemTableListInnerColumn) => { return  ; }; + +export const FavoriteColumn = memo(FavoriteColumnBase, (prevProps, nextProps) => { + const prevItem = prevProps.getRowItem?.(prevProps.rowIndex); + const nextItem = nextProps.getRowItem?.(nextProps.rowIndex); + const prevFavorite = prevItem?.[prevProps.columns[prevProps.columnIndex].id]; + const nextFavorite = nextItem?.[nextProps.columns[nextProps.columnIndex].id]; + + return ( + prevProps.rowIndex === nextProps.rowIndex && + prevProps.columnIndex === nextProps.columnIndex && + prevProps.data === nextProps.data && + prevProps.columns === nextProps.columns && + prevItem === nextItem && + prevFavorite === nextFavorite + ); +}); diff --git a/src/renderer/components/item-list/item-table-list/columns/genre-badge-column.tsx b/src/renderer/components/item-list/item-table-list/columns/genre-badge-column.tsx index 6d21d9302..5965dd340 100644 --- a/src/renderer/components/item-list/item-table-list/columns/genre-badge-column.tsx +++ b/src/renderer/components/item-list/item-table-list/columns/genre-badge-column.tsx @@ -60,6 +60,13 @@ const GenreBadgeColumn = (props: ItemTableListInnerColumn) => { return ; }; -export const GenreColumnMemo = memo(GenreBadgeColumn); +export const GenreColumnMemo = memo(GenreBadgeColumn, (prevProps, nextProps) => { + return ( + prevProps.rowIndex === nextProps.rowIndex && + prevProps.columnIndex === nextProps.columnIndex && + prevProps.data === nextProps.data && + prevProps.columns === nextProps.columns + ); +}); export { GenreColumnMemo as GenreBadgeColumn }; diff --git a/src/renderer/components/item-list/item-table-list/columns/genre-column.tsx b/src/renderer/components/item-list/item-table-list/columns/genre-column.tsx index 6cd1cb9d4..a5ca61643 100644 --- a/src/renderer/components/item-list/item-table-list/columns/genre-column.tsx +++ b/src/renderer/components/item-list/item-table-list/columns/genre-column.tsx @@ -64,6 +64,14 @@ const GenreColumn = (props: ItemTableListInnerColumn) => { return ; }; -export const GenreColumnMemo = memo(GenreColumn); +export const GenreColumnMemo = memo(GenreColumn, (prevProps, nextProps) => { + return ( + prevProps.rowIndex === nextProps.rowIndex && + prevProps.columnIndex === nextProps.columnIndex && + prevProps.data === nextProps.data && + prevProps.columns === nextProps.columns && + prevProps.size === nextProps.size + ); +}); export { GenreColumnMemo as GenreColumn }; diff --git a/src/renderer/components/item-list/item-table-list/columns/image-column.tsx b/src/renderer/components/item-list/item-table-list/columns/image-column.tsx index 8eead5878..80802e907 100644 --- a/src/renderer/components/item-list/item-table-list/columns/image-column.tsx +++ b/src/renderer/components/item-list/item-table-list/columns/image-column.tsx @@ -1,5 +1,5 @@ import clsx from 'clsx'; -import { useState } from 'react'; +import { memo, useState } from 'react'; import styles from './image-column.module.css'; @@ -19,7 +19,7 @@ import { Skeleton } from '/@/shared/components/skeleton/skeleton'; import { Folder, LibraryItem } from '/@/shared/types/domain-types'; import { Play } from '/@/shared/types/types'; -export const ImageColumn = (props: ItemTableListInnerColumn) => { +const ImageColumnBase = (props: ItemTableListInnerColumn) => { const rowItem = props.getRowItem?.(props.rowIndex) ?? (props.data as any[])[props.rowIndex]; const row: string | undefined = rowItem?.id; const item = rowItem as any; @@ -136,3 +136,18 @@ export const ImageColumn = (props: ItemTableListInnerColumn) => { ); }; + +export const ImageColumn = memo(ImageColumnBase, (prevProps, nextProps) => { + const prevItem = prevProps.getRowItem?.(prevProps.rowIndex); + const nextItem = nextProps.getRowItem?.(nextProps.rowIndex); + + return ( + prevProps.rowIndex === nextProps.rowIndex && + prevProps.columnIndex === nextProps.columnIndex && + prevProps.data === nextProps.data && + prevProps.columns === nextProps.columns && + prevProps.itemType === nextProps.itemType && + prevProps.size === nextProps.size && + prevItem === nextItem + ); +}); diff --git a/src/renderer/components/item-list/item-table-list/columns/numeric-column.tsx b/src/renderer/components/item-list/item-table-list/columns/numeric-column.tsx index b8c6b601c..31150386f 100644 --- a/src/renderer/components/item-list/item-table-list/columns/numeric-column.tsx +++ b/src/renderer/components/item-list/item-table-list/columns/numeric-column.tsx @@ -1,3 +1,5 @@ +import { memo } from 'react'; + import { ColumnNullFallback, ColumnSkeletonFixed, @@ -5,7 +7,7 @@ import { TableColumnTextContainer, } from '/@/renderer/components/item-list/item-table-list/item-table-list-column'; -export const NumericColumn = (props: ItemTableListInnerColumn) => { +const NumericColumnBase = (props: ItemTableListInnerColumn) => { const rowItem = props.getRowItem?.(props.rowIndex) ?? (props.data as any[])[props.rowIndex]; const row: number | undefined = (rowItem as any)?.[props.columns[props.columnIndex].id]; @@ -19,3 +21,12 @@ export const NumericColumn = (props: ItemTableListInnerColumn) => { return ; }; + +export const NumericColumn = memo(NumericColumnBase, (prevProps, nextProps) => { + return ( + prevProps.rowIndex === nextProps.rowIndex && + prevProps.columnIndex === nextProps.columnIndex && + prevProps.data === nextProps.data && + prevProps.columns === nextProps.columns + ); +}); diff --git a/src/renderer/components/item-list/item-table-list/columns/path-column.tsx b/src/renderer/components/item-list/item-table-list/columns/path-column.tsx index 52f5ef289..24e28a8dd 100644 --- a/src/renderer/components/item-list/item-table-list/columns/path-column.tsx +++ b/src/renderer/components/item-list/item-table-list/columns/path-column.tsx @@ -1,3 +1,5 @@ +import { memo } from 'react'; + import { ColumnNullFallback, ColumnSkeletonVariable, @@ -5,7 +7,7 @@ import { TableColumnTextContainer, } from '/@/renderer/components/item-list/item-table-list/item-table-list-column'; -export const PathColumn = (props: ItemTableListInnerColumn) => { +const PathColumnBase = (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]; @@ -23,3 +25,12 @@ export const PathColumn = (props: ItemTableListInnerColumn) => { return ; }; + +export const PathColumn = memo(PathColumnBase, (prevProps, nextProps) => { + return ( + prevProps.rowIndex === nextProps.rowIndex && + prevProps.columnIndex === nextProps.columnIndex && + prevProps.data === nextProps.data && + prevProps.columns === nextProps.columns + ); +}); diff --git a/src/renderer/components/item-list/item-table-list/columns/playlist-reorder-column.tsx b/src/renderer/components/item-list/item-table-list/columns/playlist-reorder-column.tsx index 40189c8ea..7c7072763 100644 --- a/src/renderer/components/item-list/item-table-list/columns/playlist-reorder-column.tsx +++ b/src/renderer/components/item-list/item-table-list/columns/playlist-reorder-column.tsx @@ -1,4 +1,4 @@ -import { useCallback } from 'react'; +import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { useParams } from 'react-router'; @@ -19,7 +19,7 @@ import { useLongPress } from '/@/shared/hooks/use-long-press'; import { LibraryItem } from '/@/shared/types/domain-types'; import { DragOperation, DragTarget, DragTargetMap } from '/@/shared/types/drag-and-drop'; -export const PlaylistReorderColumn = (props: ItemTableListInnerColumn) => { +const PlaylistReorderColumnBase = (props: ItemTableListInnerColumn) => { const { t } = useTranslation(); const { playlistId } = useParams() as { playlistId?: string }; const isHeaderEnabled = !!props.enableHeader; @@ -363,3 +363,18 @@ export const PlaylistReorderColumn = (props: ItemTableListInnerColumn) => { ); }; + +export const PlaylistReorderColumn = memo(PlaylistReorderColumnBase, (prevProps, nextProps) => { + const prevItem = prevProps.getRowItem?.(prevProps.rowIndex); + const nextItem = nextProps.getRowItem?.(nextProps.rowIndex); + + return ( + prevProps.rowIndex === nextProps.rowIndex && + prevProps.columnIndex === nextProps.columnIndex && + prevProps.data === nextProps.data && + prevProps.columns === nextProps.columns && + prevProps.itemType === nextProps.itemType && + prevProps.enableHeader === nextProps.enableHeader && + prevItem === nextItem + ); +}); diff --git a/src/renderer/components/item-list/item-table-list/columns/rating-column.tsx b/src/renderer/components/item-list/item-table-list/columns/rating-column.tsx index c03a7cb94..237523202 100644 --- a/src/renderer/components/item-list/item-table-list/columns/rating-column.tsx +++ b/src/renderer/components/item-list/item-table-list/columns/rating-column.tsx @@ -1,3 +1,5 @@ +import { memo } from 'react'; + import { ItemTableListInnerColumn, TableColumnContainer, @@ -6,7 +8,7 @@ import { ItemListItem } from '/@/renderer/components/item-list/types'; import { useIsMutatingRating } from '/@/renderer/features/shared/mutations/set-rating-mutation'; import { Rating } from '/@/shared/components/rating/rating'; -export const RatingColumn = (props: ItemTableListInnerColumn) => { +const RatingColumnBase = (props: ItemTableListInnerColumn) => { const rowItem = props.getRowItem?.(props.rowIndex) ?? (props.data as any[])[props.rowIndex]; const row: null | number | undefined = rowItem?.[props.columns[props.columnIndex].id]; @@ -40,3 +42,19 @@ export const RatingColumn = (props: ItemTableListInnerColumn) => { return  ; }; + +export const RatingColumn = memo(RatingColumnBase, (prevProps, nextProps) => { + const prevItem = prevProps.getRowItem?.(prevProps.rowIndex); + const nextItem = nextProps.getRowItem?.(nextProps.rowIndex); + const prevRating = prevItem?.[prevProps.columns[prevProps.columnIndex].id]; + const nextRating = nextItem?.[nextProps.columns[nextProps.columnIndex].id]; + + return ( + prevProps.rowIndex === nextProps.rowIndex && + prevProps.columnIndex === nextProps.columnIndex && + prevProps.data === nextProps.data && + prevProps.columns === nextProps.columns && + prevItem === nextItem && + prevRating === nextRating + ); +}); diff --git a/src/renderer/components/item-list/item-table-list/columns/row-index-column.tsx b/src/renderer/components/item-list/item-table-list/columns/row-index-column.tsx index f60967102..93fa6630c 100644 --- a/src/renderer/components/item-list/item-table-list/columns/row-index-column.tsx +++ b/src/renderer/components/item-list/item-table-list/columns/row-index-column.tsx @@ -18,7 +18,7 @@ import { Text } from '/@/shared/components/text/text'; import { LibraryItem, QueueSong } from '/@/shared/types/domain-types'; import { PlayerStatus } from '/@/shared/types/types'; -export const RowIndexColumn = (props: ItemTableListInnerColumn) => { +const RowIndexColumnBase = (props: ItemTableListInnerColumn) => { const { itemType } = props; switch (itemType) { @@ -32,6 +32,19 @@ export const RowIndexColumn = (props: ItemTableListInnerColumn) => { } }; +export const RowIndexColumn = memo(RowIndexColumnBase, (prevProps, nextProps) => { + return ( + prevProps.rowIndex === nextProps.rowIndex && + prevProps.columnIndex === nextProps.columnIndex && + prevProps.data === nextProps.data && + prevProps.columns === nextProps.columns && + prevProps.itemType === nextProps.itemType && + prevProps.enableExpansion === nextProps.enableExpansion && + prevProps.enableHeader === nextProps.enableHeader && + prevProps.startRowIndex === nextProps.startRowIndex + ); +}); + const DefaultRowIndexColumn = (props: ItemTableListInnerColumn) => { const { controls, diff --git a/src/renderer/components/item-list/item-table-list/columns/size-column.tsx b/src/renderer/components/item-list/item-table-list/columns/size-column.tsx index fb77d6005..5448b9ebb 100644 --- a/src/renderer/components/item-list/item-table-list/columns/size-column.tsx +++ b/src/renderer/components/item-list/item-table-list/columns/size-column.tsx @@ -1,3 +1,5 @@ +import { memo, useMemo } from 'react'; + import { ColumnNullFallback, ColumnSkeletonFixed, @@ -6,14 +8,16 @@ import { } from '/@/renderer/components/item-list/item-table-list/item-table-list-column'; import { formatSizeString } from '/@/renderer/utils/format'; -export const SizeColumn = (props: ItemTableListInnerColumn) => { +const SizeColumnBase = (props: ItemTableListInnerColumn) => { const rowItem = props.getRowItem?.(props.rowIndex) ?? (props.data as any[])[props.rowIndex]; const row: number | undefined = (rowItem as any)?.[props.columns[props.columnIndex].id]; + const formattedSize = useMemo(() => { + return typeof row === 'number' ? formatSizeString(row) : null; + }, [row]); + if (typeof row === 'number') { - return ( - {formatSizeString(row)} - ); + return {formattedSize}; } if (row === null) { @@ -22,3 +26,12 @@ export const SizeColumn = (props: ItemTableListInnerColumn) => { return ; }; + +export const SizeColumn = memo(SizeColumnBase, (prevProps, nextProps) => { + return ( + prevProps.rowIndex === nextProps.rowIndex && + prevProps.columnIndex === nextProps.columnIndex && + prevProps.data === nextProps.data && + prevProps.columns === nextProps.columns + ); +}); diff --git a/src/renderer/components/item-list/item-table-list/columns/text-column.tsx b/src/renderer/components/item-list/item-table-list/columns/text-column.tsx index fc2cc35ff..499d94569 100644 --- a/src/renderer/components/item-list/item-table-list/columns/text-column.tsx +++ b/src/renderer/components/item-list/item-table-list/columns/text-column.tsx @@ -1,4 +1,5 @@ import clsx from 'clsx'; +import { memo } from 'react'; import styles from './text-column.module.css'; @@ -9,7 +10,7 @@ import { TableColumnTextContainer, } from '/@/renderer/components/item-list/item-table-list/item-table-list-column'; -export const TextColumn = (props: ItemTableListInnerColumn) => { +const TextColumnBase = (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]; @@ -33,3 +34,13 @@ export const TextColumn = (props: ItemTableListInnerColumn) => { return ; }; + +export const TextColumn = memo(TextColumnBase, (prevProps, nextProps) => { + return ( + prevProps.rowIndex === nextProps.rowIndex && + prevProps.columnIndex === nextProps.columnIndex && + prevProps.data === nextProps.data && + prevProps.columns === nextProps.columns && + prevProps.size === nextProps.size + ); +}); diff --git a/src/renderer/components/item-list/item-table-list/columns/title-artist-column.tsx b/src/renderer/components/item-list/item-table-list/columns/title-artist-column.tsx index 60dbf1f5d..1a78c1865 100644 --- a/src/renderer/components/item-list/item-table-list/columns/title-artist-column.tsx +++ b/src/renderer/components/item-list/item-table-list/columns/title-artist-column.tsx @@ -1,5 +1,5 @@ import clsx from 'clsx'; -import { CSSProperties } from 'react'; +import { CSSProperties, memo } from 'react'; import { Link } from 'react-router'; import styles from './title-artist-column.module.css'; @@ -194,7 +194,7 @@ export const QueueSongTitleArtistColumn = (props: ItemTableListInnerColumn) => { return ; }; -export const TitleArtistColumn = (props: ItemTableListInnerColumn) => { +const TitleArtistColumnBase = (props: ItemTableListInnerColumn) => { const { itemType } = props; switch (itemType) { @@ -207,3 +207,18 @@ export const TitleArtistColumn = (props: ItemTableListInnerColumn) => { return ; } }; + +export const TitleArtistColumn = memo(TitleArtistColumnBase, (prevProps, nextProps) => { + const prevItem = prevProps.getRowItem?.(prevProps.rowIndex); + const nextItem = nextProps.getRowItem?.(nextProps.rowIndex); + + return ( + prevProps.rowIndex === nextProps.rowIndex && + prevProps.columnIndex === nextProps.columnIndex && + prevProps.data === nextProps.data && + prevProps.columns === nextProps.columns && + prevProps.itemType === nextProps.itemType && + prevProps.size === nextProps.size && + prevItem === nextItem + ); +}); 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 4647294e4..e11fa2fef 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 { memo } from 'react'; import { Link } from 'react-router'; import styles from './title-column.module.css'; @@ -14,7 +15,7 @@ import { useIsActiveRow } from '/@/renderer/components/item-list/item-table-list import { Text } from '/@/shared/components/text/text'; import { LibraryItem, QueueSong } from '/@/shared/types/domain-types'; -export const TitleColumn = (props: ItemTableListInnerColumn) => { +const TitleColumnBase = (props: ItemTableListInnerColumn) => { const { itemType } = props; switch (itemType) { @@ -28,6 +29,21 @@ export const TitleColumn = (props: ItemTableListInnerColumn) => { } }; +export const TitleColumn = memo(TitleColumnBase, (prevProps, nextProps) => { + const prevItem = prevProps.getRowItem?.(prevProps.rowIndex); + const nextItem = nextProps.getRowItem?.(nextProps.rowIndex); + + return ( + prevProps.rowIndex === nextProps.rowIndex && + prevProps.columnIndex === nextProps.columnIndex && + prevProps.data === nextProps.data && + prevProps.columns === nextProps.columns && + prevProps.itemType === nextProps.itemType && + prevProps.size === nextProps.size && + prevItem === nextItem + ); +}); + 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]; diff --git a/src/renderer/components/item-list/item-table-list/columns/title-combined-column.tsx b/src/renderer/components/item-list/item-table-list/columns/title-combined-column.tsx index 3be78b199..816d9c89a 100644 --- a/src/renderer/components/item-list/item-table-list/columns/title-combined-column.tsx +++ b/src/renderer/components/item-list/item-table-list/columns/title-combined-column.tsx @@ -361,7 +361,9 @@ export const QueueSongTitleCombinedColumn = (props: ItemTableListInnerColumn) => return ; }; -export const TitleCombinedColumn = (props: ItemTableListInnerColumn) => { +import { memo } from 'react'; + +const TitleCombinedColumnBase = (props: ItemTableListInnerColumn) => { const { itemType } = props; switch (itemType) { @@ -374,3 +376,18 @@ export const TitleCombinedColumn = (props: ItemTableListInnerColumn) => { return ; } }; + +export const TitleCombinedColumn = memo(TitleCombinedColumnBase, (prevProps, nextProps) => { + const prevItem = prevProps.getRowItem?.(prevProps.rowIndex); + const nextItem = nextProps.getRowItem?.(nextProps.rowIndex); + + return ( + prevProps.rowIndex === nextProps.rowIndex && + prevProps.columnIndex === nextProps.columnIndex && + prevProps.data === nextProps.data && + prevProps.columns === nextProps.columns && + prevProps.itemType === nextProps.itemType && + prevProps.size === nextProps.size && + prevItem === nextItem + ); +}); diff --git a/src/renderer/components/item-list/item-table-list/columns/year-column.tsx b/src/renderer/components/item-list/item-table-list/columns/year-column.tsx index 5310fb594..4ddcafbb0 100644 --- a/src/renderer/components/item-list/item-table-list/columns/year-column.tsx +++ b/src/renderer/components/item-list/item-table-list/columns/year-column.tsx @@ -1,3 +1,5 @@ +import { memo, useMemo } from 'react'; + import { ColumnNullFallback, ColumnSkeletonFixed, @@ -6,28 +8,29 @@ import { } from '/@/renderer/components/item-list/item-table-list/item-table-list-column'; import { SEPARATOR_STRING } from '/@/shared/api/utils'; -export const YearColumn = (props: ItemTableListInnerColumn) => { +const YearColumnBase = (props: ItemTableListInnerColumn) => { const rowItem = props.getRowItem?.(props.rowIndex) ?? (props.data as any[])[props.rowIndex]; const item = rowItem as any; - if (item && 'releaseYear' in item && item.releaseYear !== null) { - const releaseYear = item.releaseYear; - const originalYear = - 'originalYear' in item && item.originalYear !== null ? item.originalYear : null; + const yearDisplay = useMemo(() => { + if (item && 'releaseYear' in item && item.releaseYear !== null) { + const releaseYear = item.releaseYear; + const originalYear = + 'originalYear' in item && item.originalYear !== null ? item.originalYear : null; - if (originalYear !== null && originalYear !== releaseYear) { - return ( - - {originalYear} - {SEPARATOR_STRING} - {releaseYear} - - ); - } + if (originalYear !== null && originalYear !== releaseYear) { + return `${originalYear}${SEPARATOR_STRING}${releaseYear}`; + } - if (typeof releaseYear === 'number') { - return {releaseYear}; + if (typeof releaseYear === 'number') { + return releaseYear; + } } + return null; + }, [item]); + + if (yearDisplay !== null) { + return {yearDisplay}; } const row: number | undefined = (rowItem as any)?.[props.columns[props.columnIndex].id]; @@ -38,3 +41,16 @@ export const YearColumn = (props: ItemTableListInnerColumn) => { return ; }; + +export const YearColumn = memo(YearColumnBase, (prevProps, nextProps) => { + const prevItem = prevProps.getRowItem?.(prevProps.rowIndex); + const nextItem = nextProps.getRowItem?.(nextProps.rowIndex); + + return ( + prevProps.rowIndex === nextProps.rowIndex && + prevProps.columnIndex === nextProps.columnIndex && + prevProps.data === nextProps.data && + prevProps.columns === nextProps.columns && + prevItem === nextItem + ); +}); 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 new file mode 100644 index 000000000..3f42a6551 --- /dev/null +++ b/src/renderer/components/item-list/item-table-list/hooks/use-item-drag-drop-state.tsx @@ -0,0 +1,306 @@ +import { getDraggedItems } from '/@/renderer/components/item-list/helpers/get-dragged-items'; +import { useItemDraggingState } from '/@/renderer/components/item-list/helpers/item-list-state'; +import { ItemListStateActions } from '/@/renderer/components/item-list/helpers/item-list-state'; +import { eventEmitter } from '/@/renderer/events/event-emitter'; +import { PlayerContext } from '/@/renderer/features/player/context/player-context'; +import { useDragDrop } from '/@/renderer/hooks/use-drag-drop'; +import { Folder, LibraryItem, QueueSong, Song } from '/@/shared/types/domain-types'; +import { DragOperation, DragTarget, DragTargetMap } from '/@/shared/types/drag-and-drop'; + +interface DragDropState { + dragRef: null | React.Ref; + isDraggedOver: 'bottom' | 'top' | null; + isDragging: boolean; +} + +interface UseItemDragDropStateProps { + enableDrag: boolean; + internalState: ItemListStateActions; + isDataRow: boolean; + item: unknown; + itemType: LibraryItem; + playerContext: PlayerContext; + playlistId?: string; +} + +export const useItemDragDropState = ({ + enableDrag, + internalState, + isDataRow, + item, + itemType, + playerContext, + playlistId, +}: UseItemDragDropStateProps): DragDropState => { + const shouldEnableDrag = enableDrag && isDataRow && !!item; + + const { + isDraggedOver, + isDragging: isDraggingLocal, + ref: dragRef, + } = useDragDrop({ + drag: { + getId: () => { + if (!item || !isDataRow) { + return []; + } + + const draggedItems = getDraggedItems(item as any, internalState); + + return draggedItems.map((draggedItem) => draggedItem.id); + }, + getItem: () => { + if (!item || !isDataRow) { + return []; + } + + const draggedItems = getDraggedItems(item as any, internalState); + + 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; + } + + // 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; + } + + 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 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; + + 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); + + // 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, + }); + } + + 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; + + 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([]); + } + + return; + }, + }, + isEnabled: shouldEnableDrag, + }); + + const itemRowId = + item && typeof item === 'object' && 'id' in item && internalState + ? internalState.extractRowId(item) + : undefined; + const isDraggingState = useItemDraggingState( + internalState, + itemRowId || + (item && typeof item === 'object' && 'id' in item ? (item as any).id : undefined), + ); + const isDragging = internalState ? isDraggingState : isDraggingLocal; + + return { + dragRef: shouldEnableDrag ? dragRef : null, + isDraggedOver: isDraggedOver === 'top' || isDraggedOver === 'bottom' ? isDraggedOver : null, + isDragging, + }; +}; 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 b33382eb3..2e4e7535c 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 @@ -10,18 +10,22 @@ import { } from '@atlaskit/pragmatic-drag-and-drop/element/adapter'; import { disableNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/element/disable-native-drag-preview'; import clsx from 'clsx'; -import React, { CSSProperties, ReactElement, ReactNode, useEffect, useRef, useState } from 'react'; +import React, { + CSSProperties, + memo, + ReactElement, + ReactNode, + useEffect, + useRef, + useState, +} from 'react'; import { useParams } from 'react-router'; import { CellComponentProps } from 'react-window-v2'; import styles from './item-table-list-column.module.css'; import i18n from '/@/i18n/i18n'; -import { getDraggedItems } from '/@/renderer/components/item-list/helpers/get-dragged-items'; -import { - useItemDraggingState, - useItemSelectionState, -} from '/@/renderer/components/item-list/helpers/item-list-state'; +import { useItemSelectionState } from '/@/renderer/components/item-list/helpers/item-list-state'; import { ActionsColumn } from '/@/renderer/components/item-list/item-table-list/columns/actions-column'; import { AlbumArtistsColumn } from '/@/renderer/components/item-list/item-table-list/columns/album-artists-column'; import { AlbumColumn } from '/@/renderer/components/item-list/item-table-list/columns/album-column'; @@ -50,27 +54,22 @@ import { TitleArtistColumn } from '/@/renderer/components/item-list/item-table-l import { TitleColumn } from '/@/renderer/components/item-list/item-table-list/columns/title-column'; import { TitleCombinedColumn } from '/@/renderer/components/item-list/item-table-list/columns/title-combined-column'; import { YearColumn } from '/@/renderer/components/item-list/item-table-list/columns/year-column'; +import { useItemDragDropState } from '/@/renderer/components/item-list/item-table-list/hooks/use-item-drag-drop-state'; import { TableItemProps } from '/@/renderer/components/item-list/item-table-list/item-table-list'; import { ItemControls, ItemListItem } from '/@/renderer/components/item-list/types'; -import { eventEmitter } from '/@/renderer/events/event-emitter'; -import { useDragDrop } from '/@/renderer/hooks/use-drag-drop'; import { Flex } from '/@/shared/components/flex/flex'; import { Icon } from '/@/shared/components/icon/icon'; import { Skeleton } from '/@/shared/components/skeleton/skeleton'; import { Text } from '/@/shared/components/text/text'; import { useDoubleClick } from '/@/shared/hooks/use-double-click'; import { useMergedRef } from '/@/shared/hooks/use-merged-ref'; -import { Folder, LibraryItem, QueueSong, Song } from '/@/shared/types/domain-types'; -import { - dndUtils, - DragData, - DragOperation, - DragTarget, - DragTargetMap, -} from '/@/shared/types/drag-and-drop'; +import { LibraryItem } from '/@/shared/types/domain-types'; +import { dndUtils, DragData, DragOperation, DragTarget } from '/@/shared/types/drag-and-drop'; import { TableColumn } from '/@/shared/types/types'; -export interface ItemTableListColumn extends CellComponentProps {} +export interface ItemTableListColumn extends CellComponentProps { + columnType?: TableColumn; +} export interface ItemTableListInnerColumn extends ItemTableListColumn { controls: ItemControls; @@ -80,9 +79,9 @@ export interface ItemTableListInnerColumn extends ItemTableListColumn { type: TableColumn; } -export const ItemTableListColumn = (props: ItemTableListColumn) => { +const ItemTableListColumnBase = (props: ItemTableListColumn) => { const { playlistId } = useParams() as { playlistId?: string }; - const type = props.columns[props.columnIndex].id as TableColumn; + const type = props.columnType ?? (props.columns[props.columnIndex].id as TableColumn); const isHeaderEnabled = !!props.enableHeader; const isDataRow = isHeaderEnabled ? props.rowIndex > 0 : true; @@ -127,270 +126,16 @@ export const ItemTableListColumn = (props: ItemTableListColumn) => { } } - const { - isDraggedOver, - isDragging: isDraggingLocal, - ref: dragRef, - } = useDragDrop({ - drag: { - getId: () => { - if (!item || !isDataRow) { - return []; - } - - const draggedItems = getDraggedItems(item as any, props.internalState); - - return draggedItems.map((draggedItem) => draggedItem.id); - }, - getItem: () => { - if (!item || !isDataRow) { - return []; - } - - const draggedItems = getDraggedItems(item as any, props.internalState); - - return draggedItems; - }, - itemType: props.itemType, - onDragStart: () => { - if (!item || !isDataRow) { - return; - } - - const draggedItems = getDraggedItems(item as any, props.internalState); - if (props.internalState) { - props.internalState.setDragging(draggedItems); - } - }, - onDrop: () => { - if (props.internalState) { - props.internalState.setDragging([]); - } - }, - operation: - props.itemType === LibraryItem.QUEUE_SONG - ? [DragOperation.REORDER, DragOperation.ADD] - : props.itemType === LibraryItem.PLAYLIST_SONG - ? [DragOperation.REORDER, DragOperation.ADD] - : [DragOperation.ADD], - target: DragTargetMap[props.itemType] || DragTarget.GENERIC, - }, - drop: { - canDrop: (args) => { - if (args.source.type === DragTarget.TABLE_COLUMN) { - return false; - } - - // Allow drops for QUEUE_SONG (queue reordering) - if (props.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 ( - props.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: props.itemType, - type: DragTargetMap[props.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 droppedOnUniqueId = ( - args.self.item?.[0] as unknown as { _uniqueId: string } - )._uniqueId; - - switch (args.source.type) { - case DragTarget.ALBUM: { - props.playerContext.addToQueueByFetch( - sourceServerId, - args.source.id, - sourceItemType, - { edge: args.edge, uniqueId: droppedOnUniqueId }, - ); - break; - } - case DragTarget.ALBUM_ARTIST: { - props.playerContext.addToQueueByFetch( - sourceServerId, - args.source.id, - sourceItemType, - { edge: args.edge, uniqueId: droppedOnUniqueId }, - ); - break; - } - case DragTarget.ARTIST: { - props.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 folderIds = folders.map((folder) => folder.id); - - // Handle folders: fetch and add to queue - if (folderIds.length > 0) { - props.playerContext.addToQueueByFetch( - sourceServerId, - folderIds, - LibraryItem.FOLDER, - { edge: args.edge, uniqueId: droppedOnUniqueId }, - ); - } - - // Handle songs: add directly to queue - if (songs.length > 0) { - props.playerContext.addToQueueByData(songs, { - edge: args.edge, - uniqueId: droppedOnUniqueId, - }); - } - - break; - } - case DragTarget.GENRE: { - props.playerContext.addToQueueByFetch( - sourceServerId, - args.source.id, - sourceItemType, - { edge: args.edge, uniqueId: droppedOnUniqueId }, - ); - break; - } - case DragTarget.PLAYLIST: { - props.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') - ) { - props.playerContext.moveSelectedTo( - sourceItems, - args.edge, - droppedOnUniqueId, - ); - } - break; - } - case DragTarget.SONG: { - const sourceItems = (args.source.item || []) as Song[]; - if (sourceItems.length > 0) { - props.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; - - 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 (props.internalState) { - props.internalState.setDragging([]); - } - - return; - }, - }, - isEnabled: shouldEnableDrag, + const { dragRef, isDraggedOver, isDragging } = useItemDragDropState({ + enableDrag: !!props.enableDrag, + internalState: props.internalState, + isDataRow, + item, + itemType: props.itemType, + playerContext: props.playerContext, + playlistId, }); - const itemRowId = - item && typeof item === 'object' && 'id' in item && props.internalState - ? props.internalState.extractRowId(item) - : undefined; - const isDraggingState = useItemDraggingState( - props.internalState, - itemRowId || - (item && typeof item === 'object' && 'id' in item ? (item as any).id : undefined), - ); - const isDragging = props.internalState ? isDraggingState : isDraggingLocal; - const controls = props.controls; const dragProps = { @@ -583,6 +328,37 @@ export const ItemTableListColumn = (props: ItemTableListColumn) => { } }; +export const ItemTableListColumn = memo(ItemTableListColumnBase, (prevProps, nextProps) => { + const prevItem = prevProps.getRowItem?.(prevProps.rowIndex); + const nextItem = nextProps.getRowItem?.(nextProps.rowIndex); + + return ( + prevProps.rowIndex === nextProps.rowIndex && + prevProps.columnIndex === nextProps.columnIndex && + prevProps.data === nextProps.data && + prevProps.columns === nextProps.columns && + prevProps.style === nextProps.style && + prevProps.columnType === nextProps.columnType && + prevProps.itemType === nextProps.itemType && + prevProps.enableHeader === nextProps.enableHeader && + prevProps.enableDrag === nextProps.enableDrag && + prevProps.groups === nextProps.groups && + prevProps.groupHeaderInfoByRowIndex === nextProps.groupHeaderInfoByRowIndex && + prevProps.pinnedLeftColumnCount === nextProps.pinnedLeftColumnCount && + prevProps.pinnedLeftColumnWidths === nextProps.pinnedLeftColumnWidths && + 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 && + prevItem === nextItem + ); +}); + const NonMutedColumns = [TableColumn.TITLE, TableColumn.TITLE_ARTIST, TableColumn.TITLE_COMBINED]; export const TableColumnTextContainer = ( 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 8b26c17f8..4bf4fb385 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 @@ -43,10 +43,15 @@ import { useTableKeyboardNavigation } from '/@/renderer/components/item-list/ite import { useTablePaneSync } from '/@/renderer/components/item-list/item-table-list/hooks/use-table-pane-sync'; import { useTableRowModel } from '/@/renderer/components/item-list/item-table-list/hooks/use-table-row-model'; 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 { ItemTableListConfigProvider, ItemTableListStoreProvider, } from '/@/renderer/components/item-list/item-table-list/item-table-list-context'; +import { + MemoizedCellRouter, + useColumnCellComponents, +} from '/@/renderer/components/item-list/item-table-list/memoized-cell-router'; import { ItemControls, ItemListHandle, @@ -197,6 +202,22 @@ const VirtualizedTableGrid = ({ [calculatedColumnWidths], ); + const columnWidthMemoized = useCallback( + (index: number) => columnWidth(index + pinnedLeftColumnCount), + [columnWidth, pinnedLeftColumnCount], + ); + + const rowHeightMemoized = useCallback( + (index: number, cellProps: TableItemProps) => + getRowHeight(index + pinnedRowCount, cellProps), + [getRowHeight, pinnedRowCount], + ); + + const pinnedRightColumnWidthMemoized = useCallback( + (index: number) => columnWidth(index + pinnedLeftColumnCount + totalColumnCount), + [columnWidth, pinnedLeftColumnCount, totalColumnCount], + ); + const groupHeaderInfoByRowIndex = useMemo(() => { if (!groups || groups.length === 0) return undefined; @@ -595,14 +616,10 @@ const VirtualizedTableGrid = ({ cellProps={itemProps} className={styles.height100} columnCount={totalColumnCount} - columnWidth={(index) => { - return columnWidth(index + pinnedLeftColumnCount); - }} + columnWidth={columnWidthMemoized} onCellsRendered={handleOnCellsRendered} rowCount={totalRowCount} - rowHeight={(index, cellProps) => { - return getRowHeight(index + pinnedRowCount, cellProps); - }} + rowHeight={rowHeightMemoized} /> {pinnedLeftColumnCount > 0 && enableScrollShadow && showLeftShadow && (
@@ -669,15 +686,9 @@ const VirtualizedTableGrid = ({ cellProps={itemProps} className={clsx(styles.noScrollbar, styles.height100)} columnCount={pinnedRightColumnCount} - columnWidth={(index) => { - return columnWidth( - index + pinnedLeftColumnCount + totalColumnCount, - ); - }} + columnWidth={pinnedRightColumnWidthMemoized} rowCount={totalRowCount} - rowHeight={(index, cellProps) => { - return getRowHeight(index + pinnedRowCount, cellProps); - }} + rowHeight={rowHeightMemoized} />
@@ -785,7 +796,7 @@ export interface TableItemProps { interface ItemTableListProps { activeRowId?: string; autoFitColumns?: boolean; - CellComponent: JSXElementConstructor>; + CellComponent?: JSXElementConstructor>; cellPadding?: 'lg' | 'md' | 'sm' | 'xl' | 'xs'; columns: ItemTableListColumnConfig[]; data: unknown[]; @@ -832,7 +843,7 @@ interface ItemTableListProps { const BaseItemTableList = ({ activeRowId, autoFitColumns = false, - CellComponent, + CellComponent = ItemTableListColumn, cellPadding = 'sm', columns, data, @@ -1074,7 +1085,6 @@ const BaseItemTableList = ({ }); const getDataFn = useCallback(() => { - // For infinite lists, callers should pass `data` as the currently loaded items only. return data; }, [data]); @@ -1082,7 +1092,6 @@ const BaseItemTableList = ({ const internalState = useItemListState(getDataFn, extractRowId); - // Helper function to get ItemListStateItemWithRequiredProperties (rowId is separate, not part of item) const getStateItem = useCallback( (item: any): ItemListStateItemWithRequiredProperties | null => { if (!hasRequiredItemProperties(item)) { @@ -1532,6 +1541,25 @@ const BaseItemTableList = ({ ], ); + const columnCellComponents = useColumnCellComponents( + parsedColumns.map((c) => c.id as TableColumn), + itemType, + ); + + const optimizedCellComponent = useMemo< + JSXElementConstructor> + >(() => { + if (CellComponent && CellComponent !== ItemTableListColumn) { + return CellComponent; + } + + return (cellProps: CellComponentProps) => { + return ( + + ); + }; + }, [CellComponent, columnCellComponents]); + return ( @@ -1554,7 +1582,7 @@ const BaseItemTableList = ({ {StickyGroupRow} { + columnCellComponents: Map>>; +} + +const MemoizedCellRouterBase = (props: MemoizedCellRouterProps) => { + const columnType = props.columns[props.columnIndex]?.id as TableColumn; + const ColumnComponent = props.columnCellComponents.get(columnType); + + if (ColumnComponent) { + // eslint-disable-next-line react-hooks/static-components + return ; + } + + 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 useColumnCellComponents = ( + columns: TableColumn[], + itemType: LibraryItem, +): Map>> => { + const columnsKey = useMemo(() => columns.join(','), [columns]); + + // eslint-disable-next-line react-hooks/exhaustive-deps + return useMemo(() => createColumnCellComponents(columns, itemType), [columnsKey, itemType]); +};