WebUI: Improve sort order in Status column
[qBittorrent.git] / src / webui / www / private / scripts / dynamicTable.js
blobe34611eec3eb231898b95a07afcbca433fac9c43
1 /*
2  * MIT License
3  * Copyright (c) 2008 Ishan Arora <ishan@qbittorrent.org> & Christophe Dumez <chris@qbittorrent.org>
4  *
5  * Permission is hereby granted, free of charge, to any person obtaining a copy
6  * of this software and associated documentation files (the "Software"), to deal
7  * in the Software without restriction, including without limitation the rights
8  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9  * copies of the Software, and to permit persons to whom the Software is
10  * furnished to do so, subject to the following conditions:
11  *
12  * The above copyright notice and this permission notice shall be included in
13  * all copies or substantial portions of the Software.
14  *
15  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21  * THE SOFTWARE.
22  */
24 /**************************************************************
26     Script      : Dynamic Table
27     Version     : 0.5
28     Authors     : Ishan Arora & Christophe Dumez
29     Desc        : Programmable sortable table
30     Licence     : Open Source MIT Licence
32  **************************************************************/
34 "use strict";
36 window.qBittorrent ??= {};
37 window.qBittorrent.DynamicTable ??= (() => {
38     const exports = () => {
39         return {
40             TorrentsTable: TorrentsTable,
41             TorrentPeersTable: TorrentPeersTable,
42             SearchResultsTable: SearchResultsTable,
43             SearchPluginsTable: SearchPluginsTable,
44             TorrentTrackersTable: TorrentTrackersTable,
45             BulkRenameTorrentFilesTable: BulkRenameTorrentFilesTable,
46             TorrentFilesTable: TorrentFilesTable,
47             LogMessageTable: LogMessageTable,
48             LogPeerTable: LogPeerTable,
49             RssFeedTable: RssFeedTable,
50             RssArticleTable: RssArticleTable,
51             RssDownloaderRulesTable: RssDownloaderRulesTable,
52             RssDownloaderFeedSelectionTable: RssDownloaderFeedSelectionTable,
53             RssDownloaderArticlesTable: RssDownloaderArticlesTable,
54             TorrentWebseedsTable: TorrentWebseedsTable
55         };
56     };
58     const compareNumbers = (val1, val2) => {
59         if (val1 < val2)
60             return -1;
61         if (val1 > val2)
62             return 1;
63         return 0;
64     };
66     let DynamicTableHeaderContextMenuClass = null;
67     let ProgressColumnWidth = -1;
69     const DynamicTable = new Class({
71         initialize: function() {},
73         setup: function(dynamicTableDivId, dynamicTableFixedHeaderDivId, contextMenu) {
74             this.dynamicTableDivId = dynamicTableDivId;
75             this.dynamicTableFixedHeaderDivId = dynamicTableFixedHeaderDivId;
76             this.fixedTableHeader = $(dynamicTableFixedHeaderDivId).getElements("tr")[0];
77             this.hiddenTableHeader = $(dynamicTableDivId).getElements("tr")[0];
78             this.tableBody = $(dynamicTableDivId).getElements("tbody")[0];
79             this.rows = new Map();
80             this.selectedRows = [];
81             this.columns = [];
82             this.contextMenu = contextMenu;
83             this.sortedColumn = LocalPreferences.get("sorted_column_" + this.dynamicTableDivId, 0);
84             this.reverseSort = LocalPreferences.get("reverse_sort_" + this.dynamicTableDivId, "0");
85             this.initColumns();
86             this.loadColumnsOrder();
87             this.updateTableHeaders();
88             this.setupCommonEvents();
89             this.setupHeaderEvents();
90             this.setupHeaderMenu();
91             this.setSortedColumnIcon(this.sortedColumn, null, (this.reverseSort === "1"));
92             this.setupAltRow();
93         },
95         setupCommonEvents: function() {
96             const tableDiv = $(this.dynamicTableDivId);
97             const tableFixedHeaderDiv = $(this.dynamicTableFixedHeaderDivId);
99             const tableElement = tableFixedHeaderDiv.querySelector("table");
100             tableDiv.addEventListener("scroll", () => {
101                 tableElement.style.left = `${-tableDiv.scrollLeft}px`;
102             });
104             // if the table exists within a panel
105             const parentPanel = tableDiv.getParent(".panel");
106             if (parentPanel) {
107                 const resizeFn = (entries) => {
108                     const panel = entries[0].target;
109                     let h = panel.getBoundingClientRect().height - tableFixedHeaderDiv.getBoundingClientRect().height;
110                     tableDiv.style.height = `${h}px`;
112                     // Workaround due to inaccurate calculation of elements heights by browser
113                     let n = 2;
115                     // is panel vertical scrollbar visible or does panel content not fit?
116                     while (((panel.clientWidth !== panel.offsetWidth) || (panel.clientHeight !== panel.scrollHeight)) && (n > 0)) {
117                         --n;
118                         h -= 0.5;
119                         tableDiv.style.height = `${h}px`;
120                     }
121                 };
123                 const resizeDebouncer = window.qBittorrent.Misc.createDebounceHandler(100, (entries) => {
124                     resizeFn(entries);
125                 });
127                 const resizeObserver = new ResizeObserver(resizeDebouncer);
128                 resizeObserver.observe(parentPanel, { box: "border-box" });
129             }
130         },
132         setupHeaderEvents: function() {
133             this.currentHeaderAction = "";
134             this.canResize = false;
136             const resetElementBorderStyle = function(el, side) {
137                 if ((side === "left") || (side !== "right"))
138                     el.style.borderLeft = "";
139                 if ((side === "right") || (side !== "left"))
140                     el.style.borderRight = "";
141             };
143             const mouseMoveFn = function(e) {
144                 const brect = e.target.getBoundingClientRect();
145                 const mouseXRelative = e.clientX - brect.left;
146                 if (this.currentHeaderAction === "") {
147                     if ((brect.width - mouseXRelative) < 5) {
148                         this.resizeTh = e.target;
149                         this.canResize = true;
150                         e.target.getParent("tr").style.cursor = "col-resize";
151                     }
152                     else if ((mouseXRelative < 5) && e.target.getPrevious('[class=""]')) {
153                         this.resizeTh = e.target.getPrevious('[class=""]');
154                         this.canResize = true;
155                         e.target.getParent("tr").style.cursor = "col-resize";
156                     }
157                     else {
158                         this.canResize = false;
159                         e.target.getParent("tr").style.cursor = "";
160                     }
161                 }
162                 if (this.currentHeaderAction === "drag") {
163                     const previousVisibleSibling = e.target.getPrevious('[class=""]');
164                     let borderChangeElement = previousVisibleSibling;
165                     let changeBorderSide = "right";
167                     if (mouseXRelative > (brect.width / 2)) {
168                         borderChangeElement = e.target;
169                         this.dropSide = "right";
170                     }
171                     else {
172                         this.dropSide = "left";
173                     }
175                     e.target.getParent("tr").style.cursor = "move";
177                     if (!previousVisibleSibling) { // right most column
178                         borderChangeElement = e.target;
180                         if (mouseXRelative <= (brect.width / 2))
181                             changeBorderSide = "left";
182                     }
184                     const borderStyle = "initial solid #e60";
185                     if (changeBorderSide === "left")
186                         borderChangeElement.style.borderLeft = borderStyle;
187                     else
188                         borderChangeElement.style.borderRight = borderStyle;
190                     resetElementBorderStyle(borderChangeElement, ((changeBorderSide === "right") ? "left" : "right"));
192                     borderChangeElement.getSiblings('[class=""]').each((el) => {
193                         resetElementBorderStyle(el);
194                     });
195                 }
196                 this.lastHoverTh = e.target;
197                 this.lastClientX = e.clientX;
198             }.bind(this);
200             const mouseOutFn = function(e) {
201                 resetElementBorderStyle(e.target);
202             }.bind(this);
204             const onBeforeStart = function(el) {
205                 this.clickedTh = el;
206                 this.currentHeaderAction = "start";
207                 this.dragMovement = false;
208                 this.dragStartX = this.lastClientX;
209             }.bind(this);
211             const onStart = function(el, event) {
212                 if (this.canResize) {
213                     this.currentHeaderAction = "resize";
214                     this.startWidth = parseInt(this.resizeTh.style.width, 10);
215                 }
216                 else {
217                     this.currentHeaderAction = "drag";
218                     el.style.backgroundColor = "#C1D5E7";
219                 }
220             }.bind(this);
222             const onDrag = function(el, event) {
223                 if (this.currentHeaderAction === "resize") {
224                     let width = this.startWidth + (event.event.pageX - this.dragStartX);
225                     if (width < 16)
226                         width = 16;
227                     this.columns[this.resizeTh.columnName].width = width;
228                     this.updateColumn(this.resizeTh.columnName);
229                 }
230             }.bind(this);
232             const onComplete = function(el, event) {
233                 resetElementBorderStyle(this.lastHoverTh);
234                 el.style.backgroundColor = "";
235                 if (this.currentHeaderAction === "resize")
236                     LocalPreferences.set("column_" + this.resizeTh.columnName + "_width_" + this.dynamicTableDivId, this.columns[this.resizeTh.columnName].width);
237                 if ((this.currentHeaderAction === "drag") && (el !== this.lastHoverTh)) {
238                     this.saveColumnsOrder();
239                     const val = LocalPreferences.get("columns_order_" + this.dynamicTableDivId).split(",");
240                     val.erase(el.columnName);
241                     let pos = val.indexOf(this.lastHoverTh.columnName);
242                     if (this.dropSide === "right")
243                         ++pos;
244                     val.splice(pos, 0, el.columnName);
245                     LocalPreferences.set("columns_order_" + this.dynamicTableDivId, val.join(","));
246                     this.loadColumnsOrder();
247                     this.updateTableHeaders();
248                     while (this.tableBody.firstChild)
249                         this.tableBody.removeChild(this.tableBody.firstChild);
250                     this.updateTable(true);
251                 }
252                 if (this.currentHeaderAction === "drag") {
253                     resetElementBorderStyle(el);
254                     el.getSiblings('[class=""]').each((el) => {
255                         resetElementBorderStyle(el);
256                     });
257                 }
258                 this.currentHeaderAction = "";
259             }.bind(this);
261             const onCancel = function(el) {
262                 this.currentHeaderAction = "";
263                 this.setSortedColumn(el.columnName);
264             }.bind(this);
266             const onTouch = function(e) {
267                 const column = e.target.columnName;
268                 this.currentHeaderAction = "";
269                 this.setSortedColumn(column);
270             }.bind(this);
272             const ths = this.fixedTableHeader.getElements("th");
274             for (let i = 0; i < ths.length; ++i) {
275                 const th = ths[i];
276                 th.addEventListener("mousemove", mouseMoveFn);
277                 th.addEventListener("mouseout", mouseOutFn);
278                 th.addEventListener("touchend", onTouch, { passive: true });
279                 th.makeResizable({
280                     modifiers: {
281                         x: "",
282                         y: ""
283                     },
284                     onBeforeStart: onBeforeStart,
285                     onStart: onStart,
286                     onDrag: onDrag,
287                     onComplete: onComplete,
288                     onCancel: onCancel
289                 });
290             }
291         },
293         setupDynamicTableHeaderContextMenuClass: function() {
294             if (!DynamicTableHeaderContextMenuClass) {
295                 DynamicTableHeaderContextMenuClass = new Class({
296                     Extends: window.qBittorrent.ContextMenu.ContextMenu,
297                     updateMenuItems: function() {
298                         for (let i = 0; i < this.dynamicTable.columns.length; ++i) {
299                             if (this.dynamicTable.columns[i].caption === "")
300                                 continue;
301                             if (this.dynamicTable.columns[i].visible !== "0")
302                                 this.setItemChecked(this.dynamicTable.columns[i].name, true);
303                             else
304                                 this.setItemChecked(this.dynamicTable.columns[i].name, false);
305                         }
306                     }
307                 });
308             }
309         },
311         showColumn: function(columnName, show) {
312             this.columns[columnName].visible = show ? "1" : "0";
313             LocalPreferences.set("column_" + columnName + "_visible_" + this.dynamicTableDivId, show ? "1" : "0");
314             this.updateColumn(columnName);
315         },
317         setupHeaderMenu: function() {
318             this.setupDynamicTableHeaderContextMenuClass();
320             const menuId = this.dynamicTableDivId + "_headerMenu";
322             // reuse menu if already exists
323             const ul = $(menuId) ?? new Element("ul", {
324                 id: menuId,
325                 class: "contextMenu scrollableMenu"
326             });
328             const createLi = function(columnName, text) {
329                 const anchor = document.createElement("a");
330                 anchor.href = `#${columnName}`;
331                 anchor.textContent = text;
333                 const img = document.createElement("img");
334                 img.src = "images/checked-completed.svg";
335                 anchor.prepend(img);
337                 const listItem = document.createElement("li");
338                 listItem.appendChild(anchor);
340                 return listItem;
341             };
343             const actions = {};
345             const onMenuItemClicked = function(element, ref, action) {
346                 this.showColumn(action, this.columns[action].visible === "0");
347             }.bind(this);
349             // recreate child nodes when reusing (enables the context menu to work correctly)
350             if (ul.hasChildNodes()) {
351                 while (ul.firstChild)
352                     ul.removeChild(ul.lastChild);
353             }
355             for (let i = 0; i < this.columns.length; ++i) {
356                 const text = this.columns[i].caption;
357                 if (text === "")
358                     continue;
359                 ul.appendChild(createLi(this.columns[i].name, text));
360                 actions[this.columns[i].name] = onMenuItemClicked;
361             }
363             ul.inject(document.body);
365             this.headerContextMenu = new DynamicTableHeaderContextMenuClass({
366                 targets: "#" + this.dynamicTableFixedHeaderDivId + " tr",
367                 actions: actions,
368                 menu: menuId,
369                 offsets: {
370                     x: 0,
371                     y: 2
372                 }
373             });
375             this.headerContextMenu.dynamicTable = this;
376         },
378         initColumns: function() {},
380         newColumn: function(name, style, caption, defaultWidth, defaultVisible) {
381             const column = {};
382             column["name"] = name;
383             column["title"] = name;
384             column["visible"] = LocalPreferences.get("column_" + name + "_visible_" + this.dynamicTableDivId, defaultVisible ? "1" : "0");
385             column["force_hide"] = false;
386             column["caption"] = caption;
387             column["style"] = style;
388             column["width"] = LocalPreferences.get("column_" + name + "_width_" + this.dynamicTableDivId, defaultWidth);
389             column["dataProperties"] = [name];
390             column["getRowValue"] = function(row, pos) {
391                 if (pos === undefined)
392                     pos = 0;
393                 return row["full_data"][this.dataProperties[pos]];
394             };
395             column["compareRows"] = function(row1, row2) {
396                 const value1 = this.getRowValue(row1);
397                 const value2 = this.getRowValue(row2);
398                 if ((typeof(value1) === "number") && (typeof(value2) === "number"))
399                     return compareNumbers(value1, value2);
400                 return window.qBittorrent.Misc.naturalSortCollator.compare(value1, value2);
401             };
402             column["updateTd"] = function(td, row) {
403                 const value = this.getRowValue(row);
404                 td.textContent = value;
405                 td.title = value;
406             };
407             column["onResize"] = null;
408             this.columns.push(column);
409             this.columns[name] = column;
411             this.hiddenTableHeader.appendChild(new Element("th"));
412             this.fixedTableHeader.appendChild(new Element("th"));
413         },
415         loadColumnsOrder: function() {
416             const columnsOrder = [];
417             const val = LocalPreferences.get("columns_order_" + this.dynamicTableDivId);
418             if ((val === null) || (val === undefined))
419                 return;
420             val.split(",").forEach((v) => {
421                 if ((v in this.columns) && (!columnsOrder.contains(v)))
422                     columnsOrder.push(v);
423             });
425             for (let i = 0; i < this.columns.length; ++i) {
426                 if (!columnsOrder.contains(this.columns[i].name))
427                     columnsOrder.push(this.columns[i].name);
428             }
430             for (let i = 0; i < this.columns.length; ++i)
431                 this.columns[i] = this.columns[columnsOrder[i]];
432         },
434         saveColumnsOrder: function() {
435             let val = "";
436             for (let i = 0; i < this.columns.length; ++i) {
437                 if (i > 0)
438                     val += ",";
439                 val += this.columns[i].name;
440             }
441             LocalPreferences.set("columns_order_" + this.dynamicTableDivId, val);
442         },
444         updateTableHeaders: function() {
445             this.updateHeader(this.hiddenTableHeader);
446             this.updateHeader(this.fixedTableHeader);
447         },
449         updateHeader: function(header) {
450             const ths = header.getElements("th");
452             for (let i = 0; i < ths.length; ++i) {
453                 const th = ths[i];
454                 th._this = this;
455                 th.title = this.columns[i].caption;
456                 th.textContent = this.columns[i].caption;
457                 th.setAttribute("style", "width: " + this.columns[i].width + "px;" + this.columns[i].style);
458                 th.columnName = this.columns[i].name;
459                 th.addClass("column_" + th.columnName);
460                 if ((this.columns[i].visible === "0") || this.columns[i].force_hide)
461                     th.addClass("invisible");
462                 else
463                     th.removeClass("invisible");
464             }
465         },
467         getColumnPos: function(columnName) {
468             for (let i = 0; i < this.columns.length; ++i) {
469                 if (this.columns[i].name === columnName)
470                     return i;
471             }
472             return -1;
473         },
475         updateColumn: function(columnName) {
476             const pos = this.getColumnPos(columnName);
477             const visible = ((this.columns[pos].visible !== "0") && !this.columns[pos].force_hide);
478             const ths = this.hiddenTableHeader.getElements("th");
479             const fths = this.fixedTableHeader.getElements("th");
480             const trs = this.tableBody.getElements("tr");
481             const style = "width: " + this.columns[pos].width + "px;" + this.columns[pos].style;
483             ths[pos].setAttribute("style", style);
484             fths[pos].setAttribute("style", style);
486             if (visible) {
487                 ths[pos].removeClass("invisible");
488                 fths[pos].removeClass("invisible");
489                 for (let i = 0; i < trs.length; ++i)
490                     trs[i].getElements("td")[pos].removeClass("invisible");
491             }
492             else {
493                 ths[pos].addClass("invisible");
494                 fths[pos].addClass("invisible");
495                 for (let j = 0; j < trs.length; ++j)
496                     trs[j].getElements("td")[pos].addClass("invisible");
497             }
498             if (this.columns[pos].onResize !== null)
499                 this.columns[pos].onResize(columnName);
500         },
502         getSortedColumn: function() {
503             return LocalPreferences.get("sorted_column_" + this.dynamicTableDivId);
504         },
506         /**
507          * @param {string} column name to sort by
508          * @param {string|null} reverse defaults to implementation-specific behavior when not specified. Should only be passed when restoring previous state.
509          */
510         setSortedColumn: function(column, reverse = null) {
511             if (column !== this.sortedColumn) {
512                 const oldColumn = this.sortedColumn;
513                 this.sortedColumn = column;
514                 this.reverseSort = reverse ?? "0";
515                 this.setSortedColumnIcon(column, oldColumn, false);
516             }
517             else {
518                 // Toggle sort order
519                 this.reverseSort = reverse ?? (this.reverseSort === "0" ? "1" : "0");
520                 this.setSortedColumnIcon(column, null, (this.reverseSort === "1"));
521             }
522             LocalPreferences.set("sorted_column_" + this.dynamicTableDivId, column);
523             LocalPreferences.set("reverse_sort_" + this.dynamicTableDivId, this.reverseSort);
524             this.updateTable(false);
525         },
527         setSortedColumnIcon: function(newColumn, oldColumn, isReverse) {
528             const getCol = function(headerDivId, colName) {
529                 const colElem = $$("#" + headerDivId + " .column_" + colName);
530                 if (colElem.length === 1)
531                     return colElem[0];
532                 return null;
533             };
535             const colElem = getCol(this.dynamicTableFixedHeaderDivId, newColumn);
536             if (colElem !== null) {
537                 colElem.addClass("sorted");
538                 if (isReverse)
539                     colElem.addClass("reverse");
540                 else
541                     colElem.removeClass("reverse");
542             }
543             const oldColElem = getCol(this.dynamicTableFixedHeaderDivId, oldColumn);
544             if (oldColElem !== null) {
545                 oldColElem.removeClass("sorted");
546                 oldColElem.removeClass("reverse");
547             }
548         },
550         getSelectedRowId: function() {
551             if (this.selectedRows.length > 0)
552                 return this.selectedRows[0];
553             return "";
554         },
556         isRowSelected: function(rowId) {
557             return this.selectedRows.contains(rowId);
558         },
560         setupAltRow: function() {
561             const useAltRowColors = (LocalPreferences.get("use_alt_row_colors", "true") === "true");
562             if (useAltRowColors)
563                 document.getElementById(this.dynamicTableDivId).classList.add("altRowColors");
564         },
566         selectAll: function() {
567             this.deselectAll();
569             const trs = this.tableBody.getElements("tr");
570             for (let i = 0; i < trs.length; ++i) {
571                 const tr = trs[i];
572                 this.selectedRows.push(tr.rowId);
573                 if (!tr.hasClass("selected"))
574                     tr.addClass("selected");
575             }
576         },
578         deselectAll: function() {
579             this.selectedRows.empty();
580         },
582         selectRow: function(rowId) {
583             this.selectedRows.push(rowId);
584             this.setRowClass();
585             this.onSelectedRowChanged();
586         },
588         deselectRow: function(rowId) {
589             this.selectedRows.erase(rowId);
590             this.setRowClass();
591             this.onSelectedRowChanged();
592         },
594         selectRows: function(rowId1, rowId2) {
595             this.deselectAll();
596             if (rowId1 === rowId2) {
597                 this.selectRow(rowId1);
598                 return;
599             }
601             let select = false;
602             const that = this;
603             this.tableBody.getElements("tr").each((tr) => {
604                 if ((tr.rowId === rowId1) || (tr.rowId === rowId2)) {
605                     select = !select;
606                     that.selectedRows.push(tr.rowId);
607                 }
608                 else if (select) {
609                     that.selectedRows.push(tr.rowId);
610                 }
611             });
612             this.setRowClass();
613             this.onSelectedRowChanged();
614         },
616         reselectRows: function(rowIds) {
617             this.deselectAll();
618             this.selectedRows = rowIds.slice();
619             this.tableBody.getElements("tr").each((tr) => {
620                 if (rowIds.includes(tr.rowId))
621                     tr.addClass("selected");
622             });
623         },
625         setRowClass: function() {
626             const that = this;
627             this.tableBody.getElements("tr").each((tr) => {
628                 if (that.isRowSelected(tr.rowId))
629                     tr.addClass("selected");
630                 else
631                     tr.removeClass("selected");
632             });
633         },
635         onSelectedRowChanged: function() {},
637         updateRowData: function(data) {
638             // ensure rowId is a string
639             const rowId = `${data["rowId"]}`;
640             let row;
642             if (!this.rows.has(rowId)) {
643                 row = {
644                     "full_data": {},
645                     "rowId": rowId
646                 };
647                 this.rows.set(rowId, row);
648             }
649             else {
650                 row = this.rows.get(rowId);
651             }
653             row["data"] = data;
654             for (const x in data) {
655                 if (!Object.hasOwn(data, x))
656                     continue;
657                 row["full_data"][x] = data[x];
658             }
659         },
661         getRow: function(rowId) {
662             return this.rows.get(rowId);
663         },
665         getFilteredAndSortedRows: function() {
666             const filteredRows = [];
668             for (const row of this.getRowValues()) {
669                 filteredRows.push(row);
670                 filteredRows[row.rowId] = row;
671             }
673             filteredRows.sort((row1, row2) => {
674                 const column = this.columns[this.sortedColumn];
675                 const res = column.compareRows(row1, row2);
676                 if (this.reverseSort === "0")
677                     return res;
678                 else
679                     return -res;
680             });
681             return filteredRows;
682         },
684         getTrByRowId: function(rowId) {
685             const trs = this.tableBody.getElements("tr");
686             for (let i = 0; i < trs.length; ++i) {
687                 if (trs[i].rowId === rowId)
688                     return trs[i];
689             }
690             return null;
691         },
693         updateTable: function(fullUpdate = false) {
694             const rows = this.getFilteredAndSortedRows();
696             for (let i = 0; i < this.selectedRows.length; ++i) {
697                 if (!(this.selectedRows[i] in rows)) {
698                     this.selectedRows.splice(i, 1);
699                     --i;
700                 }
701             }
703             const trs = this.tableBody.getElements("tr");
705             for (let rowPos = 0; rowPos < rows.length; ++rowPos) {
706                 const rowId = rows[rowPos]["rowId"];
707                 let tr_found = false;
708                 for (let j = rowPos; j < trs.length; ++j) {
709                     if (trs[j]["rowId"] === rowId) {
710                         tr_found = true;
711                         if (rowPos === j)
712                             break;
713                         trs[j].inject(trs[rowPos], "before");
714                         const tmpTr = trs[j];
715                         trs.splice(j, 1);
716                         trs.splice(rowPos, 0, tmpTr);
717                         break;
718                     }
719                 }
720                 if (tr_found) { // row already exists in the table
721                     this.updateRow(trs[rowPos], fullUpdate);
722                 }
723                 else { // else create a new row in the table
724                     const tr = new Element("tr");
725                     // set tabindex so element receives keydown events
726                     // more info: https://developer.mozilla.org/en-US/docs/Web/API/Element/keydown_event
727                     tr.tabindex = "-1";
729                     const rowId = rows[rowPos]["rowId"];
730                     tr.setAttribute("data-row-id", rowId);
731                     tr["rowId"] = rowId;
733                     tr._this = this;
734                     tr.addEventListener("contextmenu", function(e) {
735                         if (!this._this.isRowSelected(this.rowId)) {
736                             this._this.deselectAll();
737                             this._this.selectRow(this.rowId);
738                         }
739                         return true;
740                     });
741                     tr.addEventListener("click", function(e) {
742                         e.preventDefault();
743                         e.stopPropagation();
745                         if (e.ctrlKey || e.metaKey) {
746                             // CTRL/CMD âŒ˜ key was pressed
747                             if (this._this.isRowSelected(this.rowId))
748                                 this._this.deselectRow(this.rowId);
749                             else
750                                 this._this.selectRow(this.rowId);
751                         }
752                         else if (e.shiftKey && (this._this.selectedRows.length === 1)) {
753                             // Shift key was pressed
754                             this._this.selectRows(this._this.getSelectedRowId(), this.rowId);
755                         }
756                         else {
757                             // Simple selection
758                             this._this.deselectAll();
759                             this._this.selectRow(this.rowId);
760                         }
761                         return false;
762                     });
763                     tr.addEventListener("touchstart", function(e) {
764                         if (!this._this.isRowSelected(this.rowId)) {
765                             this._this.deselectAll();
766                             this._this.selectRow(this.rowId);
767                         }
768                     }, { passive: true });
769                     tr.addEventListener("keydown", function(event) {
770                         switch (event.key) {
771                             case "up":
772                                 this._this.selectPreviousRow();
773                                 return false;
774                             case "down":
775                                 this._this.selectNextRow();
776                                 return false;
777                         }
778                     });
780                     this.setupTr(tr);
782                     for (let k = 0; k < this.columns.length; ++k) {
783                         const td = new Element("td");
784                         if ((this.columns[k].visible === "0") || this.columns[k].force_hide)
785                             td.addClass("invisible");
786                         td.injectInside(tr);
787                     }
789                     // Insert
790                     if (rowPos >= trs.length) {
791                         tr.inject(this.tableBody);
792                         trs.push(tr);
793                     }
794                     else {
795                         tr.inject(trs[rowPos], "before");
796                         trs.splice(rowPos, 0, tr);
797                     }
799                     // Update context menu
800                     if (this.contextMenu)
801                         this.contextMenu.addTarget(tr);
803                     this.updateRow(tr, true);
804                 }
805             }
807             const rowPos = rows.length;
809             while ((rowPos < trs.length) && (trs.length > 0))
810                 trs.pop().destroy();
811         },
813         setupTr: function(tr) {},
815         updateRow: function(tr, fullUpdate) {
816             const row = this.rows.get(tr.rowId);
817             const data = row[fullUpdate ? "full_data" : "data"];
819             const tds = tr.getElements("td");
820             for (let i = 0; i < this.columns.length; ++i) {
821                 if (Object.hasOwn(data, this.columns[i].dataProperties[0]))
822                     this.columns[i].updateTd(tds[i], row);
823             }
824             row["data"] = {};
825         },
827         removeRow: function(rowId) {
828             this.selectedRows.erase(rowId);
829             this.rows.delete(rowId);
830             const tr = this.getTrByRowId(rowId);
831             tr?.destroy();
832         },
834         clear: function() {
835             this.deselectAll();
836             this.rows.clear();
837             const trs = this.tableBody.getElements("tr");
838             while (trs.length > 0)
839                 trs.pop().destroy();
840         },
842         selectedRowsIds: function() {
843             return this.selectedRows.slice();
844         },
846         getRowIds: function() {
847             return this.rows.keys();
848         },
850         getRowValues: function() {
851             return this.rows.values();
852         },
854         getRowItems: function() {
855             return this.rows.entries();
856         },
858         getRowSize: function() {
859             return this.rows.size;
860         },
862         selectNextRow: function() {
863             const visibleRows = $(this.dynamicTableDivId).getElements("tbody tr").filter(e => e.style.display !== "none");
864             const selectedRowId = this.getSelectedRowId();
866             let selectedIndex = -1;
867             for (let i = 0; i < visibleRows.length; ++i) {
868                 const row = visibleRows[i];
869                 if (row.getAttribute("data-row-id") === selectedRowId) {
870                     selectedIndex = i;
871                     break;
872                 }
873             }
875             const isLastRowSelected = (selectedIndex >= (visibleRows.length - 1));
876             if (!isLastRowSelected) {
877                 this.deselectAll();
879                 const newRow = visibleRows[selectedIndex + 1];
880                 this.selectRow(newRow.getAttribute("data-row-id"));
881             }
882         },
884         selectPreviousRow: function() {
885             const visibleRows = $(this.dynamicTableDivId).getElements("tbody tr").filter(e => e.style.display !== "none");
886             const selectedRowId = this.getSelectedRowId();
888             let selectedIndex = -1;
889             for (let i = 0; i < visibleRows.length; ++i) {
890                 const row = visibleRows[i];
891                 if (row.getAttribute("data-row-id") === selectedRowId) {
892                     selectedIndex = i;
893                     break;
894                 }
895             }
897             const isFirstRowSelected = selectedIndex <= 0;
898             if (!isFirstRowSelected) {
899                 this.deselectAll();
901                 const newRow = visibleRows[selectedIndex - 1];
902                 this.selectRow(newRow.getAttribute("data-row-id"));
903             }
904         },
905     });
907     const TorrentsTable = new Class({
908         Extends: DynamicTable,
910         initColumns: function() {
911             this.newColumn("priority", "", "#", 30, true);
912             this.newColumn("state_icon", "cursor: default", "", 22, true);
913             this.newColumn("name", "", "QBT_TR(Name)QBT_TR[CONTEXT=TransferListModel]", 200, true);
914             this.newColumn("size", "", "QBT_TR(Size)QBT_TR[CONTEXT=TransferListModel]", 100, true);
915             this.newColumn("total_size", "", "QBT_TR(Total Size)QBT_TR[CONTEXT=TransferListModel]", 100, false);
916             this.newColumn("progress", "", "QBT_TR(Progress)QBT_TR[CONTEXT=TransferListModel]", 85, true);
917             this.newColumn("status", "", "QBT_TR(Status)QBT_TR[CONTEXT=TransferListModel]", 100, true);
918             this.newColumn("num_seeds", "", "QBT_TR(Seeds)QBT_TR[CONTEXT=TransferListModel]", 100, true);
919             this.newColumn("num_leechs", "", "QBT_TR(Peers)QBT_TR[CONTEXT=TransferListModel]", 100, true);
920             this.newColumn("dlspeed", "", "QBT_TR(Down Speed)QBT_TR[CONTEXT=TransferListModel]", 100, true);
921             this.newColumn("upspeed", "", "QBT_TR(Up Speed)QBT_TR[CONTEXT=TransferListModel]", 100, true);
922             this.newColumn("eta", "", "QBT_TR(ETA)QBT_TR[CONTEXT=TransferListModel]", 100, true);
923             this.newColumn("ratio", "", "QBT_TR(Ratio)QBT_TR[CONTEXT=TransferListModel]", 100, true);
924             this.newColumn("popularity", "", "QBT_TR(Popularity)QBT_TR[CONTEXT=TransferListModel]", 100, true);
925             this.newColumn("category", "", "QBT_TR(Category)QBT_TR[CONTEXT=TransferListModel]", 100, true);
926             this.newColumn("tags", "", "QBT_TR(Tags)QBT_TR[CONTEXT=TransferListModel]", 100, true);
927             this.newColumn("added_on", "", "QBT_TR(Added On)QBT_TR[CONTEXT=TransferListModel]", 100, true);
928             this.newColumn("completion_on", "", "QBT_TR(Completed On)QBT_TR[CONTEXT=TransferListModel]", 100, false);
929             this.newColumn("tracker", "", "QBT_TR(Tracker)QBT_TR[CONTEXT=TransferListModel]", 100, false);
930             this.newColumn("dl_limit", "", "QBT_TR(Down Limit)QBT_TR[CONTEXT=TransferListModel]", 100, false);
931             this.newColumn("up_limit", "", "QBT_TR(Up Limit)QBT_TR[CONTEXT=TransferListModel]", 100, false);
932             this.newColumn("downloaded", "", "QBT_TR(Downloaded)QBT_TR[CONTEXT=TransferListModel]", 100, false);
933             this.newColumn("uploaded", "", "QBT_TR(Uploaded)QBT_TR[CONTEXT=TransferListModel]", 100, false);
934             this.newColumn("downloaded_session", "", "QBT_TR(Session Download)QBT_TR[CONTEXT=TransferListModel]", 100, false);
935             this.newColumn("uploaded_session", "", "QBT_TR(Session Upload)QBT_TR[CONTEXT=TransferListModel]", 100, false);
936             this.newColumn("amount_left", "", "QBT_TR(Remaining)QBT_TR[CONTEXT=TransferListModel]", 100, false);
937             this.newColumn("time_active", "", "QBT_TR(Time Active)QBT_TR[CONTEXT=TransferListModel]", 100, false);
938             this.newColumn("save_path", "", "QBT_TR(Save path)QBT_TR[CONTEXT=TransferListModel]", 100, false);
939             this.newColumn("completed", "", "QBT_TR(Completed)QBT_TR[CONTEXT=TransferListModel]", 100, false);
940             this.newColumn("max_ratio", "", "QBT_TR(Ratio Limit)QBT_TR[CONTEXT=TransferListModel]", 100, false);
941             this.newColumn("seen_complete", "", "QBT_TR(Last Seen Complete)QBT_TR[CONTEXT=TransferListModel]", 100, false);
942             this.newColumn("last_activity", "", "QBT_TR(Last Activity)QBT_TR[CONTEXT=TransferListModel]", 100, false);
943             this.newColumn("availability", "", "QBT_TR(Availability)QBT_TR[CONTEXT=TransferListModel]", 100, false);
944             this.newColumn("download_path", "", "QBT_TR(Incomplete Save Path)QBT_TR[CONTEXT=TransferListModel]", 100, false);
945             this.newColumn("infohash_v1", "", "QBT_TR(Info Hash v1)QBT_TR[CONTEXT=TransferListModel]", 100, false);
946             this.newColumn("infohash_v2", "", "QBT_TR(Info Hash v2)QBT_TR[CONTEXT=TransferListModel]", 100, false);
947             this.newColumn("reannounce", "", "QBT_TR(Reannounce In)QBT_TR[CONTEXT=TransferListModel]", 100, false);
948             this.newColumn("private", "", "QBT_TR(Private)QBT_TR[CONTEXT=TransferListModel]", 100, false);
950             this.columns["state_icon"].onclick = "";
951             this.columns["state_icon"].dataProperties[0] = "state";
953             this.columns["num_seeds"].dataProperties.push("num_complete");
954             this.columns["num_leechs"].dataProperties.push("num_incomplete");
955             this.columns["time_active"].dataProperties.push("seeding_time");
957             this.initColumnsFunctions();
958         },
960         initColumnsFunctions: function() {
962             // state_icon
963             this.columns["state_icon"].updateTd = function(td, row) {
964                 let state = this.getRowValue(row);
965                 let img_path;
966                 // normalize states
967                 switch (state) {
968                     case "forcedDL":
969                     case "metaDL":
970                     case "forcedMetaDL":
971                     case "downloading":
972                         state = "downloading";
973                         img_path = "images/downloading.svg";
974                         break;
975                     case "forcedUP":
976                     case "uploading":
977                         state = "uploading";
978                         img_path = "images/upload.svg";
979                         break;
980                     case "stalledUP":
981                         state = "stalledUP";
982                         img_path = "images/stalledUP.svg";
983                         break;
984                     case "stalledDL":
985                         state = "stalledDL";
986                         img_path = "images/stalledDL.svg";
987                         break;
988                     case "stoppedDL":
989                         state = "torrent-stop";
990                         img_path = "images/stopped.svg";
991                         break;
992                     case "stoppedUP":
993                         state = "checked-completed";
994                         img_path = "images/checked-completed.svg";
995                         break;
996                     case "queuedDL":
997                     case "queuedUP":
998                         state = "queued";
999                         img_path = "images/queued.svg";
1000                         break;
1001                     case "checkingDL":
1002                     case "checkingUP":
1003                     case "queuedForChecking":
1004                     case "checkingResumeData":
1005                         state = "force-recheck";
1006                         img_path = "images/force-recheck.svg";
1007                         break;
1008                     case "moving":
1009                         state = "moving";
1010                         img_path = "images/set-location.svg";
1011                         break;
1012                     case "error":
1013                     case "unknown":
1014                     case "missingFiles":
1015                         state = "error";
1016                         img_path = "images/error.svg";
1017                         break;
1018                     default:
1019                         break; // do nothing
1020                 }
1022                 if (td.getChildren("img").length > 0) {
1023                     const img = td.getChildren("img")[0];
1024                     if (!img.src.includes(img_path)) {
1025                         img.src = img_path;
1026                         img.title = state;
1027                     }
1028                 }
1029                 else {
1030                     td.adopt(new Element("img", {
1031                         "src": img_path,
1032                         "class": "stateIcon",
1033                         "title": state
1034                     }));
1035                 }
1036             };
1038             // status
1039             this.columns["status"].updateTd = function(td, row) {
1040                 const state = this.getRowValue(row);
1041                 if (!state)
1042                     return;
1044                 let status;
1045                 switch (state) {
1046                     case "downloading":
1047                         status = "QBT_TR(Downloading)QBT_TR[CONTEXT=TransferListDelegate]";
1048                         break;
1049                     case "stalledDL":
1050                         status = "QBT_TR(Stalled)QBT_TR[CONTEXT=TransferListDelegate]";
1051                         break;
1052                     case "metaDL":
1053                         status = "QBT_TR(Downloading metadata)QBT_TR[CONTEXT=TransferListDelegate]";
1054                         break;
1055                     case "forcedMetaDL":
1056                         status = "QBT_TR([F] Downloading metadata)QBT_TR[CONTEXT=TransferListDelegate]";
1057                         break;
1058                     case "forcedDL":
1059                         status = "QBT_TR([F] Downloading)QBT_TR[CONTEXT=TransferListDelegate]";
1060                         break;
1061                     case "uploading":
1062                     case "stalledUP":
1063                         status = "QBT_TR(Seeding)QBT_TR[CONTEXT=TransferListDelegate]";
1064                         break;
1065                     case "forcedUP":
1066                         status = "QBT_TR([F] Seeding)QBT_TR[CONTEXT=TransferListDelegate]";
1067                         break;
1068                     case "queuedDL":
1069                     case "queuedUP":
1070                         status = "QBT_TR(Queued)QBT_TR[CONTEXT=TransferListDelegate]";
1071                         break;
1072                     case "checkingDL":
1073                     case "checkingUP":
1074                         status = "QBT_TR(Checking)QBT_TR[CONTEXT=TransferListDelegate]";
1075                         break;
1076                     case "queuedForChecking":
1077                         status = "QBT_TR(Queued for checking)QBT_TR[CONTEXT=TransferListDelegate]";
1078                         break;
1079                     case "checkingResumeData":
1080                         status = "QBT_TR(Checking resume data)QBT_TR[CONTEXT=TransferListDelegate]";
1081                         break;
1082                     case "stoppedDL":
1083                         status = "QBT_TR(Stopped)QBT_TR[CONTEXT=TransferListDelegate]";
1084                         break;
1085                     case "stoppedUP":
1086                         status = "QBT_TR(Completed)QBT_TR[CONTEXT=TransferListDelegate]";
1087                         break;
1088                     case "moving":
1089                         status = "QBT_TR(Moving)QBT_TR[CONTEXT=TransferListDelegate]";
1090                         break;
1091                     case "missingFiles":
1092                         status = "QBT_TR(Missing Files)QBT_TR[CONTEXT=TransferListDelegate]";
1093                         break;
1094                     case "error":
1095                         status = "QBT_TR(Errored)QBT_TR[CONTEXT=TransferListDelegate]";
1096                         break;
1097                     default:
1098                         status = "QBT_TR(Unknown)QBT_TR[CONTEXT=HttpServer]";
1099                 }
1101                 td.textContent = status;
1102                 td.title = status;
1103             };
1105             this.columns["status"].compareRows = function(row1, row2) {
1106                 return compareNumbers(row1.full_data._statusOrder, row2.full_data._statusOrder);
1107             };
1109             // priority
1110             this.columns["priority"].updateTd = function(td, row) {
1111                 const queuePos = this.getRowValue(row);
1112                 const formattedQueuePos = (queuePos < 1) ? "*" : queuePos;
1113                 td.textContent = formattedQueuePos;
1114                 td.title = formattedQueuePos;
1115             };
1117             this.columns["priority"].compareRows = function(row1, row2) {
1118                 let row1_val = this.getRowValue(row1);
1119                 let row2_val = this.getRowValue(row2);
1120                 if (row1_val < 1)
1121                     row1_val = 1000000;
1122                 if (row2_val < 1)
1123                     row2_val = 1000000;
1124                 return compareNumbers(row1_val, row2_val);
1125             };
1127             // name, category, tags
1128             this.columns["name"].compareRows = function(row1, row2) {
1129                 const row1Val = this.getRowValue(row1);
1130                 const row2Val = this.getRowValue(row2);
1131                 return row1Val.localeCompare(row2Val, undefined, { numeric: true, sensitivity: "base" });
1132             };
1133             this.columns["category"].compareRows = this.columns["name"].compareRows;
1134             this.columns["tags"].compareRows = this.columns["name"].compareRows;
1136             // size, total_size
1137             this.columns["size"].updateTd = function(td, row) {
1138                 const size = window.qBittorrent.Misc.friendlyUnit(this.getRowValue(row), false);
1139                 td.textContent = size;
1140                 td.title = size;
1141             };
1142             this.columns["total_size"].updateTd = this.columns["size"].updateTd;
1144             // progress
1145             this.columns["progress"].updateTd = function(td, row) {
1146                 const progress = this.getRowValue(row);
1147                 let progressFormatted = (progress * 100).round(1);
1148                 if ((progressFormatted === 100.0) && (progress !== 1.0))
1149                     progressFormatted = 99.9;
1151                 if (td.getChildren("div").length > 0) {
1152                     const div = td.getChildren("div")[0];
1153                     if (td.resized) {
1154                         td.resized = false;
1155                         div.setWidth(ProgressColumnWidth - 5);
1156                     }
1157                     if (div.getValue() !== progressFormatted)
1158                         div.setValue(progressFormatted);
1159                 }
1160                 else {
1161                     if (ProgressColumnWidth < 0)
1162                         ProgressColumnWidth = td.offsetWidth;
1163                     td.adopt(new window.qBittorrent.ProgressBar.ProgressBar(progressFormatted.toFloat(), {
1164                         "width": ProgressColumnWidth - 5
1165                     }));
1166                     td.resized = false;
1167                 }
1168             };
1170             this.columns["progress"].onResize = function(columnName) {
1171                 const pos = this.getColumnPos(columnName);
1172                 const trs = this.tableBody.getElements("tr");
1173                 ProgressColumnWidth = -1;
1174                 for (let i = 0; i < trs.length; ++i) {
1175                     const td = trs[i].getElements("td")[pos];
1176                     if (ProgressColumnWidth < 0)
1177                         ProgressColumnWidth = td.offsetWidth;
1178                     td.resized = true;
1179                     this.columns[columnName].updateTd(td, this.rows.get(trs[i].rowId));
1180                 }
1181             }.bind(this);
1183             // num_seeds
1184             this.columns["num_seeds"].updateTd = function(td, row) {
1185                 const num_seeds = this.getRowValue(row, 0);
1186                 const num_complete = this.getRowValue(row, 1);
1187                 let value = num_seeds;
1188                 if (num_complete !== -1)
1189                     value += " (" + num_complete + ")";
1190                 td.textContent = value;
1191                 td.title = value;
1192             };
1193             this.columns["num_seeds"].compareRows = function(row1, row2) {
1194                 const num_seeds1 = this.getRowValue(row1, 0);
1195                 const num_complete1 = this.getRowValue(row1, 1);
1197                 const num_seeds2 = this.getRowValue(row2, 0);
1198                 const num_complete2 = this.getRowValue(row2, 1);
1200                 const result = compareNumbers(num_complete1, num_complete2);
1201                 if (result !== 0)
1202                     return result;
1203                 return compareNumbers(num_seeds1, num_seeds2);
1204             };
1206             // num_leechs
1207             this.columns["num_leechs"].updateTd = this.columns["num_seeds"].updateTd;
1208             this.columns["num_leechs"].compareRows = this.columns["num_seeds"].compareRows;
1210             // dlspeed
1211             this.columns["dlspeed"].updateTd = function(td, row) {
1212                 const speed = window.qBittorrent.Misc.friendlyUnit(this.getRowValue(row), true);
1213                 td.textContent = speed;
1214                 td.title = speed;
1215             };
1217             // upspeed
1218             this.columns["upspeed"].updateTd = this.columns["dlspeed"].updateTd;
1220             // eta
1221             this.columns["eta"].updateTd = function(td, row) {
1222                 const eta = window.qBittorrent.Misc.friendlyDuration(this.getRowValue(row), window.qBittorrent.Misc.MAX_ETA);
1223                 td.textContent = eta;
1224                 td.title = eta;
1225             };
1227             // ratio
1228             this.columns["ratio"].updateTd = function(td, row) {
1229                 const ratio = this.getRowValue(row);
1230                 const string = (ratio === -1) ? "∞" : window.qBittorrent.Misc.toFixedPointString(ratio, 2);
1231                 td.textContent = string;
1232                 td.title = string;
1233             };
1235             // popularity
1236             this.columns["popularity"].updateTd = function(td, row) {
1237                 const value = this.getRowValue(row);
1238                 const popularity = (value === -1) ? "∞" : window.qBittorrent.Misc.toFixedPointString(value, 2);
1239                 td.textContent = popularity;
1240                 td.title = popularity;
1241             };
1243             // added on
1244             this.columns["added_on"].updateTd = function(td, row) {
1245                 const date = new Date(this.getRowValue(row) * 1000).toLocaleString();
1246                 td.textContent = date;
1247                 td.title = date;
1248             };
1250             // completion_on
1251             this.columns["completion_on"].updateTd = function(td, row) {
1252                 const val = this.getRowValue(row);
1253                 if ((val === 0xffffffff) || (val < 0)) {
1254                     td.textContent = "";
1255                     td.title = "";
1256                 }
1257                 else {
1258                     const date = new Date(this.getRowValue(row) * 1000).toLocaleString();
1259                     td.textContent = date;
1260                     td.title = date;
1261                 }
1262             };
1264             // tracker
1265             this.columns["tracker"].updateTd = function(td, row) {
1266                 const value = this.getRowValue(row);
1267                 const tracker = displayFullURLTrackerColumn ? value : window.qBittorrent.Misc.getHost(value);
1268                 td.textContent = tracker;
1269                 td.title = value;
1270             };
1272             //  dl_limit, up_limit
1273             this.columns["dl_limit"].updateTd = function(td, row) {
1274                 const speed = this.getRowValue(row);
1275                 if (speed === 0) {
1276                     td.textContent = "∞";
1277                     td.title = "∞";
1278                 }
1279                 else {
1280                     const formattedSpeed = window.qBittorrent.Misc.friendlyUnit(speed, true);
1281                     td.textContent = formattedSpeed;
1282                     td.title = formattedSpeed;
1283                 }
1284             };
1286             this.columns["up_limit"].updateTd = this.columns["dl_limit"].updateTd;
1288             // downloaded, uploaded, downloaded_session, uploaded_session, amount_left
1289             this.columns["downloaded"].updateTd = this.columns["size"].updateTd;
1290             this.columns["uploaded"].updateTd = this.columns["size"].updateTd;
1291             this.columns["downloaded_session"].updateTd = this.columns["size"].updateTd;
1292             this.columns["uploaded_session"].updateTd = this.columns["size"].updateTd;
1293             this.columns["amount_left"].updateTd = this.columns["size"].updateTd;
1295             // time active
1296             this.columns["time_active"].updateTd = function(td, row) {
1297                 const activeTime = this.getRowValue(row, 0);
1298                 const seedingTime = this.getRowValue(row, 1);
1299                 const time = (seedingTime > 0)
1300                     ? ("QBT_TR(%1 (seeded for %2))QBT_TR[CONTEXT=TransferListDelegate]"
1301                         .replace("%1", window.qBittorrent.Misc.friendlyDuration(activeTime))
1302                         .replace("%2", window.qBittorrent.Misc.friendlyDuration(seedingTime)))
1303                     : window.qBittorrent.Misc.friendlyDuration(activeTime);
1304                 td.textContent = time;
1305                 td.title = time;
1306             };
1308             // completed
1309             this.columns["completed"].updateTd = this.columns["size"].updateTd;
1311             // max_ratio
1312             this.columns["max_ratio"].updateTd = this.columns["ratio"].updateTd;
1314             // seen_complete
1315             this.columns["seen_complete"].updateTd = this.columns["completion_on"].updateTd;
1317             // last_activity
1318             this.columns["last_activity"].updateTd = function(td, row) {
1319                 const val = this.getRowValue(row);
1320                 if (val < 1) {
1321                     td.textContent = "∞";
1322                     td.title = "∞";
1323                 }
1324                 else {
1325                     const formattedVal = "QBT_TR(%1 ago)QBT_TR[CONTEXT=TransferListDelegate]".replace("%1", window.qBittorrent.Misc.friendlyDuration((new Date() / 1000) - val));
1326                     td.textContent = formattedVal;
1327                     td.title = formattedVal;
1328                 }
1329             };
1331             // availability
1332             this.columns["availability"].updateTd = function(td, row) {
1333                 const value = window.qBittorrent.Misc.toFixedPointString(this.getRowValue(row), 3);
1334                 td.textContent = value;
1335                 td.title = value;
1336             };
1338             // infohash_v1
1339             this.columns["infohash_v1"].updateTd = function(td, row) {
1340                 const sourceInfohashV1 = this.getRowValue(row);
1341                 const infohashV1 = (sourceInfohashV1 !== "") ? sourceInfohashV1 : "QBT_TR(N/A)QBT_TR[CONTEXT=TransferListDelegate]";
1342                 td.textContent = infohashV1;
1343                 td.title = infohashV1;
1344             };
1346             // infohash_v2
1347             this.columns["infohash_v2"].updateTd = function(td, row) {
1348                 const sourceInfohashV2 = this.getRowValue(row);
1349                 const infohashV2 = (sourceInfohashV2 !== "") ? sourceInfohashV2 : "QBT_TR(N/A)QBT_TR[CONTEXT=TransferListDelegate]";
1350                 td.textContent = infohashV2;
1351                 td.title = infohashV2;
1352             };
1354             // reannounce
1355             this.columns["reannounce"].updateTd = function(td, row) {
1356                 const time = window.qBittorrent.Misc.friendlyDuration(this.getRowValue(row));
1357                 td.textContent = time;
1358                 td.title = time;
1359             };
1361             // private
1362             this.columns["private"].updateTd = function(td, row) {
1363                 const hasMetadata = row["full_data"].has_metadata;
1364                 const isPrivate = this.getRowValue(row);
1365                 const string = hasMetadata
1366                     ? (isPrivate
1367                         ? "QBT_TR(Yes)QBT_TR[CONTEXT=PropertiesWidget]"
1368                         : "QBT_TR(No)QBT_TR[CONTEXT=PropertiesWidget]")
1369                     : "QBT_TR(N/A)QBT_TR[CONTEXT=PropertiesWidget]";
1370                 td.textContent = string;
1371                 td.title = string;
1372             };
1373         },
1375         applyFilter: function(row, filterName, categoryHash, tagHash, trackerHash, filterTerms) {
1376             const state = row["full_data"].state;
1377             let inactive = false;
1379             switch (filterName) {
1380                 case "downloading":
1381                     if ((state !== "downloading") && !state.includes("DL"))
1382                         return false;
1383                     break;
1384                 case "seeding":
1385                     if ((state !== "uploading") && (state !== "forcedUP") && (state !== "stalledUP") && (state !== "queuedUP") && (state !== "checkingUP"))
1386                         return false;
1387                     break;
1388                 case "completed":
1389                     if ((state !== "uploading") && !state.includes("UP"))
1390                         return false;
1391                     break;
1392                 case "stopped":
1393                     if (!state.includes("stopped"))
1394                         return false;
1395                     break;
1396                 case "running":
1397                     if (state.includes("stopped"))
1398                         return false;
1399                     break;
1400                 case "stalled":
1401                     if ((state !== "stalledUP") && (state !== "stalledDL"))
1402                         return false;
1403                     break;
1404                 case "stalled_uploading":
1405                     if (state !== "stalledUP")
1406                         return false;
1407                     break;
1408                 case "stalled_downloading":
1409                     if (state !== "stalledDL")
1410                         return false;
1411                     break;
1412                 case "inactive":
1413                     inactive = true;
1414                     // fallthrough
1415                 case "active": {
1416                     let r;
1417                     if (state === "stalledDL")
1418                         r = (row["full_data"].upspeed > 0);
1419                     else
1420                         r = (state === "metaDL") || (state === "forcedMetaDL") || (state === "downloading") || (state === "forcedDL") || (state === "uploading") || (state === "forcedUP");
1421                     if (r === inactive)
1422                         return false;
1423                     break;
1424                 }
1425                 case "checking":
1426                     if ((state !== "checkingUP") && (state !== "checkingDL") && (state !== "checkingResumeData"))
1427                         return false;
1428                     break;
1429                 case "moving":
1430                     if (state !== "moving")
1431                         return false;
1432                     break;
1433                 case "errored":
1434                     if ((state !== "error") && (state !== "unknown") && (state !== "missingFiles"))
1435                         return false;
1436                     break;
1437             }
1439             switch (categoryHash) {
1440                 case CATEGORIES_ALL:
1441                     break; // do nothing
1442                 case CATEGORIES_UNCATEGORIZED:
1443                     if (row["full_data"].category.length !== 0)
1444                         return false;
1445                     break; // do nothing
1446                 default:
1447                     if (!useSubcategories) {
1448                         if (categoryHash !== window.qBittorrent.Misc.genHash(row["full_data"].category))
1449                             return false;
1450                     }
1451                     else {
1452                         const selectedCategory = category_list.get(categoryHash);
1453                         if (selectedCategory !== undefined) {
1454                             const selectedCategoryName = selectedCategory.name + "/";
1455                             const torrentCategoryName = row["full_data"].category + "/";
1456                             if (!torrentCategoryName.startsWith(selectedCategoryName))
1457                                 return false;
1458                         }
1459                     }
1460                     break;
1461             }
1463             switch (tagHash) {
1464                 case TAGS_ALL:
1465                     break; // do nothing
1467                 case TAGS_UNTAGGED:
1468                     if (row["full_data"].tags.length !== 0)
1469                         return false;
1470                     break; // do nothing
1472                 default: {
1473                     const tagHashes = row["full_data"].tags.split(", ").map(tag => window.qBittorrent.Misc.genHash(tag));
1474                     if (!tagHashes.contains(tagHash))
1475                         return false;
1476                     break;
1477                 }
1478             }
1480             switch (trackerHash) {
1481                 case TRACKERS_ALL:
1482                     break; // do nothing
1483                 case TRACKERS_TRACKERLESS:
1484                     if (row["full_data"].trackers_count !== 0)
1485                         return false;
1486                     break;
1487                 default: {
1488                     const tracker = trackerList.get(trackerHash);
1489                     if (tracker) {
1490                         let found = false;
1491                         for (const torrents of tracker.trackerTorrentMap.values()) {
1492                             if (torrents.has(row["full_data"].rowId)) {
1493                                 found = true;
1494                                 break;
1495                             }
1496                         }
1497                         if (!found)
1498                             return false;
1499                     }
1500                     break;
1501                 }
1502             }
1504             if ((filterTerms !== undefined) && (filterTerms !== null)) {
1505                 const filterBy = document.getElementById("torrentsFilterSelect").value;
1506                 const textToSearch = row["full_data"][filterBy].toLowerCase();
1507                 if (filterTerms instanceof RegExp) {
1508                     if (!filterTerms.test(textToSearch))
1509                         return false;
1510                 }
1511                 else {
1512                     if ((filterTerms.length > 0) && !window.qBittorrent.Misc.containsAllTerms(textToSearch, filterTerms))
1513                         return false;
1514                 }
1515             }
1517             return true;
1518         },
1520         getFilteredTorrentsNumber: function(filterName, categoryHash, tagHash, trackerHash) {
1521             let cnt = 0;
1523             for (const row of this.rows.values()) {
1524                 if (this.applyFilter(row, filterName, categoryHash, tagHash, trackerHash, null))
1525                     ++cnt;
1526             }
1527             return cnt;
1528         },
1530         getFilteredTorrentsHashes: function(filterName, categoryHash, tagHash, trackerHash) {
1531             const rowsHashes = [];
1532             const useRegex = document.getElementById("torrentsFilterRegexBox").checked;
1533             const filterText = document.getElementById("torrentsFilterInput").value.trim().toLowerCase();
1534             let filterTerms;
1535             try {
1536                 filterTerms = (filterText.length > 0)
1537                     ? (useRegex ? new RegExp(filterText) : filterText.split(" "))
1538                     : null;
1539             }
1540             catch (e) { // SyntaxError: Invalid regex pattern
1541                 return filteredRows;
1542             }
1544             for (const row of this.rows.values()) {
1545                 if (this.applyFilter(row, filterName, categoryHash, tagHash, trackerHash, filterTerms))
1546                     rowsHashes.push(row["rowId"]);
1547             }
1549             return rowsHashes;
1550         },
1552         getFilteredAndSortedRows: function() {
1553             const filteredRows = [];
1555             const useRegex = $("torrentsFilterRegexBox").checked;
1556             const filterText = $("torrentsFilterInput").value.trim().toLowerCase();
1557             let filterTerms;
1558             try {
1559                 filterTerms = (filterText.length > 0)
1560                     ? (useRegex ? new RegExp(filterText) : filterText.split(" "))
1561                     : null;
1562             }
1563             catch (e) { // SyntaxError: Invalid regex pattern
1564                 return filteredRows;
1565             }
1567             for (const row of this.rows.values()) {
1568                 if (this.applyFilter(row, selectedStatus, selectedCategory, selectedTag, selectedTracker, filterTerms)) {
1569                     filteredRows.push(row);
1570                     filteredRows[row.rowId] = row;
1571                 }
1572             }
1574             filteredRows.sort((row1, row2) => {
1575                 const column = this.columns[this.sortedColumn];
1576                 const res = column.compareRows(row1, row2);
1577                 if (this.reverseSort === "0")
1578                     return res;
1579                 else
1580                     return -res;
1581             });
1582             return filteredRows;
1583         },
1585         setupTr: function(tr) {
1586             tr.addEventListener("dblclick", function(e) {
1587                 e.preventDefault();
1588                 e.stopPropagation();
1590                 this._this.deselectAll();
1591                 this._this.selectRow(this.rowId);
1592                 const row = this._this.rows.get(this.rowId);
1593                 const state = row["full_data"].state;
1595                 const prefKey =
1596                     (state !== "uploading")
1597                     && (state !== "stoppedUP")
1598                     && (state !== "forcedUP")
1599                     && (state !== "stalledUP")
1600                     && (state !== "queuedUP")
1601                     && (state !== "checkingUP")
1602                     ? "dblclick_download"
1603                     : "dblclick_complete";
1605                 if (LocalPreferences.get(prefKey, "1") !== "1")
1606                     return true;
1608                 if (state.includes("stopped"))
1609                     startFN();
1610                 else
1611                     stopFN();
1612                 return true;
1613             });
1614             tr.addClass("torrentsTableContextMenuTarget");
1615         },
1617         getCurrentTorrentID: function() {
1618             return this.getSelectedRowId();
1619         },
1621         onSelectedRowChanged: function() {
1622             updatePropertiesPanel();
1623         }
1624     });
1626     const TorrentPeersTable = new Class({
1627         Extends: DynamicTable,
1629         initColumns: function() {
1630             this.newColumn("country", "", "QBT_TR(Country/Region)QBT_TR[CONTEXT=PeerListWidget]", 22, true);
1631             this.newColumn("ip", "", "QBT_TR(IP)QBT_TR[CONTEXT=PeerListWidget]", 80, true);
1632             this.newColumn("port", "", "QBT_TR(Port)QBT_TR[CONTEXT=PeerListWidget]", 35, true);
1633             this.newColumn("connection", "", "QBT_TR(Connection)QBT_TR[CONTEXT=PeerListWidget]", 50, true);
1634             this.newColumn("flags", "", "QBT_TR(Flags)QBT_TR[CONTEXT=PeerListWidget]", 50, true);
1635             this.newColumn("client", "", "QBT_TR(Client)QBT_TR[CONTEXT=PeerListWidget]", 140, true);
1636             this.newColumn("peer_id_client", "", "QBT_TR(Peer ID Client)QBT_TR[CONTEXT=PeerListWidget]", 60, false);
1637             this.newColumn("progress", "", "QBT_TR(Progress)QBT_TR[CONTEXT=PeerListWidget]", 50, true);
1638             this.newColumn("dl_speed", "", "QBT_TR(Down Speed)QBT_TR[CONTEXT=PeerListWidget]", 50, true);
1639             this.newColumn("up_speed", "", "QBT_TR(Up Speed)QBT_TR[CONTEXT=PeerListWidget]", 50, true);
1640             this.newColumn("downloaded", "", "QBT_TR(Downloaded)QBT_TR[CONTEXT=PeerListWidget]", 50, true);
1641             this.newColumn("uploaded", "", "QBT_TR(Uploaded)QBT_TR[CONTEXT=PeerListWidget]", 50, true);
1642             this.newColumn("relevance", "", "QBT_TR(Relevance)QBT_TR[CONTEXT=PeerListWidget]", 30, true);
1643             this.newColumn("files", "", "QBT_TR(Files)QBT_TR[CONTEXT=PeerListWidget]", 100, true);
1645             this.columns["country"].dataProperties.push("country_code");
1646             this.columns["flags"].dataProperties.push("flags_desc");
1647             this.initColumnsFunctions();
1648         },
1650         initColumnsFunctions: function() {
1652             // country
1653             this.columns["country"].updateTd = function(td, row) {
1654                 const country = this.getRowValue(row, 0);
1655                 const country_code = this.getRowValue(row, 1);
1657                 let span = td.firstElementChild;
1658                 if (span === null) {
1659                     span = document.createElement("span");
1660                     span.classList.add("flags");
1661                     td.append(span);
1662                 }
1664                 span.style.backgroundImage = `url('images/flags/${country_code ?? "xx"}.svg')`;
1665                 span.textContent = country;
1666                 td.title = country;
1667             };
1669             // ip
1670             this.columns["ip"].compareRows = function(row1, row2) {
1671                 const ip1 = this.getRowValue(row1);
1672                 const ip2 = this.getRowValue(row2);
1674                 const a = ip1.split(".");
1675                 const b = ip2.split(".");
1677                 for (let i = 0; i < 4; ++i) {
1678                     if (a[i] !== b[i])
1679                         return a[i] - b[i];
1680                 }
1682                 return 0;
1683             };
1685             // flags
1686             this.columns["flags"].updateTd = function(td, row) {
1687                 td.textContent = this.getRowValue(row, 0);
1688                 td.title = this.getRowValue(row, 1);
1689             };
1691             // progress
1692             this.columns["progress"].updateTd = function(td, row) {
1693                 const progress = this.getRowValue(row);
1694                 let progressFormatted = (progress * 100).round(1);
1695                 if ((progressFormatted === 100.0) && (progress !== 1.0))
1696                     progressFormatted = 99.9;
1697                 progressFormatted += "%";
1698                 td.textContent = progressFormatted;
1699                 td.title = progressFormatted;
1700             };
1702             // dl_speed, up_speed
1703             this.columns["dl_speed"].updateTd = function(td, row) {
1704                 const speed = this.getRowValue(row);
1705                 if (speed === 0) {
1706                     td.textContent = "";
1707                     td.title = "";
1708                 }
1709                 else {
1710                     const formattedSpeed = window.qBittorrent.Misc.friendlyUnit(speed, true);
1711                     td.textContent = formattedSpeed;
1712                     td.title = formattedSpeed;
1713                 }
1714             };
1715             this.columns["up_speed"].updateTd = this.columns["dl_speed"].updateTd;
1717             // downloaded, uploaded
1718             this.columns["downloaded"].updateTd = function(td, row) {
1719                 const downloaded = window.qBittorrent.Misc.friendlyUnit(this.getRowValue(row), false);
1720                 td.textContent = downloaded;
1721                 td.title = downloaded;
1722             };
1723             this.columns["uploaded"].updateTd = this.columns["downloaded"].updateTd;
1725             // relevance
1726             this.columns["relevance"].updateTd = this.columns["progress"].updateTd;
1728             // files
1729             this.columns["files"].updateTd = function(td, row) {
1730                 const value = this.getRowValue(row, 0);
1731                 td.textContent = value.replace(/\n/g, ";");
1732                 td.title = value;
1733             };
1735         }
1736     });
1738     const SearchResultsTable = new Class({
1739         Extends: DynamicTable,
1741         initColumns: function() {
1742             this.newColumn("fileName", "", "QBT_TR(Name)QBT_TR[CONTEXT=SearchResultsTable]", 500, true);
1743             this.newColumn("fileSize", "", "QBT_TR(Size)QBT_TR[CONTEXT=SearchResultsTable]", 100, true);
1744             this.newColumn("nbSeeders", "", "QBT_TR(Seeders)QBT_TR[CONTEXT=SearchResultsTable]", 100, true);
1745             this.newColumn("nbLeechers", "", "QBT_TR(Leechers)QBT_TR[CONTEXT=SearchResultsTable]", 100, true);
1746             this.newColumn("engineName", "", "QBT_TR(Engine)QBT_TR[CONTEXT=SearchResultsTable]", 100, true);
1747             this.newColumn("siteUrl", "", "QBT_TR(Engine URL)QBT_TR[CONTEXT=SearchResultsTable]", 250, true);
1748             this.newColumn("pubDate", "", "QBT_TR(Published On)QBT_TR[CONTEXT=SearchResultsTable]", 200, true);
1750             this.initColumnsFunctions();
1751         },
1753         initColumnsFunctions: function() {
1754             const displaySize = function(td, row) {
1755                 const size = window.qBittorrent.Misc.friendlyUnit(this.getRowValue(row), false);
1756                 td.textContent = size;
1757                 td.title = size;
1758             };
1759             const displayNum = function(td, row) {
1760                 const value = this.getRowValue(row);
1761                 const formattedValue = (value === "-1") ? "Unknown" : value;
1762                 td.textContent = formattedValue;
1763                 td.title = formattedValue;
1764             };
1765             const displayDate = function(td, row) {
1766                 const value = this.getRowValue(row) * 1000;
1767                 const formattedValue = (isNaN(value) || (value <= 0)) ? "" : (new Date(value).toLocaleString());
1768                 td.textContent = formattedValue;
1769                 td.title = formattedValue;
1770             };
1772             this.columns["fileSize"].updateTd = displaySize;
1773             this.columns["nbSeeders"].updateTd = displayNum;
1774             this.columns["nbLeechers"].updateTd = displayNum;
1775             this.columns["pubDate"].updateTd = displayDate;
1776         },
1778         getFilteredAndSortedRows: function() {
1779             const getSizeFilters = function() {
1780                 let minSize = (window.qBittorrent.Search.searchSizeFilter.min > 0.00) ? (window.qBittorrent.Search.searchSizeFilter.min * Math.pow(1024, window.qBittorrent.Search.searchSizeFilter.minUnit)) : 0.00;
1781                 let maxSize = (window.qBittorrent.Search.searchSizeFilter.max > 0.00) ? (window.qBittorrent.Search.searchSizeFilter.max * Math.pow(1024, window.qBittorrent.Search.searchSizeFilter.maxUnit)) : 0.00;
1783                 if ((minSize > maxSize) && (maxSize > 0.00)) {
1784                     const tmp = minSize;
1785                     minSize = maxSize;
1786                     maxSize = tmp;
1787                 }
1789                 return {
1790                     min: minSize,
1791                     max: maxSize
1792                 };
1793             };
1795             const getSeedsFilters = function() {
1796                 let minSeeds = (window.qBittorrent.Search.searchSeedsFilter.min > 0) ? window.qBittorrent.Search.searchSeedsFilter.min : 0;
1797                 let maxSeeds = (window.qBittorrent.Search.searchSeedsFilter.max > 0) ? window.qBittorrent.Search.searchSeedsFilter.max : 0;
1799                 if ((minSeeds > maxSeeds) && (maxSeeds > 0)) {
1800                     const tmp = minSeeds;
1801                     minSeeds = maxSeeds;
1802                     maxSeeds = tmp;
1803                 }
1805                 return {
1806                     min: minSeeds,
1807                     max: maxSeeds
1808                 };
1809             };
1811             let filteredRows = [];
1812             const searchTerms = window.qBittorrent.Search.searchText.pattern.toLowerCase().split(" ");
1813             const filterTerms = window.qBittorrent.Search.searchText.filterPattern.toLowerCase().split(" ");
1814             const sizeFilters = getSizeFilters();
1815             const seedsFilters = getSeedsFilters();
1816             const searchInTorrentName = $("searchInTorrentName").value === "names";
1818             if (searchInTorrentName || (filterTerms.length > 0) || (window.qBittorrent.Search.searchSizeFilter.min > 0.00) || (window.qBittorrent.Search.searchSizeFilter.max > 0.00)) {
1819                 for (const row of this.getRowValues()) {
1821                     if (searchInTorrentName && !window.qBittorrent.Misc.containsAllTerms(row.full_data.fileName, searchTerms))
1822                         continue;
1823                     if ((filterTerms.length > 0) && !window.qBittorrent.Misc.containsAllTerms(row.full_data.fileName, filterTerms))
1824                         continue;
1825                     if ((sizeFilters.min > 0.00) && (row.full_data.fileSize < sizeFilters.min))
1826                         continue;
1827                     if ((sizeFilters.max > 0.00) && (row.full_data.fileSize > sizeFilters.max))
1828                         continue;
1829                     if ((seedsFilters.min > 0) && (row.full_data.nbSeeders < seedsFilters.min))
1830                         continue;
1831                     if ((seedsFilters.max > 0) && (row.full_data.nbSeeders > seedsFilters.max))
1832                         continue;
1834                     filteredRows.push(row);
1835                 }
1836             }
1837             else {
1838                 filteredRows = [...this.getRowValues()];
1839             }
1841             filteredRows.sort((row1, row2) => {
1842                 const column = this.columns[this.sortedColumn];
1843                 const res = column.compareRows(row1, row2);
1844                 if (this.reverseSort === "0")
1845                     return res;
1846                 else
1847                     return -res;
1848             });
1850             return filteredRows;
1851         },
1853         setupTr: function(tr) {
1854             tr.addClass("searchTableRow");
1855         }
1856     });
1858     const SearchPluginsTable = new Class({
1859         Extends: DynamicTable,
1861         initColumns: function() {
1862             this.newColumn("fullName", "", "QBT_TR(Name)QBT_TR[CONTEXT=SearchPluginsTable]", 175, true);
1863             this.newColumn("version", "", "QBT_TR(Version)QBT_TR[CONTEXT=SearchPluginsTable]", 100, true);
1864             this.newColumn("url", "", "QBT_TR(Url)QBT_TR[CONTEXT=SearchPluginsTable]", 175, true);
1865             this.newColumn("enabled", "", "QBT_TR(Enabled)QBT_TR[CONTEXT=SearchPluginsTable]", 100, true);
1867             this.initColumnsFunctions();
1868         },
1870         initColumnsFunctions: function() {
1871             this.columns["enabled"].updateTd = function(td, row) {
1872                 const value = this.getRowValue(row);
1873                 if (value) {
1874                     td.textContent = "QBT_TR(Yes)QBT_TR[CONTEXT=SearchPluginsTable]";
1875                     td.title = "QBT_TR(Yes)QBT_TR[CONTEXT=SearchPluginsTable]";
1876                     td.getParent("tr").addClass("green");
1877                     td.getParent("tr").removeClass("red");
1878                 }
1879                 else {
1880                     td.textContent = "QBT_TR(No)QBT_TR[CONTEXT=SearchPluginsTable]";
1881                     td.title = "QBT_TR(No)QBT_TR[CONTEXT=SearchPluginsTable]";
1882                     td.getParent("tr").addClass("red");
1883                     td.getParent("tr").removeClass("green");
1884                 }
1885             };
1886         },
1888         setupTr: function(tr) {
1889             tr.addClass("searchPluginsTableRow");
1890         }
1891     });
1893     const TorrentTrackersTable = new Class({
1894         Extends: DynamicTable,
1896         initColumns: function() {
1897             this.newColumn("tier", "", "QBT_TR(Tier)QBT_TR[CONTEXT=TrackerListWidget]", 35, true);
1898             this.newColumn("url", "", "QBT_TR(URL)QBT_TR[CONTEXT=TrackerListWidget]", 250, true);
1899             this.newColumn("status", "", "QBT_TR(Status)QBT_TR[CONTEXT=TrackerListWidget]", 125, true);
1900             this.newColumn("peers", "", "QBT_TR(Peers)QBT_TR[CONTEXT=TrackerListWidget]", 75, true);
1901             this.newColumn("seeds", "", "QBT_TR(Seeds)QBT_TR[CONTEXT=TrackerListWidget]", 75, true);
1902             this.newColumn("leeches", "", "QBT_TR(Leeches)QBT_TR[CONTEXT=TrackerListWidget]", 75, true);
1903             this.newColumn("downloaded", "", "QBT_TR(Times Downloaded)QBT_TR[CONTEXT=TrackerListWidget]", 100, true);
1904             this.newColumn("message", "", "QBT_TR(Message)QBT_TR[CONTEXT=TrackerListWidget]", 250, true);
1905         },
1906     });
1908     const BulkRenameTorrentFilesTable = new Class({
1909         Extends: DynamicTable,
1911         filterTerms: [],
1912         prevFilterTerms: [],
1913         prevRowsString: null,
1914         prevFilteredRows: [],
1915         prevSortedColumn: null,
1916         prevReverseSort: null,
1917         fileTree: new window.qBittorrent.FileTree.FileTree(),
1919         populateTable: function(root) {
1920             this.fileTree.setRoot(root);
1921             root.children.each((node) => {
1922                 this._addNodeToTable(node, 0);
1923             });
1924         },
1926         _addNodeToTable: function(node, depth) {
1927             node.depth = depth;
1929             if (node.isFolder) {
1930                 const data = {
1931                     rowId: node.rowId,
1932                     fileId: -1,
1933                     checked: node.checked,
1934                     path: node.path,
1935                     original: node.original,
1936                     renamed: node.renamed
1937                 };
1939                 node.data = data;
1940                 node.full_data = data;
1941                 this.updateRowData(data);
1942             }
1943             else {
1944                 node.data.rowId = node.rowId;
1945                 node.full_data = node.data;
1946                 this.updateRowData(node.data);
1947             }
1949             node.children.each((child) => {
1950                 this._addNodeToTable(child, depth + 1);
1951             });
1952         },
1954         getRoot: function() {
1955             return this.fileTree.getRoot();
1956         },
1958         getNode: function(rowId) {
1959             return this.fileTree.getNode(rowId);
1960         },
1962         getRow: function(node) {
1963             const rowId = this.fileTree.getRowId(node).toString();
1964             return this.rows.get(rowId);
1965         },
1967         getSelectedRows: function() {
1968             const nodes = this.fileTree.toArray();
1970             return nodes.filter(x => x.checked === 0);
1971         },
1973         initColumns: function() {
1974             // Blocks saving header width (because window width isn't saved)
1975             LocalPreferences.remove("column_" + "checked" + "_width_" + this.dynamicTableDivId);
1976             LocalPreferences.remove("column_" + "original" + "_width_" + this.dynamicTableDivId);
1977             LocalPreferences.remove("column_" + "renamed" + "_width_" + this.dynamicTableDivId);
1978             this.newColumn("checked", "", "", 50, true);
1979             this.newColumn("original", "", "QBT_TR(Original)QBT_TR[CONTEXT=TrackerListWidget]", 270, true);
1980             this.newColumn("renamed", "", "QBT_TR(Renamed)QBT_TR[CONTEXT=TrackerListWidget]", 220, true);
1982             this.initColumnsFunctions();
1983         },
1985         /**
1986          * Toggles the global checkbox and all checkboxes underneath
1987          */
1988         toggleGlobalCheckbox: function() {
1989             const checkbox = $("rootMultiRename_cb");
1990             const checkboxes = $$("input.RenamingCB");
1992             for (let i = 0; i < checkboxes.length; ++i) {
1993                 const node = this.getNode(i);
1995                 if (checkbox.checked || checkbox.indeterminate) {
1996                     const cb = checkboxes[i];
1997                     cb.checked = true;
1998                     cb.indeterminate = false;
1999                     cb.state = "checked";
2000                     node.checked = 0;
2001                     node.full_data.checked = node.checked;
2002                 }
2003                 else {
2004                     const cb = checkboxes[i];
2005                     cb.checked = false;
2006                     cb.indeterminate = false;
2007                     cb.state = "unchecked";
2008                     node.checked = 1;
2009                     node.full_data.checked = node.checked;
2010                 }
2011             }
2013             this.updateGlobalCheckbox();
2014         },
2016         toggleNodeTreeCheckbox: function(rowId, checkState) {
2017             const node = this.getNode(rowId);
2018             node.checked = checkState;
2019             node.full_data.checked = checkState;
2020             const checkbox = $(`cbRename${rowId}`);
2021             checkbox.checked = node.checked === 0;
2022             checkbox.state = checkbox.checked ? "checked" : "unchecked";
2024             for (let i = 0; i < node.children.length; ++i)
2025                 this.toggleNodeTreeCheckbox(node.children[i].rowId, checkState);
2026         },
2028         updateGlobalCheckbox: function() {
2029             const checkbox = $("rootMultiRename_cb");
2030             const checkboxes = $$("input.RenamingCB");
2031             const isAllChecked = function() {
2032                 for (let i = 0; i < checkboxes.length; ++i) {
2033                     if (!checkboxes[i].checked)
2034                         return false;
2035                 }
2036                 return true;
2037             };
2038             const isAllUnchecked = function() {
2039                 for (let i = 0; i < checkboxes.length; ++i) {
2040                     if (checkboxes[i].checked)
2041                         return false;
2042                 }
2043                 return true;
2044             };
2045             if (isAllChecked()) {
2046                 checkbox.state = "checked";
2047                 checkbox.indeterminate = false;
2048                 checkbox.checked = true;
2049             }
2050             else if (isAllUnchecked()) {
2051                 checkbox.state = "unchecked";
2052                 checkbox.indeterminate = false;
2053                 checkbox.checked = false;
2054             }
2055             else {
2056                 checkbox.state = "partial";
2057                 checkbox.indeterminate = true;
2058                 checkbox.checked = false;
2059             }
2060         },
2062         initColumnsFunctions: function() {
2063             const that = this;
2065             // checked
2066             this.columns["checked"].updateTd = function(td, row) {
2067                 const id = row.rowId;
2068                 const value = this.getRowValue(row);
2070                 const treeImg = new Element("img", {
2071                     src: "images/L.gif",
2072                     styles: {
2073                         "margin-bottom": -2
2074                     }
2075                 });
2076                 const checkbox = new Element("input");
2077                 checkbox.type = "checkbox";
2078                 checkbox.id = "cbRename" + id;
2079                 checkbox.setAttribute("data-id", id);
2080                 checkbox.className = "RenamingCB";
2081                 checkbox.addEventListener("click", (e) => {
2082                     const node = that.getNode(id);
2083                     node.checked = e.target.checked ? 0 : 1;
2084                     node.full_data.checked = node.checked;
2085                     that.updateGlobalCheckbox();
2086                     that.onRowSelectionChange(node);
2087                     e.stopPropagation();
2088                 });
2089                 checkbox.checked = (value === 0);
2090                 checkbox.state = checkbox.checked ? "checked" : "unchecked";
2091                 checkbox.indeterminate = false;
2092                 td.adopt(treeImg, checkbox);
2093             };
2095             // original
2096             this.columns["original"].updateTd = function(td, row) {
2097                 const id = row.rowId;
2098                 const fileNameId = "filesTablefileName" + id;
2099                 const node = that.getNode(id);
2101                 if (node.isFolder) {
2102                     const value = this.getRowValue(row);
2103                     const dirImgId = "renameTableDirImg" + id;
2104                     if ($(dirImgId)) {
2105                         // just update file name
2106                         $(fileNameId).textContent = value;
2107                     }
2108                     else {
2109                         const span = new Element("span", {
2110                             text: value,
2111                             id: fileNameId
2112                         });
2113                         const dirImg = new Element("img", {
2114                             src: "images/directory.svg",
2115                             styles: {
2116                                 "width": 20,
2117                                 "padding-right": 5,
2118                                 "margin-bottom": -3,
2119                                 "margin-left": (node.depth * 20)
2120                             },
2121                             id: dirImgId
2122                         });
2123                         td.replaceChildren(dirImg, span);
2124                     }
2125                 }
2126                 else { // is file
2127                     const value = this.getRowValue(row);
2128                     const span = new Element("span", {
2129                         text: value,
2130                         id: fileNameId,
2131                         styles: {
2132                             "margin-left": ((node.depth + 1) * 20)
2133                         }
2134                     });
2135                     td.replaceChildren(span);
2136                 }
2137             };
2139             // renamed
2140             this.columns["renamed"].updateTd = function(td, row) {
2141                 const id = row.rowId;
2142                 const fileNameRenamedId = "filesTablefileRenamed" + id;
2143                 const value = this.getRowValue(row);
2145                 const span = new Element("span", {
2146                     text: value,
2147                     id: fileNameRenamedId,
2148                 });
2149                 td.replaceChildren(span);
2150             };
2151         },
2153         onRowSelectionChange: function(row) {},
2155         selectRow: function() {
2156             return;
2157         },
2159         reselectRows: function(rowIds) {
2160             const that = this;
2161             this.deselectAll();
2162             this.tableBody.getElements("tr").each((tr) => {
2163                 if (rowIds.includes(tr.rowId)) {
2164                     const node = that.getNode(tr.rowId);
2165                     node.checked = 0;
2166                     node.full_data.checked = 0;
2168                     const checkbox = tr.children[0].getElement("input");
2169                     checkbox.state = "checked";
2170                     checkbox.indeterminate = false;
2171                     checkbox.checked = true;
2172                 }
2173             });
2175             this.updateGlobalCheckbox();
2176         },
2178         _sortNodesByColumn: function(nodes, column) {
2179             nodes.sort((row1, row2) => {
2180                 // list folders before files when sorting by name
2181                 if (column.name === "original") {
2182                     const node1 = this.getNode(row1.data.rowId);
2183                     const node2 = this.getNode(row2.data.rowId);
2184                     if (node1.isFolder && !node2.isFolder)
2185                         return -1;
2186                     if (node2.isFolder && !node1.isFolder)
2187                         return 1;
2188                 }
2190                 const res = column.compareRows(row1, row2);
2191                 return (this.reverseSort === "0") ? res : -res;
2192             });
2194             nodes.each((node) => {
2195                 if (node.children.length > 0)
2196                     this._sortNodesByColumn(node.children, column);
2197             });
2198         },
2200         _filterNodes: function(node, filterTerms, filteredRows) {
2201             if (node.isFolder) {
2202                 const childAdded = node.children.reduce((acc, child) => {
2203                     // we must execute the function before ORing w/ acc or we'll stop checking child nodes after the first successful match
2204                     return (this._filterNodes(child, filterTerms, filteredRows) || acc);
2205                 }, false);
2207                 if (childAdded) {
2208                     const row = this.getRow(node);
2209                     filteredRows.push(row);
2210                     return true;
2211                 }
2212             }
2214             if (window.qBittorrent.Misc.containsAllTerms(node.original, filterTerms)) {
2215                 const row = this.getRow(node);
2216                 filteredRows.push(row);
2217                 return true;
2218             }
2220             return false;
2221         },
2223         setFilter: function(text) {
2224             const filterTerms = text.trim().toLowerCase().split(" ");
2225             if ((filterTerms.length === 1) && (filterTerms[0] === ""))
2226                 this.filterTerms = [];
2227             else
2228                 this.filterTerms = filterTerms;
2229         },
2231         getFilteredAndSortedRows: function() {
2232             if (this.getRoot() === null)
2233                 return [];
2235             const generateRowsSignature = () => {
2236                 const rowsData = [];
2237                 for (const { full_data } of this.getRowValues())
2238                     rowsData.push(full_data);
2239                 return JSON.stringify(rowsData);
2240             };
2242             const getFilteredRows = function() {
2243                 if (this.filterTerms.length === 0) {
2244                     const nodeArray = this.fileTree.toArray();
2245                     const filteredRows = nodeArray.map((node) => {
2246                         return this.getRow(node);
2247                     });
2248                     return filteredRows;
2249                 }
2251                 const filteredRows = [];
2252                 this.getRoot().children.each((child) => {
2253                     this._filterNodes(child, this.filterTerms, filteredRows);
2254                 });
2255                 filteredRows.reverse();
2256                 return filteredRows;
2257             }.bind(this);
2259             const hasRowsChanged = function(rowsString, prevRowsStringString) {
2260                 const rowsChanged = (rowsString !== prevRowsStringString);
2261                 const isFilterTermsChanged = this.filterTerms.reduce((acc, term, index) => {
2262                     return (acc || (term !== this.prevFilterTerms[index]));
2263                 }, false);
2264                 const isFilterChanged = ((this.filterTerms.length !== this.prevFilterTerms.length)
2265                     || ((this.filterTerms.length > 0) && isFilterTermsChanged));
2266                 const isSortedColumnChanged = (this.prevSortedColumn !== this.sortedColumn);
2267                 const isReverseSortChanged = (this.prevReverseSort !== this.reverseSort);
2269                 return (rowsChanged || isFilterChanged || isSortedColumnChanged || isReverseSortChanged);
2270             }.bind(this);
2272             const rowsString = generateRowsSignature();
2273             if (!hasRowsChanged(rowsString, this.prevRowsString))
2274                 return this.prevFilteredRows;
2276             // sort, then filter
2277             const column = this.columns[this.sortedColumn];
2278             this._sortNodesByColumn(this.getRoot().children, column);
2279             const filteredRows = getFilteredRows();
2281             this.prevFilterTerms = this.filterTerms;
2282             this.prevRowsString = rowsString;
2283             this.prevFilteredRows = filteredRows;
2284             this.prevSortedColumn = this.sortedColumn;
2285             this.prevReverseSort = this.reverseSort;
2286             return filteredRows;
2287         },
2289         setIgnored: function(rowId, ignore) {
2290             const row = this.rows.get(rowId);
2291             if (ignore)
2292                 row.full_data.remaining = 0;
2293             else
2294                 row.full_data.remaining = (row.full_data.size * (1.0 - (row.full_data.progress / 100)));
2295         },
2297         setupTr: function(tr) {
2298             tr.addEventListener("keydown", function(event) {
2299                 switch (event.key) {
2300                     case "left":
2301                         qBittorrent.PropFiles.collapseFolder(this._this.getSelectedRowId());
2302                         return false;
2303                     case "right":
2304                         qBittorrent.PropFiles.expandFolder(this._this.getSelectedRowId());
2305                         return false;
2306                 }
2307             });
2308         }
2309     });
2311     const TorrentFilesTable = new Class({
2312         Extends: DynamicTable,
2314         filterTerms: [],
2315         prevFilterTerms: [],
2316         prevRowsString: null,
2317         prevFilteredRows: [],
2318         prevSortedColumn: null,
2319         prevReverseSort: null,
2320         fileTree: new window.qBittorrent.FileTree.FileTree(),
2322         populateTable: function(root) {
2323             this.fileTree.setRoot(root);
2324             root.children.each((node) => {
2325                 this._addNodeToTable(node, 0);
2326             });
2327         },
2329         _addNodeToTable: function(node, depth) {
2330             node.depth = depth;
2332             if (node.isFolder) {
2333                 const data = {
2334                     rowId: node.rowId,
2335                     size: node.size,
2336                     checked: node.checked,
2337                     remaining: node.remaining,
2338                     progress: node.progress,
2339                     priority: window.qBittorrent.PropFiles.normalizePriority(node.priority),
2340                     availability: node.availability,
2341                     fileId: -1,
2342                     name: node.name
2343                 };
2345                 node.data = data;
2346                 node.full_data = data;
2347                 this.updateRowData(data);
2348             }
2349             else {
2350                 node.data.rowId = node.rowId;
2351                 node.full_data = node.data;
2352                 this.updateRowData(node.data);
2353             }
2355             node.children.each((child) => {
2356                 this._addNodeToTable(child, depth + 1);
2357             });
2358         },
2360         getRoot: function() {
2361             return this.fileTree.getRoot();
2362         },
2364         getNode: function(rowId) {
2365             return this.fileTree.getNode(rowId);
2366         },
2368         getRow: function(node) {
2369             const rowId = this.fileTree.getRowId(node).toString();
2370             return this.rows.get(rowId);
2371         },
2373         initColumns: function() {
2374             this.newColumn("checked", "", "", 50, true);
2375             this.newColumn("name", "", "QBT_TR(Name)QBT_TR[CONTEXT=TrackerListWidget]", 300, true);
2376             this.newColumn("size", "", "QBT_TR(Total Size)QBT_TR[CONTEXT=TrackerListWidget]", 75, true);
2377             this.newColumn("progress", "", "QBT_TR(Progress)QBT_TR[CONTEXT=TrackerListWidget]", 100, true);
2378             this.newColumn("priority", "", "QBT_TR(Download Priority)QBT_TR[CONTEXT=TrackerListWidget]", 150, true);
2379             this.newColumn("remaining", "", "QBT_TR(Remaining)QBT_TR[CONTEXT=TrackerListWidget]", 75, true);
2380             this.newColumn("availability", "", "QBT_TR(Availability)QBT_TR[CONTEXT=TrackerListWidget]", 75, true);
2382             this.initColumnsFunctions();
2383         },
2385         initColumnsFunctions: function() {
2386             const that = this;
2387             const displaySize = function(td, row) {
2388                 const size = window.qBittorrent.Misc.friendlyUnit(this.getRowValue(row), false);
2389                 td.textContent = size;
2390                 td.title = size;
2391             };
2392             const displayPercentage = function(td, row) {
2393                 const value = window.qBittorrent.Misc.friendlyPercentage(this.getRowValue(row));
2394                 td.textContent = value;
2395                 td.title = value;
2396             };
2398             // checked
2399             this.columns["checked"].updateTd = function(td, row) {
2400                 const id = row.rowId;
2401                 const value = this.getRowValue(row);
2403                 if (window.qBittorrent.PropFiles.isDownloadCheckboxExists(id)) {
2404                     window.qBittorrent.PropFiles.updateDownloadCheckbox(id, value);
2405                 }
2406                 else {
2407                     const treeImg = new Element("img", {
2408                         src: "images/L.gif",
2409                         styles: {
2410                             "margin-bottom": -2
2411                         }
2412                     });
2413                     td.adopt(treeImg, window.qBittorrent.PropFiles.createDownloadCheckbox(id, row.full_data.fileId, value));
2414                 }
2415             };
2417             // name
2418             this.columns["name"].updateTd = function(td, row) {
2419                 const id = row.rowId;
2420                 const fileNameId = "filesTablefileName" + id;
2421                 const node = that.getNode(id);
2423                 if (node.isFolder) {
2424                     const value = this.getRowValue(row);
2425                     const collapseIconId = "filesTableCollapseIcon" + id;
2426                     const dirImgId = "filesTableDirImg" + id;
2427                     if ($(dirImgId)) {
2428                         // just update file name
2429                         $(fileNameId).textContent = value;
2430                     }
2431                     else {
2432                         const collapseIcon = new Element("img", {
2433                             src: "images/go-down.svg",
2434                             styles: {
2435                                 "margin-left": (node.depth * 20)
2436                             },
2437                             class: "filesTableCollapseIcon",
2438                             id: collapseIconId,
2439                             "data-id": id,
2440                             onclick: "qBittorrent.PropFiles.collapseIconClicked(this)"
2441                         });
2442                         const span = new Element("span", {
2443                             text: value,
2444                             id: fileNameId
2445                         });
2446                         const dirImg = new Element("img", {
2447                             src: "images/directory.svg",
2448                             styles: {
2449                                 "width": 20,
2450                                 "padding-right": 5,
2451                                 "margin-bottom": -3
2452                             },
2453                             id: dirImgId
2454                         });
2455                         td.replaceChildren(collapseIcon, dirImg, span);
2456                     }
2457                 }
2458                 else {
2459                     const value = this.getRowValue(row);
2460                     const span = new Element("span", {
2461                         text: value,
2462                         id: fileNameId,
2463                         styles: {
2464                             "margin-left": ((node.depth + 1) * 20)
2465                         }
2466                     });
2467                     td.replaceChildren(span);
2468                 }
2469             };
2471             // size
2472             this.columns["size"].updateTd = displaySize;
2474             // progress
2475             this.columns["progress"].updateTd = function(td, row) {
2476                 const id = row.rowId;
2477                 const value = this.getRowValue(row);
2479                 const progressBar = $("pbf_" + id);
2480                 if (progressBar === null) {
2481                     td.adopt(new window.qBittorrent.ProgressBar.ProgressBar(value.toFloat(), {
2482                         id: "pbf_" + id,
2483                         width: 80
2484                     }));
2485                 }
2486                 else {
2487                     progressBar.setValue(value.toFloat());
2488                 }
2489             };
2491             // priority
2492             this.columns["priority"].updateTd = function(td, row) {
2493                 const id = row.rowId;
2494                 const value = this.getRowValue(row);
2496                 if (window.qBittorrent.PropFiles.isPriorityComboExists(id))
2497                     window.qBittorrent.PropFiles.updatePriorityCombo(id, value);
2498                 else
2499                     td.adopt(window.qBittorrent.PropFiles.createPriorityCombo(id, row.full_data.fileId, value));
2500             };
2502             // remaining, availability
2503             this.columns["remaining"].updateTd = displaySize;
2504             this.columns["availability"].updateTd = displayPercentage;
2505         },
2507         _sortNodesByColumn: function(nodes, column) {
2508             nodes.sort((row1, row2) => {
2509                 // list folders before files when sorting by name
2510                 if (column.name === "name") {
2511                     const node1 = this.getNode(row1.data.rowId);
2512                     const node2 = this.getNode(row2.data.rowId);
2513                     if (node1.isFolder && !node2.isFolder)
2514                         return -1;
2515                     if (node2.isFolder && !node1.isFolder)
2516                         return 1;
2517                 }
2519                 const res = column.compareRows(row1, row2);
2520                 return (this.reverseSort === "0") ? res : -res;
2521             });
2523             nodes.each((node) => {
2524                 if (node.children.length > 0)
2525                     this._sortNodesByColumn(node.children, column);
2526             });
2527         },
2529         _filterNodes: function(node, filterTerms, filteredRows) {
2530             if (node.isFolder) {
2531                 const childAdded = node.children.reduce((acc, child) => {
2532                     // we must execute the function before ORing w/ acc or we'll stop checking child nodes after the first successful match
2533                     return (this._filterNodes(child, filterTerms, filteredRows) || acc);
2534                 }, false);
2536                 if (childAdded) {
2537                     const row = this.getRow(node);
2538                     filteredRows.push(row);
2539                     return true;
2540                 }
2541             }
2543             if (window.qBittorrent.Misc.containsAllTerms(node.name, filterTerms)) {
2544                 const row = this.getRow(node);
2545                 filteredRows.push(row);
2546                 return true;
2547             }
2549             return false;
2550         },
2552         setFilter: function(text) {
2553             const filterTerms = text.trim().toLowerCase().split(" ");
2554             if ((filterTerms.length === 1) && (filterTerms[0] === ""))
2555                 this.filterTerms = [];
2556             else
2557                 this.filterTerms = filterTerms;
2558         },
2560         getFilteredAndSortedRows: function() {
2561             if (this.getRoot() === null)
2562                 return [];
2564             const generateRowsSignature = () => {
2565                 const rowsData = [];
2566                 for (const { full_data } of this.getRowValues())
2567                     rowsData.push(full_data);
2568                 return JSON.stringify(rowsData);
2569             };
2571             const getFilteredRows = function() {
2572                 if (this.filterTerms.length === 0) {
2573                     const nodeArray = this.fileTree.toArray();
2574                     const filteredRows = nodeArray.map((node) => {
2575                         return this.getRow(node);
2576                     });
2577                     return filteredRows;
2578                 }
2580                 const filteredRows = [];
2581                 this.getRoot().children.each((child) => {
2582                     this._filterNodes(child, this.filterTerms, filteredRows);
2583                 });
2584                 filteredRows.reverse();
2585                 return filteredRows;
2586             }.bind(this);
2588             const hasRowsChanged = function(rowsString, prevRowsStringString) {
2589                 const rowsChanged = (rowsString !== prevRowsStringString);
2590                 const isFilterTermsChanged = this.filterTerms.reduce((acc, term, index) => {
2591                     return (acc || (term !== this.prevFilterTerms[index]));
2592                 }, false);
2593                 const isFilterChanged = ((this.filterTerms.length !== this.prevFilterTerms.length)
2594                     || ((this.filterTerms.length > 0) && isFilterTermsChanged));
2595                 const isSortedColumnChanged = (this.prevSortedColumn !== this.sortedColumn);
2596                 const isReverseSortChanged = (this.prevReverseSort !== this.reverseSort);
2598                 return (rowsChanged || isFilterChanged || isSortedColumnChanged || isReverseSortChanged);
2599             }.bind(this);
2601             const rowsString = generateRowsSignature();
2602             if (!hasRowsChanged(rowsString, this.prevRowsString))
2603                 return this.prevFilteredRows;
2605             // sort, then filter
2606             const column = this.columns[this.sortedColumn];
2607             this._sortNodesByColumn(this.getRoot().children, column);
2608             const filteredRows = getFilteredRows();
2610             this.prevFilterTerms = this.filterTerms;
2611             this.prevRowsString = rowsString;
2612             this.prevFilteredRows = filteredRows;
2613             this.prevSortedColumn = this.sortedColumn;
2614             this.prevReverseSort = this.reverseSort;
2615             return filteredRows;
2616         },
2618         setIgnored: function(rowId, ignore) {
2619             const row = this.rows.get(rowId.toString());
2620             if (ignore)
2621                 row.full_data.remaining = 0;
2622             else
2623                 row.full_data.remaining = (row.full_data.size * (1.0 - (row.full_data.progress / 100)));
2624         },
2626         setupTr: function(tr) {
2627             tr.addEventListener("keydown", function(event) {
2628                 switch (event.key) {
2629                     case "left":
2630                         qBittorrent.PropFiles.collapseFolder(this._this.getSelectedRowId());
2631                         return false;
2632                     case "right":
2633                         qBittorrent.PropFiles.expandFolder(this._this.getSelectedRowId());
2634                         return false;
2635                 }
2636             });
2637         }
2638     });
2640     const RssFeedTable = new Class({
2641         Extends: DynamicTable,
2642         initColumns: function() {
2643             this.newColumn("state_icon", "", "", 30, true);
2644             this.newColumn("name", "", "QBT_TR(RSS feeds)QBT_TR[CONTEXT=FeedListWidget]", -1, true);
2646             this.columns["state_icon"].dataProperties[0] = "";
2648             // map name row to "[name] ([unread])"
2649             this.columns["name"].dataProperties.push("unread");
2650             this.columns["name"].updateTd = function(td, row) {
2651                 const name = this.getRowValue(row, 0);
2652                 const unreadCount = this.getRowValue(row, 1);
2653                 const value = name + " (" + unreadCount + ")";
2654                 td.textContent = value;
2655                 td.title = value;
2656             };
2657         },
2658         setupHeaderMenu: function() {},
2659         setupHeaderEvents: function() {},
2660         getFilteredAndSortedRows: function() {
2661             return [...this.getRowValues()];
2662         },
2663         selectRow: function(rowId) {
2664             this.selectedRows.push(rowId);
2665             this.setRowClass();
2666             this.onSelectedRowChanged();
2668             let path = "";
2669             for (const row of this.getRowValues()) {
2670                 if (row.rowId === rowId) {
2671                     path = row.full_data.dataPath;
2672                     break;
2673                 }
2674             }
2675             window.qBittorrent.Rss.showRssFeed(path);
2676         },
2677         setupTr: function(tr) {
2678             tr.addEventListener("dblclick", function(e) {
2679                 if (this.rowId !== 0) {
2680                     window.qBittorrent.Rss.moveItem(this._this.rows.get(this.rowId).full_data.dataPath);
2681                     return true;
2682                 }
2683             });
2684         },
2685         updateRow: function(tr, fullUpdate) {
2686             const row = this.rows.get(tr.rowId);
2687             const data = row[fullUpdate ? "full_data" : "data"];
2689             const tds = tr.getElements("td");
2690             for (let i = 0; i < this.columns.length; ++i) {
2691                 if (Object.hasOwn(data, this.columns[i].dataProperties[0]))
2692                     this.columns[i].updateTd(tds[i], row);
2693             }
2694             row["data"] = {};
2695             tds[0].style.overflow = "visible";
2696             const indentation = row.full_data.indentation;
2697             tds[0].style.paddingLeft = (indentation * 32 + 4) + "px";
2698             tds[1].style.paddingLeft = (indentation * 32 + 4) + "px";
2699         },
2700         updateIcons: function() {
2701             // state_icon
2702             for (const row of this.getRowValues()) {
2703                 let img_path;
2704                 switch (row.full_data.status) {
2705                     case "default":
2706                         img_path = "images/application-rss.svg";
2707                         break;
2708                     case "hasError":
2709                         img_path = "images/task-reject.svg";
2710                         break;
2711                     case "isLoading":
2712                         img_path = "images/spinner.gif";
2713                         break;
2714                     case "unread":
2715                         img_path = "images/mail-inbox.svg";
2716                         break;
2717                     case "isFolder":
2718                         img_path = "images/folder-documents.svg";
2719                         break;
2720                 }
2721                 let td;
2722                 for (let i = 0; i < this.tableBody.rows.length; ++i) {
2723                     if (this.tableBody.rows[i].rowId === row.rowId) {
2724                         td = this.tableBody.rows[i].children[0];
2725                         break;
2726                     }
2727                 }
2728                 if (td.getChildren("img").length > 0) {
2729                     const img = td.getChildren("img")[0];
2730                     if (!img.src.includes(img_path)) {
2731                         img.src = img_path;
2732                         img.title = status;
2733                     }
2734                 }
2735                 else {
2736                     td.adopt(new Element("img", {
2737                         "src": img_path,
2738                         "class": "stateIcon",
2739                         "height": "22px",
2740                         "width": "22px"
2741                     }));
2742                 }
2743             };
2744         },
2745         newColumn: function(name, style, caption, defaultWidth, defaultVisible) {
2746             const column = {};
2747             column["name"] = name;
2748             column["title"] = name;
2749             column["visible"] = defaultVisible;
2750             column["force_hide"] = false;
2751             column["caption"] = caption;
2752             column["style"] = style;
2753             if (defaultWidth !== -1)
2754                 column["width"] = defaultWidth;
2756             column["dataProperties"] = [name];
2757             column["getRowValue"] = function(row, pos) {
2758                 if (pos === undefined)
2759                     pos = 0;
2760                 return row["full_data"][this.dataProperties[pos]];
2761             };
2762             column["compareRows"] = function(row1, row2) {
2763                 const value1 = this.getRowValue(row1);
2764                 const value2 = this.getRowValue(row2);
2765                 if ((typeof(value1) === "number") && (typeof(value2) === "number"))
2766                     return compareNumbers(value1, value2);
2767                 return window.qBittorrent.Misc.naturalSortCollator.compare(value1, value2);
2768             };
2769             column["updateTd"] = function(td, row) {
2770                 const value = this.getRowValue(row);
2771                 td.textContent = value;
2772                 td.title = value;
2773             };
2774             column["onResize"] = null;
2775             this.columns.push(column);
2776             this.columns[name] = column;
2778             this.hiddenTableHeader.appendChild(new Element("th"));
2779             this.fixedTableHeader.appendChild(new Element("th"));
2780         }
2781     });
2783     const RssArticleTable = new Class({
2784         Extends: DynamicTable,
2785         initColumns: function() {
2786             this.newColumn("name", "", "QBT_TR(Torrents: (double-click to download))QBT_TR[CONTEXT=RSSWidget]", -1, true);
2787         },
2788         setupHeaderMenu: function() {},
2789         setupHeaderEvents: function() {},
2790         getFilteredAndSortedRows: function() {
2791             return [...this.getRowValues()];
2792         },
2793         selectRow: function(rowId) {
2794             this.selectedRows.push(rowId);
2795             this.setRowClass();
2796             this.onSelectedRowChanged();
2798             let articleId = "";
2799             let feedUid = "";
2800             for (const row of this.getRowValues()) {
2801                 if (row.rowId === rowId) {
2802                     articleId = row.full_data.dataId;
2803                     feedUid = row.full_data.feedUid;
2804                     this.tableBody.rows[row.rowId].removeClass("unreadArticle");
2805                     break;
2806                 }
2807             }
2808             window.qBittorrent.Rss.showDetails(feedUid, articleId);
2809         },
2810         setupTr: function(tr) {
2811             tr.addEventListener("dblclick", function(e) {
2812                 showDownloadPage([this._this.rows.get(this.rowId).full_data.torrentURL]);
2813                 return true;
2814             });
2815             tr.addClass("torrentsTableContextMenuTarget");
2816         },
2817         updateRow: function(tr, fullUpdate) {
2818             const row = this.rows.get(tr.rowId);
2819             const data = row[fullUpdate ? "full_data" : "data"];
2820             if (!row.full_data.isRead)
2821                 tr.addClass("unreadArticle");
2822             else
2823                 tr.removeClass("unreadArticle");
2825             const tds = tr.getElements("td");
2826             for (let i = 0; i < this.columns.length; ++i) {
2827                 if (Object.hasOwn(data, this.columns[i].dataProperties[0]))
2828                     this.columns[i].updateTd(tds[i], row);
2829             }
2830             row["data"] = {};
2831         },
2832         newColumn: function(name, style, caption, defaultWidth, defaultVisible) {
2833             const column = {};
2834             column["name"] = name;
2835             column["title"] = name;
2836             column["visible"] = defaultVisible;
2837             column["force_hide"] = false;
2838             column["caption"] = caption;
2839             column["style"] = style;
2840             if (defaultWidth !== -1)
2841                 column["width"] = defaultWidth;
2843             column["dataProperties"] = [name];
2844             column["getRowValue"] = function(row, pos) {
2845                 if (pos === undefined)
2846                     pos = 0;
2847                 return row["full_data"][this.dataProperties[pos]];
2848             };
2849             column["compareRows"] = function(row1, row2) {
2850                 const value1 = this.getRowValue(row1);
2851                 const value2 = this.getRowValue(row2);
2852                 if ((typeof(value1) === "number") && (typeof(value2) === "number"))
2853                     return compareNumbers(value1, value2);
2854                 return window.qBittorrent.Misc.naturalSortCollator.compare(value1, value2);
2855             };
2856             column["updateTd"] = function(td, row) {
2857                 const value = this.getRowValue(row);
2858                 td.textContent = value;
2859                 td.title = value;
2860             };
2861             column["onResize"] = null;
2862             this.columns.push(column);
2863             this.columns[name] = column;
2865             this.hiddenTableHeader.appendChild(new Element("th"));
2866             this.fixedTableHeader.appendChild(new Element("th"));
2867         }
2868     });
2870     const RssDownloaderRulesTable = new Class({
2871         Extends: DynamicTable,
2872         initColumns: function() {
2873             this.newColumn("checked", "", "", 30, true);
2874             this.newColumn("name", "", "", -1, true);
2876             this.columns["checked"].updateTd = function(td, row) {
2877                 if ($("cbRssDlRule" + row.rowId) === null) {
2878                     const checkbox = new Element("input");
2879                     checkbox.type = "checkbox";
2880                     checkbox.id = "cbRssDlRule" + row.rowId;
2881                     checkbox.checked = row.full_data.checked;
2883                     checkbox.addEventListener("click", function(e) {
2884                         window.qBittorrent.RssDownloader.rssDownloaderRulesTable.updateRowData({
2885                             rowId: row.rowId,
2886                             checked: this.checked
2887                         });
2888                         window.qBittorrent.RssDownloader.modifyRuleState(row.full_data.name, "enabled", this.checked);
2889                         e.stopPropagation();
2890                     });
2892                     td.append(checkbox);
2893                 }
2894                 else {
2895                     $("cbRssDlRule" + row.rowId).checked = row.full_data.checked;
2896                 }
2897             };
2898         },
2899         setupHeaderMenu: function() {},
2900         setupHeaderEvents: function() {},
2901         getFilteredAndSortedRows: function() {
2902             return [...this.getRowValues()];
2903         },
2904         setupTr: function(tr) {
2905             tr.addEventListener("dblclick", function(e) {
2906                 window.qBittorrent.RssDownloader.renameRule(this._this.rows.get(this.rowId).full_data.name);
2907                 return true;
2908             });
2909         },
2910         newColumn: function(name, style, caption, defaultWidth, defaultVisible) {
2911             const column = {};
2912             column["name"] = name;
2913             column["title"] = name;
2914             column["visible"] = defaultVisible;
2915             column["force_hide"] = false;
2916             column["caption"] = caption;
2917             column["style"] = style;
2918             if (defaultWidth !== -1)
2919                 column["width"] = defaultWidth;
2921             column["dataProperties"] = [name];
2922             column["getRowValue"] = function(row, pos) {
2923                 if (pos === undefined)
2924                     pos = 0;
2925                 return row["full_data"][this.dataProperties[pos]];
2926             };
2927             column["compareRows"] = function(row1, row2) {
2928                 const value1 = this.getRowValue(row1);
2929                 const value2 = this.getRowValue(row2);
2930                 if ((typeof(value1) === "number") && (typeof(value2) === "number"))
2931                     return compareNumbers(value1, value2);
2932                 return window.qBittorrent.Misc.naturalSortCollator.compare(value1, value2);
2933             };
2934             column["updateTd"] = function(td, row) {
2935                 const value = this.getRowValue(row);
2936                 td.textContent = value;
2937                 td.title = value;
2938             };
2939             column["onResize"] = null;
2940             this.columns.push(column);
2941             this.columns[name] = column;
2943             this.hiddenTableHeader.appendChild(new Element("th"));
2944             this.fixedTableHeader.appendChild(new Element("th"));
2945         },
2946         selectRow: function(rowId) {
2947             this.selectedRows.push(rowId);
2948             this.setRowClass();
2949             this.onSelectedRowChanged();
2951             let name = "";
2952             for (const row of this.getRowValues()) {
2953                 if (row.rowId === rowId) {
2954                     name = row.full_data.name;
2955                     break;
2956                 }
2957             }
2958             window.qBittorrent.RssDownloader.showRule(name);
2959         }
2960     });
2962     const RssDownloaderFeedSelectionTable = new Class({
2963         Extends: DynamicTable,
2964         initColumns: function() {
2965             this.newColumn("checked", "", "", 30, true);
2966             this.newColumn("name", "", "", -1, true);
2968             this.columns["checked"].updateTd = function(td, row) {
2969                 if ($("cbRssDlFeed" + row.rowId) === null) {
2970                     const checkbox = new Element("input");
2971                     checkbox.type = "checkbox";
2972                     checkbox.id = "cbRssDlFeed" + row.rowId;
2973                     checkbox.checked = row.full_data.checked;
2975                     checkbox.addEventListener("click", function(e) {
2976                         window.qBittorrent.RssDownloader.rssDownloaderFeedSelectionTable.updateRowData({
2977                             rowId: row.rowId,
2978                             checked: this.checked
2979                         });
2980                         e.stopPropagation();
2981                     });
2983                     td.append(checkbox);
2984                 }
2985                 else {
2986                     $("cbRssDlFeed" + row.rowId).checked = row.full_data.checked;
2987                 }
2988             };
2989         },
2990         setupHeaderMenu: function() {},
2991         setupHeaderEvents: function() {},
2992         getFilteredAndSortedRows: function() {
2993             return [...this.getRowValues()];
2994         },
2995         newColumn: function(name, style, caption, defaultWidth, defaultVisible) {
2996             const column = {};
2997             column["name"] = name;
2998             column["title"] = name;
2999             column["visible"] = defaultVisible;
3000             column["force_hide"] = false;
3001             column["caption"] = caption;
3002             column["style"] = style;
3003             if (defaultWidth !== -1)
3004                 column["width"] = defaultWidth;
3006             column["dataProperties"] = [name];
3007             column["getRowValue"] = function(row, pos) {
3008                 if (pos === undefined)
3009                     pos = 0;
3010                 return row["full_data"][this.dataProperties[pos]];
3011             };
3012             column["compareRows"] = function(row1, row2) {
3013                 const value1 = this.getRowValue(row1);
3014                 const value2 = this.getRowValue(row2);
3015                 if ((typeof(value1) === "number") && (typeof(value2) === "number"))
3016                     return compareNumbers(value1, value2);
3017                 return window.qBittorrent.Misc.naturalSortCollator.compare(value1, value2);
3018             };
3019             column["updateTd"] = function(td, row) {
3020                 const value = this.getRowValue(row);
3021                 td.textContent = value;
3022                 td.title = value;
3023             };
3024             column["onResize"] = null;
3025             this.columns.push(column);
3026             this.columns[name] = column;
3028             this.hiddenTableHeader.appendChild(new Element("th"));
3029             this.fixedTableHeader.appendChild(new Element("th"));
3030         },
3031         selectRow: function() {}
3032     });
3034     const RssDownloaderArticlesTable = new Class({
3035         Extends: DynamicTable,
3036         initColumns: function() {
3037             this.newColumn("name", "", "", -1, true);
3038         },
3039         setupHeaderMenu: function() {},
3040         setupHeaderEvents: function() {},
3041         getFilteredAndSortedRows: function() {
3042             return [...this.getRowValues()];
3043         },
3044         newColumn: function(name, style, caption, defaultWidth, defaultVisible) {
3045             const column = {};
3046             column["name"] = name;
3047             column["title"] = name;
3048             column["visible"] = defaultVisible;
3049             column["force_hide"] = false;
3050             column["caption"] = caption;
3051             column["style"] = style;
3052             if (defaultWidth !== -1)
3053                 column["width"] = defaultWidth;
3055             column["dataProperties"] = [name];
3056             column["getRowValue"] = function(row, pos) {
3057                 if (pos === undefined)
3058                     pos = 0;
3059                 return row["full_data"][this.dataProperties[pos]];
3060             };
3061             column["compareRows"] = function(row1, row2) {
3062                 const value1 = this.getRowValue(row1);
3063                 const value2 = this.getRowValue(row2);
3064                 if ((typeof(value1) === "number") && (typeof(value2) === "number"))
3065                     return compareNumbers(value1, value2);
3066                 return window.qBittorrent.Misc.naturalSortCollator.compare(value1, value2);
3067             };
3068             column["updateTd"] = function(td, row) {
3069                 const value = this.getRowValue(row);
3070                 td.textContent = value;
3071                 td.title = value;
3072             };
3073             column["onResize"] = null;
3074             this.columns.push(column);
3075             this.columns[name] = column;
3077             this.hiddenTableHeader.appendChild(new Element("th"));
3078             this.fixedTableHeader.appendChild(new Element("th"));
3079         },
3080         selectRow: function() {},
3081         updateRow: function(tr, fullUpdate) {
3082             const row = this.rows.get(tr.rowId);
3083             const data = row[fullUpdate ? "full_data" : "data"];
3085             if (row.full_data.isFeed) {
3086                 tr.addClass("articleTableFeed");
3087                 tr.removeClass("articleTableArticle");
3088             }
3089             else {
3090                 tr.removeClass("articleTableFeed");
3091                 tr.addClass("articleTableArticle");
3092             }
3094             const tds = tr.getElements("td");
3095             for (let i = 0; i < this.columns.length; ++i) {
3096                 if (Object.hasOwn(data, this.columns[i].dataProperties[0]))
3097                     this.columns[i].updateTd(tds[i], row);
3098             }
3099             row["data"] = {};
3100         }
3101     });
3103     const LogMessageTable = new Class({
3104         Extends: DynamicTable,
3106         filterText: "",
3108         filteredLength: function() {
3109             return this.tableBody.getElements("tr").length;
3110         },
3112         initColumns: function() {
3113             this.newColumn("rowId", "", "QBT_TR(ID)QBT_TR[CONTEXT=ExecutionLogWidget]", 50, true);
3114             this.newColumn("message", "", "QBT_TR(Message)QBT_TR[CONTEXT=ExecutionLogWidget]", 350, true);
3115             this.newColumn("timestamp", "", "QBT_TR(Timestamp)QBT_TR[CONTEXT=ExecutionLogWidget]", 150, true);
3116             this.newColumn("type", "", "QBT_TR(Log Type)QBT_TR[CONTEXT=ExecutionLogWidget]", 100, true);
3117             this.initColumnsFunctions();
3118         },
3120         initColumnsFunctions: function() {
3121             this.columns["timestamp"].updateTd = function(td, row) {
3122                 const date = new Date(this.getRowValue(row) * 1000).toLocaleString();
3123                 td.set({ "text": date, "title": date });
3124             };
3126             this.columns["type"].updateTd = function(td, row) {
3127                 // Type of the message: Log::NORMAL: 1, Log::INFO: 2, Log::WARNING: 4, Log::CRITICAL: 8
3128                 let logLevel, addClass;
3129                 switch (this.getRowValue(row).toInt()) {
3130                     case 1:
3131                         logLevel = "QBT_TR(Normal)QBT_TR[CONTEXT=ExecutionLogWidget]";
3132                         addClass = "logNormal";
3133                         break;
3134                     case 2:
3135                         logLevel = "QBT_TR(Info)QBT_TR[CONTEXT=ExecutionLogWidget]";
3136                         addClass = "logInfo";
3137                         break;
3138                     case 4:
3139                         logLevel = "QBT_TR(Warning)QBT_TR[CONTEXT=ExecutionLogWidget]";
3140                         addClass = "logWarning";
3141                         break;
3142                     case 8:
3143                         logLevel = "QBT_TR(Critical)QBT_TR[CONTEXT=ExecutionLogWidget]";
3144                         addClass = "logCritical";
3145                         break;
3146                     default:
3147                         logLevel = "QBT_TR(Unknown)QBT_TR[CONTEXT=ExecutionLogWidget]";
3148                         addClass = "logUnknown";
3149                         break;
3150                 }
3151                 td.set({ "text": logLevel, "title": logLevel });
3152                 td.getParent("tr").className = `logTableRow${addClass}`;
3153             };
3154         },
3156         getFilteredAndSortedRows: function() {
3157             let filteredRows = [];
3158             this.filterText = window.qBittorrent.Log.getFilterText();
3159             const filterTerms = (this.filterText.length > 0) ? this.filterText.toLowerCase().split(" ") : [];
3160             const logLevels = window.qBittorrent.Log.getSelectedLevels();
3161             if ((filterTerms.length > 0) || (logLevels.length < 4)) {
3162                 for (const row of this.getRowValues()) {
3163                     if (!logLevels.includes(row.full_data.type.toString()))
3164                         continue;
3166                     if ((filterTerms.length > 0) && !window.qBittorrent.Misc.containsAllTerms(row.full_data.message, filterTerms))
3167                         continue;
3169                     filteredRows.push(row);
3170                 }
3171             }
3172             else {
3173                 filteredRows = [...this.getRowValues()];
3174             }
3176             filteredRows.sort((row1, row2) => {
3177                 const column = this.columns[this.sortedColumn];
3178                 const res = column.compareRows(row1, row2);
3179                 return (this.reverseSort === "0") ? res : -res;
3180             });
3182             return filteredRows;
3183         },
3185         setupCommonEvents: function() {},
3187         setupTr: function(tr) {
3188             tr.addClass("logTableRow");
3189         }
3190     });
3192     const LogPeerTable = new Class({
3193         Extends: LogMessageTable,
3195         initColumns: function() {
3196             this.newColumn("rowId", "", "QBT_TR(ID)QBT_TR[CONTEXT=ExecutionLogWidget]", 50, true);
3197             this.newColumn("ip", "", "QBT_TR(IP)QBT_TR[CONTEXT=ExecutionLogWidget]", 150, true);
3198             this.newColumn("timestamp", "", "QBT_TR(Timestamp)QBT_TR[CONTEXT=ExecutionLogWidget]", 150, true);
3199             this.newColumn("blocked", "", "QBT_TR(Status)QBT_TR[CONTEXT=ExecutionLogWidget]", 150, true);
3200             this.newColumn("reason", "", "QBT_TR(Reason)QBT_TR[CONTEXT=ExecutionLogWidget]", 150, true);
3202             this.columns["timestamp"].updateTd = function(td, row) {
3203                 const date = new Date(this.getRowValue(row) * 1000).toLocaleString();
3204                 td.set({ "text": date, "title": date });
3205             };
3207             this.columns["blocked"].updateTd = function(td, row) {
3208                 let status, addClass;
3209                 if (this.getRowValue(row)) {
3210                     status = "QBT_TR(Blocked)QBT_TR[CONTEXT=ExecutionLogWidget]";
3211                     addClass = "peerBlocked";
3212                 }
3213                 else {
3214                     status = "QBT_TR(Banned)QBT_TR[CONTEXT=ExecutionLogWidget]";
3215                     addClass = "peerBanned";
3216                 }
3217                 td.set({ "text": status, "title": status });
3218                 td.getParent("tr").className = `logTableRow${addClass}`;
3219             };
3220         },
3222         getFilteredAndSortedRows: function() {
3223             let filteredRows = [];
3224             this.filterText = window.qBittorrent.Log.getFilterText();
3225             const filterTerms = (this.filterText.length > 0) ? this.filterText.toLowerCase().split(" ") : [];
3226             if (filterTerms.length > 0) {
3227                 for (const row of this.getRowValues()) {
3228                     if ((filterTerms.length > 0) && !window.qBittorrent.Misc.containsAllTerms(row.full_data.ip, filterTerms))
3229                         continue;
3231                     filteredRows.push(row);
3232                 }
3233             }
3234             else {
3235                 filteredRows = [...this.getRowValues()];
3236             }
3238             filteredRows.sort((row1, row2) => {
3239                 const column = this.columns[this.sortedColumn];
3240                 const res = column.compareRows(row1, row2);
3241                 return (this.reverseSort === "0") ? res : -res;
3242             });
3244             return filteredRows;
3245         }
3246     });
3248     const TorrentWebseedsTable = new Class({
3249         Extends: DynamicTable,
3251         initColumns: function() {
3252             this.newColumn("url", "", "QBT_TR(URL)QBT_TR[CONTEXT=HttpServer]", 500, true);
3253         },
3254     });
3256     return exports();
3257 })();
3258 Object.freeze(window.qBittorrent.DynamicTable);
3260 /*************************************************************/