mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-07 04:20:12 +02:00
refactor to reuse ItemTableListColumnConfig for detail columns
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user