add Title (artist) column (#1496)

This commit is contained in:
jeffvli
2026-01-17 01:49:59 -08:00
parent d1aed5007f
commit 18d56f32cf
8 changed files with 345 additions and 4 deletions
+1
View File
@@ -1105,6 +1105,7 @@
"size": "$t(common.size)",
"songCount": "$t(entity.track_other)",
"title": "$t(common.title)",
"titleArtist": "$t(common.title) (artist)",
"titleCombined": "$t(common.title) (combined)",
"trackNumber": "track number",
"year": "$t(common.year)"
@@ -39,9 +39,7 @@ const getDefaultRowsForItemType = (
}
};
// Map TableColumn enum values to row IDs used in getDataRows
const getRowIdFromTableColumn = (tableColumn: TableColumn): null | string => {
// Map TableColumn enum values to the row IDs used in getDataRows
const columnToRowIdMap: Record<TableColumn, null | string> = {
[TableColumn.ACTIONS]: null,
[TableColumn.ALBUM]: 'album',
@@ -74,6 +72,7 @@ const getRowIdFromTableColumn = (tableColumn: TableColumn): null | string => {
[TableColumn.SKIP]: null,
[TableColumn.SONG_COUNT]: 'songCount',
[TableColumn.TITLE]: 'name',
[TableColumn.TITLE_ARTIST]: null,
[TableColumn.TITLE_COMBINED]: null,
[TableColumn.TRACK_NUMBER]: null,
[TableColumn.USER_FAVORITE]: 'userFavorite',
@@ -0,0 +1,59 @@
.title-artist {
display: flex;
gap: var(--theme-spacing-sm);
width: 100%;
height: 100%;
}
.text-container {
display: grid;
grid-template-rows: 1fr 1fr;
gap: var(--theme-spacing-xs);
min-width: 0;
}
.text-container.align-left {
text-align: left;
}
.text-container.align-center {
text-align: center;
}
.text-container.align-right {
text-align: right;
}
.text-container.compact {
gap: 0;
}
.title {
display: inline-block;
width: 100%;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.artists {
display: block;
overflow: hidden;
text-overflow: ellipsis;
font-size: var(--theme-font-size-xs) !important;
color: var(--theme-colors-foreground-muted);
white-space: nowrap;
user-select: none;
}
.folder-icon {
color: black;
fill: rgb(255 215 100);
}
.active {
color: var(--theme-colors-primary);
}
@@ -0,0 +1,209 @@
import clsx from 'clsx';
import { CSSProperties } from 'react';
import { Link } from 'react-router';
import styles from './title-artist-column.module.css';
import { getTitlePath } from '/@/renderer/components/item-list/helpers/get-title-path';
import {
ColumnNullFallback,
ColumnSkeletonVariable,
ItemTableListInnerColumn,
TableColumnContainer,
} from '/@/renderer/components/item-list/item-table-list/item-table-list-column';
import { useIsActiveRow } from '/@/renderer/components/item-list/item-table-list/item-table-list-context';
import { JoinedArtists } from '/@/renderer/features/albums/components/joined-artists';
import { Icon } from '/@/shared/components/icon/icon';
import { Text } from '/@/shared/components/text/text';
import { Folder, LibraryItem, QueueSong } from '/@/shared/types/domain-types';
export const DefaultTitleArtistColumn = (props: ItemTableListInnerColumn) => {
const rowItem = props.getRowItem?.(props.rowIndex) ?? (props.data as any[])[props.rowIndex];
const row: object | undefined = (rowItem as any)?.id;
const item = rowItem as any;
const align = props.columns[props.columnIndex]?.align || 'start';
if (item && 'name' in item && 'artists' in item) {
const rowHeight = props.getRowHeight(props.rowIndex, props);
const path = getTitlePath(props.itemType, (rowItem as any).id as string);
const item = rowItem as any;
const titleLinkProps = path
? {
component: Link,
isLink: true,
state: { item },
to: path,
}
: {};
return (
<TableColumnContainer
className={clsx(styles.titleArtist)}
containerStyle={{ '--row-height': `${rowHeight}px` } as CSSProperties}
{...props}
>
<div
className={clsx(styles.textContainer, {
[styles.alignCenter]: align === 'center',
[styles.alignLeft]: align === 'start',
[styles.alignRight]: align === 'end',
[styles.compact]: props.size === 'compact',
})}
>
<Text className={styles.title} isNoSelect size="md" {...titleLinkProps}>
{item.name as string}
</Text>
<div className={styles.artists}>
<JoinedArtists
artistName={item.albumArtist}
artists={item.albumArtists}
linkProps={{ fw: 400, isMuted: true }}
rootTextProps={{ fw: 400, isMuted: true, size: 'sm' }}
/>
</div>
</div>
</TableColumnContainer>
);
}
if (row === null) {
return <ColumnNullFallback {...props} />;
}
return <ColumnSkeletonVariable {...props} />;
};
export const QueueSongTitleArtistColumn = (props: ItemTableListInnerColumn) => {
const rowItem = props.getRowItem?.(props.rowIndex) ?? (props.data as any[])[props.rowIndex];
const row: object | undefined = rowItem as any;
const song = rowItem as QueueSong;
const isActive = useIsActiveRow(song?.id, song?._uniqueId);
const align = props.columns[props.columnIndex]?.align || 'start';
const alignClass =
align === 'center' ? 'align-center' : align === 'end' ? 'align-right' : 'align-left';
if (row && 'name' in row && 'artists' in row) {
const rowHeight = props.getRowHeight(props.rowIndex, props);
const path = getTitlePath(props.itemType, (rowItem as any).id as string);
const item = rowItem as any;
const titleLinkProps = path
? {
component: Link,
isLink: true,
state: { item },
to: path,
}
: {};
return (
<TableColumnContainer
className={clsx(styles.titleArtist, styles[alignClass])}
containerStyle={{ '--row-height': `${rowHeight}px` } as CSSProperties}
{...props}
>
<div
className={clsx(styles.textContainer, styles[alignClass], {
[styles.active]: isActive,
[styles.compact]: props.size === 'compact',
})}
>
<Text
className={clsx({
[styles.active]: isActive,
[styles.title]: true,
})}
isNoSelect
size="md"
{...titleLinkProps}
>
{row.name as string}
{song?.trackSubtitle && props.itemType !== LibraryItem.QUEUE_SONG && (
<Text
className={clsx({
[styles.active]: isActive,
})}
component="span"
isMuted
size="sm"
>
{' ('}
{song.trackSubtitle}
{')'}
</Text>
)}
</Text>
<div className={styles.artists}>
<JoinedArtists
artistName={item.artistName}
artists={item.artists}
linkProps={{ fw: 400, isMuted: true }}
rootTextProps={{ fw: 400, isMuted: true, size: 'sm' }}
/>
</div>
</div>
</TableColumnContainer>
);
}
if ((rowItem as unknown as Folder)?._itemType === LibraryItem.FOLDER) {
const rowHeight = props.getRowHeight(props.rowIndex, props);
const path = getTitlePath(props.itemType, (rowItem as any).id as string);
const item = rowItem as any;
const textStyles = isActive ? { color: 'var(--theme-colors-primary)' } : {};
const titleLinkProps = path
? {
component: Link,
isLink: true,
state: { item },
to: path,
}
: {};
const title = (rowItem as unknown as Folder)?.name;
return (
<TableColumnContainer
className={clsx(styles.titleArtist, styles[alignClass])}
containerStyle={{ '--row-height': `${rowHeight}px` } as CSSProperties}
{...props}
>
<Icon className={styles.folderIcon} icon="folder" size="2xl" />
<Text
className={styles.title}
isNoSelect
size="md"
{...titleLinkProps}
style={textStyles}
>
{title}
</Text>
</TableColumnContainer>
);
}
if (row === null) {
return <ColumnNullFallback {...props} />;
}
return <ColumnSkeletonVariable {...props} />;
};
export const TitleArtistColumn = (props: ItemTableListInnerColumn) => {
const { itemType } = props;
switch (itemType) {
case LibraryItem.FOLDER:
case LibraryItem.PLAYLIST_SONG:
case LibraryItem.QUEUE_SONG:
case LibraryItem.SONG:
return <QueueSongTitleArtistColumn {...props} />;
default:
return <DefaultTitleArtistColumn {...props} />;
}
};
@@ -49,6 +49,15 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [
value: TableColumn.TITLE_COMBINED,
width: 300,
},
{
align: 'start',
autoSize: false,
isEnabled: false,
label: i18n.t('table.config.label.titleArtist', { postProcess: 'titleCase' }),
pinned: null,
value: TableColumn.TITLE_ARTIST,
width: 300,
},
{
align: 'center',
autoSize: false,
@@ -315,6 +324,15 @@ export const ALBUM_TABLE_COLUMNS: DefaultTableColumn[] = [
value: TableColumn.TITLE_COMBINED,
width: 300,
},
{
align: 'start',
autoSize: false,
isEnabled: false,
label: i18n.t('table.config.label.titleArtist', { postProcess: 'titleCase' }),
pinned: null,
value: TableColumn.TITLE_ARTIST,
width: 300,
},
{
align: 'center',
autoSize: false,
@@ -45,6 +45,7 @@ import { RatingColumn } from '/@/renderer/components/item-list/item-table-list/c
import { RowIndexColumn } from '/@/renderer/components/item-list/item-table-list/columns/row-index-column';
import { SizeColumn } from '/@/renderer/components/item-list/item-table-list/columns/size-column';
import { TextColumn } from '/@/renderer/components/item-list/item-table-list/columns/text-column';
import { TitleArtistColumn } from '/@/renderer/components/item-list/item-table-list/columns/title-artist-column';
import { TitleColumn } from '/@/renderer/components/item-list/item-table-list/columns/title-column';
import { TitleCombinedColumn } from '/@/renderer/components/item-list/item-table-list/columns/title-combined-column';
import { YearColumn } from '/@/renderer/components/item-list/item-table-list/columns/year-column';
@@ -523,6 +524,11 @@ export const ItemTableListColumn = (props: ItemTableListColumn) => {
case TableColumn.TITLE:
return <TitleColumn {...props} {...dragProps} controls={controls} type={type} />;
case TableColumn.TITLE_ARTIST:
return (
<TitleArtistColumn {...props} {...dragProps} controls={controls} type={type} />
);
case TableColumn.TITLE_COMBINED:
return (
<TitleCombinedColumn
@@ -560,6 +566,9 @@ export const ItemTableListColumn = (props: ItemTableListColumn) => {
case TableColumn.TITLE:
return <TitleColumn {...props} {...dragProps} controls={controls} type={type} />;
case TableColumn.TITLE_ARTIST:
return <TitleArtistColumn {...props} {...dragProps} controls={controls} type={type} />;
case TableColumn.TITLE_COMBINED:
return (
<TitleCombinedColumn {...props} {...dragProps} controls={controls} type={type} />
@@ -570,7 +579,7 @@ export const ItemTableListColumn = (props: ItemTableListColumn) => {
}
};
const NonMutedColumns = [TableColumn.TITLE, TableColumn.TITLE_COMBINED];
const NonMutedColumns = [TableColumn.TITLE, TableColumn.TITLE_ARTIST, TableColumn.TITLE_COMBINED];
export const TableColumnTextContainer = (
props: ItemTableListColumn & {
@@ -1166,6 +1175,9 @@ const columnLabelMap: Record<TableColumn, ReactNode | string> = {
postProcess: 'upperCase',
}) as string,
[TableColumn.TITLE]: i18n.t('table.column.title', { postProcess: 'upperCase' }) as string,
[TableColumn.TITLE_ARTIST]: i18n.t('table.column.title', {
postProcess: 'upperCase',
}) as string,
[TableColumn.TITLE_COMBINED]: i18n.t('table.column.title', {
postProcess: 'upperCase',
}) as string,
+43 -1
View File
@@ -1958,10 +1958,52 @@ export const useSettingsStore = createWithEqualityFn<SettingsSlice>()(
}
}
if (version <= 20) {
// Add TITLE_ARTIST column to SONG and ALBUM table configs
const titleArtistColumn: ItemTableListColumnConfig = {
align: 'start',
autoSize: false,
id: TableColumn.TITLE_ARTIST,
isEnabled: false,
pinned: null,
width: 300,
};
const listKeysToUpdate: (LibraryItem | string)[] = [
LibraryItem.SONG,
LibraryItem.ALBUM,
LibraryItem.PLAYLIST_SONG,
LibraryItem.QUEUE_SONG,
ItemListKey.ALBUM_DETAIL,
ItemListKey.FULL_SCREEN,
ItemListKey.SIDE_QUEUE,
];
listKeysToUpdate.forEach((listKey) => {
const listConfig = state.lists[listKey];
if (listConfig?.table?.columns) {
const columns = listConfig.table.columns;
const hasTitleArtist = columns.some(
(col) => col.id === TableColumn.TITLE_ARTIST,
);
if (!hasTitleArtist) {
const titleCombinedIndex = columns.findIndex(
(col) => col.id === TableColumn.TITLE_COMBINED,
);
if (titleCombinedIndex >= 0) {
columns.splice(titleCombinedIndex + 1, 0, titleArtistColumn);
} else {
columns.push(titleArtistColumn);
}
}
}
});
}
return persistedState;
},
name: 'store_settings',
version: 20,
version: 21,
},
),
);
+1
View File
@@ -187,6 +187,7 @@ export enum TableColumn {
SKIP = 'skip',
SONG_COUNT = 'songCount',
TITLE = 'name',
TITLE_ARTIST = 'titleArtist',
TITLE_COMBINED = 'titleCombined',
TRACK_NUMBER = 'trackNumber',
USER_FAVORITE = 'userFavorite',