mirror of
https://github.com/jeffvli/feishin.git
synced 2026-07-22 10:26:33 +02:00
add vertical album group layout
This commit is contained in:
@@ -1233,6 +1233,7 @@
|
|||||||
"albumGroupConfig": "Album Group configuration",
|
"albumGroupConfig": "Album Group configuration",
|
||||||
"albumGroupMetadata": "Metadata fields",
|
"albumGroupMetadata": "Metadata fields",
|
||||||
"albumGroupShowFavoriteRating": "Show favorites and ratings",
|
"albumGroupShowFavoriteRating": "Show favorites and ratings",
|
||||||
|
"albumGroupVerticalLayout": "Vertical layout",
|
||||||
"albumImageSize": "Album image size",
|
"albumImageSize": "Album image size",
|
||||||
"autoFitColumns": "Auto fit columns",
|
"autoFitColumns": "Auto fit columns",
|
||||||
"autosize": "Autosize",
|
"autosize": "Autosize",
|
||||||
|
|||||||
@@ -2,9 +2,22 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--theme-spacing-xs);
|
gap: var(--theme-spacing-xs);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
min-height: 22px;
|
min-height: calc(1.875rem * var(--mantine-scale, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
.hidden {
|
.favorite {
|
||||||
visibility: hidden;
|
width: auto !important;
|
||||||
|
min-width: 0 !important;
|
||||||
|
height: calc(1.875rem * var(--mantine-scale, 1)) !important;
|
||||||
|
min-height: calc(1.875rem * var(--mantine-scale, 1)) !important;
|
||||||
|
padding-inline: var(--theme-spacing-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rating {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: calc(1.875rem * var(--mantine-scale, 1));
|
||||||
|
padding-block: var(--theme-spacing-xs);
|
||||||
|
padding-inline: var(--theme-spacing-xs) !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,17 +29,11 @@ const useAlbumGroupAlbum = (albumId: string | undefined, serverId: string | unde
|
|||||||
|
|
||||||
interface AlbumGroupControlsProps {
|
interface AlbumGroupControlsProps {
|
||||||
albumId: string | undefined;
|
albumId: string | undefined;
|
||||||
isGroupHovered: boolean;
|
|
||||||
serverId: string | undefined;
|
serverId: string | undefined;
|
||||||
serverType: ServerType | undefined;
|
serverType: ServerType | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AlbumGroupControls = ({
|
export const AlbumGroupControls = ({ albumId, serverId, serverType }: AlbumGroupControlsProps) => {
|
||||||
albumId,
|
|
||||||
isGroupHovered,
|
|
||||||
serverId,
|
|
||||||
serverType,
|
|
||||||
}: AlbumGroupControlsProps) => {
|
|
||||||
const showRatingsSetting = useShowRatings();
|
const showRatingsSetting = useShowRatings();
|
||||||
const detailQuery = useAlbumGroupAlbum(albumId, serverId);
|
const detailQuery = useAlbumGroupAlbum(albumId, serverId);
|
||||||
const setFavorite = useSetFavorite();
|
const setFavorite = useSetFavorite();
|
||||||
@@ -84,13 +78,13 @@ export const AlbumGroupControls = ({
|
|||||||
return (
|
return (
|
||||||
<div className={styles.controls}>
|
<div className={styles.controls}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
className={album.userFavorite || isGroupHovered ? undefined : styles.hidden}
|
className={styles.favorite}
|
||||||
disabled={isMutatingFavorite}
|
disabled={isMutatingFavorite}
|
||||||
icon="favorite"
|
icon="favorite"
|
||||||
iconProps={{
|
iconProps={{
|
||||||
color: album.userFavorite ? 'primary' : 'muted',
|
color: album.userFavorite ? 'primary' : 'muted',
|
||||||
fill: album.userFavorite ? 'primary' : undefined,
|
fill: album.userFavorite ? 'primary' : undefined,
|
||||||
size: 'md',
|
size: 'xs',
|
||||||
}}
|
}}
|
||||||
onClick={handleFavorite}
|
onClick={handleFavorite}
|
||||||
onDoubleClick={(event) => {
|
onDoubleClick={(event) => {
|
||||||
@@ -98,11 +92,11 @@ export const AlbumGroupControls = ({
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}}
|
}}
|
||||||
size="xs"
|
size="xs"
|
||||||
variant="subtle"
|
variant="transparent"
|
||||||
/>
|
/>
|
||||||
{showRating && (
|
{showRating && (
|
||||||
<Rating
|
<Rating
|
||||||
className={album.userRating || isGroupHovered ? undefined : styles.hidden}
|
className={styles.rating}
|
||||||
onChange={handleRating}
|
onChange={handleRating}
|
||||||
readOnly={isMutatingRating}
|
readOnly={isMutatingRating}
|
||||||
size="xs"
|
size="xs"
|
||||||
|
|||||||
@@ -7,6 +7,11 @@
|
|||||||
padding: 0 var(--theme-spacing-xs);
|
padding: 0 var(--theme-spacing-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.container.vertical {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.image-container {
|
.image-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@@ -50,8 +55,16 @@
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info.enlarged-image {
|
.container.vertical .image-container {
|
||||||
padding-top: var(--theme-spacing-xs);
|
align-self: center;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container.vertical .info {
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.album-title {
|
.album-title {
|
||||||
@@ -74,6 +87,6 @@
|
|||||||
|
|
||||||
.controls-row {
|
.controls-row {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
min-height: 22px;
|
min-height: calc(1.875rem * var(--mantine-scale, 1));
|
||||||
margin-top: var(--theme-spacing-xxs);
|
margin-top: var(--theme-spacing-xs);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
useAlbumGroupImageSize,
|
useAlbumGroupImageSize,
|
||||||
useAlbumGroupItems,
|
useAlbumGroupItems,
|
||||||
useAlbumGroupShowFavoriteRating,
|
useAlbumGroupShowFavoriteRating,
|
||||||
|
useAlbumGroupVerticalLayout,
|
||||||
usePlayButtonBehavior,
|
usePlayButtonBehavior,
|
||||||
} from '/@/renderer/store';
|
} from '/@/renderer/store';
|
||||||
import { Text } from '/@/shared/components/text/text';
|
import { Text } from '/@/shared/components/text/text';
|
||||||
@@ -54,7 +55,8 @@ export const AlbumGroupHeader = ({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const albumGroupItems = useAlbumGroupItems();
|
const albumGroupItems = useAlbumGroupItems();
|
||||||
const showFavoriteRating = useAlbumGroupShowFavoriteRating();
|
const showFavoriteRating = useAlbumGroupShowFavoriteRating();
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
const isVerticalLayout = useAlbumGroupVerticalLayout();
|
||||||
|
const [isImageHovered, setIsImageHovered] = useState(false);
|
||||||
const [resolved, setResolved] = useState<null | { forInfoHeight: number; height: number }>(
|
const [resolved, setResolved] = useState<null | { forInfoHeight: number; height: number }>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
@@ -80,10 +82,12 @@ export const AlbumGroupHeader = ({
|
|||||||
.filter((item) => item.content != null);
|
.filter((item) => item.content != null);
|
||||||
}, [albumGroupItems, metadata, song, t]);
|
}, [albumGroupItems, metadata, song, t]);
|
||||||
|
|
||||||
// The album group spans the combined row height, but when the image is
|
// Horizontal: info floor is max(image, row span) so metadata aligns with the image.
|
||||||
// enlarged the group's last row is grown so the total reaches the img size.
|
// Vertical: no minHeight floor — row-span minHeight made scrollHeight report the full
|
||||||
|
// track span (e.g. 15×40=600) instead of natural text height, then image+info overflowed
|
||||||
|
// the reserved group height and made the virtualizer thrash at the scroll bottom.
|
||||||
const infoHeight =
|
const infoHeight =
|
||||||
groupRowCount !== undefined
|
groupRowCount !== undefined && !isVerticalLayout
|
||||||
? albumImageSize > 0
|
? albumImageSize > 0
|
||||||
? Math.max(albumImageSize, groupRowCount * rowHeight)
|
? Math.max(albumImageSize, groupRowCount * rowHeight)
|
||||||
: groupRowCount * rowHeight
|
: groupRowCount * rowHeight
|
||||||
@@ -101,8 +105,6 @@ export const AlbumGroupHeader = ({
|
|||||||
? {
|
? {
|
||||||
aspectRatio: 'auto',
|
aspectRatio: 'auto',
|
||||||
height: `${albumImageSize}px`,
|
height: `${albumImageSize}px`,
|
||||||
paddingBottom: 'var(--theme-spacing-xs)',
|
|
||||||
paddingTop: 'var(--theme-spacing-xs)',
|
|
||||||
position: 'relative' as const,
|
position: 'relative' as const,
|
||||||
width: `${albumImageSize}px`,
|
width: `${albumImageSize}px`,
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
@@ -120,7 +122,12 @@ export const AlbumGroupHeader = ({
|
|||||||
const resolvedHeight = Math.max(infoHeight ?? 0, contentHeight);
|
const resolvedHeight = Math.max(infoHeight ?? 0, contentHeight);
|
||||||
|
|
||||||
if (infoHeight !== undefined) {
|
if (infoHeight !== undefined) {
|
||||||
setResolved({ forInfoHeight: infoHeight, height: resolvedHeight });
|
setResolved((prev) => {
|
||||||
|
if (prev?.forInfoHeight === infoHeight && prev.height === resolvedHeight) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
return { forInfoHeight: infoHeight, height: resolvedHeight };
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only persist heights that exceed the image/row floor. Equal values
|
// Only persist heights that exceed the image/row floor. Equal values
|
||||||
@@ -145,18 +152,21 @@ export const AlbumGroupHeader = ({
|
|||||||
groupKey,
|
groupKey,
|
||||||
groupRowCount,
|
groupRowCount,
|
||||||
infoHeight,
|
infoHeight,
|
||||||
|
isVerticalLayout,
|
||||||
metadataRows.length,
|
metadataRows.length,
|
||||||
setAlbumGroupContentHeight,
|
setAlbumGroupContentHeight,
|
||||||
|
showFavoriteRating,
|
||||||
storedContentHeight,
|
storedContentHeight,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={clsx(styles.container, isVerticalLayout && styles.vertical)}>
|
||||||
className={styles.container}
|
<div
|
||||||
onMouseEnter={() => setIsHovered(true)}
|
className={styles.imageContainer}
|
||||||
onMouseLeave={() => setIsHovered(false)}
|
onMouseEnter={() => setIsImageHovered(true)}
|
||||||
>
|
onMouseLeave={() => setIsImageHovered(false)}
|
||||||
<div className={styles.imageContainer} style={imageContainerStyle}>
|
style={imageContainerStyle}
|
||||||
|
>
|
||||||
<ItemImage
|
<ItemImage
|
||||||
className={imageColumnStyles.compactImage}
|
className={imageColumnStyles.compactImage}
|
||||||
enableDebounce
|
enableDebounce
|
||||||
@@ -166,7 +176,7 @@ export const AlbumGroupHeader = ({
|
|||||||
src={song?.imageUrl}
|
src={song?.imageUrl}
|
||||||
type="table"
|
type="table"
|
||||||
/>
|
/>
|
||||||
{isHovered && onPlay && (
|
{isImageHovered && onPlay && (
|
||||||
<div className={imageColumnStyles.playButtonOverlay}>
|
<div className={imageColumnStyles.playButtonOverlay}>
|
||||||
<PlayTooltip type={playButtonBehavior}>
|
<PlayTooltip type={playButtonBehavior}>
|
||||||
<PlayButton
|
<PlayButton
|
||||||
@@ -185,7 +195,7 @@ export const AlbumGroupHeader = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={clsx(styles.info, albumImageSize > 0 && styles.enlargedImage)}
|
className={styles.info}
|
||||||
ref={infoRef}
|
ref={infoRef}
|
||||||
style={{ minHeight: resolvedInfoHeight ?? infoHeight }}
|
style={{ minHeight: resolvedInfoHeight ?? infoHeight }}
|
||||||
>
|
>
|
||||||
@@ -213,7 +223,6 @@ export const AlbumGroupHeader = ({
|
|||||||
<div className={styles.controlsRow}>
|
<div className={styles.controlsRow}>
|
||||||
<AlbumGroupControls
|
<AlbumGroupControls
|
||||||
albumId={song?.albumId}
|
albumId={song?.albumId}
|
||||||
isGroupHovered={isHovered}
|
|
||||||
serverId={song?._serverId}
|
serverId={song?._serverId}
|
||||||
serverType={song?._serverType}
|
serverType={song?._serverType}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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).
|
* Keep in sync with album-group-header styles (title line-clamp, metadata xs, controls).
|
||||||
*/
|
*/
|
||||||
export function estimateAlbumGroupContentHeight({
|
export function estimateAlbumGroupContentHeight({
|
||||||
hasEnlargedImage,
|
|
||||||
metadataRowCount,
|
metadataRowCount,
|
||||||
showControls,
|
showControls,
|
||||||
}: {
|
}: {
|
||||||
hasEnlargedImage: boolean;
|
|
||||||
metadataRowCount: number;
|
metadataRowCount: number;
|
||||||
showControls: boolean;
|
showControls: boolean;
|
||||||
}): number {
|
}): number {
|
||||||
const TITLE_LINE_HEIGHT = 20;
|
const TITLE_LINE_HEIGHT = 20;
|
||||||
const TITLE_MAX_LINES = 3;
|
const TITLE_MAX_LINES = 3;
|
||||||
const METADATA_LINE_HEIGHT = 18;
|
const METADATA_LINE_HEIGHT = 18;
|
||||||
const CONTROLS_HEIGHT = 26;
|
const CONTROLS_HEIGHT = 38;
|
||||||
const PADDING_TOP = hasEnlargedImage ? 8 : 0;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
PADDING_TOP +
|
|
||||||
TITLE_LINE_HEIGHT * TITLE_MAX_LINES +
|
TITLE_LINE_HEIGHT * TITLE_MAX_LINES +
|
||||||
Math.max(0, metadataRowCount) * METADATA_LINE_HEIGHT +
|
Math.max(0, metadataRowCount) * METADATA_LINE_HEIGHT +
|
||||||
(showControls ? CONTROLS_HEIGHT : 0)
|
(showControls ? CONTROLS_HEIGHT : 0)
|
||||||
@@ -459,13 +455,23 @@ export function getAlbumGroupRowCount(
|
|||||||
return end - start + 1;
|
return end - start + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const ALBUM_GROUP_STACK_GAP = 12;
|
||||||
|
|
||||||
export function getAlbumGroupSpanHeight(
|
export function getAlbumGroupSpanHeight(
|
||||||
groupRowCount: number,
|
groupRowCount: number,
|
||||||
baseHeight: number,
|
baseHeight: number,
|
||||||
albumGroupImageSize: number,
|
albumGroupImageSize: number,
|
||||||
contentHeight = 0,
|
contentHeight = 0,
|
||||||
|
options?: { isVertical?: boolean },
|
||||||
): number {
|
): number {
|
||||||
const rowSpanHeight = groupRowCount * baseHeight;
|
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 =
|
const imageSpanHeight =
|
||||||
albumGroupImageSize > 0 ? Math.max(albumGroupImageSize, rowSpanHeight) : rowSpanHeight;
|
albumGroupImageSize > 0 ? Math.max(albumGroupImageSize, rowSpanHeight) : rowSpanHeight;
|
||||||
|
|
||||||
@@ -565,9 +571,6 @@ function ClampedCell({
|
|||||||
// standard height and the reserved space below is left empty (uniform
|
// standard height and the reserved space below is left empty (uniform
|
||||||
// background) for the overflowing album image.
|
// background) for the overflowing album image.
|
||||||
function getAlbumGroupClampHeight(props: ItemTableListInnerColumn): null | number {
|
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 (props.type === TableColumn.ALBUM_GROUP) return null;
|
||||||
if (!isAlbumGroupingActive(props.columns)) return null;
|
if (!isAlbumGroupingActive(props.columns)) return null;
|
||||||
|
|
||||||
@@ -583,6 +586,8 @@ function getAlbumGroupClampHeight(props: ItemTableListInnerColumn): null | numbe
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const albumImageSize = props.albumGroupImageSize ?? 0;
|
||||||
|
const isVertical = props.albumGroupVerticalLayout ?? false;
|
||||||
const baseHeight = baseRowHeightForSize(props.size);
|
const baseHeight = baseRowHeightForSize(props.size);
|
||||||
const groupRowCount = getAlbumGroupRowCount(
|
const groupRowCount = getAlbumGroupRowCount(
|
||||||
props.rowIndex,
|
props.rowIndex,
|
||||||
@@ -600,12 +605,18 @@ function getAlbumGroupClampHeight(props: ItemTableListInnerColumn): null | numbe
|
|||||||
const measuredContentHeight = groupHeightKey
|
const measuredContentHeight = groupHeightKey
|
||||||
? props.albumGroupContentHeights?.get(groupHeightKey)
|
? props.albumGroupContentHeights?.get(groupHeightKey)
|
||||||
: undefined;
|
: 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(
|
const totalGroupHeight = getAlbumGroupSpanHeight(
|
||||||
groupRowCount,
|
groupRowCount,
|
||||||
baseHeight,
|
baseHeight,
|
||||||
albumImageSize,
|
albumImageSize,
|
||||||
contentHeight,
|
contentHeight,
|
||||||
|
{ isVertical },
|
||||||
);
|
);
|
||||||
|
|
||||||
// Only clamp when the row was actually grown to fit the image or wrapped text.
|
// Only clamp when the row was actually grown to fit the image or wrapped text.
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ import {
|
|||||||
useAlbumGroupImageSize,
|
useAlbumGroupImageSize,
|
||||||
useAlbumGroupItems,
|
useAlbumGroupItems,
|
||||||
useAlbumGroupShowFavoriteRating,
|
useAlbumGroupShowFavoriteRating,
|
||||||
|
useAlbumGroupVerticalLayout,
|
||||||
usePlayerStore,
|
usePlayerStore,
|
||||||
} from '/@/renderer/store';
|
} from '/@/renderer/store';
|
||||||
import { animationProps } from '/@/shared/components/animations/animation-props';
|
import { animationProps } from '/@/shared/components/animations/animation-props';
|
||||||
@@ -235,6 +236,7 @@ const VirtualizedTableGrid = ({
|
|||||||
}: VirtualizedTableGridProps) => {
|
}: VirtualizedTableGridProps) => {
|
||||||
const { enableHeader, enableRowHoverHighlight, getRowHeight, groups } = tableConfig;
|
const { enableHeader, enableRowHoverHighlight, getRowHeight, groups } = tableConfig;
|
||||||
const albumGroupImageSize = useAlbumGroupImageSize();
|
const albumGroupImageSize = useAlbumGroupImageSize();
|
||||||
|
const albumGroupVerticalLayout = useAlbumGroupVerticalLayout();
|
||||||
const hoverDelegateRef = useRef<HTMLDivElement | null>(null);
|
const hoverDelegateRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
useRowInteractionDelegate({
|
useRowInteractionDelegate({
|
||||||
@@ -396,6 +398,7 @@ const VirtualizedTableGrid = ({
|
|||||||
() => ({
|
() => ({
|
||||||
albumGroupContentHeights,
|
albumGroupContentHeights,
|
||||||
albumGroupImageSize,
|
albumGroupImageSize,
|
||||||
|
albumGroupVerticalLayout,
|
||||||
calculatedColumnWidths,
|
calculatedColumnWidths,
|
||||||
cellPadding: tableConfig.cellPadding,
|
cellPadding: tableConfig.cellPadding,
|
||||||
columns: tableConfig.columns,
|
columns: tableConfig.columns,
|
||||||
@@ -435,6 +438,7 @@ const VirtualizedTableGrid = ({
|
|||||||
[
|
[
|
||||||
albumGroupContentHeights,
|
albumGroupContentHeights,
|
||||||
albumGroupImageSize,
|
albumGroupImageSize,
|
||||||
|
albumGroupVerticalLayout,
|
||||||
calculatedColumnWidths,
|
calculatedColumnWidths,
|
||||||
dataWithGroups,
|
dataWithGroups,
|
||||||
estimatedAlbumGroupContentHeight,
|
estimatedAlbumGroupContentHeight,
|
||||||
@@ -746,8 +750,7 @@ const MemoizedVirtualizedTableGrid = memo(VirtualizedTableGrid, (prevProps, next
|
|||||||
nextProps.calculatedColumnWidths,
|
nextProps.calculatedColumnWidths,
|
||||||
) &&
|
) &&
|
||||||
prevProps.albumGroupContentHeights === nextProps.albumGroupContentHeights &&
|
prevProps.albumGroupContentHeights === nextProps.albumGroupContentHeights &&
|
||||||
prevProps.estimatedAlbumGroupContentHeight ===
|
prevProps.estimatedAlbumGroupContentHeight === nextProps.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 &&
|
||||||
@@ -788,7 +791,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;
|
albumGroupVerticalLayout?: boolean;
|
||||||
calculatedColumnWidths?: number[];
|
calculatedColumnWidths?: number[];
|
||||||
cellPadding?: ItemTableListProps['cellPadding'];
|
cellPadding?: ItemTableListProps['cellPadding'];
|
||||||
columns: ItemTableListColumnConfig[];
|
columns: ItemTableListColumnConfig[];
|
||||||
@@ -805,6 +808,7 @@ export interface TableItemProps {
|
|||||||
enableRowHoverHighlight?: ItemTableListProps['enableRowHoverHighlight'];
|
enableRowHoverHighlight?: ItemTableListProps['enableRowHoverHighlight'];
|
||||||
enableSelection?: ItemTableListProps['enableSelection'];
|
enableSelection?: ItemTableListProps['enableSelection'];
|
||||||
enableVerticalBorders?: ItemTableListProps['enableVerticalBorders'];
|
enableVerticalBorders?: ItemTableListProps['enableVerticalBorders'];
|
||||||
|
estimatedAlbumGroupContentHeight?: number;
|
||||||
getAdjustedRowIndex?: (rowIndex: number) => number;
|
getAdjustedRowIndex?: (rowIndex: number) => number;
|
||||||
getGroupRenderData?: () => unknown[];
|
getGroupRenderData?: () => unknown[];
|
||||||
getRowHeight: (index: number, cellProps: TableItemProps) => number;
|
getRowHeight: (index: number, cellProps: TableItemProps) => number;
|
||||||
@@ -1312,6 +1316,7 @@ const BaseItemTableList = ({
|
|||||||
const albumGroupImageSize = useAlbumGroupImageSize();
|
const albumGroupImageSize = useAlbumGroupImageSize();
|
||||||
const albumGroupItems = useAlbumGroupItems();
|
const albumGroupItems = useAlbumGroupItems();
|
||||||
const albumGroupShowFavoriteRating = useAlbumGroupShowFavoriteRating();
|
const albumGroupShowFavoriteRating = useAlbumGroupShowFavoriteRating();
|
||||||
|
const albumGroupVerticalLayout = useAlbumGroupVerticalLayout();
|
||||||
const albumGroupMetadataRowCount = useMemo(
|
const albumGroupMetadataRowCount = useMemo(
|
||||||
() => albumGroupItems.filter((item) => !item.disabled).length,
|
() => albumGroupItems.filter((item) => !item.disabled).length,
|
||||||
[albumGroupItems],
|
[albumGroupItems],
|
||||||
@@ -1319,11 +1324,10 @@ const BaseItemTableList = ({
|
|||||||
const estimatedAlbumGroupContentHeight = useMemo(
|
const estimatedAlbumGroupContentHeight = useMemo(
|
||||||
() =>
|
() =>
|
||||||
estimateAlbumGroupContentHeight({
|
estimateAlbumGroupContentHeight({
|
||||||
hasEnlargedImage: (albumGroupImageSize || 96) > 0,
|
|
||||||
metadataRowCount: albumGroupMetadataRowCount,
|
metadataRowCount: albumGroupMetadataRowCount,
|
||||||
showControls: albumGroupShowFavoriteRating,
|
showControls: albumGroupShowFavoriteRating,
|
||||||
}),
|
}),
|
||||||
[albumGroupImageSize, albumGroupMetadataRowCount, albumGroupShowFavoriteRating],
|
[albumGroupMetadataRowCount, albumGroupShowFavoriteRating],
|
||||||
);
|
);
|
||||||
const baseItemCount = itemCount ?? data.length;
|
const baseItemCount = itemCount ?? data.length;
|
||||||
const [albumGroupContentHeights, setAlbumGroupContentHeights] = useState(
|
const [albumGroupContentHeights, setAlbumGroupContentHeights] = useState(
|
||||||
@@ -1341,7 +1345,7 @@ const BaseItemTableList = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setAlbumGroupContentHeights(new Map());
|
setAlbumGroupContentHeights(new Map());
|
||||||
}, [baseItemCount]);
|
}, [albumGroupShowFavoriteRating, albumGroupVerticalLayout, baseItemCount]);
|
||||||
|
|
||||||
const totalItemCount = enableHeader ? baseItemCount + 1 : baseItemCount;
|
const totalItemCount = enableHeader ? baseItemCount + 1 : baseItemCount;
|
||||||
const [centerContainerWidth, setCenterContainerWidth] = useState(0);
|
const [centerContainerWidth, setCenterContainerWidth] = useState(0);
|
||||||
@@ -1496,12 +1500,16 @@ const BaseItemTableList = ({
|
|||||||
: undefined;
|
: undefined;
|
||||||
// Prefer measured height when present; otherwise reserve with a stable
|
// Prefer measured height when present; otherwise reserve with a stable
|
||||||
// estimate so newly virtualized groups do not jump after mount measure.
|
// 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(
|
const totalGroupHeight = getAlbumGroupSpanHeight(
|
||||||
groupRowCount,
|
groupRowCount,
|
||||||
baseHeight,
|
baseHeight,
|
||||||
albumGroupImageSize,
|
albumGroupImageSize,
|
||||||
contentHeight,
|
contentHeight,
|
||||||
|
{ isVertical: albumGroupVerticalLayout },
|
||||||
);
|
);
|
||||||
const lastRowHeight = totalGroupHeight - (groupRowCount - 1) * baseHeight;
|
const lastRowHeight = totalGroupHeight - (groupRowCount - 1) * baseHeight;
|
||||||
if (lastRowHeight > baseHeight) {
|
if (lastRowHeight > baseHeight) {
|
||||||
@@ -1514,6 +1522,7 @@ const BaseItemTableList = ({
|
|||||||
},
|
},
|
||||||
[
|
[
|
||||||
albumGroupImageSize,
|
albumGroupImageSize,
|
||||||
|
albumGroupVerticalLayout,
|
||||||
enableHeader,
|
enableHeader,
|
||||||
estimatedAlbumGroupContentHeight,
|
estimatedAlbumGroupContentHeight,
|
||||||
headerHeight,
|
headerHeight,
|
||||||
@@ -1527,6 +1536,7 @@ const BaseItemTableList = ({
|
|||||||
() => ({
|
() => ({
|
||||||
albumGroupContentHeights,
|
albumGroupContentHeights,
|
||||||
albumGroupImageSize,
|
albumGroupImageSize,
|
||||||
|
albumGroupVerticalLayout,
|
||||||
cellPadding,
|
cellPadding,
|
||||||
columns: parsedColumns,
|
columns: parsedColumns,
|
||||||
controls: {} as ItemControls,
|
controls: {} as ItemControls,
|
||||||
@@ -1562,6 +1572,7 @@ const BaseItemTableList = ({
|
|||||||
[
|
[
|
||||||
albumGroupContentHeights,
|
albumGroupContentHeights,
|
||||||
albumGroupImageSize,
|
albumGroupImageSize,
|
||||||
|
albumGroupVerticalLayout,
|
||||||
cellPadding,
|
cellPadding,
|
||||||
dataWithGroups,
|
dataWithGroups,
|
||||||
enableAlternateRowColors,
|
enableAlternateRowColors,
|
||||||
|
|||||||
@@ -78,6 +78,9 @@ export const TableConfig = ({
|
|||||||
const albumGroupShowFavoriteRating = useSettingsStore(
|
const albumGroupShowFavoriteRating = useSettingsStore(
|
||||||
(state) => state.general.albumGroupShowFavoriteRating,
|
(state) => state.general.albumGroupShowFavoriteRating,
|
||||||
);
|
);
|
||||||
|
const albumGroupVerticalLayout = useSettingsStore(
|
||||||
|
(state) => state.general.albumGroupVerticalLayout,
|
||||||
|
);
|
||||||
const imageResTable = useSettingsStore((state) => state.general.imageRes.table);
|
const imageResTable = useSettingsStore((state) => state.general.imageRes.table);
|
||||||
const { setList, setSettings } = useSettingsStoreActions();
|
const { setList, setSettings } = useSettingsStoreActions();
|
||||||
const [albumGroupOpen, setAlbumGroupOpen] = useState(false);
|
const [albumGroupOpen, setAlbumGroupOpen] = useState(false);
|
||||||
@@ -185,6 +188,26 @@ export const TableConfig = ({
|
|||||||
</Text>
|
</Text>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
component: (
|
||||||
|
<ListConfigBooleanControl
|
||||||
|
onChange={(value) =>
|
||||||
|
setSettings({
|
||||||
|
general: {
|
||||||
|
albumGroupVerticalLayout: value,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
value={albumGroupVerticalLayout}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
id: 'albumGroupVerticalLayout',
|
||||||
|
label: (
|
||||||
|
<Text fw={500} pl="md" size="sm">
|
||||||
|
{t('table.config.general.albumGroupVerticalLayout')}
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
},
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
]
|
]
|
||||||
@@ -367,6 +390,7 @@ export const TableConfig = ({
|
|||||||
albumGroupOpen,
|
albumGroupOpen,
|
||||||
albumGroupImageSize,
|
albumGroupImageSize,
|
||||||
albumGroupShowFavoriteRating,
|
albumGroupShowFavoriteRating,
|
||||||
|
albumGroupVerticalLayout,
|
||||||
imageResTable,
|
imageResTable,
|
||||||
setSettings,
|
setSettings,
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -488,6 +488,7 @@ export const GeneralSettingsSchema = z.object({
|
|||||||
albumGroupImageSize: z.number(),
|
albumGroupImageSize: z.number(),
|
||||||
albumGroupItems: z.array(SortableItemSchema(AlbumGroupItemSchema)),
|
albumGroupItems: z.array(SortableItemSchema(AlbumGroupItemSchema)),
|
||||||
albumGroupShowFavoriteRating: z.boolean(),
|
albumGroupShowFavoriteRating: z.boolean(),
|
||||||
|
albumGroupVerticalLayout: z.boolean(),
|
||||||
artistBackground: z.boolean(),
|
artistBackground: z.boolean(),
|
||||||
artistBackgroundBlur: z.number(),
|
artistBackgroundBlur: z.number(),
|
||||||
artistItems: z.array(SortableItemSchema(ArtistItemSchema)),
|
artistItems: z.array(SortableItemSchema(ArtistItemSchema)),
|
||||||
@@ -1211,6 +1212,7 @@ const initialState: SettingsState = {
|
|||||||
albumGroupImageSize: 0,
|
albumGroupImageSize: 0,
|
||||||
albumGroupItems,
|
albumGroupItems,
|
||||||
albumGroupShowFavoriteRating: true,
|
albumGroupShowFavoriteRating: true,
|
||||||
|
albumGroupVerticalLayout: true,
|
||||||
artistBackground: true,
|
artistBackground: true,
|
||||||
artistBackgroundBlur: 3,
|
artistBackgroundBlur: 3,
|
||||||
artistItems,
|
artistItems,
|
||||||
@@ -2727,6 +2729,9 @@ export const useAlbumGroupImageSize = () =>
|
|||||||
export const useAlbumGroupShowFavoriteRating = () =>
|
export const useAlbumGroupShowFavoriteRating = () =>
|
||||||
useSettingsStore((state) => state.general.albumGroupShowFavoriteRating);
|
useSettingsStore((state) => state.general.albumGroupShowFavoriteRating);
|
||||||
|
|
||||||
|
export const useAlbumGroupVerticalLayout = () =>
|
||||||
|
useSettingsStore((state) => state.general.albumGroupVerticalLayout);
|
||||||
|
|
||||||
export const useVolumeWidth = () => useSettingsStore((state) => state.general.volumeWidth, shallow);
|
export const useVolumeWidth = () => useSettingsStore((state) => state.general.volumeWidth, shallow);
|
||||||
|
|
||||||
export const useFollowCurrentSong = () =>
|
export const useFollowCurrentSong = () =>
|
||||||
|
|||||||
Reference in New Issue
Block a user