Use appropriate icon for "moving" torrents in transfer list
[qBittorrent.git] / src / webui / www / private / scripts / dynamicTable.js
blobe66946348df155d52bddc27d09d3a09c89fdf71b
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) {
705             if (fullUpdate === undefined)
706                 fullUpdate = false;
708             const rows = this.getFilteredAndSortedRows();
710             for (let i = 0; i < this.selectedRows.length; ++i)
711                 if (!(this.selectedRows[i] in rows)) {
712                     this.selectedRows.splice(i, 1);
713                     --i;
714                 }
716             const trs = this.tableBody.getElements('tr');
718             for (let rowPos = 0; rowPos < rows.length; ++rowPos) {
719                 const rowId = rows[rowPos]['rowId'];
720                 let tr_found = false;
721                 for (let j = rowPos; j < trs.length; ++j)
722                     if (trs[j]['rowId'] == rowId) {
723                         tr_found = true;
724                         if (rowPos == j)
725                             break;
726                         trs[j].inject(trs[rowPos], 'before');
727                         const tmpTr = trs[j];
728                         trs.splice(j, 1);
729                         trs.splice(rowPos, 0, tmpTr);
730                         break;
731                     }
732                 if (tr_found) // row already exists in the table
733                     this.updateRow(trs[rowPos], fullUpdate);
734                 else { // else create a new row in the table
735                     const tr = new Element('tr');
736                     // set tabindex so element receives keydown events
737                     // more info: https://developer.mozilla.org/en-US/docs/Web/API/Element/keydown_event
738                     tr.setProperty("tabindex", "-1");
740                     const rowId = rows[rowPos]['rowId'];
741                     tr.setProperty("data-row-id", rowId);
742                     tr['rowId'] = rowId;
744                     tr._this = this;
745                     tr.addEvent('contextmenu', function(e) {
746                         if (!this._this.isRowSelected(this.rowId)) {
747                             this._this.deselectAll();
748                             this._this.selectRow(this.rowId);
749                         }
750                         return true;
751                     });
752                     tr.addEvent('click', function(e) {
753                         e.stop();
754                         if (e.control || e.meta) {
755                             // CTRL/CMD âŒ˜ key was pressed
756                             if (this._this.isRowSelected(this.rowId))
757                                 this._this.deselectRow(this.rowId);
758                             else
759                                 this._this.selectRow(this.rowId);
760                         }
761                         else if (e.shift && (this._this.selectedRows.length == 1)) {
762                             // Shift key was pressed
763                             this._this.selectRows(this._this.getSelectedRowId(), this.rowId);
764                         }
765                         else {
766                             // Simple selection
767                             this._this.deselectAll();
768                             this._this.selectRow(this.rowId);
769                         }
770                         return false;
771                     });
772                     tr.addEvent('touchstart', function(e) {
773                         if (!this._this.isRowSelected(this.rowId)) {
774                             this._this.deselectAll();
775                             this._this.selectRow(this.rowId);
776                         }
777                         return false;
778                     });
779                     tr.addEvent('keydown', function(event) {
780                         switch (event.key) {
781                             case "up":
782                                 this._this.selectPreviousRow();
783                                 return false;
784                             case "down":
785                                 this._this.selectNextRow();
786                                 return false;
787                         }
788                     });
790                     this.setupTr(tr);
792                     for (let k = 0; k < this.columns.length; ++k) {
793                         const td = new Element('td');
794                         if ((this.columns[k].visible == '0') || this.columns[k].force_hide)
795                             td.addClass('invisible');
796                         td.injectInside(tr);
797                     }
799                     // Insert
800                     if (rowPos >= trs.length) {
801                         tr.inject(this.tableBody);
802                         trs.push(tr);
803                     }
804                     else {
805                         tr.inject(trs[rowPos], 'before');
806                         trs.splice(rowPos, 0, tr);
807                     }
809                     // Update context menu
810                     if (this.contextMenu)
811                         this.contextMenu.addTarget(tr);
813                     this.updateRow(tr, true);
814                 }
815             }
817             let rowPos = rows.length;
819             while ((rowPos < trs.length) && (trs.length > 0)) {
820                 trs[trs.length - 1].dispose();
821                 trs.pop();
822             }
823         },
825         setupTr: function(tr) {},
827         updateRow: function(tr, fullUpdate) {
828             const row = this.rows.get(tr.rowId);
829             const data = row[fullUpdate ? 'full_data' : 'data'];
831             const tds = tr.getElements('td');
832             for (let i = 0; i < this.columns.length; ++i) {
833                 if (Object.prototype.hasOwnProperty.call(data, this.columns[i].dataProperties[0]))
834                     this.columns[i].updateTd(tds[i], row);
835             }
836             row['data'] = {};
837         },
839         removeRow: function(rowId) {
840             this.selectedRows.erase(rowId);
841             const tr = this.getTrByRowId(rowId);
842             if (tr !== null) {
843                 tr.dispose();
844                 this.rows.erase(rowId);
845                 return true;
846             }
847             return false;
848         },
850         clear: function() {
851             this.deselectAll();
852             this.rows.empty();
853             const trs = this.tableBody.getElements('tr');
854             while (trs.length > 0) {
855                 trs[trs.length - 1].dispose();
856                 trs.pop();
857             }
858         },
860         selectedRowsIds: function() {
861             return this.selectedRows.slice();
862         },
864         getRowIds: function() {
865             return this.rows.getKeys();
866         },
868         selectNextRow: function() {
869             const visibleRows = $(this.dynamicTableDivId).getElements("tbody tr").filter(e => e.getStyle("display") !== "none");
870             const selectedRowId = this.getSelectedRowId();
872             let selectedIndex = -1;
873             for (let i = 0; i < visibleRows.length; ++i) {
874                 const row = visibleRows[i];
875                 if (row.getProperty("data-row-id") === selectedRowId) {
876                     selectedIndex = i;
877                     break;
878                 }
879             }
881             const isLastRowSelected = (selectedIndex >= (visibleRows.length - 1));
882             if (!isLastRowSelected) {
883                 this.deselectAll();
885                 const newRow = visibleRows[selectedIndex + 1];
886                 this.selectRow(newRow.getProperty("data-row-id"));
887             }
888         },
890         selectPreviousRow: function() {
891             const visibleRows = $(this.dynamicTableDivId).getElements("tbody tr").filter(e => e.getStyle("display") !== "none");
892             const selectedRowId = this.getSelectedRowId();
894             let selectedIndex = -1;
895             for (let i = 0; i < visibleRows.length; ++i) {
896                 const row = visibleRows[i];
897                 if (row.getProperty("data-row-id") === selectedRowId) {
898                     selectedIndex = i;
899                     break;
900                 }
901             }
903             const isFirstRowSelected = selectedIndex <= 0;
904             if (!isFirstRowSelected) {
905                 this.deselectAll();
907                 const newRow = visibleRows[selectedIndex - 1];
908                 this.selectRow(newRow.getProperty("data-row-id"));
909             }
910         },
911     });
913     const TorrentsTable = new Class({
914         Extends: DynamicTable,
916         initColumns: function() {
917             this.newColumn('priority', '', '#', 30, true);
918             this.newColumn('state_icon', 'cursor: default', '', 22, true);
919             this.newColumn('name', '', 'QBT_TR(Name)QBT_TR[CONTEXT=TransferListModel]', 200, true);
920             this.newColumn('size', '', 'QBT_TR(Size)QBT_TR[CONTEXT=TransferListModel]', 100, true);
921             this.newColumn('total_size', '', 'QBT_TR(Total Size)QBT_TR[CONTEXT=TransferListModel]', 100, false);
922             this.newColumn('progress', '', 'QBT_TR(Done)QBT_TR[CONTEXT=TransferListModel]', 85, true);
923             this.newColumn('status', '', 'QBT_TR(Status)QBT_TR[CONTEXT=TransferListModel]', 100, true);
924             this.newColumn('num_seeds', '', 'QBT_TR(Seeds)QBT_TR[CONTEXT=TransferListModel]', 100, true);
925             this.newColumn('num_leechs', '', 'QBT_TR(Peers)QBT_TR[CONTEXT=TransferListModel]', 100, true);
926             this.newColumn('dlspeed', '', 'QBT_TR(Down Speed)QBT_TR[CONTEXT=TransferListModel]', 100, true);
927             this.newColumn('upspeed', '', 'QBT_TR(Up Speed)QBT_TR[CONTEXT=TransferListModel]', 100, true);
928             this.newColumn('eta', '', 'QBT_TR(ETA)QBT_TR[CONTEXT=TransferListModel]', 100, true);
929             this.newColumn('ratio', '', 'QBT_TR(Ratio)QBT_TR[CONTEXT=TransferListModel]', 100, true);
930             this.newColumn('category', '', 'QBT_TR(Category)QBT_TR[CONTEXT=TransferListModel]', 100, true);
931             this.newColumn('tags', '', 'QBT_TR(Tags)QBT_TR[CONTEXT=TransferListModel]', 100, true);
932             this.newColumn('added_on', '', 'QBT_TR(Added On)QBT_TR[CONTEXT=TransferListModel]', 100, true);
933             this.newColumn('completion_on', '', 'QBT_TR(Completed On)QBT_TR[CONTEXT=TransferListModel]', 100, false);
934             this.newColumn('tracker', '', 'QBT_TR(Tracker)QBT_TR[CONTEXT=TransferListModel]', 100, false);
935             this.newColumn('dl_limit', '', 'QBT_TR(Down Limit)QBT_TR[CONTEXT=TransferListModel]', 100, false);
936             this.newColumn('up_limit', '', 'QBT_TR(Up Limit)QBT_TR[CONTEXT=TransferListModel]', 100, false);
937             this.newColumn('downloaded', '', 'QBT_TR(Downloaded)QBT_TR[CONTEXT=TransferListModel]', 100, false);
938             this.newColumn('uploaded', '', 'QBT_TR(Uploaded)QBT_TR[CONTEXT=TransferListModel]', 100, false);
939             this.newColumn('downloaded_session', '', 'QBT_TR(Session Download)QBT_TR[CONTEXT=TransferListModel]', 100, false);
940             this.newColumn('uploaded_session', '', 'QBT_TR(Session Upload)QBT_TR[CONTEXT=TransferListModel]', 100, false);
941             this.newColumn('amount_left', '', 'QBT_TR(Remaining)QBT_TR[CONTEXT=TransferListModel]', 100, false);
942             this.newColumn('time_active', '', 'QBT_TR(Time Active)QBT_TR[CONTEXT=TransferListModel]', 100, false);
943             this.newColumn('save_path', '', 'QBT_TR(Save path)QBT_TR[CONTEXT=TransferListModel]', 100, false);
944             this.newColumn('completed', '', 'QBT_TR(Completed)QBT_TR[CONTEXT=TransferListModel]', 100, false);
945             this.newColumn('max_ratio', '', 'QBT_TR(Ratio Limit)QBT_TR[CONTEXT=TransferListModel]', 100, false);
946             this.newColumn('seen_complete', '', 'QBT_TR(Last Seen Complete)QBT_TR[CONTEXT=TransferListModel]', 100, false);
947             this.newColumn('last_activity', '', 'QBT_TR(Last Activity)QBT_TR[CONTEXT=TransferListModel]', 100, false);
948             this.newColumn('availability', '', 'QBT_TR(Availability)QBT_TR[CONTEXT=TransferListModel]', 100, false);
949             this.newColumn('reannounce', '', 'QBT_TR(Reannounce In)QBT_TR[CONTEXT=TransferListModel]', 100, false);
951             this.columns['state_icon'].onclick = '';
952             this.columns['state_icon'].dataProperties[0] = 'state';
954             this.columns['num_seeds'].dataProperties.push('num_complete');
955             this.columns['num_leechs'].dataProperties.push('num_incomplete');
956             this.columns['time_active'].dataProperties.push('seeding_time');
958             this.initColumnsFunctions();
959         },
961         initColumnsFunctions: function() {
963             // state_icon
964             this.columns['state_icon'].updateTd = function(td, row) {
965                 let state = this.getRowValue(row);
966                 let img_path;
967                 // normalize states
968                 switch (state) {
969                     case "forcedDL":
970                     case "metaDL":
971                     case "forcedMetaDL":
972                     case "downloading":
973                         state = "downloading";
974                         img_path = "images/downloading.svg";
975                         break;
976                     case "forcedUP":
977                     case "uploading":
978                         state = "uploading";
979                         img_path = "images/upload.svg";
980                         break;
981                     case "stalledUP":
982                         state = "stalledUP";
983                         img_path = "images/stalledUP.svg";
984                         break;
985                     case "stalledDL":
986                         state = "stalledDL";
987                         img_path = "images/stalledDL.svg";
988                         break;
989                     case "pausedDL":
990                         state = "torrent-stop";
991                         img_path = "images/stopped.svg";
992                         break;
993                     case "pausedUP":
994                         state = "checked-completed";
995                         img_path = "images/checked-completed.svg";
996                         break;
997                     case "queuedDL":
998                     case "queuedUP":
999                         state = "queued";
1000                         img_path = "images/queued.svg";
1001                         break;
1002                     case "checkingDL":
1003                     case "checkingUP":
1004                     case "queuedForChecking":
1005                     case "checkingResumeData":
1006                         state = "force-recheck";
1007                         img_path = "images/force-recheck.svg";
1008                         break;
1009                     case "moving":
1010                         state = "moving";
1011                         img_path = "images/set-location.svg";
1012                         break;
1013                     case "error":
1014                     case "unknown":
1015                     case "missingFiles":
1016                         state = "error";
1017                         img_path = "images/error.svg";
1018                         break;
1019                     default:
1020                         break; // do nothing
1021                 }
1023                 if (td.getChildren('img').length > 0) {
1024                     const img = td.getChildren('img')[0];
1025                     if (img.src.indexOf(img_path) < 0) {
1026                         img.set('src', img_path);
1027                         img.set('title', state);
1028                     }
1029                 }
1030                 else {
1031                     td.adopt(new Element('img', {
1032                         'src': img_path,
1033                         'class': 'stateIcon',
1034                         'title': state
1035                     }));
1036                 }
1037             };
1039             // status
1040             this.columns['status'].updateTd = function(td, row) {
1041                 const state = this.getRowValue(row);
1042                 if (!state)
1043                     return;
1045                 let status;
1046                 switch (state) {
1047                     case "downloading":
1048                         status = "QBT_TR(Downloading)QBT_TR[CONTEXT=TransferListDelegate]";
1049                         break;
1050                     case "stalledDL":
1051                         status = "QBT_TR(Stalled)QBT_TR[CONTEXT=TransferListDelegate]";
1052                         break;
1053                     case "metaDL":
1054                         status = "QBT_TR(Downloading metadata)QBT_TR[CONTEXT=TransferListDelegate]";
1055                         break;
1056                     case "forcedMetaDL":
1057                         status = "QBT_TR([F] Downloading metadata)QBT_TR[CONTEXT=TransferListDelegate]";
1058                         break;
1059                     case "forcedDL":
1060                         status = "QBT_TR([F] Downloading)QBT_TR[CONTEXT=TransferListDelegate]";
1061                         break;
1062                     case "uploading":
1063                     case "stalledUP":
1064                         status = "QBT_TR(Seeding)QBT_TR[CONTEXT=TransferListDelegate]";
1065                         break;
1066                     case "forcedUP":
1067                         status = "QBT_TR([F] Seeding)QBT_TR[CONTEXT=TransferListDelegate]";
1068                         break;
1069                     case "queuedDL":
1070                     case "queuedUP":
1071                         status = "QBT_TR(Queued)QBT_TR[CONTEXT=TransferListDelegate]";
1072                         break;
1073                     case "checkingDL":
1074                     case "checkingUP":
1075                         status = "QBT_TR(Checking)QBT_TR[CONTEXT=TransferListDelegate]";
1076                         break;
1077                     case "queuedForChecking":
1078                         status = "QBT_TR(Queued for checking)QBT_TR[CONTEXT=TransferListDelegate]";
1079                         break;
1080                     case "checkingResumeData":
1081                         status = "QBT_TR(Checking resume data)QBT_TR[CONTEXT=TransferListDelegate]";
1082                         break;
1083                     case "pausedDL":
1084                         status = "QBT_TR(Paused)QBT_TR[CONTEXT=TransferListDelegate]";
1085                         break;
1086                     case "pausedUP":
1087                         status = "QBT_TR(Completed)QBT_TR[CONTEXT=TransferListDelegate]";
1088                         break;
1089                     case "moving":
1090                         status = "QBT_TR(Moving)QBT_TR[CONTEXT=TransferListDelegate]";
1091                         break;
1092                     case "missingFiles":
1093                         status = "QBT_TR(Missing Files)QBT_TR[CONTEXT=TransferListDelegate]";
1094                         break;
1095                     case "error":
1096                         status = "QBT_TR(Errored)QBT_TR[CONTEXT=TransferListDelegate]";
1097                         break;
1098                     default:
1099                         status = "QBT_TR(Unknown)QBT_TR[CONTEXT=HttpServer]";
1100                 }
1102                 td.set('text', status);
1103                 td.set('title', status);
1104             };
1106             // priority
1107             this.columns['priority'].updateTd = function(td, row) {
1108                 const queuePos = this.getRowValue(row);
1109                 const formattedQueuePos = (queuePos < 1) ? '*' : queuePos;
1110                 td.set('text', formattedQueuePos);
1111                 td.set('title', formattedQueuePos);
1112             };
1114             this.columns['priority'].compareRows = function(row1, row2) {
1115                 let row1_val = this.getRowValue(row1);
1116                 let row2_val = this.getRowValue(row2);
1117                 if (row1_val < 1)
1118                     row1_val = 1000000;
1119                 if (row2_val < 1)
1120                     row2_val = 1000000;
1121                 return compareNumbers(row1_val, row2_val);
1122             };
1124             // name, category, tags
1125             this.columns['name'].compareRows = function(row1, row2) {
1126                 const row1Val = this.getRowValue(row1);
1127                 const row2Val = this.getRowValue(row2);
1128                 return row1Val.localeCompare(row2Val, undefined, { numeric: true, sensitivity: 'base' });
1129             };
1130             this.columns['category'].compareRows = this.columns['name'].compareRows;
1131             this.columns['tags'].compareRows = this.columns['name'].compareRows;
1133             // size, total_size
1134             this.columns['size'].updateTd = function(td, row) {
1135                 const size = window.qBittorrent.Misc.friendlyUnit(this.getRowValue(row), false);
1136                 td.set('text', size);
1137                 td.set('title', size);
1138             };
1139             this.columns['total_size'].updateTd = this.columns['size'].updateTd;
1141             // progress
1142             this.columns['progress'].updateTd = function(td, row) {
1143                 const progress = this.getRowValue(row);
1144                 let progressFormatted = (progress * 100).round(1);
1145                 if (progressFormatted == 100.0 && progress != 1.0)
1146                     progressFormatted = 99.9;
1148                 if (td.getChildren('div').length > 0) {
1149                     const div = td.getChildren('div')[0];
1150                     if (td.resized) {
1151                         td.resized = false;
1152                         div.setWidth(ProgressColumnWidth - 5);
1153                     }
1154                     if (div.getValue() != progressFormatted)
1155                         div.setValue(progressFormatted);
1156                 }
1157                 else {
1158                     if (ProgressColumnWidth < 0)
1159                         ProgressColumnWidth = td.offsetWidth;
1160                     td.adopt(new window.qBittorrent.ProgressBar.ProgressBar(progressFormatted.toFloat(), {
1161                         'width': ProgressColumnWidth - 5
1162                     }));
1163                     td.resized = false;
1164                 }
1165             };
1167             this.columns['progress'].onResize = function(columnName) {
1168                 const pos = this.getColumnPos(columnName);
1169                 const trs = this.tableBody.getElements('tr');
1170                 ProgressColumnWidth = -1;
1171                 for (let i = 0; i < trs.length; ++i) {
1172                     const td = trs[i].getElements('td')[pos];
1173                     if (ProgressColumnWidth < 0)
1174                         ProgressColumnWidth = td.offsetWidth;
1175                     td.resized = true;
1176                     this.columns[columnName].updateTd(td, this.rows.get(trs[i].rowId));
1177                 }
1178             }.bind(this);
1180             // num_seeds
1181             this.columns['num_seeds'].updateTd = function(td, row) {
1182                 const num_seeds = this.getRowValue(row, 0);
1183                 const num_complete = this.getRowValue(row, 1);
1184                 let value = num_seeds;
1185                 if (num_complete != -1)
1186                     value += ' (' + num_complete + ')';
1187                 td.set('text', value);
1188                 td.set('title', value);
1189             };
1190             this.columns['num_seeds'].compareRows = function(row1, row2) {
1191                 const num_seeds1 = this.getRowValue(row1, 0);
1192                 const num_complete1 = this.getRowValue(row1, 1);
1194                 const num_seeds2 = this.getRowValue(row2, 0);
1195                 const num_complete2 = this.getRowValue(row2, 1);
1197                 const result = compareNumbers(num_complete1, num_complete2);
1198                 if (result !== 0)
1199                     return result;
1200                 return compareNumbers(num_seeds1, num_seeds2);
1201             };
1203             // num_leechs
1204             this.columns['num_leechs'].updateTd = this.columns['num_seeds'].updateTd;
1205             this.columns['num_leechs'].compareRows = this.columns['num_seeds'].compareRows;
1207             // dlspeed
1208             this.columns['dlspeed'].updateTd = function(td, row) {
1209                 const speed = window.qBittorrent.Misc.friendlyUnit(this.getRowValue(row), true);
1210                 td.set('text', speed);
1211                 td.set('title', speed);
1212             };
1214             // upspeed
1215             this.columns['upspeed'].updateTd = this.columns['dlspeed'].updateTd;
1217             // eta
1218             this.columns['eta'].updateTd = function(td, row) {
1219                 const eta = window.qBittorrent.Misc.friendlyDuration(this.getRowValue(row), window.qBittorrent.Misc.MAX_ETA);
1220                 td.set('text', eta);
1221                 td.set('title', eta);
1222             };
1224             // ratio
1225             this.columns['ratio'].updateTd = function(td, row) {
1226                 const ratio = this.getRowValue(row);
1227                 const string = (ratio === -1) ? '∞' : window.qBittorrent.Misc.toFixedPointString(ratio, 2);
1228                 td.set('text', string);
1229                 td.set('title', string);
1230             };
1232             // added on
1233             this.columns['added_on'].updateTd = function(td, row) {
1234                 const date = new Date(this.getRowValue(row) * 1000).toLocaleString();
1235                 td.set('text', date);
1236                 td.set('title', date);
1237             };
1239             // completion_on
1240             this.columns['completion_on'].updateTd = function(td, row) {
1241                 const val = this.getRowValue(row);
1242                 if ((val === 0xffffffff) || (val < 0)) {
1243                     td.set('text', '');
1244                     td.set('title', '');
1245                 }
1246                 else {
1247                     const date = new Date(this.getRowValue(row) * 1000).toLocaleString();
1248                     td.set('text', date);
1249                     td.set('title', date);
1250                 }
1251             };
1253             //  dl_limit, up_limit
1254             this.columns['dl_limit'].updateTd = function(td, row) {
1255                 const speed = this.getRowValue(row);
1256                 if (speed === 0) {
1257                     td.set('text', '∞');
1258                     td.set('title', '∞');
1259                 }
1260                 else {
1261                     const formattedSpeed = window.qBittorrent.Misc.friendlyUnit(speed, true);
1262                     td.set('text', formattedSpeed);
1263                     td.set('title', formattedSpeed);
1264                 }
1265             };
1267             this.columns['up_limit'].updateTd = this.columns['dl_limit'].updateTd;
1269             // downloaded, uploaded, downloaded_session, uploaded_session, amount_left
1270             this.columns['downloaded'].updateTd = this.columns['size'].updateTd;
1271             this.columns['uploaded'].updateTd = this.columns['size'].updateTd;
1272             this.columns['downloaded_session'].updateTd = this.columns['size'].updateTd;
1273             this.columns['uploaded_session'].updateTd = this.columns['size'].updateTd;
1274             this.columns['amount_left'].updateTd = this.columns['size'].updateTd;
1276             // time active
1277             this.columns['time_active'].updateTd = function(td, row) {
1278                 const activeTime = this.getRowValue(row, 0);
1279                 const seedingTime = this.getRowValue(row, 1);
1280                 const time = (seedingTime > 0)
1281                     ? ('QBT_TR(%1 (seeded for %2))QBT_TR[CONTEXT=TransferListDelegate]'
1282                         .replace('%1', window.qBittorrent.Misc.friendlyDuration(activeTime))
1283                         .replace('%2', window.qBittorrent.Misc.friendlyDuration(seedingTime)))
1284                     : window.qBittorrent.Misc.friendlyDuration(activeTime);
1285                 td.set('text', time);
1286                 td.set('title', time);
1287             };
1289             // completed
1290             this.columns['completed'].updateTd = this.columns['size'].updateTd;
1292             // max_ratio
1293             this.columns['max_ratio'].updateTd = this.columns['ratio'].updateTd;
1295             // seen_complete
1296             this.columns['seen_complete'].updateTd = this.columns['completion_on'].updateTd;
1298             // last_activity
1299             this.columns['last_activity'].updateTd = function(td, row) {
1300                 const val = this.getRowValue(row);
1301                 if (val < 1) {
1302                     td.set('text', '∞');
1303                     td.set('title', '∞');
1304                 }
1305                 else {
1306                     const formattedVal = 'QBT_TR(%1 ago)QBT_TR[CONTEXT=TransferListDelegate]'.replace('%1', window.qBittorrent.Misc.friendlyDuration((new Date()) / 1000 - val));
1307                     td.set('text', formattedVal);
1308                     td.set('title', formattedVal);
1309                 }
1310             };
1312             // availability
1313             this.columns['availability'].updateTd = function(td, row) {
1314                 const value = window.qBittorrent.Misc.toFixedPointString(this.getRowValue(row), 3);
1315                 td.set('text', value);
1316                 td.set('title', value);
1317             };
1319             // reannounce
1320             this.columns['reannounce'].updateTd = function(td, row) {
1321                 const time = window.qBittorrent.Misc.friendlyDuration(this.getRowValue(row));
1322                 td.set('text', time);
1323                 td.set('title', time);
1324             };
1325         },
1327         applyFilter: function(row, filterName, categoryHash, tagHash, trackerHash, filterTerms) {
1328             const state = row['full_data'].state;
1329             const name = row['full_data'].name.toLowerCase();
1330             let inactive = false;
1331             let r;
1333             switch (filterName) {
1334                 case 'downloading':
1335                     if ((state != 'downloading') && (state.indexOf('DL') === -1))
1336                         return false;
1337                     break;
1338                 case 'seeding':
1339                     if (state != 'uploading' && state != 'forcedUP' && state != 'stalledUP' && state != 'queuedUP' && state != 'checkingUP')
1340                         return false;
1341                     break;
1342                 case 'completed':
1343                     if ((state != 'uploading') && (state.indexOf('UP') === -1))
1344                         return false;
1345                     break;
1346                 case 'paused':
1347                     if (state.indexOf('paused') === -1)
1348                         return false;
1349                     break;
1350                 case 'resumed':
1351                     if (state.indexOf('paused') > -1)
1352                         return false;
1353                     break;
1354                 case 'stalled':
1355                     if ((state != 'stalledUP') && (state != 'stalledDL'))
1356                         return false;
1357                     break;
1358                 case 'stalled_uploading':
1359                     if (state != 'stalledUP')
1360                         return false;
1361                     break;
1362                 case 'stalled_downloading':
1363                     if (state != 'stalledDL')
1364                         return false;
1365                     break;
1366                 case 'inactive':
1367                     inactive = true;
1368                     // fallthrough
1369                 case 'active':
1370                     if (state == 'stalledDL')
1371                         r = (row['full_data'].upspeed > 0);
1372                     else
1373                         r = state == 'metaDL' || state == 'forcedMetaDL' || state == 'downloading' || state == 'forcedDL' || state == 'uploading' || state == 'forcedUP';
1374                     if (r == inactive)
1375                         return false;
1376                     break;
1377                 case 'checking':
1378                     if (state !== 'checkingUP' && state !== 'checkingDL' && state !== 'checkingResumeData')
1379                         return false;
1380                     break;
1381                 case 'errored':
1382                     if (state != 'error' && state != "unknown" && state != "missingFiles")
1383                         return false;
1384                     break;
1385             }
1387             const categoryHashInt = parseInt(categoryHash);
1388             if (!isNaN(categoryHashInt)) {
1389                 switch (categoryHashInt) {
1390                     case CATEGORIES_ALL:
1391                         break; // do nothing
1392                     case CATEGORIES_UNCATEGORIZED:
1393                         if (row['full_data'].category.length !== 0)
1394                             return false;
1395                         break; // do nothing
1396                     default:
1397                         if (!useSubcategories) {
1398                             if (categoryHashInt !== genHash(row['full_data'].category))
1399                                 return false;
1400                         }
1401                         else {
1402                             const selectedCategoryName = category_list[categoryHash].name + "/";
1403                             const torrentCategoryName = row['full_data'].category + "/";
1404                             if (!torrentCategoryName.startsWith(selectedCategoryName))
1405                                 return false;
1406                         }
1407                 }
1408             }
1410             const tagHashInt = parseInt(tagHash);
1411             const isNumber = !isNaN(tagHashInt);
1412             if (isNumber) {
1413                 switch (tagHashInt) {
1414                     case TAGS_ALL:
1415                         break; // do nothing
1417                     case TAGS_UNTAGGED:
1418                         if (row['full_data'].tags.length !== 0)
1419                             return false;
1420                         break; // do nothing
1422                     default: {
1423                         let rowTags = row['full_data'].tags.split(', ');
1424                         rowTags = rowTags.map(function(tag) {
1425                             return genHash(tag);
1426                         });
1427                         if (!rowTags.contains(tagHashInt))
1428                             return false;
1429                         break;
1430                     }
1431                 }
1432             }
1434             const trackerHashInt = Number.parseInt(trackerHash, 10);
1435             switch (trackerHashInt) {
1436                 case TRACKERS_ALL:
1437                     break; // do nothing
1438                 case TRACKERS_TRACKERLESS:
1439                     if (row['full_data'].trackers_count !== 0)
1440                         return false;
1441                     break;
1442                 default: {
1443                     const tracker = trackerList.get(trackerHashInt);
1444                     if (tracker && !tracker.torrents.includes(row['full_data'].rowId))
1445                         return false;
1446                     break;
1447                 }
1448             }
1450             if ((filterTerms !== undefined) && (filterTerms !== null)
1451                 && (filterTerms.length > 0) && !window.qBittorrent.Misc.containsAllTerms(name, filterTerms))
1452                 return false;
1454             return true;
1455         },
1457         getFilteredTorrentsNumber: function(filterName, categoryHash, tagHash, trackerHash) {
1458             let cnt = 0;
1459             const rows = this.rows.getValues();
1461             for (let i = 0; i < rows.length; ++i)
1462                 if (this.applyFilter(rows[i], filterName, categoryHash, tagHash, trackerHash, null))
1463                     ++cnt;
1464             return cnt;
1465         },
1467         getFilteredTorrentsHashes: function(filterName, categoryHash, tagHash, trackerHash) {
1468             const rowsHashes = [];
1469             const rows = this.rows.getValues();
1471             for (let i = 0; i < rows.length; ++i)
1472                 if (this.applyFilter(rows[i], filterName, categoryHash, tagHash, trackerHash, null))
1473                     rowsHashes.push(rows[i]['rowId']);
1475             return rowsHashes;
1476         },
1478         getFilteredAndSortedRows: function() {
1479             const filteredRows = [];
1481             const rows = this.rows.getValues();
1482             const filterText = $('torrentsFilterInput').value.trim().toLowerCase();
1483             const filterTerms = (filterText.length > 0) ? filterText.split(" ") : null;
1485             for (let i = 0; i < rows.length; ++i) {
1486                 if (this.applyFilter(rows[i], selected_filter, selected_category, selectedTag, selectedTracker, filterTerms)) {
1487                     filteredRows.push(rows[i]);
1488                     filteredRows[rows[i].rowId] = rows[i];
1489                 }
1490             }
1492             filteredRows.sort(function(row1, row2) {
1493                 const column = this.columns[this.sortedColumn];
1494                 const res = column.compareRows(row1, row2);
1495                 if (this.reverseSort === '0')
1496                     return res;
1497                 else
1498                     return -res;
1499             }.bind(this));
1500             return filteredRows;
1501         },
1503         setupTr: function(tr) {
1504             tr.addEvent('dblclick', function(e) {
1505                 e.stop();
1506                 this._this.deselectAll();
1507                 this._this.selectRow(this.rowId);
1508                 const row = this._this.rows.get(this.rowId);
1509                 const state = row['full_data'].state;
1510                 if (state.indexOf('paused') > -1)
1511                     startFN();
1512                 else
1513                     pauseFN();
1514                 return true;
1515             });
1516             tr.addClass("torrentsTableContextMenuTarget");
1517         },
1519         getCurrentTorrentID: function() {
1520             return this.getSelectedRowId();
1521         },
1523         onSelectedRowChanged: function() {
1524             updatePropertiesPanel();
1525         }
1526     });
1528     const TorrentPeersTable = new Class({
1529         Extends: DynamicTable,
1531         initColumns: function() {
1532             this.newColumn('country', '', 'QBT_TR(Country/Region)QBT_TR[CONTEXT=PeerListWidget]', 22, true);
1533             this.newColumn('ip', '', 'QBT_TR(IP)QBT_TR[CONTEXT=PeerListWidget]', 80, true);
1534             this.newColumn('port', '', 'QBT_TR(Port)QBT_TR[CONTEXT=PeerListWidget]', 35, true);
1535             this.newColumn('connection', '', 'QBT_TR(Connection)QBT_TR[CONTEXT=PeerListWidget]', 50, true);
1536             this.newColumn('flags', '', 'QBT_TR(Flags)QBT_TR[CONTEXT=PeerListWidget]', 50, true);
1537             this.newColumn('client', '', 'QBT_TR(Client)QBT_TR[CONTEXT=PeerListWidget]', 140, true);
1538             this.newColumn('peer_id_client', '', 'QBT_TR(Peer ID Client)QBT_TR[CONTEXT=PeerListWidget]', 60, false);
1539             this.newColumn('progress', '', 'QBT_TR(Progress)QBT_TR[CONTEXT=PeerListWidget]', 50, true);
1540             this.newColumn('dl_speed', '', 'QBT_TR(Down Speed)QBT_TR[CONTEXT=PeerListWidget]', 50, true);
1541             this.newColumn('up_speed', '', 'QBT_TR(Up Speed)QBT_TR[CONTEXT=PeerListWidget]', 50, true);
1542             this.newColumn('downloaded', '', 'QBT_TR(Downloaded)QBT_TR[CONTEXT=PeerListWidget]', 50, true);
1543             this.newColumn('uploaded', '', 'QBT_TR(Uploaded)QBT_TR[CONTEXT=PeerListWidget]', 50, true);
1544             this.newColumn('relevance', '', 'QBT_TR(Relevance)QBT_TR[CONTEXT=PeerListWidget]', 30, true);
1545             this.newColumn('files', '', 'QBT_TR(Files)QBT_TR[CONTEXT=PeerListWidget]', 100, true);
1547             this.columns['country'].dataProperties.push('country_code');
1548             this.columns['flags'].dataProperties.push('flags_desc');
1549             this.initColumnsFunctions();
1550         },
1552         initColumnsFunctions: function() {
1554             // country
1555             this.columns['country'].updateTd = function(td, row) {
1556                 const country = this.getRowValue(row, 0);
1557                 const country_code = this.getRowValue(row, 1);
1559                 if (!country_code) {
1560                     if (td.getChildren('img').length > 0)
1561                         td.getChildren('img')[0].dispose();
1562                     return;
1563                 }
1565                 const img_path = 'images/flags/' + country_code + '.svg';
1567                 if (td.getChildren('img').length > 0) {
1568                     const img = td.getChildren('img')[0];
1569                     img.set('src', img_path);
1570                     img.set('class', 'flags');
1571                     img.set('alt', country);
1572                     img.set('title', country);
1573                 }
1574                 else
1575                     td.adopt(new Element('img', {
1576                         'src': img_path,
1577                         'class': 'flags',
1578                         'alt': country,
1579                         'title': country
1580                     }));
1581             };
1583             // ip
1584             this.columns['ip'].compareRows = function(row1, row2) {
1585                 const ip1 = this.getRowValue(row1);
1586                 const ip2 = this.getRowValue(row2);
1588                 const a = ip1.split(".");
1589                 const b = ip2.split(".");
1591                 for (let i = 0; i < 4; ++i) {
1592                     if (a[i] != b[i])
1593                         return a[i] - b[i];
1594                 }
1596                 return 0;
1597             };
1599             // flags
1600             this.columns['flags'].updateTd = function(td, row) {
1601                 td.set('text', this.getRowValue(row, 0));
1602                 td.set('title', this.getRowValue(row, 1));
1603             };
1605             // progress
1606             this.columns['progress'].updateTd = function(td, row) {
1607                 const progress = this.getRowValue(row);
1608                 let progressFormatted = (progress * 100).round(1);
1609                 if (progressFormatted == 100.0 && progress != 1.0)
1610                     progressFormatted = 99.9;
1611                 progressFormatted += "%";
1612                 td.set('text', progressFormatted);
1613                 td.set('title', progressFormatted);
1614             };
1616             // dl_speed, up_speed
1617             this.columns['dl_speed'].updateTd = function(td, row) {
1618                 const speed = this.getRowValue(row);
1619                 if (speed === 0) {
1620                     td.set('text', '');
1621                     td.set('title', '');
1622                 }
1623                 else {
1624                     const formattedSpeed = window.qBittorrent.Misc.friendlyUnit(speed, true);
1625                     td.set('text', formattedSpeed);
1626                     td.set('title', formattedSpeed);
1627                 }
1628             };
1629             this.columns['up_speed'].updateTd = this.columns['dl_speed'].updateTd;
1631             // downloaded, uploaded
1632             this.columns['downloaded'].updateTd = function(td, row) {
1633                 const downloaded = window.qBittorrent.Misc.friendlyUnit(this.getRowValue(row), false);
1634                 td.set('text', downloaded);
1635                 td.set('title', downloaded);
1636             };
1637             this.columns['uploaded'].updateTd = this.columns['downloaded'].updateTd;
1639             // relevance
1640             this.columns['relevance'].updateTd = this.columns['progress'].updateTd;
1642             // files
1643             this.columns['files'].updateTd = function(td, row) {
1644                 const value = this.getRowValue(row, 0);
1645                 td.set('text', value.replace(/\n/g, ';'));
1646                 td.set('title', value);
1647             };
1649         }
1650     });
1652     const SearchResultsTable = new Class({
1653         Extends: DynamicTable,
1655         initColumns: function() {
1656             this.newColumn('fileName', '', 'QBT_TR(Name)QBT_TR[CONTEXT=SearchResultsTable]', 500, true);
1657             this.newColumn('fileSize', '', 'QBT_TR(Size)QBT_TR[CONTEXT=SearchResultsTable]', 100, true);
1658             this.newColumn('nbSeeders', '', 'QBT_TR(Seeders)QBT_TR[CONTEXT=SearchResultsTable]', 100, true);
1659             this.newColumn('nbLeechers', '', 'QBT_TR(Leechers)QBT_TR[CONTEXT=SearchResultsTable]', 100, true);
1660             this.newColumn('siteUrl', '', 'QBT_TR(Search engine)QBT_TR[CONTEXT=SearchResultsTable]', 250, true);
1662             this.initColumnsFunctions();
1663         },
1665         initColumnsFunctions: function() {
1666             const displaySize = function(td, row) {
1667                 const size = window.qBittorrent.Misc.friendlyUnit(this.getRowValue(row), false);
1668                 td.set('text', size);
1669                 td.set('title', size);
1670             };
1671             const displayNum = function(td, row) {
1672                 const value = this.getRowValue(row);
1673                 const formattedValue = (value === "-1") ? "Unknown" : value;
1674                 td.set('text', formattedValue);
1675                 td.set('title', formattedValue);
1676             };
1678             this.columns['fileSize'].updateTd = displaySize;
1679             this.columns['nbSeeders'].updateTd = displayNum;
1680             this.columns['nbLeechers'].updateTd = displayNum;
1681         },
1683         getFilteredAndSortedRows: function() {
1684             const getSizeFilters = function() {
1685                 let minSize = (window.qBittorrent.Search.searchSizeFilter.min > 0.00) ? (window.qBittorrent.Search.searchSizeFilter.min * Math.pow(1024, window.qBittorrent.Search.searchSizeFilter.minUnit)) : 0.00;
1686                 let maxSize = (window.qBittorrent.Search.searchSizeFilter.max > 0.00) ? (window.qBittorrent.Search.searchSizeFilter.max * Math.pow(1024, window.qBittorrent.Search.searchSizeFilter.maxUnit)) : 0.00;
1688                 if ((minSize > maxSize) && (maxSize > 0.00)) {
1689                     const tmp = minSize;
1690                     minSize = maxSize;
1691                     maxSize = tmp;
1692                 }
1694                 return {
1695                     min: minSize,
1696                     max: maxSize
1697                 };
1698             };
1700             const getSeedsFilters = function() {
1701                 let minSeeds = (window.qBittorrent.Search.searchSeedsFilter.min > 0) ? window.qBittorrent.Search.searchSeedsFilter.min : 0;
1702                 let maxSeeds = (window.qBittorrent.Search.searchSeedsFilter.max > 0) ? window.qBittorrent.Search.searchSeedsFilter.max : 0;
1704                 if ((minSeeds > maxSeeds) && (maxSeeds > 0)) {
1705                     const tmp = minSeeds;
1706                     minSeeds = maxSeeds;
1707                     maxSeeds = tmp;
1708                 }
1710                 return {
1711                     min: minSeeds,
1712                     max: maxSeeds
1713                 };
1714             };
1716             let filteredRows = [];
1717             const rows = this.rows.getValues();
1718             const searchTerms = window.qBittorrent.Search.searchText.pattern.toLowerCase().split(" ");
1719             const filterTerms = window.qBittorrent.Search.searchText.filterPattern.toLowerCase().split(" ");
1720             const sizeFilters = getSizeFilters();
1721             const seedsFilters = getSeedsFilters();
1722             const searchInTorrentName = $('searchInTorrentName').get('value') === "names";
1724             if (searchInTorrentName || (filterTerms.length > 0) || (window.qBittorrent.Search.searchSizeFilter.min > 0.00) || (window.qBittorrent.Search.searchSizeFilter.max > 0.00)) {
1725                 for (let i = 0; i < rows.length; ++i) {
1726                     const row = rows[i];
1728                     if (searchInTorrentName && !window.qBittorrent.Misc.containsAllTerms(row.full_data.fileName, searchTerms))
1729                         continue;
1730                     if ((filterTerms.length > 0) && !window.qBittorrent.Misc.containsAllTerms(row.full_data.fileName, filterTerms))
1731                         continue;
1732                     if ((sizeFilters.min > 0.00) && (row.full_data.fileSize < sizeFilters.min))
1733                         continue;
1734                     if ((sizeFilters.max > 0.00) && (row.full_data.fileSize > sizeFilters.max))
1735                         continue;
1736                     if ((seedsFilters.min > 0) && (row.full_data.nbSeeders < seedsFilters.min))
1737                         continue;
1738                     if ((seedsFilters.max > 0) && (row.full_data.nbSeeders > seedsFilters.max))
1739                         continue;
1741                     filteredRows.push(row);
1742                 }
1743             }
1744             else {
1745                 filteredRows = rows;
1746             }
1748             filteredRows.sort(function(row1, row2) {
1749                 const column = this.columns[this.sortedColumn];
1750                 const res = column.compareRows(row1, row2);
1751                 if (this.reverseSort === '0')
1752                     return res;
1753                 else
1754                     return -res;
1755             }.bind(this));
1757             return filteredRows;
1758         },
1760         setupTr: function(tr) {
1761             tr.addClass("searchTableRow");
1762         }
1763     });
1765     const SearchPluginsTable = new Class({
1766         Extends: DynamicTable,
1768         initColumns: function() {
1769             this.newColumn('fullName', '', 'QBT_TR(Name)QBT_TR[CONTEXT=SearchPluginsTable]', 175, true);
1770             this.newColumn('version', '', 'QBT_TR(Version)QBT_TR[CONTEXT=SearchPluginsTable]', 100, true);
1771             this.newColumn('url', '', 'QBT_TR(Url)QBT_TR[CONTEXT=SearchPluginsTable]', 175, true);
1772             this.newColumn('enabled', '', 'QBT_TR(Enabled)QBT_TR[CONTEXT=SearchPluginsTable]', 100, true);
1774             this.initColumnsFunctions();
1775         },
1777         initColumnsFunctions: function() {
1778             this.columns['enabled'].updateTd = function(td, row) {
1779                 const value = this.getRowValue(row);
1780                 if (value) {
1781                     td.set('text', 'QBT_TR(Yes)QBT_TR[CONTEXT=SearchPluginsTable]');
1782                     td.set('title', 'QBT_TR(Yes)QBT_TR[CONTEXT=SearchPluginsTable]');
1783                     td.getParent("tr").addClass("green");
1784                     td.getParent("tr").removeClass("red");
1785                 }
1786                 else {
1787                     td.set('text', 'QBT_TR(No)QBT_TR[CONTEXT=SearchPluginsTable]');
1788                     td.set('title', 'QBT_TR(No)QBT_TR[CONTEXT=SearchPluginsTable]');
1789                     td.getParent("tr").addClass("red");
1790                     td.getParent("tr").removeClass("green");
1791                 }
1792             };
1793         },
1795         setupTr: function(tr) {
1796             tr.addClass("searchPluginsTableRow");
1797         }
1798     });
1800     const TorrentTrackersTable = new Class({
1801         Extends: DynamicTable,
1803         initColumns: function() {
1804             this.newColumn('tier', '', 'QBT_TR(Tier)QBT_TR[CONTEXT=TrackerListWidget]', 35, true);
1805             this.newColumn('url', '', 'QBT_TR(URL)QBT_TR[CONTEXT=TrackerListWidget]', 250, true);
1806             this.newColumn('status', '', 'QBT_TR(Status)QBT_TR[CONTEXT=TrackerListWidget]', 125, true);
1807             this.newColumn('peers', '', 'QBT_TR(Peers)QBT_TR[CONTEXT=TrackerListWidget]', 75, true);
1808             this.newColumn('seeds', '', 'QBT_TR(Seeds)QBT_TR[CONTEXT=TrackerListWidget]', 75, true);
1809             this.newColumn('leeches', '', 'QBT_TR(Leeches)QBT_TR[CONTEXT=TrackerListWidget]', 75, true);
1810             this.newColumn('downloaded', '', 'QBT_TR(Times Downloaded)QBT_TR[CONTEXT=TrackerListWidget]', 100, true);
1811             this.newColumn('message', '', 'QBT_TR(Message)QBT_TR[CONTEXT=TrackerListWidget]', 250, true);
1812         },
1813     });
1815     const BulkRenameTorrentFilesTable = new Class({
1816         Extends: DynamicTable,
1818         filterTerms: [],
1819         prevFilterTerms: [],
1820         prevRowsString: null,
1821         prevFilteredRows: [],
1822         prevSortedColumn: null,
1823         prevReverseSort: null,
1824         fileTree: new window.qBittorrent.FileTree.FileTree(),
1826         populateTable: function(root) {
1827             this.fileTree.setRoot(root);
1828             root.children.each(function(node) {
1829                 this._addNodeToTable(node, 0);
1830             }.bind(this));
1831         },
1833         _addNodeToTable: function(node, depth) {
1834             node.depth = depth;
1836             if (node.isFolder) {
1837                 const data = {
1838                     rowId: node.rowId,
1839                     fileId: -1,
1840                     checked: node.checked,
1841                     path: node.path,
1842                     original: node.original,
1843                     renamed: node.renamed
1844                 };
1846                 node.data = data;
1847                 node.full_data = data;
1848                 this.updateRowData(data);
1849             }
1850             else {
1851                 node.data.rowId = node.rowId;
1852                 node.full_data = node.data;
1853                 this.updateRowData(node.data);
1854             }
1856             node.children.each(function(child) {
1857                 this._addNodeToTable(child, depth + 1);
1858             }.bind(this));
1859         },
1861         getRoot: function() {
1862             return this.fileTree.getRoot();
1863         },
1865         getNode: function(rowId) {
1866             return this.fileTree.getNode(rowId);
1867         },
1869         getRow: function(node) {
1870             const rowId = this.fileTree.getRowId(node);
1871             return this.rows.get(rowId);
1872         },
1874         getSelectedRows: function() {
1875             const nodes = this.fileTree.toArray();
1877             return nodes.filter(x => x.checked == 0);
1878         },
1880         initColumns: function() {
1881             // Blocks saving header width (because window width isn't saved)
1882             LocalPreferences.remove('column_' + "checked" + '_width_' + this.dynamicTableDivId);
1883             LocalPreferences.remove('column_' + "original" + '_width_' + this.dynamicTableDivId);
1884             LocalPreferences.remove('column_' + "renamed" + '_width_' + this.dynamicTableDivId);
1885             this.newColumn('checked', '', '', 50, true);
1886             this.newColumn('original', '', 'QBT_TR(Original)QBT_TR[CONTEXT=TrackerListWidget]', 270, true);
1887             this.newColumn('renamed', '', 'QBT_TR(Renamed)QBT_TR[CONTEXT=TrackerListWidget]', 220, true);
1889             this.initColumnsFunctions();
1890         },
1892         /**
1893          * Toggles the global checkbox and all checkboxes underneath
1894          */
1895         toggleGlobalCheckbox: function() {
1896             const checkbox = $('rootMultiRename_cb');
1897             const checkboxes = $$('input.RenamingCB');
1899             for (let i = 0; i < checkboxes.length; ++i) {
1900                 const node = this.getNode(i);
1902                 if (checkbox.checked || checkbox.indeterminate) {
1903                     let cb = checkboxes[i];
1904                     cb.checked = true;
1905                     cb.indeterminate = false;
1906                     cb.state = "checked";
1907                     node.checked = 0;
1908                     node.full_data.checked = node.checked;
1909                 }
1910                 else {
1911                     let cb = checkboxes[i];
1912                     cb.checked = false;
1913                     cb.indeterminate = false;
1914                     cb.state = "unchecked";
1915                     node.checked = 1;
1916                     node.full_data.checked = node.checked;
1917                 }
1918             }
1920             this.updateGlobalCheckbox();
1921         },
1923         toggleNodeTreeCheckbox: function(rowId, checkState) {
1924             const node = this.getNode(rowId);
1925             node.checked = checkState;
1926             node.full_data.checked = checkState;
1927             const checkbox = $(`cbRename${rowId}`);
1928             checkbox.checked = node.checked == 0;
1929             checkbox.state = checkbox.checked ? "checked" : "unchecked";
1931             for (let i = 0; i < node.children.length; ++i) {
1932                 this.toggleNodeTreeCheckbox(node.children[i].rowId, checkState);
1933             }
1934         },
1936         updateGlobalCheckbox: function() {
1937             const checkbox = $('rootMultiRename_cb');
1938             const checkboxes = $$('input.RenamingCB');
1939             const isAllChecked = function() {
1940                 for (let i = 0; i < checkboxes.length; ++i) {
1941                     if (!checkboxes[i].checked)
1942                         return false;
1943                 }
1944                 return true;
1945             };
1946             const isAllUnchecked = function() {
1947                 for (let i = 0; i < checkboxes.length; ++i) {
1948                     if (checkboxes[i].checked)
1949                         return false;
1950                 }
1951                 return true;
1952             };
1953             if (isAllChecked()) {
1954                 checkbox.state = "checked";
1955                 checkbox.indeterminate = false;
1956                 checkbox.checked = true;
1957             }
1958             else if (isAllUnchecked()) {
1959                 checkbox.state = "unchecked";
1960                 checkbox.indeterminate = false;
1961                 checkbox.checked = false;
1962             }
1963             else {
1964                 checkbox.state = "partial";
1965                 checkbox.indeterminate = true;
1966                 checkbox.checked = false;
1967             }
1968         },
1970         initColumnsFunctions: function() {
1971             const that = this;
1973             // checked
1974             this.columns['checked'].updateTd = function(td, row) {
1975                 const id = row.rowId;
1976                 const value = this.getRowValue(row);
1978                 const treeImg = new Element('img', {
1979                     src: 'images/L.gif',
1980                     styles: {
1981                         'margin-bottom': -2
1982                     }
1983                 });
1984                 const checkbox = new Element('input');
1985                 checkbox.set('type', 'checkbox');
1986                 checkbox.set('id', 'cbRename' + id);
1987                 checkbox.set('data-id', id);
1988                 checkbox.set('class', 'RenamingCB');
1989                 checkbox.addEvent('click', function(e) {
1990                     const node = that.getNode(id);
1991                     node.checked = e.target.checked ? 0 : 1;
1992                     node.full_data.checked = node.checked;
1993                     that.updateGlobalCheckbox();
1994                     that.onRowSelectionChange(node);
1995                     e.stopPropagation();
1996                 });
1997                 checkbox.checked = value == 0;
1998                 checkbox.state = checkbox.checked ? "checked" : "unchecked";
1999                 checkbox.indeterminate = false;
2000                 td.adopt(treeImg, checkbox);
2001             };
2003             // original
2004             this.columns['original'].updateTd = function(td, row) {
2005                 const id = row.rowId;
2006                 const fileNameId = 'filesTablefileName' + id;
2007                 const node = that.getNode(id);
2009                 if (node.isFolder) {
2010                     const value = this.getRowValue(row);
2011                     const dirImgId = 'renameTableDirImg' + id;
2012                     if ($(dirImgId)) {
2013                         // just update file name
2014                         $(fileNameId).set('text', value);
2015                     }
2016                     else {
2017                         const span = new Element('span', {
2018                             text: value,
2019                             id: fileNameId
2020                         });
2021                         const dirImg = new Element('img', {
2022                             src: 'images/directory.svg',
2023                             styles: {
2024                                 'width': 15,
2025                                 'padding-right': 5,
2026                                 'margin-bottom': -3,
2027                                 'margin-left': (node.depth * 20)
2028                             },
2029                             id: dirImgId
2030                         });
2031                         const html = dirImg.outerHTML + span.outerHTML;
2032                         td.set('html', html);
2033                     }
2034                 }
2035                 else { // is file
2036                     const value = this.getRowValue(row);
2037                     const span = new Element('span', {
2038                         text: value,
2039                         id: fileNameId,
2040                         styles: {
2041                             'margin-left': ((node.depth + 1) * 20)
2042                         }
2043                     });
2044                     td.set('html', span.outerHTML);
2045                 }
2046             };
2048             // renamed
2049             this.columns['renamed'].updateTd = function(td, row) {
2050                 const id = row.rowId;
2051                 const fileNameRenamedId = 'filesTablefileRenamed' + id;
2052                 const value = this.getRowValue(row);
2054                 const span = new Element('span', {
2055                     text: value,
2056                     id: fileNameRenamedId,
2057                 });
2058                 td.set('html', span.outerHTML);
2059             };
2060         },
2062         onRowSelectionChange: function(row) {},
2064         selectRow: function() {
2065             return;
2066         },
2068         reselectRows: function(rowIds) {
2069             const that = this;
2070             this.deselectAll();
2071             this.tableBody.getElements('tr').each(function(tr) {
2072                 if (rowIds.indexOf(tr.rowId) > -1) {
2073                     const node = that.getNode(tr.rowId);
2074                     node.checked = 0;
2075                     node.full_data.checked = 0;
2077                     const checkbox = tr.children[0].getElement('input');
2078                     checkbox.state = "checked";
2079                     checkbox.indeterminate = false;
2080                     checkbox.checked = true;
2081                 }
2082             });
2084             this.updateGlobalCheckbox();
2085         },
2087         altRow: function() {
2088             let addClass = false;
2089             const trs = this.tableBody.getElements('tr');
2090             trs.each(function(tr) {
2091                 if (tr.hasClass("invisible"))
2092                     return;
2094                 if (addClass) {
2095                     tr.addClass("alt");
2096                     tr.removeClass("nonAlt");
2097                 }
2098                 else {
2099                     tr.removeClass("alt");
2100                     tr.addClass("nonAlt");
2101                 }
2102                 addClass = !addClass;
2103             }.bind(this));
2104         },
2106         _sortNodesByColumn: function(nodes, column) {
2107             nodes.sort(function(row1, row2) {
2108                 // list folders before files when sorting by name
2109                 if (column.name === "original") {
2110                     const node1 = this.getNode(row1.data.rowId);
2111                     const node2 = this.getNode(row2.data.rowId);
2112                     if (node1.isFolder && !node2.isFolder)
2113                         return -1;
2114                     if (node2.isFolder && !node1.isFolder)
2115                         return 1;
2116                 }
2118                 const res = column.compareRows(row1, row2);
2119                 return (this.reverseSort === '0') ? res : -res;
2120             }.bind(this));
2122             nodes.each(function(node) {
2123                 if (node.children.length > 0)
2124                     this._sortNodesByColumn(node.children, column);
2125             }.bind(this));
2126         },
2128         _filterNodes: function(node, filterTerms, filteredRows) {
2129             if (node.isFolder) {
2130                 const childAdded = node.children.reduce(function(acc, child) {
2131                     // we must execute the function before ORing w/ acc or we'll stop checking child nodes after the first successful match
2132                     return (this._filterNodes(child, filterTerms, filteredRows) || acc);
2133                 }.bind(this), false);
2135                 if (childAdded) {
2136                     const row = this.getRow(node);
2137                     filteredRows.push(row);
2138                     return true;
2139                 }
2140             }
2142             if (window.qBittorrent.Misc.containsAllTerms(node.original, filterTerms)) {
2143                 const row = this.getRow(node);
2144                 filteredRows.push(row);
2145                 return true;
2146             }
2148             return false;
2149         },
2151         setFilter: function(text) {
2152             const filterTerms = text.trim().toLowerCase().split(' ');
2153             if ((filterTerms.length === 1) && (filterTerms[0] === ''))
2154                 this.filterTerms = [];
2155             else
2156                 this.filterTerms = filterTerms;
2157         },
2159         getFilteredAndSortedRows: function() {
2160             if (this.getRoot() === null)
2161                 return [];
2163             const generateRowsSignature = function(rows) {
2164                 const rowsData = rows.map(function(row) {
2165                     return row.full_data;
2166                 });
2167                 return JSON.stringify(rowsData);
2168             };
2170             const getFilteredRows = function() {
2171                 if (this.filterTerms.length === 0) {
2172                     const nodeArray = this.fileTree.toArray();
2173                     const filteredRows = nodeArray.map(function(node) {
2174                         return this.getRow(node);
2175                     }.bind(this));
2176                     return filteredRows;
2177                 }
2179                 const filteredRows = [];
2180                 this.getRoot().children.each(function(child) {
2181                     this._filterNodes(child, this.filterTerms, filteredRows);
2182                 }.bind(this));
2183                 filteredRows.reverse();
2184                 return filteredRows;
2185             }.bind(this);
2187             const hasRowsChanged = function(rowsString, prevRowsStringString) {
2188                 const rowsChanged = (rowsString !== prevRowsStringString);
2189                 const isFilterTermsChanged = this.filterTerms.reduce(function(acc, term, index) {
2190                     return (acc || (term !== this.prevFilterTerms[index]));
2191                 }.bind(this), false);
2192                 const isFilterChanged = ((this.filterTerms.length !== this.prevFilterTerms.length)
2193                     || ((this.filterTerms.length > 0) && isFilterTermsChanged));
2194                 const isSortedColumnChanged = (this.prevSortedColumn !== this.sortedColumn);
2195                 const isReverseSortChanged = (this.prevReverseSort !== this.reverseSort);
2197                 return (rowsChanged || isFilterChanged || isSortedColumnChanged || isReverseSortChanged);
2198             }.bind(this);
2200             const rowsString = generateRowsSignature(this.rows);
2201             if (!hasRowsChanged(rowsString, this.prevRowsString)) {
2202                 return this.prevFilteredRows;
2203             }
2205             // sort, then filter
2206             const column = this.columns[this.sortedColumn];
2207             this._sortNodesByColumn(this.getRoot().children, column);
2208             const filteredRows = getFilteredRows();
2210             this.prevFilterTerms = this.filterTerms;
2211             this.prevRowsString = rowsString;
2212             this.prevFilteredRows = filteredRows;
2213             this.prevSortedColumn = this.sortedColumn;
2214             this.prevReverseSort = this.reverseSort;
2215             return filteredRows;
2216         },
2218         setIgnored: function(rowId, ignore) {
2219             const row = this.rows.get(rowId);
2220             if (ignore)
2221                 row.full_data.remaining = 0;
2222             else
2223                 row.full_data.remaining = (row.full_data.size * (1.0 - (row.full_data.progress / 100)));
2224         },
2226         setupTr: function(tr) {
2227             tr.addEvent('keydown', function(event) {
2228                 switch (event.key) {
2229                     case "left":
2230                         qBittorrent.PropFiles.collapseFolder(this._this.getSelectedRowId());
2231                         return false;
2232                     case "right":
2233                         qBittorrent.PropFiles.expandFolder(this._this.getSelectedRowId());
2234                         return false;
2235                 }
2236             });
2237         }
2238     });
2240     const TorrentFilesTable = new Class({
2241         Extends: DynamicTable,
2243         filterTerms: [],
2244         prevFilterTerms: [],
2245         prevRowsString: null,
2246         prevFilteredRows: [],
2247         prevSortedColumn: null,
2248         prevReverseSort: null,
2249         fileTree: new window.qBittorrent.FileTree.FileTree(),
2251         populateTable: function(root) {
2252             this.fileTree.setRoot(root);
2253             root.children.each(function(node) {
2254                 this._addNodeToTable(node, 0);
2255             }.bind(this));
2256         },
2258         _addNodeToTable: function(node, depth) {
2259             node.depth = depth;
2261             if (node.isFolder) {
2262                 const data = {
2263                     rowId: node.rowId,
2264                     size: node.size,
2265                     checked: node.checked,
2266                     remaining: node.remaining,
2267                     progress: node.progress,
2268                     priority: window.qBittorrent.PropFiles.normalizePriority(node.priority),
2269                     availability: node.availability,
2270                     fileId: -1,
2271                     name: node.name
2272                 };
2274                 node.data = data;
2275                 node.full_data = data;
2276                 this.updateRowData(data);
2277             }
2278             else {
2279                 node.data.rowId = node.rowId;
2280                 node.full_data = node.data;
2281                 this.updateRowData(node.data);
2282             }
2284             node.children.each(function(child) {
2285                 this._addNodeToTable(child, depth + 1);
2286             }.bind(this));
2287         },
2289         getRoot: function() {
2290             return this.fileTree.getRoot();
2291         },
2293         getNode: function(rowId) {
2294             return this.fileTree.getNode(rowId);
2295         },
2297         getRow: function(node) {
2298             const rowId = this.fileTree.getRowId(node);
2299             return this.rows.get(rowId);
2300         },
2302         initColumns: function() {
2303             this.newColumn('checked', '', '', 50, true);
2304             this.newColumn('name', '', 'QBT_TR(Name)QBT_TR[CONTEXT=TrackerListWidget]', 300, true);
2305             this.newColumn('size', '', 'QBT_TR(Total Size)QBT_TR[CONTEXT=TrackerListWidget]', 75, true);
2306             this.newColumn('progress', '', 'QBT_TR(Progress)QBT_TR[CONTEXT=TrackerListWidget]', 100, true);
2307             this.newColumn('priority', '', 'QBT_TR(Download Priority)QBT_TR[CONTEXT=TrackerListWidget]', 150, true);
2308             this.newColumn('remaining', '', 'QBT_TR(Remaining)QBT_TR[CONTEXT=TrackerListWidget]', 75, true);
2309             this.newColumn('availability', '', 'QBT_TR(Availability)QBT_TR[CONTEXT=TrackerListWidget]', 75, true);
2311             this.initColumnsFunctions();
2312         },
2314         initColumnsFunctions: function() {
2315             const that = this;
2316             const displaySize = function(td, row) {
2317                 const size = window.qBittorrent.Misc.friendlyUnit(this.getRowValue(row), false);
2318                 td.set('text', size);
2319                 td.set('title', size);
2320             };
2321             const displayPercentage = function(td, row) {
2322                 const value = window.qBittorrent.Misc.friendlyPercentage(this.getRowValue(row));
2323                 td.set('text', value);
2324                 td.set('title', value);
2325             };
2327             // checked
2328             this.columns['checked'].updateTd = function(td, row) {
2329                 const id = row.rowId;
2330                 const value = this.getRowValue(row);
2332                 if (window.qBittorrent.PropFiles.isDownloadCheckboxExists(id)) {
2333                     window.qBittorrent.PropFiles.updateDownloadCheckbox(id, value);
2334                 }
2335                 else {
2336                     const treeImg = new Element('img', {
2337                         src: 'images/L.gif',
2338                         styles: {
2339                             'margin-bottom': -2
2340                         }
2341                     });
2342                     td.adopt(treeImg, window.qBittorrent.PropFiles.createDownloadCheckbox(id, row.full_data.fileId, value));
2343                 }
2344             };
2346             // name
2347             this.columns['name'].updateTd = function(td, row) {
2348                 const id = row.rowId;
2349                 const fileNameId = 'filesTablefileName' + id;
2350                 const node = that.getNode(id);
2352                 if (node.isFolder) {
2353                     const value = this.getRowValue(row);
2354                     const collapseIconId = 'filesTableCollapseIcon' + id;
2355                     const dirImgId = 'filesTableDirImg' + id;
2356                     if ($(dirImgId)) {
2357                         // just update file name
2358                         $(fileNameId).set('text', value);
2359                     }
2360                     else {
2361                         const collapseIcon = new Element('img', {
2362                             src: 'images/go-down.svg',
2363                             styles: {
2364                                 'margin-left': (node.depth * 20)
2365                             },
2366                             class: "filesTableCollapseIcon",
2367                             id: collapseIconId,
2368                             "data-id": id,
2369                             onclick: "qBittorrent.PropFiles.collapseIconClicked(this)"
2370                         });
2371                         const span = new Element('span', {
2372                             text: value,
2373                             id: fileNameId
2374                         });
2375                         const dirImg = new Element('img', {
2376                             src: 'images/directory.svg',
2377                             styles: {
2378                                 'width': 15,
2379                                 'padding-right': 5,
2380                                 'margin-bottom': -3
2381                             },
2382                             id: dirImgId
2383                         });
2384                         const html = collapseIcon.outerHTML + dirImg.outerHTML + span.outerHTML;
2385                         td.set('html', html);
2386                     }
2387                 }
2388                 else {
2389                     const value = this.getRowValue(row);
2390                     const span = new Element('span', {
2391                         text: value,
2392                         id: fileNameId,
2393                         styles: {
2394                             'margin-left': ((node.depth + 1) * 20)
2395                         }
2396                     });
2397                     td.set('html', span.outerHTML);
2398                 }
2399             };
2401             // size
2402             this.columns['size'].updateTd = displaySize;
2404             // progress
2405             this.columns['progress'].updateTd = function(td, row) {
2406                 const id = row.rowId;
2407                 const value = this.getRowValue(row);
2409                 const progressBar = $('pbf_' + id);
2410                 if (progressBar === null) {
2411                     td.adopt(new window.qBittorrent.ProgressBar.ProgressBar(value.toFloat(), {
2412                         id: 'pbf_' + id,
2413                         width: 80
2414                     }));
2415                 }
2416                 else {
2417                     progressBar.setValue(value.toFloat());
2418                 }
2419             };
2421             // priority
2422             this.columns['priority'].updateTd = function(td, row) {
2423                 const id = row.rowId;
2424                 const value = this.getRowValue(row);
2426                 if (window.qBittorrent.PropFiles.isPriorityComboExists(id))
2427                     window.qBittorrent.PropFiles.updatePriorityCombo(id, value);
2428                 else
2429                     td.adopt(window.qBittorrent.PropFiles.createPriorityCombo(id, row.full_data.fileId, value));
2430             };
2432             // remaining, availability
2433             this.columns['remaining'].updateTd = displaySize;
2434             this.columns['availability'].updateTd = displayPercentage;
2435         },
2437         altRow: function() {
2438             let addClass = false;
2439             const trs = this.tableBody.getElements('tr');
2440             trs.each(function(tr) {
2441                 if (tr.hasClass("invisible"))
2442                     return;
2444                 if (addClass) {
2445                     tr.addClass("alt");
2446                     tr.removeClass("nonAlt");
2447                 }
2448                 else {
2449                     tr.removeClass("alt");
2450                     tr.addClass("nonAlt");
2451                 }
2452                 addClass = !addClass;
2453             }.bind(this));
2454         },
2456         _sortNodesByColumn: function(nodes, column) {
2457             nodes.sort(function(row1, row2) {
2458                 // list folders before files when sorting by name
2459                 if (column.name === "name") {
2460                     const node1 = this.getNode(row1.data.rowId);
2461                     const node2 = this.getNode(row2.data.rowId);
2462                     if (node1.isFolder && !node2.isFolder)
2463                         return -1;
2464                     if (node2.isFolder && !node1.isFolder)
2465                         return 1;
2466                 }
2468                 const res = column.compareRows(row1, row2);
2469                 return (this.reverseSort === '0') ? res : -res;
2470             }.bind(this));
2472             nodes.each(function(node) {
2473                 if (node.children.length > 0)
2474                     this._sortNodesByColumn(node.children, column);
2475             }.bind(this));
2476         },
2478         _filterNodes: function(node, filterTerms, filteredRows) {
2479             if (node.isFolder) {
2480                 const childAdded = node.children.reduce(function(acc, child) {
2481                     // we must execute the function before ORing w/ acc or we'll stop checking child nodes after the first successful match
2482                     return (this._filterNodes(child, filterTerms, filteredRows) || acc);
2483                 }.bind(this), false);
2485                 if (childAdded) {
2486                     const row = this.getRow(node);
2487                     filteredRows.push(row);
2488                     return true;
2489                 }
2490             }
2492             if (window.qBittorrent.Misc.containsAllTerms(node.name, filterTerms)) {
2493                 const row = this.getRow(node);
2494                 filteredRows.push(row);
2495                 return true;
2496             }
2498             return false;
2499         },
2501         setFilter: function(text) {
2502             const filterTerms = text.trim().toLowerCase().split(' ');
2503             if ((filterTerms.length === 1) && (filterTerms[0] === ''))
2504                 this.filterTerms = [];
2505             else
2506                 this.filterTerms = filterTerms;
2507         },
2509         getFilteredAndSortedRows: function() {
2510             if (this.getRoot() === null)
2511                 return [];
2513             const generateRowsSignature = function(rows) {
2514                 const rowsData = rows.map(function(row) {
2515                     return row.full_data;
2516                 });
2517                 return JSON.stringify(rowsData);
2518             };
2520             const getFilteredRows = function() {
2521                 if (this.filterTerms.length === 0) {
2522                     const nodeArray = this.fileTree.toArray();
2523                     const filteredRows = nodeArray.map(function(node) {
2524                         return this.getRow(node);
2525                     }.bind(this));
2526                     return filteredRows;
2527                 }
2529                 const filteredRows = [];
2530                 this.getRoot().children.each(function(child) {
2531                     this._filterNodes(child, this.filterTerms, filteredRows);
2532                 }.bind(this));
2533                 filteredRows.reverse();
2534                 return filteredRows;
2535             }.bind(this);
2537             const hasRowsChanged = function(rowsString, prevRowsStringString) {
2538                 const rowsChanged = (rowsString !== prevRowsStringString);
2539                 const isFilterTermsChanged = this.filterTerms.reduce(function(acc, term, index) {
2540                     return (acc || (term !== this.prevFilterTerms[index]));
2541                 }.bind(this), false);
2542                 const isFilterChanged = ((this.filterTerms.length !== this.prevFilterTerms.length)
2543                     || ((this.filterTerms.length > 0) && isFilterTermsChanged));
2544                 const isSortedColumnChanged = (this.prevSortedColumn !== this.sortedColumn);
2545                 const isReverseSortChanged = (this.prevReverseSort !== this.reverseSort);
2547                 return (rowsChanged || isFilterChanged || isSortedColumnChanged || isReverseSortChanged);
2548             }.bind(this);
2550             const rowsString = generateRowsSignature(this.rows);
2551             if (!hasRowsChanged(rowsString, this.prevRowsString)) {
2552                 return this.prevFilteredRows;
2553             }
2555             // sort, then filter
2556             const column = this.columns[this.sortedColumn];
2557             this._sortNodesByColumn(this.getRoot().children, column);
2558             const filteredRows = getFilteredRows();
2560             this.prevFilterTerms = this.filterTerms;
2561             this.prevRowsString = rowsString;
2562             this.prevFilteredRows = filteredRows;
2563             this.prevSortedColumn = this.sortedColumn;
2564             this.prevReverseSort = this.reverseSort;
2565             return filteredRows;
2566         },
2568         setIgnored: function(rowId, ignore) {
2569             const row = this.rows.get(rowId);
2570             if (ignore)
2571                 row.full_data.remaining = 0;
2572             else
2573                 row.full_data.remaining = (row.full_data.size * (1.0 - (row.full_data.progress / 100)));
2574         },
2576         setupTr: function(tr) {
2577             tr.addEvent('keydown', function(event) {
2578                 switch (event.key) {
2579                     case "left":
2580                         qBittorrent.PropFiles.collapseFolder(this._this.getSelectedRowId());
2581                         return false;
2582                     case "right":
2583                         qBittorrent.PropFiles.expandFolder(this._this.getSelectedRowId());
2584                         return false;
2585                 }
2586             });
2587         }
2588     });
2590     const RssFeedTable = new Class({
2591         Extends: DynamicTable,
2592         initColumns: function() {
2593             this.newColumn('state_icon', '', '', 30, true);
2594             this.newColumn('name', '', 'QBT_TR(RSS feeds)QBT_TR[CONTEXT=FeedListWidget]', -1, true);
2596             this.columns['state_icon'].dataProperties[0] = '';
2598             // map name row to "[name] ([unread])"
2599             this.columns['name'].dataProperties.push('unread');
2600             this.columns['name'].updateTd = function(td, row) {
2601                 const name = this.getRowValue(row, 0);
2602                 const unreadCount = this.getRowValue(row, 1);
2603                 let value = name + ' (' + unreadCount + ')';
2604                 td.set('text', value);
2605                 td.set('title', value);
2606             };
2607         },
2608         setupHeaderMenu: function() {},
2609         setupHeaderEvents: function() {},
2610         getFilteredAndSortedRows: function() {
2611             return this.rows.getValues();
2612         },
2613         selectRow: function(rowId) {
2614             this.selectedRows.push(rowId);
2615             this.setRowClass();
2616             this.onSelectedRowChanged();
2618             const rows = this.rows.getValues();
2619             let path = '';
2620             for (let i = 0; i < rows.length; ++i) {
2621                 if (rows[i].rowId === rowId) {
2622                     path = rows[i].full_data.dataPath;
2623                     break;
2624                 }
2625             }
2626             window.qBittorrent.Rss.showRssFeed(path);
2627         },
2628         setupTr: function(tr) {
2629             tr.addEvent('dblclick', function(e) {
2630                 if (this.rowId !== 0) {
2631                     window.qBittorrent.Rss.moveItem(this._this.rows.get(this.rowId).full_data.dataPath);
2632                     return true;
2633                 }
2634             });
2635         },
2636         updateRow: function(tr, fullUpdate) {
2637             const row = this.rows.get(tr.rowId);
2638             const data = row[fullUpdate ? 'full_data' : 'data'];
2640             const tds = tr.getElements('td');
2641             for (let i = 0; i < this.columns.length; ++i) {
2642                 if (Object.prototype.hasOwnProperty.call(data, this.columns[i].dataProperties[0]))
2643                     this.columns[i].updateTd(tds[i], row);
2644             }
2645             row['data'] = {};
2646             tds[0].style.overflow = 'visible';
2647             let indentation = row.full_data.indentation;
2648             tds[0].style.paddingLeft = (indentation * 32 + 4) + 'px';
2649             tds[1].style.paddingLeft = (indentation * 32 + 4) + 'px';
2650         },
2651         updateIcons: function() {
2652             // state_icon
2653             this.rows.each(row => {
2654                 let img_path;
2655                 switch (row.full_data.status) {
2656                     case 'default':
2657                         img_path = 'images/application-rss.svg';
2658                         break;
2659                     case 'hasError':
2660                         img_path = 'images/task-reject.svg';
2661                         break;
2662                     case 'isLoading':
2663                         img_path = 'images/spinner.gif';
2664                         break;
2665                     case 'unread':
2666                         img_path = 'images/mail-inbox.svg';
2667                         break;
2668                     case 'isFolder':
2669                         img_path = 'images/folder-documents.svg';
2670                         break;
2671                 }
2672                 let td;
2673                 for (let i = 0; i < this.tableBody.rows.length; ++i) {
2674                     if (this.tableBody.rows[i].rowId === row.rowId) {
2675                         td = this.tableBody.rows[i].children[0];
2676                         break;
2677                     }
2678                 }
2679                 if (td.getChildren('img').length > 0) {
2680                     const img = td.getChildren('img')[0];
2681                     if (img.src.indexOf(img_path) < 0) {
2682                         img.set('src', img_path);
2683                         img.set('title', status);
2684                     }
2685                 }
2686                 else {
2687                     td.adopt(new Element('img', {
2688                         'src': img_path,
2689                         'class': 'stateIcon',
2690                         'height': '22px',
2691                         'width': '22px'
2692                     }));
2693                 }
2694             });
2695         },
2696         newColumn: function(name, style, caption, defaultWidth, defaultVisible) {
2697             const column = {};
2698             column['name'] = name;
2699             column['title'] = name;
2700             column['visible'] = defaultVisible;
2701             column['force_hide'] = false;
2702             column['caption'] = caption;
2703             column['style'] = style;
2704             if (defaultWidth !== -1) {
2705                 column['width'] = defaultWidth;
2706             }
2708             column['dataProperties'] = [name];
2709             column['getRowValue'] = function(row, pos) {
2710                 if (pos === undefined)
2711                     pos = 0;
2712                 return row['full_data'][this.dataProperties[pos]];
2713             };
2714             column['compareRows'] = function(row1, row2) {
2715                 const value1 = this.getRowValue(row1);
2716                 const value2 = this.getRowValue(row2);
2717                 if ((typeof(value1) === 'number') && (typeof(value2) === 'number'))
2718                     return compareNumbers(value1, value2);
2719                 return window.qBittorrent.Misc.naturalSortCollator.compare(value1, value2);
2720             };
2721             column['updateTd'] = function(td, row) {
2722                 const value = this.getRowValue(row);
2723                 td.set('text', value);
2724                 td.set('title', value);
2725             };
2726             column['onResize'] = null;
2727             this.columns.push(column);
2728             this.columns[name] = column;
2730             this.hiddenTableHeader.appendChild(new Element('th'));
2731             this.fixedTableHeader.appendChild(new Element('th'));
2732         },
2733         setupCommonEvents: function() {
2734             const scrollFn = function() {
2735                 $(this.dynamicTableFixedHeaderDivId).getElements('table')[0].style.left = -$(this.dynamicTableDivId).scrollLeft + 'px';
2736             }.bind(this);
2738             $(this.dynamicTableDivId).addEvent('scroll', scrollFn);
2739         }
2740     });
2742     const RssArticleTable = new Class({
2743         Extends: DynamicTable,
2744         initColumns: function() {
2745             this.newColumn('name', '', 'QBT_TR(Torrents: (double-click to download))QBT_TR[CONTEXT=RSSWidget]', -1, true);
2746         },
2747         setupHeaderMenu: function() {},
2748         setupHeaderEvents: function() {},
2749         getFilteredAndSortedRows: function() {
2750             return this.rows.getValues();
2751         },
2752         selectRow: function(rowId) {
2753             this.selectedRows.push(rowId);
2754             this.setRowClass();
2755             this.onSelectedRowChanged();
2757             const rows = this.rows.getValues();
2758             let articleId = '';
2759             let feedUid = '';
2760             for (let i = 0; i < rows.length; ++i) {
2761                 if (rows[i].rowId === rowId) {
2762                     articleId = rows[i].full_data.dataId;
2763                     feedUid = rows[i].full_data.feedUid;
2764                     this.tableBody.rows[rows[i].rowId].removeClass('unreadArticle');
2765                     break;
2766                 }
2767             }
2768             window.qBittorrent.Rss.showDetails(feedUid, articleId);
2769         },
2770         setupTr: function(tr) {
2771             tr.addEvent('dblclick', function(e) {
2772                 showDownloadPage([this._this.rows.get(this.rowId).full_data.torrentURL]);
2773                 return true;
2774             });
2775             tr.addClass('torrentsTableContextMenuTarget');
2776         },
2777         updateRow: function(tr, fullUpdate) {
2778             const row = this.rows.get(tr.rowId);
2779             const data = row[fullUpdate ? 'full_data' : 'data'];
2780             if (!row.full_data.isRead)
2781                 tr.addClass('unreadArticle');
2782             else
2783                 tr.removeClass('unreadArticle');
2785             const tds = tr.getElements('td');
2786             for (let i = 0; i < this.columns.length; ++i) {
2787                 if (Object.prototype.hasOwnProperty.call(data, this.columns[i].dataProperties[0]))
2788                     this.columns[i].updateTd(tds[i], row);
2789             }
2790             row['data'] = {};
2791         },
2792         newColumn: function(name, style, caption, defaultWidth, defaultVisible) {
2793             const column = {};
2794             column['name'] = name;
2795             column['title'] = name;
2796             column['visible'] = defaultVisible;
2797             column['force_hide'] = false;
2798             column['caption'] = caption;
2799             column['style'] = style;
2800             if (defaultWidth !== -1) {
2801                 column['width'] = defaultWidth;
2802             }
2804             column['dataProperties'] = [name];
2805             column['getRowValue'] = function(row, pos) {
2806                 if (pos === undefined)
2807                     pos = 0;
2808                 return row['full_data'][this.dataProperties[pos]];
2809             };
2810             column['compareRows'] = function(row1, row2) {
2811                 const value1 = this.getRowValue(row1);
2812                 const value2 = this.getRowValue(row2);
2813                 if ((typeof(value1) === 'number') && (typeof(value2) === 'number'))
2814                     return compareNumbers(value1, value2);
2815                 return window.qBittorrent.Misc.naturalSortCollator.compare(value1, value2);
2816             };
2817             column['updateTd'] = function(td, row) {
2818                 const value = this.getRowValue(row);
2819                 td.set('text', value);
2820                 td.set('title', value);
2821             };
2822             column['onResize'] = null;
2823             this.columns.push(column);
2824             this.columns[name] = column;
2826             this.hiddenTableHeader.appendChild(new Element('th'));
2827             this.fixedTableHeader.appendChild(new Element('th'));
2828         },
2829         setupCommonEvents: function() {
2830             const scrollFn = function() {
2831                 $(this.dynamicTableFixedHeaderDivId).getElements('table')[0].style.left = -$(this.dynamicTableDivId).scrollLeft + 'px';
2832             }.bind(this);
2834             $(this.dynamicTableDivId).addEvent('scroll', scrollFn);
2835         }
2836     });
2838     const RssDownloaderRulesTable = new Class({
2839         Extends: DynamicTable,
2840         initColumns: function() {
2841             this.newColumn('checked', '', '', 30, true);
2842             this.newColumn('name', '', '', -1, true);
2844             this.columns['checked'].updateTd = function(td, row) {
2845                 if ($('cbRssDlRule' + row.rowId) === null) {
2846                     const checkbox = new Element('input');
2847                     checkbox.set('type', 'checkbox');
2848                     checkbox.set('id', 'cbRssDlRule' + row.rowId);
2849                     checkbox.checked = row.full_data.checked;
2851                     checkbox.addEvent('click', function(e) {
2852                         window.qBittorrent.RssDownloader.rssDownloaderRulesTable.updateRowData({
2853                             rowId: row.rowId,
2854                             checked: this.checked
2855                         });
2856                         window.qBittorrent.RssDownloader.modifyRuleState(row.full_data.name, 'enabled', this.checked);
2857                         e.stopPropagation();
2858                     });
2860                     td.append(checkbox);
2861                 }
2862                 else {
2863                     $('cbRssDlRule' + row.rowId).checked = row.full_data.checked;
2864                 }
2865             };
2866         },
2867         setupHeaderMenu: function() {},
2868         setupHeaderEvents: function() {},
2869         getFilteredAndSortedRows: function() {
2870             return this.rows.getValues();
2871         },
2872         setupTr: function(tr) {
2873             tr.addEvent('dblclick', function(e) {
2874                 window.qBittorrent.RssDownloader.renameRule(this._this.rows.get(this.rowId).full_data.name);
2875                 return true;
2876             });
2877         },
2878         newColumn: function(name, style, caption, defaultWidth, defaultVisible) {
2879             const column = {};
2880             column['name'] = name;
2881             column['title'] = name;
2882             column['visible'] = defaultVisible;
2883             column['force_hide'] = false;
2884             column['caption'] = caption;
2885             column['style'] = style;
2886             if (defaultWidth !== -1) {
2887                 column['width'] = defaultWidth;
2888             }
2890             column['dataProperties'] = [name];
2891             column['getRowValue'] = function(row, pos) {
2892                 if (pos === undefined)
2893                     pos = 0;
2894                 return row['full_data'][this.dataProperties[pos]];
2895             };
2896             column['compareRows'] = function(row1, row2) {
2897                 const value1 = this.getRowValue(row1);
2898                 const value2 = this.getRowValue(row2);
2899                 if ((typeof(value1) === 'number') && (typeof(value2) === 'number'))
2900                     return compareNumbers(value1, value2);
2901                 return window.qBittorrent.Misc.naturalSortCollator.compare(value1, value2);
2902             };
2903             column['updateTd'] = function(td, row) {
2904                 const value = this.getRowValue(row);
2905                 td.set('text', value);
2906                 td.set('title', value);
2907             };
2908             column['onResize'] = null;
2909             this.columns.push(column);
2910             this.columns[name] = column;
2912             this.hiddenTableHeader.appendChild(new Element('th'));
2913             this.fixedTableHeader.appendChild(new Element('th'));
2914         },
2915         selectRow: function(rowId) {
2916             this.selectedRows.push(rowId);
2917             this.setRowClass();
2918             this.onSelectedRowChanged();
2920             const rows = this.rows.getValues();
2921             let name = '';
2922             for (let i = 0; i < rows.length; ++i) {
2923                 if (rows[i].rowId === rowId) {
2924                     name = rows[i].full_data.name;
2925                     break;
2926                 }
2927             }
2928             window.qBittorrent.RssDownloader.showRule(name);
2929         }
2930     });
2932     const RssDownloaderFeedSelectionTable = new Class({
2933         Extends: DynamicTable,
2934         initColumns: function() {
2935             this.newColumn('checked', '', '', 30, true);
2936             this.newColumn('name', '', '', -1, true);
2938             this.columns['checked'].updateTd = function(td, row) {
2939                 if ($('cbRssDlFeed' + row.rowId) === null) {
2940                     const checkbox = new Element('input');
2941                     checkbox.set('type', 'checkbox');
2942                     checkbox.set('id', 'cbRssDlFeed' + row.rowId);
2943                     checkbox.checked = row.full_data.checked;
2945                     checkbox.addEvent('click', function(e) {
2946                         window.qBittorrent.RssDownloader.rssDownloaderFeedSelectionTable.updateRowData({
2947                             rowId: row.rowId,
2948                             checked: this.checked
2949                         });
2950                         e.stopPropagation();
2951                     });
2953                     td.append(checkbox);
2954                 }
2955                 else {
2956                     $('cbRssDlFeed' + row.rowId).checked = row.full_data.checked;
2957                 }
2958             };
2959         },
2960         setupHeaderMenu: function() {},
2961         setupHeaderEvents: function() {},
2962         getFilteredAndSortedRows: function() {
2963             return this.rows.getValues();
2964         },
2965         newColumn: function(name, style, caption, defaultWidth, defaultVisible) {
2966             const column = {};
2967             column['name'] = name;
2968             column['title'] = name;
2969             column['visible'] = defaultVisible;
2970             column['force_hide'] = false;
2971             column['caption'] = caption;
2972             column['style'] = style;
2973             if (defaultWidth !== -1) {
2974                 column['width'] = defaultWidth;
2975             }
2977             column['dataProperties'] = [name];
2978             column['getRowValue'] = function(row, pos) {
2979                 if (pos === undefined)
2980                     pos = 0;
2981                 return row['full_data'][this.dataProperties[pos]];
2982             };
2983             column['compareRows'] = function(row1, row2) {
2984                 const value1 = this.getRowValue(row1);
2985                 const value2 = this.getRowValue(row2);
2986                 if ((typeof(value1) === 'number') && (typeof(value2) === 'number'))
2987                     return compareNumbers(value1, value2);
2988                 return window.qBittorrent.Misc.naturalSortCollator.compare(value1, value2);
2989             };
2990             column['updateTd'] = function(td, row) {
2991                 const value = this.getRowValue(row);
2992                 td.set('text', value);
2993                 td.set('title', value);
2994             };
2995             column['onResize'] = null;
2996             this.columns.push(column);
2997             this.columns[name] = column;
2999             this.hiddenTableHeader.appendChild(new Element('th'));
3000             this.fixedTableHeader.appendChild(new Element('th'));
3001         },
3002         selectRow: function() {}
3003     });
3005     const RssDownloaderArticlesTable = new Class({
3006         Extends: DynamicTable,
3007         initColumns: function() {
3008             this.newColumn('name', '', '', -1, true);
3009         },
3010         setupHeaderMenu: function() {},
3011         setupHeaderEvents: function() {},
3012         getFilteredAndSortedRows: function() {
3013             return this.rows.getValues();
3014         },
3015         newColumn: function(name, style, caption, defaultWidth, defaultVisible) {
3016             const column = {};
3017             column['name'] = name;
3018             column['title'] = name;
3019             column['visible'] = defaultVisible;
3020             column['force_hide'] = false;
3021             column['caption'] = caption;
3022             column['style'] = style;
3023             if (defaultWidth !== -1) {
3024                 column['width'] = defaultWidth;
3025             }
3027             column['dataProperties'] = [name];
3028             column['getRowValue'] = function(row, pos) {
3029                 if (pos === undefined)
3030                     pos = 0;
3031                 return row['full_data'][this.dataProperties[pos]];
3032             };
3033             column['compareRows'] = function(row1, row2) {
3034                 const value1 = this.getRowValue(row1);
3035                 const value2 = this.getRowValue(row2);
3036                 if ((typeof(value1) === 'number') && (typeof(value2) === 'number'))
3037                     return compareNumbers(value1, value2);
3038                 return window.qBittorrent.Misc.naturalSortCollator.compare(value1, value2);
3039             };
3040             column['updateTd'] = function(td, row) {
3041                 const value = this.getRowValue(row);
3042                 td.set('text', value);
3043                 td.set('title', value);
3044             };
3045             column['onResize'] = null;
3046             this.columns.push(column);
3047             this.columns[name] = column;
3049             this.hiddenTableHeader.appendChild(new Element('th'));
3050             this.fixedTableHeader.appendChild(new Element('th'));
3051         },
3052         selectRow: function() {},
3053         updateRow: function(tr, fullUpdate) {
3054             const row = this.rows.get(tr.rowId);
3055             const data = row[fullUpdate ? 'full_data' : 'data'];
3057             if (row.full_data.isFeed) {
3058                 tr.addClass('articleTableFeed');
3059                 tr.removeClass('articleTableArticle');
3060             }
3061             else {
3062                 tr.removeClass('articleTableFeed');
3063                 tr.addClass('articleTableArticle');
3064             }
3066             const tds = tr.getElements('td');
3067             for (let i = 0; i < this.columns.length; ++i) {
3068                 if (Object.prototype.hasOwnProperty.call(data, this.columns[i].dataProperties[0]))
3069                     this.columns[i].updateTd(tds[i], row);
3070             }
3071             row['data'] = {};
3072         }
3073     });
3075     const LogMessageTable = new Class({
3076         Extends: DynamicTable,
3078         filterText: '',
3080         filteredLength: function() {
3081             return this.tableBody.getElements('tr').length;
3082         },
3084         initColumns: function() {
3085             this.newColumn('rowId', '', 'QBT_TR(ID)QBT_TR[CONTEXT=ExecutionLogWidget]', 50, true);
3086             this.newColumn('message', '', 'QBT_TR(Message)QBT_TR[CONTEXT=ExecutionLogWidget]', 350, true);
3087             this.newColumn('timestamp', '', 'QBT_TR(Timestamp)QBT_TR[CONTEXT=ExecutionLogWidget]', 150, true);
3088             this.newColumn('type', '', 'QBT_TR(Log Type)QBT_TR[CONTEXT=ExecutionLogWidget]', 100, true);
3089             this.initColumnsFunctions();
3090         },
3092         initColumnsFunctions: function() {
3093             this.columns['timestamp'].updateTd = function(td, row) {
3094                 const date = new Date(this.getRowValue(row) * 1000).toLocaleString();
3095                 td.set({ 'text': date, 'title': date });
3096             };
3098             this.columns['type'].updateTd = function(td, row) {
3099                 //Type of the message: Log::NORMAL: 1, Log::INFO: 2, Log::WARNING: 4, Log::CRITICAL: 8
3100                 let logLevel, addClass;
3101                 switch (this.getRowValue(row).toInt()) {
3102                     case 1:
3103                         logLevel = 'QBT_TR(Normal)QBT_TR[CONTEXT=ExecutionLogWidget]';
3104                         addClass = 'logNormal';
3105                         break;
3106                     case 2:
3107                         logLevel = 'QBT_TR(Info)QBT_TR[CONTEXT=ExecutionLogWidget]';
3108                         addClass = 'logInfo';
3109                         break;
3110                     case 4:
3111                         logLevel = 'QBT_TR(Warning)QBT_TR[CONTEXT=ExecutionLogWidget]';
3112                         addClass = 'logWarning';
3113                         break;
3114                     case 8:
3115                         logLevel = 'QBT_TR(Critical)QBT_TR[CONTEXT=ExecutionLogWidget]';
3116                         addClass = 'logCritical';
3117                         break;
3118                     default:
3119                         logLevel = 'QBT_TR(Unknown)QBT_TR[CONTEXT=ExecutionLogWidget]';
3120                         addClass = 'logUnknown';
3121                         break;
3122                 }
3123                 td.set({ 'text': logLevel, 'title': logLevel });
3124                 td.getParent('tr').set('class', 'logTableRow ' + addClass);
3125             };
3126         },
3128         getFilteredAndSortedRows: function() {
3129             let filteredRows = [];
3130             const rows = this.rows.getValues();
3131             this.filterText = window.qBittorrent.Log.getFilterText();
3132             const filterTerms = (this.filterText.length > 0) ? this.filterText.toLowerCase().split(' ') : [];
3133             const logLevels = window.qBittorrent.Log.getSelectedLevels();
3134             if (filterTerms.length > 0 || logLevels.length < 4) {
3135                 for (let i = 0; i < rows.length; ++i) {
3136                     if (logLevels.indexOf(rows[i].full_data.type.toString()) == -1)
3137                         continue;
3139                     if (filterTerms.length > 0 && !window.qBittorrent.Misc.containsAllTerms(rows[i].full_data.message, filterTerms))
3140                         continue;
3142                     filteredRows.push(rows[i]);
3143                 }
3144             }
3145             else {
3146                 filteredRows = rows;
3147             }
3149             filteredRows.sort(function(row1, row2) {
3150                 const column = this.columns[this.sortedColumn];
3151                 const res = column.compareRows(row1, row2);
3152                 return (this.reverseSort == '0') ? res : -res;
3153             }.bind(this));
3155             return filteredRows;
3156         },
3158         setupCommonEvents: function() {},
3160         setupTr: function(tr) {
3161             tr.addClass('logTableRow');
3162         }
3163     });
3165     const LogPeerTable = new Class({
3166         Extends: LogMessageTable,
3168         initColumns: function() {
3169             this.newColumn('rowId', '', 'QBT_TR(ID)QBT_TR[CONTEXT=ExecutionLogWidget]', 50, true);
3170             this.newColumn('ip', '', 'QBT_TR(IP)QBT_TR[CONTEXT=ExecutionLogWidget]', 150, true);
3171             this.newColumn('timestamp', '', 'QBT_TR(Timestamp)QBT_TR[CONTEXT=ExecutionLogWidget]', 150, true);
3172             this.newColumn('blocked', '', 'QBT_TR(Status)QBT_TR[CONTEXT=ExecutionLogWidget]', 150, true);
3173             this.newColumn('reason', '', 'QBT_TR(Reason)QBT_TR[CONTEXT=ExecutionLogWidget]', 150, true);
3175             this.columns['timestamp'].updateTd = function(td, row) {
3176                 const date = new Date(this.getRowValue(row) * 1000).toLocaleString();
3177                 td.set({ 'text': date, 'title': date });
3178             };
3180             this.columns['blocked'].updateTd = function(td, row) {
3181                 let status, addClass;
3182                 if (this.getRowValue(row)) {
3183                     status = 'QBT_TR(Blocked)QBT_TR[CONTEXT=ExecutionLogWidget]';
3184                     addClass = 'peerBlocked';
3185                 }
3186                 else {
3187                     status = 'QBT_TR(Banned)QBT_TR[CONTEXT=ExecutionLogWidget]';
3188                     addClass = 'peerBanned';
3189                 }
3190                 td.set({ 'text': status, 'title': status });
3191                 td.getParent('tr').set('class', 'logTableRow ' + addClass);
3192             };
3193         },
3195         getFilteredAndSortedRows: function() {
3196             let filteredRows = [];
3197             const rows = this.rows.getValues();
3198             this.filterText = window.qBittorrent.Log.getFilterText();
3199             const filterTerms = (this.filterText.length > 0) ? this.filterText.toLowerCase().split(' ') : [];
3200             if (filterTerms.length > 0) {
3201                 for (let i = 0; i < rows.length; ++i) {
3202                     if (filterTerms.length > 0 && !window.qBittorrent.Misc.containsAllTerms(rows[i].full_data.ip, filterTerms))
3203                         continue;
3205                     filteredRows.push(rows[i]);
3206                 }
3207             }
3208             else {
3209                 filteredRows = rows;
3210             }
3212             filteredRows.sort(function(row1, row2) {
3213                 const column = this.columns[this.sortedColumn];
3214                 const res = column.compareRows(row1, row2);
3215                 return (this.reverseSort == '0') ? res : -res;
3216             }.bind(this));
3218             return filteredRows;
3219         }
3220     });
3222     return exports();
3223 })();
3225 Object.freeze(window.qBittorrent.DynamicTable);
3227 /*************************************************************/