rqbit/desktop/src/configure.tsx
2024-11-20 16:12:42 +00:00

449 lines
14 KiB
TypeScript

import React, { ReactNode, useState } from "react";
import { RqbitDesktopConfig } from "./configuration";
import { ErrorComponent } from "rqbit-webui/src/components/ErrorComponent";
import { invokeAPI } from "./api";
import { ErrorDetails } from "rqbit-webui/src/api-types";
import { FormCheckbox } from "rqbit-webui/src/components/forms/FormCheckbox";
import { FormInput as FI } from "rqbit-webui/src/components/forms/FormInput";
import { ModalBody } from "rqbit-webui/src/components/modal/ModalBody";
import { Modal } from "rqbit-webui/src/components/modal/Modal";
import { Fieldset } from "rqbit-webui/src/components/forms/Fieldset";
import { ModalFooter } from "rqbit-webui/src/components/modal/ModalFooter";
import { Button } from "rqbit-webui/src/components/buttons/Button";
import { formatBytes } from "rqbit-webui/src/helper/formatBytes";
const FormCheck: React.FC<{
label: string;
name: string;
checked: boolean;
onChange: React.ChangeEventHandler<HTMLInputElement>;
disabled?: boolean;
help?: string;
}> = ({ label, name, checked, onChange, disabled, help }) => {
return (
<FormCheckbox
label={label}
name={name}
checked={checked}
onChange={onChange}
disabled={disabled}
help={help}
/>
);
};
const FormInput: React.FC<{
label: string;
name: string;
value: string | number;
inputType: string;
onChange: React.ChangeEventHandler<HTMLInputElement>;
disabled?: boolean;
help?: string;
}> = ({ label, name, value, inputType, onChange, disabled, help }) => {
return (
<FI
inputType={inputType}
name={name}
value={value as string}
onChange={onChange}
disabled={disabled}
label={label}
help={help}
/>
);
};
type TAB =
| "Home"
| "DHT"
| "Session"
| "Peer options"
| "HTTP API"
| "TCP Listen"
| "UPnP Server";
const TABS: readonly TAB[] = [
"Home",
"DHT",
"Session",
"TCP Listen",
"Peer options",
"HTTP API",
"UPnP Server",
] as const;
const Tab: React.FC<{
name: TAB;
currentTab: TAB;
children: ReactNode;
}> = ({ name, currentTab, children }) => {
const show = name === currentTab;
if (!show) {
return;
}
return <div>{children}</div>;
};
export const ConfigModal: React.FC<{
show: boolean;
handleStartReconfigure: () => void;
handleConfigured: (config: RqbitDesktopConfig) => void;
handleCancel?: () => void;
initialConfig: RqbitDesktopConfig;
defaultConfig: RqbitDesktopConfig;
}> = ({
show,
handleStartReconfigure,
handleConfigured,
handleCancel,
initialConfig,
defaultConfig,
}) => {
let [config, setConfig] = useState<RqbitDesktopConfig>(initialConfig);
let [loading, setLoading] = useState<boolean>(false);
let [tab, setTab] = useState<TAB>("Home");
const [error, setError] = useState<any | null>(null);
const handleInputChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
const name: string = e.target.name;
let value: string | number = e.target.value;
if (e.target.type == "number") {
value = e.target.valueAsNumber;
}
console.log(value, typeof value);
const [mainField, subField] = name.split(".", 2);
if (subField) {
setConfig((prevConfig: any) => ({
...prevConfig,
[mainField]: {
...prevConfig[mainField],
[subField]: value,
},
}));
} else {
setConfig((prevConfig) => ({
...prevConfig,
[name]: value,
}));
}
};
const handleToggleChange: React.ChangeEventHandler<HTMLInputElement> = (
e
) => {
const name: string = e.target.name;
const [mainField, subField] = name.split(".", 2);
if (subField) {
setConfig((prevConfig: any) => ({
...prevConfig,
[mainField]: {
...prevConfig[mainField],
[subField]: !prevConfig[mainField][subField],
},
}));
} else {
setConfig((prevConfig: any) => ({
...prevConfig,
[name]: !prevConfig[name],
}));
}
};
const handleOkClick = () => {
setError(null);
handleStartReconfigure();
setLoading(true);
invokeAPI<{}>("config_change", { config }).then(
() => {
setLoading(false);
handleConfigured(config);
},
(e: ErrorDetails) => {
setLoading(false);
setError({
text: "Error saving configuration",
details: e,
});
}
);
};
return (
<Modal
title="Configure Rqbit desktop"
isOpen={show}
onClose={handleCancel}
className="max-w-4xl"
>
<ModalBody>
<ErrorComponent error={error}></ErrorComponent>
<div className="mb-4 flex border-b">
{TABS.map((t, i) => {
const isActive = t === tab;
let classNames = "text-slate-300";
if (isActive) {
classNames =
"text-slate-800 border-b-2 border-blue-800 dark:border-blue-200 dark:text-white";
}
return (
<button
key={i}
className={`p-2 ${classNames}`}
onClick={() => setTab(t)}
>
{t}
</button>
);
})}
</div>
<Tab name="Home" currentTab={tab}>
<FormInput
label="Default download folder"
name="default_download_location"
value={config.default_download_location}
inputType="text"
onChange={handleInputChange}
help="Where to download torrents by default. You can override this per torrent."
/>
{defaultConfig.disable_upload !== undefined &&
config.disable_upload !== undefined && (
<FormCheck
label="Disable upload"
name="disable_upload"
checked={config.disable_upload}
onChange={handleToggleChange}
help="Disable uploading entirely. If this is set, rqbit won't share piece availability and will disconnect on download request.
Might be useful e.g. if rqbit upload consumes all your upload bandwidth and interferes with your other Internet usage."
/>
)}
</Tab>
<Tab name="DHT" currentTab={tab}>
<Fieldset>
<FormCheck
label="Enable DHT"
name="dht.disable"
checked={!config.dht.disable}
onChange={handleToggleChange}
help="DHT is required to read magnet links. There's no good reason to disable it, unless you know what you are doing."
/>
<FormCheck
label="Enable DHT persistence"
name="dht.disable_persistence"
checked={!config.dht.disable_persistence}
onChange={handleToggleChange}
disabled={config.dht.disable}
help="Enable to store DHT state in a file periodically. If disabled, DHT will bootstrap from scratch on restart."
/>
<FormInput
label="Persistence filename"
name="dht.persistence_filename"
value={config.dht.persistence_filename}
inputType="text"
disabled={config.dht.disable}
onChange={handleInputChange}
help="The filename to store DHT state into"
/>
</Fieldset>
</Tab>
<Tab name="TCP Listen" currentTab={tab}>
<Fieldset>
<FormCheck
label="Listen on TCP"
name="tcp_listen.disable"
checked={!config.tcp_listen.disable}
onChange={handleToggleChange}
help="Listen for torrent requests on TCP. Required for peers to be able to connect to you, mainly for uploading."
/>
<FormCheck
label="Advertise TCP port over UPnP"
name="tcp_listen.disable"
checked={!config.tcp_listen.disable}
onChange={handleToggleChange}
help="Advertise your port over UPnP to your router(s). This is required for peers to be able to connect to you from the internet. Will only work if your router has a static IP."
/>
<FormInput
inputType="number"
label="Min port"
name="tcp_listen.min_port"
value={config.tcp_listen.min_port}
disabled={config.tcp_listen.disable}
onChange={handleInputChange}
help="The min port to try to listen on. First successful is taken."
/>
<FormInput
inputType="number"
label="Max port"
name="tcp_listen.max_port"
value={config.tcp_listen.max_port}
disabled={config.tcp_listen.disable}
onChange={handleInputChange}
help="The max port to try to listen on."
/>
</Fieldset>
</Tab>
<Tab name="UPnP Server" currentTab={tab}>
<Fieldset>
<FormCheck
label="Enable UPnP media server"
name="upnp.enable_server"
checked={config.upnp.enable_server}
onChange={handleToggleChange}
help="If enabled, rqbit will advertise the media to supported LAN devices, e.g. TVs."
/>
<FormInput
inputType="text"
label="Friendly name"
name="upnp.server_friendly_name"
value={config.upnp.server_friendly_name}
disabled={!config.upnp.enable_server}
onChange={handleInputChange}
help="The name displayed on supported devices. If not set will be generated, will look smth like <rqbit at HOSTNAME>."
/>
</Fieldset>
</Tab>
<Tab name="Session" currentTab={tab}>
<Fieldset>
<FormCheck
label="Enable persistence"
name="persistence.disable"
checked={!config.persistence.disable}
onChange={handleToggleChange}
help="If you disable session persistence, rqbit won't remember the torrents you had before restart."
/>
<FormInput
label="Persistence folder"
name="persistence.folder"
inputType="text"
value={config.persistence.folder}
onChange={handleInputChange}
disabled={config.persistence.disable}
/>
<FormCheck
label="Enable fast resume (experimental)"
name="persistence.fastresume"
checked={config.persistence.fastresume}
onChange={handleToggleChange}
help="If enabled, restarting will not rehash torrents, and thus will be faster. You should not modify the downloaded files in any way if you use that."
/>
<FormInput
label="Download rate limit"
name="ratelimits.download_bps"
inputType="number"
value={config.ratelimits.download_bps ?? ""}
onChange={handleInputChange}
help={`Limit total download speed to this number of bytes per second (${
(config.ratelimits.download_bps ?? 0) > 0
? "current " +
formatBytes(config.ratelimits.download_bps ?? 0) +
" per second"
: "currently disabled"
})`}
/>
<FormInput
label="Upload rate limit"
name="ratelimits.upload_bps"
inputType="number"
value={config.ratelimits.upload_bps ?? ""}
onChange={handleInputChange}
help={`Limit total upload speed to this number of bytes per second (${
(config.ratelimits.upload_bps ?? 0) > 0
? "current " +
formatBytes(config.ratelimits.upload_bps ?? 0) +
" per second"
: "currently disabled"
})`}
/>
</Fieldset>
</Tab>
<Tab name="Peer options" currentTab={tab}>
<Fieldset>
<FormInput
label="Connect timeout (seconds)"
inputType="number"
name="peer_opts.connect_timeout"
value={config.peer_opts.connect_timeout}
onChange={handleInputChange}
help="How much to wait for outgoing connections to connect. Default is low to prefer faster peers."
/>
<FormInput
label="Read/write timeout (seconds)"
inputType="number"
name="peer_opts.read_write_timeout"
value={config.peer_opts.read_write_timeout}
onChange={handleInputChange}
help="Peer socket read/write timeout."
/>
</Fieldset>
</Tab>
<Tab name="HTTP API" currentTab={tab}>
<Fieldset>
<FormCheck
label="Enable HTTP API"
name="http_api.disable"
checked={!config.http_api.disable}
onChange={handleToggleChange}
help="If enabled you can access the HTTP API at the address below"
/>
<FormCheck
label="Read only"
name="http_api.read_only"
checked={config.http_api.read_only}
disabled={config.http_api.disable}
onChange={handleToggleChange}
help="If enabled, only GET requests will be allowed through the API"
/>
<FormInput
label="Listen address"
inputType="text"
name="http_api.listen_addr"
value={config.http_api.listen_addr}
disabled={config.http_api.disable}
onChange={handleInputChange}
help={`You'll access the API at http://${config.http_api.listen_addr}`}
/>
</Fieldset>
</Tab>
</ModalBody>
<ModalFooter>
{!!handleCancel && (
<Button variant="cancel" onClick={handleCancel}>
Cancel
</Button>
)}
<Button variant="secondary" onClick={() => setConfig(defaultConfig)}>
Reset to defaults
</Button>
<Button variant="primary" onClick={handleOkClick} disabled={loading}>
OK
</Button>
</ModalFooter>
</Modal>
);
};