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