From 14e66c3ec920711b4d3aed328917588663098ffc Mon Sep 17 00:00:00 2001 From: jeffvli Date: Tue, 14 Jul 2026 15:04:40 -0700 Subject: [PATCH] clamp album group title, improve height calulation --- .../album-group-header.module.css | 5 ++ .../item-table-list/album-group-header.tsx | 9 +++- .../item-table-list-column.tsx | 35 +++++++++++- .../item-table-list/item-table-list.tsx | 53 ++++++++++++++++--- 4 files changed, 93 insertions(+), 9 deletions(-) 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 16acaffec..08d78452e 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 @@ -55,9 +55,14 @@ } .album-title { + display: -webkit-box; + overflow: hidden; + -webkit-line-clamp: 3; + line-clamp: 3; font-weight: 500; overflow-wrap: anywhere; white-space: normal; + -webkit-box-orient: vertical; } .metadata-row { 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 5c41e9490..4a2c2e321 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 @@ -123,7 +123,13 @@ export const AlbumGroupHeader = ({ setResolved({ forInfoHeight: infoHeight, height: resolvedHeight }); } - if (groupKey !== undefined && setAlbumGroupContentHeight) { + // Only persist heights that exceed the image/row floor. Equal values + // still replaced the Map and re-rendered the virtualizer on mount. + if ( + groupKey !== undefined && + setAlbumGroupContentHeight && + contentHeight > (infoHeight ?? 0) + ) { setAlbumGroupContentHeight(groupKey, contentHeight); } }; @@ -135,6 +141,7 @@ export const AlbumGroupHeader = ({ return () => resizeObserver.disconnect(); }, [ + albumImageSize, groupKey, groupRowCount, infoHeight, 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 3991d7ded..339cd520e 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 @@ -384,6 +384,35 @@ export const ItemTableListColumn = memo(ItemTableListColumnBase, (prevProps, nex const NonMutedColumns = [TableColumn.TITLE, TableColumn.TITLE_ARTIST, TableColumn.TITLE_COMBINED]; +/** + * Stable content-height estimate for album-group info (title + metadata + controls). + * Used by the virtualizer before a group header mounts/measures, so scrolling in + * new groups does not jump when measured height is written later. + * Keep in sync with album-group-header styles (title line-clamp, metadata xs, controls). + */ +export function estimateAlbumGroupContentHeight({ + hasEnlargedImage, + metadataRowCount, + showControls, +}: { + hasEnlargedImage: boolean; + metadataRowCount: number; + showControls: boolean; +}): number { + const TITLE_LINE_HEIGHT = 20; + const TITLE_MAX_LINES = 3; + const METADATA_LINE_HEIGHT = 18; + const CONTROLS_HEIGHT = 26; + const PADDING_TOP = hasEnlargedImage ? 8 : 0; + + return ( + PADDING_TOP + + TITLE_LINE_HEIGHT * TITLE_MAX_LINES + + Math.max(0, metadataRowCount) * METADATA_LINE_HEIGHT + + (showControls ? CONTROLS_HEIGHT : 0) + ); +} + /** Stable key for album-group content heights (survives row moves; not row index). */ export function getAlbumGroupHeightKey(item: unknown, groupRowCount?: number): string | undefined { if (!item || typeof item !== 'object') return undefined; @@ -568,8 +597,10 @@ function getAlbumGroupClampHeight(props: ItemTableListInnerColumn): null | numbe ); const groupStartItem = props.getRowItem?.(groupStartRowIndex); const groupHeightKey = getAlbumGroupHeightKey(groupStartItem, groupRowCount); - const contentHeight = - (groupHeightKey ? props.albumGroupContentHeights?.get(groupHeightKey) : undefined) ?? 0; + const measuredContentHeight = groupHeightKey + ? props.albumGroupContentHeights?.get(groupHeightKey) + : undefined; + const contentHeight = measuredContentHeight ?? props.estimatedAlbumGroupContentHeight ?? 0; const totalGroupHeight = getAlbumGroupSpanHeight( groupRowCount, 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 264693733..6c00e75eb 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 @@ -45,6 +45,7 @@ import { useTablePaneSync } from '/@/renderer/components/item-list/item-table-li 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 { + estimateAlbumGroupContentHeight, getAlbumGroupHeightKey, getAlbumGroupRowCount, getAlbumGroupSpanHeight, @@ -73,7 +74,12 @@ import { ItemTableListColumnConfig, } from '/@/renderer/components/item-list/types'; import { PlayerContext, usePlayer } from '/@/renderer/features/player/context/player-context'; -import { useAlbumGroupImageSize, usePlayerStore } from '/@/renderer/store'; +import { + useAlbumGroupImageSize, + useAlbumGroupItems, + useAlbumGroupShowFavoriteRating, + usePlayerStore, +} from '/@/renderer/store'; import { animationProps } from '/@/shared/components/animations/animation-props'; import { useFocusWithin } from '/@/shared/hooks/use-focus-within'; import { useMergedRef } from '/@/shared/hooks/use-merged-ref'; @@ -183,6 +189,7 @@ interface VirtualizedTableGridProps { data: unknown[]; dataWithGroups: (null | unknown)[]; enableScrollShadow: boolean; + estimatedAlbumGroupContentHeight: number; getItem?: (index: number) => undefined | unknown; headerHeight: number; mergedRowRef: React.Ref; @@ -208,6 +215,7 @@ const VirtualizedTableGrid = ({ data, dataWithGroups, enableScrollShadow, + estimatedAlbumGroupContentHeight, getItem, headerHeight, mergedRowRef, @@ -403,6 +411,7 @@ const VirtualizedTableGrid = ({ enableRowHoverHighlight: tableConfig.enableRowHoverHighlight, enableSelection: tableConfig.enableSelection, enableVerticalBorders: tableConfig.enableVerticalBorders, + estimatedAlbumGroupContentHeight, getAdjustedRowIndex, getGroupRenderData, getRowHeight: tableConfig.getRowHeight, @@ -428,6 +437,7 @@ const VirtualizedTableGrid = ({ albumGroupImageSize, calculatedColumnWidths, dataWithGroups, + estimatedAlbumGroupContentHeight, getAdjustedRowIndex, getGroupRenderData, getRowItem, @@ -736,6 +746,8 @@ const MemoizedVirtualizedTableGrid = memo(VirtualizedTableGrid, (prevProps, next nextProps.calculatedColumnWidths, ) && prevProps.albumGroupContentHeights === nextProps.albumGroupContentHeights && + prevProps.estimatedAlbumGroupContentHeight === + nextProps.estimatedAlbumGroupContentHeight && prevProps.setAlbumGroupContentHeight === nextProps.setAlbumGroupContentHeight && prevProps.tableConfig === nextProps.tableConfig && prevProps.data === nextProps.data && @@ -776,6 +788,7 @@ export interface TableItemProps { adjustedRowIndexMap?: Map; albumGroupContentHeights?: Map; albumGroupImageSize?: number; + estimatedAlbumGroupContentHeight?: number; calculatedColumnWidths?: number[]; cellPadding?: ItemTableListProps['cellPadding']; columns: ItemTableListColumnConfig[]; @@ -1297,6 +1310,21 @@ const BaseItemTableList = ({ const { playlistId: routePlaylistId } = useParams() as { playlistId?: string }; const tableId = useId(); const albumGroupImageSize = useAlbumGroupImageSize(); + const albumGroupItems = useAlbumGroupItems(); + const albumGroupShowFavoriteRating = useAlbumGroupShowFavoriteRating(); + const albumGroupMetadataRowCount = useMemo( + () => albumGroupItems.filter((item) => !item.disabled).length, + [albumGroupItems], + ); + const estimatedAlbumGroupContentHeight = useMemo( + () => + estimateAlbumGroupContentHeight({ + hasEnlargedImage: (albumGroupImageSize || 96) > 0, + metadataRowCount: albumGroupMetadataRowCount, + showControls: albumGroupShowFavoriteRating, + }), + [albumGroupImageSize, albumGroupMetadataRowCount, albumGroupShowFavoriteRating], + ); const baseItemCount = itemCount ?? data.length; const [albumGroupContentHeights, setAlbumGroupContentHeights] = useState( () => new Map(), @@ -1463,10 +1491,12 @@ const BaseItemTableList = ({ ); const groupStartItem = cellProps.getRowItem?.(groupStartRowIndex); const groupHeightKey = getAlbumGroupHeightKey(groupStartItem, groupRowCount); - const contentHeight = - (groupHeightKey - ? cellProps.albumGroupContentHeights?.get(groupHeightKey) - : undefined) ?? 0; + const measuredContentHeight = groupHeightKey + ? cellProps.albumGroupContentHeights?.get(groupHeightKey) + : undefined; + // Prefer measured height when present; otherwise reserve with a stable + // estimate so newly virtualized groups do not jump after mount measure. + const contentHeight = measuredContentHeight ?? estimatedAlbumGroupContentHeight; const totalGroupHeight = getAlbumGroupSpanHeight( groupRowCount, baseHeight, @@ -1482,7 +1512,15 @@ const BaseItemTableList = ({ return baseHeight; }, - [albumGroupImageSize, enableHeader, headerHeight, rowHeight, pinnedRowCount, size], + [ + albumGroupImageSize, + enableHeader, + estimatedAlbumGroupContentHeight, + headerHeight, + rowHeight, + pinnedRowCount, + size, + ], ); const scrollCellProps = useMemo( @@ -1500,6 +1538,7 @@ const BaseItemTableList = ({ enableRowHoverHighlight, enableSelection, enableVerticalBorders, + estimatedAlbumGroupContentHeight, getRowHeight, getRowItem: (rowIndex: number) => { if (shouldUseAccessor && getItem) { @@ -1532,6 +1571,7 @@ const BaseItemTableList = ({ enableRowHoverHighlight, enableSelection, enableVerticalBorders, + estimatedAlbumGroupContentHeight, getItem, getRowHeight, hasAlbumGroupColumn, @@ -1887,6 +1927,7 @@ const BaseItemTableList = ({ data={data} dataWithGroups={dataWithGroups} enableScrollShadow={enableScrollShadow} + estimatedAlbumGroupContentHeight={estimatedAlbumGroupContentHeight} getItem={getItem} headerHeight={headerHeight} mergedRowRef={mergedRowRef}