Add context menu to queue items
This commit is contained in:
parent
17d5ef1f6b
commit
ffb7f915c3
6 changed files with 118 additions and 24 deletions
|
@ -1,5 +1,16 @@
|
|||
import { SetContextMenuItems } from '/@/renderer/features/context-menu/events';
|
||||
|
||||
export const QUEUE_CONTEXT_MENU_ITEMS: SetContextMenuItems = [
|
||||
{ divider: true, id: 'removeFromQueue' },
|
||||
{ id: 'moveToBottomOfQueue' },
|
||||
{ divider: true, id: 'moveToTopOfQueue' },
|
||||
{ divider: true, id: 'addToPlaylist' },
|
||||
{ id: 'addToFavorites' },
|
||||
{ divider: true, id: 'removeFromFavorites' },
|
||||
{ children: true, disabled: false, id: 'setRating' },
|
||||
{ disabled: false, id: 'deselectAll' },
|
||||
];
|
||||
|
||||
export const SONG_CONTEXT_MENU_ITEMS: SetContextMenuItems = [
|
||||
{ id: 'play' },
|
||||
{ id: 'playLast' },
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { createContext, Fragment, ReactNode, useState, useMemo, useCallback } from 'react';
|
||||
import { RowNode } from '@ag-grid-community/core';
|
||||
import { Divider, Group, Portal, Stack } from '@mantine/core';
|
||||
import {
|
||||
|
@ -9,17 +10,20 @@ import {
|
|||
} from '@mantine/hooks';
|
||||
import { closeAllModals, openContextModal, openModal } from '@mantine/modals';
|
||||
import { AnimatePresence } from 'framer-motion';
|
||||
import { createContext, Fragment, ReactNode, useState, useMemo, useCallback } from 'react';
|
||||
import isElectron from 'is-electron';
|
||||
import {
|
||||
RiAddBoxFill,
|
||||
RiAddCircleFill,
|
||||
RiArrowDownLine,
|
||||
RiArrowRightSFill,
|
||||
RiArrowUpLine,
|
||||
RiDeleteBinFill,
|
||||
RiDislikeFill,
|
||||
RiHeartFill,
|
||||
RiPlayFill,
|
||||
RiPlayListAddFill,
|
||||
RiStarFill,
|
||||
RiCloseCircleLine,
|
||||
} from 'react-icons/ri';
|
||||
import { AnyLibraryItems, LibraryItem, ServerType } from '/@/renderer/api/types';
|
||||
import {
|
||||
|
@ -40,8 +44,9 @@ import { usePlayQueueAdd } from '/@/renderer/features/player';
|
|||
import { useDeletePlaylist } from '/@/renderer/features/playlists';
|
||||
import { useRemoveFromPlaylist } from '/@/renderer/features/playlists/mutations/remove-from-playlist-mutation';
|
||||
import { useCreateFavorite, useDeleteFavorite, useUpdateRating } from '/@/renderer/features/shared';
|
||||
import { useAuthStore, useCurrentServer } from '/@/renderer/store';
|
||||
import { Play } from '/@/renderer/types';
|
||||
import { useAuthStore, useCurrentServer, useQueueControls } from '/@/renderer/store';
|
||||
import { usePlayerType } from '/@/renderer/store/settings.store';
|
||||
import { Play, PlaybackType } from '/@/renderer/types';
|
||||
|
||||
type ContextMenuContextProps = {
|
||||
closeContextMenu: () => void;
|
||||
|
@ -69,6 +74,8 @@ const JELLYFIN_IGNORED_MENU_ITEMS: ContextMenuItemType[] = ['setRating'];
|
|||
// const NAVIDROME_IGNORED_MENU_ITEMS: ContextMenuItemType[] = [];
|
||||
// const SUBSONIC_IGNORED_MENU_ITEMS: ContextMenuItemType[] = [];
|
||||
|
||||
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
|
||||
|
||||
export interface ContextMenuProviderProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
@ -85,7 +92,7 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
|
|||
data: [],
|
||||
dataNodes: [],
|
||||
menuItems: [],
|
||||
tableRef: undefined,
|
||||
tableApi: undefined,
|
||||
type: LibraryItem.SONG,
|
||||
xPos: 0,
|
||||
yPos: 0,
|
||||
|
@ -94,7 +101,7 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
|
|||
const handlePlayQueueAdd = usePlayQueueAdd();
|
||||
|
||||
const openContextMenu = (args: OpenContextMenuProps) => {
|
||||
const { xPos, yPos, menuItems, data, type, tableRef, dataNodes, context } = args;
|
||||
const { xPos, yPos, menuItems, data, type, tableApi, dataNodes, context } = args;
|
||||
|
||||
const serverType = data[0]?.serverType || useAuthStore.getState().currentServer?.type;
|
||||
let validMenuItems = menuItems;
|
||||
|
@ -119,7 +126,7 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
|
|||
data,
|
||||
dataNodes,
|
||||
menuItems: validMenuItems,
|
||||
tableRef,
|
||||
tableApi,
|
||||
type,
|
||||
xPos: calculatedXPos,
|
||||
yPos: calculatedYPos,
|
||||
|
@ -133,7 +140,7 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
|
|||
data: [],
|
||||
dataNodes: [],
|
||||
menuItems: [],
|
||||
tableRef: undefined,
|
||||
tableApi: undefined,
|
||||
type: LibraryItem.SONG,
|
||||
xPos: 0,
|
||||
yPos: 0,
|
||||
|
@ -464,11 +471,51 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
|
|||
[ctx.data, ctx.dataNodes, updateRatingMutation],
|
||||
);
|
||||
|
||||
const playerType = usePlayerType();
|
||||
const { moveToBottomOfQueue, moveToTopOfQueue, removeFromQueue } = useQueueControls();
|
||||
|
||||
const handleMoveToBottom = useCallback(() => {
|
||||
const uniqueIds = ctx.dataNodes?.map((row) => row.data.uniqueId);
|
||||
if (!uniqueIds?.length) return;
|
||||
|
||||
const playerData = moveToBottomOfQueue(uniqueIds);
|
||||
|
||||
if (playerType === PlaybackType.LOCAL) {
|
||||
mpvPlayer.setQueueNext(playerData);
|
||||
}
|
||||
}, [ctx.dataNodes, moveToBottomOfQueue, playerType]);
|
||||
|
||||
const handleMoveToTop = useCallback(() => {
|
||||
const uniqueIds = ctx.dataNodes?.map((row) => row.data.uniqueId);
|
||||
if (!uniqueIds?.length) return;
|
||||
|
||||
const playerData = moveToTopOfQueue(uniqueIds);
|
||||
|
||||
if (playerType === PlaybackType.LOCAL) {
|
||||
mpvPlayer.setQueueNext(playerData);
|
||||
}
|
||||
}, [ctx.dataNodes, moveToTopOfQueue, playerType]);
|
||||
|
||||
const handleRemoveSelected = useCallback(() => {
|
||||
const uniqueIds = ctx.dataNodes?.map((row) => row.data.uniqueId);
|
||||
if (!uniqueIds?.length) return;
|
||||
|
||||
const playerData = removeFromQueue(uniqueIds);
|
||||
|
||||
if (playerType === PlaybackType.LOCAL) {
|
||||
mpvPlayer.setQueueNext(playerData);
|
||||
}
|
||||
}, [ctx.dataNodes, playerType, removeFromQueue]);
|
||||
|
||||
const handleDeselectAll = useCallback(() => {
|
||||
ctx.tableApi?.deselectAll();
|
||||
}, [ctx.tableApi]);
|
||||
|
||||
const contextMenuItems: Record<ContextMenuItemType, ContextMenuItem> = useMemo(() => {
|
||||
return {
|
||||
addToFavorites: {
|
||||
id: 'addToFavorites',
|
||||
label: 'Add to favorites',
|
||||
label: 'Add favorite',
|
||||
leftIcon: <RiHeartFill size="1.1rem" />,
|
||||
onClick: handleAddToFavorites,
|
||||
},
|
||||
|
@ -485,6 +532,24 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
|
|||
leftIcon: <RiDeleteBinFill size="1.1rem" />,
|
||||
onClick: openDeletePlaylistModal,
|
||||
},
|
||||
deselectAll: {
|
||||
id: 'deselectAll',
|
||||
label: 'Deselect all',
|
||||
leftIcon: <RiCloseCircleLine size="1.1rem" />,
|
||||
onClick: handleDeselectAll,
|
||||
},
|
||||
moveToBottomOfQueue: {
|
||||
id: 'moveToBottomOfQueue',
|
||||
label: 'Move to bottom',
|
||||
leftIcon: <RiArrowDownLine size="1.1rem" />,
|
||||
onClick: handleMoveToBottom,
|
||||
},
|
||||
moveToTopOfQueue: {
|
||||
id: 'moveToTopOfQueue',
|
||||
label: 'Move to top',
|
||||
leftIcon: <RiArrowUpLine size="1.1rem" />,
|
||||
onClick: handleMoveToTop,
|
||||
},
|
||||
play: {
|
||||
id: 'play',
|
||||
label: 'Play',
|
||||
|
@ -505,7 +570,7 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
|
|||
},
|
||||
removeFromFavorites: {
|
||||
id: 'removeFromFavorites',
|
||||
label: 'Remove from favorites',
|
||||
label: 'Remove favorite',
|
||||
leftIcon: <RiDislikeFill size="1.1rem" />,
|
||||
onClick: handleRemoveFromFavorites,
|
||||
},
|
||||
|
@ -515,6 +580,12 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
|
|||
leftIcon: <RiDeleteBinFill size="1.1rem" />,
|
||||
onClick: handleRemoveFromPlaylist,
|
||||
},
|
||||
removeFromQueue: {
|
||||
id: 'moveToBottomOfQueue',
|
||||
label: 'Remove songs',
|
||||
leftIcon: <RiDeleteBinFill size="1.1rem" />,
|
||||
onClick: handleRemoveSelected,
|
||||
},
|
||||
setRating: {
|
||||
children: [
|
||||
{
|
||||
|
@ -594,9 +665,13 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
|
|||
}, [
|
||||
handleAddToFavorites,
|
||||
handleAddToPlaylist,
|
||||
handleDeselectAll,
|
||||
handleMoveToBottom,
|
||||
handleMoveToTop,
|
||||
handlePlay,
|
||||
handleRemoveFromFavorites,
|
||||
handleRemoveFromPlaylist,
|
||||
handleRemoveSelected,
|
||||
handleUpdateRating,
|
||||
openDeletePlaylistModal,
|
||||
]);
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import { RowNode } from '@ag-grid-community/core';
|
||||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
||||
import { GridOptions, RowNode } from '@ag-grid-community/core';
|
||||
import { createUseExternalEvents } from '@mantine/utils';
|
||||
import { MutableRefObject } from 'react';
|
||||
import { LibraryItem } from '/@/renderer/api/types';
|
||||
|
||||
export type OpenContextMenuProps = {
|
||||
|
@ -9,7 +7,7 @@ export type OpenContextMenuProps = {
|
|||
data: any[];
|
||||
dataNodes?: RowNode[];
|
||||
menuItems: SetContextMenuItems;
|
||||
tableRef?: MutableRefObject<AgGridReactType | null>;
|
||||
tableApi?: GridOptions['api'];
|
||||
type: LibraryItem;
|
||||
xPos: number;
|
||||
yPos: number;
|
||||
|
@ -30,7 +28,11 @@ export type ContextMenuItemType =
|
|||
| 'removeFromFavorites'
|
||||
| 'setRating'
|
||||
| 'deletePlaylist'
|
||||
| 'createPlaylist';
|
||||
| 'createPlaylist'
|
||||
| 'moveToBottomOfQueue'
|
||||
| 'moveToTopOfQueue'
|
||||
| 'removeFromQueue'
|
||||
| 'deselectAll';
|
||||
|
||||
export type SetContextMenuItems = {
|
||||
children?: boolean;
|
||||
|
|
|
@ -30,6 +30,7 @@ export const useHandleTableContextMenu = (
|
|||
data: selectedRows,
|
||||
dataNodes: selectedNodes,
|
||||
menuItems: contextMenuItems,
|
||||
tableApi: e.api,
|
||||
type: itemType,
|
||||
xPos: clickEvent.clientX,
|
||||
yPos: clickEvent.clientY,
|
||||
|
|
|
@ -100,15 +100,6 @@ export const PlayQueueListControls = ({ type, tableRef }: PlayQueueListOptionsPr
|
|||
>
|
||||
<RiShuffleLine size="1.1rem" />
|
||||
</Button>
|
||||
<Button
|
||||
compact
|
||||
size="md"
|
||||
tooltip={{ label: 'Move selected to top' }}
|
||||
variant="default"
|
||||
onClick={handleMoveToTop}
|
||||
>
|
||||
<RiArrowUpLine size="1.1rem" />
|
||||
</Button>
|
||||
<Button
|
||||
compact
|
||||
size="md"
|
||||
|
@ -118,6 +109,15 @@ export const PlayQueueListControls = ({ type, tableRef }: PlayQueueListOptionsPr
|
|||
>
|
||||
<RiArrowDownLine size="1.1rem" />
|
||||
</Button>
|
||||
<Button
|
||||
compact
|
||||
size="md"
|
||||
tooltip={{ label: 'Move selected to top' }}
|
||||
variant="default"
|
||||
onClick={handleMoveToTop}
|
||||
>
|
||||
<RiArrowUpLine size="1.1rem" />
|
||||
</Button>
|
||||
<Button
|
||||
compact
|
||||
size="md"
|
||||
|
|
|
@ -29,7 +29,9 @@ import { ErrorBoundary } from 'react-error-boundary';
|
|||
import { VirtualTable } from '/@/renderer/components/virtual-table';
|
||||
import { ErrorFallback } from '/@/renderer/features/action-required';
|
||||
import { PlaybackType, TableType } from '/@/renderer/types';
|
||||
import { QueueSong } from '/@/renderer/api/types';
|
||||
import { LibraryItem, QueueSong } from '/@/renderer/api/types';
|
||||
import { useHandleTableContextMenu } from '/@/renderer/features/context-menu';
|
||||
import { QUEUE_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/context-menu-items';
|
||||
|
||||
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
|
||||
|
||||
|
@ -184,6 +186,8 @@ export const PlayQueue = forwardRef(({ type }: QueueProps, ref: Ref<any>) => {
|
|||
}
|
||||
}, [currentSong, previousSong, tableConfig.followCurrentSong]);
|
||||
|
||||
const handleContextMenu = useHandleTableContextMenu(LibraryItem.SONG, QUEUE_CONTEXT_MENU_ITEMS);
|
||||
|
||||
return (
|
||||
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
||||
<VirtualGridAutoSizerContainer>
|
||||
|
@ -199,6 +203,7 @@ export const PlayQueue = forwardRef(({ type }: QueueProps, ref: Ref<any>) => {
|
|||
rowClassRules={rowClassRules}
|
||||
rowData={queue}
|
||||
rowHeight={tableConfig.rowHeight || 40}
|
||||
onCellContextMenu={handleContextMenu}
|
||||
onCellDoubleClicked={handleDoubleClick}
|
||||
onColumnMoved={handleColumnChange}
|
||||
onColumnResized={debouncedColumnChange}
|
||||
|
|
Reference in a new issue