mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-07 04:20:12 +02:00
feat(playlist): support updating playlist track order (#1875)
* feat(playlist): support updating playlist track order * force track mode when editing * use common confirmation for save * remove en editPLaylist key
This commit is contained in:
@@ -371,7 +371,6 @@
|
|||||||
},
|
},
|
||||||
"editPlaylist": {
|
"editPlaylist": {
|
||||||
"publicJellyfinNote": "Jellyfin for some reason does not expose whether a playlist is public or not. If you wish for this to remain public, please have the following input selected",
|
"publicJellyfinNote": "Jellyfin for some reason does not expose whether a playlist is public or not. If you wish for this to remain public, please have the following input selected",
|
||||||
"editNote": "manual edits are not recommended for large playlists. are you sure you accept the risk of data loss incurred by overwriting the existing playlist?",
|
|
||||||
"success": "$t(entity.playlist, {\"count\": 1}) updated successfully",
|
"success": "$t(entity.playlist, {\"count\": 1}) updated successfully",
|
||||||
"title": "edit $t(entity.playlist, {\"count\": 1})"
|
"title": "edit $t(entity.playlist, {\"count\": 1})"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import {
|
|||||||
ControllerEndpoint,
|
ControllerEndpoint,
|
||||||
InternalControllerEndpoint,
|
InternalControllerEndpoint,
|
||||||
ServerType,
|
ServerType,
|
||||||
|
SetPlaylistSongsArgs,
|
||||||
|
SetPlaylistSongsResponse,
|
||||||
} from '/@/shared/types/domain-types';
|
} from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
type ApiController = {
|
type ApiController = {
|
||||||
@@ -885,6 +887,20 @@ export const controller: GeneralController = {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
setPlaylistSongs: function (args: SetPlaylistSongsArgs): Promise<SetPlaylistSongsResponse> {
|
||||||
|
const server = getServerById(args.apiClientProps.serverId);
|
||||||
|
|
||||||
|
if (!server) {
|
||||||
|
throw new Error(
|
||||||
|
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: setPlaylistSongs`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiController(
|
||||||
|
'setPlaylistSongs',
|
||||||
|
server.type,
|
||||||
|
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } }));
|
||||||
|
},
|
||||||
setRating(args) {
|
setRating(args) {
|
||||||
const server = getServerById(args.apiClientProps.serverId);
|
const server = getServerById(args.apiClientProps.serverId);
|
||||||
|
|
||||||
|
|||||||
@@ -1769,6 +1769,24 @@ export const JellyfinController: InternalControllerEndpoint = {
|
|||||||
),
|
),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
setPlaylistSongs: async (args) => {
|
||||||
|
const { apiClientProps, body } = args;
|
||||||
|
|
||||||
|
const res = await jfApiClient(apiClientProps).updatePlaylist({
|
||||||
|
body: {
|
||||||
|
Ids: body.songIds,
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
id: body.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status !== 204) {
|
||||||
|
throw new Error('Failed to update playlist songs');
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
updateInternetRadioStation: async (args) => {
|
updateInternetRadioStation: async (args) => {
|
||||||
const { apiClientProps, body, query } = args;
|
const { apiClientProps, body, query } = args;
|
||||||
|
|
||||||
@@ -1798,14 +1816,8 @@ export const JellyfinController: InternalControllerEndpoint = {
|
|||||||
|
|
||||||
const res = await jfApiClient(apiClientProps).updatePlaylist({
|
const res = await jfApiClient(apiClientProps).updatePlaylist({
|
||||||
body: {
|
body: {
|
||||||
Genres: body.genres?.map((item) => ({ Id: item.id, Name: item.name })) || [],
|
|
||||||
IsPublic: body.public,
|
IsPublic: body.public,
|
||||||
MediaType: 'Audio',
|
|
||||||
Name: body.name,
|
Name: body.name,
|
||||||
PremiereDate: null,
|
|
||||||
ProviderIds: {},
|
|
||||||
Tags: [],
|
|
||||||
UserId: apiClientProps.server?.userId, // Required
|
|
||||||
},
|
},
|
||||||
params: {
|
params: {
|
||||||
id: query.id,
|
id: query.id,
|
||||||
@@ -1820,31 +1832,6 @@ export const JellyfinController: InternalControllerEndpoint = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// const getArtistList = async (args: ArtistListArgs): Promise<AlbumArtistListResponse> => {
|
|
||||||
// const { query, apiClientProps } = args;
|
|
||||||
|
|
||||||
// const res = await jfApiClient(apiClientProps).getAlbumArtistList({
|
|
||||||
// query: {
|
|
||||||
// Limit: query.limit,
|
|
||||||
// ParentId: query.musicFolderId,
|
|
||||||
// Recursive: true,
|
|
||||||
// SortBy: artistListSortMap.jellyfin[query.sortBy] || 'SortName,Name',
|
|
||||||
// SortOrder: sortOrderMap.jellyfin[query.sortOrder],
|
|
||||||
// StartIndex: query.startIndex,
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
|
|
||||||
// if (res.status !== 200) {
|
|
||||||
// throw new Error('Failed to get artist list');
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return {
|
|
||||||
// items: res.body.Items.map((item) => jfNormalize.albumArtist(item, apiClientProps.server)),
|
|
||||||
// startIndex: query.startIndex,
|
|
||||||
// totalRecordCount: res.body.TotalRecordCount,
|
|
||||||
// };
|
|
||||||
// };
|
|
||||||
|
|
||||||
function getLibraryId(musicFolderId?: string | string[]) {
|
function getLibraryId(musicFolderId?: string | string[]) {
|
||||||
return Array.isArray(musicFolderId) ? musicFolderId[0] : musicFolderId;
|
return Array.isArray(musicFolderId) ? musicFolderId[0] : musicFolderId;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -745,7 +745,6 @@ export const NavidromeController: InternalControllerEndpoint = {
|
|||||||
args.context?.pathReplaceWith,
|
args.context?.pathReplaceWith,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
getSongList: async (args) => {
|
getSongList: async (args) => {
|
||||||
const { apiClientProps, query } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
@@ -819,6 +818,7 @@ export const NavidromeController: InternalControllerEndpoint = {
|
|||||||
totalRecordCount: albums.totalRecordCount,
|
totalRecordCount: albums.totalRecordCount,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
getSongListCount: async ({ apiClientProps, query }) =>
|
getSongListCount: async ({ apiClientProps, query }) =>
|
||||||
NavidromeController.getSongList({
|
NavidromeController.getSongList({
|
||||||
apiClientProps,
|
apiClientProps,
|
||||||
@@ -1122,6 +1122,7 @@ export const NavidromeController: InternalControllerEndpoint = {
|
|||||||
},
|
},
|
||||||
scrobble: SubsonicController.scrobble,
|
scrobble: SubsonicController.scrobble,
|
||||||
search: SubsonicController.search,
|
search: SubsonicController.search,
|
||||||
|
setPlaylistSongs: SubsonicController.setPlaylistSongs,
|
||||||
setRating: SubsonicController.setRating,
|
setRating: SubsonicController.setRating,
|
||||||
shareItem: async (args) => {
|
shareItem: async (args) => {
|
||||||
const { apiClientProps, body } = args;
|
const { apiClientProps, body } = args;
|
||||||
|
|||||||
@@ -2118,6 +2118,22 @@ export const SubsonicController: InternalControllerEndpoint = {
|
|||||||
),
|
),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
setPlaylistSongs: async (args) => {
|
||||||
|
const { apiClientProps, body } = args;
|
||||||
|
|
||||||
|
const res = await ssApiClient(apiClientProps).createPlaylist({
|
||||||
|
query: {
|
||||||
|
playlistId: body.id,
|
||||||
|
songId: body.songIds,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw new Error('Failed to update playlist songs');
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
setRating: async (args) => {
|
setRating: async (args) => {
|
||||||
const { apiClientProps, query } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
|
|||||||
@@ -302,9 +302,9 @@ const PlaylistDetailTrackViewContent = ({ data }: { data: PlaylistSongListRespon
|
|||||||
};
|
};
|
||||||
|
|
||||||
const PlaylistDetailSongList = ({ data }: { data: PlaylistSongListResponse }) => {
|
const PlaylistDetailSongList = ({ data }: { data: PlaylistSongListResponse }) => {
|
||||||
const { displayMode } = useListContext();
|
const { displayMode, mode } = useListContext();
|
||||||
|
|
||||||
if (displayMode === LibraryItem.ALBUM) {
|
if (mode !== 'edit' && displayMode === LibraryItem.ALBUM) {
|
||||||
return <PlaylistDetailAlbumView data={data} />;
|
return <PlaylistDetailAlbumView data={data} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+29
-107
@@ -44,13 +44,7 @@ import { Modal } from '/@/shared/components/modal/modal';
|
|||||||
import { Tooltip } from '/@/shared/components/tooltip/tooltip';
|
import { Tooltip } from '/@/shared/components/tooltip/tooltip';
|
||||||
import { useDisclosure } from '/@/shared/hooks/use-disclosure';
|
import { useDisclosure } from '/@/shared/hooks/use-disclosure';
|
||||||
import { useLocalStorage } from '/@/shared/hooks/use-local-storage';
|
import { useLocalStorage } from '/@/shared/hooks/use-local-storage';
|
||||||
import {
|
import { LibraryItem, Song, SongListSort, SortOrder } from '/@/shared/types/domain-types';
|
||||||
LibraryItem,
|
|
||||||
Playlist,
|
|
||||||
SongListSort,
|
|
||||||
SortOrder,
|
|
||||||
UpdatePlaylistBody,
|
|
||||||
} from '/@/shared/types/domain-types';
|
|
||||||
import { ItemListKey } from '/@/shared/types/types';
|
import { ItemListKey } from '/@/shared/types/types';
|
||||||
|
|
||||||
interface PlaylistDetailSongListHeaderFiltersProps {
|
interface PlaylistDetailSongListHeaderFiltersProps {
|
||||||
@@ -124,7 +118,7 @@ export const PlaylistDetailSongListHeaderFilters = ({
|
|||||||
isSmartPlaylist,
|
isSmartPlaylist,
|
||||||
}: PlaylistDetailSongListHeaderFiltersProps) => {
|
}: PlaylistDetailSongListHeaderFiltersProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { listKey: listKeyFromContext, mode, setMode } = useListContext();
|
const { listData, listKey: listKeyFromContext, mode, setMode } = useListContext();
|
||||||
const { playlistId } = useParams() as { playlistId: string };
|
const { playlistId } = useParams() as { playlistId: string };
|
||||||
const playlistTarget = usePlaylistTarget();
|
const playlistTarget = usePlaylistTarget();
|
||||||
const { setPlaylistBehavior } = useSettingsStoreActions();
|
const { setPlaylistBehavior } = useSettingsStoreActions();
|
||||||
@@ -170,10 +164,19 @@ export const PlaylistDetailSongListHeaderFilters = ({
|
|||||||
key: 'playlist-header-collapsed',
|
key: 'playlist-header-collapsed',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const tracks = useMemo(() => {
|
||||||
|
if (!listData?.length) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return (listData as Song[]).map((song) => song.id);
|
||||||
|
}, [listData]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex justify="space-between" ref={containerRef}>
|
<Flex justify="space-between" ref={containerRef}>
|
||||||
<Group gap="sm" w="100%">
|
<Group gap="sm" w="100%">
|
||||||
<Button
|
<Button
|
||||||
|
disabled={isEditMode}
|
||||||
leftSection={<Icon icon="arrowLeftRight" />}
|
leftSection={<Icon icon="arrowLeftRight" />}
|
||||||
onClick={handleToggleDisplayMode}
|
onClick={handleToggleDisplayMode}
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
@@ -199,15 +202,15 @@ export const PlaylistDetailSongListHeaderFilters = ({
|
|||||||
<MoreButton onClick={handleMore} />
|
<MoreButton onClick={handleMore} />
|
||||||
</Group>
|
</Group>
|
||||||
<Group gap="sm" wrap="nowrap">
|
<Group gap="sm" wrap="nowrap">
|
||||||
{isViewEditMode && <SaveAndReplaceButton mode={mode} playlist={detailQuery.data} />}
|
{isViewEditMode && <SaveAndReplaceButton mode={mode} songIds={tracks} />}
|
||||||
{isViewEditMode && (
|
{isViewEditMode && (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => setMode?.(mode === 'edit' ? 'view' : 'edit')}
|
onClick={() => setMode?.(mode === 'edit' ? 'view' : 'edit')}
|
||||||
uppercase
|
uppercase
|
||||||
variant="subtle"
|
variant={mode === 'edit' ? 'state-error' : 'subtle'}
|
||||||
>
|
>
|
||||||
{mode === 'edit'
|
{mode === 'edit'
|
||||||
? t('common.view', { postProcess: 'titleCase' })
|
? t('common.cancel', { postProcess: 'titleCase' })
|
||||||
: t('common.edit', { postProcess: 'titleCase' })}
|
: t('common.edit', { postProcess: 'titleCase' })}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
@@ -248,39 +251,33 @@ export const PlaylistDetailSongListHeaderFilters = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const openSaveAndReplaceModal = (playlistId: string, updateBody: UpdatePlaylistBody) => {
|
export const openSaveAndReplaceModal = (
|
||||||
|
playlistId: string,
|
||||||
|
songIds: string[],
|
||||||
|
onSuccess: () => void,
|
||||||
|
) => {
|
||||||
openContextModal({
|
openContextModal({
|
||||||
innerProps: { playlistId, updateBody },
|
innerProps: { onSuccess, playlistId, songIds },
|
||||||
modalKey: 'saveAndReplace',
|
modalKey: 'saveAndReplace',
|
||||||
size: 'sm',
|
size: 'sm',
|
||||||
title: i18n.t('common.saveAndReplace', { postProcess: 'titleCase' }) as string,
|
title: i18n.t('common.saveAndReplace', { postProcess: 'titleCase' }) as string,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const SaveAndReplaceButton = ({
|
const SaveAndReplaceButton = ({ mode, songIds }: { mode?: 'edit' | 'view'; songIds: string[] }) => {
|
||||||
mode,
|
|
||||||
playlist,
|
|
||||||
}: {
|
|
||||||
mode: 'edit' | 'view' | undefined;
|
|
||||||
playlist: Playlist | undefined;
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { playlistId } = useParams() as { playlistId: string };
|
const { playlistId } = useParams() as { playlistId: string };
|
||||||
|
const { setMode } = useListContext();
|
||||||
|
|
||||||
|
const onSuccess = useCallback(() => {
|
||||||
|
setMode?.('view');
|
||||||
|
}, [setMode]);
|
||||||
|
|
||||||
const handleOpenModal = useCallback(() => {
|
const handleOpenModal = useCallback(() => {
|
||||||
if (!playlistId || !playlist) return;
|
if (!playlistId) return;
|
||||||
|
|
||||||
const updateBody: UpdatePlaylistBody = {
|
openSaveAndReplaceModal(playlistId, songIds, onSuccess);
|
||||||
comment: playlist.description ?? '',
|
}, [playlistId, songIds, onSuccess]);
|
||||||
name: playlist.name,
|
|
||||||
ownerId: playlist.ownerId ?? '',
|
|
||||||
public: playlist.public ?? false,
|
|
||||||
queryBuilderRules: playlist.rules ?? undefined,
|
|
||||||
sync: playlist.sync ?? false,
|
|
||||||
};
|
|
||||||
|
|
||||||
openSaveAndReplaceModal(playlistId, updateBody);
|
|
||||||
}, [playlistId, playlist]);
|
|
||||||
|
|
||||||
if (mode === 'view') {
|
if (mode === 'view') {
|
||||||
return null;
|
return null;
|
||||||
@@ -297,78 +294,3 @@ const SaveAndReplaceButton = ({
|
|||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
// const GenreFilterSelection = () => {
|
|
||||||
// const { t } = useTranslation();
|
|
||||||
// const { playlistId } = useParams() as { playlistId: string };
|
|
||||||
// const serverId = useCurrentServerId();
|
|
||||||
|
|
||||||
// const { data } = useQuery(playlistsQueries.songList({ query: { id: playlistId }, serverId }));
|
|
||||||
|
|
||||||
// const genres = useMemo(() => {
|
|
||||||
// const uniqueGenres = new Map<string, string>();
|
|
||||||
|
|
||||||
// data?.items.forEach((song) => {
|
|
||||||
// song.genres.forEach((genre) => {
|
|
||||||
// if (genre.id) {
|
|
||||||
// uniqueGenres.set(genre.id, genre.name);
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
|
|
||||||
// return Array.from(uniqueGenres.entries()).map(([id, name]) => ({
|
|
||||||
// label: name,
|
|
||||||
// value: id,
|
|
||||||
// }));
|
|
||||||
// }, [data?.items]);
|
|
||||||
|
|
||||||
// return (
|
|
||||||
// <Stack p="md" style={{ background: 'var(--theme-colors-surface)', height: '12rem' }}>
|
|
||||||
// <Text>{t('filter.genre', { postProcess: 'titleCase' })}</Text>
|
|
||||||
// <ScrollArea>
|
|
||||||
// <ul style={{ listStyle: 'none', margin: 0, padding: 0 }}>
|
|
||||||
// {genres.map((genre) => (
|
|
||||||
// <li key={genre.value}>{genre.label}</li>
|
|
||||||
// ))}
|
|
||||||
// </ul>
|
|
||||||
// </ScrollArea>
|
|
||||||
// </Stack>
|
|
||||||
// );
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const ArtistFilterSelection = () => {
|
|
||||||
// const { t } = useTranslation();
|
|
||||||
// const { playlistId } = useParams() as { playlistId: string };
|
|
||||||
// const serverId = useCurrentServerId();
|
|
||||||
|
|
||||||
// const { data } = useQuery(playlistsQueries.songList({ query: { id: playlistId }, serverId }));
|
|
||||||
|
|
||||||
// const artists = useMemo(() => {
|
|
||||||
// const uniqueArtists = new Map<string, string>();
|
|
||||||
|
|
||||||
// data?.items.forEach((song) => {
|
|
||||||
// song.artists.forEach((artist) => {
|
|
||||||
// if (artist.id) {
|
|
||||||
// uniqueArtists.set(artist.id, artist.name);
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
|
|
||||||
// return Array.from(uniqueArtists.entries()).map(([id, name]) => ({
|
|
||||||
// label: name,
|
|
||||||
// value: id,
|
|
||||||
// }));
|
|
||||||
// }, [data?.items]);
|
|
||||||
|
|
||||||
// return (
|
|
||||||
// <Stack style={{ height: '12rem' }}>
|
|
||||||
// <Text>{t('filter.artist', { postProcess: 'titleCase' })}</Text>
|
|
||||||
// <ScrollArea>
|
|
||||||
// <ul style={{ listStyle: 'none', margin: 0, padding: 0 }}>
|
|
||||||
// {artists.map((artist) => (
|
|
||||||
// <li key={artist.value}>{artist.label}</li>
|
|
||||||
// ))}
|
|
||||||
// </ul>
|
|
||||||
// </ScrollArea>
|
|
||||||
// </Stack>
|
|
||||||
// );
|
|
||||||
// };
|
|
||||||
|
|||||||
@@ -2,21 +2,20 @@ import { closeAllModals, ContextModalProps } from '@mantine/modals';
|
|||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { useUpdatePlaylist } from '/@/renderer/features/playlists/mutations/update-playlist-mutation';
|
import { useUpdatePlaylistTracks } from '/@/renderer/features/playlists/mutations/update-playlist-tracks-mutation';
|
||||||
import { useCurrentServerId } from '/@/renderer/store';
|
import { useCurrentServerId } from '/@/renderer/store';
|
||||||
import { ConfirmModal } from '/@/shared/components/modal/modal';
|
import { ConfirmModal } from '/@/shared/components/modal/modal';
|
||||||
import { Text } from '/@/shared/components/text/text';
|
import { Text } from '/@/shared/components/text/text';
|
||||||
import { toast } from '/@/shared/components/toast/toast';
|
import { toast } from '/@/shared/components/toast/toast';
|
||||||
import { UpdatePlaylistBody } from '/@/shared/types/domain-types';
|
|
||||||
|
|
||||||
export const SaveAndReplaceContextModal = ({
|
export const SaveAndReplaceContextModal = ({
|
||||||
innerProps,
|
innerProps,
|
||||||
}: ContextModalProps<{ playlistId: string; updateBody: UpdatePlaylistBody }>) => {
|
}: ContextModalProps<{ onSuccess: () => void; playlistId: string; songIds: string[] }>) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { playlistId, updateBody } = innerProps;
|
const { onSuccess, playlistId, songIds } = innerProps;
|
||||||
const serverId = useCurrentServerId();
|
const serverId = useCurrentServerId();
|
||||||
|
|
||||||
const updatePlaylistMutation = useUpdatePlaylist({});
|
const updatePlaylistMutation = useUpdatePlaylistTracks({});
|
||||||
|
|
||||||
const handleConfirm = useCallback(() => {
|
const handleConfirm = useCallback(() => {
|
||||||
if (!serverId || !playlistId) {
|
if (!serverId || !playlistId) {
|
||||||
@@ -27,8 +26,10 @@ export const SaveAndReplaceContextModal = ({
|
|||||||
updatePlaylistMutation.mutate(
|
updatePlaylistMutation.mutate(
|
||||||
{
|
{
|
||||||
apiClientProps: { serverId },
|
apiClientProps: { serverId },
|
||||||
body: updateBody,
|
body: {
|
||||||
query: { id: playlistId },
|
id: playlistId,
|
||||||
|
songIds,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
@@ -41,6 +42,7 @@ export const SaveAndReplaceContextModal = ({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
|
onSuccess();
|
||||||
closeAllModals();
|
closeAllModals();
|
||||||
toast.success({
|
toast.success({
|
||||||
message: t('form.editPlaylist.success', {
|
message: t('form.editPlaylist.success', {
|
||||||
@@ -50,11 +52,11 @@ export const SaveAndReplaceContextModal = ({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}, [t, serverId, playlistId, updateBody, updatePlaylistMutation]);
|
}, [serverId, playlistId, updatePlaylistMutation, songIds, t, onSuccess]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ConfirmModal loading={updatePlaylistMutation.isPending} onConfirm={handleConfirm}>
|
<ConfirmModal loading={updatePlaylistMutation.isPending} onConfirm={handleConfirm}>
|
||||||
<Text>{t('form.editPlaylist.editNote', { postProcess: 'sentenceCase' })}</Text>
|
<Text>{t('common.areYouSure', { postProcess: 'sentenceCase' })}</Text>
|
||||||
</ConfirmModal>
|
</ConfirmModal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
|
||||||
|
import { api } from '/@/renderer/api';
|
||||||
|
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||||
|
import { MutationHookArgs } from '/@/renderer/lib/react-query';
|
||||||
|
import { SetPlaylistSongsArgs } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
|
export const useUpdatePlaylistTracks = (args: MutationHookArgs) => {
|
||||||
|
const { options } = args || {};
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<null, AxiosError, SetPlaylistSongsArgs, null>({
|
||||||
|
mutationFn: (args) =>
|
||||||
|
api.controller.setPlaylistSongs({
|
||||||
|
...args,
|
||||||
|
apiClientProps: { serverId: args.apiClientProps.serverId },
|
||||||
|
}),
|
||||||
|
onSuccess: (_data, variables) => {
|
||||||
|
const { apiClientProps, body } = variables;
|
||||||
|
const serverId = apiClientProps.serverId;
|
||||||
|
|
||||||
|
if (!serverId) return;
|
||||||
|
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: queryKeys.playlists.list(serverId),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (body?.id) {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: queryKeys.playlists.detail(serverId, body.id),
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: queryKeys.playlists.songList(serverId, body.id),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -683,14 +683,9 @@ const createPlaylist = z.object({
|
|||||||
const updatePlaylist = z.null();
|
const updatePlaylist = z.null();
|
||||||
|
|
||||||
const updatePlaylistParameters = z.object({
|
const updatePlaylistParameters = z.object({
|
||||||
Genres: z.array(genreItem),
|
Ids: z.string().array().optional(),
|
||||||
IsPublic: z.boolean().optional(),
|
IsPublic: z.boolean().optional(),
|
||||||
MediaType: z.literal('Audio'),
|
Name: z.string().optional(),
|
||||||
Name: z.string(),
|
|
||||||
PremiereDate: z.null(),
|
|
||||||
ProviderIds: z.object({}),
|
|
||||||
Tags: z.array(genericItem),
|
|
||||||
UserId: z.string(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const addToPlaylist = z.object({
|
const addToPlaylist = z.object({
|
||||||
|
|||||||
@@ -467,7 +467,7 @@ const deletePlaylistParameters = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const createPlaylistParameters = z.object({
|
const createPlaylistParameters = z.object({
|
||||||
name: z.string(),
|
name: z.string().optional(),
|
||||||
playlistId: z.string().optional(),
|
playlistId: z.string().optional(),
|
||||||
songId: z.array(z.string()).optional(),
|
songId: z.array(z.string()).optional(),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1084,7 +1084,6 @@ export type UpdatePlaylistArgs = BaseEndpointArgs & {
|
|||||||
export type UpdatePlaylistBody = {
|
export type UpdatePlaylistBody = {
|
||||||
_custom?: Record<string, any>;
|
_custom?: Record<string, any>;
|
||||||
comment?: string;
|
comment?: string;
|
||||||
genres?: Genre[];
|
|
||||||
name: string;
|
name: string;
|
||||||
ownerId?: string;
|
ownerId?: string;
|
||||||
public?: boolean;
|
public?: boolean;
|
||||||
@@ -1430,6 +1429,7 @@ export type ControllerEndpoint = {
|
|||||||
savePlayQueue: (args: SaveQueueArgs) => Promise<void>;
|
savePlayQueue: (args: SaveQueueArgs) => Promise<void>;
|
||||||
scrobble: (args: ScrobbleArgs) => Promise<ScrobbleResponse>;
|
scrobble: (args: ScrobbleArgs) => Promise<ScrobbleResponse>;
|
||||||
search: (args: SearchArgs) => Promise<SearchResponse>;
|
search: (args: SearchArgs) => Promise<SearchResponse>;
|
||||||
|
setPlaylistSongs: (args: SetPlaylistSongsArgs) => Promise<SetPlaylistSongsResponse>;
|
||||||
setRating?: (args: SetRatingArgs) => Promise<RatingResponse>;
|
setRating?: (args: SetRatingArgs) => Promise<RatingResponse>;
|
||||||
shareItem?: (args: ShareItemArgs) => Promise<ShareItemResponse>;
|
shareItem?: (args: ShareItemArgs) => Promise<ShareItemResponse>;
|
||||||
updateInternetRadioStation: (
|
updateInternetRadioStation: (
|
||||||
@@ -1581,6 +1581,9 @@ export type InternalControllerEndpoint = {
|
|||||||
savePlayQueue: (args: ReplaceApiClientProps<SaveQueueArgs>) => Promise<void>;
|
savePlayQueue: (args: ReplaceApiClientProps<SaveQueueArgs>) => Promise<void>;
|
||||||
scrobble: (args: ReplaceApiClientProps<ScrobbleArgs>) => Promise<ScrobbleResponse>;
|
scrobble: (args: ReplaceApiClientProps<ScrobbleArgs>) => Promise<ScrobbleResponse>;
|
||||||
search: (args: ReplaceApiClientProps<SearchArgs>) => Promise<SearchResponse>;
|
search: (args: ReplaceApiClientProps<SearchArgs>) => Promise<SearchResponse>;
|
||||||
|
setPlaylistSongs: (
|
||||||
|
args: ReplaceApiClientProps<SetPlaylistSongsArgs>,
|
||||||
|
) => Promise<SetPlaylistSongsResponse>;
|
||||||
setRating?: (args: ReplaceApiClientProps<SetRatingArgs>) => Promise<RatingResponse>;
|
setRating?: (args: ReplaceApiClientProps<SetRatingArgs>) => Promise<RatingResponse>;
|
||||||
shareItem?: (args: ReplaceApiClientProps<ShareItemArgs>) => Promise<ShareItemResponse>;
|
shareItem?: (args: ReplaceApiClientProps<ShareItemArgs>) => Promise<ShareItemResponse>;
|
||||||
updateInternetRadioStation: (
|
updateInternetRadioStation: (
|
||||||
@@ -1637,6 +1640,15 @@ export type ServerInfo = {
|
|||||||
|
|
||||||
export type ServerInfoArgs = BaseEndpointArgs;
|
export type ServerInfoArgs = BaseEndpointArgs;
|
||||||
|
|
||||||
|
export type SetPlaylistSongsArgs = BaseEndpointArgs & { body: SetPlaylistSongsQuery };
|
||||||
|
|
||||||
|
export type SetPlaylistSongsQuery = {
|
||||||
|
id: string;
|
||||||
|
songIds: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SetPlaylistSongsResponse = null;
|
||||||
|
|
||||||
export type SimilarSongsArgs = BaseEndpointArgs & {
|
export type SimilarSongsArgs = BaseEndpointArgs & {
|
||||||
query: SimilarSongsQuery;
|
query: SimilarSongsQuery;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user