Tunio Desktop client
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

332 lines
9.8 KiB

/* eslint-disable no-await-in-loop */
import React, { useRef, useState } from 'react';
import _ from 'lodash';
import { useQuery, useQueryClient } from 'react-query';
import { useHistory } from 'react-router';
import { Form, Input, Whisper } from 'rsuite';
import {
getPlaylists,
updatePlaylistSongsLg,
createPlaylist,
batchStar,
batchUnstar,
} from '../../api/api';
import { useAppDispatch, useAppSelector } from '../../redux/hooks';
import {
addProcessingPlaylist,
removeProcessingPlaylist,
setContextMenu,
} from '../../redux/miscSlice';
import {
appendPlayQueue,
fixPlayer2Index,
removeFromPlayQueue,
setStar,
} from '../../redux/playQueueSlice';
import {
ContextMenuDivider,
ContextMenuWindow,
StyledContextMenuButton,
StyledInputPicker,
StyledButton,
StyledInputGroup,
StyledPopover,
} from './styled';
import { notifyToast } from './toast';
import { errorMessages, isFailedResponse } from '../../shared/utils';
export const ContextMenuButton = ({ text, children, ...rest }: any) => {
return (
<StyledContextMenuButton {...rest} appearance="subtle" size="sm" block>
{children}
{text}
</StyledContextMenuButton>
);
};
export const ContextMenu = ({
yPos,
xPos,
width,
numOfButtons,
numOfDividers,
hasTitle,
children,
}: any) => {
return (
<ContextMenuWindow
yPos={yPos}
xPos={xPos}
width={width}
numOfButtons={numOfButtons}
numOfDividers={numOfDividers}
hasTitle={hasTitle}
>
{children}
</ContextMenuWindow>
);
};
export const GlobalContextMenu = () => {
const history = useHistory();
const dispatch = useAppDispatch();
const queryClient = useQueryClient();
const playQueue = useAppSelector((state) => state.playQueue);
const misc = useAppSelector((state) => state.misc);
const multiSelect = useAppSelector((state) => state.multiSelect);
const playlistTriggerRef = useRef<any>();
const [selectedPlaylistId, setSelectedPlaylistId] = useState('');
const [shouldCreatePlaylist, setShouldCreatePlaylist] = useState(false);
const [newPlaylistName, setNewPlaylistName] = useState('');
const { data: playlists }: any = useQuery(['playlists', 'name'], () => getPlaylists('name'));
const handleAddToQueue = () => {
const entriesByRowIndexAsc = _.orderBy(multiSelect.selected, 'rowIndex', 'asc');
notifyToast('info', `Added ${multiSelect.selected.length} song(s) to the queue`);
dispatch(appendPlayQueue({ entries: entriesByRowIndexAsc }));
dispatch(setContextMenu({ show: false }));
};
const handleRemoveFromQueue = async () => {
dispatch(removeFromPlayQueue({ entries: multiSelect.selected }));
if (playQueue.currentPlayer === 1) {
dispatch(fixPlayer2Index());
}
dispatch(setContextMenu({ show: false }));
};
const handleAddToPlaylist = async () => {
// If the window is closed, the selectedPlaylistId will be deleted
const localSelectedPlaylistId = selectedPlaylistId;
dispatch(addProcessingPlaylist(selectedPlaylistId));
const sortedEntries = [...multiSelect.selected].sort(
(a: any, b: any) => a.rowIndex - b.rowIndex
);
try {
const res = await updatePlaylistSongsLg(localSelectedPlaylistId, sortedEntries);
if (isFailedResponse(res)) {
notifyToast('error', errorMessages(res)[0]);
} else {
notifyToast(
'success',
<>
<p>
Added {sortedEntries.length} song(s) to playlist &quot;
{playlists.find((playlist: any) => playlist.id === localSelectedPlaylistId)?.name}
&quot;
</p>
<StyledButton
appearance="link"
onClick={() => {
history.push(`/playlist/${localSelectedPlaylistId}`);
dispatch(setContextMenu({ show: false }));
}}
>
Go to playlist
</StyledButton>
</>
);
}
} catch (err) {
notifyToast('error', err);
}
dispatch(removeProcessingPlaylist(localSelectedPlaylistId));
};
const handleCreatePlaylist = async () => {
try {
const res = await createPlaylist(newPlaylistName);
if (isFailedResponse(res)) {
notifyToast('error', errorMessages(res)[0]);
} else {
await queryClient.refetchQueries(['playlists'], {
active: true,
});
notifyToast('success', `Playlist "${newPlaylistName}" created!`);
}
} catch (err) {
notifyToast('error', err);
}
};
const refetchAfterFavorite = async () => {
await queryClient.refetchQueries(['starred'], {
active: true,
});
await queryClient.refetchQueries(['album'], {
active: true,
});
await queryClient.refetchQueries(['albumList'], {
active: true,
});
await queryClient.refetchQueries(['playlist'], {
active: true,
});
};
const handleFavorite = async () => {
dispatch(setContextMenu({ show: false }));
const sortedEntries = [...multiSelect.selected].sort(
(a: any, b: any) => a.rowIndex - b.rowIndex
);
const ids = _.map(sortedEntries, 'id');
try {
const res = await batchStar(ids, sortedEntries[0].type);
if (isFailedResponse(res)) {
notifyToast('error', errorMessages(res)[0]);
} else {
ids.forEach((id) => dispatch(setStar({ id, type: 'star' })));
}
await refetchAfterFavorite();
} catch (err) {
notifyToast('error', err);
}
};
const handleUnfavorite = async () => {
dispatch(setContextMenu({ show: false }));
const starredEntries = multiSelect.selected.filter((entry: any) => entry.starred);
const ids = _.map(starredEntries, 'id');
try {
const res = await batchUnstar(ids, starredEntries[0].type);
if (isFailedResponse(res)) {
notifyToast('error', errorMessages(res)[0]);
} else {
ids.forEach((id) => dispatch(setStar({ id, type: 'unstar' })));
}
await refetchAfterFavorite();
} catch (err) {
notifyToast('error', err);
}
};
return (
<>
{misc.contextMenu.show && (
<ContextMenu
xPos={misc.contextMenu.xPos}
yPos={misc.contextMenu.yPos}
width={190}
numOfButtons={6}
numOfDividers={3}
>
<ContextMenuButton text={`Selected: ${multiSelect.selected.length}`} />
<ContextMenuDivider />
<ContextMenuButton
text="Add to queue"
onClick={handleAddToQueue}
disabled={misc.contextMenu.disabledOptions.includes('addToQueue')}
/>
<ContextMenuButton
text="Remove from current"
onClick={handleRemoveFromQueue}
disabled={misc.contextMenu.disabledOptions.includes('removeFromCurrent')}
/>
<ContextMenuDivider />
<Whisper
ref={playlistTriggerRef}
enterable
placement="autoHorizontalStart"
trigger="none"
speaker={
<StyledPopover>
<StyledInputPicker
data={playlists}
placement="autoVerticalStart"
virtualized
labelKey="name"
valueKey="id"
width={200}
onChange={(e: any) => setSelectedPlaylistId(e)}
/>
<StyledButton
disabled={
!selectedPlaylistId || misc.isProcessingPlaylist.includes(selectedPlaylistId)
}
loading={misc.isProcessingPlaylist.includes(selectedPlaylistId)}
onClick={handleAddToPlaylist}
>
Add
</StyledButton>
<div>
<StyledButton
appearance="link"
onClick={() => setShouldCreatePlaylist(!shouldCreatePlaylist)}
>
Create new playlist
</StyledButton>
</div>
{shouldCreatePlaylist && (
<Form>
<StyledInputGroup>
<Input
placeholder="Enter name..."
value={newPlaylistName}
onChange={(e) => setNewPlaylistName(e)}
/>
</StyledInputGroup>
<br />
<StyledButton
size="sm"
type="submit"
block
loading={false}
appearance="primary"
onClick={() => {
handleCreatePlaylist();
setShouldCreatePlaylist(false);
}}
>
Create playlist
</StyledButton>
</Form>
)}
</StyledPopover>
}
>
<ContextMenuButton
text="Add to playlist"
onClick={() =>
playlistTriggerRef.current.state.isOverlayShown
? playlistTriggerRef.current.close()
: playlistTriggerRef.current.open()
}
disabled={misc.contextMenu.disabledOptions.includes('addToPlaylist')}
/>
</Whisper>
<ContextMenuDivider />
<ContextMenuButton
text="Add to favorites"
onClick={handleFavorite}
disabled={misc.contextMenu.disabledOptions.includes('addToFavorites')}
/>
<ContextMenuButton
text="Remove from favorites"
onClick={handleUnfavorite}
disabled={misc.contextMenu.disabledOptions.includes('removeFromFavorites')}
/>
</ContextMenu>
)}
</>
);
};