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