From 4d5e4082bb62f0477d53f778865e45ae40c01592 Mon Sep 17 00:00:00 2001 From: jeffvli Date: Wed, 28 Dec 2022 15:32:02 -0800 Subject: [PATCH] Add base context menu provider/component --- src/renderer/app.tsx | 5 +- .../components/context-menu/index.tsx | 56 +++++++ src/renderer/components/index.ts | 1 + .../context-menu/context-menu-items.tsx | 31 ++++ .../context-menu/context-menu-provider.tsx | 150 ++++++++++++++++++ src/renderer/features/context-menu/events.ts | 36 +++++ src/renderer/features/context-menu/index.tsx | 2 + 7 files changed, 280 insertions(+), 1 deletion(-) create mode 100644 src/renderer/components/context-menu/index.tsx create mode 100644 src/renderer/features/context-menu/context-menu-items.tsx create mode 100644 src/renderer/features/context-menu/context-menu-provider.tsx create mode 100644 src/renderer/features/context-menu/events.ts create mode 100644 src/renderer/features/context-menu/index.tsx diff --git a/src/renderer/app.tsx b/src/renderer/app.tsx index 1c028999..98de6086 100644 --- a/src/renderer/app.tsx +++ b/src/renderer/app.tsx @@ -14,6 +14,7 @@ import { AppRouter } from './router/app-router'; import { useSettingsStore } from './store/settings.store'; import './styles/global.scss'; import '@ag-grid-community/styles/ag-grid.css'; +import { ContextMenuProvider } from '/@/renderer/features/context-menu'; ModuleRegistry.registerModules([ClientSideRowModelModule, InfiniteRowModelModule]); @@ -98,7 +99,9 @@ export const App = () => { }} modals={{ base: BaseContextModal }} > - + + + diff --git a/src/renderer/components/context-menu/index.tsx b/src/renderer/components/context-menu/index.tsx new file mode 100644 index 00000000..0e68f6ae --- /dev/null +++ b/src/renderer/components/context-menu/index.tsx @@ -0,0 +1,56 @@ +import { forwardRef, ReactNode, Ref } from 'react'; +import { Portal } from '@mantine/core'; +import { motion } from 'framer-motion'; +import styled from 'styled-components'; +import { _Button } from '/@/renderer/components/button'; + +interface ContextMenuProps { + children: ReactNode; + maxWidth?: number; + minWidth?: number; + xPos: number; + yPos: number; +} + +const ContextMenuContainer = styled(motion.div)>` + position: absolute; + top: ${({ yPos }) => yPos}px !important; + left: ${({ xPos }) => xPos}px !important; + z-index: 1000; + min-width: ${({ minWidth }) => minWidth}px; + max-width: ${({ maxWidth }) => maxWidth}px; + padding: 0.5rem; + background: var(--dropdown-menu-bg); + border-radius: 5px; + box-shadow: 2px 2px 10px 2px rgba(0, 0, 0, 40%); +`; + +export const ContextMenuButton = styled(_Button)` + background: var(--dropdown-menu-bg); + + & .mantine-Button-inner { + justify-content: flex-start; + } + + &:disabled { + background: transparent; + } +`; + +export const ContextMenu = forwardRef( + ({ yPos, xPos, minWidth, maxWidth, children }: ContextMenuProps, ref: Ref) => { + return ( + + + {children} + + + ); + }, +); diff --git a/src/renderer/components/index.ts b/src/renderer/components/index.ts index 04e3534b..682217ae 100644 --- a/src/renderer/components/index.ts +++ b/src/renderer/components/index.ts @@ -30,3 +30,4 @@ export * from './tooltip'; export * from './virtual-grid'; export * from './virtual-table'; export * from './motion'; +export * from './context-menu'; diff --git a/src/renderer/features/context-menu/context-menu-items.tsx b/src/renderer/features/context-menu/context-menu-items.tsx new file mode 100644 index 00000000..014a64f6 --- /dev/null +++ b/src/renderer/features/context-menu/context-menu-items.tsx @@ -0,0 +1,31 @@ +import { SetContextMenuItems } from '/@/renderer/features/context-menu/events'; + +export const SONG_CONTEXT_MENU_ITEMS: SetContextMenuItems = [ + { id: 'play' }, + { id: 'playLast' }, + { id: 'playNext' }, + { disabled: true, id: 'addToPlaylist' }, + { disabled: true, id: 'addToFavorites' }, + { disabled: true, id: 'removeFromFavorites' }, + { disabled: true, id: 'setRating' }, +]; + +export const ALBUM_CONTEXT_MENU_ITEMS: SetContextMenuItems = [ + { id: 'play' }, + { id: 'playLast' }, + { id: 'playNext' }, + { disabled: true, id: 'addToPlaylist' }, + { disabled: true, id: 'addToFavorites' }, + { disabled: true, id: 'removeFromFavorites' }, + { disabled: true, id: 'setRating' }, +]; + +export const ARTIST_CONTEXT_MENU_ITEMS: SetContextMenuItems = [ + { id: 'play' }, + { id: 'playLast' }, + { id: 'playNext' }, + { disabled: true, id: 'addToPlaylist' }, + { disabled: true, id: 'addToFavorites' }, + { disabled: true, id: 'removeFromFavorites' }, + { disabled: true, id: 'setRating' }, +]; diff --git a/src/renderer/features/context-menu/context-menu-provider.tsx b/src/renderer/features/context-menu/context-menu-provider.tsx new file mode 100644 index 00000000..546f89a7 --- /dev/null +++ b/src/renderer/features/context-menu/context-menu-provider.tsx @@ -0,0 +1,150 @@ +import { Stack } from '@mantine/core'; +import { useClickOutside, useResizeObserver, useSetState, useViewportSize } from '@mantine/hooks'; +import { createContext, useMemo, useState } from 'react'; +import { ContextMenu, ContextMenuButton } from '/@/renderer/components'; +import { + OpenContextMenuProps, + SetContextMenuItems, + useContextMenuEvents, +} from '/@/renderer/features/context-menu/events'; +import { useHandlePlayQueueAdd } from '/@/renderer/features/player/hooks/use-handle-playqueue-add'; +import { LibraryItem, Play } from '/@/renderer/types'; + +type ContextMenuContextProps = { + closeContextMenu: () => void; + openContextMenu: (args: OpenContextMenuProps) => void; +}; + +const ContextMenuContext = createContext({ + closeContextMenu: () => {}, + openContextMenu: (args: OpenContextMenuProps) => { + return args; + }, +}); + +export interface ContextMenuProviderProps { + children: React.ReactNode; +} + +export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => { + const [opened, setOpened] = useState(false); + const clickOutsideRef = useClickOutside(() => setOpened(false)); + const viewport = useViewportSize(); + const [ref, menuRect] = useResizeObserver(); + const [ctx, setCtx] = useSetState<{ + data: any[]; + menuItems: SetContextMenuItems; + type: LibraryItem; + xPos: number; + yPos: number; + }>({ + data: [], + menuItems: [], + type: LibraryItem.SONG, + xPos: 0, + yPos: 0, + }); + + const handlePlayQueueAdd = useHandlePlayQueueAdd(); + + const openContextMenu = (args: OpenContextMenuProps) => { + const { xPos, yPos, menuItems, data } = args; + + const shouldReverseY = yPos + menuRect.height > viewport.height; + const shouldReverseX = xPos + menuRect.width > viewport.width; + + const calculatedXPos = shouldReverseX ? xPos - menuRect.width : xPos; + const calculatedYPos = shouldReverseY ? yPos - menuRect.height : yPos; + + setCtx({ data, menuItems, xPos: calculatedXPos, yPos: calculatedYPos }); + setOpened(true); + }; + + const closeContextMenu = () => { + setOpened(false); + }; + + useContextMenuEvents({ + closeContextMenu, + openContextMenu, + }); + + const contextMenuItems = useMemo(() => { + return { + addToFavorites: { id: 'addToFavorites', label: 'Add to favorites', onClick: () => {} }, + addToPlaylist: { id: 'addToPlaylist', label: 'Add to playlist', onClick: () => {} }, + play: { + id: 'play', + label: 'Play', + onClick: () => { + if (ctx.type === LibraryItem.SONG) { + handlePlayQueueAdd({ byData: ctx.data, play: Play.NOW }); + } + }, + }, + playLast: { + id: 'playLast', + label: 'Play Last', + onClick: () => { + if (ctx.type === LibraryItem.SONG) { + handlePlayQueueAdd({ byData: ctx.data, play: Play.LAST }); + } + }, + }, + playNext: { + id: 'playNext', + label: 'Play Next', + onClick: () => { + if (ctx.type === LibraryItem.SONG) { + handlePlayQueueAdd({ byData: ctx.data, play: Play.NEXT }); + } + }, + }, + removeFromFavorites: { + id: 'removeFromFavorites', + label: 'Remove from favorites', + onClick: () => {}, + }, + setRating: { id: 'setRating', label: 'Set rating', onClick: () => {} }, + }; + }, [ctx.data, ctx.type, handlePlayQueueAdd]); + + return ( + + {opened && ( + + + {ctx.menuItems?.map((item) => { + return ( + + {contextMenuItems[item.id].label} + + ); + })} + + + )} + + {children} + + ); +}; diff --git a/src/renderer/features/context-menu/events.ts b/src/renderer/features/context-menu/events.ts new file mode 100644 index 00000000..60396b4d --- /dev/null +++ b/src/renderer/features/context-menu/events.ts @@ -0,0 +1,36 @@ +import { createUseExternalEvents } from '@mantine/utils'; +import { LibraryItem } from '/@/renderer/types'; + +export type OpenContextMenuProps = { + data: any[]; + menuItems: SetContextMenuItems; + type: LibraryItem; + xPos: number; + yPos: number; +}; + +export type ContextMenuEvents = { + closeContextMenu: () => void; + openContextMenu: (args: OpenContextMenuProps) => void; +}; + +export type ContextMenuItem = + | 'play' + | 'playLast' + | 'playNext' + | 'addToPlaylist' + | 'addToFavorites' + | 'removeFromFavorites' + | 'setRating'; + +export type SetContextMenuItems = { + disabled?: boolean; + id: ContextMenuItem; + onClick?: () => void; +}[]; + +export const [useContextMenuEvents, createEvent] = + createUseExternalEvents('context-menu'); + +export const openContextMenu = createEvent('openContextMenu'); +export const closeContextMenu = createEvent('closeContextMenu'); diff --git a/src/renderer/features/context-menu/index.tsx b/src/renderer/features/context-menu/index.tsx new file mode 100644 index 00000000..ddb6383a --- /dev/null +++ b/src/renderer/features/context-menu/index.tsx @@ -0,0 +1,2 @@ +export * from './events'; +export * from './context-menu-provider';