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