mirror of
https://github.com/qbittorrent/qBittorrent.git
synced 2025-12-10 00:46:48 -06:00
3863 lines
156 KiB
JavaScript
3863 lines
156 KiB
JavaScript
/*
|
|
* MIT License
|
|
* Copyright (c) 2008 Ishan Arora <ishan@qbittorrent.org> & Christophe Dumez <chris@qbittorrent.org>
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
* of this software and associated documentation files (the "Software"), to deal
|
|
* in the Software without restriction, including without limitation the rights
|
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the Software is
|
|
* furnished to do so, subject to the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be included in
|
|
* all copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
* THE SOFTWARE.
|
|
*/
|
|
|
|
/**************************************************************
|
|
|
|
Script : Dynamic Table
|
|
Version : 0.5
|
|
Authors : Ishan Arora & Christophe Dumez
|
|
Desc : Programmable sortable table
|
|
Licence : Open Source MIT Licence
|
|
|
|
**************************************************************/
|
|
|
|
"use strict";
|
|
|
|
window.qBittorrent ??= {};
|
|
window.qBittorrent.DynamicTable ??= (() => {
|
|
const exports = () => {
|
|
return {
|
|
TorrentsTable: TorrentsTable,
|
|
TorrentPeersTable: TorrentPeersTable,
|
|
SearchResultsTable: SearchResultsTable,
|
|
SearchPluginsTable: SearchPluginsTable,
|
|
TorrentTrackersTable: TorrentTrackersTable,
|
|
TorrentFilesTable: TorrentFilesTable,
|
|
BulkRenameTorrentFilesTable: BulkRenameTorrentFilesTable,
|
|
AddTorrentFilesTable: AddTorrentFilesTable,
|
|
LogMessageTable: LogMessageTable,
|
|
LogPeerTable: LogPeerTable,
|
|
RssFeedTable: RssFeedTable,
|
|
RssArticleTable: RssArticleTable,
|
|
RssDownloaderRulesTable: RssDownloaderRulesTable,
|
|
RssDownloaderFeedSelectionTable: RssDownloaderFeedSelectionTable,
|
|
RssDownloaderArticlesTable: RssDownloaderArticlesTable,
|
|
TorrentWebseedsTable: TorrentWebseedsTable,
|
|
TorrentCreationTasksTable: TorrentCreationTasksTable,
|
|
};
|
|
};
|
|
|
|
const compareNumbers = (val1, val2) => {
|
|
if (val1 < val2)
|
|
return -1;
|
|
if (val1 > val2)
|
|
return 1;
|
|
return 0;
|
|
};
|
|
|
|
const localPreferences = new window.qBittorrent.LocalPreferences.LocalPreferences();
|
|
|
|
class DynamicTable {
|
|
#DynamicTableHeaderContextMenuClass = null;
|
|
|
|
setup(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;
|
|
this.sortedColumn = localPreferences.get(`sorted_column_${this.dynamicTableDivId}`, 0);
|
|
this.reverseSort = localPreferences.get(`reverse_sort_${this.dynamicTableDivId}`, "0");
|
|
this.initColumns();
|
|
this.loadColumnsOrder();
|
|
this.updateTableHeaders();
|
|
this.setupCommonEvents();
|
|
this.setupHeaderEvents();
|
|
this.setupHeaderMenu();
|
|
this.setupAltRow();
|
|
this.setupVirtualList();
|
|
}
|
|
|
|
setupVirtualList() {
|
|
if (!this.useVirtualList)
|
|
return;
|
|
this.table.style.position = "relative";
|
|
|
|
this.renderedOffset = this.dynamicTableDiv.scrollTop;
|
|
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() {
|
|
const tableFixedHeaderDiv = document.getElementById(this.dynamicTableFixedHeaderDivId);
|
|
|
|
const tableElement = tableFixedHeaderDiv.querySelector("table");
|
|
this.dynamicTableDiv.addEventListener("scroll", (e) => {
|
|
tableElement.style.left = `${-this.dynamicTableDiv.scrollLeft}px`;
|
|
// rerender on scroll
|
|
if (this.useVirtualList) {
|
|
this.renderedOffset = this.dynamicTableDiv.scrollTop;
|
|
this.rerender();
|
|
}
|
|
});
|
|
|
|
this.dynamicTableDiv.addEventListener("click", (e) => {
|
|
const tr = e.target.closest("tr");
|
|
if (!tr) {
|
|
// clicking on the table body deselects all rows
|
|
this.deselectAll();
|
|
this.setRowClass();
|
|
return;
|
|
}
|
|
|
|
if (e.ctrlKey || e.metaKey) {
|
|
// CTRL/CMD ⌘ key was pressed
|
|
if (this.isRowSelected(tr.rowId))
|
|
this.deselectRow(tr.rowId);
|
|
else
|
|
this.selectRow(tr.rowId);
|
|
}
|
|
else if (e.shiftKey && (this.selectedRows.length === 1)) {
|
|
// Shift key was pressed
|
|
this.selectRows(this.getSelectedRowId(), tr.rowId);
|
|
}
|
|
else {
|
|
// Simple selection
|
|
this.deselectAll();
|
|
this.selectRow(tr.rowId);
|
|
}
|
|
});
|
|
|
|
this.dynamicTableDiv.addEventListener("contextmenu", (e) => {
|
|
const tr = e.target.closest("tr");
|
|
if (!tr)
|
|
return;
|
|
|
|
if (!this.isRowSelected(tr.rowId)) {
|
|
this.deselectAll();
|
|
this.selectRow(tr.rowId);
|
|
}
|
|
}, true);
|
|
|
|
this.dynamicTableDiv.addEventListener("touchstart", (e) => {
|
|
const tr = e.target.closest("tr");
|
|
if (!tr)
|
|
return;
|
|
|
|
if (!this.isRowSelected(tr.rowId)) {
|
|
this.deselectAll();
|
|
this.selectRow(tr.rowId);
|
|
}
|
|
}, { passive: true });
|
|
|
|
this.dynamicTableDiv.addEventListener("keydown", (e) => {
|
|
const tr = e.target.closest("tr");
|
|
if (!tr)
|
|
return;
|
|
|
|
switch (e.key) {
|
|
case "ArrowUp":
|
|
e.preventDefault();
|
|
this.selectPreviousRow();
|
|
this.dynamicTableDiv.querySelector(".selected").scrollIntoView({ block: "nearest" });
|
|
break;
|
|
|
|
case "ArrowDown":
|
|
e.preventDefault();
|
|
this.selectNextRow();
|
|
this.dynamicTableDiv.querySelector(".selected").scrollIntoView({ block: "nearest" });
|
|
break;
|
|
}
|
|
});
|
|
}
|
|
|
|
setupHeaderEvents() {
|
|
this.currentHeaderAction = "";
|
|
this.canResize = false;
|
|
|
|
const resetElementBorderStyle = (el, side) => {
|
|
if ((side === "left") || (side !== "right"))
|
|
el.style.borderLeft = "";
|
|
if ((side === "right") || (side !== "left"))
|
|
el.style.borderRight = "";
|
|
};
|
|
|
|
const mouseMoveFn = function(e) {
|
|
const brect = e.target.getBoundingClientRect();
|
|
const mouseXRelative = e.clientX - brect.left;
|
|
if (this.currentHeaderAction === "") {
|
|
if ((brect.width - mouseXRelative) < 5) {
|
|
this.resizeTh = e.target;
|
|
this.canResize = true;
|
|
e.target.closest("tr").style.cursor = "col-resize";
|
|
}
|
|
else if ((mouseXRelative < 5) && e.target.getPrevious('[class=""]')) {
|
|
this.resizeTh = e.target.getPrevious('[class=""]');
|
|
this.canResize = true;
|
|
e.target.closest("tr").style.cursor = "col-resize";
|
|
}
|
|
else {
|
|
this.canResize = false;
|
|
e.target.closest("tr").style.cursor = "";
|
|
}
|
|
}
|
|
if (this.currentHeaderAction === "drag") {
|
|
const previousVisibleSibling = e.target.getPrevious('[class=""]');
|
|
let borderChangeElement = previousVisibleSibling;
|
|
let changeBorderSide = "right";
|
|
|
|
if (mouseXRelative > (brect.width / 2)) {
|
|
borderChangeElement = e.target;
|
|
this.dropSide = "right";
|
|
}
|
|
else {
|
|
this.dropSide = "left";
|
|
}
|
|
|
|
e.target.closest("tr").style.cursor = "move";
|
|
|
|
if (!previousVisibleSibling) { // right most column
|
|
borderChangeElement = e.target;
|
|
|
|
if (mouseXRelative <= (brect.width / 2))
|
|
changeBorderSide = "left";
|
|
}
|
|
|
|
const borderStyle = "solid #e60";
|
|
if (changeBorderSide === "left") {
|
|
borderChangeElement.style.borderLeft = borderStyle;
|
|
borderChangeElement.style.borderLeftWidth = "initial";
|
|
}
|
|
else {
|
|
borderChangeElement.style.borderRight = borderStyle;
|
|
borderChangeElement.style.borderRightWidth = "initial";
|
|
}
|
|
|
|
resetElementBorderStyle(borderChangeElement, ((changeBorderSide === "right") ? "left" : "right"));
|
|
|
|
borderChangeElement.getSiblings('[class=""]').each((el) => {
|
|
resetElementBorderStyle(el);
|
|
});
|
|
}
|
|
this.lastHoverTh = e.target;
|
|
this.lastClientX = e.clientX;
|
|
}.bind(this);
|
|
|
|
const mouseOutFn = (e) => {
|
|
resetElementBorderStyle(e.target);
|
|
};
|
|
|
|
const onBeforeStart = function(el) {
|
|
this.clickedTh = el;
|
|
this.currentHeaderAction = "start";
|
|
this.dragMovement = false;
|
|
this.dragStartX = this.lastClientX;
|
|
}.bind(this);
|
|
|
|
const onStart = function(el, event) {
|
|
if (this.canResize) {
|
|
this.currentHeaderAction = "resize";
|
|
this.startWidth = Number.parseInt(this.resizeTh.style.width, 10);
|
|
}
|
|
else {
|
|
this.currentHeaderAction = "drag";
|
|
el.style.backgroundColor = "#C1D5E7";
|
|
}
|
|
}.bind(this);
|
|
|
|
const onDrag = function(el, event) {
|
|
if (this.currentHeaderAction === "resize") {
|
|
let width = this.startWidth + (event.event.pageX - this.dragStartX);
|
|
if (width < 16)
|
|
width = 16;
|
|
|
|
this.#setColumnWidth(this.resizeTh.columnName, width);
|
|
}
|
|
}.bind(this);
|
|
|
|
const onComplete = function(el, event) {
|
|
resetElementBorderStyle(this.lastHoverTh);
|
|
el.style.backgroundColor = "";
|
|
if (this.currentHeaderAction === "resize")
|
|
this.saveColumnWidth(this.resizeTh.columnName);
|
|
if ((this.currentHeaderAction === "drag") && (el !== this.lastHoverTh)) {
|
|
this.saveColumnsOrder();
|
|
const val = localPreferences.get(`columns_order_${this.dynamicTableDivId}`).split(",");
|
|
val.erase(el.columnName);
|
|
let pos = val.indexOf(this.lastHoverTh.columnName);
|
|
if (this.dropSide === "right")
|
|
++pos;
|
|
val.splice(pos, 0, el.columnName);
|
|
localPreferences.set(`columns_order_${this.dynamicTableDivId}`, val.join(","));
|
|
this.loadColumnsOrder();
|
|
this.updateTableHeaders();
|
|
this.tableBody.replaceChildren();
|
|
this.updateTable(true);
|
|
this.reselectRows(this.selectedRowsIds());
|
|
}
|
|
if (this.currentHeaderAction === "drag") {
|
|
resetElementBorderStyle(el);
|
|
el.getSiblings('[class=""]').each((el) => {
|
|
resetElementBorderStyle(el);
|
|
});
|
|
}
|
|
this.currentHeaderAction = "";
|
|
}.bind(this);
|
|
|
|
const onCancel = function(el) {
|
|
this.currentHeaderAction = "";
|
|
|
|
// ignore click/touch events performed when on the column's resize area
|
|
if (!this.canResize)
|
|
this.setSortedColumn(el.columnName);
|
|
}.bind(this);
|
|
|
|
const onTouch = function(e) {
|
|
const column = e.target.columnName;
|
|
this.currentHeaderAction = "";
|
|
this.setSortedColumn(column);
|
|
}.bind(this);
|
|
|
|
const onDoubleClick = function(e) {
|
|
e.preventDefault();
|
|
this.currentHeaderAction = "";
|
|
|
|
// only resize when hovering on the column's resize area
|
|
if (this.canResize) {
|
|
this.currentHeaderAction = "resize";
|
|
this.autoResizeColumn(e.target.columnName);
|
|
onComplete(e.target);
|
|
}
|
|
}.bind(this);
|
|
|
|
for (const th of this.getRowCells(this.fixedTableHeader)) {
|
|
th.addEventListener("mousemove", mouseMoveFn);
|
|
th.addEventListener("mouseout", mouseOutFn);
|
|
th.addEventListener("touchend", onTouch, { passive: true });
|
|
th.addEventListener("dblclick", onDoubleClick);
|
|
th.makeResizable({
|
|
modifiers: {
|
|
x: "",
|
|
y: ""
|
|
},
|
|
onBeforeStart: onBeforeStart,
|
|
onStart: onStart,
|
|
onDrag: onDrag,
|
|
onComplete: onComplete,
|
|
onCancel: onCancel
|
|
});
|
|
}
|
|
}
|
|
|
|
setupDynamicTableHeaderContextMenuClass() {
|
|
this.#DynamicTableHeaderContextMenuClass ??= class extends window.qBittorrent.ContextMenu.ContextMenu {
|
|
updateMenuItems() {
|
|
for (let i = 0; i < this.dynamicTable.columns.length; ++i) {
|
|
if (this.dynamicTable.columns[i].caption === "")
|
|
continue;
|
|
if (this.dynamicTable.columns[i].visible !== "0")
|
|
this.setItemChecked(this.dynamicTable.columns[i].name, true);
|
|
else
|
|
this.setItemChecked(this.dynamicTable.columns[i].name, false);
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
showColumn(columnName, show) {
|
|
this.columns[columnName].visible = show ? "1" : "0";
|
|
localPreferences.set(`column_${columnName}_visible_${this.dynamicTableDivId}`, show ? "1" : "0");
|
|
this.updateColumn(columnName);
|
|
this.columns[columnName].onVisibilityChange?.(columnName);
|
|
}
|
|
|
|
#calculateColumnBodyWidth(column) {
|
|
const columnIndex = this.getColumnPos(column.name);
|
|
const bodyColumn = document.getElementById(this.dynamicTableDivId).querySelectorAll("tr>th")[columnIndex];
|
|
const canvas = document.createElement("canvas");
|
|
const context = canvas.getContext("2d");
|
|
context.font = window.getComputedStyle(bodyColumn, null).getPropertyValue("font");
|
|
|
|
const longestTd = { value: "", width: 0 };
|
|
for (const tr of this.getTrs()) {
|
|
const tds = this.getRowCells(tr);
|
|
const td = tds[columnIndex];
|
|
|
|
const buffer = column.calculateBuffer(tr.rowId);
|
|
const valueWidth = context.measureText(td.textContent).width;
|
|
if ((valueWidth + buffer) > (longestTd.width)) {
|
|
longestTd.value = td.textContent;
|
|
longestTd.width = valueWidth + buffer;
|
|
}
|
|
}
|
|
|
|
// slight buffer to prevent clipping
|
|
return longestTd.width + 10;
|
|
}
|
|
|
|
#setColumnWidth(columnName, width) {
|
|
const column = this.columns[columnName];
|
|
column.width = width;
|
|
|
|
const pos = this.getColumnPos(column.name);
|
|
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);
|
|
}
|
|
|
|
autoResizeColumn(columnName) {
|
|
const column = this.columns[columnName];
|
|
|
|
let width = column.staticWidth ?? 0;
|
|
if (column.staticWidth === null) {
|
|
// check required min body width
|
|
const bodyTextWidth = this.#calculateColumnBodyWidth(column);
|
|
|
|
// check required min header width
|
|
const columnIndex = this.getColumnPos(column.name);
|
|
const headColumn = document.getElementById(this.dynamicTableFixedHeaderDivId).querySelectorAll("tr>th")[columnIndex];
|
|
const canvas = document.createElement("canvas");
|
|
const context = canvas.getContext("2d");
|
|
context.font = window.getComputedStyle(headColumn, null).getPropertyValue("font");
|
|
const columnTitle = column.caption;
|
|
const sortedIconWidth = 20;
|
|
const headTextWidth = context.measureText(columnTitle).width + sortedIconWidth;
|
|
|
|
width = Math.max(headTextWidth, bodyTextWidth);
|
|
}
|
|
|
|
this.#setColumnWidth(column.name, width);
|
|
this.saveColumnWidth(column.name);
|
|
}
|
|
|
|
saveColumnWidth(columnName) {
|
|
localPreferences.set(`column_${columnName}_width_${this.dynamicTableDivId}`, this.columns[columnName].width);
|
|
}
|
|
|
|
setupHeaderMenu() {
|
|
this.setupDynamicTableHeaderContextMenuClass();
|
|
|
|
const menuId = `${this.dynamicTableDivId}_headerMenu`;
|
|
|
|
// reuse menu if already exists
|
|
let ul = document.getElementById(menuId);
|
|
if (ul === null) {
|
|
ul = document.createElement("ul");
|
|
ul.id = menuId;
|
|
ul.className = "contextMenu scrollableMenu";
|
|
}
|
|
|
|
const createLi = (columnName, text) => {
|
|
const anchor = document.createElement("a");
|
|
anchor.href = `#${columnName}`;
|
|
anchor.textContent = text;
|
|
|
|
const img = document.createElement("img");
|
|
img.src = "images/checked-completed.svg";
|
|
anchor.prepend(img);
|
|
|
|
const listItem = document.createElement("li");
|
|
listItem.appendChild(anchor);
|
|
|
|
return listItem;
|
|
};
|
|
|
|
const actions = {
|
|
autoResizeAction: function(element, ref, action) {
|
|
this.autoResizeColumn(element.columnName);
|
|
}.bind(this),
|
|
|
|
autoResizeAllAction: function(element, ref, action) {
|
|
for (const { name } of this.columns)
|
|
this.autoResizeColumn(name);
|
|
}.bind(this),
|
|
};
|
|
|
|
const onMenuItemClicked = function(element, ref, action) {
|
|
this.showColumn(action, this.columns[action].visible === "0");
|
|
}.bind(this);
|
|
|
|
// recreate child elements when reusing (enables the context menu to work correctly)
|
|
ul.replaceChildren();
|
|
|
|
for (let i = 0; i < this.columns.length; ++i) {
|
|
const text = this.columns[i].caption;
|
|
if (text === "")
|
|
continue;
|
|
ul.appendChild(createLi(this.columns[i].name, text));
|
|
actions[this.columns[i].name] = onMenuItemClicked;
|
|
}
|
|
|
|
const createResizeElement = (text, href) => {
|
|
const anchor = document.createElement("a");
|
|
anchor.href = href;
|
|
anchor.textContent = text;
|
|
|
|
const spacer = document.createElement("span");
|
|
spacer.style = "display: inline-block; width: calc(.5em + 16px);";
|
|
anchor.prepend(spacer);
|
|
|
|
const li = document.createElement("li");
|
|
li.appendChild(anchor);
|
|
return li;
|
|
};
|
|
|
|
const autoResizeAllElement = createResizeElement("QBT_TR(Resize All)QBT_TR[CONTEXT=ListWidget]", "#autoResizeAllAction");
|
|
const autoResizeElement = createResizeElement("QBT_TR(Resize)QBT_TR[CONTEXT=ListWidget]", "#autoResizeAction");
|
|
|
|
ul.firstElementChild.classList.add("separator");
|
|
ul.insertBefore(autoResizeAllElement, ul.firstElementChild);
|
|
ul.insertBefore(autoResizeElement, ul.firstElementChild);
|
|
document.body.append(ul);
|
|
|
|
this.headerContextMenu = new this.#DynamicTableHeaderContextMenuClass({
|
|
targets: `#${this.dynamicTableFixedHeaderDivId} tr th`,
|
|
actions: actions,
|
|
menu: menuId,
|
|
offsets: {
|
|
x: 0,
|
|
y: 2
|
|
}
|
|
});
|
|
|
|
this.headerContextMenu.dynamicTable = this;
|
|
}
|
|
|
|
initColumns() {}
|
|
|
|
newColumn(name, style, caption, defaultWidth, defaultVisible) {
|
|
const column = {};
|
|
column["name"] = name;
|
|
column["title"] = name;
|
|
column["visible"] = localPreferences.get(`column_${name}_visible_${this.dynamicTableDivId}`, (defaultVisible ? "1" : "0"));
|
|
column["force_hide"] = false;
|
|
column["caption"] = caption;
|
|
column["style"] = style;
|
|
column["width"] = localPreferences.get(`column_${name}_width_${this.dynamicTableDivId}`, defaultWidth);
|
|
column["dataProperties"] = [name];
|
|
column["getRowValue"] = function(row, pos) {
|
|
if (pos === undefined)
|
|
pos = 0;
|
|
return row["full_data"][this.dataProperties[pos]];
|
|
};
|
|
column["compareRows"] = function(row1, row2) {
|
|
const value1 = this.getRowValue(row1);
|
|
const value2 = this.getRowValue(row2);
|
|
if ((typeof(value1) === "number") && (typeof(value2) === "number"))
|
|
return compareNumbers(value1, value2);
|
|
return window.qBittorrent.Misc.naturalSortCollator.compare(value1, value2);
|
|
};
|
|
column["updateTd"] = function(td, row) {
|
|
const value = this.getRowValue(row);
|
|
td.textContent = value;
|
|
td.title = value;
|
|
};
|
|
column["isVisible"] = function() {
|
|
return (this.visible === "1") && !this.force_hide;
|
|
};
|
|
column["onResize"] = null;
|
|
column["onVisibilityChange"] = null;
|
|
column["staticWidth"] = null;
|
|
column["calculateBuffer"] = () => 0;
|
|
this.columns.push(column);
|
|
this.columns[name] = column;
|
|
|
|
this.hiddenTableHeader.append(document.createElement("th"));
|
|
this.fixedTableHeader.append(document.createElement("th"));
|
|
}
|
|
|
|
loadColumnsOrder() {
|
|
const columnsOrder = [];
|
|
const val = localPreferences.get(`columns_order_${this.dynamicTableDivId}`);
|
|
if ((val === null) || (val === undefined))
|
|
return;
|
|
for (const v of val.split(",")) {
|
|
if ((v in this.columns) && (!columnsOrder.contains(v)))
|
|
columnsOrder.push(v);
|
|
}
|
|
|
|
for (let i = 0; i < this.columns.length; ++i) {
|
|
if (!columnsOrder.contains(this.columns[i].name))
|
|
columnsOrder.push(this.columns[i].name);
|
|
}
|
|
|
|
for (let i = 0; i < this.columns.length; ++i)
|
|
this.columns[i] = this.columns[columnsOrder[i]];
|
|
}
|
|
|
|
saveColumnsOrder() {
|
|
let val = "";
|
|
for (let i = 0; i < this.columns.length; ++i) {
|
|
if (i > 0)
|
|
val += ",";
|
|
val += this.columns[i].name;
|
|
}
|
|
localPreferences.set(`columns_order_${this.dynamicTableDivId}`, val);
|
|
}
|
|
|
|
updateTableHeaders() {
|
|
this.updateHeader(this.hiddenTableHeader);
|
|
this.updateHeader(this.fixedTableHeader);
|
|
this.setSortedColumnIcon(this.sortedColumn, null, (this.reverseSort === "1"));
|
|
}
|
|
|
|
updateHeader(header) {
|
|
const ths = this.getRowCells(header);
|
|
for (const [i, th] of ths.entries()) {
|
|
if (th.columnName !== this.columns[i].name) {
|
|
th.title = this.columns[i].caption;
|
|
th.textContent = this.columns[i].caption;
|
|
th.style.cssText = `width: ${this.columns[i].width}px; ${this.columns[i].style}`;
|
|
th.columnName = this.columns[i].name;
|
|
th.className = `column_${th.columnName}`;
|
|
th.classList.toggle("invisible", ((this.columns[i].visible === "0") || this.columns[i].force_hide));
|
|
}
|
|
}
|
|
}
|
|
|
|
getColumnPos(columnName) {
|
|
for (let i = 0; i < this.columns.length; ++i) {
|
|
if (this.columns[i].name === columnName)
|
|
return i;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
updateColumn(columnName, updateCellData = false) {
|
|
const column = this.columns[columnName];
|
|
const pos = this.getColumnPos(columnName);
|
|
const ths = this.getRowCells(this.hiddenTableHeader);
|
|
const fths = this.getRowCells(this.fixedTableHeader);
|
|
const action = column.isVisible() ? "remove" : "add";
|
|
ths[pos].classList[action]("invisible");
|
|
fths[pos].classList[action]("invisible");
|
|
|
|
for (const tr of this.getTrs()) {
|
|
const td = this.getRowCells(tr)[pos];
|
|
td.classList[action]("invisible");
|
|
if (updateCellData)
|
|
column.updateTd(td, this.rows.get(tr.rowId));
|
|
}
|
|
}
|
|
|
|
getSortedColumn() {
|
|
return localPreferences.get(`sorted_column_${this.dynamicTableDivId}`);
|
|
}
|
|
|
|
/**
|
|
* @param {string} column name to sort by
|
|
* @param {string|null} reverse defaults to implementation-specific behavior when not specified. Should only be passed when restoring previous state.
|
|
*/
|
|
setSortedColumn(column, reverse = null) {
|
|
if (column !== this.sortedColumn) {
|
|
const oldColumn = this.sortedColumn;
|
|
this.sortedColumn = column;
|
|
this.reverseSort = reverse ?? "0";
|
|
this.setSortedColumnIcon(column, oldColumn, false);
|
|
}
|
|
else {
|
|
// Toggle sort order
|
|
this.reverseSort = reverse ?? (this.reverseSort === "0" ? "1" : "0");
|
|
this.setSortedColumnIcon(column, null, (this.reverseSort === "1"));
|
|
}
|
|
localPreferences.set(`sorted_column_${this.dynamicTableDivId}`, column);
|
|
localPreferences.set(`reverse_sort_${this.dynamicTableDivId}`, this.reverseSort);
|
|
this.updateTable(false);
|
|
}
|
|
|
|
setSortedColumnIcon(newColumn, oldColumn, isReverse) {
|
|
const getCol = (headerDivId, colName) => {
|
|
const colElem = document.querySelectorAll(`#${headerDivId} .column_${colName}`);
|
|
if (colElem.length === 1)
|
|
return colElem[0];
|
|
return null;
|
|
};
|
|
|
|
const colElem = getCol(this.dynamicTableFixedHeaderDivId, newColumn);
|
|
if (colElem !== null) {
|
|
colElem.classList.add("sorted");
|
|
colElem.classList.toggle("reverse", isReverse);
|
|
}
|
|
const oldColElem = getCol(this.dynamicTableFixedHeaderDivId, oldColumn);
|
|
if (oldColElem !== null)
|
|
oldColElem.classList.remove("sorted", "reverse");
|
|
}
|
|
|
|
getSelectedRowId() {
|
|
if (this.selectedRows.length > 0)
|
|
return this.selectedRows[0];
|
|
return "";
|
|
}
|
|
|
|
isRowSelected(rowId) {
|
|
return this.selectedRows.contains(rowId);
|
|
}
|
|
|
|
setupAltRow() {
|
|
const useAltRowColors = (localPreferences.get("use_alt_row_colors", "true") === "true");
|
|
if (useAltRowColors)
|
|
document.getElementById(this.dynamicTableDivId).classList.add("altRowColors");
|
|
}
|
|
|
|
selectAll() {
|
|
this.deselectAll();
|
|
for (const row of this.getFilteredAndSortedRows())
|
|
this.selectedRows.push(row.rowId);
|
|
this.setRowClass();
|
|
}
|
|
|
|
deselectAll() {
|
|
this.selectedRows.empty();
|
|
}
|
|
|
|
selectRow(rowId) {
|
|
this.selectedRows.push(rowId);
|
|
this.setRowClass();
|
|
this.onSelectedRowChanged();
|
|
}
|
|
|
|
deselectRow(rowId) {
|
|
this.selectedRows.erase(rowId);
|
|
this.setRowClass();
|
|
this.onSelectedRowChanged();
|
|
}
|
|
|
|
selectRows(rowId1, rowId2) {
|
|
this.deselectAll();
|
|
if (rowId1 === rowId2) {
|
|
this.selectRow(rowId1);
|
|
return;
|
|
}
|
|
|
|
let select = false;
|
|
for (const tr of this.getTrs()) {
|
|
if ((tr.rowId === rowId1) || (tr.rowId === rowId2)) {
|
|
select = !select;
|
|
this.selectedRows.push(tr.rowId);
|
|
}
|
|
else if (select) {
|
|
this.selectedRows.push(tr.rowId);
|
|
}
|
|
}
|
|
this.setRowClass();
|
|
this.onSelectedRowChanged();
|
|
}
|
|
|
|
reselectRows(rowIds) {
|
|
this.deselectAll();
|
|
this.selectedRows = rowIds.slice();
|
|
this.setRowClass();
|
|
}
|
|
|
|
setRowClass() {
|
|
for (const tr of this.getTrs())
|
|
tr.classList.toggle("selected", this.isRowSelected(tr.rowId));
|
|
}
|
|
|
|
onSelectedRowChanged() {}
|
|
|
|
updateRowData(data) {
|
|
// ensure rowId is a string
|
|
const rowId = `${data["rowId"]}`;
|
|
let row;
|
|
|
|
if (!this.rows.has(rowId)) {
|
|
row = {
|
|
full_data: {},
|
|
rowId: rowId
|
|
};
|
|
this.rows.set(rowId, row);
|
|
}
|
|
else {
|
|
row = this.rows.get(rowId);
|
|
}
|
|
|
|
row["data"] = data;
|
|
for (const x in data) {
|
|
if (!Object.hasOwn(data, x))
|
|
continue;
|
|
row["full_data"][x] = data[x];
|
|
}
|
|
}
|
|
|
|
getTrs() {
|
|
return this.tableBody.querySelectorAll("tr");
|
|
}
|
|
|
|
getRowCells(tr) {
|
|
return tr.querySelectorAll("td, th");
|
|
}
|
|
|
|
getRow(rowId) {
|
|
return this.rows.get(rowId);
|
|
}
|
|
|
|
getFilteredAndSortedRows() {
|
|
const filteredRows = [];
|
|
|
|
for (const row of this.getRowValues()) {
|
|
filteredRows.push(row);
|
|
filteredRows[row.rowId] = row;
|
|
}
|
|
|
|
const column = this.columns[this.sortedColumn];
|
|
const isReverseSort = (this.reverseSort === "0");
|
|
filteredRows.sort((row1, row2) => {
|
|
const result = column.compareRows(row1, row2);
|
|
return isReverseSort ? result : -result;
|
|
});
|
|
return filteredRows;
|
|
}
|
|
|
|
getTrByRowId(rowId) {
|
|
return Array.prototype.find.call(this.getTrs(), (tr => tr.rowId === rowId));
|
|
}
|
|
|
|
updateTable(fullUpdate = false) {
|
|
const rows = this.getFilteredAndSortedRows();
|
|
|
|
for (let i = 0; i < this.selectedRows.length; ++i) {
|
|
if (!(this.selectedRows[i] in rows)) {
|
|
this.selectedRows.splice(i, 1);
|
|
--i;
|
|
}
|
|
}
|
|
|
|
if (this.useVirtualList) {
|
|
// rerender on table update
|
|
this.rerender(rows);
|
|
}
|
|
else {
|
|
const trs = [...this.getTrs()];
|
|
const trMap = new Map(trs.map(tr => [tr.rowId, tr]));
|
|
|
|
for (const row of rows) {
|
|
const rowId = row.rowId;
|
|
const existingTr = trMap.get(rowId);
|
|
if (existingTr !== undefined) {
|
|
this.updateRow(existingTr, fullUpdate);
|
|
}
|
|
else {
|
|
const tr = this.createRowElement(row);
|
|
|
|
// TODO look into using DocumentFragment or appending all trs at once for add'l performance gains
|
|
// add to end of table - we'll move into the proper order later
|
|
this.tableBody.appendChild(tr);
|
|
trMap.set(rowId, tr);
|
|
|
|
this.updateRow(tr, true);
|
|
}
|
|
}
|
|
|
|
// reorder table rows
|
|
let prevTr = null;
|
|
for (const [rowPos, { rowId }] of rows.entries()) {
|
|
const tr = trMap.get(rowId);
|
|
trMap.delete(rowId);
|
|
|
|
const isInCorrectLocation = rowId === trs[rowPos]?.rowId;
|
|
if (!isInCorrectLocation) {
|
|
// move row into correct location
|
|
if (prevTr === null) {
|
|
// insert as first row in table
|
|
if (trs.length === 0)
|
|
this.tableBody.append(tr);
|
|
else
|
|
trs[0].before(tr);
|
|
}
|
|
else {
|
|
prevTr.after(tr);
|
|
}
|
|
}
|
|
prevTr = tr;
|
|
}
|
|
|
|
for (const tr of trMap.values())
|
|
tr.remove();
|
|
}
|
|
}
|
|
|
|
rerender(rows = this.getFilteredAndSortedRows()) {
|
|
// set the scrollable height
|
|
const tableHeight = rows.length * this.rowHeight;
|
|
if (tableHeight !== this.previousTableHeight) {
|
|
this.previousTableHeight = tableHeight;
|
|
this.table.style.height = `${tableHeight}px`;
|
|
}
|
|
|
|
if (this.renderedHeight === 0)
|
|
return;
|
|
// show extra rows at top/bottom to reduce flickering
|
|
const extraRowCount = 20;
|
|
// 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 renderedOffset
|
|
let startRow = Math.max((Math.trunc(this.renderedOffset / 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);
|
|
}
|
|
|
|
createRowElement(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;
|
|
|
|
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);
|
|
}
|
|
|
|
this.updateRowElement(tr, row.rowId, top);
|
|
|
|
// update context menu
|
|
this.contextMenu?.addTarget(tr);
|
|
return tr;
|
|
}
|
|
|
|
updateRowElement(tr, rowId, top) {
|
|
tr.dataset.rowId = rowId;
|
|
tr.rowId = rowId;
|
|
|
|
tr.className = "";
|
|
|
|
if (this.useVirtualList) {
|
|
tr.style.position = "absolute";
|
|
tr.style.top = `${top}px`;
|
|
tr.style.height = `${this.rowHeight}px`;
|
|
}
|
|
}
|
|
|
|
getRowData(row, fullUpdate) {
|
|
return row[fullUpdate ? "full_data" : "data"];
|
|
}
|
|
|
|
updateRow(tr, fullUpdate) {
|
|
const row = this.rows.get(tr.rowId);
|
|
const data = this.getRowData(row, fullUpdate);
|
|
|
|
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);
|
|
}
|
|
row.data = {};
|
|
}
|
|
|
|
removeRow(rowId) {
|
|
this.selectedRows.erase(rowId);
|
|
this.rows.delete(rowId);
|
|
if (this.useVirtualList) {
|
|
this.rerender();
|
|
}
|
|
else {
|
|
const tr = this.getTrByRowId(rowId);
|
|
tr?.remove();
|
|
}
|
|
}
|
|
|
|
clear() {
|
|
this.deselectAll();
|
|
this.rows.clear();
|
|
if (this.useVirtualList) {
|
|
this.rerender();
|
|
}
|
|
else {
|
|
for (const tr of this.getTrs())
|
|
tr.remove();
|
|
}
|
|
}
|
|
|
|
selectedRowsIds() {
|
|
return this.selectedRows.slice();
|
|
}
|
|
|
|
getRowIds() {
|
|
return this.rows.keys();
|
|
}
|
|
|
|
getRowValues() {
|
|
return this.rows.values();
|
|
}
|
|
|
|
getRowItems() {
|
|
return this.rows.entries();
|
|
}
|
|
|
|
getRowSize() {
|
|
return this.rows.size;
|
|
}
|
|
|
|
selectNextRow() {
|
|
const visibleRows = Array.prototype.filter.call(this.getTrs(), (tr => !tr.classList.contains("invisible") && (tr.style.display !== "none")));
|
|
const selectedRowId = this.getSelectedRowId();
|
|
|
|
let selectedIndex = -1;
|
|
for (const [i, row] of visibleRows.entries()) {
|
|
if (row.getAttribute("data-row-id") === selectedRowId) {
|
|
selectedIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
const isLastRowSelected = (selectedIndex >= (visibleRows.length - 1));
|
|
if (!isLastRowSelected) {
|
|
this.deselectAll();
|
|
|
|
const newRow = visibleRows[selectedIndex + 1];
|
|
this.selectRow(newRow.getAttribute("data-row-id"));
|
|
}
|
|
}
|
|
|
|
selectPreviousRow() {
|
|
const visibleRows = Array.prototype.filter.call(this.getTrs(), (tr => !tr.classList.contains("invisible") && (tr.style.display !== "none")));
|
|
const selectedRowId = this.getSelectedRowId();
|
|
|
|
let selectedIndex = -1;
|
|
for (const [i, row] of visibleRows.entries()) {
|
|
if (row.getAttribute("data-row-id") === selectedRowId) {
|
|
selectedIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
const isFirstRowSelected = selectedIndex <= 0;
|
|
if (!isFirstRowSelected) {
|
|
this.deselectAll();
|
|
|
|
const newRow = visibleRows[selectedIndex - 1];
|
|
this.selectRow(newRow.getAttribute("data-row-id"));
|
|
}
|
|
}
|
|
}
|
|
|
|
class TorrentsTable extends DynamicTable {
|
|
setupVirtualList() {
|
|
super.setupVirtualList();
|
|
this.rowHeight = 22;
|
|
}
|
|
|
|
initColumns() {
|
|
this.newColumn("priority", "", "#", 30, true);
|
|
this.newColumn("state_icon", "", "QBT_TR(Status Icon)QBT_TR[CONTEXT=TransferListModel]", 30, false);
|
|
this.newColumn("name", "", "QBT_TR(Name)QBT_TR[CONTEXT=TransferListModel]", 200, true);
|
|
this.newColumn("size", "", "QBT_TR(Size)QBT_TR[CONTEXT=TransferListModel]", 100, true);
|
|
this.newColumn("total_size", "", "QBT_TR(Total Size)QBT_TR[CONTEXT=TransferListModel]", 100, false);
|
|
this.newColumn("progress", "", "QBT_TR(Progress)QBT_TR[CONTEXT=TransferListModel]", 85, true);
|
|
this.newColumn("status", "", "QBT_TR(Status)QBT_TR[CONTEXT=TransferListModel]", 100, true);
|
|
this.newColumn("num_seeds", "", "QBT_TR(Seeds)QBT_TR[CONTEXT=TransferListModel]", 100, true);
|
|
this.newColumn("num_leechs", "", "QBT_TR(Peers)QBT_TR[CONTEXT=TransferListModel]", 100, true);
|
|
this.newColumn("dlspeed", "", "QBT_TR(Down Speed)QBT_TR[CONTEXT=TransferListModel]", 100, true);
|
|
this.newColumn("upspeed", "", "QBT_TR(Up Speed)QBT_TR[CONTEXT=TransferListModel]", 100, true);
|
|
this.newColumn("eta", "", "QBT_TR(ETA)QBT_TR[CONTEXT=TransferListModel]", 100, true);
|
|
this.newColumn("ratio", "", "QBT_TR(Ratio)QBT_TR[CONTEXT=TransferListModel]", 100, true);
|
|
this.newColumn("popularity", "", "QBT_TR(Popularity)QBT_TR[CONTEXT=TransferListModel]", 100, true);
|
|
this.newColumn("category", "", "QBT_TR(Category)QBT_TR[CONTEXT=TransferListModel]", 100, true);
|
|
this.newColumn("tags", "", "QBT_TR(Tags)QBT_TR[CONTEXT=TransferListModel]", 100, true);
|
|
this.newColumn("added_on", "", "QBT_TR(Added On)QBT_TR[CONTEXT=TransferListModel]", 100, true);
|
|
this.newColumn("completion_on", "", "QBT_TR(Completed On)QBT_TR[CONTEXT=TransferListModel]", 100, false);
|
|
this.newColumn("tracker", "", "QBT_TR(Tracker)QBT_TR[CONTEXT=TransferListModel]", 100, false);
|
|
this.newColumn("dl_limit", "", "QBT_TR(Down Limit)QBT_TR[CONTEXT=TransferListModel]", 100, false);
|
|
this.newColumn("up_limit", "", "QBT_TR(Up Limit)QBT_TR[CONTEXT=TransferListModel]", 100, false);
|
|
this.newColumn("downloaded", "", "QBT_TR(Downloaded)QBT_TR[CONTEXT=TransferListModel]", 100, false);
|
|
this.newColumn("uploaded", "", "QBT_TR(Uploaded)QBT_TR[CONTEXT=TransferListModel]", 100, false);
|
|
this.newColumn("downloaded_session", "", "QBT_TR(Session Download)QBT_TR[CONTEXT=TransferListModel]", 100, false);
|
|
this.newColumn("uploaded_session", "", "QBT_TR(Session Upload)QBT_TR[CONTEXT=TransferListModel]", 100, false);
|
|
this.newColumn("amount_left", "", "QBT_TR(Remaining)QBT_TR[CONTEXT=TransferListModel]", 100, false);
|
|
this.newColumn("time_active", "", "QBT_TR(Time Active)QBT_TR[CONTEXT=TransferListModel]", 100, false);
|
|
this.newColumn("save_path", "", "QBT_TR(Save path)QBT_TR[CONTEXT=TransferListModel]", 100, false);
|
|
this.newColumn("completed", "", "QBT_TR(Completed)QBT_TR[CONTEXT=TransferListModel]", 100, false);
|
|
this.newColumn("max_ratio", "", "QBT_TR(Ratio Limit)QBT_TR[CONTEXT=TransferListModel]", 100, false);
|
|
this.newColumn("seen_complete", "", "QBT_TR(Last Seen Complete)QBT_TR[CONTEXT=TransferListModel]", 100, false);
|
|
this.newColumn("last_activity", "", "QBT_TR(Last Activity)QBT_TR[CONTEXT=TransferListModel]", 100, false);
|
|
this.newColumn("availability", "", "QBT_TR(Availability)QBT_TR[CONTEXT=TransferListModel]", 100, false);
|
|
this.newColumn("download_path", "", "QBT_TR(Incomplete Save Path)QBT_TR[CONTEXT=TransferListModel]", 100, false);
|
|
this.newColumn("infohash_v1", "", "QBT_TR(Info Hash v1)QBT_TR[CONTEXT=TransferListModel]", 100, false);
|
|
this.newColumn("infohash_v2", "", "QBT_TR(Info Hash v2)QBT_TR[CONTEXT=TransferListModel]", 100, false);
|
|
this.newColumn("reannounce", "", "QBT_TR(Reannounce In)QBT_TR[CONTEXT=TransferListModel]", 100, false);
|
|
this.newColumn("private", "", "QBT_TR(Private)QBT_TR[CONTEXT=TransferListModel]", 100, false);
|
|
|
|
this.columns["state_icon"].dataProperties[0] = "state";
|
|
this.columns["name"].dataProperties.push("state");
|
|
this.columns["num_seeds"].dataProperties.push("num_complete");
|
|
this.columns["num_leechs"].dataProperties.push("num_incomplete");
|
|
this.columns["time_active"].dataProperties.push("seeding_time");
|
|
|
|
this.initColumnsFunctions();
|
|
}
|
|
|
|
initColumnsFunctions() {
|
|
const getStateIconClasses = (state) => {
|
|
let stateClass = "stateUnknown";
|
|
// normalize states
|
|
switch (state) {
|
|
case "forcedDL":
|
|
case "metaDL":
|
|
case "forcedMetaDL":
|
|
case "downloading":
|
|
stateClass = "stateDownloading";
|
|
break;
|
|
case "forcedUP":
|
|
case "uploading":
|
|
stateClass = "stateUploading";
|
|
break;
|
|
case "stalledUP":
|
|
stateClass = "stateStalledUP";
|
|
break;
|
|
case "stalledDL":
|
|
stateClass = "stateStalledDL";
|
|
break;
|
|
case "stoppedDL":
|
|
stateClass = "stateStoppedDL";
|
|
break;
|
|
case "stoppedUP":
|
|
stateClass = "stateStoppedUP";
|
|
break;
|
|
case "queuedDL":
|
|
case "queuedUP":
|
|
stateClass = "stateQueued";
|
|
break;
|
|
case "checkingDL":
|
|
case "checkingUP":
|
|
case "queuedForChecking":
|
|
case "checkingResumeData":
|
|
stateClass = "stateChecking";
|
|
break;
|
|
case "moving":
|
|
stateClass = "stateMoving";
|
|
break;
|
|
case "error":
|
|
case "unknown":
|
|
case "missingFiles":
|
|
stateClass = "stateError";
|
|
break;
|
|
default:
|
|
break; // do nothing
|
|
}
|
|
|
|
return `stateIcon ${stateClass}`;
|
|
};
|
|
|
|
// state_icon
|
|
this.columns["state_icon"].updateTd = function(td, row) {
|
|
const state = this.getRowValue(row);
|
|
let div = td.firstElementChild;
|
|
if (div === null) {
|
|
div = document.createElement("div");
|
|
td.append(div);
|
|
}
|
|
|
|
div.className = `${getStateIconClasses(state)} stateIconColumn`;
|
|
};
|
|
|
|
this.columns["state_icon"].onVisibilityChange = (columnName) => {
|
|
// show state icon in name column only when standalone
|
|
// state icon column is hidden
|
|
this.updateColumn("name", true);
|
|
};
|
|
|
|
// name
|
|
this.columns["name"].updateTd = function(td, row) {
|
|
const name = this.getRowValue(row, 0);
|
|
const state = this.getRowValue(row, 1);
|
|
let span = td.firstElementChild;
|
|
if (span === null) {
|
|
span = document.createElement("span");
|
|
td.append(span);
|
|
}
|
|
|
|
span.className = this.isStateIconShown() ? `${getStateIconClasses(state)}` : "";
|
|
span.textContent = name;
|
|
td.title = name;
|
|
};
|
|
|
|
this.columns["name"].isStateIconShown = () => !this.columns["state_icon"].isVisible();
|
|
|
|
// status
|
|
this.columns["status"].updateTd = function(td, row) {
|
|
const state = this.getRowValue(row);
|
|
if (!state)
|
|
return;
|
|
|
|
let status;
|
|
switch (state) {
|
|
case "downloading":
|
|
status = "QBT_TR(Downloading)QBT_TR[CONTEXT=TransferListDelegate]";
|
|
break;
|
|
case "stalledDL":
|
|
status = "QBT_TR(Stalled)QBT_TR[CONTEXT=TransferListDelegate]";
|
|
break;
|
|
case "metaDL":
|
|
status = "QBT_TR(Downloading metadata)QBT_TR[CONTEXT=TransferListDelegate]";
|
|
break;
|
|
case "forcedMetaDL":
|
|
status = "QBT_TR([F] Downloading metadata)QBT_TR[CONTEXT=TransferListDelegate]";
|
|
break;
|
|
case "forcedDL":
|
|
status = "QBT_TR([F] Downloading)QBT_TR[CONTEXT=TransferListDelegate]";
|
|
break;
|
|
case "uploading":
|
|
case "stalledUP":
|
|
status = "QBT_TR(Seeding)QBT_TR[CONTEXT=TransferListDelegate]";
|
|
break;
|
|
case "forcedUP":
|
|
status = "QBT_TR([F] Seeding)QBT_TR[CONTEXT=TransferListDelegate]";
|
|
break;
|
|
case "queuedDL":
|
|
case "queuedUP":
|
|
status = "QBT_TR(Queued)QBT_TR[CONTEXT=TransferListDelegate]";
|
|
break;
|
|
case "checkingDL":
|
|
case "checkingUP":
|
|
status = "QBT_TR(Checking)QBT_TR[CONTEXT=TransferListDelegate]";
|
|
break;
|
|
case "queuedForChecking":
|
|
status = "QBT_TR(Queued for checking)QBT_TR[CONTEXT=TransferListDelegate]";
|
|
break;
|
|
case "checkingResumeData":
|
|
status = "QBT_TR(Checking resume data)QBT_TR[CONTEXT=TransferListDelegate]";
|
|
break;
|
|
case "stoppedDL":
|
|
status = "QBT_TR(Stopped)QBT_TR[CONTEXT=TransferListDelegate]";
|
|
break;
|
|
case "stoppedUP":
|
|
status = "QBT_TR(Completed)QBT_TR[CONTEXT=TransferListDelegate]";
|
|
break;
|
|
case "moving":
|
|
status = "QBT_TR(Moving)QBT_TR[CONTEXT=TransferListDelegate]";
|
|
break;
|
|
case "missingFiles":
|
|
status = "QBT_TR(Missing Files)QBT_TR[CONTEXT=TransferListDelegate]";
|
|
break;
|
|
case "error":
|
|
status = "QBT_TR(Errored)QBT_TR[CONTEXT=TransferListDelegate]";
|
|
break;
|
|
default:
|
|
status = "QBT_TR(Unknown)QBT_TR[CONTEXT=HttpServer]";
|
|
}
|
|
|
|
td.textContent = status;
|
|
td.title = status;
|
|
};
|
|
|
|
this.columns["status"].compareRows = (row1, row2) => {
|
|
return compareNumbers(row1.full_data._statusOrder, row2.full_data._statusOrder);
|
|
};
|
|
|
|
// priority
|
|
this.columns["priority"].updateTd = function(td, row) {
|
|
const queuePos = this.getRowValue(row);
|
|
const formattedQueuePos = (queuePos < 1) ? "*" : queuePos;
|
|
td.textContent = formattedQueuePos;
|
|
td.title = formattedQueuePos;
|
|
};
|
|
|
|
this.columns["priority"].compareRows = function(row1, row2) {
|
|
let row1_val = this.getRowValue(row1);
|
|
let row2_val = this.getRowValue(row2);
|
|
if (row1_val < 1)
|
|
row1_val = 1000000;
|
|
if (row2_val < 1)
|
|
row2_val = 1000000;
|
|
return compareNumbers(row1_val, row2_val);
|
|
};
|
|
|
|
// name, category, tags
|
|
this.columns["name"].compareRows = function(row1, row2) {
|
|
const row1Val = this.getRowValue(row1);
|
|
const row2Val = this.getRowValue(row2);
|
|
return row1Val.localeCompare(row2Val, undefined, { numeric: true, sensitivity: "base" });
|
|
};
|
|
this.columns["category"].compareRows = this.columns["name"].compareRows;
|
|
this.columns["tags"].compareRows = this.columns["name"].compareRows;
|
|
|
|
// size, total_size
|
|
this.columns["size"].updateTd = function(td, row) {
|
|
const size = window.qBittorrent.Misc.friendlyUnit(this.getRowValue(row), false);
|
|
td.textContent = size;
|
|
td.title = size;
|
|
};
|
|
this.columns["total_size"].updateTd = this.columns["size"].updateTd;
|
|
|
|
// progress
|
|
this.columns["progress"].updateTd = function(td, row) {
|
|
const progress = this.getRowValue(row);
|
|
const progressFormatted = window.qBittorrent.Misc.toFixedPointString((progress * 100), 1);
|
|
|
|
const div = td.firstElementChild;
|
|
if (div !== null) {
|
|
if (div.getValue() !== progressFormatted)
|
|
div.setValue(progressFormatted);
|
|
}
|
|
else {
|
|
td.append(new window.qBittorrent.ProgressBar.ProgressBar(progressFormatted));
|
|
}
|
|
};
|
|
this.columns["progress"].staticWidth = 100;
|
|
|
|
// num_seeds
|
|
this.columns["num_seeds"].updateTd = function(td, row) {
|
|
const num_seeds = this.getRowValue(row, 0);
|
|
const num_complete = this.getRowValue(row, 1);
|
|
let value = num_seeds;
|
|
if (num_complete !== -1)
|
|
value += ` (${num_complete})`;
|
|
td.textContent = value;
|
|
td.title = value;
|
|
};
|
|
this.columns["num_seeds"].compareRows = function(row1, row2) {
|
|
const num_seeds1 = this.getRowValue(row1, 0);
|
|
const num_complete1 = this.getRowValue(row1, 1);
|
|
|
|
const num_seeds2 = this.getRowValue(row2, 0);
|
|
const num_complete2 = this.getRowValue(row2, 1);
|
|
|
|
const result = compareNumbers(num_complete1, num_complete2);
|
|
if (result !== 0)
|
|
return result;
|
|
return compareNumbers(num_seeds1, num_seeds2);
|
|
};
|
|
|
|
// num_leechs
|
|
this.columns["num_leechs"].updateTd = this.columns["num_seeds"].updateTd;
|
|
this.columns["num_leechs"].compareRows = this.columns["num_seeds"].compareRows;
|
|
|
|
// dlspeed
|
|
this.columns["dlspeed"].updateTd = function(td, row) {
|
|
const speed = window.qBittorrent.Misc.friendlyUnit(this.getRowValue(row), true);
|
|
td.textContent = speed;
|
|
td.title = speed;
|
|
};
|
|
|
|
// upspeed
|
|
this.columns["upspeed"].updateTd = this.columns["dlspeed"].updateTd;
|
|
|
|
// eta
|
|
this.columns["eta"].updateTd = function(td, row) {
|
|
const eta = window.qBittorrent.Misc.friendlyDuration(this.getRowValue(row), window.qBittorrent.Misc.MAX_ETA);
|
|
td.textContent = eta;
|
|
td.title = eta;
|
|
};
|
|
|
|
// ratio
|
|
this.columns["ratio"].updateTd = function(td, row) {
|
|
const ratio = this.getRowValue(row);
|
|
const string = (ratio === -1) ? "∞" : window.qBittorrent.Misc.toFixedPointString(ratio, 2);
|
|
td.textContent = string;
|
|
td.title = string;
|
|
};
|
|
|
|
// popularity
|
|
this.columns["popularity"].updateTd = function(td, row) {
|
|
const value = this.getRowValue(row);
|
|
const popularity = (value === -1) ? "∞" : window.qBittorrent.Misc.toFixedPointString(value, 2);
|
|
td.textContent = popularity;
|
|
td.title = popularity;
|
|
};
|
|
|
|
// added on
|
|
this.columns["added_on"].updateTd = function(td, row) {
|
|
const date = new Date(this.getRowValue(row) * 1000).toLocaleString();
|
|
td.textContent = date;
|
|
td.title = date;
|
|
};
|
|
|
|
// completion_on
|
|
this.columns["completion_on"].updateTd = function(td, row) {
|
|
const val = this.getRowValue(row);
|
|
if ((val === 0xffffffff) || (val < 0)) {
|
|
td.textContent = "";
|
|
td.title = "";
|
|
}
|
|
else {
|
|
const date = new Date(this.getRowValue(row) * 1000).toLocaleString();
|
|
td.textContent = date;
|
|
td.title = date;
|
|
}
|
|
};
|
|
|
|
// tracker
|
|
this.columns["tracker"].updateTd = function(td, row) {
|
|
const value = this.getRowValue(row);
|
|
const tracker = displayFullURLTrackerColumn ? value : window.qBittorrent.Misc.getHost(value);
|
|
td.textContent = tracker;
|
|
td.title = value;
|
|
};
|
|
|
|
// dl_limit, up_limit
|
|
this.columns["dl_limit"].updateTd = function(td, row) {
|
|
const speed = this.getRowValue(row);
|
|
if (speed === 0) {
|
|
td.textContent = "∞";
|
|
td.title = "∞";
|
|
}
|
|
else {
|
|
const formattedSpeed = window.qBittorrent.Misc.friendlyUnit(speed, true);
|
|
td.textContent = formattedSpeed;
|
|
td.title = formattedSpeed;
|
|
}
|
|
};
|
|
|
|
this.columns["up_limit"].updateTd = this.columns["dl_limit"].updateTd;
|
|
|
|
// downloaded, uploaded, downloaded_session, uploaded_session, amount_left
|
|
this.columns["downloaded"].updateTd = this.columns["size"].updateTd;
|
|
this.columns["uploaded"].updateTd = this.columns["size"].updateTd;
|
|
this.columns["downloaded_session"].updateTd = this.columns["size"].updateTd;
|
|
this.columns["uploaded_session"].updateTd = this.columns["size"].updateTd;
|
|
this.columns["amount_left"].updateTd = this.columns["size"].updateTd;
|
|
|
|
// time active
|
|
this.columns["time_active"].updateTd = function(td, row) {
|
|
const activeTime = this.getRowValue(row, 0);
|
|
const seedingTime = this.getRowValue(row, 1);
|
|
const time = (seedingTime > 0)
|
|
? ("QBT_TR(%1 (seeded for %2))QBT_TR[CONTEXT=TransferListDelegate]"
|
|
.replace("%1", window.qBittorrent.Misc.friendlyDuration(activeTime))
|
|
.replace("%2", window.qBittorrent.Misc.friendlyDuration(seedingTime)))
|
|
: window.qBittorrent.Misc.friendlyDuration(activeTime);
|
|
td.textContent = time;
|
|
td.title = time;
|
|
};
|
|
|
|
// completed
|
|
this.columns["completed"].updateTd = this.columns["size"].updateTd;
|
|
|
|
// max_ratio
|
|
this.columns["max_ratio"].updateTd = this.columns["ratio"].updateTd;
|
|
|
|
// seen_complete
|
|
this.columns["seen_complete"].updateTd = this.columns["completion_on"].updateTd;
|
|
|
|
// last_activity
|
|
this.columns["last_activity"].updateTd = function(td, row) {
|
|
const val = this.getRowValue(row);
|
|
if (val < 1) {
|
|
td.textContent = "∞";
|
|
td.title = "∞";
|
|
}
|
|
else {
|
|
const formattedVal = "QBT_TR(%1 ago)QBT_TR[CONTEXT=TransferListDelegate]".replace("%1", window.qBittorrent.Misc.friendlyDuration((Date.now() / 1000) - val));
|
|
td.textContent = formattedVal;
|
|
td.title = formattedVal;
|
|
}
|
|
};
|
|
|
|
// availability
|
|
this.columns["availability"].updateTd = function(td, row) {
|
|
const value = window.qBittorrent.Misc.toFixedPointString(this.getRowValue(row), 3);
|
|
td.textContent = value;
|
|
td.title = value;
|
|
};
|
|
|
|
// infohash_v1
|
|
this.columns["infohash_v1"].updateTd = function(td, row) {
|
|
const sourceInfohashV1 = this.getRowValue(row);
|
|
const infohashV1 = (sourceInfohashV1 !== "") ? sourceInfohashV1 : "QBT_TR(N/A)QBT_TR[CONTEXT=TransferListDelegate]";
|
|
td.textContent = infohashV1;
|
|
td.title = infohashV1;
|
|
};
|
|
|
|
// infohash_v2
|
|
this.columns["infohash_v2"].updateTd = function(td, row) {
|
|
const sourceInfohashV2 = this.getRowValue(row);
|
|
const infohashV2 = (sourceInfohashV2 !== "") ? sourceInfohashV2 : "QBT_TR(N/A)QBT_TR[CONTEXT=TransferListDelegate]";
|
|
td.textContent = infohashV2;
|
|
td.title = infohashV2;
|
|
};
|
|
|
|
// reannounce
|
|
this.columns["reannounce"].updateTd = function(td, row) {
|
|
const time = window.qBittorrent.Misc.friendlyDuration(this.getRowValue(row));
|
|
td.textContent = time;
|
|
td.title = time;
|
|
};
|
|
|
|
// private
|
|
this.columns["private"].updateTd = function(td, row) {
|
|
const hasMetadata = row["full_data"].has_metadata;
|
|
const isPrivate = this.getRowValue(row);
|
|
const string = hasMetadata
|
|
? (isPrivate
|
|
? "QBT_TR(Yes)QBT_TR[CONTEXT=PropertiesWidget]"
|
|
: "QBT_TR(No)QBT_TR[CONTEXT=PropertiesWidget]")
|
|
: "QBT_TR(N/A)QBT_TR[CONTEXT=PropertiesWidget]";
|
|
td.textContent = string;
|
|
td.title = string;
|
|
};
|
|
}
|
|
|
|
applyFilter(row, filterName, category, tag, trackerHost, filterTerms) {
|
|
const { state, upspeed, dlspeed } = row["full_data"];
|
|
let inactive = false;
|
|
|
|
switch (filterName) {
|
|
case "downloading":
|
|
if ((state !== "downloading") && !state.includes("DL"))
|
|
return false;
|
|
break;
|
|
case "seeding":
|
|
if ((state !== "uploading") && (state !== "forcedUP") && (state !== "stalledUP") && (state !== "queuedUP") && (state !== "checkingUP"))
|
|
return false;
|
|
break;
|
|
case "completed":
|
|
if ((state !== "uploading") && !state.includes("UP"))
|
|
return false;
|
|
break;
|
|
case "stopped":
|
|
if (!state.includes("stopped"))
|
|
return false;
|
|
break;
|
|
case "running":
|
|
if (state.includes("stopped"))
|
|
return false;
|
|
break;
|
|
case "stalled":
|
|
if ((state !== "stalledUP") && (state !== "stalledDL"))
|
|
return false;
|
|
break;
|
|
case "stalled_uploading":
|
|
if (state !== "stalledUP")
|
|
return false;
|
|
break;
|
|
case "stalled_downloading":
|
|
if (state !== "stalledDL")
|
|
return false;
|
|
break;
|
|
case "inactive":
|
|
inactive = true;
|
|
// fallthrough
|
|
case "active": {
|
|
const r = (upspeed > 0) || (dlspeed > 0);
|
|
if (r === inactive)
|
|
return false;
|
|
break;
|
|
}
|
|
case "checking":
|
|
if ((state !== "checkingUP") && (state !== "checkingDL") && (state !== "checkingResumeData"))
|
|
return false;
|
|
break;
|
|
case "moving":
|
|
if (state !== "moving")
|
|
return false;
|
|
break;
|
|
case "errored":
|
|
if ((state !== "error") && (state !== "unknown") && (state !== "missingFiles"))
|
|
return false;
|
|
break;
|
|
}
|
|
|
|
switch (category) {
|
|
case CATEGORIES_ALL:
|
|
break; // do nothing
|
|
|
|
case CATEGORIES_UNCATEGORIZED:
|
|
if (row["full_data"].category.length > 0)
|
|
return false;
|
|
break; // do nothing
|
|
|
|
default:
|
|
if (!useSubcategories) {
|
|
if (category !== row["full_data"].category)
|
|
return false;
|
|
}
|
|
else {
|
|
const selectedCategory = window.qBittorrent.Client.categoryMap.get(category);
|
|
if (selectedCategory !== undefined) {
|
|
const selectedCategoryName = `${category}/`;
|
|
const torrentCategoryName = `${row["full_data"].category}/`;
|
|
if (!torrentCategoryName.startsWith(selectedCategoryName))
|
|
return false;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
switch (tag) {
|
|
case TAGS_ALL:
|
|
break; // do nothing
|
|
|
|
case TAGS_UNTAGGED:
|
|
if (row["full_data"].tags.length > 0)
|
|
return false;
|
|
break; // do nothing
|
|
|
|
default: {
|
|
const tags = row["full_data"].tags.split(", ");
|
|
if (!tags.contains(tag))
|
|
return false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
switch (trackerHost) {
|
|
case TRACKERS_ALL:
|
|
break; // do nothing
|
|
|
|
case TRACKERS_ANNOUNCE_ERROR:
|
|
if (!row["full_data"]["has_other_announce_error"])
|
|
return false;
|
|
break;
|
|
|
|
case TRACKERS_ERROR:
|
|
if (!row["full_data"]["has_tracker_error"])
|
|
return false;
|
|
break;
|
|
|
|
case TRACKERS_TRACKERLESS:
|
|
if (row["full_data"].trackers_count > 0)
|
|
return false;
|
|
break;
|
|
|
|
case TRACKERS_WARNING:
|
|
if (!row["full_data"]["has_tracker_warning"])
|
|
return false;
|
|
break;
|
|
|
|
default: {
|
|
const trackerTorrentMap = trackerMap.get(trackerHost);
|
|
if (trackerTorrentMap !== undefined) {
|
|
let found = false;
|
|
for (const torrents of trackerTorrentMap.values()) {
|
|
if (torrents.has(row["full_data"].rowId)) {
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!found)
|
|
return false;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ((filterTerms !== undefined) && (filterTerms !== null)) {
|
|
const filterBy = document.getElementById("torrentsFilterSelect").value;
|
|
const textToSearch = row["full_data"][filterBy].toLowerCase();
|
|
if (filterTerms instanceof RegExp) {
|
|
if (!filterTerms.test(textToSearch))
|
|
return false;
|
|
}
|
|
else {
|
|
if ((filterTerms.length > 0) && !window.qBittorrent.Misc.containsAllTerms(textToSearch, filterTerms))
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
getFilteredTorrentsNumber(filterName, category, tag, tracker) {
|
|
let cnt = 0;
|
|
|
|
for (const row of this.rows.values()) {
|
|
if (this.applyFilter(row, filterName, category, tag, tracker, null))
|
|
++cnt;
|
|
}
|
|
return cnt;
|
|
}
|
|
|
|
getFilteredTorrentsHashes(filterName, category, tag, tracker) {
|
|
const rowsHashes = [];
|
|
const useRegex = document.getElementById("torrentsFilterRegexBox").checked;
|
|
const filterText = document.getElementById("torrentsFilterInput").value.trim().toLowerCase();
|
|
let filterTerms;
|
|
try {
|
|
filterTerms = (filterText.length > 0)
|
|
? (useRegex ? new RegExp(filterText) : filterText.split(" "))
|
|
: null;
|
|
}
|
|
catch (e) { // SyntaxError: Invalid regex pattern
|
|
return filteredRows;
|
|
}
|
|
|
|
for (const row of this.rows.values()) {
|
|
if (this.applyFilter(row, filterName, category, tag, tracker, filterTerms))
|
|
rowsHashes.push(row["rowId"]);
|
|
}
|
|
|
|
return rowsHashes;
|
|
}
|
|
|
|
getFilteredAndSortedRows() {
|
|
const filteredRows = [];
|
|
|
|
const useRegex = document.getElementById("torrentsFilterRegexBox").checked;
|
|
const filterText = document.getElementById("torrentsFilterInput").value.trim().toLowerCase();
|
|
let filterTerms;
|
|
try {
|
|
filterTerms = (filterText.length > 0)
|
|
? (useRegex ? new RegExp(filterText) : filterText.split(" "))
|
|
: null;
|
|
}
|
|
catch (e) { // SyntaxError: Invalid regex pattern
|
|
return filteredRows;
|
|
}
|
|
|
|
for (const row of this.rows.values()) {
|
|
if (this.applyFilter(row, selectedStatus, selectedCategory, selectedTag, selectedTracker, filterTerms)) {
|
|
filteredRows.push(row);
|
|
filteredRows[row.rowId] = row;
|
|
}
|
|
}
|
|
|
|
const column = this.columns[this.sortedColumn];
|
|
const isReverseSort = (this.reverseSort === "0");
|
|
filteredRows.sort((row1, row2) => {
|
|
const result = column.compareRows(row1, row2);
|
|
return isReverseSort ? result : -result;
|
|
});
|
|
return filteredRows;
|
|
}
|
|
|
|
setupCommonEvents() {
|
|
super.setupCommonEvents();
|
|
this.dynamicTableDiv.addEventListener("dblclick", (e) => {
|
|
const tr = e.target.closest("tr");
|
|
if (!tr)
|
|
return;
|
|
|
|
this.deselectAll();
|
|
this.selectRow(tr.rowId);
|
|
const row = this.getRow(tr.rowId);
|
|
const state = row["full_data"].state;
|
|
|
|
const prefKey =
|
|
(state !== "uploading")
|
|
&& (state !== "stoppedUP")
|
|
&& (state !== "forcedUP")
|
|
&& (state !== "stalledUP")
|
|
&& (state !== "queuedUP")
|
|
&& (state !== "checkingUP")
|
|
? "dblclick_download"
|
|
: "dblclick_complete";
|
|
|
|
if (localPreferences.get(prefKey, "1") !== "1")
|
|
return true;
|
|
|
|
if (state.includes("stopped"))
|
|
startFN();
|
|
else
|
|
stopFN();
|
|
});
|
|
}
|
|
|
|
getCurrentTorrentID() {
|
|
return this.getSelectedRowId();
|
|
}
|
|
|
|
onSelectedRowChanged() {
|
|
updatePropertiesPanel();
|
|
}
|
|
|
|
isStopped(hash) {
|
|
const row = this.getRow(hash);
|
|
return (row === undefined) ? true : row.full_data.state.includes("stopped");
|
|
}
|
|
}
|
|
|
|
class TorrentPeersTable extends DynamicTable {
|
|
initColumns() {
|
|
this.newColumn("country", "", "QBT_TR(Country/Region)QBT_TR[CONTEXT=PeerListWidget]", 22, true);
|
|
this.newColumn("ip", "", "QBT_TR(IP/Address)QBT_TR[CONTEXT=PeerListWidget]", 80, true);
|
|
this.newColumn("port", "", "QBT_TR(Port)QBT_TR[CONTEXT=PeerListWidget]", 35, true);
|
|
this.newColumn("connection", "", "QBT_TR(Connection)QBT_TR[CONTEXT=PeerListWidget]", 50, true);
|
|
this.newColumn("flags", "", "QBT_TR(Flags)QBT_TR[CONTEXT=PeerListWidget]", 50, true);
|
|
this.newColumn("client", "", "QBT_TR(Client)QBT_TR[CONTEXT=PeerListWidget]", 140, true);
|
|
this.newColumn("peer_id_client", "", "QBT_TR(Peer ID Client)QBT_TR[CONTEXT=PeerListWidget]", 60, false);
|
|
this.newColumn("progress", "", "QBT_TR(Progress)QBT_TR[CONTEXT=PeerListWidget]", 50, true);
|
|
this.newColumn("dl_speed", "", "QBT_TR(Down Speed)QBT_TR[CONTEXT=PeerListWidget]", 50, true);
|
|
this.newColumn("up_speed", "", "QBT_TR(Up Speed)QBT_TR[CONTEXT=PeerListWidget]", 50, true);
|
|
this.newColumn("downloaded", "", "QBT_TR(Downloaded)QBT_TR[CONTEXT=PeerListWidget]", 50, true);
|
|
this.newColumn("uploaded", "", "QBT_TR(Uploaded)QBT_TR[CONTEXT=PeerListWidget]", 50, true);
|
|
this.newColumn("relevance", "", "QBT_TR(Relevance)QBT_TR[CONTEXT=PeerListWidget]", 30, true);
|
|
this.newColumn("files", "", "QBT_TR(Files)QBT_TR[CONTEXT=PeerListWidget]", 100, true);
|
|
|
|
this.columns["country"].dataProperties.push("country_code");
|
|
this.columns["flags"].dataProperties.push("flags_desc");
|
|
this.initColumnsFunctions();
|
|
}
|
|
|
|
initColumnsFunctions() {
|
|
// country
|
|
this.columns["country"].updateTd = function(td, row) {
|
|
const country = this.getRowValue(row, 0);
|
|
const country_code = this.getRowValue(row, 1);
|
|
|
|
let span = td.firstElementChild;
|
|
if (span === null) {
|
|
span = document.createElement("span");
|
|
span.classList.add("flags");
|
|
td.append(span);
|
|
}
|
|
|
|
span.style.backgroundImage = `url('images/flags/${country_code || "xx"}.svg')`;
|
|
span.textContent = country;
|
|
td.title = country;
|
|
};
|
|
|
|
// ip
|
|
this.columns["ip"].compareRows = function(row1, row2) {
|
|
const ip1 = this.getRowValue(row1);
|
|
const ip2 = this.getRowValue(row2);
|
|
|
|
return window.qBittorrent.Misc.naturalSortCollator.compare(ip1, ip2);
|
|
};
|
|
|
|
// flags
|
|
this.columns["flags"].updateTd = function(td, row) {
|
|
td.textContent = this.getRowValue(row, 0);
|
|
td.title = this.getRowValue(row, 1);
|
|
};
|
|
|
|
// progress
|
|
this.columns["progress"].updateTd = function(td, row) {
|
|
const progress = this.getRowValue(row);
|
|
const progressFormatted = `${window.qBittorrent.Misc.toFixedPointString((progress * 100), 1)}%`;
|
|
td.textContent = progressFormatted;
|
|
td.title = progressFormatted;
|
|
};
|
|
|
|
// dl_speed, up_speed
|
|
this.columns["dl_speed"].updateTd = function(td, row) {
|
|
const speed = this.getRowValue(row);
|
|
if (speed === 0) {
|
|
td.textContent = "";
|
|
td.title = "";
|
|
}
|
|
else {
|
|
const formattedSpeed = window.qBittorrent.Misc.friendlyUnit(speed, true);
|
|
td.textContent = formattedSpeed;
|
|
td.title = formattedSpeed;
|
|
}
|
|
};
|
|
this.columns["up_speed"].updateTd = this.columns["dl_speed"].updateTd;
|
|
|
|
// downloaded, uploaded
|
|
this.columns["downloaded"].updateTd = function(td, row) {
|
|
const downloaded = window.qBittorrent.Misc.friendlyUnit(this.getRowValue(row), false);
|
|
td.textContent = downloaded;
|
|
td.title = downloaded;
|
|
};
|
|
this.columns["uploaded"].updateTd = this.columns["downloaded"].updateTd;
|
|
|
|
// relevance
|
|
this.columns["relevance"].updateTd = this.columns["progress"].updateTd;
|
|
this.columns["relevance"].staticWidth = 100;
|
|
|
|
// files
|
|
this.columns["files"].updateTd = function(td, row) {
|
|
const value = this.getRowValue(row, 0);
|
|
td.textContent = value.replace(/\n/g, ";");
|
|
td.title = value;
|
|
};
|
|
|
|
}
|
|
}
|
|
|
|
class SearchResultsTable extends DynamicTable {
|
|
initColumns() {
|
|
this.newColumn("fileName", "", "QBT_TR(Name)QBT_TR[CONTEXT=SearchResultsTable]", 500, true);
|
|
this.newColumn("fileSize", "", "QBT_TR(Size)QBT_TR[CONTEXT=SearchResultsTable]", 100, true);
|
|
this.newColumn("nbSeeders", "", "QBT_TR(Seeders)QBT_TR[CONTEXT=SearchResultsTable]", 100, true);
|
|
this.newColumn("nbLeechers", "", "QBT_TR(Leechers)QBT_TR[CONTEXT=SearchResultsTable]", 100, true);
|
|
this.newColumn("engineName", "", "QBT_TR(Engine)QBT_TR[CONTEXT=SearchResultsTable]", 100, true);
|
|
this.newColumn("siteUrl", "", "QBT_TR(Engine URL)QBT_TR[CONTEXT=SearchResultsTable]", 250, true);
|
|
this.newColumn("pubDate", "", "QBT_TR(Published On)QBT_TR[CONTEXT=SearchResultsTable]", 200, true);
|
|
|
|
this.initColumnsFunctions();
|
|
}
|
|
|
|
initColumnsFunctions() {
|
|
const displaySize = function(td, row) {
|
|
const size = window.qBittorrent.Misc.friendlyUnit(this.getRowValue(row), false);
|
|
td.textContent = size;
|
|
td.title = size;
|
|
};
|
|
const displayNum = function(td, row) {
|
|
const value = this.getRowValue(row);
|
|
const formattedValue = (value === "-1") ? "Unknown" : value;
|
|
td.textContent = formattedValue;
|
|
td.title = formattedValue;
|
|
};
|
|
const displayDate = function(td, row) {
|
|
const value = this.getRowValue(row) * 1000;
|
|
const formattedValue = (Number.isNaN(value) || (value <= 0)) ? "" : (new Date(value).toLocaleString());
|
|
td.textContent = formattedValue;
|
|
td.title = formattedValue;
|
|
};
|
|
|
|
this.columns["fileSize"].updateTd = displaySize;
|
|
this.columns["nbSeeders"].updateTd = displayNum;
|
|
this.columns["nbLeechers"].updateTd = displayNum;
|
|
this.columns["pubDate"].updateTd = displayDate;
|
|
}
|
|
|
|
getFilteredAndSortedRows() {
|
|
const getSizeFilters = () => {
|
|
let minSize = (window.qBittorrent.Search.searchSizeFilter.min > 0) ? (window.qBittorrent.Search.searchSizeFilter.min * Math.pow(1024, window.qBittorrent.Search.searchSizeFilter.minUnit)) : 0;
|
|
let maxSize = (window.qBittorrent.Search.searchSizeFilter.max > 0) ? (window.qBittorrent.Search.searchSizeFilter.max * Math.pow(1024, window.qBittorrent.Search.searchSizeFilter.maxUnit)) : 0;
|
|
|
|
if ((minSize > maxSize) && (maxSize > 0)) {
|
|
const tmp = minSize;
|
|
minSize = maxSize;
|
|
maxSize = tmp;
|
|
}
|
|
|
|
return {
|
|
min: minSize,
|
|
max: maxSize
|
|
};
|
|
};
|
|
|
|
const getSeedsFilters = () => {
|
|
let minSeeds = (window.qBittorrent.Search.searchSeedsFilter.min > 0) ? window.qBittorrent.Search.searchSeedsFilter.min : 0;
|
|
let maxSeeds = (window.qBittorrent.Search.searchSeedsFilter.max > 0) ? window.qBittorrent.Search.searchSeedsFilter.max : 0;
|
|
|
|
if ((minSeeds > maxSeeds) && (maxSeeds > 0)) {
|
|
const tmp = minSeeds;
|
|
minSeeds = maxSeeds;
|
|
maxSeeds = tmp;
|
|
}
|
|
|
|
return {
|
|
min: minSeeds,
|
|
max: maxSeeds
|
|
};
|
|
};
|
|
|
|
let filteredRows = [];
|
|
const searchTerms = window.qBittorrent.Search.searchText.pattern.toLowerCase().split(" ");
|
|
const filterTerms = window.qBittorrent.Search.searchText.filterPattern.toLowerCase().split(" ");
|
|
const sizeFilters = getSizeFilters();
|
|
const seedsFilters = getSeedsFilters();
|
|
const searchInTorrentName = document.getElementById("searchInTorrentName").value === "names";
|
|
|
|
if (searchInTorrentName || (filterTerms.length > 0) || (window.qBittorrent.Search.searchSizeFilter.min > 0) || (window.qBittorrent.Search.searchSizeFilter.max > 0)) {
|
|
for (const row of this.getRowValues()) {
|
|
|
|
if (searchInTorrentName && !window.qBittorrent.Misc.containsAllTerms(row.full_data.fileName, searchTerms))
|
|
continue;
|
|
if ((filterTerms.length > 0) && !window.qBittorrent.Misc.containsAllTerms(row.full_data.fileName, filterTerms))
|
|
continue;
|
|
if ((sizeFilters.min > 0) && (row.full_data.fileSize < sizeFilters.min))
|
|
continue;
|
|
if ((sizeFilters.max > 0) && (row.full_data.fileSize > sizeFilters.max))
|
|
continue;
|
|
if ((seedsFilters.min > 0) && (row.full_data.nbSeeders < seedsFilters.min))
|
|
continue;
|
|
if ((seedsFilters.max > 0) && (row.full_data.nbSeeders > seedsFilters.max))
|
|
continue;
|
|
|
|
filteredRows.push(row);
|
|
}
|
|
}
|
|
else {
|
|
filteredRows = [...this.getRowValues()];
|
|
}
|
|
|
|
const column = this.columns[this.sortedColumn];
|
|
const isReverseSort = (this.reverseSort === "0");
|
|
filteredRows.sort((row1, row2) => {
|
|
const result = column.compareRows(row1, row2);
|
|
return isReverseSort ? result : -result;
|
|
});
|
|
|
|
return filteredRows;
|
|
}
|
|
}
|
|
|
|
class SearchPluginsTable extends DynamicTable {
|
|
initColumns() {
|
|
this.newColumn("fullName", "", "QBT_TR(Name)QBT_TR[CONTEXT=SearchPluginsTable]", 175, true);
|
|
this.newColumn("version", "", "QBT_TR(Version)QBT_TR[CONTEXT=SearchPluginsTable]", 100, true);
|
|
this.newColumn("url", "", "QBT_TR(Url)QBT_TR[CONTEXT=SearchPluginsTable]", 175, true);
|
|
this.newColumn("enabled", "", "QBT_TR(Enabled)QBT_TR[CONTEXT=SearchPluginsTable]", 100, true);
|
|
|
|
this.initColumnsFunctions();
|
|
}
|
|
|
|
initColumnsFunctions() {
|
|
this.columns["enabled"].updateTd = function(td, row) {
|
|
const value = this.getRowValue(row);
|
|
if (value) {
|
|
td.textContent = "QBT_TR(Yes)QBT_TR[CONTEXT=SearchPluginsTable]";
|
|
td.title = "QBT_TR(Yes)QBT_TR[CONTEXT=SearchPluginsTable]";
|
|
td.closest("tr").classList.add("green");
|
|
td.closest("tr").classList.remove("red");
|
|
}
|
|
else {
|
|
td.textContent = "QBT_TR(No)QBT_TR[CONTEXT=SearchPluginsTable]";
|
|
td.title = "QBT_TR(No)QBT_TR[CONTEXT=SearchPluginsTable]";
|
|
td.closest("tr").classList.add("red");
|
|
td.closest("tr").classList.remove("green");
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
class TorrentTrackersTable extends DynamicTable {
|
|
collapseState = new Map(); // { rowId: String, isCollapsed: bool }
|
|
|
|
isTrackerCollapsed(id) {
|
|
return this.collapseState.get(id) ?? true;
|
|
}
|
|
|
|
toggleTrackerCollapsed(id) {
|
|
this.collapseState.set(id, !this.isTrackerCollapsed(id));
|
|
this.#updateTrackerRowState(id, this.isTrackerCollapsed(id));
|
|
}
|
|
|
|
#updateEndpointVisibility(endpoint, shouldHide) {
|
|
const span = document.getElementById(`trackersTableTrackerUrl${endpoint}`);
|
|
// span won't exist if row has been filtered out
|
|
if (span === null)
|
|
return;
|
|
const tr = span.parentElement.parentElement;
|
|
tr.classList.toggle("invisible", shouldHide);
|
|
}
|
|
|
|
#updateTrackerCollapseIcon(tracker, isCollapsed) {
|
|
const span = document.getElementById(`trackersTableTrackerUrl${tracker}`);
|
|
// 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);
|
|
}
|
|
|
|
#updateTrackerRowState(id, shouldCollapse) {
|
|
// collapsed rows will be filtered out when using virtual list
|
|
if (this.useVirtualList)
|
|
return;
|
|
|
|
this.#updateTrackerCollapseIcon(id, shouldCollapse);
|
|
|
|
for (const row of this.getRowValues()) {
|
|
const parentId = row.full_data._tracker;
|
|
if (parentId === id)
|
|
this.#updateEndpointVisibility(row.rowId, shouldCollapse);
|
|
}
|
|
}
|
|
|
|
clearCollapseState() {
|
|
this.collapseState.clear();
|
|
}
|
|
|
|
initColumns() {
|
|
this.newColumn("url", "", "QBT_TR(URL/Announce Endpoint)QBT_TR[CONTEXT=TrackerListWidget]", 250, true);
|
|
this.newColumn("tier", "", "QBT_TR(Tier)QBT_TR[CONTEXT=TrackerListWidget]", 35, true);
|
|
this.newColumn("btVersion", "", "QBT_TR(BT Protocol)QBT_TR[CONTEXT=TrackerListWidget]", 35, true);
|
|
this.newColumn("status", "", "QBT_TR(Status)QBT_TR[CONTEXT=TrackerListWidget]", 125, true);
|
|
this.newColumn("peers", "", "QBT_TR(Peers)QBT_TR[CONTEXT=TrackerListWidget]", 75, true);
|
|
this.newColumn("seeds", "", "QBT_TR(Seeds)QBT_TR[CONTEXT=TrackerListWidget]", 75, true);
|
|
this.newColumn("leeches", "", "QBT_TR(Leeches)QBT_TR[CONTEXT=TrackerListWidget]", 75, true);
|
|
this.newColumn("downloaded", "", "QBT_TR(Times Downloaded)QBT_TR[CONTEXT=TrackerListWidget]", 100, true);
|
|
this.newColumn("message", "", "QBT_TR(Message)QBT_TR[CONTEXT=TrackerListWidget]", 250, true);
|
|
this.newColumn("nextAnnounce", "", "QBT_TR(Next Announce)QBT_TR[CONTEXT=TrackerListWidget]", 150, true);
|
|
this.newColumn("minAnnounce", "", "QBT_TR(Min Announce)QBT_TR[CONTEXT=TrackerListWidget]", 150, true);
|
|
|
|
this.initColumnsFunctions();
|
|
}
|
|
|
|
initColumnsFunctions() {
|
|
const naturalSort = function(row1, row2) {
|
|
if (!row1.full_data._sortable || !row2.full_data._sortable)
|
|
return 0;
|
|
|
|
const value1 = this.getRowValue(row1);
|
|
const value2 = this.getRowValue(row2);
|
|
return window.qBittorrent.Misc.naturalSortCollator.compare(value1, value2);
|
|
};
|
|
|
|
this.columns["url"].updateTd = (td, row) => {
|
|
const id = row.rowId;
|
|
const data = row.full_data;
|
|
|
|
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;
|
|
this.toggleTrackerCollapsed(id);
|
|
if (this.useVirtualList)
|
|
this.rerender();
|
|
});
|
|
td.append(collapseIcon);
|
|
}
|
|
if (data._isTracker) {
|
|
collapseIcon.style.display = "inline";
|
|
collapseIcon.style.visibility = data._hasEndpoints ? "visible" : "hidden";
|
|
collapseIcon.dataset.id = id;
|
|
collapseIcon.classList.toggle("rotate", this.isTrackerCollapsed(id));
|
|
}
|
|
else {
|
|
collapseIcon.style.display = "none";
|
|
}
|
|
|
|
let span = td.children[1];
|
|
if (span === undefined) {
|
|
span = document.createElement("span");
|
|
td.append(span);
|
|
}
|
|
span.id = `trackersTableTrackerUrl${id}`;
|
|
span.textContent = data.url;
|
|
span.style.marginLeft = data._isTracker ? "0" : "20px";
|
|
};
|
|
|
|
this.columns["url"].compareRows = naturalSort;
|
|
this.columns["status"].compareRows = naturalSort;
|
|
this.columns["message"].compareRows = naturalSort;
|
|
|
|
const sortNumbers = function(row1, row2) {
|
|
if (!row1.full_data._sortable || !row2.full_data._sortable)
|
|
return 0;
|
|
|
|
const value1 = this.getRowValue(row1);
|
|
const value2 = this.getRowValue(row2);
|
|
if (value1 === "")
|
|
return -1;
|
|
if (value2 === "")
|
|
return 1;
|
|
return compareNumbers(value1, value2);
|
|
};
|
|
|
|
this.columns["tier"].compareRows = sortNumbers;
|
|
|
|
const sortMixed = function(row1, row2) {
|
|
if (!row1.full_data._sortable || !row2.full_data._sortable)
|
|
return 0;
|
|
|
|
const value1 = this.getRowValue(row1);
|
|
const value2 = this.getRowValue(row2);
|
|
if (value1 === "QBT_TR(N/A)QBT_TR[CONTEXT=TrackerListWidget]")
|
|
return -1;
|
|
if (value2 === "QBT_TR(N/A)QBT_TR[CONTEXT=TrackerListWidget]")
|
|
return 1;
|
|
return compareNumbers(value1, value2);
|
|
};
|
|
|
|
this.columns["peers"].compareRows = sortMixed;
|
|
this.columns["seeds"].compareRows = sortMixed;
|
|
this.columns["leeches"].compareRows = sortMixed;
|
|
this.columns["downloaded"].compareRows = sortMixed;
|
|
|
|
this.columns["status"].updateTd = function(td, row) {
|
|
let statusClass = "trackerUnknown";
|
|
const status = this.getRowValue(row);
|
|
switch (status) {
|
|
case "QBT_TR(Disabled)QBT_TR[CONTEXT=TrackerListWidget]":
|
|
statusClass = "trackerDisabled";
|
|
break;
|
|
case "QBT_TR(Not contacted yet)QBT_TR[CONTEXT=TrackerListWidget]":
|
|
statusClass = "trackerNotContacted";
|
|
break;
|
|
case "QBT_TR(Working)QBT_TR[CONTEXT=TrackerListWidget]":
|
|
statusClass = "trackerWorking";
|
|
break;
|
|
case "QBT_TR(Updating...)QBT_TR[CONTEXT=TrackerListWidget]":
|
|
statusClass = "trackerUpdating";
|
|
break;
|
|
case "QBT_TR(Not working)QBT_TR[CONTEXT=TrackerListWidget]":
|
|
case "QBT_TR(Tracker error)QBT_TR[CONTEXT=TrackerListWidget]":
|
|
case "QBT_TR(Unreachable)QBT_TR[CONTEXT=TrackerListWidget]":
|
|
statusClass = "trackerNotWorking";
|
|
break;
|
|
}
|
|
|
|
for (const c of [...td.classList]) {
|
|
if (c.startsWith("tracker"))
|
|
td.classList.remove(c);
|
|
}
|
|
td.classList.add(statusClass);
|
|
td.textContent = status;
|
|
td.title = status;
|
|
};
|
|
|
|
const friendlyDuration = function(td, row) {
|
|
const value = this.getRowValue(row) ?? 0;
|
|
const seconds = Math.max(value - (Date.now() / 1000), 0);
|
|
const duration = window.qBittorrent.Misc.friendlyDuration(seconds, window.qBittorrent.Misc.MAX_ETA);
|
|
td.textContent = duration;
|
|
td.title = duration;
|
|
};
|
|
|
|
this.columns["nextAnnounce"].updateTd = friendlyDuration;
|
|
this.columns["minAnnounce"].updateTd = friendlyDuration;
|
|
}
|
|
|
|
getFilteredAndSortedRows() {
|
|
const trackers = [];
|
|
const trakcerEndpoints = new Map();
|
|
|
|
for (const row of this.getRowValues()) {
|
|
const tracker = row.full_data._tracker;
|
|
if (tracker) {
|
|
if (this.useVirtualList && this.isTrackerCollapsed(tracker))
|
|
continue;
|
|
const endpoints = trakcerEndpoints.get(tracker);
|
|
if (endpoints === undefined)
|
|
trakcerEndpoints.set(tracker, [row]);
|
|
else
|
|
endpoints.push(row);
|
|
}
|
|
else {
|
|
trackers.push(row);
|
|
}
|
|
}
|
|
|
|
const column = this.columns[this.sortedColumn];
|
|
const isReverseSort = this.reverseSort === "0";
|
|
const sortRows = (row1, row2) => {
|
|
const result = column.compareRows(row1, row2);
|
|
return isReverseSort ? result : -result;
|
|
};
|
|
|
|
const result = [];
|
|
for (const tracker of trackers.sort(sortRows)) {
|
|
result.push(tracker);
|
|
const endpoints = trakcerEndpoints.get(tracker.rowId) || [];
|
|
result.push(...endpoints.sort(sortRows));
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
updateTable(fullUpdate = false) {
|
|
super.updateTable(fullUpdate);
|
|
if (!this.useVirtualList) {
|
|
for (const row of this.getRowValues()) {
|
|
if (row.full_data._isTracker)
|
|
continue;
|
|
this.#updateEndpointVisibility(row.rowId, this.isTrackerCollapsed(row.full_data._tracker));
|
|
}
|
|
}
|
|
}
|
|
|
|
setupCommonEvents() {
|
|
super.setupCommonEvents();
|
|
this.dynamicTableDiv.addEventListener("dblclick", (e) => {
|
|
const tr = e.target.closest("tr");
|
|
if (!tr || (tr.rowId.startsWith("** [") || tr.rowId.startsWith("endpoint|")))
|
|
return;
|
|
|
|
window.qBittorrent.PropTrackers.editTracker(tr);
|
|
});
|
|
}
|
|
}
|
|
|
|
class TorrentFilesTable extends DynamicTable {
|
|
filterTerms = [];
|
|
prevFilterTerms = [];
|
|
prevRowsString = null;
|
|
prevFilteredRows = [];
|
|
prevSortedColumn = null;
|
|
prevReverseSort = null;
|
|
fileTree = new window.qBittorrent.FileTree.FileTree();
|
|
supportCollapsing = true;
|
|
collapseState = new Map();
|
|
fileNameColumn = "name";
|
|
|
|
isCollapsed(id) {
|
|
if (!this.supportCollapsing)
|
|
return false;
|
|
return this.collapseState.get(id)?.collapsed ?? false;
|
|
}
|
|
|
|
expandNode(id) {
|
|
if (!this.supportCollapsing)
|
|
return;
|
|
|
|
const state = this.collapseState.get(id);
|
|
if (state !== undefined)
|
|
state.collapsed = false;
|
|
this.#updateNodeState(id, false);
|
|
}
|
|
|
|
collapseNode(id) {
|
|
if (!this.supportCollapsing)
|
|
return;
|
|
|
|
const state = this.collapseState.get(id);
|
|
if (state !== undefined)
|
|
state.collapsed = true;
|
|
this.#updateNodeState(id, true);
|
|
}
|
|
|
|
expandAllNodes() {
|
|
if (!this.supportCollapsing)
|
|
return;
|
|
|
|
for (const [key, _] of this.collapseState)
|
|
this.expandNode(key);
|
|
}
|
|
|
|
collapseAllNodes() {
|
|
if (!this.supportCollapsing)
|
|
return;
|
|
|
|
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(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);
|
|
|
|
this.#updateNodeChildVisibility(node, shouldCollapse);
|
|
}
|
|
|
|
#updateNodeChildVisibility(root, shouldHide) {
|
|
const stack = [...root.children];
|
|
while (stack.length > 0) {
|
|
const node = stack.pop();
|
|
|
|
this.#updateNodeVisibility(node, (shouldHide ? shouldHide : this.isCollapsed(node.root.rowId)));
|
|
|
|
stack.push(...node.children);
|
|
}
|
|
}
|
|
|
|
clear() {
|
|
super.clear();
|
|
this.collapseState.clear();
|
|
}
|
|
|
|
setupVirtualList() {
|
|
super.setupVirtualList();
|
|
this.rowHeight = 29.5;
|
|
}
|
|
|
|
expandFolder(id) {
|
|
if (!this.supportCollapsing)
|
|
return;
|
|
|
|
const node = this.getNode(id);
|
|
if (node.isFolder)
|
|
this.expandNode(node.rowId);
|
|
}
|
|
|
|
collapseFolder(id) {
|
|
if (!this.supportCollapsing)
|
|
return;
|
|
|
|
const node = this.getNode(id);
|
|
if (node.isFolder)
|
|
this.collapseNode(node.rowId);
|
|
}
|
|
|
|
isAllCheckboxesChecked() {
|
|
return this.fileTree.toArray().every((node) => node.checked === window.qBittorrent.FileTree.TriState.Checked);
|
|
}
|
|
|
|
isAllCheckboxesUnchecked() {
|
|
return this.fileTree.toArray().every((node) => node.checked !== window.qBittorrent.FileTree.TriState.Checked);
|
|
}
|
|
|
|
populateTable(root) {
|
|
this.fileTree.setRoot(root);
|
|
for (const node of root.children)
|
|
this.#addNodeToTable(node, 0, root);
|
|
}
|
|
|
|
#addNodeToTable(node, depth, parent) {
|
|
node.depth = depth;
|
|
node.parent = parent;
|
|
|
|
if (node.isFolder && this.supportCollapsing && !this.collapseState.has(node.rowId))
|
|
this.collapseState.set(node.rowId, { depth: depth, collapsed: false });
|
|
|
|
this.updateRowData({
|
|
rowId: node.rowId,
|
|
});
|
|
|
|
for (const child of node.children)
|
|
this.#addNodeToTable(child, depth + 1, node);
|
|
}
|
|
|
|
getFileTreeArray() {
|
|
return this.fileTree.toArray();
|
|
}
|
|
|
|
getRoot() {
|
|
return this.fileTree.getRoot();
|
|
}
|
|
|
|
getNode(rowId) {
|
|
return this.fileTree.getNode(rowId);
|
|
}
|
|
|
|
getRow(node) {
|
|
const rowId = this.fileTree.getRowId(node).toString();
|
|
return this.rows.get(rowId);
|
|
}
|
|
|
|
getRowFileId(rowId) {
|
|
const node = this.getNode(rowId);
|
|
return node.fileId;
|
|
}
|
|
|
|
getRowData(row, fullUpdate) {
|
|
return this.getNode(row.rowId);
|
|
}
|
|
|
|
calculateRemaining() {
|
|
this.fileTree.getRoot().calculateRemaining();
|
|
}
|
|
|
|
initColumns() {
|
|
this.newColumn("checked", "", "", 50, true);
|
|
this.newColumn("name", "", "QBT_TR(Name)QBT_TR[CONTEXT=TrackerListWidget]", 300, true);
|
|
this.newColumn("size", "", "QBT_TR(Total Size)QBT_TR[CONTEXT=TrackerListWidget]", 75, true);
|
|
this.newColumn("progress", "", "QBT_TR(Progress)QBT_TR[CONTEXT=TrackerListWidget]", 100, true);
|
|
this.newColumn("priority", "", "QBT_TR(Download Priority)QBT_TR[CONTEXT=TrackerListWidget]", 150, true);
|
|
this.newColumn("remaining", "", "QBT_TR(Remaining)QBT_TR[CONTEXT=TrackerListWidget]", 75, true);
|
|
this.newColumn("availability", "", "QBT_TR(Availability)QBT_TR[CONTEXT=TrackerListWidget]", 75, true);
|
|
|
|
this.initColumnsFunctions();
|
|
}
|
|
|
|
initColumnsFunctions() {
|
|
const that = this;
|
|
const displaySize = function(td, row) {
|
|
const size = window.qBittorrent.Misc.friendlyUnit(this.getRowValue(row), false);
|
|
td.textContent = size;
|
|
td.title = size;
|
|
};
|
|
const displayPercentage = function(td, row) {
|
|
const value = window.qBittorrent.Misc.friendlyPercentage(this.getRowValue(row));
|
|
td.textContent = value;
|
|
td.title = value;
|
|
};
|
|
|
|
// checked
|
|
this.columns["checked"].updateTd = function(td, row) {
|
|
const id = row.rowId;
|
|
const value = this.getRowValue(row);
|
|
const fileId = that.getRowFileId(id);
|
|
|
|
if (td.firstElementChild === null) {
|
|
const treeImg = document.createElement("img");
|
|
treeImg.src = "images/L.svg";
|
|
treeImg.style.marginBottom = "-2px";
|
|
td.append(treeImg);
|
|
}
|
|
|
|
const downloadCheckbox = td.children[1];
|
|
if (downloadCheckbox === undefined)
|
|
td.append(window.qBittorrent.TorrentContent.createDownloadCheckbox(id, fileId, value));
|
|
else
|
|
window.qBittorrent.TorrentContent.updateDownloadCheckbox(downloadCheckbox, id, fileId, value);
|
|
|
|
};
|
|
this.columns["checked"].staticWidth = 50;
|
|
|
|
// name
|
|
this.columns["name"].updateTd = function(td, row) {
|
|
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) {
|
|
collapseIcon.style.marginLeft = `${node.depth * 20}px`;
|
|
collapseIcon.style.display = "inline";
|
|
collapseIcon.dataset.id = id;
|
|
collapseIcon.classList.toggle("rotate", that.isCollapsed(node.rowId));
|
|
}
|
|
else {
|
|
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);
|
|
// folders add 20px for folder icon and 15px for collapse icon
|
|
const folderBuffer = node.isFolder ? 35 : 0;
|
|
return (node.depth * 20) + folderBuffer;
|
|
};
|
|
|
|
// size
|
|
this.columns["size"].updateTd = displaySize;
|
|
|
|
// progress
|
|
if (this.columns["progress"] !== undefined) {
|
|
this.columns["progress"].updateTd = function(td, row) {
|
|
const value = Number(this.getRowValue(row));
|
|
|
|
const progressBar = td.firstElementChild;
|
|
if (progressBar === null)
|
|
td.append(new window.qBittorrent.ProgressBar.ProgressBar(value));
|
|
else
|
|
progressBar.setValue(value);
|
|
};
|
|
this.columns["progress"].staticWidth = 100;
|
|
}
|
|
|
|
// priority
|
|
this.columns["priority"].updateTd = function(td, row) {
|
|
const id = row.rowId;
|
|
const value = this.getRowValue(row);
|
|
const fileId = that.getRowFileId(id);
|
|
|
|
const priorityCombo = td.firstElementChild;
|
|
if (priorityCombo === null)
|
|
td.append(window.qBittorrent.TorrentContent.createPriorityCombo(id, fileId, value));
|
|
else
|
|
window.qBittorrent.TorrentContent.updatePriorityCombo(priorityCombo, id, fileId, value);
|
|
};
|
|
this.columns["priority"].staticWidth = 140;
|
|
|
|
// remaining, availability
|
|
if (this.columns["remaining"] !== undefined)
|
|
this.columns["remaining"].updateTd = displaySize;
|
|
if (this.columns["availability"] !== undefined)
|
|
this.columns["availability"].updateTd = displayPercentage;
|
|
|
|
for (const column of this.columns) {
|
|
column["getRowValue"] = function(row, pos = 0) {
|
|
const node = that.getNode(row.rowId);
|
|
return node[this.dataProperties[pos]];
|
|
};
|
|
}
|
|
}
|
|
|
|
#sortNodesByColumn(root, column) {
|
|
const isColumnName = (column.name === this.fileNameColumn);
|
|
const isReverseSort = (this.reverseSort === "0");
|
|
|
|
const stack = [root];
|
|
while (stack.length > 0) {
|
|
const node = stack.pop();
|
|
|
|
node.children.sort((node1, node2) => {
|
|
// list folders before files when sorting by name
|
|
if (isColumnName) {
|
|
if (node1.isFolder && !node2.isFolder)
|
|
return -1;
|
|
if (!node1.isFolder && node2.isFolder)
|
|
return 1;
|
|
}
|
|
|
|
const result = column.compareRows(node1, node2);
|
|
return isReverseSort ? result : -result;
|
|
});
|
|
|
|
stack.push(...node.children);
|
|
}
|
|
}
|
|
|
|
#filterNodes(root, filterTerms) {
|
|
const ret = [];
|
|
const stack = [root];
|
|
const visited = [];
|
|
|
|
while (stack.length > 0) {
|
|
const node = stack.at(-1);
|
|
|
|
if (node.isFolder && (!this.useVirtualList || !this.isCollapsed(node.rowId))) {
|
|
const lastVisited = visited.at(-1);
|
|
|
|
if ((visited.length <= 0) || (lastVisited !== node)) {
|
|
visited.push(node);
|
|
stack.push(...node.children);
|
|
continue;
|
|
}
|
|
|
|
// has children added or itself matches
|
|
if (lastVisited.has_children_added || window.qBittorrent.Misc.containsAllTerms(node.name, filterTerms)) {
|
|
ret.push(this.getRow(node));
|
|
delete node.has_children_added;
|
|
|
|
// propagate up
|
|
const parent = node.root;
|
|
if (parent !== undefined)
|
|
parent.has_children_added = true;
|
|
}
|
|
|
|
visited.pop();
|
|
}
|
|
else {
|
|
if (window.qBittorrent.Misc.containsAllTerms(node[this.fileNameColumn], filterTerms)) {
|
|
ret.push(this.getRow(node));
|
|
|
|
const parent = node.root;
|
|
if (parent !== undefined)
|
|
parent.has_children_added = true;
|
|
}
|
|
}
|
|
|
|
stack.pop();
|
|
}
|
|
|
|
ret.reverse();
|
|
return ret;
|
|
}
|
|
|
|
setFilter(text) {
|
|
const filterTerms = text.trim().toLowerCase().split(" ");
|
|
if ((filterTerms.length === 1) && (filterTerms[0] === ""))
|
|
this.filterTerms = [];
|
|
else
|
|
this.filterTerms = filterTerms;
|
|
}
|
|
|
|
generateRowsSignature() {
|
|
const rowsData = [];
|
|
for (const { rowId } of this.getRowValues())
|
|
rowsData.push({ ...this.getNode(rowId).serialize(), collapsed: this.isCollapsed(rowId) });
|
|
return JSON.stringify(rowsData);
|
|
}
|
|
|
|
getFilteredAndSortedRows() {
|
|
const root = this.getRoot();
|
|
if (root === null)
|
|
return [];
|
|
|
|
const hasRowsChanged = function(rowsString, prevRowsStringString) {
|
|
const rowsChanged = (rowsString !== prevRowsStringString);
|
|
const isFilterTermsChanged = this.filterTerms.reduce((acc, term, index) => {
|
|
return (acc || (term !== this.prevFilterTerms[index]));
|
|
}, false);
|
|
const isFilterChanged = ((this.filterTerms.length !== this.prevFilterTerms.length)
|
|
|| ((this.filterTerms.length > 0) && isFilterTermsChanged));
|
|
const isSortedColumnChanged = (this.prevSortedColumn !== this.sortedColumn);
|
|
const isReverseSortChanged = (this.prevReverseSort !== this.reverseSort);
|
|
|
|
return (rowsChanged || isFilterChanged || isSortedColumnChanged || isReverseSortChanged);
|
|
}.bind(this);
|
|
|
|
const rowsString = this.generateRowsSignature();
|
|
if (!hasRowsChanged(rowsString, this.prevRowsString))
|
|
return this.prevFilteredRows;
|
|
|
|
// sort, then filter
|
|
this.#sortNodesByColumn(root, this.columns[this.sortedColumn]);
|
|
const rows = (() => {
|
|
if (this.filterTerms.length === 0) {
|
|
const nodeArray = this.fileTree.toArray();
|
|
const filteredRows = nodeArray.map(node => this.getRow(node));
|
|
return filteredRows;
|
|
}
|
|
|
|
return this.#filterNodes(root.children[0], this.filterTerms);
|
|
})();
|
|
|
|
this.prevFilterTerms = this.filterTerms;
|
|
this.prevRowsString = rowsString;
|
|
this.prevFilteredRows = rows;
|
|
this.prevSortedColumn = this.sortedColumn;
|
|
this.prevReverseSort = this.reverseSort;
|
|
return rows;
|
|
}
|
|
|
|
setupCommonEvents() {
|
|
super.setupCommonEvents();
|
|
this.dynamicTableDiv.addEventListener("keydown", (e) => {
|
|
const tr = e.target.closest("tr");
|
|
if (!tr)
|
|
return;
|
|
|
|
switch (e.key) {
|
|
case "ArrowLeft":
|
|
e.preventDefault();
|
|
this.collapseFolder(this.getSelectedRowId());
|
|
break;
|
|
case "ArrowRight":
|
|
e.preventDefault();
|
|
this.expandFolder(this.getSelectedRowId());
|
|
break;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
class BulkRenameTorrentFilesTable extends TorrentFilesTable {
|
|
prevCheckboxNum = null;
|
|
supportCollapsing = false;
|
|
fileNameColumn = "original";
|
|
|
|
setupVirtualList() {
|
|
super.setupVirtualList();
|
|
this.rowHeight = 29;
|
|
}
|
|
|
|
getSelectedRows() {
|
|
const nodes = this.fileTree.toArray();
|
|
return nodes.filter(x => x.checked === 0);
|
|
}
|
|
|
|
initColumns() {
|
|
// Blocks saving header width (because window width isn't saved)
|
|
localPreferences.remove(`column_checked_width_${this.dynamicTableDivId}`);
|
|
localPreferences.remove(`column_original_width_${this.dynamicTableDivId}`);
|
|
localPreferences.remove(`column_renamed_width_${this.dynamicTableDivId}`);
|
|
this.newColumn("checked", "", "", 50, true);
|
|
this.newColumn("original", "", "QBT_TR(Original)QBT_TR[CONTEXT=TrackerListWidget]", 270, true);
|
|
this.newColumn("renamed", "", "QBT_TR(Renamed)QBT_TR[CONTEXT=TrackerListWidget]", 220, true);
|
|
|
|
this.initColumnsFunctions();
|
|
}
|
|
|
|
/**
|
|
* Toggles the global checkbox and all checkboxes underneath
|
|
*/
|
|
toggleGlobalCheckbox() {
|
|
const checkbox = document.getElementById("rootMultiRename_cb");
|
|
const isChecked = checkbox.checked || checkbox.indeterminate;
|
|
|
|
for (const cb of document.querySelectorAll("input.RenamingCB")) {
|
|
cb.indeterminate = false;
|
|
if (isChecked) {
|
|
cb.checked = true;
|
|
cb.state = "checked";
|
|
}
|
|
else {
|
|
cb.checked = false;
|
|
cb.state = "unchecked";
|
|
}
|
|
}
|
|
|
|
const nodes = this.fileTree.toArray();
|
|
for (const node of nodes)
|
|
node.checked = isChecked ? 0 : 1;
|
|
|
|
this.updateGlobalCheckbox();
|
|
}
|
|
|
|
toggleNodeTreeCheckbox(rowId, checkState) {
|
|
const node = this.getNode(rowId);
|
|
node.checked = checkState;
|
|
const checkbox = document.getElementById(`cbRename${rowId}`);
|
|
checkbox.checked = node.checked === 0;
|
|
checkbox.state = checkbox.checked ? "checked" : "unchecked";
|
|
|
|
for (let i = 0; i < node.children.length; ++i)
|
|
this.toggleNodeTreeCheckbox(node.children[i].rowId, checkState);
|
|
}
|
|
|
|
updateGlobalCheckbox() {
|
|
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;
|
|
checkbox.checked = true;
|
|
}
|
|
else if (isAllUnchecked()) {
|
|
checkbox.state = "unchecked";
|
|
checkbox.indeterminate = false;
|
|
checkbox.checked = false;
|
|
}
|
|
else {
|
|
checkbox.state = "partial";
|
|
checkbox.indeterminate = true;
|
|
checkbox.checked = false;
|
|
}
|
|
}
|
|
|
|
initColumnsFunctions() {
|
|
const that = this;
|
|
|
|
// checked
|
|
this.columns["checked"].updateTd = (td, row) => {
|
|
const id = row.rowId;
|
|
const node = that.getNode(id);
|
|
|
|
if (td.firstElementChild === null) {
|
|
const treeImg = document.createElement("img");
|
|
treeImg.src = "images/L.svg";
|
|
treeImg.style.marginBottom = "-2px";
|
|
td.append(treeImg);
|
|
}
|
|
|
|
let checkbox = td.children[1];
|
|
if (checkbox === undefined) {
|
|
checkbox = document.createElement("input");
|
|
checkbox.type = "checkbox";
|
|
checkbox.className = "RenamingCB";
|
|
checkbox.addEventListener("click", (e) => {
|
|
e.stopPropagation();
|
|
const targetId = e.target.dataset.id;
|
|
const ids = [];
|
|
// when holding shift, set all files between the previously selected one and the clicked one
|
|
if (e.shiftKey && (that.prevCheckboxNum !== null) && (targetId !== that.prevCheckboxNum)) {
|
|
const targetState = that.tableBody.querySelector(`.RenamingCB[data-id="${that.prevCheckboxNum}"]`).checked;
|
|
const checkboxes = that.tableBody.getElementsByClassName("RenamingCB");
|
|
let started = false;
|
|
for (const cb of checkboxes) {
|
|
const currId = cb.dataset.id;
|
|
if ((currId === targetId) || (currId === that.prevCheckboxNum)) {
|
|
if (started) {
|
|
ids.push(currId);
|
|
cb.checked = targetState;
|
|
break;
|
|
}
|
|
started = true;
|
|
}
|
|
if (started) {
|
|
ids.push(currId);
|
|
cb.checked = targetState;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
ids.push(targetId);
|
|
}
|
|
for (const id of ids) {
|
|
const node = that.getNode(id);
|
|
node.checked = e.target.checked ? 0 : 1;
|
|
}
|
|
that.updateGlobalCheckbox();
|
|
that.onRowSelectionChange(that.getNode(targetId));
|
|
that.prevCheckboxNum = targetId;
|
|
});
|
|
checkbox.indeterminate = false;
|
|
td.append(checkbox);
|
|
}
|
|
checkbox.id = `cbRename${id}`;
|
|
checkbox.dataset.id = id;
|
|
checkbox.checked = (node.checked === 0);
|
|
checkbox.state = checkbox.checked ? "checked" : "unchecked";
|
|
};
|
|
this.columns["checked"].staticWidth = 50;
|
|
|
|
// original
|
|
this.columns["original"].updateTd = function(td, row) {
|
|
const id = row.rowId;
|
|
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) {
|
|
dirImg.style.display = "inline";
|
|
dirImg.style.marginLeft = `${node.depth * 20}px`;
|
|
}
|
|
else {
|
|
dirImg.style.display = "none";
|
|
}
|
|
|
|
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 = (td, row) => {
|
|
const id = row.rowId;
|
|
const fileNameRenamedId = `filesTablefileRenamed${id}`;
|
|
const node = that.getNode(id);
|
|
|
|
let span = td.firstElementChild;
|
|
if (span === null) {
|
|
span = document.createElement("span");
|
|
td.append(span);
|
|
}
|
|
span.id = fileNameRenamedId;
|
|
span.textContent = node.renamed;
|
|
};
|
|
|
|
for (const column of this.columns) {
|
|
column["getRowValue"] = function(row, pos = 0) {
|
|
const node = that.getNode(row.rowId);
|
|
return node[this.dataProperties[pos]];
|
|
};
|
|
}
|
|
}
|
|
|
|
onRowSelectionChange(row) {}
|
|
|
|
selectRow() {}
|
|
|
|
reselectRows(rowIds) {
|
|
this.deselectAll();
|
|
for (const tr of this.getTrs()) {
|
|
if (rowIds.includes(tr.rowId)) {
|
|
const node = this.getNode(tr.rowId);
|
|
node.checked = 0;
|
|
|
|
const checkbox = tr.querySelector(".RenamingCB");
|
|
checkbox.state = "checked";
|
|
checkbox.indeterminate = false;
|
|
checkbox.checked = true;
|
|
}
|
|
}
|
|
this.updateGlobalCheckbox();
|
|
}
|
|
|
|
generateRowsSignature() {
|
|
const rowsData = [];
|
|
for (const { full_data } of this.getRowValues())
|
|
rowsData.push(full_data);
|
|
return JSON.stringify(rowsData);
|
|
}
|
|
|
|
setupCommonEvents() {
|
|
const headerDiv = document.getElementById("bulkRenameFilesTableFixedHeaderDiv");
|
|
this.dynamicTableDiv.addEventListener("scroll", (e) => {
|
|
headerDiv.scrollLeft = this.dynamicTableDiv.scrollLeft;
|
|
// rerender on scroll
|
|
if (this.useVirtualList) {
|
|
this.renderedOffset = this.dynamicTableDiv.scrollTop;
|
|
this.rerender();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
class AddTorrentFilesTable extends TorrentFilesTable {
|
|
initColumns() {
|
|
this.newColumn("checked", "", "", 50, true);
|
|
this.newColumn("name", "", "QBT_TR(Name)QBT_TR[CONTEXT=TrackerListWidget]", 190, true);
|
|
this.newColumn("size", "", "QBT_TR(Total Size)QBT_TR[CONTEXT=TrackerListWidget]", 75, true);
|
|
this.newColumn("priority", "", "QBT_TR(Download Priority)QBT_TR[CONTEXT=TrackerListWidget]", 140, true);
|
|
|
|
this.initColumnsFunctions();
|
|
}
|
|
}
|
|
|
|
class RssFeedTable extends DynamicTable {
|
|
initColumns() {
|
|
this.newColumn("state_icon", "", "", 30, true);
|
|
this.newColumn("name", "", "QBT_TR(RSS feeds)QBT_TR[CONTEXT=FeedListWidget]", -1, true);
|
|
|
|
this.columns["state_icon"].dataProperties[0] = "";
|
|
|
|
// map name row to "[name] ([unread])"
|
|
this.columns["name"].dataProperties.push("unread");
|
|
this.columns["name"].updateTd = function(td, row) {
|
|
const name = this.getRowValue(row, 0);
|
|
const unreadCount = this.getRowValue(row, 1);
|
|
const value = `${name} (${unreadCount})`;
|
|
td.textContent = value;
|
|
td.title = value;
|
|
};
|
|
}
|
|
setupHeaderMenu() {}
|
|
setupHeaderEvents() {}
|
|
getFilteredAndSortedRows() {
|
|
return [...this.getRowValues()];
|
|
}
|
|
selectRow(rowId) {
|
|
this.selectedRows.push(rowId);
|
|
this.setRowClass();
|
|
this.onSelectedRowChanged();
|
|
|
|
let path = "";
|
|
for (const row of this.getRowValues()) {
|
|
if (row.rowId === rowId) {
|
|
path = row.full_data.dataPath;
|
|
break;
|
|
}
|
|
}
|
|
window.qBittorrent.Rss.showRssFeed(path);
|
|
}
|
|
setupCommonEvents() {
|
|
super.setupCommonEvents();
|
|
this.dynamicTableDiv.addEventListener("dblclick", (e) => {
|
|
const tr = e.target.closest("tr");
|
|
if (!tr || (tr.rowId === "0"))
|
|
return;
|
|
|
|
window.qBittorrent.Rss.moveItem(this.getRow(tr.rowId).full_data.dataPath);
|
|
});
|
|
}
|
|
updateRow(tr, fullUpdate) {
|
|
const row = this.rows.get(tr.rowId);
|
|
const tds = this.getRowCells(tr);
|
|
|
|
tds[0].style.overflow = "visible";
|
|
const indentation = row.full_data.indentation;
|
|
tds[0].style.paddingLeft = `${indentation * 32 + 4}px`;
|
|
tds[1].style.paddingLeft = `${indentation * 32 + 4}px`;
|
|
|
|
return super.updateRow(tr, fullUpdate);
|
|
}
|
|
updateIcons() {
|
|
// state_icon
|
|
for (const row of this.getRowValues()) {
|
|
let img_path;
|
|
switch (row.full_data.status) {
|
|
case "default":
|
|
img_path = "images/application-rss.svg";
|
|
break;
|
|
case "hasError":
|
|
img_path = "images/task-reject.svg";
|
|
break;
|
|
case "isLoading":
|
|
img_path = "images/spinner.svg";
|
|
break;
|
|
case "unread":
|
|
img_path = "images/mail-inbox.svg";
|
|
break;
|
|
case "isFolder":
|
|
img_path = "images/folder-documents.svg";
|
|
break;
|
|
}
|
|
let td;
|
|
for (let i = 0; i < this.tableBody.rows.length; ++i) {
|
|
if (this.tableBody.rows[i].rowId === row.rowId) {
|
|
td = this.tableBody.rows[i].children[0];
|
|
break;
|
|
}
|
|
}
|
|
if (td.getChildren("img").length > 0) {
|
|
const img = td.getChildren("img")[0];
|
|
if (!img.src.includes(img_path)) {
|
|
img.src = img_path;
|
|
img.title = status;
|
|
}
|
|
}
|
|
else {
|
|
const img = document.createElement("img");
|
|
img.src = img_path;
|
|
img.className = "stateIcon";
|
|
img.width = "22";
|
|
img.height = "22";
|
|
td.append(img);
|
|
}
|
|
}
|
|
}
|
|
newColumn(name, style, caption, defaultWidth, defaultVisible) {
|
|
const column = {};
|
|
column["name"] = name;
|
|
column["title"] = name;
|
|
column["visible"] = defaultVisible;
|
|
column["force_hide"] = false;
|
|
column["caption"] = caption;
|
|
column["style"] = style;
|
|
if (defaultWidth !== -1)
|
|
column["width"] = defaultWidth;
|
|
|
|
column["dataProperties"] = [name];
|
|
column["getRowValue"] = function(row, pos) {
|
|
if (pos === undefined)
|
|
pos = 0;
|
|
return row["full_data"][this.dataProperties[pos]];
|
|
};
|
|
column["compareRows"] = function(row1, row2) {
|
|
const value1 = this.getRowValue(row1);
|
|
const value2 = this.getRowValue(row2);
|
|
if ((typeof(value1) === "number") && (typeof(value2) === "number"))
|
|
return compareNumbers(value1, value2);
|
|
return window.qBittorrent.Misc.naturalSortCollator.compare(value1, value2);
|
|
};
|
|
column["updateTd"] = function(td, row) {
|
|
const value = this.getRowValue(row);
|
|
td.textContent = value;
|
|
td.title = value;
|
|
};
|
|
column["onResize"] = null;
|
|
this.columns.push(column);
|
|
this.columns[name] = column;
|
|
|
|
this.hiddenTableHeader.append(document.createElement("th"));
|
|
this.fixedTableHeader.append(document.createElement("th"));
|
|
}
|
|
}
|
|
|
|
class RssArticleTable extends DynamicTable {
|
|
initColumns() {
|
|
this.newColumn("name", "", "QBT_TR(Torrents: (double-click to download))QBT_TR[CONTEXT=RSSWidget]", -1, true);
|
|
}
|
|
setupHeaderMenu() {}
|
|
setupHeaderEvents() {}
|
|
getFilteredAndSortedRows() {
|
|
return [...this.getRowValues()];
|
|
}
|
|
selectRow(rowId) {
|
|
this.selectedRows.push(rowId);
|
|
this.setRowClass();
|
|
this.onSelectedRowChanged();
|
|
|
|
let articleId = "";
|
|
let feedUid = "";
|
|
for (const row of this.getRowValues()) {
|
|
if (row.rowId === rowId) {
|
|
articleId = row.full_data.dataId;
|
|
feedUid = row.full_data.feedUid;
|
|
this.tableBody.rows[row.rowId].classList.remove("unreadArticle");
|
|
break;
|
|
}
|
|
}
|
|
window.qBittorrent.Rss.showDetails(feedUid, articleId);
|
|
}
|
|
|
|
setupCommonEvents() {
|
|
super.setupCommonEvents();
|
|
this.dynamicTableDiv.addEventListener("dblclick", (e) => {
|
|
const tr = e.target.closest("tr");
|
|
if (!tr)
|
|
return;
|
|
|
|
const { name, torrentURL } = this.getRow(tr.rowId).full_data;
|
|
qBittorrent.Client.createAddTorrentWindow(name, torrentURL);
|
|
});
|
|
}
|
|
updateRow(tr, fullUpdate) {
|
|
const row = this.rows.get(tr.rowId);
|
|
tr.classList.toggle("unreadArticle", !row.full_data.isRead);
|
|
|
|
return super.updateRow(tr, fullUpdate);
|
|
}
|
|
newColumn(name, style, caption, defaultWidth, defaultVisible) {
|
|
const column = {};
|
|
column["name"] = name;
|
|
column["title"] = name;
|
|
column["visible"] = defaultVisible;
|
|
column["force_hide"] = false;
|
|
column["caption"] = caption;
|
|
column["style"] = style;
|
|
if (defaultWidth !== -1)
|
|
column["width"] = defaultWidth;
|
|
|
|
column["dataProperties"] = [name];
|
|
column["getRowValue"] = function(row, pos) {
|
|
if (pos === undefined)
|
|
pos = 0;
|
|
return row["full_data"][this.dataProperties[pos]];
|
|
};
|
|
column["compareRows"] = function(row1, row2) {
|
|
const value1 = this.getRowValue(row1);
|
|
const value2 = this.getRowValue(row2);
|
|
if ((typeof(value1) === "number") && (typeof(value2) === "number"))
|
|
return compareNumbers(value1, value2);
|
|
return window.qBittorrent.Misc.naturalSortCollator.compare(value1, value2);
|
|
};
|
|
column["updateTd"] = function(td, row) {
|
|
const value = this.getRowValue(row);
|
|
td.textContent = value;
|
|
td.title = value;
|
|
};
|
|
column["onResize"] = null;
|
|
this.columns.push(column);
|
|
this.columns[name] = column;
|
|
|
|
this.hiddenTableHeader.append(document.createElement("th"));
|
|
this.fixedTableHeader.append(document.createElement("th"));
|
|
}
|
|
}
|
|
|
|
class RssDownloaderRulesTable extends DynamicTable {
|
|
initColumns() {
|
|
this.newColumn("checked", "", "", 30, true);
|
|
this.newColumn("name", "", "", -1, true);
|
|
|
|
this.columns["checked"].updateTd = (td, row) => {
|
|
if (document.getElementById(`cbRssDlRule${row.rowId}`) === null) {
|
|
const checkbox = document.createElement("input");
|
|
checkbox.type = "checkbox";
|
|
checkbox.id = `cbRssDlRule${row.rowId}`;
|
|
checkbox.checked = row.full_data.checked;
|
|
|
|
checkbox.addEventListener("click", function(e) {
|
|
window.qBittorrent.RssDownloader.rssDownloaderRulesTable.updateRowData({
|
|
rowId: row.rowId,
|
|
checked: this.checked
|
|
});
|
|
window.qBittorrent.RssDownloader.modifyRuleState(row.full_data.name, "enabled", this.checked);
|
|
e.stopPropagation();
|
|
});
|
|
|
|
td.append(checkbox);
|
|
}
|
|
else {
|
|
document.getElementById(`cbRssDlRule${row.rowId}`).checked = row.full_data.checked;
|
|
}
|
|
};
|
|
this.columns["checked"].staticWidth = 50;
|
|
}
|
|
setupHeaderMenu() {}
|
|
setupHeaderEvents() {}
|
|
getFilteredAndSortedRows() {
|
|
return [...this.getRowValues()];
|
|
}
|
|
|
|
setupCommonEvents() {
|
|
super.setupCommonEvents();
|
|
this.dynamicTableDiv.addEventListener("dblclick", (e) => {
|
|
const tr = e.target.closest("tr");
|
|
if (!tr)
|
|
return;
|
|
|
|
window.qBittorrent.RssDownloader.renameRule(this.getRow(tr.rowId).full_data.name);
|
|
});
|
|
}
|
|
newColumn(name, style, caption, defaultWidth, defaultVisible) {
|
|
const column = {};
|
|
column["name"] = name;
|
|
column["title"] = name;
|
|
column["visible"] = defaultVisible;
|
|
column["force_hide"] = false;
|
|
column["caption"] = caption;
|
|
column["style"] = style;
|
|
if (defaultWidth !== -1)
|
|
column["width"] = defaultWidth;
|
|
|
|
column["dataProperties"] = [name];
|
|
column["getRowValue"] = function(row, pos) {
|
|
if (pos === undefined)
|
|
pos = 0;
|
|
return row["full_data"][this.dataProperties[pos]];
|
|
};
|
|
column["compareRows"] = function(row1, row2) {
|
|
const value1 = this.getRowValue(row1);
|
|
const value2 = this.getRowValue(row2);
|
|
if ((typeof(value1) === "number") && (typeof(value2) === "number"))
|
|
return compareNumbers(value1, value2);
|
|
return window.qBittorrent.Misc.naturalSortCollator.compare(value1, value2);
|
|
};
|
|
column["updateTd"] = function(td, row) {
|
|
const value = this.getRowValue(row);
|
|
td.textContent = value;
|
|
td.title = value;
|
|
};
|
|
column["onResize"] = null;
|
|
this.columns.push(column);
|
|
this.columns[name] = column;
|
|
|
|
this.hiddenTableHeader.append(document.createElement("th"));
|
|
this.fixedTableHeader.append(document.createElement("th"));
|
|
}
|
|
selectRow(rowId) {
|
|
this.selectedRows.push(rowId);
|
|
this.setRowClass();
|
|
this.onSelectedRowChanged();
|
|
|
|
let name = "";
|
|
for (const row of this.getRowValues()) {
|
|
if (row.rowId === rowId) {
|
|
name = row.full_data.name;
|
|
break;
|
|
}
|
|
}
|
|
window.qBittorrent.RssDownloader.showRule(name);
|
|
}
|
|
}
|
|
|
|
class RssDownloaderFeedSelectionTable extends DynamicTable {
|
|
initColumns() {
|
|
this.newColumn("checked", "", "", 30, true);
|
|
this.newColumn("name", "", "", -1, true);
|
|
|
|
this.columns["checked"].updateTd = (td, row) => {
|
|
if (document.getElementById(`cbRssDlFeed${row.rowId}`) === null) {
|
|
const checkbox = document.createElement("input");
|
|
checkbox.type = "checkbox";
|
|
checkbox.id = `cbRssDlFeed${row.rowId}`;
|
|
checkbox.checked = row.full_data.checked;
|
|
|
|
checkbox.addEventListener("click", function(e) {
|
|
window.qBittorrent.RssDownloader.rssDownloaderFeedSelectionTable.updateRowData({
|
|
rowId: row.rowId,
|
|
checked: this.checked
|
|
});
|
|
e.stopPropagation();
|
|
});
|
|
|
|
td.append(checkbox);
|
|
}
|
|
else {
|
|
document.getElementById(`cbRssDlFeed${row.rowId}`).checked = row.full_data.checked;
|
|
}
|
|
};
|
|
this.columns["checked"].staticWidth = 50;
|
|
}
|
|
setupHeaderMenu() {}
|
|
setupHeaderEvents() {}
|
|
getFilteredAndSortedRows() {
|
|
return [...this.getRowValues()];
|
|
}
|
|
newColumn(name, style, caption, defaultWidth, defaultVisible) {
|
|
const column = {};
|
|
column["name"] = name;
|
|
column["title"] = name;
|
|
column["visible"] = defaultVisible;
|
|
column["force_hide"] = false;
|
|
column["caption"] = caption;
|
|
column["style"] = style;
|
|
if (defaultWidth !== -1)
|
|
column["width"] = defaultWidth;
|
|
|
|
column["dataProperties"] = [name];
|
|
column["getRowValue"] = function(row, pos) {
|
|
if (pos === undefined)
|
|
pos = 0;
|
|
return row["full_data"][this.dataProperties[pos]];
|
|
};
|
|
column["compareRows"] = function(row1, row2) {
|
|
const value1 = this.getRowValue(row1);
|
|
const value2 = this.getRowValue(row2);
|
|
if ((typeof(value1) === "number") && (typeof(value2) === "number"))
|
|
return compareNumbers(value1, value2);
|
|
return window.qBittorrent.Misc.naturalSortCollator.compare(value1, value2);
|
|
};
|
|
column["updateTd"] = function(td, row) {
|
|
const value = this.getRowValue(row);
|
|
td.textContent = value;
|
|
td.title = value;
|
|
};
|
|
column["onResize"] = null;
|
|
this.columns.push(column);
|
|
this.columns[name] = column;
|
|
|
|
this.hiddenTableHeader.append(document.createElement("th"));
|
|
this.fixedTableHeader.append(document.createElement("th"));
|
|
}
|
|
selectRow() {}
|
|
}
|
|
|
|
class RssDownloaderArticlesTable extends DynamicTable {
|
|
initColumns() {
|
|
this.newColumn("name", "", "", -1, true);
|
|
}
|
|
setupHeaderMenu() {}
|
|
setupHeaderEvents() {}
|
|
getFilteredAndSortedRows() {
|
|
return [...this.getRowValues()];
|
|
}
|
|
newColumn(name, style, caption, defaultWidth, defaultVisible) {
|
|
const column = {};
|
|
column["name"] = name;
|
|
column["title"] = name;
|
|
column["visible"] = defaultVisible;
|
|
column["force_hide"] = false;
|
|
column["caption"] = caption;
|
|
column["style"] = style;
|
|
if (defaultWidth !== -1)
|
|
column["width"] = defaultWidth;
|
|
|
|
column["dataProperties"] = [name];
|
|
column["getRowValue"] = function(row, pos) {
|
|
if (pos === undefined)
|
|
pos = 0;
|
|
return row["full_data"][this.dataProperties[pos]];
|
|
};
|
|
column["compareRows"] = function(row1, row2) {
|
|
const value1 = this.getRowValue(row1);
|
|
const value2 = this.getRowValue(row2);
|
|
if ((typeof(value1) === "number") && (typeof(value2) === "number"))
|
|
return compareNumbers(value1, value2);
|
|
return window.qBittorrent.Misc.naturalSortCollator.compare(value1, value2);
|
|
};
|
|
column["updateTd"] = function(td, row) {
|
|
const value = this.getRowValue(row);
|
|
td.textContent = value;
|
|
td.title = value;
|
|
};
|
|
column["onResize"] = null;
|
|
this.columns.push(column);
|
|
this.columns[name] = column;
|
|
|
|
this.hiddenTableHeader.append(document.createElement("th"));
|
|
this.fixedTableHeader.append(document.createElement("th"));
|
|
}
|
|
selectRow() {}
|
|
updateRow(tr, fullUpdate) {
|
|
const row = this.rows.get(tr.rowId);
|
|
tr.classList.toggle("articleTableFeed", row.full_data.isFeed);
|
|
tr.classList.toggle("articleTableArticle", !row.full_data.isFeed);
|
|
|
|
return super.updateRow(tr, fullUpdate);
|
|
}
|
|
}
|
|
|
|
class LogMessageTable extends DynamicTable {
|
|
filterText = "";
|
|
|
|
initColumns() {
|
|
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("timestamp", "", "QBT_TR(Timestamp)QBT_TR[CONTEXT=ExecutionLogWidget]", 150, true);
|
|
this.newColumn("type", "", "QBT_TR(Log Type)QBT_TR[CONTEXT=ExecutionLogWidget]", 100, true);
|
|
this.initColumnsFunctions();
|
|
}
|
|
|
|
initColumnsFunctions() {
|
|
this.columns["timestamp"].updateTd = function(td, row) {
|
|
const date = new Date(this.getRowValue(row) * 1000).toLocaleString();
|
|
td.textContent = date;
|
|
td.title = date;
|
|
};
|
|
|
|
this.columns["type"].updateTd = function(td, row) {
|
|
// Type of the message: Log::NORMAL: 1, Log::INFO: 2, Log::WARNING: 4, Log::CRITICAL: 8
|
|
let logLevel, addClass;
|
|
switch (Number(this.getRowValue(row))) {
|
|
case 1:
|
|
logLevel = "QBT_TR(Normal)QBT_TR[CONTEXT=ExecutionLogWidget]";
|
|
addClass = "logNormal";
|
|
break;
|
|
case 2:
|
|
logLevel = "QBT_TR(Info)QBT_TR[CONTEXT=ExecutionLogWidget]";
|
|
addClass = "logInfo";
|
|
break;
|
|
case 4:
|
|
logLevel = "QBT_TR(Warning)QBT_TR[CONTEXT=ExecutionLogWidget]";
|
|
addClass = "logWarning";
|
|
break;
|
|
case 8:
|
|
logLevel = "QBT_TR(Critical)QBT_TR[CONTEXT=ExecutionLogWidget]";
|
|
addClass = "logCritical";
|
|
break;
|
|
default:
|
|
logLevel = "QBT_TR(Unknown)QBT_TR[CONTEXT=ExecutionLogWidget]";
|
|
addClass = "logUnknown";
|
|
break;
|
|
}
|
|
td.textContent = logLevel;
|
|
td.title = logLevel;
|
|
td.closest("tr").classList.add(`logTableRow${addClass}`);
|
|
};
|
|
}
|
|
|
|
getFilteredAndSortedRows() {
|
|
let filteredRows = [];
|
|
this.filterText = window.qBittorrent.Log.getFilterText();
|
|
const filterTerms = (this.filterText.length > 0) ? this.filterText.toLowerCase().split(" ") : [];
|
|
const logLevels = window.qBittorrent.Log.getSelectedLevels();
|
|
if ((filterTerms.length > 0) || (logLevels.length < 4)) {
|
|
for (const row of this.getRowValues()) {
|
|
if (!logLevels.includes(row.full_data.type.toString()))
|
|
continue;
|
|
|
|
if ((filterTerms.length > 0) && !window.qBittorrent.Misc.containsAllTerms(row.full_data.message, filterTerms))
|
|
continue;
|
|
|
|
filteredRows.push(row);
|
|
}
|
|
}
|
|
else {
|
|
filteredRows = [...this.getRowValues()];
|
|
}
|
|
|
|
const column = this.columns[this.sortedColumn];
|
|
const isReverseSort = (this.reverseSort === "0");
|
|
filteredRows.sort((row1, row2) => {
|
|
const result = column.compareRows(row1, row2);
|
|
return isReverseSort ? result : -result;
|
|
});
|
|
|
|
this.filteredLength = filteredRows.length;
|
|
|
|
return filteredRows;
|
|
}
|
|
}
|
|
|
|
class LogPeerTable extends LogMessageTable {
|
|
initColumns() {
|
|
this.newColumn("rowId", "", "QBT_TR(ID)QBT_TR[CONTEXT=ExecutionLogWidget]", 50, true);
|
|
this.newColumn("ip", "", "QBT_TR(IP)QBT_TR[CONTEXT=ExecutionLogWidget]", 150, true);
|
|
this.newColumn("timestamp", "", "QBT_TR(Timestamp)QBT_TR[CONTEXT=ExecutionLogWidget]", 150, true);
|
|
this.newColumn("blocked", "", "QBT_TR(Status)QBT_TR[CONTEXT=ExecutionLogWidget]", 150, true);
|
|
this.newColumn("reason", "", "QBT_TR(Reason)QBT_TR[CONTEXT=ExecutionLogWidget]", 150, true);
|
|
|
|
this.columns["timestamp"].updateTd = function(td, row) {
|
|
const date = new Date(this.getRowValue(row) * 1000).toLocaleString();
|
|
td.textContent = date;
|
|
td.title = date;
|
|
};
|
|
|
|
this.columns["blocked"].updateTd = function(td, row) {
|
|
let status, addClass;
|
|
if (this.getRowValue(row)) {
|
|
status = "QBT_TR(Blocked)QBT_TR[CONTEXT=ExecutionLogWidget]";
|
|
addClass = "peerBlocked";
|
|
}
|
|
else {
|
|
status = "QBT_TR(Banned)QBT_TR[CONTEXT=ExecutionLogWidget]";
|
|
addClass = "peerBanned";
|
|
}
|
|
td.textContent = status;
|
|
td.title = status;
|
|
td.closest("tr").classList.add(`logTableRow${addClass}`);
|
|
};
|
|
}
|
|
|
|
getFilteredAndSortedRows() {
|
|
let filteredRows = [];
|
|
this.filterText = window.qBittorrent.Log.getFilterText();
|
|
const filterTerms = (this.filterText.length > 0) ? this.filterText.toLowerCase().split(" ") : [];
|
|
if (filterTerms.length > 0) {
|
|
for (const row of this.getRowValues()) {
|
|
if ((filterTerms.length > 0) && !window.qBittorrent.Misc.containsAllTerms(row.full_data.ip, filterTerms))
|
|
continue;
|
|
|
|
filteredRows.push(row);
|
|
}
|
|
}
|
|
else {
|
|
filteredRows = [...this.getRowValues()];
|
|
}
|
|
|
|
const column = this.columns[this.sortedColumn];
|
|
const isReverseSort = (this.reverseSort === "0");
|
|
filteredRows.sort((row1, row2) => {
|
|
const result = column.compareRows(row1, row2);
|
|
return isReverseSort ? result : -result;
|
|
});
|
|
|
|
return filteredRows;
|
|
}
|
|
}
|
|
|
|
class TorrentWebseedsTable extends DynamicTable {
|
|
initColumns() {
|
|
this.newColumn("url", "", "QBT_TR(URL)QBT_TR[CONTEXT=HttpServer]", 500, true);
|
|
}
|
|
}
|
|
|
|
class TorrentCreationTasksTable extends DynamicTable {
|
|
initColumns() {
|
|
this.newColumn("state_icon", "", "QBT_TR(Status Icon)QBT_TR[CONTEXT=TorrentCreator]", 30, false);
|
|
this.newColumn("source_path", "", "QBT_TR(Source Path)QBT_TR[CONTEXT=TorrentCreator]", 200, true);
|
|
this.newColumn("progress", "", "QBT_TR(Progress)QBT_TR[CONTEXT=TorrentCreator]", 85, true);
|
|
this.newColumn("status", "", "QBT_TR(Status)QBT_TR[CONTEXT=TorrentCreator]", 100, true);
|
|
this.newColumn("torrent_format", "", "QBT_TR(Format)QBT_TR[CONTEXT=TorrentCreator]", 100, true);
|
|
this.newColumn("piece_size", "", "QBT_TR(Piece Size)QBT_TR[CONTEXT=TorrentCreator]", 100, true);
|
|
this.newColumn("private", "", "QBT_TR(Private)QBT_TR[CONTEXT=TorrentCreator]", 30, true);
|
|
this.newColumn("added_on", "", "QBT_TR(Added On)QBT_TR[CONTEXT=TorrentCreator]", 100, true);
|
|
this.newColumn("start_on", "", "QBT_TR(Started On)QBT_TR[CONTEXT=TorrentCreator]", 100, true);
|
|
this.newColumn("completion_on", "", "QBT_TR(Completed On)QBT_TR[CONTEXT=TorrentCreator]", 100, true);
|
|
this.newColumn("trackers", "", "QBT_TR(Trackers)QBT_TR[CONTEXT=TorrentCreator]", 100, true);
|
|
this.newColumn("web_seeds", "", "QBT_TR(Web Seeds)QBT_TR[CONTEXT=TorrentCreator]", 100, false);
|
|
this.newColumn("comment", "", "QBT_TR(Comment)QBT_TR[CONTEXT=TorrentCreator]", 100, true);
|
|
this.newColumn("source", "", "QBT_TR(Source)QBT_TR[CONTEXT=TorrentCreator]", 100, false);
|
|
this.newColumn("error_message", "", "QBT_TR(Error Message)QBT_TR[CONTEXT=TorrentCreator]", 100, true);
|
|
|
|
this.columns["state_icon"].dataProperties[0] = "status";
|
|
this.columns["source_path"].dataProperties.push("status");
|
|
|
|
this.initColumnsFunctions();
|
|
}
|
|
|
|
initColumnsFunctions() {
|
|
const getStateIconClasses = (state) => {
|
|
let stateClass = "stateUnknown";
|
|
// normalize states
|
|
switch (state) {
|
|
case "Running":
|
|
stateClass = "stateRunning";
|
|
break;
|
|
case "Queued":
|
|
stateClass = "stateQueued";
|
|
break;
|
|
case "Finished":
|
|
stateClass = "stateStoppedUP";
|
|
break;
|
|
case "Failed":
|
|
stateClass = "stateError";
|
|
break;
|
|
}
|
|
|
|
return `stateIcon ${stateClass}`;
|
|
};
|
|
|
|
// state_icon
|
|
this.columns["state_icon"].updateTd = function(td, row) {
|
|
const state = this.getRowValue(row);
|
|
let div = td.firstElementChild;
|
|
if (div === null) {
|
|
div = document.createElement("div");
|
|
td.append(div);
|
|
}
|
|
|
|
div.className = `${getStateIconClasses(state)} stateIconColumn`;
|
|
};
|
|
|
|
this.columns["state_icon"].onVisibilityChange = (columnName) => {
|
|
// show state icon in name column only when standalone
|
|
// state icon column is hidden
|
|
this.updateColumn("name", true);
|
|
};
|
|
|
|
// source_path
|
|
this.columns["source_path"].updateTd = function(td, row) {
|
|
const name = this.getRowValue(row, 0);
|
|
const state = this.getRowValue(row, 1);
|
|
let span = td.firstElementChild;
|
|
if (span === null) {
|
|
span = document.createElement("span");
|
|
td.append(span);
|
|
}
|
|
|
|
span.className = this.isStateIconShown() ? getStateIconClasses(state) : "";
|
|
span.textContent = name;
|
|
td.title = name;
|
|
};
|
|
|
|
this.columns["source_path"].isStateIconShown = () => !this.columns["state_icon"].isVisible();
|
|
|
|
// status
|
|
this.columns["status"].updateTd = function(td, row) {
|
|
const state = this.getRowValue(row);
|
|
if (!state)
|
|
return;
|
|
|
|
let status = "QBT_TR(Unknown)QBT_TR[CONTEXT=HttpServer]";
|
|
switch (state) {
|
|
case "Queued":
|
|
status = "QBT_TR(Queued)QBT_TR[CONTEXT=TorrentCreator]";
|
|
break;
|
|
case "Running":
|
|
status = "QBT_TR(Running)QBT_TR[CONTEXT=TorrentCreator]";
|
|
break;
|
|
case "Finished":
|
|
status = "QBT_TR(Finished)QBT_TR[CONTEXT=TorrentCreator]";
|
|
break;
|
|
case "Failed":
|
|
status = "QBT_TR(Failed)QBT_TR[CONTEXT=TorrentCreator]";
|
|
break;
|
|
}
|
|
|
|
td.textContent = status;
|
|
td.title = status;
|
|
};
|
|
|
|
// torrent_format
|
|
this.columns["torrent_format"].updateTd = function(td, row) {
|
|
const torrentFormat = this.getRowValue(row);
|
|
if (!torrentFormat)
|
|
return;
|
|
|
|
let format = "QBT_TR(Unknown)QBT_TR[CONTEXT=HttpServer]";
|
|
switch (torrentFormat) {
|
|
case "v1":
|
|
format = "V1";
|
|
break;
|
|
case "v2":
|
|
format = "V2";
|
|
break;
|
|
case "hybrid":
|
|
format = "QBT_TR(Hybrid)QBT_TR[CONTEXT=TorrentCreator]";
|
|
break;
|
|
}
|
|
|
|
td.textContent = format;
|
|
td.title = format;
|
|
};
|
|
|
|
// progress
|
|
this.columns["progress"].updateTd = function(td, row) {
|
|
const progress = this.getRowValue(row);
|
|
|
|
const div = td.firstElementChild;
|
|
if (div !== null) {
|
|
if (div.getValue() !== progress)
|
|
div.setValue(progress);
|
|
}
|
|
else {
|
|
td.append(new window.qBittorrent.ProgressBar.ProgressBar(progress));
|
|
}
|
|
};
|
|
this.columns["progress"].staticWidth = 100;
|
|
|
|
// piece_size
|
|
this.columns["piece_size"].updateTd = function(td, row) {
|
|
const pieceSize = this.getRowValue(row);
|
|
const size = (pieceSize === 0) ? "QBT_TR(N/A)QBT_TR[CONTEXT=TorrentCreator]" : window.qBittorrent.Misc.friendlyUnit(pieceSize, false);
|
|
td.textContent = size;
|
|
td.title = size;
|
|
};
|
|
|
|
// private
|
|
this.columns["private"].updateTd = function(td, row) {
|
|
const isPrivate = this.getRowValue(row);
|
|
const string = isPrivate
|
|
? "QBT_TR(Yes)QBT_TR[CONTEXT=PropertiesWidget]"
|
|
: "QBT_TR(No)QBT_TR[CONTEXT=PropertiesWidget]";
|
|
td.textContent = string;
|
|
td.title = string;
|
|
};
|
|
|
|
const displayDate = function(td, row) {
|
|
const val = this.getRowValue(row);
|
|
if (!val) {
|
|
td.textContent = "";
|
|
td.title = "";
|
|
}
|
|
else {
|
|
const date = new Date(val).toLocaleString();
|
|
td.textContent = date;
|
|
td.title = date;
|
|
}
|
|
};
|
|
|
|
// added_on, start_on, completion_on
|
|
this.columns["added_on"].updateTd = displayDate;
|
|
this.columns["start_on"].updateTd = displayDate;
|
|
this.columns["completion_on"].updateTd = displayDate;
|
|
}
|
|
|
|
setupCommonEvents() {
|
|
super.setupCommonEvents();
|
|
this.dynamicTableDiv.addEventListener("dblclick", (e) => {
|
|
const tr = e.target.closest("tr");
|
|
if (!tr)
|
|
return;
|
|
|
|
this.deselectAll();
|
|
this.selectRow(tr.rowId);
|
|
|
|
window.qBittorrent.TorrentCreator.exportTorrents();
|
|
});
|
|
}
|
|
}
|
|
|
|
return exports();
|
|
})();
|
|
Object.freeze(window.qBittorrent.DynamicTable);
|
|
|
|
/*************************************************************/
|