reimplement smart playlists

This commit is contained in:
jeffvli
2025-11-28 21:27:27 -08:00
parent 9d3c44ef15
commit 06d0c715af
8 changed files with 381 additions and 193 deletions
@@ -8,6 +8,7 @@ import { AppRoute } from '/@/renderer/router/routes';
import { useCurrentServerId } from '/@/renderer/store';
import { ContextMenu } from '/@/shared/components/context-menu/context-menu';
import { ConfirmModal } from '/@/shared/components/modal/modal';
import { Text } from '/@/shared/components/text/text';
import { toast } from '/@/shared/components/toast/toast';
import { Playlist } from '/@/shared/types/domain-types';
@@ -21,31 +22,30 @@ export const DeletePlaylistAction = ({ items }: DeletePlaylistActionProps) => {
const serverId = useCurrentServerId();
const deletePlaylistMutation = useDeletePlaylist({});
const handleDeletePlaylist = useCallback(() => {
const handleDeletePlaylist = useCallback(async () => {
if (items.length === 0 || !serverId) return;
const playlist = items[0];
try {
await Promise.all(
items.map((playlist) =>
deletePlaylistMutation.mutateAsync({
apiClientProps: { serverId },
query: { id: playlist.id },
}),
),
);
navigate(AppRoute.PLAYLISTS, { replace: true });
toast.success({
message: t('action.deletePlaylist', { postProcess: 'sentenceCase' }),
});
} catch (err: any) {
toast.error({
message: err.message,
title: t('error.genericError', { postProcess: 'sentenceCase' }),
});
}
deletePlaylistMutation.mutate(
{
apiClientProps: { serverId },
query: { id: playlist.id },
},
{
onError: (err) => {
toast.error({
message: err.message,
title: t('error.genericError', { postProcess: 'sentenceCase' }),
});
},
onSuccess: () => {
navigate(AppRoute.PLAYLISTS, { replace: true });
toast.success({
message: t('action.deletePlaylist', { postProcess: 'sentenceCase' }),
});
},
},
);
closeAllModals();
}, [deletePlaylistMutation, items, navigate, serverId, t]);
@@ -55,7 +55,7 @@ export const DeletePlaylistAction = ({ items }: DeletePlaylistActionProps) => {
openModal({
children: (
<ConfirmModal onConfirm={handleDeletePlaylist}>
{t('common.areYouSure', { postProcess: 'sentenceCase' })}
<Text>{t('common.areYouSure', { postProcess: 'sentenceCase' })}</Text>
</ConfirmModal>
),
title: t('form.deletePlaylist.title', { postProcess: 'sentenceCase' }),
@@ -4,7 +4,6 @@ import { ListFilters } from '/@/renderer/features/shared/components/list-filters
import { ListRefreshButton } from '/@/renderer/features/shared/components/list-refresh-button';
import { ListSortByDropdown } from '/@/renderer/features/shared/components/list-sort-by-dropdown';
import { ListSortOrderToggleButton } from '/@/renderer/features/shared/components/list-sort-order-toggle-button';
import { useContainerQuery } from '/@/renderer/hooks';
import { Divider } from '/@/shared/components/divider/divider';
import { Flex } from '/@/shared/components/flex/flex';
import { Group } from '/@/shared/components/group/group';
@@ -12,11 +11,9 @@ import { GenreListSort, LibraryItem, SortOrder } from '/@/shared/types/domain-ty
import { ItemListKey } from '/@/shared/types/types';
export const GenreListHeaderFilters = () => {
const { ref, ...cq } = useContainerQuery();
return (
<Flex justify="space-between">
<Group gap="sm" ref={ref} w="100%">
<Group gap="sm" w="100%">
<ListSortByDropdown
defaultSortByValue={GenreListSort.NAME}
itemType={LibraryItem.GENRE}
@@ -43,18 +43,29 @@ export const CreatePlaylistForm = ({ onCancel }: CreatePlaylistFormProps) => {
},
});
const [isSmartPlaylist, setIsSmartPlaylist] = useState(false);
const [step, setStep] = useState<1 | 2>(1);
const handleSubmit = form.onSubmit((values) => {
if (isSmartPlaylist) {
values._custom!.navidrome = {
...values._custom?.navidrome,
rules: queryBuilderRef.current?.getFilters(),
};
if (!server) return;
// If creating a smart playlist and we're on the first step, advance to step 2
// to configure the query instead of submitting immediately.
if (isSmartPlaylist && step === 1) {
setStep(2);
return;
}
const smartPlaylist = queryBuilderRef.current?.getFilters();
if (!server) return;
const rules =
isSmartPlaylist && smartPlaylist?.filters
? {
...convertQueryGroupToNDQuery(smartPlaylist.filters),
limit: smartPlaylist.extraFilters.limit,
order: smartPlaylist.extraFilters.sortOrder,
sort: smartPlaylist.extraFilters.sortBy,
}
: undefined;
mutation.mutate(
{
@@ -64,16 +75,10 @@ export const CreatePlaylistForm = ({ onCancel }: CreatePlaylistFormProps) => {
_custom: {
navidrome: {
...values._custom?.navidrome,
rules:
isSmartPlaylist && smartPlaylist?.filters
? {
...convertQueryGroupToNDQuery(smartPlaylist.filters),
limit: smartPlaylist.extraFilters.limit,
order: smartPlaylist.extraFilters.sortOrder,
sort: smartPlaylist.extraFilters.sortBy,
}
: undefined,
rules,
},
// Top-level rules field is what Navidrome expects for smart playlists.
...(rules ? { rules } : {}),
},
},
},
@@ -100,51 +105,63 @@ export const CreatePlaylistForm = ({ onCancel }: CreatePlaylistFormProps) => {
return (
<form onSubmit={handleSubmit}>
<Stack>
<TextInput
data-autofocus
label={t('form.createPlaylist.input', {
context: 'name',
postProcess: 'titleCase',
})}
required
{...form.getInputProps('name')}
/>
{server?.type === ServerType.NAVIDROME && (
<Textarea
autosize
label={t('form.createPlaylist.input', {
context: 'description',
postProcess: 'titleCase',
})}
minRows={5}
{...form.getInputProps('comment')}
/>
)}
<Group>
{isPublicDisplayed && (
<Switch
{step === 1 && (
<>
<TextInput
data-autofocus
label={t('form.createPlaylist.input', {
context: 'public',
context: 'name',
postProcess: 'titleCase',
})}
{...form.getInputProps('public', {
type: 'checkbox',
})}
required
{...form.getInputProps('name')}
/>
)}
{server?.type === ServerType.NAVIDROME &&
hasFeature(server, ServerFeature.PLAYLISTS_SMART) && (
<Switch
label="Is smart playlist?"
onChange={(e) => setIsSmartPlaylist(e.currentTarget.checked)}
{server?.type === ServerType.NAVIDROME && (
<Textarea
autosize
label={t('form.createPlaylist.input', {
context: 'description',
postProcess: 'titleCase',
})}
minRows={5}
{...form.getInputProps('comment')}
/>
)}
</Group>
{server?.type === ServerType.NAVIDROME && isSmartPlaylist && (
<Group>
{isPublicDisplayed && (
<Switch
label={t('form.createPlaylist.input', {
context: 'public',
postProcess: 'titleCase',
})}
{...form.getInputProps('public', {
type: 'checkbox',
})}
/>
)}
{server?.type === ServerType.NAVIDROME &&
hasFeature(server, ServerFeature.PLAYLISTS_SMART) && (
<Switch
checked={isSmartPlaylist}
label="Is smart playlist?"
onChange={(e) => {
const next = e.currentTarget.checked;
setIsSmartPlaylist(next);
if (!next) {
setStep(1);
}
}}
/>
)}
</Group>
</>
)}
{isSmartPlaylist && step === 2 && (
<Stack pt="1rem">
<Text>Query Editor</Text>
<PlaylistQueryBuilder
isSaving={false}
isSaving={mutation.isPending}
limit={undefined}
query={undefined}
ref={queryBuilderRef}
@@ -155,6 +172,11 @@ export const CreatePlaylistForm = ({ onCancel }: CreatePlaylistFormProps) => {
)}
<Group justify="flex-end">
{isSmartPlaylist && step === 2 && (
<ModalButton onClick={() => setStep(1)} px="2xl" uppercase variant="subtle">
Back
</ModalButton>
)}
<ModalButton onClick={onCancel} px="2xl" uppercase variant="subtle">
{t('common.cancel')}
</ModalButton>
@@ -164,7 +186,9 @@ export const CreatePlaylistForm = ({ onCancel }: CreatePlaylistFormProps) => {
type="submit"
variant="filled"
>
{t('common.create')}
{isSmartPlaylist && step === 1
? t('common.confirm', { postProcess: 'sentenceCase' })
: t('common.create')}
</ModalButton>
</Group>
</Stack>
@@ -1,80 +1,42 @@
import { closeAllModals, openModal } from '@mantine/modals';
import { useQuery } from '@tanstack/react-query';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate, useParams } from 'react-router';
import { useParams } from 'react-router';
import i18n from '/@/i18n/i18n';
import { PLAYLIST_SONG_TABLE_COLUMNS } from '/@/renderer/components/item-list/item-table-list/default-columns';
import { ContextMenuController } from '/@/renderer/features/context-menu/context-menu-controller';
import { playlistsQueries } from '/@/renderer/features/playlists/api/playlists-api';
import { useDeletePlaylist } from '/@/renderer/features/playlists/mutations/delete-playlist-mutation';
import { ListConfigMenu } from '/@/renderer/features/shared/components/list-config-menu';
import { ListRefreshButton } from '/@/renderer/features/shared/components/list-refresh-button';
import { ListSortByDropdown } from '/@/renderer/features/shared/components/list-sort-by-dropdown';
import { ListSortOrderToggleButton } from '/@/renderer/features/shared/components/list-sort-order-toggle-button';
import { useContainerQuery } from '/@/renderer/hooks';
import { AppRoute } from '/@/renderer/router/routes';
import { MoreButton } from '/@/renderer/features/shared/components/more-button';
import { useCurrentServerId } from '/@/renderer/store';
import { Divider } from '/@/shared/components/divider/divider';
import { Flex } from '/@/shared/components/flex/flex';
import { Group } from '/@/shared/components/group/group';
import { ConfirmModal } from '/@/shared/components/modal/modal';
import { Text } from '/@/shared/components/text/text';
import { toast } from '/@/shared/components/toast/toast';
import { LibraryItem, SongListSort, SortOrder } from '/@/shared/types/domain-types';
import { ItemListKey, Play } from '/@/shared/types/types';
import { ItemListKey } from '/@/shared/types/types';
export const PlaylistDetailSongListHeaderFilters = () => {
const { t } = useTranslation();
const { playlistId } = useParams() as { playlistId: string };
const serverId = useCurrentServerId();
const navigate = useNavigate();
const detailQuery = useQuery(playlistsQueries.detail({ query: { id: playlistId }, serverId }));
const isSmartPlaylist = detailQuery.data?.rules;
const { ref, ...cq } = useContainerQuery();
const deletePlaylistMutation = useDeletePlaylist({});
const handleDeletePlaylist = useCallback(() => {
const handleMore = (event: React.MouseEvent<HTMLButtonElement>) => {
if (!detailQuery.data) return;
deletePlaylistMutation?.mutate(
{
apiClientProps: { serverId: detailQuery.data._serverId },
query: { id: detailQuery.data.id },
},
{
onError: (err) => {
toast.error({
message: err.message,
title: t('error.genericError', { postProcess: 'sentenceCase' }),
});
},
onSuccess: () => {
navigate(AppRoute.PLAYLISTS, { replace: true });
},
},
);
closeAllModals();
}, [deletePlaylistMutation, detailQuery.data, navigate, t]);
const openDeletePlaylistModal = () => {
openModal({
children: (
<ConfirmModal onConfirm={handleDeletePlaylist}>
<Text>Are you sure you want to delete this playlist?</Text>
</ConfirmModal>
),
title: t('form.deletePlaylist.title', { postProcess: 'sentenceCase' }),
ContextMenuController.call({
cmd: {
items: [detailQuery.data],
type: LibraryItem.PLAYLIST,
},
event,
});
};
return (
<Flex justify="space-between">
<Group gap="sm" ref={ref} w="100%">
<Group gap="sm" w="100%">
<ListSortByDropdown
defaultSortByValue={SongListSort.ID}
itemType={LibraryItem.PLAYLIST_SONG}
@@ -86,6 +48,7 @@ export const PlaylistDetailSongListHeaderFilters = () => {
listKey={ItemListKey.PLAYLIST_SONG}
/>
<ListRefreshButton listKey={ItemListKey.PLAYLIST_SONG} />
<MoreButton onClick={handleMore} />
</Group>
<Group gap="sm" wrap="nowrap">
<ListConfigMenu
@@ -11,12 +11,19 @@ import { LibraryHeaderBar } from '/@/renderer/features/shared/components/library
import { ListSearchInput } from '/@/renderer/features/shared/components/list-search-input';
import { useCurrentServer } from '/@/renderer/store';
import { formatDurationString } from '/@/renderer/utils';
import { Badge } from '/@/shared/components/badge/badge';
import { SpinnerIcon } from '/@/shared/components/spinner/spinner';
import { Stack } from '/@/shared/components/stack/stack';
import { LibraryItem } from '/@/shared/types/domain-types';
export const PlaylistDetailSongListHeader = () => {
interface PlaylistDetailSongListHeaderProps {
isSmartPlaylist?: boolean;
onConvertToSmart?: () => void;
onDelete?: () => void;
onToggleQueryBuilder?: () => void;
}
export const PlaylistDetailSongListHeader = ({
isSmartPlaylist: isSmartPlaylistProp,
}: PlaylistDetailSongListHeaderProps) => {
const { t } = useTranslation();
const { playlistId } = useParams() as { playlistId: string };
const { itemCount } = useListContext();
@@ -26,7 +33,7 @@ export const PlaylistDetailSongListHeader = () => {
);
if (detailQuery.isLoading) return null;
const isSmartPlaylist = detailQuery?.data?.rules;
const isSmartPlaylist = isSmartPlaylistProp ?? detailQuery?.data?.rules;
const playlistDuration = detailQuery?.data?.duration;
return (
@@ -38,15 +45,17 @@ export const PlaylistDetailSongListHeader = () => {
itemType={LibraryItem.PLAYLIST}
/>
<LibraryHeaderBar.Title>{detailQuery?.data?.name}</LibraryHeaderBar.Title>
{!!playlistDuration && <Badge>{formatDurationString(playlistDuration)}</Badge>}
<Badge>
{itemCount === null || itemCount === undefined ? (
<SpinnerIcon />
) : (
itemCount
)}
</Badge>
{isSmartPlaylist && <Badge size="lg">{t('entity.smartPlaylist')}</Badge>}
{isSmartPlaylist && (
<LibraryHeaderBar.Badge>{t('entity.smartPlaylist')}</LibraryHeaderBar.Badge>
)}
{!!playlistDuration && (
<LibraryHeaderBar.Badge>
{formatDurationString(playlistDuration)}
</LibraryHeaderBar.Badge>
)}
<LibraryHeaderBar.Badge isLoading={!itemCount}>
{itemCount}
</LibraryHeaderBar.Badge>
</LibraryHeaderBar>
<ListSearchInput />
</PageHeader>
@@ -3,6 +3,11 @@ import { AxiosError } from 'axios';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import {
applyDeletePlaylistOptimisticUpdates,
PreviousQueryData,
restorePlaylistQueryData,
} from '/@/renderer/features/playlists/mutations/playlist-optimistic-updates';
import { MutationHookArgs } from '/@/renderer/lib/react-query';
import { DeletePlaylistArgs, DeletePlaylistResponse } from '/@/shared/types/domain-types';
@@ -10,25 +15,32 @@ export const useDeletePlaylist = (args: MutationHookArgs) => {
const { options } = args || {};
const queryClient = useQueryClient();
return useMutation<DeletePlaylistResponse, AxiosError, DeletePlaylistArgs, null>({
mutationFn: (args) => {
return api.controller.deletePlaylist({
...args,
apiClientProps: { serverId: args.apiClientProps.serverId },
});
return useMutation<DeletePlaylistResponse, AxiosError, DeletePlaylistArgs, PreviousQueryData[]>(
{
mutationFn: (args) => {
return api.controller.deletePlaylist({
...args,
apiClientProps: { serverId: args.apiClientProps.serverId },
});
},
onError: (_error, _variables, context) => {
if (context) {
restorePlaylistQueryData(queryClient, context);
}
},
onMutate: (variables) => {
queryClient.cancelQueries({
queryKey: queryKeys.playlists.list(variables.apiClientProps.serverId),
});
return applyDeletePlaylistOptimisticUpdates(queryClient, variables);
},
onSuccess: (_data, variables) => {
queryClient.invalidateQueries({
exact: false,
queryKey: queryKeys.playlists.list(variables.apiClientProps.serverId),
});
},
...options,
},
onMutate: (variables) => {
queryClient.cancelQueries({
queryKey: queryKeys.playlists.list(variables.apiClientProps.serverId),
});
return null;
},
onSuccess: (_data, variables) => {
queryClient.invalidateQueries({
exact: false,
queryKey: queryKeys.playlists.list(variables.apiClientProps.serverId),
});
},
...options,
});
);
};
@@ -0,0 +1,149 @@
import { QueryClient } from '@tanstack/react-query';
import { queryKeys } from '/@/renderer/api/query-keys';
import { infiniteLoaderDataQueryKey } from '/@/renderer/components/item-list/helpers/item-list-infinite-loader';
import {
DeletePlaylistArgs,
LibraryItem,
Playlist,
PlaylistListResponse,
} from '/@/shared/types/domain-types';
export interface PreviousQueryData {
data: unknown;
queryKey: readonly unknown[];
}
export const applyDeletePlaylistOptimisticUpdates = (
queryClient: QueryClient,
variables: DeletePlaylistArgs,
): PreviousQueryData[] => {
const previousQueries: PreviousQueryData[] = [];
const playlistId = variables.query.id;
// Update detail query - remove it
const detailQueryKey = queryKeys.playlists.detail(
variables.apiClientProps.serverId,
playlistId,
);
const detailQueries = queryClient.getQueriesData({
exact: false,
queryKey: detailQueryKey,
});
if (detailQueries.length) {
detailQueries.forEach(([queryKey, data]) => {
if (data) {
previousQueries.push({ data, queryKey });
queryClient.setQueryData(queryKey, undefined);
}
});
}
// Update list queries - remove the playlist from items
const listQueryKey = queryKeys.playlists.list(variables.apiClientProps.serverId);
const listQueries = queryClient.getQueriesData({
exact: false,
queryKey: listQueryKey,
});
if (listQueries.length) {
listQueries.forEach(([queryKey, data]) => {
if (data) {
previousQueries.push({ data, queryKey });
queryClient.setQueryData(queryKey, (prev: PlaylistListResponse | undefined) => {
if (prev) {
return {
...prev,
items: prev.items.filter((item: Playlist) => item.id !== playlistId),
totalRecordCount: Math.max(
0,
(prev.totalRecordCount || prev.items.length) - 1,
),
};
}
return prev;
});
}
});
}
// Update infinite loader queries - remove the playlist from data array
const infiniteLoaderQueryKey = infiniteLoaderDataQueryKey(
variables.apiClientProps.serverId,
LibraryItem.PLAYLIST,
);
const infiniteLoaderQueries = queryClient.getQueriesData({
exact: false,
queryKey: infiniteLoaderQueryKey,
});
if (infiniteLoaderQueries.length) {
infiniteLoaderQueries.forEach(([queryKey, data]) => {
if (data) {
previousQueries.push({ data, queryKey });
queryClient.setQueryData(
queryKey,
(
prev:
| undefined
| {
data: unknown[];
pagesLoaded: Record<string, boolean>;
},
) => {
if (prev && prev.data) {
return {
...prev,
data: prev.data.filter((item: any) => {
if (!item || !item.id) {
return true;
}
return item.id !== playlistId;
}),
};
}
return prev;
},
);
}
});
}
// Update songList query - remove it
const songListQueryKey = queryKeys.playlists.songList(
variables.apiClientProps.serverId,
playlistId,
);
const songListQueries = queryClient.getQueriesData({
exact: false,
queryKey: songListQueryKey,
});
if (songListQueries.length) {
songListQueries.forEach(([queryKey, data]) => {
if (data) {
previousQueries.push({ data, queryKey });
queryClient.setQueryData(queryKey, undefined);
}
});
}
return previousQueries;
};
export const restorePlaylistQueryData = (
queryClient: QueryClient,
previousQueries: PreviousQueryData[],
): void => {
previousQueries.forEach(({ data, queryKey }) => {
queryClient.setQueryData(queryKey, data);
});
};
@@ -1,11 +1,10 @@
import { closeAllModals, openModal } from '@mantine/modals';
import { useQuery } from '@tanstack/react-query';
import { motion } from 'motion/react';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { generatePath, useNavigate, useParams } from 'react-router';
import { ItemListHandle } from '/@/renderer/components/item-list/types';
import { ListContext } from '/@/renderer/context/list-context';
import { playlistsQueries } from '/@/renderer/features/playlists/api/playlists-api';
import { PlaylistDetailSongListContent } from '/@/renderer/features/playlists/components/playlist-detail-song-list-content';
@@ -22,15 +21,15 @@ import { useCurrentServer } from '/@/renderer/store';
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
import { Box } from '/@/shared/components/box/box';
import { Group } from '/@/shared/components/group/group';
import { ConfirmModal } from '/@/shared/components/modal/modal';
import { Text } from '/@/shared/components/text/text';
import { toast } from '/@/shared/components/toast/toast';
import { ServerType, SongListSort } from '/@/shared/types/domain-types';
import { ItemListKey, Play } from '/@/shared/types/types';
import { ItemListKey } from '/@/shared/types/types';
const PlaylistDetailSongListRoute = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const tableRef = useRef<ItemListHandle | null>(null);
const { playlistId } = useParams() as { playlistId: string };
const server = useCurrentServer();
@@ -44,6 +43,8 @@ const PlaylistDetailSongListRoute = () => {
filter: Record<string, any>,
extraFilters: { limit?: number; sortBy?: string; sortOrder?: string },
) => {
if (!detailQuery?.data) return;
const rules = {
...filter,
limit: extraFilters.limit || undefined,
@@ -51,19 +52,15 @@ const PlaylistDetailSongListRoute = () => {
sort: extraFilters.sortBy || 'dateAdded',
};
if (!detailQuery?.data) return;
createPlaylistMutation.mutate(
{
apiClientProps: { serverId: detailQuery?.data?._serverId },
body: {
_custom: {
navidrome: {
owner: detailQuery?.data?.owner || '',
ownerId: detailQuery?.data?.ownerId || '',
rules,
sync: detailQuery?.data?.sync || false,
},
owner: detailQuery?.data?.owner || '',
ownerId: detailQuery?.data?.ownerId || '',
rules,
sync: detailQuery?.data?.sync || false,
},
comment: detailQuery?.data?.description || '',
name: detailQuery?.data?.name,
@@ -94,6 +91,15 @@ const PlaylistDetailSongListRoute = () => {
filter: Record<string, any>,
extraFilters: { limit?: number; sortBy?: string; sortOrder?: string },
) => {
if (!detailQuery?.data) return;
const rules = {
...filter,
limit: extraFilters.limit || undefined,
order: extraFilters.sortOrder || 'desc',
sort: extraFilters.sortBy || 'dateAdded',
};
openModal({
children: (
<SaveAsPlaylistForm
@@ -102,14 +108,11 @@ const PlaylistDetailSongListRoute = () => {
navidrome: {
owner: detailQuery?.data?.owner || '',
ownerId: detailQuery?.data?.ownerId || '',
rules: {
...filter,
limit: extraFilters.limit || undefined,
order: extraFilters.sortOrder || 'desc',
sort: extraFilters.sortBy || 'dateAdded',
},
rules,
sync: detailQuery?.data?.sync || false,
},
rules,
sync: detailQuery?.data?.sync || false,
},
comment: detailQuery?.data?.description || '',
name: detailQuery?.data?.name,
@@ -130,6 +133,41 @@ const PlaylistDetailSongListRoute = () => {
});
};
const openDeletePlaylistModal = () => {
openModal({
children: (
<ConfirmModal
onConfirm={() => {
if (!detailQuery?.data) return;
deletePlaylistMutation?.mutate(
{
apiClientProps: { serverId: detailQuery.data._serverId },
query: { id: detailQuery.data.id },
},
{
onError: (err) => {
toast.error({
message: err.message,
title: t('error.genericError', {
postProcess: 'sentenceCase',
}),
});
},
onSuccess: () => {
navigate(AppRoute.PLAYLISTS, { replace: true });
},
},
);
closeAllModals();
}}
>
<Text>Are you sure you want to delete this playlist?</Text>
</ConfirmModal>
),
title: t('form.deletePlaylist.title', { postProcess: 'sentenceCase' }),
});
};
const isSmartPlaylist =
!detailQuery?.isLoading &&
detailQuery?.data?.rules &&
@@ -158,13 +196,6 @@ const PlaylistDetailSongListRoute = () => {
const [itemCount, setItemCount] = useState<number | undefined>(undefined);
const handlePlay = (_play: Play) => {
// handlePlayQueueAdd?.({
// byData: filterSortedSongs,
// playType: play,
// });
};
const providerValue = useMemo(() => {
return {
customFilters: undefined,
@@ -175,7 +206,6 @@ const PlaylistDetailSongListRoute = () => {
};
}, [playlistId, itemCount]);
// Update item count when playlist songs are loaded
useEffect(() => {
if (
playlistSongs.data?.totalRecordCount !== undefined &&
@@ -190,12 +220,16 @@ const PlaylistDetailSongListRoute = () => {
<ListContext.Provider value={providerValue}>
<LibraryContainer>
<PlaylistDetailSongListHeader
handlePlay={handlePlay}
handleToggleShowQueryBuilder={handleToggleShowQueryBuilder}
itemCount={itemCount}
tableRef={tableRef}
isSmartPlaylist={!!isSmartPlaylist}
onConvertToSmart={() => {
if (!isSmartPlaylist) {
setShowQueryBuilder(true);
setIsQueryBuilderExpanded(true);
}
}}
onDelete={() => openDeletePlaylistModal()}
onToggleQueryBuilder={handleToggleShowQueryBuilder}
/>
{(isSmartPlaylist || showQueryBuilder) && (
<motion.div>
<Box h="100%" mah="35vh" p="md" w="100%">