add more memoization to the ItemTableList

This commit is contained in:
jeffvli
2026-01-21 12:40:38 -08:00
parent f4072c183b
commit f6012d3b03
29 changed files with 955 additions and 391 deletions
@@ -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 };
};
@@ -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<CellComponentProps<TableItemProps>> => {
return React.memo(
(props: CellComponentProps<TableItemProps>) => {
return <ItemTableListColumn {...props} columnType={columnType} itemType={itemType} />;
},
(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<TableColumn, React.ComponentType<CellComponentProps<TableItemProps>>> => {
const componentMap = new Map<
TableColumn,
React.ComponentType<CellComponentProps<TableItemProps>>
>();
columns.forEach((columnType) => {
componentMap.set(columnType, createColumnCellComponent(columnType, itemType));
});
return componentMap;
};
@@ -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<HTMLButtonElement>) => {
@@ -51,3 +53,16 @@ export const ActionsColumn = (props: ItemTableListInnerColumn) => {
return <TableColumnContainer {...props}>&nbsp;</TableColumnContainer>;
};
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
);
});
@@ -54,6 +54,14 @@ const AlbumArtistsColumn = (props: ItemTableListInnerColumn) => {
return <ColumnSkeletonVariable {...props} />;
};
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 };
@@ -75,6 +75,18 @@ const AlbumColumn = (props: ItemTableListInnerColumn) => {
return <ColumnSkeletonVariable {...props} />;
};
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 };
@@ -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 };
@@ -45,6 +45,18 @@ const ComposerColumn = (props: ItemTableListInnerColumn) => {
return <ColumnSkeletonVariable {...props} />;
};
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 };
@@ -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 (
<TableColumnTextContainer {...props}>
<Tooltip label={getDateTooltipLabel(row)} multiline={false}>
<span>{formatDateAbsolute(row)}</span>
<Tooltip label={tooltipLabel} multiline={false}>
<span>{formattedDate}</span>
</Tooltip>
</TableColumnTextContainer>
);
@@ -50,42 +62,70 @@ export const DateColumn = (props: ItemTableListInnerColumn) => {
return <ColumnSkeletonFixed {...props} />;
};
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 (
<TableColumnTextContainer {...props}>
<Tooltip label={getDateTooltipLabel(releaseDate)} multiline={false}>
<span>{displayText}</span>
</Tooltip>
</TableColumnTextContainer>
);
}
if (typeof releaseDate === 'string' && releaseDate) {
return (
<TableColumnTextContainer {...props}>
<Tooltip label={getDateTooltipLabel(releaseDate)} multiline={false}>
<span>{formatDateAbsoluteUTC(releaseDate)}</span>
</Tooltip>
</TableColumnTextContainer>
);
}
if (releaseDateContent) {
return (
<TableColumnTextContainer {...props}>
<Tooltip label={releaseDateContent.tooltipLabel} multiline={false}>
<span>{releaseDateContent.displayText}</span>
</Tooltip>
</TableColumnTextContainer>
);
}
if (row === null) {
@@ -98,8 +138,8 @@ export const AbsoluteDateColumn = (props: ItemTableListInnerColumn) => {
if (typeof row === 'string' && row) {
return (
<TableColumnTextContainer {...props}>
<Tooltip label={getDateTooltipLabel(row)} multiline={false}>
<span>{formatDateAbsoluteUTC(row)}</span>
<Tooltip label={tooltipLabel} multiline={false}>
<span>{formattedDate}</span>
</Tooltip>
</TableColumnTextContainer>
);
@@ -112,15 +152,35 @@ export const AbsoluteDateColumn = (props: ItemTableListInnerColumn) => {
return <ColumnSkeletonFixed {...props} />;
};
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 (
<TableColumnTextContainer {...props}>
<Tooltip label={getDateTooltipLabel(row)} multiline={false}>
<span>{formatDateRelative(row)}</span>
<Tooltip label={tooltipLabel} multiline={false}>
<span>{formattedDate}</span>
</Tooltip>
</TableColumnTextContainer>
);
@@ -132,3 +192,12 @@ export const RelativeDateColumn = (props: ItemTableListInnerColumn) => {
return <ColumnSkeletonFixed {...props} />;
};
export const RelativeDateColumn = memo(RelativeDateColumnBase, (prevProps, nextProps) => {
return (
prevProps.rowIndex === nextProps.rowIndex &&
prevProps.columnIndex === nextProps.columnIndex &&
prevProps.data === nextProps.data &&
prevProps.columns === nextProps.columns
);
});
@@ -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 <ColumnSkeletonFixed {...props} />;
};
export const DefaultColumn = memo(DefaultColumnBase, (prevProps, nextProps) => {
return (
prevProps.rowIndex === nextProps.rowIndex &&
prevProps.columnIndex === nextProps.columnIndex &&
prevProps.data === nextProps.data &&
prevProps.columns === nextProps.columns
);
});
@@ -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 (
<TableColumnTextContainer {...props}>{formatDuration(row)}</TableColumnTextContainer>
);
return <TableColumnTextContainer {...props}>{formattedDuration}</TableColumnTextContainer>;
}
if (row === null) {
@@ -23,3 +26,12 @@ export const DurationColumn = (props: ItemTableListInnerColumn) => {
return <ColumnSkeletonFixed {...props} />;
};
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
);
});
@@ -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 <TableColumnContainer {...props}>&nbsp;</TableColumnContainer>;
};
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
);
});
@@ -60,6 +60,13 @@ const GenreBadgeColumn = (props: ItemTableListInnerColumn) => {
return <ColumnSkeletonVariable {...props} />;
};
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 };
@@ -64,6 +64,14 @@ const GenreColumn = (props: ItemTableListInnerColumn) => {
return <ColumnSkeletonVariable {...props} />;
};
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 };
@@ -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) => {
</TableColumnContainer>
);
};
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
);
});
@@ -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 <ColumnSkeletonFixed {...props} />;
};
export const NumericColumn = memo(NumericColumnBase, (prevProps, nextProps) => {
return (
prevProps.rowIndex === nextProps.rowIndex &&
prevProps.columnIndex === nextProps.columnIndex &&
prevProps.data === nextProps.data &&
prevProps.columns === nextProps.columns
);
});
@@ -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 <ColumnSkeletonVariable {...props} />;
};
export const PathColumn = memo(PathColumnBase, (prevProps, nextProps) => {
return (
prevProps.rowIndex === nextProps.rowIndex &&
prevProps.columnIndex === nextProps.columnIndex &&
prevProps.data === nextProps.data &&
prevProps.columns === nextProps.columns
);
});
@@ -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) => {
</TableColumnContainer>
);
};
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
);
});
@@ -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 <TableColumnContainer {...props}>&nbsp;</TableColumnContainer>;
};
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
);
});
@@ -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,
@@ -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 (
<TableColumnTextContainer {...props}>{formatSizeString(row)}</TableColumnTextContainer>
);
return <TableColumnTextContainer {...props}>{formattedSize}</TableColumnTextContainer>;
}
if (row === null) {
@@ -22,3 +26,12 @@ export const SizeColumn = (props: ItemTableListInnerColumn) => {
return <ColumnSkeletonFixed {...props} />;
};
export const SizeColumn = memo(SizeColumnBase, (prevProps, nextProps) => {
return (
prevProps.rowIndex === nextProps.rowIndex &&
prevProps.columnIndex === nextProps.columnIndex &&
prevProps.data === nextProps.data &&
prevProps.columns === nextProps.columns
);
});
@@ -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 <ColumnSkeletonVariable {...props} />;
};
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
);
});
@@ -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 <ColumnSkeletonVariable {...props} />;
};
export const TitleArtistColumn = (props: ItemTableListInnerColumn) => {
const TitleArtistColumnBase = (props: ItemTableListInnerColumn) => {
const { itemType } = props;
switch (itemType) {
@@ -207,3 +207,18 @@ export const TitleArtistColumn = (props: ItemTableListInnerColumn) => {
return <DefaultTitleArtistColumn {...props} />;
}
};
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
);
});
@@ -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];
@@ -361,7 +361,9 @@ export const QueueSongTitleCombinedColumn = (props: ItemTableListInnerColumn) =>
return <ColumnSkeletonVariable {...props} />;
};
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 <DefaultTitleCombinedColumn {...props} />;
}
};
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
);
});
@@ -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 (
<TableColumnTextContainer {...props}>
{originalYear}
{SEPARATOR_STRING}
{releaseYear}
</TableColumnTextContainer>
);
}
if (originalYear !== null && originalYear !== releaseYear) {
return `${originalYear}${SEPARATOR_STRING}${releaseYear}`;
}
if (typeof releaseYear === 'number') {
return <TableColumnTextContainer {...props}>{releaseYear}</TableColumnTextContainer>;
if (typeof releaseYear === 'number') {
return releaseYear;
}
}
return null;
}, [item]);
if (yearDisplay !== null) {
return <TableColumnTextContainer {...props}>{yearDisplay}</TableColumnTextContainer>;
}
const row: number | undefined = (rowItem as any)?.[props.columns[props.columnIndex].id];
@@ -38,3 +41,16 @@ export const YearColumn = (props: ItemTableListInnerColumn) => {
return <ColumnSkeletonFixed {...props} />;
};
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
);
});
@@ -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<HTMLDivElement>;
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<HTMLDivElement>({
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,
};
};
@@ -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<TableItemProps> {}
export interface ItemTableListColumn extends CellComponentProps<TableItemProps> {
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<HTMLDivElement>({
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 = (
@@ -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 && (
<div className={styles.itemTableLeftScrollShadow} />
@@ -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}
/>
</div>
</div>
@@ -785,7 +796,7 @@ export interface TableItemProps {
interface ItemTableListProps {
activeRowId?: string;
autoFitColumns?: boolean;
CellComponent: JSXElementConstructor<CellComponentProps<TableItemProps>>;
CellComponent?: JSXElementConstructor<CellComponentProps<TableItemProps>>;
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<CellComponentProps<TableItemProps>>
>(() => {
if (CellComponent && CellComponent !== ItemTableListColumn) {
return CellComponent;
}
return (cellProps: CellComponentProps<TableItemProps>) => {
return (
<MemoizedCellRouter {...cellProps} columnCellComponents={columnCellComponents} />
);
};
}, [CellComponent, columnCellComponents]);
return (
<ItemTableListStoreProvider activeRowId={activeRowId}>
<ItemTableListConfigProvider value={tableConfigValue}>
@@ -1554,7 +1582,7 @@ const BaseItemTableList = ({
{StickyGroupRow}
<MemoizedVirtualizedTableGrid
calculatedColumnWidths={calculatedColumnWidths}
CellComponent={CellComponent}
CellComponent={optimizedCellComponent}
cellPadding={cellPadding}
controls={controls}
data={data}
@@ -0,0 +1,54 @@
import React, { memo, useMemo } from 'react';
import { CellComponentProps } from 'react-window-v2';
import { createColumnCellComponents } from './cell-component-factory';
import { TableItemProps } from './item-table-list';
import { ItemTableListColumn } from './item-table-list-column';
import { LibraryItem } from '/@/shared/types/domain-types';
import { TableColumn } from '/@/shared/types/types';
interface MemoizedCellRouterProps extends CellComponentProps<TableItemProps> {
columnCellComponents: Map<TableColumn, React.ComponentType<CellComponentProps<TableItemProps>>>;
}
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 <ColumnComponent {...props} />;
}
return <ItemTableListColumn {...props} />;
};
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<TableColumn, React.ComponentType<CellComponentProps<TableItemProps>>> => {
const columnsKey = useMemo(() => columns.join(','), [columns]);
// eslint-disable-next-line react-hooks/exhaustive-deps
return useMemo(() => createColumnCellComponents(columns, itemType), [columnsKey, itemType]);
};