diff --git a/crates/librqbit/webui/index.html b/crates/librqbit/webui/index.html index 29217f0..2e09371 100644 --- a/crates/librqbit/webui/index.html +++ b/crates/librqbit/webui/index.html @@ -8,6 +8,8 @@ + diff --git a/crates/librqbit/webui/src/api.ts b/crates/librqbit/webui/src/api.ts index e99026f..eba0f63 100644 --- a/crates/librqbit/webui/src/api.ts +++ b/crates/librqbit/webui/src/api.ts @@ -69,8 +69,13 @@ export interface LiveTorrentStats { } | 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: string, + state: 'initializing' | 'paused' | 'live' | 'error', error: string | null, progress_bytes: number, finished: boolean, diff --git a/crates/librqbit/webui/src/index.tsx b/crates/librqbit/webui/src/index.tsx index c938f00..efdb1b8 100644 --- a/crates/librqbit/webui/src/index.tsx +++ b/crates/librqbit/webui/src/index.tsx @@ -1,7 +1,7 @@ -import { StrictMode, createContext, useContext, useEffect, useRef, useState } from 'react'; +import { MouseEventHandler, StrictMode, createContext, useContext, useEffect, useRef, useState } from 'react'; import ReactDOM from 'react-dom/client'; import { ProgressBar, Button, Container, Row, Col, Alert, Modal, Form, Spinner, Table } from 'react-bootstrap'; -import { AddTorrentResponse, TorrentDetails, TorrentFile, TorrentId, TorrentStats, ErrorDetails, API } from './api'; +import { AddTorrentResponse, TorrentDetails, TorrentFile, TorrentId, TorrentStats, ErrorDetails, API, STATE_INITIALIZING, STATE_LIVE } from './api'; interface Error { text: string, @@ -15,6 +15,135 @@ interface ContextType { const AppContext = createContext(null); +const IconButton: React.FC<{ + className: string, + onClick: () => void, + disabled?: boolean, + color?: string, +}> = ({ className, onClick, disabled, color }) => { + const onClickStopPropagation = (e) => { + e.stopPropagation(); + if (disabled) { + return; + } + onClick(); + } + return +} + +const DeleteTorrentModal = ({ id, show, onHide }) => { + if (!show) { + return null; + } + const [deleteFiles, setDeleteFiles] = useState(false); + const [error, setError] = useState(null); + const [deleting, setDeleting] = useState(false); + + const close = () => { + setDeleteFiles(false); + setError(null); + setDeleting(false); + onHide(); + } + + const deleteTorrent = () => { + setDeleting(true); + + const call = deleteFiles ? API.delete : API.forget; + + call(id).then(() => { + close(); + }).catch((e) => { + setError({ + text: `Error deleting torrent id=${id}`, + details: e, + }); + setDeleting(false); + }) + } + + return + + Delete torrent + + +
+ + setDeleteFiles(!deleteFiles)}> + + +
+ {error && } +
+ + {deleting && } + + + +
+} + +const TorrentActions: React.FC<{ + id: number, statsResponse: TorrentStats +}> = ({ id, statsResponse }) => { + let state = statsResponse.state; + + let [disabled, setDisabled] = useState(false); + let [deleting, setDeleting] = useState(false); + + const canPause = state == 'live'; + const canUnpause = state == 'paused'; + + const ctx = useContext(AppContext); + + const unpause = () => { + setDisabled(true); + API.start(id).finally(() => setDisabled(false)).catch((e) => { + ctx.setCloseableError({ + text: `Error starting torrent id=${id}`, + details: e, + }); + }) + }; + + const pause = () => { + setDisabled(true); + API.pause(id).finally(() => setDisabled(false)).catch((e) => { + ctx.setCloseableError({ + text: `Error pausing torrent id=${id}`, + details: e, + }); + }) + }; + + const startDeleting = () => { + setDisabled(true); + setDeleting(true); + } + + const cancelDeleting = () => { + setDisabled(false); + setDeleting(false); + } + + return + + {canUnpause && } + {canPause && } + + + + +} + const TorrentRow: React.FC<{ id: number, detailsResponse: TorrentDetails, statsResponse: TorrentStats }> = ({ id, detailsResponse, statsResponse }) => { @@ -24,7 +153,7 @@ const TorrentRow: React.FC<{ const progressBytes = statsResponse?.progress_bytes ?? 0; const finished = statsResponse?.finished || false; const progressPercentage = error ? 100 : (progressBytes / totalBytes) * 100; - const isAnimated = (state == "initializing" || state == "live") && !finished; + const isAnimated = (state == STATE_INITIALIZING || state == STATE_LIVE) && !finished; const progressLabel = error ? 'Error' : `${progressPercentage.toFixed(2)}%`; const progressBarVariant = error ? 'danger' : finished ? 'success' : 'info'; @@ -40,7 +169,7 @@ const TorrentRow: React.FC<{ if (finished) { return 'Completed'; } - if (state == 'initializing') { + if (state == STATE_INITIALIZING) { return 'Checking files'; } return statsResponse.live?.download_speed.human_readable ?? "N/A"; @@ -58,7 +187,7 @@ const TorrentRow: React.FC<{ return ( - + {detailsResponse ? <>
@@ -77,6 +206,9 @@ const TorrentRow: React.FC<{ {formatDownloadSped()} {getCompletionETA(statsResponse)} {formatPeersString()} + + + : }