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:
tehcneko 2025-04-20 17:18:26 +08:00 committed by GitHub
commit b4a16f6464
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 474 additions and 359 deletions

View file

@ -41,7 +41,7 @@
// Setup the dynamic table for bulk renaming // Setup the dynamic table for bulk renaming
const bulkRenameFilesTable = new window.qBittorrent.DynamicTable.BulkRenameTorrentFilesTable(); 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 // Inject checkbox into the first column of the table header
const tableHeaders = document.querySelectorAll("#bulkRenameFilesTableFixedHeaderDiv .dynamicTableHeader th"); const tableHeaders = document.querySelectorAll("#bulkRenameFilesTableFixedHeaderDiv .dynamicTableHeader th");
@ -172,7 +172,10 @@
// Update renamed column for matched rows // Update renamed column for matched rows
for (let i = 0; i < matchedRows.length; ++i) { for (let i = 0; i < matchedRows.length; ++i) {
const row = matchedRows[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) => { fileRenamer.onInvalidRegex = (err) => {
@ -287,11 +290,6 @@
event.preventDefault(); event.preventDefault();
window.qBittorrent.Client.closeWindow(windowEl); 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 handleTorrentFiles = (files, selectedRows) => {
const rows = files.map((file, index) => { const rows = files.map((file, index) => {

View file

@ -71,14 +71,18 @@ window.qBittorrent.DynamicTable ??= (() => {
initialize: () => {}, initialize: () => {},
setup: function(dynamicTableDivId, dynamicTableFixedHeaderDivId, contextMenu) { setup: function(dynamicTableDivId, dynamicTableFixedHeaderDivId, contextMenu, useVirtualList = false) {
this.dynamicTableDivId = dynamicTableDivId; this.dynamicTableDivId = dynamicTableDivId;
this.dynamicTableFixedHeaderDivId = dynamicTableFixedHeaderDivId; this.dynamicTableFixedHeaderDivId = dynamicTableFixedHeaderDivId;
this.dynamicTableDiv = document.getElementById(dynamicTableDivId); this.dynamicTableDiv = document.getElementById(dynamicTableDivId);
this.useVirtualList = useVirtualList && (LocalPreferences.get("use_virtual_list", "false") === "true");
this.fixedTableHeader = document.querySelector(`#${dynamicTableFixedHeaderDivId} thead tr`); this.fixedTableHeader = document.querySelector(`#${dynamicTableFixedHeaderDivId} thead tr`);
this.hiddenTableHeader = this.dynamicTableDiv.querySelector(`thead tr`); this.hiddenTableHeader = this.dynamicTableDiv.querySelector(`thead tr`);
this.table = this.dynamicTableDiv.querySelector(`table`);
this.tableBody = this.dynamicTableDiv.querySelector(`tbody`); this.tableBody = this.dynamicTableDiv.querySelector(`tbody`);
this.rowHeight = 26;
this.rows = new Map(); this.rows = new Map();
this.cachedElements = [];
this.selectedRows = []; this.selectedRows = [];
this.columns = []; this.columns = [];
this.contextMenu = contextMenu; this.contextMenu = contextMenu;
@ -91,14 +95,34 @@ window.qBittorrent.DynamicTable ??= (() => {
this.setupHeaderEvents(); this.setupHeaderEvents();
this.setupHeaderMenu(); this.setupHeaderMenu();
this.setupAltRow(); 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() { setupCommonEvents: function() {
const tableFixedHeaderDiv = $(this.dynamicTableFixedHeaderDivId); const tableFixedHeaderDiv = document.getElementById(this.dynamicTableFixedHeaderDivId);
const tableElement = tableFixedHeaderDiv.querySelector("table"); const tableElement = tableFixedHeaderDiv.querySelector("table");
this.dynamicTableDiv.addEventListener("scroll", function() { this.dynamicTableDiv.addEventListener("scroll", (e) => {
tableElement.style.left = `${-this.scrollLeft}px`; tableElement.style.left = `${-this.dynamicTableDiv.scrollLeft}px`;
// rerender on scroll
if (this.useVirtualList)
this.rerender();
}); });
this.dynamicTableDiv.addEventListener("click", (e) => { this.dynamicTableDiv.addEventListener("click", (e) => {
@ -404,6 +428,9 @@ window.qBittorrent.DynamicTable ??= (() => {
const style = `width: ${column.width}px; ${column.style}`; const style = `width: ${column.width}px; ${column.style}`;
this.getRowCells(this.hiddenTableHeader)[pos].style.cssText = style; this.getRowCells(this.hiddenTableHeader)[pos].style.cssText = style;
this.getRowCells(this.fixedTableHeader)[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); column.onResize?.(column.name);
}, },
@ -707,10 +734,9 @@ window.qBittorrent.DynamicTable ??= (() => {
selectAll: function() { selectAll: function() {
this.deselectAll(); this.deselectAll();
for (const tr of this.getTrs()) { for (const row of this.getFilteredAndSortedRows())
this.selectedRows.push(tr.rowId); this.selectedRows.push(row.rowId);
tr.classList.add("selected"); this.setRowClass();
}
}, },
deselectAll: function() { 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) { for (let rowPos = 0; rowPos < rows.length; ++rowPos) {
const rowId = rows[rowPos]["rowId"]; const rowId = rows[rowPos].rowId;
let tr_found = false; let tr_found = false;
for (let j = rowPos; j < trs.length; ++j) { for (let j = rowPos; j < trs.length; ++j) {
if (trs[j]["rowId"] === rowId) { if (trs[j].rowId === rowId) {
tr_found = true; tr_found = true;
if (rowPos === j) if (rowPos === j)
break;
trs[j].inject(trs[rowPos], "before");
const tmpTr = trs[j];
trs.splice(j, 1);
trs.splice(rowPos, 0, tmpTr);
break; break;
trs[j].inject(trs[rowPos], "before"); }
const tmpTr = trs[j]; }
trs.splice(j, 1); if (tr_found) { // row already exists in the table
trs.splice(rowPos, 0, tmpTr); this.updateRow(trs[rowPos], fullUpdate);
break; }
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) { createRowElement: function(row, top = -1) {
const td = document.createElement("td"); const tr = document.createElement("tr");
if ((this.columns[k].visible === "0") || this.columns[k].force_hide) // set tabindex so element receives keydown events
td.classList.add("invisible"); // more info: https://developer.mozilla.org/en-US/docs/Web/API/Element/keydown_event
tr.append(td); tr.tabIndex = -1;
}
// Insert for (let k = 0; k < this.columns.length; ++k) {
if (rowPos >= trs.length) { const td = document.createElement("td");
tr.inject(this.tableBody); if ((this.columns[k].visible === "0") || this.columns[k].force_hide)
trs.push(tr); td.classList.add("invisible");
} tr.append(td);
else {
tr.inject(trs[rowPos], "before");
trs.splice(rowPos, 0, tr);
}
// Update context menu
this.contextMenu?.addTarget(tr);
this.updateRow(tr, true);
}
} }
const rowPos = rows.length; this.updateRowElement(tr, row.rowId, top);
while ((rowPos < trs.length) && (trs.length > 0)) // update context menu
trs.pop().destroy(); 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) { updateRow: function(tr, fullUpdate) {
@ -898,6 +998,11 @@ window.qBittorrent.DynamicTable ??= (() => {
const tds = this.getRowCells(tr); const tds = this.getRowCells(tr);
for (let i = 0; i < this.columns.length; ++i) { 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))) if (this.columns[i].dataProperties.some(prop => Object.hasOwn(data, prop)))
this.columns[i].updateTd(tds[i], row); this.columns[i].updateTd(tds[i], row);
} }
@ -907,15 +1012,25 @@ window.qBittorrent.DynamicTable ??= (() => {
removeRow: function(rowId) { removeRow: function(rowId) {
this.selectedRows.erase(rowId); this.selectedRows.erase(rowId);
this.rows.delete(rowId); this.rows.delete(rowId);
const tr = this.getTrByRowId(rowId); if (this.useVirtualList) {
tr?.destroy(); this.rerender();
}
else {
const tr = this.getTrByRowId(rowId);
tr?.destroy();
}
}, },
clear: function() { clear: function() {
this.deselectAll(); this.deselectAll();
this.rows.clear(); this.rows.clear();
for (const tr of this.getTrs()) if (this.useVirtualList) {
tr.destroy(); this.rerender();
}
else {
for (const tr of this.getTrs())
tr.destroy();
}
}, },
selectedRowsIds: function() { selectedRowsIds: function() {
@ -986,6 +1101,12 @@ window.qBittorrent.DynamicTable ??= (() => {
const TorrentsTable = new Class({ const TorrentsTable = new Class({
Extends: DynamicTable, Extends: DynamicTable,
setupVirtualList: function() {
this.parent();
this.rowHeight = 22;
},
initColumns: function() { initColumns: function() {
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);
@ -2075,6 +2196,12 @@ window.qBittorrent.DynamicTable ??= (() => {
prevReverseSort: null, prevReverseSort: null,
fileTree: new window.qBittorrent.FileTree.FileTree(), fileTree: new window.qBittorrent.FileTree.FileTree(),
setupVirtualList: function() {
this.parent();
this.rowHeight = 29;
},
populateTable: function(root) { populateTable: function(root) {
this.fileTree.setRoot(root); this.fileTree.setRoot(root);
root.children.each((node) => { root.children.each((node) => {
@ -2145,30 +2272,30 @@ window.qBittorrent.DynamicTable ??= (() => {
* Toggles the global checkbox and all checkboxes underneath * Toggles the global checkbox and all checkboxes underneath
*/ */
toggleGlobalCheckbox: function() { toggleGlobalCheckbox: function() {
const checkbox = $("rootMultiRename_cb"); const checkbox = document.getElementById("rootMultiRename_cb");
const checkboxes = document.querySelectorAll("input.RenamingCB"); const checkboxes = document.querySelectorAll("input.RenamingCB");
for (let i = 0; i < checkboxes.length; ++i) { for (let i = 0; i < checkboxes.length; ++i) {
const node = this.getNode(i);
if (checkbox.checked || checkbox.indeterminate) { if (checkbox.checked || checkbox.indeterminate) {
const cb = checkboxes[i]; const cb = checkboxes[i];
cb.checked = true; cb.checked = true;
cb.indeterminate = false; cb.indeterminate = false;
cb.state = "checked"; cb.state = "checked";
node.checked = 0;
node.full_data.checked = node.checked;
} }
else { else {
const cb = checkboxes[i]; const cb = checkboxes[i];
cb.checked = false; cb.checked = false;
cb.indeterminate = false; cb.indeterminate = false;
cb.state = "unchecked"; 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(); this.updateGlobalCheckbox();
}, },
@ -2176,7 +2303,7 @@ window.qBittorrent.DynamicTable ??= (() => {
const node = this.getNode(rowId); const node = this.getNode(rowId);
node.checked = checkState; node.checked = checkState;
node.full_data.checked = checkState; node.full_data.checked = checkState;
const checkbox = $(`cbRename${rowId}`); const checkbox = document.getElementById(`cbRename${rowId}`);
checkbox.checked = node.checked === 0; checkbox.checked = node.checked === 0;
checkbox.state = checkbox.checked ? "checked" : "unchecked"; checkbox.state = checkbox.checked ? "checked" : "unchecked";
@ -2184,11 +2311,11 @@ window.qBittorrent.DynamicTable ??= (() => {
this.toggleNodeTreeCheckbox(node.children[i].rowId, checkState); this.toggleNodeTreeCheckbox(node.children[i].rowId, checkState);
}, },
updateGlobalCheckbox: () => { updateGlobalCheckbox: function() {
const checkbox = $("rootMultiRename_cb"); const checkbox = document.getElementById("rootMultiRename_cb");
const checkboxes = document.querySelectorAll("input.RenamingCB"); const nodes = this.fileTree.toArray();
const isAllChecked = Array.prototype.every.call(checkboxes, (checkbox => checkbox.checked)); const isAllChecked = nodes.every((node) => node.checked === 0);
const isAllUnchecked = (() => Array.prototype.every.call(checkboxes, (checkbox => !checkbox.checked))); const isAllUnchecked = (() => nodes.every((node) => node.checked !== 0));
if (isAllChecked) { if (isAllChecked) {
checkbox.state = "checked"; checkbox.state = "checked";
checkbox.indeterminate = false; checkbox.indeterminate = false;
@ -2210,82 +2337,86 @@ window.qBittorrent.DynamicTable ??= (() => {
const that = this; const that = this;
// checked // checked
this.columns["checked"].updateTd = function(td, row) { this.columns["checked"].updateTd = (td, row) => {
const id = row.rowId; const id = row.rowId;
const value = this.getRowValue(row); const node = that.getNode(id);
const treeImg = document.createElement("img"); if (td.firstElementChild === null) {
treeImg.src = "images/L.gif"; const treeImg = document.createElement("img");
treeImg.style.marginBottom = "-2px"; treeImg.src = "images/L.gif";
treeImg.style.marginBottom = "-2px";
td.append(treeImg);
}
const checkbox = document.createElement("input"); let checkbox = td.children[1];
checkbox.type = "checkbox"; 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.id = `cbRename${id}`;
checkbox.setAttribute("data-id", id); checkbox.dataset.id = id;
checkbox.className = "RenamingCB"; checkbox.checked = (node.checked === 0);
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.state = checkbox.checked ? "checked" : "unchecked"; checkbox.state = checkbox.checked ? "checked" : "unchecked";
checkbox.indeterminate = false;
td.replaceChildren(treeImg, checkbox);
}; };
this.columns["checked"].staticWidth = 50; this.columns["checked"].staticWidth = 50;
// original // original
this.columns["original"].updateTd = function(td, row) { this.columns["original"].updateTd = function(td, row) {
const id = row.rowId; const id = row.rowId;
const fileNameId = `filesTablefileName${id}`;
const node = that.getNode(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) { if (node.isFolder) {
const value = this.getRowValue(row); dirImg.style.display = "inline";
const dirImgId = `renameTableDirImg${id}`; dirImg.style.marginLeft = `${node.depth * 20}px`;
if ($(dirImgId)) { }
// just update file name else {
$(fileNameId).textContent = value; dirImg.style.display = "none";
} }
else {
const span = document.createElement("span");
span.textContent = value;
span.id = fileNameId;
const dirImg = document.createElement("img"); let span = td.children[1];
dirImg.src = "images/directory.svg"; if (span === undefined) {
dirImg.style.width = "20px"; span = document.createElement("span");
dirImg.style.paddingRight = "5px"; td.append(span);
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);
} }
span.textContent = value;
span.style.marginLeft = node.isFolder ? "0" : `${(node.depth + 1) * 20}px`;
}; };
// renamed // renamed
this.columns["renamed"].updateTd = function(td, row) { this.columns["renamed"].updateTd = (td, row) => {
const id = row.rowId; const id = row.rowId;
const fileNameRenamedId = `filesTablefileRenamed${id}`; const fileNameRenamedId = `filesTablefileRenamed${id}`;
const value = this.getRowValue(row); const node = that.getNode(id);
const span = document.createElement("span"); let span = td.firstElementChild;
span.textContent = value; if (span === null) {
span = document.createElement("span");
td.append(span);
}
span.id = fileNameRenamedId; 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))); 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({ const TorrentFilesTable = new Class({
@ -2445,6 +2584,108 @@ window.qBittorrent.DynamicTable ??= (() => {
prevReverseSort: null, prevReverseSort: null,
fileTree: new window.qBittorrent.FileTree.FileTree(), 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) { populateTable: function(root) {
this.fileTree.setRoot(root); this.fileTree.setRoot(root);
root.children.each((node) => { root.children.each((node) => {
@ -2456,6 +2697,8 @@ window.qBittorrent.DynamicTable ??= (() => {
node.depth = depth; node.depth = depth;
if (node.isFolder) { if (node.isFolder) {
if (!this.collapseState.has(node.rowId))
this.collapseState.set(node.rowId, { depth: depth, collapsed: depth > 0 });
const data = { const data = {
rowId: node.rowId, rowId: node.rowId,
size: node.size, size: node.size,
@ -2470,7 +2713,7 @@ window.qBittorrent.DynamicTable ??= (() => {
node.data = data; node.data = data;
node.full_data = data; node.full_data = data;
this.updateRowData(data); this.updateRowData(data, depth);
} }
else { else {
node.data.rowId = node.rowId; node.data.rowId = node.rowId;
@ -2496,6 +2739,11 @@ window.qBittorrent.DynamicTable ??= (() => {
return this.rows.get(rowId); return this.rows.get(rowId);
}, },
getRowFileId: function(rowId) {
const row = this.rows.get(rowId);
return row?.full_data.fileId;
},
initColumns: function() { initColumns: function() {
this.newColumn("checked", "", "", 50, true); this.newColumn("checked", "", "", 50, true);
this.newColumn("name", "", "QBT_TR(Name)QBT_TR[CONTEXT=TrackerListWidget]", 300, 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 id = row.rowId;
const value = this.getRowValue(row); const value = this.getRowValue(row);
if (window.qBittorrent.PropFiles.isDownloadCheckboxExists(id)) { if (td.firstElementChild === null) {
window.qBittorrent.PropFiles.updateDownloadCheckbox(id, value);
}
else {
const treeImg = document.createElement("img"); const treeImg = document.createElement("img");
treeImg.src = "images/L.gif"; treeImg.src = "images/L.gif";
treeImg.style.marginBottom = "-2px"; 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; this.columns["checked"].staticWidth = 50;
@ -2543,46 +2795,56 @@ window.qBittorrent.DynamicTable ??= (() => {
const id = row.rowId; const id = row.rowId;
const fileNameId = `filesTablefileName${id}`; const fileNameId = `filesTablefileName${id}`;
const node = that.getNode(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) { if (node.isFolder) {
const value = this.getRowValue(row); collapseIcon.style.marginLeft = `${node.depth * 20}px`;
const collapseIconId = `filesTableCollapseIcon${id}`; collapseIcon.style.display = "inline";
const dirImgId = `filesTableDirImg${id}`; collapseIcon.dataset.id = id;
if ($(dirImgId)) { collapseIcon.classList.toggle("rotate", that.isCollapsed(node.rowId));
// 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);
}
} }
else { else {
const value = this.getRowValue(row); collapseIcon.style.display = "none";
const span = document.createElement("span");
span.textContent = value;
span.id = fileNameId;
span.style.marginLeft = `${(node.depth + 1) * 20}px`;
td.replaceChildren(span);
} }
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) => { this.columns["name"].calculateBuffer = (rowId) => {
const node = that.getNode(rowId); const node = that.getNode(rowId);
@ -2599,10 +2861,9 @@ window.qBittorrent.DynamicTable ??= (() => {
const id = row.rowId; const id = row.rowId;
const value = Number(this.getRowValue(row)); const value = Number(this.getRowValue(row));
const progressBar = $(`pbf_${id}`); const progressBar = td.firstElementChild;
if (progressBar === null) { if (progressBar === null) {
td.append(new window.qBittorrent.ProgressBar.ProgressBar(value, { td.append(new window.qBittorrent.ProgressBar.ProgressBar(value, {
id: `pbf_${id}`,
width: 80 width: 80
})); }));
} }
@ -2617,10 +2878,11 @@ window.qBittorrent.DynamicTable ??= (() => {
const id = row.rowId; const id = row.rowId;
const value = this.getRowValue(row); const value = this.getRowValue(row);
if (window.qBittorrent.PropFiles.isPriorityComboExists(id)) const priorityCombo = td.firstElementChild;
window.qBittorrent.PropFiles.updatePriorityCombo(id, value); if (priorityCombo === null)
else
td.append(window.qBittorrent.PropFiles.createPriorityCombo(id, row.full_data.fileId, value)); 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; this.columns["priority"].staticWidth = 140;
@ -2652,7 +2914,7 @@ window.qBittorrent.DynamicTable ??= (() => {
}, },
_filterNodes: function(node, filterTerms, filteredRows) { _filterNodes: function(node, filterTerms, filteredRows) {
if (node.isFolder) { if (node.isFolder && (!this.useVirtualList || !this.isCollapsed(node.rowId))) {
const childAdded = node.children.reduce((acc, child) => { 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 // 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); return (this._filterNodes(child, filterTerms, filteredRows) || acc);
@ -2689,19 +2951,11 @@ window.qBittorrent.DynamicTable ??= (() => {
const generateRowsSignature = () => { const generateRowsSignature = () => {
const rowsData = []; const rowsData = [];
for (const { full_data } of this.getRowValues()) 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); return JSON.stringify(rowsData);
}; };
const getFilteredRows = function() { 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 = []; const filteredRows = [];
this.getRoot().children.each((child) => { this.getRoot().children.each((child) => {
this._filterNodes(child, this.filterTerms, filteredRows); this._filterNodes(child, this.filterTerms, filteredRows);
@ -2758,11 +3012,11 @@ window.qBittorrent.DynamicTable ??= (() => {
switch (e.key) { switch (e.key) {
case "ArrowLeft": case "ArrowLeft":
e.preventDefault(); e.preventDefault();
window.qBittorrent.PropFiles.collapseFolder(this.getSelectedRowId()); this.collapseFolder(this.getSelectedRowId());
break; break;
case "ArrowRight": case "ArrowRight":
e.preventDefault(); e.preventDefault();
window.qBittorrent.PropFiles.expandFolder(this.getSelectedRowId()); this.expandFolder(this.getSelectedRowId());
break; break;
} }
}); });
@ -3247,10 +3501,6 @@ window.qBittorrent.DynamicTable ??= (() => {
filterText: "", filterText: "",
filteredLength: function() {
return this.tableBody.rows.length;
},
initColumns: function() { initColumns: function() {
this.newColumn("rowId", "", "QBT_TR(ID)QBT_TR[CONTEXT=ExecutionLogWidget]", 50, true); this.newColumn("rowId", "", "QBT_TR(ID)QBT_TR[CONTEXT=ExecutionLogWidget]", 50, true);
this.newColumn("message", "", "QBT_TR(Message)QBT_TR[CONTEXT=ExecutionLogWidget]", 350, true); this.newColumn("message", "", "QBT_TR(Message)QBT_TR[CONTEXT=ExecutionLogWidget]", 350, true);
@ -3293,7 +3543,7 @@ window.qBittorrent.DynamicTable ??= (() => {
} }
td.textContent = logLevel; td.textContent = logLevel;
td.title = 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; return (this.reverseSort === "0") ? res : -res;
}); });
this.filteredLength = filteredRows.length;
return filteredRows; return filteredRows;
}, },
}); });
@ -3355,7 +3607,7 @@ window.qBittorrent.DynamicTable ??= (() => {
} }
td.textContent = status; td.textContent = status;
td.title = status; td.title = status;
td.closest("tr").className = `logTableRow${addClass}`; td.closest("tr").classList.add(`logTableRow${addClass}`);
}; };
}, },

View file

@ -33,16 +33,11 @@ window.qBittorrent.PropFiles ??= (() => {
const exports = () => { const exports = () => {
return { return {
normalizePriority: normalizePriority, normalizePriority: normalizePriority,
isDownloadCheckboxExists: isDownloadCheckboxExists,
createDownloadCheckbox: createDownloadCheckbox, createDownloadCheckbox: createDownloadCheckbox,
updateDownloadCheckbox: updateDownloadCheckbox, updateDownloadCheckbox: updateDownloadCheckbox,
isPriorityComboExists: isPriorityComboExists,
createPriorityCombo: createPriorityCombo, createPriorityCombo: createPriorityCombo,
updatePriorityCombo: updatePriorityCombo, updatePriorityCombo: updatePriorityCombo,
updateData: updateData, updateData: updateData,
collapseIconClicked: collapseIconClicked,
expandFolder: expandFolder,
collapseFolder: collapseFolder,
clear: clear clear: clear
}; };
}; };
@ -126,25 +121,20 @@ window.qBittorrent.PropFiles ??= (() => {
updateGlobalCheckbox(); updateGlobalCheckbox();
}; };
const isDownloadCheckboxExists = (id) => {
return $(`cbPrio${id}`) !== null;
};
const createDownloadCheckbox = (id, fileId, checked) => { const createDownloadCheckbox = (id, fileId, checked) => {
const checkbox = document.createElement("input"); const checkbox = document.createElement("input");
checkbox.type = "checkbox"; checkbox.type = "checkbox";
checkbox.id = `cbPrio${id}`;
checkbox.setAttribute("data-id", id); checkbox.setAttribute("data-id", id);
checkbox.setAttribute("data-file-id", fileId); checkbox.setAttribute("data-file-id", fileId);
checkbox.className = "DownloadedCB";
checkbox.addEventListener("click", fileCheckboxClicked); checkbox.addEventListener("click", fileCheckboxClicked);
updateCheckbox(checkbox, checked); updateCheckbox(checkbox, checked);
return checkbox; return checkbox;
}; };
const updateDownloadCheckbox = (id, checked) => { const updateDownloadCheckbox = (checkbox, id, fileId, checked) => {
const checkbox = $(`cbPrio${id}`); checkbox.setAttribute("data-id", id);
checkbox.setAttribute("data-file-id", fileId);
updateCheckbox(checkbox, checked); updateCheckbox(checkbox, checked);
}; };
@ -162,10 +152,6 @@ window.qBittorrent.PropFiles ??= (() => {
} }
}; };
const isPriorityComboExists = (id) => {
return $(`comboPrio${id}`) !== null;
};
const createPriorityCombo = (id, fileId, selectedPriority) => { const createPriorityCombo = (id, fileId, selectedPriority) => {
const createOption = (priority, isSelected, text) => { const createOption = (priority, isSelected, text) => {
const option = document.createElement("option"); const option = document.createElement("option");
@ -195,8 +181,10 @@ window.qBittorrent.PropFiles ??= (() => {
return select; return select;
}; };
const updatePriorityCombo = (id, selectedPriority) => { const updatePriorityCombo = (combobox, id, fileId, selectedPriority) => {
const combobox = $(`comboPrio${id}`); combobox.id = `comboPrio${id}`;
combobox.setAttribute("data-id", id);
combobox.setAttribute("data-file-id", fileId);
if (Number(combobox.value) !== selectedPriority) if (Number(combobox.value) !== selectedPriority)
selectComboboxPriority(combobox, selectedPriority); selectComboboxPriority(combobox, selectedPriority);
}; };
@ -258,9 +246,9 @@ window.qBittorrent.PropFiles ??= (() => {
const updateGlobalCheckbox = () => { const updateGlobalCheckbox = () => {
const checkbox = $("tristate_cb"); const checkbox = $("tristate_cb");
if (isAllCheckboxesChecked()) if (torrentFilesTable.isAllCheckboxesChecked())
setCheckboxChecked(checkbox); setCheckboxChecked(checkbox);
else if (isAllCheckboxesUnchecked()) else if (torrentFilesTable.isAllCheckboxesUnchecked())
setCheckboxUnchecked(checkbox); setCheckboxUnchecked(checkbox);
else else
setCheckboxPartial(checkbox); setCheckboxPartial(checkbox);
@ -283,10 +271,6 @@ window.qBittorrent.PropFiles ??= (() => {
checkbox.indeterminate = true; 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) => { const setFilePriority = (ids, fileIds, priority) => {
if (current_hash === "") if (current_hash === "")
return; return;
@ -317,8 +301,6 @@ window.qBittorrent.PropFiles ??= (() => {
if (combobox !== null) if (combobox !== null)
selectComboboxPriority(combobox, priority); selectComboboxPriority(combobox, priority);
}); });
torrentFilesTable.updateTable(false);
}; };
let loadTorrentFilesDataTimer = -1; let loadTorrentFilesDataTimer = -1;
@ -364,7 +346,7 @@ window.qBittorrent.PropFiles ??= (() => {
else { else {
handleNewTorrentFiles(files); handleNewTorrentFiles(files);
if (loadedNewTorrent) if (loadedNewTorrent)
collapseAllNodes(); torrentFilesTable.collapseAllNodes();
} }
}) })
.finally(() => { .finally(() => {
@ -470,29 +452,6 @@ window.qBittorrent.PropFiles ??= (() => {
torrentFilesTable.reselectRows(selectedFiles); 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 filesPriorityMenuClicked = (priority) => {
const selectedRows = torrentFilesTable.selectedRowsIds(); const selectedRows = torrentFilesTable.selectedRowsIds();
if (selectedRows.length === 0) if (selectedRows.length === 0)
@ -501,9 +460,8 @@ window.qBittorrent.PropFiles ??= (() => {
const rowIds = []; const rowIds = [];
const fileIds = []; const fileIds = [];
selectedRows.forEach((rowId) => { selectedRows.forEach((rowId) => {
const elem = $(`comboPrio${rowId}`);
rowIds.push(rowId); rowIds.push(rowId);
fileIds.push(elem.getAttribute("data-file-id")); fileIds.push(torrentFilesTable.getRowFileId(rowId));
}); });
const uniqueRowIds = {}; 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 // inject checkbox into table header
const tableHeaders = document.querySelectorAll("#torrentFilesTableFixedHeaderDiv .dynamicTableHeader th"); const tableHeaders = document.querySelectorAll("#torrentFilesTableFixedHeaderDiv .dynamicTableHeader th");
if (tableHeaders.length > 0) { if (tableHeaders.length > 0) {
@ -641,111 +599,12 @@ window.qBittorrent.PropFiles ??= (() => {
torrentFilesTable.updateTable(); torrentFilesTable.updateTable();
if (value.trim() === "") if (value.trim() === "")
collapseAllNodes(); torrentFilesTable.collapseAllNodes();
else else
expandAllNodes(); torrentFilesTable.expandAllNodes();
}, window.qBittorrent.Misc.FILTER_INPUT_DELAY); }, 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 = () => { const clear = () => {
torrentFilesTable.clear(); torrentFilesTable.clear();
}; };

View file

@ -187,7 +187,7 @@ window.qBittorrent.PropPeers ??= (() => {
} }
}); });
torrentPeersTable.setup("torrentPeersTableDiv", "torrentPeersTableFixedHeaderDiv", torrentPeersContextMenu); torrentPeersTable.setup("torrentPeersTableDiv", "torrentPeersTableFixedHeaderDiv", torrentPeersContextMenu, true);
return exports(); return exports();
})(); })();

View file

@ -248,7 +248,7 @@ window.qBittorrent.PropTrackers ??= (() => {
} }
}); });
torrentTrackersTable.setup("torrentTrackersTableDiv", "torrentTrackersTableFixedHeaderDiv", torrentTrackersContextMenu); torrentTrackersTable.setup("torrentTrackersTableDiv", "torrentTrackersTableFixedHeaderDiv", torrentTrackersContextMenu, true);
return exports(); return exports();
})(); })();

View file

@ -223,7 +223,7 @@ window.qBittorrent.PropWebseeds ??= (() => {
} }
}); });
torrentWebseedsTable.setup("torrentWebseedsTableDiv", "torrentWebseedsTableFixedHeaderDiv", torrentWebseedsContextMenu); torrentWebseedsTable.setup("torrentWebseedsTableDiv", "torrentWebseedsTableFixedHeaderDiv", torrentWebseedsContextMenu, true);
return exports(); return exports();
})(); })();

View file

@ -223,8 +223,8 @@
} }
}); });
tableInfo["main"].instance.setup("logMessageTableDiv", "logMessageTableFixedHeaderDiv", logTableContextMenu); tableInfo["main"].instance.setup("logMessageTableDiv", "logMessageTableFixedHeaderDiv", logTableContextMenu, true);
tableInfo["peer"].instance.setup("logPeerTableDiv", "logPeerTableFixedHeaderDiv", logTableContextMenu); tableInfo["peer"].instance.setup("logPeerTableDiv", "logPeerTableFixedHeaderDiv", logTableContextMenu, true);
MUI.Panels.instances.LogPanel.contentEl.style.height = "100%"; MUI.Panels.instances.LogPanel.contentEl.style.height = "100%";
@ -331,7 +331,7 @@
if (curTab === undefined) if (curTab === undefined)
curTab = currentSelectedTab; curTab = currentSelectedTab;
$("numFilteredLogs").textContent = tableInfo[curTab].instance.filteredLength(); $("numFilteredLogs").textContent = tableInfo[curTab].instance.filteredLength;
$("numTotalLogs").textContent = tableInfo[curTab].instance.getRowSize(); $("numTotalLogs").textContent = tableInfo[curTab].instance.getRowSize();
}; };

View file

@ -106,6 +106,10 @@
<input type="checkbox" id="displayFullURLTrackerColumn"> <input type="checkbox" id="displayFullURLTrackerColumn">
<label for="displayFullURLTrackerColumn">QBT_TR(Display full announce URL in the Tracker column)QBT_TR[CONTEXT=OptionsDialog]</label> <label for="displayFullURLTrackerColumn">QBT_TR(Display full announce URL in the Tracker column)QBT_TR[CONTEXT=OptionsDialog]</label>
</div> </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> </fieldset>
</div> </div>
@ -2221,6 +2225,7 @@ Use ';' to split multiple entries. Can use wildcard '*'.)QBT_TR[CONTEXT=OptionsD
$("statusBarExternalIP").checked = pref.status_bar_external_ip; $("statusBarExternalIP").checked = pref.status_bar_external_ip;
$("performanceWarning").checked = pref.performance_warning; $("performanceWarning").checked = pref.performance_warning;
document.getElementById("displayFullURLTrackerColumn").checked = (LocalPreferences.get("full_url_tracker_column", "false") === "true"); 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"); document.getElementById("hideZeroFiltersCheckbox").checked = (LocalPreferences.get("hide_zero_status_filters", "false") === "true");
$("dblclickDownloadSelect").value = LocalPreferences.get("dblclick_download", "1"); $("dblclickDownloadSelect").value = LocalPreferences.get("dblclick_download", "1");
$("dblclickCompleteSelect").value = LocalPreferences.get("dblclick_complete", "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["status_bar_external_ip"] = $("statusBarExternalIP").checked;
settings["performance_warning"] = $("performanceWarning").checked; settings["performance_warning"] = $("performanceWarning").checked;
LocalPreferences.set("full_url_tracker_column", document.getElementById("displayFullURLTrackerColumn").checked.toString()); 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("hide_zero_status_filters", document.getElementById("hideZeroFiltersCheckbox").checked.toString());
LocalPreferences.set("dblclick_download", $("dblclickDownloadSelect").value); LocalPreferences.set("dblclick_download", $("dblclickDownloadSelect").value);
LocalPreferences.set("dblclick_complete", $("dblclickCompleteSelect").value); LocalPreferences.set("dblclick_complete", $("dblclickCompleteSelect").value);

View file

@ -111,7 +111,7 @@
}); });
const setup = () => { const setup = () => {
torrentsTable.setup("torrentsTableDiv", "torrentsTableFixedHeaderDiv", contextMenu); torrentsTable.setup("torrentsTableDiv", "torrentsTableFixedHeaderDiv", contextMenu, true);
}; };
return exports(); return exports();