diff --git a/src/renderer/components/item-list/item-table-list/album-group-controls.module.css b/src/renderer/components/item-list/item-table-list/album-group-controls.module.css index 92112684c..a276bd72c 100644 --- a/src/renderer/components/item-list/item-table-list/album-group-controls.module.css +++ b/src/renderer/components/item-list/item-table-list/album-group-controls.module.css @@ -2,9 +2,9 @@ display: flex; gap: var(--theme-spacing-xs); align-items: center; - margin-top: var(--theme-spacing-xxs); + min-height: 22px; } .hidden { - display: none; + visibility: hidden; } diff --git a/src/renderer/components/item-list/item-table-list/album-group-header.module.css b/src/renderer/components/item-list/item-table-list/album-group-header.module.css index 92488f672..7f0c08c9a 100644 --- a/src/renderer/components/item-list/item-table-list/album-group-header.module.css +++ b/src/renderer/components/item-list/item-table-list/album-group-header.module.css @@ -1,6 +1,7 @@ .container { display: flex; gap: var(--theme-spacing-sm); + align-items: flex-start; width: 100%; height: 100%; padding: 0 var(--theme-spacing-xs); @@ -42,15 +43,23 @@ } .info { - display: flex; - flex-direction: column; + display: grid; + grid-template-rows: auto auto auto; + gap: 0; + align-content: start; min-width: 0; padding: 0; - overflow: hidden; + padding-top: calc(var(--theme-spacing-xs) * 0.5); +} + +.info.enlarged-image { + padding-top: var(--theme-spacing-xs); } .album-name { font-size: var(--theme-font-size-sm); + line-height: 1.25; + overflow-wrap: anywhere; } .album-name a { @@ -64,5 +73,13 @@ .artist-name { font-size: var(--theme-font-size-xs); + line-height: 1.25; + overflow-wrap: anywhere; opacity: 0.7; } + +.controls-row { + flex-shrink: 0; + min-height: 22px; + margin-top: var(--theme-spacing-xxs); +} diff --git a/src/renderer/components/item-list/item-table-list/album-group-header.tsx b/src/renderer/components/item-list/item-table-list/album-group-header.tsx index 520377304..eff8334e6 100644 --- a/src/renderer/components/item-list/item-table-list/album-group-header.tsx +++ b/src/renderer/components/item-list/item-table-list/album-group-header.tsx @@ -1,4 +1,5 @@ -import { ReactElement, useState } from 'react'; +import clsx from 'clsx'; +import { ReactElement, useLayoutEffect, useRef, useState } from 'react'; import { generatePath, Link } from 'react-router'; import imageColumnStyles from '../item-detail-list/columns/image-column.module.css'; @@ -21,6 +22,8 @@ import { Play } from '/@/shared/types/types'; interface AlbumGroupHeaderProps { groupRowCount?: number; onPlay?: (playType: Play) => void; + rowIndex?: number; + setAlbumGroupContentHeight?: (rowIndex: number, height: number) => void; size?: 'compact' | 'large' | 'normal'; song: Song | undefined; } @@ -28,10 +31,13 @@ interface AlbumGroupHeaderProps { export const AlbumGroupHeader = ({ groupRowCount, onPlay, + rowIndex, + setAlbumGroupContentHeight, size = 'normal', song, }: AlbumGroupHeaderProps): ReactElement => { const [isHovered, setIsHovered] = useState(false); + const [resolvedInfoHeight, setResolvedInfoHeight] = useState(); const playButtonBehavior = usePlayButtonBehavior(); const albumImageSize = useAlbumGroupImageSize(); const rowHeight = { @@ -66,6 +72,31 @@ export const AlbumGroupHeader = ({ } : undefined; + const infoRef = useRef(null); + + useLayoutEffect(() => { + const infoEl = infoRef.current; + if (!infoEl) return; + + const measure = () => { + const contentHeight = infoEl.scrollHeight; + const resolved = Math.max(infoHeight ?? 0, contentHeight); + + setResolvedInfoHeight(resolved); + + if (rowIndex !== undefined && setAlbumGroupContentHeight) { + setAlbumGroupContentHeight(rowIndex, contentHeight); + } + }; + + measure(); + + const resizeObserver = new ResizeObserver(measure); + resizeObserver.observe(infoEl); + + return () => resizeObserver.disconnect(); + }, [infoHeight, rowIndex, setAlbumGroupContentHeight]); + return (
)}
-
+
0 && styles.enlargedImage)} + ref={infoRef} + style={{ minHeight: resolvedInfoHeight ?? infoHeight }} + >
{song?.albumId && albumPath ? ( @@ -118,12 +153,14 @@ export const AlbumGroupHeader = ({ rootTextProps={{ fw: 400, size: 'xs' }} />
- +
+ +
); diff --git a/src/renderer/components/item-list/item-table-list/columns/album-group-column.tsx b/src/renderer/components/item-list/item-table-list/columns/album-group-column.tsx index 65d829472..a8567c1cf 100644 --- a/src/renderer/components/item-list/item-table-list/columns/album-group-column.tsx +++ b/src/renderer/components/item-list/item-table-list/columns/album-group-column.tsx @@ -93,6 +93,8 @@ export const AlbumGroupColumn = (props: ItemTableListInnerColumn) => { 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 d07293429..d700a758a 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 @@ -414,6 +414,38 @@ export function getAlbumGroupRowCount( return end - start + 1; } +export function getAlbumGroupSpanHeight( + groupRowCount: number, + baseHeight: number, + albumGroupImageSize: number, + contentHeight = 0, +): number { + const rowSpanHeight = groupRowCount * baseHeight; + const imageSpanHeight = + albumGroupImageSize > 0 ? Math.max(albumGroupImageSize, rowSpanHeight) : rowSpanHeight; + + return Math.max(imageSpanHeight, contentHeight); +} + +export function getAlbumGroupStartRowIndex( + rowIndex: number, + getRowItem: ((index: number) => unknown) | undefined, + enableHeader: boolean | undefined, +): number { + const item = getRowItem?.(rowIndex) as null | undefined | { album?: string }; + if (!item?.album) return rowIndex; + + const firstDataRow = enableHeader ? 1 : 0; + let start = rowIndex; + while (start > firstDataRow) { + const prevItem = getRowItem?.(start - 1) as null | undefined | { album?: string }; + if (!prevItem || prevItem.album !== item.album) break; + start--; + } + + return start; +} + export function isAlbumGroupingActive(columns: { id: string; isEnabled?: boolean }[]): boolean { return columns.some((col) => col.id === TableColumn.ALBUM_GROUP && col.isEnabled); } @@ -513,9 +545,21 @@ function getAlbumGroupClampHeight(props: ItemTableListInnerColumn): null | numbe props.enableHeader, props.data.length, ); + const groupStartRowIndex = getAlbumGroupStartRowIndex( + props.rowIndex, + props.getRowItem, + props.enableHeader, + ); + const contentHeight = props.albumGroupContentHeights?.get(groupStartRowIndex) ?? 0; + const totalGroupHeight = getAlbumGroupSpanHeight( + groupRowCount, + baseHeight, + albumImageSize, + contentHeight, + ); - // Only clamp when the row was actually grown to fit the image. - if (albumImageSize <= groupRowCount * baseHeight) return null; + // Only clamp when the row was actually grown to fit the image or wrapped text. + if (totalGroupHeight <= groupRowCount * baseHeight) return null; return baseHeight; } 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 db1d544a7..8842933c6 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 @@ -46,6 +46,8 @@ import { useTableRowModel } from '/@/renderer/components/item-list/item-table-li import { useTableScrollToIndex } from '/@/renderer/components/item-list/item-table-list/hooks/use-table-scroll-to-index'; import { getAlbumGroupRowCount, + getAlbumGroupSpanHeight, + getAlbumGroupStartRowIndex, isLastInAlbumGroup, ItemTableListColumn, } from '/@/renderer/components/item-list/item-table-list/item-table-list-column'; @@ -174,6 +176,7 @@ const ItemTableScrollShadowRight = memo(function ItemTableScrollShadowRight({ ItemTableScrollShadowRight.displayName = 'ItemTableScrollShadowRight'; interface VirtualizedTableGridProps { + albumGroupContentHeights: Map; calculatedColumnWidths: number[]; CellComponent: JSXElementConstructor>; data: unknown[]; @@ -191,12 +194,14 @@ interface VirtualizedTableGridProps { pinnedRowCount: number; pinnedRowRef: React.RefObject; scrollShadowStore: TableScrollShadowStore; + setAlbumGroupContentHeight: (rowIndex: number, height: number) => void; tableConfig: ItemTableListConfig; totalColumnCount: number; totalRowCount: number; } const VirtualizedTableGrid = ({ + albumGroupContentHeights, calculatedColumnWidths, CellComponent, data, @@ -214,6 +219,7 @@ const VirtualizedTableGrid = ({ pinnedRowCount, pinnedRowRef, scrollShadowStore, + setAlbumGroupContentHeight, tableConfig, totalColumnCount, totalRowCount, @@ -377,41 +383,15 @@ const VirtualizedTableGrid = ({ ], ); - const gridOnlyProps = useMemo( - () => ({ - calculatedColumnWidths, - data: dataWithGroups, - getAdjustedRowIndex, - getGroupRenderData, - getRowItem, - groupHeaderInfoByRowIndex, - hasAlbumGroupColumn: parsedColumns.some((col) => col.id === TableColumn.ALBUM_GROUP), - pinnedLeftColumnCount, - pinnedLeftColumnWidths, - pinnedRightColumnCount, - pinnedRightColumnWidths, - }), - [ - calculatedColumnWidths, - dataWithGroups, - getRowItem, - getAdjustedRowIndex, - getGroupRenderData, - groupHeaderInfoByRowIndex, - parsedColumns, - pinnedLeftColumnCount, - pinnedLeftColumnWidths, - pinnedRightColumnCount, - pinnedRightColumnWidths, - ], - ); - const itemProps: TableItemProps = useMemo( () => ({ + albumGroupContentHeights, albumGroupImageSize, + calculatedColumnWidths, cellPadding: tableConfig.cellPadding, columns: tableConfig.columns, controls: tableConfig.controls, + data: dataWithGroups, enableAlternateRowColors: tableConfig.enableAlternateRowColors, enableColumnReorder: tableConfig.enableColumnReorder, enableColumnResize: tableConfig.enableColumnResize, @@ -422,18 +402,43 @@ const VirtualizedTableGrid = ({ enableRowHoverHighlight: tableConfig.enableRowHoverHighlight, enableSelection: tableConfig.enableSelection, enableVerticalBorders: tableConfig.enableVerticalBorders, + getAdjustedRowIndex, + getGroupRenderData, getRowHeight: tableConfig.getRowHeight, + getRowItem, + groupHeaderInfoByRowIndex, groups: tableConfig.groups, + hasAlbumGroupColumn: parsedColumns.some((col) => col.id === TableColumn.ALBUM_GROUP), internalState: tableConfig.internalState, itemType: tableConfig.itemType, + pinnedLeftColumnCount, + pinnedLeftColumnWidths, + pinnedRightColumnCount, + pinnedRightColumnWidths, playerContext: tableConfig.playerContext, playlistId: tableConfig.playlistId, + setAlbumGroupContentHeight, size: tableConfig.size, startRowIndex: tableConfig.startRowIndex, tableId: tableConfig.tableId, - ...gridOnlyProps, }), - [albumGroupImageSize, gridOnlyProps, tableConfig], + [ + albumGroupContentHeights, + albumGroupImageSize, + calculatedColumnWidths, + dataWithGroups, + getAdjustedRowIndex, + getGroupRenderData, + getRowItem, + groupHeaderInfoByRowIndex, + parsedColumns, + pinnedLeftColumnCount, + pinnedLeftColumnWidths, + pinnedRightColumnCount, + pinnedRightColumnWidths, + setAlbumGroupContentHeight, + tableConfig, + ], ); const pinnedLeftGridMinWidthPx = useMemo(() => { @@ -729,6 +734,8 @@ const MemoizedVirtualizedTableGrid = memo(VirtualizedTableGrid, (prevProps, next prevProps.calculatedColumnWidths, nextProps.calculatedColumnWidths, ) && + prevProps.albumGroupContentHeights === nextProps.albumGroupContentHeights && + prevProps.setAlbumGroupContentHeight === nextProps.setAlbumGroupContentHeight && prevProps.tableConfig === nextProps.tableConfig && prevProps.data === nextProps.data && prevProps.dataWithGroups === nextProps.dataWithGroups && @@ -766,6 +773,7 @@ export interface TableGroupHeader { export interface TableItemProps { adjustedRowIndexMap?: Map; + albumGroupContentHeights?: Map; albumGroupImageSize?: number; calculatedColumnWidths?: number[]; cellPadding?: ItemTableListProps['cellPadding']; @@ -799,6 +807,7 @@ export interface TableItemProps { pinnedRightColumnWidths?: number[]; playerContext: PlayerContext; playlistId?: string; + setAlbumGroupContentHeight?: (rowIndex: number, height: number) => void; size?: ItemTableListProps['size']; startRowIndex?: number; tableId: string; @@ -1286,6 +1295,23 @@ const BaseItemTableList = ({ const tableId = useId(); const albumGroupImageSize = useAlbumGroupImageSize(); const baseItemCount = itemCount ?? data.length; + const [albumGroupContentHeights, setAlbumGroupContentHeights] = useState( + () => new Map(), + ); + + const setAlbumGroupContentHeight = useCallback((rowIndex: number, height: number) => { + setAlbumGroupContentHeights((prev) => { + if (prev.get(rowIndex) === height) return prev; + const next = new Map(prev); + next.set(rowIndex, height); + return next; + }); + }, []); + + useEffect(() => { + setAlbumGroupContentHeights(new Map()); + }, [baseItemCount, data]); + const totalItemCount = enableHeader ? baseItemCount + 1 : baseItemCount; const [centerContainerWidth, setCenterContainerWidth] = useState(0); const [totalContainerWidth, setTotalContainerWidth] = useState(0); @@ -1444,12 +1470,10 @@ const BaseItemTableList = ({ return headerHeight; } - // When an album image is enlarged beyond the album group's combined - // row height, grow the group's LAST row to reserve the leftover - // space (so the following album isn't clipped). Other rows keep - // their standard height. + // Grow the group's LAST row when the album image or wrapped album + // info (title + artists + controls) needs more than the standard + // combined row height. Other rows keep their standard height. if ( - albumGroupImageSize > baseHeight && cellProps?.hasAlbumGroupColumn && isLastInAlbumGroup( index, @@ -1466,7 +1490,20 @@ const BaseItemTableList = ({ cellProps.enableHeader, cellProps.data.length, ); - const lastRowHeight = albumGroupImageSize - (groupRowCount - 1) * baseHeight; + const groupStartRowIndex = getAlbumGroupStartRowIndex( + index, + cellProps.getRowItem, + cellProps.enableHeader, + ); + const contentHeight = + cellProps.albumGroupContentHeights?.get(groupStartRowIndex) ?? 0; + const totalGroupHeight = getAlbumGroupSpanHeight( + groupRowCount, + baseHeight, + albumGroupImageSize, + contentHeight, + ); + const lastRowHeight = totalGroupHeight - (groupRowCount - 1) * baseHeight; if (lastRowHeight > baseHeight) { return lastRowHeight; } @@ -1775,6 +1812,7 @@ const BaseItemTableList = ({ totalColumnCount={totalColumnCount} />