mirror of
https://github.com/idanoo/autobrr
synced 2025-07-25 17:59:14 +00:00
Feature: Radarr (#13)
* feat(web): add and update radarr * feat: add radarr download client * feat: add tests
This commit is contained in:
parent
0c4aaa29b0
commit
455284a94b
35 changed files with 2898 additions and 3348 deletions
File diff suppressed because it is too large
Load diff
1
web/src/components/alerts/index.ts
Normal file
1
web/src/components/alerts/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { default as AlertWarning } from "./warning";
|
32
web/src/components/alerts/warning.tsx
Normal file
32
web/src/components/alerts/warning.tsx
Normal file
|
@ -0,0 +1,32 @@
|
|||
import { ExclamationIcon } from "@heroicons/react/solid";
|
||||
import React from "react";
|
||||
|
||||
interface props {
|
||||
title: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
function AlertWarning({ title, text }: props) {
|
||||
return (
|
||||
<div className="p-4">
|
||||
<div className="rounded-md bg-yellow-50 p-4">
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
<ExclamationIcon
|
||||
className="h-5 w-5 text-yellow-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<h3 className="text-sm font-medium text-yellow-800">{title}</h3>
|
||||
<div className="mt-2 text-sm text-yellow-700">
|
||||
<p>{text}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AlertWarning;
|
|
@ -1,97 +0,0 @@
|
|||
import {Field} from "react-final-form";
|
||||
import {Listbox, Transition} from "@headlessui/react";
|
||||
import {CheckIcon, SelectorIcon} from "@heroicons/react/solid";
|
||||
import React, {Fragment} from "react";
|
||||
import {classNames} from "../../styles/utils";
|
||||
|
||||
|
||||
interface SelectOption {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface props {
|
||||
name: string;
|
||||
label: string;
|
||||
optionDefaultText: string;
|
||||
options: SelectOption[];
|
||||
}
|
||||
|
||||
function SelectField({name, label, optionDefaultText, options}: props) {
|
||||
return (
|
||||
<div className="col-span-6 sm:col-span-6">
|
||||
<Field
|
||||
name={name}
|
||||
type="select"
|
||||
render={({input}) => (
|
||||
<Listbox value={input.value} onChange={input.onChange}>
|
||||
{({open}) => (
|
||||
<div
|
||||
className="space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
|
||||
<Listbox.Label
|
||||
className="block text-sm font-medium text-gray-900 sm:mt-px sm:pt-2">{label}</Listbox.Label>
|
||||
<div className="mt-2 relative">
|
||||
<Listbox.Button
|
||||
className="bg-white relative w-full border border-gray-300 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
||||
<span
|
||||
className="block truncate">{input.value ? options.find(c => c.value === input.value)!.label : optionDefaultText}</span>
|
||||
<span
|
||||
className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
||||
<SelectorIcon className="h-5 w-5 text-gray-400" aria-hidden="true"/>
|
||||
</span>
|
||||
</Listbox.Button>
|
||||
|
||||
<Transition
|
||||
show={open}
|
||||
as={Fragment}
|
||||
leave="transition ease-in duration-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Listbox.Options
|
||||
static
|
||||
className="absolute z-10 mt-1 w-full bg-white shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm"
|
||||
>
|
||||
{options.map((opt) => (
|
||||
<Listbox.Option
|
||||
key={opt.value}
|
||||
className={({active}) =>
|
||||
classNames(
|
||||
active ? 'text-white bg-indigo-600' : 'text-gray-900',
|
||||
'cursor-default select-none relative py-2 pl-3 pr-9'
|
||||
)
|
||||
}
|
||||
value={opt.value}
|
||||
>
|
||||
{({selected, active}) => (
|
||||
<>
|
||||
<span className={classNames(selected ? 'font-semibold' : 'font-normal', 'block truncate')}>
|
||||
{opt.label}
|
||||
</span>
|
||||
|
||||
{selected ? (
|
||||
<span
|
||||
className={classNames(
|
||||
active ? 'text-white' : 'text-indigo-600',
|
||||
'absolute inset-y-0 right-0 flex items-center pr-4'
|
||||
)}
|
||||
>
|
||||
<CheckIcon className="h-5 w-5" aria-hidden="true"/>
|
||||
</span>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Listbox>
|
||||
)}/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SelectField;
|
47
web/src/components/inputs/compact/NumberField.tsx
Normal file
47
web/src/components/inputs/compact/NumberField.tsx
Normal file
|
@ -0,0 +1,47 @@
|
|||
import { Field } from "react-final-form";
|
||||
import React from "react";
|
||||
import Error from "../Error";
|
||||
import { classNames } from "../../../styles/utils";
|
||||
|
||||
interface Props {
|
||||
name: string;
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
const NumberField: React.FC<Props> = ({
|
||||
name,
|
||||
label,
|
||||
placeholder,
|
||||
required,
|
||||
className,
|
||||
}) => (
|
||||
<div className="col-span-12 sm:col-span-6">
|
||||
<label htmlFor={name} className="block text-sm font-medium text-gray-700">
|
||||
{label}
|
||||
</label>
|
||||
|
||||
<Field name={name} parse={(v) => v & parseInt(v, 10)}>
|
||||
{({ input, meta }) => (
|
||||
<div className="sm:col-span-2">
|
||||
<input
|
||||
type="number"
|
||||
{...input}
|
||||
className={classNames(
|
||||
meta.touched && meta.error
|
||||
? "focus:ring-red-500 focus:border-red-500 border-red-500"
|
||||
: "focus:ring-indigo-500 focus:border-indigo-500 border-gray-300",
|
||||
"block w-full shadow-sm sm:text-sm rounded-md"
|
||||
)}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
<Error name={name} classNames="block text-red-500 mt-2" />
|
||||
</div>
|
||||
)}
|
||||
</Field>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default NumberField;
|
111
web/src/components/inputs/compact/SelectField.tsx
Normal file
111
web/src/components/inputs/compact/SelectField.tsx
Normal file
|
@ -0,0 +1,111 @@
|
|||
import { Field } from "react-final-form";
|
||||
import { Listbox, Transition } from "@headlessui/react";
|
||||
import { CheckIcon, SelectorIcon } from "@heroicons/react/solid";
|
||||
import { Fragment } from "react";
|
||||
import { classNames } from "../../../styles/utils";
|
||||
|
||||
interface SelectOption {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface props {
|
||||
name: string;
|
||||
label: string;
|
||||
optionDefaultText: string;
|
||||
options: SelectOption[];
|
||||
}
|
||||
|
||||
function SelectField({ name, label, optionDefaultText, options }: props) {
|
||||
return (
|
||||
<div className="col-span-6">
|
||||
<Field
|
||||
name={name}
|
||||
type="select"
|
||||
render={({ input }) => (
|
||||
<Listbox value={input.value} onChange={input.onChange}>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Listbox.Label className="block text-xs font-bold text-gray-700 uppercase tracking-wide">
|
||||
{label}
|
||||
</Listbox.Label>
|
||||
<div className="mt-2 relative">
|
||||
<Listbox.Button className="bg-white relative w-full border border-gray-300 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
||||
<span className="block truncate">
|
||||
{input.value
|
||||
? options.find((c) => c.value === input.value)!.label
|
||||
: optionDefaultText}
|
||||
</span>
|
||||
<span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
||||
<SelectorIcon
|
||||
className="h-5 w-5 text-gray-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</span>
|
||||
</Listbox.Button>
|
||||
|
||||
<Transition
|
||||
show={open}
|
||||
as={Fragment}
|
||||
leave="transition ease-in duration-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Listbox.Options
|
||||
static
|
||||
className="absolute z-10 mt-1 w-full bg-white shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm"
|
||||
>
|
||||
{options.map((opt) => (
|
||||
<Listbox.Option
|
||||
key={opt.value}
|
||||
className={({ active }) =>
|
||||
classNames(
|
||||
active
|
||||
? "text-white bg-indigo-600"
|
||||
: "text-gray-900",
|
||||
"cursor-default select-none relative py-2 pl-3 pr-9"
|
||||
)
|
||||
}
|
||||
value={opt.value}
|
||||
>
|
||||
{({ selected, active }) => (
|
||||
<>
|
||||
<span
|
||||
className={classNames(
|
||||
selected ? "font-semibold" : "font-normal",
|
||||
"block truncate"
|
||||
)}
|
||||
>
|
||||
{opt.label}
|
||||
</span>
|
||||
|
||||
{selected ? (
|
||||
<span
|
||||
className={classNames(
|
||||
active ? "text-white" : "text-indigo-600",
|
||||
"absolute inset-y-0 right-0 flex items-center pr-4"
|
||||
)}
|
||||
>
|
||||
<CheckIcon
|
||||
className="h-5 w-5"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</span>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Listbox>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SelectField;
|
2
web/src/components/inputs/compact/index.ts
Normal file
2
web/src/components/inputs/compact/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export { default as NumberField } from "./NumberField";
|
||||
export { default as SelectField } from "./SelectField";
|
|
@ -5,4 +5,3 @@ export { default as TextAreaWide } from "./TextAreaWide";
|
|||
export { default as MultiSelectField } from "./MultiSelectField";
|
||||
export { default as RadioFieldset } from "./RadioFieldset";
|
||||
export { default as SwitchGroup } from "./SwitchGroup";
|
||||
export { default as SelectField } from "./SelectField";
|
||||
|
|
54
web/src/components/inputs/wide/NumberField.tsx
Normal file
54
web/src/components/inputs/wide/NumberField.tsx
Normal file
|
@ -0,0 +1,54 @@
|
|||
import { Field } from "react-final-form";
|
||||
import React from "react";
|
||||
import Error from "../Error";
|
||||
import { classNames } from "../../../styles/utils";
|
||||
|
||||
interface Props {
|
||||
name: string;
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
const NumberFieldWide: React.FC<Props> = ({
|
||||
name,
|
||||
label,
|
||||
placeholder,
|
||||
required,
|
||||
className,
|
||||
}) => (
|
||||
<div className="space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
|
||||
<div>
|
||||
<label
|
||||
htmlFor={name}
|
||||
className="block text-sm font-medium text-gray-900 sm:mt-px sm:pt-2"
|
||||
>
|
||||
{label} {required && <span className="text-gray-500">*</span>}
|
||||
</label>
|
||||
</div>
|
||||
<div className="sm:col-span-2">
|
||||
<Field
|
||||
name={name}
|
||||
parse={(v) => v & parseInt(v, 10)}
|
||||
render={({ input, meta }) => (
|
||||
<input
|
||||
{...input}
|
||||
id={name}
|
||||
type="number"
|
||||
className={classNames(
|
||||
meta.touched && meta.error
|
||||
? "focus:ring-red-500 focus:border-red-500 border-red-500"
|
||||
: "focus:ring-indigo-500 focus:border-indigo-500 border-gray-300",
|
||||
"block w-full shadow-sm sm:text-sm rounded-md"
|
||||
)}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Error name={name} classNames="block text-red-500 mt-2" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default NumberFieldWide;
|
105
web/src/components/inputs/wide/RadioFieldsetWide.tsx
Normal file
105
web/src/components/inputs/wide/RadioFieldsetWide.tsx
Normal file
|
@ -0,0 +1,105 @@
|
|||
import { Field, useFormState } from "react-final-form";
|
||||
import { RadioGroup } from "@headlessui/react";
|
||||
import { classNames } from "../../../styles/utils";
|
||||
import { Fragment } from "react";
|
||||
import { radioFieldsetOption } from "../RadioFieldset";
|
||||
|
||||
interface props {
|
||||
name: string;
|
||||
legend: string;
|
||||
options: radioFieldsetOption[];
|
||||
}
|
||||
|
||||
function RadioFieldsetWide({ name, legend, options }: props) {
|
||||
const { values } = useFormState();
|
||||
|
||||
return (
|
||||
<fieldset>
|
||||
<div className="space-y-2 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:px-6 sm:py-5">
|
||||
<div>
|
||||
<legend className="text-sm font-medium text-gray-900">
|
||||
{legend}
|
||||
</legend>
|
||||
</div>
|
||||
<div className="space-y-5 sm:col-span-2">
|
||||
<div className="space-y-5 sm:mt-0">
|
||||
<Field
|
||||
name={name}
|
||||
type="radio"
|
||||
render={({ input }) => (
|
||||
<RadioGroup value={values[name]} onChange={input.onChange}>
|
||||
<RadioGroup.Label className="sr-only">
|
||||
Privacy setting
|
||||
</RadioGroup.Label>
|
||||
<div className="bg-white rounded-md -space-y-px">
|
||||
{options.map((setting, settingIdx) => (
|
||||
<RadioGroup.Option
|
||||
key={setting.value}
|
||||
value={setting.value}
|
||||
className={({ checked }) =>
|
||||
classNames(
|
||||
settingIdx === 0
|
||||
? "rounded-tl-md rounded-tr-md"
|
||||
: "",
|
||||
settingIdx === options.length - 1
|
||||
? "rounded-bl-md rounded-br-md"
|
||||
: "",
|
||||
checked
|
||||
? "bg-indigo-50 border-indigo-200 z-10"
|
||||
: "border-gray-200",
|
||||
"relative border p-4 flex cursor-pointer focus:outline-none"
|
||||
)
|
||||
}
|
||||
>
|
||||
{({ active, checked }) => (
|
||||
<Fragment>
|
||||
<span
|
||||
className={classNames(
|
||||
checked
|
||||
? "bg-indigo-600 border-transparent"
|
||||
: "bg-white border-gray-300",
|
||||
active
|
||||
? "ring-2 ring-offset-2 ring-indigo-500"
|
||||
: "",
|
||||
"h-4 w-4 mt-0.5 cursor-pointer rounded-full border flex items-center justify-center"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<span className="rounded-full bg-white w-1.5 h-1.5" />
|
||||
</span>
|
||||
<div className="ml-3 flex flex-col">
|
||||
<RadioGroup.Label
|
||||
as="span"
|
||||
className={classNames(
|
||||
checked ? "text-indigo-900" : "text-gray-900",
|
||||
"block text-sm font-medium"
|
||||
)}
|
||||
>
|
||||
{setting.label}
|
||||
</RadioGroup.Label>
|
||||
<RadioGroup.Description
|
||||
as="span"
|
||||
className={classNames(
|
||||
checked ? "text-indigo-700" : "text-gray-500",
|
||||
"block text-sm"
|
||||
)}
|
||||
>
|
||||
{setting.description}
|
||||
</RadioGroup.Description>
|
||||
</div>
|
||||
</Fragment>
|
||||
)}
|
||||
</RadioGroup.Option>
|
||||
))}
|
||||
</div>
|
||||
</RadioGroup>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
);
|
||||
}
|
||||
|
||||
export default RadioFieldsetWide;
|
111
web/src/components/inputs/wide/SelectField.tsx
Normal file
111
web/src/components/inputs/wide/SelectField.tsx
Normal file
|
@ -0,0 +1,111 @@
|
|||
import { Field } from "react-final-form";
|
||||
import { Listbox, Transition } from "@headlessui/react";
|
||||
import { CheckIcon, SelectorIcon } from "@heroicons/react/solid";
|
||||
import React, { Fragment } from "react";
|
||||
import { classNames } from "../../../styles/utils";
|
||||
|
||||
interface SelectOption {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface props {
|
||||
name: string;
|
||||
label: string;
|
||||
optionDefaultText: string;
|
||||
options: SelectOption[];
|
||||
}
|
||||
|
||||
function SelectField({ name, label, optionDefaultText, options }: props) {
|
||||
return (
|
||||
<div className="col-span-6 sm:col-span-6">
|
||||
<Field
|
||||
name={name}
|
||||
type="select"
|
||||
render={({ input }) => (
|
||||
<Listbox value={input.value} onChange={input.onChange}>
|
||||
{({ open }) => (
|
||||
<div className="space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
|
||||
<Listbox.Label className="block text-sm font-medium text-gray-900 sm:mt-px sm:pt-2">
|
||||
{label}
|
||||
</Listbox.Label>
|
||||
<div className="mt-2 relative">
|
||||
<Listbox.Button className="bg-white relative w-full border border-gray-300 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
||||
<span className="block truncate">
|
||||
{input.value
|
||||
? options.find((c) => c.value === input.value)!.label
|
||||
: optionDefaultText}
|
||||
</span>
|
||||
<span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
||||
<SelectorIcon
|
||||
className="h-5 w-5 text-gray-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</span>
|
||||
</Listbox.Button>
|
||||
|
||||
<Transition
|
||||
show={open}
|
||||
as={Fragment}
|
||||
leave="transition ease-in duration-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Listbox.Options
|
||||
static
|
||||
className="absolute z-10 mt-1 w-full bg-white shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm"
|
||||
>
|
||||
{options.map((opt) => (
|
||||
<Listbox.Option
|
||||
key={opt.value}
|
||||
className={({ active }) =>
|
||||
classNames(
|
||||
active
|
||||
? "text-white bg-indigo-600"
|
||||
: "text-gray-900",
|
||||
"cursor-default select-none relative py-2 pl-3 pr-9"
|
||||
)
|
||||
}
|
||||
value={opt.value}
|
||||
>
|
||||
{({ selected, active }) => (
|
||||
<>
|
||||
<span
|
||||
className={classNames(
|
||||
selected ? "font-semibold" : "font-normal",
|
||||
"block truncate"
|
||||
)}
|
||||
>
|
||||
{opt.label}
|
||||
</span>
|
||||
|
||||
{selected ? (
|
||||
<span
|
||||
className={classNames(
|
||||
active ? "text-white" : "text-indigo-600",
|
||||
"absolute inset-y-0 right-0 flex items-center pr-4"
|
||||
)}
|
||||
>
|
||||
<CheckIcon
|
||||
className="h-5 w-5"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</span>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Listbox>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SelectField;
|
3
web/src/components/inputs/wide/index.ts
Normal file
3
web/src/components/inputs/wide/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export { default as NumberFieldWide } from "./NumberField";
|
||||
export { default as RadioFieldsetWide } from "./RadioFieldsetWide";
|
||||
export { default as SelectFieldWide } from "./SelectField";
|
Loading…
Add table
Add a link
Reference in a new issue