Add album grouping column (#1722)

* Add album grouping column

---------

Co-authored-by: jeffvli <jeffvictorli@gmail.com>
This commit is contained in:
Norman
2026-02-26 20:34:55 -08:00
committed by GitHub
parent 4918b412b2
commit eb8913479b
18 changed files with 467 additions and 120 deletions
Executable → Regular
+1
View File
@@ -1131,6 +1131,7 @@
"label": {
"actions": "$t(common.action, {\"count\": 2})",
"album": "$t(entity.album, {\"count\": 1})",
"albumGroup": "album group",
"albumCount": "$t(entity.album, {\"count\": 2})",
"albumArtist": "$t(entity.albumArtist, {\"count\": 1})",
"artist": "$t(entity.artist, {\"count\": 1})",
@@ -49,6 +49,7 @@ const getRowIdFromTableColumn = (tableColumn: TableColumn): null | string => {
[TableColumn.ALBUM]: 'album',
[TableColumn.ALBUM_ARTIST]: 'albumArtists',
[TableColumn.ALBUM_COUNT]: 'albumCount',
[TableColumn.ALBUM_GROUP]: null,
[TableColumn.ARTIST]: 'artists',
[TableColumn.BIOGRAPHY]: null,
[TableColumn.BIT_DEPTH]: 'bitDepth',
@@ -0,0 +1,33 @@
.container {
display: flex;
gap: var(--theme-spacing-sm);
width: 100%;
height: 100%;
padding: 0 var(--theme-spacing-xs);
}
.image-container {
position: relative;
box-sizing: border-box;
flex-shrink: 0;
height: 100%;
aspect-ratio: 1;
padding-top: calc(var(--theme-spacing-xs) * 0.5);
}
.info {
display: flex;
flex-direction: column;
min-width: 0;
padding: 0;
overflow: hidden;
}
.album-name {
font-size: var(--theme-font-size-sm);
}
.artist-name {
font-size: var(--theme-font-size-xs);
opacity: 0.7;
}
@@ -0,0 +1,79 @@
import { ReactElement, useState } from 'react';
import imageColumnStyles from '../item-detail-list/columns/image-column.module.css';
import styles from './album-group-header.module.css';
import { TableItemSize } from './item-table-list';
import { ItemImage } from '/@/renderer/components/item-image/item-image';
import { PlayButton } from '/@/renderer/features/shared/components/play-button';
import {
LONG_PRESS_PLAY_BEHAVIOR,
PlayTooltip,
} from '/@/renderer/features/shared/components/play-button-group';
import { usePlayButtonBehavior } from '/@/renderer/store';
import { LibraryItem, Song } from '/@/shared/types/domain-types';
import { Play } from '/@/shared/types/types';
interface AlbumGroupHeaderProps {
groupRowCount?: number;
onPlay?: (playType: Play) => void;
size?: 'compact' | 'large' | 'normal';
song: Song | undefined;
}
export const AlbumGroupHeader = ({
groupRowCount,
onPlay,
size = 'normal',
song,
}: AlbumGroupHeaderProps): ReactElement => {
const [isHovered, setIsHovered] = useState(false);
const playButtonBehavior = usePlayButtonBehavior();
const rowHeight = {
compact: TableItemSize.COMPACT,
large: TableItemSize.LARGE,
normal: TableItemSize.DEFAULT,
}[size];
const infoHeight = groupRowCount !== undefined ? groupRowCount * rowHeight : undefined;
return (
<div className={styles.container}>
<div
className={styles.imageContainer}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<ItemImage
className={imageColumnStyles.compactImage}
enableDebounce
enableViewport={false}
id={song?.imageId}
itemType={LibraryItem.SONG}
src={song?.imageUrl}
type="table"
/>
{isHovered && onPlay && (
<div className={imageColumnStyles.playButtonOverlay}>
<PlayTooltip type={playButtonBehavior}>
<PlayButton
fill
onClick={(e) => {
e.stopPropagation();
onPlay(playButtonBehavior);
}}
onLongPress={(e) => {
e.stopPropagation();
onPlay(LONG_PRESS_PLAY_BEHAVIOR[playButtonBehavior]);
}}
/>
</PlayTooltip>
</div>
)}
</div>
<div className={styles.info} style={{ height: infoHeight }}>
<div className={styles.albumName}>{song?.album ?? ''}</div>
<div className={styles.artistName}>{song?.albumArtistName ?? ''}</div>
</div>
</div>
);
};
@@ -0,0 +1,90 @@
import { useCallback } from 'react';
import { AlbumGroupHeader } from '/@/renderer/components/item-list/item-table-list/album-group-header';
import {
isLastInAlbumGroup,
ItemTableListInnerColumn,
TableColumnContainer,
} from '/@/renderer/components/item-list/item-table-list/item-table-list-column';
import { Song } from '/@/shared/types/domain-types';
import { Play } from '/@/shared/types/types';
export const AlbumGroupColumn = (props: ItemTableListInnerColumn) => {
const firstDataRow = props.enableHeader ? 1 : 0;
const item = props.getRowItem?.(props.rowIndex) as null | Song | undefined;
const handlePlay = useCallback(
(playType: Play) => {
if (!item || !props.controls?.onDoubleClick) return;
const isHeaderEnabled = !!props.enableHeader;
const index = isHeaderEnabled ? props.rowIndex - 1 : props.rowIndex;
props.controls.onDoubleClick({
event: null,
index,
internalState: (props as any).internalState,
item,
itemType: props.itemType,
meta: { playType },
});
},
[item, props],
);
if (!item?.album) {
return <div style={props.style} />;
}
// Check if this is the first row of a new album group (by album name)
let isFirstInGroup = true;
if (props.rowIndex > firstDataRow) {
const prevItem = props.getRowItem?.(props.rowIndex - 1) as null | Song | undefined;
// If prevItem is undefined (not loaded yet), assume same group to avoid duplicates
if (prevItem === undefined || prevItem?.album === item.album) {
isFirstInGroup = false;
}
}
if (!isFirstInGroup) {
// For non-first rows, add border-bottom on the last row of the group
const needsBorder =
props.enableHorizontalBorders &&
isLastInAlbumGroup(
props.rowIndex,
props.getRowItem,
!!props.enableHeader,
props.data.length,
);
return (
<div
style={{
...props.style,
...(needsBorder
? { borderBottom: '1px solid var(--theme-colors-border)' }
: {}),
}}
/>
);
}
let groupRowCount = 1;
const totalDataRows = props.data.length + firstDataRow;
for (let idx = props.rowIndex + 1; idx < totalDataRows; idx++) {
const nextItem = props.getRowItem?.(idx) as null | Song | undefined;
if (!nextItem || nextItem.album !== item.album) break;
groupRowCount++;
}
return (
<TableColumnContainer {...props} enableAlternateRowColors={false}>
<AlbumGroupHeader
groupRowCount={groupRowCount}
onPlay={handlePlay}
size={props.size === 'default' ? 'normal' : props.size}
song={item}
/>
</TableColumnContainer>
);
};
@@ -6,6 +6,10 @@
height: 100%;
}
.title-combined.no-image {
grid-template-columns: 1fr;
}
.text-container {
display: grid;
grid-template-rows: 1fr 1fr;
@@ -81,6 +81,7 @@ export const DefaultTitleCombinedColumn = (props: ItemTableListInnerColumn) => {
const rowHeight = props.getRowHeight(props.rowIndex, props);
const path = getTitlePath(props.itemType, (rowItem as any).id as string);
const align = props.columns[props.columnIndex]?.align || 'start';
const hasAlbumGroupColumn = props.hasAlbumGroupColumn ?? false;
const item = rowItem as any;
const titleLinkProps = path
@@ -94,46 +95,53 @@ export const DefaultTitleCombinedColumn = (props: ItemTableListInnerColumn) => {
return (
<TableColumnContainer
className={styles.titleCombined}
className={clsx(styles.titleCombined, {
[styles.noImage]: hasAlbumGroupColumn,
})}
containerStyle={{ '--row-height': `${rowHeight}px` } as CSSProperties}
{...props}
>
<div
className={styles.imageContainer}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<ItemImage
containerClassName={styles.image}
enableDebounce={true}
enableViewport={false}
explicitStatus={item?.explicitStatus}
id={item?.imageId}
itemType={item?._itemType}
src={item?.imageUrl}
type="table"
/>
{isHovered && (
<div
className={clsx(styles.playButtonOverlay, {
[styles.compactPlayButtonOverlay]: props.size === 'compact',
})}
>
<PlayTooltip
disabled={props.itemType === LibraryItem.QUEUE_SONG}
type={playButtonBehavior}
{!hasAlbumGroupColumn && (
<div
className={styles.imageContainer}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<ItemImage
containerClassName={styles.image}
enableDebounce={true}
enableViewport={false}
explicitStatus={item?.explicitStatus}
id={item?.imageId}
itemType={item?._itemType}
src={item?.imageUrl}
type="table"
/>
{isHovered && (
<div
className={clsx(styles.playButtonOverlay, {
[styles.compactPlayButtonOverlay]: props.size === 'compact',
})}
>
<PlayButton
fill
onClick={(e) => handlePlay(playButtonBehavior, e)}
onLongPress={(e) =>
handlePlay(LONG_PRESS_PLAY_BEHAVIOR[playButtonBehavior], e)
}
/>
</PlayTooltip>
</div>
)}
</div>
<PlayTooltip
disabled={props.itemType === LibraryItem.QUEUE_SONG}
type={playButtonBehavior}
>
<PlayButton
fill
onClick={(e) => handlePlay(playButtonBehavior, e)}
onLongPress={(e) =>
handlePlay(
LONG_PRESS_PLAY_BEHAVIOR[playButtonBehavior],
e,
)
}
/>
</PlayTooltip>
</div>
)}
</div>
)}
<div
className={clsx(styles.textContainer, {
[styles.alignCenter]: align === 'center',
@@ -224,6 +232,7 @@ export const QueueSongTitleCombinedColumn = (props: ItemTableListInnerColumn) =>
const rowHeight = props.getRowHeight(props.rowIndex, props);
const path = getTitlePath(props.itemType, (rowItem as any).id as string);
const align = props.columns[props.columnIndex]?.align || 'start';
const hasAlbumGroupColumn = props.hasAlbumGroupColumn ?? false;
const item = rowItem as any;
@@ -238,45 +247,52 @@ export const QueueSongTitleCombinedColumn = (props: ItemTableListInnerColumn) =>
return (
<TableColumnContainer
className={styles.titleCombined}
className={clsx(styles.titleCombined, {
[styles.noImage]: hasAlbumGroupColumn,
})}
containerStyle={{ '--row-height': `${rowHeight}px` } as CSSProperties}
{...props}
>
<div
className={styles.imageContainer}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<ItemImage
containerClassName={styles.image}
explicitStatus={item?.explicitStatus}
id={item?.imageId}
itemType={item?._itemType}
serverId={item?._serverId}
src={item?.imageUrl}
type="table"
/>
{isHovered && (
<div
className={clsx(styles.playButtonOverlay, {
[styles.compactPlayButtonOverlay]: props.size === 'compact',
})}
>
<PlayTooltip
disabled={props.itemType === LibraryItem.QUEUE_SONG}
type={playButtonBehavior}
{!hasAlbumGroupColumn && (
<div
className={styles.imageContainer}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<ItemImage
containerClassName={styles.image}
explicitStatus={item?.explicitStatus}
id={item?.imageId}
itemType={item?._itemType}
serverId={item?._serverId}
src={item?.imageUrl}
type="table"
/>
{isHovered && (
<div
className={clsx(styles.playButtonOverlay, {
[styles.compactPlayButtonOverlay]: props.size === 'compact',
})}
>
<PlayButton
fill
onClick={(e) => handlePlay(playButtonBehavior, e)}
onLongPress={(e) =>
handlePlay(LONG_PRESS_PLAY_BEHAVIOR[playButtonBehavior], e)
}
/>
</PlayTooltip>
</div>
)}
</div>
<PlayTooltip
disabled={props.itemType === LibraryItem.QUEUE_SONG}
type={playButtonBehavior}
>
<PlayButton
fill
onClick={(e) => handlePlay(playButtonBehavior, e)}
onLongPress={(e) =>
handlePlay(
LONG_PRESS_PLAY_BEHAVIOR[playButtonBehavior],
e,
)
}
/>
</PlayTooltip>
</div>
)}
</div>
)}
<div
className={clsx(styles.textContainer, {
[styles.active]: isActive,
@@ -13,6 +13,15 @@ export type DefaultTableColumn = {
};
export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [
{
align: 'start',
autoSize: false,
isEnabled: false,
label: i18n.t('table.config.label.albumGroup', { postProcess: 'titleCase' }),
pinned: 'left',
value: TableColumn.ALBUM_GROUP,
width: 200,
},
{
align: 'center',
autoSize: false,
@@ -19,24 +19,45 @@ export const useTableColumnModel = ({
const calculatedColumnWidths = useMemo(() => {
const baseWidths = parsedColumns.map((c) => c.width);
// When autoSizeColumns is enabled, treat all widths as proportions and scale to fit container
// When autoSizeColumns is enabled, treat unpinned widths as proportions and scale to fit container.
// Pinned columns keep their base width so they don't get squeezed.
if (autoFitColumns) {
const totalReferenceWidth = baseWidths.reduce((sum, width) => sum + width, 0);
const pinnedWidth = parsedColumns.reduce(
(sum, col, idx) => (col.pinned !== null ? sum + baseWidths[idx] : sum),
0,
);
const unpinnedIndices: number[] = [];
parsedColumns.forEach((col, idx) => {
if (col.pinned === null) {
unpinnedIndices.push(idx);
}
});
if (totalReferenceWidth === 0 || totalContainerWidth === 0) {
const unpinnedReferenceWidth = unpinnedIndices.reduce(
(sum, idx) => sum + baseWidths[idx],
0,
);
const availableForUnpinned = totalContainerWidth - pinnedWidth;
if (unpinnedReferenceWidth === 0 || availableForUnpinned <= 0) {
return baseWidths.map((width) => Math.round(width));
}
const scaleFactor = totalContainerWidth / totalReferenceWidth;
const scaledWidths = baseWidths.map((width) => Math.round(width * scaleFactor));
const scaleFactor = availableForUnpinned / unpinnedReferenceWidth;
const scaledWidths = baseWidths.map((width, idx) => {
if (parsedColumns[idx].pinned !== null) {
return Math.round(width);
}
return Math.round(width * scaleFactor);
});
// Adjust for rounding errors: ensure total equals totalContainerWidth
// Adjust for rounding errors on unpinned columns only
const totalScaled = scaledWidths.reduce((sum, width) => sum + width, 0);
const difference = totalContainerWidth - totalScaled;
if (difference !== 0 && scaledWidths.length > 0) {
const sortedIndices = scaledWidths
.map((width, idx) => ({ idx, width }))
if (difference !== 0 && unpinnedIndices.length > 0) {
const sortedIndices = unpinnedIndices
.map((idx) => ({ idx, width: scaledWidths[idx] }))
.sort((a, b) => b.width - a.width);
const adjustmentPerColumn = Math.sign(difference);
@@ -30,6 +30,7 @@ import { isNoHorizontalPaddingColumn } from '/@/renderer/components/item-list/it
import { ActionsColumn } from '/@/renderer/components/item-list/item-table-list/columns/actions-column';
import { AlbumArtistsColumn } from '/@/renderer/components/item-list/item-table-list/columns/album-artists-column';
import { AlbumColumn } from '/@/renderer/components/item-list/item-table-list/columns/album-column';
import { AlbumGroupColumn } from '/@/renderer/components/item-list/item-table-list/columns/album-group-column';
import { ArtistsColumn } from '/@/renderer/components/item-list/item-table-list/columns/artists-column';
import { ComposerColumn } from '/@/renderer/components/item-list/item-table-list/columns/composer-column';
import { CountColumn } from '/@/renderer/components/item-list/item-table-list/columns/count-column';
@@ -213,6 +214,11 @@ const ItemTableListColumnBase = (props: ItemTableListColumn) => {
case TableColumn.SONG_COUNT:
return <CountColumn {...props} {...dragProps} controls={controls} type={type} />;
case TableColumn.ALBUM_GROUP:
return (
<AlbumGroupColumn {...props} {...dragProps} controls={controls} type={type} />
);
case TableColumn.ARTIST:
return <ArtistsColumn {...props} {...dragProps} controls={controls} type={type} />;
@@ -362,6 +368,27 @@ export const ItemTableListColumn = memo(ItemTableListColumnBase, (prevProps, nex
const NonMutedColumns = [TableColumn.TITLE, TableColumn.TITLE_ARTIST, TableColumn.TITLE_COMBINED];
export function isAlbumGroupingActive(columns: { id: string; isEnabled?: boolean }[]): boolean {
return columns.some((col) => col.id === TableColumn.ALBUM_GROUP && col.isEnabled);
}
export function isLastInAlbumGroup(
rowIndex: number,
getRowItem: ((index: number) => unknown) | undefined,
enableHeader: boolean | undefined,
dataLength: number,
): boolean {
const item = getRowItem?.(rowIndex) as null | undefined | { album?: string };
if (!item?.album) return true;
const nextRowIndex = rowIndex + 1;
const maxRow = enableHeader ? dataLength + 1 : dataLength;
if (nextRowIndex >= maxRow) return true;
const nextItem = getRowItem?.(nextRowIndex) as null | undefined | { album?: string };
return !nextItem || nextItem.album !== item.album;
}
export const TableColumnTextContainer = (
props: ItemTableListColumn & {
children: React.ReactNode;
@@ -493,7 +520,14 @@ export const TableColumnTextContainer = (
props.enableHorizontalBorders &&
props.enableHeader &&
props.rowIndex > 0 &&
(props.rowIndex === 1 || !isLastRow),
(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}
@@ -641,13 +675,24 @@ export const TableColumnContainer = (
[styles.paddingXl]: props.cellPadding === 'xl',
[styles.paddingXs]: props.cellPadding === 'xs',
[styles.right]: props.columns[props.columnIndex].align === 'end',
[styles.rowHoverHighlightEnabled]: isDataRow && props.enableRowHoverHighlight,
[styles.rowSelected]: isDataRow && isSelected,
[styles.rowHoverHighlightEnabled]:
isDataRow &&
props.enableRowHoverHighlight &&
props.type !== TableColumn.ALBUM_GROUP,
[styles.rowSelected]:
isDataRow && isSelected && props.type !== TableColumn.ALBUM_GROUP,
[styles.withHorizontalBorder]:
props.enableHorizontalBorders &&
props.enableHeader &&
props.rowIndex > 0 &&
(props.rowIndex === 1 || !isLastRow),
(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}
@@ -898,6 +943,9 @@ export const columnLabelMap: Record<TableColumn, ReactNode | string> = {
[TableColumn.ALBUM_COUNT]: i18n.t('table.column.albumCount', {
postProcess: 'upperCase',
}) as string,
[TableColumn.ALBUM_GROUP]: i18n.t('table.config.label.albumGroup', {
postProcess: 'upperCase',
}) as string,
[TableColumn.ARTIST]: i18n.t('table.column.artist', { postProcess: 'upperCase' }) as string,
[TableColumn.BIOGRAPHY]: i18n.t('table.column.biography', {
postProcess: 'upperCase',
@@ -94,7 +94,7 @@ const hasRequiredStateItemProperties = (
);
};
enum TableItemSize {
export enum TableItemSize {
COMPACT = 40,
DEFAULT = 64,
LARGE = 88,
@@ -204,17 +204,6 @@ const VirtualizedTableGrid = ({
[columnWidth, pinnedLeftColumnCount],
);
const rowHeightMemoized = useCallback(
(index: number, cellProps: TableItemProps) =>
getRowHeight(index + pinnedRowCount, cellProps),
[getRowHeight, pinnedRowCount],
);
const pinnedRightColumnWidthMemoized = useCallback(
(index: number) => columnWidth(index + pinnedLeftColumnCount + totalColumnCount),
[columnWidth, pinnedLeftColumnCount, totalColumnCount],
);
const groupHeaderInfoByRowIndex = useMemo(() => {
if (!groups || groups.length === 0) return undefined;
@@ -231,6 +220,19 @@ const VirtualizedTableGrid = ({
return map;
}, [groups, enableHeader]);
const rowHeightMemoized = useCallback(
(index: number, cellProps: TableItemProps) => {
const adjustedIndex = index + pinnedRowCount;
return getRowHeight(adjustedIndex, cellProps);
},
[getRowHeight, pinnedRowCount],
);
const pinnedRightColumnWidthMemoized = useCallback(
(index: number) => columnWidth(index + pinnedLeftColumnCount + totalColumnCount),
[columnWidth, pinnedLeftColumnCount, totalColumnCount],
);
const getGroupRenderData = useCallback(() => data, [data]);
// Calculate pinned column widths for group header positioning
@@ -349,6 +351,7 @@ const VirtualizedTableGrid = ({
controls,
enableHeader,
getRowHeight,
hasAlbumGroupColumn: parsedColumns.some((col) => col.id === TableColumn.ALBUM_GROUP),
internalState,
itemType,
playerContext,
@@ -802,6 +805,7 @@ export interface TableItemProps {
getRowItem?: (rowIndex: number) => null | undefined | unknown;
groupHeaderInfoByRowIndex?: Map<number, { groupIndex: number; startDataIndex: number }>;
groups?: TableGroupHeader[];
hasAlbumGroupColumn?: boolean;
internalState: ItemListStateActions;
itemType: ItemTableListProps['itemType'];
onRowClick?: (item: any, event: React.MouseEvent<HTMLDivElement>) => void;
View File
@@ -21,7 +21,7 @@ import {
PlaylistSongListResponse,
Song,
} from '/@/shared/types/domain-types';
import { ItemListKey, Play } from '/@/shared/types/types';
import { ItemListKey, Play, TableColumn } from '/@/shared/types/types';
interface PlaylistDetailSongListTableProps
extends Omit<ItemListTableComponentProps<PlaylistSongListQuery>, 'query'> {
@@ -68,6 +68,10 @@ export const PlaylistDetailSongListTable = forwardRef<any, PlaylistDetailSongLis
const { searchTerm } = useSearchTermFilter();
const { query } = usePlaylistSongListFilters();
const albumGroupingEnabled = columns.some(
(col) => col.id === TableColumn.ALBUM_GROUP && col.isEnabled,
);
const songDataFromData = useMemo(() => {
let list = data?.items || [];
if (searchTerm) {
@@ -117,6 +121,11 @@ export const PlaylistDetailSongListTable = forwardRef<any, PlaylistDetailSongLis
};
}, []);
const effectiveColumns = useMemo(() => {
if (albumGroupingEnabled) return columns;
return columns.filter((col) => col.id !== TableColumn.ALBUM_GROUP);
}, [columns, albumGroupingEnabled]);
const isPaginated =
typeof currentPage === 'number' &&
typeof itemsPerPage === 'number' &&
@@ -135,7 +144,7 @@ export const PlaylistDetailSongListTable = forwardRef<any, PlaylistDetailSongLis
activeRowId={currentSong?.id}
autoFitColumns={autoFitColumns}
CellComponent={ItemTableListColumn}
columns={columns}
columns={effectiveColumns}
data={dataToRender}
enableAlternateRowColors={enableAlternateRowColors}
enableExpansion={false}
@@ -262,7 +262,6 @@ export const TableConfig = ({
id: 'autoFitColumns',
label: t('table.config.general.autoFitColumns', { postProcess: 'sentenceCase' }),
},
...(extraOptions || []),
];
@@ -774,25 +773,18 @@ const TableColumnItem = memo(
className={clsx(styles.group, styles.numberInput)}
hideControls={false}
leftSection={
<>
{item.pinned === null && (
<Tooltip
label={t('table.config.general.autosize', {
postProcess: 'sentenceCase',
})}
>
<Checkbox
checked={item.autoSize}
disabled={item.pinned !== null}
id={item.id}
onChange={(e) =>
handleAutoSize(item, e.currentTarget.checked)
}
size="xs"
/>
</Tooltip>
)}
</>
<Tooltip
label={t('table.config.general.autosize', {
postProcess: 'sentenceCase',
})}
>
<Checkbox
checked={item.autoSize}
id={item.id}
onChange={(e) => handleAutoSize(item, e.currentTarget.checked)}
size="xs"
/>
</Tooltip>
}
max={2000}
min={0}
+35 -1
View File
@@ -2239,10 +2239,44 @@ export const useSettingsStore = createWithEqualityFn<SettingsSlice>()(
}
}
if (version <= 26) {
// Add ALBUM_GROUP column to the song table config
const listKeysToUpdate: ItemListKey[] = [
ItemListKey.SONG,
ItemListKey.FOLDER,
ItemListKey.PLAYLIST_SONG,
ItemListKey.ALBUM_ARTIST_SONG,
ItemListKey.GENRE_SONG,
ItemListKey.QUEUE_SONG,
ItemListKey.FULL_SCREEN,
ItemListKey.SIDE_QUEUE,
];
listKeysToUpdate.forEach((listKey) => {
const listConfig = state.lists[listKey as keyof typeof state.lists];
if (listConfig?.table?.columns) {
const columns = listConfig.table.columns;
const hasAlbumGroup = columns.some(
(col) => col.id === TableColumn.ALBUM_GROUP,
);
if (!hasAlbumGroup) {
columns.push({
align: 'start',
autoSize: false,
id: TableColumn.ALBUM_GROUP,
isEnabled: false,
pinned: 'left',
width: 200,
});
}
}
});
}
return persistedState;
},
name: 'store_settings',
version: 25,
version: 26,
},
),
);
+6 -1
View File
@@ -158,7 +158,12 @@ export const sortSongList = (songs: Song[], sortBy: SongListSort, sortOrder: Sor
case SongListSort.ALBUM_ARTIST:
results = orderBy(
results,
[(v) => v.albumArtists[0]?.name.toLowerCase(), 'discNumber', 'trackNumber'],
[
(v) => v.albumArtists[0]?.name.toLowerCase(),
(v) => v.album?.toLowerCase(),
'discNumber',
'trackNumber',
],
[order, order, order, order],
);
break;
View File
+1
View File
@@ -166,6 +166,7 @@ export enum TableColumn {
ALBUM = 'album',
ALBUM_ARTIST = 'albumArtists',
ALBUM_COUNT = 'albumCount',
ALBUM_GROUP = 'albumGroup',
ARTIST = 'artists',
BIOGRAPHY = 'biography',
BIT_DEPTH = 'bitDepth',