mirror of
https://github.com/antebudimir/feishin.git
synced 2026-01-01 10:23:33 +00:00
Add ratings support (#21)
* Update rating types for multiserver support * Add rating mutation * Add rating support to table views * Add rating support on playerbar * Add hovercard component * Handle rating from context menu - Improve context menu components - Allow left / right icons - Allow nested menus * Add selected item count * Fix context menu auto direction * Add transition and move portal for context menu * Re-use context menu for all item dropdowns * Add ratings to detail pages / double click to clear * Bump react-query package
This commit is contained in:
parent
f50ec5cf31
commit
22fec8f9d3
27 changed files with 1189 additions and 503 deletions
|
|
@ -7,7 +7,7 @@ export const SONG_CONTEXT_MENU_ITEMS: SetContextMenuItems = [
|
|||
{ divider: true, id: 'addToPlaylist' },
|
||||
{ id: 'addToFavorites' },
|
||||
{ divider: true, id: 'removeFromFavorites' },
|
||||
{ disabled: true, id: 'setRating' },
|
||||
{ children: true, disabled: false, id: 'setRating' },
|
||||
];
|
||||
|
||||
export const PLAYLIST_SONG_CONTEXT_MENU_ITEMS: SetContextMenuItems = [
|
||||
|
|
@ -18,7 +18,7 @@ export const PLAYLIST_SONG_CONTEXT_MENU_ITEMS: SetContextMenuItems = [
|
|||
{ divider: true, id: 'removeFromPlaylist' },
|
||||
{ id: 'addToFavorites' },
|
||||
{ divider: true, id: 'removeFromFavorites' },
|
||||
{ disabled: true, id: 'setRating' },
|
||||
{ children: true, disabled: false, id: 'setRating' },
|
||||
];
|
||||
|
||||
export const ALBUM_CONTEXT_MENU_ITEMS: SetContextMenuItems = [
|
||||
|
|
@ -28,7 +28,7 @@ export const ALBUM_CONTEXT_MENU_ITEMS: SetContextMenuItems = [
|
|||
{ divider: true, id: 'addToPlaylist' },
|
||||
{ id: 'addToFavorites' },
|
||||
{ id: 'removeFromFavorites' },
|
||||
{ disabled: true, id: 'setRating' },
|
||||
{ children: true, disabled: false, id: 'setRating' },
|
||||
];
|
||||
|
||||
export const ARTIST_CONTEXT_MENU_ITEMS: SetContextMenuItems = [
|
||||
|
|
@ -38,7 +38,7 @@ export const ARTIST_CONTEXT_MENU_ITEMS: SetContextMenuItems = [
|
|||
{ divider: true, id: 'addToPlaylist' },
|
||||
{ id: 'addToFavorites' },
|
||||
{ divider: true, id: 'removeFromFavorites' },
|
||||
{ disabled: true, id: 'setRating' },
|
||||
{ children: true, disabled: false, id: 'setRating' },
|
||||
];
|
||||
|
||||
export const PLAYLIST_CONTEXT_MENU_ITEMS: SetContextMenuItems = [
|
||||
|
|
|
|||
|
|
@ -1,17 +1,45 @@
|
|||
import { Divider, Group, Stack } from '@mantine/core';
|
||||
import { useClickOutside, useResizeObserver, useSetState, useViewportSize } from '@mantine/hooks';
|
||||
import { closeAllModals, openContextModal, openModal } from '@mantine/modals';
|
||||
import { createContext, Fragment, useState } from 'react';
|
||||
import { LibraryItem, ServerType } from '/@/renderer/api/types';
|
||||
import { ConfirmModal, ContextMenu, ContextMenuButton, Text, toast } from '/@/renderer/components';
|
||||
import { RowNode } from '@ag-grid-community/core';
|
||||
import { Divider, Group, Portal, Stack } from '@mantine/core';
|
||||
import {
|
||||
useClickOutside,
|
||||
useMergedRef,
|
||||
useResizeObserver,
|
||||
useSetState,
|
||||
useViewportSize,
|
||||
} 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 {
|
||||
RiAddBoxFill,
|
||||
RiAddCircleFill,
|
||||
RiArrowRightSFill,
|
||||
RiDeleteBinFill,
|
||||
RiDislikeFill,
|
||||
RiHeartFill,
|
||||
RiPlayFill,
|
||||
RiPlayListAddFill,
|
||||
RiStarFill,
|
||||
} from 'react-icons/ri';
|
||||
import { AnyLibraryItems, LibraryItem, ServerType } from '/@/renderer/api/types';
|
||||
import {
|
||||
ConfirmModal,
|
||||
ContextMenu,
|
||||
ContextMenuButton,
|
||||
HoverCard,
|
||||
Rating,
|
||||
Text,
|
||||
toast,
|
||||
} from '/@/renderer/components';
|
||||
import {
|
||||
ContextMenuItemType,
|
||||
OpenContextMenuProps,
|
||||
useContextMenuEvents,
|
||||
} from '/@/renderer/features/context-menu/events';
|
||||
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 } from '/@/renderer/features/shared';
|
||||
import { useCreateFavorite, useDeleteFavorite, useUpdateRating } from '/@/renderer/features/shared';
|
||||
import { useCurrentServer } from '/@/renderer/store';
|
||||
import { Play } from '/@/renderer/types';
|
||||
|
||||
|
|
@ -20,6 +48,16 @@ type ContextMenuContextProps = {
|
|||
openContextMenu: (args: OpenContextMenuProps) => void;
|
||||
};
|
||||
|
||||
type ContextMenuItem = {
|
||||
children?: ContextMenuItem[];
|
||||
disabled?: boolean;
|
||||
id: string;
|
||||
label: string | ReactNode;
|
||||
leftIcon?: ReactNode;
|
||||
onClick?: (...args: any) => any;
|
||||
rightIcon?: ReactNode;
|
||||
};
|
||||
|
||||
const ContextMenuContext = createContext<ContextMenuContextProps>({
|
||||
closeContextMenu: () => {},
|
||||
openContextMenu: (args: OpenContextMenuProps) => {
|
||||
|
|
@ -34,6 +72,7 @@ export interface ContextMenuProviderProps {
|
|||
export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
|
||||
const [opened, setOpened] = useState(false);
|
||||
const clickOutsideRef = useClickOutside(() => setOpened(false));
|
||||
|
||||
const viewport = useViewportSize();
|
||||
const server = useCurrentServer();
|
||||
const serverType = server?.type;
|
||||
|
|
@ -53,11 +92,16 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
|
|||
const openContextMenu = (args: OpenContextMenuProps) => {
|
||||
const { xPos, yPos, menuItems, data, type, tableRef, dataNodes, context } = args;
|
||||
|
||||
const shouldReverseY = yPos + menuRect.height > viewport.height;
|
||||
const shouldReverseX = xPos + menuRect.width > viewport.width;
|
||||
// If the context menu dimension can't be automatically calculated, calculate it manually
|
||||
// This is a hacky way since resize observer may not automatically recalculate when not rendered
|
||||
const menuHeight = menuRect.height || (menuItems.length + 1) * 50;
|
||||
const menuWidth = menuRect.width || 220;
|
||||
|
||||
const calculatedXPos = shouldReverseX ? xPos - menuRect.width : xPos;
|
||||
const calculatedYPos = shouldReverseY ? yPos - menuRect.height : yPos;
|
||||
const shouldReverseY = yPos + menuHeight > viewport.height;
|
||||
const shouldReverseX = xPos + menuWidth > viewport.width;
|
||||
|
||||
const calculatedXPos = shouldReverseX ? xPos - menuWidth : xPos;
|
||||
const calculatedYPos = shouldReverseY ? yPos - menuHeight : yPos;
|
||||
|
||||
setCtx({
|
||||
context,
|
||||
|
|
@ -90,44 +134,47 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
|
|||
openContextMenu,
|
||||
});
|
||||
|
||||
const handlePlay = (play: Play) => {
|
||||
switch (ctx.type) {
|
||||
case LibraryItem.ALBUM:
|
||||
handlePlayQueueAdd?.({
|
||||
byItemType: { id: ctx.data.map((item) => item.id), type: ctx.type },
|
||||
play,
|
||||
});
|
||||
break;
|
||||
case LibraryItem.ARTIST:
|
||||
handlePlayQueueAdd?.({
|
||||
byItemType: { id: ctx.data.map((item) => item.id), type: ctx.type },
|
||||
play,
|
||||
});
|
||||
break;
|
||||
case LibraryItem.ALBUM_ARTIST:
|
||||
handlePlayQueueAdd?.({
|
||||
byItemType: { id: ctx.data.map((item) => item.id), type: ctx.type },
|
||||
play,
|
||||
});
|
||||
break;
|
||||
case LibraryItem.SONG:
|
||||
handlePlayQueueAdd?.({ byData: ctx.data, play });
|
||||
break;
|
||||
case LibraryItem.PLAYLIST:
|
||||
for (const item of ctx.data) {
|
||||
const handlePlay = useCallback(
|
||||
(play: Play) => {
|
||||
switch (ctx.type) {
|
||||
case LibraryItem.ALBUM:
|
||||
handlePlayQueueAdd?.({
|
||||
byItemType: { id: [item.id], type: ctx.type },
|
||||
byItemType: { id: ctx.data.map((item) => item.id), type: ctx.type },
|
||||
play,
|
||||
});
|
||||
}
|
||||
break;
|
||||
case LibraryItem.ARTIST:
|
||||
handlePlayQueueAdd?.({
|
||||
byItemType: { id: ctx.data.map((item) => item.id), type: ctx.type },
|
||||
play,
|
||||
});
|
||||
break;
|
||||
case LibraryItem.ALBUM_ARTIST:
|
||||
handlePlayQueueAdd?.({
|
||||
byItemType: { id: ctx.data.map((item) => item.id), type: ctx.type },
|
||||
play,
|
||||
});
|
||||
break;
|
||||
case LibraryItem.SONG:
|
||||
handlePlayQueueAdd?.({ byData: ctx.data, play });
|
||||
break;
|
||||
case LibraryItem.PLAYLIST:
|
||||
for (const item of ctx.data) {
|
||||
handlePlayQueueAdd?.({
|
||||
byItemType: { id: [item.id], type: ctx.type },
|
||||
play,
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
};
|
||||
break;
|
||||
}
|
||||
},
|
||||
[ctx.data, ctx.type, handlePlayQueueAdd],
|
||||
);
|
||||
|
||||
const deletePlaylistMutation = useDeletePlaylist();
|
||||
|
||||
const handleDeletePlaylist = () => {
|
||||
const handleDeletePlaylist = useCallback(() => {
|
||||
for (const item of ctx.data) {
|
||||
deletePlaylistMutation?.mutate(
|
||||
{ query: { id: item.id } },
|
||||
|
|
@ -148,9 +195,9 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
|
|||
);
|
||||
}
|
||||
closeAllModals();
|
||||
};
|
||||
}, [ctx.data, deletePlaylistMutation]);
|
||||
|
||||
const openDeletePlaylistModal = () => {
|
||||
const openDeletePlaylistModal = useCallback(() => {
|
||||
openModal({
|
||||
children: (
|
||||
<ConfirmModal onConfirm={handleDeletePlaylist}>
|
||||
|
|
@ -170,17 +217,30 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
|
|||
),
|
||||
title: 'Delete playlist(s)',
|
||||
});
|
||||
};
|
||||
}, [ctx.data, handleDeletePlaylist]);
|
||||
|
||||
const createFavoriteMutation = useCreateFavorite();
|
||||
const deleteFavoriteMutation = useDeleteFavorite();
|
||||
const handleAddToFavorites = () => {
|
||||
if (!ctx.dataNodes) return;
|
||||
const nodesToFavorite = ctx.dataNodes.filter((item) => !item.data.userFavorite);
|
||||
const handleAddToFavorites = useCallback(() => {
|
||||
if (!ctx.dataNodes && !ctx.data) return;
|
||||
|
||||
let itemsToFavorite: AnyLibraryItems = [];
|
||||
let nodesToFavorite: RowNode<any>[] = [];
|
||||
|
||||
if (ctx.dataNodes) {
|
||||
nodesToFavorite = ctx.dataNodes.filter((item) => !item.data.userFavorite);
|
||||
} else {
|
||||
itemsToFavorite = ctx.data.filter((item) => !item.userFavorite);
|
||||
}
|
||||
|
||||
const idsToFavorite = nodesToFavorite
|
||||
? nodesToFavorite.map((node) => node.data.id)
|
||||
: itemsToFavorite.map((item) => item.id);
|
||||
|
||||
createFavoriteMutation.mutate(
|
||||
{
|
||||
query: {
|
||||
id: nodesToFavorite.map((item) => item.data.id),
|
||||
id: idsToFavorite,
|
||||
type: ctx.type,
|
||||
},
|
||||
},
|
||||
|
|
@ -192,22 +252,36 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
|
|||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
for (const node of nodesToFavorite) {
|
||||
node.setData({ ...node.data, userFavorite: true });
|
||||
if (ctx.dataNodes) {
|
||||
for (const node of nodesToFavorite) {
|
||||
node.setData({ ...node.data, userFavorite: true });
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
}, [createFavoriteMutation, ctx.data, ctx.dataNodes, ctx.type]);
|
||||
|
||||
const handleRemoveFromFavorites = () => {
|
||||
if (!ctx.dataNodes) return;
|
||||
const nodesToUnfavorite = ctx.dataNodes.filter((item) => item.data.userFavorite);
|
||||
const handleRemoveFromFavorites = useCallback(() => {
|
||||
if (!ctx.dataNodes && !ctx.data) return;
|
||||
|
||||
let itemsToUnfavorite: AnyLibraryItems = [];
|
||||
let nodesToUnfavorite: RowNode<any>[] = [];
|
||||
|
||||
if (ctx.dataNodes) {
|
||||
nodesToUnfavorite = ctx.dataNodes.filter((item) => !item.data.userFavorite);
|
||||
} else {
|
||||
itemsToUnfavorite = ctx.data.filter((item) => !item.userFavorite);
|
||||
}
|
||||
|
||||
const idsToUnfavorite = nodesToUnfavorite
|
||||
? nodesToUnfavorite.map((node) => node.data.id)
|
||||
: itemsToUnfavorite.map((item) => item.id);
|
||||
|
||||
deleteFavoriteMutation.mutate(
|
||||
{
|
||||
query: {
|
||||
id: nodesToUnfavorite.map((item) => item.data.id),
|
||||
id: idsToUnfavorite,
|
||||
type: ctx.type,
|
||||
},
|
||||
},
|
||||
|
|
@ -219,28 +293,60 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
|
|||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
}, [ctx.data, ctx.dataNodes, ctx.type, deleteFavoriteMutation]);
|
||||
|
||||
const handleAddToPlaylist = useCallback(() => {
|
||||
if (!ctx.dataNodes && !ctx.data) return;
|
||||
|
||||
const albumId: string[] = [];
|
||||
const artistId: string[] = [];
|
||||
const songId: string[] = [];
|
||||
|
||||
if (ctx.dataNodes) {
|
||||
for (const node of ctx.dataNodes) {
|
||||
switch (node.data.type) {
|
||||
case LibraryItem.ALBUM:
|
||||
albumId.push(node.data.id);
|
||||
break;
|
||||
case LibraryItem.ARTIST:
|
||||
artistId.push(node.data.id);
|
||||
break;
|
||||
case LibraryItem.SONG:
|
||||
songId.push(node.data.id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const item of ctx.data) {
|
||||
switch (item.type) {
|
||||
case LibraryItem.ALBUM:
|
||||
albumId.push(item.id);
|
||||
break;
|
||||
case LibraryItem.ARTIST:
|
||||
artistId.push(item.id);
|
||||
break;
|
||||
case LibraryItem.SONG:
|
||||
songId.push(item.id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleAddToPlaylist = () => {
|
||||
if (!ctx.dataNodes) return;
|
||||
openContextModal({
|
||||
innerProps: {
|
||||
albumId:
|
||||
ctx.type === LibraryItem.ALBUM ? ctx.dataNodes.map((node) => node.data.id) : undefined,
|
||||
artistId:
|
||||
ctx.type === LibraryItem.ARTIST ? ctx.dataNodes.map((node) => node.data.id) : undefined,
|
||||
songId:
|
||||
ctx.type === LibraryItem.SONG ? ctx.dataNodes.map((node) => node.data.id) : undefined,
|
||||
albumId: albumId.length > 0 ? albumId : undefined,
|
||||
artistId: artistId.length > 0 ? artistId : undefined,
|
||||
songId: songId.length > 0 ? songId : undefined,
|
||||
},
|
||||
modal: 'addToPlaylist',
|
||||
size: 'md',
|
||||
title: 'Add to playlist',
|
||||
});
|
||||
};
|
||||
}, [ctx.data, ctx.dataNodes]);
|
||||
|
||||
const removeFromPlaylistMutation = useRemoveFromPlaylist();
|
||||
|
||||
const handleRemoveFromPlaylist = () => {
|
||||
const handleRemoveFromPlaylist = useCallback(() => {
|
||||
const songId =
|
||||
(serverType === ServerType.NAVIDROME || ServerType.JELLYFIN
|
||||
? ctx.dataNodes?.map((node) => node.data.playlistItemId)
|
||||
|
|
@ -284,48 +390,198 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
|
|||
),
|
||||
title: 'Remove song(s) from playlist',
|
||||
});
|
||||
};
|
||||
}, [
|
||||
ctx.context?.playlistId,
|
||||
ctx.context?.tableRef,
|
||||
ctx.dataNodes,
|
||||
removeFromPlaylistMutation,
|
||||
serverType,
|
||||
]);
|
||||
|
||||
const contextMenuItems = {
|
||||
addToFavorites: {
|
||||
id: 'addToFavorites',
|
||||
label: 'Add to favorites',
|
||||
onClick: handleAddToFavorites,
|
||||
const updateRatingMutation = useUpdateRating();
|
||||
|
||||
const handleUpdateRating = useCallback(
|
||||
(rating: number) => {
|
||||
if (!ctx.dataNodes || !ctx.data) return;
|
||||
|
||||
let uniqueServerIds: string[] = [];
|
||||
let items: AnyLibraryItems = [];
|
||||
|
||||
if (ctx.dataNodes) {
|
||||
uniqueServerIds = ctx.dataNodes.reduce((acc, node) => {
|
||||
if (!acc.includes(node.data.serverId)) {
|
||||
acc.push(node.data.serverId);
|
||||
}
|
||||
return acc;
|
||||
}, [] as string[]);
|
||||
} else {
|
||||
uniqueServerIds = ctx.data.reduce((acc, item) => {
|
||||
if (!acc.includes(item.serverId)) {
|
||||
acc.push(item.serverId);
|
||||
}
|
||||
return acc;
|
||||
}, [] as string[]);
|
||||
}
|
||||
|
||||
for (const serverId of uniqueServerIds) {
|
||||
if (ctx.dataNodes) {
|
||||
items = ctx.dataNodes
|
||||
.filter((node) => node.data.serverId === serverId)
|
||||
.map((node) => node.data);
|
||||
} else {
|
||||
items = ctx.data.filter((item) => item.serverId === serverId);
|
||||
}
|
||||
|
||||
updateRatingMutation.mutate({
|
||||
_serverId: serverId,
|
||||
query: {
|
||||
item: items,
|
||||
rating,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
addToPlaylist: { id: 'addToPlaylist', label: 'Add to playlist', onClick: handleAddToPlaylist },
|
||||
createPlaylist: { id: 'createPlaylist', label: 'Create playlist', onClick: () => {} },
|
||||
deletePlaylist: {
|
||||
id: 'deletePlaylist',
|
||||
label: 'Delete playlist',
|
||||
onClick: openDeletePlaylistModal,
|
||||
},
|
||||
play: {
|
||||
id: 'play',
|
||||
label: 'Play',
|
||||
onClick: () => handlePlay(Play.NOW),
|
||||
},
|
||||
playLast: {
|
||||
id: 'playLast',
|
||||
label: 'Add to queue',
|
||||
onClick: () => handlePlay(Play.LAST),
|
||||
},
|
||||
playNext: {
|
||||
id: 'playNext',
|
||||
label: 'Add to queue next',
|
||||
onClick: () => handlePlay(Play.NEXT),
|
||||
},
|
||||
removeFromFavorites: {
|
||||
id: 'removeFromFavorites',
|
||||
label: 'Remove from favorites',
|
||||
onClick: handleRemoveFromFavorites,
|
||||
},
|
||||
removeFromPlaylist: {
|
||||
id: 'removeFromPlaylist',
|
||||
label: 'Remove from playlist',
|
||||
onClick: handleRemoveFromPlaylist,
|
||||
},
|
||||
setRating: { id: 'setRating', label: 'Set rating', onClick: () => {} },
|
||||
};
|
||||
[ctx.data, ctx.dataNodes, updateRatingMutation],
|
||||
);
|
||||
|
||||
const contextMenuItems: Record<ContextMenuItemType, ContextMenuItem> = useMemo(() => {
|
||||
return {
|
||||
addToFavorites: {
|
||||
id: 'addToFavorites',
|
||||
label: 'Add to favorites',
|
||||
leftIcon: <RiHeartFill size="1.1rem" />,
|
||||
onClick: handleAddToFavorites,
|
||||
},
|
||||
addToPlaylist: {
|
||||
id: 'addToPlaylist',
|
||||
label: 'Add to playlist',
|
||||
leftIcon: <RiPlayListAddFill size="1.1rem" />,
|
||||
onClick: handleAddToPlaylist,
|
||||
},
|
||||
createPlaylist: { id: 'createPlaylist', label: 'Create playlist', onClick: () => {} },
|
||||
deletePlaylist: {
|
||||
id: 'deletePlaylist',
|
||||
label: 'Delete playlist',
|
||||
leftIcon: <RiDeleteBinFill size="1.1rem" />,
|
||||
onClick: openDeletePlaylistModal,
|
||||
},
|
||||
play: {
|
||||
id: 'play',
|
||||
label: 'Play',
|
||||
leftIcon: <RiPlayFill size="1.1rem" />,
|
||||
onClick: () => handlePlay(Play.NOW),
|
||||
},
|
||||
playLast: {
|
||||
id: 'playLast',
|
||||
label: 'Add to queue',
|
||||
leftIcon: <RiAddBoxFill size="1.1rem" />,
|
||||
onClick: () => handlePlay(Play.LAST),
|
||||
},
|
||||
playNext: {
|
||||
id: 'playNext',
|
||||
label: 'Add to queue next',
|
||||
leftIcon: <RiAddCircleFill size="1.1rem" />,
|
||||
onClick: () => handlePlay(Play.NEXT),
|
||||
},
|
||||
removeFromFavorites: {
|
||||
id: 'removeFromFavorites',
|
||||
label: 'Remove from favorites',
|
||||
leftIcon: <RiDislikeFill size="1.1rem" />,
|
||||
onClick: handleRemoveFromFavorites,
|
||||
},
|
||||
removeFromPlaylist: {
|
||||
id: 'removeFromPlaylist',
|
||||
label: 'Remove from playlist',
|
||||
leftIcon: <RiDeleteBinFill size="1.1rem" />,
|
||||
onClick: handleRemoveFromPlaylist,
|
||||
},
|
||||
setRating: {
|
||||
children: [
|
||||
{
|
||||
id: 'zeroStar',
|
||||
label: (
|
||||
<Rating
|
||||
readOnly
|
||||
value={0}
|
||||
onClick={() => {}}
|
||||
/>
|
||||
),
|
||||
onClick: () => handleUpdateRating(0),
|
||||
},
|
||||
{
|
||||
id: 'oneStar',
|
||||
label: (
|
||||
<Rating
|
||||
readOnly
|
||||
value={1}
|
||||
onClick={() => {}}
|
||||
/>
|
||||
),
|
||||
onClick: () => handleUpdateRating(1),
|
||||
},
|
||||
{
|
||||
id: 'twoStar',
|
||||
label: (
|
||||
<Rating
|
||||
readOnly
|
||||
value={2}
|
||||
onClick={() => {}}
|
||||
/>
|
||||
),
|
||||
onClick: () => handleUpdateRating(2),
|
||||
},
|
||||
{
|
||||
id: 'threeStar',
|
||||
label: (
|
||||
<Rating
|
||||
readOnly
|
||||
value={3}
|
||||
onClick={() => {}}
|
||||
/>
|
||||
),
|
||||
onClick: () => handleUpdateRating(3),
|
||||
},
|
||||
{
|
||||
id: 'fourStar',
|
||||
label: (
|
||||
<Rating
|
||||
readOnly
|
||||
value={4}
|
||||
onClick={() => {}}
|
||||
/>
|
||||
),
|
||||
onClick: () => handleUpdateRating(4),
|
||||
},
|
||||
{
|
||||
id: 'fiveStar',
|
||||
label: (
|
||||
<Rating
|
||||
readOnly
|
||||
value={5}
|
||||
onClick={() => {}}
|
||||
/>
|
||||
),
|
||||
onClick: () => handleUpdateRating(5),
|
||||
},
|
||||
],
|
||||
id: 'setRating',
|
||||
label: 'Set rating',
|
||||
leftIcon: <RiStarFill size="1.1rem" />,
|
||||
onClick: () => {},
|
||||
rightIcon: <RiArrowRightSFill size="1.2rem" />,
|
||||
},
|
||||
};
|
||||
}, [
|
||||
handleAddToFavorites,
|
||||
handleAddToPlaylist,
|
||||
handlePlay,
|
||||
handleRemoveFromFavorites,
|
||||
handleRemoveFromPlaylist,
|
||||
handleUpdateRating,
|
||||
openDeletePlaylistModal,
|
||||
]);
|
||||
|
||||
const mergedRef = useMergedRef(ref, clickOutsideRef);
|
||||
|
||||
return (
|
||||
<ContextMenuContext.Provider
|
||||
|
|
@ -334,43 +590,89 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
|
|||
openContextMenu,
|
||||
}}
|
||||
>
|
||||
{opened && (
|
||||
<ContextMenu
|
||||
ref={ref}
|
||||
minWidth={125}
|
||||
xPos={ctx.xPos}
|
||||
yPos={ctx.yPos}
|
||||
>
|
||||
<Stack
|
||||
ref={clickOutsideRef}
|
||||
spacing={0}
|
||||
onClick={closeContextMenu}
|
||||
>
|
||||
{ctx.menuItems?.map((item) => {
|
||||
return (
|
||||
<Fragment key={`context-menu-${item.id}`}>
|
||||
<ContextMenuButton
|
||||
as="button"
|
||||
disabled={item.disabled}
|
||||
onClick={contextMenuItems[item.id as keyof typeof contextMenuItems].onClick}
|
||||
>
|
||||
{contextMenuItems[item.id as keyof typeof contextMenuItems].label}
|
||||
</ContextMenuButton>
|
||||
{item.divider && (
|
||||
<Divider
|
||||
key={`context-menu-divider-${item.id}`}
|
||||
color="rgb(62, 62, 62)"
|
||||
size="sm"
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
</ContextMenu>
|
||||
)}
|
||||
<Portal>
|
||||
<AnimatePresence>
|
||||
{opened && (
|
||||
<ContextMenu
|
||||
ref={mergedRef}
|
||||
minWidth={125}
|
||||
xPos={ctx.xPos}
|
||||
yPos={ctx.yPos}
|
||||
>
|
||||
<Stack spacing={0}>
|
||||
<Stack
|
||||
spacing={0}
|
||||
onClick={closeContextMenu}
|
||||
>
|
||||
{ctx.menuItems?.map((item) => {
|
||||
return (
|
||||
<Fragment key={`context-menu-${item.id}`}>
|
||||
{item.children ? (
|
||||
<HoverCard
|
||||
offset={5}
|
||||
position="right"
|
||||
>
|
||||
<HoverCard.Target>
|
||||
<ContextMenuButton
|
||||
disabled={item.disabled}
|
||||
leftIcon={contextMenuItems[item.id].leftIcon}
|
||||
rightIcon={contextMenuItems[item.id].rightIcon}
|
||||
onClick={contextMenuItems[item.id].onClick}
|
||||
>
|
||||
{contextMenuItems[item.id].label}
|
||||
</ContextMenuButton>
|
||||
</HoverCard.Target>
|
||||
<HoverCard.Dropdown>
|
||||
<Stack spacing={0}>
|
||||
{contextMenuItems[item.id].children?.map((child) => (
|
||||
<>
|
||||
<ContextMenuButton
|
||||
key={`sub-${child.id}`}
|
||||
disabled={child.disabled}
|
||||
leftIcon={child.leftIcon}
|
||||
rightIcon={child.rightIcon}
|
||||
onClick={child.onClick}
|
||||
>
|
||||
{child.label}
|
||||
</ContextMenuButton>
|
||||
</>
|
||||
))}
|
||||
</Stack>
|
||||
</HoverCard.Dropdown>
|
||||
</HoverCard>
|
||||
) : (
|
||||
<ContextMenuButton
|
||||
disabled={item.disabled}
|
||||
leftIcon={contextMenuItems[item.id].leftIcon}
|
||||
rightIcon={contextMenuItems[item.id].rightIcon}
|
||||
onClick={contextMenuItems[item.id].onClick}
|
||||
>
|
||||
{contextMenuItems[item.id].label}
|
||||
</ContextMenuButton>
|
||||
)}
|
||||
|
||||
{children}
|
||||
{item.divider && (
|
||||
<Divider
|
||||
key={`context-menu-divider-${item.id}`}
|
||||
color="rgb(62, 62, 62)"
|
||||
size="sm"
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
<Divider
|
||||
color="rgb(62, 62, 62)"
|
||||
size="sm"
|
||||
/>
|
||||
<ContextMenuButton disabled>{ctx.data?.length} selected</ContextMenuButton>
|
||||
</Stack>
|
||||
</ContextMenu>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
{children}
|
||||
</Portal>
|
||||
</ContextMenuContext.Provider>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ export type ContextMenuEvents = {
|
|||
openContextMenu: (args: OpenContextMenuProps) => void;
|
||||
};
|
||||
|
||||
export type ContextMenuItem =
|
||||
export type ContextMenuItemType =
|
||||
| 'play'
|
||||
| 'playLast'
|
||||
| 'playNext'
|
||||
|
|
@ -33,9 +33,10 @@ export type ContextMenuItem =
|
|||
| 'createPlaylist';
|
||||
|
||||
export type SetContextMenuItems = {
|
||||
children?: boolean;
|
||||
disabled?: boolean;
|
||||
divider?: boolean;
|
||||
id: ContextMenuItem;
|
||||
id: ContextMenuItemType;
|
||||
onClick?: () => void;
|
||||
}[];
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { CellContextMenuEvent } from '@ag-grid-community/core';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import { LibraryItem } from '/@/renderer/api/types';
|
||||
import { Album, AlbumArtist, Artist, LibraryItem, QueueSong, Song } from '/@/renderer/api/types';
|
||||
import { openContextMenu, SetContextMenuItems } from '/@/renderer/features/context-menu/events';
|
||||
|
||||
export const useHandleTableContextMenu = (
|
||||
|
|
@ -38,3 +38,30 @@ export const useHandleTableContextMenu = (
|
|||
|
||||
return handleContextMenu;
|
||||
};
|
||||
|
||||
export const useHandleGeneralContextMenu = (
|
||||
itemType: LibraryItem,
|
||||
contextMenuItems: SetContextMenuItems,
|
||||
context?: any,
|
||||
) => {
|
||||
const handleContextMenu = (
|
||||
e: any,
|
||||
data: Song[] | QueueSong[] | AlbumArtist[] | Artist[] | Album[],
|
||||
) => {
|
||||
if (!e) return;
|
||||
const clickEvent = e as MouseEvent;
|
||||
clickEvent.preventDefault();
|
||||
|
||||
openContextMenu({
|
||||
context,
|
||||
data,
|
||||
dataNodes: undefined,
|
||||
menuItems: contextMenuItems,
|
||||
type: itemType,
|
||||
xPos: clickEvent.clientX + 15,
|
||||
yPos: clickEvent.clientY + 5,
|
||||
});
|
||||
};
|
||||
|
||||
return handleContextMenu;
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue