mirror of
https://github.com/idanoo/autobrr
synced 2025-07-23 08:49:13 +00:00
refactor: various frontend improvements (#101)
* Removed recoil and replaced it with react-ridge-state, a 0.4kb alternative. * Added AuthContext and SettingsContext persistent localStorage states. * Fixed tailwind.config.js incorrect key directive. See https://tailwindcss.com/docs/content-configuration#safelisting-classes. * Changed darkMode in Tailwind to "class" and started manually adjusting the theme according to the appropriate media query. * Added possibility of changing the theme manually via the Settings tab. * Changed Releases.tsx behavior to show the UI only when the HTTP request succeeded and there is some data (i.e. table is non-empty). * Changed the table color of screens/filters/list.tsx to a one notch lighter shade of gray for eye-comfort. * Replaced "User" in the header, with the users real username. * Made data version, commit and date fields optional in settings/Application.tsx. * Started working on a RegExp playground, which works fine, but JS won't cooperate and return the right match length. Either way, the RegExp must be implemented on backend and then must be communicated with the frontend. Otherwise a potential for incorrect results exists. * Removed Layout.tsx, since it was redundant. * Created a Checkbox component class for easier and consistent future use. * Rewritten App.tsx, Login.tsx, Logout.tsx to accomodate for new changes. * Fixed previous mistake regarding tailwind.config.js purge key, since we're still using old postcss7 from October last year * Removed package-lock.json from both root and web directories. * Refresh TypeScript configuration to support a types/ directory containing d.ts. The effect of this is that types don't have to be imported anymore and are at all times available globally. This also unifies them into a single source of truth, which will be a lot easier to manage in the future. Note: Only certain interop types have been moved at the time of writing. * Fixed minor Checkbox argument mistake. * fix: remove length from data check * chore: lock files are annoying * fix: select * fix: wip release filtering
This commit is contained in:
parent
53d75ef4d5
commit
20138030e1
40 changed files with 2596 additions and 2453 deletions
|
@ -8,13 +8,6 @@
|
|||
"@craco/craco": "^6.1.2",
|
||||
"@headlessui/react": "^1.2.0",
|
||||
"@heroicons/react": "^1.0.1",
|
||||
"@testing-library/jest-dom": "^5.11.4",
|
||||
"@testing-library/react": "^11.1.0",
|
||||
"@testing-library/user-event": "^12.1.10",
|
||||
"@types/jest": "^26.0.15",
|
||||
"@types/node": "^12.0.0",
|
||||
"@types/react": "^17.0.0",
|
||||
"@types/react-dom": "^17.0.0",
|
||||
"date-fns": "^2.25.0",
|
||||
"formik": "^2.2.9",
|
||||
"react": "^17.0.2",
|
||||
|
@ -23,12 +16,11 @@
|
|||
"react-hot-toast": "^2.1.1",
|
||||
"react-multi-select-component": "^4.0.2",
|
||||
"react-query": "^3.18.1",
|
||||
"react-ridge-state": "^4.2.2",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-scripts": "4.0.3",
|
||||
"react-select": "5.0.0-beta.0",
|
||||
"react-table": "^7.7.0",
|
||||
"recoil": "^0.4.0",
|
||||
"typescript": "^4.1.2",
|
||||
"web-vitals": "^1.0.1"
|
||||
},
|
||||
"scripts": {
|
||||
|
@ -56,11 +48,19 @@
|
|||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/forms": "^0.3.2",
|
||||
"@types/react-router-dom": "^5.1.7",
|
||||
"@types/react-table": "^7.7.7",
|
||||
"autoprefixer": "^9",
|
||||
"postcss": "^7",
|
||||
"tailwindcss": "npm:@tailwindcss/postcss7-compat"
|
||||
"typescript": "^4.1.2",
|
||||
"tailwindcss": "npm:@tailwindcss/postcss7-compat",
|
||||
"@tailwindcss/forms": "^0.3.2",
|
||||
"@testing-library/jest-dom": "^5.11.4",
|
||||
"@testing-library/react": "^11.1.0",
|
||||
"@testing-library/user-event": "^12.1.10",
|
||||
"@types/react-router-dom": "^5.1.7",
|
||||
"@types/react-table": "^7.7.7",
|
||||
"@types/jest": "^26.0.15",
|
||||
"@types/node": "^12.0.0",
|
||||
"@types/react": "^17.0.0",
|
||||
"@types/react-dom": "^17.0.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,31 +1,42 @@
|
|||
import { QueryClient, QueryClientProvider } from "react-query";
|
||||
import { BrowserRouter as Router, Route } from "react-router-dom";
|
||||
import { ReactQueryDevtools } from "react-query/devtools";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
|
||||
import Base from "./screens/Base";
|
||||
import Login from "./screens/auth/login";
|
||||
import Logout from "./screens/auth/logout";
|
||||
import Base from "./screens/Base";
|
||||
import { ReactQueryDevtools } from "react-query/devtools";
|
||||
import Layout from "./components/Layout";
|
||||
import { baseUrl } from "./utils";
|
||||
|
||||
import { AuthContext, SettingsContext } from "./utils/Context";
|
||||
|
||||
function Protected() {
|
||||
return (
|
||||
<Layout auth={true}>
|
||||
<>
|
||||
<Toaster position="top-right" />
|
||||
<Base />
|
||||
</Layout>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const queryClient = new QueryClient()
|
||||
export const queryClient = new QueryClient();
|
||||
|
||||
function App() {
|
||||
const authContext = AuthContext.useValue();
|
||||
const settings = SettingsContext.useValue();
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Router basename={baseUrl()}>
|
||||
<Route exact={true} path="/login" component={Login} />
|
||||
<Route exact={true} path="/logout" component={Logout} />
|
||||
<Route exact={true} path="/*" component={Protected} />
|
||||
{authContext.isLoggedIn ? (
|
||||
<Route exact path="/*" component={Protected} />
|
||||
) : (
|
||||
<Route exact path="/*" component={Login} />
|
||||
)}
|
||||
<Route exact path="/logout" component={Logout} />
|
||||
</Router>
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
{settings.debug ? (
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
) : null}
|
||||
</QueryClientProvider>
|
||||
)
|
||||
};
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import {Action, DownloadClient, Filter, Indexer, Network} from "../domain/interfaces";
|
||||
import {baseUrl, sseBaseUrl} from "../utils";
|
||||
|
||||
function baseClient(endpoint: string, method: string, { body, ...customConfig}: any = {}) {
|
||||
|
|
34
web/src/components/Checkbox.tsx
Normal file
34
web/src/components/Checkbox.tsx
Normal file
|
@ -0,0 +1,34 @@
|
|||
import { Switch } from "@headlessui/react";
|
||||
|
||||
interface CheckboxProps {
|
||||
value: boolean;
|
||||
setValue: (newValue: boolean) => void;
|
||||
label: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export const Checkbox = ({ label, description, value, setValue }: CheckboxProps) => (
|
||||
<Switch.Group as="li" className="py-4 flex items-center justify-between">
|
||||
<div className="flex flex-col">
|
||||
<Switch.Label as="p" className="text-sm font-medium text-gray-900 dark:text-white" passive>
|
||||
{label}
|
||||
</Switch.Label>
|
||||
{description === undefined ? null : (
|
||||
<Switch.Description className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{description}
|
||||
</Switch.Description>
|
||||
)}
|
||||
</div>
|
||||
<Switch
|
||||
checked={value}
|
||||
onChange={setValue}
|
||||
className={
|
||||
`${value ? 'bg-teal-500 dark:bg-blue-500' : 'bg-gray-200 dark:bg-gray-700'
|
||||
} ml-4 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500`}
|
||||
>
|
||||
<span
|
||||
className={`${value ? 'translate-x-5' : 'translate-x-0'} inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200`}
|
||||
/>
|
||||
</Switch>
|
||||
</Switch.Group>
|
||||
);
|
|
@ -1,40 +0,0 @@
|
|||
import {isLoggedIn} from "../state/state";
|
||||
import {useRecoilState} from "recoil";
|
||||
import {useEffect, useState} from "react";
|
||||
import { Fragment } from "react";
|
||||
import {Redirect} from "react-router-dom";
|
||||
import APIClient from "../api/APIClient";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
|
||||
export default function Layout({auth=false, authFallback="/login", children}: any) {
|
||||
const [loggedIn, setLoggedIn] = useRecoilState(isLoggedIn);
|
||||
const [loading, setLoading] = useState(auth);
|
||||
|
||||
useEffect(() => {
|
||||
// check token
|
||||
APIClient.auth.test()
|
||||
.then(r => {
|
||||
setLoggedIn(true);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(a => {
|
||||
setLoading(false);
|
||||
})
|
||||
|
||||
}, [setLoggedIn])
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{loading ? null : (
|
||||
<Fragment>
|
||||
{auth && !loggedIn ? <Redirect to={authFallback} /> : (
|
||||
<Fragment>
|
||||
<Toaster position="top-right" />
|
||||
{children}
|
||||
</Fragment>
|
||||
)}
|
||||
</Fragment>
|
||||
)}
|
||||
</Fragment>
|
||||
)
|
||||
}
|
|
@ -10,8 +10,8 @@ const DEBUG: FC<DebugProps> = ({ values }) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="w-1/2 mx-auto mt-2 flex flex-col mt-12 mb-12">
|
||||
<pre className="mt-2 dark:text-gray-500">{JSON.stringify(values, 0 as any, 2)}</pre>
|
||||
<div className="w-full p-2 flex flex-col mt-12 mb-12 bg-gray-100 dark:bg-gray-900">
|
||||
<pre className="dark:text-gray-400">{JSON.stringify(values, 0 as any, 2)}</pre>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -2,6 +2,6 @@ export { ErrorField, CheckboxField } from "./common";
|
|||
export { TextField, NumberField, PasswordField } from "./input";
|
||||
export { NumberFieldWide, PasswordFieldWide, SwitchGroupWide, TextFieldWide } from "./input_wide";
|
||||
export { RadioFieldsetWide } from "./radio";
|
||||
export { DownloadClientSelect, MultiSelect, Select} from "./select";
|
||||
export { MultiSelect, Select, SelectWide, DownloadClientSelect, IndexerMultiSelect} from "./select";
|
||||
export { SwitchGroup } from "./switch";
|
||||
|
||||
|
|
|
@ -2,7 +2,6 @@ import { Fragment } from "react";
|
|||
import { MultiSelect as RMSC} from "react-multi-select-component";
|
||||
import { Transition, Listbox } from "@headlessui/react";
|
||||
import { CheckIcon, SelectorIcon } from '@heroicons/react/solid';
|
||||
import { Action, DownloadClient } from "../../domain/interfaces";
|
||||
import { classNames, COL_WIDTHS } from "../../utils";
|
||||
import { Field } from "formik";
|
||||
|
||||
|
@ -56,6 +55,48 @@ const MultiSelect: React.FC<MultiSelectProps> = ({
|
|||
</div>
|
||||
);
|
||||
|
||||
const IndexerMultiSelect: React.FC<MultiSelectProps> = ({
|
||||
name,
|
||||
label,
|
||||
options,
|
||||
className,
|
||||
columns,
|
||||
}) => (
|
||||
<div
|
||||
className={classNames(
|
||||
columns ? `col-span-${columns}` : "col-span-12"
|
||||
)}
|
||||
>
|
||||
<label
|
||||
className="block mb-2 text-xs font-bold tracking-wide text-gray-700 uppercase dark:text-gray-200"
|
||||
htmlFor={label}
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
|
||||
<Field name={name} type="select" multiple={true}>
|
||||
{({
|
||||
field,
|
||||
form: { setFieldValue },
|
||||
}: any) => (
|
||||
<RMSC
|
||||
{...field}
|
||||
type="select"
|
||||
options={options}
|
||||
labelledBy={name}
|
||||
value={field.value && field.value.map((item: any) => options.find((o: any) => o.value?.id === item.id))}
|
||||
onChange={(values: any) => {
|
||||
let am = values && values.map((i: any) => i.value)
|
||||
|
||||
setFieldValue(field.name, am)
|
||||
}}
|
||||
className="dark:bg-gray-700 dark"
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
</div>
|
||||
);
|
||||
|
||||
interface DownloadClientSelectProps {
|
||||
name: string;
|
||||
action: Action;
|
||||
|
@ -268,4 +309,105 @@ function Select({ name, label, optionDefaultText, options }: SelectFieldProps) {
|
|||
);
|
||||
}
|
||||
|
||||
export { MultiSelect, DownloadClientSelect, Select }
|
||||
function SelectWide({ name, label, optionDefaultText, options }: SelectFieldProps) {
|
||||
return (
|
||||
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
||||
|
||||
<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">
|
||||
<Field name={name} type="select">
|
||||
{({
|
||||
field,
|
||||
form: { setFieldValue },
|
||||
}: any) => (
|
||||
<Listbox
|
||||
value={field.value}
|
||||
onChange={(value: any) => setFieldValue(field?.name, value)}
|
||||
>
|
||||
{({ open }) => (
|
||||
<div className="py-4 flex items-center justify-between">
|
||||
|
||||
<Listbox.Label className="block text-sm font-medium text-gray-900 dark:text-white">
|
||||
{label}
|
||||
</Listbox.Label>
|
||||
<div className="w-full">
|
||||
<Listbox.Button className="bg-white dark:bg-gray-800 relative w-full border border-gray-300 dark:border-gray-700 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 dark:text-gray-200 sm:text-sm">
|
||||
<span className="block truncate">
|
||||
{field.value
|
||||
? options.find((c) => c.value === field.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 dark:text-gray-300"
|
||||
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 dark:bg-gray-800 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 dark:text-gray-100 bg-indigo-600 dark:bg-gray-800"
|
||||
: "text-gray-900 dark:text-gray-300",
|
||||
"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 dark:text-gray-100" : "text-indigo-600 dark:text-gray-700",
|
||||
"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>
|
||||
)}
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { MultiSelect, Select, SelectWide, DownloadClientSelect, IndexerMultiSelect }
|
|
@ -1,5 +1,3 @@
|
|||
import {DOWNLOAD_CLIENT_TYPES} from "./interfaces";
|
||||
|
||||
export const resolutions = [
|
||||
"2160p",
|
||||
"1080p",
|
||||
|
@ -151,21 +149,46 @@ export const releaseTypeMusic = [
|
|||
|
||||
export const RELEASE_TYPE_MUSIC_OPTIONS = releaseTypeMusic.map(v => ({ value: v, label: v, key: v}));
|
||||
|
||||
export interface radioFieldsetOption {
|
||||
export interface RadioFieldsetOption {
|
||||
label: string;
|
||||
description: string;
|
||||
value: string;
|
||||
value: ActionType;
|
||||
}
|
||||
|
||||
export const DownloadClientTypeOptions: radioFieldsetOption[] = [
|
||||
{label: "qBittorrent", description: "Add torrents directly to qBittorrent", value: DOWNLOAD_CLIENT_TYPES.qBittorrent},
|
||||
{label: "Deluge", description: "Add torrents directly to Deluge", value: DOWNLOAD_CLIENT_TYPES.DelugeV1},
|
||||
{label: "Deluge 2", description: "Add torrents directly to Deluge 2", value: DOWNLOAD_CLIENT_TYPES.DelugeV2},
|
||||
{label: "Radarr", description: "Send to Radarr and let it decide", value: DOWNLOAD_CLIENT_TYPES.Radarr},
|
||||
{label: "Sonarr", description: "Send to Sonarr and let it decide", value: DOWNLOAD_CLIENT_TYPES.Sonarr},
|
||||
{label: "Lidarr", description: "Send to Lidarr and let it decide", value: DOWNLOAD_CLIENT_TYPES.Lidarr},
|
||||
export const DownloadClientTypeOptions: RadioFieldsetOption[] = [
|
||||
{
|
||||
label: "qBittorrent",
|
||||
description: "Add torrents directly to qBittorrent",
|
||||
value: "QBITTORRENT"
|
||||
},
|
||||
{
|
||||
label: "Deluge",
|
||||
description: "Add torrents directly to Deluge",
|
||||
value: "DELUGE_V1"
|
||||
},
|
||||
{
|
||||
label: "Deluge 2",
|
||||
description: "Add torrents directly to Deluge 2",
|
||||
value: "DELUGE_V2"
|
||||
},
|
||||
{
|
||||
label: "Radarr",
|
||||
description: "Send to Radarr and let it decide",
|
||||
value: "RADARR"
|
||||
},
|
||||
{
|
||||
label: "Sonarr",
|
||||
description: "Send to Sonarr and let it decide",
|
||||
value: "SONARR"
|
||||
},
|
||||
{
|
||||
label: "Lidarr",
|
||||
description: "Send to Lidarr and let it decide",
|
||||
value: "LIDARR"
|
||||
},
|
||||
];
|
||||
export const DownloadClientTypeNameMap = {
|
||||
|
||||
export const DownloadClientTypeNameMap: Record<DownloadClientType | string, string> = {
|
||||
"DELUGE_V1": "Deluge v1",
|
||||
"DELUGE_V2": "Deluge v2",
|
||||
"QBITTORRENT": "qBittorrent",
|
||||
|
@ -174,16 +197,16 @@ export const DownloadClientTypeNameMap = {
|
|||
"LIDARR": "Lidarr",
|
||||
};
|
||||
|
||||
export const ActionTypeOptions: radioFieldsetOption[] = [
|
||||
export const ActionTypeOptions: RadioFieldsetOption[] = [
|
||||
{label: "Test", description: "A simple action to test a filter.", value: "TEST"},
|
||||
{label: "Watch dir", description: "Add filtered torrents to a watch directory", value: "WATCH_FOLDER"},
|
||||
{label: "Exec", description: "Run a custom command after a filter match", value: "EXEC"},
|
||||
{label: "qBittorrent", description: "Add torrents directly to qBittorrent", value: "QBITTORRENT"},
|
||||
{label: "Deluge", description: "Add torrents directly to Deluge", value: "DELUGE_V1"},
|
||||
{label: "Deluge v2", description: "Add torrents directly to Deluge 2", value: "DELUGE_V2"},
|
||||
{label: "Radarr", description: "Send to Radarr and let it decide", value: DOWNLOAD_CLIENT_TYPES.Radarr},
|
||||
{label: "Sonarr", description: "Send to Sonarr and let it decide", value: DOWNLOAD_CLIENT_TYPES.Sonarr},
|
||||
{label: "Lidarr", description: "Send to Lidarr and let it decide", value: DOWNLOAD_CLIENT_TYPES.Lidarr},
|
||||
{label: "Radarr", description: "Send to Radarr and let it decide", value: "RADARR"},
|
||||
{label: "Sonarr", description: "Send to Sonarr and let it decide", value: "SONARR"},
|
||||
{label: "Lidarr", description: "Send to Lidarr and let it decide", value: "LIDARR"},
|
||||
];
|
||||
|
||||
export const ActionTypeNameMap = {
|
||||
|
@ -196,4 +219,4 @@ export const ActionTypeNameMap = {
|
|||
"RADARR": "Radarr",
|
||||
"SONARR": "Sonarr",
|
||||
"LIDARR": "Lidarr",
|
||||
};
|
||||
};
|
|
@ -1,224 +0,0 @@
|
|||
export interface APP {
|
||||
baseUrl: string;
|
||||
}
|
||||
|
||||
export interface Action {
|
||||
id: number;
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
type: ActionType;
|
||||
exec_cmd: string;
|
||||
exec_args: string;
|
||||
watch_folder: string;
|
||||
category: string;
|
||||
tags: string;
|
||||
label: string;
|
||||
save_path: string;
|
||||
paused: boolean;
|
||||
ignore_rules: boolean;
|
||||
limit_upload_speed: number;
|
||||
limit_download_speed: number;
|
||||
client_id: number;
|
||||
filter_id: number;
|
||||
// settings: object;
|
||||
}
|
||||
|
||||
export interface Indexer {
|
||||
id: number;
|
||||
name: string;
|
||||
identifier: string;
|
||||
enabled: boolean;
|
||||
settings: object | any;
|
||||
}
|
||||
|
||||
export interface IndexerSchema {
|
||||
// id: number;
|
||||
name: string;
|
||||
identifier: string;
|
||||
description: string;
|
||||
language: string;
|
||||
privacy: string;
|
||||
protocol: string;
|
||||
urls: string[];
|
||||
settings: IndexerSchemaSettings[];
|
||||
irc: IndexerSchemaIRC;
|
||||
}
|
||||
|
||||
|
||||
export interface IndexerSchemaSettings {
|
||||
name: string;
|
||||
type: string;
|
||||
required: boolean;
|
||||
label: string;
|
||||
help: string;
|
||||
description: string;
|
||||
default: string;
|
||||
}
|
||||
|
||||
export interface IndexerSchemaIRC {
|
||||
network: string;
|
||||
server: string;
|
||||
port: number;
|
||||
tls: boolean;
|
||||
nickserv: boolean;
|
||||
announcers: string[];
|
||||
channels: string[];
|
||||
invite: string[];
|
||||
invite_command: string;
|
||||
settings: IndexerSchemaSettings[];
|
||||
}
|
||||
|
||||
export interface Filter {
|
||||
id: number;
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
shows: string;
|
||||
min_size: string;
|
||||
max_size: string;
|
||||
match_sites: string[];
|
||||
except_sites: string[];
|
||||
delay: number;
|
||||
years: string;
|
||||
resolutions: string[];
|
||||
sources: string[];
|
||||
codecs: string[];
|
||||
containers: string[];
|
||||
match_release_types: string[];
|
||||
quality: string[];
|
||||
formats: string[];
|
||||
match_hdr: string[];
|
||||
except_hdr: string[];
|
||||
log_score: number;
|
||||
log: boolean;
|
||||
cue: boolean;
|
||||
perfect_flac: boolean;
|
||||
artists: string;
|
||||
albums: string;
|
||||
seasons: string;
|
||||
episodes: string;
|
||||
match_releases: string;
|
||||
except_releases: string;
|
||||
match_release_groups: string;
|
||||
except_release_groups: string;
|
||||
match_categories: string;
|
||||
except_categories: string;
|
||||
tags: string;
|
||||
except_tags: string;
|
||||
match_uploaders: string;
|
||||
except_uploaders: string;
|
||||
freeleech: boolean;
|
||||
freeleech_percent: string;
|
||||
actions: Action[];
|
||||
indexers: Indexer[];
|
||||
}
|
||||
|
||||
export type ActionType = 'TEST' | 'EXEC' | 'WATCH_FOLDER' | 'QBITTORRENT' | 'DELUGE_V1' | 'DELUGE_V2' | 'RADARR' | 'SONARR' | 'LIDARR';
|
||||
export const ACTIONTYPES: ActionType[] = ['TEST', 'EXEC' , 'WATCH_FOLDER' , 'QBITTORRENT' , 'DELUGE_V1', 'DELUGE_V2', 'RADARR', 'SONARR', 'LIDARR'];
|
||||
|
||||
|
||||
export type DownloadClientType = 'QBITTORRENT' | 'DELUGE_V1' | 'DELUGE_V2' | 'RADARR' | 'SONARR' | 'LIDARR';
|
||||
|
||||
export enum DOWNLOAD_CLIENT_TYPES {
|
||||
qBittorrent = 'QBITTORRENT',
|
||||
DelugeV1 = 'DELUGE_V1',
|
||||
DelugeV2 = 'DELUGE_V2',
|
||||
Radarr = 'RADARR',
|
||||
Sonarr = 'SONARR',
|
||||
Lidarr = 'LIDARR'
|
||||
}
|
||||
|
||||
export interface DownloadClient {
|
||||
id?: number;
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
host: string;
|
||||
port: number;
|
||||
ssl: boolean;
|
||||
username: string;
|
||||
password: string;
|
||||
type: DownloadClientType;
|
||||
settings: object;
|
||||
}
|
||||
|
||||
export interface NickServ {
|
||||
account: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface Network {
|
||||
id?: number;
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
server: string;
|
||||
port: number;
|
||||
tls: boolean;
|
||||
invite_command: string;
|
||||
nickserv: {
|
||||
account: string;
|
||||
password: string;
|
||||
}
|
||||
channels: Channel[];
|
||||
settings: object;
|
||||
}
|
||||
|
||||
export interface Channel {
|
||||
name: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface SASL {
|
||||
mechanism: string;
|
||||
plain: {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
}
|
||||
|
||||
export interface Config {
|
||||
host: string;
|
||||
port: number;
|
||||
log_level: string;
|
||||
log_path: string;
|
||||
base_url: string;
|
||||
version: string;
|
||||
commit: string;
|
||||
date: string;
|
||||
}
|
||||
|
||||
export interface Release {
|
||||
id: number;
|
||||
filter_status: string;
|
||||
rejections: string[];
|
||||
indexer: string;
|
||||
filter: string;
|
||||
protocol: string;
|
||||
title: string;
|
||||
size: number;
|
||||
raw: string;
|
||||
timestamp: Date
|
||||
action_status: ReleaseActionStatus[]
|
||||
}
|
||||
|
||||
export interface ReleaseActionStatus {
|
||||
id: number;
|
||||
status: string;
|
||||
action: string;
|
||||
type: string;
|
||||
rejections: string[];
|
||||
timestamp: Date
|
||||
// raw: string;
|
||||
}
|
||||
|
||||
export interface ReleaseFindResponse {
|
||||
data: Release[];
|
||||
next_cursor: number;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface ReleaseStats {
|
||||
total_count: number;
|
||||
filtered_count: number;
|
||||
filter_rejected_count: number;
|
||||
push_approved_count: number;
|
||||
push_rejected_count: number;
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
import { Fragment, useEffect } from "react";
|
||||
import { useMutation } from "react-query";
|
||||
import { Filter } from "../../domain/interfaces";
|
||||
import { queryClient } from "../../App";
|
||||
import { XIcon } from "@heroicons/react/solid";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
|
|
|
@ -1,13 +1,8 @@
|
|||
import { Fragment, useRef, useState } from "react";
|
||||
import { useMutation } from "react-query";
|
||||
import {
|
||||
DOWNLOAD_CLIENT_TYPES,
|
||||
DownloadClient,
|
||||
} from "../../domain/interfaces";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { XIcon } from "@heroicons/react/solid";
|
||||
import { sleep, classNames } from "../../utils";
|
||||
|
||||
import { Form, Formik, useFormikContext } from "formik";
|
||||
import DEBUG from "../../components/debug";
|
||||
import { queryClient } from "../../App";
|
||||
|
@ -37,7 +32,7 @@ interface InitialValuesSettings {
|
|||
|
||||
interface InitialValues {
|
||||
name: string;
|
||||
type: DOWNLOAD_CLIENT_TYPES;
|
||||
type: DownloadClientType;
|
||||
enabled: boolean;
|
||||
host: string;
|
||||
port: number;
|
||||
|
@ -326,7 +321,7 @@ export function DownloadClientAddForm({ isOpen, toggle }: any) {
|
|||
|
||||
let initialValues: InitialValues = {
|
||||
name: "",
|
||||
type: DOWNLOAD_CLIENT_TYPES.qBittorrent,
|
||||
type: "QBITTORRENT",
|
||||
enabled: true,
|
||||
host: "",
|
||||
port: 10000,
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import { Fragment } from "react";
|
||||
import { useMutation, useQuery } from "react-query";
|
||||
import { Channel, Indexer, IndexerSchema, IndexerSchemaSettings, Network } from "../../domain/interfaces";
|
||||
import { sleep } from "../../utils";
|
||||
import { XIcon } from "@heroicons/react/solid";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { Field, FieldProps, Form, Formik } from "formik";
|
||||
import DEBUG from "../../components/debug";
|
||||
import Select, { components, InputProps } from "react-select";
|
||||
import Select, { components } from "react-select";
|
||||
import { queryClient } from "../../App";
|
||||
import APIClient from "../../api/APIClient";
|
||||
import { TextFieldWide, PasswordFieldWide, SwitchGroupWide } from "../../components/inputs/input_wide";
|
||||
|
@ -15,7 +14,33 @@ import { toast } from 'react-hot-toast'
|
|||
import Toast from '../../components/notifications/Toast';
|
||||
import { SlideOver } from "../../components/panels";
|
||||
|
||||
const Input = ({ type, ...rest }: InputProps) => <components.Input {...rest} />;
|
||||
const Input = (props: any) => {
|
||||
return (
|
||||
<components.Input
|
||||
{...props}
|
||||
inputClassName="outline-none border-none shadow-none focus:ring-transparent"
|
||||
className="text-gray-400 dark:text-gray-100"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const Control = (props: any) => {
|
||||
return (
|
||||
<components.Control
|
||||
{...props}
|
||||
className="block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:text-gray-100 sm:text-sm"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const Menu = (props: any) => {
|
||||
return (
|
||||
<components.Menu
|
||||
{...props}
|
||||
className="dark:bg-gray-800 border border-gray-300 dark:border-gray-700 dark:text-gray-400 hover:bg-red-800 rounded-md shadow-sm"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface AddProps {
|
||||
isOpen: boolean;
|
||||
|
@ -177,11 +202,9 @@ export function IndexerAddForm({ isOpen, toggle }: AddProps) {
|
|||
irc: {},
|
||||
settings: {},
|
||||
}}
|
||||
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
{({ values }) => {
|
||||
return (
|
||||
{({ values }) => (
|
||||
<Form className="h-full flex flex-col bg-white dark:bg-gray-800 shadow-xl overflow-y-scroll">
|
||||
<div className="flex-1">
|
||||
<div className="px-4 py-6 bg-gray-50 dark:bg-gray-900 sm:px-6">
|
||||
|
@ -207,12 +230,12 @@ export function IndexerAddForm({ isOpen, toggle }: AddProps) {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className="py-6 space-y-6 py-0 space-y-0 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<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 className="py-6 space-y-6 space-y-0 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<div className="py-4 flex items-center justify-between 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="identifier"
|
||||
className="block text-sm font-medium text-gray-900 dark:text-white sm:mt-px sm:pt-2"
|
||||
className="block text-sm font-medium text-gray-900 dark:text-white"
|
||||
>
|
||||
Indexer
|
||||
</label>
|
||||
|
@ -223,7 +246,7 @@ export function IndexerAddForm({ isOpen, toggle }: AddProps) {
|
|||
<Select {...field}
|
||||
isClearable={true}
|
||||
isSearchable={true}
|
||||
components={{ Input }}
|
||||
components={{ Input, Control, Menu }}
|
||||
placeholder="Choose an indexer"
|
||||
value={field?.value && field.value.value}
|
||||
onChange={(option: any) => {
|
||||
|
@ -233,7 +256,8 @@ export function IndexerAddForm({ isOpen, toggle }: AddProps) {
|
|||
options={data && data.sort((a, b): any => a.name.localeCompare(b.name)).map(v => ({
|
||||
label: v.name,
|
||||
value: v.identifier
|
||||
}))} />
|
||||
}))}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
|
||||
|
@ -273,8 +297,7 @@ export function IndexerAddForm({ isOpen, toggle }: AddProps) {
|
|||
|
||||
<DEBUG values={values} />
|
||||
</Form>
|
||||
)
|
||||
}}
|
||||
)}
|
||||
</Formik>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { useMutation } from "react-query";
|
||||
import { Channel, Network } from "../../domain/interfaces";
|
||||
import { XIcon } from "@heroicons/react/solid";
|
||||
import { queryClient } from "../../App";
|
||||
|
||||
|
|
|
@ -2,21 +2,22 @@ import React from 'react';
|
|||
import ReactDOM from 'react-dom';
|
||||
import './index.css';
|
||||
|
||||
import {RecoilRoot} from 'recoil';
|
||||
import {APP} from "./domain/interfaces";
|
||||
import App from "./App";
|
||||
|
||||
import { InitializeGlobalContext } from "./utils/Context";
|
||||
|
||||
declare global {
|
||||
interface Window { APP: APP; }
|
||||
}
|
||||
|
||||
window.APP = window.APP || {};
|
||||
|
||||
// Initializes auth and theme contexts
|
||||
InitializeGlobalContext();
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<RecoilRoot>
|
||||
<App />
|
||||
</RecoilRoot>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root')
|
||||
document.getElementById("root")
|
||||
);
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
import { Fragment } from 'react'
|
||||
import { Disclosure, Menu, Transition } from '@headlessui/react'
|
||||
import { ChevronDownIcon, MenuIcon, XIcon } from '@heroicons/react/outline'
|
||||
import { Fragment } from "react";
|
||||
import { NavLink, Link, Route, Switch } from "react-router-dom";
|
||||
import { Disclosure, Menu, Transition } from "@headlessui/react";
|
||||
import { ChevronDownIcon, MenuIcon, XIcon } from "@heroicons/react/outline";
|
||||
|
||||
import Logs from "./Logs";
|
||||
import Settings from "./Settings";
|
||||
|
||||
import { Releases } from "./Releases";
|
||||
import { Dashboard } from "./Dashboard";
|
||||
import { FilterDetails, Filters } from "./filters";
|
||||
import Logs from './Logs';
|
||||
import { Releases } from "./Releases";
|
||||
import { AuthContext } from '../utils/Context';
|
||||
|
||||
import logo from '../logo.png';
|
||||
|
||||
function classNames(...classes: string[]) {
|
||||
|
@ -14,10 +18,17 @@ function classNames(...classes: string[]) {
|
|||
}
|
||||
|
||||
export default function Base() {
|
||||
const nav = [{ name: 'Dashboard', path: "/" }, { name: 'Filters', path: "/filters" }, { name: 'Releases', path: "/releases" }, { name: "Settings", path: "/settings" }, { name: "Logs", path: "/logs" }]
|
||||
const authContext = AuthContext.useValue();
|
||||
const nav = [
|
||||
{ name: 'Dashboard', path: "/" },
|
||||
{ name: 'Filters', path: "/filters" },
|
||||
{ name: 'Releases', path: "/releases" },
|
||||
{ name: "Settings", path: "/settings" },
|
||||
{ name: "Logs", path: "/logs" }
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
<div>
|
||||
<Disclosure as="nav" className="bg-gray-900 pb-48">
|
||||
{({ open }) => (
|
||||
<>
|
||||
|
@ -49,12 +60,6 @@ export default function Base() {
|
|||
return true
|
||||
}
|
||||
|
||||
// if (item.path ==="/" && location.pathname ==="/") {
|
||||
// console.log("match base");
|
||||
|
||||
// return true
|
||||
// }
|
||||
|
||||
if (!match) {
|
||||
return false;
|
||||
}
|
||||
|
@ -86,7 +91,7 @@ export default function Base() {
|
|||
className="max-w-xs rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-800 focus:ring-white">
|
||||
<span
|
||||
className="hidden text-gray-300 text-sm font-medium sm:block">
|
||||
<span className="sr-only">Open user menu for </span>User
|
||||
<span className="sr-only">Open user menu for </span>{authContext.username}
|
||||
</span>
|
||||
<ChevronDownIcon
|
||||
className="hidden flex-shrink-0 ml-1 h-5 w-5 text-gray-400 sm:block"
|
||||
|
@ -111,7 +116,7 @@ export default function Base() {
|
|||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<Link
|
||||
to="settings"
|
||||
to="/settings"
|
||||
className={classNames(
|
||||
active ? 'bg-gray-100 dark:bg-gray-600' : '',
|
||||
'block px-4 py-2 text-sm text-gray-700 dark:text-gray-200'
|
||||
|
@ -186,7 +191,7 @@ export default function Base() {
|
|||
</div>
|
||||
<div className="mt-3 px-2 space-y-1">
|
||||
<Link
|
||||
to="settings"
|
||||
to="/settings"
|
||||
className="block px-3 py-2 rounded-md text-base font-medium text-gray-400 hover:text-white hover:bg-gray-700"
|
||||
>
|
||||
Settings
|
||||
|
|
|
@ -4,7 +4,6 @@ import App from '../App'
|
|||
import { useTable, useFilters, useGlobalFilter, useSortBy, usePagination } from 'react-table'
|
||||
import APIClient from '../api/APIClient'
|
||||
import { useQuery } from 'react-query'
|
||||
import { ReleaseFindResponse, ReleaseStats } from '../domain/interfaces'
|
||||
import { EmptyListState } from '../components/emptystates'
|
||||
import { ReleaseStatusCell } from './Releases'
|
||||
|
||||
|
|
|
@ -7,7 +7,6 @@ import { useQuery } from "react-query"
|
|||
import { useTable, useSortBy, usePagination } from "react-table"
|
||||
import APIClient from "../api/APIClient"
|
||||
import { EmptyListState } from "../components/emptystates"
|
||||
import { ReleaseActionStatus } from "../domain/interfaces"
|
||||
import { classNames } from "../utils"
|
||||
|
||||
export function Releases() {
|
||||
|
@ -294,24 +293,22 @@ function Table() {
|
|||
// Render the UI for your table
|
||||
return (
|
||||
<>
|
||||
<div className="sm:flex sm:gap-x-2">
|
||||
{/* <GlobalFilter
|
||||
preGlobalFilteredRows={preGlobalFilteredRows}
|
||||
globalFilter={state.globalFilter}
|
||||
setGlobalFilter={setGlobalFilter}
|
||||
/> */}
|
||||
{/* {headerGroups.map((headerGroup: { headers: any[] }) =>
|
||||
headerGroup.headers.map((column) =>
|
||||
column.Filter ? (
|
||||
<div className="mt-2 sm:mt-0" key={column.id}>
|
||||
{column.render("Filter")}
|
||||
</div>
|
||||
) : null
|
||||
)
|
||||
)} */}
|
||||
</div>
|
||||
{isSuccess ?
|
||||
{isSuccess && data ? (
|
||||
<div className="flex flex-col mt-4">
|
||||
{/* <GlobalFilter
|
||||
preGlobalFilteredRows={preGlobalFilteredRows}
|
||||
globalFilter={state.globalFilter}
|
||||
setGlobalFilter={setGlobalFilter}
|
||||
/> */}
|
||||
{/* {headerGroups.map((headerGroup: { headers: any[] }) =>
|
||||
headerGroup.headers.map((column) =>
|
||||
column.Filter ? (
|
||||
<div className="mt-2 sm:mt-0" key={column.id}>
|
||||
{column.render("Filter")}
|
||||
</div>
|
||||
) : null
|
||||
)
|
||||
)} */}
|
||||
<div className="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
||||
<div className="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
|
||||
<div className="overflow-hidden bg-white shadow dark:bg-gray-800 sm:rounded-lg">
|
||||
|
@ -373,7 +370,6 @@ function Table() {
|
|||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
{/* Pagination */}
|
||||
<div className="flex items-center justify-between px-6 py-3 border-t border-gray-200 dark:border-gray-700">
|
||||
<div className="flex justify-between flex-1 sm:hidden">
|
||||
|
@ -421,8 +417,7 @@ function Table() {
|
|||
</PageButton>
|
||||
<PageButton
|
||||
onClick={() => nextPage()}
|
||||
disabled={!canNextPage
|
||||
}>
|
||||
disabled={!canNextPage}>
|
||||
<span className="sr-only">Next</span>
|
||||
<ChevronRightIcon className="w-5 h-5 text-gray-400" aria-hidden="true" />
|
||||
</PageButton>
|
||||
|
@ -438,13 +433,11 @@ function Table() {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
: <EmptyListState text="No recent activity" />}
|
||||
) : <EmptyListState text="No recent activity" />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,17 +1,19 @@
|
|||
import {CogIcon, DownloadIcon, KeyIcon} from '@heroicons/react/outline'
|
||||
import {NavLink, Route, Switch as RouteSwitch, useLocation, useRouteMatch} from "react-router-dom";
|
||||
|
||||
import IndexerSettings from "./settings/Indexer";
|
||||
import IrcSettings from "./settings/Irc";
|
||||
import ApplicationSettings from "./settings/Application";
|
||||
import DownloadClientSettings from "./settings/DownloadClient";
|
||||
import {classNames} from "../utils";
|
||||
import ActionSettings from "./settings/Action";
|
||||
import { RegexPlayground } from './settings/RegexPlayground';
|
||||
|
||||
const subNavigation = [
|
||||
{name: 'Application', href: '', icon: CogIcon, current: true},
|
||||
{name: 'Indexers', href: 'indexers', icon: KeyIcon, current: false},
|
||||
{name: 'IRC', href: 'irc', icon: KeyIcon, current: false},
|
||||
{name: 'Clients', href: 'clients', icon: DownloadIcon, current: false},
|
||||
// {name: 'Regex Playground', href: 'regex-playground', icon: CogIcon, current: false}
|
||||
// {name: 'Actions', href: 'actions', icon: PlayIcon, current: false},
|
||||
// {name: 'Rules', href: 'rules', icon: ClipboardCheckIcon, current: false},
|
||||
// {name: 'Notifications', href: 'notifications', icon: BellIcon, current: false},
|
||||
|
@ -97,10 +99,13 @@ export default function Settings() {
|
|||
<DownloadClientSettings/>
|
||||
</Route>
|
||||
|
||||
<Route path={`${url}/actions`}>
|
||||
{/*<Route path={`${url}/actions`}>
|
||||
<ActionSettings/>
|
||||
</Route>
|
||||
</Route>*/}
|
||||
|
||||
<Route path={`${url}/regex-playground`}>
|
||||
<RegexPlayground />
|
||||
</Route>
|
||||
</RouteSwitch>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,40 +1,36 @@
|
|||
import { useMutation } from "react-query";
|
||||
import APIClient from "../../api/APIClient";
|
||||
import { Form, Formik } from "formik";
|
||||
import { useRecoilState } from "recoil";
|
||||
import { isLoggedIn } from "../../state/state";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { useEffect } from "react";
|
||||
import logo from "../../logo.png"
|
||||
import { useMutation } from "react-query";
|
||||
import { Form, Formik } from "formik";
|
||||
|
||||
import APIClient from "../../api/APIClient";
|
||||
import { TextField, PasswordField } from "../../components/inputs";
|
||||
|
||||
interface loginData {
|
||||
import logo from "../../logo.png";
|
||||
import { AuthContext } from "../../utils/Context";
|
||||
|
||||
interface LoginData {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
function Login() {
|
||||
const [loggedIn, setLoggedIn] = useRecoilState(isLoggedIn);
|
||||
let history = useHistory();
|
||||
const history = useHistory();
|
||||
const [, setAuthContext] = AuthContext.use();
|
||||
|
||||
useEffect(() => {
|
||||
if (loggedIn) {
|
||||
// setLoading(false);
|
||||
history.push('/');
|
||||
} else {
|
||||
// setLoading(false);
|
||||
const mutation = useMutation(
|
||||
(data: LoginData) => APIClient.auth.login(data.username, data.password),
|
||||
{
|
||||
onSuccess: (_, variables: LoginData) => {
|
||||
setAuthContext({
|
||||
username: variables.username,
|
||||
isLoggedIn: true
|
||||
});
|
||||
history.push("/");
|
||||
},
|
||||
}
|
||||
}, [loggedIn, history])
|
||||
);
|
||||
|
||||
const mutation = useMutation((data: loginData) => APIClient.auth.login(data.username, data.password), {
|
||||
onSuccess: () => {
|
||||
setLoggedIn(true);
|
||||
},
|
||||
})
|
||||
|
||||
const handleSubmit = (data: any) => {
|
||||
mutation.mutate(data)
|
||||
}
|
||||
const handleSubmit = (data: any) => mutation.mutate(data);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
|
||||
|
@ -45,27 +41,19 @@ function Login() {
|
|||
alt="logo"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="sm:mx-auto sm:w-full sm:max-w-md">
|
||||
<div className="bg-white dark:bg-gray-800 py-8 px-4 shadow sm:rounded-lg sm:px-10">
|
||||
|
||||
<Formik
|
||||
initialValues={{
|
||||
username: "",
|
||||
password: "",
|
||||
}}
|
||||
initialValues={{ username: "", password: "" }}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
{() => (
|
||||
<Form>
|
||||
|
||||
<div className="space-y-6">
|
||||
|
||||
<TextField name="username" label="Username" columns={6} autoComplete="username" />
|
||||
<PasswordField name="password" label="Password" columns={6} autoComplete="current-password" />
|
||||
</div>
|
||||
|
||||
|
||||
<div className="mt-6">
|
||||
<button
|
||||
type="submit"
|
||||
|
|
|
@ -1,23 +1,26 @@
|
|||
import APIClient from "../../api/APIClient";
|
||||
import {useRecoilState} from "recoil";
|
||||
import {isLoggedIn} from "../../state/state";
|
||||
import {useEffect} from "react";
|
||||
import {useCookies} from "react-cookie";
|
||||
import {useHistory} from "react-router-dom";
|
||||
|
||||
function Logout() {
|
||||
const [loggedIn, setLoggedIn] = useRecoilState(isLoggedIn);
|
||||
let history = useHistory();
|
||||
import APIClient from "../../api/APIClient";
|
||||
import { AuthContext } from "../../utils/Context";
|
||||
|
||||
function Logout() {
|
||||
const history = useHistory();
|
||||
|
||||
const [, setAuthContext] = AuthContext.use();
|
||||
const [,, removeCookie] = useCookies(['user_session']);
|
||||
|
||||
useEffect(() => {
|
||||
APIClient.auth.logout().then(r => {
|
||||
removeCookie("user_session")
|
||||
setLoggedIn(false);
|
||||
history.push('/login');
|
||||
})
|
||||
}, [loggedIn, history, removeCookie, setLoggedIn])
|
||||
useEffect(
|
||||
() => {
|
||||
APIClient.auth.logout().then(r => {
|
||||
setAuthContext({ username: "", isLoggedIn: false });
|
||||
removeCookie("user_session");
|
||||
history.push('/login');
|
||||
})
|
||||
},
|
||||
[history, removeCookie, setAuthContext]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-800 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
|
||||
|
|
|
@ -12,7 +12,6 @@ import {
|
|||
useParams,
|
||||
useRouteMatch
|
||||
} from "react-router-dom";
|
||||
import { Action, ActionType, DownloadClient, Filter, Indexer } from "../../domain/interfaces";
|
||||
import { useToggle } from "../../hooks/hooks";
|
||||
import { useMutation, useQuery } from "react-query";
|
||||
import { queryClient } from "../../App";
|
||||
|
@ -21,7 +20,6 @@ import { CONTAINER_OPTIONS, CODECS_OPTIONS, RESOLUTION_OPTIONS, SOURCES_OPTIONS,
|
|||
import DEBUG from "../../components/debug";
|
||||
import { TitleSubtitle } from "../../components/headings";
|
||||
import { buildPath, classNames } from "../../utils";
|
||||
import SelectM from "react-select";
|
||||
import APIClient from "../../api/APIClient";
|
||||
|
||||
import { toast } from 'react-hot-toast'
|
||||
|
@ -30,7 +28,7 @@ import Toast from '../../components/notifications/Toast';
|
|||
import { Field, FieldArray, Form, Formik } from "formik";
|
||||
import { AlertWarning } from "../../components/alerts";
|
||||
import { DeleteModal } from "../../components/modals";
|
||||
import { NumberField, TextField, SwitchGroup, Select, MultiSelect, DownloadClientSelect, CheckboxField } from "../../components/inputs";
|
||||
import { NumberField, TextField, SwitchGroup, Select, MultiSelect, DownloadClientSelect, IndexerMultiSelect, CheckboxField } from "../../components/inputs";
|
||||
|
||||
const tabs = [
|
||||
{ name: 'General', href: '', current: true },
|
||||
|
@ -312,7 +310,12 @@ function General({ indexers }: GeneralProps) {
|
|||
|
||||
let opts = indexers ? indexers.map(v => ({
|
||||
label: v.name,
|
||||
value: v
|
||||
value: {
|
||||
id: v.id,
|
||||
name: v.name,
|
||||
identifier: v.identifier,
|
||||
enabled: v.enabled
|
||||
}
|
||||
})) : [];
|
||||
|
||||
return (
|
||||
|
@ -323,36 +326,7 @@ function General({ indexers }: GeneralProps) {
|
|||
<TextField name="name" label="Filter name" columns={6} placeholder="eg. Filter 1" />
|
||||
|
||||
<div className="col-span-6">
|
||||
<label htmlFor="indexers" className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
|
||||
Indexers
|
||||
</label>
|
||||
|
||||
<Field name="indexers" type="select" multiple={true}>
|
||||
{({
|
||||
field,
|
||||
form: { setFieldValue },
|
||||
}: any) => {
|
||||
return (
|
||||
<SelectM
|
||||
{...field}
|
||||
value={field.value && field.value.map((v: any) => ({
|
||||
label: v.name,
|
||||
value: v
|
||||
}))}
|
||||
onChange={(values: any) => {
|
||||
let am = values && values.map((i: any) => i.value)
|
||||
|
||||
setFieldValue(field.name, am)
|
||||
}}
|
||||
isClearable={true}
|
||||
isMulti={true}
|
||||
placeholder="Choose indexers"
|
||||
className="mt-2 block w-full focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||
options={opts}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
</Field>
|
||||
<IndexerMultiSelect name="indexers" options={opts} label="Indexers" columns={6} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -375,12 +349,7 @@ function General({ indexers }: GeneralProps) {
|
|||
);
|
||||
}
|
||||
|
||||
// interface FilterTabGeneralProps {
|
||||
// filter: Filter;
|
||||
// }
|
||||
|
||||
function MoviesTv() {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mt-6 grid grid-cols-12 gap-6">
|
||||
|
@ -480,7 +449,7 @@ function Advanced() {
|
|||
|
||||
return (
|
||||
<div>
|
||||
<div className="mt-6 lg:pb-8 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="mt-6 lg:pb-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex justify-between items-center cursor-pointer" onClick={toggleReleases}>
|
||||
<div className="-ml-2 -mt-2 flex flex-wrap items-baseline">
|
||||
<h3 className="ml-2 mt-2 text-lg leading-6 font-medium text-gray-900 dark:text-gray-200">Releases</h3>
|
||||
|
@ -503,7 +472,7 @@ function Advanced() {
|
|||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-6 lg:pb-8 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="mt-6 lg:pb-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex justify-between items-center cursor-pointer" onClick={toggleGroups}>
|
||||
<div className="-ml-2 -mt-2 flex flex-wrap items-baseline">
|
||||
<h3 className="ml-2 mt-2 text-lg leading-6 font-medium text-gray-900 dark:text-gray-200">Groups</h3>
|
||||
|
@ -526,7 +495,7 @@ function Advanced() {
|
|||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-6 lg:pb-8 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="mt-6 lg:pb-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex justify-between items-center cursor-pointer" onClick={toggleCategories}>
|
||||
<div className="-ml-2 -mt-2 flex flex-wrap items-baseline">
|
||||
<h3 className="ml-2 mt-2 text-lg leading-6 font-medium text-gray-900 dark:text-gray-200">Categories and tags</h3>
|
||||
|
@ -552,7 +521,7 @@ function Advanced() {
|
|||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-6 lg:pb-8 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="mt-6 lg:pb-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex justify-between items-center cursor-pointer" onClick={toggleUploaders}>
|
||||
<div className="-ml-2 -mt-2 flex flex-wrap items-baseline">
|
||||
<h3 className="ml-2 mt-2 text-lg leading-6 font-medium text-gray-900 dark:text-gray-200">Uploaders</h3>
|
||||
|
@ -575,7 +544,7 @@ function Advanced() {
|
|||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-6 lg:pb-8 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="mt-6 lg:pb-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex justify-between items-center cursor-pointer" onClick={toggleFreeleech}>
|
||||
<div className="-ml-2 -mt-2 flex flex-wrap items-baseline">
|
||||
<h3 className="ml-2 mt-2 text-lg leading-6 font-medium text-gray-900 dark:text-gray-200">Freeleech</h3>
|
||||
|
@ -893,7 +862,7 @@ function FilterActionsItem({ action, clients, idx, remove }: FilterActionsItemPr
|
|||
)}
|
||||
</Field>
|
||||
|
||||
<button className="px-4 py-4 w-full flex block" type="button" onClick={toggleEdit}>
|
||||
<button className="px-4 py-4 w-full flex" type="button" onClick={toggleEdit}>
|
||||
<div className="min-w-0 flex-1 sm:flex sm:items-center sm:justify-between">
|
||||
<div className="truncate">
|
||||
<div className="flex text-sm">
|
||||
|
|
|
@ -5,7 +5,6 @@ import { EmptyListState } from "../../components/emptystates";
|
|||
import {
|
||||
Link,
|
||||
} from "react-router-dom";
|
||||
import { Filter } from "../../domain/interfaces";
|
||||
import { useToggle } from "../../hooks/hooks";
|
||||
import { useMutation, useQuery } from "react-query";
|
||||
import { classNames } from "../../utils";
|
||||
|
@ -51,7 +50,7 @@ export default function Filters() {
|
|||
</header>
|
||||
|
||||
<div className="max-w-7xl mx-auto pb-12 px-4 sm:px-6 lg:px-8">
|
||||
<div className="bg-white dark:bg-gray-900 light:rounded-lg light:shadow">
|
||||
<div className="bg-white dark:bg-gray-800 light:rounded-lg light:shadow">
|
||||
<div className="relative inset-0 light:py-3 light:px-3 light:sm:px-3 light:lg:px-3 h-full">
|
||||
{data && data.length > 0 ? <FilterList filters={data} /> :
|
||||
<EmptyListState text="No filters here.." buttonText="Add new" buttonOnClick={toggleCreateFilter} />}
|
||||
|
@ -72,8 +71,8 @@ function FilterList({ filters }: FilterListProps) {
|
|||
<div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
||||
<div className="light:py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
|
||||
<div className="shadow overflow-hidden border-b border-gray-200 dark:border-gray-800 sm:rounded-lg">
|
||||
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-800">
|
||||
<thead className="bg-gray-50 dark:bg-gray-900 text-gray-500 dark:text-gray-400">
|
||||
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<thead className="bg-gray-50 dark:bg-gray-800 text-gray-500 dark:text-gray-400">
|
||||
<tr>
|
||||
<th
|
||||
scope="col"
|
||||
|
@ -98,7 +97,7 @@ function FilterList({ filters }: FilterListProps) {
|
|||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-800">
|
||||
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-800">
|
||||
{filters.map((filter: Filter, idx) => (
|
||||
<FilterListItem filter={filter} key={idx} idx={idx} />
|
||||
))}
|
||||
|
@ -135,7 +134,7 @@ function FilterListItem({ filter, idx }: FilterListItemProps) {
|
|||
|
||||
return (
|
||||
<tr key={filter.name}
|
||||
className={idx % 2 === 0 ? 'bg-white dark:bg-gray-900' : 'bg-gray-50 dark:bg-gray-900'}>
|
||||
className={idx % 2 === 0 ? 'bg-white dark:bg-gray-800' : 'bg-gray-50 dark:bg-gray-800'}>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-100">
|
||||
<Switch
|
||||
checked={enabled}
|
||||
|
@ -161,7 +160,7 @@ function FilterListItem({ filter, idx }: FilterListItemProps) {
|
|||
</Link>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{filter.indexers && filter.indexers.map(t =>
|
||||
<span key={t.id} className="mr-2 inline-flex items-center px-2.5 py-0.5 rounded-md text-sm font-medium bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-400">{t.name}</span>)}</td>
|
||||
<span key={t.id} className="mr-2 inline-flex items-center px-2.5 py-0.5 rounded-md text-sm font-medium bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-400">{t.name}</span>)}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<Link to={`filters/${filter.id.toString()}`} className="text-indigo-600 dark:text-gray-200 hover:text-indigo-900 dark:hover:text-gray-400">
|
||||
Edit
|
||||
|
|
|
@ -70,32 +70,15 @@ function ActionSettings() {
|
|||
<tr>
|
||||
<td>empty</td>
|
||||
</tr>
|
||||
{/*{downloadclients.map((client, personIdx) => (*/}
|
||||
{/* <tr key={client.name}*/}
|
||||
{/* className={personIdx % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>*/}
|
||||
{/* <td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{client.name}</td>*/}
|
||||
{/* <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{client.type}</td>*/}
|
||||
{/* <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{client.port}</td>*/}
|
||||
{/* <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{client.enabled}</td>*/}
|
||||
{/* <td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">*/}
|
||||
{/* <Link to="edit" className="text-indigo-600 hover:text-indigo-900">*/}
|
||||
{/* Edit*/}
|
||||
{/* </Link>*/}
|
||||
{/* </td>*/}
|
||||
{/* </tr>*/}
|
||||
{/*))}*/}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default ActionSettings;
|
|
@ -1,25 +1,22 @@
|
|||
import React, { useState } from "react";
|
||||
import { Switch } from "@headlessui/react";
|
||||
import { classNames } from "../../utils";
|
||||
// import {useRecoilState} from "recoil";
|
||||
// import {configState} from "../../state/state";
|
||||
import { useQuery } from "react-query";
|
||||
import { Config } from "../../domain/interfaces";
|
||||
|
||||
import APIClient from "../../api/APIClient";
|
||||
import { Checkbox } from "../../components/Checkbox";
|
||||
import { SettingsContext } from "../../utils/Context";
|
||||
|
||||
|
||||
function ApplicationSettings() {
|
||||
const [isDebug, setIsDebug] = useState(true)
|
||||
// const [config] = useRecoilState(configState)
|
||||
const [settings, setSettings] = SettingsContext.use();
|
||||
|
||||
const { isLoading, data } = useQuery<Config, Error>(['config'], () => APIClient.config.get(),
|
||||
const { isLoading, data } = useQuery<Config, Error>(
|
||||
['config'],
|
||||
() => APIClient.config.get(),
|
||||
{
|
||||
retry: false,
|
||||
refetchOnWindowFocus: false,
|
||||
onError: err => {
|
||||
console.log(err)
|
||||
}
|
||||
},
|
||||
)
|
||||
onError: err => console.log(err)
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<form className="divide-y divide-gray-200 dark:divide-gray-700 lg:col-span-9" action="#" method="POST">
|
||||
|
@ -32,7 +29,6 @@ function ApplicationSettings() {
|
|||
</div>
|
||||
|
||||
{!isLoading && data && (
|
||||
|
||||
<div className="mt-6 grid grid-cols-12 gap-6">
|
||||
<div className="col-span-6 sm:col-span-4">
|
||||
<label htmlFor="host" className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
|
||||
|
@ -79,69 +75,53 @@ function ApplicationSettings() {
|
|||
)}
|
||||
</div>
|
||||
|
||||
<div className="pt-6 pb-6 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<div className="pb-6 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<div className="px-4 py-5 sm:p-0">
|
||||
<dl className="sm:divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<div className="py-4 sm:py-5 sm:grid sm:grid-cols-4 sm:gap-4 sm:px-6">
|
||||
<dt className="font-medium text-gray-500 dark:text-white">Version:</dt>
|
||||
<dd className="mt-1 font-semibold text-gray-900 dark:text-white sm:mt-0 sm:col-span-2">{data?.version}</dd>
|
||||
</div>
|
||||
<div className="py-4 sm:py-5 sm:grid sm:grid-cols-4 sm:gap-4 sm:px-6 dark:bg-gray-700">
|
||||
<dt className="font-medium text-gray-500 dark:text-white">Commit:</dt>
|
||||
<dd className="mt-1 font-semibold text-gray-900 dark:text-white sm:mt-0 sm:col-span-2">{data?.commit}</dd>
|
||||
</div>
|
||||
<div className="py-4 sm:py-5 sm:grid sm:grid-cols-4 sm:gap-4 sm:px-6">
|
||||
<dt className="font-medium text-gray-500 dark:text-white">Date:</dt>
|
||||
<dd className="mt-1 font-semibold text-gray-900 dark:text-white sm:mt-0 sm:col-span-2">{data?.date}</dd>
|
||||
</div>
|
||||
{data?.version ? (
|
||||
<div className="py-4 sm:py-5 sm:grid sm:grid-cols-4 sm:gap-4 sm:px-6">
|
||||
<dt className="font-medium text-gray-500 dark:text-white">Version:</dt>
|
||||
<dd className="mt-1 text-gray-900 dark:text-white sm:mt-0 sm:col-span-2">{data?.version}</dd>
|
||||
</div>
|
||||
) : null}
|
||||
{data?.commit ? (
|
||||
<div className="py-4 sm:py-5 sm:grid sm:grid-cols-4 sm:gap-4 sm:px-6">
|
||||
<dt className="font-medium text-gray-500 dark:text-white">Commit:</dt>
|
||||
<dd className="mt-1 text-gray-900 dark:text-white sm:mt-0 sm:col-span-2">{data.commit}</dd>
|
||||
</div>
|
||||
) : null}
|
||||
{data?.date ? (
|
||||
<div className="py-4 sm:py-5 sm:grid sm:grid-cols-4 sm:gap-4 sm:px-6">
|
||||
<dt className="font-medium text-gray-500 dark:text-white">Date:</dt>
|
||||
<dd className="mt-1 text-gray-900 dark:text-white sm:mt-0 sm:col-span-2">{data?.date}</dd>
|
||||
</div>
|
||||
) : null}
|
||||
</dl>
|
||||
</div>
|
||||
<div className="px-4 sm:px-6">
|
||||
<ul className="mt-2 divide-y divide-gray-200">
|
||||
<Switch.Group as="li" className="py-4 flex items-center justify-between">
|
||||
<div className="flex flex-col">
|
||||
<Switch.Label as="p" className="text-sm font-medium text-gray-900 dark:text-white" passive>
|
||||
Debug
|
||||
</Switch.Label>
|
||||
<Switch.Description className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Enable debug mode to get more logs.
|
||||
</Switch.Description>
|
||||
</div>
|
||||
<Switch
|
||||
checked={isDebug}
|
||||
disabled={true}
|
||||
onChange={setIsDebug}
|
||||
className={classNames(
|
||||
isDebug ? 'bg-teal-500 dark:bg-blue-500' : 'bg-gray-200 dark:bg-gray-700',
|
||||
'ml-4 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500'
|
||||
)}
|
||||
>
|
||||
<span className="sr-only">Use setting</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={classNames(
|
||||
isDebug ? 'translate-x-5' : 'translate-x-0',
|
||||
'inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200'
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
</Switch.Group>
|
||||
</ul>
|
||||
</div>
|
||||
{/*<div className="mt-4 py-4 px-4 flex justify-end sm:px-6">*/}
|
||||
{/* <button*/}
|
||||
{/* type="button"*/}
|
||||
{/* className="bg-white border border-gray-300 rounded-md shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"*/}
|
||||
{/* >*/}
|
||||
{/* Cancel*/}
|
||||
{/* </button>*/}
|
||||
{/* <button*/}
|
||||
{/* type="submit"*/}
|
||||
{/* className="ml-5 bg-indigo-700 border border-transparent rounded-md shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-white hover:bg-indigo-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"*/}
|
||||
{/* >*/}
|
||||
{/* Save*/}
|
||||
{/* </button>*/}
|
||||
{/*</div>*/}
|
||||
<ul className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<div className="px-4 sm:px-6 py-1">
|
||||
<Checkbox
|
||||
label="Debug"
|
||||
description="Enable debug mode to get more logs."
|
||||
value={settings.debug}
|
||||
setValue={(newValue: boolean) => setSettings({
|
||||
...settings,
|
||||
debug: newValue
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div className="px-4 sm:px-6 py-1">
|
||||
<Checkbox
|
||||
label="Dark theme"
|
||||
description="Switch between dark and light theme"
|
||||
value={settings.darkTheme}
|
||||
setValue={(newValue: boolean) => setSettings({
|
||||
...settings,
|
||||
darkTheme: newValue
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</ul>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { DownloadClient } from "../../domain/interfaces";
|
||||
import { useToggle } from "../../hooks/hooks";
|
||||
import { Switch } from "@headlessui/react";
|
||||
import { useQuery } from "react-query";
|
||||
|
|
|
@ -2,7 +2,6 @@ import { useEffect } from "react";
|
|||
import { useToggle } from "../../hooks/hooks";
|
||||
import { useQuery } from "react-query";
|
||||
import { IndexerAddForm, IndexerUpdateForm } from "../../forms";
|
||||
import { Indexer } from "../../domain/interfaces";
|
||||
import { Switch } from "@headlessui/react";
|
||||
import { classNames } from "../../utils";
|
||||
import { EmptySimple } from "../../components/emptystates";
|
||||
|
|
|
@ -1,41 +1,11 @@
|
|||
import { useEffect } from "react";
|
||||
import { IrcNetworkAddForm, IrcNetworkUpdateForm } from "../../forms";
|
||||
import { useToggle } from "../../hooks/hooks";
|
||||
import { useQuery } from "react-query";
|
||||
import { EmptySimple } from "../../components/emptystates";
|
||||
import APIClient from "../../api/APIClient";
|
||||
import { formatDistanceToNowStrict, formatISO9075 } from "date-fns";
|
||||
|
||||
interface IrcNetwork {
|
||||
id: number;
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
addr: string;
|
||||
server: string;
|
||||
port: string;
|
||||
nick: string;
|
||||
username: string;
|
||||
realname: string;
|
||||
pass: string;
|
||||
connected: boolean;
|
||||
connected_since: string;
|
||||
tls: boolean;
|
||||
nickserv: {
|
||||
account: string;
|
||||
}
|
||||
channels: Channel[]
|
||||
}
|
||||
import APIClient from "../../api/APIClient";
|
||||
import { useToggle } from "../../hooks/hooks";
|
||||
import { EmptySimple } from "../../components/emptystates";
|
||||
import { IrcNetworkAddForm, IrcNetworkUpdateForm } from "../../forms";
|
||||
|
||||
interface Channel {
|
||||
id: number;
|
||||
enabled: boolean;
|
||||
name: string;
|
||||
password: string;
|
||||
detached: boolean;
|
||||
monitoring: boolean;
|
||||
monitoring_since: string;
|
||||
last_announce: string;
|
||||
}
|
||||
|
||||
function IsEmptyDate(date: string) {
|
||||
if (date !== "0001-01-01T00:00:00Z") {
|
||||
|
@ -57,9 +27,6 @@ function simplifyDate(date: string) {
|
|||
function IrcSettings() {
|
||||
const [addNetworkIsOpen, toggleAddNetwork] = useToggle(false)
|
||||
|
||||
useEffect(() => {
|
||||
}, []);
|
||||
|
||||
const { data } = useQuery<IrcNetwork[], Error>('networks', APIClient.irc.getNetworks,
|
||||
{
|
||||
refetchOnWindowFocus: false
|
||||
|
@ -167,13 +134,11 @@ const LiItem = ({ idx, network }: LiItemProps) => {
|
|||
Edit
|
||||
</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{edit && (
|
||||
<div className="px-4 py-4 flex border-b border-x-0 dark:border-gray-600 dark:bg-gray-700">
|
||||
<div className="min-w-full">
|
||||
<ol>
|
||||
|
||||
<li className="grid grid-cols-12 gap-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="col-span-4 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Channel</div>
|
||||
<div className="col-span-4 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Monitoring since</div>
|
||||
|
@ -181,9 +146,7 @@ const LiItem = ({ idx, network }: LiItemProps) => {
|
|||
</li>
|
||||
{network.channels.map(c => (
|
||||
<li key={c.id} className="text-gray-500 dark:text-gray-400">
|
||||
|
||||
<div className="grid grid-cols-12 gap-4 items-center py-4">
|
||||
|
||||
<div className="col-span-4 flex items-center sm:px-6 ">
|
||||
<span className="relative inline-flex items-center">
|
||||
{
|
||||
|
|
99
web/src/screens/settings/RegexPlayground.tsx
Normal file
99
web/src/screens/settings/RegexPlayground.tsx
Normal file
|
@ -0,0 +1,99 @@
|
|||
import React, { useRef, useState } from "react";
|
||||
|
||||
export const RegexPlayground = () => {
|
||||
const regexRef = useRef<HTMLInputElement>(null);
|
||||
const [output, setOutput] = useState<Array<React.ReactElement>>();
|
||||
|
||||
const onInput = (text: string) => {
|
||||
if (!regexRef || !regexRef.current)
|
||||
return;
|
||||
|
||||
const regexp = new RegExp(regexRef.current.value, "g");
|
||||
|
||||
const results: Array<React.ReactElement> = [];
|
||||
text.split("\n").forEach((line, index) => {
|
||||
const matches = line.matchAll(regexp);
|
||||
|
||||
let lastIndex = 0;
|
||||
// @ts-ignore
|
||||
for (const match of matches) {
|
||||
if (match.index === undefined)
|
||||
continue;
|
||||
|
||||
const start = match.index;
|
||||
results.push(
|
||||
<span key={`match=${start}`}>
|
||||
{line.substring(lastIndex, start)}
|
||||
<span className="bg-green-200 text-black font-bold">
|
||||
{line.substring(start, start + match.length)}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
lastIndex = start + match.length;
|
||||
}
|
||||
|
||||
if (lastIndex < line.length) {
|
||||
results.push(
|
||||
<span key={`last-${lastIndex + 1}`}>
|
||||
{line.substring(lastIndex)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (lastIndex > 0)
|
||||
results.push(<br key={`line-delim-${index}`}/>);
|
||||
});
|
||||
|
||||
setOutput(results);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="divide-y divide-gray-200 dark:divide-gray-700 lg:col-span-9">
|
||||
<div className="py-6 px-4 sm:p-6 lg:pb-8">
|
||||
<div>
|
||||
<h2 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Application</h2>
|
||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
Regex playground. Experiment with your filters here. WIP.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-6 py-4">
|
||||
<label
|
||||
htmlFor="input-regex"
|
||||
className="block text-sm font-medium text-gray-300"
|
||||
>
|
||||
RegExp filter
|
||||
</label>
|
||||
<input
|
||||
ref={regexRef}
|
||||
id="input-regex"
|
||||
type="text"
|
||||
autoComplete="true"
|
||||
className="mt-1 mb-4 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:text-gray-100 sm:text-sm"
|
||||
/>
|
||||
<label
|
||||
htmlFor="input-lines"
|
||||
className="block text-sm font-medium text-gray-300"
|
||||
>
|
||||
Lines to match
|
||||
</label>
|
||||
<div
|
||||
id="input-lines"
|
||||
className="mt-1 mb-4 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:text-gray-100 sm:text-sm"
|
||||
onInput={(e) => onInput(e.currentTarget.innerText ?? "")}
|
||||
contentEditable
|
||||
></div>
|
||||
</div>
|
||||
<div className="py-4 px-4 sm:p-6 lg:pb-8">
|
||||
<div>
|
||||
<h3 className="text-md leading-6 font-medium text-gray-900 dark:text-white">
|
||||
Matches
|
||||
</h3>
|
||||
<p className="mt-1 text-lg text-gray-500 dark:text-gray-400">
|
||||
{output}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
import { atom } from "recoil";
|
||||
|
||||
export const configState = atom({
|
||||
key: "configState",
|
||||
default: {
|
||||
host: "127.0.0.1",
|
||||
port: 8989,
|
||||
base_url: "",
|
||||
log_path: "",
|
||||
log_level: "DEBUG",
|
||||
}
|
||||
});
|
||||
|
||||
export const isLoggedIn = atom({
|
||||
key: 'isLoggedIn',
|
||||
default: false,
|
||||
})
|
65
web/src/types/Action.d.ts
vendored
Normal file
65
web/src/types/Action.d.ts
vendored
Normal file
|
@ -0,0 +1,65 @@
|
|||
interface Action {
|
||||
id: number;
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
type: ActionType;
|
||||
exec_cmd: string;
|
||||
exec_args: string;
|
||||
watch_folder: string;
|
||||
category: string;
|
||||
tags: string;
|
||||
label: string;
|
||||
save_path: string;
|
||||
paused: boolean;
|
||||
ignore_rules: boolean;
|
||||
limit_upload_speed: number;
|
||||
limit_download_speed: number;
|
||||
client_id: number;
|
||||
filter_id: number;
|
||||
}
|
||||
|
||||
interface Filter {
|
||||
id: number;
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
shows: string;
|
||||
min_size: string;
|
||||
max_size: string;
|
||||
match_sites: string[];
|
||||
except_sites: string[];
|
||||
delay: number;
|
||||
years: string;
|
||||
resolutions: string[];
|
||||
sources: string[];
|
||||
codecs: string[];
|
||||
containers: string[];
|
||||
match_release_types: string[];
|
||||
quality: string[];
|
||||
formats: string[];
|
||||
match_hdr: string[];
|
||||
except_hdr: string[];
|
||||
log_score: number;
|
||||
log: boolean;
|
||||
cue: boolean;
|
||||
perfect_flac: boolean;
|
||||
artists: string;
|
||||
albums: string;
|
||||
seasons: string;
|
||||
episodes: string;
|
||||
match_releases: string;
|
||||
except_releases: string;
|
||||
match_release_groups: string;
|
||||
except_release_groups: string;
|
||||
match_categories: string;
|
||||
except_categories: string;
|
||||
tags: string;
|
||||
except_tags: string;
|
||||
match_uploaders: string;
|
||||
except_uploaders: string;
|
||||
freeleech: boolean;
|
||||
freeleech_percent: string;
|
||||
actions: Action[];
|
||||
indexers: Indexer[];
|
||||
}
|
||||
|
||||
type ActionType = 'TEST' | 'EXEC' | 'WATCH_FOLDER' | DownloadClientType;
|
20
web/src/types/Download.d.ts
vendored
Normal file
20
web/src/types/Download.d.ts
vendored
Normal file
|
@ -0,0 +1,20 @@
|
|||
type DownloadClientType =
|
||||
'QBITTORRENT' |
|
||||
'DELUGE_V1' |
|
||||
'DELUGE_V2' |
|
||||
'RADARR' |
|
||||
'SONARR' |
|
||||
'LIDARR';
|
||||
|
||||
interface DownloadClient {
|
||||
id?: number;
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
host: string;
|
||||
port: number;
|
||||
ssl: boolean;
|
||||
username: string;
|
||||
password: string;
|
||||
type: DownloadClientType;
|
||||
settings: object;
|
||||
}
|
3
web/src/types/Global.d.ts
vendored
Normal file
3
web/src/types/Global.d.ts
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
interface APP {
|
||||
baseUrl: string;
|
||||
}
|
42
web/src/types/Indexer.d.ts
vendored
Normal file
42
web/src/types/Indexer.d.ts
vendored
Normal file
|
@ -0,0 +1,42 @@
|
|||
interface Indexer {
|
||||
id: number;
|
||||
name: string;
|
||||
identifier: string;
|
||||
enabled: boolean;
|
||||
settings: object | any;
|
||||
}
|
||||
|
||||
interface IndexerSchema {
|
||||
name: string;
|
||||
identifier: string;
|
||||
description: string;
|
||||
language: string;
|
||||
privacy: string;
|
||||
protocol: string;
|
||||
urls: string[];
|
||||
settings: IndexerSchemaSettings[];
|
||||
irc: IndexerSchemaIRC;
|
||||
}
|
||||
|
||||
interface IndexerSchemaSettings {
|
||||
name: string;
|
||||
type: string;
|
||||
required: boolean;
|
||||
label: string;
|
||||
help: string;
|
||||
description: string;
|
||||
default: string;
|
||||
}
|
||||
|
||||
interface IndexerSchemaIRC {
|
||||
network: string;
|
||||
server: string;
|
||||
port: number;
|
||||
tls: boolean;
|
||||
nickserv: boolean;
|
||||
announcers: string[];
|
||||
channels: string[];
|
||||
invite: string[];
|
||||
invite_command: string;
|
||||
settings: IndexerSchemaSettings[];
|
||||
}
|
75
web/src/types/Irc.d.ts
vendored
Normal file
75
web/src/types/Irc.d.ts
vendored
Normal file
|
@ -0,0 +1,75 @@
|
|||
interface IrcNetwork {
|
||||
id: number;
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
addr: string;
|
||||
server: string;
|
||||
port: string;
|
||||
nick: string;
|
||||
username: string;
|
||||
realname: string;
|
||||
pass: string;
|
||||
connected: boolean;
|
||||
connected_since: string;
|
||||
tls: boolean;
|
||||
nickserv: {
|
||||
account: string;
|
||||
}
|
||||
channels: IrcNetworkChannel[];
|
||||
}
|
||||
|
||||
interface IrcNetworkChannel {
|
||||
id: number;
|
||||
enabled: boolean;
|
||||
name: string;
|
||||
password: string;
|
||||
detached: boolean;
|
||||
monitoring: boolean;
|
||||
monitoring_since: string;
|
||||
last_announce: string;
|
||||
}
|
||||
|
||||
interface NickServ {
|
||||
account: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
interface Network {
|
||||
id?: number;
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
server: string;
|
||||
port: number;
|
||||
tls: boolean;
|
||||
invite_command: string;
|
||||
nickserv: {
|
||||
account: string;
|
||||
password: string;
|
||||
}
|
||||
channels: Channel[];
|
||||
settings: object;
|
||||
}
|
||||
|
||||
interface Channel {
|
||||
name: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
interface SASL {
|
||||
mechanism: string;
|
||||
plain: {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
}
|
||||
|
||||
interface Config {
|
||||
host: string;
|
||||
port: number;
|
||||
log_level: string;
|
||||
log_path: string;
|
||||
base_url: string;
|
||||
version: string;
|
||||
commit: string;
|
||||
date: string;
|
||||
}
|
36
web/src/types/Release.d.ts
vendored
Normal file
36
web/src/types/Release.d.ts
vendored
Normal file
|
@ -0,0 +1,36 @@
|
|||
interface Release {
|
||||
id: number;
|
||||
filter_status: string;
|
||||
rejections: string[];
|
||||
indexer: string;
|
||||
filter: string;
|
||||
protocol: string;
|
||||
title: string;
|
||||
size: number;
|
||||
raw: string;
|
||||
timestamp: Date
|
||||
action_status: ReleaseActionStatus[]
|
||||
}
|
||||
|
||||
interface ReleaseActionStatus {
|
||||
id: number;
|
||||
status: string;
|
||||
action: string;
|
||||
type: string;
|
||||
rejections: string[];
|
||||
timestamp: Date
|
||||
}
|
||||
|
||||
interface ReleaseFindResponse {
|
||||
data: Release[];
|
||||
next_cursor: number;
|
||||
count: number;
|
||||
}
|
||||
|
||||
interface ReleaseStats {
|
||||
total_count: number;
|
||||
filtered_count: number;
|
||||
filter_rejected_count: number;
|
||||
push_approved_count: number;
|
||||
push_rejected_count: number;
|
||||
}
|
71
web/src/utils/Context.ts
Normal file
71
web/src/utils/Context.ts
Normal file
|
@ -0,0 +1,71 @@
|
|||
import { newRidgeState } from "react-ridge-state";
|
||||
|
||||
|
||||
export const InitializeGlobalContext = () => {
|
||||
const auth_ctx = localStorage.getItem("auth");
|
||||
if (auth_ctx)
|
||||
AuthContext.set(JSON.parse(auth_ctx));
|
||||
|
||||
const settings_ctx = localStorage.getItem("settings");
|
||||
if (settings_ctx) {
|
||||
SettingsContext.set(JSON.parse(settings_ctx));
|
||||
} else {
|
||||
// Only check for light theme, otherwise dark theme is the default
|
||||
const userMedia = window.matchMedia("(prefers-color-scheme: light)");
|
||||
if (userMedia.matches) {
|
||||
SettingsContext.set((state) => ({
|
||||
...state,
|
||||
darkTheme: false
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface AuthInfo {
|
||||
username: string;
|
||||
isLoggedIn: boolean;
|
||||
}
|
||||
|
||||
export const AuthContext = newRidgeState<AuthInfo>(
|
||||
{
|
||||
username: "",
|
||||
isLoggedIn: false
|
||||
},
|
||||
{
|
||||
onSet: (new_state) => {
|
||||
try {
|
||||
localStorage.setItem("auth", JSON.stringify(new_state));
|
||||
} catch (e) {
|
||||
console.log("An error occurred while trying to modify the local auth context state.");
|
||||
console.log("Error:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
interface SettingsType {
|
||||
debug: boolean;
|
||||
darkTheme: boolean;
|
||||
}
|
||||
|
||||
export const SettingsContext = newRidgeState<SettingsType>(
|
||||
{
|
||||
debug: false,
|
||||
darkTheme: true
|
||||
},
|
||||
{
|
||||
onSet: (new_state) => {
|
||||
try {
|
||||
if (new_state.darkTheme) {
|
||||
document.documentElement.classList.add("dark");
|
||||
} else {
|
||||
document.documentElement.classList.remove("dark");
|
||||
}
|
||||
localStorage.setItem("settings", JSON.stringify(new_state));
|
||||
} catch (e) {
|
||||
console.log("An error occurred while trying to modify the local settings context state.");
|
||||
console.log("Error:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
|
@ -1,7 +1,6 @@
|
|||
const colors = require('tailwindcss/colors')
|
||||
|
||||
module.exports = {
|
||||
// mode: 'jit',
|
||||
purge: {
|
||||
content: [
|
||||
'./src/**/*.{tsx,ts,html,css}',
|
||||
|
@ -21,7 +20,7 @@ module.exports = {
|
|||
'col-span-12',
|
||||
],
|
||||
},
|
||||
darkMode: 'media', // or 'media' or 'class'
|
||||
darkMode: 'class', // or 'media' or 'class'
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
|
|
|
@ -1,26 +1,28 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"target": "ESNext",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
"DOM",
|
||||
"DOM.Iterable",
|
||||
"ESNext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"types": [],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"esModuleInterop": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
"jsx": "react-jsx",
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
"./src",
|
||||
"./types"
|
||||
]
|
||||
}
|
||||
|
|
3513
web/yarn.lock
3513
web/yarn.lock
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue