mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-17 06:00:20 +02:00
add context to item list table
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
|||||||
TableColumnContainer,
|
TableColumnContainer,
|
||||||
TableColumnTextContainer,
|
TableColumnTextContainer,
|
||||||
} from '/@/renderer/components/item-list/item-table-list/item-table-list-column';
|
} from '/@/renderer/components/item-list/item-table-list/item-table-list-column';
|
||||||
|
import { useIsActiveRow } from '/@/renderer/components/item-list/item-table-list/item-table-list-context';
|
||||||
import { ItemListItem } from '/@/renderer/components/item-list/types';
|
import { ItemListItem } from '/@/renderer/components/item-list/types';
|
||||||
import { usePlayerStatus } from '/@/renderer/store';
|
import { usePlayerStatus } from '/@/renderer/store';
|
||||||
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||||
@@ -87,9 +88,7 @@ const DefaultRowIndexColumn = (props: ItemTableListInnerColumn) => {
|
|||||||
const QueueSongRowIndexColumn = (props: ItemTableListInnerColumn) => {
|
const QueueSongRowIndexColumn = (props: ItemTableListInnerColumn) => {
|
||||||
const status = usePlayerStatus();
|
const status = usePlayerStatus();
|
||||||
const song = props.data[props.rowIndex] as QueueSong;
|
const song = props.data[props.rowIndex] as QueueSong;
|
||||||
const isActive =
|
const isActive = useIsActiveRow(song?.id, song?._uniqueId);
|
||||||
!!props.activeRowId &&
|
|
||||||
(props.activeRowId === song?.id || props.activeRowId === song?._uniqueId);
|
|
||||||
|
|
||||||
const isActiveAndPlaying = isActive && status === PlayerStatus.PLAYING;
|
const isActiveAndPlaying = isActive && status === PlayerStatus.PLAYING;
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
ItemTableListInnerColumn,
|
ItemTableListInnerColumn,
|
||||||
TableColumnContainer,
|
TableColumnContainer,
|
||||||
} from '/@/renderer/components/item-list/item-table-list/item-table-list-column';
|
} from '/@/renderer/components/item-list/item-table-list/item-table-list-column';
|
||||||
|
import { useIsActiveRow } from '/@/renderer/components/item-list/item-table-list/item-table-list-context';
|
||||||
import { Text } from '/@/shared/components/text/text';
|
import { Text } from '/@/shared/components/text/text';
|
||||||
import { LibraryItem, QueueSong } from '/@/shared/types/domain-types';
|
import { LibraryItem, QueueSong } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
@@ -75,9 +76,7 @@ function QueueSongTitleColumn(props: ItemTableListInnerColumn) {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const song = props.data[props.rowIndex] as QueueSong;
|
const song = props.data[props.rowIndex] as QueueSong;
|
||||||
const isActive =
|
const isActive = useIsActiveRow(song?.id, song?._uniqueId);
|
||||||
!!props.activeRowId &&
|
|
||||||
(props.activeRowId === song?.id || props.activeRowId === song?._uniqueId);
|
|
||||||
|
|
||||||
if (typeof row === 'string') {
|
if (typeof row === 'string') {
|
||||||
const path = getTitlePath(props.itemType, (props.data[props.rowIndex] as any).id as string);
|
const path = getTitlePath(props.itemType, (props.data[props.rowIndex] as any).id as string);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
ItemTableListInnerColumn,
|
ItemTableListInnerColumn,
|
||||||
TableColumnContainer,
|
TableColumnContainer,
|
||||||
} from '/@/renderer/components/item-list/item-table-list/item-table-list-column';
|
} from '/@/renderer/components/item-list/item-table-list/item-table-list-column';
|
||||||
|
import { useIsActiveRow } from '/@/renderer/components/item-list/item-table-list/item-table-list-context';
|
||||||
import { JoinedArtists } from '/@/renderer/features/albums/components/joined-artists';
|
import { JoinedArtists } from '/@/renderer/features/albums/components/joined-artists';
|
||||||
import { PlayButton } from '/@/renderer/features/shared/components/play-button';
|
import { PlayButton } from '/@/renderer/features/shared/components/play-button';
|
||||||
import {
|
import {
|
||||||
@@ -162,9 +163,7 @@ export const QueueSongTitleCombinedColumn = (props: ItemTableListInnerColumn) =>
|
|||||||
const internalState = (props as any).internalState;
|
const internalState = (props as any).internalState;
|
||||||
const playButtonBehavior = usePlayButtonBehavior();
|
const playButtonBehavior = usePlayButtonBehavior();
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
const isActive =
|
const isActive = useIsActiveRow(song?.id, song?._uniqueId);
|
||||||
!!props.activeRowId &&
|
|
||||||
(props.activeRowId === song?.id || props.activeRowId === song?._uniqueId);
|
|
||||||
|
|
||||||
const handlePlay = (playType: Play, event: React.MouseEvent<HTMLButtonElement>) => {
|
const handlePlay = (playType: Play, event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
if (!item) {
|
if (!item) {
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
import React, { createContext, useContext, useEffect, useMemo, useRef } from 'react';
|
||||||
|
import { useSyncExternalStore } from 'react';
|
||||||
|
|
||||||
|
import { ItemListStateActions } from '/@/renderer/components/item-list/helpers/item-list-state';
|
||||||
|
import { ItemControls, ItemTableListColumnConfig } from '/@/renderer/components/item-list/types';
|
||||||
|
import { PlayerContext } from '/@/renderer/features/player/context/player-context';
|
||||||
|
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stage A/B: Provide table-scoped config + external stores so churny values can update
|
||||||
|
* without forcing `cellProps` identity changes (and therefore without rerendering every visible cell).
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type ItemTableListConfig = {
|
||||||
|
cellPadding: 'lg' | 'md' | 'sm' | 'xl' | 'xs';
|
||||||
|
columns: ItemTableListColumnConfig[];
|
||||||
|
controls: ItemControls;
|
||||||
|
enableHeader: boolean;
|
||||||
|
enableRowHoverHighlight: boolean;
|
||||||
|
enableSelection: boolean;
|
||||||
|
internalState: ItemListStateActions;
|
||||||
|
itemType: LibraryItem;
|
||||||
|
playerContext: PlayerContext;
|
||||||
|
size: 'compact' | 'default' | 'large';
|
||||||
|
startRowIndex?: number;
|
||||||
|
tableId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ItemTableListConfigContext = createContext<ItemTableListConfig | null>(null);
|
||||||
|
|
||||||
|
export const ItemTableListConfigProvider = ({
|
||||||
|
children,
|
||||||
|
value,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
value: ItemTableListConfig;
|
||||||
|
}) => {
|
||||||
|
// Keep reference stable when the input reference is stable.
|
||||||
|
const memoValue = useMemo(() => value, [value]);
|
||||||
|
return (
|
||||||
|
<ItemTableListConfigContext.Provider value={memoValue}>
|
||||||
|
{children}
|
||||||
|
</ItemTableListConfigContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useItemTableListConfig = (): ItemTableListConfig | null => {
|
||||||
|
return useContext(ItemTableListConfigContext);
|
||||||
|
};
|
||||||
|
|
||||||
|
type ItemTableListStoreContextValue = {
|
||||||
|
activeRowStore: ActiveRowStore;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ActiveRowStore {
|
||||||
|
private activeRowId: null | string = null;
|
||||||
|
private listeners = new Set<() => void>();
|
||||||
|
|
||||||
|
getActiveRowId(): null | string {
|
||||||
|
return this.activeRowId;
|
||||||
|
}
|
||||||
|
|
||||||
|
setActiveRowId(next: null | string | undefined): void {
|
||||||
|
const normalized = next ?? null;
|
||||||
|
if (this.activeRowId === normalized) return;
|
||||||
|
this.activeRowId = normalized;
|
||||||
|
this.listeners.forEach((l) => l());
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribe(listener: () => void): () => void {
|
||||||
|
this.listeners.add(listener);
|
||||||
|
return () => {
|
||||||
|
this.listeners.delete(listener);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ItemTableListStoreContext = createContext<ItemTableListStoreContextValue | null>(null);
|
||||||
|
|
||||||
|
export const ItemTableListStoreProvider = ({
|
||||||
|
activeRowId,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
activeRowId?: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) => {
|
||||||
|
const storeRef = useRef<ActiveRowStore | null>(null);
|
||||||
|
if (!storeRef.current) {
|
||||||
|
storeRef.current = new ActiveRowStore();
|
||||||
|
}
|
||||||
|
const store = storeRef.current;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
store.setActiveRowId(activeRowId);
|
||||||
|
}, [activeRowId, store]);
|
||||||
|
|
||||||
|
const value = useMemo<ItemTableListStoreContextValue>(
|
||||||
|
() => ({ activeRowStore: store }),
|
||||||
|
[store],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ItemTableListStoreContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</ItemTableListStoreContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useItemTableListStore = (): ItemTableListStoreContextValue | null => {
|
||||||
|
return useContext(ItemTableListStoreContext);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useActiveRowSubscription = <T,>(selector: (activeRowId: null | string) => T): T => {
|
||||||
|
const store = useItemTableListStore()?.activeRowStore ?? null;
|
||||||
|
|
||||||
|
return useSyncExternalStore(store?.subscribe.bind(store) || (() => () => {}), () =>
|
||||||
|
selector(store?.getActiveRowId() ?? null),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useIsActiveRow = (...rowIds: Array<string | undefined>): boolean => {
|
||||||
|
return useActiveRowSubscription((activeRowId) =>
|
||||||
|
rowIds.some((id) => !!id && id === activeRowId),
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -36,6 +36,10 @@ import { parseTableColumns } from '/@/renderer/components/item-list/helpers/pars
|
|||||||
import { useListHotkeys } from '/@/renderer/components/item-list/helpers/use-list-hotkeys';
|
import { useListHotkeys } from '/@/renderer/components/item-list/helpers/use-list-hotkeys';
|
||||||
import { useStickyTableGroupRows } from '/@/renderer/components/item-list/item-table-list/hooks/use-sticky-table-group-rows';
|
import { useStickyTableGroupRows } from '/@/renderer/components/item-list/item-table-list/hooks/use-sticky-table-group-rows';
|
||||||
import { useStickyTableHeader } from '/@/renderer/components/item-list/item-table-list/hooks/use-sticky-table-header';
|
import { useStickyTableHeader } from '/@/renderer/components/item-list/item-table-list/hooks/use-sticky-table-header';
|
||||||
|
import {
|
||||||
|
ItemTableListConfigProvider,
|
||||||
|
ItemTableListStoreProvider,
|
||||||
|
} from '/@/renderer/components/item-list/item-table-list/item-table-list-context';
|
||||||
import {
|
import {
|
||||||
ItemControls,
|
ItemControls,
|
||||||
ItemListHandle,
|
ItemListHandle,
|
||||||
@@ -88,7 +92,6 @@ enum TableItemSize {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface VirtualizedTableGridProps {
|
interface VirtualizedTableGridProps {
|
||||||
activeRowId?: string;
|
|
||||||
calculatedColumnWidths: number[];
|
calculatedColumnWidths: number[];
|
||||||
CellComponent: JSXElementConstructor<CellComponentProps<TableItemProps>>;
|
CellComponent: JSXElementConstructor<CellComponentProps<TableItemProps>>;
|
||||||
cellPadding: 'lg' | 'md' | 'sm' | 'xl' | 'xs';
|
cellPadding: 'lg' | 'md' | 'sm' | 'xl' | 'xs';
|
||||||
@@ -132,7 +135,6 @@ interface VirtualizedTableGridProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const VirtualizedTableGrid = ({
|
const VirtualizedTableGrid = ({
|
||||||
activeRowId,
|
|
||||||
calculatedColumnWidths,
|
calculatedColumnWidths,
|
||||||
CellComponent,
|
CellComponent,
|
||||||
cellPadding,
|
cellPadding,
|
||||||
@@ -442,7 +444,6 @@ const VirtualizedTableGrid = ({
|
|||||||
|
|
||||||
const dynamicDataProps = useMemo(
|
const dynamicDataProps = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
activeRowId,
|
|
||||||
calculatedColumnWidths,
|
calculatedColumnWidths,
|
||||||
data: dataWithGroups,
|
data: dataWithGroups,
|
||||||
getAdjustedRowIndex,
|
getAdjustedRowIndex,
|
||||||
@@ -455,7 +456,6 @@ const VirtualizedTableGrid = ({
|
|||||||
startRowIndex,
|
startRowIndex,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
activeRowId,
|
|
||||||
calculatedColumnWidths,
|
calculatedColumnWidths,
|
||||||
dataWithGroups,
|
dataWithGroups,
|
||||||
getAdjustedRowIndex,
|
getAdjustedRowIndex,
|
||||||
@@ -779,7 +779,6 @@ VirtualizedTableGrid.displayName = 'VirtualizedTableGrid';
|
|||||||
|
|
||||||
const MemoizedVirtualizedTableGrid = memo(VirtualizedTableGrid, (prevProps, nextProps) => {
|
const MemoizedVirtualizedTableGrid = memo(VirtualizedTableGrid, (prevProps, nextProps) => {
|
||||||
return (
|
return (
|
||||||
prevProps.activeRowId === nextProps.activeRowId &&
|
|
||||||
prevProps.calculatedColumnWidths === nextProps.calculatedColumnWidths &&
|
prevProps.calculatedColumnWidths === nextProps.calculatedColumnWidths &&
|
||||||
prevProps.cellPadding === nextProps.cellPadding &&
|
prevProps.cellPadding === nextProps.cellPadding &&
|
||||||
prevProps.controls === nextProps.controls &&
|
prevProps.controls === nextProps.controls &&
|
||||||
@@ -837,7 +836,6 @@ export interface TableGroupHeader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface TableItemProps {
|
export interface TableItemProps {
|
||||||
activeRowId?: string;
|
|
||||||
adjustedRowIndexMap?: Map<number, number>;
|
adjustedRowIndexMap?: Map<number, number>;
|
||||||
calculatedColumnWidths?: number[];
|
calculatedColumnWidths?: number[];
|
||||||
cellPadding?: ItemTableListProps['cellPadding'];
|
cellPadding?: ItemTableListProps['cellPadding'];
|
||||||
@@ -2531,70 +2529,104 @@ const BaseItemTableList = ({
|
|||||||
itemType,
|
itemType,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const tableConfigValue = useMemo(
|
||||||
|
() => ({
|
||||||
|
cellPadding,
|
||||||
|
columns: parsedColumns,
|
||||||
|
controls,
|
||||||
|
enableHeader,
|
||||||
|
enableRowHoverHighlight,
|
||||||
|
enableSelection,
|
||||||
|
internalState,
|
||||||
|
itemType,
|
||||||
|
playerContext,
|
||||||
|
size,
|
||||||
|
startRowIndex,
|
||||||
|
tableId,
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
cellPadding,
|
||||||
|
parsedColumns,
|
||||||
|
controls,
|
||||||
|
enableHeader,
|
||||||
|
enableRowHoverHighlight,
|
||||||
|
enableSelection,
|
||||||
|
internalState,
|
||||||
|
itemType,
|
||||||
|
playerContext,
|
||||||
|
size,
|
||||||
|
startRowIndex,
|
||||||
|
tableId,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<ItemTableListStoreProvider activeRowId={activeRowId}>
|
||||||
className={styles.itemTableListContainer}
|
<ItemTableListConfigProvider value={tableConfigValue}>
|
||||||
onKeyDown={handleKeyDown}
|
<motion.div
|
||||||
onMouseDown={(e) => {
|
className={styles.itemTableListContainer}
|
||||||
const element = e.currentTarget as HTMLDivElement;
|
onKeyDown={handleKeyDown}
|
||||||
// Focus without scrolling into view
|
onMouseDown={(e) => {
|
||||||
if (element.focus) {
|
const element = e.currentTarget as HTMLDivElement;
|
||||||
element.focus({ preventScroll: true });
|
// Focus without scrolling into view
|
||||||
}
|
if (element.focus) {
|
||||||
}}
|
element.focus({ preventScroll: true });
|
||||||
ref={mergedContainerRef}
|
}
|
||||||
tabIndex={0}
|
}}
|
||||||
{...animationProps.fadeIn}
|
ref={mergedContainerRef}
|
||||||
transition={{ duration: enableEntranceAnimation ? 1 : 0, ease: 'anticipate' }}
|
tabIndex={0}
|
||||||
>
|
{...animationProps.fadeIn}
|
||||||
{StickyHeader}
|
transition={{ duration: enableEntranceAnimation ? 1 : 0, ease: 'anticipate' }}
|
||||||
{StickyGroupRow}
|
>
|
||||||
<MemoizedVirtualizedTableGrid
|
{StickyHeader}
|
||||||
activeRowId={activeRowId}
|
{StickyGroupRow}
|
||||||
calculatedColumnWidths={calculatedColumnWidths}
|
<MemoizedVirtualizedTableGrid
|
||||||
CellComponent={CellComponent}
|
calculatedColumnWidths={calculatedColumnWidths}
|
||||||
cellPadding={cellPadding}
|
CellComponent={CellComponent}
|
||||||
controls={controls}
|
cellPadding={cellPadding}
|
||||||
data={data}
|
controls={controls}
|
||||||
dataWithGroups={dataWithGroups}
|
data={data}
|
||||||
enableAlternateRowColors={enableAlternateRowColors}
|
dataWithGroups={dataWithGroups}
|
||||||
enableColumnReorder={!!onColumnReordered}
|
enableAlternateRowColors={enableAlternateRowColors}
|
||||||
enableColumnResize={!!onColumnResized}
|
enableColumnReorder={!!onColumnReordered}
|
||||||
enableDrag={enableDrag}
|
enableColumnResize={!!onColumnResized}
|
||||||
enableExpansion={enableExpansion}
|
enableDrag={enableDrag}
|
||||||
enableHeader={enableHeader}
|
enableExpansion={enableExpansion}
|
||||||
enableHorizontalBorders={enableHorizontalBorders}
|
enableHeader={enableHeader}
|
||||||
enableRowHoverHighlight={enableRowHoverHighlight}
|
enableHorizontalBorders={enableHorizontalBorders}
|
||||||
enableScrollShadow={enableScrollShadow}
|
enableRowHoverHighlight={enableRowHoverHighlight}
|
||||||
enableSelection={enableSelection}
|
enableScrollShadow={enableScrollShadow}
|
||||||
enableVerticalBorders={enableVerticalBorders}
|
enableSelection={enableSelection}
|
||||||
getRowHeight={getRowHeight}
|
enableVerticalBorders={enableVerticalBorders}
|
||||||
groups={groups}
|
getRowHeight={getRowHeight}
|
||||||
headerHeight={headerHeight}
|
groups={groups}
|
||||||
internalState={internalState}
|
headerHeight={headerHeight}
|
||||||
itemType={itemType}
|
internalState={internalState}
|
||||||
mergedRowRef={mergedRowRef}
|
itemType={itemType}
|
||||||
onRangeChanged={onRangeChanged}
|
mergedRowRef={mergedRowRef}
|
||||||
parsedColumns={parsedColumns}
|
onRangeChanged={onRangeChanged}
|
||||||
pinnedLeftColumnCount={pinnedLeftColumnCount}
|
parsedColumns={parsedColumns}
|
||||||
pinnedLeftColumnRef={pinnedLeftColumnRef}
|
pinnedLeftColumnCount={pinnedLeftColumnCount}
|
||||||
pinnedRightColumnCount={pinnedRightColumnCount}
|
pinnedLeftColumnRef={pinnedLeftColumnRef}
|
||||||
pinnedRightColumnRef={pinnedRightColumnRef}
|
pinnedRightColumnCount={pinnedRightColumnCount}
|
||||||
pinnedRowCount={pinnedRowCount}
|
pinnedRightColumnRef={pinnedRightColumnRef}
|
||||||
pinnedRowRef={pinnedRowRef}
|
pinnedRowCount={pinnedRowCount}
|
||||||
playerContext={playerContext}
|
pinnedRowRef={pinnedRowRef}
|
||||||
showLeftShadow={showLeftShadow}
|
playerContext={playerContext}
|
||||||
showRightShadow={showRightShadow}
|
showLeftShadow={showLeftShadow}
|
||||||
showTopShadow={showTopShadow}
|
showRightShadow={showRightShadow}
|
||||||
size={size}
|
showTopShadow={showTopShadow}
|
||||||
startRowIndex={startRowIndex}
|
size={size}
|
||||||
tableId={tableId}
|
startRowIndex={startRowIndex}
|
||||||
totalColumnCount={totalColumnCount}
|
tableId={tableId}
|
||||||
totalRowCount={totalRowCount}
|
totalColumnCount={totalColumnCount}
|
||||||
/>
|
totalRowCount={totalRowCount}
|
||||||
<ExpandedContainer internalState={internalState} itemType={itemType} />
|
/>
|
||||||
{/* {enableSelectionDialog && <SelectionDialog internalState={internalState} />} */}
|
<ExpandedContainer internalState={internalState} itemType={itemType} />
|
||||||
</motion.div>
|
{/* {enableSelectionDialog && <SelectionDialog internalState={internalState} />} */}
|
||||||
|
</motion.div>
|
||||||
|
</ItemTableListConfigProvider>
|
||||||
|
</ItemTableListStoreProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user