mirror of
https://github.com/qbittorrent/qBittorrent
synced 2025-08-20 13:23:34 -07:00
WebUI: Optimize table performance with virtual list
Adding virtual list support to dynamic tables to improve performance on large lists, I observed a 100x performance improvement on rendering on a torrent table with 5000 torrents. This optimization is disabled by default and can be enabled in options. PR #22502.
This commit is contained in:
parent
250fef4ee7
commit
b4a16f6464
9 changed files with 474 additions and 359 deletions
|
@ -41,7 +41,7 @@
|
|||
|
||||
// Setup the dynamic table for bulk renaming
|
||||
const bulkRenameFilesTable = new window.qBittorrent.DynamicTable.BulkRenameTorrentFilesTable();
|
||||
bulkRenameFilesTable.setup("bulkRenameFilesTableDiv", "bulkRenameFilesTableFixedHeaderDiv", bulkRenameFilesContextMenu);
|
||||
bulkRenameFilesTable.setup("bulkRenameFilesTableDiv", "bulkRenameFilesTableFixedHeaderDiv", bulkRenameFilesContextMenu, true);
|
||||
|
||||
// Inject checkbox into the first column of the table header
|
||||
const tableHeaders = document.querySelectorAll("#bulkRenameFilesTableFixedHeaderDiv .dynamicTableHeader th");
|
||||
|
@ -172,7 +172,10 @@
|
|||
// Update renamed column for matched rows
|
||||
for (let i = 0; i < matchedRows.length; ++i) {
|
||||
const row = matchedRows[i];
|
||||
$(`filesTablefileRenamed${row.rowId}`).textContent = row.renamed;
|
||||
const element = document.getElementById(`filesTablefileRenamed${row.rowId}`);
|
||||
if (element === null)
|
||||
continue;
|
||||
element.textContent = row.renamed;
|
||||
}
|
||||
};
|
||||
fileRenamer.onInvalidRegex = (err) => {
|
||||
|
@ -287,11 +290,6 @@
|
|||
event.preventDefault();
|
||||
window.qBittorrent.Client.closeWindow(windowEl);
|
||||
});
|
||||
// synchronize header scrolling to table body
|
||||
$("bulkRenameFilesTableDiv").onscroll = function() {
|
||||
const length = $(this).scrollLeft;
|
||||
$("bulkRenameFilesTableFixedHeaderDiv").scrollLeft = length;
|
||||
};
|
||||
|
||||
const handleTorrentFiles = (files, selectedRows) => {
|
||||
const rows = files.map((file, index) => {
|
||||
|
|
|
@ -71,14 +71,18 @@ window.qBittorrent.DynamicTable ??= (() => {
|
|||
|
||||
initialize: () => {},
|
||||
|
||||
setup: function(dynamicTableDivId, dynamicTableFixedHeaderDivId, contextMenu) {
|
||||
setup: function(dynamicTableDivId, dynamicTableFixedHeaderDivId, contextMenu, useVirtualList = false) {
|
||||
this.dynamicTableDivId = dynamicTableDivId;
|
||||
this.dynamicTableFixedHeaderDivId = dynamicTableFixedHeaderDivId;
|
||||
this.dynamicTableDiv = document.getElementById(dynamicTableDivId);
|
||||
this.useVirtualList = useVirtualList && (LocalPreferences.get("use_virtual_list", "false") === "true");
|
||||
this.fixedTableHeader = document.querySelector(`#${dynamicTableFixedHeaderDivId} thead tr`);
|
||||
this.hiddenTableHeader = this.dynamicTableDiv.querySelector(`thead tr`);
|
||||
this.table = this.dynamicTableDiv.querySelector(`table`);
|
||||
this.tableBody = this.dynamicTableDiv.querySelector(`tbody`);
|
||||
this.rowHeight = 26;
|
||||
this.rows = new Map();
|
||||
this.cachedElements = [];
|
||||
this.selectedRows = [];
|
||||
this.columns = [];
|
||||
this.contextMenu = contextMenu;
|
||||
|
@ -91,14 +95,34 @@ window.qBittorrent.DynamicTable ??= (() => {
|
|||
this.setupHeaderEvents();
|
||||
this.setupHeaderMenu();
|
||||
this.setupAltRow();
|
||||
this.setupVirtualList();
|
||||
},
|
||||
|
||||
setupVirtualList: function() {
|
||||
if (!this.useVirtualList)
|
||||
return;
|
||||
this.table.style.position = "relative";
|
||||
|
||||
this.renderedHeight = this.dynamicTableDiv.offsetHeight;
|
||||
const resizeCallback = window.qBittorrent.Misc.createDebounceHandler(100, () => {
|
||||
const height = this.dynamicTableDiv.offsetHeight;
|
||||
const needRerender = this.renderedHeight < height;
|
||||
this.renderedHeight = height;
|
||||
if (needRerender)
|
||||
this.rerender();
|
||||
});
|
||||
new ResizeObserver(resizeCallback).observe(this.dynamicTableDiv);
|
||||
},
|
||||
|
||||
setupCommonEvents: function() {
|
||||
const tableFixedHeaderDiv = $(this.dynamicTableFixedHeaderDivId);
|
||||
const tableFixedHeaderDiv = document.getElementById(this.dynamicTableFixedHeaderDivId);
|
||||
|
||||
const tableElement = tableFixedHeaderDiv.querySelector("table");
|
||||
this.dynamicTableDiv.addEventListener("scroll", function() {
|
||||
tableElement.style.left = `${-this.scrollLeft}px`;
|
||||
this.dynamicTableDiv.addEventListener("scroll", (e) => {
|
||||
tableElement.style.left = `${-this.dynamicTableDiv.scrollLeft}px`;
|
||||
// rerender on scroll
|
||||
if (this.useVirtualList)
|
||||
this.rerender();
|
||||
});
|
||||
|
||||
this.dynamicTableDiv.addEventListener("click", (e) => {
|
||||
|
@ -404,6 +428,9 @@ window.qBittorrent.DynamicTable ??= (() => {
|
|||
const style = `width: ${column.width}px; ${column.style}`;
|
||||
this.getRowCells(this.hiddenTableHeader)[pos].style.cssText = style;
|
||||
this.getRowCells(this.fixedTableHeader)[pos].style.cssText = style;
|
||||
// rerender on column resize
|
||||
if (this.useVirtualList)
|
||||
this.rerender();
|
||||
|
||||
column.onResize?.(column.name);
|
||||
},
|
||||
|
@ -707,10 +734,9 @@ window.qBittorrent.DynamicTable ??= (() => {
|
|||
|
||||
selectAll: function() {
|
||||
this.deselectAll();
|
||||
for (const tr of this.getTrs()) {
|
||||
this.selectedRows.push(tr.rowId);
|
||||
tr.classList.add("selected");
|
||||
}
|
||||
for (const row of this.getFilteredAndSortedRows())
|
||||
this.selectedRows.push(row.rowId);
|
||||
this.setRowClass();
|
||||
},
|
||||
|
||||
deselectAll: function() {
|
||||
|
@ -832,64 +858,138 @@ window.qBittorrent.DynamicTable ??= (() => {
|
|||
}
|
||||
}
|
||||
|
||||
const trs = [...this.getTrs()];
|
||||
if (this.useVirtualList) {
|
||||
// rerender on table update
|
||||
this.rerender(rows);
|
||||
}
|
||||
else {
|
||||
const trs = [...this.getTrs()];
|
||||
|
||||
for (let rowPos = 0; rowPos < rows.length; ++rowPos) {
|
||||
const rowId = rows[rowPos]["rowId"];
|
||||
let tr_found = false;
|
||||
for (let j = rowPos; j < trs.length; ++j) {
|
||||
if (trs[j]["rowId"] === rowId) {
|
||||
tr_found = true;
|
||||
if (rowPos === j)
|
||||
for (let rowPos = 0; rowPos < rows.length; ++rowPos) {
|
||||
const rowId = rows[rowPos].rowId;
|
||||
let tr_found = false;
|
||||
for (let j = rowPos; j < trs.length; ++j) {
|
||||
if (trs[j].rowId === rowId) {
|
||||
tr_found = true;
|
||||
if (rowPos === j)
|
||||
break;
|
||||
trs[j].inject(trs[rowPos], "before");
|
||||
const tmpTr = trs[j];
|
||||
trs.splice(j, 1);
|
||||
trs.splice(rowPos, 0, tmpTr);
|
||||
break;
|
||||
trs[j].inject(trs[rowPos], "before");
|
||||
const tmpTr = trs[j];
|
||||
trs.splice(j, 1);
|
||||
trs.splice(rowPos, 0, tmpTr);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (tr_found) { // row already exists in the table
|
||||
this.updateRow(trs[rowPos], fullUpdate);
|
||||
}
|
||||
else { // else create a new row in the table
|
||||
const tr = this.createRowElement(rows[rowPos]);
|
||||
|
||||
// Insert
|
||||
if (rowPos >= trs.length) {
|
||||
tr.inject(this.tableBody);
|
||||
trs.push(tr);
|
||||
}
|
||||
else {
|
||||
tr.inject(trs[rowPos], "before");
|
||||
trs.splice(rowPos, 0, tr);
|
||||
}
|
||||
|
||||
this.updateRow(tr, true);
|
||||
}
|
||||
}
|
||||
if (tr_found) { // row already exists in the table
|
||||
this.updateRow(trs[rowPos], fullUpdate);
|
||||
|
||||
const rowPos = rows.length;
|
||||
|
||||
while ((rowPos < trs.length) && (trs.length > 0))
|
||||
trs.pop().destroy();
|
||||
}
|
||||
},
|
||||
|
||||
rerender: function(rows = this.getFilteredAndSortedRows()) {
|
||||
// set the scrollable height
|
||||
this.table.style.height = `${rows.length * this.rowHeight}px`;
|
||||
|
||||
// show extra 6 rows at top/bottom to reduce flickering
|
||||
const extraRowCount = 6;
|
||||
// how many rows can be shown in the visible area
|
||||
const visibleRowCount = Math.ceil(this.renderedHeight / this.rowHeight) + (extraRowCount * 2);
|
||||
// start position of visible rows, offsetted by scrollTop
|
||||
let startRow = Math.max((Math.trunc(this.dynamicTableDiv.scrollTop / this.rowHeight) - extraRowCount), 0);
|
||||
// ensure startRow is even
|
||||
if ((startRow % 2) === 1)
|
||||
startRow = Math.max(0, startRow - 1);
|
||||
const endRow = Math.min((startRow + visibleRowCount), rows.length);
|
||||
|
||||
const elements = [];
|
||||
for (let i = startRow; i < endRow; ++i) {
|
||||
const row = rows[i];
|
||||
if (row === undefined)
|
||||
continue;
|
||||
const offset = i * this.rowHeight;
|
||||
const position = i - startRow;
|
||||
// reuse existing elements
|
||||
let element = this.cachedElements[position];
|
||||
if (element !== undefined)
|
||||
this.updateRowElement(element, row.rowId, offset);
|
||||
else
|
||||
element = this.cachedElements[position] = this.createRowElement(row, offset);
|
||||
elements.push(element);
|
||||
}
|
||||
this.tableBody.replaceChildren(...elements);
|
||||
|
||||
// update row classes
|
||||
this.setRowClass();
|
||||
|
||||
// update visible rows
|
||||
for (const row of this.tableBody.children)
|
||||
this.updateRow(row, true);
|
||||
|
||||
// refresh row height based on first row
|
||||
setTimeout(() => {
|
||||
if (this.tableBody.firstChild === null)
|
||||
return;
|
||||
const tr = this.tableBody.firstChild;
|
||||
if (this.rowHeight !== tr.offsetHeight) {
|
||||
this.rowHeight = tr.offsetHeight;
|
||||
// rerender on row height change
|
||||
this.rerender();
|
||||
}
|
||||
else { // else create a new row in the table
|
||||
const tr = document.createElement("tr");
|
||||
// set tabindex so element receives keydown events
|
||||
// more info: https://developer.mozilla.org/en-US/docs/Web/API/Element/keydown_event
|
||||
tr.tabIndex = -1;
|
||||
|
||||
const rowId = rows[rowPos]["rowId"];
|
||||
tr.setAttribute("data-row-id", rowId);
|
||||
tr["rowId"] = rowId;
|
||||
});
|
||||
},
|
||||
|
||||
for (let k = 0; k < this.columns.length; ++k) {
|
||||
const td = document.createElement("td");
|
||||
if ((this.columns[k].visible === "0") || this.columns[k].force_hide)
|
||||
td.classList.add("invisible");
|
||||
tr.append(td);
|
||||
}
|
||||
createRowElement: function(row, top = -1) {
|
||||
const tr = document.createElement("tr");
|
||||
// set tabindex so element receives keydown events
|
||||
// more info: https://developer.mozilla.org/en-US/docs/Web/API/Element/keydown_event
|
||||
tr.tabIndex = -1;
|
||||
|
||||
// Insert
|
||||
if (rowPos >= trs.length) {
|
||||
tr.inject(this.tableBody);
|
||||
trs.push(tr);
|
||||
}
|
||||
else {
|
||||
tr.inject(trs[rowPos], "before");
|
||||
trs.splice(rowPos, 0, tr);
|
||||
}
|
||||
|
||||
// Update context menu
|
||||
this.contextMenu?.addTarget(tr);
|
||||
|
||||
this.updateRow(tr, true);
|
||||
}
|
||||
for (let k = 0; k < this.columns.length; ++k) {
|
||||
const td = document.createElement("td");
|
||||
if ((this.columns[k].visible === "0") || this.columns[k].force_hide)
|
||||
td.classList.add("invisible");
|
||||
tr.append(td);
|
||||
}
|
||||
|
||||
const rowPos = rows.length;
|
||||
this.updateRowElement(tr, row.rowId, top);
|
||||
|
||||
while ((rowPos < trs.length) && (trs.length > 0))
|
||||
trs.pop().destroy();
|
||||
// update context menu
|
||||
this.contextMenu?.addTarget(tr);
|
||||
return tr;
|
||||
},
|
||||
|
||||
updateRowElement: function(tr, rowId, top) {
|
||||
tr.dataset.rowId = rowId;
|
||||
tr.rowId = rowId;
|
||||
|
||||
tr.className = "";
|
||||
|
||||
if (this.useVirtualList) {
|
||||
tr.style.position = "absolute";
|
||||
tr.style.top = `${top}px`;
|
||||
}
|
||||
},
|
||||
|
||||
updateRow: function(tr, fullUpdate) {
|
||||
|
@ -898,6 +998,11 @@ window.qBittorrent.DynamicTable ??= (() => {
|
|||
|
||||
const tds = this.getRowCells(tr);
|
||||
for (let i = 0; i < this.columns.length; ++i) {
|
||||
// required due to position: absolute breaks table layout
|
||||
if (this.useVirtualList) {
|
||||
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)))
|
||||
this.columns[i].updateTd(tds[i], row);
|
||||
}
|
||||
|
@ -907,15 +1012,25 @@ window.qBittorrent.DynamicTable ??= (() => {
|
|||
removeRow: function(rowId) {
|
||||
this.selectedRows.erase(rowId);
|
||||
this.rows.delete(rowId);
|
||||
const tr = this.getTrByRowId(rowId);
|
||||
tr?.destroy();
|
||||
if (this.useVirtualList) {
|
||||
this.rerender();
|
||||
}
|
||||
else {
|
||||
const tr = this.getTrByRowId(rowId);
|
||||
tr?.destroy();
|
||||
}
|
||||
},
|
||||
|
||||
clear: function() {
|
||||
this.deselectAll();
|
||||
this.rows.clear();
|
||||
for (const tr of this.getTrs())
|
||||
tr.destroy();
|
||||
if (this.useVirtualList) {
|
||||
this.rerender();
|
||||
}
|
||||
else {
|
||||
for (const tr of this.getTrs())
|
||||
tr.destroy();
|
||||
}
|
||||
},
|
||||
|
||||
selectedRowsIds: function() {
|
||||
|
@ -986,6 +1101,12 @@ window.qBittorrent.DynamicTable ??= (() => {
|
|||
const TorrentsTable = new Class({
|
||||
Extends: DynamicTable,
|
||||
|
||||
setupVirtualList: function() {
|
||||
this.parent();
|
||||
|
||||
this.rowHeight = 22;
|
||||
},
|
||||
|
||||
initColumns: function() {
|
||||
this.newColumn("priority", "", "#", 30, true);
|
||||
this.newColumn("state_icon", "", "QBT_TR(Status Icon)QBT_TR[CONTEXT=TransferListModel]", 30, false);
|
||||
|
@ -2075,6 +2196,12 @@ window.qBittorrent.DynamicTable ??= (() => {
|
|||
prevReverseSort: null,
|
||||
fileTree: new window.qBittorrent.FileTree.FileTree(),
|
||||
|
||||
setupVirtualList: function() {
|
||||
this.parent();
|
||||
|
||||
this.rowHeight = 29;
|
||||
},
|
||||
|
||||
populateTable: function(root) {
|
||||
this.fileTree.setRoot(root);
|
||||
root.children.each((node) => {
|
||||
|
@ -2145,30 +2272,30 @@ window.qBittorrent.DynamicTable ??= (() => {
|
|||
* Toggles the global checkbox and all checkboxes underneath
|
||||
*/
|
||||
toggleGlobalCheckbox: function() {
|
||||
const checkbox = $("rootMultiRename_cb");
|
||||
const checkbox = document.getElementById("rootMultiRename_cb");
|
||||
const checkboxes = document.querySelectorAll("input.RenamingCB");
|
||||
|
||||
for (let i = 0; i < checkboxes.length; ++i) {
|
||||
const node = this.getNode(i);
|
||||
|
||||
if (checkbox.checked || checkbox.indeterminate) {
|
||||
const cb = checkboxes[i];
|
||||
cb.checked = true;
|
||||
cb.indeterminate = false;
|
||||
cb.state = "checked";
|
||||
node.checked = 0;
|
||||
node.full_data.checked = node.checked;
|
||||
}
|
||||
else {
|
||||
const cb = checkboxes[i];
|
||||
cb.checked = false;
|
||||
cb.indeterminate = false;
|
||||
cb.state = "unchecked";
|
||||
node.checked = 1;
|
||||
node.full_data.checked = node.checked;
|
||||
}
|
||||
}
|
||||
|
||||
const nodes = this.fileTree.toArray();
|
||||
for (const node of nodes) {
|
||||
node.checked = (checkbox.checked || checkbox.indeterminate) ? 0 : 1;
|
||||
node.full_data.checked = node.checked;
|
||||
}
|
||||
|
||||
this.updateGlobalCheckbox();
|
||||
},
|
||||
|
||||
|
@ -2176,7 +2303,7 @@ window.qBittorrent.DynamicTable ??= (() => {
|
|||
const node = this.getNode(rowId);
|
||||
node.checked = checkState;
|
||||
node.full_data.checked = checkState;
|
||||
const checkbox = $(`cbRename${rowId}`);
|
||||
const checkbox = document.getElementById(`cbRename${rowId}`);
|
||||
checkbox.checked = node.checked === 0;
|
||||
checkbox.state = checkbox.checked ? "checked" : "unchecked";
|
||||
|
||||
|
@ -2184,11 +2311,11 @@ window.qBittorrent.DynamicTable ??= (() => {
|
|||
this.toggleNodeTreeCheckbox(node.children[i].rowId, checkState);
|
||||
},
|
||||
|
||||
updateGlobalCheckbox: () => {
|
||||
const checkbox = $("rootMultiRename_cb");
|
||||
const checkboxes = document.querySelectorAll("input.RenamingCB");
|
||||
const isAllChecked = Array.prototype.every.call(checkboxes, (checkbox => checkbox.checked));
|
||||
const isAllUnchecked = (() => Array.prototype.every.call(checkboxes, (checkbox => !checkbox.checked)));
|
||||
updateGlobalCheckbox: function() {
|
||||
const checkbox = document.getElementById("rootMultiRename_cb");
|
||||
const nodes = this.fileTree.toArray();
|
||||
const isAllChecked = nodes.every((node) => node.checked === 0);
|
||||
const isAllUnchecked = (() => nodes.every((node) => node.checked !== 0));
|
||||
if (isAllChecked) {
|
||||
checkbox.state = "checked";
|
||||
checkbox.indeterminate = false;
|
||||
|
@ -2210,82 +2337,86 @@ window.qBittorrent.DynamicTable ??= (() => {
|
|||
const that = this;
|
||||
|
||||
// checked
|
||||
this.columns["checked"].updateTd = function(td, row) {
|
||||
this.columns["checked"].updateTd = (td, row) => {
|
||||
const id = row.rowId;
|
||||
const value = this.getRowValue(row);
|
||||
const node = that.getNode(id);
|
||||
|
||||
const treeImg = document.createElement("img");
|
||||
treeImg.src = "images/L.gif";
|
||||
treeImg.style.marginBottom = "-2px";
|
||||
if (td.firstElementChild === null) {
|
||||
const treeImg = document.createElement("img");
|
||||
treeImg.src = "images/L.gif";
|
||||
treeImg.style.marginBottom = "-2px";
|
||||
td.append(treeImg);
|
||||
}
|
||||
|
||||
const checkbox = document.createElement("input");
|
||||
checkbox.type = "checkbox";
|
||||
let checkbox = td.children[1];
|
||||
if (checkbox === undefined) {
|
||||
checkbox = document.createElement("input");
|
||||
checkbox.type = "checkbox";
|
||||
checkbox.className = "RenamingCB";
|
||||
checkbox.addEventListener("click", (e) => {
|
||||
const id = e.target.dataset.id;
|
||||
const node = that.getNode(id);
|
||||
node.checked = e.target.checked ? 0 : 1;
|
||||
node.full_data.checked = node.checked;
|
||||
that.updateGlobalCheckbox();
|
||||
that.onRowSelectionChange(node);
|
||||
e.stopPropagation();
|
||||
});
|
||||
checkbox.indeterminate = false;
|
||||
td.append(checkbox);
|
||||
}
|
||||
checkbox.id = `cbRename${id}`;
|
||||
checkbox.setAttribute("data-id", id);
|
||||
checkbox.className = "RenamingCB";
|
||||
checkbox.addEventListener("click", (e) => {
|
||||
const node = that.getNode(id);
|
||||
node.checked = e.target.checked ? 0 : 1;
|
||||
node.full_data.checked = node.checked;
|
||||
that.updateGlobalCheckbox();
|
||||
that.onRowSelectionChange(node);
|
||||
e.stopPropagation();
|
||||
});
|
||||
checkbox.checked = (value === 0);
|
||||
checkbox.dataset.id = id;
|
||||
checkbox.checked = (node.checked === 0);
|
||||
checkbox.state = checkbox.checked ? "checked" : "unchecked";
|
||||
checkbox.indeterminate = false;
|
||||
td.replaceChildren(treeImg, checkbox);
|
||||
};
|
||||
this.columns["checked"].staticWidth = 50;
|
||||
|
||||
// original
|
||||
this.columns["original"].updateTd = function(td, row) {
|
||||
const id = row.rowId;
|
||||
const fileNameId = `filesTablefileName${id}`;
|
||||
const node = that.getNode(id);
|
||||
const value = this.getRowValue(row);
|
||||
|
||||
let dirImg = td.children[0];
|
||||
if (dirImg === undefined) {
|
||||
dirImg = document.createElement("img");
|
||||
dirImg.src = "images/directory.svg";
|
||||
dirImg.style.width = "20px";
|
||||
dirImg.style.paddingRight = "5px";
|
||||
dirImg.style.marginBottom = "-3px";
|
||||
td.append(dirImg);
|
||||
}
|
||||
if (node.isFolder) {
|
||||
const value = this.getRowValue(row);
|
||||
const dirImgId = `renameTableDirImg${id}`;
|
||||
if ($(dirImgId)) {
|
||||
// just update file name
|
||||
$(fileNameId).textContent = value;
|
||||
}
|
||||
else {
|
||||
const span = document.createElement("span");
|
||||
span.textContent = value;
|
||||
span.id = fileNameId;
|
||||
dirImg.style.display = "inline";
|
||||
dirImg.style.marginLeft = `${node.depth * 20}px`;
|
||||
}
|
||||
else {
|
||||
dirImg.style.display = "none";
|
||||
}
|
||||
|
||||
const dirImg = document.createElement("img");
|
||||
dirImg.src = "images/directory.svg";
|
||||
dirImg.style.width = "20px";
|
||||
dirImg.style.paddingRight = "5px";
|
||||
dirImg.style.marginBottom = "-3px";
|
||||
dirImg.style.marginLeft = `${node.depth * 20}px`;
|
||||
dirImg.id = dirImgId;
|
||||
td.replaceChildren(dirImg, span);
|
||||
}
|
||||
}
|
||||
else { // is file
|
||||
const value = this.getRowValue(row);
|
||||
const span = document.createElement("span");
|
||||
span.textContent = value;
|
||||
span.id = fileNameId;
|
||||
span.style.marginLeft = `${(node.depth + 1) * 20}px`;
|
||||
td.replaceChildren(span);
|
||||
let span = td.children[1];
|
||||
if (span === undefined) {
|
||||
span = document.createElement("span");
|
||||
td.append(span);
|
||||
}
|
||||
span.textContent = value;
|
||||
span.style.marginLeft = node.isFolder ? "0" : `${(node.depth + 1) * 20}px`;
|
||||
};
|
||||
|
||||
// renamed
|
||||
this.columns["renamed"].updateTd = function(td, row) {
|
||||
this.columns["renamed"].updateTd = (td, row) => {
|
||||
const id = row.rowId;
|
||||
const fileNameRenamedId = `filesTablefileRenamed${id}`;
|
||||
const value = this.getRowValue(row);
|
||||
const node = that.getNode(id);
|
||||
|
||||
const span = document.createElement("span");
|
||||
span.textContent = value;
|
||||
let span = td.firstElementChild;
|
||||
if (span === null) {
|
||||
span = document.createElement("span");
|
||||
td.append(span);
|
||||
}
|
||||
span.id = fileNameRenamedId;
|
||||
td.replaceChildren(span);
|
||||
span.textContent = node.renamed;
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -2431,7 +2562,15 @@ window.qBittorrent.DynamicTable ??= (() => {
|
|||
row.full_data.remaining = (row.full_data.size * (1.0 - (row.full_data.progress / 100)));
|
||||
},
|
||||
|
||||
setupCommonEvents: () => {}
|
||||
setupCommonEvents: function() {
|
||||
const headerDiv = document.getElementById("bulkRenameFilesTableFixedHeaderDiv");
|
||||
this.dynamicTableDiv.addEventListener("scroll", (e) => {
|
||||
headerDiv.scrollLeft = this.dynamicTableDiv.scrollLeft;
|
||||
// rerender on scroll
|
||||
if (this.useVirtualList)
|
||||
this.rerender();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const TorrentFilesTable = new Class({
|
||||
|
@ -2445,6 +2584,108 @@ window.qBittorrent.DynamicTable ??= (() => {
|
|||
prevReverseSort: null,
|
||||
fileTree: new window.qBittorrent.FileTree.FileTree(),
|
||||
|
||||
initialize: function() {
|
||||
this.collapseState = new Map();
|
||||
},
|
||||
|
||||
isCollapsed: function(id) {
|
||||
return this.collapseState.get(id)?.collapsed ?? false;
|
||||
},
|
||||
|
||||
expandNode: function(id) {
|
||||
const state = this.collapseState.get(id);
|
||||
if (state !== undefined)
|
||||
state.collapsed = false;
|
||||
this._updateNodeState(id, false);
|
||||
},
|
||||
|
||||
collapseNode: function(id) {
|
||||
const state = this.collapseState.get(id);
|
||||
if (state !== undefined)
|
||||
state.collapsed = true;
|
||||
this._updateNodeState(id, true);
|
||||
},
|
||||
|
||||
expandAllNodes: function() {
|
||||
for (const [key, _] of this.collapseState)
|
||||
this.expandNode(key);
|
||||
},
|
||||
|
||||
collapseAllNodes: function() {
|
||||
for (const [key, state] of this.collapseState) {
|
||||
// collapse all nodes except root
|
||||
if (state.depth >= 1)
|
||||
this.collapseNode(key);
|
||||
}
|
||||
},
|
||||
|
||||
_updateNodeVisibility: (node, shouldHide) => {
|
||||
const span = document.getElementById(`filesTablefileName${node.rowId}`);
|
||||
// span won't exist if row has been filtered out
|
||||
if (span === null)
|
||||
return;
|
||||
const tr = span.parentElement.parentElement;
|
||||
tr.classList.toggle("invisible", shouldHide);
|
||||
},
|
||||
|
||||
_updateNodeCollapseIcon: (node, isCollapsed) => {
|
||||
const span = document.getElementById(`filesTablefileName${node.rowId}`);
|
||||
// span won't exist if row has been filtered out
|
||||
if (span === null)
|
||||
return;
|
||||
const td = span.parentElement;
|
||||
|
||||
// rotate the collapse icon
|
||||
const collapseIcon = td.firstElementChild;
|
||||
collapseIcon.classList.toggle("rotate", isCollapsed);
|
||||
},
|
||||
|
||||
_updateNodeState: function(id, shouldCollapse) {
|
||||
// collapsed rows will be filtered out when using virtual list
|
||||
if (this.useVirtualList)
|
||||
return;
|
||||
const node = this.getNode(id);
|
||||
if (!node.isFolder)
|
||||
return;
|
||||
|
||||
this._updateNodeCollapseIcon(node, shouldCollapse);
|
||||
|
||||
for (const child of node.children)
|
||||
this._updateNodeVisibility(child, shouldCollapse);
|
||||
},
|
||||
|
||||
clear: function() {
|
||||
this.parent();
|
||||
|
||||
this.collapseState.clear();
|
||||
},
|
||||
|
||||
setupVirtualList: function() {
|
||||
this.parent();
|
||||
|
||||
this.rowHeight = 29.5;
|
||||
},
|
||||
|
||||
expandFolder: function(id) {
|
||||
const node = this.getNode(id);
|
||||
if (node.isFolder)
|
||||
this.expandNode(node);
|
||||
},
|
||||
|
||||
collapseFolder: function(id) {
|
||||
const node = this.getNode(id);
|
||||
if (node.isFolder)
|
||||
this.collapseNode(node);
|
||||
},
|
||||
|
||||
isAllCheckboxesChecked: function() {
|
||||
return this.fileTree.toArray().every((node) => node.checked === 1);
|
||||
},
|
||||
|
||||
isAllCheckboxesUnchecked: function() {
|
||||
return this.fileTree.toArray().every((node) => node.checked !== 1);
|
||||
},
|
||||
|
||||
populateTable: function(root) {
|
||||
this.fileTree.setRoot(root);
|
||||
root.children.each((node) => {
|
||||
|
@ -2456,6 +2697,8 @@ window.qBittorrent.DynamicTable ??= (() => {
|
|||
node.depth = depth;
|
||||
|
||||
if (node.isFolder) {
|
||||
if (!this.collapseState.has(node.rowId))
|
||||
this.collapseState.set(node.rowId, { depth: depth, collapsed: depth > 0 });
|
||||
const data = {
|
||||
rowId: node.rowId,
|
||||
size: node.size,
|
||||
|
@ -2470,7 +2713,7 @@ window.qBittorrent.DynamicTable ??= (() => {
|
|||
|
||||
node.data = data;
|
||||
node.full_data = data;
|
||||
this.updateRowData(data);
|
||||
this.updateRowData(data, depth);
|
||||
}
|
||||
else {
|
||||
node.data.rowId = node.rowId;
|
||||
|
@ -2496,6 +2739,11 @@ window.qBittorrent.DynamicTable ??= (() => {
|
|||
return this.rows.get(rowId);
|
||||
},
|
||||
|
||||
getRowFileId: function(rowId) {
|
||||
const row = this.rows.get(rowId);
|
||||
return row?.full_data.fileId;
|
||||
},
|
||||
|
||||
initColumns: function() {
|
||||
this.newColumn("checked", "", "", 50, true);
|
||||
this.newColumn("name", "", "QBT_TR(Name)QBT_TR[CONTEXT=TrackerListWidget]", 300, true);
|
||||
|
@ -2526,15 +2774,19 @@ window.qBittorrent.DynamicTable ??= (() => {
|
|||
const id = row.rowId;
|
||||
const value = this.getRowValue(row);
|
||||
|
||||
if (window.qBittorrent.PropFiles.isDownloadCheckboxExists(id)) {
|
||||
window.qBittorrent.PropFiles.updateDownloadCheckbox(id, value);
|
||||
}
|
||||
else {
|
||||
if (td.firstElementChild === null) {
|
||||
const treeImg = document.createElement("img");
|
||||
treeImg.src = "images/L.gif";
|
||||
treeImg.style.marginBottom = "-2px";
|
||||
td.append(treeImg, window.qBittorrent.PropFiles.createDownloadCheckbox(id, row.full_data.fileId, value));
|
||||
td.append(treeImg);
|
||||
}
|
||||
|
||||
const downloadCheckbox = td.children[1];
|
||||
if (downloadCheckbox === undefined)
|
||||
td.append(window.qBittorrent.PropFiles.createDownloadCheckbox(id, row.full_data.fileId, value));
|
||||
else
|
||||
window.qBittorrent.PropFiles.updateDownloadCheckbox(downloadCheckbox, id, row.full_data.fileId, value);
|
||||
|
||||
};
|
||||
this.columns["checked"].staticWidth = 50;
|
||||
|
||||
|
@ -2543,46 +2795,56 @@ window.qBittorrent.DynamicTable ??= (() => {
|
|||
const id = row.rowId;
|
||||
const fileNameId = `filesTablefileName${id}`;
|
||||
const node = that.getNode(id);
|
||||
const value = this.getRowValue(row);
|
||||
|
||||
let collapseIcon = td.firstElementChild;
|
||||
if (collapseIcon === null) {
|
||||
collapseIcon = document.createElement("img");
|
||||
collapseIcon.src = "images/go-down.svg";
|
||||
collapseIcon.className = "filesTableCollapseIcon";
|
||||
collapseIcon.addEventListener("click", (e) => {
|
||||
const id = collapseIcon.dataset.id;
|
||||
const node = that.getNode(id);
|
||||
if (node !== null) {
|
||||
if (that.isCollapsed(node.rowId))
|
||||
that.expandNode(node.rowId);
|
||||
else
|
||||
that.collapseNode(node.rowId);
|
||||
if (that.useVirtualList)
|
||||
that.rerender();
|
||||
}
|
||||
});
|
||||
td.append(collapseIcon);
|
||||
}
|
||||
if (node.isFolder) {
|
||||
const value = this.getRowValue(row);
|
||||
const collapseIconId = `filesTableCollapseIcon${id}`;
|
||||
const dirImgId = `filesTableDirImg${id}`;
|
||||
if ($(dirImgId)) {
|
||||
// just update file name
|
||||
$(fileNameId).textContent = value;
|
||||
}
|
||||
else {
|
||||
const collapseIcon = document.createElement("img");
|
||||
collapseIcon.src = "images/go-down.svg";
|
||||
collapseIcon.style.marginLeft = `${node.depth * 20}px`;
|
||||
collapseIcon.className = "filesTableCollapseIcon";
|
||||
collapseIcon.id = collapseIconId;
|
||||
collapseIcon.setAttribute("data-id", id);
|
||||
collapseIcon.addEventListener("click", function(e) { qBittorrent.PropFiles.collapseIconClicked(this); });
|
||||
|
||||
const span = document.createElement("span");
|
||||
span.textContent = value;
|
||||
span.id = fileNameId;
|
||||
|
||||
const dirImg = document.createElement("img");
|
||||
dirImg.src = "images/directory.svg";
|
||||
dirImg.style.width = "20px";
|
||||
dirImg.style.paddingRight = "5px";
|
||||
dirImg.style.marginBottom = "-3px";
|
||||
dirImg.id = dirImgId;
|
||||
|
||||
td.replaceChildren(collapseIcon, dirImg, span);
|
||||
}
|
||||
collapseIcon.style.marginLeft = `${node.depth * 20}px`;
|
||||
collapseIcon.style.display = "inline";
|
||||
collapseIcon.dataset.id = id;
|
||||
collapseIcon.classList.toggle("rotate", that.isCollapsed(node.rowId));
|
||||
}
|
||||
else {
|
||||
const value = this.getRowValue(row);
|
||||
const span = document.createElement("span");
|
||||
span.textContent = value;
|
||||
span.id = fileNameId;
|
||||
span.style.marginLeft = `${(node.depth + 1) * 20}px`;
|
||||
td.replaceChildren(span);
|
||||
collapseIcon.style.display = "none";
|
||||
}
|
||||
|
||||
let dirImg = td.children[1];
|
||||
if (dirImg === undefined) {
|
||||
dirImg = document.createElement("img");
|
||||
dirImg.src = "images/directory.svg";
|
||||
dirImg.style.width = "20px";
|
||||
dirImg.style.paddingRight = "5px";
|
||||
dirImg.style.marginBottom = "-3px";
|
||||
td.append(dirImg);
|
||||
}
|
||||
dirImg.style.display = node.isFolder ? "inline" : "none";
|
||||
|
||||
let span = td.children[2];
|
||||
if (span === undefined) {
|
||||
span = document.createElement("span");
|
||||
td.append(span);
|
||||
}
|
||||
span.id = fileNameId;
|
||||
span.textContent = value;
|
||||
span.style.marginLeft = node.isFolder ? "0" : `${(node.depth + 1) * 20}px`;
|
||||
};
|
||||
this.columns["name"].calculateBuffer = (rowId) => {
|
||||
const node = that.getNode(rowId);
|
||||
|
@ -2599,10 +2861,9 @@ window.qBittorrent.DynamicTable ??= (() => {
|
|||
const id = row.rowId;
|
||||
const value = Number(this.getRowValue(row));
|
||||
|
||||
const progressBar = $(`pbf_${id}`);
|
||||
const progressBar = td.firstElementChild;
|
||||
if (progressBar === null) {
|
||||
td.append(new window.qBittorrent.ProgressBar.ProgressBar(value, {
|
||||
id: `pbf_${id}`,
|
||||
width: 80
|
||||
}));
|
||||
}
|
||||
|
@ -2617,10 +2878,11 @@ window.qBittorrent.DynamicTable ??= (() => {
|
|||
const id = row.rowId;
|
||||
const value = this.getRowValue(row);
|
||||
|
||||
if (window.qBittorrent.PropFiles.isPriorityComboExists(id))
|
||||
window.qBittorrent.PropFiles.updatePriorityCombo(id, value);
|
||||
else
|
||||
const priorityCombo = td.firstElementChild;
|
||||
if (priorityCombo === null)
|
||||
td.append(window.qBittorrent.PropFiles.createPriorityCombo(id, row.full_data.fileId, value));
|
||||
else
|
||||
window.qBittorrent.PropFiles.updatePriorityCombo(priorityCombo, id, row.full_data.fileId, value);
|
||||
};
|
||||
this.columns["priority"].staticWidth = 140;
|
||||
|
||||
|
@ -2652,7 +2914,7 @@ window.qBittorrent.DynamicTable ??= (() => {
|
|||
},
|
||||
|
||||
_filterNodes: function(node, filterTerms, filteredRows) {
|
||||
if (node.isFolder) {
|
||||
if (node.isFolder && (!this.useVirtualList || !this.isCollapsed(node.rowId))) {
|
||||
const childAdded = node.children.reduce((acc, child) => {
|
||||
// we must execute the function before ORing w/ acc or we'll stop checking child nodes after the first successful match
|
||||
return (this._filterNodes(child, filterTerms, filteredRows) || acc);
|
||||
|
@ -2689,19 +2951,11 @@ window.qBittorrent.DynamicTable ??= (() => {
|
|||
const generateRowsSignature = () => {
|
||||
const rowsData = [];
|
||||
for (const { full_data } of this.getRowValues())
|
||||
rowsData.push(full_data);
|
||||
rowsData.push({ ...full_data, collapsed: this.isCollapsed(full_data.rowId) });
|
||||
return JSON.stringify(rowsData);
|
||||
};
|
||||
|
||||
const getFilteredRows = function() {
|
||||
if (this.filterTerms.length === 0) {
|
||||
const nodeArray = this.fileTree.toArray();
|
||||
const filteredRows = nodeArray.map((node) => {
|
||||
return this.getRow(node);
|
||||
});
|
||||
return filteredRows;
|
||||
}
|
||||
|
||||
const filteredRows = [];
|
||||
this.getRoot().children.each((child) => {
|
||||
this._filterNodes(child, this.filterTerms, filteredRows);
|
||||
|
@ -2758,11 +3012,11 @@ window.qBittorrent.DynamicTable ??= (() => {
|
|||
switch (e.key) {
|
||||
case "ArrowLeft":
|
||||
e.preventDefault();
|
||||
window.qBittorrent.PropFiles.collapseFolder(this.getSelectedRowId());
|
||||
this.collapseFolder(this.getSelectedRowId());
|
||||
break;
|
||||
case "ArrowRight":
|
||||
e.preventDefault();
|
||||
window.qBittorrent.PropFiles.expandFolder(this.getSelectedRowId());
|
||||
this.expandFolder(this.getSelectedRowId());
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
@ -3247,10 +3501,6 @@ window.qBittorrent.DynamicTable ??= (() => {
|
|||
|
||||
filterText: "",
|
||||
|
||||
filteredLength: function() {
|
||||
return this.tableBody.rows.length;
|
||||
},
|
||||
|
||||
initColumns: function() {
|
||||
this.newColumn("rowId", "", "QBT_TR(ID)QBT_TR[CONTEXT=ExecutionLogWidget]", 50, true);
|
||||
this.newColumn("message", "", "QBT_TR(Message)QBT_TR[CONTEXT=ExecutionLogWidget]", 350, true);
|
||||
|
@ -3293,7 +3543,7 @@ window.qBittorrent.DynamicTable ??= (() => {
|
|||
}
|
||||
td.textContent = logLevel;
|
||||
td.title = logLevel;
|
||||
td.closest("tr").className = `logTableRow${addClass}`;
|
||||
td.closest("tr").classList.add(`logTableRow${addClass}`);
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -3323,6 +3573,8 @@ window.qBittorrent.DynamicTable ??= (() => {
|
|||
return (this.reverseSort === "0") ? res : -res;
|
||||
});
|
||||
|
||||
this.filteredLength = filteredRows.length;
|
||||
|
||||
return filteredRows;
|
||||
},
|
||||
});
|
||||
|
@ -3355,7 +3607,7 @@ window.qBittorrent.DynamicTable ??= (() => {
|
|||
}
|
||||
td.textContent = status;
|
||||
td.title = status;
|
||||
td.closest("tr").className = `logTableRow${addClass}`;
|
||||
td.closest("tr").classList.add(`logTableRow${addClass}`);
|
||||
};
|
||||
},
|
||||
|
||||
|
|
|
@ -33,16 +33,11 @@ window.qBittorrent.PropFiles ??= (() => {
|
|||
const exports = () => {
|
||||
return {
|
||||
normalizePriority: normalizePriority,
|
||||
isDownloadCheckboxExists: isDownloadCheckboxExists,
|
||||
createDownloadCheckbox: createDownloadCheckbox,
|
||||
updateDownloadCheckbox: updateDownloadCheckbox,
|
||||
isPriorityComboExists: isPriorityComboExists,
|
||||
createPriorityCombo: createPriorityCombo,
|
||||
updatePriorityCombo: updatePriorityCombo,
|
||||
updateData: updateData,
|
||||
collapseIconClicked: collapseIconClicked,
|
||||
expandFolder: expandFolder,
|
||||
collapseFolder: collapseFolder,
|
||||
clear: clear
|
||||
};
|
||||
};
|
||||
|
@ -126,25 +121,20 @@ window.qBittorrent.PropFiles ??= (() => {
|
|||
updateGlobalCheckbox();
|
||||
};
|
||||
|
||||
const isDownloadCheckboxExists = (id) => {
|
||||
return $(`cbPrio${id}`) !== null;
|
||||
};
|
||||
|
||||
const createDownloadCheckbox = (id, fileId, checked) => {
|
||||
const checkbox = document.createElement("input");
|
||||
checkbox.type = "checkbox";
|
||||
checkbox.id = `cbPrio${id}`;
|
||||
checkbox.setAttribute("data-id", id);
|
||||
checkbox.setAttribute("data-file-id", fileId);
|
||||
checkbox.className = "DownloadedCB";
|
||||
checkbox.addEventListener("click", fileCheckboxClicked);
|
||||
|
||||
updateCheckbox(checkbox, checked);
|
||||
return checkbox;
|
||||
};
|
||||
|
||||
const updateDownloadCheckbox = (id, checked) => {
|
||||
const checkbox = $(`cbPrio${id}`);
|
||||
const updateDownloadCheckbox = (checkbox, id, fileId, checked) => {
|
||||
checkbox.setAttribute("data-id", id);
|
||||
checkbox.setAttribute("data-file-id", fileId);
|
||||
updateCheckbox(checkbox, checked);
|
||||
};
|
||||
|
||||
|
@ -162,10 +152,6 @@ window.qBittorrent.PropFiles ??= (() => {
|
|||
}
|
||||
};
|
||||
|
||||
const isPriorityComboExists = (id) => {
|
||||
return $(`comboPrio${id}`) !== null;
|
||||
};
|
||||
|
||||
const createPriorityCombo = (id, fileId, selectedPriority) => {
|
||||
const createOption = (priority, isSelected, text) => {
|
||||
const option = document.createElement("option");
|
||||
|
@ -195,8 +181,10 @@ window.qBittorrent.PropFiles ??= (() => {
|
|||
return select;
|
||||
};
|
||||
|
||||
const updatePriorityCombo = (id, selectedPriority) => {
|
||||
const combobox = $(`comboPrio${id}`);
|
||||
const updatePriorityCombo = (combobox, id, fileId, selectedPriority) => {
|
||||
combobox.id = `comboPrio${id}`;
|
||||
combobox.setAttribute("data-id", id);
|
||||
combobox.setAttribute("data-file-id", fileId);
|
||||
if (Number(combobox.value) !== selectedPriority)
|
||||
selectComboboxPriority(combobox, selectedPriority);
|
||||
};
|
||||
|
@ -258,9 +246,9 @@ window.qBittorrent.PropFiles ??= (() => {
|
|||
|
||||
const updateGlobalCheckbox = () => {
|
||||
const checkbox = $("tristate_cb");
|
||||
if (isAllCheckboxesChecked())
|
||||
if (torrentFilesTable.isAllCheckboxesChecked())
|
||||
setCheckboxChecked(checkbox);
|
||||
else if (isAllCheckboxesUnchecked())
|
||||
else if (torrentFilesTable.isAllCheckboxesUnchecked())
|
||||
setCheckboxUnchecked(checkbox);
|
||||
else
|
||||
setCheckboxPartial(checkbox);
|
||||
|
@ -283,10 +271,6 @@ window.qBittorrent.PropFiles ??= (() => {
|
|||
checkbox.indeterminate = true;
|
||||
};
|
||||
|
||||
const isAllCheckboxesChecked = () => Array.prototype.every.call(document.querySelectorAll("input.DownloadedCB"), (checkbox => checkbox.checked));
|
||||
|
||||
const isAllCheckboxesUnchecked = () => Array.prototype.every.call(document.querySelectorAll("input.DownloadedCB"), (checkbox => !checkbox.checked));
|
||||
|
||||
const setFilePriority = (ids, fileIds, priority) => {
|
||||
if (current_hash === "")
|
||||
return;
|
||||
|
@ -317,8 +301,6 @@ window.qBittorrent.PropFiles ??= (() => {
|
|||
if (combobox !== null)
|
||||
selectComboboxPriority(combobox, priority);
|
||||
});
|
||||
|
||||
torrentFilesTable.updateTable(false);
|
||||
};
|
||||
|
||||
let loadTorrentFilesDataTimer = -1;
|
||||
|
@ -364,7 +346,7 @@ window.qBittorrent.PropFiles ??= (() => {
|
|||
else {
|
||||
handleNewTorrentFiles(files);
|
||||
if (loadedNewTorrent)
|
||||
collapseAllNodes();
|
||||
torrentFilesTable.collapseAllNodes();
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
|
@ -470,29 +452,6 @@ window.qBittorrent.PropFiles ??= (() => {
|
|||
torrentFilesTable.reselectRows(selectedFiles);
|
||||
};
|
||||
|
||||
const collapseIconClicked = (event) => {
|
||||
const id = event.getAttribute("data-id");
|
||||
const node = torrentFilesTable.getNode(id);
|
||||
const isCollapsed = (event.parentElement.getAttribute("data-collapsed") === "true");
|
||||
|
||||
if (isCollapsed)
|
||||
expandNode(node);
|
||||
else
|
||||
collapseNode(node);
|
||||
};
|
||||
|
||||
const expandFolder = (id) => {
|
||||
const node = torrentFilesTable.getNode(id);
|
||||
if (node.isFolder)
|
||||
expandNode(node);
|
||||
};
|
||||
|
||||
const collapseFolder = (id) => {
|
||||
const node = torrentFilesTable.getNode(id);
|
||||
if (node.isFolder)
|
||||
collapseNode(node);
|
||||
};
|
||||
|
||||
const filesPriorityMenuClicked = (priority) => {
|
||||
const selectedRows = torrentFilesTable.selectedRowsIds();
|
||||
if (selectedRows.length === 0)
|
||||
|
@ -501,9 +460,8 @@ window.qBittorrent.PropFiles ??= (() => {
|
|||
const rowIds = [];
|
||||
const fileIds = [];
|
||||
selectedRows.forEach((rowId) => {
|
||||
const elem = $(`comboPrio${rowId}`);
|
||||
rowIds.push(rowId);
|
||||
fileIds.push(elem.getAttribute("data-file-id"));
|
||||
fileIds.push(torrentFilesTable.getRowFileId(rowId));
|
||||
});
|
||||
|
||||
const uniqueRowIds = {};
|
||||
|
@ -607,7 +565,7 @@ window.qBittorrent.PropFiles ??= (() => {
|
|||
}
|
||||
});
|
||||
|
||||
torrentFilesTable.setup("torrentFilesTableDiv", "torrentFilesTableFixedHeaderDiv", torrentFilesContextMenu);
|
||||
torrentFilesTable.setup("torrentFilesTableDiv", "torrentFilesTableFixedHeaderDiv", torrentFilesContextMenu, true);
|
||||
// inject checkbox into table header
|
||||
const tableHeaders = document.querySelectorAll("#torrentFilesTableFixedHeaderDiv .dynamicTableHeader th");
|
||||
if (tableHeaders.length > 0) {
|
||||
|
@ -641,111 +599,12 @@ window.qBittorrent.PropFiles ??= (() => {
|
|||
torrentFilesTable.updateTable();
|
||||
|
||||
if (value.trim() === "")
|
||||
collapseAllNodes();
|
||||
torrentFilesTable.collapseAllNodes();
|
||||
else
|
||||
expandAllNodes();
|
||||
torrentFilesTable.expandAllNodes();
|
||||
}, window.qBittorrent.Misc.FILTER_INPUT_DELAY);
|
||||
});
|
||||
|
||||
/**
|
||||
* Show/hide a node's row
|
||||
*/
|
||||
const _hideNode = (node, shouldHide) => {
|
||||
const span = $(`filesTablefileName${node.rowId}`);
|
||||
// span won't exist if row has been filtered out
|
||||
if (span === null)
|
||||
return;
|
||||
const rowElem = span.parentElement.parentElement;
|
||||
rowElem.classList.toggle("invisible", shouldHide);
|
||||
};
|
||||
|
||||
/**
|
||||
* Update a node's collapsed state and icon
|
||||
*/
|
||||
const _updateNodeState = (node, isCollapsed) => {
|
||||
const span = $(`filesTablefileName${node.rowId}`);
|
||||
// span won't exist if row has been filtered out
|
||||
if (span === null)
|
||||
return;
|
||||
const td = span.parentElement;
|
||||
|
||||
// store collapsed state
|
||||
td.setAttribute("data-collapsed", isCollapsed);
|
||||
|
||||
// rotate the collapse icon
|
||||
const collapseIcon = td.getElementsByClassName("filesTableCollapseIcon")[0];
|
||||
collapseIcon.classList.toggle("rotate", isCollapsed);
|
||||
};
|
||||
|
||||
const _isCollapsed = (node) => {
|
||||
const span = $(`filesTablefileName${node.rowId}`);
|
||||
if (span === null)
|
||||
return true;
|
||||
|
||||
const td = span.parentElement;
|
||||
return td.getAttribute("data-collapsed") === "true";
|
||||
};
|
||||
|
||||
const expandNode = (node) => {
|
||||
_collapseNode(node, false, false, false);
|
||||
};
|
||||
|
||||
const collapseNode = (node) => {
|
||||
_collapseNode(node, true, false, false);
|
||||
};
|
||||
|
||||
const expandAllNodes = () => {
|
||||
const root = torrentFilesTable.getRoot();
|
||||
root.children.each((node) => {
|
||||
node.children.each((child) => {
|
||||
_collapseNode(child, false, true, false);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const collapseAllNodes = () => {
|
||||
const root = torrentFilesTable.getRoot();
|
||||
root.children.each((node) => {
|
||||
node.children.each((child) => {
|
||||
_collapseNode(child, true, true, false);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Collapses a folder node with the option to recursively collapse all children
|
||||
* @param {FolderNode} node the node to collapse/expand
|
||||
* @param {boolean} shouldCollapse true if the node should be collapsed, false if it should be expanded
|
||||
* @param {boolean} applyToChildren true if the node's children should also be collapsed, recursively
|
||||
* @param {boolean} isChildNode true if the current node is a child of the original node we collapsed/expanded
|
||||
*/
|
||||
const _collapseNode = (node, shouldCollapse, applyToChildren, isChildNode) => {
|
||||
if (!node.isFolder)
|
||||
return;
|
||||
|
||||
const shouldExpand = !shouldCollapse;
|
||||
const isNodeCollapsed = _isCollapsed(node);
|
||||
const nodeInCorrectState = ((shouldCollapse && isNodeCollapsed) || (shouldExpand && !isNodeCollapsed));
|
||||
const canSkipNode = (isChildNode && (!applyToChildren || nodeInCorrectState));
|
||||
if (!isChildNode || applyToChildren || !canSkipNode)
|
||||
_updateNodeState(node, shouldCollapse);
|
||||
|
||||
node.children.each((child) => {
|
||||
_hideNode(child, shouldCollapse);
|
||||
|
||||
if (!child.isFolder)
|
||||
return;
|
||||
|
||||
// don't expand children that have been independently collapsed, unless applyToChildren is true
|
||||
const shouldExpandChildren = (shouldExpand && applyToChildren);
|
||||
const isChildCollapsed = _isCollapsed(child);
|
||||
if (!shouldExpandChildren && isChildCollapsed)
|
||||
return;
|
||||
|
||||
_collapseNode(child, shouldCollapse, applyToChildren, true);
|
||||
});
|
||||
};
|
||||
|
||||
const clear = () => {
|
||||
torrentFilesTable.clear();
|
||||
};
|
||||
|
|
|
@ -187,7 +187,7 @@ window.qBittorrent.PropPeers ??= (() => {
|
|||
}
|
||||
});
|
||||
|
||||
torrentPeersTable.setup("torrentPeersTableDiv", "torrentPeersTableFixedHeaderDiv", torrentPeersContextMenu);
|
||||
torrentPeersTable.setup("torrentPeersTableDiv", "torrentPeersTableFixedHeaderDiv", torrentPeersContextMenu, true);
|
||||
|
||||
return exports();
|
||||
})();
|
||||
|
|
|
@ -248,7 +248,7 @@ window.qBittorrent.PropTrackers ??= (() => {
|
|||
}
|
||||
});
|
||||
|
||||
torrentTrackersTable.setup("torrentTrackersTableDiv", "torrentTrackersTableFixedHeaderDiv", torrentTrackersContextMenu);
|
||||
torrentTrackersTable.setup("torrentTrackersTableDiv", "torrentTrackersTableFixedHeaderDiv", torrentTrackersContextMenu, true);
|
||||
|
||||
return exports();
|
||||
})();
|
||||
|
|
|
@ -223,7 +223,7 @@ window.qBittorrent.PropWebseeds ??= (() => {
|
|||
}
|
||||
});
|
||||
|
||||
torrentWebseedsTable.setup("torrentWebseedsTableDiv", "torrentWebseedsTableFixedHeaderDiv", torrentWebseedsContextMenu);
|
||||
torrentWebseedsTable.setup("torrentWebseedsTableDiv", "torrentWebseedsTableFixedHeaderDiv", torrentWebseedsContextMenu, true);
|
||||
|
||||
return exports();
|
||||
})();
|
||||
|
|
|
@ -223,8 +223,8 @@
|
|||
}
|
||||
});
|
||||
|
||||
tableInfo["main"].instance.setup("logMessageTableDiv", "logMessageTableFixedHeaderDiv", logTableContextMenu);
|
||||
tableInfo["peer"].instance.setup("logPeerTableDiv", "logPeerTableFixedHeaderDiv", logTableContextMenu);
|
||||
tableInfo["main"].instance.setup("logMessageTableDiv", "logMessageTableFixedHeaderDiv", logTableContextMenu, true);
|
||||
tableInfo["peer"].instance.setup("logPeerTableDiv", "logPeerTableFixedHeaderDiv", logTableContextMenu, true);
|
||||
|
||||
MUI.Panels.instances.LogPanel.contentEl.style.height = "100%";
|
||||
|
||||
|
@ -331,7 +331,7 @@
|
|||
if (curTab === undefined)
|
||||
curTab = currentSelectedTab;
|
||||
|
||||
$("numFilteredLogs").textContent = tableInfo[curTab].instance.filteredLength();
|
||||
$("numFilteredLogs").textContent = tableInfo[curTab].instance.filteredLength;
|
||||
$("numTotalLogs").textContent = tableInfo[curTab].instance.getRowSize();
|
||||
};
|
||||
|
||||
|
|
|
@ -106,6 +106,10 @@
|
|||
<input type="checkbox" id="displayFullURLTrackerColumn">
|
||||
<label for="displayFullURLTrackerColumn">QBT_TR(Display full announce URL in the Tracker column)QBT_TR[CONTEXT=OptionsDialog]</label>
|
||||
</div>
|
||||
<div class="formRow" style="margin-bottom: 3px;">
|
||||
<input type="checkbox" id="useVirtualList">
|
||||
<label for="useVirtualList">QBT_TR(Enable optimized table rendering (experimental))QBT_TR[CONTEXT=OptionsDialog]</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
|
@ -2221,6 +2225,7 @@ Use ';' to split multiple entries. Can use wildcard '*'.)QBT_TR[CONTEXT=OptionsD
|
|||
$("statusBarExternalIP").checked = pref.status_bar_external_ip;
|
||||
$("performanceWarning").checked = pref.performance_warning;
|
||||
document.getElementById("displayFullURLTrackerColumn").checked = (LocalPreferences.get("full_url_tracker_column", "false") === "true");
|
||||
document.getElementById("useVirtualList").checked = (LocalPreferences.get("use_virtual_list", "false") === "true");
|
||||
document.getElementById("hideZeroFiltersCheckbox").checked = (LocalPreferences.get("hide_zero_status_filters", "false") === "true");
|
||||
$("dblclickDownloadSelect").value = LocalPreferences.get("dblclick_download", "1");
|
||||
$("dblclickCompleteSelect").value = LocalPreferences.get("dblclick_complete", "1");
|
||||
|
@ -2653,6 +2658,7 @@ Use ';' to split multiple entries. Can use wildcard '*'.)QBT_TR[CONTEXT=OptionsD
|
|||
settings["status_bar_external_ip"] = $("statusBarExternalIP").checked;
|
||||
settings["performance_warning"] = $("performanceWarning").checked;
|
||||
LocalPreferences.set("full_url_tracker_column", document.getElementById("displayFullURLTrackerColumn").checked.toString());
|
||||
LocalPreferences.set("use_virtual_list", document.getElementById("useVirtualList").checked.toString());
|
||||
LocalPreferences.set("hide_zero_status_filters", document.getElementById("hideZeroFiltersCheckbox").checked.toString());
|
||||
LocalPreferences.set("dblclick_download", $("dblclickDownloadSelect").value);
|
||||
LocalPreferences.set("dblclick_complete", $("dblclickCompleteSelect").value);
|
||||
|
|
|
@ -111,7 +111,7 @@
|
|||
});
|
||||
|
||||
const setup = () => {
|
||||
torrentsTable.setup("torrentsTableDiv", "torrentsTableFixedHeaderDiv", contextMenu);
|
||||
torrentsTable.setup("torrentsTableDiv", "torrentsTableFixedHeaderDiv", contextMenu, true);
|
||||
};
|
||||
|
||||
return exports();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue