add additional height calculation for album group

This commit is contained in:
jeffvli
2026-07-10 01:45:39 -07:00
parent f957fb0a05
commit 67a2be1559
6 changed files with 191 additions and 52 deletions
@@ -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;
}
@@ -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);
}
@@ -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<number | undefined>();
const playButtonBehavior = usePlayButtonBehavior();
const albumImageSize = useAlbumGroupImageSize();
const rowHeight = {
@@ -66,6 +72,31 @@ export const AlbumGroupHeader = ({
}
: undefined;
const infoRef = useRef<HTMLDivElement>(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 (
<div
className={styles.container}
@@ -100,7 +131,11 @@ export const AlbumGroupHeader = ({
</div>
)}
</div>
<div className={styles.info} style={{ height: infoHeight }}>
<div
className={clsx(styles.info, albumImageSize > 0 && styles.enlargedImage)}
ref={infoRef}
style={{ minHeight: resolvedInfoHeight ?? infoHeight }}
>
<div className={styles.albumName}>
{song?.albumId && albumPath ? (
<Link state={{ item: song }} to={albumPath}>
@@ -118,12 +153,14 @@ export const AlbumGroupHeader = ({
rootTextProps={{ fw: 400, size: 'xs' }}
/>
</div>
<AlbumGroupControls
albumId={song?.albumId}
isGroupHovered={isHovered}
serverId={song?._serverId}
serverType={song?._serverType}
/>
<div className={styles.controlsRow}>
<AlbumGroupControls
albumId={song?.albumId}
isGroupHovered={isHovered}
serverId={song?._serverId}
serverType={song?._serverType}
/>
</div>
</div>
</div>
);
@@ -93,6 +93,8 @@ export const AlbumGroupColumn = (props: ItemTableListInnerColumn) => {
<AlbumGroupHeader
groupRowCount={groupRowCount}
onPlay={handlePlay}
rowIndex={props.rowIndex}
setAlbumGroupContentHeight={props.setAlbumGroupContentHeight}
size={props.size === 'default' ? 'normal' : props.size}
song={item}
/>
@@ -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;
}
@@ -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<number, number>;
calculatedColumnWidths: number[];
CellComponent: JSXElementConstructor<CellComponentProps<TableItemProps>>;
data: unknown[];
@@ -191,12 +194,14 @@ interface VirtualizedTableGridProps {
pinnedRowCount: number;
pinnedRowRef: React.RefObject<HTMLDivElement | null>;
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<number, number>;
albumGroupContentHeights?: Map<number, number>;
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<number, number>(),
);
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}
/>
<MemoizedVirtualizedTableGrid
albumGroupContentHeights={albumGroupContentHeights}
calculatedColumnWidths={displayColumnWidths}
CellComponent={optimizedCellComponent}
data={data}
@@ -1792,6 +1830,7 @@ const BaseItemTableList = ({
pinnedRowCount={pinnedRowCount}
pinnedRowRef={pinnedRowRef}
scrollShadowStore={scrollShadowStore}
setAlbumGroupContentHeight={setAlbumGroupContentHeight}
tableConfig={tableConfigValue}
totalColumnCount={totalColumnCount}
totalRowCount={totalRowCount}