From 3d67b02724753a7c92f95e503b59213c7f953da3 Mon Sep 17 00:00:00 2001 From: jeffvli Date: Sun, 8 Feb 2026 19:48:57 -0800 Subject: [PATCH] refactor to reuse ItemTableListColumnConfig for detail columns --- .../item-detail/item-detail.module.css | 42 +---- .../components/item-detail/item-detail.tsx | 143 ++++++++++++++---- .../components/album-list-header-filters.tsx | 16 +- .../shared/components/list-config-menu.tsx | 26 +++- 4 files changed, 152 insertions(+), 75 deletions(-) diff --git a/src/renderer/components/item-detail/item-detail.module.css b/src/renderer/components/item-detail/item-detail.module.css index 21ce8d3f9..68ed87ecc 100644 --- a/src/renderer/components/item-detail/item-detail.module.css +++ b/src/renderer/components/item-detail/item-detail.module.css @@ -88,53 +88,15 @@ table-layout: fixed; } -.row .track-col-number { - width: 3.5rem; - min-width: 3.5rem; - max-width: 3.5rem; - overflow: hidden; - text-overflow: ellipsis; - color: var(--theme-colors-foreground-muted); - text-align: center; - white-space: nowrap; -} - -.row .track-col-title { - width: auto; - min-width: 0; +.row .track-header-cell { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.row .track-col-duration { - width: 4rem; - min-width: 4rem; - max-width: 4rem; +.row .track-cell { overflow: hidden; text-overflow: ellipsis; - color: var(--theme-colors-foreground-muted); - text-align: center; - white-space: nowrap; -} - -.row .track-col-favorite { - width: 2.5rem; - min-width: 2.5rem; - max-width: 2.5rem; - overflow: hidden; - text-overflow: ellipsis; - text-align: center; - white-space: nowrap; -} - -.row .track-col-rating { - width: 5.5rem; - min-width: 5.5rem; - max-width: 5.5rem; - overflow: hidden; - text-overflow: ellipsis; - text-align: center; white-space: nowrap; } diff --git a/src/renderer/components/item-detail/item-detail.tsx b/src/renderer/components/item-detail/item-detail.tsx index 619506987..ea4c931ac 100644 --- a/src/renderer/components/item-detail/item-detail.tsx +++ b/src/renderer/components/item-detail/item-detail.tsx @@ -18,17 +18,24 @@ import { useItemListState, useItemSelectionState, } from '/@/renderer/components/item-list/helpers/item-list-state'; +import { parseTableColumns } from '/@/renderer/components/item-list/helpers/parse-table-columns'; +import { + pickTableColumns, + SONG_TABLE_COLUMNS, +} from '/@/renderer/components/item-list/item-table-list/default-columns'; import { useItemDragDropState } from '/@/renderer/components/item-list/item-table-list/hooks/use-item-drag-drop-state'; -import { ItemControls } from '/@/renderer/components/item-list/types'; +import { ItemControls, ItemTableListColumnConfig } from '/@/renderer/components/item-list/types'; import { albumQueries } from '/@/renderer/features/albums/api/album-api'; import { usePlayer } from '/@/renderer/features/player/context/player-context'; import { useIsMutatingCreateFavorite } from '/@/renderer/features/shared/mutations/create-favorite-mutation'; import { useIsMutatingDeleteFavorite } from '/@/renderer/features/shared/mutations/delete-favorite-mutation'; import { AppRoute } from '/@/renderer/router/routes'; +import { useSettingsStore } from '/@/renderer/store'; import { Icon } from '/@/shared/components/icon/icon'; import { ReadOnlyRating } from '/@/shared/components/read-only-rating/read-only-rating'; import { Skeleton } from '/@/shared/components/skeleton/skeleton'; import { Album, LibraryItem, Song } from '/@/shared/types/domain-types'; +import { ItemListKey, TableColumn } from '/@/shared/types/types'; interface ItemDetailListProps { currentPage?: number; @@ -45,22 +52,28 @@ interface ItemDetailListProps { interface RowData { controls?: ItemControls; data: unknown[]; + enableTrackTableHeader: boolean; getItem?: (index: number) => unknown; internalState: ItemListStateActions; isMutatingFavorite: boolean; queryClient: ReturnType; registerSongs: (albumId: string, songs: Song[]) => void; + trackColumns: ItemTableListColumnConfig[]; } interface TrackRowProps { + columns: ItemTableListColumnConfig[]; internalState: ItemListStateActions; isMutatingFavorite: boolean; onFavoriteClick: (song: Song) => void; song: Song; } +const textAlignFromAlign = (align: ItemTableListColumnConfig['align']) => + align === 'start' ? 'left' : align === 'end' ? 'right' : 'center'; + const TrackRow = memo( - ({ internalState, isMutatingFavorite, onFavoriteClick, song }: TrackRowProps) => { + ({ columns, internalState, isMutatingFavorite, onFavoriteClick, song }: TrackRowProps) => { const playerContext = usePlayer(); const { dragRef, isDragging } = useItemDragDropState({ enableDrag: true, @@ -166,33 +179,75 @@ const TrackRow = memo( onClick={handleRowClick} ref={dragRef ?? undefined} > - - {discAndCol} - - {song.name} - - {formatDuration(song.duration)} - - -
{ - event.stopPropagation(); - event.preventDefault(); - onFavoriteClick(song); - }} - onDoubleClick={(event) => { - event.stopPropagation(); - event.preventDefault(); - }} - role="button" - > - -
- - - - + {columns.map((col) => { + const widthStyle = col.autoSize + ? { minWidth: col.width } + : { + maxWidth: col.width, + minWidth: col.width, + width: col.width, + }; + const style: React.CSSProperties = { + fontFamily: + col.id === TableColumn.DURATION || col.id === TableColumn.TRACK_NUMBER + ? 'monospace' + : undefined, + textAlign: textAlignFromAlign(col.align), + ...widthStyle, + }; + + let content: React.ReactNode; + switch (col.id) { + case TableColumn.DISC_NUMBER: + case TableColumn.TRACK_NUMBER: + content = discAndCol; + break; + case TableColumn.DURATION: + content = formatDuration(song.duration); + break; + case TableColumn.TITLE: + content = song.name; + break; + case TableColumn.USER_FAVORITE: + content = ( +
{ + event.stopPropagation(); + event.preventDefault(); + onFavoriteClick(song); + }} + onDoubleClick={(event) => { + event.stopPropagation(); + event.preventDefault(); + }} + role="button" + > + +
+ ); + break; + case TableColumn.USER_RATING: + content = ( + + ); + break; + default: { + const raw = (song as Record)[col.id]; + content = + raw !== undefined && raw !== null && typeof raw !== 'object' + ? String(raw) + : '—'; + break; + } + } + + return ( + + {content} + + ); + })} ); }, @@ -206,12 +261,14 @@ const RowContent = memo( ({ controls, data, + enableTrackTableHeader, getItem, index, internalState, isMutatingFavorite, queryClient, registerSongs, + trackColumns, }: RowContentProps) => { const [showControls, setShowControls] = useState(false); const item = useMemo(() => { @@ -323,6 +380,7 @@ const RowContent = memo( {songs.map((song) => ( prev.index === next.index && prev.data === next.data && + prev.enableTrackTableHeader === next.enableTrackTableHeader && prev.getItem === next.getItem && prev.internalState === next.internalState && prev.queryClient === next.queryClient && prev.isMutatingFavorite === next.isMutatingFavorite && prev.controls === next.controls && - prev.registerSongs === next.registerSongs, + prev.registerSongs === next.registerSongs && + prev.trackColumns === next.trackColumns, ); RowContent.displayName = 'RowContent'; @@ -416,6 +476,25 @@ export const ItemDetailList = ({ const internalState = useItemListState(getDataFn, extractRowIdSong); + const tableConfig = useSettingsStore((state) => state.lists[ItemListKey.ALBUM_DETAIL]?.table); + const trackColumns = useMemo((): ItemTableListColumnConfig[] => { + const raw = tableConfig?.columns; + if (raw && raw.length > 0) { + return parseTableColumns(raw); + } + return pickTableColumns({ + columns: SONG_TABLE_COLUMNS, + enabledColumns: [ + TableColumn.TRACK_NUMBER, + TableColumn.TITLE, + TableColumn.DURATION, + TableColumn.USER_FAVORITE, + TableColumn.USER_RATING, + ], + }); + }, [tableConfig?.columns]); + const enableTrackTableHeader = tableConfig?.enableHeader ?? false; + const handleRowsRendered = useCallback( (range: { startIndex: number; stopIndex: number }) => { if (onRangeChanged) { @@ -444,20 +523,24 @@ export const ItemDetailList = ({ () => ({ controls, data: dataSource, + enableTrackTableHeader, getItem, internalState, isMutatingFavorite, queryClient, registerSongs, + trackColumns, }), [ controls, dataSource, + enableTrackTableHeader, getItem, internalState, isMutatingFavorite, queryClient, registerSongs, + trackColumns, ], ); diff --git a/src/renderer/features/albums/components/album-list-header-filters.tsx b/src/renderer/features/albums/components/album-list-header-filters.tsx index 207a6f7aa..b0c011640 100644 --- a/src/renderer/features/albums/components/album-list-header-filters.tsx +++ b/src/renderer/features/albums/components/album-list-header-filters.tsx @@ -1,7 +1,10 @@ import { useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { ALBUM_TABLE_COLUMNS } from '/@/renderer/components/item-list/item-table-list/default-columns'; +import { + ALBUM_TABLE_COLUMNS, + SONG_TABLE_COLUMNS, +} from '/@/renderer/components/item-list/item-table-list/default-columns'; import { useListContext } from '/@/renderer/context/list-context'; import { useAlbumListFilters } from '/@/renderer/features/albums/hooks/use-album-list-filters'; import { ListConfigMenu } from '/@/renderer/features/shared/components/list-config-menu'; @@ -94,6 +97,17 @@ export const AlbumListHeaderFilters = ({ toggleGenreTarget }: { toggleGenreTarge diff --git a/src/renderer/features/shared/components/list-config-menu.tsx b/src/renderer/features/shared/components/list-config-menu.tsx index 07b5c7e86..8bd47cbf0 100644 --- a/src/renderer/features/shared/components/list-config-menu.tsx +++ b/src/renderer/features/shared/components/list-config-menu.tsx @@ -72,6 +72,12 @@ export const ListConfigBooleanControl = ({ ); }; +export interface ListConfigMenuDetailConfig { + listKey: ItemListKey; + optionsConfig?: ListConfigMenuOptionsConfig['detail']; + tableColumnsData: { label: string; value: string }[]; +} + export interface ListConfigMenuDisplayTypeConfig { disabled?: boolean; hidden?: boolean; @@ -84,6 +90,9 @@ export interface ListConfigMenuOptionConfig { } export interface ListConfigMenuOptionsConfig { + detail?: { + [key: string]: ListConfigMenuOptionConfig; + }; grid?: { [key: string]: ListConfigMenuOptionConfig; }; @@ -94,6 +103,7 @@ export interface ListConfigMenuOptionsConfig { interface ListConfigMenuProps { buttonProps?: ActionIconProps; + detailConfig?: ListConfigMenuDetailConfig; displayTypes?: ListConfigMenuDisplayTypeConfig[]; listKey: ItemListKey; optionsConfig?: ListConfigMenuOptionsConfig; @@ -181,6 +191,18 @@ const Config = ({ ...props }: ListConfigMenuProps & { displayType: ListDisplayType }) => { switch (displayType) { + case ListDisplayType.DETAIL: + if (props.detailConfig) { + return ( + + ); + } + return null; + case ListDisplayType.GRID: return ( ); - case ListDisplayType.DETAIL: - // Detail view doesn't have specific configuration options - return null; - default: return null; }