🔀 Merge branch 'dev' into overseerr-integration

This commit is contained in:
ajnart
2022-07-24 21:36:55 +02:00
100 changed files with 4558 additions and 11317 deletions

View File

@@ -1,25 +1,30 @@
import {
Modal,
ActionIcon,
Anchor,
Button,
Center,
Group,
TextInput,
Image,
Button,
Select,
LoadingOverlay,
ActionIcon,
Tooltip,
Modal,
MultiSelect,
ScrollArea,
Select,
Switch,
Tabs,
TextInput,
Title,
Anchor,
Text,
Tooltip,
} from '@mantine/core';
import { useForm } from '@mantine/form';
import { useEffect, useState } from 'react';
import { useDebouncedValue } from '@mantine/hooks';
import { IconApps as Apps } from '@tabler/icons';
import { useEffect, useState } from 'react';
import { v4 as uuidv4 } from 'uuid';
import { useDebouncedValue } from '@mantine/hooks';
import { useConfig } from '../../tools/state';
import { ServiceTypeList } from '../../tools/types';
import { tryMatchPort, ServiceTypeList, StatusCodes } from '../../tools/types';
import Tip from '../layout/Tip';
export function AddItemShelfButton(props: any) {
const [opened, setOpened] = useState(false);
@@ -50,11 +55,13 @@ export function AddItemShelfButton(props: any) {
);
}
function MatchIcon(name: string, form: any) {
function MatchIcon(name: string | undefined, form: any) {
if (name === undefined || name === '') return null;
fetch(
`https://cdn.jsdelivr.net/gh/walkxhub/dashboard-icons/png/${name
.replace(/\s+/g, '-')
.toLowerCase()}.png`
.toLowerCase()
.replace(/^dash\.$/, 'dashdot')}.png`
).then((res) => {
if (res.ok) {
form.setFieldValue('icon', res.url);
@@ -65,28 +72,13 @@ function MatchIcon(name: string, form: any) {
}
function MatchService(name: string, form: any) {
const service = ServiceTypeList.find((s) => s === name);
const service = ServiceTypeList.find((s) => s.toLowerCase() === name.toLowerCase());
if (service) {
form.setFieldValue('type', service);
}
}
function MatchPort(name: string, form: any) {
const portmap = [
{ name: 'qBittorrent', value: '8080' },
{ name: 'Sonarr', value: '8989' },
{ name: 'Radarr', value: '7878' },
{ name: 'Lidarr', value: '8686' },
{ name: 'Readarr', value: '8686' },
{ name: 'Deluge', value: '8112' },
{ name: 'Transmission', value: '9091' },
];
// Match name with portmap key
const port = portmap.find((p) => p.name === name);
if (port) {
form.setFieldValue('url', `http://localhost:${port.value}`);
}
}
const DEFAULT_ICON = '/favicon.svg';
export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } & any) {
const { setOpened } = props;
@@ -107,22 +99,21 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
type: props.type ?? 'Other',
category: props.category ?? undefined,
name: props.name ?? '',
icon: props.icon ?? '/favicon.svg',
icon: props.icon ?? DEFAULT_ICON,
url: props.url ?? '',
apiKey: props.apiKey ?? (undefined as unknown as string),
username: props.username ?? (undefined as unknown as string),
password: props.password ?? (undefined as unknown as string),
openedUrl: props.openedUrl ?? (undefined as unknown as string),
status: props.status ?? ['200'],
newTab: props.newTab ?? true,
},
validate: {
apiKey: () => null,
// Validate icon with a regex
icon: (value: string) => {
// Regex to match everything that ends with and icon extension
if (!value.match(/\.(png|jpg|jpeg|gif|svg)$/)) {
return 'Please enter a valid icon URL';
}
return null;
},
icon: (value: string) =>
// Disable matching to allow any values
null,
// Validate url with a regex http/https
url: (value: string) => {
try {
@@ -132,15 +123,21 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
}
return null;
},
status: (value: string[]) => {
if (!value.length) {
return 'Please select a status code';
}
return null;
},
},
});
const [debounced, cancel] = useDebouncedValue(form.values.name, 250);
useEffect(() => {
if (form.values.name !== debounced) return;
if (form.values.name !== debounced || form.values.icon !== DEFAULT_ICON) return;
MatchIcon(form.values.name, form);
MatchService(form.values.name, form);
MatchPort(form.values.name, form);
tryMatchPort(form.values.name, form);
}, [debounced]);
// Try to set const hostname to new URL(form.values.url).hostname)
@@ -166,6 +163,12 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
</Center>
<form
onSubmit={form.onSubmit(() => {
if (JSON.stringify(form.values.status) === JSON.stringify(['200'])) {
form.values.status = undefined;
}
if (form.values.newTab === true) {
form.values.newTab = undefined;
}
// If service already exists, update it.
if (config.services && config.services.find((s) => s.id === form.values.id)) {
setConfig({
@@ -190,126 +193,171 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
form.reset();
})}
>
<Group direction="column" grow>
<TextInput
required
label="Service name"
placeholder="Plex"
{...form.getInputProps('name')}
/>
<Tabs grow>
<Tabs.Tab label="Options">
<ScrollArea style={{ height: 500 }} scrollbarSize={4}>
<Group direction="column" grow>
<TextInput
required
label="Service name"
placeholder="Plex"
{...form.getInputProps('name')}
/>
<TextInput
required
label="Icon url"
placeholder="https://i.gifer.com/ANPC.gif"
{...form.getInputProps('icon')}
/>
<TextInput
required
label="Service url"
placeholder="http://localhost:7575"
{...form.getInputProps('url')}
/>
<Select
label="Service type"
defaultValue="Other"
placeholder="Pick one"
required
searchable
data={ServiceTypeList}
{...form.getInputProps('type')}
/>
<Select
label="Category"
data={categoryList}
placeholder="Select a category or create a new one"
nothingFound="Nothing found"
searchable
clearable
creatable
onClick={(e) => {
e.preventDefault();
}}
getCreateLabel={(query) => `+ Create "${query}"`}
onCreate={(query) => {}}
{...form.getInputProps('category')}
/>
<LoadingOverlay visible={isLoading} />
{(form.values.type === 'Sonarr' ||
form.values.type === 'Radarr' ||
form.values.type === 'Lidarr' ||
form.values.type === 'Readarr') && (
<>
<TextInput
<TextInput
required
label="Icon URL"
placeholder={DEFAULT_ICON}
{...form.getInputProps('icon')}
/>
<TextInput
required
label="Service URL"
placeholder="http://localhost:7575"
{...form.getInputProps('url')}
/>
<TextInput
label="On Click URL"
placeholder="http://sonarr.example.com"
{...form.getInputProps('openedUrl')}
/>
<Select
label="Service type"
defaultValue="Other"
placeholder="Pick one"
required
searchable
data={ServiceTypeList}
{...form.getInputProps('type')}
/>
<Select
label="Category"
data={categoryList}
placeholder="Select a category or create a new one"
nothingFound="Nothing found"
searchable
clearable
creatable
onClick={(e) => {
e.preventDefault();
}}
getCreateLabel={(query) => `+ Create "${query}"`}
onCreate={(query) => {}}
{...form.getInputProps('category')}
/>
<LoadingOverlay visible={isLoading} />
{(form.values.type === 'Sonarr' ||
form.values.type === 'Radarr' ||
form.values.type === 'Lidarr' ||
form.values.type === 'Readarr') && (
<>
<TextInput
required
label="API key"
placeholder="Your API key"
value={form.values.apiKey}
onChange={(event) => {
form.setFieldValue('apiKey', event.currentTarget.value);
}}
error={form.errors.apiKey && 'Invalid API key'}
/>
<Tip>
Get your API key{' '}
<Anchor
target="_blank"
weight="bold"
style={{ fontStyle: 'inherit', fontSize: 'inherit' }}
href={`${hostname}/settings/general`}
>
here.
</Anchor>
</Tip>
</>
)}
{form.values.type === 'qBittorrent' && (
<>
<TextInput
required
label="Username"
placeholder="admin"
value={form.values.username}
onChange={(event) => {
form.setFieldValue('username', event.currentTarget.value);
}}
error={form.errors.username && 'Invalid username'}
/>
<TextInput
required
label="Password"
placeholder="adminadmin"
value={form.values.password}
onChange={(event) => {
form.setFieldValue('password', event.currentTarget.value);
}}
error={form.errors.password && 'Invalid password'}
/>
</>
)}
{form.values.type === 'Deluge' && (
<>
<TextInput
label="Password"
placeholder="password"
value={form.values.password}
onChange={(event) => {
form.setFieldValue('password', event.currentTarget.value);
}}
error={form.errors.password && 'Invalid password'}
/>
</>
)}
{form.values.type === 'Transmission' && (
<>
<TextInput
label="Username"
placeholder="admin"
value={form.values.username}
onChange={(event) => {
form.setFieldValue('username', event.currentTarget.value);
}}
error={form.errors.username && 'Invalid username'}
/>
<TextInput
label="Password"
placeholder="adminadmin"
value={form.values.password}
onChange={(event) => {
form.setFieldValue('password', event.currentTarget.value);
}}
error={form.errors.password && 'Invalid password'}
/>
</>
)}
</Group>
</ScrollArea>
</Tabs.Tab>
<Tabs.Tab label="Advanced Options">
<Group direction="column" grow>
<MultiSelect
required
label="API key"
placeholder="Your API key"
value={form.values.apiKey}
onChange={(event) => {
form.setFieldValue('apiKey', event.currentTarget.value);
}}
error={form.errors.apiKey && 'Invalid API key'}
label="HTTP Status Codes"
data={StatusCodes}
placeholder="Select valid status codes"
clearButtonLabel="Clear selection"
nothingFound="Nothing found"
defaultValue={['200']}
clearable
searchable
{...form.getInputProps('status')}
/>
<Text
style={{
alignSelf: 'center',
fontSize: '0.75rem',
textAlign: 'center',
color: '#a0aec0',
}}
>
tip: Get your API key{' '}
<Anchor
target="_blank"
weight="bold"
style={{ fontStyle: 'inherit', fontSize: 'inherit' }}
href={`${hostname}/settings/general`}
>
here
</Anchor>
</Text>
</>
)}
{form.values.type === 'qBittorrent' && (
<>
<TextInput
required
label="Username"
placeholder="admin"
value={form.values.username}
onChange={(event) => {
form.setFieldValue('username', event.currentTarget.value);
}}
error={form.errors.username && 'Invalid username'}
<Switch
label="Open service in new tab"
defaultChecked={form.values.newTab}
{...form.getInputProps('newTab')}
/>
<TextInput
required
label="Password"
placeholder="adminadmin"
value={form.values.password}
onChange={(event) => {
form.setFieldValue('password', event.currentTarget.value);
}}
error={form.errors.password && 'Invalid password'}
/>
</>
)}
{form.values.type === 'Deluge' && (
<>
<TextInput
required
label="Password"
placeholder="deluge"
value={form.values.password}
onChange={(event) => {
form.setFieldValue('password', event.currentTarget.value);
}}
error={form.errors.password && 'Invalid password'}
/>
</>
)}
</Group>
</Group>
</Tabs.Tab>
</Tabs>
<Group grow position="center" mt="xl">
<Button type="submit">{props.message ?? 'Add service'}</Button>
</Group>