3 * Copyright (c) 2008 Ishan Arora <ishan@qbittorrent.org> & Christophe Dumez <chris@qbittorrent.org>
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:
12 * The above copyright notice and this permission notice shall be included in
13 * all copies or substantial portions of the Software.
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
24 /**************************************************************
26 Script : Dynamic Table
28 Authors : Ishan Arora & Christophe Dumez
29 Desc : Programmable sortable table
30 Licence : Open Source MIT Licence
32 **************************************************************/
36 if (window.qBittorrent === undefined) {
37 window.qBittorrent = {};
40 window.qBittorrent.DynamicTable = (function() {
41 const exports = function() {
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
60 const compareNumbers = (val1, val2) => {
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 = [];
84 this.contextMenu = contextMenu;
85 this.sortedColumn = LocalPreferences.get('sorted_column_' + this.dynamicTableDivId, 0);
86 this.reverseSort = LocalPreferences.get('reverse_sort_' + this.dynamicTableDivId, '0');
88 this.loadColumnsOrder();
89 this.updateTableHeaders();
90 this.setupCommonEvents();
91 this.setupHeaderEvents();
92 this.setupHeaderMenu();
93 this.setSortedColumnIcon(this.sortedColumn, null, (this.reverseSort === '1'));
96 setupCommonEvents: function() {
97 const scrollFn = function() {
98 $(this.dynamicTableFixedHeaderDivId).getElements('table')[0].style.left = -$(this.dynamicTableDivId).scrollLeft + 'px';
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
114 // is panel vertical scrollbar visible or does panel content not fit?
115 while (((panel.clientWidth != panel.offsetWidth) || (panel.clientHeight != panel.scrollHeight)) && (n > 0)) {
118 $(this.dynamicTableDivId).style.height = h + 'px';
121 this.lastPanelHeight = panel.getBoundingClientRect().height;
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
138 const panel = tableDiv.getParent('.panel');
139 if (this.lastPanelHeight != panel.getBoundingClientRect().height) {
140 this.lastPanelHeight = panel.getBoundingClientRect().height;
141 panel.fireEvent('resize');
145 setInterval(checkResizeFn, 500);
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', '');
159 if (side === 'right' || side !== 'left') {
160 el.setStyle('border-right-style', '');
161 el.setStyle('border-right-color', '');
162 el.setStyle('border-right-width', '');
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';
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';
181 this.canResize = false;
182 e.target.getParent("tr").style.cursor = '';
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';
195 this.dropSide = 'left';
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';
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);
217 this.lastHoverTh = e.target;
218 this.lastClientX = e.event.clientX;
221 const mouseOutFn = function(e) {
222 resetElementBorderStyle(e.target);
225 const onBeforeStart = function(el) {
227 this.currentHeaderAction = 'start';
228 this.dragMovement = false;
229 this.dragStartX = this.lastClientX;
232 const onStart = function(el, event) {
233 if (this.canResize) {
234 this.currentHeaderAction = 'resize';
235 this.startWidth = this.resizeTh.getStyle('width').toFloat();
238 this.currentHeaderAction = 'drag';
239 el.setStyle('background-color', '#C1D5E7');
243 const onDrag = function(el, event) {
244 if (this.currentHeaderAction === 'resize') {
245 let width = this.startWidth + (event.page.x - this.dragStartX);
248 this.columns[this.resizeTh.columnName].width = width;
249 this.updateColumn(this.resizeTh.columnName);
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')
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);
273 if (this.currentHeaderAction === 'drag') {
274 resetElementBorderStyle(el);
275 el.getSiblings('[class=""]').each(function(el) {
276 resetElementBorderStyle(el);
279 this.currentHeaderAction = '';
282 const onCancel = function(el) {
283 this.currentHeaderAction = '';
284 this.setSortedColumn(el.columnName);
287 const onTouch = function(e) {
288 const column = e.target.columnName;
289 this.currentHeaderAction = '';
290 this.setSortedColumn(column);
293 const ths = this.fixedTableHeader.getElements('th');
295 for (let i = 0; i < ths.length; ++i) {
297 th.addEvent('mousemove', mouseMoveFn);
298 th.addEvent('mouseout', mouseOutFn);
299 th.addEvent('touchend', onTouch);
305 onBeforeStart: onBeforeStart,
308 onComplete: onComplete,
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 === '')
322 if (this.dynamicTable.columns[i].visible !== '0')
323 this.setItemChecked(this.dynamicTable.columns[i].name, true);
325 this.setItemChecked(this.dynamicTable.columns[i].name, false);
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);
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', {
346 class: 'contextMenu scrollableMenu'
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', {
358 const onMenuItemClicked = function(element, ref, action) {
359 this.showColumn(action, this.columns[action].visible === '0');
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);
369 for (let i = 0; i < this.columns.length; ++i) {
370 const text = this.columns[i].caption;
373 ul.appendChild(createLi(this.columns[i].name, text));
374 actions[this.columns[i].name] = onMenuItemClicked;
377 ul.inject(document.body);
379 this.headerContextMenu = new DynamicTableHeaderContextMenuClass({
380 targets: '#' + this.dynamicTableFixedHeaderDivId + ' tr',
389 this.headerContextMenu.dynamicTable = this;
392 initColumns: function() {},
394 newColumn: function(name, style, caption, defaultWidth, defaultVisible) {
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)
407 return row['full_data'][this.dataProperties[pos]];
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);
416 column['updateTd'] = function(td, row) {
417 const value = this.getRowValue(row);
418 td.set('text', value);
419 td.set('title', value);
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'));
429 loadColumnsOrder: function() {
430 const columnsOrder = [];
431 const val = LocalPreferences.get('columns_order_' + this.dynamicTableDivId);
432 if (val === null || val === undefined)
434 val.split(',').forEach(function(v) {
435 if ((v in this.columns) && (!columnsOrder.contains(v)))
436 columnsOrder.push(v);
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]];
447 saveColumnsOrder: function() {
449 for (let i = 0; i < this.columns.length; ++i) {
452 val += this.columns[i].name;
454 LocalPreferences.set('columns_order_' + this.dynamicTableDivId, val);
457 updateTableHeaders: function() {
458 this.updateHeader(this.hiddenTableHeader);
459 this.updateHeader(this.fixedTableHeader);
462 updateHeader: function(header) {
463 const ths = header.getElements('th');
465 for (let i = 0; i < ths.length; ++i) {
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');
476 th.removeClass('invisible');
480 getColumnPos: function(columnName) {
481 for (let i = 0; i < this.columns.length; ++i)
482 if (this.columns[i].name == columnName)
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);
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');
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');
510 if (this.columns[pos].onResize !== null) {
511 this.columns[pos].onResize(columnName);
515 getSortedColumn: function() {
516 return LocalPreferences.get('sorted_column_' + this.dynamicTableDivId);
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);
528 this.reverseSort = this.reverseSort === '0' ? '1' : '0';
529 this.setSortedColumnIcon(column, null, (this.reverseSort === '1'));
531 LocalPreferences.set('sorted_column_' + this.dynamicTableDivId, column);
532 LocalPreferences.set('reverse_sort_' + this.dynamicTableDivId, this.reverseSort);
533 this.updateTable(false);
536 setSortedColumnIcon: function(newColumn, oldColumn, isReverse) {
537 const getCol = function(headerDivId, colName) {
538 const colElem = $$("#" + headerDivId + " .column_" + colName);
539 if (colElem.length == 1)
544 const colElem = getCol(this.dynamicTableFixedHeaderDivId, newColumn);
545 if (colElem !== null) {
546 colElem.addClass('sorted');
548 colElem.addClass('reverse');
550 colElem.removeClass('reverse');
552 const oldColElem = getCol(this.dynamicTableFixedHeaderDivId, oldColumn);
553 if (oldColElem !== null) {
554 oldColElem.removeClass('sorted');
555 oldColElem.removeClass('reverse');
559 getSelectedRowId: function() {
560 if (this.selectedRows.length > 0)
561 return this.selectedRows[0];
565 isRowSelected: function(rowId) {
566 return this.selectedRows.contains(rowId);
570 if (!MUI.ieLegacySupport)
573 const trs = this.tableBody.getElements('tr');
574 trs.each(function(el, i) {
579 el.removeClass('alt');
584 selectAll: function() {
587 const trs = this.tableBody.getElements('tr');
588 for (let i = 0; i < trs.length; ++i) {
590 this.selectedRows.push(tr.rowId);
591 if (!tr.hasClass('selected'))
592 tr.addClass('selected');
596 deselectAll: function() {
597 this.selectedRows.empty();
600 selectRow: function(rowId) {
601 this.selectedRows.push(rowId);
603 this.onSelectedRowChanged();
606 deselectRow: function(rowId) {
607 this.selectedRows.erase(rowId);
609 this.onSelectedRowChanged();
612 selectRows: function(rowId1, rowId2) {
614 if (rowId1 === rowId2) {
615 this.selectRow(rowId1);
621 this.tableBody.getElements('tr').each(function(tr) {
622 if ((tr.rowId == rowId1) || (tr.rowId == rowId2)) {
624 that.selectedRows.push(tr.rowId);
627 that.selectedRows.push(tr.rowId);
631 this.onSelectedRowChanged();
634 reselectRows: function(rowIds) {
636 this.selectedRows = rowIds.slice();
637 this.tableBody.getElements('tr').each(function(tr) {
638 if (rowIds.indexOf(tr.rowId) > -1)
639 tr.addClass('selected');
643 setRowClass: function() {
645 this.tableBody.getElements('tr').each(function(tr) {
646 if (that.isRowSelected(tr.rowId))
647 tr.addClass('selected');
649 tr.removeClass('selected');
653 onSelectedRowChanged: function() {},
655 updateRowData: function(data) {
656 // ensure rowId is a string
657 const rowId = `${data['rowId']}`;
660 if (!this.rows.has(rowId)) {
665 this.rows.set(rowId, row);
668 row = this.rows.get(rowId);
671 for (const x in data)
672 row['full_data'][x] = data[x];
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];
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')
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)
704 updateTable: function(fullUpdate) {
705 if (fullUpdate === undefined)
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);
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) {
726 trs[j].inject(trs[rowPos], 'before');
727 const tmpTr = trs[j];
729 trs.splice(rowPos, 0, tmpTr);
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);
745 tr.addEvent('contextmenu', function(e) {
746 if (!this._this.isRowSelected(this.rowId)) {
747 this._this.deselectAll();
748 this._this.selectRow(this.rowId);
752 tr.addEvent('click', function(e) {
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);
759 this._this.selectRow(this.rowId);
761 else if (e.shift && (this._this.selectedRows.length == 1)) {
762 // Shift key was pressed
763 this._this.selectRows(this._this.getSelectedRowId(), this.rowId);
767 this._this.deselectAll();
768 this._this.selectRow(this.rowId);
772 tr.addEvent('touchstart', function(e) {
773 if (!this._this.isRowSelected(this.rowId)) {
774 this._this.deselectAll();
775 this._this.selectRow(this.rowId);
779 tr.addEvent('keydown', function(event) {
782 this._this.selectPreviousRow();
785 this._this.selectNextRow();
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');
800 if (rowPos >= trs.length) {
801 tr.inject(this.tableBody);
805 tr.inject(trs[rowPos], 'before');
806 trs.splice(rowPos, 0, tr);
809 // Update context menu
810 if (this.contextMenu)
811 this.contextMenu.addTarget(tr);
813 this.updateRow(tr, true);
817 let rowPos = rows.length;
819 while ((rowPos < trs.length) && (trs.length > 0)) {
820 trs[trs.length - 1].dispose();
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);
839 removeRow: function(rowId) {
840 this.selectedRows.erase(rowId);
841 const tr = this.getTrByRowId(rowId);
844 this.rows.erase(rowId);
853 const trs = this.tableBody.getElements('tr');
854 while (trs.length > 0) {
855 trs[trs.length - 1].dispose();
860 selectedRowsIds: function() {
861 return this.selectedRows.slice();
864 getRowIds: function() {
865 return this.rows.getKeys();
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) {
881 const isLastRowSelected = (selectedIndex >= (visibleRows.length - 1));
882 if (!isLastRowSelected) {
885 const newRow = visibleRows[selectedIndex + 1];
886 this.selectRow(newRow.getProperty("data-row-id"));
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) {
903 const isFirstRowSelected = selectedIndex <= 0;
904 if (!isFirstRowSelected) {
907 const newRow = visibleRows[selectedIndex - 1];
908 this.selectRow(newRow.getProperty("data-row-id"));
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();
961 initColumnsFunctions: function() {
964 this.columns['state_icon'].updateTd = function(td, row) {
965 let state = this.getRowValue(row);
973 state = "downloading";
974 img_path = "images/downloading.svg";
979 img_path = "images/upload.svg";
983 img_path = "images/stalledUP.svg";
987 img_path = "images/stalledDL.svg";
990 state = "torrent-stop";
991 img_path = "images/stopped.svg";
994 state = "checked-completed";
995 img_path = "images/checked-completed.svg";
1000 img_path = "images/queued.svg";
1004 case "queuedForChecking":
1005 case "checkingResumeData":
1006 state = "force-recheck";
1007 img_path = "images/force-recheck.svg";
1011 img_path = "images/set-location.svg";
1015 case "missingFiles":
1017 img_path = "images/error.svg";
1020 break; // do nothing
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);
1031 td.adopt(new Element('img', {
1033 'class': 'stateIcon',
1040 this.columns['status'].updateTd = function(td, row) {
1041 const state = this.getRowValue(row);
1048 status = "QBT_TR(Downloading)QBT_TR[CONTEXT=TransferListDelegate]";
1051 status = "QBT_TR(Stalled)QBT_TR[CONTEXT=TransferListDelegate]";
1054 status = "QBT_TR(Downloading metadata)QBT_TR[CONTEXT=TransferListDelegate]";
1056 case "forcedMetaDL":
1057 status = "QBT_TR([F] Downloading metadata)QBT_TR[CONTEXT=TransferListDelegate]";
1060 status = "QBT_TR([F] Downloading)QBT_TR[CONTEXT=TransferListDelegate]";
1064 status = "QBT_TR(Seeding)QBT_TR[CONTEXT=TransferListDelegate]";
1067 status = "QBT_TR([F] Seeding)QBT_TR[CONTEXT=TransferListDelegate]";
1071 status = "QBT_TR(Queued)QBT_TR[CONTEXT=TransferListDelegate]";
1075 status = "QBT_TR(Checking)QBT_TR[CONTEXT=TransferListDelegate]";
1077 case "queuedForChecking":
1078 status = "QBT_TR(Queued for checking)QBT_TR[CONTEXT=TransferListDelegate]";
1080 case "checkingResumeData":
1081 status = "QBT_TR(Checking resume data)QBT_TR[CONTEXT=TransferListDelegate]";
1084 status = "QBT_TR(Paused)QBT_TR[CONTEXT=TransferListDelegate]";
1087 status = "QBT_TR(Completed)QBT_TR[CONTEXT=TransferListDelegate]";
1090 status = "QBT_TR(Moving)QBT_TR[CONTEXT=TransferListDelegate]";
1092 case "missingFiles":
1093 status = "QBT_TR(Missing Files)QBT_TR[CONTEXT=TransferListDelegate]";
1096 status = "QBT_TR(Errored)QBT_TR[CONTEXT=TransferListDelegate]";
1099 status = "QBT_TR(Unknown)QBT_TR[CONTEXT=HttpServer]";
1102 td.set('text', status);
1103 td.set('title', status);
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);
1114 this.columns['priority'].compareRows = function(row1, row2) {
1115 let row1_val = this.getRowValue(row1);
1116 let row2_val = this.getRowValue(row2);
1121 return compareNumbers(row1_val, row2_val);
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' });
1130 this.columns['category'].compareRows = this.columns['name'].compareRows;
1131 this.columns['tags'].compareRows = this.columns['name'].compareRows;
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);
1139 this.columns['total_size'].updateTd = this.columns['size'].updateTd;
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];
1152 div.setWidth(ProgressColumnWidth - 5);
1154 if (div.getValue() != progressFormatted)
1155 div.setValue(progressFormatted);
1158 if (ProgressColumnWidth < 0)
1159 ProgressColumnWidth = td.offsetWidth;
1160 td.adopt(new window.qBittorrent.ProgressBar.ProgressBar(progressFormatted.toFloat(), {
1161 'width': ProgressColumnWidth - 5
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;
1176 this.columns[columnName].updateTd(td, this.rows.get(trs[i].rowId));
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);
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);
1200 return compareNumbers(num_seeds1, num_seeds2);
1204 this.columns['num_leechs'].updateTd = this.columns['num_seeds'].updateTd;
1205 this.columns['num_leechs'].compareRows = this.columns['num_seeds'].compareRows;
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);
1215 this.columns['upspeed'].updateTd = this.columns['dlspeed'].updateTd;
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);
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);
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);
1240 this.columns['completion_on'].updateTd = function(td, row) {
1241 const val = this.getRowValue(row);
1242 if ((val === 0xffffffff) || (val < 0)) {
1244 td.set('title', '');
1247 const date = new Date(this.getRowValue(row) * 1000).toLocaleString();
1248 td.set('text', date);
1249 td.set('title', date);
1253 // dl_limit, up_limit
1254 this.columns['dl_limit'].updateTd = function(td, row) {
1255 const speed = this.getRowValue(row);
1257 td.set('text', '∞');
1258 td.set('title', '∞');
1261 const formattedSpeed = window.qBittorrent.Misc.friendlyUnit(speed, true);
1262 td.set('text', formattedSpeed);
1263 td.set('title', formattedSpeed);
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;
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);
1290 this.columns['completed'].updateTd = this.columns['size'].updateTd;
1293 this.columns['max_ratio'].updateTd = this.columns['ratio'].updateTd;
1296 this.columns['seen_complete'].updateTd = this.columns['completion_on'].updateTd;
1299 this.columns['last_activity'].updateTd = function(td, row) {
1300 const val = this.getRowValue(row);
1302 td.set('text', '∞');
1303 td.set('title', '∞');
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);
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);
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);
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;
1333 switch (filterName) {
1335 if ((state != 'downloading') && (state.indexOf('DL') === -1))
1339 if (state != 'uploading' && state != 'forcedUP' && state != 'stalledUP' && state != 'queuedUP' && state != 'checkingUP')
1343 if ((state != 'uploading') && (state.indexOf('UP') === -1))
1347 if (state.indexOf('paused') === -1)
1351 if (state.indexOf('paused') > -1)
1355 if ((state != 'stalledUP') && (state != 'stalledDL'))
1358 case 'stalled_uploading':
1359 if (state != 'stalledUP')
1362 case 'stalled_downloading':
1363 if (state != 'stalledDL')
1370 if (state == 'stalledDL')
1371 r = (row['full_data'].upspeed > 0);
1373 r = state == 'metaDL' || state == 'forcedMetaDL' || state == 'downloading' || state == 'forcedDL' || state == 'uploading' || state == 'forcedUP';
1378 if (state !== 'checkingUP' && state !== 'checkingDL' && state !== 'checkingResumeData')
1382 if (state != 'error' && state != "unknown" && state != "missingFiles")
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)
1395 break; // do nothing
1397 if (!useSubcategories) {
1398 if (categoryHashInt !== genHash(row['full_data'].category))
1402 const selectedCategoryName = category_list[categoryHash].name + "/";
1403 const torrentCategoryName = row['full_data'].category + "/";
1404 if (!torrentCategoryName.startsWith(selectedCategoryName))
1410 const tagHashInt = parseInt(tagHash);
1411 const isNumber = !isNaN(tagHashInt);
1413 switch (tagHashInt) {
1415 break; // do nothing
1418 if (row['full_data'].tags.length !== 0)
1420 break; // do nothing
1423 let rowTags = row['full_data'].tags.split(', ');
1424 rowTags = rowTags.map(function(tag) {
1425 return genHash(tag);
1427 if (!rowTags.contains(tagHashInt))
1434 const trackerHashInt = Number.parseInt(trackerHash, 10);
1435 switch (trackerHashInt) {
1437 break; // do nothing
1438 case TRACKERS_TRACKERLESS:
1439 if (row['full_data'].trackers_count !== 0)
1443 const tracker = trackerList.get(trackerHashInt);
1444 if (tracker && !tracker.torrents.includes(row['full_data'].rowId))
1450 if ((filterTerms !== undefined) && (filterTerms !== null)
1451 && (filterTerms.length > 0) && !window.qBittorrent.Misc.containsAllTerms(name, filterTerms))
1457 getFilteredTorrentsNumber: function(filterName, categoryHash, tagHash, trackerHash) {
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))
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']);
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];
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')
1500 return filteredRows;
1503 setupTr: function(tr) {
1504 tr.addEvent('dblclick', function(e) {
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)
1516 tr.addClass("torrentsTableContextMenuTarget");
1519 getCurrentTorrentID: function() {
1520 return this.getSelectedRowId();
1523 onSelectedRowChanged: function() {
1524 updatePropertiesPanel();
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();
1552 initColumnsFunctions: function() {
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();
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);
1575 td.adopt(new Element('img', {
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) {
1600 this.columns['flags'].updateTd = function(td, row) {
1601 td.set('text', this.getRowValue(row, 0));
1602 td.set('title', this.getRowValue(row, 1));
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);
1616 // dl_speed, up_speed
1617 this.columns['dl_speed'].updateTd = function(td, row) {
1618 const speed = this.getRowValue(row);
1621 td.set('title', '');
1624 const formattedSpeed = window.qBittorrent.Misc.friendlyUnit(speed, true);
1625 td.set('text', formattedSpeed);
1626 td.set('title', formattedSpeed);
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);
1637 this.columns['uploaded'].updateTd = this.columns['downloaded'].updateTd;
1640 this.columns['relevance'].updateTd = this.columns['progress'].updateTd;
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);
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();
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);
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);
1678 this.columns['fileSize'].updateTd = displaySize;
1679 this.columns['nbSeeders'].updateTd = displayNum;
1680 this.columns['nbLeechers'].updateTd = displayNum;
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;
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;
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))
1730 if ((filterTerms.length > 0) && !window.qBittorrent.Misc.containsAllTerms(row.full_data.fileName, filterTerms))
1732 if ((sizeFilters.min > 0.00) && (row.full_data.fileSize < sizeFilters.min))
1734 if ((sizeFilters.max > 0.00) && (row.full_data.fileSize > sizeFilters.max))
1736 if ((seedsFilters.min > 0) && (row.full_data.nbSeeders < seedsFilters.min))
1738 if ((seedsFilters.max > 0) && (row.full_data.nbSeeders > seedsFilters.max))
1741 filteredRows.push(row);
1745 filteredRows = rows;
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')
1757 return filteredRows;
1760 setupTr: function(tr) {
1761 tr.addClass("searchTableRow");
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();
1777 initColumnsFunctions: function() {
1778 this.columns['enabled'].updateTd = function(td, row) {
1779 const value = this.getRowValue(row);
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");
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");
1795 setupTr: function(tr) {
1796 tr.addClass("searchPluginsTableRow");
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);
1815 const BulkRenameTorrentFilesTable = new Class({
1816 Extends: DynamicTable,
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);
1833 _addNodeToTable: function(node, depth) {
1836 if (node.isFolder) {
1840 checked: node.checked,
1842 original: node.original,
1843 renamed: node.renamed
1847 node.full_data = data;
1848 this.updateRowData(data);
1851 node.data.rowId = node.rowId;
1852 node.full_data = node.data;
1853 this.updateRowData(node.data);
1856 node.children.each(function(child) {
1857 this._addNodeToTable(child, depth + 1);
1861 getRoot: function() {
1862 return this.fileTree.getRoot();
1865 getNode: function(rowId) {
1866 return this.fileTree.getNode(rowId);
1869 getRow: function(node) {
1870 const rowId = this.fileTree.getRowId(node);
1871 return this.rows.get(rowId);
1874 getSelectedRows: function() {
1875 const nodes = this.fileTree.toArray();
1877 return nodes.filter(x => x.checked == 0);
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();
1893 * Toggles the global checkbox and all checkboxes underneath
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];
1905 cb.indeterminate = false;
1906 cb.state = "checked";
1908 node.full_data.checked = node.checked;
1911 let cb = checkboxes[i];
1913 cb.indeterminate = false;
1914 cb.state = "unchecked";
1916 node.full_data.checked = node.checked;
1920 this.updateGlobalCheckbox();
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);
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)
1946 const isAllUnchecked = function() {
1947 for (let i = 0; i < checkboxes.length; ++i) {
1948 if (checkboxes[i].checked)
1953 if (isAllChecked()) {
1954 checkbox.state = "checked";
1955 checkbox.indeterminate = false;
1956 checkbox.checked = true;
1958 else if (isAllUnchecked()) {
1959 checkbox.state = "unchecked";
1960 checkbox.indeterminate = false;
1961 checkbox.checked = false;
1964 checkbox.state = "partial";
1965 checkbox.indeterminate = true;
1966 checkbox.checked = false;
1970 initColumnsFunctions: function() {
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',
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();
1997 checkbox.checked = value == 0;
1998 checkbox.state = checkbox.checked ? "checked" : "unchecked";
1999 checkbox.indeterminate = false;
2000 td.adopt(treeImg, checkbox);
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;
2013 // just update file name
2014 $(fileNameId).set('text', value);
2017 const span = new Element('span', {
2021 const dirImg = new Element('img', {
2022 src: 'images/directory.svg',
2026 'margin-bottom': -3,
2027 'margin-left': (node.depth * 20)
2031 const html = dirImg.outerHTML + span.outerHTML;
2032 td.set('html', html);
2036 const value = this.getRowValue(row);
2037 const span = new Element('span', {
2041 'margin-left': ((node.depth + 1) * 20)
2044 td.set('html', span.outerHTML);
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', {
2056 id: fileNameRenamedId,
2058 td.set('html', span.outerHTML);
2062 onRowSelectionChange: function(row) {},
2064 selectRow: function() {
2068 reselectRows: function(rowIds) {
2071 this.tableBody.getElements('tr').each(function(tr) {
2072 if (rowIds.indexOf(tr.rowId) > -1) {
2073 const node = that.getNode(tr.rowId);
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;
2084 this.updateGlobalCheckbox();
2087 altRow: function() {
2088 let addClass = false;
2089 const trs = this.tableBody.getElements('tr');
2090 trs.each(function(tr) {
2091 if (tr.hasClass("invisible"))
2096 tr.removeClass("nonAlt");
2099 tr.removeClass("alt");
2100 tr.addClass("nonAlt");
2102 addClass = !addClass;
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)
2114 if (node2.isFolder && !node1.isFolder)
2118 const res = column.compareRows(row1, row2);
2119 return (this.reverseSort === '0') ? res : -res;
2122 nodes.each(function(node) {
2123 if (node.children.length > 0)
2124 this._sortNodesByColumn(node.children, column);
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);
2136 const row = this.getRow(node);
2137 filteredRows.push(row);
2142 if (window.qBittorrent.Misc.containsAllTerms(node.original, filterTerms)) {
2143 const row = this.getRow(node);
2144 filteredRows.push(row);
2151 setFilter: function(text) {
2152 const filterTerms = text.trim().toLowerCase().split(' ');
2153 if ((filterTerms.length === 1) && (filterTerms[0] === ''))
2154 this.filterTerms = [];
2156 this.filterTerms = filterTerms;
2159 getFilteredAndSortedRows: function() {
2160 if (this.getRoot() === null)
2163 const generateRowsSignature = function(rows) {
2164 const rowsData = rows.map(function(row) {
2165 return row.full_data;
2167 return JSON.stringify(rowsData);
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);
2176 return filteredRows;
2179 const filteredRows = [];
2180 this.getRoot().children.each(function(child) {
2181 this._filterNodes(child, this.filterTerms, filteredRows);
2183 filteredRows.reverse();
2184 return filteredRows;
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);
2200 const rowsString = generateRowsSignature(this.rows);
2201 if (!hasRowsChanged(rowsString, this.prevRowsString)) {
2202 return this.prevFilteredRows;
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;
2218 setIgnored: function(rowId, ignore) {
2219 const row = this.rows.get(rowId);
2221 row.full_data.remaining = 0;
2223 row.full_data.remaining = (row.full_data.size * (1.0 - (row.full_data.progress / 100)));
2226 setupTr: function(tr) {
2227 tr.addEvent('keydown', function(event) {
2228 switch (event.key) {
2230 qBittorrent.PropFiles.collapseFolder(this._this.getSelectedRowId());
2233 qBittorrent.PropFiles.expandFolder(this._this.getSelectedRowId());
2240 const TorrentFilesTable = new Class({
2241 Extends: DynamicTable,
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);
2258 _addNodeToTable: function(node, depth) {
2261 if (node.isFolder) {
2265 checked: node.checked,
2266 remaining: node.remaining,
2267 progress: node.progress,
2268 priority: window.qBittorrent.PropFiles.normalizePriority(node.priority),
2269 availability: node.availability,
2275 node.full_data = data;
2276 this.updateRowData(data);
2279 node.data.rowId = node.rowId;
2280 node.full_data = node.data;
2281 this.updateRowData(node.data);
2284 node.children.each(function(child) {
2285 this._addNodeToTable(child, depth + 1);
2289 getRoot: function() {
2290 return this.fileTree.getRoot();
2293 getNode: function(rowId) {
2294 return this.fileTree.getNode(rowId);
2297 getRow: function(node) {
2298 const rowId = this.fileTree.getRowId(node);
2299 return this.rows.get(rowId);
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();
2314 initColumnsFunctions: function() {
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);
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);
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);
2336 const treeImg = new Element('img', {
2337 src: 'images/L.gif',
2342 td.adopt(treeImg, window.qBittorrent.PropFiles.createDownloadCheckbox(id, row.full_data.fileId, value));
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;
2357 // just update file name
2358 $(fileNameId).set('text', value);
2361 const collapseIcon = new Element('img', {
2362 src: 'images/go-down.svg',
2364 'margin-left': (node.depth * 20)
2366 class: "filesTableCollapseIcon",
2369 onclick: "qBittorrent.PropFiles.collapseIconClicked(this)"
2371 const span = new Element('span', {
2375 const dirImg = new Element('img', {
2376 src: 'images/directory.svg',
2384 const html = collapseIcon.outerHTML + dirImg.outerHTML + span.outerHTML;
2385 td.set('html', html);
2389 const value = this.getRowValue(row);
2390 const span = new Element('span', {
2394 'margin-left': ((node.depth + 1) * 20)
2397 td.set('html', span.outerHTML);
2402 this.columns['size'].updateTd = displaySize;
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(), {
2417 progressBar.setValue(value.toFloat());
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);
2429 td.adopt(window.qBittorrent.PropFiles.createPriorityCombo(id, row.full_data.fileId, value));
2432 // remaining, availability
2433 this.columns['remaining'].updateTd = displaySize;
2434 this.columns['availability'].updateTd = displayPercentage;
2437 altRow: function() {
2438 let addClass = false;
2439 const trs = this.tableBody.getElements('tr');
2440 trs.each(function(tr) {
2441 if (tr.hasClass("invisible"))
2446 tr.removeClass("nonAlt");
2449 tr.removeClass("alt");
2450 tr.addClass("nonAlt");
2452 addClass = !addClass;
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)
2464 if (node2.isFolder && !node1.isFolder)
2468 const res = column.compareRows(row1, row2);
2469 return (this.reverseSort === '0') ? res : -res;
2472 nodes.each(function(node) {
2473 if (node.children.length > 0)
2474 this._sortNodesByColumn(node.children, column);
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);
2486 const row = this.getRow(node);
2487 filteredRows.push(row);
2492 if (window.qBittorrent.Misc.containsAllTerms(node.name, filterTerms)) {
2493 const row = this.getRow(node);
2494 filteredRows.push(row);
2501 setFilter: function(text) {
2502 const filterTerms = text.trim().toLowerCase().split(' ');
2503 if ((filterTerms.length === 1) && (filterTerms[0] === ''))
2504 this.filterTerms = [];
2506 this.filterTerms = filterTerms;
2509 getFilteredAndSortedRows: function() {
2510 if (this.getRoot() === null)
2513 const generateRowsSignature = function(rows) {
2514 const rowsData = rows.map(function(row) {
2515 return row.full_data;
2517 return JSON.stringify(rowsData);
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);
2526 return filteredRows;
2529 const filteredRows = [];
2530 this.getRoot().children.each(function(child) {
2531 this._filterNodes(child, this.filterTerms, filteredRows);
2533 filteredRows.reverse();
2534 return filteredRows;
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);
2550 const rowsString = generateRowsSignature(this.rows);
2551 if (!hasRowsChanged(rowsString, this.prevRowsString)) {
2552 return this.prevFilteredRows;
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;
2568 setIgnored: function(rowId, ignore) {
2569 const row = this.rows.get(rowId);
2571 row.full_data.remaining = 0;
2573 row.full_data.remaining = (row.full_data.size * (1.0 - (row.full_data.progress / 100)));
2576 setupTr: function(tr) {
2577 tr.addEvent('keydown', function(event) {
2578 switch (event.key) {
2580 qBittorrent.PropFiles.collapseFolder(this._this.getSelectedRowId());
2583 qBittorrent.PropFiles.expandFolder(this._this.getSelectedRowId());
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);
2608 setupHeaderMenu: function() {},
2609 setupHeaderEvents: function() {},
2610 getFilteredAndSortedRows: function() {
2611 return this.rows.getValues();
2613 selectRow: function(rowId) {
2614 this.selectedRows.push(rowId);
2616 this.onSelectedRowChanged();
2618 const rows = this.rows.getValues();
2620 for (let i = 0; i < rows.length; ++i) {
2621 if (rows[i].rowId === rowId) {
2622 path = rows[i].full_data.dataPath;
2626 window.qBittorrent.Rss.showRssFeed(path);
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);
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);
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';
2651 updateIcons: function() {
2653 this.rows.each(row => {
2655 switch (row.full_data.status) {
2657 img_path = 'images/application-rss.svg';
2660 img_path = 'images/task-reject.svg';
2663 img_path = 'images/spinner.gif';
2666 img_path = 'images/mail-inbox.svg';
2669 img_path = 'images/folder-documents.svg';
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];
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);
2687 td.adopt(new Element('img', {
2689 'class': 'stateIcon',
2696 newColumn: function(name, style, caption, defaultWidth, defaultVisible) {
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;
2708 column['dataProperties'] = [name];
2709 column['getRowValue'] = function(row, pos) {
2710 if (pos === undefined)
2712 return row['full_data'][this.dataProperties[pos]];
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);
2721 column['updateTd'] = function(td, row) {
2722 const value = this.getRowValue(row);
2723 td.set('text', value);
2724 td.set('title', value);
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'));
2733 setupCommonEvents: function() {
2734 const scrollFn = function() {
2735 $(this.dynamicTableFixedHeaderDivId).getElements('table')[0].style.left = -$(this.dynamicTableDivId).scrollLeft + 'px';
2738 $(this.dynamicTableDivId).addEvent('scroll', scrollFn);
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);
2747 setupHeaderMenu: function() {},
2748 setupHeaderEvents: function() {},
2749 getFilteredAndSortedRows: function() {
2750 return this.rows.getValues();
2752 selectRow: function(rowId) {
2753 this.selectedRows.push(rowId);
2755 this.onSelectedRowChanged();
2757 const rows = this.rows.getValues();
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');
2768 window.qBittorrent.Rss.showDetails(feedUid, articleId);
2770 setupTr: function(tr) {
2771 tr.addEvent('dblclick', function(e) {
2772 showDownloadPage([this._this.rows.get(this.rowId).full_data.torrentURL]);
2775 tr.addClass('torrentsTableContextMenuTarget');
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');
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);
2792 newColumn: function(name, style, caption, defaultWidth, defaultVisible) {
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;
2804 column['dataProperties'] = [name];
2805 column['getRowValue'] = function(row, pos) {
2806 if (pos === undefined)
2808 return row['full_data'][this.dataProperties[pos]];
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);
2817 column['updateTd'] = function(td, row) {
2818 const value = this.getRowValue(row);
2819 td.set('text', value);
2820 td.set('title', value);
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'));
2829 setupCommonEvents: function() {
2830 const scrollFn = function() {
2831 $(this.dynamicTableFixedHeaderDivId).getElements('table')[0].style.left = -$(this.dynamicTableDivId).scrollLeft + 'px';
2834 $(this.dynamicTableDivId).addEvent('scroll', scrollFn);
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({
2854 checked: this.checked
2856 window.qBittorrent.RssDownloader.modifyRuleState(row.full_data.name, 'enabled', this.checked);
2857 e.stopPropagation();
2860 td.append(checkbox);
2863 $('cbRssDlRule' + row.rowId).checked = row.full_data.checked;
2867 setupHeaderMenu: function() {},
2868 setupHeaderEvents: function() {},
2869 getFilteredAndSortedRows: function() {
2870 return this.rows.getValues();
2872 setupTr: function(tr) {
2873 tr.addEvent('dblclick', function(e) {
2874 window.qBittorrent.RssDownloader.renameRule(this._this.rows.get(this.rowId).full_data.name);
2878 newColumn: function(name, style, caption, defaultWidth, defaultVisible) {
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;
2890 column['dataProperties'] = [name];
2891 column['getRowValue'] = function(row, pos) {
2892 if (pos === undefined)
2894 return row['full_data'][this.dataProperties[pos]];
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);
2903 column['updateTd'] = function(td, row) {
2904 const value = this.getRowValue(row);
2905 td.set('text', value);
2906 td.set('title', value);
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'));
2915 selectRow: function(rowId) {
2916 this.selectedRows.push(rowId);
2918 this.onSelectedRowChanged();
2920 const rows = this.rows.getValues();
2922 for (let i = 0; i < rows.length; ++i) {
2923 if (rows[i].rowId === rowId) {
2924 name = rows[i].full_data.name;
2928 window.qBittorrent.RssDownloader.showRule(name);
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({
2948 checked: this.checked
2950 e.stopPropagation();
2953 td.append(checkbox);
2956 $('cbRssDlFeed' + row.rowId).checked = row.full_data.checked;
2960 setupHeaderMenu: function() {},
2961 setupHeaderEvents: function() {},
2962 getFilteredAndSortedRows: function() {
2963 return this.rows.getValues();
2965 newColumn: function(name, style, caption, defaultWidth, defaultVisible) {
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;
2977 column['dataProperties'] = [name];
2978 column['getRowValue'] = function(row, pos) {
2979 if (pos === undefined)
2981 return row['full_data'][this.dataProperties[pos]];
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);
2990 column['updateTd'] = function(td, row) {
2991 const value = this.getRowValue(row);
2992 td.set('text', value);
2993 td.set('title', value);
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'));
3002 selectRow: function() {}
3005 const RssDownloaderArticlesTable = new Class({
3006 Extends: DynamicTable,
3007 initColumns: function() {
3008 this.newColumn('name', '', '', -1, true);
3010 setupHeaderMenu: function() {},
3011 setupHeaderEvents: function() {},
3012 getFilteredAndSortedRows: function() {
3013 return this.rows.getValues();
3015 newColumn: function(name, style, caption, defaultWidth, defaultVisible) {
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;
3027 column['dataProperties'] = [name];
3028 column['getRowValue'] = function(row, pos) {
3029 if (pos === undefined)
3031 return row['full_data'][this.dataProperties[pos]];
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);
3040 column['updateTd'] = function(td, row) {
3041 const value = this.getRowValue(row);
3042 td.set('text', value);
3043 td.set('title', value);
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'));
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');
3062 tr.removeClass('articleTableFeed');
3063 tr.addClass('articleTableArticle');
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);
3075 const LogMessageTable = new Class({
3076 Extends: DynamicTable,
3080 filteredLength: function() {
3081 return this.tableBody.getElements('tr').length;
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();
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 });
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()) {
3103 logLevel = 'QBT_TR(Normal)QBT_TR[CONTEXT=ExecutionLogWidget]';
3104 addClass = 'logNormal';
3107 logLevel = 'QBT_TR(Info)QBT_TR[CONTEXT=ExecutionLogWidget]';
3108 addClass = 'logInfo';
3111 logLevel = 'QBT_TR(Warning)QBT_TR[CONTEXT=ExecutionLogWidget]';
3112 addClass = 'logWarning';
3115 logLevel = 'QBT_TR(Critical)QBT_TR[CONTEXT=ExecutionLogWidget]';
3116 addClass = 'logCritical';
3119 logLevel = 'QBT_TR(Unknown)QBT_TR[CONTEXT=ExecutionLogWidget]';
3120 addClass = 'logUnknown';
3123 td.set({ 'text': logLevel, 'title': logLevel });
3124 td.getParent('tr').set('class', 'logTableRow ' + addClass);
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)
3139 if (filterTerms.length > 0 && !window.qBittorrent.Misc.containsAllTerms(rows[i].full_data.message, filterTerms))
3142 filteredRows.push(rows[i]);
3146 filteredRows = rows;
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;
3155 return filteredRows;
3158 setupCommonEvents: function() {},
3160 setupTr: function(tr) {
3161 tr.addClass('logTableRow');
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 });
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';
3187 status = 'QBT_TR(Banned)QBT_TR[CONTEXT=ExecutionLogWidget]';
3188 addClass = 'peerBanned';
3190 td.set({ 'text': status, 'title': status });
3191 td.getParent('tr').set('class', 'logTableRow ' + addClass);
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))
3205 filteredRows.push(rows[i]);
3209 filteredRows = rows;
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;
3218 return filteredRows;
3225 Object.freeze(window.qBittorrent.DynamicTable);
3227 /*************************************************************/