From d8b190c2b7cc15be76f880e8ab2aaf4edd9052e8 Mon Sep 17 00:00:00 2001 From: jeffvli Date: Sun, 7 Dec 2025 12:39:28 -0800 Subject: [PATCH] add skip to duplicates to playlist ctx menu - persist value --- .../actions/add-to-playlist-action.tsx | 76 ++++++++++++++++++- .../add-to-playlist-context-modal.tsx | 12 ++- 2 files changed, 83 insertions(+), 5 deletions(-) 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(