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