3 * Copyright (c) 2008 Ishan Arora <ishan@qbittorrent.org> & Christophe Dumez <chris@qbittorrent.org>
5 * Permission is hereby granted, free of charge, to any person obtaining a copy
6 * of this software and associated documentation files (the "Software"), to deal
7 * in the Software without restriction, including without limitation the rights
8 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 * copies of the Software, and to permit persons to whom the Software is
10 * furnished to do so, subject to the following conditions:
12 * The above copyright notice and this permission notice shall be included in
13 * all copies or substantial portions of the Software.
15 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24 /**************************************************************
26 Script : Dynamic Table
28 Authors : Ishan Arora & Christophe Dumez
29 Desc : Programmable sortable table
30 Licence : Open Source MIT Licence
32 **************************************************************/
36 if (window
.qBittorrent
=== undefined) {
37 window
.qBittorrent
= {};
40 window
.qBittorrent
.DynamicTable
= (function() {
41 const exports = function() {
43 TorrentsTable
: TorrentsTable
,
44 TorrentPeersTable
: TorrentPeersTable
,
45 SearchResultsTable
: SearchResultsTable
,
46 SearchPluginsTable
: SearchPluginsTable
,
47 TorrentTrackersTable
: TorrentTrackersTable
,
48 BulkRenameTorrentFilesTable
: BulkRenameTorrentFilesTable
,
49 TorrentFilesTable
: TorrentFilesTable
,
50 LogMessageTable
: LogMessageTable
,
51 LogPeerTable
: LogPeerTable
,
52 RssFeedTable
: RssFeedTable
,
53 RssArticleTable
: RssArticleTable
,
54 RssDownloaderRulesTable
: RssDownloaderRulesTable
,
55 RssDownloaderFeedSelectionTable
: RssDownloaderFeedSelectionTable
,
56 RssDownloaderArticlesTable
: RssDownloaderArticlesTable
60 const compareNumbers
= (val1
, val2
) => {
68 let DynamicTableHeaderContextMenuClass
= null;
69 let ProgressColumnWidth
= -1;
71 const DynamicTable
= new Class({
73 initialize: function() {},
75 setup: function(dynamicTableDivId
, dynamicTableFixedHeaderDivId
, contextMenu
) {
76 this.dynamicTableDivId
= dynamicTableDivId
;
77 this.dynamicTableFixedHeaderDivId
= dynamicTableFixedHeaderDivId
;
78 this.fixedTableHeader
= $(dynamicTableFixedHeaderDivId
).getElements('tr')[0];
79 this.hiddenTableHeader
= $(dynamicTableDivId
).getElements('tr')[0];
80 this.tableBody
= $(dynamicTableDivId
).getElements('tbody')[0];
81 this.rows
= new Hash();
82 this.selectedRows
= [];
84 this.contextMenu
= contextMenu
;
85 this.sortedColumn
= LocalPreferences
.get('sorted_column_' + this.dynamicTableDivId
, 0);
86 this.reverseSort
= LocalPreferences
.get('reverse_sort_' + this.dynamicTableDivId
, '0');
88 this.loadColumnsOrder();
89 this.updateTableHeaders();
90 this.setupCommonEvents();
91 this.setupHeaderEvents();
92 this.setupHeaderMenu();
93 this.setSortedColumnIcon(this.sortedColumn
, null, (this.reverseSort
=== '1'));
96 setupCommonEvents: function() {
97 const scrollFn = function() {
98 $(this.dynamicTableFixedHeaderDivId
).getElements('table')[0].style
.left
= -$(this.dynamicTableDivId
).scrollLeft
+ 'px';
101 $(this.dynamicTableDivId
).addEvent('scroll', scrollFn
);
103 // if the table exists within a panel
104 if ($(this.dynamicTableDivId
).getParent('.panel')) {
105 const resizeFn = function() {
106 const panel
= $(this.dynamicTableDivId
).getParent('.panel');
107 let h
= panel
.getBoundingClientRect().height
- $(this.dynamicTableFixedHeaderDivId
).getBoundingClientRect().height
;
108 $(this.dynamicTableDivId
).style
.height
= h
+ 'px';
110 // Workaround due to inaccurate calculation of elements heights by browser
114 // is panel vertical scrollbar visible or does panel content not fit?
115 while (((panel
.clientWidth
!= panel
.offsetWidth
) || (panel
.clientHeight
!= panel
.scrollHeight
)) && (n
> 0)) {
118 $(this.dynamicTableDivId
).style
.height
= h
+ 'px';
121 this.lastPanelHeight
= panel
.getBoundingClientRect().height
;
124 $(this.dynamicTableDivId
).getParent('.panel').addEvent('resize', resizeFn
);
126 this.lastPanelHeight
= 0;
128 // Workaround. Resize event is called not always (for example it isn't called when browser window changes it's size)
130 const checkResizeFn = function() {
131 const tableDiv
= $(this.dynamicTableDivId
);
133 // dynamicTableDivId is not visible on the UI
138 const panel
= tableDiv
.getParent('.panel');
139 if (this.lastPanelHeight
!= panel
.getBoundingClientRect().height
) {
140 this.lastPanelHeight
= panel
.getBoundingClientRect().height
;
141 panel
.fireEvent('resize');
145 setInterval(checkResizeFn
, 500);
149 setupHeaderEvents: function() {
150 this.currentHeaderAction
= '';
151 this.canResize
= false;
153 const resetElementBorderStyle = function(el
, side
) {
154 if (side
=== 'left' || side
!== 'right') {
155 el
.setStyle('border-left-style', '');
156 el
.setStyle('border-left-color', '');
157 el
.setStyle('border-left-width', '');
159 if (side
=== 'right' || side
!== 'left') {
160 el
.setStyle('border-right-style', '');
161 el
.setStyle('border-right-color', '');
162 el
.setStyle('border-right-width', '');
166 const mouseMoveFn = function(e
) {
167 const brect
= e
.target
.getBoundingClientRect();
168 const mouseXRelative
= e
.event
.clientX
- brect
.left
;
169 if (this.currentHeaderAction
=== '') {
170 if (brect
.width
- mouseXRelative
< 5) {
171 this.resizeTh
= e
.target
;
172 this.canResize
= true;
173 e
.target
.getParent("tr").style
.cursor
= 'col-resize';
175 else if ((mouseXRelative
< 5) && e
.target
.getPrevious('[class=""]')) {
176 this.resizeTh
= e
.target
.getPrevious('[class=""]');
177 this.canResize
= true;
178 e
.target
.getParent("tr").style
.cursor
= 'col-resize';
181 this.canResize
= false;
182 e
.target
.getParent("tr").style
.cursor
= '';
185 if (this.currentHeaderAction
=== 'drag') {
186 const previousVisibleSibling
= e
.target
.getPrevious('[class=""]');
187 let borderChangeElement
= previousVisibleSibling
;
188 let changeBorderSide
= 'right';
190 if (mouseXRelative
> brect
.width
/ 2) {
191 borderChangeElement
= e
.target
;
192 this.dropSide
= 'right';
195 this.dropSide
= 'left';
198 e
.target
.getParent("tr").style
.cursor
= 'move';
200 if (!previousVisibleSibling
) { // right most column
201 borderChangeElement
= e
.target
;
203 if (mouseXRelative
<= brect
.width
/ 2)
204 changeBorderSide
= 'left';
207 borderChangeElement
.setStyle('border-' + changeBorderSide
+ '-style', 'solid');
208 borderChangeElement
.setStyle('border-' + changeBorderSide
+ '-color', '#e60');
209 borderChangeElement
.setStyle('border-' + changeBorderSide
+ '-width', 'initial');
211 resetElementBorderStyle(borderChangeElement
, changeBorderSide
=== 'right' ? 'left' : 'right');
213 borderChangeElement
.getSiblings('[class=""]').each(function(el
) {
214 resetElementBorderStyle(el
);
217 this.lastHoverTh
= e
.target
;
218 this.lastClientX
= e
.event
.clientX
;
221 const mouseOutFn = function(e
) {
222 resetElementBorderStyle(e
.target
);
225 const onBeforeStart = function(el
) {
227 this.currentHeaderAction
= 'start';
228 this.dragMovement
= false;
229 this.dragStartX
= this.lastClientX
;
232 const onStart = function(el
, event
) {
233 if (this.canResize
) {
234 this.currentHeaderAction
= 'resize';
235 this.startWidth
= this.resizeTh
.getStyle('width').toFloat();
238 this.currentHeaderAction
= 'drag';
239 el
.setStyle('background-color', '#C1D5E7');
243 const onDrag = function(el
, event
) {
244 if (this.currentHeaderAction
=== 'resize') {
245 let width
= this.startWidth
+ (event
.page
.x
- this.dragStartX
);
248 this.columns
[this.resizeTh
.columnName
].width
= width
;
249 this.updateColumn(this.resizeTh
.columnName
);
253 const onComplete = function(el
, event
) {
254 resetElementBorderStyle(this.lastHoverTh
);
255 el
.setStyle('background-color', '');
256 if (this.currentHeaderAction
=== 'resize')
257 LocalPreferences
.set('column_' + this.resizeTh
.columnName
+ '_width_' + this.dynamicTableDivId
, this.columns
[this.resizeTh
.columnName
].width
);
258 if ((this.currentHeaderAction
=== 'drag') && (el
!== this.lastHoverTh
)) {
259 this.saveColumnsOrder();
260 const val
= LocalPreferences
.get('columns_order_' + this.dynamicTableDivId
).split(',');
261 val
.erase(el
.columnName
);
262 let pos
= val
.indexOf(this.lastHoverTh
.columnName
);
263 if (this.dropSide
=== 'right')
265 val
.splice(pos
, 0, el
.columnName
);
266 LocalPreferences
.set('columns_order_' + this.dynamicTableDivId
, val
.join(','));
267 this.loadColumnsOrder();
268 this.updateTableHeaders();
269 while (this.tableBody
.firstChild
)
270 this.tableBody
.removeChild(this.tableBody
.firstChild
);
271 this.updateTable(true);
273 if (this.currentHeaderAction
=== 'drag') {
274 resetElementBorderStyle(el
);
275 el
.getSiblings('[class=""]').each(function(el
) {
276 resetElementBorderStyle(el
);
279 this.currentHeaderAction
= '';
282 const onCancel = function(el
) {
283 this.currentHeaderAction
= '';
284 this.setSortedColumn(el
.columnName
);
287 const onTouch = function(e
) {
288 const column
= e
.target
.columnName
;
289 this.currentHeaderAction
= '';
290 this.setSortedColumn(column
);
293 const ths
= this.fixedTableHeader
.getElements('th');
295 for (let i
= 0; i
< ths
.length
; ++i
) {
297 th
.addEvent('mousemove', mouseMoveFn
);
298 th
.addEvent('mouseout', mouseOutFn
);
299 th
.addEvent('touchend', onTouch
);
305 onBeforeStart
: onBeforeStart
,
308 onComplete
: onComplete
,
314 setupDynamicTableHeaderContextMenuClass: function() {
315 if (!DynamicTableHeaderContextMenuClass
) {
316 DynamicTableHeaderContextMenuClass
= new Class({
317 Extends
: window
.qBittorrent
.ContextMenu
.ContextMenu
,
318 updateMenuItems: function() {
319 for (let i
= 0; i
< this.dynamicTable
.columns
.length
; ++i
) {
320 if (this.dynamicTable
.columns
[i
].caption
=== '')
322 if (this.dynamicTable
.columns
[i
].visible
!== '0')
323 this.setItemChecked(this.dynamicTable
.columns
[i
].name
, true);
325 this.setItemChecked(this.dynamicTable
.columns
[i
].name
, false);
332 showColumn: function(columnName
, show
) {
333 this.columns
[columnName
].visible
= show
? '1' : '0';
334 LocalPreferences
.set('column_' + columnName
+ '_visible_' + this.dynamicTableDivId
, show
? '1' : '0');
335 this.updateColumn(columnName
);
338 setupHeaderMenu: function() {
339 this.setupDynamicTableHeaderContextMenuClass();
341 const menuId
= this.dynamicTableDivId
+ '_headerMenu';
343 // reuse menu if already exists
344 const ul
= $(menuId
) ?? new Element('ul', {
346 class: 'contextMenu scrollableMenu'
349 const createLi = function(columnName
, text
) {
350 const html
= '<a href="#' + columnName
+ '" ><img src="images/checked-completed.svg"/>' + window
.qBittorrent
.Misc
.escapeHtml(text
) + '</a>';
351 return new Element('li', {
358 const onMenuItemClicked = function(element
, ref
, action
) {
359 this.showColumn(action
, this.columns
[action
].visible
=== '0');
362 // recreate child nodes when reusing (enables the context menu to work correctly)
363 if (ul
.hasChildNodes()) {
364 while (ul
.firstChild
) {
365 ul
.removeChild(ul
.lastChild
);
369 for (let i
= 0; i
< this.columns
.length
; ++i
) {
370 const text
= this.columns
[i
].caption
;
373 ul
.appendChild(createLi(this.columns
[i
].name
, text
));
374 actions
[this.columns
[i
].name
] = onMenuItemClicked
;
377 ul
.inject(document
.body
);
379 this.headerContextMenu
= new DynamicTableHeaderContextMenuClass({
380 targets
: '#' + this.dynamicTableFixedHeaderDivId
+ ' tr',
389 this.headerContextMenu
.dynamicTable
= this;
392 initColumns: function() {},
394 newColumn: function(name
, style
, caption
, defaultWidth
, defaultVisible
) {
396 column
['name'] = name
;
397 column
['title'] = name
;
398 column
['visible'] = LocalPreferences
.get('column_' + name
+ '_visible_' + this.dynamicTableDivId
, defaultVisible
? '1' : '0');
399 column
['force_hide'] = false;
400 column
['caption'] = caption
;
401 column
['style'] = style
;
402 column
['width'] = LocalPreferences
.get('column_' + name
+ '_width_' + this.dynamicTableDivId
, defaultWidth
);
403 column
['dataProperties'] = [name
];
404 column
['getRowValue'] = function(row
, pos
) {
405 if (pos
=== undefined)
407 return row
['full_data'][this.dataProperties
[pos
]];
409 column
['compareRows'] = function(row1
, row2
) {
410 const value1
= this.getRowValue(row1
);
411 const value2
= this.getRowValue(row2
);
412 if ((typeof(value1
) === 'number') && (typeof(value2
) === 'number'))
413 return compareNumbers(value1
, value2
);
414 return window
.qBittorrent
.Misc
.naturalSortCollator
.compare(value1
, value2
);
416 column
['updateTd'] = function(td
, row
) {
417 const value
= this.getRowValue(row
);
418 td
.set('text', value
);
419 td
.set('title', value
);
421 column
['onResize'] = null;
422 this.columns
.push(column
);
423 this.columns
[name
] = column
;
425 this.hiddenTableHeader
.appendChild(new Element('th'));
426 this.fixedTableHeader
.appendChild(new Element('th'));
429 loadColumnsOrder: function() {
430 const columnsOrder
= [];
431 const val
= LocalPreferences
.get('columns_order_' + this.dynamicTableDivId
);
432 if (val
=== null || val
=== undefined)
434 val
.split(',').forEach(function(v
) {
435 if ((v
in this.columns
) && (!columnsOrder
.contains(v
)))
436 columnsOrder
.push(v
);
439 for (let i
= 0; i
< this.columns
.length
; ++i
)
440 if (!columnsOrder
.contains(this.columns
[i
].name
))
441 columnsOrder
.push(this.columns
[i
].name
);
443 for (let i
= 0; i
< this.columns
.length
; ++i
)
444 this.columns
[i
] = this.columns
[columnsOrder
[i
]];
447 saveColumnsOrder: function() {
449 for (let i
= 0; i
< this.columns
.length
; ++i
) {
452 val
+= this.columns
[i
].name
;
454 LocalPreferences
.set('columns_order_' + this.dynamicTableDivId
, val
);
457 updateTableHeaders: function() {
458 this.updateHeader(this.hiddenTableHeader
);
459 this.updateHeader(this.fixedTableHeader
);
462 updateHeader: function(header
) {
463 const ths
= header
.getElements('th');
465 for (let i
= 0; i
< ths
.length
; ++i
) {
468 th
.setAttribute('title', this.columns
[i
].caption
);
469 th
.set('text', this.columns
[i
].caption
);
470 th
.setAttribute('style', 'width: ' + this.columns
[i
].width
+ 'px;' + this.columns
[i
].style
);
471 th
.columnName
= this.columns
[i
].name
;
472 th
.addClass('column_' + th
.columnName
);
473 if ((this.columns
[i
].visible
== '0') || this.columns
[i
].force_hide
)
474 th
.addClass('invisible');
476 th
.removeClass('invisible');
480 getColumnPos: function(columnName
) {
481 for (let i
= 0; i
< this.columns
.length
; ++i
)
482 if (this.columns
[i
].name
== columnName
)
487 updateColumn: function(columnName
) {
488 const pos
= this.getColumnPos(columnName
);
489 const visible
= ((this.columns
[pos
].visible
!= '0') && !this.columns
[pos
].force_hide
);
490 const ths
= this.hiddenTableHeader
.getElements('th');
491 const fths
= this.fixedTableHeader
.getElements('th');
492 const trs
= this.tableBody
.getElements('tr');
493 const style
= 'width: ' + this.columns
[pos
].width
+ 'px;' + this.columns
[pos
].style
;
495 ths
[pos
].setAttribute('style', style
);
496 fths
[pos
].setAttribute('style', style
);
499 ths
[pos
].removeClass('invisible');
500 fths
[pos
].removeClass('invisible');
501 for (let i
= 0; i
< trs
.length
; ++i
)
502 trs
[i
].getElements('td')[pos
].removeClass('invisible');
505 ths
[pos
].addClass('invisible');
506 fths
[pos
].addClass('invisible');
507 for (let j
= 0; j
< trs
.length
; ++j
)
508 trs
[j
].getElements('td')[pos
].addClass('invisible');
510 if (this.columns
[pos
].onResize
!== null) {
511 this.columns
[pos
].onResize(columnName
);
515 getSortedColumn: function() {
516 return LocalPreferences
.get('sorted_column_' + this.dynamicTableDivId
);
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);
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)
548 const colElem
= getCol(this.dynamicTableFixedHeaderDivId
, newColumn
);
549 if (colElem
!== null) {
550 colElem
.addClass('sorted');
552 colElem
.addClass('reverse');
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];
569 isRowSelected: function(rowId
) {
570 return this.selectedRows
.contains(rowId
);
574 if (!MUI
.ieLegacySupport
)
577 const trs
= this.tableBody
.getElements('tr');
578 trs
.each(function(el
, i
) {
583 el
.removeClass('alt');
588 selectAll: function() {
591 const trs
= this.tableBody
.getElements('tr');
592 for (let i
= 0; i
< trs
.length
; ++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
);
607 this.onSelectedRowChanged();
610 deselectRow: function(rowId
) {
611 this.selectedRows
.erase(rowId
);
613 this.onSelectedRowChanged();
616 selectRows: function(rowId1
, rowId2
) {
618 if (rowId1
=== rowId2
) {
619 this.selectRow(rowId1
);
625 this.tableBody
.getElements('tr').each(function(tr
) {
626 if ((tr
.rowId
== rowId1
) || (tr
.rowId
== rowId2
)) {
628 that
.selectedRows
.push(tr
.rowId
);
631 that
.selectedRows
.push(tr
.rowId
);
635 this.onSelectedRowChanged();
638 reselectRows: function(rowIds
) {
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() {
649 this.tableBody
.getElements('tr').each(function(tr
) {
650 if (that
.isRowSelected(tr
.rowId
))
651 tr
.addClass('selected');
653 tr
.removeClass('selected');
657 onSelectedRowChanged: function() {},
659 updateRowData: function(data
) {
660 // ensure rowId is a string
661 const rowId
= `${data['rowId']}`;
664 if (!this.rows
.has(rowId
)) {
669 this.rows
.set(rowId
, row
);
672 row
= this.rows
.get(rowId
);
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')
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
)
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);
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
) {
727 trs
[j
].inject(trs
[rowPos
], 'before');
728 const tmpTr
= trs
[j
];
730 trs
.splice(rowPos
, 0, tmpTr
);
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
);
746 tr
.addEvent('contextmenu', function(e
) {
747 if (!this._this
.isRowSelected(this.rowId
)) {
748 this._this
.deselectAll();
749 this._this
.selectRow(this.rowId
);
753 tr
.addEvent('click', function(e
) {
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
);
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
);
768 this._this
.deselectAll();
769 this._this
.selectRow(this.rowId
);
773 tr
.addEvent('touchstart', function(e
) {
774 if (!this._this
.isRowSelected(this.rowId
)) {
775 this._this
.deselectAll();
776 this._this
.selectRow(this.rowId
);
780 tr
.addEvent('keydown', function(event
) {
783 this._this
.selectPreviousRow();
786 this._this
.selectNextRow();
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');
801 if (rowPos
>= trs
.length
) {
802 tr
.inject(this.tableBody
);
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)) {
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
);
839 removeRow: function(rowId
) {
840 this.selectedRows
.erase(rowId
);
841 const tr
= this.getTrByRowId(rowId
);
844 this.rows
.erase(rowId
);
853 const trs
= this.tableBody
.getElements('tr');
854 while (trs
.length
> 0) {
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
) {
880 const isLastRowSelected
= (selectedIndex
>= (visibleRows
.length
- 1));
881 if (!isLastRowSelected
) {
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
) {
902 const isFirstRowSelected
= selectedIndex
<= 0;
903 if (!isFirstRowSelected
) {
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() {
963 this.columns
['state_icon'].updateTd = function(td
, row
) {
964 let state
= this.getRowValue(row
);
972 state
= "downloading";
973 img_path
= "images/downloading.svg";
978 img_path
= "images/upload.svg";
982 img_path
= "images/stalledUP.svg";
986 img_path
= "images/stalledDL.svg";
989 state
= "torrent-stop";
990 img_path
= "images/stopped.svg";
993 state
= "checked-completed";
994 img_path
= "images/checked-completed.svg";
999 img_path
= "images/queued.svg";
1003 case "queuedForChecking":
1004 case "checkingResumeData":
1005 state
= "force-recheck";
1006 img_path
= "images/force-recheck.svg";
1010 img_path
= "images/set-location.svg";
1014 case "missingFiles":
1016 img_path
= "images/error.svg";
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
);
1030 td
.adopt(new Element('img', {
1032 'class': 'stateIcon',
1039 this.columns
['status'].updateTd = function(td
, row
) {
1040 const state
= this.getRowValue(row
);
1047 status
= "QBT_TR(Downloading)QBT_TR[CONTEXT=TransferListDelegate]";
1050 status
= "QBT_TR(Stalled)QBT_TR[CONTEXT=TransferListDelegate]";
1053 status
= "QBT_TR(Downloading metadata)QBT_TR[CONTEXT=TransferListDelegate]";
1055 case "forcedMetaDL":
1056 status
= "QBT_TR([F] Downloading metadata)QBT_TR[CONTEXT=TransferListDelegate]";
1059 status
= "QBT_TR([F] Downloading)QBT_TR[CONTEXT=TransferListDelegate]";
1063 status
= "QBT_TR(Seeding)QBT_TR[CONTEXT=TransferListDelegate]";
1066 status
= "QBT_TR([F] Seeding)QBT_TR[CONTEXT=TransferListDelegate]";
1070 status
= "QBT_TR(Queued)QBT_TR[CONTEXT=TransferListDelegate]";
1074 status
= "QBT_TR(Checking)QBT_TR[CONTEXT=TransferListDelegate]";
1076 case "queuedForChecking":
1077 status
= "QBT_TR(Queued for checking)QBT_TR[CONTEXT=TransferListDelegate]";
1079 case "checkingResumeData":
1080 status
= "QBT_TR(Checking resume data)QBT_TR[CONTEXT=TransferListDelegate]";
1083 status
= "QBT_TR(Stopped)QBT_TR[CONTEXT=TransferListDelegate]";
1086 status
= "QBT_TR(Completed)QBT_TR[CONTEXT=TransferListDelegate]";
1089 status
= "QBT_TR(Moving)QBT_TR[CONTEXT=TransferListDelegate]";
1091 case "missingFiles":
1092 status
= "QBT_TR(Missing Files)QBT_TR[CONTEXT=TransferListDelegate]";
1095 status
= "QBT_TR(Errored)QBT_TR[CONTEXT=TransferListDelegate]";
1098 status
= "QBT_TR(Unknown)QBT_TR[CONTEXT=HttpServer]";
1101 td
.set('text', status
);
1102 td
.set('title', status
);
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
);
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
;
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
;
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];
1151 div
.setWidth(ProgressColumnWidth
- 5);
1153 if (div
.getValue() != progressFormatted
)
1154 div
.setValue(progressFormatted
);
1157 if (ProgressColumnWidth
< 0)
1158 ProgressColumnWidth
= td
.offsetWidth
;
1159 td
.adopt(new window
.qBittorrent
.ProgressBar
.ProgressBar(progressFormatted
.toFloat(), {
1160 'width': ProgressColumnWidth
- 5
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
;
1175 this.columns
[columnName
].updateTd(td
, this.rows
.get(trs
[i
].rowId
));
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
);
1199 return compareNumbers(num_seeds1
, num_seeds2
);
1203 this.columns
['num_leechs'].updateTd
= this.columns
['num_seeds'].updateTd
;
1204 this.columns
['num_leechs'].compareRows
= this.columns
['num_seeds'].compareRows
;
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
);
1214 this.columns
['upspeed'].updateTd
= this.columns
['dlspeed'].updateTd
;
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
);
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
);
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
);
1239 this.columns
['completion_on'].updateTd = function(td
, row
) {
1240 const val
= this.getRowValue(row
);
1241 if ((val
=== 0xffffffff) || (val
< 0)) {
1243 td
.set('title', '');
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
);
1256 td
.set('text', '∞');
1257 td
.set('title', '∞');
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
;
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
);
1289 this.columns
['completed'].updateTd
= this.columns
['size'].updateTd
;
1292 this.columns
['max_ratio'].updateTd
= this.columns
['ratio'].updateTd
;
1295 this.columns
['seen_complete'].updateTd
= this.columns
['completion_on'].updateTd
;
1298 this.columns
['last_activity'].updateTd = function(td
, row
) {
1299 const val
= this.getRowValue(row
);
1301 td
.set('text', '∞');
1302 td
.set('title', '∞');
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
);
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
);
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;
1332 switch (filterName
) {
1334 if ((state
!= 'downloading') && (state
.indexOf('DL') === -1))
1338 if (state
!= 'uploading' && state
!= 'forcedUP' && state
!= 'stalledUP' && state
!= 'queuedUP' && state
!= 'checkingUP')
1342 if ((state
!= 'uploading') && (state
.indexOf('UP') === -1))
1346 if (state
.indexOf('stopped') === -1)
1350 if (state
.indexOf('stopped') > -1)
1354 if ((state
!= 'stalledUP') && (state
!= 'stalledDL'))
1357 case 'stalled_uploading':
1358 if (state
!= 'stalledUP')
1361 case 'stalled_downloading':
1362 if (state
!= 'stalledDL')
1369 if (state
== 'stalledDL')
1370 r
= (row
['full_data'].upspeed
> 0);
1372 r
= state
== 'metaDL' || state
== 'forcedMetaDL' || state
== 'downloading' || state
== 'forcedDL' || state
== 'uploading' || state
== 'forcedUP';
1377 if (state
!== 'checkingUP' && state
!== 'checkingDL' && state
!== 'checkingResumeData')
1381 if (state
!== 'moving')
1385 if (state
!= 'error' && state
!= "unknown" && state
!= "missingFiles")
1390 switch (categoryHash
) {
1391 case CATEGORIES_ALL
:
1392 break; // do nothing
1393 case CATEGORIES_UNCATEGORIZED
:
1394 if (row
['full_data'].category
.length
!== 0)
1396 break; // do nothing
1398 if (!useSubcategories
) {
1399 if (categoryHash
!== window
.qBittorrent
.Client
.genHash(row
['full_data'].category
))
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
))
1416 break; // do nothing
1419 if (row
['full_data'].tags
.length
!== 0)
1421 break; // do nothing
1424 const tagHashes
= row
['full_data'].tags
.split(', ').map(tag
=> window
.qBittorrent
.Client
.genHash(tag
));
1425 if (!tagHashes
.contains(tagHash
))
1431 const trackerHashInt
= Number
.parseInt(trackerHash
, 10);
1432 switch (trackerHashInt
) {
1434 break; // do nothing
1435 case TRACKERS_TRACKERLESS
:
1436 if (row
['full_data'].trackers_count
!== 0)
1440 const tracker
= trackerList
.get(trackerHashInt
);
1443 for (const torrents
of tracker
.trackerTorrentMap
.values()) {
1444 if (torrents
.includes(row
['full_data'].rowId
)) {
1456 if ((filterTerms
!== undefined) && (filterTerms
!== null)) {
1457 if (filterTerms
instanceof RegExp
) {
1458 if (!filterTerms
.test(name
))
1462 if ((filterTerms
.length
> 0) && !window
.qBittorrent
.Misc
.containsAllTerms(name
, filterTerms
))
1470 getFilteredTorrentsNumber: function(filterName
, categoryHash
, tagHash
, trackerHash
) {
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))
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']);
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(" "))
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')
1518 return filteredRows
;
1521 setupTr: function(tr
) {
1522 tr
.addEvent('dblclick', function(e
) {
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)
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() {
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();
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
);
1593 td
.adopt(new Element('img', {
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
) {
1618 this.columns
['flags'].updateTd = function(td
, row
) {
1619 td
.set('text', this.getRowValue(row
, 0));
1620 td
.set('title', this.getRowValue(row
, 1));
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
);
1639 td
.set('title', '');
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
;
1658 this.columns
['relevance'].updateTd
= this.columns
['progress'].updateTd
;
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
;
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
;
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
))
1748 if ((filterTerms
.length
> 0) && !window
.qBittorrent
.Misc
.containsAllTerms(row
.full_data
.fileName
, filterTerms
))
1750 if ((sizeFilters
.min
> 0.00) && (row
.full_data
.fileSize
< sizeFilters
.min
))
1752 if ((sizeFilters
.max
> 0.00) && (row
.full_data
.fileSize
> sizeFilters
.max
))
1754 if ((seedsFilters
.min
> 0) && (row
.full_data
.nbSeeders
< seedsFilters
.min
))
1756 if ((seedsFilters
.max
> 0) && (row
.full_data
.nbSeeders
> seedsFilters
.max
))
1759 filteredRows
.push(row
);
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')
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
);
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");
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
,
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);
1851 _addNodeToTable: function(node
, depth
) {
1854 if (node
.isFolder
) {
1858 checked
: node
.checked
,
1860 original
: node
.original
,
1861 renamed
: node
.renamed
1865 node
.full_data
= data
;
1866 this.updateRowData(data
);
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);
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
];
1923 cb
.indeterminate
= false;
1924 cb
.state
= "checked";
1926 node
.full_data
.checked
= node
.checked
;
1929 let cb
= checkboxes
[i
];
1931 cb
.indeterminate
= false;
1932 cb
.state
= "unchecked";
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
)
1964 const isAllUnchecked = function() {
1965 for (let i
= 0; i
< checkboxes
.length
; ++i
) {
1966 if (checkboxes
[i
].checked
)
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;
1982 checkbox
.state
= "partial";
1983 checkbox
.indeterminate
= true;
1984 checkbox
.checked
= false;
1988 initColumnsFunctions: function() {
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',
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
);
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
;
2031 // just update file name
2032 $(fileNameId
).set('text', value
);
2035 const span
= new Element('span', {
2039 const dirImg
= new Element('img', {
2040 src
: 'images/directory.svg',
2044 'margin-bottom': -3,
2045 'margin-left': (node
.depth
* 20)
2049 const html
= dirImg
.outerHTML
+ span
.outerHTML
;
2050 td
.set('html', html
);
2054 const value
= this.getRowValue(row
);
2055 const span
= new Element('span', {
2059 'margin-left': ((node
.depth
+ 1) * 20)
2062 td
.set('html', span
.outerHTML
);
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', {
2074 id
: fileNameRenamedId
,
2076 td
.set('html', span
.outerHTML
);
2080 onRowSelectionChange: function(row
) {},
2082 selectRow: function() {
2086 reselectRows: function(rowIds
) {
2089 this.tableBody
.getElements('tr').each(function(tr
) {
2090 if (rowIds
.indexOf(tr
.rowId
) > -1) {
2091 const node
= that
.getNode(tr
.rowId
);
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"))
2114 tr
.removeClass("nonAlt");
2117 tr
.removeClass("alt");
2118 tr
.addClass("nonAlt");
2120 addClass
= !addClass
;
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
)
2132 if (node2
.isFolder
&& !node1
.isFolder
)
2136 const res
= column
.compareRows(row1
, row2
);
2137 return (this.reverseSort
=== '0') ? res
: -res
;
2140 nodes
.each(function(node
) {
2141 if (node
.children
.length
> 0)
2142 this._sortNodesByColumn(node
.children
, column
);
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);
2154 const row
= this.getRow(node
);
2155 filteredRows
.push(row
);
2160 if (window
.qBittorrent
.Misc
.containsAllTerms(node
.original
, filterTerms
)) {
2161 const row
= this.getRow(node
);
2162 filteredRows
.push(row
);
2169 setFilter: function(text
) {
2170 const filterTerms
= text
.trim().toLowerCase().split(' ');
2171 if ((filterTerms
.length
=== 1) && (filterTerms
[0] === ''))
2172 this.filterTerms
= [];
2174 this.filterTerms
= filterTerms
;
2177 getFilteredAndSortedRows: function() {
2178 if (this.getRoot() === null)
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
);
2194 return filteredRows
;
2197 const filteredRows
= [];
2198 this.getRoot().children
.each(function(child
) {
2199 this._filterNodes(child
, this.filterTerms
, filteredRows
);
2201 filteredRows
.reverse();
2202 return filteredRows
;
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
);
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
);
2239 row
.full_data
.remaining
= 0;
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
) {
2248 qBittorrent
.PropFiles
.collapseFolder(this._this
.getSelectedRowId());
2251 qBittorrent
.PropFiles
.expandFolder(this._this
.getSelectedRowId());
2258 const TorrentFilesTable
= new Class({
2259 Extends
: DynamicTable
,
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);
2276 _addNodeToTable: function(node
, depth
) {
2279 if (node
.isFolder
) {
2283 checked
: node
.checked
,
2284 remaining
: node
.remaining
,
2285 progress
: node
.progress
,
2286 priority
: window
.qBittorrent
.PropFiles
.normalizePriority(node
.priority
),
2287 availability
: node
.availability
,
2293 node
.full_data
= data
;
2294 this.updateRowData(data
);
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);
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() {
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
);
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
);
2354 const treeImg
= new Element('img', {
2355 src
: 'images/L.gif',
2360 td
.adopt(treeImg
, window
.qBittorrent
.PropFiles
.createDownloadCheckbox(id
, row
.full_data
.fileId
, value
));
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
;
2375 // just update file name
2376 $(fileNameId
).set('text', value
);
2379 const collapseIcon
= new Element('img', {
2380 src
: 'images/go-down.svg',
2382 'margin-left': (node
.depth
* 20)
2384 class: "filesTableCollapseIcon",
2387 onclick
: "qBittorrent.PropFiles.collapseIconClicked(this)"
2389 const span
= new Element('span', {
2393 const dirImg
= new Element('img', {
2394 src
: 'images/directory.svg',
2402 const html
= collapseIcon
.outerHTML
+ dirImg
.outerHTML
+ span
.outerHTML
;
2403 td
.set('html', html
);
2407 const value
= this.getRowValue(row
);
2408 const span
= new Element('span', {
2412 'margin-left': ((node
.depth
+ 1) * 20)
2415 td
.set('html', span
.outerHTML
);
2420 this.columns
['size'].updateTd
= displaySize
;
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(), {
2435 progressBar
.setValue(value
.toFloat());
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
);
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"))
2464 tr
.removeClass("nonAlt");
2467 tr
.removeClass("alt");
2468 tr
.addClass("nonAlt");
2470 addClass
= !addClass
;
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
)
2482 if (node2
.isFolder
&& !node1
.isFolder
)
2486 const res
= column
.compareRows(row1
, row2
);
2487 return (this.reverseSort
=== '0') ? res
: -res
;
2490 nodes
.each(function(node
) {
2491 if (node
.children
.length
> 0)
2492 this._sortNodesByColumn(node
.children
, column
);
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);
2504 const row
= this.getRow(node
);
2505 filteredRows
.push(row
);
2510 if (window
.qBittorrent
.Misc
.containsAllTerms(node
.name
, filterTerms
)) {
2511 const row
= this.getRow(node
);
2512 filteredRows
.push(row
);
2519 setFilter: function(text
) {
2520 const filterTerms
= text
.trim().toLowerCase().split(' ');
2521 if ((filterTerms
.length
=== 1) && (filterTerms
[0] === ''))
2522 this.filterTerms
= [];
2524 this.filterTerms
= filterTerms
;
2527 getFilteredAndSortedRows: function() {
2528 if (this.getRoot() === null)
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
);
2544 return filteredRows
;
2547 const filteredRows
= [];
2548 this.getRoot().children
.each(function(child
) {
2549 this._filterNodes(child
, this.filterTerms
, filteredRows
);
2551 filteredRows
.reverse();
2552 return filteredRows
;
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
);
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
);
2589 row
.full_data
.remaining
= 0;
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
) {
2598 qBittorrent
.PropFiles
.collapseFolder(this._this
.getSelectedRowId());
2601 qBittorrent
.PropFiles
.expandFolder(this._this
.getSelectedRowId());
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
);
2634 this.onSelectedRowChanged();
2636 const rows
= this.rows
.getValues();
2638 for (let i
= 0; i
< rows
.length
; ++i
) {
2639 if (rows
[i
].rowId
=== rowId
) {
2640 path
= rows
[i
].full_data
.dataPath
;
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
);
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
);
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() {
2671 this.rows
.each(row
=> {
2673 switch (row
.full_data
.status
) {
2675 img_path
= 'images/application-rss.svg';
2678 img_path
= 'images/task-reject.svg';
2681 img_path
= 'images/spinner.gif';
2684 img_path
= 'images/mail-inbox.svg';
2687 img_path
= 'images/folder-documents.svg';
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];
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
);
2705 td
.adopt(new Element('img', {
2707 'class': 'stateIcon',
2714 newColumn: function(name
, style
, caption
, defaultWidth
, defaultVisible
) {
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)
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';
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
);
2773 this.onSelectedRowChanged();
2775 const rows
= this.rows
.getValues();
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');
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
]);
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');
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
);
2810 newColumn: function(name
, style
, caption
, defaultWidth
, defaultVisible
) {
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)
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';
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({
2872 checked
: this.checked
2874 window
.qBittorrent
.RssDownloader
.modifyRuleState(row
.full_data
.name
, 'enabled', this.checked
);
2875 e
.stopPropagation();
2878 td
.append(checkbox
);
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
);
2896 newColumn: function(name
, style
, caption
, defaultWidth
, defaultVisible
) {
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)
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
);
2936 this.onSelectedRowChanged();
2938 const rows
= this.rows
.getValues();
2940 for (let i
= 0; i
< rows
.length
; ++i
) {
2941 if (rows
[i
].rowId
=== rowId
) {
2942 name
= rows
[i
].full_data
.name
;
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({
2966 checked
: this.checked
2968 e
.stopPropagation();
2971 td
.append(checkbox
);
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
) {
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)
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
) {
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)
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');
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
);
3093 const LogMessageTable
= new Class({
3094 Extends
: DynamicTable
,
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()) {
3121 logLevel
= 'QBT_TR(Normal)QBT_TR[CONTEXT=ExecutionLogWidget]';
3122 addClass
= 'logNormal';
3125 logLevel
= 'QBT_TR(Info)QBT_TR[CONTEXT=ExecutionLogWidget]';
3126 addClass
= 'logInfo';
3129 logLevel
= 'QBT_TR(Warning)QBT_TR[CONTEXT=ExecutionLogWidget]';
3130 addClass
= 'logWarning';
3133 logLevel
= 'QBT_TR(Critical)QBT_TR[CONTEXT=ExecutionLogWidget]';
3134 addClass
= 'logCritical';
3137 logLevel
= 'QBT_TR(Unknown)QBT_TR[CONTEXT=ExecutionLogWidget]';
3138 addClass
= 'logUnknown';
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)
3157 if (filterTerms
.length
> 0 && !window
.qBittorrent
.Misc
.containsAllTerms(rows
[i
].full_data
.message
, filterTerms
))
3160 filteredRows
.push(rows
[i
]);
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
;
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';
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
))
3223 filteredRows
.push(rows
[i
]);
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
;
3236 return filteredRows
;
3243 Object
.freeze(window
.qBittorrent
.DynamicTable
);
3245 /*************************************************************/