diff --git a/src/renderer/features/context-menu/actions/add-to-playlist-action.tsx b/src/renderer/features/context-menu/actions/add-to-playlist-action.tsx
index a8be21ae5..6789c3e00 100644
--- a/src/renderer/features/context-menu/actions/add-to-playlist-action.tsx
+++ b/src/renderer/features/context-menu/actions/add-to-playlist-action.tsx
@@ -4,6 +4,8 @@ import Fuse from 'fuse.js';
import { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
+import { api } from '/@/renderer/api';
+import { queryKeys } from '/@/renderer/api/query-keys';
import {
getAlbumArtistSongsById,
getAlbumSongsById,
@@ -15,11 +17,14 @@ import { playlistsQueries } from '/@/renderer/features/playlists/api/playlists-a
import { useRecentPlaylists } from '/@/renderer/features/playlists/hooks/use-recent-playlists';
import { useAddToPlaylist } from '/@/renderer/features/playlists/mutations/add-to-playlist-mutation';
import { useCurrentServer, useCurrentServerId } from '/@/renderer/store';
+import { Checkbox } from '/@/shared/components/checkbox/checkbox';
import { ContextMenu } from '/@/shared/components/context-menu/context-menu';
import { Icon } from '/@/shared/components/icon/icon';
import { Spinner } from '/@/shared/components/spinner/spinner';
import { TextInput } from '/@/shared/components/text-input/text-input';
import { toast } from '/@/shared/components/toast/toast';
+import { Tooltip } from '/@/shared/components/tooltip/tooltip';
+import { useLocalStorage } from '/@/shared/hooks/use-local-storage';
import { LibraryItem, PlaylistListSort, SortOrder } from '/@/shared/types/domain-types';
interface AddToPlaylistActionProps {
@@ -33,6 +38,10 @@ export const AddToPlaylistAction = ({ items, itemType }: AddToPlaylistActionProp
const serverId = useCurrentServerId();
const queryClient = useQueryClient();
const [searchTerm, setSearchTerm] = useState('');
+ const [skipDuplicates, setSkipDuplicates] = useLocalStorage({
+ defaultValue: true,
+ key: 'playlist-skip-duplicate',
+ });
const addToPlaylistMutation = useAddToPlaylist({});
const playlistsQuery = useQuery(
@@ -193,8 +202,47 @@ export const AddToPlaylistAction = ({ items, itemType }: AddToPlaylistActionProp
}
if (allSongIds.length === 0) {
- toast.error({
- message: t('error.noItemsSelected', { postProcess: 'sentenceCase' }),
+ toast.warn({
+ message: t('common.noResultsFromQuery', { postProcess: 'sentenceCase' }),
+ });
+ return;
+ }
+
+ let songsToAdd: string[] = allSongIds;
+
+ if (skipDuplicates) {
+ const queryKey = queryKeys.playlists.songList(serverId, playlistId);
+
+ const playlistSongsRes = await queryClient.fetchQuery({
+ queryFn: ({ signal }) => {
+ return api.controller.getPlaylistSongList({
+ apiClientProps: {
+ serverId,
+ signal,
+ },
+ query: {
+ id: playlistId,
+ },
+ });
+ },
+ queryKey,
+ });
+
+ const playlistSongIds = playlistSongsRes?.items?.map((song) => song.id);
+ const uniqueSongIds: string[] = [];
+
+ for (const songId of allSongIds) {
+ if (!playlistSongIds?.includes(songId)) {
+ uniqueSongIds.push(songId);
+ }
+ }
+
+ songsToAdd = uniqueSongIds;
+ }
+
+ if (songsToAdd.length === 0) {
+ toast.warn({
+ message: t('common.noResultsFromQuery', { postProcess: 'sentenceCase' }),
});
return;
}
@@ -203,7 +251,7 @@ export const AddToPlaylistAction = ({ items, itemType }: AddToPlaylistActionProp
{
apiClientProps: { serverId },
body: {
- songId: allSongIds,
+ songId: songsToAdd,
},
query: {
id: playlistId,
@@ -222,7 +270,7 @@ export const AddToPlaylistAction = ({ items, itemType }: AddToPlaylistActionProp
toast.success({
message: t('form.addToPlaylist.success', {
- message: allSongIds.length,
+ message: songsToAdd.length,
numOfPlaylists: 1,
postProcess: 'sentenceCase',
}),
@@ -243,7 +291,9 @@ export const AddToPlaylistAction = ({ items, itemType }: AddToPlaylistActionProp
getSongsByPlaylist,
itemType,
items,
+ queryClient,
serverId,
+ skipDuplicates,
t,
],
);
@@ -307,6 +357,24 @@ export const AddToPlaylistAction = ({ items, itemType }: AddToPlaylistActionProp
onPointerDown={(e) => e.stopPropagation()}
pb="xs"
placeholder={t('common.search', { postProcess: 'sentenceCase' })}
+ rightSection={
+
+ {
+ setSkipDuplicates(e.target.checked);
+ e.stopPropagation();
+ }}
+ onClick={(e) => e.stopPropagation()}
+ size="sm"
+ />
+
+ }
size="sm"
value={searchTerm}
/>
diff --git a/src/renderer/features/playlists/components/add-to-playlist-context-modal.tsx b/src/renderer/features/playlists/components/add-to-playlist-context-modal.tsx
index e51476dfc..a8f6fd986 100644
--- a/src/renderer/features/playlists/components/add-to-playlist-context-modal.tsx
+++ b/src/renderer/features/playlists/components/add-to-playlist-context-modal.tsx
@@ -37,6 +37,7 @@ import { TextInput } from '/@/shared/components/text-input/text-input';
import { Text } from '/@/shared/components/text/text';
import { toast } from '/@/shared/components/toast/toast';
import { useForm } from '/@/shared/hooks/use-form';
+import { useLocalStorage } from '/@/shared/hooks/use-local-storage';
import { Playlist, PlaylistListSort, SortOrder } from '/@/shared/types/domain-types';
export const AddToPlaylistContextModal = ({
@@ -61,14 +62,23 @@ export const AddToPlaylistContextModal = ({
const rowRefs = useRef<(HTMLTableRowElement | null)[]>([]);
const formRef = useRef(null);
+ const [skipDuplicates, setSkipDuplicates] = useLocalStorage({
+ defaultValue: true,
+ key: 'playlist-skip-duplicate',
+ });
+
const form = useForm({
initialValues: {
newPlaylists: [] as string[],
selectedPlaylistIds: initialSelectedIds || [],
- skipDuplicates: true,
+ skipDuplicates: skipDuplicates,
},
});
+ form.watch('skipDuplicates', (event) => {
+ setSkipDuplicates(event.value);
+ });
+
const addToPlaylistMutation = useAddToPlaylist({});
const playlistList = useQuery(