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.
This commit is contained in:
Stiliyan Tonev (Bark) 2025-06-20 10:34:25 +03:00
commit 37bf7b3134
2 changed files with 52 additions and 1 deletions

View file

@ -163,6 +163,9 @@ window.qBittorrent.DynamicTable ??= (() => {
}, true); }, true);
this.dynamicTableDiv.addEventListener("touchstart", (e) => { this.dynamicTableDiv.addEventListener("touchstart", (e) => {
// ignore touch events on checkboxes, otherwise breaks on mobile
if (e.target.classList.contains("selectRowCheckbox"))
return;
const tr = e.target.closest("tr"); const tr = e.target.closest("tr");
if (!tr) if (!tr)
return; return;
@ -588,6 +591,8 @@ window.qBittorrent.DynamicTable ??= (() => {
column["onVisibilityChange"] = null; column["onVisibilityChange"] = null;
column["staticWidth"] = null; column["staticWidth"] = null;
column["calculateBuffer"] = () => 0; column["calculateBuffer"] = () => 0;
column["jsManaged"] = false;
this.columns.push(column); this.columns.push(column);
this.columns[name] = column; this.columns[name] = column;
@ -736,10 +741,12 @@ window.qBittorrent.DynamicTable ??= (() => {
for (const row of this.getFilteredAndSortedRows()) for (const row of this.getFilteredAndSortedRows())
this.selectedRows.push(row.rowId); this.selectedRows.push(row.rowId);
this.setRowClass(); this.setRowClass();
this.onSelectedRowChanged();
} }
deselectAll() { deselectAll() {
this.selectedRows.empty(); this.selectedRows.empty();
this.onSelectedRowChanged();
} }
selectRow(rowId) { selectRow(rowId) {
@ -1000,7 +1007,7 @@ window.qBittorrent.DynamicTable ??= (() => {
tds[i].style.width = `${this.columns[i].width}px`; tds[i].style.width = `${this.columns[i].width}px`;
tds[i].style.maxWidth = `${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); this.columns[i].updateTd(tds[i], row);
} }
row["data"] = {}; row["data"] = {};
@ -1102,6 +1109,11 @@ window.qBittorrent.DynamicTable ??= (() => {
} }
initColumns() { initColumns() {
if (LocalPreferences.get("use_checkbox", "false") === "true") {
this.newColumn("selected", "width: 20px; text-align: center;", "", 20, true);
this.columns["selected"].jsManaged = true;
}
this.newColumn("priority", "", "#", 30, true); this.newColumn("priority", "", "#", 30, true);
this.newColumn("state_icon", "", "QBT_TR(Status Icon)QBT_TR[CONTEXT=TransferListModel]", 30, false); 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); this.newColumn("name", "", "QBT_TR(Name)QBT_TR[CONTEXT=TransferListModel]", 200, true);
@ -1202,6 +1214,29 @@ window.qBittorrent.DynamicTable ??= (() => {
return `stateIcon ${stateClass}`; return `stateIcon ${stateClass}`;
}; };
if (LocalPreferences.get("use_checkbox", "false") === "true") {
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 = this.isRowSelected(row.rowId);
if (td.firstElementChild === null) {
htmlInput.addEventListener("click", (e) => {
e.stopPropagation();
if (htmlInput.checked)
this.selectRow(row.rowId);
else
this.deselectRow(row.rowId);
});
td.append(htmlInput);
}
};
}
// state_icon // state_icon
this.columns["state_icon"].updateTd = function(td, row) { this.columns["state_icon"].updateTd = function(td, row) {
const state = this.getRowValue(row); const state = this.getRowValue(row);
@ -1817,6 +1852,15 @@ window.qBittorrent.DynamicTable ??= (() => {
onSelectedRowChanged() { onSelectedRowChanged() {
updatePropertiesPanel(); 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);
}
} }
} }

View file

@ -1031,6 +1031,10 @@
<div class="formRow"> <div class="formRow">
<a href="https://github.com/qbittorrent/qBittorrent/wiki/List-of-known-alternate-WebUIs" target="_blank">QBT_TR(List of alternative WebUI)QBT_TR[CONTEXT=OptionsDialog]</a> <a href="https://github.com/qbittorrent/qBittorrent/wiki/List-of-known-alternate-WebUIs" target="_blank">QBT_TR(List of alternative WebUI)QBT_TR[CONTEXT=OptionsDialog]</a>
</div> </div>
<div class="formRow">
<input type="checkbox" id="useCheckbox">
<label for="useCheckbox">QBT_TR(Use checkboxes in WebUI)QBT_TR[CONTEXT=OptionsDialog]</label>
</div>
</fieldset> </fieldset>
<fieldset class="settings"> <fieldset class="settings">
@ -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("use_https_checkbox").checked = pref.use_https;
document.getElementById("ssl_cert_text").value = pref.web_ui_https_cert_path; 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("ssl_key_text").value = pref.web_ui_https_key_path;
document.getElementById("useCheckbox").checked = LocalPreferences.get("use_checkbox", "false") === "true";
updateHttpsSettings(); updateHttpsSettings();
// Authentication // 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_enabled"] = alternative_webui_enabled;
settings["alternative_webui_path"] = webui_files_location_textarea; settings["alternative_webui_path"] = webui_files_location_textarea;
settings["use_checkbox"] = document.getElementById("useCheckbox").checked;
LocalPreferences.set("use_checkbox", settings["use_checkbox"]);
// Security // Security
settings["web_ui_clickjacking_protection_enabled"] = document.getElementById("clickjacking_protection_checkbox").checked; settings["web_ui_clickjacking_protection_enabled"] = document.getElementById("clickjacking_protection_checkbox").checked;