Add ability to add/remove songs from playlist (#17)

* Add api for add/remove playlist items

* Add playlistItemId property to normalized Song

- This is used for Navidrome to delete songs from playlists

* Add mutations for add/remove from playlist

* Add context modal for playlist add

* Add remove from playlist from context menu

* Set jellyfin to use playlistItemId

* Adjust font sizing

* Add playlist add from detail pages

* Bump mantine to v6-alpha.2
This commit is contained in:
Jeff 2023-01-29 18:40:26 -08:00 committed by GitHub
parent be39c2bc1f
commit 59f4f43e84
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 1120 additions and 982 deletions

View file

@ -4,9 +4,20 @@ export const SONG_CONTEXT_MENU_ITEMS: SetContextMenuItems = [
{ id: 'play' },
{ id: 'playLast' },
{ divider: true, id: 'playNext' },
{ disabled: true, id: 'addToPlaylist' },
{ divider: true, id: 'addToPlaylist' },
{ id: 'addToFavorites' },
{ id: 'removeFromFavorites' },
{ divider: true, id: 'removeFromFavorites' },
{ disabled: true, id: 'setRating' },
];
export const PLAYLIST_SONG_CONTEXT_MENU_ITEMS: SetContextMenuItems = [
{ id: 'play' },
{ id: 'playLast' },
{ divider: true, id: 'playNext' },
{ id: 'addToPlaylist' },
{ divider: true, id: 'removeFromPlaylist' },
{ id: 'addToFavorites' },
{ divider: true, id: 'removeFromFavorites' },
{ disabled: true, id: 'setRating' },
];
@ -14,7 +25,7 @@ export const ALBUM_CONTEXT_MENU_ITEMS: SetContextMenuItems = [
{ id: 'play' },
{ id: 'playLast' },
{ divider: true, id: 'playNext' },
{ disabled: true, id: 'addToPlaylist' },
{ divider: true, id: 'addToPlaylist' },
{ id: 'addToFavorites' },
{ id: 'removeFromFavorites' },
{ disabled: true, id: 'setRating' },
@ -24,9 +35,9 @@ export const ARTIST_CONTEXT_MENU_ITEMS: SetContextMenuItems = [
{ id: 'play' },
{ id: 'playLast' },
{ divider: true, id: 'playNext' },
{ disabled: true, id: 'addToPlaylist' },
{ divider: true, id: 'addToPlaylist' },
{ id: 'addToFavorites' },
{ id: 'removeFromFavorites' },
{ divider: true, id: 'removeFromFavorites' },
{ disabled: true, id: 'setRating' },
];

View file

@ -1,8 +1,8 @@
import { Divider, Group, Stack } from '@mantine/core';
import { useClickOutside, useResizeObserver, useSetState, useViewportSize } from '@mantine/hooks';
import { closeAllModals, openModal } from '@mantine/modals';
import { closeAllModals, openContextModal, openModal } from '@mantine/modals';
import { createContext, Fragment, useState } from 'react';
import { LibraryItem } from '/@/renderer/api/types';
import { LibraryItem, ServerType } from '/@/renderer/api/types';
import { ConfirmModal, ContextMenu, ContextMenuButton, Text, toast } from '/@/renderer/components';
import {
OpenContextMenuProps,
@ -10,7 +10,9 @@ import {
} 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 { useCurrentServer } from '/@/renderer/store';
import { Play } from '/@/renderer/types';
type ContextMenuContextProps = {
@ -33,6 +35,8 @@ 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;
const [ref, menuRect] = useResizeObserver();
const [ctx, setCtx] = useSetState<OpenContextMenuProps>({
data: [],
@ -47,7 +51,7 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
const handlePlayQueueAdd = usePlayQueueAdd();
const openContextMenu = (args: OpenContextMenuProps) => {
const { xPos, yPos, menuItems, data, type, tableRef, dataNodes } = args;
const { xPos, yPos, menuItems, data, type, tableRef, dataNodes, context } = args;
const shouldReverseY = yPos + menuRect.height > viewport.height;
const shouldReverseX = xPos + menuRect.width > viewport.width;
@ -56,6 +60,7 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
const calculatedYPos = shouldReverseY ? yPos - menuRect.height : yPos;
setCtx({
context,
data,
dataNodes,
menuItems,
@ -216,13 +221,78 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
);
};
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,
},
modal: 'addToPlaylist',
size: 'md',
title: 'Add to playlist',
});
};
const removeFromPlaylistMutation = useRemoveFromPlaylist();
const handleRemoveFromPlaylist = () => {
const songId =
(serverType === ServerType.NAVIDROME || ServerType.JELLYFIN
? ctx.dataNodes?.map((node) => node.data.playlistItemId)
: ctx.dataNodes?.map((node) => node.data.id)) || [];
const confirm = () => {
removeFromPlaylistMutation.mutate(
{
query: {
id: ctx.context.playlistId,
songId,
},
},
{
onError: (err) => {
toast.error({
message: err.message,
title: 'Error removing song(s) from playlist',
});
},
onSuccess: () => {
toast.success({
message: `${songId.length} song(s) were removed from the playlist`,
title: 'Song(s) removed from playlist',
});
ctx.context?.tableRef?.current?.api?.refreshInfiniteCache();
closeAllModals();
},
},
);
};
openModal({
children: (
<ConfirmModal
loading={removeFromPlaylistMutation.isLoading}
onConfirm={confirm}
>
Are you sure you want to remove the following song(s) from the playlist?
</ConfirmModal>
),
title: 'Remove song(s) from playlist',
});
};
const contextMenuItems = {
addToFavorites: {
id: 'addToFavorites',
label: 'Add to favorites',
onClick: handleAddToFavorites,
},
addToPlaylist: { id: 'addToPlaylist', label: 'Add to playlist', onClick: () => {} },
addToPlaylist: { id: 'addToPlaylist', label: 'Add to playlist', onClick: handleAddToPlaylist },
createPlaylist: { id: 'createPlaylist', label: 'Create playlist', onClick: () => {} },
deletePlaylist: {
id: 'deletePlaylist',
@ -249,6 +319,11 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
label: 'Remove from favorites',
onClick: handleRemoveFromFavorites,
},
removeFromPlaylist: {
id: 'removeFromPlaylist',
label: 'Remove from playlist',
onClick: handleRemoveFromPlaylist,
},
setRating: { id: 'setRating', label: 'Set rating', onClick: () => {} },
};

View file

@ -5,6 +5,7 @@ import { MutableRefObject } from 'react';
import { LibraryItem } from '/@/renderer/api/types';
export type OpenContextMenuProps = {
context?: any;
data: any[];
dataNodes?: RowNode[];
menuItems: SetContextMenuItems;
@ -24,6 +25,7 @@ export type ContextMenuItem =
| 'playLast'
| 'playNext'
| 'addToPlaylist'
| 'removeFromPlaylist'
| 'addToFavorites'
| 'removeFromFavorites'
| 'setRating'

View file

@ -6,6 +6,7 @@ import { openContextMenu, SetContextMenuItems } from '/@/renderer/features/conte
export const useHandleTableContextMenu = (
itemType: LibraryItem,
contextMenuItems: SetContextMenuItems,
context?: any,
) => {
const handleContextMenu = (e: CellContextMenuEvent) => {
if (!e.event) return;
@ -25,6 +26,7 @@ export const useHandleTableContextMenu = (
}
openContextMenu({
context,
data: selectedRows,
dataNodes: selectedNodes,
menuItems: contextMenuItems,