From 5629b072a8b796bbbac38652cfc2907e43c21da5 Mon Sep 17 00:00:00 2001 From: "Stiliyan Tonev (Bark)" Date: Fri, 20 Jun 2025 10:34:25 +0300 Subject: [PATCH] WebUI: Add checkboxes for easy selection on mobile Adds a setting that when turned `ON` renders a left most column that represents a checkbox in the torrents table The state of this checkbox updates on torrent select/deselect to highlight the currently selected torrents Checking/Unchecking the box allows for easier management of multiple torrents through a touch-screen device where selecting multiple non sequential torrents differs from using a mouse. --- src/webui/www/private/scripts/dynamicTable.js | 49 ++++++++++++++++++- src/webui/www/private/views/preferences.html | 7 +++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/src/webui/www/private/scripts/dynamicTable.js b/src/webui/www/private/scripts/dynamicTable.js index bb4492a86..d38da9713 100644 --- a/src/webui/www/private/scripts/dynamicTable.js +++ b/src/webui/www/private/scripts/dynamicTable.js @@ -160,6 +160,8 @@ window.qBittorrent.DynamicTable ??= (() => { }, true); this.dynamicTableDiv.addEventListener("touchstart", (e) => { + if (e.target.classList.contains("selectRowCheckbox")) + return; // ignore touch events on checkboxes, otherwise breaks on mobile const tr = e.target.closest("tr"); if (!tr) return; @@ -585,6 +587,8 @@ window.qBittorrent.DynamicTable ??= (() => { column["onVisibilityChange"] = null; column["staticWidth"] = null; column["calculateBuffer"] = () => 0; + column["jsManaged"] = false; + this.columns.push(column); this.columns[name] = column; @@ -733,10 +737,12 @@ window.qBittorrent.DynamicTable ??= (() => { for (const row of this.getFilteredAndSortedRows()) this.selectedRows.push(row.rowId); this.setRowClass(); + this.onSelectedRowChanged(); } deselectAll() { this.selectedRows.empty(); + this.onSelectedRowChanged(); } selectRow(rowId) { @@ -1012,7 +1018,10 @@ window.qBittorrent.DynamicTable ??= (() => { tds[i].style.width = `${this.columns[i].width}px`; tds[i].style.maxWidth = `${this.columns[i].width}px`; } - if (this.columns[i].dataProperties.some(prop => Object.hasOwn(data, prop))) + if ( + this.columns[i].dataProperties.some(prop => Object.hasOwn(data, prop)) + || (this.columns[i].jsManaged) + ) this.columns[i].updateTd(tds[i], row); } row["data"] = {}; @@ -1108,12 +1117,18 @@ window.qBittorrent.DynamicTable ??= (() => { } class TorrentsTable extends DynamicTable { + setupVirtualList() { super.setupVirtualList(); this.rowHeight = 22; } initColumns() { + if (LocalPreferences.get("use_checkboxes_in_ui", "false") === "true") { + this.newColumn("selected", "width: 20px; text-align: center;", "", 20, true); + this.columns["selected"].jsManaged = true; + } + this.newColumn("priority", "", "#", 30, true); this.newColumn("state_icon", "", "QBT_TR(Status Icon)QBT_TR[CONTEXT=TransferListModel]", 30, false); this.newColumn("name", "", "QBT_TR(Name)QBT_TR[CONTEXT=TransferListModel]", 200, true); @@ -1213,6 +1228,29 @@ window.qBittorrent.DynamicTable ??= (() => { return `stateIcon ${stateClass}`; }; + if (LocalPreferences.get("use_checkboxes_in_ui", "false") === "true") { + const self = this; + this.columns["selected"].updateTd = (td, row) => { + let htmlInput = td.firstElementChild; + if (htmlInput === null) + htmlInput = document.createElement("input"); + + htmlInput.type = "checkbox"; + htmlInput.className = "selectRowCheckbox"; + htmlInput.checked = self.isRowSelected(row.rowId); + + if (td.firstElementChild === null) { + htmlInput.addEventListener("click", (e) => { + e.stopPropagation(); + if (htmlInput.checked) + self.selectRow(row.rowId); + else + self.deselectRow(row.rowId); + }); + td.append(htmlInput); + } + }; + } // state_icon this.columns["state_icon"].updateTd = function(td, row) { @@ -1829,6 +1867,15 @@ window.qBittorrent.DynamicTable ??= (() => { onSelectedRowChanged() { updatePropertiesPanel(); + this.updateSelectedCheckboxes(); + } + + updateSelectedCheckboxes() { + const checkboxes = document.getElementsByClassName("selectRowCheckbox"); + for (const checkbox of checkboxes) { + const rowId = checkbox.closest("tr").rowId; + checkbox.checked = this.isRowSelected(rowId); + } } } diff --git a/src/webui/www/private/views/preferences.html b/src/webui/www/private/views/preferences.html index 26a5389ac..e827c9c39 100644 --- a/src/webui/www/private/views/preferences.html +++ b/src/webui/www/private/views/preferences.html @@ -1031,6 +1031,10 @@
QBT_TR(List of alternative WebUI)QBT_TR[CONTEXT=OptionsDialog]
+
+ + +
@@ -2533,6 +2537,7 @@ Use ';' to split multiple entries. Can use wildcard '*'.)QBT_TR[CONTEXT=OptionsD document.getElementById("use_https_checkbox").checked = pref.use_https; document.getElementById("ssl_cert_text").value = pref.web_ui_https_cert_path; document.getElementById("ssl_key_text").value = pref.web_ui_https_key_path; + document.getElementById("use_checkboxes_in_ui").checked = LocalPreferences.get("use_checkboxes_in_ui", "false") === "true"; updateHttpsSettings(); // Authentication @@ -3031,6 +3036,8 @@ Use ';' to split multiple entries. Can use wildcard '*'.)QBT_TR[CONTEXT=OptionsD } settings["alternative_webui_enabled"] = alternative_webui_enabled; settings["alternative_webui_path"] = webui_files_location_textarea; + settings["use_checkboxes_in_ui"] = document.getElementById("use_checkboxes_in_ui").checked; + LocalPreferences.set("use_checkboxes_in_ui", settings["use_checkboxes_in_ui"]); // Security settings["web_ui_clickjacking_protection_enabled"] = document.getElementById("clickjacking_protection_checkbox").checked;