mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-14 20:40:21 +02:00
add initial files
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
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]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{servers &&
|
||||
servers.data.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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from './routes/ServersRoute';
|
||||
export * from './queries/useCreateServer';
|
||||
export * from './queries/useServers';
|
||||
export * from './components/AddServerModal';
|
||||
export * from './components/ServerList';
|
||||
@@ -0,0 +1,70 @@
|
||||
import axios from 'axios';
|
||||
import md5 from 'md5';
|
||||
import { useMutation } from 'react-query';
|
||||
import { serversApi } from 'renderer/api/serversApi';
|
||||
import { randomString } from 'renderer/utils';
|
||||
|
||||
export const validateServer = async (options: {
|
||||
legacyAuth: boolean;
|
||||
password: string;
|
||||
serverType: string;
|
||||
url: string;
|
||||
username: string;
|
||||
}) => {
|
||||
const { serverType, url, username, password, legacyAuth } = options;
|
||||
const cleanServerUrl = url.replace(/\/$/, '');
|
||||
|
||||
try {
|
||||
if (serverType === 'subsonic') {
|
||||
let testConnection;
|
||||
let token;
|
||||
if (legacyAuth) {
|
||||
token = `u=${username}&p=${password}`;
|
||||
testConnection = await axios.get(
|
||||
`${cleanServerUrl}/rest/ping.view?v=1.13.0&c=sonixd&f=json&${token}`
|
||||
);
|
||||
} else {
|
||||
const salt = randomString();
|
||||
const hash = md5(password + salt);
|
||||
token = `u=${username}&s=${salt}&t=${hash}`;
|
||||
|
||||
testConnection = await axios.get(
|
||||
`${cleanServerUrl}/rest/ping.view?v=1.13.0&c=sonixd&f=json&${token}`
|
||||
);
|
||||
}
|
||||
|
||||
if (testConnection.data['subsonic-response'].status === 'failed') {
|
||||
return {
|
||||
error: `${testConnection.data['subsonic-response'].error.message}`,
|
||||
};
|
||||
}
|
||||
return { token, userId: '' };
|
||||
}
|
||||
|
||||
const { data } = await axios.post(
|
||||
`${cleanServerUrl}/users/authenticatebyname`,
|
||||
{ pw: password, username },
|
||||
{
|
||||
headers: {
|
||||
'X-Emby-Authorization': `MediaBrowser Client="Sonixd", Device="PC", DeviceId="Sonixd", Version="1.0.0-alpha1"`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return { token: data.AccessToken, userId: data.User.Id };
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
return { error: err.message };
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const useCreateServer = () => {
|
||||
return useMutation({
|
||||
mutationFn: serversApi.createServer,
|
||||
onError: (e) => console.log(e),
|
||||
onSuccess: (e) => console.log(e),
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
import md5 from 'md5';
|
||||
import { useQuery } from 'react-query';
|
||||
import { queryKeys } from 'renderer/api/queryKeys';
|
||||
import { serversApi } from 'renderer/api/serversApi';
|
||||
import { ServerFolderResponse } from 'renderer/api/types';
|
||||
import { ServerFolderAuth } from 'types';
|
||||
|
||||
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?.data?.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?.data?.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,
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Title } from '@mantine/core';
|
||||
import { ServerList } from '../components/ServerList';
|
||||
|
||||
export const ServersRoute = () => {
|
||||
return (
|
||||
<div>
|
||||
<Title>Servers</Title>
|
||||
<ServerList />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user