From ffb7f915c3acdc97dbd6f98f3d72b8bf13b4fcb5 Mon Sep 17 00:00:00 2001 From: jeffvli Date: Wed, 8 Feb 2023 11:46:39 -0800 Subject: [PATCH] Add context menu to queue items --- .../context-menu/context-menu-items.tsx | 11 +++ .../context-menu/context-menu-provider.tsx | 93 +++++++++++++++++-- src/renderer/features/context-menu/events.ts | 12 ++- .../hooks/use-handle-context-menu.ts | 1 + .../components/play-queue-list-controls.tsx | 18 ++-- .../now-playing/components/play-queue.tsx | 7 +- 6 files changed, 118 insertions(+), 24 deletions(-) diff --git a/src/renderer/features/context-menu/context-menu-items.tsx b/src/renderer/features/context-menu/context-menu-items.tsx index 86a0e9e3..69b4191f 100644 --- a/src/renderer/features/context-menu/context-menu-items.tsx +++ b/src/renderer/features/context-menu/context-menu-items.tsx @@ -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' }, diff --git a/src/renderer/features/context-menu/context-menu-provider.tsx b/src/renderer/features/context-menu/context-menu-provider.tsx index 1b5c089f..255fe0b1 100644 --- a/src/renderer/features/context-menu/context-menu-provider.tsx +++ b/src/renderer/features/context-menu/context-menu-provider.tsx @@ -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 = useMemo(() => { return { addToFavorites: { id: 'addToFavorites', - label: 'Add to favorites', + label: 'Add favorite', leftIcon: , onClick: handleAddToFavorites, }, @@ -485,6 +532,24 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => { leftIcon: , onClick: openDeletePlaylistModal, }, + deselectAll: { + id: 'deselectAll', + label: 'Deselect all', + leftIcon: , + onClick: handleDeselectAll, + }, + moveToBottomOfQueue: { + id: 'moveToBottomOfQueue', + label: 'Move to bottom', + leftIcon: , + onClick: handleMoveToBottom, + }, + moveToTopOfQueue: { + id: 'moveToTopOfQueue', + label: 'Move to top', + leftIcon: , + 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: , onClick: handleRemoveFromFavorites, }, @@ -515,6 +580,12 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => { leftIcon: , onClick: handleRemoveFromPlaylist, }, + removeFromQueue: { + id: 'moveToBottomOfQueue', + label: 'Remove songs', + leftIcon: , + onClick: handleRemoveSelected, + }, setRating: { children: [ { @@ -594,9 +665,13 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => { }, [ handleAddToFavorites, handleAddToPlaylist, + handleDeselectAll, + handleMoveToBottom, + handleMoveToTop, handlePlay, handleRemoveFromFavorites, handleRemoveFromPlaylist, + handleRemoveSelected, handleUpdateRating, openDeletePlaylistModal, ]); diff --git a/src/renderer/features/context-menu/events.ts b/src/renderer/features/context-menu/events.ts index 08306139..c4bdb3e5 100644 --- a/src/renderer/features/context-menu/events.ts +++ b/src/renderer/features/context-menu/events.ts @@ -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; + 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; diff --git a/src/renderer/features/context-menu/hooks/use-handle-context-menu.ts b/src/renderer/features/context-menu/hooks/use-handle-context-menu.ts index f844eb0f..0e69b908 100644 --- a/src/renderer/features/context-menu/hooks/use-handle-context-menu.ts +++ b/src/renderer/features/context-menu/hooks/use-handle-context-menu.ts @@ -30,6 +30,7 @@ export const useHandleTableContextMenu = ( data: selectedRows, dataNodes: selectedNodes, menuItems: contextMenuItems, + tableApi: e.api, type: itemType, xPos: clickEvent.clientX, yPos: clickEvent.clientY, diff --git a/src/renderer/features/now-playing/components/play-queue-list-controls.tsx b/src/renderer/features/now-playing/components/play-queue-list-controls.tsx index 28714216..870f6459 100644 --- a/src/renderer/features/now-playing/components/play-queue-list-controls.tsx +++ b/src/renderer/features/now-playing/components/play-queue-list-controls.tsx @@ -100,15 +100,6 @@ export const PlayQueueListControls = ({ type, tableRef }: PlayQueueListOptionsPr > - +