mirror of
https://github.com/qbittorrent/qBittorrent.git
synced 2025-12-19 06:57:24 -06: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:
@@ -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();
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user