setup vscode for consistent JS formatting
This commit is contained in:
parent
a641717245
commit
ec63e1cef7
15 changed files with 1675 additions and 1338 deletions
6
.vscode/settings.json
vendored
Normal file
6
.vscode/settings.json
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"[javascript]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,21 +1,27 @@
|
||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>rqbit web 4.0.0-beta.0</title>
|
||||||
|
<link rel="icon" type="image/svg+xml" href="assets/logo.svg" />
|
||||||
|
<!-- Include Bootstrap CSS -->
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
|
||||||
|
integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM"
|
||||||
|
crossorigin="anonymous"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css"
|
||||||
|
integrity="sha384-4LISF5TTJX/fLmGSxO53rV4miRxdg84mZsxmO8Rx5jGtp/LbrixFETvWa5a6sESd"
|
||||||
|
crossorigin="anonymous"
|
||||||
|
/>
|
||||||
|
</head>
|
||||||
|
|
||||||
<head>
|
<body>
|
||||||
<meta charset="UTF-8">
|
<div id="app"></div>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<script type="module" src="src/main.tsx"></script>
|
||||||
<title>rqbit web 4.0.0-beta.0</title>
|
</body>
|
||||||
<link rel="icon" type="image/svg+xml" href="assets/logo.svg">
|
|
||||||
<!-- Include Bootstrap CSS -->
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
|
|
||||||
integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous" />
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css"
|
|
||||||
integrity="sha384-4LISF5TTJX/fLmGSxO53rV4miRxdg84mZsxmO8Rx5jGtp/LbrixFETvWa5a6sESd" crossorigin="anonymous">
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div id="app"></div>
|
|
||||||
<script type="module" src="src/main.tsx"></script>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
15
crates/librqbit/webui/node_modules/.package-lock.json
generated
vendored
15
crates/librqbit/webui/node_modules/.package-lock.json
generated
vendored
|
|
@ -304,6 +304,21 @@
|
||||||
"node": "^10 || ^12 || >=14"
|
"node": "^10 || ^12 || >=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/prettier": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==",
|
||||||
|
"dev": true,
|
||||||
|
"bin": {
|
||||||
|
"prettier": "bin/prettier.cjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/prop-types": {
|
"node_modules/prop-types": {
|
||||||
"version": "15.8.1",
|
"version": "15.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||||
|
|
|
||||||
16
crates/librqbit/webui/package-lock.json
generated
16
crates/librqbit/webui/package-lock.json
generated
|
|
@ -12,6 +12,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^18.2.38",
|
"@types/react": "^18.2.38",
|
||||||
"@types/react-dom": "^18.2.16",
|
"@types/react-dom": "^18.2.16",
|
||||||
|
"prettier": "3.1.0",
|
||||||
"typescript": "^5.3.2",
|
"typescript": "^5.3.2",
|
||||||
"vite": "^4.5.1"
|
"vite": "^4.5.1"
|
||||||
}
|
}
|
||||||
|
|
@ -653,6 +654,21 @@
|
||||||
"node": "^10 || ^12 || >=14"
|
"node": "^10 || ^12 || >=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/prettier": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==",
|
||||||
|
"dev": true,
|
||||||
|
"bin": {
|
||||||
|
"prettier": "bin/prettier.cjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/prop-types": {
|
"node_modules/prop-types": {
|
||||||
"version": "15.8.1",
|
"version": "15.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^18.2.38",
|
"@types/react": "^18.2.38",
|
||||||
"@types/react-dom": "^18.2.16",
|
"@types/react-dom": "^18.2.16",
|
||||||
|
"prettier": "3.1.0",
|
||||||
"typescript": "^5.3.2",
|
"typescript": "^5.3.2",
|
||||||
"vite": "^4.5.1"
|
"vite": "^4.5.1"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,131 +1,132 @@
|
||||||
// Interface for the Torrent API response
|
// Interface for the Torrent API response
|
||||||
export interface TorrentId {
|
export interface TorrentId {
|
||||||
id: number;
|
id: number;
|
||||||
info_hash: string;
|
info_hash: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TorrentFile {
|
export interface TorrentFile {
|
||||||
name: string;
|
name: string;
|
||||||
length: number;
|
length: number;
|
||||||
included: boolean;
|
included: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Interface for the Torrent Details API response
|
// Interface for the Torrent Details API response
|
||||||
export interface TorrentDetails {
|
export interface TorrentDetails {
|
||||||
info_hash: string,
|
info_hash: string;
|
||||||
files: Array<TorrentFile>;
|
files: Array<TorrentFile>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AddTorrentResponse {
|
export interface AddTorrentResponse {
|
||||||
id: number | null;
|
id: number | null;
|
||||||
details: TorrentDetails;
|
details: TorrentDetails;
|
||||||
output_folder: string,
|
output_folder: string;
|
||||||
seen_peers?: Array<string>;
|
seen_peers?: Array<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ListTorrentsResponse {
|
export interface ListTorrentsResponse {
|
||||||
torrents: Array<TorrentId>;
|
torrents: Array<TorrentId>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Speed {
|
export interface Speed {
|
||||||
mbps: number;
|
mbps: number;
|
||||||
human_readable: string;
|
human_readable: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Interface for the Torrent Stats API response
|
// Interface for the Torrent Stats API response
|
||||||
export interface LiveTorrentStats {
|
export interface LiveTorrentStats {
|
||||||
snapshot: {
|
snapshot: {
|
||||||
have_bytes: number;
|
have_bytes: number;
|
||||||
downloaded_and_checked_bytes: number;
|
downloaded_and_checked_bytes: number;
|
||||||
downloaded_and_checked_pieces: number;
|
downloaded_and_checked_pieces: number;
|
||||||
fetched_bytes: number;
|
fetched_bytes: number;
|
||||||
uploaded_bytes: number;
|
uploaded_bytes: number;
|
||||||
initially_needed_bytes: number;
|
initially_needed_bytes: number;
|
||||||
remaining_bytes: number;
|
remaining_bytes: number;
|
||||||
total_bytes: number;
|
total_bytes: number;
|
||||||
total_piece_download_ms: number;
|
total_piece_download_ms: number;
|
||||||
peer_stats: {
|
peer_stats: {
|
||||||
queued: number;
|
queued: number;
|
||||||
connecting: number;
|
connecting: number;
|
||||||
live: number;
|
live: number;
|
||||||
seen: number;
|
seen: number;
|
||||||
dead: number;
|
dead: number;
|
||||||
not_needed: number;
|
not_needed: number;
|
||||||
};
|
|
||||||
};
|
};
|
||||||
average_piece_download_time: {
|
};
|
||||||
secs: number;
|
average_piece_download_time: {
|
||||||
nanos: number;
|
secs: number;
|
||||||
|
nanos: number;
|
||||||
|
};
|
||||||
|
download_speed: Speed;
|
||||||
|
upload_speed: Speed;
|
||||||
|
all_time_download_speed: {
|
||||||
|
mbps: number;
|
||||||
|
human_readable: string;
|
||||||
|
};
|
||||||
|
time_remaining: {
|
||||||
|
human_readable: string;
|
||||||
|
duration?: {
|
||||||
|
secs: number;
|
||||||
};
|
};
|
||||||
download_speed: Speed;
|
} | null;
|
||||||
upload_speed: Speed;
|
|
||||||
all_time_download_speed: {
|
|
||||||
mbps: number;
|
|
||||||
human_readable: string;
|
|
||||||
};
|
|
||||||
time_remaining: {
|
|
||||||
human_readable: string;
|
|
||||||
duration?: {
|
|
||||||
secs: number,
|
|
||||||
}
|
|
||||||
} | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const STATE_INITIALIZING = 'initializing';
|
export const STATE_INITIALIZING = "initializing";
|
||||||
export const STATE_PAUSED = 'paused';
|
export const STATE_PAUSED = "paused";
|
||||||
export const STATE_LIVE = 'live';
|
export const STATE_LIVE = "live";
|
||||||
export const STATE_ERROR = 'error';
|
export const STATE_ERROR = "error";
|
||||||
|
|
||||||
export interface TorrentStats {
|
export interface TorrentStats {
|
||||||
state: 'initializing' | 'paused' | 'live' | 'error',
|
state: "initializing" | "paused" | "live" | "error";
|
||||||
error: string | null,
|
error: string | null;
|
||||||
progress_bytes: number,
|
progress_bytes: number;
|
||||||
finished: boolean,
|
finished: boolean;
|
||||||
total_bytes: number,
|
total_bytes: number;
|
||||||
live: LiveTorrentStats | null;
|
live: LiveTorrentStats | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface ErrorDetails {
|
export interface ErrorDetails {
|
||||||
id?: number,
|
id?: number;
|
||||||
method?: string,
|
method?: string;
|
||||||
path?: string,
|
path?: string;
|
||||||
status?: number,
|
status?: number;
|
||||||
statusText?: string,
|
statusText?: string;
|
||||||
text: string,
|
text: string;
|
||||||
};
|
}
|
||||||
|
|
||||||
export type Duration = number;
|
export type Duration = number;
|
||||||
|
|
||||||
export interface PeerConnectionOptions {
|
export interface PeerConnectionOptions {
|
||||||
connect_timeout?: Duration | null;
|
connect_timeout?: Duration | null;
|
||||||
read_write_timeout?: Duration | null;
|
read_write_timeout?: Duration | null;
|
||||||
keep_alive_interval?: Duration | null;
|
keep_alive_interval?: Duration | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AddTorrentOptions {
|
export interface AddTorrentOptions {
|
||||||
paused?: boolean;
|
paused?: boolean;
|
||||||
only_files_regex?: string | null;
|
only_files_regex?: string | null;
|
||||||
only_files?: number[] | null;
|
only_files?: number[] | null;
|
||||||
overwrite?: boolean;
|
overwrite?: boolean;
|
||||||
list_only?: boolean;
|
list_only?: boolean;
|
||||||
output_folder?: string | null;
|
output_folder?: string | null;
|
||||||
sub_folder?: string | null;
|
sub_folder?: string | null;
|
||||||
peer_opts?: PeerConnectionOptions | null;
|
peer_opts?: PeerConnectionOptions | null;
|
||||||
force_tracker_interval?: Duration | null;
|
force_tracker_interval?: Duration | null;
|
||||||
initial_peers?: string[] | null; // Assuming SocketAddr is equivalent to a string in TypeScript
|
initial_peers?: string[] | null; // Assuming SocketAddr is equivalent to a string in TypeScript
|
||||||
preferred_id?: number | null;
|
preferred_id?: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface RqbitAPI {
|
export interface RqbitAPI {
|
||||||
listTorrents: () => Promise<ListTorrentsResponse>,
|
listTorrents: () => Promise<ListTorrentsResponse>;
|
||||||
getTorrentDetails: (index: number) => Promise<TorrentDetails>,
|
getTorrentDetails: (index: number) => Promise<TorrentDetails>;
|
||||||
getTorrentStats: (index: number) => Promise<TorrentStats>;
|
getTorrentStats: (index: number) => Promise<TorrentStats>;
|
||||||
uploadTorrent: (data: string | File, opts?: AddTorrentOptions) => Promise<AddTorrentResponse>;
|
uploadTorrent: (
|
||||||
|
data: string | File,
|
||||||
|
opts?: AddTorrentOptions
|
||||||
|
) => Promise<AddTorrentResponse>;
|
||||||
|
|
||||||
pause: (index: number) => Promise<void>;
|
pause: (index: number) => Promise<void>;
|
||||||
start: (index: number) => Promise<void>;
|
start: (index: number) => Promise<void>;
|
||||||
forget: (index: number) => Promise<void>;
|
forget: (index: number) => Promise<void>;
|
||||||
delete: (index: number) => Promise<void>;
|
delete: (index: number) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
@ -1,103 +1,121 @@
|
||||||
import { AddTorrentResponse, ErrorDetails, ListTorrentsResponse, RqbitAPI, TorrentDetails, TorrentStats } from "./api-types";
|
import {
|
||||||
|
AddTorrentResponse,
|
||||||
|
ErrorDetails,
|
||||||
|
ListTorrentsResponse,
|
||||||
|
RqbitAPI,
|
||||||
|
TorrentDetails,
|
||||||
|
TorrentStats,
|
||||||
|
} from "./api-types";
|
||||||
|
|
||||||
// Define API URL and base path
|
// Define API URL and base path
|
||||||
const apiUrl = (window.origin === 'null' || window.origin === 'http://localhost:3031') ? 'http://localhost:3030' : '';
|
const apiUrl =
|
||||||
|
window.origin === "null" || window.origin === "http://localhost:3031"
|
||||||
|
? "http://localhost:3030"
|
||||||
|
: "";
|
||||||
|
|
||||||
const makeRequest = async (method: string, path: string, data?: any): Promise<any> => {
|
const makeRequest = async (
|
||||||
console.log(method, path);
|
method: string,
|
||||||
const url = apiUrl + path;
|
path: string,
|
||||||
const options: RequestInit = {
|
data?: any
|
||||||
method,
|
): Promise<any> => {
|
||||||
headers: {
|
console.log(method, path);
|
||||||
'Accept': 'application/json',
|
const url = apiUrl + path;
|
||||||
},
|
const options: RequestInit = {
|
||||||
body: data,
|
method,
|
||||||
};
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
},
|
||||||
|
body: data,
|
||||||
|
};
|
||||||
|
|
||||||
let error: ErrorDetails = {
|
let error: ErrorDetails = {
|
||||||
method: method,
|
method: method,
|
||||||
path: path,
|
path: path,
|
||||||
text: ''
|
text: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
let response: Response;
|
let response: Response;
|
||||||
|
|
||||||
|
try {
|
||||||
|
response = await fetch(url, options);
|
||||||
|
} catch (e) {
|
||||||
|
error.text = "network error";
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
error.status = response.status;
|
||||||
|
error.statusText = `${response.status} ${response.statusText}`;
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorBody = await response.text();
|
||||||
try {
|
try {
|
||||||
response = await fetch(url, options);
|
const json = JSON.parse(errorBody);
|
||||||
|
error.text =
|
||||||
|
json.human_readable !== undefined
|
||||||
|
? json.human_readable
|
||||||
|
: JSON.stringify(json, null, 2);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.text = 'network error';
|
error.text = errorBody;
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
}
|
||||||
|
return Promise.reject(error);
|
||||||
error.status = response.status;
|
}
|
||||||
error.statusText = `${response.status} ${response.statusText}`;
|
const result = await response.json();
|
||||||
|
return result;
|
||||||
if (!response.ok) {
|
};
|
||||||
const errorBody = await response.text();
|
|
||||||
try {
|
|
||||||
const json = JSON.parse(errorBody);
|
|
||||||
error.text = json.human_readable !== undefined ? json.human_readable : JSON.stringify(json, null, 2);
|
|
||||||
} catch (e) {
|
|
||||||
error.text = errorBody;
|
|
||||||
}
|
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
|
||||||
const result = await response.json();
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const API: RqbitAPI & { getVersion: () => Promise<string> } = {
|
export const API: RqbitAPI & { getVersion: () => Promise<string> } = {
|
||||||
listTorrents: (): Promise<ListTorrentsResponse> => makeRequest('GET', '/torrents'),
|
listTorrents: (): Promise<ListTorrentsResponse> =>
|
||||||
getTorrentDetails: (index: number): Promise<TorrentDetails> => {
|
makeRequest("GET", "/torrents"),
|
||||||
return makeRequest('GET', `/torrents/${index}`);
|
getTorrentDetails: (index: number): Promise<TorrentDetails> => {
|
||||||
},
|
return makeRequest("GET", `/torrents/${index}`);
|
||||||
getTorrentStats: (index: number): Promise<TorrentStats> => {
|
},
|
||||||
return makeRequest('GET', `/torrents/${index}/stats/v1`);
|
getTorrentStats: (index: number): Promise<TorrentStats> => {
|
||||||
},
|
return makeRequest("GET", `/torrents/${index}/stats/v1`);
|
||||||
|
},
|
||||||
|
|
||||||
uploadTorrent: (data, opts): Promise<AddTorrentResponse> => {
|
uploadTorrent: (data, opts): Promise<AddTorrentResponse> => {
|
||||||
let url = '/torrents?&overwrite=true';
|
let url = "/torrents?&overwrite=true";
|
||||||
if (opts?.list_only) {
|
if (opts?.list_only) {
|
||||||
url += '&list_only=true';
|
url += "&list_only=true";
|
||||||
}
|
|
||||||
if (opts?.only_files != null) {
|
|
||||||
url += `&only_files=${opts.only_files.join(',')}`;
|
|
||||||
}
|
|
||||||
if (opts?.peer_opts?.connect_timeout) {
|
|
||||||
url += `&peer_connect_timeout=${opts.peer_opts.connect_timeout}`;
|
|
||||||
}
|
|
||||||
if (opts?.peer_opts?.read_write_timeout) {
|
|
||||||
url += `&peer_read_write_timeout=${opts.peer_opts.read_write_timeout}`;
|
|
||||||
}
|
|
||||||
if (opts?.initial_peers) {
|
|
||||||
url += `&initial_peers=${opts.initial_peers.join(',')}`;
|
|
||||||
}
|
|
||||||
if (opts?.output_folder) {
|
|
||||||
url += `&output_folder=${opts.output_folder}`;
|
|
||||||
}
|
|
||||||
if (typeof data === 'string') {
|
|
||||||
url += '&is_url=true';
|
|
||||||
}
|
|
||||||
return makeRequest('POST', url, data)
|
|
||||||
},
|
|
||||||
|
|
||||||
pause: (index: number): Promise<void> => {
|
|
||||||
return makeRequest('POST', `/torrents/${index}/pause`);
|
|
||||||
},
|
|
||||||
|
|
||||||
start: (index: number): Promise<void> => {
|
|
||||||
return makeRequest('POST', `/torrents/${index}/start`);
|
|
||||||
},
|
|
||||||
|
|
||||||
forget: (index: number): Promise<void> => {
|
|
||||||
return makeRequest('POST', `/torrents/${index}/forget`);
|
|
||||||
},
|
|
||||||
|
|
||||||
delete: (index: number): Promise<void> => {
|
|
||||||
return makeRequest('POST', `/torrents/${index}/delete`);
|
|
||||||
},
|
|
||||||
getVersion: async (): Promise<string> => {
|
|
||||||
const r = await makeRequest('GET', '/');
|
|
||||||
return r.version;
|
|
||||||
}
|
}
|
||||||
}
|
if (opts?.only_files != null) {
|
||||||
|
url += `&only_files=${opts.only_files.join(",")}`;
|
||||||
|
}
|
||||||
|
if (opts?.peer_opts?.connect_timeout) {
|
||||||
|
url += `&peer_connect_timeout=${opts.peer_opts.connect_timeout}`;
|
||||||
|
}
|
||||||
|
if (opts?.peer_opts?.read_write_timeout) {
|
||||||
|
url += `&peer_read_write_timeout=${opts.peer_opts.read_write_timeout}`;
|
||||||
|
}
|
||||||
|
if (opts?.initial_peers) {
|
||||||
|
url += `&initial_peers=${opts.initial_peers.join(",")}`;
|
||||||
|
}
|
||||||
|
if (opts?.output_folder) {
|
||||||
|
url += `&output_folder=${opts.output_folder}`;
|
||||||
|
}
|
||||||
|
if (typeof data === "string") {
|
||||||
|
url += "&is_url=true";
|
||||||
|
}
|
||||||
|
return makeRequest("POST", url, data);
|
||||||
|
},
|
||||||
|
|
||||||
|
pause: (index: number): Promise<void> => {
|
||||||
|
return makeRequest("POST", `/torrents/${index}/pause`);
|
||||||
|
},
|
||||||
|
|
||||||
|
start: (index: number): Promise<void> => {
|
||||||
|
return makeRequest("POST", `/torrents/${index}/start`);
|
||||||
|
},
|
||||||
|
|
||||||
|
forget: (index: number): Promise<void> => {
|
||||||
|
return makeRequest("POST", `/torrents/${index}/forget`);
|
||||||
|
},
|
||||||
|
|
||||||
|
delete: (index: number): Promise<void> => {
|
||||||
|
return makeRequest("POST", `/torrents/${index}/delete`);
|
||||||
|
},
|
||||||
|
getVersion: async (): Promise<string> => {
|
||||||
|
const r = await makeRequest("GET", "/");
|
||||||
|
return r.version;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,33 @@
|
||||||
import { StrictMode, useEffect, useState } from "react";
|
import { StrictMode, useEffect, useState } from "react";
|
||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from "react-dom/client";
|
||||||
import { RqbitWebUI, APIContext, customSetInterval } from "./rqbit-web";
|
import { RqbitWebUI, APIContext, customSetInterval } from "./rqbit-web";
|
||||||
import { API } from "./http-api";
|
import { API } from "./http-api";
|
||||||
|
|
||||||
const RootWithVersion = () => {
|
const RootWithVersion = () => {
|
||||||
let [title, setTitle] = useState<string>("rqbit web UI");
|
let [title, setTitle] = useState<string>("rqbit web UI");
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const refreshVersion = () => API.getVersion().then((version) => {
|
const refreshVersion = () =>
|
||||||
setTitle(`rqbit web UI - v${version}`);
|
API.getVersion().then(
|
||||||
return 10000;
|
(version) => {
|
||||||
}, (e) => {
|
setTitle(`rqbit web UI - v${version}`);
|
||||||
return 1000;
|
return 10000;
|
||||||
});
|
},
|
||||||
return customSetInterval(refreshVersion, 0)
|
(e) => {
|
||||||
}, [])
|
return 1000;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return customSetInterval(refreshVersion, 0);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return <StrictMode>
|
return (
|
||||||
<APIContext.Provider value={API}>
|
<StrictMode>
|
||||||
<RqbitWebUI title={title} />
|
<APIContext.Provider value={API}>
|
||||||
</APIContext.Provider>
|
<RqbitWebUI title={title} />
|
||||||
</StrictMode>;
|
</APIContext.Provider>
|
||||||
}
|
</StrictMode>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('app') as HTMLInputElement).render(
|
ReactDOM.createRoot(document.getElementById("app") as HTMLInputElement).render(
|
||||||
<RootWithVersion />
|
<RootWithVersion />
|
||||||
);
|
);
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,19 +1,17 @@
|
||||||
<!doctype html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>rqbit web 4.0.0-beta.0</title>
|
||||||
|
<link rel="icon" type="image/svg+xml" href="assets/logo.svg" />
|
||||||
|
<!-- Include Bootstrap CSS -->
|
||||||
|
<link rel="stylesheet" href="/src/styles/bootstrap.min.css" />
|
||||||
|
<link rel="stylesheet" href="/src/styles/bootstrap-icons.css" />
|
||||||
|
</head>
|
||||||
|
|
||||||
<head>
|
<body>
|
||||||
<meta charset="UTF-8">
|
<div id="root"></div>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
<title>rqbit web 4.0.0-beta.0</title>
|
</body>
|
||||||
<link rel="icon" type="image/svg+xml" href="assets/logo.svg">
|
|
||||||
<!-- Include Bootstrap CSS -->
|
|
||||||
<link rel="stylesheet" href="/src/styles/bootstrap.min.css" />
|
|
||||||
<link rel="stylesheet" href="/src/styles/bootstrap-icons.css" />
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div id="root"></div>
|
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
@ -1,90 +1,107 @@
|
||||||
import { AddTorrentResponse, ListTorrentsResponse, RqbitAPI, TorrentDetails, TorrentStats, ErrorDetails } from "./rqbit-webui-src/api-types";
|
import {
|
||||||
|
AddTorrentResponse,
|
||||||
|
ListTorrentsResponse,
|
||||||
|
RqbitAPI,
|
||||||
|
TorrentDetails,
|
||||||
|
TorrentStats,
|
||||||
|
ErrorDetails,
|
||||||
|
} from "./rqbit-webui-src/api-types";
|
||||||
|
|
||||||
import { InvokeArgs, invoke } from "@tauri-apps/api/tauri"
|
import { InvokeArgs, invoke } from "@tauri-apps/api/tauri";
|
||||||
|
|
||||||
interface InvokeErrorResponse {
|
interface InvokeErrorResponse {
|
||||||
error_kind: string,
|
error_kind: string;
|
||||||
human_readable: string,
|
human_readable: string;
|
||||||
status: number,
|
status: number;
|
||||||
status_text: string,
|
status_text: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function errorToUIError(path: string): (e: InvokeErrorResponse) => Promise<never> {
|
function errorToUIError(
|
||||||
return (e: InvokeErrorResponse) => {
|
path: string
|
||||||
console.log(e);
|
): (e: InvokeErrorResponse) => Promise<never> {
|
||||||
let reason: ErrorDetails = {
|
return (e: InvokeErrorResponse) => {
|
||||||
method: 'INVOKE',
|
console.log(e);
|
||||||
path: path,
|
let reason: ErrorDetails = {
|
||||||
text: e.human_readable,
|
method: "INVOKE",
|
||||||
status: e.status,
|
path: path,
|
||||||
statusText: e.status_text
|
text: e.human_readable,
|
||||||
};
|
status: e.status,
|
||||||
return Promise.reject(reason);
|
statusText: e.status_text,
|
||||||
}
|
};
|
||||||
|
return Promise.reject(reason);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function invokeAPI<Response>(name: string, params?: InvokeArgs): Promise<Response> {
|
export async function invokeAPI<Response>(
|
||||||
console.log("invoking", name, params);
|
name: string,
|
||||||
const result = await invoke<Response>(name, params).catch(errorToUIError(name));
|
params?: InvokeArgs
|
||||||
console.log(result);
|
): Promise<Response> {
|
||||||
return result;
|
console.log("invoking", name, params);
|
||||||
|
const result = await invoke<Response>(name, params).catch(
|
||||||
|
errorToUIError(name)
|
||||||
|
);
|
||||||
|
console.log(result);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function readFileAsBase64(file: File): Promise<string> {
|
async function readFileAsBase64(file: File): Promise<string> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
|
|
||||||
reader.onload = function (event) {
|
reader.onload = function (event) {
|
||||||
const base64String = (event?.target?.result as string)?.split(',')[1];
|
const base64String = (event?.target?.result as string)?.split(",")[1];
|
||||||
if (base64String) {
|
if (base64String) {
|
||||||
resolve(base64String);
|
resolve(base64String);
|
||||||
} else {
|
} else {
|
||||||
reject(new Error('Failed to read file as base64.'));
|
reject(new Error("Failed to read file as base64."));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
reader.onerror = function (error) {
|
reader.onerror = function (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
reject(error);
|
reject(error);
|
||||||
};
|
};
|
||||||
|
|
||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(file);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export const API: RqbitAPI = {
|
export const API: RqbitAPI = {
|
||||||
listTorrents: async function (): Promise<ListTorrentsResponse> {
|
listTorrents: async function (): Promise<ListTorrentsResponse> {
|
||||||
return await invokeAPI<ListTorrentsResponse>("torrents_list");
|
return await invokeAPI<ListTorrentsResponse>("torrents_list");
|
||||||
},
|
},
|
||||||
getTorrentDetails: async function (id: number): Promise<TorrentDetails> {
|
getTorrentDetails: async function (id: number): Promise<TorrentDetails> {
|
||||||
return await invokeAPI<TorrentDetails>("torrent_details", { id });
|
return await invokeAPI<TorrentDetails>("torrent_details", { id });
|
||||||
},
|
},
|
||||||
getTorrentStats: async function (id: number): Promise<TorrentStats> {
|
getTorrentStats: async function (id: number): Promise<TorrentStats> {
|
||||||
return await invokeAPI<TorrentStats>("torrent_stats", { id });
|
return await invokeAPI<TorrentStats>("torrent_stats", { id });
|
||||||
},
|
},
|
||||||
uploadTorrent: async function (data, opts): Promise<AddTorrentResponse> {
|
uploadTorrent: async function (data, opts): Promise<AddTorrentResponse> {
|
||||||
if (data instanceof File) {
|
if (data instanceof File) {
|
||||||
let contents = await readFileAsBase64(data);
|
let contents = await readFileAsBase64(data);
|
||||||
return await invokeAPI<AddTorrentResponse>("torrent_create_from_base64_file", {
|
return await invokeAPI<AddTorrentResponse>(
|
||||||
contents,
|
"torrent_create_from_base64_file",
|
||||||
opts: opts ?? {},
|
{
|
||||||
});
|
contents,
|
||||||
|
opts: opts ?? {},
|
||||||
}
|
}
|
||||||
return await invokeAPI<AddTorrentResponse>("torrent_create_from_url", {
|
);
|
||||||
url: data,
|
|
||||||
opts: opts ?? {},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
pause: function (id: number): Promise<void> {
|
|
||||||
return invokeAPI<void>("torrent_action_pause", { id });
|
|
||||||
},
|
|
||||||
start: function (id: number): Promise<void> {
|
|
||||||
return invokeAPI<void>("torrent_action_start", { id });
|
|
||||||
},
|
|
||||||
forget: function (id: number): Promise<void> {
|
|
||||||
return invokeAPI<void>("torrent_action_forget", { id });
|
|
||||||
},
|
|
||||||
delete: function (id: number): Promise<void> {
|
|
||||||
return invokeAPI<void>("torrent_action_delete", { id });
|
|
||||||
}
|
}
|
||||||
}
|
return await invokeAPI<AddTorrentResponse>("torrent_create_from_url", {
|
||||||
|
url: data,
|
||||||
|
opts: opts ?? {},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
pause: function (id: number): Promise<void> {
|
||||||
|
return invokeAPI<void>("torrent_action_pause", { id });
|
||||||
|
},
|
||||||
|
start: function (id: number): Promise<void> {
|
||||||
|
return invokeAPI<void>("torrent_action_start", { id });
|
||||||
|
},
|
||||||
|
forget: function (id: number): Promise<void> {
|
||||||
|
return invokeAPI<void>("torrent_action_forget", { id });
|
||||||
|
},
|
||||||
|
delete: function (id: number): Promise<void> {
|
||||||
|
return invokeAPI<void>("torrent_action_delete", { id });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -3,48 +3,48 @@ type Duration = string;
|
||||||
type SocketAddr = string;
|
type SocketAddr = string;
|
||||||
|
|
||||||
interface RqbitDesktopConfigDht {
|
interface RqbitDesktopConfigDht {
|
||||||
disable: boolean;
|
disable: boolean;
|
||||||
disable_persistence: boolean;
|
disable_persistence: boolean;
|
||||||
persistence_filename: PathLike;
|
persistence_filename: PathLike;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RqbitDesktopConfigTcpListen {
|
interface RqbitDesktopConfigTcpListen {
|
||||||
disable: boolean;
|
disable: boolean;
|
||||||
min_port: number;
|
min_port: number;
|
||||||
max_port: number;
|
max_port: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RqbitDesktopConfigPersistence {
|
interface RqbitDesktopConfigPersistence {
|
||||||
disable: boolean;
|
disable: boolean;
|
||||||
filename: PathLike;
|
filename: PathLike;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RqbitDesktopConfigPeerOpts {
|
interface RqbitDesktopConfigPeerOpts {
|
||||||
connect_timeout: Duration;
|
connect_timeout: Duration;
|
||||||
read_write_timeout: Duration;
|
read_write_timeout: Duration;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RqbitDesktopConfigHttpApi {
|
interface RqbitDesktopConfigHttpApi {
|
||||||
disable: boolean;
|
disable: boolean;
|
||||||
listen_addr: SocketAddr;
|
listen_addr: SocketAddr;
|
||||||
read_only: boolean;
|
read_only: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RqbitDesktopConfigUpnp {
|
interface RqbitDesktopConfigUpnp {
|
||||||
disable: boolean;
|
disable: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RqbitDesktopConfig {
|
export interface RqbitDesktopConfig {
|
||||||
default_download_location: PathLike;
|
default_download_location: PathLike;
|
||||||
dht: RqbitDesktopConfigDht;
|
dht: RqbitDesktopConfigDht;
|
||||||
tcp_listen: RqbitDesktopConfigTcpListen;
|
tcp_listen: RqbitDesktopConfigTcpListen;
|
||||||
upnp: RqbitDesktopConfigUpnp;
|
upnp: RqbitDesktopConfigUpnp;
|
||||||
persistence: RqbitDesktopConfigPersistence;
|
persistence: RqbitDesktopConfigPersistence;
|
||||||
peer_opts: RqbitDesktopConfigPeerOpts;
|
peer_opts: RqbitDesktopConfigPeerOpts;
|
||||||
http_api: RqbitDesktopConfigHttpApi;
|
http_api: RqbitDesktopConfigHttpApi;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CurrentDesktopState {
|
export interface CurrentDesktopState {
|
||||||
config: RqbitDesktopConfig | null,
|
config: RqbitDesktopConfig | null;
|
||||||
configured: boolean,
|
configured: boolean;
|
||||||
}
|
}
|
||||||
|
|
@ -6,310 +6,314 @@ import { invokeAPI } from "./api";
|
||||||
import { ErrorDetails } from "./rqbit-webui-src/api-types";
|
import { ErrorDetails } from "./rqbit-webui-src/api-types";
|
||||||
|
|
||||||
const FormCheck: React.FC<{
|
const FormCheck: React.FC<{
|
||||||
label: string,
|
label: string;
|
||||||
name: string,
|
name: string;
|
||||||
checked: boolean,
|
checked: boolean;
|
||||||
onChange: (e: any) => void,
|
onChange: (e: any) => void;
|
||||||
disabled?: boolean,
|
disabled?: boolean;
|
||||||
help?: string,
|
help?: string;
|
||||||
}> = ({ label, name, checked, onChange, disabled, help }) => {
|
}> = ({ label, name, checked, onChange, disabled, help }) => {
|
||||||
return <Form.Group as={Row} controlId={name} className="mb-3">
|
return (
|
||||||
<Form.Label className="col-4">{label}</Form.Label>
|
<Form.Group as={Row} controlId={name} className="mb-3">
|
||||||
<div className="col-8">
|
<Form.Label className="col-4">{label}</Form.Label>
|
||||||
<Form.Check
|
<div className="col-8">
|
||||||
type="switch"
|
<Form.Check
|
||||||
name={name}
|
type="switch"
|
||||||
checked={checked}
|
name={name}
|
||||||
onChange={onChange}
|
checked={checked}
|
||||||
disabled={disabled}
|
onChange={onChange}
|
||||||
/>
|
disabled={disabled}
|
||||||
</div>
|
/>
|
||||||
{help && <div className="form-text">{help}</div>}
|
</div>
|
||||||
|
{help && <div className="form-text">{help}</div>}
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
}
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const FormInput: React.FC<{
|
const FormInput: React.FC<{
|
||||||
label: string,
|
label: string;
|
||||||
name: string,
|
name: string;
|
||||||
value: string | number,
|
value: string | number;
|
||||||
inputType: string,
|
inputType: string;
|
||||||
onChange: (e: any) => void,
|
onChange: (e: any) => void;
|
||||||
disabled?: boolean,
|
disabled?: boolean;
|
||||||
help?: string
|
help?: string;
|
||||||
}> = ({ label, name, value, inputType, onChange, disabled, help }) => {
|
}> = ({ label, name, value, inputType, onChange, disabled, help }) => {
|
||||||
return <Form.Group as={Row} controlId={name} className="mb-3">
|
return (
|
||||||
<Form.Label className="col-4 col-form-label">{label}</Form.Label>
|
<Form.Group as={Row} controlId={name} className="mb-3">
|
||||||
<div className="col-8">
|
<Form.Label className="col-4 col-form-label">{label}</Form.Label>
|
||||||
<Form.Control
|
<div className="col-8">
|
||||||
type={inputType}
|
<Form.Control
|
||||||
name={name}
|
type={inputType}
|
||||||
value={value}
|
name={name}
|
||||||
onChange={onChange}
|
value={value}
|
||||||
disabled={disabled}
|
onChange={onChange}
|
||||||
/>
|
disabled={disabled}
|
||||||
</div>
|
/>
|
||||||
{help && <div className="form-text">{help}</div>}
|
</div>
|
||||||
|
{help && <div className="form-text">{help}</div>}
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
}
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const ConfigModal: React.FC<{
|
export const ConfigModal: React.FC<{
|
||||||
show: boolean,
|
show: boolean;
|
||||||
handleStartReconfigure: () => void,
|
handleStartReconfigure: () => void;
|
||||||
handleConfigured: (config: RqbitDesktopConfig) => void,
|
handleConfigured: (config: RqbitDesktopConfig) => void;
|
||||||
handleCancel?: () => void,
|
handleCancel?: () => void;
|
||||||
initialConfig: RqbitDesktopConfig,
|
initialConfig: RqbitDesktopConfig;
|
||||||
defaultConfig: RqbitDesktopConfig,
|
defaultConfig: RqbitDesktopConfig;
|
||||||
}> = ({ show, handleStartReconfigure, handleConfigured, handleCancel, initialConfig, defaultConfig }) => {
|
}> = ({
|
||||||
let [config, setConfig] = useState<RqbitDesktopConfig>(initialConfig);
|
show,
|
||||||
let [loading, setLoading] = useState<boolean>(false);
|
handleStartReconfigure,
|
||||||
|
handleConfigured,
|
||||||
|
handleCancel,
|
||||||
|
initialConfig,
|
||||||
|
defaultConfig,
|
||||||
|
}) => {
|
||||||
|
let [config, setConfig] = useState<RqbitDesktopConfig>(initialConfig);
|
||||||
|
let [loading, setLoading] = useState<boolean>(false);
|
||||||
|
|
||||||
const [error, setError] = useState<any | null>(null);
|
const [error, setError] = useState<any | null>(null);
|
||||||
|
|
||||||
const handleInputChange = (e: any) => {
|
const handleInputChange = (e: any) => {
|
||||||
const name: string = e.target.name;
|
const name: string = e.target.name;
|
||||||
const value: any = e.target.value;
|
const value: any = e.target.value;
|
||||||
const [mainField, subField] = name.split('.', 2);
|
const [mainField, subField] = name.split(".", 2);
|
||||||
|
|
||||||
if (subField) {
|
if (subField) {
|
||||||
setConfig((prevConfig: any) => ({
|
setConfig((prevConfig: any) => ({
|
||||||
...prevConfig,
|
...prevConfig,
|
||||||
[mainField]: {
|
[mainField]: {
|
||||||
...prevConfig[mainField],
|
...prevConfig[mainField],
|
||||||
[subField]: value,
|
[subField]: value,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
setConfig((prevConfig) => ({
|
setConfig((prevConfig) => ({
|
||||||
...prevConfig,
|
...prevConfig,
|
||||||
[name]: value,
|
[name]: value,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleToggleChange = (e: any) => {
|
const handleToggleChange = (e: any) => {
|
||||||
const name: string = e.target.name;
|
const name: string = e.target.name;
|
||||||
const [mainField, subField] = name.split('.', 2);
|
const [mainField, subField] = name.split(".", 2);
|
||||||
|
|
||||||
if (subField) {
|
if (subField) {
|
||||||
setConfig((prevConfig: any) => ({
|
setConfig((prevConfig: any) => ({
|
||||||
...prevConfig,
|
...prevConfig,
|
||||||
[mainField]: {
|
[mainField]: {
|
||||||
...prevConfig[mainField],
|
...prevConfig[mainField],
|
||||||
[subField]: !prevConfig[mainField][subField],
|
[subField]: !prevConfig[mainField][subField],
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
setConfig((prevConfig: any) => ({
|
setConfig((prevConfig: any) => ({
|
||||||
...prevConfig,
|
...prevConfig,
|
||||||
[name]: !prevConfig[name],
|
[name]: !prevConfig[name],
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOkClick = () => {
|
const handleOkClick = () => {
|
||||||
setError(null);
|
setError(null);
|
||||||
handleStartReconfigure();
|
handleStartReconfigure();
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
invokeAPI<{}>("config_change", { config }).then(
|
invokeAPI<{}>("config_change", { config }).then(
|
||||||
() => {
|
() => {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
handleConfigured(config);
|
handleConfigured(config);
|
||||||
},
|
},
|
||||||
(e: ErrorDetails) => {
|
(e: ErrorDetails) => {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setError({
|
setError({
|
||||||
text: "Error saving configuration",
|
text: "Error saving configuration",
|
||||||
details: e,
|
details: e,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal show={show} size='xl' onHide={handleCancel}>
|
|
||||||
<Modal.Header closeButton>
|
|
||||||
<Modal.Title>Configure Rqbit desktop</Modal.Title>
|
|
||||||
</Modal.Header>
|
|
||||||
<Modal.Body>
|
|
||||||
<ErrorComponent error={error}></ErrorComponent>
|
|
||||||
<Tabs
|
|
||||||
defaultActiveKey="home"
|
|
||||||
id="rqbit-config"
|
|
||||||
className="mb-3">
|
|
||||||
|
|
||||||
<Tab className="mb-3" eventKey="home" title="Home">
|
|
||||||
<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."
|
|
||||||
/>
|
|
||||||
</Tab>
|
|
||||||
|
|
||||||
<Tab className="mb-3" eventKey="dht" title="DHT">
|
|
||||||
<legend>DHT config</legend>
|
|
||||||
|
|
||||||
<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"
|
|
||||||
/>
|
|
||||||
</Tab>
|
|
||||||
|
|
||||||
<Tab className="mb-3" eventKey="tcp_listen" title="TCP">
|
|
||||||
<legend>TCP Listener config</legend>
|
|
||||||
|
|
||||||
<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 over UPnP"
|
|
||||||
name="tcp_listen.disable"
|
|
||||||
checked={!config.tcp_listen.disable}
|
|
||||||
onChange={handleToggleChange}
|
|
||||||
help="Advertise your port over UPnP. 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."
|
|
||||||
/>
|
|
||||||
</Tab>
|
|
||||||
|
|
||||||
|
|
||||||
<Tab className="mb-3" eventKey="session_persistence" title="Session">
|
|
||||||
<legend>Session persistence</legend>
|
|
||||||
|
|
||||||
<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 filename"
|
|
||||||
name="persistence.filename"
|
|
||||||
inputType="text"
|
|
||||||
value={config.persistence.filename}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
disabled={config.persistence.disable}
|
|
||||||
/>
|
|
||||||
</Tab>
|
|
||||||
|
|
||||||
<Tab className="mb-3" eventKey="peer_opts" title="Peer options">
|
|
||||||
<legend>Peer connection options</legend>
|
|
||||||
|
|
||||||
<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."
|
|
||||||
/>
|
|
||||||
</Tab>
|
|
||||||
|
|
||||||
<Tab className="mb-3" eventKey="http_api" title="HTTP API">
|
|
||||||
<legend>HTTP API config</legend>
|
|
||||||
|
|
||||||
<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}`}
|
|
||||||
/>
|
|
||||||
</Tab>
|
|
||||||
|
|
||||||
</Tabs>
|
|
||||||
</Modal.Body>
|
|
||||||
<Modal.Footer>
|
|
||||||
|
|
||||||
{!!handleCancel &&
|
|
||||||
<Button variant="secondary" onClick={handleCancel}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
<Button variant="secondary" onClick={() => setConfig(defaultConfig)}>
|
|
||||||
Reset to defaults
|
|
||||||
</Button>
|
|
||||||
<Button variant="primary" onClick={handleOkClick} disabled={loading}>
|
|
||||||
OK
|
|
||||||
</Button>
|
|
||||||
</Modal.Footer>
|
|
||||||
</Modal>
|
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal show={show} size="xl" onHide={handleCancel}>
|
||||||
|
<Modal.Header closeButton>
|
||||||
|
<Modal.Title>Configure Rqbit desktop</Modal.Title>
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Body>
|
||||||
|
<ErrorComponent error={error}></ErrorComponent>
|
||||||
|
<Tabs defaultActiveKey="home" id="rqbit-config" className="mb-3">
|
||||||
|
<Tab className="mb-3" eventKey="home" title="Home">
|
||||||
|
<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."
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
|
|
||||||
|
<Tab className="mb-3" eventKey="dht" title="DHT">
|
||||||
|
<legend>DHT config</legend>
|
||||||
|
|
||||||
|
<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"
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
|
|
||||||
|
<Tab className="mb-3" eventKey="tcp_listen" title="TCP">
|
||||||
|
<legend>TCP Listener config</legend>
|
||||||
|
|
||||||
|
<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 over UPnP"
|
||||||
|
name="tcp_listen.disable"
|
||||||
|
checked={!config.tcp_listen.disable}
|
||||||
|
onChange={handleToggleChange}
|
||||||
|
help="Advertise your port over UPnP. 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."
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
|
|
||||||
|
<Tab className="mb-3" eventKey="session_persistence" title="Session">
|
||||||
|
<legend>Session persistence</legend>
|
||||||
|
|
||||||
|
<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 filename"
|
||||||
|
name="persistence.filename"
|
||||||
|
inputType="text"
|
||||||
|
value={config.persistence.filename}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
disabled={config.persistence.disable}
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
|
|
||||||
|
<Tab className="mb-3" eventKey="peer_opts" title="Peer options">
|
||||||
|
<legend>Peer connection options</legend>
|
||||||
|
|
||||||
|
<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."
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
|
|
||||||
|
<Tab className="mb-3" eventKey="http_api" title="HTTP API">
|
||||||
|
<legend>HTTP API config</legend>
|
||||||
|
|
||||||
|
<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}`}
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
</Modal.Body>
|
||||||
|
<Modal.Footer>
|
||||||
|
{!!handleCancel && (
|
||||||
|
<Button variant="secondary" onClick={handleCancel}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button variant="secondary" onClick={() => setConfig(defaultConfig)}>
|
||||||
|
Reset to defaults
|
||||||
|
</Button>
|
||||||
|
<Button variant="primary" onClick={handleOkClick} disabled={loading}>
|
||||||
|
OK
|
||||||
|
</Button>
|
||||||
|
</Modal.Footer>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { StrictMode } from "react";
|
import { StrictMode } from "react";
|
||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from "react-dom/client";
|
||||||
import { APIContext } from "./rqbit-webui-src/rqbit-web";
|
import { APIContext } from "./rqbit-webui-src/rqbit-web";
|
||||||
import { API } from "./api";
|
import { API } from "./api";
|
||||||
import { invoke } from "@tauri-apps/api";
|
import { invoke } from "@tauri-apps/api";
|
||||||
|
|
@ -7,23 +7,29 @@ import { CurrentDesktopState, RqbitDesktopConfig } from "./configuration";
|
||||||
import { RqbitDesktop } from "./rqbit-desktop";
|
import { RqbitDesktop } from "./rqbit-desktop";
|
||||||
|
|
||||||
async function get_version(): Promise<string> {
|
async function get_version(): Promise<string> {
|
||||||
return invoke<string>("get_version");
|
return invoke<string>("get_version");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function get_default_config(): Promise<RqbitDesktopConfig> {
|
async function get_default_config(): Promise<RqbitDesktopConfig> {
|
||||||
return invoke<RqbitDesktopConfig>("config_default");
|
return invoke<RqbitDesktopConfig>("config_default");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function get_current_config(): Promise<CurrentDesktopState> {
|
async function get_current_config(): Promise<CurrentDesktopState> {
|
||||||
return invoke<CurrentDesktopState>("config_current");
|
return invoke<CurrentDesktopState>("config_current");
|
||||||
}
|
}
|
||||||
|
|
||||||
Promise.all([get_version(), get_default_config(), get_current_config()]).then(([version, defaultConfig, currentState]) => {
|
Promise.all([get_version(), get_default_config(), get_current_config()]).then(
|
||||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
([version, defaultConfig, currentState]) => {
|
||||||
<StrictMode>
|
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||||
<APIContext.Provider value={API}>
|
<StrictMode>
|
||||||
<RqbitDesktop version={version} defaultConfig={defaultConfig} currentState={currentState} />
|
<APIContext.Provider value={API}>
|
||||||
</APIContext.Provider>
|
<RqbitDesktop
|
||||||
</StrictMode>
|
version={version}
|
||||||
|
defaultConfig={defaultConfig}
|
||||||
|
currentState={currentState}
|
||||||
|
/>
|
||||||
|
</APIContext.Provider>
|
||||||
|
</StrictMode>
|
||||||
);
|
);
|
||||||
})
|
}
|
||||||
|
);
|
||||||
|
|
|
||||||
|
|
@ -3,41 +3,49 @@ import { RqbitWebUI } from "./rqbit-webui-src/rqbit-web";
|
||||||
import { CurrentDesktopState, RqbitDesktopConfig } from "./configuration";
|
import { CurrentDesktopState, RqbitDesktopConfig } from "./configuration";
|
||||||
import { ConfigModal } from "./configure";
|
import { ConfigModal } from "./configure";
|
||||||
|
|
||||||
|
|
||||||
export const RqbitDesktop: React.FC<{
|
export const RqbitDesktop: React.FC<{
|
||||||
version: string,
|
version: string;
|
||||||
defaultConfig: RqbitDesktopConfig,
|
defaultConfig: RqbitDesktopConfig;
|
||||||
currentState: CurrentDesktopState,
|
currentState: CurrentDesktopState;
|
||||||
}> = ({ version, defaultConfig, currentState }) => {
|
}> = ({ version, defaultConfig, currentState }) => {
|
||||||
let [configured, setConfigured] = useState<boolean>(currentState.configured);
|
let [configured, setConfigured] = useState<boolean>(currentState.configured);
|
||||||
let [config, setConfig] = useState<RqbitDesktopConfig>(currentState.config ?? defaultConfig);
|
let [config, setConfig] = useState<RqbitDesktopConfig>(
|
||||||
let [configurationOpened, setConfigurationOpened] = useState<boolean>(false);
|
currentState.config ?? defaultConfig
|
||||||
|
);
|
||||||
|
let [configurationOpened, setConfigurationOpened] = useState<boolean>(false);
|
||||||
|
|
||||||
return <>
|
return (
|
||||||
{configured && <RqbitWebUI title={`Rqbit Desktop v${version}`}></RqbitWebUI>}
|
<>
|
||||||
{configured && <a
|
{configured && (
|
||||||
className="bi bi-sliders2 position-absolute top-0 start-0 p-3 text-primary"
|
<RqbitWebUI title={`Rqbit Desktop v${version}`}></RqbitWebUI>
|
||||||
onClick={(e) => {
|
)}
|
||||||
e.stopPropagation();
|
{configured && (
|
||||||
setConfigurationOpened(true);
|
<a
|
||||||
}}
|
className="bi bi-sliders2 position-absolute top-0 start-0 p-3 text-primary"
|
||||||
href="#"
|
onClick={(e) => {
|
||||||
aria-label="Settings" />}
|
e.stopPropagation();
|
||||||
<ConfigModal
|
setConfigurationOpened(true);
|
||||||
show={!configured || configurationOpened}
|
}}
|
||||||
handleStartReconfigure={() => {
|
href="#"
|
||||||
setConfigured(false);
|
aria-label="Settings"
|
||||||
}}
|
|
||||||
handleCancel={() => {
|
|
||||||
setConfigurationOpened(false);
|
|
||||||
}}
|
|
||||||
handleConfigured={(config) => {
|
|
||||||
setConfig(config);
|
|
||||||
setConfigurationOpened(false);
|
|
||||||
setConfigured(true);
|
|
||||||
}}
|
|
||||||
initialConfig={config}
|
|
||||||
defaultConfig={defaultConfig}
|
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
<ConfigModal
|
||||||
|
show={!configured || configurationOpened}
|
||||||
|
handleStartReconfigure={() => {
|
||||||
|
setConfigured(false);
|
||||||
|
}}
|
||||||
|
handleCancel={() => {
|
||||||
|
setConfigurationOpened(false);
|
||||||
|
}}
|
||||||
|
handleConfigured={(config) => {
|
||||||
|
setConfig(config);
|
||||||
|
setConfigurationOpened(false);
|
||||||
|
setConfigured(true);
|
||||||
|
}}
|
||||||
|
initialConfig={config}
|
||||||
|
defaultConfig={defaultConfig}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
}
|
);
|
||||||
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue