mirror of
https://github.com/jeffvli/feishin.git
synced 2026-07-05 18:19:56 +02:00
feat: album group has a config and can set the image size (#2153)
* Created a new album group configuration which includes (for now) an option to set the image size of the album group artwork.
This commit is contained in:
@@ -1218,6 +1218,8 @@
|
|||||||
"config": {
|
"config": {
|
||||||
"general": {
|
"general": {
|
||||||
"advancedSettings": "Advanced settings",
|
"advancedSettings": "Advanced settings",
|
||||||
|
"albumGroupConfig": "Album Group configuration",
|
||||||
|
"albumImageSize": "Album image size",
|
||||||
"autoFitColumns": "Auto fit columns",
|
"autoFitColumns": "Auto fit columns",
|
||||||
"autosize": "Autosize",
|
"autosize": "Autosize",
|
||||||
"moveUp": "Move up",
|
"moveUp": "Move up",
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
LONG_PRESS_PLAY_BEHAVIOR,
|
LONG_PRESS_PLAY_BEHAVIOR,
|
||||||
PlayTooltip,
|
PlayTooltip,
|
||||||
} from '/@/renderer/features/shared/components/play-button-group';
|
} from '/@/renderer/features/shared/components/play-button-group';
|
||||||
import { usePlayButtonBehavior } from '/@/renderer/store';
|
import { useAlbumGroupImageSize, usePlayButtonBehavior } from '/@/renderer/store';
|
||||||
import { LibraryItem, Song } from '/@/shared/types/domain-types';
|
import { LibraryItem, Song } from '/@/shared/types/domain-types';
|
||||||
import { Play } from '/@/shared/types/types';
|
import { Play } from '/@/shared/types/types';
|
||||||
|
|
||||||
@@ -29,12 +29,33 @@ export const AlbumGroupHeader = ({
|
|||||||
}: AlbumGroupHeaderProps): ReactElement => {
|
}: AlbumGroupHeaderProps): ReactElement => {
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
const playButtonBehavior = usePlayButtonBehavior();
|
const playButtonBehavior = usePlayButtonBehavior();
|
||||||
|
const albumImageSize = useAlbumGroupImageSize();
|
||||||
const rowHeight = {
|
const rowHeight = {
|
||||||
compact: TableItemSize.COMPACT,
|
compact: TableItemSize.COMPACT,
|
||||||
large: TableItemSize.LARGE,
|
large: TableItemSize.LARGE,
|
||||||
normal: TableItemSize.DEFAULT,
|
normal: TableItemSize.DEFAULT,
|
||||||
}[size];
|
}[size];
|
||||||
const infoHeight = groupRowCount !== undefined ? groupRowCount * rowHeight : undefined;
|
// The album group spans the combined row height, but when the image is
|
||||||
|
// enlarged the group's last row is grown so the total reaches the img size.
|
||||||
|
const infoHeight =
|
||||||
|
groupRowCount !== undefined
|
||||||
|
? albumImageSize > 0
|
||||||
|
? Math.max(albumImageSize, groupRowCount * rowHeight)
|
||||||
|
: groupRowCount * rowHeight
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const imageContainerStyle =
|
||||||
|
albumImageSize > 0
|
||||||
|
? {
|
||||||
|
aspectRatio: 'auto',
|
||||||
|
height: `${albumImageSize}px`,
|
||||||
|
paddingBottom: 'var(--theme-spacing-xs)',
|
||||||
|
paddingTop: 'var(--theme-spacing-xs)',
|
||||||
|
position: 'relative' as const,
|
||||||
|
width: `${albumImageSize}px`,
|
||||||
|
zIndex: 1,
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
@@ -42,6 +63,7 @@ export const AlbumGroupHeader = ({
|
|||||||
className={styles.imageContainer}
|
className={styles.imageContainer}
|
||||||
onMouseEnter={() => setIsHovered(true)}
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
onMouseLeave={() => setIsHovered(false)}
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
|
style={imageContainerStyle}
|
||||||
>
|
>
|
||||||
<ItemImage
|
<ItemImage
|
||||||
className={imageColumnStyles.compactImage}
|
className={imageColumnStyles.compactImage}
|
||||||
|
|||||||
@@ -64,6 +64,12 @@ export const AlbumGroupColumn = (props: ItemTableListInnerColumn) => {
|
|||||||
...(needsBorder
|
...(needsBorder
|
||||||
? { borderBottom: '1px solid var(--theme-colors-border)' }
|
? { borderBottom: '1px solid var(--theme-colors-border)' }
|
||||||
: {}),
|
: {}),
|
||||||
|
// When the cover is enlarged it overflows down from the
|
||||||
|
// group's first row into these cells; let hover/click pass
|
||||||
|
// through to reach it.
|
||||||
|
...((props.albumGroupImageSize ?? 0) > 0
|
||||||
|
? { pointerEvents: 'none' as const }
|
||||||
|
: {}),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -31,6 +31,11 @@
|
|||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.container.no-vertical-padding {
|
||||||
|
padding-top: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.container.center {
|
.container.center {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
@@ -57,7 +57,10 @@ import { TitleCombinedColumn } from '/@/renderer/components/item-list/item-table
|
|||||||
import { TrackNumberColumn } from '/@/renderer/components/item-list/item-table-list/columns/track-number-column';
|
import { TrackNumberColumn } from '/@/renderer/components/item-list/item-table-list/columns/track-number-column';
|
||||||
import { YearColumn } from '/@/renderer/components/item-list/item-table-list/columns/year-column';
|
import { YearColumn } from '/@/renderer/components/item-list/item-table-list/columns/year-column';
|
||||||
import { useItemDragDropState } from '/@/renderer/components/item-list/item-table-list/hooks/use-item-drag-drop-state';
|
import { useItemDragDropState } from '/@/renderer/components/item-list/item-table-list/hooks/use-item-drag-drop-state';
|
||||||
import { TableItemProps } from '/@/renderer/components/item-list/item-table-list/item-table-list';
|
import {
|
||||||
|
TableItemProps,
|
||||||
|
TableItemSize,
|
||||||
|
} from '/@/renderer/components/item-list/item-table-list/item-table-list';
|
||||||
import { useItemTableListColumnResizeLive } from '/@/renderer/components/item-list/item-table-list/item-table-list-context';
|
import { useItemTableListColumnResizeLive } from '/@/renderer/components/item-list/item-table-list/item-table-list-context';
|
||||||
import { ItemControls, ItemListItem } from '/@/renderer/components/item-list/types';
|
import { ItemControls, ItemListItem } from '/@/renderer/components/item-list/types';
|
||||||
import { Flex } from '/@/shared/components/flex/flex';
|
import { Flex } from '/@/shared/components/flex/flex';
|
||||||
@@ -381,6 +384,36 @@ 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];
|
||||||
|
|
||||||
|
// Counts how many consecutive rows belong to the same album group as `rowIndex`.
|
||||||
|
export function getAlbumGroupRowCount(
|
||||||
|
rowIndex: number,
|
||||||
|
getRowItem: ((index: number) => unknown) | undefined,
|
||||||
|
enableHeader: boolean | undefined,
|
||||||
|
dataLength: number,
|
||||||
|
): number {
|
||||||
|
const item = getRowItem?.(rowIndex) as null | undefined | { album?: string };
|
||||||
|
if (!item?.album) return 1;
|
||||||
|
|
||||||
|
const firstDataRow = enableHeader ? 1 : 0;
|
||||||
|
const maxRow = enableHeader ? dataLength + 1 : dataLength;
|
||||||
|
|
||||||
|
let start = rowIndex;
|
||||||
|
while (start > firstDataRow) {
|
||||||
|
const prevItem = getRowItem?.(start - 1) as null | undefined | { album?: string };
|
||||||
|
if (!prevItem || prevItem.album !== item.album) break;
|
||||||
|
start--;
|
||||||
|
}
|
||||||
|
|
||||||
|
let end = rowIndex;
|
||||||
|
while (end + 1 < maxRow) {
|
||||||
|
const nextItem = getRowItem?.(end + 1) as null | undefined | { album?: string };
|
||||||
|
if (!nextItem || nextItem.album !== item.album) break;
|
||||||
|
end++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return end - start + 1;
|
||||||
|
}
|
||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
@@ -402,6 +435,106 @@ export function isLastInAlbumGroup(
|
|||||||
return !nextItem || nextItem.album !== item.album;
|
return !nextItem || nextItem.album !== item.album;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function baseRowHeightForSize(size: ItemTableListColumn['size']): number {
|
||||||
|
if (size === 'compact') return TableItemSize.COMPACT;
|
||||||
|
if (size === 'large') return TableItemSize.LARGE;
|
||||||
|
return TableItemSize.DEFAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wraps a clamped cell with the spacer that fills the reserved (grown) height
|
||||||
|
// below it. The spacer carries the group's bottom/right borders so they align
|
||||||
|
// across all columns.
|
||||||
|
function ClampedCell({
|
||||||
|
cell,
|
||||||
|
clampHeight,
|
||||||
|
outerStyle,
|
||||||
|
showHorizontalBorder,
|
||||||
|
showVerticalBorder,
|
||||||
|
}: {
|
||||||
|
cell: ReactElement;
|
||||||
|
clampHeight: null | number;
|
||||||
|
outerStyle?: CSSProperties;
|
||||||
|
showHorizontalBorder: boolean;
|
||||||
|
showVerticalBorder: boolean;
|
||||||
|
}): ReactElement {
|
||||||
|
const grownHeight = typeof outerStyle?.height === 'number' ? outerStyle.height : 0;
|
||||||
|
const spacerHeight = clampHeight !== null ? grownHeight - clampHeight : 0;
|
||||||
|
|
||||||
|
if (clampHeight === null || spacerHeight <= 0) return cell;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={outerStyle}>
|
||||||
|
{cell}
|
||||||
|
<div
|
||||||
|
aria-hidden
|
||||||
|
style={{
|
||||||
|
borderBottom: showHorizontalBorder
|
||||||
|
? '1px solid var(--theme-colors-border)'
|
||||||
|
: undefined,
|
||||||
|
borderRight: showVerticalBorder
|
||||||
|
? '1px solid var(--theme-colors-border)'
|
||||||
|
: undefined,
|
||||||
|
height: spacerHeight,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// When an enlarged album image extends past the album group's combined row
|
||||||
|
// height, the last row of the group is grown (in getRowHeight) to reserve the
|
||||||
|
// leftover space. This returns the standard (un-grown) height to clamp that
|
||||||
|
// row's non-album cells to, so the track content + hover/selection stay at
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
const isDataRow = props.enableHeader ? props.rowIndex > 0 : true;
|
||||||
|
if (!isDataRow) return null;
|
||||||
|
|
||||||
|
const item = props.getRowItem?.(props.rowIndex) as null | undefined | { album?: string };
|
||||||
|
if (!item?.album) return null;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!isLastInAlbumGroup(props.rowIndex, props.getRowItem, props.enableHeader, props.data.length)
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseHeight = baseRowHeightForSize(props.size);
|
||||||
|
const groupRowCount = getAlbumGroupRowCount(
|
||||||
|
props.rowIndex,
|
||||||
|
props.getRowItem,
|
||||||
|
props.enableHeader,
|
||||||
|
props.data.length,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Only clamp when the row was actually grown to fit the image.
|
||||||
|
if (albumImageSize <= groupRowCount * baseHeight) return null;
|
||||||
|
|
||||||
|
return baseHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showHorizontalBorderFor(props: ItemTableListInnerColumn, isLastRow: boolean): boolean {
|
||||||
|
if (!props.enableHorizontalBorders || !props.enableHeader || props.rowIndex <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (isAlbumGroupingActive(props.columns)) {
|
||||||
|
return isLastInAlbumGroup(
|
||||||
|
props.rowIndex,
|
||||||
|
props.getRowItem,
|
||||||
|
!!props.enableHeader,
|
||||||
|
props.data.length,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return props.rowIndex === 1 || !isLastRow;
|
||||||
|
}
|
||||||
|
|
||||||
export const TableColumnTextContainer = (
|
export const TableColumnTextContainer = (
|
||||||
props: ItemTableListColumn & {
|
props: ItemTableListColumn & {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -425,6 +558,7 @@ export const TableColumnTextContainer = (
|
|||||||
? props.internalState.extractRowId(item)
|
? props.internalState.extractRowId(item)
|
||||||
: undefined;
|
: undefined;
|
||||||
const isSelected = useItemSelectionState(props.internalState, itemRowId || undefined);
|
const isSelected = useItemSelectionState(props.internalState, itemRowId || undefined);
|
||||||
|
const clampHeight = getAlbumGroupClampHeight(props);
|
||||||
|
|
||||||
const isDragging = props.isDragging ?? false;
|
const isDragging = props.isDragging ?? false;
|
||||||
const mergedRef = useMergedRef(containerRef, props.dragRef ?? null);
|
const mergedRef = useMergedRef(containerRef, props.dragRef ?? null);
|
||||||
@@ -507,7 +641,10 @@ export const TableColumnTextContainer = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
const showHorizontalBorder = showHorizontalBorderFor(props, isLastRow);
|
||||||
|
const showVerticalBorder = !!props.enableVerticalBorders && !isLastColumn;
|
||||||
|
|
||||||
|
const cell = (
|
||||||
<div
|
<div
|
||||||
className={clsx(styles.container, props.containerClassName, {
|
className={clsx(styles.container, props.containerClassName, {
|
||||||
[styles.alternateRowEven]:
|
[styles.alternateRowEven]:
|
||||||
@@ -529,25 +666,16 @@ export const TableColumnTextContainer = (
|
|||||||
[styles.right]: props.columns[props.columnIndex].align === 'end',
|
[styles.right]: props.columns[props.columnIndex].align === 'end',
|
||||||
[styles.rowHoverHighlightEnabled]: isDataRow && props.enableRowHoverHighlight,
|
[styles.rowHoverHighlightEnabled]: isDataRow && props.enableRowHoverHighlight,
|
||||||
[styles.rowSelected]: isDataRow && isSelected,
|
[styles.rowSelected]: isDataRow && isSelected,
|
||||||
[styles.withHorizontalBorder]:
|
// When clamped, the bottom border is drawn on the spacer below
|
||||||
props.enableHorizontalBorders &&
|
// instead.
|
||||||
props.enableHeader &&
|
[styles.withHorizontalBorder]: showHorizontalBorder && clampHeight === null,
|
||||||
props.rowIndex > 0 &&
|
[styles.withVerticalBorder]: showVerticalBorder,
|
||||||
(isAlbumGroupingActive(props.columns)
|
|
||||||
? isLastInAlbumGroup(
|
|
||||||
props.rowIndex,
|
|
||||||
props.getRowItem,
|
|
||||||
!!props.enableHeader,
|
|
||||||
props.data.length,
|
|
||||||
)
|
|
||||||
: props.rowIndex === 1 || !isLastRow),
|
|
||||||
[styles.withVerticalBorder]: props.enableVerticalBorders && !isLastColumn,
|
|
||||||
})}
|
})}
|
||||||
data-row-index={isDataRow ? `${props.tableId}-${props.rowIndex}` : undefined}
|
data-row-index={isDataRow ? `${props.tableId}-${props.rowIndex}` : undefined}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
onContextMenu={handleContextMenu}
|
onContextMenu={handleContextMenu}
|
||||||
ref={mergedRef}
|
ref={mergedRef}
|
||||||
style={props.style}
|
style={clampHeight !== null ? { height: clampHeight } : props.style}
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
className={clsx(styles.content, props.className, {
|
className={clsx(styles.content, props.className, {
|
||||||
@@ -561,6 +689,16 @@ export const TableColumnTextContainer = (
|
|||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ClampedCell
|
||||||
|
cell={cell}
|
||||||
|
clampHeight={clampHeight}
|
||||||
|
outerStyle={props.style}
|
||||||
|
showHorizontalBorder={showHorizontalBorder}
|
||||||
|
showVerticalBorder={showVerticalBorder}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TableColumnContainer = (
|
export const TableColumnContainer = (
|
||||||
@@ -586,6 +724,7 @@ export const TableColumnContainer = (
|
|||||||
? props.internalState.extractRowId(item)
|
? props.internalState.extractRowId(item)
|
||||||
: undefined;
|
: undefined;
|
||||||
const isSelected = useItemSelectionState(props.internalState, itemRowId || undefined);
|
const isSelected = useItemSelectionState(props.internalState, itemRowId || undefined);
|
||||||
|
const clampHeight = getAlbumGroupClampHeight(props);
|
||||||
|
|
||||||
const isDragging = props.isDragging ?? false;
|
const isDragging = props.isDragging ?? false;
|
||||||
const mergedRef = useMergedRef(containerRef, props.dragRef ?? null);
|
const mergedRef = useMergedRef(containerRef, props.dragRef ?? null);
|
||||||
@@ -668,7 +807,10 @@ export const TableColumnContainer = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
const showHorizontalBorder = showHorizontalBorderFor(props, isLastRow);
|
||||||
|
const showVerticalBorder = !!props.enableVerticalBorders && !isLastColumn;
|
||||||
|
|
||||||
|
const cell = (
|
||||||
<div
|
<div
|
||||||
className={clsx(styles.container, props.className, {
|
className={clsx(styles.container, props.className, {
|
||||||
[styles.alternateRowEven]:
|
[styles.alternateRowEven]:
|
||||||
@@ -682,6 +824,8 @@ export const TableColumnContainer = (
|
|||||||
[styles.large]: props.size === 'large',
|
[styles.large]: props.size === 'large',
|
||||||
[styles.left]: props.columns[props.columnIndex].align === 'start',
|
[styles.left]: props.columns[props.columnIndex].align === 'start',
|
||||||
[styles.noHorizontalPadding]: isNoHorizontalPaddingColumn(props.type),
|
[styles.noHorizontalPadding]: isNoHorizontalPaddingColumn(props.type),
|
||||||
|
[styles.noVerticalPadding]:
|
||||||
|
props.type === TableColumn.ALBUM_GROUP && (props.albumGroupImageSize ?? 0) > 0,
|
||||||
[styles.paddingLg]: props.cellPadding === 'lg',
|
[styles.paddingLg]: props.cellPadding === 'lg',
|
||||||
[styles.paddingMd]: props.cellPadding === 'md',
|
[styles.paddingMd]: props.cellPadding === 'md',
|
||||||
[styles.paddingSm]: props.cellPadding === 'sm',
|
[styles.paddingSm]: props.cellPadding === 'sm',
|
||||||
@@ -694,29 +838,33 @@ export const TableColumnContainer = (
|
|||||||
props.type !== TableColumn.ALBUM_GROUP,
|
props.type !== TableColumn.ALBUM_GROUP,
|
||||||
[styles.rowSelected]:
|
[styles.rowSelected]:
|
||||||
isDataRow && isSelected && props.type !== TableColumn.ALBUM_GROUP,
|
isDataRow && isSelected && props.type !== TableColumn.ALBUM_GROUP,
|
||||||
[styles.withHorizontalBorder]:
|
// When clamped, the bottom border is drawn on the spacer below instead.
|
||||||
props.enableHorizontalBorders &&
|
[styles.withHorizontalBorder]: showHorizontalBorder && clampHeight === null,
|
||||||
props.enableHeader &&
|
[styles.withVerticalBorder]: showVerticalBorder,
|
||||||
props.rowIndex > 0 &&
|
|
||||||
(isAlbumGroupingActive(props.columns)
|
|
||||||
? isLastInAlbumGroup(
|
|
||||||
props.rowIndex,
|
|
||||||
props.getRowItem,
|
|
||||||
!!props.enableHeader,
|
|
||||||
props.data.length,
|
|
||||||
)
|
|
||||||
: props.rowIndex === 1 || !isLastRow),
|
|
||||||
[styles.withVerticalBorder]: props.enableVerticalBorders && !isLastColumn,
|
|
||||||
})}
|
})}
|
||||||
data-row-index={isDataRow ? `${props.tableId}-${props.rowIndex}` : undefined}
|
data-row-index={isDataRow ? `${props.tableId}-${props.rowIndex}` : undefined}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
onContextMenu={handleContextMenu}
|
onContextMenu={handleContextMenu}
|
||||||
ref={mergedRef}
|
ref={mergedRef}
|
||||||
style={{ ...props.containerStyle, ...props.style }}
|
style={
|
||||||
|
clampHeight !== null
|
||||||
|
? { ...props.containerStyle, height: clampHeight }
|
||||||
|
: { ...props.containerStyle, ...props.style }
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ClampedCell
|
||||||
|
cell={cell}
|
||||||
|
clampHeight={clampHeight}
|
||||||
|
outerStyle={props.style}
|
||||||
|
showHorizontalBorder={showHorizontalBorder}
|
||||||
|
showVerticalBorder={showVerticalBorder}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
interface ColumnResizeHandleProps {
|
interface ColumnResizeHandleProps {
|
||||||
|
|||||||
@@ -44,7 +44,11 @@ import { useTableKeyboardNavigation } from '/@/renderer/components/item-list/ite
|
|||||||
import { useTablePaneSync } from '/@/renderer/components/item-list/item-table-list/hooks/use-table-pane-sync';
|
import { useTablePaneSync } from '/@/renderer/components/item-list/item-table-list/hooks/use-table-pane-sync';
|
||||||
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 { ItemTableListColumn } from '/@/renderer/components/item-list/item-table-list/item-table-list-column';
|
import {
|
||||||
|
getAlbumGroupRowCount,
|
||||||
|
isLastInAlbumGroup,
|
||||||
|
ItemTableListColumn,
|
||||||
|
} from '/@/renderer/components/item-list/item-table-list/item-table-list-column';
|
||||||
import {
|
import {
|
||||||
ItemTableListColumnResizeLiveProvider,
|
ItemTableListColumnResizeLiveProvider,
|
||||||
type ItemTableListConfig,
|
type ItemTableListConfig,
|
||||||
@@ -66,7 +70,7 @@ import {
|
|||||||
ItemTableListColumnConfig,
|
ItemTableListColumnConfig,
|
||||||
} from '/@/renderer/components/item-list/types';
|
} from '/@/renderer/components/item-list/types';
|
||||||
import { PlayerContext, usePlayer } from '/@/renderer/features/player/context/player-context';
|
import { PlayerContext, usePlayer } from '/@/renderer/features/player/context/player-context';
|
||||||
import { usePlayerStore } from '/@/renderer/store';
|
import { useAlbumGroupImageSize, usePlayerStore } from '/@/renderer/store';
|
||||||
import { animationProps } from '/@/shared/components/animations/animation-props';
|
import { animationProps } from '/@/shared/components/animations/animation-props';
|
||||||
import { useFocusWithin } from '/@/shared/hooks/use-focus-within';
|
import { useFocusWithin } from '/@/shared/hooks/use-focus-within';
|
||||||
import { useMergedRef } from '/@/shared/hooks/use-merged-ref';
|
import { useMergedRef } from '/@/shared/hooks/use-merged-ref';
|
||||||
@@ -215,6 +219,7 @@ const VirtualizedTableGrid = ({
|
|||||||
totalRowCount,
|
totalRowCount,
|
||||||
}: VirtualizedTableGridProps) => {
|
}: VirtualizedTableGridProps) => {
|
||||||
const { enableHeader, enableRowHoverHighlight, getRowHeight, groups } = tableConfig;
|
const { enableHeader, enableRowHoverHighlight, getRowHeight, groups } = tableConfig;
|
||||||
|
const albumGroupImageSize = useAlbumGroupImageSize();
|
||||||
const hoverDelegateRef = useRef<HTMLDivElement | null>(null);
|
const hoverDelegateRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
useRowInteractionDelegate({
|
useRowInteractionDelegate({
|
||||||
@@ -403,6 +408,7 @@ const VirtualizedTableGrid = ({
|
|||||||
|
|
||||||
const itemProps: TableItemProps = useMemo(
|
const itemProps: TableItemProps = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
|
albumGroupImageSize,
|
||||||
cellPadding: tableConfig.cellPadding,
|
cellPadding: tableConfig.cellPadding,
|
||||||
columns: tableConfig.columns,
|
columns: tableConfig.columns,
|
||||||
controls: tableConfig.controls,
|
controls: tableConfig.controls,
|
||||||
@@ -427,7 +433,7 @@ const VirtualizedTableGrid = ({
|
|||||||
tableId: tableConfig.tableId,
|
tableId: tableConfig.tableId,
|
||||||
...gridOnlyProps,
|
...gridOnlyProps,
|
||||||
}),
|
}),
|
||||||
[gridOnlyProps, tableConfig],
|
[albumGroupImageSize, gridOnlyProps, tableConfig],
|
||||||
);
|
);
|
||||||
|
|
||||||
const pinnedLeftGridMinWidthPx = useMemo(() => {
|
const pinnedLeftGridMinWidthPx = useMemo(() => {
|
||||||
@@ -760,6 +766,7 @@ export interface TableGroupHeader {
|
|||||||
|
|
||||||
export interface TableItemProps {
|
export interface TableItemProps {
|
||||||
adjustedRowIndexMap?: Map<number, number>;
|
adjustedRowIndexMap?: Map<number, number>;
|
||||||
|
albumGroupImageSize?: number;
|
||||||
calculatedColumnWidths?: number[];
|
calculatedColumnWidths?: number[];
|
||||||
cellPadding?: ItemTableListProps['cellPadding'];
|
cellPadding?: ItemTableListProps['cellPadding'];
|
||||||
columns: ItemTableListColumnConfig[];
|
columns: ItemTableListColumnConfig[];
|
||||||
@@ -1275,6 +1282,7 @@ const BaseItemTableList = ({
|
|||||||
}: ItemTableListProps) => {
|
}: ItemTableListProps) => {
|
||||||
const { playlistId: routePlaylistId } = useParams() as { playlistId?: string };
|
const { playlistId: routePlaylistId } = useParams() as { playlistId?: string };
|
||||||
const tableId = useId();
|
const tableId = useId();
|
||||||
|
const albumGroupImageSize = useAlbumGroupImageSize();
|
||||||
const baseItemCount = itemCount ?? data.length;
|
const baseItemCount = itemCount ?? data.length;
|
||||||
const totalItemCount = enableHeader ? baseItemCount + 1 : baseItemCount;
|
const totalItemCount = enableHeader ? baseItemCount + 1 : baseItemCount;
|
||||||
const [centerContainerWidth, setCenterContainerWidth] = useState(0);
|
const [centerContainerWidth, setCenterContainerWidth] = useState(0);
|
||||||
@@ -1434,9 +1442,38 @@ const BaseItemTableList = ({
|
|||||||
return headerHeight;
|
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.
|
||||||
|
if (
|
||||||
|
albumGroupImageSize > baseHeight &&
|
||||||
|
cellProps?.hasAlbumGroupColumn &&
|
||||||
|
isLastInAlbumGroup(
|
||||||
|
index,
|
||||||
|
cellProps.getRowItem,
|
||||||
|
cellProps.enableHeader,
|
||||||
|
cellProps.data.length,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
const item = cellProps.getRowItem?.(index) as null | undefined | { album?: string };
|
||||||
|
if (item?.album) {
|
||||||
|
const groupRowCount = getAlbumGroupRowCount(
|
||||||
|
index,
|
||||||
|
cellProps.getRowItem,
|
||||||
|
cellProps.enableHeader,
|
||||||
|
cellProps.data.length,
|
||||||
|
);
|
||||||
|
const lastRowHeight = albumGroupImageSize - (groupRowCount - 1) * baseHeight;
|
||||||
|
if (lastRowHeight > baseHeight) {
|
||||||
|
return lastRowHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return baseHeight;
|
return baseHeight;
|
||||||
},
|
},
|
||||||
[enableHeader, headerHeight, rowHeight, pinnedRowCount, size],
|
[albumGroupImageSize, enableHeader, headerHeight, rowHeight, pinnedRowCount, size],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Create a wrapper for getRowHeight that doesn't require cellProps (for sticky group rows hook)
|
// Create a wrapper for getRowHeight that doesn't require cellProps (for sticky group rows hook)
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import {
|
|||||||
} from '/@/renderer/store';
|
} from '/@/renderer/store';
|
||||||
import { ActionIcon, ActionIconGroup } from '/@/shared/components/action-icon/action-icon';
|
import { ActionIcon, ActionIconGroup } from '/@/shared/components/action-icon/action-icon';
|
||||||
import { Badge } from '/@/shared/components/badge/badge';
|
import { Badge } from '/@/shared/components/badge/badge';
|
||||||
|
import { Button } from '/@/shared/components/button/button';
|
||||||
import { Checkbox } from '/@/shared/components/checkbox/checkbox';
|
import { Checkbox } from '/@/shared/components/checkbox/checkbox';
|
||||||
import { Divider } from '/@/shared/components/divider/divider';
|
import { Divider } from '/@/shared/components/divider/divider';
|
||||||
import { Group } from '/@/shared/components/group/group';
|
import { Group } from '/@/shared/components/group/group';
|
||||||
@@ -41,7 +42,7 @@ import { Text } from '/@/shared/components/text/text';
|
|||||||
import { Tooltip } from '/@/shared/components/tooltip/tooltip';
|
import { Tooltip } from '/@/shared/components/tooltip/tooltip';
|
||||||
import { useDebouncedState } from '/@/shared/hooks/use-debounced-state';
|
import { useDebouncedState } from '/@/shared/hooks/use-debounced-state';
|
||||||
import { dndUtils, DragData, DragOperation, DragTarget } from '/@/shared/types/drag-and-drop';
|
import { dndUtils, DragData, DragOperation, DragTarget } from '/@/shared/types/drag-and-drop';
|
||||||
import { ItemListKey, ListPaginationType } from '/@/shared/types/types';
|
import { ItemListKey, ListPaginationType, TableColumn } from '/@/shared/types/types';
|
||||||
|
|
||||||
interface TableConfigProps {
|
interface TableConfigProps {
|
||||||
enablePinColumnButtons?: boolean;
|
enablePinColumnButtons?: boolean;
|
||||||
@@ -72,10 +73,18 @@ export const TableConfig = ({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const list = useSettingsStore((state) => state.lists[listKey]) as ItemListSettings;
|
const list = useSettingsStore((state) => state.lists[listKey]) as ItemListSettings;
|
||||||
const { setList } = useSettingsStoreActions();
|
const albumGroupImageSize = useSettingsStore((state) => state.general.albumGroupImageSize);
|
||||||
|
const imageResTable = useSettingsStore((state) => state.general.imageRes.table);
|
||||||
|
const { setList, setSettings } = useSettingsStoreActions();
|
||||||
|
const [albumGroupOpen, setAlbumGroupOpen] = useState(false);
|
||||||
|
|
||||||
const table = tableKey === 'detail' ? (list?.detail ?? list?.table) : list?.table;
|
const table = tableKey === 'detail' ? (list?.detail ?? list?.table) : list?.table;
|
||||||
|
|
||||||
|
const hasAlbumGroupColumn = useMemo(
|
||||||
|
() => table.columns.some((column) => column.id === TableColumn.ALBUM_GROUP),
|
||||||
|
[table.columns],
|
||||||
|
);
|
||||||
|
|
||||||
const setTableUpdate = useCallback(
|
const setTableUpdate = useCallback(
|
||||||
(patch: Partial<DataTableProps>) => {
|
(patch: Partial<DataTableProps>) => {
|
||||||
if (tableKey === 'detail') {
|
if (tableKey === 'detail') {
|
||||||
@@ -90,6 +99,73 @@ export const TableConfig = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const advancedSettings = useMemo(() => {
|
const advancedSettings = useMemo(() => {
|
||||||
|
const albumGroupOptions =
|
||||||
|
hasAlbumGroupColumn && tableKey === 'main'
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
component: (
|
||||||
|
<Group justify="flex-end" w="100%">
|
||||||
|
<Button
|
||||||
|
onClick={() => setAlbumGroupOpen((prev) => !prev)}
|
||||||
|
size="compact-md"
|
||||||
|
variant={albumGroupOpen ? 'subtle' : 'filled'}
|
||||||
|
>
|
||||||
|
{t(albumGroupOpen ? 'common.close' : 'common.edit')}
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
),
|
||||||
|
id: 'albumGroupConfig',
|
||||||
|
label: t('table.config.general.albumGroupConfig'),
|
||||||
|
},
|
||||||
|
...(albumGroupOpen
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
component: (
|
||||||
|
<Group justify="flex-end" w="100%">
|
||||||
|
<NumberInput
|
||||||
|
max={2000}
|
||||||
|
min={0}
|
||||||
|
onChange={(value) => {
|
||||||
|
const size = Math.max(
|
||||||
|
0,
|
||||||
|
Math.min(
|
||||||
|
2000,
|
||||||
|
typeof value === 'number' ? value : 0,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
setSettings({
|
||||||
|
general: {
|
||||||
|
albumGroupImageSize: size,
|
||||||
|
// Source table art must be at least as
|
||||||
|
// large as the displayed album image.
|
||||||
|
...(size >= imageResTable
|
||||||
|
? { imageRes: { table: size } }
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
rightSection={
|
||||||
|
<Text isMuted isNoSelect pr="lg" size="sm">
|
||||||
|
px
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
value={albumGroupImageSize}
|
||||||
|
width={90}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
),
|
||||||
|
id: 'albumImageSize',
|
||||||
|
label: (
|
||||||
|
<Text pl="md">
|
||||||
|
{t('table.config.general.albumImageSize')}
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
]
|
||||||
|
: [];
|
||||||
|
|
||||||
const allOptions = [
|
const allOptions = [
|
||||||
{
|
{
|
||||||
component: (
|
component: (
|
||||||
@@ -238,6 +314,7 @@ export const TableConfig = ({
|
|||||||
id: 'autoFitColumns',
|
id: 'autoFitColumns',
|
||||||
label: t('table.config.general.autoFitColumns'),
|
label: t('table.config.general.autoFitColumns'),
|
||||||
},
|
},
|
||||||
|
...albumGroupOptions,
|
||||||
...(extraOptions || []),
|
...(extraOptions || []),
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -262,6 +339,11 @@ export const TableConfig = ({
|
|||||||
listKey,
|
listKey,
|
||||||
setTableUpdate,
|
setTableUpdate,
|
||||||
optionsConfig,
|
optionsConfig,
|
||||||
|
hasAlbumGroupColumn,
|
||||||
|
albumGroupOpen,
|
||||||
|
albumGroupImageSize,
|
||||||
|
imageResTable,
|
||||||
|
setSettings,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -474,6 +474,7 @@ export const GeneralSettingsSchema = z.object({
|
|||||||
),
|
),
|
||||||
albumBackground: z.boolean(),
|
albumBackground: z.boolean(),
|
||||||
albumBackgroundBlur: z.number(),
|
albumBackgroundBlur: z.number(),
|
||||||
|
albumGroupImageSize: z.number(),
|
||||||
artistBackground: z.boolean(),
|
artistBackground: z.boolean(),
|
||||||
artistBackgroundBlur: z.number(),
|
artistBackgroundBlur: z.number(),
|
||||||
artistItems: z.array(SortableItemSchema(ArtistItemSchema)),
|
artistItems: z.array(SortableItemSchema(ArtistItemSchema)),
|
||||||
@@ -1166,6 +1167,7 @@ const initialState: SettingsState = {
|
|||||||
accent: 'rgb(53, 116, 252)',
|
accent: 'rgb(53, 116, 252)',
|
||||||
albumBackground: false,
|
albumBackground: false,
|
||||||
albumBackgroundBlur: 3,
|
albumBackgroundBlur: 3,
|
||||||
|
albumGroupImageSize: 0,
|
||||||
artistBackground: true,
|
artistBackground: true,
|
||||||
artistBackgroundBlur: 3,
|
artistBackgroundBlur: 3,
|
||||||
artistItems,
|
artistItems,
|
||||||
@@ -2645,6 +2647,9 @@ export const useSkipButtons = () => useSettingsStore((state) => state.general.sk
|
|||||||
|
|
||||||
export const useImageRes = () => useSettingsStore((state) => state.general.imageRes, shallow);
|
export const useImageRes = () => useSettingsStore((state) => state.general.imageRes, shallow);
|
||||||
|
|
||||||
|
export const useAlbumGroupImageSize = () =>
|
||||||
|
useSettingsStore((state) => state.general.albumGroupImageSize);
|
||||||
|
|
||||||
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