Prep for tauri
This commit is contained in:
parent
ab7d0f68d9
commit
950ed816d1
6 changed files with 158 additions and 136 deletions
|
|
@ -32,13 +32,13 @@ use crate::torrent_state::ManagedTorrentHandle;
|
||||||
// Public API
|
// Public API
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct HttpApi {
|
pub struct HttpApi {
|
||||||
inner: Arc<ApiInternal>,
|
inner: Arc<Api>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HttpApi {
|
impl HttpApi {
|
||||||
pub fn new(session: Arc<Session>, rust_log_reload_tx: Option<UnboundedSender<String>>) -> Self {
|
pub fn new(session: Arc<Session>, rust_log_reload_tx: Option<UnboundedSender<String>>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
inner: Arc::new(ApiInternal::new(session, rust_log_reload_tx)),
|
inner: Arc::new(Api::new(session, rust_log_reload_tx)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -274,14 +274,14 @@ impl HttpApi {
|
||||||
type Result<T> = std::result::Result<T, ApiError>;
|
type Result<T> = std::result::Result<T, ApiError>;
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
struct TorrentListResponseItem {
|
pub struct TorrentListResponseItem {
|
||||||
id: usize,
|
pub id: usize,
|
||||||
info_hash: String,
|
pub info_hash: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
struct TorrentListResponse {
|
pub struct TorrentListResponse {
|
||||||
torrents: Vec<TorrentListResponseItem>,
|
pub torrents: Vec<TorrentListResponseItem>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
|
|
@ -292,7 +292,7 @@ pub struct TorrentDetailsResponseFile {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Serialize)]
|
#[derive(Default, Serialize)]
|
||||||
struct EmptyJsonResponse {}
|
pub struct EmptyJsonResponse {}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct TorrentDetailsResponse {
|
pub struct TorrentDetailsResponse {
|
||||||
|
|
@ -414,14 +414,14 @@ impl TorrentAddQueryParams {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Private HTTP API internals. Agnostic of web framework.
|
// Private HTTP API internals. Agnostic of web framework.
|
||||||
struct ApiInternal {
|
pub struct Api {
|
||||||
session: Arc<Session>,
|
session: Arc<Session>,
|
||||||
rust_log_reload_tx: Option<UnboundedSender<String>>,
|
rust_log_reload_tx: Option<UnboundedSender<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
type ApiState = Arc<ApiInternal>;
|
type ApiState = Arc<Api>;
|
||||||
|
|
||||||
impl ApiInternal {
|
impl Api {
|
||||||
pub fn new(session: Arc<Session>, rust_log_reload_tx: Option<UnboundedSender<String>>) -> Self {
|
pub fn new(session: Arc<Session>, rust_log_reload_tx: Option<UnboundedSender<String>>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
session,
|
session,
|
||||||
|
|
@ -429,13 +429,13 @@ impl ApiInternal {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mgr_handle(&self, idx: TorrentId) -> Result<ManagedTorrentHandle> {
|
pub fn mgr_handle(&self, idx: TorrentId) -> Result<ManagedTorrentHandle> {
|
||||||
self.session
|
self.session
|
||||||
.get(idx)
|
.get(idx)
|
||||||
.ok_or(ApiError::torrent_not_found(idx))
|
.ok_or(ApiError::torrent_not_found(idx))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn api_torrent_list(&self) -> TorrentListResponse {
|
pub fn api_torrent_list(&self) -> TorrentListResponse {
|
||||||
let items = self.session.with_torrents(|torrents| {
|
let items = self.session.with_torrents(|torrents| {
|
||||||
torrents
|
torrents
|
||||||
.map(|(id, mgr)| TorrentListResponseItem {
|
.map(|(id, mgr)| TorrentListResponseItem {
|
||||||
|
|
@ -447,14 +447,18 @@ impl ApiInternal {
|
||||||
TorrentListResponse { torrents: items }
|
TorrentListResponse { torrents: items }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn api_torrent_details(&self, idx: TorrentId) -> Result<TorrentDetailsResponse> {
|
pub fn api_torrent_details(&self, idx: TorrentId) -> Result<TorrentDetailsResponse> {
|
||||||
let handle = self.mgr_handle(idx)?;
|
let handle = self.mgr_handle(idx)?;
|
||||||
let info_hash = handle.info().info_hash;
|
let info_hash = handle.info().info_hash;
|
||||||
let only_files = handle.only_files();
|
let only_files = handle.only_files();
|
||||||
make_torrent_details(&info_hash, &handle.info().info, only_files.as_deref())
|
make_torrent_details(&info_hash, &handle.info().info, only_files.as_deref())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn api_peer_stats(&self, idx: TorrentId, filter: PeerStatsFilter) -> Result<PeerStatsSnapshot> {
|
pub fn api_peer_stats(
|
||||||
|
&self,
|
||||||
|
idx: TorrentId,
|
||||||
|
filter: PeerStatsFilter,
|
||||||
|
) -> Result<PeerStatsSnapshot> {
|
||||||
let handle = self.mgr_handle(idx)?;
|
let handle = self.mgr_handle(idx)?;
|
||||||
Ok(handle
|
Ok(handle
|
||||||
.live()
|
.live()
|
||||||
|
|
@ -462,7 +466,7 @@ impl ApiInternal {
|
||||||
.per_peer_stats_snapshot(filter))
|
.per_peer_stats_snapshot(filter))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn api_torrent_action_pause(&self, idx: TorrentId) -> Result<EmptyJsonResponse> {
|
pub fn api_torrent_action_pause(&self, idx: TorrentId) -> Result<EmptyJsonResponse> {
|
||||||
let handle = self.mgr_handle(idx)?;
|
let handle = self.mgr_handle(idx)?;
|
||||||
handle
|
handle
|
||||||
.pause()
|
.pause()
|
||||||
|
|
@ -471,7 +475,7 @@ impl ApiInternal {
|
||||||
Ok(Default::default())
|
Ok(Default::default())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn api_torrent_action_start(&self, idx: TorrentId) -> Result<EmptyJsonResponse> {
|
pub fn api_torrent_action_start(&self, idx: TorrentId) -> Result<EmptyJsonResponse> {
|
||||||
let handle = self.mgr_handle(idx)?;
|
let handle = self.mgr_handle(idx)?;
|
||||||
self.session
|
self.session
|
||||||
.unpause(&handle)
|
.unpause(&handle)
|
||||||
|
|
@ -480,21 +484,21 @@ impl ApiInternal {
|
||||||
Ok(Default::default())
|
Ok(Default::default())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn api_torrent_action_forget(&self, idx: TorrentId) -> Result<EmptyJsonResponse> {
|
pub fn api_torrent_action_forget(&self, idx: TorrentId) -> Result<EmptyJsonResponse> {
|
||||||
self.session
|
self.session
|
||||||
.delete(idx, false)
|
.delete(idx, false)
|
||||||
.context("error forgetting torrent")?;
|
.context("error forgetting torrent")?;
|
||||||
Ok(Default::default())
|
Ok(Default::default())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn api_torrent_action_delete(&self, idx: TorrentId) -> Result<EmptyJsonResponse> {
|
pub fn api_torrent_action_delete(&self, idx: TorrentId) -> Result<EmptyJsonResponse> {
|
||||||
self.session
|
self.session
|
||||||
.delete(idx, true)
|
.delete(idx, true)
|
||||||
.context("error deleting torrent with files")?;
|
.context("error deleting torrent with files")?;
|
||||||
Ok(Default::default())
|
Ok(Default::default())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn api_set_rust_log(&self, new_value: String) -> Result<EmptyJsonResponse> {
|
pub fn api_set_rust_log(&self, new_value: String) -> Result<EmptyJsonResponse> {
|
||||||
let tx = self
|
let tx = self
|
||||||
.rust_log_reload_tx
|
.rust_log_reload_tx
|
||||||
.as_ref()
|
.as_ref()
|
||||||
|
|
@ -553,7 +557,7 @@ impl ApiInternal {
|
||||||
Ok(response)
|
Ok(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn api_dht_stats(&self) -> Result<DhtStats> {
|
pub fn api_dht_stats(&self) -> Result<DhtStats> {
|
||||||
self.session
|
self.session
|
||||||
.get_dht()
|
.get_dht()
|
||||||
.as_ref()
|
.as_ref()
|
||||||
|
|
@ -561,23 +565,23 @@ impl ApiInternal {
|
||||||
.ok_or(ApiError::dht_disabled())
|
.ok_or(ApiError::dht_disabled())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn api_dht_table(&self) -> Result<impl Serialize> {
|
pub fn api_dht_table(&self) -> Result<impl Serialize> {
|
||||||
let dht = self.session.get_dht().ok_or(ApiError::dht_disabled())?;
|
let dht = self.session.get_dht().ok_or(ApiError::dht_disabled())?;
|
||||||
Ok(dht.with_routing_table(|r| r.clone()))
|
Ok(dht.with_routing_table(|r| r.clone()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn api_stats_v0(&self, idx: TorrentId) -> Result<LiveStats> {
|
pub fn api_stats_v0(&self, idx: TorrentId) -> Result<LiveStats> {
|
||||||
let mgr = self.mgr_handle(idx)?;
|
let mgr = self.mgr_handle(idx)?;
|
||||||
let live = mgr.live().context("torrent not live")?;
|
let live = mgr.live().context("torrent not live")?;
|
||||||
Ok(LiveStats::from(&*live))
|
Ok(LiveStats::from(&*live))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn api_stats_v1(&self, idx: TorrentId) -> Result<TorrentStats> {
|
pub fn api_stats_v1(&self, idx: TorrentId) -> Result<TorrentStats> {
|
||||||
let mgr = self.mgr_handle(idx)?;
|
let mgr = self.mgr_handle(idx)?;
|
||||||
Ok(mgr.stats())
|
Ok(mgr.stats())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn api_dump_haves(&self, idx: usize) -> Result<String> {
|
pub fn api_dump_haves(&self, idx: usize) -> Result<String> {
|
||||||
let mgr = self.mgr_handle(idx)?;
|
let mgr = self.mgr_handle(idx)?;
|
||||||
Ok(mgr.with_chunk_tracker(|chunks| format!("{:?}", chunks.get_have_pieces()))?)
|
Ok(mgr.with_chunk_tracker(|chunks| format!("{:?}", chunks.get_have_pieces()))?)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,7 @@
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
<script type="module" src="src/main.tsx"></script>
|
||||||
<script type="module" src="src/index.tsx"></script>
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
109
crates/librqbit/webui/src/api-types.ts
Normal file
109
crates/librqbit/webui/src/api-types.ts
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
// Interface for the Torrent API response
|
||||||
|
export interface TorrentId {
|
||||||
|
id: number;
|
||||||
|
info_hash: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TorrentFile {
|
||||||
|
name: string;
|
||||||
|
length: number;
|
||||||
|
included: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interface for the Torrent Details API response
|
||||||
|
export interface TorrentDetails {
|
||||||
|
info_hash: string,
|
||||||
|
files: Array<TorrentFile>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AddTorrentResponse {
|
||||||
|
id: number | null;
|
||||||
|
details: TorrentDetails;
|
||||||
|
seen_peers?: Array<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ListTorrentsResponse {
|
||||||
|
torrents: Array<TorrentId>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
average_piece_download_time: {
|
||||||
|
secs: number;
|
||||||
|
nanos: number;
|
||||||
|
};
|
||||||
|
download_speed: {
|
||||||
|
mbps: number;
|
||||||
|
human_readable: string;
|
||||||
|
};
|
||||||
|
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_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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface ErrorDetails {
|
||||||
|
id?: number,
|
||||||
|
method?: string,
|
||||||
|
path?: string,
|
||||||
|
status?: number,
|
||||||
|
statusText?: string,
|
||||||
|
text: string,
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface RqbitAPI {
|
||||||
|
listTorrents: () => Promise<ListTorrentsResponse>,
|
||||||
|
getTorrentDetails: (index: number) => Promise<TorrentDetails>,
|
||||||
|
getTorrentStats: (index: number) => Promise<TorrentStats>;
|
||||||
|
uploadTorrent: (data: string | File, opts?: {
|
||||||
|
listOnly?: boolean,
|
||||||
|
selectedFiles?: Array<number>,
|
||||||
|
unpopularTorrent?: boolean,
|
||||||
|
initialPeers?: Array<string>,
|
||||||
|
}) => Promise<AddTorrentResponse>;
|
||||||
|
|
||||||
|
pause: (index: number) => Promise<void>;
|
||||||
|
start: (index: number) => Promise<void>;
|
||||||
|
forget: (index: number) => Promise<void>;
|
||||||
|
delete: (index: number) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
@ -1,100 +1,8 @@
|
||||||
|
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' : '';
|
||||||
|
|
||||||
// Interface for the Torrent API response
|
|
||||||
export interface TorrentId {
|
|
||||||
id: number;
|
|
||||||
info_hash: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TorrentFile {
|
|
||||||
name: string;
|
|
||||||
length: number;
|
|
||||||
included: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Interface for the Torrent Details API response
|
|
||||||
export interface TorrentDetails {
|
|
||||||
info_hash: string,
|
|
||||||
files: Array<TorrentFile>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AddTorrentResponse {
|
|
||||||
id: number | null;
|
|
||||||
details: TorrentDetails;
|
|
||||||
seen_peers?: Array<string>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ListTorrentsResponse {
|
|
||||||
torrents: Array<TorrentId>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
average_piece_download_time: {
|
|
||||||
secs: number;
|
|
||||||
nanos: number;
|
|
||||||
};
|
|
||||||
download_speed: {
|
|
||||||
mbps: number;
|
|
||||||
human_readable: string;
|
|
||||||
};
|
|
||||||
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_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;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export interface ErrorDetails {
|
|
||||||
id?: number,
|
|
||||||
method?: string,
|
|
||||||
path?: string,
|
|
||||||
status?: number,
|
|
||||||
statusText?: string,
|
|
||||||
text: string,
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
const makeRequest = async (method: string, path: string, data?: any): Promise<any> => {
|
const makeRequest = async (method: string, path: string, data?: any): Promise<any> => {
|
||||||
console.log(method, path);
|
console.log(method, path);
|
||||||
const url = apiUrl + path;
|
const url = apiUrl + path;
|
||||||
|
|
@ -138,7 +46,7 @@ const makeRequest = async (method: string, path: string, data?: any): Promise<an
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const API = {
|
export const API: RqbitAPI = {
|
||||||
listTorrents: (): Promise<ListTorrentsResponse> => makeRequest('GET', '/torrents'),
|
listTorrents: (): Promise<ListTorrentsResponse> => makeRequest('GET', '/torrents'),
|
||||||
getTorrentDetails: (index: number): Promise<TorrentDetails> => {
|
getTorrentDetails: (index: number): Promise<TorrentDetails> => {
|
||||||
return makeRequest('GET', `/torrents/${index}`);
|
return makeRequest('GET', `/torrents/${index}`);
|
||||||
|
|
|
||||||
9
crates/librqbit/webui/src/main.tsx
Normal file
9
crates/librqbit/webui/src/main.tsx
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { StrictMode } from "react";
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import { RqbitWebUI } from "./rqbit-web";
|
||||||
|
import { API } from "./api";
|
||||||
|
|
||||||
|
globalThis.API = API;
|
||||||
|
|
||||||
|
const torrentsContainer = document.getElementById('app');
|
||||||
|
ReactDOM.createRoot(torrentsContainer).render(<StrictMode><RqbitWebUI /></StrictMode >);
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
import { MouseEventHandler, StrictMode, createContext, useContext, useEffect, useRef, useState } from 'react';
|
import { StrictMode, createContext, useContext, useEffect, useRef, useState } from 'react';
|
||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
import { ProgressBar, Button, Container, Row, Col, Alert, Modal, Form, Spinner, Table } from 'react-bootstrap';
|
import { ProgressBar, Button, Container, Row, Col, Alert, Modal, Form, Spinner } from 'react-bootstrap';
|
||||||
import { AddTorrentResponse, TorrentDetails, TorrentFile, TorrentId, TorrentStats, ErrorDetails, API, STATE_INITIALIZING, STATE_LIVE, STATE_PAUSED, STATE_ERROR } from './api';
|
import { AddTorrentResponse, TorrentDetails, TorrentId, TorrentStats, ErrorDetails, STATE_INITIALIZING, STATE_LIVE, STATE_PAUSED, STATE_ERROR, RqbitAPI } from './api-types';
|
||||||
|
|
||||||
|
declare const API: RqbitAPI;
|
||||||
|
|
||||||
interface Error {
|
interface Error {
|
||||||
text: string,
|
text: string,
|
||||||
|
|
@ -308,7 +310,7 @@ const TorrentsList = (props: { torrents: Array<TorrentId>, loading: boolean }) =
|
||||||
</>;
|
</>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Root = () => {
|
export const RqbitWebUI = () => {
|
||||||
const [closeableError, setCloseableError] = useState<Error>(null);
|
const [closeableError, setCloseableError] = useState<Error>(null);
|
||||||
const [otherError, setOtherError] = useState<Error>(null);
|
const [otherError, setOtherError] = useState<Error>(null);
|
||||||
|
|
||||||
|
|
@ -694,12 +696,3 @@ function loopUntilSuccess<T>(callback: () => Promise<T>, interval: number): () =
|
||||||
|
|
||||||
return () => clearTimeout(timeoutId);
|
return () => clearTimeout(timeoutId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// List all torrents on page load and set up auto-refresh
|
|
||||||
async function init(): Promise<void> {
|
|
||||||
const torrentsContainer = document.getElementById('app');
|
|
||||||
ReactDOM.createRoot(torrentsContainer).render(<StrictMode><Root /></StrictMode>);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call init function on page load
|
|
||||||
document.addEventListener('DOMContentLoaded', init);
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue