mirror of
https://github.com/jeffvli/feishin.git
synced 2026-06-20 19:04:23 +02:00
660 lines
26 KiB
TypeScript
660 lines
26 KiB
TypeScript
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
|
import clsx from 'clsx';
|
|
import throttle from 'lodash/throttle';
|
|
import { AnimatePresence } from 'motion/react';
|
|
import { useOverlayScrollbars } from 'overlayscrollbars-react';
|
|
import { memo, type ReactElement, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
import { generatePath, Link } from 'react-router';
|
|
import { List, RowComponentProps, useDynamicRowHeight } from 'react-window-v2';
|
|
|
|
import styles from './item-detail.module.css';
|
|
|
|
import { ItemCardControls } from '/@/renderer/components/item-card/item-card-controls';
|
|
import { ItemImage } from '/@/renderer/components/item-image/item-image';
|
|
import { useDefaultItemListControls } from '/@/renderer/components/item-list/helpers/item-list-controls';
|
|
import {
|
|
ItemListStateActions,
|
|
ItemListStateItemWithRequiredProperties,
|
|
useItemListState,
|
|
useItemSelectionState,
|
|
} from '/@/renderer/components/item-list/helpers/item-list-state';
|
|
import { parseTableColumns } from '/@/renderer/components/item-list/helpers/parse-table-columns';
|
|
import { getDetailListCellComponent } from '/@/renderer/components/item-list/item-detail-list/columns';
|
|
import {
|
|
getTrackColumnFixed,
|
|
shouldShowHoverOnlyColumnContent,
|
|
} from '/@/renderer/components/item-list/item-detail-list/utils';
|
|
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, 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 { 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;
|
|
data?: unknown[];
|
|
getItem?: (index: number) => unknown;
|
|
internalState?: ItemListStateActions;
|
|
itemCount?: number;
|
|
items?: unknown[];
|
|
onRangeChanged?: (range: { startIndex: number; stopIndex: number }) => Promise<void> | void;
|
|
rowHeight?: number;
|
|
}
|
|
|
|
interface RowData {
|
|
columnWidthPercents: number[];
|
|
controls?: ItemControls;
|
|
data: unknown[];
|
|
enableAlternateRowColors: boolean;
|
|
enableHorizontalBorders: boolean;
|
|
enableRowHoverHighlight: boolean;
|
|
enableVerticalBorders: boolean;
|
|
getItem?: (index: number) => unknown;
|
|
internalState: ItemListStateActions;
|
|
isMutatingFavorite: boolean;
|
|
registerSongs: (albumId: string, songs: Song[]) => void;
|
|
trackColumns: ItemTableListColumnConfig[];
|
|
trackTableSize: 'compact' | 'default' | 'large';
|
|
}
|
|
|
|
interface TrackRowProps {
|
|
columns: ItemTableListColumnConfig[];
|
|
columnWidthPercents: number[];
|
|
controls?: ItemControls;
|
|
enableAlternateRowColors: boolean;
|
|
enableHorizontalBorders: boolean;
|
|
enableRowHoverHighlight: boolean;
|
|
enableVerticalBorders: boolean;
|
|
internalState: ItemListStateActions;
|
|
isMutatingFavorite: boolean;
|
|
onFavoriteClick: (song: Song) => void;
|
|
rowIndex: number;
|
|
size: 'compact' | 'default' | 'large';
|
|
song: Song;
|
|
}
|
|
|
|
const textAlignFromAlign = (align: ItemTableListColumnConfig['align']) =>
|
|
align === 'start' ? 'left' : align === 'end' ? 'right' : 'center';
|
|
|
|
const TrackRow = memo(
|
|
({
|
|
columns,
|
|
columnWidthPercents,
|
|
controls,
|
|
enableAlternateRowColors,
|
|
enableHorizontalBorders,
|
|
enableRowHoverHighlight,
|
|
enableVerticalBorders,
|
|
internalState,
|
|
isMutatingFavorite,
|
|
onFavoriteClick,
|
|
rowIndex,
|
|
size,
|
|
song,
|
|
}: TrackRowProps) => {
|
|
const playerContext = usePlayer();
|
|
const { dragRef, isDragging } = useItemDragDropState<HTMLDivElement>({
|
|
enableDrag: true,
|
|
internalState,
|
|
isDataRow: true,
|
|
item: song,
|
|
itemType: LibraryItem.SONG,
|
|
playerContext,
|
|
});
|
|
const [isRowHovered, setIsRowHovered] = useState(false);
|
|
const isSelected = useItemSelectionState(internalState, song.id);
|
|
|
|
const handleRowClick = useCallback(
|
|
(e: React.MouseEvent) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
if (e.ctrlKey || e.metaKey) {
|
|
internalState.toggleSelected(song);
|
|
} else if (e.shiftKey) {
|
|
const selectedItems = internalState.getSelected();
|
|
const lastSelectedItem = selectedItems[selectedItems.length - 1];
|
|
|
|
if (
|
|
lastSelectedItem &&
|
|
typeof lastSelectedItem === 'object' &&
|
|
lastSelectedItem !== null
|
|
) {
|
|
const data = internalState.getData();
|
|
const validData = data.filter((d) => d && typeof d === 'object');
|
|
const lastRowId = internalState.extractRowId(lastSelectedItem);
|
|
if (!lastRowId) {
|
|
internalState.setSelected([song]);
|
|
return;
|
|
}
|
|
const lastIndex = internalState.findItemIndex(lastRowId);
|
|
const currentIndex = internalState.findItemIndex(song.id);
|
|
|
|
if (lastIndex !== -1 && currentIndex !== -1) {
|
|
const startIndex = Math.min(lastIndex, currentIndex);
|
|
const stopIndex = Math.max(lastIndex, currentIndex);
|
|
const rangeItems: ItemListStateItemWithRequiredProperties[] = [];
|
|
for (let i = startIndex; i <= stopIndex; i++) {
|
|
const rangeItem = validData[i];
|
|
if (
|
|
rangeItem &&
|
|
typeof rangeItem === 'object' &&
|
|
'_serverId' in rangeItem &&
|
|
'_itemType' in rangeItem
|
|
) {
|
|
const rangeRowId = internalState.extractRowId(rangeItem);
|
|
if (rangeRowId) {
|
|
rangeItems.push(
|
|
rangeItem as ItemListStateItemWithRequiredProperties,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
const currentSelected = internalState.getSelected();
|
|
const newSelected = [
|
|
...currentSelected.filter(
|
|
(
|
|
selectedItem,
|
|
): selectedItem is ItemListStateItemWithRequiredProperties =>
|
|
typeof selectedItem === 'object' && selectedItem !== null,
|
|
),
|
|
];
|
|
rangeItems.forEach((rangeItem) => {
|
|
const rangeRowId = internalState.extractRowId(rangeItem);
|
|
if (
|
|
rangeRowId &&
|
|
!newSelected.some(
|
|
(selected) =>
|
|
internalState.extractRowId(selected) === rangeRowId,
|
|
)
|
|
) {
|
|
newSelected.push(rangeItem);
|
|
}
|
|
});
|
|
internalState.setSelected(newSelected);
|
|
} else {
|
|
internalState.setSelected([song]);
|
|
}
|
|
} else {
|
|
internalState.setSelected([song]);
|
|
}
|
|
} else {
|
|
internalState.setSelected([song]);
|
|
}
|
|
},
|
|
[internalState, song],
|
|
);
|
|
|
|
return (
|
|
<div
|
|
className={clsx(styles.trackRow, {
|
|
[styles.trackRowAlternateEven]: enableAlternateRowColors && rowIndex % 2 === 0,
|
|
[styles.trackRowAlternateOdd]: enableAlternateRowColors && rowIndex % 2 === 1,
|
|
[styles.trackRowDragging]: isDragging,
|
|
[styles.trackRowHorizontalBorderVisible]:
|
|
enableHorizontalBorders && rowIndex > 0,
|
|
[styles.trackRowHoverHighlightEnabled]: enableRowHoverHighlight,
|
|
[styles.trackRowSelected]: isSelected,
|
|
[styles.trackRowSizeCompact]: size === 'compact',
|
|
[styles.trackRowSizeDefault]: size === 'default',
|
|
[styles.trackRowSizeLarge]: size === 'large',
|
|
[styles.trackRowWithHorizontalBorder]: rowIndex > 0,
|
|
})}
|
|
onClick={handleRowClick}
|
|
onMouseEnter={() => setIsRowHovered(true)}
|
|
onMouseLeave={() => setIsRowHovered(false)}
|
|
ref={dragRef ?? undefined}
|
|
role="row"
|
|
>
|
|
{columns.map((col, colIndex) => {
|
|
const percent = columnWidthPercents[colIndex] ?? 0;
|
|
const { fixedWidth, isFixedColumn } = getTrackColumnFixed(col.id);
|
|
const style: React.CSSProperties = {
|
|
flex: isFixedColumn ? `0 0 ${fixedWidth}px` : `${percent} 1 0`,
|
|
fontFamily:
|
|
col.id === TableColumn.DURATION || col.id === TableColumn.TRACK_NUMBER
|
|
? 'monospace'
|
|
: undefined,
|
|
minWidth: isFixedColumn ? fixedWidth : 0,
|
|
textAlign: textAlignFromAlign(col.align),
|
|
};
|
|
const CellComponent = getDetailListCellComponent(col.id);
|
|
const isTitleColumn = col.id === TableColumn.TITLE;
|
|
const isImageColumn = col.id === TableColumn.IMAGE;
|
|
const showHoverContent = shouldShowHoverOnlyColumnContent(
|
|
col.id,
|
|
isRowHovered,
|
|
song,
|
|
);
|
|
|
|
const content = showHoverContent ? (
|
|
<CellComponent
|
|
columnId={col.id}
|
|
controls={controls}
|
|
internalState={internalState}
|
|
isMutatingFavorite={isMutatingFavorite}
|
|
isRowHovered={isRowHovered}
|
|
onFavoriteClick={onFavoriteClick}
|
|
rowIndex={rowIndex}
|
|
size={size}
|
|
song={song}
|
|
/>
|
|
) : (
|
|
'\u00A0'
|
|
);
|
|
|
|
const isLastColumn = colIndex === columns.length - 1;
|
|
return (
|
|
<div
|
|
className={clsx(styles.trackCell, {
|
|
[styles.trackCellImage]: isImageColumn,
|
|
[styles.trackCellMuted]: !isTitleColumn,
|
|
[styles.trackCellVerticalBorderVisible]:
|
|
enableVerticalBorders && !isLastColumn,
|
|
[styles.trackCellWithVerticalBorder]: !isLastColumn,
|
|
})}
|
|
key={col.id}
|
|
role="cell"
|
|
style={style}
|
|
>
|
|
{content}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
},
|
|
);
|
|
|
|
TrackRow.displayName = 'TrackRow';
|
|
|
|
type RowContentProps = Omit<RowComponentProps<RowData>, 'style'>;
|
|
|
|
const RowContent = memo(
|
|
({
|
|
columnWidthPercents,
|
|
controls,
|
|
data,
|
|
enableAlternateRowColors,
|
|
enableHorizontalBorders,
|
|
enableRowHoverHighlight,
|
|
enableVerticalBorders,
|
|
getItem,
|
|
index,
|
|
internalState,
|
|
isMutatingFavorite,
|
|
registerSongs,
|
|
trackColumns,
|
|
trackTableSize,
|
|
}: RowContentProps) => {
|
|
const [showControls, setShowControls] = useState(false);
|
|
const item = useMemo(() => {
|
|
if (getItem) {
|
|
return getItem(index) as Album | undefined;
|
|
}
|
|
|
|
return (data?.[index] as Album | undefined) || undefined;
|
|
}, [data, getItem, index]);
|
|
|
|
const { data: songData } = useQuery({
|
|
enabled: !!item && !!item.id,
|
|
...albumQueries.detail({
|
|
query: {
|
|
id: item?.id || '',
|
|
},
|
|
serverId: item?._serverId || '',
|
|
}),
|
|
});
|
|
|
|
const songs = useMemo(() => {
|
|
return (
|
|
songData?.songs ||
|
|
Array.from({ length: item?.songCount || 0 }, (_, i) => ({
|
|
duration: 0,
|
|
id: `${item?.id}-${i}`,
|
|
name: '',
|
|
trackNumber: i + 1,
|
|
}))
|
|
);
|
|
}, [songData, item?.id, item?.songCount]);
|
|
|
|
useEffect(() => {
|
|
if (item?.id && songData?.songs?.length) {
|
|
registerSongs(item.id, songData.songs as Song[]);
|
|
}
|
|
}, [item?.id, registerSongs, songData?.songs]);
|
|
|
|
const onFavoriteClick = useCallback((song: Song) => {
|
|
// TODO: toggle favorite for song
|
|
void song;
|
|
}, []);
|
|
|
|
if (!item) {
|
|
return (
|
|
<>
|
|
<div className={styles.left}>
|
|
<div className={styles.metadata}>
|
|
<Skeleton className={styles.skeletonImage} />
|
|
<Skeleton className={styles.skeletonTitle} />
|
|
<Skeleton className={styles.skeletonArtist} />
|
|
</div>
|
|
</div>
|
|
<div className={styles.right}>
|
|
<div
|
|
className={clsx(styles.skeletonTracks, {
|
|
[styles.skeletonTracksSizeCompact]: trackTableSize === 'compact',
|
|
[styles.skeletonTracksSizeDefault]: trackTableSize === 'default',
|
|
[styles.skeletonTracksSizeLarge]: trackTableSize === 'large',
|
|
})}
|
|
>
|
|
{Array.from({ length: 10 }).map((_, i) => (
|
|
<div className={styles.skeletonTrackRow} key={i}>
|
|
<Skeleton className={styles.skeletonTrackCell} />
|
|
<Skeleton className={styles.skeletonTrackCellTitle} />
|
|
<Skeleton className={styles.skeletonTrackCell} />
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<div className={styles.left}>
|
|
<div className={styles.metadata}>
|
|
<Link
|
|
className={styles.imageWrapper}
|
|
onMouseEnter={() => setShowControls(true)}
|
|
onMouseLeave={() => setShowControls(false)}
|
|
state={{ item }}
|
|
to={generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, {
|
|
albumId: item.id,
|
|
})}
|
|
>
|
|
<ItemImage
|
|
className={styles.image}
|
|
id={item.imageId}
|
|
itemType={item._itemType}
|
|
type="itemCard"
|
|
/>
|
|
<AnimatePresence>
|
|
{controls && showControls && (
|
|
<ItemCardControls
|
|
controls={controls}
|
|
enableExpansion={false}
|
|
internalState={internalState}
|
|
item={item}
|
|
itemType={item._itemType}
|
|
showRating={true}
|
|
type="compact"
|
|
/>
|
|
)}
|
|
</AnimatePresence>
|
|
</Link>
|
|
<div className={styles.title}>{item.name}</div>
|
|
<div className={styles.artist}>{item.albumArtistName}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className={styles.right}>
|
|
<div className={styles.tracksTable} role="table">
|
|
{songs.map((song, rowIndex) => (
|
|
<TrackRow
|
|
columns={trackColumns}
|
|
columnWidthPercents={columnWidthPercents}
|
|
controls={controls}
|
|
enableAlternateRowColors={enableAlternateRowColors}
|
|
enableHorizontalBorders={enableHorizontalBorders}
|
|
enableRowHoverHighlight={enableRowHoverHighlight}
|
|
enableVerticalBorders={enableVerticalBorders}
|
|
internalState={internalState}
|
|
isMutatingFavorite={isMutatingFavorite}
|
|
key={song.id}
|
|
onFavoriteClick={onFavoriteClick}
|
|
rowIndex={rowIndex}
|
|
size={trackTableSize}
|
|
song={song as Song}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
},
|
|
(prev, next) =>
|
|
prev.index === next.index &&
|
|
prev.data === next.data &&
|
|
prev.columnWidthPercents === next.columnWidthPercents &&
|
|
prev.enableAlternateRowColors === next.enableAlternateRowColors &&
|
|
prev.enableHorizontalBorders === next.enableHorizontalBorders &&
|
|
prev.enableRowHoverHighlight === next.enableRowHoverHighlight &&
|
|
prev.enableVerticalBorders === next.enableVerticalBorders &&
|
|
prev.getItem === next.getItem &&
|
|
prev.internalState === next.internalState &&
|
|
prev.isMutatingFavorite === next.isMutatingFavorite &&
|
|
prev.controls === next.controls &&
|
|
prev.registerSongs === next.registerSongs &&
|
|
prev.trackColumns === next.trackColumns &&
|
|
prev.trackTableSize === next.trackTableSize,
|
|
);
|
|
|
|
RowContent.displayName = 'RowContent';
|
|
|
|
const RowComponent = memo((props: RowComponentProps<RowData>): ReactElement => {
|
|
const { style, ...rowContentProps } = props;
|
|
return (
|
|
<div className={styles.row} style={style}>
|
|
<RowContent {...rowContentProps} />
|
|
</div>
|
|
);
|
|
});
|
|
|
|
RowComponent.displayName = 'ItemDetailRow';
|
|
|
|
export const ItemDetailList = ({
|
|
currentPage,
|
|
data,
|
|
getItem,
|
|
itemCount: externalItemCount,
|
|
items,
|
|
onRangeChanged,
|
|
}: ItemDetailListProps) => {
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
const queryClient = useQueryClient();
|
|
const controls = useDefaultItemListControls();
|
|
const isMutatingCreateFavorite = useIsMutatingCreateFavorite();
|
|
const isMutatingDeleteFavorite = useIsMutatingDeleteFavorite();
|
|
const isMutatingFavorite = isMutatingCreateFavorite || isMutatingDeleteFavorite;
|
|
|
|
const rowHeight = useDynamicRowHeight({
|
|
defaultRowHeight: 300,
|
|
});
|
|
|
|
const isInfinite = data !== undefined || getItem !== undefined;
|
|
const isPaginated = items !== undefined || currentPage !== undefined;
|
|
|
|
const dataSource = useMemo(() => {
|
|
if (isInfinite && data) {
|
|
return data;
|
|
}
|
|
if (isPaginated && items) {
|
|
return items;
|
|
}
|
|
return [];
|
|
}, [data, isInfinite, isPaginated, items]);
|
|
|
|
const itemCount = useMemo(() => {
|
|
if (externalItemCount !== undefined) {
|
|
return externalItemCount;
|
|
}
|
|
return dataSource.length;
|
|
}, [dataSource.length, externalItemCount]);
|
|
|
|
// Accumulate songs from each row for selection/drag state (keyed by album id)
|
|
const songsByAlbumRef = useRef<Map<string, Song[]>>(new Map());
|
|
const registerSongs = useCallback((albumId: string, songs: Song[]) => {
|
|
songsByAlbumRef.current.set(albumId, songs);
|
|
}, []);
|
|
|
|
// Flattened songs in album order for ItemListState (selection/drag are per-song)
|
|
const getDataFn = useCallback(() => {
|
|
const map = songsByAlbumRef.current;
|
|
return dataSource.flatMap((album) => map.get((album as Album).id) ?? []);
|
|
}, [dataSource]);
|
|
|
|
const extractRowIdSong = useCallback((item: unknown) => (item as Song).id, []);
|
|
|
|
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 trackTableSize = tableConfig?.size ?? 'default';
|
|
const enableRowHoverHighlight = tableConfig?.enableRowHoverHighlight ?? true;
|
|
const enableAlternateRowColors = tableConfig?.enableAlternateRowColors ?? false;
|
|
const enableHorizontalBorders = tableConfig?.enableHorizontalBorders ?? false;
|
|
const enableVerticalBorders = tableConfig?.enableVerticalBorders ?? false;
|
|
|
|
const columnWidthPercents = useMemo(() => {
|
|
const total = trackColumns.reduce((sum, c) => sum + c.width, 0);
|
|
if (total <= 0) {
|
|
return trackColumns.map(() => 100 / Math.max(1, trackColumns.length));
|
|
}
|
|
return trackColumns.map((c) => (c.width / total) * 100);
|
|
}, [trackColumns]);
|
|
|
|
const handleRowsRendered = useCallback(
|
|
(range: { startIndex: number; stopIndex: number }) => {
|
|
if (onRangeChanged) {
|
|
onRangeChanged(range);
|
|
}
|
|
},
|
|
[onRangeChanged],
|
|
);
|
|
|
|
const throttledHandleRowsRendered = useMemo(
|
|
() =>
|
|
throttle(handleRowsRendered, 150, {
|
|
leading: true,
|
|
trailing: true,
|
|
}),
|
|
[handleRowsRendered],
|
|
);
|
|
|
|
useEffect(() => {
|
|
return () => {
|
|
throttledHandleRowsRendered.cancel();
|
|
};
|
|
}, [throttledHandleRowsRendered]);
|
|
|
|
const rowProps = useMemo<RowData>(
|
|
() => ({
|
|
columnWidthPercents,
|
|
controls,
|
|
data: dataSource,
|
|
enableAlternateRowColors,
|
|
enableHorizontalBorders,
|
|
enableRowHoverHighlight,
|
|
enableVerticalBorders,
|
|
getItem,
|
|
internalState,
|
|
isMutatingFavorite,
|
|
queryClient,
|
|
registerSongs,
|
|
trackColumns,
|
|
trackTableSize,
|
|
}),
|
|
[
|
|
columnWidthPercents,
|
|
controls,
|
|
dataSource,
|
|
enableAlternateRowColors,
|
|
enableHorizontalBorders,
|
|
enableRowHoverHighlight,
|
|
enableVerticalBorders,
|
|
getItem,
|
|
internalState,
|
|
isMutatingFavorite,
|
|
queryClient,
|
|
registerSongs,
|
|
trackColumns,
|
|
trackTableSize,
|
|
],
|
|
);
|
|
|
|
const [initialize, osInstance] = useOverlayScrollbars({
|
|
defer: false,
|
|
events: {
|
|
initialized(osInstance) {
|
|
const { viewport } = osInstance.elements();
|
|
viewport.style.overflowX = `var(--os-viewport-overflow-x)`;
|
|
},
|
|
},
|
|
options: {
|
|
overflow: { x: 'hidden', y: 'scroll' },
|
|
paddingAbsolute: true,
|
|
scrollbars: {
|
|
autoHide: 'leave',
|
|
autoHideDelay: 500,
|
|
pointers: ['mouse', 'pen', 'touch'],
|
|
theme: 'feishin-os-scrollbar',
|
|
visibility: 'visible',
|
|
},
|
|
},
|
|
});
|
|
|
|
useEffect(() => {
|
|
const { current: container } = containerRef;
|
|
|
|
if (!container || !container.firstElementChild) {
|
|
return;
|
|
}
|
|
|
|
const viewport = container.firstElementChild as HTMLElement;
|
|
|
|
initialize({
|
|
elements: { viewport },
|
|
target: container,
|
|
});
|
|
|
|
return () => osInstance()?.destroy();
|
|
}, [initialize, osInstance]);
|
|
|
|
return (
|
|
<div className={styles.container} ref={containerRef}>
|
|
<List
|
|
onRowsRendered={throttledHandleRowsRendered}
|
|
rowComponent={RowComponent as (props: RowComponentProps<RowData>) => ReactElement}
|
|
rowCount={itemCount}
|
|
rowHeight={rowHeight}
|
|
rowProps={rowProps}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|