setup vscode for consistent JS formatting

This commit is contained in:
Igor Katson 2023-12-07 14:11:12 +00:00
parent a641717245
commit ec63e1cef7
No known key found for this signature in database
GPG key ID: B4EC22B66D61A3F5
15 changed files with 1675 additions and 1338 deletions

6
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,6 @@
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}

View file

@ -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"> </html>
<!-- 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>

View file

@ -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",

View file

@ -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",

View file

@ -14,7 +14,8 @@
"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"
} }
} }

View file

@ -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>;
} }

View file

@ -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;
},
};

View file

@ -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

View file

@ -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"> </html>
<!-- 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>

View file

@ -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 });
},
};

View file

@ -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;
} }

View file

@ -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>
);
}; };

View file

@ -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>
); );
}) }
);

View file

@ -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}
/>
</> </>
} );
};