diff --git a/crates/librqbit/webui/webui/index.html b/crates/librqbit/webui/webui/index.html index c116f87..e2b73b2 100644 --- a/crates/librqbit/webui/webui/index.html +++ b/crates/librqbit/webui/webui/index.html @@ -12,19 +12,8 @@

rqbit web 0.0.1-alpha

- -
-
-
- - - -
+
- - - - diff --git a/crates/librqbit/webui/webui/package-lock.json b/crates/librqbit/webui/webui/package-lock.json index c2075f3..eaaae27 100644 --- a/crates/librqbit/webui/webui/package-lock.json +++ b/crates/librqbit/webui/webui/package-lock.json @@ -5,10 +5,14 @@ "packages": { "": { "dependencies": { - "preact": "^10.13.1" + "preact": "^10.13.1", + "react": "^18.2.0", + "react-dom": "^18.2.0" }, "devDependencies": { "@preact/preset-vite": "^2.5.0", + "@types/react": "^18.2.38", + "@types/react-dom": "^18.2.16", "typescript": "^5.3.2", "vite": "^4.3.2" } @@ -855,6 +859,38 @@ "node": ">= 8.0.0" } }, + "node_modules/@types/prop-types": { + "version": "15.7.11", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", + "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==", + "dev": true + }, + "node_modules/@types/react": { + "version": "18.2.38", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.38.tgz", + "integrity": "sha512-cBBXHzuPtQK6wNthuVMV6IjHAFkdl/FOPFIlkd81/Cd1+IqkHu/A+w4g43kaQQoYHik/ruaQBDL72HyCy1vuMw==", + "dev": true, + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.2.16", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.16.tgz", + "integrity": "sha512-766c37araZ9vxtYs25gvY2wNdFWsT2ZiUvOd0zMhTaoGj6B911N8CKQWgXXJoPMLF3J82thpRqQA7Rf3rBwyJw==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/scheduler": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.7.tgz", + "integrity": "sha512-8g25Nl3AuB1KulTlSUsUhUo/oBgBU6XIXQ+XURpeioEbEJvkO7qI4vDfREv3vJYHHzqXjcAHvoJy4pTtSQNZtA==", + "dev": true + }, "node_modules/ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", @@ -963,6 +999,12 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, + "node_modules/csstype": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==", + "dev": true + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -1124,8 +1166,7 @@ "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "node_modules/jsesc": { "version": "2.5.2", @@ -1157,6 +1198,17 @@ "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==", "dev": true }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -1257,6 +1309,29 @@ "url": "https://opencollective.com/preact" } }, + "node_modules/react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + }, + "peerDependencies": { + "react": "^18.2.0" + } + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -1290,6 +1365,14 @@ "fsevents": "~2.3.2" } }, + "node_modules/scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", diff --git a/crates/librqbit/webui/webui/package.json b/crates/librqbit/webui/webui/package.json index 6b88995..876feca 100644 --- a/crates/librqbit/webui/webui/package.json +++ b/crates/librqbit/webui/webui/package.json @@ -3,15 +3,17 @@ "type": "module", "scripts": { "dev": "vite", - "build": "vite build", + "build": "vite build && ./post-build", "preview": "vite preview" }, "dependencies": { - "preact": "^10.13.1" + "react": "^18.2.0", + "react-dom": "^18.2.0" }, "devDependencies": { - "@preact/preset-vite": "^2.5.0", + "@types/react": "^18.2.38", + "@types/react-dom": "^18.2.16", "typescript": "^5.3.2", "vite": "^4.3.2" } -} +} \ No newline at end of file diff --git a/crates/librqbit/webui/webui/post-build b/crates/librqbit/webui/webui/post-build new file mode 100755 index 0000000..05ab978 --- /dev/null +++ b/crates/librqbit/webui/webui/post-build @@ -0,0 +1,25 @@ +#!/usr/bin/env python + +import os +import json + +os.chdir('dist') + +with open('manifest.json', 'r') as f: + manifest = json.load(f) + +for replacements in manifest.values(): + js_file = replacements['file'] + target_file = replacements['src'] + + with open(target_file, 'r') as f: + target_content = f.read() + + target_content = target_content.replace("/" + js_file, 'app.js') + with open(target_file, 'w') as f: + f.write(target_content) + + os.rename(js_file, 'app.js') + +os.rmdir('assets') +os.remove('manifest.json') \ No newline at end of file diff --git a/crates/librqbit/webui/webui/public/vite.svg b/crates/librqbit/webui/webui/public/vite.svg deleted file mode 100644 index e7b8dfb..0000000 --- a/crates/librqbit/webui/webui/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/crates/librqbit/webui/webui/src/assets/preact.svg b/crates/librqbit/webui/webui/src/assets/preact.svg deleted file mode 100644 index 908f17d..0000000 --- a/crates/librqbit/webui/webui/src/assets/preact.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/crates/librqbit/webui/webui/src/index.tsx b/crates/librqbit/webui/webui/src/index.tsx index 373d6eb..6c6b587 100644 --- a/crates/librqbit/webui/webui/src/index.tsx +++ b/crates/librqbit/webui/webui/src/index.tsx @@ -1,9 +1,23 @@ -import { Fragment, h, render, Component } from 'preact'; -import { useEffect, useState } from 'preact/hooks'; +import { Fragment, createContext, useContext, useEffect, useRef, useState } from 'react'; +import * as ReactDOM from 'react-dom'; // Define API URL and base path const apiUrl = (window.origin === 'null' || window.origin === 'http://localhost:3031') ? 'http://localhost:3030' : ''; +interface ErrorType { + status: number, + statusText: number, + body: number, +}; + +let defaultContext: { + setError: any, +} = { + setError: null, +}; + +const AppContext = createContext(defaultContext); + // Interface for the Torrent API response interface Torrent { id: number; @@ -119,14 +133,13 @@ async function getTorrentStats(index: number): Promise { return makeRequest('GET', `/torrents/${index}/stats`); } -function TorrentRow(props) { - const { detailsResponse, statsResponse } = this.props; +function TorrentRow({ detailsResponse, statsResponse }) { const totalBytes = statsResponse.snapshot.total_bytes; const downloadedBytes = statsResponse.snapshot.have_bytes; const downloadPercentage = (downloadedBytes / totalBytes) * 100; return ( -
+
{/* Create and render columns */} @@ -140,21 +153,21 @@ function TorrentRow(props) { // Define a Preact component for a column const Column = ({ label, value }) => ( -
-

{label}

+
+

{label}

{value}

); // Define a Preact component for a column with a progress bar const ColumnWithProgressBar = ({ label, percentage }) => ( -
-

{label}

-
-
+
+

{label}

+
+
-

{percentage.toFixed(2)}%

-
+

{percentage.toFixed(2)}%

+
); const DeferredTorrent = ({ torrent }) => { @@ -209,160 +222,101 @@ const DeferredTorrent = ({ torrent }) => { updateStatsResponse(stats); }); await Promise.all([a, b]); - // setTimeout(update, 5000); + setTimeout(update, 500); }; useEffect(() => { let timer = setTimeout(update, 0); return () => clearTimeout(timer); - }); + }, []); return } -const Root = () => { +var globalCtx = null; + +const TorrentsList = () => { const [torrents, updateTorrents] = useState([]); + globalCtx = useContext(AppContext); const update = async () => { let response = await makeRequest('GET', '/torrents'); updateTorrents(response.torrents); - // setTimeout(update, 500); + setTimeout(update, 500); }; + useEffect(() => { let timer = setTimeout(update, 0); return () => clearTimeout(timer); - }); + }, []); + + let torrentsComponents = torrents.map((t: Torrent) => + + ); return ( - <> - {torrents.map((t: Torrent) => { - - - - })} - +
+ {torrentsComponents} +
) }; +const Root = () => { + const [error, setError] = useState(null); + + const Error = ({ error }) => { + if (error == null) { + return null; + } + + let ctx = useContext(AppContext); + + return (
+ Error ${error.status}: {error.statusText}
+ {error.body} + +
); + }; + + const FileInput = () => { + const inputRef = useRef(); + + const inputOnChange = (e) => { + let file = e.target.files[0]; + makeRequest('POST', '/torrents?overwrite=true', file); + // e.target.clear(); + } + + const onClick = (e) => { + inputRef.current.click(); + } + + return (
+ + +
); + }; + + const Buttons = () => { + return ( +
+ + +
+ ); + } + + return + + + + +} + // Render function to display all torrents async function displayTorrents() { // Get the torrents container const torrentsContainer = document.getElementById('output'); - render(, torrentsContainer); -} - -// Function to update HTML for a torrent row -function updateTorrentRow(torrentRow: HTMLDivElement, detailsResponse: TorrentDetails, statsResponse: TorrentStats) { - // Calculate download percentage - const totalBytes = statsResponse.snapshot.total_bytes; - const downloadedBytes = statsResponse.snapshot.have_bytes; - const downloadPercentage = (downloadedBytes / totalBytes) * 100; - - // Display basic information about the torrent - const largestFileName = getLargestFileName(detailsResponse); - const downloadSpeed = statsResponse.download_speed.human_readable; - const eta = getCompletionETA(statsResponse); - const peers = `${statsResponse.snapshot.peer_stats.live} / ${statsResponse.snapshot.peer_stats.seen}`; - - // Update or create columns in the torrent row - updateOrCreateColumnContent(torrentRow, 'Name', largestFileName); - updateOrCreateColumnContent(torrentRow, 'Size', `${formatBytesToGB(totalBytes)} GB`); - updateOrCreateColumnWithProgressBar(torrentRow, 'Progress', downloadPercentage); - updateOrCreateColumnContent(torrentRow, 'Download Speed', downloadSpeed); - updateOrCreateColumnContent(torrentRow, 'ETA', eta); - updateOrCreateColumnContent(torrentRow, 'Peers', peers); -} - -// Function to update or create the content of a column in a torrent row -function updateOrCreateColumnContent(torrentRow: HTMLDivElement, human_label: string, value: string) { - let label = human_label.toLowerCase().replace(" ", "-"); - let column = torrentRow.querySelector(`.column-${label}`); - - // If the column doesn't exist, create a new one - if (!column) { - column = document.createElement('div'); - column.classList.add(`column-${label}`, 'me-3', 'p-2'); - torrentRow.appendChild(column); - } - - // Update the content of the existing or newly created column - const contentParagraph = column.querySelector('p:last-child'); - if (contentParagraph) { - contentParagraph.textContent = value; - } else { - column.innerHTML = `

