refactor to reuse ItemTableListColumnConfig for detail columns

This commit is contained in:
jeffvli
2026-02-08 19:48:57 -08:00
parent b8aa006b1c
commit 3d67b02724
4 changed files with 152 additions and 75 deletions
@@ -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;
}
@@ -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<typeof useQueryClient>;
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<HTMLTableRowElement>({
enableDrag: true,
@@ -166,33 +179,75 @@ const TrackRow = memo(
onClick={handleRowClick}
ref={dragRef ?? undefined}
>
<td className={styles.trackColNumber} style={{ fontFamily: 'monospace' }}>
{discAndCol}
</td>
<td className={styles.trackColTitle}>{song.name}</td>
<td className={styles.trackColDuration} style={{ fontFamily: 'monospace' }}>
{formatDuration(song.duration)}
</td>
<td className={styles.trackColFavorite}>
<div
aria-disabled={isMutatingFavorite}
onClick={(event) => {
event.stopPropagation();
event.preventDefault();
onFavoriteClick(song);
}}
onDoubleClick={(event) => {
event.stopPropagation();
event.preventDefault();
}}
role="button"
>
<Icon icon="favorite" size="xs" />
</div>
</td>
<td className={styles.trackColRating}>
<ReadOnlyRating size="md" value={song.userRating} />
</td>
{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 = (
<div
aria-disabled={isMutatingFavorite}
onClick={(event) => {
event.stopPropagation();
event.preventDefault();
onFavoriteClick(song);
}}
onDoubleClick={(event) => {
event.stopPropagation();
event.preventDefault();
}}
role="button"
>
<Icon icon="favorite" size="xs" />
</div>
);
break;
case TableColumn.USER_RATING:
content = (
<ReadOnlyRating size="md" value={song.userRating ?? undefined} />
);
break;
default: {
const raw = (song as Record<string, unknown>)[col.id];
content =
raw !== undefined && raw !== null && typeof raw !== 'object'
? String(raw)
: '—';
break;
}
}
return (
<td className={styles.trackCell} key={col.id} style={style}>
{content}
</td>
);
})}
</tr>
);
},
@@ -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(
<tbody>
{songs.map((song) => (
<TrackRow
columns={trackColumns}
internalState={internalState}
isMutatingFavorite={isMutatingFavorite}
key={song.id}
@@ -339,12 +397,14 @@ const RowContent = memo(
(prev, next) =>
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,
],
);
@@ -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
<Group gap="sm" wrap="nowrap">
<ListDisplayTypeToggleButton listKey={ItemListKey.ALBUM} />
<ListConfigMenu
detailConfig={{
listKey: ItemListKey.ALBUM_DETAIL,
optionsConfig: {
autoFitColumns: { hidden: true },
enableHeader: { hidden: true },
itemsPerPage: { hidden: true },
pagination: { hidden: true },
size: { hidden: true },
},
tableColumnsData: SONG_TABLE_COLUMNS,
}}
listKey={ItemListKey.ALBUM}
tableColumnsData={ALBUM_TABLE_COLUMNS}
/>
@@ -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 (
<TableConfig
listKey={props.detailConfig.listKey}
optionsConfig={props.detailConfig.optionsConfig}
tableColumnsData={props.detailConfig.tableColumnsData}
/>
);
}
return null;
case ListDisplayType.GRID:
return (
<GridConfig
@@ -199,10 +221,6 @@ const Config = ({
/>
);
case ListDisplayType.DETAIL:
// Detail view doesn't have specific configuration options
return null;
default:
return null;
}