feishin/src/renderer/features/playlists/components/playlist-detail-content.tsx

323 lines
9.9 KiB
TypeScript
Raw Normal View History

2023-01-06 13:50:40 -08:00
import { CellContextMenuEvent, ColDef, RowDoubleClickedEvent } from '@ag-grid-community/core';
2023-01-02 02:04:23 -08:00
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { Box, Group } from '@mantine/core';
import { closeAllModals, openModal } from '@mantine/modals';
import { useQueryClient } from '@tanstack/react-query';
2023-01-02 02:04:23 -08:00
import { sortBy } from 'lodash';
import { MutableRefObject, useMemo, useRef } from 'react';
import { RiMoreFill } from 'react-icons/ri';
import { generatePath, useNavigate, useParams } from 'react-router';
2023-01-02 02:04:23 -08:00
import { Link } from 'react-router-dom';
import styled from 'styled-components';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
2023-01-06 13:50:40 -08:00
import {
LibraryItem,
QueueSong,
SortOrder,
UserListQuery,
UserListSort,
} from '/@/renderer/api/types';
import {
Button,
ConfirmModal,
DropdownMenu,
getColumnDefs,
MotionGroup,
toast,
useFixedTableHeader,
VirtualTable,
} from '/@/renderer/components';
2023-01-02 02:04:23 -08:00
import { openContextMenu } from '/@/renderer/features/context-menu';
import { SONG_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/context-menu-items';
import { usePlayQueueAdd } from '/@/renderer/features/player';
import { UpdatePlaylistForm } from '/@/renderer/features/playlists/components/update-playlist-form';
import { useDeletePlaylist } from '/@/renderer/features/playlists/mutations/delete-playlist-mutation';
import { usePlaylistDetail } from '/@/renderer/features/playlists/queries/playlist-detail-query';
2023-01-02 02:04:23 -08:00
import { usePlaylistSongListInfinite } from '/@/renderer/features/playlists/queries/playlist-song-list-query';
import { PlayButton, PLAY_TYPES } from '/@/renderer/features/shared';
2023-01-02 02:04:23 -08:00
import { AppRoute } from '/@/renderer/router/routes';
import { useCurrentServer, useSongListStore } from '/@/renderer/store';
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
import { Play } from '/@/renderer/types';
2023-01-02 02:04:23 -08:00
const ContentContainer = styled.div`
2023-01-06 13:50:40 -08:00
position: relative;
2023-01-02 02:04:23 -08:00
display: flex;
flex-direction: column;
max-width: 1920px;
padding: 1rem 2rem 5rem;
overflow: hidden;
.ag-theme-alpine-dark {
--ag-header-background-color: rgba(0, 0, 0, 0%);
}
`;
interface PlaylistDetailContentProps {
tableRef: MutableRefObject<AgGridReactType | null>;
}
export const PlaylistDetailContent = ({ tableRef }: PlaylistDetailContentProps) => {
const navigate = useNavigate();
2023-01-02 02:04:23 -08:00
const { playlistId } = useParams() as { playlistId: string };
const page = useSongListStore();
const handlePlayQueueAdd = usePlayQueueAdd();
const detailQuery = usePlaylistDetail({ id: playlistId });
const playButtonBehavior = usePlayButtonBehavior();
const queryClient = useQueryClient();
const server = useCurrentServer();
2023-01-02 02:04:23 -08:00
const playlistSongsQueryInfinite = usePlaylistSongListInfinite(
{
id: playlistId,
limit: 50,
startIndex: 0,
},
{ cacheTime: 0, keepPreviousData: false },
2023-01-02 02:04:23 -08:00
);
const handleLoadMore = () => {
playlistSongsQueryInfinite.fetchNextPage();
};
const columnDefs: ColDef[] = useMemo(
() =>
getColumnDefs(page.table.columns).filter((c) => c.colId !== 'album' && c.colId !== 'artist'),
[page.table.columns],
);
const defaultColumnDefs: ColDef = useMemo(() => {
return {
lockPinned: true,
lockVisible: true,
resizable: true,
};
}, []);
const handleContextMenu = (e: CellContextMenuEvent) => {
if (!e.event) return;
const clickEvent = e.event as MouseEvent;
clickEvent.preventDefault();
const selectedNodes = e.api.getSelectedNodes();
const selectedIds = selectedNodes.map((node) => node.data.id);
let selectedRows = sortBy(selectedNodes, ['rowIndex']).map((node) => node.data);
if (!selectedIds.includes(e.data.id)) {
e.api.deselectAll();
e.node.setSelected(true);
selectedRows = [e.data];
}
openContextMenu({
data: selectedRows,
menuItems: SONG_CONTEXT_MENU_ITEMS,
type: LibraryItem.SONG,
xPos: clickEvent.clientX,
yPos: clickEvent.clientY,
});
};
const playlistSongData = useMemo(
() => playlistSongsQueryInfinite.data?.pages.flatMap((p) => p.items),
[playlistSongsQueryInfinite.data?.pages],
);
const { intersectRef, tableContainerRef } = useFixedTableHeader();
const deletePlaylistMutation = useDeletePlaylist();
const handleDeletePlaylist = () => {
deletePlaylistMutation.mutate(
{ query: { id: playlistId } },
{
onError: (err) => {
toast.error({
message: err.message,
title: 'Error deleting playlist',
});
},
onSuccess: () => {
toast.success({
message: `${detailQuery?.data?.name} was successfully deleted`,
title: 'Playlist deleted',
});
closeAllModals();
navigate(AppRoute.PLAYLISTS);
},
},
);
};
const openDeletePlaylist = () => {
openModal({
children: (
<ConfirmModal
loading={deletePlaylistMutation.isLoading}
onConfirm={handleDeletePlaylist}
>
Are you sure you want to delete this playlist?
</ConfirmModal>
),
title: 'Delete playlist',
});
};
const handlePlay = (playType?: Play) => {
handlePlayQueueAdd?.({
byItemType: {
id: [playlistId],
type: LibraryItem.PLAYLIST,
},
play: playType || playButtonBehavior,
});
};
const openUpdatePlaylistModal = async () => {
const query: UserListQuery = {
sortBy: UserListSort.NAME,
sortOrder: SortOrder.ASC,
startIndex: 0,
};
const users = await queryClient.fetchQuery({
queryFn: ({ signal }) => api.controller.getUserList({ query, server, signal }),
queryKey: queryKeys.users.list(server?.id || '', query),
});
const normalizedUsers = api.normalize.userList(users, server);
openModal({
children: (
<UpdatePlaylistForm
body={{
comment: detailQuery?.data?.description || undefined,
genres: detailQuery?.data?.genres,
name: detailQuery?.data?.name,
ndParams: {
owner: detailQuery?.data?.owner || undefined,
ownerId: detailQuery?.data?.ownerId || undefined,
public: detailQuery?.data?.public || false,
rules: detailQuery?.data?.rules || undefined,
sync: detailQuery?.data?.sync || undefined,
},
}}
query={{ id: playlistId }}
users={normalizedUsers.items}
onCancel={closeAllModals}
/>
),
title: 'Edit playlist',
});
};
2023-01-06 13:50:40 -08:00
const handleRowDoubleClick = (e: RowDoubleClickedEvent<QueueSong>) => {
if (!e.data) return;
handlePlayQueueAdd?.({
byData: [e.data],
play: playButtonBehavior,
});
};
const loadMoreRef = useRef<HTMLButtonElement | null>(null);
2023-01-02 02:04:23 -08:00
return (
<ContentContainer>
<Group
ref={intersectRef}
maw="1920px"
p="1rem"
position="apart"
>
<Group>
<PlayButton onClick={() => handlePlay()} />
<DropdownMenu position="bottom-start">
<DropdownMenu.Target>
<Button
compact
variant="subtle"
>
<RiMoreFill size={20} />
</Button>
</DropdownMenu.Target>
<DropdownMenu.Dropdown>
{PLAY_TYPES.filter((type) => type.play !== playButtonBehavior).map((type) => (
<DropdownMenu.Item
key={`playtype-${type.play}`}
onClick={() => handlePlay(type.play)}
>
{type.label}
</DropdownMenu.Item>
))}
<DropdownMenu.Divider />
<DropdownMenu.Item onClick={openUpdatePlaylistModal}>Edit playlist</DropdownMenu.Item>
<DropdownMenu.Item onClick={openDeletePlaylist}>Delete playlist</DropdownMenu.Item>
</DropdownMenu.Dropdown>
</DropdownMenu>
<Button
compact
component={Link}
to={generatePath(AppRoute.PLAYLISTS_DETAIL_SONGS, { playlistId })}
variant="subtle"
>
View full playlist
</Button>
</Group>
</Group>
<Box ref={tableContainerRef}>
<VirtualTable
ref={tableRef}
animateRows
2023-01-06 13:50:40 -08:00
deselectOnClickOutside
detailRowAutoHeight
maintainColumnOrder
suppressCellFocus
suppressCopyRowsToClipboard
2023-01-06 13:50:40 -08:00
suppressHorizontalScroll
suppressLoadingOverlay
suppressMoveWhenRowDragging
suppressPaginationPanel
suppressRowDrag
suppressScrollOnNewData
columnDefs={columnDefs}
defaultColDef={defaultColumnDefs}
enableCellChangeFlash={false}
2023-01-06 13:50:40 -08:00
getRowId={(data) => {
// It's possible that there are duplicate song ids in a playlist
return `${data.data.id}-${data.data.pageIndex}`;
}}
rowData={playlistSongData}
rowHeight={60}
rowSelection="multiple"
onCellContextMenu={handleContextMenu}
onGridReady={(params) => {
params.api.setDomLayout('autoHeight');
params.api.sizeColumnsToFit();
}}
onGridSizeChanged={(params) => {
params.api.sizeColumnsToFit();
}}
2023-01-06 13:50:40 -08:00
onRowDoubleClicked={handleRowDoubleClick}
/>
</Box>
<MotionGroup
2023-01-02 02:04:23 -08:00
p="2rem"
position="center"
onViewportEnter={handleLoadMore}
2023-01-02 02:04:23 -08:00
>
<Button
ref={loadMoreRef}
2023-01-02 02:04:23 -08:00
compact
disabled={!playlistSongsQueryInfinite.hasNextPage}
loading={playlistSongsQueryInfinite.isFetchingNextPage}
variant="subtle"
onClick={handleLoadMore}
>
Load more
</Button>
</MotionGroup>
2023-01-02 02:04:23 -08:00
</ContentContainer>
);
};