WebUI: Fix error when category doesn't exist
[qBittorrent.git] / src / webui / www / private / scripts / dynamicTable.js
blobc01d72cd73937d02dd2ba3d1dc2338754b62d252
1 /*
2 * MIT License
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
21 * THE SOFTWARE.
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
60 const compareNumbers = (val1, val2) => {
61 if (val1 < val2)
62 return -1;
63 if (val1 > val2)
64 return 1;
65 return 0;
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'));
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';
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;
138 const panel = tableDiv.getParent('.panel');
139 if (this.lastPanelHeight != panel.getBoundingClientRect().height) {
140 this.lastPanelHeight = panel.getBoundingClientRect().height;
141 panel.fireEvent('resize');
143 }.bind(this);
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';
180 else {
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';
194 else {
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;
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();
237 else {
238 this.currentHeaderAction = 'drag';
239 el.setStyle('background-color', '#C1D5E7');
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);
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);
273 if (this.currentHeaderAction === 'drag') {
274 resetElementBorderStyle(el);
275 el.getSiblings('[class=""]').each(function(el) {
276 resetElementBorderStyle(el);
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: ''
305 onBeforeStart: onBeforeStart,
306 onStart: onStart,
307 onDrag: onDrag,
308 onComplete: onComplete,
309 onCancel: onCancel
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);
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', {
345 id: menuId,
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', {
352 html: html
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);
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;
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
389 this.headerContextMenu.dynamicTable = this;
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]];
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)
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]];
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;
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) {
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');
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;
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');
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');
510 if (this.columns[pos].onResize !== null) {
511 this.columns[pos].onResize(columnName);
515 getSortedColumn: function() {
516 return LocalPreferences.get('sorted_column_' + this.dynamicTableDivId);
520 * @param {string} column name to sort by
521 * @param {string|null} reverse defaults to implementation-specific behavior when not specified. Should only be passed when restoring previous state.
523 setSortedColumn: function(column, reverse = null) {
524 if (column != this.sortedColumn) {
525 const oldColumn = this.sortedColumn;
526 this.sortedColumn = column;
527 this.reverseSort = reverse ?? '0';
528 this.setSortedColumnIcon(column, oldColumn, false);
530 else {
531 // Toggle sort order
532 this.reverseSort = reverse ?? (this.reverseSort === '0' ? '1' : '0');
533 this.setSortedColumnIcon(column, null, (this.reverseSort === '1'));
535 LocalPreferences.set('sorted_column_' + this.dynamicTableDivId, column);
536 LocalPreferences.set('reverse_sort_' + this.dynamicTableDivId, this.reverseSort);
537 this.updateTable(false);
540 setSortedColumnIcon: function(newColumn, oldColumn, isReverse) {
541 const getCol = function(headerDivId, colName) {
542 const colElem = $$("#" + headerDivId + " .column_" + colName);
543 if (colElem.length == 1)
544 return colElem[0];
545 return null;
548 const colElem = getCol(this.dynamicTableFixedHeaderDivId, newColumn);
549 if (colElem !== null) {
550 colElem.addClass('sorted');
551 if (isReverse)
552 colElem.addClass('reverse');
553 else
554 colElem.removeClass('reverse');
556 const oldColElem = getCol(this.dynamicTableFixedHeaderDivId, oldColumn);
557 if (oldColElem !== null) {
558 oldColElem.removeClass('sorted');
559 oldColElem.removeClass('reverse');
563 getSelectedRowId: function() {
564 if (this.selectedRows.length > 0)
565 return this.selectedRows[0];
566 return '';
569 isRowSelected: function(rowId) {
570 return this.selectedRows.contains(rowId);
573 altRow: function() {
574 if (!MUI.ieLegacySupport)
575 return;
577 const trs = this.tableBody.getElements('tr');
578 trs.each(function(el, i) {
579 if (i % 2) {
580 el.addClass('alt');
582 else {
583 el.removeClass('alt');
585 }.bind(this));
588 selectAll: function() {
589 this.deselectAll();
591 const trs = this.tableBody.getElements('tr');
592 for (let i = 0; i < trs.length; ++i) {
593 const tr = trs[i];
594 this.selectedRows.push(tr.rowId);
595 if (!tr.hasClass('selected'))
596 tr.addClass('selected');
600 deselectAll: function() {
601 this.selectedRows.empty();
604 selectRow: function(rowId) {
605 this.selectedRows.push(rowId);
606 this.setRowClass();
607 this.onSelectedRowChanged();
610 deselectRow: function(rowId) {
611 this.selectedRows.erase(rowId);
612 this.setRowClass();
613 this.onSelectedRowChanged();
616 selectRows: function(rowId1, rowId2) {
617 this.deselectAll();
618 if (rowId1 === rowId2) {
619 this.selectRow(rowId1);
620 return;
623 let select = false;
624 const that = this;
625 this.tableBody.getElements('tr').each(function(tr) {
626 if ((tr.rowId == rowId1) || (tr.rowId == rowId2)) {
627 select = !select;
628 that.selectedRows.push(tr.rowId);
630 else if (select) {
631 that.selectedRows.push(tr.rowId);
634 this.setRowClass();
635 this.onSelectedRowChanged();
638 reselectRows: function(rowIds) {
639 this.deselectAll();
640 this.selectedRows = rowIds.slice();
641 this.tableBody.getElements('tr').each(function(tr) {
642 if (rowIds.indexOf(tr.rowId) > -1)
643 tr.addClass('selected');
647 setRowClass: function() {
648 const that = this;
649 this.tableBody.getElements('tr').each(function(tr) {
650 if (that.isRowSelected(tr.rowId))
651 tr.addClass('selected');
652 else
653 tr.removeClass('selected');
657 onSelectedRowChanged: function() {},
659 updateRowData: function(data) {
660 // ensure rowId is a string
661 const rowId = `${data['rowId']}`;
662 let row;
664 if (!this.rows.has(rowId)) {
665 row = {
666 'full_data': {},
667 'rowId': rowId
669 this.rows.set(rowId, row);
671 else
672 row = this.rows.get(rowId);
674 row['data'] = data;
675 for (const x in data)
676 row['full_data'][x] = data[x];
679 getFilteredAndSortedRows: function() {
680 const filteredRows = [];
682 const rows = this.rows.getValues();
684 for (let i = 0; i < rows.length; ++i) {
685 filteredRows.push(rows[i]);
686 filteredRows[rows[i].rowId] = rows[i];
689 filteredRows.sort(function(row1, row2) {
690 const column = this.columns[this.sortedColumn];
691 const res = column.compareRows(row1, row2);
692 if (this.reverseSort === '0')
693 return res;
694 else
695 return -res;
696 }.bind(this));
697 return filteredRows;
700 getTrByRowId: function(rowId) {
701 const trs = this.tableBody.getElements('tr');
702 for (let i = 0; i < trs.length; ++i)
703 if (trs[i].rowId == rowId)
704 return trs[i];
705 return null;
708 updateTable: function(fullUpdate = false) {
709 const rows = this.getFilteredAndSortedRows();
711 for (let i = 0; i < this.selectedRows.length; ++i)
712 if (!(this.selectedRows[i] in rows)) {
713 this.selectedRows.splice(i, 1);
714 --i;
717 const trs = this.tableBody.getElements('tr');
719 for (let rowPos = 0; rowPos < rows.length; ++rowPos) {
720 const rowId = rows[rowPos]['rowId'];
721 let tr_found = false;
722 for (let j = rowPos; j < trs.length; ++j)
723 if (trs[j]['rowId'] == rowId) {
724 tr_found = true;
725 if (rowPos == j)
726 break;
727 trs[j].inject(trs[rowPos], 'before');
728 const tmpTr = trs[j];
729 trs.splice(j, 1);
730 trs.splice(rowPos, 0, tmpTr);
731 break;
733 if (tr_found) // row already exists in the table
734 this.updateRow(trs[rowPos], fullUpdate);
735 else { // else create a new row in the table
736 const tr = new Element('tr');
737 // set tabindex so element receives keydown events
738 // more info: https://developer.mozilla.org/en-US/docs/Web/API/Element/keydown_event
739 tr.setProperty("tabindex", "-1");
741 const rowId = rows[rowPos]['rowId'];
742 tr.setProperty("data-row-id", rowId);
743 tr['rowId'] = rowId;
745 tr._this = this;
746 tr.addEvent('contextmenu', function(e) {
747 if (!this._this.isRowSelected(this.rowId)) {
748 this._this.deselectAll();
749 this._this.selectRow(this.rowId);
751 return true;
753 tr.addEvent('click', function(e) {
754 e.stop();
755 if (e.control || e.meta) {
756 // CTRL/CMD ⌘ key was pressed
757 if (this._this.isRowSelected(this.rowId))
758 this._this.deselectRow(this.rowId);
759 else
760 this._this.selectRow(this.rowId);
762 else if (e.shift && (this._this.selectedRows.length == 1)) {
763 // Shift key was pressed
764 this._this.selectRows(this._this.getSelectedRowId(), this.rowId);
766 else {
767 // Simple selection
768 this._this.deselectAll();
769 this._this.selectRow(this.rowId);
771 return false;
773 tr.addEvent('touchstart', function(e) {
774 if (!this._this.isRowSelected(this.rowId)) {
775 this._this.deselectAll();
776 this._this.selectRow(this.rowId);
778 return false;
780 tr.addEvent('keydown', function(event) {
781 switch (event.key) {
782 case "up":
783 this._this.selectPreviousRow();
784 return false;
785 case "down":
786 this._this.selectNextRow();
787 return false;
791 this.setupTr(tr);
793 for (let k = 0; k < this.columns.length; ++k) {
794 const td = new Element('td');
795 if ((this.columns[k].visible == '0') || this.columns[k].force_hide)
796 td.addClass('invisible');
797 td.injectInside(tr);
800 // Insert
801 if (rowPos >= trs.length) {
802 tr.inject(this.tableBody);
803 trs.push(tr);
805 else {
806 tr.inject(trs[rowPos], 'before');
807 trs.splice(rowPos, 0, tr);
810 // Update context menu
811 if (this.contextMenu)
812 this.contextMenu.addTarget(tr);
814 this.updateRow(tr, true);
818 let rowPos = rows.length;
820 while ((rowPos < trs.length) && (trs.length > 0)) {
821 trs.pop().destroy();
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.hasOwn(data, this.columns[i].dataProperties[0]))
834 this.columns[i].updateTd(tds[i], row);
836 row['data'] = {};
839 removeRow: function(rowId) {
840 this.selectedRows.erase(rowId);
841 const tr = this.getTrByRowId(rowId);
842 if (tr !== null) {
843 tr.destroy();
844 this.rows.erase(rowId);
845 return true;
847 return false;
850 clear: function() {
851 this.deselectAll();
852 this.rows.empty();
853 const trs = this.tableBody.getElements('tr');
854 while (trs.length > 0) {
855 trs.pop().destroy();
859 selectedRowsIds: function() {
860 return this.selectedRows.slice();
863 getRowIds: function() {
864 return this.rows.getKeys();
867 selectNextRow: function() {
868 const visibleRows = $(this.dynamicTableDivId).getElements("tbody tr").filter(e => e.getStyle("display") !== "none");
869 const selectedRowId = this.getSelectedRowId();
871 let selectedIndex = -1;
872 for (let i = 0; i < visibleRows.length; ++i) {
873 const row = visibleRows[i];
874 if (row.getProperty("data-row-id") === selectedRowId) {
875 selectedIndex = i;
876 break;
880 const isLastRowSelected = (selectedIndex >= (visibleRows.length - 1));
881 if (!isLastRowSelected) {
882 this.deselectAll();
884 const newRow = visibleRows[selectedIndex + 1];
885 this.selectRow(newRow.getProperty("data-row-id"));
889 selectPreviousRow: function() {
890 const visibleRows = $(this.dynamicTableDivId).getElements("tbody tr").filter(e => e.getStyle("display") !== "none");
891 const selectedRowId = this.getSelectedRowId();
893 let selectedIndex = -1;
894 for (let i = 0; i < visibleRows.length; ++i) {
895 const row = visibleRows[i];
896 if (row.getProperty("data-row-id") === selectedRowId) {
897 selectedIndex = i;
898 break;
902 const isFirstRowSelected = selectedIndex <= 0;
903 if (!isFirstRowSelected) {
904 this.deselectAll();
906 const newRow = visibleRows[selectedIndex - 1];
907 this.selectRow(newRow.getProperty("data-row-id"));
912 const TorrentsTable = new Class({
913 Extends: DynamicTable,
915 initColumns: function() {
916 this.newColumn('priority', '', '#', 30, true);
917 this.newColumn('state_icon', 'cursor: default', '', 22, true);
918 this.newColumn('name', '', 'QBT_TR(Name)QBT_TR[CONTEXT=TransferListModel]', 200, true);
919 this.newColumn('size', '', 'QBT_TR(Size)QBT_TR[CONTEXT=TransferListModel]', 100, true);
920 this.newColumn('total_size', '', 'QBT_TR(Total Size)QBT_TR[CONTEXT=TransferListModel]', 100, false);
921 this.newColumn('progress', '', 'QBT_TR(Done)QBT_TR[CONTEXT=TransferListModel]', 85, true);
922 this.newColumn('status', '', 'QBT_TR(Status)QBT_TR[CONTEXT=TransferListModel]', 100, true);
923 this.newColumn('num_seeds', '', 'QBT_TR(Seeds)QBT_TR[CONTEXT=TransferListModel]', 100, true);
924 this.newColumn('num_leechs', '', 'QBT_TR(Peers)QBT_TR[CONTEXT=TransferListModel]', 100, true);
925 this.newColumn('dlspeed', '', 'QBT_TR(Down Speed)QBT_TR[CONTEXT=TransferListModel]', 100, true);
926 this.newColumn('upspeed', '', 'QBT_TR(Up Speed)QBT_TR[CONTEXT=TransferListModel]', 100, true);
927 this.newColumn('eta', '', 'QBT_TR(ETA)QBT_TR[CONTEXT=TransferListModel]', 100, true);
928 this.newColumn('ratio', '', 'QBT_TR(Ratio)QBT_TR[CONTEXT=TransferListModel]', 100, true);
929 this.newColumn('category', '', 'QBT_TR(Category)QBT_TR[CONTEXT=TransferListModel]', 100, true);
930 this.newColumn('tags', '', 'QBT_TR(Tags)QBT_TR[CONTEXT=TransferListModel]', 100, true);
931 this.newColumn('added_on', '', 'QBT_TR(Added On)QBT_TR[CONTEXT=TransferListModel]', 100, true);
932 this.newColumn('completion_on', '', 'QBT_TR(Completed On)QBT_TR[CONTEXT=TransferListModel]', 100, false);
933 this.newColumn('tracker', '', 'QBT_TR(Tracker)QBT_TR[CONTEXT=TransferListModel]', 100, false);
934 this.newColumn('dl_limit', '', 'QBT_TR(Down Limit)QBT_TR[CONTEXT=TransferListModel]', 100, false);
935 this.newColumn('up_limit', '', 'QBT_TR(Up Limit)QBT_TR[CONTEXT=TransferListModel]', 100, false);
936 this.newColumn('downloaded', '', 'QBT_TR(Downloaded)QBT_TR[CONTEXT=TransferListModel]', 100, false);
937 this.newColumn('uploaded', '', 'QBT_TR(Uploaded)QBT_TR[CONTEXT=TransferListModel]', 100, false);
938 this.newColumn('downloaded_session', '', 'QBT_TR(Session Download)QBT_TR[CONTEXT=TransferListModel]', 100, false);
939 this.newColumn('uploaded_session', '', 'QBT_TR(Session Upload)QBT_TR[CONTEXT=TransferListModel]', 100, false);
940 this.newColumn('amount_left', '', 'QBT_TR(Remaining)QBT_TR[CONTEXT=TransferListModel]', 100, false);
941 this.newColumn('time_active', '', 'QBT_TR(Time Active)QBT_TR[CONTEXT=TransferListModel]', 100, false);
942 this.newColumn('save_path', '', 'QBT_TR(Save path)QBT_TR[CONTEXT=TransferListModel]', 100, false);
943 this.newColumn('completed', '', 'QBT_TR(Completed)QBT_TR[CONTEXT=TransferListModel]', 100, false);
944 this.newColumn('max_ratio', '', 'QBT_TR(Ratio Limit)QBT_TR[CONTEXT=TransferListModel]', 100, false);
945 this.newColumn('seen_complete', '', 'QBT_TR(Last Seen Complete)QBT_TR[CONTEXT=TransferListModel]', 100, false);
946 this.newColumn('last_activity', '', 'QBT_TR(Last Activity)QBT_TR[CONTEXT=TransferListModel]', 100, false);
947 this.newColumn('availability', '', 'QBT_TR(Availability)QBT_TR[CONTEXT=TransferListModel]', 100, false);
948 this.newColumn('reannounce', '', 'QBT_TR(Reannounce In)QBT_TR[CONTEXT=TransferListModel]', 100, false);
950 this.columns['state_icon'].onclick = '';
951 this.columns['state_icon'].dataProperties[0] = 'state';
953 this.columns['num_seeds'].dataProperties.push('num_complete');
954 this.columns['num_leechs'].dataProperties.push('num_incomplete');
955 this.columns['time_active'].dataProperties.push('seeding_time');
957 this.initColumnsFunctions();
960 initColumnsFunctions: function() {
962 // state_icon
963 this.columns['state_icon'].updateTd = function(td, row) {
964 let state = this.getRowValue(row);
965 let img_path;
966 // normalize states
967 switch (state) {
968 case "forcedDL":
969 case "metaDL":
970 case "forcedMetaDL":
971 case "downloading":
972 state = "downloading";
973 img_path = "images/downloading.svg";
974 break;
975 case "forcedUP":
976 case "uploading":
977 state = "uploading";
978 img_path = "images/upload.svg";
979 break;
980 case "stalledUP":
981 state = "stalledUP";
982 img_path = "images/stalledUP.svg";
983 break;
984 case "stalledDL":
985 state = "stalledDL";
986 img_path = "images/stalledDL.svg";
987 break;
988 case "stoppedDL":
989 state = "torrent-stop";
990 img_path = "images/stopped.svg";
991 break;
992 case "stoppedUP":
993 state = "checked-completed";
994 img_path = "images/checked-completed.svg";
995 break;
996 case "queuedDL":
997 case "queuedUP":
998 state = "queued";
999 img_path = "images/queued.svg";
1000 break;
1001 case "checkingDL":
1002 case "checkingUP":
1003 case "queuedForChecking":
1004 case "checkingResumeData":
1005 state = "force-recheck";
1006 img_path = "images/force-recheck.svg";
1007 break;
1008 case "moving":
1009 state = "moving";
1010 img_path = "images/set-location.svg";
1011 break;
1012 case "error":
1013 case "unknown":
1014 case "missingFiles":
1015 state = "error";
1016 img_path = "images/error.svg";
1017 break;
1018 default:
1019 break; // do nothing
1022 if (td.getChildren('img').length > 0) {
1023 const img = td.getChildren('img')[0];
1024 if (img.src.indexOf(img_path) < 0) {
1025 img.set('src', img_path);
1026 img.set('title', state);
1029 else {
1030 td.adopt(new Element('img', {
1031 'src': img_path,
1032 'class': 'stateIcon',
1033 'title': state
1034 }));
1038 // status
1039 this.columns['status'].updateTd = function(td, row) {
1040 const state = this.getRowValue(row);
1041 if (!state)
1042 return;
1044 let status;
1045 switch (state) {
1046 case "downloading":
1047 status = "QBT_TR(Downloading)QBT_TR[CONTEXT=TransferListDelegate]";
1048 break;
1049 case "stalledDL":
1050 status = "QBT_TR(Stalled)QBT_TR[CONTEXT=TransferListDelegate]";
1051 break;
1052 case "metaDL":
1053 status = "QBT_TR(Downloading metadata)QBT_TR[CONTEXT=TransferListDelegate]";
1054 break;
1055 case "forcedMetaDL":
1056 status = "QBT_TR([F] Downloading metadata)QBT_TR[CONTEXT=TransferListDelegate]";
1057 break;
1058 case "forcedDL":
1059 status = "QBT_TR([F] Downloading)QBT_TR[CONTEXT=TransferListDelegate]";
1060 break;
1061 case "uploading":
1062 case "stalledUP":
1063 status = "QBT_TR(Seeding)QBT_TR[CONTEXT=TransferListDelegate]";
1064 break;
1065 case "forcedUP":
1066 status = "QBT_TR([F] Seeding)QBT_TR[CONTEXT=TransferListDelegate]";
1067 break;
1068 case "queuedDL":
1069 case "queuedUP":
1070 status = "QBT_TR(Queued)QBT_TR[CONTEXT=TransferListDelegate]";
1071 break;
1072 case "checkingDL":
1073 case "checkingUP":
1074 status = "QBT_TR(Checking)QBT_TR[CONTEXT=TransferListDelegate]";
1075 break;
1076 case "queuedForChecking":
1077 status = "QBT_TR(Queued for checking)QBT_TR[CONTEXT=TransferListDelegate]";
1078 break;
1079 case "checkingResumeData":
1080 status = "QBT_TR(Checking resume data)QBT_TR[CONTEXT=TransferListDelegate]";
1081 break;
1082 case "stoppedDL":
1083 status = "QBT_TR(Stopped)QBT_TR[CONTEXT=TransferListDelegate]";
1084 break;
1085 case "stoppedUP":
1086 status = "QBT_TR(Completed)QBT_TR[CONTEXT=TransferListDelegate]";
1087 break;
1088 case "moving":
1089 status = "QBT_TR(Moving)QBT_TR[CONTEXT=TransferListDelegate]";
1090 break;
1091 case "missingFiles":
1092 status = "QBT_TR(Missing Files)QBT_TR[CONTEXT=TransferListDelegate]";
1093 break;
1094 case "error":
1095 status = "QBT_TR(Errored)QBT_TR[CONTEXT=TransferListDelegate]";
1096 break;
1097 default:
1098 status = "QBT_TR(Unknown)QBT_TR[CONTEXT=HttpServer]";
1101 td.set('text', status);
1102 td.set('title', status);
1105 // priority
1106 this.columns['priority'].updateTd = function(td, row) {
1107 const queuePos = this.getRowValue(row);
1108 const formattedQueuePos = (queuePos < 1) ? '*' : queuePos;
1109 td.set('text', formattedQueuePos);
1110 td.set('title', formattedQueuePos);
1113 this.columns['priority'].compareRows = function(row1, row2) {
1114 let row1_val = this.getRowValue(row1);
1115 let row2_val = this.getRowValue(row2);
1116 if (row1_val < 1)
1117 row1_val = 1000000;
1118 if (row2_val < 1)
1119 row2_val = 1000000;
1120 return compareNumbers(row1_val, row2_val);
1123 // name, category, tags
1124 this.columns['name'].compareRows = function(row1, row2) {
1125 const row1Val = this.getRowValue(row1);
1126 const row2Val = this.getRowValue(row2);
1127 return row1Val.localeCompare(row2Val, undefined, { numeric: true, sensitivity: 'base' });
1129 this.columns['category'].compareRows = this.columns['name'].compareRows;
1130 this.columns['tags'].compareRows = this.columns['name'].compareRows;
1132 // size, total_size
1133 this.columns['size'].updateTd = function(td, row) {
1134 const size = window.qBittorrent.Misc.friendlyUnit(this.getRowValue(row), false);
1135 td.set('text', size);
1136 td.set('title', size);
1138 this.columns['total_size'].updateTd = this.columns['size'].updateTd;
1140 // progress
1141 this.columns['progress'].updateTd = function(td, row) {
1142 const progress = this.getRowValue(row);
1143 let progressFormatted = (progress * 100).round(1);
1144 if (progressFormatted == 100.0 && progress != 1.0)
1145 progressFormatted = 99.9;
1147 if (td.getChildren('div').length > 0) {
1148 const div = td.getChildren('div')[0];
1149 if (td.resized) {
1150 td.resized = false;
1151 div.setWidth(ProgressColumnWidth - 5);
1153 if (div.getValue() != progressFormatted)
1154 div.setValue(progressFormatted);
1156 else {
1157 if (ProgressColumnWidth < 0)
1158 ProgressColumnWidth = td.offsetWidth;
1159 td.adopt(new window.qBittorrent.ProgressBar.ProgressBar(progressFormatted.toFloat(), {
1160 'width': ProgressColumnWidth - 5
1161 }));
1162 td.resized = false;
1166 this.columns['progress'].onResize = function(columnName) {
1167 const pos = this.getColumnPos(columnName);
1168 const trs = this.tableBody.getElements('tr');
1169 ProgressColumnWidth = -1;
1170 for (let i = 0; i < trs.length; ++i) {
1171 const td = trs[i].getElements('td')[pos];
1172 if (ProgressColumnWidth < 0)
1173 ProgressColumnWidth = td.offsetWidth;
1174 td.resized = true;
1175 this.columns[columnName].updateTd(td, this.rows.get(trs[i].rowId));
1177 }.bind(this);
1179 // num_seeds
1180 this.columns['num_seeds'].updateTd = function(td, row) {
1181 const num_seeds = this.getRowValue(row, 0);
1182 const num_complete = this.getRowValue(row, 1);
1183 let value = num_seeds;
1184 if (num_complete != -1)
1185 value += ' (' + num_complete + ')';
1186 td.set('text', value);
1187 td.set('title', value);
1189 this.columns['num_seeds'].compareRows = function(row1, row2) {
1190 const num_seeds1 = this.getRowValue(row1, 0);
1191 const num_complete1 = this.getRowValue(row1, 1);
1193 const num_seeds2 = this.getRowValue(row2, 0);
1194 const num_complete2 = this.getRowValue(row2, 1);
1196 const result = compareNumbers(num_complete1, num_complete2);
1197 if (result !== 0)
1198 return result;
1199 return compareNumbers(num_seeds1, num_seeds2);
1202 // num_leechs
1203 this.columns['num_leechs'].updateTd = this.columns['num_seeds'].updateTd;
1204 this.columns['num_leechs'].compareRows = this.columns['num_seeds'].compareRows;
1206 // dlspeed
1207 this.columns['dlspeed'].updateTd = function(td, row) {
1208 const speed = window.qBittorrent.Misc.friendlyUnit(this.getRowValue(row), true);
1209 td.set('text', speed);
1210 td.set('title', speed);
1213 // upspeed
1214 this.columns['upspeed'].updateTd = this.columns['dlspeed'].updateTd;
1216 // eta
1217 this.columns['eta'].updateTd = function(td, row) {
1218 const eta = window.qBittorrent.Misc.friendlyDuration(this.getRowValue(row), window.qBittorrent.Misc.MAX_ETA);
1219 td.set('text', eta);
1220 td.set('title', eta);
1223 // ratio
1224 this.columns['ratio'].updateTd = function(td, row) {
1225 const ratio = this.getRowValue(row);
1226 const string = (ratio === -1) ? '∞' : window.qBittorrent.Misc.toFixedPointString(ratio, 2);
1227 td.set('text', string);
1228 td.set('title', string);
1231 // added on
1232 this.columns['added_on'].updateTd = function(td, row) {
1233 const date = new Date(this.getRowValue(row) * 1000).toLocaleString();
1234 td.set('text', date);
1235 td.set('title', date);
1238 // completion_on
1239 this.columns['completion_on'].updateTd = function(td, row) {
1240 const val = this.getRowValue(row);
1241 if ((val === 0xffffffff) || (val < 0)) {
1242 td.set('text', '');
1243 td.set('title', '');
1245 else {
1246 const date = new Date(this.getRowValue(row) * 1000).toLocaleString();
1247 td.set('text', date);
1248 td.set('title', date);
1252 // dl_limit, up_limit
1253 this.columns['dl_limit'].updateTd = function(td, row) {
1254 const speed = this.getRowValue(row);
1255 if (speed === 0) {
1256 td.set('text', '∞');
1257 td.set('title', '∞');
1259 else {
1260 const formattedSpeed = window.qBittorrent.Misc.friendlyUnit(speed, true);
1261 td.set('text', formattedSpeed);
1262 td.set('title', formattedSpeed);
1266 this.columns['up_limit'].updateTd = this.columns['dl_limit'].updateTd;
1268 // downloaded, uploaded, downloaded_session, uploaded_session, amount_left
1269 this.columns['downloaded'].updateTd = this.columns['size'].updateTd;
1270 this.columns['uploaded'].updateTd = this.columns['size'].updateTd;
1271 this.columns['downloaded_session'].updateTd = this.columns['size'].updateTd;
1272 this.columns['uploaded_session'].updateTd = this.columns['size'].updateTd;
1273 this.columns['amount_left'].updateTd = this.columns['size'].updateTd;
1275 // time active
1276 this.columns['time_active'].updateTd = function(td, row) {
1277 const activeTime = this.getRowValue(row, 0);
1278 const seedingTime = this.getRowValue(row, 1);
1279 const time = (seedingTime > 0)
1280 ? ('QBT_TR(%1 (seeded for %2))QBT_TR[CONTEXT=TransferListDelegate]'
1281 .replace('%1', window.qBittorrent.Misc.friendlyDuration(activeTime))
1282 .replace('%2', window.qBittorrent.Misc.friendlyDuration(seedingTime)))
1283 : window.qBittorrent.Misc.friendlyDuration(activeTime);
1284 td.set('text', time);
1285 td.set('title', time);
1288 // completed
1289 this.columns['completed'].updateTd = this.columns['size'].updateTd;
1291 // max_ratio
1292 this.columns['max_ratio'].updateTd = this.columns['ratio'].updateTd;
1294 // seen_complete
1295 this.columns['seen_complete'].updateTd = this.columns['completion_on'].updateTd;
1297 // last_activity
1298 this.columns['last_activity'].updateTd = function(td, row) {
1299 const val = this.getRowValue(row);
1300 if (val < 1) {
1301 td.set('text', '∞');
1302 td.set('title', '∞');
1304 else {
1305 const formattedVal = 'QBT_TR(%1 ago)QBT_TR[CONTEXT=TransferListDelegate]'.replace('%1', window.qBittorrent.Misc.friendlyDuration((new Date()) / 1000 - val));
1306 td.set('text', formattedVal);
1307 td.set('title', formattedVal);
1311 // availability
1312 this.columns['availability'].updateTd = function(td, row) {
1313 const value = window.qBittorrent.Misc.toFixedPointString(this.getRowValue(row), 3);
1314 td.set('text', value);
1315 td.set('title', value);
1318 // reannounce
1319 this.columns['reannounce'].updateTd = function(td, row) {
1320 const time = window.qBittorrent.Misc.friendlyDuration(this.getRowValue(row));
1321 td.set('text', time);
1322 td.set('title', time);
1326 applyFilter: function(row, filterName, categoryHash, tagHash, trackerHash, filterTerms) {
1327 const state = row['full_data'].state;
1328 const name = row['full_data'].name.toLowerCase();
1329 let inactive = false;
1330 let r;
1332 switch (filterName) {
1333 case 'downloading':
1334 if ((state != 'downloading') && (state.indexOf('DL') === -1))
1335 return false;
1336 break;
1337 case 'seeding':
1338 if (state != 'uploading' && state != 'forcedUP' && state != 'stalledUP' && state != 'queuedUP' && state != 'checkingUP')
1339 return false;
1340 break;
1341 case 'completed':
1342 if ((state != 'uploading') && (state.indexOf('UP') === -1))
1343 return false;
1344 break;
1345 case 'stopped':
1346 if (state.indexOf('stopped') === -1)
1347 return false;
1348 break;
1349 case 'running':
1350 if (state.indexOf('stopped') > -1)
1351 return false;
1352 break;
1353 case 'stalled':
1354 if ((state != 'stalledUP') && (state != 'stalledDL'))
1355 return false;
1356 break;
1357 case 'stalled_uploading':
1358 if (state != 'stalledUP')
1359 return false;
1360 break;
1361 case 'stalled_downloading':
1362 if (state != 'stalledDL')
1363 return false;
1364 break;
1365 case 'inactive':
1366 inactive = true;
1367 // fallthrough
1368 case 'active':
1369 if (state == 'stalledDL')
1370 r = (row['full_data'].upspeed > 0);
1371 else
1372 r = state == 'metaDL' || state == 'forcedMetaDL' || state == 'downloading' || state == 'forcedDL' || state == 'uploading' || state == 'forcedUP';
1373 if (r == inactive)
1374 return false;
1375 break;
1376 case 'checking':
1377 if (state !== 'checkingUP' && state !== 'checkingDL' && state !== 'checkingResumeData')
1378 return false;
1379 break;
1380 case 'moving':
1381 if (state !== 'moving')
1382 return false;
1383 break;
1384 case 'errored':
1385 if (state != 'error' && state != "unknown" && state != "missingFiles")
1386 return false;
1387 break;
1390 switch (categoryHash) {
1391 case CATEGORIES_ALL:
1392 break; // do nothing
1393 case CATEGORIES_UNCATEGORIZED:
1394 if (row['full_data'].category.length !== 0)
1395 return false;
1396 break; // do nothing
1397 default:
1398 if (!useSubcategories) {
1399 if (categoryHash !== window.qBittorrent.Client.genHash(row['full_data'].category))
1400 return false;
1402 else {
1403 const selectedCategory = category_list.get(categoryHash);
1404 if (selectedCategory !== undefined) {
1405 const selectedCategoryName = selectedCategory.name + "/";
1406 const torrentCategoryName = row['full_data'].category + "/";
1407 if (!torrentCategoryName.startsWith(selectedCategoryName))
1408 return false;
1411 break;
1414 switch (tagHash) {
1415 case TAGS_ALL:
1416 break; // do nothing
1418 case TAGS_UNTAGGED:
1419 if (row['full_data'].tags.length !== 0)
1420 return false;
1421 break; // do nothing
1423 default: {
1424 const tagHashes = row['full_data'].tags.split(', ').map(tag => window.qBittorrent.Client.genHash(tag));
1425 if (!tagHashes.contains(tagHash))
1426 return false;
1427 break;
1431 const trackerHashInt = Number.parseInt(trackerHash, 10);
1432 switch (trackerHashInt) {
1433 case TRACKERS_ALL:
1434 break; // do nothing
1435 case TRACKERS_TRACKERLESS:
1436 if (row['full_data'].trackers_count !== 0)
1437 return false;
1438 break;
1439 default: {
1440 const tracker = trackerList.get(trackerHashInt);
1441 if (tracker) {
1442 let found = false;
1443 for (const torrents of tracker.trackerTorrentMap.values()) {
1444 if (torrents.includes(row['full_data'].rowId)) {
1445 found = true;
1446 break;
1449 if (!found)
1450 return false;
1452 break;
1456 if ((filterTerms !== undefined) && (filterTerms !== null)) {
1457 if (filterTerms instanceof RegExp) {
1458 if (!filterTerms.test(name))
1459 return false;
1461 else {
1462 if ((filterTerms.length > 0) && !window.qBittorrent.Misc.containsAllTerms(name, filterTerms))
1463 return false;
1467 return true;
1470 getFilteredTorrentsNumber: function(filterName, categoryHash, tagHash, trackerHash) {
1471 let cnt = 0;
1472 const rows = this.rows.getValues();
1474 for (let i = 0; i < rows.length; ++i) {
1475 if (this.applyFilter(rows[i], filterName, categoryHash, tagHash, trackerHash, null))
1476 ++cnt;
1478 return cnt;
1481 getFilteredTorrentsHashes: function(filterName, categoryHash, tagHash, trackerHash) {
1482 const rowsHashes = [];
1483 const rows = this.rows.getValues();
1485 for (let i = 0; i < rows.length; ++i) {
1486 if (this.applyFilter(rows[i], filterName, categoryHash, tagHash, trackerHash, null))
1487 rowsHashes.push(rows[i]['rowId']);
1490 return rowsHashes;
1493 getFilteredAndSortedRows: function() {
1494 const filteredRows = [];
1496 const rows = this.rows.getValues();
1497 const useRegex = $('torrentsFilterRegexBox').checked;
1498 const filterText = $('torrentsFilterInput').value.trim().toLowerCase();
1499 const filterTerms = (filterText.length > 0)
1500 ? (useRegex ? new RegExp(filterText) : filterText.split(" "))
1501 : null;
1503 for (let i = 0; i < rows.length; ++i) {
1504 if (this.applyFilter(rows[i], selected_filter, selected_category, selectedTag, selectedTracker, filterTerms)) {
1505 filteredRows.push(rows[i]);
1506 filteredRows[rows[i].rowId] = rows[i];
1510 filteredRows.sort(function(row1, row2) {
1511 const column = this.columns[this.sortedColumn];
1512 const res = column.compareRows(row1, row2);
1513 if (this.reverseSort === '0')
1514 return res;
1515 else
1516 return -res;
1517 }.bind(this));
1518 return filteredRows;
1521 setupTr: function(tr) {
1522 tr.addEvent('dblclick', function(e) {
1523 e.stop();
1524 this._this.deselectAll();
1525 this._this.selectRow(this.rowId);
1526 const row = this._this.rows.get(this.rowId);
1527 const state = row['full_data'].state;
1528 if (state.indexOf('stopped') > -1)
1529 startFN();
1530 else
1531 stopFN();
1532 return true;
1534 tr.addClass("torrentsTableContextMenuTarget");
1537 getCurrentTorrentID: function() {
1538 return this.getSelectedRowId();
1541 onSelectedRowChanged: function() {
1542 updatePropertiesPanel();
1546 const TorrentPeersTable = new Class({
1547 Extends: DynamicTable,
1549 initColumns: function() {
1550 this.newColumn('country', '', 'QBT_TR(Country/Region)QBT_TR[CONTEXT=PeerListWidget]', 22, true);
1551 this.newColumn('ip', '', 'QBT_TR(IP)QBT_TR[CONTEXT=PeerListWidget]', 80, true);
1552 this.newColumn('port', '', 'QBT_TR(Port)QBT_TR[CONTEXT=PeerListWidget]', 35, true);
1553 this.newColumn('connection', '', 'QBT_TR(Connection)QBT_TR[CONTEXT=PeerListWidget]', 50, true);
1554 this.newColumn('flags', '', 'QBT_TR(Flags)QBT_TR[CONTEXT=PeerListWidget]', 50, true);
1555 this.newColumn('client', '', 'QBT_TR(Client)QBT_TR[CONTEXT=PeerListWidget]', 140, true);
1556 this.newColumn('peer_id_client', '', 'QBT_TR(Peer ID Client)QBT_TR[CONTEXT=PeerListWidget]', 60, false);
1557 this.newColumn('progress', '', 'QBT_TR(Progress)QBT_TR[CONTEXT=PeerListWidget]', 50, true);
1558 this.newColumn('dl_speed', '', 'QBT_TR(Down Speed)QBT_TR[CONTEXT=PeerListWidget]', 50, true);
1559 this.newColumn('up_speed', '', 'QBT_TR(Up Speed)QBT_TR[CONTEXT=PeerListWidget]', 50, true);
1560 this.newColumn('downloaded', '', 'QBT_TR(Downloaded)QBT_TR[CONTEXT=PeerListWidget]', 50, true);
1561 this.newColumn('uploaded', '', 'QBT_TR(Uploaded)QBT_TR[CONTEXT=PeerListWidget]', 50, true);
1562 this.newColumn('relevance', '', 'QBT_TR(Relevance)QBT_TR[CONTEXT=PeerListWidget]', 30, true);
1563 this.newColumn('files', '', 'QBT_TR(Files)QBT_TR[CONTEXT=PeerListWidget]', 100, true);
1565 this.columns['country'].dataProperties.push('country_code');
1566 this.columns['flags'].dataProperties.push('flags_desc');
1567 this.initColumnsFunctions();
1570 initColumnsFunctions: function() {
1572 // country
1573 this.columns['country'].updateTd = function(td, row) {
1574 const country = this.getRowValue(row, 0);
1575 const country_code = this.getRowValue(row, 1);
1577 if (!country_code) {
1578 if (td.getChildren('img').length > 0)
1579 td.getChildren('img')[0].destroy();
1580 return;
1583 const img_path = 'images/flags/' + country_code + '.svg';
1585 if (td.getChildren('img').length > 0) {
1586 const img = td.getChildren('img')[0];
1587 img.set('src', img_path);
1588 img.set('class', 'flags');
1589 img.set('alt', country);
1590 img.set('title', country);
1592 else
1593 td.adopt(new Element('img', {
1594 'src': img_path,
1595 'class': 'flags',
1596 'alt': country,
1597 'title': country
1598 }));
1601 // ip
1602 this.columns['ip'].compareRows = function(row1, row2) {
1603 const ip1 = this.getRowValue(row1);
1604 const ip2 = this.getRowValue(row2);
1606 const a = ip1.split(".");
1607 const b = ip2.split(".");
1609 for (let i = 0; i < 4; ++i) {
1610 if (a[i] != b[i])
1611 return a[i] - b[i];
1614 return 0;
1617 // flags
1618 this.columns['flags'].updateTd = function(td, row) {
1619 td.set('text', this.getRowValue(row, 0));
1620 td.set('title', this.getRowValue(row, 1));
1623 // progress
1624 this.columns['progress'].updateTd = function(td, row) {
1625 const progress = this.getRowValue(row);
1626 let progressFormatted = (progress * 100).round(1);
1627 if (progressFormatted == 100.0 && progress != 1.0)
1628 progressFormatted = 99.9;
1629 progressFormatted += "%";
1630 td.set('text', progressFormatted);
1631 td.set('title', progressFormatted);
1634 // dl_speed, up_speed
1635 this.columns['dl_speed'].updateTd = function(td, row) {
1636 const speed = this.getRowValue(row);
1637 if (speed === 0) {
1638 td.set('text', '');
1639 td.set('title', '');
1641 else {
1642 const formattedSpeed = window.qBittorrent.Misc.friendlyUnit(speed, true);
1643 td.set('text', formattedSpeed);
1644 td.set('title', formattedSpeed);
1647 this.columns['up_speed'].updateTd = this.columns['dl_speed'].updateTd;
1649 // downloaded, uploaded
1650 this.columns['downloaded'].updateTd = function(td, row) {
1651 const downloaded = window.qBittorrent.Misc.friendlyUnit(this.getRowValue(row), false);
1652 td.set('text', downloaded);
1653 td.set('title', downloaded);
1655 this.columns['uploaded'].updateTd = this.columns['downloaded'].updateTd;
1657 // relevance
1658 this.columns['relevance'].updateTd = this.columns['progress'].updateTd;
1660 // files
1661 this.columns['files'].updateTd = function(td, row) {
1662 const value = this.getRowValue(row, 0);
1663 td.set('text', value.replace(/\n/g, ';'));
1664 td.set('title', value);
1670 const SearchResultsTable = new Class({
1671 Extends: DynamicTable,
1673 initColumns: function() {
1674 this.newColumn('fileName', '', 'QBT_TR(Name)QBT_TR[CONTEXT=SearchResultsTable]', 500, true);
1675 this.newColumn('fileSize', '', 'QBT_TR(Size)QBT_TR[CONTEXT=SearchResultsTable]', 100, true);
1676 this.newColumn('nbSeeders', '', 'QBT_TR(Seeders)QBT_TR[CONTEXT=SearchResultsTable]', 100, true);
1677 this.newColumn('nbLeechers', '', 'QBT_TR(Leechers)QBT_TR[CONTEXT=SearchResultsTable]', 100, true);
1678 this.newColumn('siteUrl', '', 'QBT_TR(Search engine)QBT_TR[CONTEXT=SearchResultsTable]', 250, true);
1680 this.initColumnsFunctions();
1683 initColumnsFunctions: function() {
1684 const displaySize = function(td, row) {
1685 const size = window.qBittorrent.Misc.friendlyUnit(this.getRowValue(row), false);
1686 td.set('text', size);
1687 td.set('title', size);
1689 const displayNum = function(td, row) {
1690 const value = this.getRowValue(row);
1691 const formattedValue = (value === "-1") ? "Unknown" : value;
1692 td.set('text', formattedValue);
1693 td.set('title', formattedValue);
1696 this.columns['fileSize'].updateTd = displaySize;
1697 this.columns['nbSeeders'].updateTd = displayNum;
1698 this.columns['nbLeechers'].updateTd = displayNum;
1701 getFilteredAndSortedRows: function() {
1702 const getSizeFilters = function() {
1703 let minSize = (window.qBittorrent.Search.searchSizeFilter.min > 0.00) ? (window.qBittorrent.Search.searchSizeFilter.min * Math.pow(1024, window.qBittorrent.Search.searchSizeFilter.minUnit)) : 0.00;
1704 let maxSize = (window.qBittorrent.Search.searchSizeFilter.max > 0.00) ? (window.qBittorrent.Search.searchSizeFilter.max * Math.pow(1024, window.qBittorrent.Search.searchSizeFilter.maxUnit)) : 0.00;
1706 if ((minSize > maxSize) && (maxSize > 0.00)) {
1707 const tmp = minSize;
1708 minSize = maxSize;
1709 maxSize = tmp;
1712 return {
1713 min: minSize,
1714 max: maxSize
1718 const getSeedsFilters = function() {
1719 let minSeeds = (window.qBittorrent.Search.searchSeedsFilter.min > 0) ? window.qBittorrent.Search.searchSeedsFilter.min : 0;
1720 let maxSeeds = (window.qBittorrent.Search.searchSeedsFilter.max > 0) ? window.qBittorrent.Search.searchSeedsFilter.max : 0;
1722 if ((minSeeds > maxSeeds) && (maxSeeds > 0)) {
1723 const tmp = minSeeds;
1724 minSeeds = maxSeeds;
1725 maxSeeds = tmp;
1728 return {
1729 min: minSeeds,
1730 max: maxSeeds
1734 let filteredRows = [];
1735 const rows = this.rows.getValues();
1736 const searchTerms = window.qBittorrent.Search.searchText.pattern.toLowerCase().split(" ");
1737 const filterTerms = window.qBittorrent.Search.searchText.filterPattern.toLowerCase().split(" ");
1738 const sizeFilters = getSizeFilters();
1739 const seedsFilters = getSeedsFilters();
1740 const searchInTorrentName = $('searchInTorrentName').get('value') === "names";
1742 if (searchInTorrentName || (filterTerms.length > 0) || (window.qBittorrent.Search.searchSizeFilter.min > 0.00) || (window.qBittorrent.Search.searchSizeFilter.max > 0.00)) {
1743 for (let i = 0; i < rows.length; ++i) {
1744 const row = rows[i];
1746 if (searchInTorrentName && !window.qBittorrent.Misc.containsAllTerms(row.full_data.fileName, searchTerms))
1747 continue;
1748 if ((filterTerms.length > 0) && !window.qBittorrent.Misc.containsAllTerms(row.full_data.fileName, filterTerms))
1749 continue;
1750 if ((sizeFilters.min > 0.00) && (row.full_data.fileSize < sizeFilters.min))
1751 continue;
1752 if ((sizeFilters.max > 0.00) && (row.full_data.fileSize > sizeFilters.max))
1753 continue;
1754 if ((seedsFilters.min > 0) && (row.full_data.nbSeeders < seedsFilters.min))
1755 continue;
1756 if ((seedsFilters.max > 0) && (row.full_data.nbSeeders > seedsFilters.max))
1757 continue;
1759 filteredRows.push(row);
1762 else {
1763 filteredRows = rows;
1766 filteredRows.sort(function(row1, row2) {
1767 const column = this.columns[this.sortedColumn];
1768 const res = column.compareRows(row1, row2);
1769 if (this.reverseSort === '0')
1770 return res;
1771 else
1772 return -res;
1773 }.bind(this));
1775 return filteredRows;
1778 setupTr: function(tr) {
1779 tr.addClass("searchTableRow");
1783 const SearchPluginsTable = new Class({
1784 Extends: DynamicTable,
1786 initColumns: function() {
1787 this.newColumn('fullName', '', 'QBT_TR(Name)QBT_TR[CONTEXT=SearchPluginsTable]', 175, true);
1788 this.newColumn('version', '', 'QBT_TR(Version)QBT_TR[CONTEXT=SearchPluginsTable]', 100, true);
1789 this.newColumn('url', '', 'QBT_TR(Url)QBT_TR[CONTEXT=SearchPluginsTable]', 175, true);
1790 this.newColumn('enabled', '', 'QBT_TR(Enabled)QBT_TR[CONTEXT=SearchPluginsTable]', 100, true);
1792 this.initColumnsFunctions();
1795 initColumnsFunctions: function() {
1796 this.columns['enabled'].updateTd = function(td, row) {
1797 const value = this.getRowValue(row);
1798 if (value) {
1799 td.set('text', 'QBT_TR(Yes)QBT_TR[CONTEXT=SearchPluginsTable]');
1800 td.set('title', 'QBT_TR(Yes)QBT_TR[CONTEXT=SearchPluginsTable]');
1801 td.getParent("tr").addClass("green");
1802 td.getParent("tr").removeClass("red");
1804 else {
1805 td.set('text', 'QBT_TR(No)QBT_TR[CONTEXT=SearchPluginsTable]');
1806 td.set('title', 'QBT_TR(No)QBT_TR[CONTEXT=SearchPluginsTable]');
1807 td.getParent("tr").addClass("red");
1808 td.getParent("tr").removeClass("green");
1813 setupTr: function(tr) {
1814 tr.addClass("searchPluginsTableRow");
1818 const TorrentTrackersTable = new Class({
1819 Extends: DynamicTable,
1821 initColumns: function() {
1822 this.newColumn('tier', '', 'QBT_TR(Tier)QBT_TR[CONTEXT=TrackerListWidget]', 35, true);
1823 this.newColumn('url', '', 'QBT_TR(URL)QBT_TR[CONTEXT=TrackerListWidget]', 250, true);
1824 this.newColumn('status', '', 'QBT_TR(Status)QBT_TR[CONTEXT=TrackerListWidget]', 125, true);
1825 this.newColumn('peers', '', 'QBT_TR(Peers)QBT_TR[CONTEXT=TrackerListWidget]', 75, true);
1826 this.newColumn('seeds', '', 'QBT_TR(Seeds)QBT_TR[CONTEXT=TrackerListWidget]', 75, true);
1827 this.newColumn('leeches', '', 'QBT_TR(Leeches)QBT_TR[CONTEXT=TrackerListWidget]', 75, true);
1828 this.newColumn('downloaded', '', 'QBT_TR(Times Downloaded)QBT_TR[CONTEXT=TrackerListWidget]', 100, true);
1829 this.newColumn('message', '', 'QBT_TR(Message)QBT_TR[CONTEXT=TrackerListWidget]', 250, true);
1833 const BulkRenameTorrentFilesTable = new Class({
1834 Extends: DynamicTable,
1836 filterTerms: [],
1837 prevFilterTerms: [],
1838 prevRowsString: null,
1839 prevFilteredRows: [],
1840 prevSortedColumn: null,
1841 prevReverseSort: null,
1842 fileTree: new window.qBittorrent.FileTree.FileTree(),
1844 populateTable: function(root) {
1845 this.fileTree.setRoot(root);
1846 root.children.each(function(node) {
1847 this._addNodeToTable(node, 0);
1848 }.bind(this));
1851 _addNodeToTable: function(node, depth) {
1852 node.depth = depth;
1854 if (node.isFolder) {
1855 const data = {
1856 rowId: node.rowId,
1857 fileId: -1,
1858 checked: node.checked,
1859 path: node.path,
1860 original: node.original,
1861 renamed: node.renamed
1864 node.data = data;
1865 node.full_data = data;
1866 this.updateRowData(data);
1868 else {
1869 node.data.rowId = node.rowId;
1870 node.full_data = node.data;
1871 this.updateRowData(node.data);
1874 node.children.each(function(child) {
1875 this._addNodeToTable(child, depth + 1);
1876 }.bind(this));
1879 getRoot: function() {
1880 return this.fileTree.getRoot();
1883 getNode: function(rowId) {
1884 return this.fileTree.getNode(rowId);
1887 getRow: function(node) {
1888 const rowId = this.fileTree.getRowId(node);
1889 return this.rows.get(rowId);
1892 getSelectedRows: function() {
1893 const nodes = this.fileTree.toArray();
1895 return nodes.filter(x => x.checked == 0);
1898 initColumns: function() {
1899 // Blocks saving header width (because window width isn't saved)
1900 LocalPreferences.remove('column_' + "checked" + '_width_' + this.dynamicTableDivId);
1901 LocalPreferences.remove('column_' + "original" + '_width_' + this.dynamicTableDivId);
1902 LocalPreferences.remove('column_' + "renamed" + '_width_' + this.dynamicTableDivId);
1903 this.newColumn('checked', '', '', 50, true);
1904 this.newColumn('original', '', 'QBT_TR(Original)QBT_TR[CONTEXT=TrackerListWidget]', 270, true);
1905 this.newColumn('renamed', '', 'QBT_TR(Renamed)QBT_TR[CONTEXT=TrackerListWidget]', 220, true);
1907 this.initColumnsFunctions();
1911 * Toggles the global checkbox and all checkboxes underneath
1913 toggleGlobalCheckbox: function() {
1914 const checkbox = $('rootMultiRename_cb');
1915 const checkboxes = $$('input.RenamingCB');
1917 for (let i = 0; i < checkboxes.length; ++i) {
1918 const node = this.getNode(i);
1920 if (checkbox.checked || checkbox.indeterminate) {
1921 let cb = checkboxes[i];
1922 cb.checked = true;
1923 cb.indeterminate = false;
1924 cb.state = "checked";
1925 node.checked = 0;
1926 node.full_data.checked = node.checked;
1928 else {
1929 let cb = checkboxes[i];
1930 cb.checked = false;
1931 cb.indeterminate = false;
1932 cb.state = "unchecked";
1933 node.checked = 1;
1934 node.full_data.checked = node.checked;
1938 this.updateGlobalCheckbox();
1941 toggleNodeTreeCheckbox: function(rowId, checkState) {
1942 const node = this.getNode(rowId);
1943 node.checked = checkState;
1944 node.full_data.checked = checkState;
1945 const checkbox = $(`cbRename${rowId}`);
1946 checkbox.checked = node.checked == 0;
1947 checkbox.state = checkbox.checked ? "checked" : "unchecked";
1949 for (let i = 0; i < node.children.length; ++i) {
1950 this.toggleNodeTreeCheckbox(node.children[i].rowId, checkState);
1954 updateGlobalCheckbox: function() {
1955 const checkbox = $('rootMultiRename_cb');
1956 const checkboxes = $$('input.RenamingCB');
1957 const isAllChecked = function() {
1958 for (let i = 0; i < checkboxes.length; ++i) {
1959 if (!checkboxes[i].checked)
1960 return false;
1962 return true;
1964 const isAllUnchecked = function() {
1965 for (let i = 0; i < checkboxes.length; ++i) {
1966 if (checkboxes[i].checked)
1967 return false;
1969 return true;
1971 if (isAllChecked()) {
1972 checkbox.state = "checked";
1973 checkbox.indeterminate = false;
1974 checkbox.checked = true;
1976 else if (isAllUnchecked()) {
1977 checkbox.state = "unchecked";
1978 checkbox.indeterminate = false;
1979 checkbox.checked = false;
1981 else {
1982 checkbox.state = "partial";
1983 checkbox.indeterminate = true;
1984 checkbox.checked = false;
1988 initColumnsFunctions: function() {
1989 const that = this;
1991 // checked
1992 this.columns['checked'].updateTd = function(td, row) {
1993 const id = row.rowId;
1994 const value = this.getRowValue(row);
1996 const treeImg = new Element('img', {
1997 src: 'images/L.gif',
1998 styles: {
1999 'margin-bottom': -2
2002 const checkbox = new Element('input');
2003 checkbox.set('type', 'checkbox');
2004 checkbox.set('id', 'cbRename' + id);
2005 checkbox.set('data-id', id);
2006 checkbox.set('class', 'RenamingCB');
2007 checkbox.addEvent('click', function(e) {
2008 const node = that.getNode(id);
2009 node.checked = e.target.checked ? 0 : 1;
2010 node.full_data.checked = node.checked;
2011 that.updateGlobalCheckbox();
2012 that.onRowSelectionChange(node);
2013 e.stopPropagation();
2015 checkbox.checked = value == 0;
2016 checkbox.state = checkbox.checked ? "checked" : "unchecked";
2017 checkbox.indeterminate = false;
2018 td.adopt(treeImg, checkbox);
2021 // original
2022 this.columns['original'].updateTd = function(td, row) {
2023 const id = row.rowId;
2024 const fileNameId = 'filesTablefileName' + id;
2025 const node = that.getNode(id);
2027 if (node.isFolder) {
2028 const value = this.getRowValue(row);
2029 const dirImgId = 'renameTableDirImg' + id;
2030 if ($(dirImgId)) {
2031 // just update file name
2032 $(fileNameId).set('text', value);
2034 else {
2035 const span = new Element('span', {
2036 text: value,
2037 id: fileNameId
2039 const dirImg = new Element('img', {
2040 src: 'images/directory.svg',
2041 styles: {
2042 'width': 15,
2043 'padding-right': 5,
2044 'margin-bottom': -3,
2045 'margin-left': (node.depth * 20)
2047 id: dirImgId
2049 const html = dirImg.outerHTML + span.outerHTML;
2050 td.set('html', html);
2053 else { // is file
2054 const value = this.getRowValue(row);
2055 const span = new Element('span', {
2056 text: value,
2057 id: fileNameId,
2058 styles: {
2059 'margin-left': ((node.depth + 1) * 20)
2062 td.set('html', span.outerHTML);
2066 // renamed
2067 this.columns['renamed'].updateTd = function(td, row) {
2068 const id = row.rowId;
2069 const fileNameRenamedId = 'filesTablefileRenamed' + id;
2070 const value = this.getRowValue(row);
2072 const span = new Element('span', {
2073 text: value,
2074 id: fileNameRenamedId,
2076 td.set('html', span.outerHTML);
2080 onRowSelectionChange: function(row) {},
2082 selectRow: function() {
2083 return;
2086 reselectRows: function(rowIds) {
2087 const that = this;
2088 this.deselectAll();
2089 this.tableBody.getElements('tr').each(function(tr) {
2090 if (rowIds.indexOf(tr.rowId) > -1) {
2091 const node = that.getNode(tr.rowId);
2092 node.checked = 0;
2093 node.full_data.checked = 0;
2095 const checkbox = tr.children[0].getElement('input');
2096 checkbox.state = "checked";
2097 checkbox.indeterminate = false;
2098 checkbox.checked = true;
2102 this.updateGlobalCheckbox();
2105 altRow: function() {
2106 let addClass = false;
2107 const trs = this.tableBody.getElements('tr');
2108 trs.each(function(tr) {
2109 if (tr.hasClass("invisible"))
2110 return;
2112 if (addClass) {
2113 tr.addClass("alt");
2114 tr.removeClass("nonAlt");
2116 else {
2117 tr.removeClass("alt");
2118 tr.addClass("nonAlt");
2120 addClass = !addClass;
2121 }.bind(this));
2124 _sortNodesByColumn: function(nodes, column) {
2125 nodes.sort(function(row1, row2) {
2126 // list folders before files when sorting by name
2127 if (column.name === "original") {
2128 const node1 = this.getNode(row1.data.rowId);
2129 const node2 = this.getNode(row2.data.rowId);
2130 if (node1.isFolder && !node2.isFolder)
2131 return -1;
2132 if (node2.isFolder && !node1.isFolder)
2133 return 1;
2136 const res = column.compareRows(row1, row2);
2137 return (this.reverseSort === '0') ? res : -res;
2138 }.bind(this));
2140 nodes.each(function(node) {
2141 if (node.children.length > 0)
2142 this._sortNodesByColumn(node.children, column);
2143 }.bind(this));
2146 _filterNodes: function(node, filterTerms, filteredRows) {
2147 if (node.isFolder) {
2148 const childAdded = node.children.reduce(function(acc, child) {
2149 // we must execute the function before ORing w/ acc or we'll stop checking child nodes after the first successful match
2150 return (this._filterNodes(child, filterTerms, filteredRows) || acc);
2151 }.bind(this), false);
2153 if (childAdded) {
2154 const row = this.getRow(node);
2155 filteredRows.push(row);
2156 return true;
2160 if (window.qBittorrent.Misc.containsAllTerms(node.original, filterTerms)) {
2161 const row = this.getRow(node);
2162 filteredRows.push(row);
2163 return true;
2166 return false;
2169 setFilter: function(text) {
2170 const filterTerms = text.trim().toLowerCase().split(' ');
2171 if ((filterTerms.length === 1) && (filterTerms[0] === ''))
2172 this.filterTerms = [];
2173 else
2174 this.filterTerms = filterTerms;
2177 getFilteredAndSortedRows: function() {
2178 if (this.getRoot() === null)
2179 return [];
2181 const generateRowsSignature = function(rows) {
2182 const rowsData = rows.map(function(row) {
2183 return row.full_data;
2185 return JSON.stringify(rowsData);
2188 const getFilteredRows = function() {
2189 if (this.filterTerms.length === 0) {
2190 const nodeArray = this.fileTree.toArray();
2191 const filteredRows = nodeArray.map(function(node) {
2192 return this.getRow(node);
2193 }.bind(this));
2194 return filteredRows;
2197 const filteredRows = [];
2198 this.getRoot().children.each(function(child) {
2199 this._filterNodes(child, this.filterTerms, filteredRows);
2200 }.bind(this));
2201 filteredRows.reverse();
2202 return filteredRows;
2203 }.bind(this);
2205 const hasRowsChanged = function(rowsString, prevRowsStringString) {
2206 const rowsChanged = (rowsString !== prevRowsStringString);
2207 const isFilterTermsChanged = this.filterTerms.reduce(function(acc, term, index) {
2208 return (acc || (term !== this.prevFilterTerms[index]));
2209 }.bind(this), false);
2210 const isFilterChanged = ((this.filterTerms.length !== this.prevFilterTerms.length)
2211 || ((this.filterTerms.length > 0) && isFilterTermsChanged));
2212 const isSortedColumnChanged = (this.prevSortedColumn !== this.sortedColumn);
2213 const isReverseSortChanged = (this.prevReverseSort !== this.reverseSort);
2215 return (rowsChanged || isFilterChanged || isSortedColumnChanged || isReverseSortChanged);
2216 }.bind(this);
2218 const rowsString = generateRowsSignature(this.rows);
2219 if (!hasRowsChanged(rowsString, this.prevRowsString)) {
2220 return this.prevFilteredRows;
2223 // sort, then filter
2224 const column = this.columns[this.sortedColumn];
2225 this._sortNodesByColumn(this.getRoot().children, column);
2226 const filteredRows = getFilteredRows();
2228 this.prevFilterTerms = this.filterTerms;
2229 this.prevRowsString = rowsString;
2230 this.prevFilteredRows = filteredRows;
2231 this.prevSortedColumn = this.sortedColumn;
2232 this.prevReverseSort = this.reverseSort;
2233 return filteredRows;
2236 setIgnored: function(rowId, ignore) {
2237 const row = this.rows.get(rowId);
2238 if (ignore)
2239 row.full_data.remaining = 0;
2240 else
2241 row.full_data.remaining = (row.full_data.size * (1.0 - (row.full_data.progress / 100)));
2244 setupTr: function(tr) {
2245 tr.addEvent('keydown', function(event) {
2246 switch (event.key) {
2247 case "left":
2248 qBittorrent.PropFiles.collapseFolder(this._this.getSelectedRowId());
2249 return false;
2250 case "right":
2251 qBittorrent.PropFiles.expandFolder(this._this.getSelectedRowId());
2252 return false;
2258 const TorrentFilesTable = new Class({
2259 Extends: DynamicTable,
2261 filterTerms: [],
2262 prevFilterTerms: [],
2263 prevRowsString: null,
2264 prevFilteredRows: [],
2265 prevSortedColumn: null,
2266 prevReverseSort: null,
2267 fileTree: new window.qBittorrent.FileTree.FileTree(),
2269 populateTable: function(root) {
2270 this.fileTree.setRoot(root);
2271 root.children.each(function(node) {
2272 this._addNodeToTable(node, 0);
2273 }.bind(this));
2276 _addNodeToTable: function(node, depth) {
2277 node.depth = depth;
2279 if (node.isFolder) {
2280 const data = {
2281 rowId: node.rowId,
2282 size: node.size,
2283 checked: node.checked,
2284 remaining: node.remaining,
2285 progress: node.progress,
2286 priority: window.qBittorrent.PropFiles.normalizePriority(node.priority),
2287 availability: node.availability,
2288 fileId: -1,
2289 name: node.name
2292 node.data = data;
2293 node.full_data = data;
2294 this.updateRowData(data);
2296 else {
2297 node.data.rowId = node.rowId;
2298 node.full_data = node.data;
2299 this.updateRowData(node.data);
2302 node.children.each(function(child) {
2303 this._addNodeToTable(child, depth + 1);
2304 }.bind(this));
2307 getRoot: function() {
2308 return this.fileTree.getRoot();
2311 getNode: function(rowId) {
2312 return this.fileTree.getNode(rowId);
2315 getRow: function(node) {
2316 const rowId = this.fileTree.getRowId(node);
2317 return this.rows.get(rowId);
2320 initColumns: function() {
2321 this.newColumn('checked', '', '', 50, true);
2322 this.newColumn('name', '', 'QBT_TR(Name)QBT_TR[CONTEXT=TrackerListWidget]', 300, true);
2323 this.newColumn('size', '', 'QBT_TR(Total Size)QBT_TR[CONTEXT=TrackerListWidget]', 75, true);
2324 this.newColumn('progress', '', 'QBT_TR(Progress)QBT_TR[CONTEXT=TrackerListWidget]', 100, true);
2325 this.newColumn('priority', '', 'QBT_TR(Download Priority)QBT_TR[CONTEXT=TrackerListWidget]', 150, true);
2326 this.newColumn('remaining', '', 'QBT_TR(Remaining)QBT_TR[CONTEXT=TrackerListWidget]', 75, true);
2327 this.newColumn('availability', '', 'QBT_TR(Availability)QBT_TR[CONTEXT=TrackerListWidget]', 75, true);
2329 this.initColumnsFunctions();
2332 initColumnsFunctions: function() {
2333 const that = this;
2334 const displaySize = function(td, row) {
2335 const size = window.qBittorrent.Misc.friendlyUnit(this.getRowValue(row), false);
2336 td.set('text', size);
2337 td.set('title', size);
2339 const displayPercentage = function(td, row) {
2340 const value = window.qBittorrent.Misc.friendlyPercentage(this.getRowValue(row));
2341 td.set('text', value);
2342 td.set('title', value);
2345 // checked
2346 this.columns['checked'].updateTd = function(td, row) {
2347 const id = row.rowId;
2348 const value = this.getRowValue(row);
2350 if (window.qBittorrent.PropFiles.isDownloadCheckboxExists(id)) {
2351 window.qBittorrent.PropFiles.updateDownloadCheckbox(id, value);
2353 else {
2354 const treeImg = new Element('img', {
2355 src: 'images/L.gif',
2356 styles: {
2357 'margin-bottom': -2
2360 td.adopt(treeImg, window.qBittorrent.PropFiles.createDownloadCheckbox(id, row.full_data.fileId, value));
2364 // name
2365 this.columns['name'].updateTd = function(td, row) {
2366 const id = row.rowId;
2367 const fileNameId = 'filesTablefileName' + id;
2368 const node = that.getNode(id);
2370 if (node.isFolder) {
2371 const value = this.getRowValue(row);
2372 const collapseIconId = 'filesTableCollapseIcon' + id;
2373 const dirImgId = 'filesTableDirImg' + id;
2374 if ($(dirImgId)) {
2375 // just update file name
2376 $(fileNameId).set('text', value);
2378 else {
2379 const collapseIcon = new Element('img', {
2380 src: 'images/go-down.svg',
2381 styles: {
2382 'margin-left': (node.depth * 20)
2384 class: "filesTableCollapseIcon",
2385 id: collapseIconId,
2386 "data-id": id,
2387 onclick: "qBittorrent.PropFiles.collapseIconClicked(this)"
2389 const span = new Element('span', {
2390 text: value,
2391 id: fileNameId
2393 const dirImg = new Element('img', {
2394 src: 'images/directory.svg',
2395 styles: {
2396 'width': 15,
2397 'padding-right': 5,
2398 'margin-bottom': -3
2400 id: dirImgId
2402 const html = collapseIcon.outerHTML + dirImg.outerHTML + span.outerHTML;
2403 td.set('html', html);
2406 else {
2407 const value = this.getRowValue(row);
2408 const span = new Element('span', {
2409 text: value,
2410 id: fileNameId,
2411 styles: {
2412 'margin-left': ((node.depth + 1) * 20)
2415 td.set('html', span.outerHTML);
2419 // size
2420 this.columns['size'].updateTd = displaySize;
2422 // progress
2423 this.columns['progress'].updateTd = function(td, row) {
2424 const id = row.rowId;
2425 const value = this.getRowValue(row);
2427 const progressBar = $('pbf_' + id);
2428 if (progressBar === null) {
2429 td.adopt(new window.qBittorrent.ProgressBar.ProgressBar(value.toFloat(), {
2430 id: 'pbf_' + id,
2431 width: 80
2432 }));
2434 else {
2435 progressBar.setValue(value.toFloat());
2439 // priority
2440 this.columns['priority'].updateTd = function(td, row) {
2441 const id = row.rowId;
2442 const value = this.getRowValue(row);
2444 if (window.qBittorrent.PropFiles.isPriorityComboExists(id))
2445 window.qBittorrent.PropFiles.updatePriorityCombo(id, value);
2446 else
2447 td.adopt(window.qBittorrent.PropFiles.createPriorityCombo(id, row.full_data.fileId, value));
2450 // remaining, availability
2451 this.columns['remaining'].updateTd = displaySize;
2452 this.columns['availability'].updateTd = displayPercentage;
2455 altRow: function() {
2456 let addClass = false;
2457 const trs = this.tableBody.getElements('tr');
2458 trs.each(function(tr) {
2459 if (tr.hasClass("invisible"))
2460 return;
2462 if (addClass) {
2463 tr.addClass("alt");
2464 tr.removeClass("nonAlt");
2466 else {
2467 tr.removeClass("alt");
2468 tr.addClass("nonAlt");
2470 addClass = !addClass;
2471 }.bind(this));
2474 _sortNodesByColumn: function(nodes, column) {
2475 nodes.sort(function(row1, row2) {
2476 // list folders before files when sorting by name
2477 if (column.name === "name") {
2478 const node1 = this.getNode(row1.data.rowId);
2479 const node2 = this.getNode(row2.data.rowId);
2480 if (node1.isFolder && !node2.isFolder)
2481 return -1;
2482 if (node2.isFolder && !node1.isFolder)
2483 return 1;
2486 const res = column.compareRows(row1, row2);
2487 return (this.reverseSort === '0') ? res : -res;
2488 }.bind(this));
2490 nodes.each(function(node) {
2491 if (node.children.length > 0)
2492 this._sortNodesByColumn(node.children, column);
2493 }.bind(this));
2496 _filterNodes: function(node, filterTerms, filteredRows) {
2497 if (node.isFolder) {
2498 const childAdded = node.children.reduce(function(acc, child) {
2499 // we must execute the function before ORing w/ acc or we'll stop checking child nodes after the first successful match
2500 return (this._filterNodes(child, filterTerms, filteredRows) || acc);
2501 }.bind(this), false);
2503 if (childAdded) {
2504 const row = this.getRow(node);
2505 filteredRows.push(row);
2506 return true;
2510 if (window.qBittorrent.Misc.containsAllTerms(node.name, filterTerms)) {
2511 const row = this.getRow(node);
2512 filteredRows.push(row);
2513 return true;
2516 return false;
2519 setFilter: function(text) {
2520 const filterTerms = text.trim().toLowerCase().split(' ');
2521 if ((filterTerms.length === 1) && (filterTerms[0] === ''))
2522 this.filterTerms = [];
2523 else
2524 this.filterTerms = filterTerms;
2527 getFilteredAndSortedRows: function() {
2528 if (this.getRoot() === null)
2529 return [];
2531 const generateRowsSignature = function(rows) {
2532 const rowsData = rows.map(function(row) {
2533 return row.full_data;
2535 return JSON.stringify(rowsData);
2538 const getFilteredRows = function() {
2539 if (this.filterTerms.length === 0) {
2540 const nodeArray = this.fileTree.toArray();
2541 const filteredRows = nodeArray.map(function(node) {
2542 return this.getRow(node);
2543 }.bind(this));
2544 return filteredRows;
2547 const filteredRows = [];
2548 this.getRoot().children.each(function(child) {
2549 this._filterNodes(child, this.filterTerms, filteredRows);
2550 }.bind(this));
2551 filteredRows.reverse();
2552 return filteredRows;
2553 }.bind(this);
2555 const hasRowsChanged = function(rowsString, prevRowsStringString) {
2556 const rowsChanged = (rowsString !== prevRowsStringString);
2557 const isFilterTermsChanged = this.filterTerms.reduce(function(acc, term, index) {
2558 return (acc || (term !== this.prevFilterTerms[index]));
2559 }.bind(this), false);
2560 const isFilterChanged = ((this.filterTerms.length !== this.prevFilterTerms.length)
2561 || ((this.filterTerms.length > 0) && isFilterTermsChanged));
2562 const isSortedColumnChanged = (this.prevSortedColumn !== this.sortedColumn);
2563 const isReverseSortChanged = (this.prevReverseSort !== this.reverseSort);
2565 return (rowsChanged || isFilterChanged || isSortedColumnChanged || isReverseSortChanged);
2566 }.bind(this);
2568 const rowsString = generateRowsSignature(this.rows);
2569 if (!hasRowsChanged(rowsString, this.prevRowsString)) {
2570 return this.prevFilteredRows;
2573 // sort, then filter
2574 const column = this.columns[this.sortedColumn];
2575 this._sortNodesByColumn(this.getRoot().children, column);
2576 const filteredRows = getFilteredRows();
2578 this.prevFilterTerms = this.filterTerms;
2579 this.prevRowsString = rowsString;
2580 this.prevFilteredRows = filteredRows;
2581 this.prevSortedColumn = this.sortedColumn;
2582 this.prevReverseSort = this.reverseSort;
2583 return filteredRows;
2586 setIgnored: function(rowId, ignore) {
2587 const row = this.rows.get(rowId);
2588 if (ignore)
2589 row.full_data.remaining = 0;
2590 else
2591 row.full_data.remaining = (row.full_data.size * (1.0 - (row.full_data.progress / 100)));
2594 setupTr: function(tr) {
2595 tr.addEvent('keydown', function(event) {
2596 switch (event.key) {
2597 case "left":
2598 qBittorrent.PropFiles.collapseFolder(this._this.getSelectedRowId());
2599 return false;
2600 case "right":
2601 qBittorrent.PropFiles.expandFolder(this._this.getSelectedRowId());
2602 return false;
2608 const RssFeedTable = new Class({
2609 Extends: DynamicTable,
2610 initColumns: function() {
2611 this.newColumn('state_icon', '', '', 30, true);
2612 this.newColumn('name', '', 'QBT_TR(RSS feeds)QBT_TR[CONTEXT=FeedListWidget]', -1, true);
2614 this.columns['state_icon'].dataProperties[0] = '';
2616 // map name row to "[name] ([unread])"
2617 this.columns['name'].dataProperties.push('unread');
2618 this.columns['name'].updateTd = function(td, row) {
2619 const name = this.getRowValue(row, 0);
2620 const unreadCount = this.getRowValue(row, 1);
2621 let value = name + ' (' + unreadCount + ')';
2622 td.set('text', value);
2623 td.set('title', value);
2626 setupHeaderMenu: function() {},
2627 setupHeaderEvents: function() {},
2628 getFilteredAndSortedRows: function() {
2629 return this.rows.getValues();
2631 selectRow: function(rowId) {
2632 this.selectedRows.push(rowId);
2633 this.setRowClass();
2634 this.onSelectedRowChanged();
2636 const rows = this.rows.getValues();
2637 let path = '';
2638 for (let i = 0; i < rows.length; ++i) {
2639 if (rows[i].rowId === rowId) {
2640 path = rows[i].full_data.dataPath;
2641 break;
2644 window.qBittorrent.Rss.showRssFeed(path);
2646 setupTr: function(tr) {
2647 tr.addEvent('dblclick', function(e) {
2648 if (this.rowId !== 0) {
2649 window.qBittorrent.Rss.moveItem(this._this.rows.get(this.rowId).full_data.dataPath);
2650 return true;
2654 updateRow: function(tr, fullUpdate) {
2655 const row = this.rows.get(tr.rowId);
2656 const data = row[fullUpdate ? 'full_data' : 'data'];
2658 const tds = tr.getElements('td');
2659 for (let i = 0; i < this.columns.length; ++i) {
2660 if (Object.hasOwn(data, this.columns[i].dataProperties[0]))
2661 this.columns[i].updateTd(tds[i], row);
2663 row['data'] = {};
2664 tds[0].style.overflow = 'visible';
2665 let indentation = row.full_data.indentation;
2666 tds[0].style.paddingLeft = (indentation * 32 + 4) + 'px';
2667 tds[1].style.paddingLeft = (indentation * 32 + 4) + 'px';
2669 updateIcons: function() {
2670 // state_icon
2671 this.rows.each(row => {
2672 let img_path;
2673 switch (row.full_data.status) {
2674 case 'default':
2675 img_path = 'images/application-rss.svg';
2676 break;
2677 case 'hasError':
2678 img_path = 'images/task-reject.svg';
2679 break;
2680 case 'isLoading':
2681 img_path = 'images/spinner.gif';
2682 break;
2683 case 'unread':
2684 img_path = 'images/mail-inbox.svg';
2685 break;
2686 case 'isFolder':
2687 img_path = 'images/folder-documents.svg';
2688 break;
2690 let td;
2691 for (let i = 0; i < this.tableBody.rows.length; ++i) {
2692 if (this.tableBody.rows[i].rowId === row.rowId) {
2693 td = this.tableBody.rows[i].children[0];
2694 break;
2697 if (td.getChildren('img').length > 0) {
2698 const img = td.getChildren('img')[0];
2699 if (img.src.indexOf(img_path) < 0) {
2700 img.set('src', img_path);
2701 img.set('title', status);
2704 else {
2705 td.adopt(new Element('img', {
2706 'src': img_path,
2707 'class': 'stateIcon',
2708 'height': '22px',
2709 'width': '22px'
2710 }));
2714 newColumn: function(name, style, caption, defaultWidth, defaultVisible) {
2715 const column = {};
2716 column['name'] = name;
2717 column['title'] = name;
2718 column['visible'] = defaultVisible;
2719 column['force_hide'] = false;
2720 column['caption'] = caption;
2721 column['style'] = style;
2722 if (defaultWidth !== -1) {
2723 column['width'] = defaultWidth;
2726 column['dataProperties'] = [name];
2727 column['getRowValue'] = function(row, pos) {
2728 if (pos === undefined)
2729 pos = 0;
2730 return row['full_data'][this.dataProperties[pos]];
2732 column['compareRows'] = function(row1, row2) {
2733 const value1 = this.getRowValue(row1);
2734 const value2 = this.getRowValue(row2);
2735 if ((typeof(value1) === 'number') && (typeof(value2) === 'number'))
2736 return compareNumbers(value1, value2);
2737 return window.qBittorrent.Misc.naturalSortCollator.compare(value1, value2);
2739 column['updateTd'] = function(td, row) {
2740 const value = this.getRowValue(row);
2741 td.set('text', value);
2742 td.set('title', value);
2744 column['onResize'] = null;
2745 this.columns.push(column);
2746 this.columns[name] = column;
2748 this.hiddenTableHeader.appendChild(new Element('th'));
2749 this.fixedTableHeader.appendChild(new Element('th'));
2751 setupCommonEvents: function() {
2752 const scrollFn = function() {
2753 $(this.dynamicTableFixedHeaderDivId).getElements('table')[0].style.left = -$(this.dynamicTableDivId).scrollLeft + 'px';
2754 }.bind(this);
2756 $(this.dynamicTableDivId).addEvent('scroll', scrollFn);
2760 const RssArticleTable = new Class({
2761 Extends: DynamicTable,
2762 initColumns: function() {
2763 this.newColumn('name', '', 'QBT_TR(Torrents: (double-click to download))QBT_TR[CONTEXT=RSSWidget]', -1, true);
2765 setupHeaderMenu: function() {},
2766 setupHeaderEvents: function() {},
2767 getFilteredAndSortedRows: function() {
2768 return this.rows.getValues();
2770 selectRow: function(rowId) {
2771 this.selectedRows.push(rowId);
2772 this.setRowClass();
2773 this.onSelectedRowChanged();
2775 const rows = this.rows.getValues();
2776 let articleId = '';
2777 let feedUid = '';
2778 for (let i = 0; i < rows.length; ++i) {
2779 if (rows[i].rowId === rowId) {
2780 articleId = rows[i].full_data.dataId;
2781 feedUid = rows[i].full_data.feedUid;
2782 this.tableBody.rows[rows[i].rowId].removeClass('unreadArticle');
2783 break;
2786 window.qBittorrent.Rss.showDetails(feedUid, articleId);
2788 setupTr: function(tr) {
2789 tr.addEvent('dblclick', function(e) {
2790 showDownloadPage([this._this.rows.get(this.rowId).full_data.torrentURL]);
2791 return true;
2793 tr.addClass('torrentsTableContextMenuTarget');
2795 updateRow: function(tr, fullUpdate) {
2796 const row = this.rows.get(tr.rowId);
2797 const data = row[fullUpdate ? 'full_data' : 'data'];
2798 if (!row.full_data.isRead)
2799 tr.addClass('unreadArticle');
2800 else
2801 tr.removeClass('unreadArticle');
2803 const tds = tr.getElements('td');
2804 for (let i = 0; i < this.columns.length; ++i) {
2805 if (Object.hasOwn(data, this.columns[i].dataProperties[0]))
2806 this.columns[i].updateTd(tds[i], row);
2808 row['data'] = {};
2810 newColumn: function(name, style, caption, defaultWidth, defaultVisible) {
2811 const column = {};
2812 column['name'] = name;
2813 column['title'] = name;
2814 column['visible'] = defaultVisible;
2815 column['force_hide'] = false;
2816 column['caption'] = caption;
2817 column['style'] = style;
2818 if (defaultWidth !== -1) {
2819 column['width'] = defaultWidth;
2822 column['dataProperties'] = [name];
2823 column['getRowValue'] = function(row, pos) {
2824 if (pos === undefined)
2825 pos = 0;
2826 return row['full_data'][this.dataProperties[pos]];
2828 column['compareRows'] = function(row1, row2) {
2829 const value1 = this.getRowValue(row1);
2830 const value2 = this.getRowValue(row2);
2831 if ((typeof(value1) === 'number') && (typeof(value2) === 'number'))
2832 return compareNumbers(value1, value2);
2833 return window.qBittorrent.Misc.naturalSortCollator.compare(value1, value2);
2835 column['updateTd'] = function(td, row) {
2836 const value = this.getRowValue(row);
2837 td.set('text', value);
2838 td.set('title', value);
2840 column['onResize'] = null;
2841 this.columns.push(column);
2842 this.columns[name] = column;
2844 this.hiddenTableHeader.appendChild(new Element('th'));
2845 this.fixedTableHeader.appendChild(new Element('th'));
2847 setupCommonEvents: function() {
2848 const scrollFn = function() {
2849 $(this.dynamicTableFixedHeaderDivId).getElements('table')[0].style.left = -$(this.dynamicTableDivId).scrollLeft + 'px';
2850 }.bind(this);
2852 $(this.dynamicTableDivId).addEvent('scroll', scrollFn);
2856 const RssDownloaderRulesTable = new Class({
2857 Extends: DynamicTable,
2858 initColumns: function() {
2859 this.newColumn('checked', '', '', 30, true);
2860 this.newColumn('name', '', '', -1, true);
2862 this.columns['checked'].updateTd = function(td, row) {
2863 if ($('cbRssDlRule' + row.rowId) === null) {
2864 const checkbox = new Element('input');
2865 checkbox.set('type', 'checkbox');
2866 checkbox.set('id', 'cbRssDlRule' + row.rowId);
2867 checkbox.checked = row.full_data.checked;
2869 checkbox.addEvent('click', function(e) {
2870 window.qBittorrent.RssDownloader.rssDownloaderRulesTable.updateRowData({
2871 rowId: row.rowId,
2872 checked: this.checked
2874 window.qBittorrent.RssDownloader.modifyRuleState(row.full_data.name, 'enabled', this.checked);
2875 e.stopPropagation();
2878 td.append(checkbox);
2880 else {
2881 $('cbRssDlRule' + row.rowId).checked = row.full_data.checked;
2885 setupHeaderMenu: function() {},
2886 setupHeaderEvents: function() {},
2887 getFilteredAndSortedRows: function() {
2888 return this.rows.getValues();
2890 setupTr: function(tr) {
2891 tr.addEvent('dblclick', function(e) {
2892 window.qBittorrent.RssDownloader.renameRule(this._this.rows.get(this.rowId).full_data.name);
2893 return true;
2896 newColumn: function(name, style, caption, defaultWidth, defaultVisible) {
2897 const column = {};
2898 column['name'] = name;
2899 column['title'] = name;
2900 column['visible'] = defaultVisible;
2901 column['force_hide'] = false;
2902 column['caption'] = caption;
2903 column['style'] = style;
2904 if (defaultWidth !== -1) {
2905 column['width'] = defaultWidth;
2908 column['dataProperties'] = [name];
2909 column['getRowValue'] = function(row, pos) {
2910 if (pos === undefined)
2911 pos = 0;
2912 return row['full_data'][this.dataProperties[pos]];
2914 column['compareRows'] = function(row1, row2) {
2915 const value1 = this.getRowValue(row1);
2916 const value2 = this.getRowValue(row2);
2917 if ((typeof(value1) === 'number') && (typeof(value2) === 'number'))
2918 return compareNumbers(value1, value2);
2919 return window.qBittorrent.Misc.naturalSortCollator.compare(value1, value2);
2921 column['updateTd'] = function(td, row) {
2922 const value = this.getRowValue(row);
2923 td.set('text', value);
2924 td.set('title', value);
2926 column['onResize'] = null;
2927 this.columns.push(column);
2928 this.columns[name] = column;
2930 this.hiddenTableHeader.appendChild(new Element('th'));
2931 this.fixedTableHeader.appendChild(new Element('th'));
2933 selectRow: function(rowId) {
2934 this.selectedRows.push(rowId);
2935 this.setRowClass();
2936 this.onSelectedRowChanged();
2938 const rows = this.rows.getValues();
2939 let name = '';
2940 for (let i = 0; i < rows.length; ++i) {
2941 if (rows[i].rowId === rowId) {
2942 name = rows[i].full_data.name;
2943 break;
2946 window.qBittorrent.RssDownloader.showRule(name);
2950 const RssDownloaderFeedSelectionTable = new Class({
2951 Extends: DynamicTable,
2952 initColumns: function() {
2953 this.newColumn('checked', '', '', 30, true);
2954 this.newColumn('name', '', '', -1, true);
2956 this.columns['checked'].updateTd = function(td, row) {
2957 if ($('cbRssDlFeed' + row.rowId) === null) {
2958 const checkbox = new Element('input');
2959 checkbox.set('type', 'checkbox');
2960 checkbox.set('id', 'cbRssDlFeed' + row.rowId);
2961 checkbox.checked = row.full_data.checked;
2963 checkbox.addEvent('click', function(e) {
2964 window.qBittorrent.RssDownloader.rssDownloaderFeedSelectionTable.updateRowData({
2965 rowId: row.rowId,
2966 checked: this.checked
2968 e.stopPropagation();
2971 td.append(checkbox);
2973 else {
2974 $('cbRssDlFeed' + row.rowId).checked = row.full_data.checked;
2978 setupHeaderMenu: function() {},
2979 setupHeaderEvents: function() {},
2980 getFilteredAndSortedRows: function() {
2981 return this.rows.getValues();
2983 newColumn: function(name, style, caption, defaultWidth, defaultVisible) {
2984 const column = {};
2985 column['name'] = name;
2986 column['title'] = name;
2987 column['visible'] = defaultVisible;
2988 column['force_hide'] = false;
2989 column['caption'] = caption;
2990 column['style'] = style;
2991 if (defaultWidth !== -1) {
2992 column['width'] = defaultWidth;
2995 column['dataProperties'] = [name];
2996 column['getRowValue'] = function(row, pos) {
2997 if (pos === undefined)
2998 pos = 0;
2999 return row['full_data'][this.dataProperties[pos]];
3001 column['compareRows'] = function(row1, row2) {
3002 const value1 = this.getRowValue(row1);
3003 const value2 = this.getRowValue(row2);
3004 if ((typeof(value1) === 'number') && (typeof(value2) === 'number'))
3005 return compareNumbers(value1, value2);
3006 return window.qBittorrent.Misc.naturalSortCollator.compare(value1, value2);
3008 column['updateTd'] = function(td, row) {
3009 const value = this.getRowValue(row);
3010 td.set('text', value);
3011 td.set('title', value);
3013 column['onResize'] = null;
3014 this.columns.push(column);
3015 this.columns[name] = column;
3017 this.hiddenTableHeader.appendChild(new Element('th'));
3018 this.fixedTableHeader.appendChild(new Element('th'));
3020 selectRow: function() {}
3023 const RssDownloaderArticlesTable = new Class({
3024 Extends: DynamicTable,
3025 initColumns: function() {
3026 this.newColumn('name', '', '', -1, true);
3028 setupHeaderMenu: function() {},
3029 setupHeaderEvents: function() {},
3030 getFilteredAndSortedRows: function() {
3031 return this.rows.getValues();
3033 newColumn: function(name, style, caption, defaultWidth, defaultVisible) {
3034 const column = {};
3035 column['name'] = name;
3036 column['title'] = name;
3037 column['visible'] = defaultVisible;
3038 column['force_hide'] = false;
3039 column['caption'] = caption;
3040 column['style'] = style;
3041 if (defaultWidth !== -1) {
3042 column['width'] = defaultWidth;
3045 column['dataProperties'] = [name];
3046 column['getRowValue'] = function(row, pos) {
3047 if (pos === undefined)
3048 pos = 0;
3049 return row['full_data'][this.dataProperties[pos]];
3051 column['compareRows'] = function(row1, row2) {
3052 const value1 = this.getRowValue(row1);
3053 const value2 = this.getRowValue(row2);
3054 if ((typeof(value1) === 'number') && (typeof(value2) === 'number'))
3055 return compareNumbers(value1, value2);
3056 return window.qBittorrent.Misc.naturalSortCollator.compare(value1, value2);
3058 column['updateTd'] = function(td, row) {
3059 const value = this.getRowValue(row);
3060 td.set('text', value);
3061 td.set('title', value);
3063 column['onResize'] = null;
3064 this.columns.push(column);
3065 this.columns[name] = column;
3067 this.hiddenTableHeader.appendChild(new Element('th'));
3068 this.fixedTableHeader.appendChild(new Element('th'));
3070 selectRow: function() {},
3071 updateRow: function(tr, fullUpdate) {
3072 const row = this.rows.get(tr.rowId);
3073 const data = row[fullUpdate ? 'full_data' : 'data'];
3075 if (row.full_data.isFeed) {
3076 tr.addClass('articleTableFeed');
3077 tr.removeClass('articleTableArticle');
3079 else {
3080 tr.removeClass('articleTableFeed');
3081 tr.addClass('articleTableArticle');
3084 const tds = tr.getElements('td');
3085 for (let i = 0; i < this.columns.length; ++i) {
3086 if (Object.hasOwn(data, this.columns[i].dataProperties[0]))
3087 this.columns[i].updateTd(tds[i], row);
3089 row['data'] = {};
3093 const LogMessageTable = new Class({
3094 Extends: DynamicTable,
3096 filterText: '',
3098 filteredLength: function() {
3099 return this.tableBody.getElements('tr').length;
3102 initColumns: function() {
3103 this.newColumn('rowId', '', 'QBT_TR(ID)QBT_TR[CONTEXT=ExecutionLogWidget]', 50, true);
3104 this.newColumn('message', '', 'QBT_TR(Message)QBT_TR[CONTEXT=ExecutionLogWidget]', 350, true);
3105 this.newColumn('timestamp', '', 'QBT_TR(Timestamp)QBT_TR[CONTEXT=ExecutionLogWidget]', 150, true);
3106 this.newColumn('type', '', 'QBT_TR(Log Type)QBT_TR[CONTEXT=ExecutionLogWidget]', 100, true);
3107 this.initColumnsFunctions();
3110 initColumnsFunctions: function() {
3111 this.columns['timestamp'].updateTd = function(td, row) {
3112 const date = new Date(this.getRowValue(row) * 1000).toLocaleString();
3113 td.set({ 'text': date, 'title': date });
3116 this.columns['type'].updateTd = function(td, row) {
3117 //Type of the message: Log::NORMAL: 1, Log::INFO: 2, Log::WARNING: 4, Log::CRITICAL: 8
3118 let logLevel, addClass;
3119 switch (this.getRowValue(row).toInt()) {
3120 case 1:
3121 logLevel = 'QBT_TR(Normal)QBT_TR[CONTEXT=ExecutionLogWidget]';
3122 addClass = 'logNormal';
3123 break;
3124 case 2:
3125 logLevel = 'QBT_TR(Info)QBT_TR[CONTEXT=ExecutionLogWidget]';
3126 addClass = 'logInfo';
3127 break;
3128 case 4:
3129 logLevel = 'QBT_TR(Warning)QBT_TR[CONTEXT=ExecutionLogWidget]';
3130 addClass = 'logWarning';
3131 break;
3132 case 8:
3133 logLevel = 'QBT_TR(Critical)QBT_TR[CONTEXT=ExecutionLogWidget]';
3134 addClass = 'logCritical';
3135 break;
3136 default:
3137 logLevel = 'QBT_TR(Unknown)QBT_TR[CONTEXT=ExecutionLogWidget]';
3138 addClass = 'logUnknown';
3139 break;
3141 td.set({ 'text': logLevel, 'title': logLevel });
3142 td.getParent('tr').set('class', 'logTableRow ' + addClass);
3146 getFilteredAndSortedRows: function() {
3147 let filteredRows = [];
3148 const rows = this.rows.getValues();
3149 this.filterText = window.qBittorrent.Log.getFilterText();
3150 const filterTerms = (this.filterText.length > 0) ? this.filterText.toLowerCase().split(' ') : [];
3151 const logLevels = window.qBittorrent.Log.getSelectedLevels();
3152 if (filterTerms.length > 0 || logLevels.length < 4) {
3153 for (let i = 0; i < rows.length; ++i) {
3154 if (logLevels.indexOf(rows[i].full_data.type.toString()) == -1)
3155 continue;
3157 if (filterTerms.length > 0 && !window.qBittorrent.Misc.containsAllTerms(rows[i].full_data.message, filterTerms))
3158 continue;
3160 filteredRows.push(rows[i]);
3163 else {
3164 filteredRows = rows;
3167 filteredRows.sort(function(row1, row2) {
3168 const column = this.columns[this.sortedColumn];
3169 const res = column.compareRows(row1, row2);
3170 return (this.reverseSort == '0') ? res : -res;
3171 }.bind(this));
3173 return filteredRows;
3176 setupCommonEvents: function() {},
3178 setupTr: function(tr) {
3179 tr.addClass('logTableRow');
3183 const LogPeerTable = new Class({
3184 Extends: LogMessageTable,
3186 initColumns: function() {
3187 this.newColumn('rowId', '', 'QBT_TR(ID)QBT_TR[CONTEXT=ExecutionLogWidget]', 50, true);
3188 this.newColumn('ip', '', 'QBT_TR(IP)QBT_TR[CONTEXT=ExecutionLogWidget]', 150, true);
3189 this.newColumn('timestamp', '', 'QBT_TR(Timestamp)QBT_TR[CONTEXT=ExecutionLogWidget]', 150, true);
3190 this.newColumn('blocked', '', 'QBT_TR(Status)QBT_TR[CONTEXT=ExecutionLogWidget]', 150, true);
3191 this.newColumn('reason', '', 'QBT_TR(Reason)QBT_TR[CONTEXT=ExecutionLogWidget]', 150, true);
3193 this.columns['timestamp'].updateTd = function(td, row) {
3194 const date = new Date(this.getRowValue(row) * 1000).toLocaleString();
3195 td.set({ 'text': date, 'title': date });
3198 this.columns['blocked'].updateTd = function(td, row) {
3199 let status, addClass;
3200 if (this.getRowValue(row)) {
3201 status = 'QBT_TR(Blocked)QBT_TR[CONTEXT=ExecutionLogWidget]';
3202 addClass = 'peerBlocked';
3204 else {
3205 status = 'QBT_TR(Banned)QBT_TR[CONTEXT=ExecutionLogWidget]';
3206 addClass = 'peerBanned';
3208 td.set({ 'text': status, 'title': status });
3209 td.getParent('tr').set('class', 'logTableRow ' + addClass);
3213 getFilteredAndSortedRows: function() {
3214 let filteredRows = [];
3215 const rows = this.rows.getValues();
3216 this.filterText = window.qBittorrent.Log.getFilterText();
3217 const filterTerms = (this.filterText.length > 0) ? this.filterText.toLowerCase().split(' ') : [];
3218 if (filterTerms.length > 0) {
3219 for (let i = 0; i < rows.length; ++i) {
3220 if (filterTerms.length > 0 && !window.qBittorrent.Misc.containsAllTerms(rows[i].full_data.ip, filterTerms))
3221 continue;
3223 filteredRows.push(rows[i]);
3226 else {
3227 filteredRows = rows;
3230 filteredRows.sort(function(row1, row2) {
3231 const column = this.columns[this.sortedColumn];
3232 const res = column.compareRows(row1, row2);
3233 return (this.reverseSort == '0') ? res : -res;
3234 }.bind(this));
3236 return filteredRows;
3240 return exports();
3241 })();
3243 Object.freeze(window.qBittorrent.DynamicTable);
3245 /*************************************************************/