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()}
+
+
+
>
:
}