clamp album group title, improve height calulation

This commit is contained in:
jeffvli
2026-07-14 15:04:40 -07:00
parent 5c7a644a71
commit 14e66c3ec9
4 changed files with 93 additions and 9 deletions
@@ -55,9 +55,14 @@
} }
.album-title { .album-title {
display: -webkit-box;
overflow: hidden;
-webkit-line-clamp: 3;
line-clamp: 3;
font-weight: 500; font-weight: 500;
overflow-wrap: anywhere; overflow-wrap: anywhere;
white-space: normal; white-space: normal;
-webkit-box-orient: vertical;
} }
.metadata-row { .metadata-row {
@@ -123,7 +123,13 @@ export const AlbumGroupHeader = ({
setResolved({ forInfoHeight: infoHeight, height: resolvedHeight }); 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); setAlbumGroupContentHeight(groupKey, contentHeight);
} }
}; };
@@ -135,6 +141,7 @@ export const AlbumGroupHeader = ({
return () => resizeObserver.disconnect(); return () => resizeObserver.disconnect();
}, [ }, [
albumImageSize,
groupKey, groupKey,
groupRowCount, groupRowCount,
infoHeight, infoHeight,
@@ -384,6 +384,35 @@ export const ItemTableListColumn = memo(ItemTableListColumnBase, (prevProps, nex
const NonMutedColumns = [TableColumn.TITLE, TableColumn.TITLE_ARTIST, TableColumn.TITLE_COMBINED]; 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). */ /** Stable key for album-group content heights (survives row moves; not row index). */
export function getAlbumGroupHeightKey(item: unknown, groupRowCount?: number): string | undefined { export function getAlbumGroupHeightKey(item: unknown, groupRowCount?: number): string | undefined {
if (!item || typeof item !== 'object') return undefined; if (!item || typeof item !== 'object') return undefined;
@@ -568,8 +597,10 @@ function getAlbumGroupClampHeight(props: ItemTableListInnerColumn): null | numbe
); );
const groupStartItem = props.getRowItem?.(groupStartRowIndex); const groupStartItem = props.getRowItem?.(groupStartRowIndex);
const groupHeightKey = getAlbumGroupHeightKey(groupStartItem, groupRowCount); const groupHeightKey = getAlbumGroupHeightKey(groupStartItem, groupRowCount);
const contentHeight = const measuredContentHeight = groupHeightKey
(groupHeightKey ? props.albumGroupContentHeights?.get(groupHeightKey) : undefined) ?? 0; ? props.albumGroupContentHeights?.get(groupHeightKey)
: undefined;
const contentHeight = measuredContentHeight ?? props.estimatedAlbumGroupContentHeight ?? 0;
const totalGroupHeight = getAlbumGroupSpanHeight( const totalGroupHeight = getAlbumGroupSpanHeight(
groupRowCount, groupRowCount,
baseHeight, baseHeight,
@@ -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 { 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 { useTableScrollToIndex } from '/@/renderer/components/item-list/item-table-list/hooks/use-table-scroll-to-index';
import { import {
estimateAlbumGroupContentHeight,
getAlbumGroupHeightKey, getAlbumGroupHeightKey,
getAlbumGroupRowCount, getAlbumGroupRowCount,
getAlbumGroupSpanHeight, getAlbumGroupSpanHeight,
@@ -73,7 +74,12 @@ import {
ItemTableListColumnConfig, ItemTableListColumnConfig,
} from '/@/renderer/components/item-list/types'; } from '/@/renderer/components/item-list/types';
import { PlayerContext, usePlayer } from '/@/renderer/features/player/context/player-context'; 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 { animationProps } from '/@/shared/components/animations/animation-props';
import { useFocusWithin } from '/@/shared/hooks/use-focus-within'; import { useFocusWithin } from '/@/shared/hooks/use-focus-within';
import { useMergedRef } from '/@/shared/hooks/use-merged-ref'; import { useMergedRef } from '/@/shared/hooks/use-merged-ref';
@@ -183,6 +189,7 @@ interface VirtualizedTableGridProps {
data: unknown[]; data: unknown[];
dataWithGroups: (null | unknown)[]; dataWithGroups: (null | unknown)[];
enableScrollShadow: boolean; enableScrollShadow: boolean;
estimatedAlbumGroupContentHeight: number;
getItem?: (index: number) => undefined | unknown; getItem?: (index: number) => undefined | unknown;
headerHeight: number; headerHeight: number;
mergedRowRef: React.Ref<HTMLDivElement>; mergedRowRef: React.Ref<HTMLDivElement>;
@@ -208,6 +215,7 @@ const VirtualizedTableGrid = ({
data, data,
dataWithGroups, dataWithGroups,
enableScrollShadow, enableScrollShadow,
estimatedAlbumGroupContentHeight,
getItem, getItem,
headerHeight, headerHeight,
mergedRowRef, mergedRowRef,
@@ -403,6 +411,7 @@ const VirtualizedTableGrid = ({
enableRowHoverHighlight: tableConfig.enableRowHoverHighlight, enableRowHoverHighlight: tableConfig.enableRowHoverHighlight,
enableSelection: tableConfig.enableSelection, enableSelection: tableConfig.enableSelection,
enableVerticalBorders: tableConfig.enableVerticalBorders, enableVerticalBorders: tableConfig.enableVerticalBorders,
estimatedAlbumGroupContentHeight,
getAdjustedRowIndex, getAdjustedRowIndex,
getGroupRenderData, getGroupRenderData,
getRowHeight: tableConfig.getRowHeight, getRowHeight: tableConfig.getRowHeight,
@@ -428,6 +437,7 @@ const VirtualizedTableGrid = ({
albumGroupImageSize, albumGroupImageSize,
calculatedColumnWidths, calculatedColumnWidths,
dataWithGroups, dataWithGroups,
estimatedAlbumGroupContentHeight,
getAdjustedRowIndex, getAdjustedRowIndex,
getGroupRenderData, getGroupRenderData,
getRowItem, getRowItem,
@@ -736,6 +746,8 @@ const MemoizedVirtualizedTableGrid = memo(VirtualizedTableGrid, (prevProps, next
nextProps.calculatedColumnWidths, nextProps.calculatedColumnWidths,
) && ) &&
prevProps.albumGroupContentHeights === nextProps.albumGroupContentHeights && prevProps.albumGroupContentHeights === nextProps.albumGroupContentHeights &&
prevProps.estimatedAlbumGroupContentHeight ===
nextProps.estimatedAlbumGroupContentHeight &&
prevProps.setAlbumGroupContentHeight === nextProps.setAlbumGroupContentHeight && prevProps.setAlbumGroupContentHeight === nextProps.setAlbumGroupContentHeight &&
prevProps.tableConfig === nextProps.tableConfig && prevProps.tableConfig === nextProps.tableConfig &&
prevProps.data === nextProps.data && prevProps.data === nextProps.data &&
@@ -776,6 +788,7 @@ export interface TableItemProps {
adjustedRowIndexMap?: Map<number, number>; adjustedRowIndexMap?: Map<number, number>;
albumGroupContentHeights?: Map<string, number>; albumGroupContentHeights?: Map<string, number>;
albumGroupImageSize?: number; albumGroupImageSize?: number;
estimatedAlbumGroupContentHeight?: number;
calculatedColumnWidths?: number[]; calculatedColumnWidths?: number[];
cellPadding?: ItemTableListProps['cellPadding']; cellPadding?: ItemTableListProps['cellPadding'];
columns: ItemTableListColumnConfig[]; columns: ItemTableListColumnConfig[];
@@ -1297,6 +1310,21 @@ const BaseItemTableList = ({
const { playlistId: routePlaylistId } = useParams() as { playlistId?: string }; const { playlistId: routePlaylistId } = useParams() as { playlistId?: string };
const tableId = useId(); const tableId = useId();
const albumGroupImageSize = useAlbumGroupImageSize(); 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 baseItemCount = itemCount ?? data.length;
const [albumGroupContentHeights, setAlbumGroupContentHeights] = useState( const [albumGroupContentHeights, setAlbumGroupContentHeights] = useState(
() => new Map<string, number>(), () => new Map<string, number>(),
@@ -1463,10 +1491,12 @@ const BaseItemTableList = ({
); );
const groupStartItem = cellProps.getRowItem?.(groupStartRowIndex); const groupStartItem = cellProps.getRowItem?.(groupStartRowIndex);
const groupHeightKey = getAlbumGroupHeightKey(groupStartItem, groupRowCount); const groupHeightKey = getAlbumGroupHeightKey(groupStartItem, groupRowCount);
const contentHeight = const measuredContentHeight = groupHeightKey
(groupHeightKey ? cellProps.albumGroupContentHeights?.get(groupHeightKey)
? cellProps.albumGroupContentHeights?.get(groupHeightKey) : undefined;
: undefined) ?? 0; // 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( const totalGroupHeight = getAlbumGroupSpanHeight(
groupRowCount, groupRowCount,
baseHeight, baseHeight,
@@ -1482,7 +1512,15 @@ const BaseItemTableList = ({
return baseHeight; return baseHeight;
}, },
[albumGroupImageSize, enableHeader, headerHeight, rowHeight, pinnedRowCount, size], [
albumGroupImageSize,
enableHeader,
estimatedAlbumGroupContentHeight,
headerHeight,
rowHeight,
pinnedRowCount,
size,
],
); );
const scrollCellProps = useMemo<TableItemProps>( const scrollCellProps = useMemo<TableItemProps>(
@@ -1500,6 +1538,7 @@ const BaseItemTableList = ({
enableRowHoverHighlight, enableRowHoverHighlight,
enableSelection, enableSelection,
enableVerticalBorders, enableVerticalBorders,
estimatedAlbumGroupContentHeight,
getRowHeight, getRowHeight,
getRowItem: (rowIndex: number) => { getRowItem: (rowIndex: number) => {
if (shouldUseAccessor && getItem) { if (shouldUseAccessor && getItem) {
@@ -1532,6 +1571,7 @@ const BaseItemTableList = ({
enableRowHoverHighlight, enableRowHoverHighlight,
enableSelection, enableSelection,
enableVerticalBorders, enableVerticalBorders,
estimatedAlbumGroupContentHeight,
getItem, getItem,
getRowHeight, getRowHeight,
hasAlbumGroupColumn, hasAlbumGroupColumn,
@@ -1887,6 +1927,7 @@ const BaseItemTableList = ({
data={data} data={data}
dataWithGroups={dataWithGroups} dataWithGroups={dataWithGroups}
enableScrollShadow={enableScrollShadow} enableScrollShadow={enableScrollShadow}
estimatedAlbumGroupContentHeight={estimatedAlbumGroupContentHeight}
getItem={getItem} getItem={getItem}
headerHeight={headerHeight} headerHeight={headerHeight}
mergedRowRef={mergedRowRef} mergedRowRef={mergedRowRef}