From f4a215cf6195ef1c57696a1f4df6fb5b358fd435 Mon Sep 17 00:00:00 2001 From: iiPython Date: Sat, 23 Mar 2024 04:08:38 -0500 Subject: [PATCH 1/6] add share item feature --- src/i18n/locales/en.json | 9 +- src/renderer/api/controller.ts | 16 ++++ src/renderer/api/navidrome/navidrome-api.ts | 9 ++ .../api/navidrome/navidrome-controller.ts | 24 +++++ src/renderer/api/navidrome/navidrome-types.ts | 13 +++ src/renderer/api/types.ts | 12 +++ src/renderer/app.tsx | 7 +- .../context-menu/context-menu-items.tsx | 6 +- .../context-menu/context-menu-provider.tsx | 24 +++++ src/renderer/features/context-menu/events.ts | 1 + .../components/share-item-context-modal.tsx | 88 +++++++++++++++++++ src/renderer/features/sharing/index.ts | 2 + .../sharing/mutations/share-item-mutation.ts | 30 +++++++ 13 files changed, 237 insertions(+), 4 deletions(-) create mode 100644 src/renderer/features/sharing/components/share-item-context-modal.tsx create mode 100644 src/renderer/features/sharing/index.ts create mode 100644 src/renderer/features/sharing/mutations/share-item-mutation.ts diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index b7965f778..24066c92e 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -98,6 +98,7 @@ "setting": "setting", "setting_one": "setting", "setting_other": "settings", + "share": "share", "size": "size", "sortOrder": "order", "title": "title", @@ -251,6 +252,11 @@ "input_optionMatchAll": "match all", "input_optionMatchAny": "match any" }, + "shareItem": { + "allowDownloading": "allow downloading", + "description": "description", + "success": "share link copied to clipboard" + }, "updateServer": { "success": "server updated successfully", "title": "update server" @@ -306,7 +312,8 @@ "removeFromFavorites": "$t(action.removeFromFavorites)", "removeFromPlaylist": "$t(action.removeFromPlaylist)", "removeFromQueue": "$t(action.removeFromQueue)", - "setRating": "$t(action.setRating)" + "setRating": "$t(action.setRating)", + "shareItem": "share item" }, "fullscreenPlayer": { "config": { diff --git a/src/renderer/api/controller.ts b/src/renderer/api/controller.ts index de76e7211..a34bb9a9f 100644 --- a/src/renderer/api/controller.ts +++ b/src/renderer/api/controller.ts @@ -8,6 +8,7 @@ import type { AlbumArtistDetailArgs, AlbumArtistListArgs, SetRatingArgs, + ShareItemArgs, GenreListArgs, CreatePlaylistArgs, DeletePlaylistArgs, @@ -55,6 +56,7 @@ import type { SimilarSongsArgs, Song, ServerType, + ShareItemResponse, } from '/@/renderer/api/types'; import { DeletePlaylistResponse, RandomSongListArgs } from './types'; import { ndController } from '/@/renderer/api/navidrome/navidrome-controller'; @@ -102,6 +104,7 @@ export type ControllerEndpoint = Partial<{ scrobble: (args: ScrobbleArgs) => Promise; search: (args: SearchArgs) => Promise; setRating: (args: SetRatingArgs) => Promise; + shareItem: (args: ShareItemArgs) => Promise; updatePlaylist: (args: UpdatePlaylistArgs) => Promise; }>; @@ -149,6 +152,7 @@ const endpoints: ApiController = { scrobble: jfController.scrobble, search: jfController.search, setRating: undefined, + shareItem: undefined, updatePlaylist: jfController.updatePlaylist, }, navidrome: { @@ -188,6 +192,7 @@ const endpoints: ApiController = { scrobble: ssController.scrobble, search: ssController.search3, setRating: ssController.setRating, + shareItem: ndController.shareItem, updatePlaylist: ndController.updatePlaylist, }, subsonic: { @@ -223,6 +228,7 @@ const endpoints: ApiController = { scrobble: ssController.scrobble, search: ssController.search3, setRating: undefined, + shareItem: undefined, updatePlaylist: undefined, }, }; @@ -457,6 +463,15 @@ const updateRating = async (args: SetRatingArgs) => { )?.(args); }; +const shareItem = async (args: ShareItemArgs) => { + return ( + apiController( + 'shareItem', + args.apiClientProps.server?.type, + ) as ControllerEndpoint['shareItem'] + )?.(args); +}; + const getTopSongList = async (args: TopSongListArgs) => { return ( apiController( @@ -555,6 +570,7 @@ export const controller = { removeFromPlaylist, scrobble, search, + shareItem, updatePlaylist, updateRating, }; diff --git a/src/renderer/api/navidrome/navidrome-api.ts b/src/renderer/api/navidrome/navidrome-api.ts index b9eb93f9f..4fd053482 100644 --- a/src/renderer/api/navidrome/navidrome-api.ts +++ b/src/renderer/api/navidrome/navidrome-api.ts @@ -157,6 +157,15 @@ export const contract = c.router({ 500: resultWithHeaders(ndType._response.error), }, }, + shareItem: { + body: ndType._parameters.shareItem, + method: 'POST', + path: 'share', + responses: { + 200: resultWithHeaders(ndType._response.shareItem), + 500: resultWithHeaders(ndType._response.error), + }, + }, updatePlaylist: { body: ndType._parameters.updatePlaylist, method: 'PUT', diff --git a/src/renderer/api/navidrome/navidrome-controller.ts b/src/renderer/api/navidrome/navidrome-controller.ts index 33eb367ae..7d2cfef5c 100644 --- a/src/renderer/api/navidrome/navidrome-controller.ts +++ b/src/renderer/api/navidrome/navidrome-controller.ts @@ -47,6 +47,8 @@ import { genreListSortMap, ServerInfo, ServerInfoArgs, + ShareItemArgs, + ShareItemResponse, } from '../types'; import { hasFeature } from '/@/renderer/api/utils'; import { ServerFeature, ServerFeatures } from '/@/renderer/api/features-types'; @@ -547,6 +549,27 @@ const getServerInfo = async (args: ServerInfoArgs): Promise => { return { features, id: apiClientProps.server?.id, version: ping.body.serverVersion! }; }; +const shareItem = async (args: ShareItemArgs): Promise => { + const { body, apiClientProps } = args; + + const res = await ndApiClient(apiClientProps).shareItem({ + body: { + description: body.description, + downloadable: body.downloadable, + resourceIds: body.resourceIds, + resourceType: body.resourceType, + }, + }); + + if (res.status !== 200) { + throw new Error('Failed to share item'); + } + + return { + id: res.body.data.id, + }; +}; + export const ndController = { addToPlaylist, authenticate, @@ -565,5 +588,6 @@ export const ndController = { getSongList, getUserList, removeFromPlaylist, + shareItem, updatePlaylist, }; diff --git a/src/renderer/api/navidrome/navidrome-types.ts b/src/renderer/api/navidrome/navidrome-types.ts index 6165046a8..0887dc5c8 100644 --- a/src/renderer/api/navidrome/navidrome-types.ts +++ b/src/renderer/api/navidrome/navidrome-types.ts @@ -343,6 +343,17 @@ const removeFromPlaylistParameters = z.object({ id: z.array(z.string()), }); +const shareItem = z.object({ + id: z.string(), +}); + +const shareItemParameters = z.object({ + description: z.string(), + downloadable: z.boolean(), + resourceIds: z.string(), + resourceType: z.string(), +}); + export enum NavidromeExtensions { SMART_PLAYLISTS = 'smartPlaylists', } @@ -365,6 +376,7 @@ export const ndType = { genreList: genreListParameters, playlistList: playlistListParameters, removeFromPlaylist: removeFromPlaylistParameters, + shareItem: shareItemParameters, songList: songListParameters, updatePlaylist: updatePlaylistParameters, userList: userListParameters, @@ -386,6 +398,7 @@ export const ndType = { playlistSong, playlistSongList, removeFromPlaylist, + shareItem, song, songList, updatePlaylist, diff --git a/src/renderer/api/types.ts b/src/renderer/api/types.ts index d6d3d957b..4f91cd209 100644 --- a/src/renderer/api/types.ts +++ b/src/renderer/api/types.ts @@ -766,6 +766,18 @@ export type RatingQuery = { export type SetRatingArgs = { query: RatingQuery; serverId?: string } & BaseEndpointArgs; +// Sharing +export type ShareItemResponse = { id: string } | undefined; + +export type ShareItemBody = { + description: string; + downloadable: boolean; + resourceIds: string; + resourceType: string; +}; + +export type ShareItemArgs = { body: ShareItemBody; serverId?: string } & BaseEndpointArgs; + // Add to playlist export type AddToPlaylistResponse = null | undefined; diff --git a/src/renderer/app.tsx b/src/renderer/app.tsx index abdbd7906..67baf10f7 100644 --- a/src/renderer/app.tsx +++ b/src/renderer/app.tsx @@ -21,6 +21,7 @@ import { ContextMenuProvider } from '/@/renderer/features/context-menu'; import { useHandlePlayQueueAdd } from '/@/renderer/features/player/hooks/use-handle-playqueue-add'; import { PlayQueueHandlerContext } from '/@/renderer/features/player'; import { AddToPlaylistContextModal } from '/@/renderer/features/playlists'; +import { ShareItemContextModal } from '/@/renderer/features/sharing'; import { getMpvProperties } from '/@/renderer/features/settings/components/playback/mpv-settings'; import { PlayerState, usePlayerStore, useQueueControls } from '/@/renderer/store'; import { FontType, PlaybackType, PlayerStatus } from '/@/renderer/types'; @@ -259,7 +260,11 @@ export const App = () => { transition: 'fade', }, }} - modals={{ addToPlaylist: AddToPlaylistContextModal, base: BaseContextModal }} + modals={{ + addToPlaylist: AddToPlaylistContextModal, + base: BaseContextModal, + shareItem: ShareItemContextModal, + }} > diff --git a/src/renderer/features/context-menu/context-menu-items.tsx b/src/renderer/features/context-menu/context-menu-items.tsx index e7ee6c486..4a5ae6c12 100644 --- a/src/renderer/features/context-menu/context-menu-items.tsx +++ b/src/renderer/features/context-menu/context-menu-items.tsx @@ -18,7 +18,8 @@ export const SONG_CONTEXT_MENU_ITEMS: SetContextMenuItems = [ { divider: true, id: 'addToPlaylist' }, { id: 'addToFavorites' }, { divider: true, id: 'removeFromFavorites' }, - { children: true, disabled: false, id: 'setRating' }, + { children: true, disabled: false, divider: true, id: 'setRating' }, + { id: 'shareItem' }, ]; export const PLAYLIST_SONG_CONTEXT_MENU_ITEMS: SetContextMenuItems = [ @@ -49,7 +50,8 @@ export const ALBUM_CONTEXT_MENU_ITEMS: SetContextMenuItems = [ { divider: true, id: 'addToPlaylist' }, { id: 'addToFavorites' }, { id: 'removeFromFavorites' }, - { children: true, disabled: false, id: 'setRating' }, + { children: true, disabled: false, divider: true, id: 'setRating' }, + { id: 'shareItem' }, ]; export const GENRE_CONTEXT_MENU_ITEMS: SetContextMenuItems = [ diff --git a/src/renderer/features/context-menu/context-menu-provider.tsx b/src/renderer/features/context-menu/context-menu-provider.tsx index 34ebc325e..3548b532c 100644 --- a/src/renderer/features/context-menu/context-menu-provider.tsx +++ b/src/renderer/features/context-menu/context-menu-provider.tsx @@ -25,6 +25,7 @@ import { RiPlayListAddFill, RiStarFill, RiCloseCircleLine, + RiShareForwardFill, } from 'react-icons/ri'; import { AnyLibraryItems, LibraryItem, ServerType, AnyLibraryItem } from '/@/renderer/api/types'; import { @@ -600,6 +601,22 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => { } }, [ctx.dataNodes, moveToTopOfQueue, playbackType]); + const handleShareItem = useCallback(() => { + if (!ctx.dataNodes && !ctx.data) return; + + const uniqueIds = ctx.data.map((node) => node.id); + + openContextModal({ + innerProps: { + itemIds: uniqueIds, + resourceType: ctx.data[0].itemType, + }, + modal: 'shareItem', + size: 'md', + title: t('page.contextMenu.shareItem', { postProcess: 'sentenceCase' }), + }); + }, [ctx.data, ctx.dataNodes, t]); + const handleRemoveSelected = useCallback(() => { const uniqueIds = ctx.dataNodes?.map((row) => row.data.uniqueId); if (!uniqueIds?.length) return; @@ -775,6 +792,12 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => { onClick: () => {}, rightIcon: , }, + shareItem: { + id: 'shareItem', + label: t('page.contextMenu.shareItem', { postProcess: 'sentenceCase' }), + leftIcon: , + onClick: handleShareItem, + }, }; }, [ handleAddToFavorites, @@ -788,6 +811,7 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => { handleRemoveSelected, handleUpdateRating, openDeletePlaylistModal, + handleShareItem, t, ]); diff --git a/src/renderer/features/context-menu/events.ts b/src/renderer/features/context-menu/events.ts index dedcf8193..e3200cd9d 100644 --- a/src/renderer/features/context-menu/events.ts +++ b/src/renderer/features/context-menu/events.ts @@ -28,6 +28,7 @@ export type ContextMenuItemType = | 'addToFavorites' | 'removeFromFavorites' | 'setRating' + | 'shareItem' | 'deletePlaylist' | 'createPlaylist' | 'moveToBottomOfQueue' diff --git a/src/renderer/features/sharing/components/share-item-context-modal.tsx b/src/renderer/features/sharing/components/share-item-context-modal.tsx new file mode 100644 index 000000000..d77fb3a80 --- /dev/null +++ b/src/renderer/features/sharing/components/share-item-context-modal.tsx @@ -0,0 +1,88 @@ +import { Box, Group, Stack, TextInput } from '@mantine/core'; +import { useForm } from '@mantine/form'; +import { closeModal, ContextModalProps } from '@mantine/modals'; +import { Button, Switch, toast } from '/@/renderer/components'; +import { useCurrentServer } from '/@/renderer/store'; +import { useTranslation } from 'react-i18next'; +import { useShareItem } from '../mutations/share-item-mutation'; + +export const ShareItemContextModal = ({ + id, + innerProps, +}: ContextModalProps<{ + itemIds: string[]; + resourceType: string; +}>) => { + const { t } = useTranslation(); + const { itemIds, resourceType } = innerProps; + const server = useCurrentServer(); + + const shareItemMutation = useShareItem({}); + + const form = useForm({ + initialValues: { + allowDownloading: false, + description: '', + }, + }); + + const handleSubmit = form.onSubmit(async (values) => { + shareItemMutation.mutate({ + body: { + description: values.description, + downloadable: values.allowDownloading, + resourceIds: itemIds.join(), + resourceType, + }, + serverId: server?.id, + }); + + toast.success({ + message: t('form.shareItem.success', { + postProcess: 'sentenceCase', + }), + }); + closeModal(id); + return null; + }); + + return ( + +
+ + + + + + + + + + +
+
+ ); +}; diff --git a/src/renderer/features/sharing/index.ts b/src/renderer/features/sharing/index.ts new file mode 100644 index 000000000..74f988e20 --- /dev/null +++ b/src/renderer/features/sharing/index.ts @@ -0,0 +1,2 @@ +export * from './components/share-item-context-modal'; +export * from './mutations/share-item-mutation'; diff --git a/src/renderer/features/sharing/mutations/share-item-mutation.ts b/src/renderer/features/sharing/mutations/share-item-mutation.ts new file mode 100644 index 000000000..81637bbf0 --- /dev/null +++ b/src/renderer/features/sharing/mutations/share-item-mutation.ts @@ -0,0 +1,30 @@ +import { useMutation } from '@tanstack/react-query'; +import { AnyLibraryItems, ShareItemResponse, ShareItemArgs } from '/@/renderer/api/types'; +import { AxiosError } from 'axios'; +import { api } from '/@/renderer/api'; +import { MutationHookArgs } from '/@/renderer/lib/react-query'; +import { getServerById } from '/@/renderer/store'; + +export const useShareItem = (args: MutationHookArgs) => { + const { options } = args || {}; + + return useMutation< + ShareItemResponse, + AxiosError, + Omit, + { previous: { items: AnyLibraryItems } | undefined } + >({ + mutationFn: (args) => { + const server = getServerById(args.serverId); + if (!server) throw new Error('Server not found'); + return api.controller.shareItem({ ...args, apiClientProps: { server } }); + }, + onSuccess: (_data, variables) => { + if (!_data?.id) throw new Error('Failed to share item'); + const server = getServerById(variables.serverId); + if (!server) throw new Error('Server not found'); + navigator.clipboard.writeText(`${server.url}/share/${_data.id}`); + }, + ...options, + }); +}; From 22f320e6e4ad58bedd43d97ee98282d6bf949860 Mon Sep 17 00:00:00 2001 From: iiPython Date: Wed, 27 Mar 2024 23:02:53 -0500 Subject: [PATCH 2/6] take care of (mostly) everything --- src/i18n/locales/en.json | 5 +- src/renderer/api/features-types.ts | 1 + src/renderer/api/navidrome/navidrome-api.ts | 1 + .../api/navidrome/navidrome-controller.ts | 3 + src/renderer/api/navidrome/navidrome-types.ts | 2 + src/renderer/api/types.ts | 1 + .../context-menu/context-menu-provider.tsx | 7 ++ .../components/share-item-context-modal.tsx | 68 +++++++++++++++---- .../sharing/mutations/share-item-mutation.ts | 6 -- 9 files changed, 74 insertions(+), 20 deletions(-) diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 24066c92e..36c3daf01 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -255,7 +255,10 @@ "shareItem": { "allowDownloading": "allow downloading", "description": "description", - "success": "share link copied to clipboard" + "setExpiration": "set expiration", + "success": "share link copied to clipboard", + "expireInvalid": "expiration must be in the future", + "createFailed": "failed to create share (is sharing enabled?)" }, "updateServer": { "success": "server updated successfully", diff --git a/src/renderer/api/features-types.ts b/src/renderer/api/features-types.ts index 779c846eb..f1ccc3e76 100644 --- a/src/renderer/api/features-types.ts +++ b/src/renderer/api/features-types.ts @@ -4,6 +4,7 @@ export enum ServerFeature { LYRICS_MULTIPLE_STRUCTURED = 'lyricsMultipleStructured', LYRICS_SINGLE_STRUCTURED = 'lyricsSingleStructured', PLAYLISTS_SMART = 'playlistsSmart', + SHARING_ALBUM_SONG = 'sharingAlbumSong', } export type ServerFeatures = Partial>; diff --git a/src/renderer/api/navidrome/navidrome-api.ts b/src/renderer/api/navidrome/navidrome-api.ts index 4fd053482..535a1535d 100644 --- a/src/renderer/api/navidrome/navidrome-api.ts +++ b/src/renderer/api/navidrome/navidrome-api.ts @@ -163,6 +163,7 @@ export const contract = c.router({ path: 'share', responses: { 200: resultWithHeaders(ndType._response.shareItem), + 404: resultWithHeaders(ndType._response.error), 500: resultWithHeaders(ndType._response.error), }, }, diff --git a/src/renderer/api/navidrome/navidrome-controller.ts b/src/renderer/api/navidrome/navidrome-controller.ts index 7d2cfef5c..bd7a10425 100644 --- a/src/renderer/api/navidrome/navidrome-controller.ts +++ b/src/renderer/api/navidrome/navidrome-controller.ts @@ -486,6 +486,7 @@ const removeFromPlaylist = async ( const VERSION_INFO: Array<[string, Record]> = [ ['0.48.0', { [ServerFeature.PLAYLISTS_SMART]: [1] }], + ['0.49.3', { [ServerFeature.SHARING_ALBUM_SONG]: [1] }], ]; const getFeatures = (version: string): Record => { @@ -544,6 +545,7 @@ const getServerInfo = async (args: ServerInfoArgs): Promise => { const features: ServerFeatures = { lyricsMultipleStructured: !!navidromeFeatures[SubsonicExtensions.SONG_LYRICS], playlistsSmart: !!navidromeFeatures[NavidromeExtensions.SMART_PLAYLISTS], + sharingAlbumSong: !!navidromeFeatures[NavidromeExtensions.SHARING], }; return { features, id: apiClientProps.server?.id, version: ping.body.serverVersion! }; @@ -556,6 +558,7 @@ const shareItem = async (args: ShareItemArgs): Promise => { body: { description: body.description, downloadable: body.downloadable, + expires: body.expires, resourceIds: body.resourceIds, resourceType: body.resourceType, }, diff --git a/src/renderer/api/navidrome/navidrome-types.ts b/src/renderer/api/navidrome/navidrome-types.ts index 0887dc5c8..b9797e5d1 100644 --- a/src/renderer/api/navidrome/navidrome-types.ts +++ b/src/renderer/api/navidrome/navidrome-types.ts @@ -350,11 +350,13 @@ const shareItem = z.object({ const shareItemParameters = z.object({ description: z.string(), downloadable: z.boolean(), + expires: z.number(), resourceIds: z.string(), resourceType: z.string(), }); export enum NavidromeExtensions { + SHARING = 'sharing', SMART_PLAYLISTS = 'smartPlaylists', } diff --git a/src/renderer/api/types.ts b/src/renderer/api/types.ts index 4f91cd209..6492eb1f6 100644 --- a/src/renderer/api/types.ts +++ b/src/renderer/api/types.ts @@ -772,6 +772,7 @@ export type ShareItemResponse = { id: string } | undefined; export type ShareItemBody = { description: string; downloadable: boolean; + expires: number; resourceIds: string; resourceType: string; }; diff --git a/src/renderer/features/context-menu/context-menu-provider.tsx b/src/renderer/features/context-menu/context-menu-provider.tsx index 3548b532c..7afb65d84 100644 --- a/src/renderer/features/context-menu/context-menu-provider.tsx +++ b/src/renderer/features/context-menu/context-menu-provider.tsx @@ -11,6 +11,8 @@ import { import { closeAllModals, openContextModal, openModal } from '@mantine/modals'; import { AnimatePresence } from 'framer-motion'; import isElectron from 'is-electron'; +import { ServerFeature } from '/@/renderer/api/features-types'; +import { hasFeature } from '/@/renderer/api/utils'; import { useTranslation } from 'react-i18next'; import { RiAddBoxFill, @@ -793,6 +795,10 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => { rightIcon: , }, shareItem: { + disabled: !( + server?.type === ServerType.NAVIDROME && + hasFeature(server, ServerFeature.SHARING_ALBUM_SONG) + ), id: 'shareItem', label: t('page.contextMenu.shareItem', { postProcess: 'sentenceCase' }), leftIcon: , @@ -812,6 +818,7 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => { handleUpdateRating, openDeletePlaylistModal, handleShareItem, + server, t, ]); diff --git a/src/renderer/features/sharing/components/share-item-context-modal.tsx b/src/renderer/features/sharing/components/share-item-context-modal.tsx index d77fb3a80..9a6abb581 100644 --- a/src/renderer/features/sharing/components/share-item-context-modal.tsx +++ b/src/renderer/features/sharing/components/share-item-context-modal.tsx @@ -1,4 +1,5 @@ import { Box, Group, Stack, TextInput } from '@mantine/core'; +import { DateTimePicker } from '@mantine/dates'; import { useForm } from '@mantine/form'; import { closeModal, ContextModalProps } from '@mantine/modals'; import { Button, Switch, toast } from '/@/renderer/components'; @@ -19,29 +20,59 @@ export const ShareItemContextModal = ({ const shareItemMutation = useShareItem({}); + // Uses the same default as Navidrome: 1 year + const defaultDate = new Date(); + defaultDate.setFullYear(defaultDate.getFullYear() + 1); + const form = useForm({ initialValues: { allowDownloading: false, description: '', + expires: defaultDate, + }, + validate: { + expires: (value) => + value > new Date() + ? null + : t('form.shareItem.expireInvalid', { + postProcess: 'sentenceCase', + }), }, }); const handleSubmit = form.onSubmit(async (values) => { - shareItemMutation.mutate({ - body: { - description: values.description, - downloadable: values.allowDownloading, - resourceIds: itemIds.join(), - resourceType, + shareItemMutation.mutate( + { + body: { + description: values.description, + downloadable: values.allowDownloading, + expires: values.expires.getTime(), + resourceIds: itemIds.join(), + resourceType, + }, + serverId: server?.id, + }, + { + onError: () => { + toast.error({ + message: t('form.shareItem.createFailed', { + postProcess: 'sentenceCase', + }), + }); + }, + onSuccess: (_data) => { + if (!server) throw new Error('Server not found'); + if (!_data?.id) throw new Error('Failed to share item'); + navigator.clipboard.writeText(`${server.url}/share/${_data.id}`); + toast.success({ + message: t('form.shareItem.success', { + postProcess: 'sentenceCase', + }), + }); + }, }, - serverId: server?.id, - }); + ); - toast.success({ - message: t('form.shareItem.success', { - postProcess: 'sentenceCase', - }), - }); closeModal(id); return null; }); @@ -63,6 +94,17 @@ export const ShareItemContextModal = ({ })} {...form.getInputProps('allowDownloading')} /> +