From f540381caff2cdfe9ac4413c9bd94019ebffc0f0 Mon Sep 17 00:00:00 2001 From: tehcneko Date: Thu, 3 Apr 2025 17:16:12 +0800 Subject: [PATCH] WebUI: Support creating new torrents Implemented the torrent creator using WebAPI from #20366 in WebUI, the interface is mostly inspired by GUI and VueTorrent. Closes #5614. PR #22459. --- src/webui/api/torrentcreatorcontroller.cpp | 16 +- src/webui/www/private/css/dynamicTable.css | 7 +- src/webui/www/private/css/style.css | 1 + .../www/private/images/torrent-creator.svg | 1 + src/webui/www/private/index.html | 5 +- src/webui/www/private/scripts/dynamicTable.js | 222 +++++++++++++- src/webui/www/private/scripts/mocha-init.js | 26 ++ .../www/private/views/createtorrent.html | 233 +++++++++++++++ .../www/private/views/torrentcreator.html | 282 ++++++++++++++++++ src/webui/www/webui.qrc | 3 + 10 files changed, 790 insertions(+), 6 deletions(-) create mode 100644 src/webui/www/private/images/torrent-creator.svg create mode 100644 src/webui/www/private/views/createtorrent.html create mode 100644 src/webui/www/private/views/torrentcreator.html diff --git a/src/webui/api/torrentcreatorcontroller.cpp b/src/webui/api/torrentcreatorcontroller.cpp index 8167ec99f..e3f3769e7 100644 --- a/src/webui/api/torrentcreatorcontroller.cpp +++ b/src/webui/api/torrentcreatorcontroller.cpp @@ -32,6 +32,7 @@ #include #include #include +#include #include "base/global.h" #include "base/bittorrent/torrentcreationmanager.h" @@ -88,6 +89,17 @@ namespace } #endif + QStringList parseUrls(const QString &urlsParam) + { + // Empty lines are preserved because they indicate new tracker tier and will be ignored in url seeds. + const QStringList encodedUrls = urlsParam.split(u'|'); + QStringList urls; + urls.reserve(encodedUrls.size()); + for (const QString &urlStr : encodedUrls) + urls << QUrl::fromPercentEncoding(urlStr.toLatin1()); + return urls; + } + QString taskStatusString(const std::shared_ptr task) { if (task->isFailed()) @@ -130,8 +142,8 @@ void TorrentCreatorController::addTaskAction() .torrentFilePath = Path(params()[KEY_TORRENT_FILE_PATH]), .comment = params()[KEY_COMMENT], .source = params()[KEY_SOURCE], - .trackers = params()[KEY_TRACKERS].split(u'|'), - .urlSeeds = params()[KEY_URL_SEEDS].split(u'|') + .trackers = parseUrls(params()[KEY_TRACKERS]), + .urlSeeds = parseUrls(params()[KEY_URL_SEEDS]) }; bool const startSeeding = parseBool(params()[u"startSeeding"_s]).value_or(createTorrentParams.torrentFilePath.isEmpty()); diff --git a/src/webui/www/private/css/dynamicTable.css b/src/webui/www/private/css/dynamicTable.css index 9ca435394..525c675ac 100644 --- a/src/webui/www/private/css/dynamicTable.css +++ b/src/webui/www/private/css/dynamicTable.css @@ -22,7 +22,8 @@ color: var(--color-text-white); } -#transferList .stateIcon { +#transferList .stateIcon, +#torrentCreatorContentView .stateIcon { background: left center / contain no-repeat; margin-left: 3px; padding-left: 1.65em; @@ -74,6 +75,10 @@ background-image: url("../images/error.svg"); } + &.stateRunning { + background-image: url("../images/torrent-start.svg"); + } + &.stateUnknown { background-image: none; } diff --git a/src/webui/www/private/css/style.css b/src/webui/www/private/css/style.css index f28e0a5cf..46a850315 100644 --- a/src/webui/www/private/css/style.css +++ b/src/webui/www/private/css/style.css @@ -114,6 +114,7 @@ input[type="number"], input[type="password"], input[type="button"], button, +textarea, select { border: 1px solid var(--color-border-default); border-radius: 3px; diff --git a/src/webui/www/private/images/torrent-creator.svg b/src/webui/www/private/images/torrent-creator.svg new file mode 100644 index 000000000..324a1a492 --- /dev/null +++ b/src/webui/www/private/images/torrent-creator.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/webui/www/private/index.html b/src/webui/www/private/index.html index da0b74d7e..354f1b2b0 100644 --- a/src/webui/www/private/index.html +++ b/src/webui/www/private/index.html @@ -93,9 +93,10 @@
  • QBT_TR(Tools)QBT_TR[CONTEXT=MainWindow]
  • diff --git a/src/webui/www/private/scripts/dynamicTable.js b/src/webui/www/private/scripts/dynamicTable.js index f5f5ff122..bbf5e5479 100644 --- a/src/webui/www/private/scripts/dynamicTable.js +++ b/src/webui/www/private/scripts/dynamicTable.js @@ -51,7 +51,8 @@ window.qBittorrent.DynamicTable ??= (() => { RssDownloaderRulesTable: RssDownloaderRulesTable, RssDownloaderFeedSelectionTable: RssDownloaderFeedSelectionTable, RssDownloaderArticlesTable: RssDownloaderArticlesTable, - TorrentWebseedsTable: TorrentWebseedsTable + TorrentWebseedsTable: TorrentWebseedsTable, + TorrentCreationTasksTable: TorrentCreationTasksTable, }; }; @@ -3392,6 +3393,225 @@ window.qBittorrent.DynamicTable ??= (() => { }, }); + const TorrentCreationTasksTable = new Class({ + Extends: DynamicTable, + + initColumns: function() { + this.newColumn("state_icon", "", "QBT_TR(Status Icon)QBT_TR[CONTEXT=TorrentCreator]", 30, false); + this.newColumn("source_path", "", "QBT_TR(Source Path)QBT_TR[CONTEXT=TorrentCreator]", 200, true); + this.newColumn("progress", "", "QBT_TR(Progress)QBT_TR[CONTEXT=TorrentCreator]", 85, true); + this.newColumn("status", "", "QBT_TR(Status)QBT_TR[CONTEXT=TorrentCreator]", 100, true); + this.newColumn("torrent_format", "", "QBT_TR(Format)QBT_TR[CONTEXT=TorrentCreator]", 100, true); + this.newColumn("piece_size", "", "QBT_TR(Piece Size)QBT_TR[CONTEXT=TorrentCreator]", 100, true); + this.newColumn("private", "", "QBT_TR(Private)QBT_TR[CONTEXT=TorrentCreator]", 30, true); + this.newColumn("added_on", "", "QBT_TR(Added On)QBT_TR[CONTEXT=TorrentCreator]", 100, true); + this.newColumn("start_on", "", "QBT_TR(Started On)QBT_TR[CONTEXT=TorrentCreator]", 100, true); + this.newColumn("completion_on", "", "QBT_TR(Completed On)QBT_TR[CONTEXT=TorrentCreator]", 100, true); + this.newColumn("trackers", "", "QBT_TR(Trackers)QBT_TR[CONTEXT=TorrentCreator]", 100, true); + this.newColumn("web_seeds", "", "QBT_TR(Web Seeds)QBT_TR[CONTEXT=TorrentCreator]", 100, false); + this.newColumn("comment", "", "QBT_TR(Comment)QBT_TR[CONTEXT=TorrentCreator]", 100, true); + this.newColumn("source", "", "QBT_TR(Source)QBT_TR[CONTEXT=TorrentCreator]", 100, false); + this.newColumn("error_message", "", "QBT_TR(Error Message)QBT_TR[CONTEXT=TorrentCreator]", 100, true); + + this.columns["state_icon"].dataProperties[0] = "status"; + this.columns["source_path"].dataProperties.push("status"); + + this.initColumnsFunctions(); + }, + + initColumnsFunctions: function() { + const getStateIconClasses = (state) => { + let stateClass = "stateUnknown"; + // normalize states + switch (state) { + case "Running": + stateClass = "stateRunning"; + break; + case "Queued": + stateClass = "stateQueued"; + break; + case "Finished": + stateClass = "stateStoppedUP"; + break; + case "Failed": + stateClass = "stateError"; + break; + } + + return `stateIcon ${stateClass}`; + }; + + // state_icon + this.columns["state_icon"].updateTd = function(td, row) { + const state = this.getRowValue(row); + let div = td.firstElementChild; + if (div === null) { + div = document.createElement("div"); + td.append(div); + } + + div.className = `${getStateIconClasses(state)} stateIconColumn`; + }; + + this.columns["state_icon"].onVisibilityChange = (columnName) => { + // show state icon in name column only when standalone + // state icon column is hidden + this.updateColumn("name", true); + }; + + // source_path + this.columns["source_path"].updateTd = function(td, row) { + const name = this.getRowValue(row, 0); + const state = this.getRowValue(row, 1); + let span = td.firstElementChild; + if (span === null) { + span = document.createElement("span"); + td.append(span); + } + + span.className = this.isStateIconShown() ? getStateIconClasses(state) : ""; + span.textContent = name; + td.title = name; + }; + + this.columns["source_path"].isStateIconShown = () => !this.columns["state_icon"].isVisible(); + + // status + this.columns["status"].updateTd = function(td, row) { + const state = this.getRowValue(row); + if (!state) + return; + + let status = "QBT_TR(Unknown)QBT_TR[CONTEXT=HttpServer]"; + switch (state) { + case "Queued": + status = "QBT_TR(Queued)QBT_TR[CONTEXT=TorrentCreator]"; + break; + case "Running": + status = "QBT_TR(Running)QBT_TR[CONTEXT=TorrentCreator]"; + break; + case "Finished": + status = "QBT_TR(Finished)QBT_TR[CONTEXT=TorrentCreator]"; + break; + case "Failed": + status = "QBT_TR(Failed)QBT_TR[CONTEXT=TorrentCreator]"; + break; + } + + td.textContent = status; + td.title = status; + }; + + // torrent_format + this.columns["torrent_format"].updateTd = function(td, row) { + const torrentFormat = this.getRowValue(row); + if (!torrentFormat) + return; + + let format = "QBT_TR(Unknown)QBT_TR[CONTEXT=HttpServer]"; + switch (torrentFormat) { + case "v1": + format = "V1"; + break; + case "v2": + format = "V2"; + break; + case "hybrid": + format = "QBT_TR(Hybrid)QBT_TR[CONTEXT=TorrentCreator]"; + break; + } + + td.textContent = format; + td.title = format; + }; + + // progress + this.columns["progress"].updateTd = function(td, row) { + const progress = this.getRowValue(row); + + const div = td.firstElementChild; + if (div !== null) { + if (td.resized) { + td.resized = false; + div.setWidth(progressColumnWidth - 5); + } + if (div.getValue() !== progress) + div.setValue(progress); + } + else { + if (progressColumnWidth < 0) + progressColumnWidth = td.offsetWidth; + td.append(new window.qBittorrent.ProgressBar.ProgressBar(progress, { + width: progressColumnWidth - 5 + })); + td.resized = false; + } + }; + this.columns["progress"].staticWidth = 100; + this.columns["progress"].onResize = function(columnName) { + const pos = this.getColumnPos(columnName); + progressColumnWidth = -1; + for (const tr of this.getTrs()) { + const td = this.getRowCells(tr)[pos]; + if (progressColumnWidth < 0) + progressColumnWidth = td.offsetWidth; + td.resized = true; + this.columns[columnName].updateTd(td, this.getRow(tr.rowId)); + } + }.bind(this); + + // piece_size + this.columns["piece_size"].updateTd = function(td, row) { + const pieceSize = this.getRowValue(row); + const size = (pieceSize === 0) ? "QBT_TR(N/A)QBT_TR[CONTEXT=TorrentCreator]" : window.qBittorrent.Misc.friendlyUnit(pieceSize, false); + td.textContent = size; + td.title = size; + }; + + // private + this.columns["private"].updateTd = function(td, row) { + const isPrivate = this.getRowValue(row); + const string = isPrivate + ? "QBT_TR(Yes)QBT_TR[CONTEXT=PropertiesWidget]" + : "QBT_TR(No)QBT_TR[CONTEXT=PropertiesWidget]"; + td.textContent = string; + td.title = string; + }; + + const displayDate = function(td, row) { + const val = this.getRowValue(row); + if (!val) { + td.textContent = ""; + td.title = ""; + } + else { + const date = new Date(val).toLocaleString(); + td.textContent = date; + td.title = date; + } + }; + + // added_on, start_on, completion_on + this.columns["added_on"].updateTd = displayDate; + this.columns["start_on"].updateTd = displayDate; + this.columns["completion_on"].updateTd = displayDate; + }, + + setupCommonEvents: function() { + this.parent(); + this.dynamicTableDiv.addEventListener("dblclick", (e) => { + const tr = e.target.closest("tr"); + if (!tr) + return; + + this.deselectAll(); + this.selectRow(tr.rowId); + + window.qBittorrent.TorrentCreator.exportTorrents(); + }); + }, + }); + return exports(); })(); Object.freeze(window.qBittorrent.DynamicTable); diff --git a/src/webui/www/private/scripts/mocha-init.js b/src/webui/www/private/scripts/mocha-init.js index f13f9be88..52bba2d4d 100644 --- a/src/webui/www/private/scripts/mocha-init.js +++ b/src/webui/www/private/scripts/mocha-init.js @@ -208,6 +208,32 @@ const initializeWindows = () => { updateMainData(); }; + addClickEvent("torrentCreator", (e) => { + e.preventDefault(); + e.stopPropagation(); + + const id = "torrentCreatorPage"; + new MochaUI.Window({ + id: id, + icon: "images/torrent-creator.svg", + title: "QBT_TR(Torrent Creator)QBT_TR[CONTEXT=TorrentCreator]", + loadMethod: "xhr", + contentURL: "views/torrentcreator.html", + scrollbars: true, + maximizable: true, + paddingVertical: 0, + paddingHorizontal: 0, + width: loadWindowWidth(id, 900), + height: loadWindowHeight(id, 400), + onResize: () => { + saveWindowSize(id); + }, + onClose: () => { + window.qBittorrent.TorrentCreator.unload(); + } + }); + }); + addClickEvent("preferences", (e) => { e.preventDefault(); e.stopPropagation(); diff --git a/src/webui/www/private/views/createtorrent.html b/src/webui/www/private/views/createtorrent.html new file mode 100644 index 000000000..a85e5a57d --- /dev/null +++ b/src/webui/www/private/views/createtorrent.html @@ -0,0 +1,233 @@ + +
    +
    + QBT_TR(Select file/folder to share:)QBT_TR[CONTEXT=TorrentCreator] +
    + + +
    +
    +
    + QBT_TR(Settings)QBT_TR[CONTEXT=TorrentCreator] +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + + QBT_TR(KiB)QBT_TR[CONTEXT=OptionsDialog] +
    +
    +
    + QBT_TR(Fields)QBT_TR[CONTEXT=TorrentCreator] + + + + + + + + + + + + + + + + + + + + +
    + + + +
    + + + +
    + + + +
    + + + +
    +
    + +
    +
    + + diff --git a/src/webui/www/private/views/torrentcreator.html b/src/webui/www/private/views/torrentcreator.html new file mode 100644 index 000000000..02f00d95d --- /dev/null +++ b/src/webui/www/private/views/torrentcreator.html @@ -0,0 +1,282 @@ + + +
    + +
    + +
    +
    +
    + + + + +
    +
    +
    + + + + + +
    +
    +
    +
    + diff --git a/src/webui/www/webui.qrc b/src/webui/www/webui.qrc index 1a77d3a28..bc637f56c 100644 --- a/src/webui/www/webui.qrc +++ b/src/webui/www/webui.qrc @@ -370,6 +370,7 @@ private/images/task-complete.svg private/images/task-reject.svg private/images/toolbox-divider.gif + private/images/torrent-creator.svg private/images/torrent-magnet.svg private/images/torrent-start-forced.svg private/images/torrent-start.svg @@ -428,6 +429,7 @@ private/views/confirmdeletion.html private/views/confirmRecheck.html private/views/cookies.html + private/views/createtorrent.html private/views/filters.html private/views/installsearchplugin.html private/views/log.html @@ -441,6 +443,7 @@ private/views/search.html private/views/searchplugins.html private/views/statistics.html + private/views/torrentcreator.html private/views/transferlist.html public/css/login.css public/css/noscript.css