Add context menu to queue items

This commit is contained in:
jeffvli 2023-02-08 11:46:39 -08:00
parent 17d5ef1f6b
commit ffb7f915c3
6 changed files with 118 additions and 24 deletions

View file

@ -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' },

View file

@ -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,
]);

View file

@ -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;

View file

@ -30,6 +30,7 @@ export const useHandleTableContextMenu = (
data: selectedRows,
dataNodes: selectedNodes,
menuItems: contextMenuItems,
tableApi: e.api,
type: itemType,
xPos: clickEvent.clientX,
yPos: clickEvent.clientY,

View file

@ -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"

View file

@ -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}