From ec63e1cef7877ee83559d87d042cb60da7d1d3f2 Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Thu, 7 Dec 2023 14:11:12 +0000 Subject: [PATCH] setup vscode for consistent JS formatting --- .vscode/settings.json | 6 + crates/librqbit/webui/index.html | 44 +- .../webui/node_modules/.package-lock.json | 15 + crates/librqbit/webui/package-lock.json | 16 + crates/librqbit/webui/package.json | 3 +- crates/librqbit/webui/src/api-types.ts | 179 +- crates/librqbit/webui/src/http-api.ts | 198 ++- crates/librqbit/webui/src/main.tsx | 44 +- crates/librqbit/webui/src/rqbit-web.tsx | 1577 ++++++++++------- desktop/index.html | 32 +- desktop/src/api.tsx | 161 +- desktop/src/configuration.tsx | 48 +- desktop/src/configure.tsx | 586 +++--- desktop/src/main.tsx | 30 +- desktop/src/rqbit-desktop.tsx | 74 +- 15 files changed, 1675 insertions(+), 1338 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..8b856a5 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "editor.defaultFormatter": "esbenp.prettier-vscode", + "[javascript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + } +} diff --git a/crates/librqbit/webui/index.html b/crates/librqbit/webui/index.html index 7499433..b155cbf 100644 --- a/crates/librqbit/webui/index.html +++ b/crates/librqbit/webui/index.html @@ -1,21 +1,27 @@ - + + + + + rqbit web 4.0.0-beta.0 + + + + + - - - - rqbit web 4.0.0-beta.0 - - - - - - - -
- - - - \ No newline at end of file + +
+ + + diff --git a/crates/librqbit/webui/node_modules/.package-lock.json b/crates/librqbit/webui/node_modules/.package-lock.json index 19778b0..3a23394 100644 --- a/crates/librqbit/webui/node_modules/.package-lock.json +++ b/crates/librqbit/webui/node_modules/.package-lock.json @@ -304,6 +304,21 @@ "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": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", diff --git a/crates/librqbit/webui/package-lock.json b/crates/librqbit/webui/package-lock.json index cf75918..d8e0a2b 100644 --- a/crates/librqbit/webui/package-lock.json +++ b/crates/librqbit/webui/package-lock.json @@ -12,6 +12,7 @@ "devDependencies": { "@types/react": "^18.2.38", "@types/react-dom": "^18.2.16", + "prettier": "3.1.0", "typescript": "^5.3.2", "vite": "^4.5.1" } @@ -653,6 +654,21 @@ "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": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", diff --git a/crates/librqbit/webui/package.json b/crates/librqbit/webui/package.json index 67b6b0b..9f8ec6a 100644 --- a/crates/librqbit/webui/package.json +++ b/crates/librqbit/webui/package.json @@ -14,7 +14,8 @@ "devDependencies": { "@types/react": "^18.2.38", "@types/react-dom": "^18.2.16", + "prettier": "3.1.0", "typescript": "^5.3.2", "vite": "^4.5.1" } -} \ No newline at end of file +} diff --git a/crates/librqbit/webui/src/api-types.ts b/crates/librqbit/webui/src/api-types.ts index 1a5fd44..cf6956b 100644 --- a/crates/librqbit/webui/src/api-types.ts +++ b/crates/librqbit/webui/src/api-types.ts @@ -1,131 +1,132 @@ // Interface for the Torrent API response export interface TorrentId { - id: number; - info_hash: string; + id: number; + info_hash: string; } export interface TorrentFile { - name: string; - length: number; - included: boolean; + name: string; + length: number; + included: boolean; } // Interface for the Torrent Details API response export interface TorrentDetails { - info_hash: string, - files: Array; + info_hash: string; + files: Array; } export interface AddTorrentResponse { - id: number | null; - details: TorrentDetails; - output_folder: string, - seen_peers?: Array; + id: number | null; + details: TorrentDetails; + output_folder: string; + seen_peers?: Array; } export interface ListTorrentsResponse { - torrents: Array; + torrents: Array; } export interface Speed { - mbps: number; - human_readable: string; + mbps: number; + human_readable: string; } // Interface for the Torrent Stats API response export interface LiveTorrentStats { - snapshot: { - have_bytes: number; - downloaded_and_checked_bytes: number; - downloaded_and_checked_pieces: number; - fetched_bytes: number; - uploaded_bytes: number; - initially_needed_bytes: number; - remaining_bytes: number; - total_bytes: number; - total_piece_download_ms: number; - peer_stats: { - queued: number; - connecting: number; - live: number; - seen: number; - dead: number; - not_needed: number; - }; + snapshot: { + have_bytes: number; + downloaded_and_checked_bytes: number; + downloaded_and_checked_pieces: number; + fetched_bytes: number; + uploaded_bytes: number; + initially_needed_bytes: number; + remaining_bytes: number; + total_bytes: number; + total_piece_download_ms: number; + peer_stats: { + queued: number; + connecting: number; + live: number; + seen: number; + dead: number; + not_needed: number; }; - average_piece_download_time: { - secs: number; - nanos: number; + }; + average_piece_download_time: { + 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; - upload_speed: Speed; - all_time_download_speed: { - mbps: number; - human_readable: string; - }; - time_remaining: { - human_readable: string; - duration?: { - secs: number, - } - } | null; + } | null; } -export const STATE_INITIALIZING = 'initializing'; -export const STATE_PAUSED = 'paused'; -export const STATE_LIVE = 'live'; -export const STATE_ERROR = 'error'; +export const STATE_INITIALIZING = "initializing"; +export const STATE_PAUSED = "paused"; +export const STATE_LIVE = "live"; +export const STATE_ERROR = "error"; export interface TorrentStats { - state: 'initializing' | 'paused' | 'live' | 'error', - error: string | null, - progress_bytes: number, - finished: boolean, - total_bytes: number, - live: LiveTorrentStats | null; + state: "initializing" | "paused" | "live" | "error"; + error: string | null; + progress_bytes: number; + finished: boolean; + total_bytes: number; + live: LiveTorrentStats | null; } - export interface ErrorDetails { - id?: number, - method?: string, - path?: string, - status?: number, - statusText?: string, - text: string, -}; + id?: number; + method?: string; + path?: string; + status?: number; + statusText?: string; + text: string; +} export type Duration = number; export interface PeerConnectionOptions { - connect_timeout?: Duration | null; - read_write_timeout?: Duration | null; - keep_alive_interval?: Duration | null; + connect_timeout?: Duration | null; + read_write_timeout?: Duration | null; + keep_alive_interval?: Duration | null; } export interface AddTorrentOptions { - paused?: boolean; - only_files_regex?: string | null; - only_files?: number[] | null; - overwrite?: boolean; - list_only?: boolean; - output_folder?: string | null; - sub_folder?: string | null; - peer_opts?: PeerConnectionOptions | null; - force_tracker_interval?: Duration | null; - initial_peers?: string[] | null; // Assuming SocketAddr is equivalent to a string in TypeScript - preferred_id?: number | null; + paused?: boolean; + only_files_regex?: string | null; + only_files?: number[] | null; + overwrite?: boolean; + list_only?: boolean; + output_folder?: string | null; + sub_folder?: string | null; + peer_opts?: PeerConnectionOptions | null; + force_tracker_interval?: Duration | null; + initial_peers?: string[] | null; // Assuming SocketAddr is equivalent to a string in TypeScript + preferred_id?: number | null; } - export interface RqbitAPI { - listTorrents: () => Promise, - getTorrentDetails: (index: number) => Promise, - getTorrentStats: (index: number) => Promise; - uploadTorrent: (data: string | File, opts?: AddTorrentOptions) => Promise; + listTorrents: () => Promise; + getTorrentDetails: (index: number) => Promise; + getTorrentStats: (index: number) => Promise; + uploadTorrent: ( + data: string | File, + opts?: AddTorrentOptions + ) => Promise; - pause: (index: number) => Promise; - start: (index: number) => Promise; - forget: (index: number) => Promise; - delete: (index: number) => Promise; -} \ No newline at end of file + pause: (index: number) => Promise; + start: (index: number) => Promise; + forget: (index: number) => Promise; + delete: (index: number) => Promise; +} diff --git a/crates/librqbit/webui/src/http-api.ts b/crates/librqbit/webui/src/http-api.ts index b485912..7117966 100644 --- a/crates/librqbit/webui/src/http-api.ts +++ b/crates/librqbit/webui/src/http-api.ts @@ -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 -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 => { - console.log(method, path); - const url = apiUrl + path; - const options: RequestInit = { - method, - headers: { - 'Accept': 'application/json', - }, - body: data, - }; +const makeRequest = async ( + method: string, + path: string, + data?: any +): Promise => { + console.log(method, path); + const url = apiUrl + path; + const options: RequestInit = { + method, + headers: { + Accept: "application/json", + }, + body: data, + }; - let error: ErrorDetails = { - method: method, - path: path, - text: '' - }; + let error: ErrorDetails = { + method: method, + path: path, + 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 { - 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) { - error.text = 'network error'; - return Promise.reject(error); + error.text = errorBody; } - - error.status = response.status; - error.statusText = `${response.status} ${response.statusText}`; - - 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; -} + return Promise.reject(error); + } + const result = await response.json(); + return result; +}; export const API: RqbitAPI & { getVersion: () => Promise } = { - listTorrents: (): Promise => makeRequest('GET', '/torrents'), - getTorrentDetails: (index: number): Promise => { - return makeRequest('GET', `/torrents/${index}`); - }, - getTorrentStats: (index: number): Promise => { - return makeRequest('GET', `/torrents/${index}/stats/v1`); - }, + listTorrents: (): Promise => + makeRequest("GET", "/torrents"), + getTorrentDetails: (index: number): Promise => { + return makeRequest("GET", `/torrents/${index}`); + }, + getTorrentStats: (index: number): Promise => { + return makeRequest("GET", `/torrents/${index}/stats/v1`); + }, - uploadTorrent: (data, opts): Promise => { - let url = '/torrents?&overwrite=true'; - if (opts?.list_only) { - 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 => { - return makeRequest('POST', `/torrents/${index}/pause`); - }, - - start: (index: number): Promise => { - return makeRequest('POST', `/torrents/${index}/start`); - }, - - forget: (index: number): Promise => { - return makeRequest('POST', `/torrents/${index}/forget`); - }, - - delete: (index: number): Promise => { - return makeRequest('POST', `/torrents/${index}/delete`); - }, - getVersion: async (): Promise => { - const r = await makeRequest('GET', '/'); - return r.version; + uploadTorrent: (data, opts): Promise => { + let url = "/torrents?&overwrite=true"; + if (opts?.list_only) { + url += "&list_only=true"; } -} \ No newline at end of file + 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 => { + return makeRequest("POST", `/torrents/${index}/pause`); + }, + + start: (index: number): Promise => { + return makeRequest("POST", `/torrents/${index}/start`); + }, + + forget: (index: number): Promise => { + return makeRequest("POST", `/torrents/${index}/forget`); + }, + + delete: (index: number): Promise => { + return makeRequest("POST", `/torrents/${index}/delete`); + }, + getVersion: async (): Promise => { + const r = await makeRequest("GET", "/"); + return r.version; + }, +}; diff --git a/crates/librqbit/webui/src/main.tsx b/crates/librqbit/webui/src/main.tsx index 07aeeff..dde0bf5 100644 --- a/crates/librqbit/webui/src/main.tsx +++ b/crates/librqbit/webui/src/main.tsx @@ -1,27 +1,33 @@ 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 { API } from "./http-api"; const RootWithVersion = () => { - let [title, setTitle] = useState("rqbit web UI"); - useEffect(() => { - const refreshVersion = () => API.getVersion().then((version) => { - setTitle(`rqbit web UI - v${version}`); - return 10000; - }, (e) => { - return 1000; - }); - return customSetInterval(refreshVersion, 0) - }, []) + let [title, setTitle] = useState("rqbit web UI"); + useEffect(() => { + const refreshVersion = () => + API.getVersion().then( + (version) => { + setTitle(`rqbit web UI - v${version}`); + return 10000; + }, + (e) => { + return 1000; + } + ); + return customSetInterval(refreshVersion, 0); + }, []); - return - - - - ; -} + return ( + + + + + + ); +}; -ReactDOM.createRoot(document.getElementById('app') as HTMLInputElement).render( - +ReactDOM.createRoot(document.getElementById("app") as HTMLInputElement).render( + ); diff --git a/crates/librqbit/webui/src/rqbit-web.tsx b/crates/librqbit/webui/src/rqbit-web.tsx index 846dc26..2a68103 100644 --- a/crates/librqbit/webui/src/rqbit-web.tsx +++ b/crates/librqbit/webui/src/rqbit-web.tsx @@ -1,829 +1,1064 @@ -import { MouseEventHandler, RefObject, createContext, useContext, useEffect, useRef, useState } from 'react'; -import { ProgressBar, Button, Container, Row, Col, Alert, Modal, Form, Spinner } from 'react-bootstrap'; -import { AddTorrentResponse, TorrentDetails, TorrentId, TorrentStats, ErrorDetails as ApiErrorDetails, STATE_INITIALIZING, STATE_LIVE, STATE_PAUSED, STATE_ERROR, RqbitAPI, AddTorrentOptions } from './api-types'; +import { + MouseEventHandler, + RefObject, + createContext, + useContext, + useEffect, + useRef, + useState, +} from "react"; +import { + ProgressBar, + Button, + Container, + Row, + Col, + Alert, + Modal, + Form, + Spinner, +} from "react-bootstrap"; +import { + AddTorrentResponse, + TorrentDetails, + TorrentId, + TorrentStats, + ErrorDetails as ApiErrorDetails, + STATE_INITIALIZING, + STATE_LIVE, + STATE_PAUSED, + STATE_ERROR, + RqbitAPI, + AddTorrentOptions, +} from "./api-types"; export interface Error { - text: string, - details?: ApiErrorDetails, + text: string; + details?: ApiErrorDetails; } interface ContextType { - setCloseableError: (error: Error | null) => void, - refreshTorrents: () => void, + setCloseableError: (error: Error | null) => void; + refreshTorrents: () => void; } export const APIContext = createContext({ - listTorrents: () => { - throw new Error('Function not implemented.'); - }, - getTorrentDetails: () => { - throw new Error('Function not implemented.'); - }, - getTorrentStats: () => { - throw new Error('Function not implemented.'); - }, - uploadTorrent: () => { - throw new Error('Function not implemented.'); - }, - pause: () => { - throw new Error('Function not implemented.'); - }, - start: () => { - throw new Error('Function not implemented.'); - }, - forget: () => { - throw new Error('Function not implemented.'); - }, - delete: () => { - throw new Error('Function not implemented.'); - } + listTorrents: () => { + throw new Error("Function not implemented."); + }, + getTorrentDetails: () => { + throw new Error("Function not implemented."); + }, + getTorrentStats: () => { + throw new Error("Function not implemented."); + }, + uploadTorrent: () => { + throw new Error("Function not implemented."); + }, + pause: () => { + throw new Error("Function not implemented."); + }, + start: () => { + throw new Error("Function not implemented."); + }, + forget: () => { + throw new Error("Function not implemented."); + }, + delete: () => { + throw new Error("Function not implemented."); + }, }); const AppContext = createContext({ - setCloseableError: (_) => { }, - refreshTorrents: () => { }, + setCloseableError: (_) => {}, + refreshTorrents: () => {}, }); -const RefreshTorrentStatsContext = createContext({ refresh: () => { } }); +const RefreshTorrentStatsContext = createContext({ refresh: () => {} }); const IconButton: React.FC<{ - className: string, - onClick: () => void, - disabled?: boolean, - color?: string, + className: string; + onClick: () => void; + disabled?: boolean; + color?: string; }> = ({ className, onClick, disabled, color }) => { - const onClickStopPropagation: MouseEventHandler = (e) => { - e.stopPropagation(); - if (disabled) { - return; - } - onClick(); + const onClickStopPropagation: MouseEventHandler = (e) => { + e.stopPropagation(); + if (disabled) { + return; } - return -} + onClick(); + }; + return ( + + ); +}; const DeleteTorrentModal: React.FC<{ - id: number, - show: boolean, - onHide: () => void + id: number; + show: boolean; + onHide: () => void; }> = ({ id, show, onHide }) => { - if (!show) { - return null; - } - const [deleteFiles, setDeleteFiles] = useState(false); - const [error, setError] = useState(null); - const [deleting, setDeleting] = useState(false); + if (!show) { + return null; + } + const [deleteFiles, setDeleteFiles] = useState(false); + const [error, setError] = useState(null); + const [deleting, setDeleting] = useState(false); - const ctx = useContext(AppContext); - const API = useContext(APIContext); + const ctx = useContext(AppContext); + const API = useContext(APIContext); - const close = () => { - setDeleteFiles(false); - setError(null); + const close = () => { + setDeleteFiles(false); + setError(null); + setDeleting(false); + onHide(); + }; + + const deleteTorrent = () => { + setDeleting(true); + + const call = deleteFiles ? API.delete : API.forget; + + call(id) + .then(() => { + ctx.refreshTorrents(); + close(); + }) + .catch((e) => { + setError({ + text: `Error deleting torrent id=${id}`, + details: e, + }); setDeleting(false); - onHide(); - } + }); + }; - const deleteTorrent = () => { - setDeleting(true); - - const call = deleteFiles ? API.delete : API.forget; - - call(id).then(() => { - ctx.refreshTorrents(); - close(); - }).catch((e) => { - setError({ - text: `Error deleting torrent id=${id}`, - details: e, - }); - setDeleting(false); - }) - } - - return - - Delete torrent - - -
- - setDeleteFiles(!deleteFiles)}> - - -
- {error && } -
- - {deleting && } - - - + return ( + + Delete torrent + +
+ + setDeleteFiles(!deleteFiles)} + > + +
+ {error && } +
+ + {deleting && } + + +
-} + ); +}; const TorrentActions: React.FC<{ - id: number, statsResponse: TorrentStats + id: number; + statsResponse: TorrentStats; }> = ({ id, statsResponse }) => { - let state = statsResponse.state; + let state = statsResponse.state; - let [disabled, setDisabled] = useState(false); - let [deleting, setDeleting] = useState(false); + let [disabled, setDisabled] = useState(false); + let [deleting, setDeleting] = useState(false); - let refreshCtx = useContext(RefreshTorrentStatsContext); + let refreshCtx = useContext(RefreshTorrentStatsContext); - const canPause = state == 'live'; - const canUnpause = state == 'paused' || state == 'error'; + const canPause = state == "live"; + const canUnpause = state == "paused" || state == "error"; - const ctx = useContext(AppContext); - const API = useContext(APIContext); + const ctx = useContext(AppContext); + const API = useContext(APIContext); - const unpause = () => { - setDisabled(true); - API.start(id).then(() => { refreshCtx.refresh() }, (e) => { - ctx.setCloseableError({ - text: `Error starting torrent id=${id}`, - details: e, - }); - }).finally(() => setDisabled(false)) - }; + const unpause = () => { + setDisabled(true); + API.start(id) + .then( + () => { + refreshCtx.refresh(); + }, + (e) => { + ctx.setCloseableError({ + text: `Error starting torrent id=${id}`, + details: e, + }); + } + ) + .finally(() => setDisabled(false)); + }; - const pause = () => { - setDisabled(true); - API.pause(id).then(() => { refreshCtx.refresh() }, (e) => { - ctx.setCloseableError({ - text: `Error pausing torrent id=${id}`, - details: e, - }); - }).finally(() => setDisabled(false)) - }; + const pause = () => { + setDisabled(true); + API.pause(id) + .then( + () => { + refreshCtx.refresh(); + }, + (e) => { + ctx.setCloseableError({ + text: `Error pausing torrent id=${id}`, + details: e, + }); + } + ) + .finally(() => setDisabled(false)); + }; - const startDeleting = () => { - setDisabled(true); - setDeleting(true); - } + const startDeleting = () => { + setDisabled(true); + setDeleting(true); + }; - const cancelDeleting = () => { - setDisabled(false); - setDeleting(false); - } + const cancelDeleting = () => { + setDisabled(false); + setDeleting(false); + }; - return - - {canUnpause && } - {canPause && } - - - + return ( + + + {canUnpause && ( + + )} + {canPause && ( + + )} + + + -} + ); +}; -const Speed: React.FC<{ statsResponse: TorrentStats }> = ({ statsResponse }) => { - switch (statsResponse.state) { - case STATE_PAUSED: return 'Paused'; - case STATE_INITIALIZING: return 'Checking files'; - case STATE_ERROR: return 'Error'; - } - // Unknown state - if (statsResponse.state != 'live' || statsResponse.live === null) { - return statsResponse.state; - } +const Speed: React.FC<{ statsResponse: TorrentStats }> = ({ + statsResponse, +}) => { + switch (statsResponse.state) { + case STATE_PAUSED: + return "Paused"; + case STATE_INITIALIZING: + return "Checking files"; + case STATE_ERROR: + return "Error"; + } + // Unknown state + if (statsResponse.state != "live" || statsResponse.live === null) { + return statsResponse.state; + } - return <> - {!statsResponse.finished && -
↓ {statsResponse.live.download_speed.human_readable}
} -
- ↑ {statsResponse.live.upload_speed.human_readable} - {statsResponse.live.snapshot.uploaded_bytes > 0 && - ({formatBytes(statsResponse.live.snapshot.uploaded_bytes)})}
+ return ( + <> + {!statsResponse.finished && ( +
+ ↓ {statsResponse.live.download_speed.human_readable} +
+ )} +
+ ↑ {statsResponse.live.upload_speed.human_readable} + {statsResponse.live.snapshot.uploaded_bytes > 0 && ( + + {" "} + ({formatBytes(statsResponse.live.snapshot.uploaded_bytes)}) + + )} +
-} + ); +}; const TorrentRow: React.FC<{ - id: number, - detailsResponse: TorrentDetails | null, - statsResponse: TorrentStats | null + id: number; + detailsResponse: TorrentDetails | null; + statsResponse: TorrentStats | null; }> = ({ id, detailsResponse, statsResponse }) => { - const state = statsResponse?.state ?? ""; - const error = statsResponse?.error; - const totalBytes = statsResponse?.total_bytes ?? 1; - const progressBytes = statsResponse?.progress_bytes ?? 0; - const finished = statsResponse?.finished || false; - const progressPercentage = error ? 100 : (progressBytes / totalBytes) * 100; - const isAnimated = (state == STATE_INITIALIZING || state == STATE_LIVE) && !finished; - const progressLabel = error ? 'Error' : `${progressPercentage.toFixed(2)}%`; - const progressBarVariant = error ? 'danger' : finished ? 'success' : state == STATE_INITIALIZING ? 'warning' : 'primary'; + const state = statsResponse?.state ?? ""; + const error = statsResponse?.error; + const totalBytes = statsResponse?.total_bytes ?? 1; + const progressBytes = statsResponse?.progress_bytes ?? 0; + const finished = statsResponse?.finished || false; + const progressPercentage = error ? 100 : (progressBytes / totalBytes) * 100; + const isAnimated = + (state == STATE_INITIALIZING || state == STATE_LIVE) && !finished; + const progressLabel = error ? "Error" : `${progressPercentage.toFixed(2)}%`; + const progressBarVariant = error + ? "danger" + : finished + ? "success" + : state == STATE_INITIALIZING + ? "warning" + : "primary"; - const formatPeersString = () => { - let peer_stats = statsResponse?.live?.snapshot.peer_stats; - if (!peer_stats) { - return ''; - } - return `${peer_stats.live} / ${peer_stats.seen}`; + const formatPeersString = () => { + let peer_stats = statsResponse?.live?.snapshot.peer_stats; + if (!peer_stats) { + return ""; } + return `${peer_stats.live} / ${peer_stats.seen}`; + }; - let classNames = []; + let classNames = []; - if (error) { - classNames.push('bg-warning'); - } else { - if (id % 2 == 0) { - classNames.push('bg-light'); - } + if (error) { + classNames.push("bg-warning"); + } else { + if (id % 2 == 0) { + classNames.push("bg-light"); } + } - return ( - - - {detailsResponse ? - <> -
- {getLargestFileName(detailsResponse)} -
- {error &&

Error: {error}

} - - : } -
- {statsResponse ? - <> - {`${formatBytes(totalBytes)} `} - - - - - - - {getCompletionETA(statsResponse)} - {formatPeersString()} - - - - - : - } - -
- ); -} + return ( + + + {detailsResponse ? ( + <> +
+ {getLargestFileName(detailsResponse)} +
+ {error && ( +

+ Error: {error} +

+ )} + + ) : ( + + )} +
+ {statsResponse ? ( + <> + {`${formatBytes(totalBytes)} `} + + + + + + + {getCompletionETA(statsResponse)} + + {formatPeersString()} + + + + + + ) : ( + + + + )} +
+ ); +}; const Column: React.FC<{ - label: string, - size?: number, - children?: any + label: string; + size?: number; + children?: any; }> = ({ size, label, children }) => ( - -
{label}
- {children} - + +
{label}
+ {children} + ); const Torrent: React.FC<{ - id: number, - torrent: TorrentId + id: number; + torrent: TorrentId; }> = ({ id, torrent }) => { - const [detailsResponse, updateDetailsResponse] = useState(null); - const [statsResponse, updateStatsResponse] = useState(null); - const [forceStatsRefresh, setForceStatsRefresh] = useState(0); - const API = useContext(APIContext); + const [detailsResponse, updateDetailsResponse] = + useState(null); + const [statsResponse, updateStatsResponse] = useState( + null + ); + const [forceStatsRefresh, setForceStatsRefresh] = useState(0); + const API = useContext(APIContext); - const forceStatsRefreshCallback = () => { - setForceStatsRefresh(forceStatsRefresh + 1); + const forceStatsRefreshCallback = () => { + setForceStatsRefresh(forceStatsRefresh + 1); + }; + + // Update details once. + useEffect(() => { + if (detailsResponse === null) { + return loopUntilSuccess(async () => { + await API.getTorrentDetails(torrent.id).then(updateDetailsResponse); + }, 1000); } + }, [detailsResponse]); - // Update details once. - useEffect(() => { - if (detailsResponse === null) { - return loopUntilSuccess(async () => { - await API.getTorrentDetails(torrent.id).then(updateDetailsResponse); - }, 1000); - } - }, [detailsResponse]); - - // Update stats once then forever. - useEffect(() => customSetInterval((async () => { + // Update stats once then forever. + useEffect( + () => + customSetInterval(async () => { const errorInterval = 10000; const liveInterval = 1000; const nonLiveInterval = 10000; - return API.getTorrentStats(torrent.id).then((stats) => { + return API.getTorrentStats(torrent.id) + .then((stats) => { updateStatsResponse(stats); return stats; - }).then((stats) => { - if (stats.state == STATE_INITIALIZING || stats.state == STATE_LIVE) { + }) + .then( + (stats) => { + if ( + stats.state == STATE_INITIALIZING || + stats.state == STATE_LIVE + ) { return liveInterval; + } + return nonLiveInterval; + }, + () => { + return errorInterval; } - return nonLiveInterval; - }, () => { - return errorInterval; - }); - }), 0), [forceStatsRefresh]); + ); + }, 0), + [forceStatsRefresh] + ); - return - - -} + return ( + + + + ); +}; -const TorrentsList = (props: { torrents: Array | null, loading: boolean }) => { - if (props.torrents === null && props.loading) { - return - } - // The app either just started, or there was an error loading torrents. - if (props.torrents === null) { - return; - } +const TorrentsList = (props: { + torrents: Array | null; + loading: boolean; +}) => { + if (props.torrents === null && props.loading) { + return ; + } + // The app either just started, or there was an error loading torrents. + if (props.torrents === null) { + return; + } - if (props.torrents.length === 0) { - return
-

No existing torrents found. Add them through buttons below.

-
; - } - return
- {props.torrents.map((t: TorrentId) => - - )} -
; + if (props.torrents.length === 0) { + return ( +
+

No existing torrents found. Add them through buttons below.

+
+ ); + } + return ( +
+ {props.torrents.map((t: TorrentId) => ( + + ))} +
+ ); }; export const RqbitWebUI = (props: { title: string }) => { - const [closeableError, setCloseableError] = useState(null); - const [otherError, setOtherError] = useState(null); + const [closeableError, setCloseableError] = useState(null); + const [otherError, setOtherError] = useState(null); - const [torrents, setTorrents] = useState | null>(null); - const [torrentsLoading, setTorrentsLoading] = useState(false); - const API = useContext(APIContext); + const [torrents, setTorrents] = useState | null>(null); + const [torrentsLoading, setTorrentsLoading] = useState(false); + const API = useContext(APIContext); - const refreshTorrents = async () => { - setTorrentsLoading(true); - let torrents = await API.listTorrents().finally(() => setTorrentsLoading(false)); - setTorrents(torrents.torrents); - }; + const refreshTorrents = async () => { + setTorrentsLoading(true); + let torrents = await API.listTorrents().finally(() => + setTorrentsLoading(false) + ); + setTorrents(torrents.torrents); + }; - useEffect(() => { - return customSetInterval(async () => - refreshTorrents().then(() => { - setOtherError(null); - return 5000; - }, (e) => { - setOtherError({ text: 'Error refreshing torrents', details: e }); - console.error(e); - return 5000; - }), 0); - }, []); + useEffect(() => { + return customSetInterval( + async () => + refreshTorrents().then( + () => { + setOtherError(null); + return 5000; + }, + (e) => { + setOtherError({ text: "Error refreshing torrents", details: e }); + console.error(e); + return 5000; + } + ), + 0 + ); + }, []); - const context: ContextType = { - setCloseableError, - refreshTorrents, - } + const context: ContextType = { + setCloseableError, + refreshTorrents, + }; - return -
-

{props.title}

- -
-
-} + return ( + +
+

{props.title}

+ +
+
+ ); +}; -const ErrorDetails = (props: { details: ApiErrorDetails | null | undefined }) => { - let { details } = props; - if (!details) { - return null; - } - return <> - {details.statusText &&

{details.statusText}

} -
{details.text}
+const ErrorDetails = (props: { + details: ApiErrorDetails | null | undefined; +}) => { + let { details } = props; + if (!details) { + return null; + } + return ( + <> + {details.statusText && ( +

+ {details.statusText} +

+ )} +
{details.text}
-} + ); +}; -export const ErrorComponent = (props: { error: Error | null, remove?: () => void }) => { - let { error, remove } = props; +export const ErrorComponent = (props: { + error: Error | null; + remove?: () => void; +}) => { + let { error, remove } = props; - if (error == null) { - return null; - } + if (error == null) { + return null; + } - return ( - {error.text} + return ( + + {error.text} - - ); + + + ); }; const UploadButton: React.FC<{ - buttonText: string, - onClick: () => void, - data: string | File | null, - resetData: () => void, - variant: string, + buttonText: string; + onClick: () => void; + data: string | File | null; + resetData: () => void; + variant: string; }> = ({ buttonText, onClick, data, resetData, variant }) => { - const [loading, setLoading] = useState(false); - const [listTorrentResponse, setListTorrentResponse] = useState(null); - const [listTorrentError, setListTorrentError] = useState(null); - const API = useContext(APIContext); + const [loading, setLoading] = useState(false); + const [listTorrentResponse, setListTorrentResponse] = + useState(null); + const [listTorrentError, setListTorrentError] = useState(null); + const API = useContext(APIContext); - // Get the torrent file list if there's data. - useEffect(() => { - if (data === null) { - return; - } - - let t = setTimeout(async () => { - setLoading(true); - try { - const response = await API.uploadTorrent(data, { list_only: true }); - setListTorrentResponse(response); - } catch (e) { - setListTorrentError({ text: 'Error listing torrent files', details: e as ApiErrorDetails }); - } finally { - setLoading(false); - } - }, 0); - return () => clearTimeout(t); - }, [data]); - - const clear = () => { - resetData(); - setListTorrentError(null); - setListTorrentResponse(null); - setLoading(false); + // Get the torrent file list if there's data. + useEffect(() => { + if (data === null) { + return; } - return ( - <> - + let t = setTimeout(async () => { + setLoading(true); + try { + const response = await API.uploadTorrent(data, { list_only: true }); + setListTorrentResponse(response); + } catch (e) { + setListTorrentError({ + text: "Error listing torrent files", + details: e as ApiErrorDetails, + }); + } finally { + setLoading(false); + } + }, 0); + return () => clearTimeout(t); + }, [data]); - {data && } - - ); + const clear = () => { + resetData(); + setListTorrentError(null); + setListTorrentResponse(null); + setLoading(false); + }; + + return ( + <> + + + {data && ( + + )} + + ); }; const UrlPromptModal: React.FC<{ - show: boolean, - setUrl: (_: string) => void, - cancel: () => void, + show: boolean; + setUrl: (_: string) => void; + cancel: () => void; }> = ({ show, setUrl, cancel }) => { - let [inputValue, setInputValue] = useState(''); - return - - Add torrent - - -
- - Enter magnet or HTTP(S) URL to the .torrent - { setInputValue(u.target.value) }} /> - -
-
- - - - -
-} + let [inputValue, setInputValue] = useState(""); + return ( + + + Add torrent + + +
+ + Enter magnet or HTTP(S) URL to the .torrent + { + setInputValue(u.target.value); + }} + /> + +
+
+ + + + +
+ ); +}; const MagnetInput = () => { - let [magnet, setMagnet] = useState(null); + let [magnet, setMagnet] = useState(null); - let [showModal, setShowModal] = useState(false); + let [showModal, setShowModal] = useState(false); - return ( - <> - { - setShowModal(true); - }} - data={magnet} - resetData={() => setMagnet(null)} - /> + return ( + <> + { + setShowModal(true); + }} + data={magnet} + resetData={() => setMagnet(null)} + /> - { - setShowModal(false); - setMagnet(url); - }} - cancel={() => { - setShowModal(false); - setMagnet(null); - }} /> - - ); + { + setShowModal(false); + setMagnet(url); + }} + cancel={() => { + setShowModal(false); + setMagnet(null); + }} + /> + + ); }; const FileInput = () => { - const inputRef = useRef() as RefObject; - const [file, setFile] = useState(null); + const inputRef = useRef() as RefObject; + const [file, setFile] = useState(null); - const onFileChange = async () => { - if (!inputRef?.current?.files) { - return; - } - const file = inputRef.current.files[0]; - setFile(file); - }; - - const reset = () => { - if (!inputRef?.current) { - return; - } - inputRef.current.value = ''; - setFile(null); + const onFileChange = async () => { + if (!inputRef?.current?.files) { + return; } + const file = inputRef.current.files[0]; + setFile(file); + }; - const onClick = () => { - if (!inputRef?.current) { - return; - } - inputRef.current.click(); + const reset = () => { + if (!inputRef?.current) { + return; } + inputRef.current.value = ""; + setFile(null); + }; - return ( - <> - - - - ); + const onClick = () => { + if (!inputRef?.current) { + return; + } + inputRef.current.click(); + }; + + return ( + <> + + + + ); }; const FileSelectionModal = (props: { - onHide: () => void, - listTorrentResponse: AddTorrentResponse | null, - listTorrentError: Error | null, - listTorrentLoading: boolean, - data: string | File, + onHide: () => void; + listTorrentResponse: AddTorrentResponse | null; + listTorrentError: Error | null; + listTorrentLoading: boolean; + data: string | File; }) => { - let { onHide, listTorrentResponse, listTorrentError, listTorrentLoading, data } = props; + let { + onHide, + listTorrentResponse, + listTorrentError, + listTorrentLoading, + data, + } = props; - const [selectedFiles, setSelectedFiles] = useState([]); - const [uploading, setUploading] = useState(false); - const [uploadError, setUploadError] = useState(null); - const [unpopularTorrent, setUnpopularTorrent] = useState(false); - const [outputFolder, setOutputFolder] = useState(''); - const ctx = useContext(AppContext); - const API = useContext(APIContext); + const [selectedFiles, setSelectedFiles] = useState([]); + const [uploading, setUploading] = useState(false); + const [uploadError, setUploadError] = useState(null); + const [unpopularTorrent, setUnpopularTorrent] = useState(false); + const [outputFolder, setOutputFolder] = useState(""); + const ctx = useContext(AppContext); + const API = useContext(APIContext); - useEffect(() => { - console.log(listTorrentResponse); - setSelectedFiles(listTorrentResponse ? listTorrentResponse.details.files.map((_, id) => id) : []); - setOutputFolder(listTorrentResponse?.output_folder || ''); - }, [listTorrentResponse]); - - const clear = () => { - onHide(); - setSelectedFiles([]); - setUploadError(null); - setUploading(false); - } - - const handleToggleFile = (toggledId: number) => { - if (selectedFiles.includes(toggledId)) { - setSelectedFiles(selectedFiles.filter((i) => i !== toggledId)); - } else { - setSelectedFiles([...selectedFiles, toggledId]); - } - }; - - const handleUpload = async () => { - if (!listTorrentResponse) { - return; - } - setUploading(true); - let initialPeers = listTorrentResponse.seen_peers ? listTorrentResponse.seen_peers.slice(0, 32) : null; - let opts: AddTorrentOptions = { - overwrite: true, - only_files: selectedFiles, - initial_peers: initialPeers, - output_folder: outputFolder, - }; - if (unpopularTorrent) { - opts.peer_opts = { - connect_timeout: 20, - read_write_timeout: 60, - }; - } - API.uploadTorrent(data, opts).then(() => { - onHide(); - ctx.refreshTorrents(); - }, - (e) => { - setUploadError({ text: 'Error starting torrent', details: e }); - } - ).finally(() => setUploading(false)); - }; - - const getBody = () => { - if (listTorrentLoading) { - return ; - } else if (listTorrentError) { - return ; - } else if (listTorrentResponse) { - return
-
- Pick the files to download - {listTorrentResponse.details.files.map((file, index) => ( - - handleToggleFile(index)}> - - - ))} -
-
- Options - - Output folder - setOutputFolder(e.target.value)} - /> - - - setUnpopularTorrent(!unpopularTorrent)}> - - This might be useful for unpopular torrents with few peers. It will slow down fast torrents though. - -
-
- } - }; - - return ( - - - Add torrent - - - {getBody()} - - - - {uploading && } - - - - + useEffect(() => { + console.log(listTorrentResponse); + setSelectedFiles( + listTorrentResponse + ? listTorrentResponse.details.files.map((_, id) => id) + : [] ); + setOutputFolder(listTorrentResponse?.output_folder || ""); + }, [listTorrentResponse]); + + const clear = () => { + onHide(); + setSelectedFiles([]); + setUploadError(null); + setUploading(false); + }; + + const handleToggleFile = (toggledId: number) => { + if (selectedFiles.includes(toggledId)) { + setSelectedFiles(selectedFiles.filter((i) => i !== toggledId)); + } else { + setSelectedFiles([...selectedFiles, toggledId]); + } + }; + + const handleUpload = async () => { + if (!listTorrentResponse) { + return; + } + setUploading(true); + let initialPeers = listTorrentResponse.seen_peers + ? listTorrentResponse.seen_peers.slice(0, 32) + : null; + let opts: AddTorrentOptions = { + overwrite: true, + only_files: selectedFiles, + initial_peers: initialPeers, + output_folder: outputFolder, + }; + if (unpopularTorrent) { + opts.peer_opts = { + connect_timeout: 20, + read_write_timeout: 60, + }; + } + API.uploadTorrent(data, opts) + .then( + () => { + onHide(); + ctx.refreshTorrents(); + }, + (e) => { + setUploadError({ text: "Error starting torrent", details: e }); + } + ) + .finally(() => setUploading(false)); + }; + + const getBody = () => { + if (listTorrentLoading) { + return ; + } else if (listTorrentError) { + return ; + } else if (listTorrentResponse) { + return ( +
+
+ Pick the files to download + {listTorrentResponse.details.files.map((file, index) => ( + + handleToggleFile(index)} + > + + ))} +
+
+ Options + + Output folder + setOutputFolder(e.target.value)} + /> + + + setUnpopularTorrent(!unpopularTorrent)} + > + + This might be useful for unpopular torrents with few peers. It + will slow down fast torrents though. + + +
+
+ ); + } + }; + + return ( + + + Add torrent + + + {getBody()} + + + + {uploading && } + + + + + ); }; const Buttons = () => { - return ( -
- - -
- ); + return ( +
+ + +
+ ); }; const RootContent = (props: { - closeableError: ApiErrorDetails | null, - otherError: ApiErrorDetails | null, - torrents: Array | null, - torrentsLoading: boolean + closeableError: ApiErrorDetails | null; + otherError: ApiErrorDetails | null; + torrents: Array | null; + torrentsLoading: boolean; }) => { - let ctx = useContext(AppContext); - return - ctx.setCloseableError(null)} /> - - - + let ctx = useContext(AppContext); + return ( + + ctx.setCloseableError(null)} + /> + + + + ); }; function formatBytes(bytes: number): string { - if (bytes === 0) return '0 Bytes'; + if (bytes === 0) return "0 Bytes"; - const k = 1024; - const sizes = ['Bytes', 'KB', 'MB', 'GB']; + const k = 1024; + const sizes = ["Bytes", "KB", "MB", "GB"]; - const i = Math.floor(Math.log(bytes) / Math.log(k)); + const i = Math.floor(Math.log(bytes) / Math.log(k)); - return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]; } function getLargestFileName(torrentDetails: TorrentDetails): string { - const largestFile = torrentDetails.files.filter( - (f) => f.included - ).reduce( - (prev: any, current: any) => (prev.length > current.length) ? prev : current + const largestFile = torrentDetails.files + .filter((f) => f.included) + .reduce((prev: any, current: any) => + prev.length > current.length ? prev : current ); - return largestFile.name; + return largestFile.name; } function getCompletionETA(stats: TorrentStats): string { - let duration = stats?.live?.time_remaining?.duration?.secs; - if (duration == null) { - return 'N/A'; - } - return formatSecondsToTime(duration); + let duration = stats?.live?.time_remaining?.duration?.secs; + if (duration == null) { + return "N/A"; + } + return formatSecondsToTime(duration); } function formatSecondsToTime(seconds: number): string { - const hours = Math.floor(seconds / 3600); - const minutes = Math.floor((seconds % 3600) / 60); - const remainingSeconds = seconds % 60; + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + const remainingSeconds = seconds % 60; - const formatUnit = (value: number, unit: string) => (value > 0 ? `${value}${unit}` : ''); + const formatUnit = (value: number, unit: string) => + value > 0 ? `${value}${unit}` : ""; - if (hours > 0) { - return `${formatUnit(hours, 'h')} ${formatUnit(minutes, 'm')}`.trim(); - } else if (minutes > 0) { - return `${formatUnit(minutes, 'm')} ${formatUnit(remainingSeconds, 's')}`.trim(); - } else { - return `${formatUnit(remainingSeconds, 's')}`.trim(); - } + if (hours > 0) { + return `${formatUnit(hours, "h")} ${formatUnit(minutes, "m")}`.trim(); + } else if (minutes > 0) { + return `${formatUnit(minutes, "m")} ${formatUnit( + remainingSeconds, + "s" + )}`.trim(); + } else { + return `${formatUnit(remainingSeconds, "s")}`.trim(); + } } // Run a function with initial interval, then run it forever with the interval that the // callback returns. // Returns a callback to clear it. -export function customSetInterval(asyncCallback: () => Promise, initialInterval: number): () => void { - let timeoutId: number; - let currentInterval: number = initialInterval; +export function customSetInterval( + asyncCallback: () => Promise, + initialInterval: number +): () => void { + let timeoutId: number; + let currentInterval: number = initialInterval; - const executeCallback = async () => { - currentInterval = await asyncCallback(); - if (currentInterval === null || currentInterval === undefined) { - throw 'asyncCallback returned null or undefined'; - } - scheduleNext(); + const executeCallback = async () => { + currentInterval = await asyncCallback(); + if (currentInterval === null || currentInterval === undefined) { + throw "asyncCallback returned null or undefined"; } - - let scheduleNext = () => { - timeoutId = setTimeout(executeCallback, currentInterval); - } - scheduleNext(); + }; - return () => { - clearTimeout(timeoutId); - }; + let scheduleNext = () => { + timeoutId = setTimeout(executeCallback, currentInterval); + }; + + scheduleNext(); + + return () => { + clearTimeout(timeoutId); + }; } -export function loopUntilSuccess(callback: () => Promise, interval: number): () => void { - let timeoutId: number; +export function loopUntilSuccess( + callback: () => Promise, + interval: number +): () => void { + let timeoutId: number; - const executeCallback = async () => { - let retry = await callback().then(() => false, () => true); - if (retry) { - scheduleNext(); - } + const executeCallback = async () => { + let retry = await callback().then( + () => false, + () => true + ); + if (retry) { + scheduleNext(); } + }; - let scheduleNext = (overrideInterval?: number) => { - timeoutId = setTimeout(executeCallback, overrideInterval !== undefined ? overrideInterval : interval); - } + let scheduleNext = (overrideInterval?: number) => { + timeoutId = setTimeout( + executeCallback, + overrideInterval !== undefined ? overrideInterval : interval + ); + }; - scheduleNext(0); + scheduleNext(0); - return () => clearTimeout(timeoutId); -} \ No newline at end of file + return () => clearTimeout(timeoutId); +} diff --git a/desktop/index.html b/desktop/index.html index abee9c6..1bb4043 100644 --- a/desktop/index.html +++ b/desktop/index.html @@ -1,19 +1,17 @@ - + + + + + rqbit web 4.0.0-beta.0 + + + + + - - - - rqbit web 4.0.0-beta.0 - - - - - - - -
- - - - \ No newline at end of file + +
+ + + diff --git a/desktop/src/api.tsx b/desktop/src/api.tsx index 8bcb7e0..07c0739 100644 --- a/desktop/src/api.tsx +++ b/desktop/src/api.tsx @@ -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 { - error_kind: string, - human_readable: string, - status: number, - status_text: string, + error_kind: string; + human_readable: string; + status: number; + status_text: string; } -function errorToUIError(path: string): (e: InvokeErrorResponse) => Promise { - return (e: InvokeErrorResponse) => { - console.log(e); - let reason: ErrorDetails = { - method: 'INVOKE', - path: path, - text: e.human_readable, - status: e.status, - statusText: e.status_text - }; - return Promise.reject(reason); - } +function errorToUIError( + path: string +): (e: InvokeErrorResponse) => Promise { + return (e: InvokeErrorResponse) => { + console.log(e); + let reason: ErrorDetails = { + method: "INVOKE", + path: path, + text: e.human_readable, + status: e.status, + statusText: e.status_text, + }; + return Promise.reject(reason); + }; } -export async function invokeAPI(name: string, params?: InvokeArgs): Promise { - console.log("invoking", name, params); - const result = await invoke(name, params).catch(errorToUIError(name)); - console.log(result); - return result; +export async function invokeAPI( + name: string, + params?: InvokeArgs +): Promise { + console.log("invoking", name, params); + const result = await invoke(name, params).catch( + errorToUIError(name) + ); + console.log(result); + return result; } async function readFileAsBase64(file: File): Promise { - return new Promise((resolve, reject) => { - const reader = new FileReader(); + return new Promise((resolve, reject) => { + const reader = new FileReader(); - reader.onload = function (event) { - const base64String = (event?.target?.result as string)?.split(',')[1]; - if (base64String) { - resolve(base64String); - } else { - reject(new Error('Failed to read file as base64.')); - } - }; + reader.onload = function (event) { + const base64String = (event?.target?.result as string)?.split(",")[1]; + if (base64String) { + resolve(base64String); + } else { + reject(new Error("Failed to read file as base64.")); + } + }; - reader.onerror = function (error) { - console.log(error); - reject(error); - }; + reader.onerror = function (error) { + console.log(error); + reject(error); + }; - reader.readAsDataURL(file); - }); + reader.readAsDataURL(file); + }); } export const API: RqbitAPI = { - listTorrents: async function (): Promise { - return await invokeAPI("torrents_list"); - }, - getTorrentDetails: async function (id: number): Promise { - return await invokeAPI("torrent_details", { id }); - }, - getTorrentStats: async function (id: number): Promise { - return await invokeAPI("torrent_stats", { id }); - }, - uploadTorrent: async function (data, opts): Promise { - if (data instanceof File) { - let contents = await readFileAsBase64(data); - return await invokeAPI("torrent_create_from_base64_file", { - contents, - opts: opts ?? {}, - }); + listTorrents: async function (): Promise { + return await invokeAPI("torrents_list"); + }, + getTorrentDetails: async function (id: number): Promise { + return await invokeAPI("torrent_details", { id }); + }, + getTorrentStats: async function (id: number): Promise { + return await invokeAPI("torrent_stats", { id }); + }, + uploadTorrent: async function (data, opts): Promise { + if (data instanceof File) { + let contents = await readFileAsBase64(data); + return await invokeAPI( + "torrent_create_from_base64_file", + { + contents, + opts: opts ?? {}, } - return await invokeAPI("torrent_create_from_url", { - url: data, - opts: opts ?? {}, - }); - }, - pause: function (id: number): Promise { - return invokeAPI("torrent_action_pause", { id }); - }, - start: function (id: number): Promise { - return invokeAPI("torrent_action_start", { id }); - }, - forget: function (id: number): Promise { - return invokeAPI("torrent_action_forget", { id }); - }, - delete: function (id: number): Promise { - return invokeAPI("torrent_action_delete", { id }); + ); } -} \ No newline at end of file + return await invokeAPI("torrent_create_from_url", { + url: data, + opts: opts ?? {}, + }); + }, + pause: function (id: number): Promise { + return invokeAPI("torrent_action_pause", { id }); + }, + start: function (id: number): Promise { + return invokeAPI("torrent_action_start", { id }); + }, + forget: function (id: number): Promise { + return invokeAPI("torrent_action_forget", { id }); + }, + delete: function (id: number): Promise { + return invokeAPI("torrent_action_delete", { id }); + }, +}; diff --git a/desktop/src/configuration.tsx b/desktop/src/configuration.tsx index da39cc3..416eabd 100644 --- a/desktop/src/configuration.tsx +++ b/desktop/src/configuration.tsx @@ -3,48 +3,48 @@ type Duration = string; type SocketAddr = string; interface RqbitDesktopConfigDht { - disable: boolean; - disable_persistence: boolean; - persistence_filename: PathLike; + disable: boolean; + disable_persistence: boolean; + persistence_filename: PathLike; } interface RqbitDesktopConfigTcpListen { - disable: boolean; - min_port: number; - max_port: number; + disable: boolean; + min_port: number; + max_port: number; } interface RqbitDesktopConfigPersistence { - disable: boolean; - filename: PathLike; + disable: boolean; + filename: PathLike; } interface RqbitDesktopConfigPeerOpts { - connect_timeout: Duration; - read_write_timeout: Duration; + connect_timeout: Duration; + read_write_timeout: Duration; } interface RqbitDesktopConfigHttpApi { - disable: boolean; - listen_addr: SocketAddr; - read_only: boolean; + disable: boolean; + listen_addr: SocketAddr; + read_only: boolean; } interface RqbitDesktopConfigUpnp { - disable: boolean; + disable: boolean; } export interface RqbitDesktopConfig { - default_download_location: PathLike; - dht: RqbitDesktopConfigDht; - tcp_listen: RqbitDesktopConfigTcpListen; - upnp: RqbitDesktopConfigUpnp; - persistence: RqbitDesktopConfigPersistence; - peer_opts: RqbitDesktopConfigPeerOpts; - http_api: RqbitDesktopConfigHttpApi; + default_download_location: PathLike; + dht: RqbitDesktopConfigDht; + tcp_listen: RqbitDesktopConfigTcpListen; + upnp: RqbitDesktopConfigUpnp; + persistence: RqbitDesktopConfigPersistence; + peer_opts: RqbitDesktopConfigPeerOpts; + http_api: RqbitDesktopConfigHttpApi; } export interface CurrentDesktopState { - config: RqbitDesktopConfig | null, - configured: boolean, -} \ No newline at end of file + config: RqbitDesktopConfig | null; + configured: boolean; +} diff --git a/desktop/src/configure.tsx b/desktop/src/configure.tsx index fe36eee..f7be26d 100644 --- a/desktop/src/configure.tsx +++ b/desktop/src/configure.tsx @@ -6,310 +6,314 @@ import { invokeAPI } from "./api"; import { ErrorDetails } from "./rqbit-webui-src/api-types"; const FormCheck: React.FC<{ - label: string, - name: string, - checked: boolean, - onChange: (e: any) => void, - disabled?: boolean, - help?: string, + label: string; + name: string; + checked: boolean; + onChange: (e: any) => void; + disabled?: boolean; + help?: string; }> = ({ label, name, checked, onChange, disabled, help }) => { - return - {label} -
- -
- {help &&
{help}
} + return ( + + {label} +
+ +
+ {help &&
{help}
}
-} + ); +}; const FormInput: React.FC<{ - label: string, - name: string, - value: string | number, - inputType: string, - onChange: (e: any) => void, - disabled?: boolean, - help?: string + label: string; + name: string; + value: string | number; + inputType: string; + onChange: (e: any) => void; + disabled?: boolean; + help?: string; }> = ({ label, name, value, inputType, onChange, disabled, help }) => { - return - {label} -
- -
- {help &&
{help}
} + return ( + + {label} +
+ +
+ {help &&
{help}
}
-} + ); +}; export const ConfigModal: React.FC<{ - show: boolean, - handleStartReconfigure: () => void, - handleConfigured: (config: RqbitDesktopConfig) => void, - handleCancel?: () => void, - initialConfig: RqbitDesktopConfig, - defaultConfig: RqbitDesktopConfig, -}> = ({ show, handleStartReconfigure, handleConfigured, handleCancel, initialConfig, defaultConfig }) => { - let [config, setConfig] = useState(initialConfig); - let [loading, setLoading] = useState(false); + show: boolean; + handleStartReconfigure: () => void; + handleConfigured: (config: RqbitDesktopConfig) => void; + handleCancel?: () => void; + initialConfig: RqbitDesktopConfig; + defaultConfig: RqbitDesktopConfig; +}> = ({ + show, + handleStartReconfigure, + handleConfigured, + handleCancel, + initialConfig, + defaultConfig, +}) => { + let [config, setConfig] = useState(initialConfig); + let [loading, setLoading] = useState(false); - const [error, setError] = useState(null); + const [error, setError] = useState(null); - const handleInputChange = (e: any) => { - const name: string = e.target.name; - const value: any = e.target.value; - const [mainField, subField] = name.split('.', 2); + const handleInputChange = (e: any) => { + const name: string = e.target.name; + const value: any = e.target.value; + const [mainField, subField] = name.split(".", 2); - if (subField) { - setConfig((prevConfig: any) => ({ - ...prevConfig, - [mainField]: { - ...prevConfig[mainField], - [subField]: value, - }, - })); - } else { - setConfig((prevConfig) => ({ - ...prevConfig, - [name]: value, - })); - } - }; + if (subField) { + setConfig((prevConfig: any) => ({ + ...prevConfig, + [mainField]: { + ...prevConfig[mainField], + [subField]: value, + }, + })); + } else { + setConfig((prevConfig) => ({ + ...prevConfig, + [name]: value, + })); + } + }; - const handleToggleChange = (e: any) => { - const name: string = e.target.name; - const [mainField, subField] = name.split('.', 2); + const handleToggleChange = (e: any) => { + const name: string = e.target.name; + const [mainField, subField] = name.split(".", 2); - if (subField) { - setConfig((prevConfig: any) => ({ - ...prevConfig, - [mainField]: { - ...prevConfig[mainField], - [subField]: !prevConfig[mainField][subField], - }, - })); - } else { - setConfig((prevConfig: any) => ({ - ...prevConfig, - [name]: !prevConfig[name], - })); - } - }; + if (subField) { + setConfig((prevConfig: any) => ({ + ...prevConfig, + [mainField]: { + ...prevConfig[mainField], + [subField]: !prevConfig[mainField][subField], + }, + })); + } else { + setConfig((prevConfig: any) => ({ + ...prevConfig, + [name]: !prevConfig[name], + })); + } + }; - const handleOkClick = () => { - setError(null); - handleStartReconfigure(); - setLoading(true); - invokeAPI<{}>("config_change", { config }).then( - () => { - setLoading(false); - handleConfigured(config); - }, - (e: ErrorDetails) => { - setLoading(false); - setError({ - text: "Error saving configuration", - details: e, - }); - } - ) - }; - - return ( - - - Configure Rqbit desktop - - - - - - - - - - - DHT config - - - - - - - - - - TCP Listener config - - - - - - - - - - - - - Session persistence - - - - - - - - Peer connection options - - - - - - - - HTTP API config - - - - - - - - - - - - - {!!handleCancel && - - } - - - - + const handleOkClick = () => { + setError(null); + handleStartReconfigure(); + setLoading(true); + invokeAPI<{}>("config_change", { config }).then( + () => { + setLoading(false); + handleConfigured(config); + }, + (e: ErrorDetails) => { + setLoading(false); + setError({ + text: "Error saving configuration", + details: e, + }); + } ); + }; + + return ( + + + Configure Rqbit desktop + + + + + + + + + + DHT config + + + + + + + + + + TCP Listener config + + + + + + + + + + + + Session persistence + + + + + + + + Peer connection options + + + + + + + + HTTP API config + + + + + + + + + + + {!!handleCancel && ( + + )} + + + + + ); }; diff --git a/desktop/src/main.tsx b/desktop/src/main.tsx index d65c787..cb49c7f 100644 --- a/desktop/src/main.tsx +++ b/desktop/src/main.tsx @@ -1,5 +1,5 @@ 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 { API } from "./api"; import { invoke } from "@tauri-apps/api"; @@ -7,23 +7,29 @@ import { CurrentDesktopState, RqbitDesktopConfig } from "./configuration"; import { RqbitDesktop } from "./rqbit-desktop"; async function get_version(): Promise { - return invoke("get_version"); + return invoke("get_version"); } async function get_default_config(): Promise { - return invoke("config_default"); + return invoke("config_default"); } async function get_current_config(): Promise { - return invoke("config_current"); + return invoke("config_current"); } -Promise.all([get_version(), get_default_config(), get_current_config()]).then(([version, defaultConfig, currentState]) => { - ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( - - - - - +Promise.all([get_version(), get_default_config(), get_current_config()]).then( + ([version, defaultConfig, currentState]) => { + ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( + + + + + ); -}) \ No newline at end of file + } +); diff --git a/desktop/src/rqbit-desktop.tsx b/desktop/src/rqbit-desktop.tsx index f61015d..6b540ba 100644 --- a/desktop/src/rqbit-desktop.tsx +++ b/desktop/src/rqbit-desktop.tsx @@ -3,41 +3,49 @@ import { RqbitWebUI } from "./rqbit-webui-src/rqbit-web"; import { CurrentDesktopState, RqbitDesktopConfig } from "./configuration"; import { ConfigModal } from "./configure"; - export const RqbitDesktop: React.FC<{ - version: string, - defaultConfig: RqbitDesktopConfig, - currentState: CurrentDesktopState, + version: string; + defaultConfig: RqbitDesktopConfig; + currentState: CurrentDesktopState; }> = ({ version, defaultConfig, currentState }) => { - let [configured, setConfigured] = useState(currentState.configured); - let [config, setConfig] = useState(currentState.config ?? defaultConfig); - let [configurationOpened, setConfigurationOpened] = useState(false); + let [configured, setConfigured] = useState(currentState.configured); + let [config, setConfig] = useState( + currentState.config ?? defaultConfig + ); + let [configurationOpened, setConfigurationOpened] = useState(false); - return <> - {configured && } - {configured && { - e.stopPropagation(); - setConfigurationOpened(true); - }} - href="#" - aria-label="Settings" />} - { - setConfigured(false); - }} - handleCancel={() => { - setConfigurationOpened(false); - }} - handleConfigured={(config) => { - setConfig(config); - setConfigurationOpened(false); - setConfigured(true); - }} - initialConfig={config} - defaultConfig={defaultConfig} + return ( + <> + {configured && ( + + )} + {configured && ( + { + e.stopPropagation(); + setConfigurationOpened(true); + }} + href="#" + aria-label="Settings" /> + )} + { + setConfigured(false); + }} + handleCancel={() => { + setConfigurationOpened(false); + }} + handleConfigured={(config) => { + setConfig(config); + setConfigurationOpened(false); + setConfigured(true); + }} + initialConfig={config} + defaultConfig={defaultConfig} + /> -} \ No newline at end of file + ); +};