${human_label}

${value}

`; - } -} - - -// Function to update or create the content of a progress bar column in a torrent row -function updateOrCreateColumnWithProgressBar(torrentRow: HTMLDivElement, label: string, percentage: number) { - let column = torrentRow.querySelector('.column-progress'); - - // If the column doesn't exist, create a new one - if (!column) { - column = document.createElement('div'); - column.classList.add('column-progress', 'me-3', 'p-2'); - torrentRow.appendChild(column); - } - - // Update the value of the progress bar in the existing or newly created column - const progressBar = column.querySelector('.progress-bar') as HTMLElement; - const progressPercentage = column.querySelector('p:last-child') as HTMLElement; - - if (progressBar && progressPercentage) { - progressBar.style.width = `${percentage}%`; - progressPercentage.textContent = `${percentage.toFixed(2)}%`; - } else { - column.innerHTML = ` -

${label}

-
-
-
-

${percentage.toFixed(2)}%

`; - } -} - - -// Function to render HTML for a torrent row -function renderTorrentRow(torrentsContainer: HTMLElement, torrentId: number, detailsResponse: TorrentDetails, statsResponse: TorrentStats) { - // Check if the torrent row already exists - let torrentRow = document.getElementById(`torrent-${torrentId}`) as HTMLDivElement; - - // If the torrent row doesn't exist, create a new one - if (!torrentRow) { - torrentRow = document.createElement('div'); - torrentRow.id = `torrent-${torrentId}`; - torrentRow.classList.add('torrent-row', 'd-flex', 'flex-row', 'p-3', 'bg-light', 'rounded', 'mb-3'); - torrentsContainer.appendChild(torrentRow); - } - - // Update columns in the torrent row - updateTorrentRow(torrentRow, detailsResponse, statsResponse); - - return torrentRow; -} - - -// Function to create a column div -function createColumn(label: string, value: string, columnClass: string): HTMLDivElement { - const columnDiv = document.createElement('div'); - columnDiv.classList.add(columnClass, 'me-3', 'p-2'); - columnDiv.innerHTML = `

