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