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; display: flex;
gap: var(--theme-spacing-xs); gap: var(--theme-spacing-xs);
align-items: center; align-items: center;
margin-top: var(--theme-spacing-xxs); min-height: 22px;
} }
.hidden { .hidden {
display: none; visibility: hidden;
} }
@@ -1,6 +1,7 @@
.container { .container {
display: flex; display: flex;
gap: var(--theme-spacing-sm); gap: var(--theme-spacing-sm);
align-items: flex-start;
width: 100%; width: 100%;
height: 100%; height: 100%;
padding: 0 var(--theme-spacing-xs); padding: 0 var(--theme-spacing-xs);
@@ -42,15 +43,23 @@
} }
.info { .info {
display: flex; display: grid;
flex-direction: column; grid-template-rows: auto auto auto;
gap: 0;
align-content: start;
min-width: 0; min-width: 0;
padding: 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 { .album-name {
font-size: var(--theme-font-size-sm); font-size: var(--theme-font-size-sm);
line-height: 1.25;
overflow-wrap: anywhere;
} }
.album-name a { .album-name a {
@@ -64,5 +73,13 @@
.artist-name { .artist-name {
font-size: var(--theme-font-size-xs); font-size: var(--theme-font-size-xs);
line-height: 1.25;
overflow-wrap: anywhere;
opacity: 0.7; 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 { generatePath, Link } from 'react-router';
import imageColumnStyles from '../item-detail-list/columns/image-column.module.css'; import imageColumnStyles from '../item-detail-list/columns/image-column.module.css';
@@ -21,6 +22,8 @@ import { Play } from '/@/shared/types/types';
interface AlbumGroupHeaderProps { interface AlbumGroupHeaderProps {
groupRowCount?: number; groupRowCount?: number;
onPlay?: (playType: Play) => void; onPlay?: (playType: Play) => void;
rowIndex?: number;
setAlbumGroupContentHeight?: (rowIndex: number, height: number) => void;
size?: 'compact' | 'large' | 'normal'; size?: 'compact' | 'large' | 'normal';
song: Song | undefined; song: Song | undefined;
} }
@@ -28,10 +31,13 @@ interface AlbumGroupHeaderProps {
export const AlbumGroupHeader = ({ export const AlbumGroupHeader = ({
groupRowCount, groupRowCount,
onPlay, onPlay,
rowIndex,
setAlbumGroupContentHeight,
size = 'normal', size = 'normal',
song, song,
}: AlbumGroupHeaderProps): ReactElement => { }: AlbumGroupHeaderProps): ReactElement => {
const [isHovered, setIsHovered] = useState(false); const [isHovered, setIsHovered] = useState(false);
const [resolvedInfoHeight, setResolvedInfoHeight] = useState<number | undefined>();
const playButtonBehavior = usePlayButtonBehavior(); const playButtonBehavior = usePlayButtonBehavior();
const albumImageSize = useAlbumGroupImageSize(); const albumImageSize = useAlbumGroupImageSize();
const rowHeight = { const rowHeight = {
@@ -66,6 +72,31 @@ export const AlbumGroupHeader = ({
} }
: undefined; : 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 ( return (
<div <div
className={styles.container} className={styles.container}
@@ -100,7 +131,11 @@ export const AlbumGroupHeader = ({
</div> </div>
)} )}
</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}> <div className={styles.albumName}>
{song?.albumId && albumPath ? ( {song?.albumId && albumPath ? (
<Link state={{ item: song }} to={albumPath}> <Link state={{ item: song }} to={albumPath}>
@@ -118,6 +153,7 @@ export const AlbumGroupHeader = ({
rootTextProps={{ fw: 400, size: 'xs' }} rootTextProps={{ fw: 400, size: 'xs' }}
/> />
</div> </div>
<div className={styles.controlsRow}>
<AlbumGroupControls <AlbumGroupControls
albumId={song?.albumId} albumId={song?.albumId}
isGroupHovered={isHovered} isGroupHovered={isHovered}
@@ -126,5 +162,6 @@ export const AlbumGroupHeader = ({
/> />
</div> </div>
</div> </div>
</div>
); );
}; };
@@ -93,6 +93,8 @@ export const AlbumGroupColumn = (props: ItemTableListInnerColumn) => {
<AlbumGroupHeader <AlbumGroupHeader
groupRowCount={groupRowCount} groupRowCount={groupRowCount}
onPlay={handlePlay} onPlay={handlePlay}
rowIndex={props.rowIndex}
setAlbumGroupContentHeight={props.setAlbumGroupContentHeight}
size={props.size === 'default' ? 'normal' : props.size} size={props.size === 'default' ? 'normal' : props.size}
song={item} song={item}
/> />
@@ -414,6 +414,38 @@ export function getAlbumGroupRowCount(
return end - start + 1; 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 { export function isAlbumGroupingActive(columns: { id: string; isEnabled?: boolean }[]): boolean {
return columns.some((col) => col.id === TableColumn.ALBUM_GROUP && col.isEnabled); return columns.some((col) => col.id === TableColumn.ALBUM_GROUP && col.isEnabled);
} }
@@ -513,9 +545,21 @@ function getAlbumGroupClampHeight(props: ItemTableListInnerColumn): null | numbe
props.enableHeader, props.enableHeader,
props.data.length, 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. // Only clamp when the row was actually grown to fit the image or wrapped text.
if (albumImageSize <= groupRowCount * baseHeight) return null; if (totalGroupHeight <= groupRowCount * baseHeight) return null;
return baseHeight; 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 { useTableScrollToIndex } from '/@/renderer/components/item-list/item-table-list/hooks/use-table-scroll-to-index';
import { import {
getAlbumGroupRowCount, getAlbumGroupRowCount,
getAlbumGroupSpanHeight,
getAlbumGroupStartRowIndex,
isLastInAlbumGroup, isLastInAlbumGroup,
ItemTableListColumn, ItemTableListColumn,
} from '/@/renderer/components/item-list/item-table-list/item-table-list-column'; } from '/@/renderer/components/item-list/item-table-list/item-table-list-column';
@@ -174,6 +176,7 @@ const ItemTableScrollShadowRight = memo(function ItemTableScrollShadowRight({
ItemTableScrollShadowRight.displayName = 'ItemTableScrollShadowRight'; ItemTableScrollShadowRight.displayName = 'ItemTableScrollShadowRight';
interface VirtualizedTableGridProps { interface VirtualizedTableGridProps {
albumGroupContentHeights: Map<number, number>;
calculatedColumnWidths: number[]; calculatedColumnWidths: number[];
CellComponent: JSXElementConstructor<CellComponentProps<TableItemProps>>; CellComponent: JSXElementConstructor<CellComponentProps<TableItemProps>>;
data: unknown[]; data: unknown[];
@@ -191,12 +194,14 @@ 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;
tableConfig: ItemTableListConfig; tableConfig: ItemTableListConfig;
totalColumnCount: number; totalColumnCount: number;
totalRowCount: number; totalRowCount: number;
} }
const VirtualizedTableGrid = ({ const VirtualizedTableGrid = ({
albumGroupContentHeights,
calculatedColumnWidths, calculatedColumnWidths,
CellComponent, CellComponent,
data, data,
@@ -214,6 +219,7 @@ const VirtualizedTableGrid = ({
pinnedRowCount, pinnedRowCount,
pinnedRowRef, pinnedRowRef,
scrollShadowStore, scrollShadowStore,
setAlbumGroupContentHeight,
tableConfig, tableConfig,
totalColumnCount, totalColumnCount,
totalRowCount, 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( const itemProps: TableItemProps = useMemo(
() => ({ () => ({
albumGroupContentHeights,
albumGroupImageSize, albumGroupImageSize,
calculatedColumnWidths,
cellPadding: tableConfig.cellPadding, cellPadding: tableConfig.cellPadding,
columns: tableConfig.columns, columns: tableConfig.columns,
controls: tableConfig.controls, controls: tableConfig.controls,
data: dataWithGroups,
enableAlternateRowColors: tableConfig.enableAlternateRowColors, enableAlternateRowColors: tableConfig.enableAlternateRowColors,
enableColumnReorder: tableConfig.enableColumnReorder, enableColumnReorder: tableConfig.enableColumnReorder,
enableColumnResize: tableConfig.enableColumnResize, enableColumnResize: tableConfig.enableColumnResize,
@@ -422,18 +402,43 @@ const VirtualizedTableGrid = ({
enableRowHoverHighlight: tableConfig.enableRowHoverHighlight, enableRowHoverHighlight: tableConfig.enableRowHoverHighlight,
enableSelection: tableConfig.enableSelection, enableSelection: tableConfig.enableSelection,
enableVerticalBorders: tableConfig.enableVerticalBorders, enableVerticalBorders: tableConfig.enableVerticalBorders,
getAdjustedRowIndex,
getGroupRenderData,
getRowHeight: tableConfig.getRowHeight, getRowHeight: tableConfig.getRowHeight,
getRowItem,
groupHeaderInfoByRowIndex,
groups: tableConfig.groups, groups: tableConfig.groups,
hasAlbumGroupColumn: parsedColumns.some((col) => col.id === TableColumn.ALBUM_GROUP),
internalState: tableConfig.internalState, internalState: tableConfig.internalState,
itemType: tableConfig.itemType, itemType: tableConfig.itemType,
pinnedLeftColumnCount,
pinnedLeftColumnWidths,
pinnedRightColumnCount,
pinnedRightColumnWidths,
playerContext: tableConfig.playerContext, playerContext: tableConfig.playerContext,
playlistId: tableConfig.playlistId, playlistId: tableConfig.playlistId,
setAlbumGroupContentHeight,
size: tableConfig.size, size: tableConfig.size,
startRowIndex: tableConfig.startRowIndex, startRowIndex: tableConfig.startRowIndex,
tableId: tableConfig.tableId, 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(() => { const pinnedLeftGridMinWidthPx = useMemo(() => {
@@ -729,6 +734,8 @@ const MemoizedVirtualizedTableGrid = memo(VirtualizedTableGrid, (prevProps, next
prevProps.calculatedColumnWidths, prevProps.calculatedColumnWidths,
nextProps.calculatedColumnWidths, nextProps.calculatedColumnWidths,
) && ) &&
prevProps.albumGroupContentHeights === nextProps.albumGroupContentHeights &&
prevProps.setAlbumGroupContentHeight === nextProps.setAlbumGroupContentHeight &&
prevProps.tableConfig === nextProps.tableConfig && prevProps.tableConfig === nextProps.tableConfig &&
prevProps.data === nextProps.data && prevProps.data === nextProps.data &&
prevProps.dataWithGroups === nextProps.dataWithGroups && prevProps.dataWithGroups === nextProps.dataWithGroups &&
@@ -766,6 +773,7 @@ export interface TableGroupHeader {
export interface TableItemProps { export interface TableItemProps {
adjustedRowIndexMap?: Map<number, number>; adjustedRowIndexMap?: Map<number, number>;
albumGroupContentHeights?: Map<number, number>;
albumGroupImageSize?: number; albumGroupImageSize?: number;
calculatedColumnWidths?: number[]; calculatedColumnWidths?: number[];
cellPadding?: ItemTableListProps['cellPadding']; cellPadding?: ItemTableListProps['cellPadding'];
@@ -799,6 +807,7 @@ export interface TableItemProps {
pinnedRightColumnWidths?: number[]; pinnedRightColumnWidths?: number[];
playerContext: PlayerContext; playerContext: PlayerContext;
playlistId?: string; playlistId?: string;
setAlbumGroupContentHeight?: (rowIndex: number, height: number) => void;
size?: ItemTableListProps['size']; size?: ItemTableListProps['size'];
startRowIndex?: number; startRowIndex?: number;
tableId: string; tableId: string;
@@ -1286,6 +1295,23 @@ const BaseItemTableList = ({
const tableId = useId(); const tableId = useId();
const albumGroupImageSize = useAlbumGroupImageSize(); const albumGroupImageSize = useAlbumGroupImageSize();
const baseItemCount = itemCount ?? data.length; 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 totalItemCount = enableHeader ? baseItemCount + 1 : baseItemCount;
const [centerContainerWidth, setCenterContainerWidth] = useState(0); const [centerContainerWidth, setCenterContainerWidth] = useState(0);
const [totalContainerWidth, setTotalContainerWidth] = useState(0); const [totalContainerWidth, setTotalContainerWidth] = useState(0);
@@ -1444,12 +1470,10 @@ const BaseItemTableList = ({
return headerHeight; return headerHeight;
} }
// When an album image is enlarged beyond the album group's combined // Grow the group's LAST row when the album image or wrapped album
// row height, grow the group's LAST row to reserve the leftover // info (title + artists + controls) needs more than the standard
// space (so the following album isn't clipped). Other rows keep // combined row height. Other rows keep their standard height.
// their standard height.
if ( if (
albumGroupImageSize > baseHeight &&
cellProps?.hasAlbumGroupColumn && cellProps?.hasAlbumGroupColumn &&
isLastInAlbumGroup( isLastInAlbumGroup(
index, index,
@@ -1466,7 +1490,20 @@ const BaseItemTableList = ({
cellProps.enableHeader, cellProps.enableHeader,
cellProps.data.length, 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) { if (lastRowHeight > baseHeight) {
return lastRowHeight; return lastRowHeight;
} }
@@ -1775,6 +1812,7 @@ const BaseItemTableList = ({
totalColumnCount={totalColumnCount} totalColumnCount={totalColumnCount}
/> />
<MemoizedVirtualizedTableGrid <MemoizedVirtualizedTableGrid
albumGroupContentHeights={albumGroupContentHeights}
calculatedColumnWidths={displayColumnWidths} calculatedColumnWidths={displayColumnWidths}
CellComponent={optimizedCellComponent} CellComponent={optimizedCellComponent}
data={data} data={data}
@@ -1792,6 +1830,7 @@ const BaseItemTableList = ({
pinnedRowCount={pinnedRowCount} pinnedRowCount={pinnedRowCount}
pinnedRowRef={pinnedRowRef} pinnedRowRef={pinnedRowRef}
scrollShadowStore={scrollShadowStore} scrollShadowStore={scrollShadowStore}
setAlbumGroupContentHeight={setAlbumGroupContentHeight}
tableConfig={tableConfigValue} tableConfig={tableConfigValue}
totalColumnCount={totalColumnCount} totalColumnCount={totalColumnCount}
totalRowCount={totalRowCount} totalRowCount={totalRowCount}