0 && styles.enlargedImage)}
+ className={styles.info}
ref={infoRef}
style={{ minHeight: resolvedInfoHeight ?? infoHeight }}
>
@@ -213,7 +223,6 @@ export const AlbumGroupHeader = ({
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 339cd520e..3df04e6f5 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
@@ -391,22 +391,18 @@ const NonMutedColumns = [TableColumn.TITLE, TableColumn.TITLE_ARTIST, TableColum
* 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;
+ const CONTROLS_HEIGHT = 38;
return (
- PADDING_TOP +
TITLE_LINE_HEIGHT * TITLE_MAX_LINES +
Math.max(0, metadataRowCount) * METADATA_LINE_HEIGHT +
(showControls ? CONTROLS_HEIGHT : 0)
@@ -459,13 +455,23 @@ export function getAlbumGroupRowCount(
return end - start + 1;
}
+export const ALBUM_GROUP_STACK_GAP = 12;
+
export function getAlbumGroupSpanHeight(
groupRowCount: number,
baseHeight: number,
albumGroupImageSize: number,
contentHeight = 0,
+ options?: { isVertical?: boolean },
): number {
const rowSpanHeight = groupRowCount * baseHeight;
+ const isVertical = options?.isVertical ?? false;
+
+ if (isVertical) {
+ const imageSize = albumGroupImageSize > 0 ? albumGroupImageSize : 96;
+ return Math.max(rowSpanHeight, imageSize + ALBUM_GROUP_STACK_GAP + contentHeight);
+ }
+
const imageSpanHeight =
albumGroupImageSize > 0 ? Math.max(albumGroupImageSize, rowSpanHeight) : rowSpanHeight;
@@ -565,9 +571,6 @@ function ClampedCell({
// standard height and the reserved space below is left empty (uniform
// background) for the overflowing album image.
function getAlbumGroupClampHeight(props: ItemTableListInnerColumn): null | number {
- const albumImageSize = props.albumGroupImageSize ?? 0;
-
- if (albumImageSize <= 0) return null;
if (props.type === TableColumn.ALBUM_GROUP) return null;
if (!isAlbumGroupingActive(props.columns)) return null;
@@ -583,6 +586,8 @@ function getAlbumGroupClampHeight(props: ItemTableListInnerColumn): null | numbe
return null;
}
+ const albumImageSize = props.albumGroupImageSize ?? 0;
+ const isVertical = props.albumGroupVerticalLayout ?? false;
const baseHeight = baseRowHeightForSize(props.size);
const groupRowCount = getAlbumGroupRowCount(
props.rowIndex,
@@ -600,12 +605,18 @@ function getAlbumGroupClampHeight(props: ItemTableListInnerColumn): null | numbe
const measuredContentHeight = groupHeightKey
? props.albumGroupContentHeights?.get(groupHeightKey)
: undefined;
- const contentHeight = measuredContentHeight ?? props.estimatedAlbumGroupContentHeight ?? 0;
+ // Never reserve less than the estimate — early measure can miss the async
+ // favorites/ratings row (AlbumGroupControls returns null until album loads).
+ const contentHeight = Math.max(
+ measuredContentHeight ?? 0,
+ props.estimatedAlbumGroupContentHeight ?? 0,
+ );
const totalGroupHeight = getAlbumGroupSpanHeight(
groupRowCount,
baseHeight,
albumImageSize,
contentHeight,
+ { isVertical },
);
// Only clamp when the row was actually grown to fit the image or wrapped text.
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 6c00e75eb..47c6f98be 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
@@ -78,6 +78,7 @@ import {
useAlbumGroupImageSize,
useAlbumGroupItems,
useAlbumGroupShowFavoriteRating,
+ useAlbumGroupVerticalLayout,
usePlayerStore,
} from '/@/renderer/store';
import { animationProps } from '/@/shared/components/animations/animation-props';
@@ -235,6 +236,7 @@ const VirtualizedTableGrid = ({
}: VirtualizedTableGridProps) => {
const { enableHeader, enableRowHoverHighlight, getRowHeight, groups } = tableConfig;
const albumGroupImageSize = useAlbumGroupImageSize();
+ const albumGroupVerticalLayout = useAlbumGroupVerticalLayout();
const hoverDelegateRef = useRef
(null);
useRowInteractionDelegate({
@@ -396,6 +398,7 @@ const VirtualizedTableGrid = ({
() => ({
albumGroupContentHeights,
albumGroupImageSize,
+ albumGroupVerticalLayout,
calculatedColumnWidths,
cellPadding: tableConfig.cellPadding,
columns: tableConfig.columns,
@@ -435,6 +438,7 @@ const VirtualizedTableGrid = ({
[
albumGroupContentHeights,
albumGroupImageSize,
+ albumGroupVerticalLayout,
calculatedColumnWidths,
dataWithGroups,
estimatedAlbumGroupContentHeight,
@@ -746,8 +750,7 @@ const MemoizedVirtualizedTableGrid = memo(VirtualizedTableGrid, (prevProps, next
nextProps.calculatedColumnWidths,
) &&
prevProps.albumGroupContentHeights === nextProps.albumGroupContentHeights &&
- prevProps.estimatedAlbumGroupContentHeight ===
- nextProps.estimatedAlbumGroupContentHeight &&
+ prevProps.estimatedAlbumGroupContentHeight === nextProps.estimatedAlbumGroupContentHeight &&
prevProps.setAlbumGroupContentHeight === nextProps.setAlbumGroupContentHeight &&
prevProps.tableConfig === nextProps.tableConfig &&
prevProps.data === nextProps.data &&
@@ -788,7 +791,7 @@ export interface TableItemProps {
adjustedRowIndexMap?: Map;
albumGroupContentHeights?: Map;
albumGroupImageSize?: number;
- estimatedAlbumGroupContentHeight?: number;
+ albumGroupVerticalLayout?: boolean;
calculatedColumnWidths?: number[];
cellPadding?: ItemTableListProps['cellPadding'];
columns: ItemTableListColumnConfig[];
@@ -805,6 +808,7 @@ export interface TableItemProps {
enableRowHoverHighlight?: ItemTableListProps['enableRowHoverHighlight'];
enableSelection?: ItemTableListProps['enableSelection'];
enableVerticalBorders?: ItemTableListProps['enableVerticalBorders'];
+ estimatedAlbumGroupContentHeight?: number;
getAdjustedRowIndex?: (rowIndex: number) => number;
getGroupRenderData?: () => unknown[];
getRowHeight: (index: number, cellProps: TableItemProps) => number;
@@ -1312,6 +1316,7 @@ const BaseItemTableList = ({
const albumGroupImageSize = useAlbumGroupImageSize();
const albumGroupItems = useAlbumGroupItems();
const albumGroupShowFavoriteRating = useAlbumGroupShowFavoriteRating();
+ const albumGroupVerticalLayout = useAlbumGroupVerticalLayout();
const albumGroupMetadataRowCount = useMemo(
() => albumGroupItems.filter((item) => !item.disabled).length,
[albumGroupItems],
@@ -1319,11 +1324,10 @@ const BaseItemTableList = ({
const estimatedAlbumGroupContentHeight = useMemo(
() =>
estimateAlbumGroupContentHeight({
- hasEnlargedImage: (albumGroupImageSize || 96) > 0,
metadataRowCount: albumGroupMetadataRowCount,
showControls: albumGroupShowFavoriteRating,
}),
- [albumGroupImageSize, albumGroupMetadataRowCount, albumGroupShowFavoriteRating],
+ [albumGroupMetadataRowCount, albumGroupShowFavoriteRating],
);
const baseItemCount = itemCount ?? data.length;
const [albumGroupContentHeights, setAlbumGroupContentHeights] = useState(
@@ -1341,7 +1345,7 @@ const BaseItemTableList = ({
useEffect(() => {
setAlbumGroupContentHeights(new Map());
- }, [baseItemCount]);
+ }, [albumGroupShowFavoriteRating, albumGroupVerticalLayout, baseItemCount]);
const totalItemCount = enableHeader ? baseItemCount + 1 : baseItemCount;
const [centerContainerWidth, setCenterContainerWidth] = useState(0);
@@ -1496,12 +1500,16 @@ const BaseItemTableList = ({
: 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 contentHeight = Math.max(
+ measuredContentHeight ?? 0,
+ estimatedAlbumGroupContentHeight,
+ );
const totalGroupHeight = getAlbumGroupSpanHeight(
groupRowCount,
baseHeight,
albumGroupImageSize,
contentHeight,
+ { isVertical: albumGroupVerticalLayout },
);
const lastRowHeight = totalGroupHeight - (groupRowCount - 1) * baseHeight;
if (lastRowHeight > baseHeight) {
@@ -1514,6 +1522,7 @@ const BaseItemTableList = ({
},
[
albumGroupImageSize,
+ albumGroupVerticalLayout,
enableHeader,
estimatedAlbumGroupContentHeight,
headerHeight,
@@ -1527,6 +1536,7 @@ const BaseItemTableList = ({
() => ({
albumGroupContentHeights,
albumGroupImageSize,
+ albumGroupVerticalLayout,
cellPadding,
columns: parsedColumns,
controls: {} as ItemControls,
@@ -1562,6 +1572,7 @@ const BaseItemTableList = ({
[
albumGroupContentHeights,
albumGroupImageSize,
+ albumGroupVerticalLayout,
cellPadding,
dataWithGroups,
enableAlternateRowColors,
diff --git a/src/renderer/features/shared/components/table-config.tsx b/src/renderer/features/shared/components/table-config.tsx
index 23eca1952..9cd85fece 100644
--- a/src/renderer/features/shared/components/table-config.tsx
+++ b/src/renderer/features/shared/components/table-config.tsx
@@ -78,6 +78,9 @@ export const TableConfig = ({
const albumGroupShowFavoriteRating = useSettingsStore(
(state) => state.general.albumGroupShowFavoriteRating,
);
+ const albumGroupVerticalLayout = useSettingsStore(
+ (state) => state.general.albumGroupVerticalLayout,
+ );
const imageResTable = useSettingsStore((state) => state.general.imageRes.table);
const { setList, setSettings } = useSettingsStoreActions();
const [albumGroupOpen, setAlbumGroupOpen] = useState(false);
@@ -185,6 +188,26 @@ export const TableConfig = ({
),
},
+ {
+ component: (
+
+ setSettings({
+ general: {
+ albumGroupVerticalLayout: value,
+ },
+ })
+ }
+ value={albumGroupVerticalLayout}
+ />
+ ),
+ id: 'albumGroupVerticalLayout',
+ label: (
+
+ {t('table.config.general.albumGroupVerticalLayout')}
+
+ ),
+ },
]
: []),
]
@@ -367,6 +390,7 @@ export const TableConfig = ({
albumGroupOpen,
albumGroupImageSize,
albumGroupShowFavoriteRating,
+ albumGroupVerticalLayout,
imageResTable,
setSettings,
]);
diff --git a/src/renderer/store/settings.store.ts b/src/renderer/store/settings.store.ts
index e425b4d45..114cad413 100644
--- a/src/renderer/store/settings.store.ts
+++ b/src/renderer/store/settings.store.ts
@@ -488,6 +488,7 @@ export const GeneralSettingsSchema = z.object({
albumGroupImageSize: z.number(),
albumGroupItems: z.array(SortableItemSchema(AlbumGroupItemSchema)),
albumGroupShowFavoriteRating: z.boolean(),
+ albumGroupVerticalLayout: z.boolean(),
artistBackground: z.boolean(),
artistBackgroundBlur: z.number(),
artistItems: z.array(SortableItemSchema(ArtistItemSchema)),
@@ -1211,6 +1212,7 @@ const initialState: SettingsState = {
albumGroupImageSize: 0,
albumGroupItems,
albumGroupShowFavoriteRating: true,
+ albumGroupVerticalLayout: true,
artistBackground: true,
artistBackgroundBlur: 3,
artistItems,
@@ -2727,6 +2729,9 @@ export const useAlbumGroupImageSize = () =>
export const useAlbumGroupShowFavoriteRating = () =>
useSettingsStore((state) => state.general.albumGroupShowFavoriteRating);
+export const useAlbumGroupVerticalLayout = () =>
+ useSettingsStore((state) => state.general.albumGroupVerticalLayout);
+
export const useVolumeWidth = () => useSettingsStore((state) => state.general.volumeWidth, shallow);
export const useFollowCurrentSong = () =>