Add create/update playlist mutations and form
This commit is contained in:
parent
82f107d835
commit
88f53c17db
11 changed files with 409 additions and 145 deletions
|
@ -33,6 +33,8 @@ import type {
|
||||||
PlaylistSongListArgs,
|
PlaylistSongListArgs,
|
||||||
ArtistListArgs,
|
ArtistListArgs,
|
||||||
RawArtistListResponse,
|
RawArtistListResponse,
|
||||||
|
UpdatePlaylistArgs,
|
||||||
|
RawUpdatePlaylistResponse,
|
||||||
} from '/@/renderer/api/types';
|
} from '/@/renderer/api/types';
|
||||||
import { subsonicApi } from '/@/renderer/api/subsonic.api';
|
import { subsonicApi } from '/@/renderer/api/subsonic.api';
|
||||||
import { jellyfinApi } from '/@/renderer/api/jellyfin.api';
|
import { jellyfinApi } from '/@/renderer/api/jellyfin.api';
|
||||||
|
@ -60,7 +62,7 @@ export type ControllerEndpoint = Partial<{
|
||||||
getPlaylistSongList: (args: PlaylistSongListArgs) => Promise<RawSongListResponse>;
|
getPlaylistSongList: (args: PlaylistSongListArgs) => Promise<RawSongListResponse>;
|
||||||
getSongDetail: (args: SongDetailArgs) => Promise<RawSongDetailResponse>;
|
getSongDetail: (args: SongDetailArgs) => Promise<RawSongDetailResponse>;
|
||||||
getSongList: (args: SongListArgs) => Promise<RawSongListResponse>;
|
getSongList: (args: SongListArgs) => Promise<RawSongListResponse>;
|
||||||
updatePlaylist: () => void;
|
updatePlaylist: (args: UpdatePlaylistArgs) => Promise<RawUpdatePlaylistResponse>;
|
||||||
updateRating: (args: RatingArgs) => Promise<RawRatingResponse>;
|
updateRating: (args: RatingArgs) => Promise<RawRatingResponse>;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
@ -120,7 +122,7 @@ const endpoints: ApiController = {
|
||||||
getPlaylistSongList: navidromeApi.getPlaylistSongList,
|
getPlaylistSongList: navidromeApi.getPlaylistSongList,
|
||||||
getSongDetail: navidromeApi.getSongDetail,
|
getSongDetail: navidromeApi.getSongDetail,
|
||||||
getSongList: navidromeApi.getSongList,
|
getSongList: navidromeApi.getSongList,
|
||||||
updatePlaylist: undefined,
|
updatePlaylist: navidromeApi.updatePlaylist,
|
||||||
updateRating: subsonicApi.updateRating,
|
updateRating: subsonicApi.updateRating,
|
||||||
},
|
},
|
||||||
subsonic: {
|
subsonic: {
|
||||||
|
@ -203,7 +205,16 @@ const getPlaylistList = async (args: PlaylistListArgs) => {
|
||||||
return (apiController('getPlaylistList') as ControllerEndpoint['getPlaylistList'])?.(args);
|
return (apiController('getPlaylistList') as ControllerEndpoint['getPlaylistList'])?.(args);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const createPlaylist = async (args: CreatePlaylistArgs) => {
|
||||||
|
return (apiController('createPlaylist') as ControllerEndpoint['createPlaylist'])?.(args);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatePlaylist = async (args: UpdatePlaylistArgs) => {
|
||||||
|
return (apiController('updatePlaylist') as ControllerEndpoint['updatePlaylist'])?.(args);
|
||||||
|
};
|
||||||
|
|
||||||
export const controller = {
|
export const controller = {
|
||||||
|
createPlaylist,
|
||||||
getAlbumArtistList,
|
getAlbumArtistList,
|
||||||
getAlbumDetail,
|
getAlbumDetail,
|
||||||
getAlbumList,
|
getAlbumList,
|
||||||
|
@ -212,4 +223,5 @@ export const controller = {
|
||||||
getMusicFolderList,
|
getMusicFolderList,
|
||||||
getPlaylistList,
|
getPlaylistList,
|
||||||
getSongList,
|
getSongList,
|
||||||
|
updatePlaylist,
|
||||||
};
|
};
|
||||||
|
|
|
@ -32,6 +32,8 @@ import type {
|
||||||
NDSongListResponse,
|
NDSongListResponse,
|
||||||
NDAlbumArtist,
|
NDAlbumArtist,
|
||||||
NDPlaylist,
|
NDPlaylist,
|
||||||
|
NDUpdatePlaylistParams,
|
||||||
|
NDUpdatePlaylistResponse,
|
||||||
} from '/@/renderer/api/navidrome.types';
|
} from '/@/renderer/api/navidrome.types';
|
||||||
import { NDPlaylistListSort, NDSongListSort, NDSortOrder } from '/@/renderer/api/navidrome.types';
|
import { NDPlaylistListSort, NDSongListSort, NDSortOrder } from '/@/renderer/api/navidrome.types';
|
||||||
import type {
|
import type {
|
||||||
|
@ -53,6 +55,8 @@ import type {
|
||||||
PlaylistSongListArgs,
|
PlaylistSongListArgs,
|
||||||
AlbumArtist,
|
AlbumArtist,
|
||||||
Playlist,
|
Playlist,
|
||||||
|
UpdatePlaylistResponse,
|
||||||
|
UpdatePlaylistArgs,
|
||||||
} from '/@/renderer/api/types';
|
} from '/@/renderer/api/types';
|
||||||
import {
|
import {
|
||||||
playlistListSortMap,
|
playlistListSortMap,
|
||||||
|
@ -286,7 +290,7 @@ const getSongDetail = async (args: SongDetailArgs): Promise<NDSongDetail> => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const createPlaylist = async (args: CreatePlaylistArgs): Promise<CreatePlaylistResponse> => {
|
const createPlaylist = async (args: CreatePlaylistArgs): Promise<CreatePlaylistResponse> => {
|
||||||
const { query, server, signal } = args;
|
const { query, server } = args;
|
||||||
|
|
||||||
const json: NDCreatePlaylistParams = {
|
const json: NDCreatePlaylistParams = {
|
||||||
comment: query.comment,
|
comment: query.comment,
|
||||||
|
@ -299,7 +303,6 @@ const createPlaylist = async (args: CreatePlaylistArgs): Promise<CreatePlaylistR
|
||||||
headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` },
|
headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` },
|
||||||
json,
|
json,
|
||||||
prefixUrl: server?.url,
|
prefixUrl: server?.url,
|
||||||
signal,
|
|
||||||
})
|
})
|
||||||
.json<NDCreatePlaylistResponse>();
|
.json<NDCreatePlaylistResponse>();
|
||||||
|
|
||||||
|
@ -309,6 +312,33 @@ const createPlaylist = async (args: CreatePlaylistArgs): Promise<CreatePlaylistR
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updatePlaylist = async (args: UpdatePlaylistArgs): Promise<UpdatePlaylistResponse> => {
|
||||||
|
const { query, server, signal } = args;
|
||||||
|
|
||||||
|
const previous = query.previous as NDPlaylist;
|
||||||
|
|
||||||
|
const json: NDUpdatePlaylistParams = {
|
||||||
|
...previous,
|
||||||
|
comment: query.comment || '',
|
||||||
|
name: query.name,
|
||||||
|
public: query.public || false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const data = await api
|
||||||
|
.post(`api/playlist/${previous.id}`, {
|
||||||
|
headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` },
|
||||||
|
json,
|
||||||
|
prefixUrl: server?.url,
|
||||||
|
signal,
|
||||||
|
})
|
||||||
|
.json<NDUpdatePlaylistResponse>();
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: data.id,
|
||||||
|
name: query.name,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const deletePlaylist = async (args: DeletePlaylistArgs): Promise<NDDeletePlaylist> => {
|
const deletePlaylist = async (args: DeletePlaylistArgs): Promise<NDDeletePlaylist> => {
|
||||||
const { query, server, signal } = args;
|
const { query, server, signal } = args;
|
||||||
|
|
||||||
|
@ -552,6 +582,7 @@ export const navidromeApi = {
|
||||||
getPlaylistSongList,
|
getPlaylistSongList,
|
||||||
getSongDetail,
|
getSongDetail,
|
||||||
getSongList,
|
getSongList,
|
||||||
|
updatePlaylist,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ndNormalize = {
|
export const ndNormalize = {
|
||||||
|
|
|
@ -259,7 +259,8 @@ export type NDAlbumArtistListParams = {
|
||||||
export type NDCreatePlaylistParams = {
|
export type NDCreatePlaylistParams = {
|
||||||
comment?: string;
|
comment?: string;
|
||||||
name: string;
|
name: string;
|
||||||
public: boolean;
|
public?: boolean;
|
||||||
|
rules?: Record<string, any> | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NDCreatePlaylistResponse = {
|
export type NDCreatePlaylistResponse = {
|
||||||
|
@ -268,6 +269,10 @@ export type NDCreatePlaylistResponse = {
|
||||||
|
|
||||||
export type NDCreatePlaylist = NDCreatePlaylistResponse;
|
export type NDCreatePlaylist = NDCreatePlaylistResponse;
|
||||||
|
|
||||||
|
export type NDUpdatePlaylistParams = NDPlaylist;
|
||||||
|
|
||||||
|
export type NDUpdatePlaylistResponse = NDPlaylist;
|
||||||
|
|
||||||
export type NDDeletePlaylistParams = {
|
export type NDDeletePlaylistParams = {
|
||||||
id: string;
|
id: string;
|
||||||
};
|
};
|
||||||
|
|
|
@ -120,16 +120,6 @@ export interface BasePaginatedResponse<T> {
|
||||||
totalRecordCount: number;
|
totalRecordCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ApiError = {
|
|
||||||
error: {
|
|
||||||
message: string;
|
|
||||||
path: string;
|
|
||||||
trace: string[];
|
|
||||||
};
|
|
||||||
response: string;
|
|
||||||
statusCode: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type AuthenticationResponse = {
|
export type AuthenticationResponse = {
|
||||||
credential: string;
|
credential: string;
|
||||||
ndCredential?: string;
|
ndCredential?: string;
|
||||||
|
@ -740,10 +730,30 @@ export type RawCreatePlaylistResponse = CreatePlaylistResponse | undefined;
|
||||||
|
|
||||||
export type CreatePlaylistResponse = { id: string; name: string };
|
export type CreatePlaylistResponse = { id: string; name: string };
|
||||||
|
|
||||||
export type CreatePlaylistQuery = { comment?: string; name: string; public?: boolean };
|
export type CreatePlaylistQuery = {
|
||||||
|
comment?: string;
|
||||||
|
name: string;
|
||||||
|
public?: boolean;
|
||||||
|
rules?: Record<string, any>;
|
||||||
|
};
|
||||||
|
|
||||||
export type CreatePlaylistArgs = { query: CreatePlaylistQuery } & BaseEndpointArgs;
|
export type CreatePlaylistArgs = { query: CreatePlaylistQuery } & BaseEndpointArgs;
|
||||||
|
|
||||||
|
// Update Playlist
|
||||||
|
export type RawUpdatePlaylistResponse = UpdatePlaylistResponse | undefined;
|
||||||
|
|
||||||
|
export type UpdatePlaylistResponse = { id: string; name: string };
|
||||||
|
|
||||||
|
export type UpdatePlaylistQuery = {
|
||||||
|
comment?: string;
|
||||||
|
name: string;
|
||||||
|
previous: RawPlaylistDetailResponse;
|
||||||
|
public?: boolean;
|
||||||
|
rules?: Record<string, any>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UpdatePlaylistArgs = { query: UpdatePlaylistQuery } & BaseEndpointArgs;
|
||||||
|
|
||||||
// Delete Playlist
|
// Delete Playlist
|
||||||
export type RawDeletePlaylistResponse = NDDeletePlaylist | undefined;
|
export type RawDeletePlaylistResponse = NDDeletePlaylist | undefined;
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,76 @@
|
||||||
|
import { Group, Stack } from '@mantine/core';
|
||||||
|
import { useForm } from '@mantine/form';
|
||||||
|
import { CreatePlaylistQuery } from '/@/renderer/api/types';
|
||||||
|
import { Button, Switch, TextInput, toast } from '/@/renderer/components';
|
||||||
|
import { useCreatePlaylist } from '/@/renderer/features/playlists/mutations/create-playlist-mutation';
|
||||||
|
|
||||||
|
interface CreatePlaylistFormProps {
|
||||||
|
onCancel: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CreatePlaylistForm = ({ onCancel }: CreatePlaylistFormProps) => {
|
||||||
|
const mutation = useCreatePlaylist();
|
||||||
|
|
||||||
|
const form = useForm<CreatePlaylistQuery>({
|
||||||
|
initialValues: {
|
||||||
|
comment: '',
|
||||||
|
name: '',
|
||||||
|
public: false,
|
||||||
|
rules: undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSubmit = form.onSubmit((values) => {
|
||||||
|
mutation.mutate(
|
||||||
|
{ query: values },
|
||||||
|
{
|
||||||
|
onError: (err) => {
|
||||||
|
toast.error({ message: err.message, title: 'Error creating playlist' });
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success({ message: 'Playlist created successfully' });
|
||||||
|
onCancel();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const isSubmitDisabled = !form.values.name || mutation.isLoading;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<Stack>
|
||||||
|
<TextInput
|
||||||
|
data-autofocus
|
||||||
|
required
|
||||||
|
label="Name"
|
||||||
|
{...form.getInputProps('name')}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
label="Description"
|
||||||
|
{...form.getInputProps('comment')}
|
||||||
|
/>
|
||||||
|
<Switch
|
||||||
|
label="Is Public?"
|
||||||
|
{...form.getInputProps('public')}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<Group position="right">
|
||||||
|
<Button
|
||||||
|
variant="subtle"
|
||||||
|
onClick={onCancel}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
disabled={isSubmitDisabled}
|
||||||
|
loading={mutation.isLoading}
|
||||||
|
type="submit"
|
||||||
|
variant="filled"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1 +1,2 @@
|
||||||
export * from './queries/playlist-list-query';
|
export * from './queries/playlist-list-query';
|
||||||
|
export * from './components/create-playlist-form';
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { HTTPError } from 'ky';
|
||||||
|
import { api } from '/@/renderer/api';
|
||||||
|
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||||
|
import { CreatePlaylistArgs, RawCreatePlaylistResponse } from '/@/renderer/api/types';
|
||||||
|
import { MutationOptions } from '/@/renderer/lib/react-query';
|
||||||
|
import { useCurrentServer } from '/@/renderer/store';
|
||||||
|
|
||||||
|
export const useCreatePlaylist = (options?: MutationOptions) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const server = useCurrentServer();
|
||||||
|
|
||||||
|
return useMutation<
|
||||||
|
RawCreatePlaylistResponse,
|
||||||
|
HTTPError,
|
||||||
|
Omit<CreatePlaylistArgs, 'server'>,
|
||||||
|
null
|
||||||
|
>({
|
||||||
|
mutationFn: (args) => api.controller.createPlaylist({ ...args, server }),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries(queryKeys.playlists.list(server?.id || ''));
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
};
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { HTTPError } from 'ky';
|
||||||
|
import { api } from '/@/renderer/api';
|
||||||
|
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||||
|
import { RawUpdatePlaylistResponse, UpdatePlaylistArgs } from '/@/renderer/api/types';
|
||||||
|
import { MutationOptions } from '/@/renderer/lib/react-query';
|
||||||
|
import { useCurrentServer } from '/@/renderer/store';
|
||||||
|
|
||||||
|
export const useUpdatePlaylist = (options?: MutationOptions) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const server = useCurrentServer();
|
||||||
|
|
||||||
|
return useMutation<
|
||||||
|
RawUpdatePlaylistResponse,
|
||||||
|
HTTPError,
|
||||||
|
Omit<UpdatePlaylistArgs, 'server'>,
|
||||||
|
null
|
||||||
|
>({
|
||||||
|
mutationFn: (args) => api.controller.updatePlaylist({ ...args, server }),
|
||||||
|
onSuccess: (data) => {
|
||||||
|
queryClient.invalidateQueries(queryKeys.playlists.list(server?.id || ''));
|
||||||
|
|
||||||
|
if (data?.id) {
|
||||||
|
queryClient.invalidateQueries(queryKeys.playlists.detail(server?.id || '', data.id));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
};
|
|
@ -1,16 +1,16 @@
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
|
import { createPolymorphicComponent, Flex, FlexProps } from '@mantine/core';
|
||||||
import type { LinkProps } from 'react-router-dom';
|
import type { LinkProps } from 'react-router-dom';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import styled, { css } from 'styled-components';
|
import styled, { css } from 'styled-components';
|
||||||
|
|
||||||
interface ListItemProps {
|
interface ListItemProps extends FlexProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
to?: string;
|
to?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StyledItem = styled.div`
|
const StyledItem = styled(Flex)`
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
font-family: var(--content-font-family);
|
font-family: var(--content-font-family);
|
||||||
|
|
||||||
|
@ -32,11 +32,7 @@ const ItemStyle = css`
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Box = styled.div`
|
const _ItemLink = styled(StyledItem)<LinkProps & { disabled?: boolean }>`
|
||||||
${ItemStyle}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ItemLink = styled(Link)<LinkProps & { disabled?: boolean }>`
|
|
||||||
opacity: ${(props) => props.disabled && 0.6};
|
opacity: ${(props) => props.disabled && 0.6};
|
||||||
pointer-events: ${(props) => props.disabled && 'none'};
|
pointer-events: ${(props) => props.disabled && 'none'};
|
||||||
|
|
||||||
|
@ -47,12 +43,15 @@ const ItemLink = styled(Link)<LinkProps & { disabled?: boolean }>`
|
||||||
${ItemStyle}
|
${ItemStyle}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const SidebarItem = ({ to, children, ...rest }: ListItemProps) => {
|
const ItemLink = createPolymorphicComponent<'a', ListItemProps>(_ItemLink);
|
||||||
|
|
||||||
|
export const SidebarItem = ({ to, children, ...props }: ListItemProps) => {
|
||||||
if (to) {
|
if (to) {
|
||||||
return (
|
return (
|
||||||
<ItemLink
|
<ItemLink
|
||||||
|
component={Link}
|
||||||
to={to}
|
to={to}
|
||||||
{...rest}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</ItemLink>
|
</ItemLink>
|
||||||
|
@ -61,15 +60,13 @@ export const SidebarItem = ({ to, children, ...rest }: ListItemProps) => {
|
||||||
return (
|
return (
|
||||||
<StyledItem
|
<StyledItem
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
{...rest}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</StyledItem>
|
</StyledItem>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
SidebarItem.Box = Box;
|
|
||||||
|
|
||||||
SidebarItem.Link = ItemLink;
|
SidebarItem.Link = ItemLink;
|
||||||
|
|
||||||
SidebarItem.defaultProps = {
|
SidebarItem.defaultProps = {
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
|
import { MouseEvent } from 'react';
|
||||||
import { Stack, Grid, Accordion, Center, Group } from '@mantine/core';
|
import { Stack, Grid, Accordion, Center, Group } from '@mantine/core';
|
||||||
|
import { closeAllModals, openModal } from '@mantine/modals';
|
||||||
import { SpotlightProvider } from '@mantine/spotlight';
|
import { SpotlightProvider } from '@mantine/spotlight';
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
import { BsCollection } from 'react-icons/bs';
|
import { BsCollection } from 'react-icons/bs';
|
||||||
import { Button, TextInput } from '/@/renderer/components';
|
import { Button, ScrollArea, TextInput } from '/@/renderer/components';
|
||||||
import {
|
import {
|
||||||
|
RiAddFill,
|
||||||
RiAlbumFill,
|
RiAlbumFill,
|
||||||
RiAlbumLine,
|
RiAlbumLine,
|
||||||
RiArrowDownSLine,
|
RiArrowDownSLine,
|
||||||
|
@ -16,22 +19,26 @@ import {
|
||||||
RiFolder3Line,
|
RiFolder3Line,
|
||||||
RiHome5Fill,
|
RiHome5Fill,
|
||||||
RiHome5Line,
|
RiHome5Line,
|
||||||
|
RiMenuUnfoldLine,
|
||||||
RiMusicFill,
|
RiMusicFill,
|
||||||
RiMusicLine,
|
RiMusicLine,
|
||||||
RiPlayListLine,
|
RiPlayListLine,
|
||||||
RiSearchLine,
|
RiSearchLine,
|
||||||
|
RiUserVoiceFill,
|
||||||
RiUserVoiceLine,
|
RiUserVoiceLine,
|
||||||
} from 'react-icons/ri';
|
} from 'react-icons/ri';
|
||||||
import { useNavigate, Link, useLocation } from 'react-router-dom';
|
import { useNavigate, Link, useLocation, generatePath } from 'react-router-dom';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { SidebarItem } from '/@/renderer/features/sidebar/components/sidebar-item';
|
import { SidebarItem } from '/@/renderer/features/sidebar/components/sidebar-item';
|
||||||
import { AppRoute } from '/@/renderer/router/routes';
|
import { AppRoute } from '/@/renderer/router/routes';
|
||||||
import { useSidebarStore, useAppStoreActions, useCurrentSong } from '/@/renderer/store';
|
import { useSidebarStore, useAppStoreActions, useCurrentSong } from '/@/renderer/store';
|
||||||
import { fadeIn } from '/@/renderer/styles';
|
import { fadeIn } from '/@/renderer/styles';
|
||||||
|
import { CreatePlaylistForm, usePlaylistList } from '/@/renderer/features/playlists';
|
||||||
|
import { PlaylistListSort, SortOrder } from '/@/renderer/api/types';
|
||||||
|
|
||||||
const SidebarContainer = styled.div`
|
const SidebarContainer = styled.div`
|
||||||
height: 100%;
|
height: 100%;
|
||||||
max-height: calc(100vh - 85px); // Account for and playerbar
|
max-height: calc(100vh - 85px); // Account for playerbar
|
||||||
user-select: none;
|
user-select: none;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
@ -68,20 +75,35 @@ export const Sidebar = () => {
|
||||||
const sidebar = useSidebarStore();
|
const sidebar = useSidebarStore();
|
||||||
const { setSidebar } = useAppStoreActions();
|
const { setSidebar } = useAppStoreActions();
|
||||||
const imageUrl = useCurrentSong()?.imageUrl;
|
const imageUrl = useCurrentSong()?.imageUrl;
|
||||||
|
|
||||||
const showImage = sidebar.image;
|
const showImage = sidebar.image;
|
||||||
|
|
||||||
|
const playlistsQuery = usePlaylistList({
|
||||||
|
limit: 0,
|
||||||
|
sortBy: PlaylistListSort.NAME,
|
||||||
|
sortOrder: SortOrder.ASC,
|
||||||
|
startIndex: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleCreatePlaylistModal = (e: MouseEvent<HTMLButtonElement>) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
openModal({
|
||||||
|
children: <CreatePlaylistForm onCancel={() => closeAllModals()} />,
|
||||||
|
size: 'sm',
|
||||||
|
title: 'Create Playlist',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarContainer>
|
<SidebarContainer>
|
||||||
<Stack
|
<Stack
|
||||||
|
h="100%"
|
||||||
justify="space-between"
|
justify="space-between"
|
||||||
spacing={0}
|
spacing={0}
|
||||||
sx={{ height: '100%' }}
|
|
||||||
>
|
>
|
||||||
<Stack
|
<Stack
|
||||||
sx={{
|
spacing={0}
|
||||||
maxHeight: showImage ? `calc(100% - ${sidebar.leftWidth})` : '100%',
|
sx={{ maxHeight: showImage ? `calc(100% - ${sidebar.leftWidth})` : '100%' }}
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<ActionsContainer p={10}>
|
<ActionsContainer p={10}>
|
||||||
<Grid.Col span={8}>
|
<Grid.Col span={8}>
|
||||||
|
@ -120,121 +142,167 @@ export const Sidebar = () => {
|
||||||
</Group>
|
</Group>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
</ActionsContainer>
|
</ActionsContainer>
|
||||||
<Stack
|
|
||||||
spacing={0}
|
<ScrollArea
|
||||||
sx={{ overflowY: 'auto' }}
|
offsetScrollbars={false}
|
||||||
|
scrollbarSize={6}
|
||||||
>
|
>
|
||||||
<SidebarItem to={AppRoute.HOME}>
|
<Stack spacing={0}>
|
||||||
<Group>
|
<SidebarItem
|
||||||
{location.pathname === AppRoute.HOME ? (
|
px="1rem"
|
||||||
<RiHome5Fill size={15} />
|
py="0.5rem"
|
||||||
) : (
|
to={AppRoute.HOME}
|
||||||
<RiHome5Line size={15} />
|
>
|
||||||
)}
|
<Group fw="600">
|
||||||
Home
|
{location.pathname === AppRoute.HOME ? (
|
||||||
</Group>
|
<RiHome5Fill size={15} />
|
||||||
</SidebarItem>
|
) : (
|
||||||
<Accordion
|
<RiHome5Line size={15} />
|
||||||
multiple
|
)}
|
||||||
styles={{
|
Home
|
||||||
control: {
|
</Group>
|
||||||
'&:hover': { background: 'none', color: 'var(--sidebar-fg-hover)' },
|
</SidebarItem>
|
||||||
color: 'var(--sidebar-fg)',
|
<Accordion
|
||||||
transition: 'color 0.2s ease-in-out',
|
multiple
|
||||||
},
|
styles={{
|
||||||
item: { borderBottom: 'none', color: 'var(--sidebar-fg)' },
|
control: {
|
||||||
itemTitle: { color: 'var(--sidebar-fg)' },
|
'&:hover': { background: 'none', color: 'var(--sidebar-fg-hover)' },
|
||||||
panel: {
|
color: 'var(--sidebar-fg)',
|
||||||
marginLeft: '1rem',
|
padding: '1rem 1rem',
|
||||||
},
|
transition: 'color 0.2s ease-in-out',
|
||||||
}}
|
},
|
||||||
value={sidebar.expanded}
|
item: { borderBottom: 'none', color: 'var(--sidebar-fg)' },
|
||||||
onChange={(e) => setSidebar({ expanded: e })}
|
itemTitle: { color: 'var(--sidebar-fg)' },
|
||||||
>
|
label: { fontWeight: 600 },
|
||||||
<Accordion.Item value="library">
|
panel: { padding: '0 1rem' },
|
||||||
<Accordion.Control p="1rem">
|
}}
|
||||||
<Group>
|
value={sidebar.expanded}
|
||||||
{location.pathname.includes('/library/') ? (
|
onChange={(e) => setSidebar({ expanded: e })}
|
||||||
<RiDatabaseFill size={15} />
|
>
|
||||||
) : (
|
<Accordion.Item value="library">
|
||||||
<RiDatabaseLine size={15} />
|
<Accordion.Control>
|
||||||
)}
|
|
||||||
Library
|
|
||||||
</Group>
|
|
||||||
</Accordion.Control>
|
|
||||||
<Accordion.Panel>
|
|
||||||
<SidebarItem to={AppRoute.LIBRARY_ALBUMS}>
|
|
||||||
<Group>
|
<Group>
|
||||||
{location.pathname === AppRoute.LIBRARY_ALBUMS ? (
|
{location.pathname.includes('/library/') ? (
|
||||||
<RiAlbumFill />
|
<RiDatabaseFill size={15} />
|
||||||
) : (
|
) : (
|
||||||
<RiAlbumLine />
|
<RiDatabaseLine size={15} />
|
||||||
)}
|
)}
|
||||||
Albums
|
Library
|
||||||
</Group>
|
</Group>
|
||||||
</SidebarItem>
|
</Accordion.Control>
|
||||||
<SidebarItem to={AppRoute.LIBRARY_SONGS}>
|
<Accordion.Panel>
|
||||||
|
<SidebarItem to={AppRoute.LIBRARY_ALBUMS}>
|
||||||
|
<Group>
|
||||||
|
{location.pathname === AppRoute.LIBRARY_ALBUMS ? (
|
||||||
|
<RiAlbumFill />
|
||||||
|
) : (
|
||||||
|
<RiAlbumLine />
|
||||||
|
)}
|
||||||
|
Albums
|
||||||
|
</Group>
|
||||||
|
</SidebarItem>
|
||||||
|
<SidebarItem to={AppRoute.LIBRARY_SONGS}>
|
||||||
|
<Group>
|
||||||
|
{location.pathname === AppRoute.LIBRARY_SONGS ? (
|
||||||
|
<RiMusicFill />
|
||||||
|
) : (
|
||||||
|
<RiMusicLine />
|
||||||
|
)}
|
||||||
|
Tracks
|
||||||
|
</Group>
|
||||||
|
</SidebarItem>
|
||||||
|
<SidebarItem to={AppRoute.LIBRARY_ALBUMARTISTS}>
|
||||||
|
<Group>
|
||||||
|
{location.pathname === AppRoute.LIBRARY_ALBUMARTISTS ? (
|
||||||
|
<RiUserVoiceFill />
|
||||||
|
) : (
|
||||||
|
<RiUserVoiceLine />
|
||||||
|
)}
|
||||||
|
Album Artists
|
||||||
|
</Group>
|
||||||
|
</SidebarItem>
|
||||||
|
<SidebarItem
|
||||||
|
disabled
|
||||||
|
to={AppRoute.LIBRARY_FOLDERS}
|
||||||
|
>
|
||||||
|
<Group>
|
||||||
|
<RiFlag2Line />
|
||||||
|
Genres
|
||||||
|
</Group>
|
||||||
|
</SidebarItem>
|
||||||
|
<SidebarItem
|
||||||
|
disabled
|
||||||
|
to={AppRoute.LIBRARY_FOLDERS}
|
||||||
|
>
|
||||||
|
<Group>
|
||||||
|
<RiFolder3Line />
|
||||||
|
Folders
|
||||||
|
</Group>
|
||||||
|
</SidebarItem>
|
||||||
|
</Accordion.Panel>
|
||||||
|
</Accordion.Item>
|
||||||
|
<Accordion.Item value="collections">
|
||||||
|
<Accordion.Control disabled>
|
||||||
<Group>
|
<Group>
|
||||||
{location.pathname === AppRoute.LIBRARY_SONGS ? (
|
<BsCollection size={15} />
|
||||||
<RiMusicFill />
|
Collections
|
||||||
) : (
|
|
||||||
<RiMusicLine />
|
|
||||||
)}
|
|
||||||
Tracks
|
|
||||||
</Group>
|
</Group>
|
||||||
</SidebarItem>
|
</Accordion.Control>
|
||||||
<SidebarItem to={AppRoute.LIBRARY_ALBUMARTISTS}>
|
<Accordion.Panel />
|
||||||
<Group>
|
</Accordion.Item>
|
||||||
<RiUserVoiceLine />
|
<Accordion.Item value="playlists">
|
||||||
Album Artists
|
<Accordion.Control>
|
||||||
|
<Group
|
||||||
|
noWrap
|
||||||
|
position="apart"
|
||||||
|
>
|
||||||
|
<Group noWrap>
|
||||||
|
<RiPlayListLine size={15} />
|
||||||
|
Playlists
|
||||||
|
</Group>
|
||||||
|
<Group
|
||||||
|
noWrap
|
||||||
|
spacing="xs"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
compact
|
||||||
|
component="div"
|
||||||
|
h={13}
|
||||||
|
tooltip={{ label: 'Create playlist', openDelay: 500 }}
|
||||||
|
variant="subtle"
|
||||||
|
onClick={handleCreatePlaylistModal}
|
||||||
|
>
|
||||||
|
<RiAddFill size={13} />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
compact
|
||||||
|
component={Link}
|
||||||
|
h={13}
|
||||||
|
to={AppRoute.PLAYLISTS}
|
||||||
|
tooltip={{ label: 'Playlist list', openDelay: 500 }}
|
||||||
|
variant="subtle"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<RiMenuUnfoldLine size={13} />
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
</Group>
|
</Group>
|
||||||
</SidebarItem>
|
</Accordion.Control>
|
||||||
<SidebarItem
|
<Accordion.Panel>
|
||||||
disabled
|
{playlistsQuery?.data?.items?.map((playlist) => (
|
||||||
to={AppRoute.LIBRARY_FOLDERS}
|
<SidebarItem
|
||||||
>
|
key={`sidebar-playlist-${playlist.id}`}
|
||||||
<Group>
|
p={0}
|
||||||
<RiFlag2Line />
|
to={generatePath(AppRoute.PLAYLISTS_DETAIL, { playlistId: playlist.id })}
|
||||||
Genres
|
>
|
||||||
</Group>
|
{playlist.name}
|
||||||
</SidebarItem>
|
</SidebarItem>
|
||||||
<SidebarItem
|
))}
|
||||||
disabled
|
</Accordion.Panel>
|
||||||
to={AppRoute.LIBRARY_FOLDERS}
|
</Accordion.Item>
|
||||||
>
|
</Accordion>
|
||||||
<Group>
|
</Stack>
|
||||||
<RiFolder3Line />
|
</ScrollArea>
|
||||||
Folders
|
|
||||||
</Group>
|
|
||||||
</SidebarItem>
|
|
||||||
</Accordion.Panel>
|
|
||||||
</Accordion.Item>
|
|
||||||
<Accordion.Item value="collections">
|
|
||||||
<Accordion.Control
|
|
||||||
disabled
|
|
||||||
p="1rem"
|
|
||||||
>
|
|
||||||
<Group>
|
|
||||||
<BsCollection size={15} />
|
|
||||||
Collections
|
|
||||||
</Group>
|
|
||||||
</Accordion.Control>
|
|
||||||
<Accordion.Panel />
|
|
||||||
</Accordion.Item>
|
|
||||||
<Accordion.Item value="playlists">
|
|
||||||
<Accordion.Control
|
|
||||||
disabled
|
|
||||||
p="1rem"
|
|
||||||
>
|
|
||||||
<Group>
|
|
||||||
<RiPlayListLine size={15} />
|
|
||||||
Playlists
|
|
||||||
</Group>
|
|
||||||
</Accordion.Control>
|
|
||||||
<Accordion.Panel />
|
|
||||||
</Accordion.Item>
|
|
||||||
</Accordion>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
<AnimatePresence
|
<AnimatePresence
|
||||||
initial={false}
|
initial={false}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { UseQueryOptions, DefaultOptions } from '@tanstack/react-query';
|
import type { UseQueryOptions, DefaultOptions, UseMutationOptions } from '@tanstack/react-query';
|
||||||
import { QueryClient, QueryCache } from '@tanstack/react-query';
|
import { QueryClient, QueryCache } from '@tanstack/react-query';
|
||||||
import { toast } from '/@/renderer/components';
|
import { toast } from '/@/renderer/components';
|
||||||
|
|
||||||
|
@ -49,3 +49,13 @@ export type QueryOptions = {
|
||||||
suspense?: UseQueryOptions['suspense'];
|
suspense?: UseQueryOptions['suspense'];
|
||||||
useErrorBoundary?: boolean;
|
useErrorBoundary?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type MutationOptions = {
|
||||||
|
mutationKey: UseMutationOptions['mutationKey'];
|
||||||
|
onError?: (err: any) => void;
|
||||||
|
onSettled?: any;
|
||||||
|
onSuccess?: any;
|
||||||
|
retry?: UseQueryOptions['retry'];
|
||||||
|
retryDelay?: UseQueryOptions['retryDelay'];
|
||||||
|
useErrorBoundary?: boolean;
|
||||||
|
};
|
||||||
|
|
Reference in a new issue