From 18d56f32cf988654fbbe48669681abe5093d90b9 Mon Sep 17 00:00:00 2001 From: jeffvli Date: Sat, 17 Jan 2026 01:49:59 -0800 Subject: [PATCH] add Title (artist) column (#1496) --- src/i18n/locales/en.json | 1 + .../item-list/helpers/use-grid-rows.ts | 3 +- .../columns/title-artist-column.module.css | 59 +++++ .../columns/title-artist-column.tsx | 209 ++++++++++++++++++ .../item-table-list/default-columns.ts | 18 ++ .../item-table-list-column.tsx | 14 +- src/renderer/store/settings.store.ts | 44 +++- src/shared/types/types.ts | 1 + 8 files changed, 345 insertions(+), 4 deletions(-) create mode 100644 src/renderer/components/item-list/item-table-list/columns/title-artist-column.module.css create mode 100644 src/renderer/components/item-list/item-table-list/columns/title-artist-column.tsx diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 33ea01b6f..ac60ebc58 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -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)" diff --git a/src/renderer/components/item-list/helpers/use-grid-rows.ts b/src/renderer/components/item-list/helpers/use-grid-rows.ts index 17af243ba..ae462732f 100644 --- a/src/renderer/components/item-list/helpers/use-grid-rows.ts +++ b/src/renderer/components/item-list/helpers/use-grid-rows.ts @@ -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.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', diff --git a/src/renderer/components/item-list/item-table-list/columns/title-artist-column.module.css b/src/renderer/components/item-list/item-table-list/columns/title-artist-column.module.css new file mode 100644 index 000000000..1e3fac942 --- /dev/null +++ b/src/renderer/components/item-list/item-table-list/columns/title-artist-column.module.css @@ -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); +} diff --git a/src/renderer/components/item-list/item-table-list/columns/title-artist-column.tsx b/src/renderer/components/item-list/item-table-list/columns/title-artist-column.tsx new file mode 100644 index 000000000..60dbf1f5d --- /dev/null +++ b/src/renderer/components/item-list/item-table-list/columns/title-artist-column.tsx @@ -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 ( + +
+ + {item.name as string} + +
+ +
+
+
+ ); + } + + if (row === null) { + return ; + } + + return ; +}; + +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 ( + +
+ + {row.name as string} + {song?.trackSubtitle && props.itemType !== LibraryItem.QUEUE_SONG && ( + + {' ('} + {song.trackSubtitle} + {')'} + + )} + +
+ +
+
+
+ ); + } + + 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 ( + + + + {title} + + + ); + } + + if (row === null) { + return ; + } + + return ; +}; + +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 ; + default: + return ; + } +}; diff --git a/src/renderer/components/item-list/item-table-list/default-columns.ts b/src/renderer/components/item-list/item-table-list/default-columns.ts index b2233f5d4..e35a0dd40 100644 --- a/src/renderer/components/item-list/item-table-list/default-columns.ts +++ b/src/renderer/components/item-list/item-table-list/default-columns.ts @@ -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, diff --git a/src/renderer/components/item-list/item-table-list/item-table-list-column.tsx b/src/renderer/components/item-list/item-table-list/item-table-list-column.tsx index 28a541072..43a3a062a 100644 --- a/src/renderer/components/item-list/item-table-list/item-table-list-column.tsx +++ b/src/renderer/components/item-list/item-table-list/item-table-list-column.tsx @@ -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 ; + case TableColumn.TITLE_ARTIST: + return ( + + ); + case TableColumn.TITLE_COMBINED: return ( { case TableColumn.TITLE: return ; + case TableColumn.TITLE_ARTIST: + return ; + case TableColumn.TITLE_COMBINED: return ( @@ -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 = { 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, diff --git a/src/renderer/store/settings.store.ts b/src/renderer/store/settings.store.ts index 2edb2b745..a65711f7b 100644 --- a/src/renderer/store/settings.store.ts +++ b/src/renderer/store/settings.store.ts @@ -1958,10 +1958,52 @@ export const useSettingsStore = createWithEqualityFn()( } } + 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, }, ), ); diff --git a/src/shared/types/types.ts b/src/shared/types/types.ts index 3c6e81e6f..0aa56b385 100644 --- a/src/shared/types/types.ts +++ b/src/shared/types/types.ts @@ -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',