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; table-layout: fixed;
} }
.row .track-col-number { .row .track-header-cell {
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;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
.row .track-col-duration { .row .track-cell {
width: 4rem;
min-width: 4rem;
max-width: 4rem;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; 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; white-space: nowrap;
} }
@@ -18,17 +18,24 @@ import {
useItemListState, useItemListState,
useItemSelectionState, useItemSelectionState,
} from '/@/renderer/components/item-list/helpers/item-list-state'; } 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 { 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 { albumQueries } from '/@/renderer/features/albums/api/album-api';
import { usePlayer } from '/@/renderer/features/player/context/player-context'; import { usePlayer } from '/@/renderer/features/player/context/player-context';
import { useIsMutatingCreateFavorite } from '/@/renderer/features/shared/mutations/create-favorite-mutation'; import { useIsMutatingCreateFavorite } from '/@/renderer/features/shared/mutations/create-favorite-mutation';
import { useIsMutatingDeleteFavorite } from '/@/renderer/features/shared/mutations/delete-favorite-mutation'; import { useIsMutatingDeleteFavorite } from '/@/renderer/features/shared/mutations/delete-favorite-mutation';
import { AppRoute } from '/@/renderer/router/routes'; import { AppRoute } from '/@/renderer/router/routes';
import { useSettingsStore } from '/@/renderer/store';
import { Icon } from '/@/shared/components/icon/icon'; import { Icon } from '/@/shared/components/icon/icon';
import { ReadOnlyRating } from '/@/shared/components/read-only-rating/read-only-rating'; import { ReadOnlyRating } from '/@/shared/components/read-only-rating/read-only-rating';
import { Skeleton } from '/@/shared/components/skeleton/skeleton'; import { Skeleton } from '/@/shared/components/skeleton/skeleton';
import { Album, LibraryItem, Song } from '/@/shared/types/domain-types'; import { Album, LibraryItem, Song } from '/@/shared/types/domain-types';
import { ItemListKey, TableColumn } from '/@/shared/types/types';
interface ItemDetailListProps { interface ItemDetailListProps {
currentPage?: number; currentPage?: number;
@@ -45,22 +52,28 @@ interface ItemDetailListProps {
interface RowData { interface RowData {
controls?: ItemControls; controls?: ItemControls;
data: unknown[]; data: unknown[];
enableTrackTableHeader: boolean;
getItem?: (index: number) => unknown; getItem?: (index: number) => unknown;
internalState: ItemListStateActions; internalState: ItemListStateActions;
isMutatingFavorite: boolean; isMutatingFavorite: boolean;
queryClient: ReturnType<typeof useQueryClient>; queryClient: ReturnType<typeof useQueryClient>;
registerSongs: (albumId: string, songs: Song[]) => void; registerSongs: (albumId: string, songs: Song[]) => void;
trackColumns: ItemTableListColumnConfig[];
} }
interface TrackRowProps { interface TrackRowProps {
columns: ItemTableListColumnConfig[];
internalState: ItemListStateActions; internalState: ItemListStateActions;
isMutatingFavorite: boolean; isMutatingFavorite: boolean;
onFavoriteClick: (song: Song) => void; onFavoriteClick: (song: Song) => void;
song: Song; song: Song;
} }
const textAlignFromAlign = (align: ItemTableListColumnConfig['align']) =>
align === 'start' ? 'left' : align === 'end' ? 'right' : 'center';
const TrackRow = memo( const TrackRow = memo(
({ internalState, isMutatingFavorite, onFavoriteClick, song }: TrackRowProps) => { ({ columns, internalState, isMutatingFavorite, onFavoriteClick, song }: TrackRowProps) => {
const playerContext = usePlayer(); const playerContext = usePlayer();
const { dragRef, isDragging } = useItemDragDropState<HTMLTableRowElement>({ const { dragRef, isDragging } = useItemDragDropState<HTMLTableRowElement>({
enableDrag: true, enableDrag: true,
@@ -166,33 +179,75 @@ const TrackRow = memo(
onClick={handleRowClick} onClick={handleRowClick}
ref={dragRef ?? undefined} ref={dragRef ?? undefined}
> >
<td className={styles.trackColNumber} style={{ fontFamily: 'monospace' }}> {columns.map((col) => {
{discAndCol} const widthStyle = col.autoSize
</td> ? { minWidth: col.width }
<td className={styles.trackColTitle}>{song.name}</td> : {
<td className={styles.trackColDuration} style={{ fontFamily: 'monospace' }}> maxWidth: col.width,
{formatDuration(song.duration)} minWidth: col.width,
</td> width: col.width,
<td className={styles.trackColFavorite}> };
<div const style: React.CSSProperties = {
aria-disabled={isMutatingFavorite} fontFamily:
onClick={(event) => { col.id === TableColumn.DURATION || col.id === TableColumn.TRACK_NUMBER
event.stopPropagation(); ? 'monospace'
event.preventDefault(); : undefined,
onFavoriteClick(song); textAlign: textAlignFromAlign(col.align),
}} ...widthStyle,
onDoubleClick={(event) => { };
event.stopPropagation();
event.preventDefault(); let content: React.ReactNode;
}} switch (col.id) {
role="button" case TableColumn.DISC_NUMBER:
> case TableColumn.TRACK_NUMBER:
<Icon icon="favorite" size="xs" /> content = discAndCol;
</div> break;
</td> case TableColumn.DURATION:
<td className={styles.trackColRating}> content = formatDuration(song.duration);
<ReadOnlyRating size="md" value={song.userRating} /> break;
</td> 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> </tr>
); );
}, },
@@ -206,12 +261,14 @@ const RowContent = memo(
({ ({
controls, controls,
data, data,
enableTrackTableHeader,
getItem, getItem,
index, index,
internalState, internalState,
isMutatingFavorite, isMutatingFavorite,
queryClient, queryClient,
registerSongs, registerSongs,
trackColumns,
}: RowContentProps) => { }: RowContentProps) => {
const [showControls, setShowControls] = useState(false); const [showControls, setShowControls] = useState(false);
const item = useMemo(() => { const item = useMemo(() => {
@@ -323,6 +380,7 @@ const RowContent = memo(
<tbody> <tbody>
{songs.map((song) => ( {songs.map((song) => (
<TrackRow <TrackRow
columns={trackColumns}
internalState={internalState} internalState={internalState}
isMutatingFavorite={isMutatingFavorite} isMutatingFavorite={isMutatingFavorite}
key={song.id} key={song.id}
@@ -339,12 +397,14 @@ const RowContent = memo(
(prev, next) => (prev, next) =>
prev.index === next.index && prev.index === next.index &&
prev.data === next.data && prev.data === next.data &&
prev.enableTrackTableHeader === next.enableTrackTableHeader &&
prev.getItem === next.getItem && prev.getItem === next.getItem &&
prev.internalState === next.internalState && prev.internalState === next.internalState &&
prev.queryClient === next.queryClient && prev.queryClient === next.queryClient &&
prev.isMutatingFavorite === next.isMutatingFavorite && prev.isMutatingFavorite === next.isMutatingFavorite &&
prev.controls === next.controls && prev.controls === next.controls &&
prev.registerSongs === next.registerSongs, prev.registerSongs === next.registerSongs &&
prev.trackColumns === next.trackColumns,
); );
RowContent.displayName = 'RowContent'; RowContent.displayName = 'RowContent';
@@ -416,6 +476,25 @@ export const ItemDetailList = ({
const internalState = useItemListState(getDataFn, extractRowIdSong); 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( const handleRowsRendered = useCallback(
(range: { startIndex: number; stopIndex: number }) => { (range: { startIndex: number; stopIndex: number }) => {
if (onRangeChanged) { if (onRangeChanged) {
@@ -444,20 +523,24 @@ export const ItemDetailList = ({
() => ({ () => ({
controls, controls,
data: dataSource, data: dataSource,
enableTrackTableHeader,
getItem, getItem,
internalState, internalState,
isMutatingFavorite, isMutatingFavorite,
queryClient, queryClient,
registerSongs, registerSongs,
trackColumns,
}), }),
[ [
controls, controls,
dataSource, dataSource,
enableTrackTableHeader,
getItem, getItem,
internalState, internalState,
isMutatingFavorite, isMutatingFavorite,
queryClient, queryClient,
registerSongs, registerSongs,
trackColumns,
], ],
); );
@@ -1,7 +1,10 @@
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; 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 { useListContext } from '/@/renderer/context/list-context';
import { useAlbumListFilters } from '/@/renderer/features/albums/hooks/use-album-list-filters'; import { useAlbumListFilters } from '/@/renderer/features/albums/hooks/use-album-list-filters';
import { ListConfigMenu } from '/@/renderer/features/shared/components/list-config-menu'; import { ListConfigMenu } from '/@/renderer/features/shared/components/list-config-menu';
@@ -94,6 +97,17 @@ export const AlbumListHeaderFilters = ({ toggleGenreTarget }: { toggleGenreTarge
<Group gap="sm" wrap="nowrap"> <Group gap="sm" wrap="nowrap">
<ListDisplayTypeToggleButton listKey={ItemListKey.ALBUM} /> <ListDisplayTypeToggleButton listKey={ItemListKey.ALBUM} />
<ListConfigMenu <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} listKey={ItemListKey.ALBUM}
tableColumnsData={ALBUM_TABLE_COLUMNS} 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 { export interface ListConfigMenuDisplayTypeConfig {
disabled?: boolean; disabled?: boolean;
hidden?: boolean; hidden?: boolean;
@@ -84,6 +90,9 @@ export interface ListConfigMenuOptionConfig {
} }
export interface ListConfigMenuOptionsConfig { export interface ListConfigMenuOptionsConfig {
detail?: {
[key: string]: ListConfigMenuOptionConfig;
};
grid?: { grid?: {
[key: string]: ListConfigMenuOptionConfig; [key: string]: ListConfigMenuOptionConfig;
}; };
@@ -94,6 +103,7 @@ export interface ListConfigMenuOptionsConfig {
interface ListConfigMenuProps { interface ListConfigMenuProps {
buttonProps?: ActionIconProps; buttonProps?: ActionIconProps;
detailConfig?: ListConfigMenuDetailConfig;
displayTypes?: ListConfigMenuDisplayTypeConfig[]; displayTypes?: ListConfigMenuDisplayTypeConfig[];
listKey: ItemListKey; listKey: ItemListKey;
optionsConfig?: ListConfigMenuOptionsConfig; optionsConfig?: ListConfigMenuOptionsConfig;
@@ -181,6 +191,18 @@ const Config = ({
...props ...props
}: ListConfigMenuProps & { displayType: ListDisplayType }) => { }: ListConfigMenuProps & { displayType: ListDisplayType }) => {
switch (displayType) { 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: case ListDisplayType.GRID:
return ( return (
<GridConfig <GridConfig
@@ -199,10 +221,6 @@ const Config = ({
/> />
); );
case ListDisplayType.DETAIL:
// Detail view doesn't have specific configuration options
return null;
default: default:
return null; return null;
} }