mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-08 21:10:12 +02:00
326 lines
12 KiB
TypeScript
326 lines
12 KiB
TypeScript
import clsx from 'clsx';
|
|
import { forwardRef, ReactElement, useEffect, useMemo, useRef, useState } from 'react';
|
|
|
|
import styles from './play-queue.module.css';
|
|
|
|
import { useItemListColumnReorder } from '/@/renderer/components/item-list/helpers/use-item-list-column-reorder';
|
|
import { useItemListColumnResize } from '/@/renderer/components/item-list/helpers/use-item-list-column-resize';
|
|
import {
|
|
ItemTableList,
|
|
TableGroupHeader,
|
|
} from '/@/renderer/components/item-list/item-table-list/item-table-list';
|
|
import { ItemTableListColumn } from '/@/renderer/components/item-list/item-table-list/item-table-list-column';
|
|
import { ItemListHandle } from '/@/renderer/components/item-list/types';
|
|
import { useIsPlayerFetching, usePlayer } from '/@/renderer/features/player/context/player-context';
|
|
import { searchLibraryItems } from '/@/renderer/features/shared/utils';
|
|
import { useDragDrop } from '/@/renderer/hooks/use-drag-drop';
|
|
import {
|
|
subscribeCurrentTrack,
|
|
subscribePlayerQueue,
|
|
useListSettings,
|
|
usePlayerActions,
|
|
usePlayerQueueType,
|
|
usePlayerSong,
|
|
} from '/@/renderer/store';
|
|
import { Flex } from '/@/shared/components/flex/flex';
|
|
import { LoadingOverlay } from '/@/shared/components/loading-overlay/loading-overlay';
|
|
import { Text } from '/@/shared/components/text/text';
|
|
import { useDebouncedValue } from '/@/shared/hooks/use-debounced-value';
|
|
import { useFocusWithin } from '/@/shared/hooks/use-focus-within';
|
|
import { useHotkeys } from '/@/shared/hooks/use-hotkeys';
|
|
import { useMergedRef } from '/@/shared/hooks/use-merged-ref';
|
|
import { LibraryItem, QueueSong, Song } from '/@/shared/types/domain-types';
|
|
import { DragTarget } from '/@/shared/types/drag-and-drop';
|
|
import { ItemListKey, Play, PlayerQueueType } from '/@/shared/types/types';
|
|
|
|
type QueueProps = {
|
|
listKey: ItemListKey;
|
|
searchTerm: string | undefined;
|
|
};
|
|
|
|
export const PlayQueue = forwardRef<ItemListHandle, QueueProps>(({ listKey, searchTerm }, ref) => {
|
|
const { table } = useListSettings(listKey) || {};
|
|
|
|
const isFetching = useIsPlayerFetching();
|
|
const tableRef = useRef<ItemListHandle>(null);
|
|
const previousSongCountRef = useRef<number>(0);
|
|
const mergedRef = useMergedRef(ref, tableRef);
|
|
const { getQueue } = usePlayerActions();
|
|
const queueType = usePlayerQueueType();
|
|
|
|
const [debouncedSearchTerm] = useDebouncedValue(searchTerm, 200);
|
|
|
|
const [data, setData] = useState<QueueSong[]>([]);
|
|
const [groups, setGroups] = useState<TableGroupHeader[]>([]);
|
|
const [containerKey, setContainerKey] = useState<string>(() => Math.random().toString(36));
|
|
|
|
useEffect(() => {
|
|
const setQueue = () => {
|
|
const queue = getQueue() || { groups: [], items: [] };
|
|
|
|
setData(queue.items);
|
|
|
|
if (queueType === PlayerQueueType.PRIORITY && queue.groups && queue.groups.length > 0) {
|
|
const transformedGroups: TableGroupHeader[] = queue.groups.map((group) => ({
|
|
itemCount: group.count,
|
|
render: (): ReactElement => {
|
|
return (
|
|
<div className={styles.groupRow}>
|
|
<Text
|
|
fw={600}
|
|
overflow="visible"
|
|
size="md"
|
|
style={{
|
|
textWrap: 'nowrap',
|
|
whiteSpace: 'nowrap',
|
|
}}
|
|
>
|
|
{group.name}
|
|
</Text>
|
|
</div>
|
|
);
|
|
},
|
|
rowHeight: 40,
|
|
}));
|
|
setGroups(transformedGroups);
|
|
} else {
|
|
setGroups([]);
|
|
}
|
|
};
|
|
|
|
const unsub = subscribePlayerQueue(() => {
|
|
setQueue();
|
|
});
|
|
|
|
const unsubCurrentTrack = subscribeCurrentTrack((e) => {
|
|
if (e.index !== -1) {
|
|
tableRef.current?.scrollToIndex(e.index, {
|
|
align: 'top',
|
|
behavior: 'smooth',
|
|
});
|
|
}
|
|
});
|
|
|
|
setQueue();
|
|
|
|
return () => {
|
|
unsub();
|
|
unsubCurrentTrack();
|
|
};
|
|
}, [getQueue, queueType, tableRef]);
|
|
|
|
useEffect(() => {
|
|
const currentCount = data.length;
|
|
const previousCount = previousSongCountRef.current;
|
|
|
|
if (previousCount === 0 && currentCount > 0) {
|
|
setContainerKey(Math.random().toString(36));
|
|
}
|
|
|
|
previousSongCountRef.current = currentCount;
|
|
}, [data.length]);
|
|
|
|
const filteredData: QueueSong[] = useMemo(() => {
|
|
if (debouncedSearchTerm) {
|
|
const searched = searchLibraryItems(data, debouncedSearchTerm, LibraryItem.SONG);
|
|
return searched;
|
|
}
|
|
|
|
return data;
|
|
}, [data, debouncedSearchTerm]);
|
|
|
|
const isEmpty = filteredData.length === 0;
|
|
|
|
const { handleColumnReordered } = useItemListColumnReorder({
|
|
itemListKey: listKey,
|
|
});
|
|
|
|
const { handleColumnResized } = useItemListColumnResize({
|
|
itemListKey: listKey,
|
|
});
|
|
|
|
const currentSong = usePlayerSong();
|
|
const currentSongUniqueId = currentSong?._uniqueId;
|
|
|
|
const { focused, ref: containerFocusRef } = useFocusWithin();
|
|
const player = usePlayer();
|
|
|
|
useHotkeys([
|
|
[
|
|
'delete',
|
|
() => {
|
|
if (focused) {
|
|
const selectedItems =
|
|
tableRef.current?.internalState.getSelected() as QueueSong[];
|
|
|
|
if (!selectedItems || selectedItems.length === 0) {
|
|
return;
|
|
}
|
|
|
|
player.clearSelected(selectedItems);
|
|
}
|
|
},
|
|
],
|
|
]);
|
|
|
|
return (
|
|
<div className={styles.container} key={containerKey} ref={containerFocusRef}>
|
|
<LoadingOverlay pos="absolute" visible={isFetching} />
|
|
<ItemTableList
|
|
activeRowId={currentSongUniqueId}
|
|
autoFitColumns={table.autoFitColumns}
|
|
CellComponent={ItemTableListColumn}
|
|
columns={table.columns}
|
|
data={filteredData}
|
|
enableAlternateRowColors={table.enableAlternateRowColors}
|
|
enableDrag
|
|
enableExpansion={false}
|
|
enableHeader
|
|
enableHorizontalBorders={table.enableHorizontalBorders}
|
|
enableRowHoverHighlight={table.enableRowHoverHighlight}
|
|
enableSelection
|
|
enableVerticalBorders={table.enableVerticalBorders}
|
|
getRowId="_uniqueId"
|
|
groups={groups.length > 0 ? groups : undefined}
|
|
initialTop={{
|
|
to: 0,
|
|
type: 'offset',
|
|
}}
|
|
itemType={LibraryItem.QUEUE_SONG}
|
|
onColumnReordered={handleColumnReordered}
|
|
onColumnResized={handleColumnResized}
|
|
ref={mergedRef}
|
|
size={table.size}
|
|
/>
|
|
{isEmpty && <EmptyQueueDropZone />}
|
|
</div>
|
|
);
|
|
});
|
|
|
|
const EmptyQueueDropZone = () => {
|
|
const playerContext = usePlayer();
|
|
|
|
const { isDraggedOver, ref } = useDragDrop<HTMLDivElement>({
|
|
drop: {
|
|
canDrop: () => {
|
|
return true;
|
|
},
|
|
getData: () => {
|
|
return {
|
|
id: [],
|
|
item: [],
|
|
itemType: LibraryItem.QUEUE_SONG,
|
|
type: DragTarget.QUEUE_SONG,
|
|
};
|
|
},
|
|
onDrag: () => {
|
|
return;
|
|
},
|
|
onDragLeave: () => {
|
|
return;
|
|
},
|
|
onDrop: (args) => {
|
|
if (args.self.type === DragTarget.QUEUE_SONG) {
|
|
const sourceServerId = (
|
|
args.source.item?.[0] as unknown as { _serverId: string }
|
|
)?._serverId;
|
|
|
|
const sourceItemType = args.source.itemType as LibraryItem;
|
|
|
|
switch (args.source.type) {
|
|
case DragTarget.ALBUM: {
|
|
if (sourceServerId) {
|
|
playerContext.addToQueueByFetch(
|
|
sourceServerId,
|
|
args.source.id,
|
|
sourceItemType,
|
|
Play.NOW,
|
|
);
|
|
}
|
|
break;
|
|
}
|
|
case DragTarget.ALBUM_ARTIST: {
|
|
if (sourceServerId) {
|
|
playerContext.addToQueueByFetch(
|
|
sourceServerId,
|
|
args.source.id,
|
|
sourceItemType,
|
|
Play.NOW,
|
|
);
|
|
}
|
|
break;
|
|
}
|
|
case DragTarget.ARTIST: {
|
|
if (sourceServerId) {
|
|
playerContext.addToQueueByFetch(
|
|
sourceServerId,
|
|
args.source.id,
|
|
sourceItemType,
|
|
Play.NOW,
|
|
);
|
|
}
|
|
break;
|
|
}
|
|
case DragTarget.GENRE: {
|
|
if (sourceServerId) {
|
|
playerContext.addToQueueByFetch(
|
|
sourceServerId,
|
|
args.source.id,
|
|
sourceItemType,
|
|
Play.NOW,
|
|
);
|
|
}
|
|
break;
|
|
}
|
|
case DragTarget.PLAYLIST: {
|
|
if (sourceServerId) {
|
|
playerContext.addToQueueByFetch(
|
|
sourceServerId,
|
|
args.source.id,
|
|
sourceItemType,
|
|
Play.NOW,
|
|
);
|
|
}
|
|
break;
|
|
}
|
|
case DragTarget.QUEUE_SONG: {
|
|
const sourceItems = (args.source.item || []) as QueueSong[];
|
|
if (sourceItems.length > 0) {
|
|
playerContext.addToQueueByData(sourceItems, Play.NOW);
|
|
}
|
|
break;
|
|
}
|
|
case DragTarget.SONG: {
|
|
const sourceItems = (args.source.item || []) as Song[];
|
|
if (sourceItems.length > 0) {
|
|
playerContext.addToQueueByData(sourceItems, Play.NOW);
|
|
}
|
|
break;
|
|
}
|
|
default: {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return;
|
|
},
|
|
},
|
|
isEnabled: true,
|
|
});
|
|
|
|
return (
|
|
<Flex
|
|
align="center"
|
|
className={clsx(styles.dropZone, {
|
|
[styles.draggedOver]: isDraggedOver,
|
|
})}
|
|
direction="column"
|
|
gap="md"
|
|
justify="center"
|
|
ref={ref}
|
|
w="100%"
|
|
/>
|
|
);
|
|
};
|