${label}

${value}

`; - return columnDiv; -} - - -// Function to create a column div with a progress bar -function createColumnWithProgressBar(label: string, percentage: number): HTMLDivElement { - const columnDiv = document.createElement('div'); - columnDiv.classList.add('column', 'me-3', 'p-2'); - columnDiv.innerHTML = ` -

${label}

-
-
-
-

${percentage.toFixed(2)}%

`; - return columnDiv; + ReactDOM.render(, torrentsContainer); } // Function to format bytes to GB @@ -391,29 +345,11 @@ function getCompletionETA(stats: TorrentStats): string { // Helper function to display API errors in an alert function displayApiError(error: ApiError): void { - const errorAlert = document.getElementById('error-alert'); - if (errorAlert) { - errorAlert.innerHTML = ` - - `; - } -} - -// Helper function to clear the error alert -function clearErrorAlert(): void { - const errorAlert = document.getElementById('error-alert'); - if (errorAlert) { - errorAlert.innerHTML = ''; // Clear the content - } + globalCtx.setError(error); } // List all torrents on page load and set up auto-refresh async function init(): Promise { - console.log('init'); await displayTorrents(); } @@ -436,9 +372,6 @@ async function handleFileInputChange(): Promise { } } -// Add event listeners for buttons -document.getElementById('add-magnet-button')?.addEventListener('click', addTorrentFromMagnet); - // Update the event listener for the file input button const fileInputButton = document.getElementById('upload-file-button'); fileInputButton?.addEventListener('click', () => { diff --git a/crates/librqbit/webui/webui/tsconfig.json b/crates/librqbit/webui/webui/tsconfig.json index 12bb30b..aab5593 100644 --- a/crates/librqbit/webui/webui/tsconfig.json +++ b/crates/librqbit/webui/webui/tsconfig.json @@ -6,15 +6,12 @@ "noEmit": true, "allowJs": true, "checkJs": true, - /* Preact Config */ "jsx": "react-jsx", - "jsxImportSource": "preact", "skipLibCheck": true, - "paths": { - "react": ["./node_modules/preact/compat/"], - "react-dom": ["./node_modules/preact/compat/"] - } }, - "include": ["node_modules/vite/client.d.ts", "**/*"] -} + "include": [ + "node_modules/vite/client.d.ts", + "**/*" + ] +} \ No newline at end of file diff --git a/crates/librqbit/webui/webui/vite.config.ts b/crates/librqbit/webui/webui/vite.config.ts index b741e57..d327d39 100644 --- a/crates/librqbit/webui/webui/vite.config.ts +++ b/crates/librqbit/webui/webui/vite.config.ts @@ -1,10 +1,12 @@ import { defineConfig } from 'vite'; -import preact from '@preact/preset-vite'; // https://vitejs.dev/config/ export default defineConfig({ - plugins: [preact()], server: { port: 3031 + }, + build: { + manifest: true } + });