fix album group height calculation on index reorder

This commit is contained in:
jeffvli
2026-07-11 19:20:22 -07:00
parent 13c9602ffb
commit 2e6e7839c1
5 changed files with 63 additions and 22 deletions
@@ -31,21 +31,21 @@ import { LibraryItem, Song } from '/@/shared/types/domain-types';
import { Play } from '/@/shared/types/types'; import { Play } from '/@/shared/types/types';
interface AlbumGroupHeaderProps { interface AlbumGroupHeaderProps {
groupKey?: string;
groupRowCount?: number; groupRowCount?: number;
metadata: AlbumGroupMetadata; metadata: AlbumGroupMetadata;
onPlay?: (playType: Play) => void; onPlay?: (playType: Play) => void;
rowIndex?: number; setAlbumGroupContentHeight?: (groupKey: string, height: number) => void;
setAlbumGroupContentHeight?: (rowIndex: number, height: number) => void;
size?: AlbumGroupTextSize; size?: AlbumGroupTextSize;
song: Song | undefined; song: Song | undefined;
storedContentHeight?: number; storedContentHeight?: number;
} }
export const AlbumGroupHeader = ({ export const AlbumGroupHeader = ({
groupKey,
groupRowCount, groupRowCount,
metadata, metadata,
onPlay, onPlay,
rowIndex,
setAlbumGroupContentHeight, setAlbumGroupContentHeight,
size = 'normal', size = 'normal',
song, song,
@@ -55,7 +55,9 @@ export const AlbumGroupHeader = ({
const albumGroupItems = useAlbumGroupItems(); const albumGroupItems = useAlbumGroupItems();
const showFavoriteRating = useAlbumGroupShowFavoriteRating(); const showFavoriteRating = useAlbumGroupShowFavoriteRating();
const [isHovered, setIsHovered] = useState(false); const [isHovered, setIsHovered] = useState(false);
const [resolvedInfoHeight, setResolvedInfoHeight] = useState<number | undefined>(); const [resolved, setResolved] = useState<null | { forInfoHeight: number; height: number }>(
null,
);
const playButtonBehavior = usePlayButtonBehavior(); const playButtonBehavior = usePlayButtonBehavior();
const albumImageSize = useAlbumGroupImageSize(); const albumImageSize = useAlbumGroupImageSize();
const rowHeight = { const rowHeight = {
@@ -87,6 +89,13 @@ export const AlbumGroupHeader = ({
: groupRowCount * rowHeight : groupRowCount * rowHeight
: undefined; : undefined;
// Ignore resolved height from a previous (larger) group span so minHeight
// cannot keep scrollHeight measurement stuck after a split/shrink.
const resolvedInfoHeight =
resolved && infoHeight !== undefined && resolved.forInfoHeight === infoHeight
? resolved.height
: undefined;
const imageContainerStyle = const imageContainerStyle =
albumImageSize > 0 albumImageSize > 0
? { ? {
@@ -108,12 +117,14 @@ export const AlbumGroupHeader = ({
const measure = () => { const measure = () => {
const contentHeight = infoEl.scrollHeight; const contentHeight = infoEl.scrollHeight;
const resolved = Math.max(infoHeight ?? 0, contentHeight); const resolvedHeight = Math.max(infoHeight ?? 0, contentHeight);
setResolvedInfoHeight(resolved); if (infoHeight !== undefined) {
setResolved({ forInfoHeight: infoHeight, height: resolvedHeight });
}
if (rowIndex !== undefined && setAlbumGroupContentHeight) { if (groupKey !== undefined && setAlbumGroupContentHeight) {
setAlbumGroupContentHeight(rowIndex, contentHeight); setAlbumGroupContentHeight(groupKey, contentHeight);
} }
}; };
@@ -124,9 +135,10 @@ export const AlbumGroupHeader = ({
return () => resizeObserver.disconnect(); return () => resizeObserver.disconnect();
}, [ }, [
groupKey,
groupRowCount,
infoHeight, infoHeight,
metadataRows.length, metadataRows.length,
rowIndex,
setAlbumGroupContentHeight, setAlbumGroupContentHeight,
storedContentHeight, storedContentHeight,
]); ]);
@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next';
import { AlbumGroupHeader } from '/@/renderer/components/item-list/item-table-list/album-group-header'; import { AlbumGroupHeader } from '/@/renderer/components/item-list/item-table-list/album-group-header';
import { computeAlbumGroupMetadata } from '/@/renderer/components/item-list/item-table-list/album-group-metadata'; import { computeAlbumGroupMetadata } from '/@/renderer/components/item-list/item-table-list/album-group-metadata';
import { import {
getAlbumGroupHeightKey,
isLastInAlbumGroup, isLastInAlbumGroup,
ItemTableListInnerColumn, ItemTableListInnerColumn,
TableColumnContainer, TableColumnContainer,
@@ -89,6 +90,10 @@ export const AlbumGroupColumn = (props: ItemTableListInnerColumn) => {
} }
const metadata = computeAlbumGroupMetadata(groupSongs, groupRowCount, t); const metadata = computeAlbumGroupMetadata(groupSongs, groupRowCount, t);
const groupHeightKey = getAlbumGroupHeightKey(item, groupRowCount);
const storedContentHeight = groupHeightKey
? props.albumGroupContentHeights?.get(groupHeightKey)
: undefined;
return ( return (
<TableColumnContainer <TableColumnContainer
@@ -98,14 +103,14 @@ export const AlbumGroupColumn = (props: ItemTableListInnerColumn) => {
isDraggedOver={null} isDraggedOver={null}
> >
<AlbumGroupHeader <AlbumGroupHeader
groupKey={groupHeightKey}
groupRowCount={groupRowCount} groupRowCount={groupRowCount}
metadata={metadata} metadata={metadata}
onPlay={handlePlay} onPlay={handlePlay}
rowIndex={props.rowIndex}
setAlbumGroupContentHeight={props.setAlbumGroupContentHeight} setAlbumGroupContentHeight={props.setAlbumGroupContentHeight}
size={props.size === 'default' ? 'normal' : props.size} size={props.size === 'default' ? 'normal' : props.size}
song={item} song={item}
storedContentHeight={props.albumGroupContentHeights?.get(props.rowIndex)} storedContentHeight={storedContentHeight}
/> />
</TableColumnContainer> </TableColumnContainer>
); );
@@ -19,7 +19,7 @@ export const useTableScrollToIndex = ({
rowRef, rowRef,
scrollCellProps, scrollCellProps,
}: { }: {
albumGroupContentHeights: Map<number, number>; albumGroupContentHeights: Map<string, number>;
autoScrollToActiveRow: boolean; autoScrollToActiveRow: boolean;
enableHeader: boolean; enableHeader: boolean;
getRowHeight: (index: number, cellProps: TableItemProps) => number; getRowHeight: (index: number, cellProps: TableItemProps) => number;
@@ -384,6 +384,22 @@ 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 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;
let itemKey: string | undefined;
if ('_uniqueId' in item && typeof (item as { _uniqueId?: unknown })._uniqueId === 'string') {
itemKey = (item as { _uniqueId: string })._uniqueId;
} else if ('id' in item && typeof (item as { id?: unknown }).id === 'string') {
itemKey = (item as { id: string }).id;
}
if (!itemKey) return undefined;
if (groupRowCount === undefined) return itemKey;
return `${itemKey}:${groupRowCount}`;
}
// Counts how many consecutive rows belong to the same album group as `rowIndex`. // Counts how many consecutive rows belong to the same album group as `rowIndex`.
export function getAlbumGroupRowCount( export function getAlbumGroupRowCount(
rowIndex: number, rowIndex: number,
@@ -550,7 +566,10 @@ function getAlbumGroupClampHeight(props: ItemTableListInnerColumn): null | numbe
props.getRowItem, props.getRowItem,
props.enableHeader, props.enableHeader,
); );
const contentHeight = props.albumGroupContentHeights?.get(groupStartRowIndex) ?? 0; const groupStartItem = props.getRowItem?.(groupStartRowIndex);
const groupHeightKey = getAlbumGroupHeightKey(groupStartItem, groupRowCount);
const contentHeight =
(groupHeightKey ? props.albumGroupContentHeights?.get(groupHeightKey) : undefined) ?? 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 {
getAlbumGroupHeightKey,
getAlbumGroupRowCount, getAlbumGroupRowCount,
getAlbumGroupSpanHeight, getAlbumGroupSpanHeight,
getAlbumGroupStartRowIndex, getAlbumGroupStartRowIndex,
@@ -176,7 +177,7 @@ const ItemTableScrollShadowRight = memo(function ItemTableScrollShadowRight({
ItemTableScrollShadowRight.displayName = 'ItemTableScrollShadowRight'; ItemTableScrollShadowRight.displayName = 'ItemTableScrollShadowRight';
interface VirtualizedTableGridProps { interface VirtualizedTableGridProps {
albumGroupContentHeights: Map<number, number>; albumGroupContentHeights: Map<string, number>;
calculatedColumnWidths: number[]; calculatedColumnWidths: number[];
CellComponent: JSXElementConstructor<CellComponentProps<TableItemProps>>; CellComponent: JSXElementConstructor<CellComponentProps<TableItemProps>>;
data: unknown[]; data: unknown[];
@@ -194,7 +195,7 @@ interface VirtualizedTableGridProps {
pinnedRowCount: number; pinnedRowCount: number;
pinnedRowRef: React.RefObject<HTMLDivElement | null>; pinnedRowRef: React.RefObject<HTMLDivElement | null>;
scrollShadowStore: TableScrollShadowStore; scrollShadowStore: TableScrollShadowStore;
setAlbumGroupContentHeight: (rowIndex: number, height: number) => void; setAlbumGroupContentHeight: (groupKey: string, height: number) => void;
tableConfig: ItemTableListConfig; tableConfig: ItemTableListConfig;
totalColumnCount: number; totalColumnCount: number;
totalRowCount: number; totalRowCount: number;
@@ -773,7 +774,7 @@ export interface TableGroupHeader {
export interface TableItemProps { export interface TableItemProps {
adjustedRowIndexMap?: Map<number, number>; adjustedRowIndexMap?: Map<number, number>;
albumGroupContentHeights?: Map<number, number>; albumGroupContentHeights?: Map<string, number>;
albumGroupImageSize?: number; albumGroupImageSize?: number;
calculatedColumnWidths?: number[]; calculatedColumnWidths?: number[];
cellPadding?: ItemTableListProps['cellPadding']; cellPadding?: ItemTableListProps['cellPadding'];
@@ -807,7 +808,7 @@ export interface TableItemProps {
pinnedRightColumnWidths?: number[]; pinnedRightColumnWidths?: number[];
playerContext: PlayerContext; playerContext: PlayerContext;
playlistId?: string; playlistId?: string;
setAlbumGroupContentHeight?: (rowIndex: number, height: number) => void; setAlbumGroupContentHeight?: (groupKey: string, height: number) => void;
size?: ItemTableListProps['size']; size?: ItemTableListProps['size'];
startRowIndex?: number; startRowIndex?: number;
tableId: string; tableId: string;
@@ -1298,14 +1299,14 @@ const BaseItemTableList = ({
const albumGroupImageSize = useAlbumGroupImageSize(); const albumGroupImageSize = useAlbumGroupImageSize();
const baseItemCount = itemCount ?? data.length; const baseItemCount = itemCount ?? data.length;
const [albumGroupContentHeights, setAlbumGroupContentHeights] = useState( const [albumGroupContentHeights, setAlbumGroupContentHeights] = useState(
() => new Map<number, number>(), () => new Map<string, number>(),
); );
const setAlbumGroupContentHeight = useCallback((rowIndex: number, height: number) => { const setAlbumGroupContentHeight = useCallback((groupKey: string, height: number) => {
setAlbumGroupContentHeights((prev) => { setAlbumGroupContentHeights((prev) => {
if (prev.get(rowIndex) === height) return prev; if (prev.get(groupKey) === height) return prev;
const next = new Map(prev); const next = new Map(prev);
next.set(rowIndex, height); next.set(groupKey, height);
return next; return next;
}); });
}, []); }, []);
@@ -1460,8 +1461,12 @@ const BaseItemTableList = ({
cellProps.getRowItem, cellProps.getRowItem,
cellProps.enableHeader, cellProps.enableHeader,
); );
const groupStartItem = cellProps.getRowItem?.(groupStartRowIndex);
const groupHeightKey = getAlbumGroupHeightKey(groupStartItem, groupRowCount);
const contentHeight = const contentHeight =
cellProps.albumGroupContentHeights?.get(groupStartRowIndex) ?? 0; (groupHeightKey
? cellProps.albumGroupContentHeights?.get(groupHeightKey)
: undefined) ?? 0;
const totalGroupHeight = getAlbumGroupSpanHeight( const totalGroupHeight = getAlbumGroupSpanHeight(
groupRowCount, groupRowCount,
baseHeight, baseHeight,