diff --git a/src/renderer/features/playlists/components/update-playlist-form.tsx b/src/renderer/features/playlists/components/update-playlist-form.tsx index dd5730505..e412366f4 100644 --- a/src/renderer/features/playlists/components/update-playlist-form.tsx +++ b/src/renderer/features/playlists/components/update-playlist-form.tsx @@ -1,21 +1,31 @@ import { closeModal, ContextModalProps } from '@mantine/modals'; import { useQuery } from '@tanstack/react-query'; import { t } from 'i18next'; +import { type ReactNode, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { ItemImage } from '/@/renderer/components/item-image/item-image'; +import { useDeletePlaylistImage } from '/@/renderer/features/playlists/mutations/delete-playlist-image-mutation'; import { useUpdatePlaylist } from '/@/renderer/features/playlists/mutations/update-playlist-mutation'; +import { useUploadPlaylistImage } from '/@/renderer/features/playlists/mutations/upload-playlist-image-mutation'; import { sharedQueries } from '/@/renderer/features/shared/api/shared-api'; import { useCurrentServer, useCurrentServerId, usePermissions } from '/@/renderer/store'; import { hasFeature } from '/@/shared/api/utils'; +import { ActionIcon } from '/@/shared/components/action-icon/action-icon'; +import { Box } from '/@/shared/components/box/box'; +import { FileButton } from '/@/shared/components/file-button/file-button'; +import { Flex } from '/@/shared/components/flex/flex'; import { Group } from '/@/shared/components/group/group'; import { ModalButton } from '/@/shared/components/modal/model-shared'; import { Select } from '/@/shared/components/select/select'; import { Stack } from '/@/shared/components/stack/stack'; import { Switch } from '/@/shared/components/switch/switch'; import { TextInput } from '/@/shared/components/text-input/text-input'; +import { Textarea } from '/@/shared/components/textarea/textarea'; import { toast } from '/@/shared/components/toast/toast'; import { useForm } from '/@/shared/hooks/use-form'; import { + LibraryItem, ServerType, SortOrder, UpdatePlaylistBody, @@ -24,17 +34,41 @@ import { } from '/@/shared/types/domain-types'; import { ServerFeature } from '/@/shared/types/features-types'; +type PlaylistImageProps = { + imageId: null | string; + imageUrl: null | string; + uploadedImage?: string; +}; + export const UpdatePlaylistContextModal = ({ id, innerProps, }: ContextModalProps<{ body: Partial; + playlistImage?: PlaylistImageProps; query: UpdatePlaylistQuery; }>) => { const { t } = useTranslation(); - const mutation = useUpdatePlaylist({}); + const updateMutation = useUpdatePlaylist({}); + const uploadImageMutation = useUploadPlaylistImage({}); + const deleteImageMutation = useDeletePlaylistImage({}); const server = useCurrentServer(); - const { body, query } = innerProps; + const { body, playlistImage, query } = innerProps; + + const [pendingFile, setPendingFile] = useState(null); + const [pendingPreviewUrl, setPendingPreviewUrl] = useState(null); + const [removeCustomCover, setRemoveCustomCover] = useState(false); + const [isSaving, setIsSaving] = useState(false); + + useEffect(() => { + if (!pendingFile) { + setPendingPreviewUrl(null); + return; + } + const url = URL.createObjectURL(pendingFile); + setPendingPreviewUrl(url); + return () => URL.revokeObjectURL(url); + }, [pendingFile]); const form = useForm({ initialValues: { @@ -47,91 +81,259 @@ export const UpdatePlaylistContextModal = ({ }, }); - const handleSubmit = form.onSubmit((values) => { - mutation.mutate( - { - apiClientProps: { serverId: server?.id || '' }, + const handleSubmit = form.onSubmit(async (values) => { + if (!server?.id) return; + + setIsSaving(true); + try { + await updateMutation.mutateAsync({ + apiClientProps: { serverId: server.id }, body: values, query, - }, - { - onError: (err) => { - toast.error({ - message: err.message, - title: t('error.genericError', { postProcess: 'sentenceCase' }), - }); - }, - onSuccess: () => { - toast.success({ - message: t('form.editPlaylist.success', { postProcess: 'sentenceCase' }), - }); - closeModal(id); - }, - }, - ); + }); + + if (pendingFile) { + const buffer = await pendingFile.arrayBuffer(); + await uploadImageMutation.mutateAsync({ + apiClientProps: { serverId: server.id }, + body: { image: new Uint8Array(buffer) }, + query: { id: query.id }, + }); + } else if (removeCustomCover && playlistImage?.uploadedImage) { + await deleteImageMutation.mutateAsync({ + apiClientProps: { serverId: server.id }, + query: { id: query.id }, + }); + } + + toast.success({ + message: t('form.editPlaylist.success', { postProcess: 'sentenceCase' }), + }); + closeModal(id); + } catch (err: any) { + toast.error({ + message: err?.message, + title: t('error.genericError', { postProcess: 'sentenceCase' }), + }); + } finally { + setIsSaving(false); + } }); const isPublicDisplayed = hasFeature(server, ServerFeature.PUBLIC_PLAYLIST); const isOwnerDisplayed = server?.type === ServerType.NAVIDROME; const isCommentDisplayed = server?.type === ServerType.NAVIDROME; - const isSubmitDisabled = !form.values.name || mutation.isPending; + const isCoverImageDisplayed = hasFeature(server, ServerFeature.PLAYLIST_IMAGE_UPLOAD); + const isSubmitDisabled = !form.values.name || isSaving; + const hadUploadedCover = !!playlistImage?.uploadedImage; + + const fieldNodes: ReactNode[] = [ + , + ]; + + if (isCommentDisplayed) { + fieldNodes.push( +