mirror of
https://github.com/jeffvli/sonixd.git
synced 2026-04-29 02:32:37 -04:00
Move servers to separate feature/route
- Add deviceId to localstorage - Add individual server auth to localstorage
This commit is contained in:
@@ -1,17 +1,15 @@
|
||||
import { axios } from 'renderer/lib';
|
||||
import { getServerUrl } from 'renderer/utils';
|
||||
import { PingResponse, UserResponse } from './types';
|
||||
|
||||
const login = async (
|
||||
server: string,
|
||||
serverUrl: string,
|
||||
body: {
|
||||
password: string;
|
||||
username: string;
|
||||
}
|
||||
) => {
|
||||
const serverUrl = getServerUrl(server);
|
||||
const { data } = await axios.post<UserResponse>(
|
||||
`${serverUrl}/auth/login`,
|
||||
`${serverUrl}/api/auth/login`,
|
||||
body,
|
||||
{
|
||||
withCredentials: true,
|
||||
@@ -21,9 +19,8 @@ const login = async (
|
||||
return data;
|
||||
};
|
||||
|
||||
const ping = async (server: string) => {
|
||||
const serverUrl = getServerUrl(server);
|
||||
const { data } = await axios.get<PingResponse>(`${serverUrl}/auth/ping`, {
|
||||
const ping = async (serverUrl: string) => {
|
||||
const { data } = await axios.get<PingResponse>(`${serverUrl}/api/auth/ping`, {
|
||||
timeout: 2000,
|
||||
});
|
||||
|
||||
|
||||
@@ -3,4 +3,3 @@ export * from './usersApi';
|
||||
export * from './serversApi';
|
||||
export * from './queries/useAlbum';
|
||||
export * from './queryKeys';
|
||||
export * from './types';
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { axios } from 'renderer/lib';
|
||||
import { ServerResponse, ServersResponse } from './types';
|
||||
import { ServerResponse } from './types';
|
||||
|
||||
const getServers = async () => {
|
||||
const { data } = await axios.get<ServersResponse>('/servers');
|
||||
const { data } = await axios.get<ServerResponse[]>('/servers');
|
||||
return data;
|
||||
};
|
||||
|
||||
const create = async (body: {
|
||||
const createServer = async (body: {
|
||||
name: string;
|
||||
remoteUserId: string;
|
||||
token: string;
|
||||
@@ -18,6 +18,6 @@ const create = async (body: {
|
||||
};
|
||||
|
||||
export const serversApi = {
|
||||
create,
|
||||
createServer,
|
||||
getServers,
|
||||
};
|
||||
|
||||
@@ -5,30 +5,20 @@ export type BaseResponse = {
|
||||
statusCode: number;
|
||||
};
|
||||
|
||||
export type ServerResponse = Server;
|
||||
|
||||
export type ServersResponse = Server[];
|
||||
|
||||
export type UserResponse = User;
|
||||
|
||||
export type UsersResponse = User[];
|
||||
|
||||
export type PingResponse = Ping;
|
||||
|
||||
export interface Server {
|
||||
export type ServerResponse = {
|
||||
createdAt: string;
|
||||
id: number;
|
||||
name: string;
|
||||
remoteUserId: string;
|
||||
serverFolder?: ServerFolder[];
|
||||
serverFolder?: ServerFolderResponse[];
|
||||
serverType: string;
|
||||
token: string;
|
||||
updatedAt: string;
|
||||
url: string;
|
||||
username: string;
|
||||
}
|
||||
};
|
||||
|
||||
export interface ServerFolder {
|
||||
export type ServerFolderResponse = {
|
||||
createdAt: string;
|
||||
enabled: boolean;
|
||||
id: number;
|
||||
@@ -37,9 +27,9 @@ export interface ServerFolder {
|
||||
remoteId: string;
|
||||
serverId: number;
|
||||
updatedAt: string;
|
||||
}
|
||||
};
|
||||
|
||||
export interface User {
|
||||
export type UserResponse = {
|
||||
createdAt: string;
|
||||
enabled: boolean;
|
||||
id: number;
|
||||
@@ -47,10 +37,51 @@ export interface User {
|
||||
password: string;
|
||||
updatedAt: string;
|
||||
username: string;
|
||||
}
|
||||
};
|
||||
|
||||
export interface Ping {
|
||||
export type PingResponse = {
|
||||
description: string;
|
||||
name: string;
|
||||
version: string;
|
||||
}
|
||||
};
|
||||
|
||||
export type GenreResponse = {
|
||||
createdAt: string;
|
||||
id: number;
|
||||
name: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type AlbumResponse = {
|
||||
albumArtistId: number;
|
||||
createdAt: string;
|
||||
date: string;
|
||||
genres: GenreResponse[];
|
||||
id: number;
|
||||
name: string;
|
||||
remoteCreatedAt: string;
|
||||
remoteId: string;
|
||||
serverFolderId: number;
|
||||
songs: SongResponse[];
|
||||
updatedAt: string;
|
||||
year: number;
|
||||
};
|
||||
|
||||
export type SongResponse = {
|
||||
albumId: number;
|
||||
artistName: null;
|
||||
bitRate: number;
|
||||
container: string;
|
||||
createdAt: string;
|
||||
date: string;
|
||||
disc: number;
|
||||
duration: number;
|
||||
id: number;
|
||||
name: string;
|
||||
remoteCreatedAt: string;
|
||||
remoteId: string;
|
||||
serverFolderId: number;
|
||||
track: number;
|
||||
updatedAt: string;
|
||||
year: number;
|
||||
};
|
||||
|
||||
@@ -1,19 +1,35 @@
|
||||
import md5 from 'md5';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { useMutation } from 'react-query';
|
||||
import { authApi } from 'renderer/api';
|
||||
import { useAppDispatch } from 'renderer/hooks';
|
||||
import { login } from 'renderer/store/authSlice';
|
||||
|
||||
export const useLogin = (
|
||||
server: string,
|
||||
serverUrl: string,
|
||||
body: {
|
||||
password: string;
|
||||
username: string;
|
||||
}
|
||||
) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: () => authApi.login(server, body),
|
||||
mutationFn: () => authApi.login(serverUrl, body),
|
||||
onSuccess: () => {
|
||||
dispatch(login(serverUrl));
|
||||
|
||||
if (!localStorage.getItem('device_id')) {
|
||||
localStorage.setItem('device_id', nanoid());
|
||||
}
|
||||
|
||||
localStorage.setItem(
|
||||
'authentication',
|
||||
JSON.stringify({ isAuthenticated: true, serverUrl: server })
|
||||
JSON.stringify({
|
||||
isAuthenticated: true,
|
||||
key: md5(serverUrl),
|
||||
serverUrl,
|
||||
})
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
margin: auto;
|
||||
padding: 3rem;
|
||||
background: rgba(50, 50, 50, 0.4);
|
||||
border-radius: 5%;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.button {
|
||||
|
||||
@@ -12,16 +12,13 @@ import { useDebouncedValue } from '@mantine/hooks';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { AlertCircle, CircleCheck } from 'tabler-icons-react';
|
||||
import { useAppDispatch } from 'renderer/hooks';
|
||||
import { login } from 'renderer/store/authSlice';
|
||||
import { getServerUrl } from 'renderer/utils';
|
||||
import { normalizeServerUrl } from 'renderer/utils';
|
||||
import { useLogin } from '../queries/useLogin';
|
||||
import { usePingServer } from '../queries/usePingServer';
|
||||
import styles from './LoginRoute.module.scss';
|
||||
|
||||
export const LoginRoute = () => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const [username, setUsername] = useState(searchParams.get('username') || '');
|
||||
@@ -35,7 +32,7 @@ export const LoginRoute = () => {
|
||||
mutate: handleLogin,
|
||||
isLoading,
|
||||
isError,
|
||||
} = useLogin(server, {
|
||||
} = useLogin(normalizeServerUrl(server), {
|
||||
password,
|
||||
username,
|
||||
});
|
||||
@@ -44,7 +41,7 @@ export const LoginRoute = () => {
|
||||
isLoading: isCheckingServer,
|
||||
isSuccess: isValidServer,
|
||||
isFetched,
|
||||
} = usePingServer(debouncedServer);
|
||||
} = usePingServer(normalizeServerUrl(debouncedServer));
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
@@ -54,9 +51,7 @@ export const LoginRoute = () => {
|
||||
e.preventDefault();
|
||||
handleLogin(undefined, {
|
||||
onError: () => {},
|
||||
onSuccess: () => {
|
||||
dispatch(login(getServerUrl(server)));
|
||||
},
|
||||
onSuccess: () => {},
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
||||
85
src/renderer/features/servers/components/AddServerModal.tsx
Normal file
85
src/renderer/features/servers/components/AddServerModal.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Modal,
|
||||
ModalProps,
|
||||
PasswordInput,
|
||||
SegmentedControl,
|
||||
Stack,
|
||||
TextInput,
|
||||
} from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useCreateServer, validateServer } from '../queries/useCreateServer';
|
||||
|
||||
export const AddServerModal = ({ ...rest }: ModalProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
legacyAuth: false,
|
||||
name: '',
|
||||
password: '',
|
||||
serverType: 'jellyfin',
|
||||
url: 'http://',
|
||||
username: '',
|
||||
},
|
||||
});
|
||||
|
||||
const createServerMutation = useCreateServer();
|
||||
|
||||
return (
|
||||
<Modal centered title={t('server.add.title')} {...rest}>
|
||||
<form
|
||||
onSubmit={form.onSubmit(async (values) => {
|
||||
const res = await validateServer(values);
|
||||
|
||||
if (res?.token) {
|
||||
createServerMutation.mutateAsync({
|
||||
...values,
|
||||
remoteUserId: res.userId,
|
||||
token: res.token,
|
||||
});
|
||||
}
|
||||
})}
|
||||
>
|
||||
<Stack>
|
||||
<SegmentedControl
|
||||
data={[
|
||||
{ label: 'Jellyfin', value: 'jellyfin' },
|
||||
{ label: 'Subsonic', value: 'subsonic' },
|
||||
]}
|
||||
{...form.getInputProps('serverType')}
|
||||
/>
|
||||
<TextInput
|
||||
required
|
||||
label={t('server.name')}
|
||||
{...form.getInputProps('name')}
|
||||
/>
|
||||
<TextInput
|
||||
required
|
||||
label={t('server.url')}
|
||||
{...form.getInputProps('url')}
|
||||
/>
|
||||
<TextInput
|
||||
required
|
||||
label={t('server.username')}
|
||||
{...form.getInputProps('username')}
|
||||
/>
|
||||
<PasswordInput
|
||||
required
|
||||
label={t('server.password')}
|
||||
{...form.getInputProps('password')}
|
||||
/>
|
||||
{form.getInputProps('serverType').value === 'subsonic' && (
|
||||
<Checkbox
|
||||
label={t('server.legacyauth')}
|
||||
{...form.getInputProps('legacyAuth', { type: 'checkbox' })}
|
||||
/>
|
||||
)}
|
||||
<Button type="submit">{t('server.submit')}</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
63
src/renderer/features/servers/components/EditServerModal.tsx
Normal file
63
src/renderer/features/servers/components/EditServerModal.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
ModalProps,
|
||||
PasswordInput,
|
||||
SegmentedControl,
|
||||
Stack,
|
||||
TextInput,
|
||||
} from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ServerResponse } from 'renderer/api/types';
|
||||
|
||||
interface EditServerModalProps extends ModalProps {
|
||||
server: ServerResponse | undefined;
|
||||
}
|
||||
|
||||
export const EditServerModal = ({ server }: EditServerModalProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
legacyAuth: false,
|
||||
name: server?.name,
|
||||
password: '',
|
||||
serverType: server?.serverType,
|
||||
url: server?.url,
|
||||
username: server?.username,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<form onSubmit={form.onSubmit(async () => {})}>
|
||||
<Stack>
|
||||
<SegmentedControl
|
||||
disabled
|
||||
data={[
|
||||
{ label: 'Jellyfin', value: 'jellyfin' },
|
||||
{ label: 'Subsonic', value: 'subsonic' },
|
||||
]}
|
||||
{...form.getInputProps('serverType')}
|
||||
/>
|
||||
<TextInput label={t('server.name')} {...form.getInputProps('name')} />
|
||||
<TextInput label={t('server.url')} {...form.getInputProps('url')} />
|
||||
<TextInput
|
||||
label={t('server.username')}
|
||||
{...form.getInputProps('username')}
|
||||
/>
|
||||
<PasswordInput
|
||||
label={t('server.password')}
|
||||
{...form.getInputProps('password')}
|
||||
/>
|
||||
{form.getInputProps('serverType').value === 'subsonic' && (
|
||||
<Checkbox
|
||||
label={t('server.legacyauth')}
|
||||
{...form.getInputProps('legacyAuth', { type: 'checkbox' })}
|
||||
/>
|
||||
)}
|
||||
<Button type="submit">{t('server.submit')}</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
.item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
max-width: 50vw;
|
||||
margin: 1rem;
|
||||
padding: 1rem;
|
||||
outline: 1px #fff solid;
|
||||
}
|
||||
84
src/renderer/features/servers/components/ServerList.tsx
Normal file
84
src/renderer/features/servers/components/ServerList.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Text } from '@mantine/core';
|
||||
import { useDisclosure } from '@mantine/hooks';
|
||||
import { EditCircle } from 'tabler-icons-react';
|
||||
import { ServerResponse } from 'renderer/api/types';
|
||||
import { IconButton } from 'renderer/components';
|
||||
import { useServers } from '../queries/useServers';
|
||||
import { EditServerModal } from './EditServerModal';
|
||||
import styles from './ServerList.module.scss';
|
||||
|
||||
export const ServerList = () => {
|
||||
const { data: servers } = useServers();
|
||||
const [editServerModal, editServerHandlers] = useDisclosure(false);
|
||||
const [opened, setOpened] = useState(false);
|
||||
|
||||
const [selectedServer, setSelectedServer] = useState<
|
||||
ServerResponse | undefined
|
||||
>();
|
||||
|
||||
const handleClickServer = (server: ServerResponse) => {
|
||||
setSelectedServer(server);
|
||||
editServerHandlers.open();
|
||||
};
|
||||
|
||||
const handleKeyDownServer = (
|
||||
e: React.KeyboardEvent<HTMLDivElement>,
|
||||
server: ServerResponse
|
||||
) => {
|
||||
if (e.key === ' ' || e.key === 'Enter') {
|
||||
setSelectedServer(server);
|
||||
editServerHandlers.open();
|
||||
}
|
||||
};
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setOpened(false);
|
||||
editServerHandlers.close();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (editServerModal === true) {
|
||||
setTimeout(() => setOpened(true));
|
||||
} else {
|
||||
setTimeout(() => setSelectedServer(undefined), 100);
|
||||
}
|
||||
}, [editServerModal]);
|
||||
|
||||
console.log('opened', opened);
|
||||
|
||||
return (
|
||||
<>
|
||||
{servers &&
|
||||
servers.map((server) => (
|
||||
<>
|
||||
<div
|
||||
className={styles.item}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => handleClickServer(server)}
|
||||
onKeyDown={(e) => handleKeyDownServer(e, server)}
|
||||
>
|
||||
<div>
|
||||
{server.name}
|
||||
<Text>Hello</Text>
|
||||
</div>
|
||||
<IconButton
|
||||
icon={<EditCircle />}
|
||||
onClick={() => editServerHandlers.toggle()}
|
||||
>
|
||||
Edit
|
||||
</IconButton>
|
||||
</div>
|
||||
{selectedServer && (
|
||||
<EditServerModal
|
||||
opened={opened}
|
||||
server={selectedServer}
|
||||
onClose={handleCloseModal}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
5
src/renderer/features/servers/index.ts
Normal file
5
src/renderer/features/servers/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export * from './routes/ServersRoute';
|
||||
export * from './queries/useCreateServer';
|
||||
export * from './queries/useServers';
|
||||
export * from './components/AddServerModal';
|
||||
export * from './components/ServerList';
|
||||
@@ -61,9 +61,9 @@ export const validateServer = async (options: {
|
||||
return null;
|
||||
};
|
||||
|
||||
export const useCreateServers = () => {
|
||||
export const useCreateServer = () => {
|
||||
return useMutation({
|
||||
mutationFn: serversApi.create,
|
||||
mutationFn: serversApi.createServer,
|
||||
onError: (e) => console.log(e),
|
||||
onSuccess: (e) => console.log(e),
|
||||
});
|
||||
77
src/renderer/features/servers/queries/useServers.ts
Normal file
77
src/renderer/features/servers/queries/useServers.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import md5 from 'md5';
|
||||
import { useQuery } from 'react-query';
|
||||
import { queryKeys, serversApi } from 'renderer/api';
|
||||
import { ServerFolderResponse } from 'renderer/api/types';
|
||||
|
||||
export interface ServerFolderAuth {
|
||||
id: number;
|
||||
locked: boolean;
|
||||
serverId: number;
|
||||
token: string;
|
||||
type: string;
|
||||
url: string;
|
||||
userId: string;
|
||||
username: string;
|
||||
}
|
||||
|
||||
export const useServers = () => {
|
||||
return useQuery({
|
||||
onSuccess: (servers) => {
|
||||
const { serverUrl } = JSON.parse(
|
||||
localStorage.getItem('authentication') || '{}'
|
||||
);
|
||||
const storedServersKey = `servers_${md5(serverUrl)}`;
|
||||
const serversFromLocalStorage = localStorage.getItem(storedServersKey);
|
||||
|
||||
// If a custom account/token is set for a server, use that instead of the default one
|
||||
if (serversFromLocalStorage) {
|
||||
const existingServers = JSON.parse(serversFromLocalStorage);
|
||||
|
||||
// The 'locked' property determines whether or not to skip updating the server auth
|
||||
const skipped = existingServers.filter(
|
||||
(server: ServerFolderAuth) => server.locked
|
||||
);
|
||||
|
||||
const store = servers.flatMap((server) =>
|
||||
server.serverFolder?.map((serverFolder: ServerFolderResponse) => {
|
||||
if (skipped.includes(serverFolder.id)) {
|
||||
return existingServers.find(
|
||||
(s: ServerFolderAuth) => s.id === serverFolder.id
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
id: serverFolder.id,
|
||||
locked: false,
|
||||
serverId: server.id,
|
||||
token: server.token,
|
||||
type: server.serverType,
|
||||
url: server.url,
|
||||
userId: server.remoteUserId,
|
||||
username: server.username,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return localStorage.setItem(storedServersKey, JSON.stringify(store));
|
||||
}
|
||||
|
||||
const store = servers.flatMap((server) =>
|
||||
server.serverFolder?.map((serverFolder: ServerFolderResponse) => ({
|
||||
id: serverFolder.id,
|
||||
locked: false,
|
||||
serverId: server.id,
|
||||
token: server.token,
|
||||
type: server.serverType,
|
||||
url: server.url,
|
||||
userId: server.remoteUserId,
|
||||
username: server.username,
|
||||
}))
|
||||
);
|
||||
|
||||
return localStorage.setItem(storedServersKey, JSON.stringify(store));
|
||||
},
|
||||
queryFn: () => serversApi.getServers(),
|
||||
queryKey: queryKeys.servers,
|
||||
});
|
||||
};
|
||||
11
src/renderer/features/servers/routes/ServersRoute.tsx
Normal file
11
src/renderer/features/servers/routes/ServersRoute.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Title } from '@mantine/core';
|
||||
import { ServerList } from '../components/ServerList';
|
||||
|
||||
export const ServersRoute = () => {
|
||||
return (
|
||||
<div>
|
||||
<Title>Servers</Title>
|
||||
<ServerList />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -4,7 +4,7 @@ import { useNavigate } from 'react-router';
|
||||
import { Logout, Server, Settings } from 'tabler-icons-react';
|
||||
import { useAppDispatch } from 'renderer/hooks';
|
||||
import { logout } from 'renderer/store/authSlice';
|
||||
import { ServersModal } from './servers/components/ServersModal';
|
||||
import { AddServerModal } from '../servers/components/AddServerModal';
|
||||
import styles from './UserMenu.module.scss';
|
||||
|
||||
export const UserMenu = () => {
|
||||
@@ -42,7 +42,7 @@ export const UserMenu = () => {
|
||||
Logout
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
<ServersModal
|
||||
<AddServerModal
|
||||
opened={addServerModal}
|
||||
onClose={() => addServerHandlers.close()}
|
||||
/>
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
.container {
|
||||
padding: 10px;
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { Grid } from '@mantine/core';
|
||||
import { Server } from 'server/types/types';
|
||||
import styles from './ServerList.module.scss';
|
||||
|
||||
interface ServerListProps {
|
||||
servers: Server[] | undefined;
|
||||
}
|
||||
|
||||
export const ServerList = ({ servers }: ServerListProps) => {
|
||||
return (
|
||||
<>
|
||||
{servers &&
|
||||
servers.map((server) => (
|
||||
<div className={styles.container}>
|
||||
<Grid align="center" px="sm">
|
||||
<Grid.Col span={9}>{server.name || server.url}</Grid.Col>
|
||||
<Grid.Col span={3} />
|
||||
</Grid>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,97 +0,0 @@
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Modal,
|
||||
ModalProps,
|
||||
PasswordInput,
|
||||
SegmentedControl,
|
||||
Stack,
|
||||
TextInput,
|
||||
} from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { useBooleanToggle } from '@mantine/hooks';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useCreateServers, validateServer } from '../queries/useCreateServers';
|
||||
import { useServers } from '../queries/useServers';
|
||||
import { ServerList } from './ServerList';
|
||||
|
||||
export const ServersModal = ({ ...rest }: ModalProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [createNew, toggleCreateNew] = useBooleanToggle(false);
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
legacyAuth: false,
|
||||
name: '',
|
||||
password: '',
|
||||
serverType: 'jellyfin',
|
||||
url: 'http://',
|
||||
username: '',
|
||||
},
|
||||
});
|
||||
|
||||
const { data } = useServers({});
|
||||
|
||||
const createServerMutation = useCreateServers();
|
||||
|
||||
return (
|
||||
<Modal centered title={t('server.title')} {...rest}>
|
||||
<ServerList servers={data} />
|
||||
<Button variant="subtle" onClick={() => toggleCreateNew()}>
|
||||
{t('server.new')}
|
||||
</Button>
|
||||
{createNew && (
|
||||
<form
|
||||
onSubmit={form.onSubmit(async (values) => {
|
||||
const res = await validateServer(values);
|
||||
|
||||
if (res?.token) {
|
||||
createServerMutation.mutateAsync({
|
||||
...values,
|
||||
remoteUserId: res.userId,
|
||||
token: res.token,
|
||||
});
|
||||
}
|
||||
})}
|
||||
>
|
||||
<Stack>
|
||||
<SegmentedControl
|
||||
data={[
|
||||
{ label: 'Jellyfin', value: 'jellyfin' },
|
||||
{ label: 'Subsonic', value: 'subsonic' },
|
||||
]}
|
||||
{...form.getInputProps('serverType')}
|
||||
/>
|
||||
<TextInput
|
||||
required
|
||||
label={t('server.name')}
|
||||
{...form.getInputProps('name')}
|
||||
/>
|
||||
<TextInput
|
||||
required
|
||||
label={t('server.url')}
|
||||
{...form.getInputProps('url')}
|
||||
/>
|
||||
<TextInput
|
||||
required
|
||||
label={t('server.username')}
|
||||
{...form.getInputProps('username')}
|
||||
/>
|
||||
<PasswordInput
|
||||
required
|
||||
label={t('server.password')}
|
||||
{...form.getInputProps('password')}
|
||||
/>
|
||||
{form.getInputProps('serverType').value === 'subsonic' && (
|
||||
<Checkbox
|
||||
label={t('server.legacyauth')}
|
||||
{...form.getInputProps('legacyAuth', { type: 'checkbox' })}
|
||||
/>
|
||||
)}
|
||||
<Button type="submit">{t('server.submit')}</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -1,18 +0,0 @@
|
||||
import { useQuery } from 'react-query';
|
||||
import { queryKeys } from 'renderer/api';
|
||||
import { serversApi } from 'renderer/api/serversApi';
|
||||
import { ExtractFnReturnType, QueryConfig } from 'renderer/lib';
|
||||
|
||||
type UseServersOptions = {
|
||||
config?: QueryConfig<QueryFnType>;
|
||||
};
|
||||
|
||||
type QueryFnType = typeof serversApi.getServers;
|
||||
|
||||
export const useServers = ({ config }: UseServersOptions) => {
|
||||
return useQuery<ExtractFnReturnType<QueryFnType>>({
|
||||
...config,
|
||||
queryFn: () => serversApi.getServers(),
|
||||
queryKey: queryKeys.servers,
|
||||
});
|
||||
};
|
||||
@@ -1,7 +1,9 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
import md5 from 'md5';
|
||||
|
||||
export interface AuthState {
|
||||
isAuthenticated: boolean;
|
||||
key: string;
|
||||
serverUrl: string;
|
||||
}
|
||||
|
||||
@@ -11,6 +13,7 @@ const persistedAuthState = JSON.parse(
|
||||
|
||||
const initialState: AuthState = {
|
||||
isAuthenticated: persistedAuthState.isAuthenticated,
|
||||
key: persistedAuthState.key,
|
||||
serverUrl: persistedAuthState.serverUrl,
|
||||
};
|
||||
|
||||
@@ -21,6 +24,7 @@ export const authSlice = createSlice({
|
||||
login: (state: AuthState, action: PayloadAction<string>) => {
|
||||
state.isAuthenticated = true;
|
||||
state.serverUrl = action.payload;
|
||||
state.key = md5(action.payload);
|
||||
},
|
||||
|
||||
logout: (state: AuthState) => {
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
export const getServerUrl = (url: string) => {
|
||||
if (url[url.length - 1] === '/') {
|
||||
return `${url}api`;
|
||||
}
|
||||
|
||||
return `${url}/api`;
|
||||
};
|
||||
@@ -1,2 +1,2 @@
|
||||
export * from './randomString';
|
||||
export * from './getServerUrl';
|
||||
export * from './normalizeServerUrl';
|
||||
|
||||
4
src/renderer/utils/normalizeServerUrl.ts
Normal file
4
src/renderer/utils/normalizeServerUrl.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export const normalizeServerUrl = (url: string) => {
|
||||
// Remove trailing slash
|
||||
return url.endsWith('/') ? url.slice(0, -1) : url;
|
||||
};
|
||||
Reference in New Issue
Block a user