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 window
.qBittorrent
??= {};
37 window
.qBittorrent
.DynamicTable
??= (() => {
38 const exports
= () => {
40 TorrentsTable
: TorrentsTable
,
41 TorrentPeersTable
: TorrentPeersTable
,
42 SearchResultsTable
: SearchResultsTable
,
43 SearchPluginsTable
: SearchPluginsTable
,
44 TorrentTrackersTable
: TorrentTrackersTable
,
45 BulkRenameTorrentFilesTable
: BulkRenameTorrentFilesTable
,
46 TorrentFilesTable
: TorrentFilesTable
,
47 LogMessageTable
: LogMessageTable
,
48 LogPeerTable
: LogPeerTable
,
49 RssFeedTable
: RssFeedTable
,
50 RssArticleTable
: RssArticleTable
,
51 RssDownloaderRulesTable
: RssDownloaderRulesTable
,
52 RssDownloaderFeedSelectionTable
: RssDownloaderFeedSelectionTable
,
53 RssDownloaderArticlesTable
: RssDownloaderArticlesTable
57 const compareNumbers
= (val1
, val2
) => {
65 let DynamicTableHeaderContextMenuClass
= null;
66 let ProgressColumnWidth
= -1;
68 const DynamicTable
= new Class({
70 initialize: function() {},
72 setup: function(dynamicTableDivId
, dynamicTableFixedHeaderDivId
, contextMenu
) {
73 this.dynamicTableDivId
= dynamicTableDivId
;
74 this.dynamicTableFixedHeaderDivId
= dynamicTableFixedHeaderDivId
;
75 this.fixedTableHeader
= $(dynamicTableFixedHeaderDivId
).getElements("tr")[0];
76 this.hiddenTableHeader
= $(dynamicTableDivId
).getElements("tr")[0];
77 this.tableBody
= $(dynamicTableDivId
).getElements("tbody")[0];
78 this.rows
= new Hash();
79 this.selectedRows
= [];
81 this.contextMenu
= contextMenu
;
82 this.sortedColumn
= LocalPreferences
.get("sorted_column_" + this.dynamicTableDivId
, 0);
83 this.reverseSort
= LocalPreferences
.get("reverse_sort_" + this.dynamicTableDivId
, "0");
85 this.loadColumnsOrder();
86 this.updateTableHeaders();
87 this.setupCommonEvents();
88 this.setupHeaderEvents();
89 this.setupHeaderMenu();
90 this.setSortedColumnIcon(this.sortedColumn
, null, (this.reverseSort
=== "1"));
94 setupCommonEvents: function() {
95 const tableDiv
= $(this.dynamicTableDivId
);
96 const tableFixedHeaderDiv
= $(this.dynamicTableFixedHeaderDivId
);
98 const tableElement
= tableFixedHeaderDiv
.querySelector("table");
99 tableDiv
.addEventListener("scroll", () => {
100 tableElement
.style
.left
= `${-tableDiv.scrollLeft}px`;
103 // if the table exists within a panel
104 const parentPanel
= tableDiv
.getParent(".panel");
106 const resizeFn
= (entries
) => {
107 const panel
= entries
[0].target
;
108 let h
= panel
.getBoundingClientRect().height
- tableFixedHeaderDiv
.getBoundingClientRect().height
;
109 tableDiv
.style
.height
= `${h}px`;
111 // 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 tableDiv
.style
.height
= `${h}px`;
122 const resizeDebouncer
= window
.qBittorrent
.Misc
.createDebounceHandler(100, (entries
) => {
126 const resizeObserver
= new ResizeObserver(resizeDebouncer
);
127 resizeObserver
.observe(parentPanel
, { box
: "border-box" });
131 setupHeaderEvents: function() {
132 this.currentHeaderAction
= "";
133 this.canResize
= false;
135 const resetElementBorderStyle = function(el
, side
) {
136 if ((side
=== "left") || (side
!== "right"))
137 el
.style
.borderLeft
= "";
138 if ((side
=== "right") || (side
!== "left"))
139 el
.style
.borderRight
= "";
142 const mouseMoveFn = function(e
) {
143 const brect
= e
.target
.getBoundingClientRect();
144 const mouseXRelative
= e
.clientX
- brect
.left
;
145 if (this.currentHeaderAction
=== "") {
146 if ((brect
.width
- mouseXRelative
) < 5) {
147 this.resizeTh
= e
.target
;
148 this.canResize
= true;
149 e
.target
.getParent("tr").style
.cursor
= "col-resize";
151 else if ((mouseXRelative
< 5) && e
.target
.getPrevious('[class=""]')) {
152 this.resizeTh
= e
.target
.getPrevious('[class=""]');
153 this.canResize
= true;
154 e
.target
.getParent("tr").style
.cursor
= "col-resize";
157 this.canResize
= false;
158 e
.target
.getParent("tr").style
.cursor
= "";
161 if (this.currentHeaderAction
=== "drag") {
162 const previousVisibleSibling
= e
.target
.getPrevious('[class=""]');
163 let borderChangeElement
= previousVisibleSibling
;
164 let changeBorderSide
= "right";
166 if (mouseXRelative
> (brect
.width
/ 2)) {
167 borderChangeElement
= e
.target
;
168 this.dropSide
= "right";
171 this.dropSide
= "left";
174 e
.target
.getParent("tr").style
.cursor
= "move";
176 if (!previousVisibleSibling
) { // right most column
177 borderChangeElement
= e
.target
;
179 if (mouseXRelative
<= (brect
.width
/ 2))
180 changeBorderSide
= "left";
183 const borderStyle
= "initial solid #e60";
184 if (changeBorderSide
=== "left")
185 borderChangeElement
.style
.borderLeft
= borderStyle
;
187 borderChangeElement
.style
.borderRight
= borderStyle
;
189 resetElementBorderStyle(borderChangeElement
, ((changeBorderSide
=== "right") ? "left" : "right"));
191 borderChangeElement
.getSiblings('[class=""]').each((el
) => {
192 resetElementBorderStyle(el
);
195 this.lastHoverTh
= e
.target
;
196 this.lastClientX
= e
.clientX
;
199 const mouseOutFn = function(e
) {
200 resetElementBorderStyle(e
.target
);
203 const onBeforeStart = function(el
) {
205 this.currentHeaderAction
= "start";
206 this.dragMovement
= false;
207 this.dragStartX
= this.lastClientX
;
210 const onStart = function(el
, event
) {
211 if (this.canResize
) {
212 this.currentHeaderAction
= "resize";
213 this.startWidth
= parseInt(this.resizeTh
.style
.width
, 10);
216 this.currentHeaderAction
= "drag";
217 el
.style
.backgroundColor
= "#C1D5E7";
221 const onDrag = function(el
, event
) {
222 if (this.currentHeaderAction
=== "resize") {
223 let width
= this.startWidth
+ (event
.event
.pageX
- this.dragStartX
);
226 this.columns
[this.resizeTh
.columnName
].width
= width
;
227 this.updateColumn(this.resizeTh
.columnName
);
231 const onComplete = function(el
, event
) {
232 resetElementBorderStyle(this.lastHoverTh
);
233 el
.style
.backgroundColor
= "";
234 if (this.currentHeaderAction
=== "resize")
235 LocalPreferences
.set("column_" + this.resizeTh
.columnName
+ "_width_" + this.dynamicTableDivId
, this.columns
[this.resizeTh
.columnName
].width
);
236 if ((this.currentHeaderAction
=== "drag") && (el
!== this.lastHoverTh
)) {
237 this.saveColumnsOrder();
238 const val
= LocalPreferences
.get("columns_order_" + this.dynamicTableDivId
).split(",");
239 val
.erase(el
.columnName
);
240 let pos
= val
.indexOf(this.lastHoverTh
.columnName
);
241 if (this.dropSide
=== "right")
243 val
.splice(pos
, 0, el
.columnName
);
244 LocalPreferences
.set("columns_order_" + this.dynamicTableDivId
, val
.join(","));
245 this.loadColumnsOrder();
246 this.updateTableHeaders();
247 while (this.tableBody
.firstChild
)
248 this.tableBody
.removeChild(this.tableBody
.firstChild
);
249 this.updateTable(true);
251 if (this.currentHeaderAction
=== "drag") {
252 resetElementBorderStyle(el
);
253 el
.getSiblings('[class=""]').each((el
) => {
254 resetElementBorderStyle(el
);
257 this.currentHeaderAction
= "";
260 const onCancel = function(el
) {
261 this.currentHeaderAction
= "";
262 this.setSortedColumn(el
.columnName
);
265 const onTouch = function(e
) {
266 const column
= e
.target
.columnName
;
267 this.currentHeaderAction
= "";
268 this.setSortedColumn(column
);
271 const ths
= this.fixedTableHeader
.getElements("th");
273 for (let i
= 0; i
< ths
.length
; ++i
) {
275 th
.addEventListener("mousemove", mouseMoveFn
);
276 th
.addEventListener("mouseout", mouseOutFn
);
277 th
.addEventListener("touchend", onTouch
, { passive
: true });
283 onBeforeStart
: onBeforeStart
,
286 onComplete
: onComplete
,
292 setupDynamicTableHeaderContextMenuClass: function() {
293 if (!DynamicTableHeaderContextMenuClass
) {
294 DynamicTableHeaderContextMenuClass
= new Class({
295 Extends
: window
.qBittorrent
.ContextMenu
.ContextMenu
,
296 updateMenuItems: function() {
297 for (let i
= 0; i
< this.dynamicTable
.columns
.length
; ++i
) {
298 if (this.dynamicTable
.columns
[i
].caption
=== "")
300 if (this.dynamicTable
.columns
[i
].visible
!== "0")
301 this.setItemChecked(this.dynamicTable
.columns
[i
].name
, true);
303 this.setItemChecked(this.dynamicTable
.columns
[i
].name
, false);
310 showColumn: function(columnName
, show
) {
311 this.columns
[columnName
].visible
= show
? "1" : "0";
312 LocalPreferences
.set("column_" + columnName
+ "_visible_" + this.dynamicTableDivId
, show
? "1" : "0");
313 this.updateColumn(columnName
);
316 setupHeaderMenu: function() {
317 this.setupDynamicTableHeaderContextMenuClass();
319 const menuId
= this.dynamicTableDivId
+ "_headerMenu";
321 // reuse menu if already exists
322 const ul
= $(menuId
) ?? new Element("ul", {
324 class: "contextMenu scrollableMenu"
327 const createLi = function(columnName
, text
) {
328 const anchor
= document
.createElement("a");
329 anchor
.href
= `#${columnName}`;
330 anchor
.textContent
= text
;
332 const img
= document
.createElement("img");
333 img
.src
= "images/checked-completed.svg";
336 const listItem
= document
.createElement("li");
337 listItem
.appendChild(anchor
);
344 const onMenuItemClicked = function(element
, ref
, action
) {
345 this.showColumn(action
, this.columns
[action
].visible
=== "0");
348 // recreate child nodes when reusing (enables the context menu to work correctly)
349 if (ul
.hasChildNodes()) {
350 while (ul
.firstChild
)
351 ul
.removeChild(ul
.lastChild
);
354 for (let i
= 0; i
< this.columns
.length
; ++i
) {
355 const text
= this.columns
[i
].caption
;
358 ul
.appendChild(createLi(this.columns
[i
].name
, text
));
359 actions
[this.columns
[i
].name
] = onMenuItemClicked
;
362 ul
.inject(document
.body
);
364 this.headerContextMenu
= new DynamicTableHeaderContextMenuClass({
365 targets
: "#" + this.dynamicTableFixedHeaderDivId
+ " tr",
374 this.headerContextMenu
.dynamicTable
= this;
377 initColumns: function() {},
379 newColumn: function(name
, style
, caption
, defaultWidth
, defaultVisible
) {
381 column
["name"] = name
;
382 column
["title"] = name
;
383 column
["visible"] = LocalPreferences
.get("column_" + name
+ "_visible_" + this.dynamicTableDivId
, defaultVisible
? "1" : "0");
384 column
["force_hide"] = false;
385 column
["caption"] = caption
;
386 column
["style"] = style
;
387 column
["width"] = LocalPreferences
.get("column_" + name
+ "_width_" + this.dynamicTableDivId
, defaultWidth
);
388 column
["dataProperties"] = [name
];
389 column
["getRowValue"] = function(row
, pos
) {
390 if (pos
=== undefined)
392 return row
["full_data"][this.dataProperties
[pos
]];
394 column
["compareRows"] = function(row1
, row2
) {
395 const value1
= this.getRowValue(row1
);
396 const value2
= this.getRowValue(row2
);
397 if ((typeof(value1
) === "number") && (typeof(value2
) === "number"))
398 return compareNumbers(value1
, value2
);
399 return window
.qBittorrent
.Misc
.naturalSortCollator
.compare(value1
, value2
);
401 column
["updateTd"] = function(td
, row
) {
402 const value
= this.getRowValue(row
);
403 td
.textContent
= value
;
406 column
["onResize"] = null;
407 this.columns
.push(column
);
408 this.columns
[name
] = column
;
410 this.hiddenTableHeader
.appendChild(new Element("th"));
411 this.fixedTableHeader
.appendChild(new Element("th"));
414 loadColumnsOrder: function() {
415 const columnsOrder
= [];
416 const val
= LocalPreferences
.get("columns_order_" + this.dynamicTableDivId
);
417 if ((val
=== null) || (val
=== undefined))
419 val
.split(",").forEach((v
) => {
420 if ((v
in this.columns
) && (!columnsOrder
.contains(v
)))
421 columnsOrder
.push(v
);
424 for (let i
= 0; i
< this.columns
.length
; ++i
) {
425 if (!columnsOrder
.contains(this.columns
[i
].name
))
426 columnsOrder
.push(this.columns
[i
].name
);
429 for (let i
= 0; i
< this.columns
.length
; ++i
)
430 this.columns
[i
] = this.columns
[columnsOrder
[i
]];
433 saveColumnsOrder: function() {
435 for (let i
= 0; i
< this.columns
.length
; ++i
) {
438 val
+= this.columns
[i
].name
;
440 LocalPreferences
.set("columns_order_" + this.dynamicTableDivId
, val
);
443 updateTableHeaders: function() {
444 this.updateHeader(this.hiddenTableHeader
);
445 this.updateHeader(this.fixedTableHeader
);
448 updateHeader: function(header
) {
449 const ths
= header
.getElements("th");
451 for (let i
= 0; i
< ths
.length
; ++i
) {
454 th
.title
= this.columns
[i
].caption
;
455 th
.textContent
= this.columns
[i
].caption
;
456 th
.setAttribute("style", "width: " + this.columns
[i
].width
+ "px;" + this.columns
[i
].style
);
457 th
.columnName
= this.columns
[i
].name
;
458 th
.addClass("column_" + th
.columnName
);
459 if ((this.columns
[i
].visible
=== "0") || this.columns
[i
].force_hide
)
460 th
.addClass("invisible");
462 th
.removeClass("invisible");
466 getColumnPos: function(columnName
) {
467 for (let i
= 0; i
< this.columns
.length
; ++i
) {
468 if (this.columns
[i
].name
=== columnName
)
474 updateColumn: function(columnName
) {
475 const pos
= this.getColumnPos(columnName
);
476 const visible
= ((this.columns
[pos
].visible
!== "0") && !this.columns
[pos
].force_hide
);
477 const ths
= this.hiddenTableHeader
.getElements("th");
478 const fths
= this.fixedTableHeader
.getElements("th");
479 const trs
= this.tableBody
.getElements("tr");
480 const style
= "width: " + this.columns
[pos
].width
+ "px;" + this.columns
[pos
].style
;
482 ths
[pos
].setAttribute("style", style
);
483 fths
[pos
].setAttribute("style", style
);
486 ths
[pos
].removeClass("invisible");
487 fths
[pos
].removeClass("invisible");
488 for (let i
= 0; i
< trs
.length
; ++i
)
489 trs
[i
].getElements("td")[pos
].removeClass("invisible");
492 ths
[pos
].addClass("invisible");
493 fths
[pos
].addClass("invisible");
494 for (let j
= 0; j
< trs
.length
; ++j
)
495 trs
[j
].getElements("td")[pos
].addClass("invisible");
497 if (this.columns
[pos
].onResize
!== null)
498 this.columns
[pos
].onResize(columnName
);
501 getSortedColumn: function() {
502 return LocalPreferences
.get("sorted_column_" + this.dynamicTableDivId
);
506 * @param {string} column name to sort by
507 * @param {string|null} reverse defaults to implementation-specific behavior when not specified. Should only be passed when restoring previous state.
509 setSortedColumn: function(column
, reverse
= null) {
510 if (column
!== this.sortedColumn
) {
511 const oldColumn
= this.sortedColumn
;
512 this.sortedColumn
= column
;
513 this.reverseSort
= reverse
?? "0";
514 this.setSortedColumnIcon(column
, oldColumn
, false);
518 this.reverseSort
= reverse
?? (this.reverseSort
=== "0" ? "1" : "0");
519 this.setSortedColumnIcon(column
, null, (this.reverseSort
=== "1"));
521 LocalPreferences
.set("sorted_column_" + this.dynamicTableDivId
, column
);
522 LocalPreferences
.set("reverse_sort_" + this.dynamicTableDivId
, this.reverseSort
);
523 this.updateTable(false);
526 setSortedColumnIcon: function(newColumn
, oldColumn
, isReverse
) {
527 const getCol = function(headerDivId
, colName
) {
528 const colElem
= $$("#" + headerDivId
+ " .column_" + colName
);
529 if (colElem
.length
=== 1)
534 const colElem
= getCol(this.dynamicTableFixedHeaderDivId
, newColumn
);
535 if (colElem
!== null) {
536 colElem
.addClass("sorted");
538 colElem
.addClass("reverse");
540 colElem
.removeClass("reverse");
542 const oldColElem
= getCol(this.dynamicTableFixedHeaderDivId
, oldColumn
);
543 if (oldColElem
!== null) {
544 oldColElem
.removeClass("sorted");
545 oldColElem
.removeClass("reverse");
549 getSelectedRowId: function() {
550 if (this.selectedRows
.length
> 0)
551 return this.selectedRows
[0];
555 isRowSelected: function(rowId
) {
556 return this.selectedRows
.contains(rowId
);
559 setupAltRow: function() {
560 const useAltRowColors
= (LocalPreferences
.get("use_alt_row_colors", "true") === "true");
562 document
.getElementById(this.dynamicTableDivId
).classList
.add("altRowColors");
565 selectAll: function() {
568 const trs
= this.tableBody
.getElements("tr");
569 for (let i
= 0; i
< trs
.length
; ++i
) {
571 this.selectedRows
.push(tr
.rowId
);
572 if (!tr
.hasClass("selected"))
573 tr
.addClass("selected");
577 deselectAll: function() {
578 this.selectedRows
.empty();
581 selectRow: function(rowId
) {
582 this.selectedRows
.push(rowId
);
584 this.onSelectedRowChanged();
587 deselectRow: function(rowId
) {
588 this.selectedRows
.erase(rowId
);
590 this.onSelectedRowChanged();
593 selectRows: function(rowId1
, rowId2
) {
595 if (rowId1
=== rowId2
) {
596 this.selectRow(rowId1
);
602 this.tableBody
.getElements("tr").each((tr
) => {
603 if ((tr
.rowId
=== rowId1
) || (tr
.rowId
=== rowId2
)) {
605 that
.selectedRows
.push(tr
.rowId
);
608 that
.selectedRows
.push(tr
.rowId
);
612 this.onSelectedRowChanged();
615 reselectRows: function(rowIds
) {
617 this.selectedRows
= rowIds
.slice();
618 this.tableBody
.getElements("tr").each((tr
) => {
619 if (rowIds
.includes(tr
.rowId
))
620 tr
.addClass("selected");
624 setRowClass: function() {
626 this.tableBody
.getElements("tr").each((tr
) => {
627 if (that
.isRowSelected(tr
.rowId
))
628 tr
.addClass("selected");
630 tr
.removeClass("selected");
634 onSelectedRowChanged: function() {},
636 updateRowData: function(data
) {
637 // ensure rowId is a string
638 const rowId
= `${data["rowId"]}`;
641 if (!this.rows
.has(rowId
)) {
646 this.rows
.set(rowId
, row
);
649 row
= this.rows
.get(rowId
);
653 for (const x
in data
) {
654 if (!Object
.hasOwn(data
, x
))
656 row
["full_data"][x
] = data
[x
];
660 getRow: function(rowId
) {
661 return this.rows
.get(rowId
);
664 getFilteredAndSortedRows: function() {
665 const filteredRows
= [];
667 const rows
= this.rows
.getValues();
669 for (let i
= 0; i
< rows
.length
; ++i
) {
670 filteredRows
.push(rows
[i
]);
671 filteredRows
[rows
[i
].rowId
] = rows
[i
];
674 filteredRows
.sort((row1
, row2
) => {
675 const column
= this.columns
[this.sortedColumn
];
676 const res
= column
.compareRows(row1
, row2
);
677 if (this.reverseSort
=== "0")
685 getTrByRowId: function(rowId
) {
686 const trs
= this.tableBody
.getElements("tr");
687 for (let i
= 0; i
< trs
.length
; ++i
) {
688 if (trs
[i
].rowId
=== rowId
)
694 updateTable: function(fullUpdate
= false) {
695 const rows
= this.getFilteredAndSortedRows();
697 for (let i
= 0; i
< this.selectedRows
.length
; ++i
) {
698 if (!(this.selectedRows
[i
] in rows
)) {
699 this.selectedRows
.splice(i
, 1);
704 const trs
= this.tableBody
.getElements("tr");
706 for (let rowPos
= 0; rowPos
< rows
.length
; ++rowPos
) {
707 const rowId
= rows
[rowPos
]["rowId"];
708 let tr_found
= false;
709 for (let j
= rowPos
; j
< trs
.length
; ++j
) {
710 if (trs
[j
]["rowId"] === rowId
) {
714 trs
[j
].inject(trs
[rowPos
], "before");
715 const tmpTr
= trs
[j
];
717 trs
.splice(rowPos
, 0, tmpTr
);
721 if (tr_found
) { // row already exists in the table
722 this.updateRow(trs
[rowPos
], fullUpdate
);
724 else { // else create a new row in the table
725 const tr
= new Element("tr");
726 // set tabindex so element receives keydown events
727 // more info: https://developer.mozilla.org/en-US/docs/Web/API/Element/keydown_event
730 const rowId
= rows
[rowPos
]["rowId"];
731 tr
.setAttribute("data-row-id", rowId
);
735 tr
.addEventListener("contextmenu", function(e
) {
736 if (!this._this
.isRowSelected(this.rowId
)) {
737 this._this
.deselectAll();
738 this._this
.selectRow(this.rowId
);
742 tr
.addEventListener("click", function(e
) {
746 if (e
.ctrlKey
|| e
.metaKey
) {
747 // CTRL/CMD ⌘ key was pressed
748 if (this._this
.isRowSelected(this.rowId
))
749 this._this
.deselectRow(this.rowId
);
751 this._this
.selectRow(this.rowId
);
753 else if (e
.shiftKey
&& (this._this
.selectedRows
.length
=== 1)) {
754 // Shift key was pressed
755 this._this
.selectRows(this._this
.getSelectedRowId(), this.rowId
);
759 this._this
.deselectAll();
760 this._this
.selectRow(this.rowId
);
764 tr
.addEventListener("touchstart", function(e
) {
765 if (!this._this
.isRowSelected(this.rowId
)) {
766 this._this
.deselectAll();
767 this._this
.selectRow(this.rowId
);
769 }, { passive
: true });
770 tr
.addEventListener("keydown", function(event
) {
773 this._this
.selectPreviousRow();
776 this._this
.selectNextRow();
783 for (let k
= 0; k
< this.columns
.length
; ++k
) {
784 const td
= new Element("td");
785 if ((this.columns
[k
].visible
=== "0") || this.columns
[k
].force_hide
)
786 td
.addClass("invisible");
791 if (rowPos
>= trs
.length
) {
792 tr
.inject(this.tableBody
);
796 tr
.inject(trs
[rowPos
], "before");
797 trs
.splice(rowPos
, 0, tr
);
800 // Update context menu
801 if (this.contextMenu
)
802 this.contextMenu
.addTarget(tr
);
804 this.updateRow(tr
, true);
808 const rowPos
= rows
.length
;
810 while ((rowPos
< trs
.length
) && (trs
.length
> 0))
814 setupTr: function(tr
) {},
816 updateRow: function(tr
, fullUpdate
) {
817 const row
= this.rows
.get(tr
.rowId
);
818 const data
= row
[fullUpdate
? "full_data" : "data"];
820 const tds
= tr
.getElements("td");
821 for (let i
= 0; i
< this.columns
.length
; ++i
) {
822 if (Object
.hasOwn(data
, this.columns
[i
].dataProperties
[0]))
823 this.columns
[i
].updateTd(tds
[i
], row
);
828 removeRow: function(rowId
) {
829 this.selectedRows
.erase(rowId
);
830 if (this.rows
.has(rowId
))
831 this.rows
.erase(rowId
);
832 const tr
= this.getTrByRowId(rowId
);
840 const trs
= this.tableBody
.getElements("tr");
841 while (trs
.length
> 0)
845 selectedRowsIds: function() {
846 return this.selectedRows
.slice();
849 getRowIds: function() {
850 return this.rows
.getKeys();
853 selectNextRow: function() {
854 const visibleRows
= $(this.dynamicTableDivId
).getElements("tbody tr").filter(e
=> e
.style
.display
!== "none");
855 const selectedRowId
= this.getSelectedRowId();
857 let selectedIndex
= -1;
858 for (let i
= 0; i
< visibleRows
.length
; ++i
) {
859 const row
= visibleRows
[i
];
860 if (row
.getAttribute("data-row-id") === selectedRowId
) {
866 const isLastRowSelected
= (selectedIndex
>= (visibleRows
.length
- 1));
867 if (!isLastRowSelected
) {
870 const newRow
= visibleRows
[selectedIndex
+ 1];
871 this.selectRow(newRow
.getAttribute("data-row-id"));
875 selectPreviousRow: function() {
876 const visibleRows
= $(this.dynamicTableDivId
).getElements("tbody tr").filter(e
=> e
.style
.display
!== "none");
877 const selectedRowId
= this.getSelectedRowId();
879 let selectedIndex
= -1;
880 for (let i
= 0; i
< visibleRows
.length
; ++i
) {
881 const row
= visibleRows
[i
];
882 if (row
.getAttribute("data-row-id") === selectedRowId
) {
888 const isFirstRowSelected
= selectedIndex
<= 0;
889 if (!isFirstRowSelected
) {
892 const newRow
= visibleRows
[selectedIndex
- 1];
893 this.selectRow(newRow
.getAttribute("data-row-id"));
898 const TorrentsTable
= new Class({
899 Extends
: DynamicTable
,
901 initColumns: function() {
902 this.newColumn("priority", "", "#", 30, true);
903 this.newColumn("state_icon", "cursor: default", "", 22, true);
904 this.newColumn("name", "", "QBT_TR(Name)QBT_TR[CONTEXT=TransferListModel]", 200, true);
905 this.newColumn("size", "", "QBT_TR(Size)QBT_TR[CONTEXT=TransferListModel]", 100, true);
906 this.newColumn("total_size", "", "QBT_TR(Total Size)QBT_TR[CONTEXT=TransferListModel]", 100, false);
907 this.newColumn("progress", "", "QBT_TR(Progress)QBT_TR[CONTEXT=TransferListModel]", 85, true);
908 this.newColumn("status", "", "QBT_TR(Status)QBT_TR[CONTEXT=TransferListModel]", 100, true);
909 this.newColumn("num_seeds", "", "QBT_TR(Seeds)QBT_TR[CONTEXT=TransferListModel]", 100, true);
910 this.newColumn("num_leechs", "", "QBT_TR(Peers)QBT_TR[CONTEXT=TransferListModel]", 100, true);
911 this.newColumn("dlspeed", "", "QBT_TR(Down Speed)QBT_TR[CONTEXT=TransferListModel]", 100, true);
912 this.newColumn("upspeed", "", "QBT_TR(Up Speed)QBT_TR[CONTEXT=TransferListModel]", 100, true);
913 this.newColumn("eta", "", "QBT_TR(ETA)QBT_TR[CONTEXT=TransferListModel]", 100, true);
914 this.newColumn("ratio", "", "QBT_TR(Ratio)QBT_TR[CONTEXT=TransferListModel]", 100, true);
915 this.newColumn("popularity", "", "QBT_TR(Popularity)QBT_TR[CONTEXT=TransferListModel]", 100, true);
916 this.newColumn("category", "", "QBT_TR(Category)QBT_TR[CONTEXT=TransferListModel]", 100, true);
917 this.newColumn("tags", "", "QBT_TR(Tags)QBT_TR[CONTEXT=TransferListModel]", 100, true);
918 this.newColumn("added_on", "", "QBT_TR(Added On)QBT_TR[CONTEXT=TransferListModel]", 100, true);
919 this.newColumn("completion_on", "", "QBT_TR(Completed On)QBT_TR[CONTEXT=TransferListModel]", 100, false);
920 this.newColumn("tracker", "", "QBT_TR(Tracker)QBT_TR[CONTEXT=TransferListModel]", 100, false);
921 this.newColumn("dl_limit", "", "QBT_TR(Down Limit)QBT_TR[CONTEXT=TransferListModel]", 100, false);
922 this.newColumn("up_limit", "", "QBT_TR(Up Limit)QBT_TR[CONTEXT=TransferListModel]", 100, false);
923 this.newColumn("downloaded", "", "QBT_TR(Downloaded)QBT_TR[CONTEXT=TransferListModel]", 100, false);
924 this.newColumn("uploaded", "", "QBT_TR(Uploaded)QBT_TR[CONTEXT=TransferListModel]", 100, false);
925 this.newColumn("downloaded_session", "", "QBT_TR(Session Download)QBT_TR[CONTEXT=TransferListModel]", 100, false);
926 this.newColumn("uploaded_session", "", "QBT_TR(Session Upload)QBT_TR[CONTEXT=TransferListModel]", 100, false);
927 this.newColumn("amount_left", "", "QBT_TR(Remaining)QBT_TR[CONTEXT=TransferListModel]", 100, false);
928 this.newColumn("time_active", "", "QBT_TR(Time Active)QBT_TR[CONTEXT=TransferListModel]", 100, false);
929 this.newColumn("save_path", "", "QBT_TR(Save path)QBT_TR[CONTEXT=TransferListModel]", 100, false);
930 this.newColumn("completed", "", "QBT_TR(Completed)QBT_TR[CONTEXT=TransferListModel]", 100, false);
931 this.newColumn("max_ratio", "", "QBT_TR(Ratio Limit)QBT_TR[CONTEXT=TransferListModel]", 100, false);
932 this.newColumn("seen_complete", "", "QBT_TR(Last Seen Complete)QBT_TR[CONTEXT=TransferListModel]", 100, false);
933 this.newColumn("last_activity", "", "QBT_TR(Last Activity)QBT_TR[CONTEXT=TransferListModel]", 100, false);
934 this.newColumn("availability", "", "QBT_TR(Availability)QBT_TR[CONTEXT=TransferListModel]", 100, false);
935 this.newColumn("download_path", "", "QBT_TR(Incomplete Save Path)QBT_TR[CONTEXT=TransferListModel]", 100, false);
936 this.newColumn("infohash_v1", "", "QBT_TR(Info Hash v1)QBT_TR[CONTEXT=TransferListModel]", 100, false);
937 this.newColumn("infohash_v2", "", "QBT_TR(Info Hash v2)QBT_TR[CONTEXT=TransferListModel]", 100, false);
938 this.newColumn("reannounce", "", "QBT_TR(Reannounce In)QBT_TR[CONTEXT=TransferListModel]", 100, false);
939 this.newColumn("private", "", "QBT_TR(Private)QBT_TR[CONTEXT=TransferListModel]", 100, false);
941 this.columns
["state_icon"].onclick
= "";
942 this.columns
["state_icon"].dataProperties
[0] = "state";
944 this.columns
["num_seeds"].dataProperties
.push("num_complete");
945 this.columns
["num_leechs"].dataProperties
.push("num_incomplete");
946 this.columns
["time_active"].dataProperties
.push("seeding_time");
948 this.initColumnsFunctions();
951 initColumnsFunctions: function() {
954 this.columns
["state_icon"].updateTd = function(td
, row
) {
955 let state
= this.getRowValue(row
);
963 state
= "downloading";
964 img_path
= "images/downloading.svg";
969 img_path
= "images/upload.svg";
973 img_path
= "images/stalledUP.svg";
977 img_path
= "images/stalledDL.svg";
980 state
= "torrent-stop";
981 img_path
= "images/stopped.svg";
984 state
= "checked-completed";
985 img_path
= "images/checked-completed.svg";
990 img_path
= "images/queued.svg";
994 case "queuedForChecking":
995 case "checkingResumeData":
996 state
= "force-recheck";
997 img_path
= "images/force-recheck.svg";
1001 img_path
= "images/set-location.svg";
1005 case "missingFiles":
1007 img_path
= "images/error.svg";
1010 break; // do nothing
1013 if (td
.getChildren("img").length
> 0) {
1014 const img
= td
.getChildren("img")[0];
1015 if (!img
.src
.includes(img_path
)) {
1021 td
.adopt(new Element("img", {
1023 "class": "stateIcon",
1030 this.columns
["status"].updateTd = function(td
, row
) {
1031 const state
= this.getRowValue(row
);
1038 status
= "QBT_TR(Downloading)QBT_TR[CONTEXT=TransferListDelegate]";
1041 status
= "QBT_TR(Stalled)QBT_TR[CONTEXT=TransferListDelegate]";
1044 status
= "QBT_TR(Downloading metadata)QBT_TR[CONTEXT=TransferListDelegate]";
1046 case "forcedMetaDL":
1047 status
= "QBT_TR([F] Downloading metadata)QBT_TR[CONTEXT=TransferListDelegate]";
1050 status
= "QBT_TR([F] Downloading)QBT_TR[CONTEXT=TransferListDelegate]";
1054 status
= "QBT_TR(Seeding)QBT_TR[CONTEXT=TransferListDelegate]";
1057 status
= "QBT_TR([F] Seeding)QBT_TR[CONTEXT=TransferListDelegate]";
1061 status
= "QBT_TR(Queued)QBT_TR[CONTEXT=TransferListDelegate]";
1065 status
= "QBT_TR(Checking)QBT_TR[CONTEXT=TransferListDelegate]";
1067 case "queuedForChecking":
1068 status
= "QBT_TR(Queued for checking)QBT_TR[CONTEXT=TransferListDelegate]";
1070 case "checkingResumeData":
1071 status
= "QBT_TR(Checking resume data)QBT_TR[CONTEXT=TransferListDelegate]";
1074 status
= "QBT_TR(Stopped)QBT_TR[CONTEXT=TransferListDelegate]";
1077 status
= "QBT_TR(Completed)QBT_TR[CONTEXT=TransferListDelegate]";
1080 status
= "QBT_TR(Moving)QBT_TR[CONTEXT=TransferListDelegate]";
1082 case "missingFiles":
1083 status
= "QBT_TR(Missing Files)QBT_TR[CONTEXT=TransferListDelegate]";
1086 status
= "QBT_TR(Errored)QBT_TR[CONTEXT=TransferListDelegate]";
1089 status
= "QBT_TR(Unknown)QBT_TR[CONTEXT=HttpServer]";
1092 td
.textContent
= status
;
1097 this.columns
["priority"].updateTd = function(td
, row
) {
1098 const queuePos
= this.getRowValue(row
);
1099 const formattedQueuePos
= (queuePos
< 1) ? "*" : queuePos
;
1100 td
.textContent
= formattedQueuePos
;
1101 td
.title
= formattedQueuePos
;
1104 this.columns
["priority"].compareRows = function(row1
, row2
) {
1105 let row1_val
= this.getRowValue(row1
);
1106 let row2_val
= this.getRowValue(row2
);
1111 return compareNumbers(row1_val
, row2_val
);
1114 // name, category, tags
1115 this.columns
["name"].compareRows = function(row1
, row2
) {
1116 const row1Val
= this.getRowValue(row1
);
1117 const row2Val
= this.getRowValue(row2
);
1118 return row1Val
.localeCompare(row2Val
, undefined, { numeric
: true, sensitivity
: "base" });
1120 this.columns
["category"].compareRows
= this.columns
["name"].compareRows
;
1121 this.columns
["tags"].compareRows
= this.columns
["name"].compareRows
;
1124 this.columns
["size"].updateTd = function(td
, row
) {
1125 const size
= window
.qBittorrent
.Misc
.friendlyUnit(this.getRowValue(row
), false);
1126 td
.textContent
= size
;
1129 this.columns
["total_size"].updateTd
= this.columns
["size"].updateTd
;
1132 this.columns
["progress"].updateTd = function(td
, row
) {
1133 const progress
= this.getRowValue(row
);
1134 let progressFormatted
= (progress
* 100).round(1);
1135 if ((progressFormatted
=== 100.0) && (progress
!== 1.0))
1136 progressFormatted
= 99.9;
1138 if (td
.getChildren("div").length
> 0) {
1139 const div
= td
.getChildren("div")[0];
1142 div
.setWidth(ProgressColumnWidth
- 5);
1144 if (div
.getValue() !== progressFormatted
)
1145 div
.setValue(progressFormatted
);
1148 if (ProgressColumnWidth
< 0)
1149 ProgressColumnWidth
= td
.offsetWidth
;
1150 td
.adopt(new window
.qBittorrent
.ProgressBar
.ProgressBar(progressFormatted
.toFloat(), {
1151 "width": ProgressColumnWidth
- 5
1157 this.columns
["progress"].onResize = function(columnName
) {
1158 const pos
= this.getColumnPos(columnName
);
1159 const trs
= this.tableBody
.getElements("tr");
1160 ProgressColumnWidth
= -1;
1161 for (let i
= 0; i
< trs
.length
; ++i
) {
1162 const td
= trs
[i
].getElements("td")[pos
];
1163 if (ProgressColumnWidth
< 0)
1164 ProgressColumnWidth
= td
.offsetWidth
;
1166 this.columns
[columnName
].updateTd(td
, this.rows
.get(trs
[i
].rowId
));
1171 this.columns
["num_seeds"].updateTd = function(td
, row
) {
1172 const num_seeds
= this.getRowValue(row
, 0);
1173 const num_complete
= this.getRowValue(row
, 1);
1174 let value
= num_seeds
;
1175 if (num_complete
!== -1)
1176 value
+= " (" + num_complete
+ ")";
1177 td
.textContent
= value
;
1180 this.columns
["num_seeds"].compareRows = function(row1
, row2
) {
1181 const num_seeds1
= this.getRowValue(row1
, 0);
1182 const num_complete1
= this.getRowValue(row1
, 1);
1184 const num_seeds2
= this.getRowValue(row2
, 0);
1185 const num_complete2
= this.getRowValue(row2
, 1);
1187 const result
= compareNumbers(num_complete1
, num_complete2
);
1190 return compareNumbers(num_seeds1
, num_seeds2
);
1194 this.columns
["num_leechs"].updateTd
= this.columns
["num_seeds"].updateTd
;
1195 this.columns
["num_leechs"].compareRows
= this.columns
["num_seeds"].compareRows
;
1198 this.columns
["dlspeed"].updateTd = function(td
, row
) {
1199 const speed
= window
.qBittorrent
.Misc
.friendlyUnit(this.getRowValue(row
), true);
1200 td
.textContent
= speed
;
1205 this.columns
["upspeed"].updateTd
= this.columns
["dlspeed"].updateTd
;
1208 this.columns
["eta"].updateTd = function(td
, row
) {
1209 const eta
= window
.qBittorrent
.Misc
.friendlyDuration(this.getRowValue(row
), window
.qBittorrent
.Misc
.MAX_ETA
);
1210 td
.textContent
= eta
;
1215 this.columns
["ratio"].updateTd = function(td
, row
) {
1216 const ratio
= this.getRowValue(row
);
1217 const string
= (ratio
=== -1) ? "∞" : window
.qBittorrent
.Misc
.toFixedPointString(ratio
, 2);
1218 td
.textContent
= string
;
1223 this.columns
["popularity"].updateTd = function(td
, row
) {
1224 const value
= this.getRowValue(row
);
1225 const popularity
= (value
=== -1) ? "∞" : window
.qBittorrent
.Misc
.toFixedPointString(value
, 2);
1226 td
.textContent
= popularity
;
1227 td
.title
= popularity
;
1231 this.columns
["added_on"].updateTd = function(td
, row
) {
1232 const date
= new Date(this.getRowValue(row
) * 1000).toLocaleString();
1233 td
.textContent
= date
;
1238 this.columns
["completion_on"].updateTd = function(td
, row
) {
1239 const val
= this.getRowValue(row
);
1240 if ((val
=== 0xffffffff) || (val
< 0)) {
1241 td
.textContent
= "";
1245 const date
= new Date(this.getRowValue(row
) * 1000).toLocaleString();
1246 td
.textContent
= date
;
1252 this.columns
["tracker"].updateTd = function(td
, row
) {
1253 const value
= this.getRowValue(row
);
1254 const tracker
= displayFullURLTrackerColumn
? value
: window
.qBittorrent
.Misc
.getHost(value
);
1255 td
.textContent
= tracker
;
1259 // dl_limit, up_limit
1260 this.columns
["dl_limit"].updateTd = function(td
, row
) {
1261 const speed
= this.getRowValue(row
);
1263 td
.textContent
= "∞";
1267 const formattedSpeed
= window
.qBittorrent
.Misc
.friendlyUnit(speed
, true);
1268 td
.textContent
= formattedSpeed
;
1269 td
.title
= formattedSpeed
;
1273 this.columns
["up_limit"].updateTd
= this.columns
["dl_limit"].updateTd
;
1275 // downloaded, uploaded, downloaded_session, uploaded_session, amount_left
1276 this.columns
["downloaded"].updateTd
= this.columns
["size"].updateTd
;
1277 this.columns
["uploaded"].updateTd
= this.columns
["size"].updateTd
;
1278 this.columns
["downloaded_session"].updateTd
= this.columns
["size"].updateTd
;
1279 this.columns
["uploaded_session"].updateTd
= this.columns
["size"].updateTd
;
1280 this.columns
["amount_left"].updateTd
= this.columns
["size"].updateTd
;
1283 this.columns
["time_active"].updateTd = function(td
, row
) {
1284 const activeTime
= this.getRowValue(row
, 0);
1285 const seedingTime
= this.getRowValue(row
, 1);
1286 const time
= (seedingTime
> 0)
1287 ? ("QBT_TR(%1 (seeded for %2))QBT_TR[CONTEXT=TransferListDelegate]"
1288 .replace("%1", window
.qBittorrent
.Misc
.friendlyDuration(activeTime
))
1289 .replace("%2", window
.qBittorrent
.Misc
.friendlyDuration(seedingTime
)))
1290 : window
.qBittorrent
.Misc
.friendlyDuration(activeTime
);
1291 td
.textContent
= time
;
1296 this.columns
["completed"].updateTd
= this.columns
["size"].updateTd
;
1299 this.columns
["max_ratio"].updateTd
= this.columns
["ratio"].updateTd
;
1302 this.columns
["seen_complete"].updateTd
= this.columns
["completion_on"].updateTd
;
1305 this.columns
["last_activity"].updateTd = function(td
, row
) {
1306 const val
= this.getRowValue(row
);
1308 td
.textContent
= "∞";
1312 const formattedVal
= "QBT_TR(%1 ago)QBT_TR[CONTEXT=TransferListDelegate]".replace("%1", window
.qBittorrent
.Misc
.friendlyDuration((new Date() / 1000) - val
));
1313 td
.textContent
= formattedVal
;
1314 td
.title
= formattedVal
;
1319 this.columns
["availability"].updateTd = function(td
, row
) {
1320 const value
= window
.qBittorrent
.Misc
.toFixedPointString(this.getRowValue(row
), 3);
1321 td
.textContent
= value
;
1326 this.columns
["infohash_v1"].updateTd = function(td
, row
) {
1327 const sourceInfohashV1
= this.getRowValue(row
);
1328 const infohashV1
= (sourceInfohashV1
!== "") ? sourceInfohashV1
: "QBT_TR(N/A)QBT_TR[CONTEXT=TransferListDelegate]";
1329 td
.textContent
= infohashV1
;
1330 td
.title
= infohashV1
;
1334 this.columns
["infohash_v2"].updateTd = function(td
, row
) {
1335 const sourceInfohashV2
= this.getRowValue(row
);
1336 const infohashV2
= (sourceInfohashV2
!== "") ? sourceInfohashV2
: "QBT_TR(N/A)QBT_TR[CONTEXT=TransferListDelegate]";
1337 td
.textContent
= infohashV2
;
1338 td
.title
= infohashV2
;
1342 this.columns
["reannounce"].updateTd = function(td
, row
) {
1343 const time
= window
.qBittorrent
.Misc
.friendlyDuration(this.getRowValue(row
));
1344 td
.textContent
= time
;
1349 this.columns
["private"].updateTd = function(td
, row
) {
1350 const hasMetadata
= row
["full_data"].has_metadata
;
1351 const isPrivate
= this.getRowValue(row
);
1352 const string
= hasMetadata
1354 ? "QBT_TR(Yes)QBT_TR[CONTEXT=PropertiesWidget]"
1355 : "QBT_TR(No)QBT_TR[CONTEXT=PropertiesWidget]")
1356 : "QBT_TR(N/A)QBT_TR[CONTEXT=PropertiesWidget]";
1357 td
.textContent
= string
;
1362 applyFilter: function(row
, filterName
, categoryHash
, tagHash
, trackerHash
, filterTerms
) {
1363 const state
= row
["full_data"].state
;
1364 let inactive
= false;
1366 switch (filterName
) {
1368 if ((state
!== "downloading") && !state
.includes("DL"))
1372 if ((state
!== "uploading") && (state
!== "forcedUP") && (state
!== "stalledUP") && (state
!== "queuedUP") && (state
!== "checkingUP"))
1376 if ((state
!== "uploading") && !state
.includes("UP"))
1380 if (!state
.includes("stopped"))
1384 if (state
.includes("stopped"))
1388 if ((state
!== "stalledUP") && (state
!== "stalledDL"))
1391 case "stalled_uploading":
1392 if (state
!== "stalledUP")
1395 case "stalled_downloading":
1396 if (state
!== "stalledDL")
1404 if (state
=== "stalledDL")
1405 r
= (row
["full_data"].upspeed
> 0);
1407 r
= (state
=== "metaDL") || (state
=== "forcedMetaDL") || (state
=== "downloading") || (state
=== "forcedDL") || (state
=== "uploading") || (state
=== "forcedUP");
1413 if ((state
!== "checkingUP") && (state
!== "checkingDL") && (state
!== "checkingResumeData"))
1417 if (state
!== "moving")
1421 if ((state
!== "error") && (state
!== "unknown") && (state
!== "missingFiles"))
1426 switch (categoryHash
) {
1427 case CATEGORIES_ALL
:
1428 break; // do nothing
1429 case CATEGORIES_UNCATEGORIZED
:
1430 if (row
["full_data"].category
.length
!== 0)
1432 break; // do nothing
1434 if (!useSubcategories
) {
1435 if (categoryHash
!== window
.qBittorrent
.Misc
.genHash(row
["full_data"].category
))
1439 const selectedCategory
= category_list
.get(categoryHash
);
1440 if (selectedCategory
!== undefined) {
1441 const selectedCategoryName
= selectedCategory
.name
+ "/";
1442 const torrentCategoryName
= row
["full_data"].category
+ "/";
1443 if (!torrentCategoryName
.startsWith(selectedCategoryName
))
1452 break; // do nothing
1455 if (row
["full_data"].tags
.length
!== 0)
1457 break; // do nothing
1460 const tagHashes
= row
["full_data"].tags
.split(", ").map(tag
=> window
.qBittorrent
.Misc
.genHash(tag
));
1461 if (!tagHashes
.contains(tagHash
))
1467 switch (trackerHash
) {
1469 break; // do nothing
1470 case TRACKERS_TRACKERLESS
:
1471 if (row
["full_data"].trackers_count
!== 0)
1475 const tracker
= trackerList
.get(trackerHash
);
1478 for (const torrents
of tracker
.trackerTorrentMap
.values()) {
1479 if (torrents
.includes(row
["full_data"].rowId
)) {
1491 if ((filterTerms
!== undefined) && (filterTerms
!== null)) {
1492 const filterBy
= document
.getElementById("torrentsFilterSelect").value
;
1493 const textToSearch
= row
["full_data"][filterBy
].toLowerCase();
1494 if (filterTerms
instanceof RegExp
) {
1495 if (!filterTerms
.test(textToSearch
))
1499 if ((filterTerms
.length
> 0) && !window
.qBittorrent
.Misc
.containsAllTerms(textToSearch
, filterTerms
))
1507 getFilteredTorrentsNumber: function(filterName
, categoryHash
, tagHash
, trackerHash
) {
1509 const rows
= this.rows
.getValues();
1511 for (let i
= 0; i
< rows
.length
; ++i
) {
1512 if (this.applyFilter(rows
[i
], filterName
, categoryHash
, tagHash
, trackerHash
, null))
1518 getFilteredTorrentsHashes: function(filterName
, categoryHash
, tagHash
, trackerHash
) {
1519 const rowsHashes
= [];
1520 const rows
= this.rows
.getValues();
1522 for (let i
= 0; i
< rows
.length
; ++i
) {
1523 if (this.applyFilter(rows
[i
], filterName
, categoryHash
, tagHash
, trackerHash
, null))
1524 rowsHashes
.push(rows
[i
]["rowId"]);
1530 getFilteredAndSortedRows: function() {
1531 const filteredRows
= [];
1533 const useRegex
= $("torrentsFilterRegexBox").checked
;
1534 const filterText
= $("torrentsFilterInput").value
.trim().toLowerCase();
1537 filterTerms
= (filterText
.length
> 0)
1538 ? (useRegex
? new RegExp(filterText
) : filterText
.split(" "))
1541 catch (e
) { // SyntaxError: Invalid regex pattern
1542 return filteredRows
;
1545 const rows
= this.rows
.getValues();
1546 for (let i
= 0; i
< rows
.length
; ++i
) {
1547 if (this.applyFilter(rows
[i
], selectedStatus
, selectedCategory
, selectedTag
, selectedTracker
, filterTerms
)) {
1548 filteredRows
.push(rows
[i
]);
1549 filteredRows
[rows
[i
].rowId
] = rows
[i
];
1553 filteredRows
.sort((row1
, row2
) => {
1554 const column
= this.columns
[this.sortedColumn
];
1555 const res
= column
.compareRows(row1
, row2
);
1556 if (this.reverseSort
=== "0")
1561 return filteredRows
;
1564 setupTr: function(tr
) {
1565 tr
.addEventListener("dblclick", function(e
) {
1567 e
.stopPropagation();
1569 this._this
.deselectAll();
1570 this._this
.selectRow(this.rowId
);
1571 const row
= this._this
.rows
.get(this.rowId
);
1572 const state
= row
["full_data"].state
;
1575 (state
!== "uploading")
1576 && (state
!== "stoppedUP")
1577 && (state
!== "forcedUP")
1578 && (state
!== "stalledUP")
1579 && (state
!== "queuedUP")
1580 && (state
!== "checkingUP")
1581 ? "dblclick_download"
1582 : "dblclick_complete";
1584 if (LocalPreferences
.get(prefKey
, "1") !== "1")
1587 if (state
.includes("stopped"))
1593 tr
.addClass("torrentsTableContextMenuTarget");
1596 getCurrentTorrentID: function() {
1597 return this.getSelectedRowId();
1600 onSelectedRowChanged: function() {
1601 updatePropertiesPanel();
1605 const TorrentPeersTable
= new Class({
1606 Extends
: DynamicTable
,
1608 initColumns: function() {
1609 this.newColumn("country", "", "QBT_TR(Country/Region)QBT_TR[CONTEXT=PeerListWidget]", 22, true);
1610 this.newColumn("ip", "", "QBT_TR(IP)QBT_TR[CONTEXT=PeerListWidget]", 80, true);
1611 this.newColumn("port", "", "QBT_TR(Port)QBT_TR[CONTEXT=PeerListWidget]", 35, true);
1612 this.newColumn("connection", "", "QBT_TR(Connection)QBT_TR[CONTEXT=PeerListWidget]", 50, true);
1613 this.newColumn("flags", "", "QBT_TR(Flags)QBT_TR[CONTEXT=PeerListWidget]", 50, true);
1614 this.newColumn("client", "", "QBT_TR(Client)QBT_TR[CONTEXT=PeerListWidget]", 140, true);
1615 this.newColumn("peer_id_client", "", "QBT_TR(Peer ID Client)QBT_TR[CONTEXT=PeerListWidget]", 60, false);
1616 this.newColumn("progress", "", "QBT_TR(Progress)QBT_TR[CONTEXT=PeerListWidget]", 50, true);
1617 this.newColumn("dl_speed", "", "QBT_TR(Down Speed)QBT_TR[CONTEXT=PeerListWidget]", 50, true);
1618 this.newColumn("up_speed", "", "QBT_TR(Up Speed)QBT_TR[CONTEXT=PeerListWidget]", 50, true);
1619 this.newColumn("downloaded", "", "QBT_TR(Downloaded)QBT_TR[CONTEXT=PeerListWidget]", 50, true);
1620 this.newColumn("uploaded", "", "QBT_TR(Uploaded)QBT_TR[CONTEXT=PeerListWidget]", 50, true);
1621 this.newColumn("relevance", "", "QBT_TR(Relevance)QBT_TR[CONTEXT=PeerListWidget]", 30, true);
1622 this.newColumn("files", "", "QBT_TR(Files)QBT_TR[CONTEXT=PeerListWidget]", 100, true);
1624 this.columns
["country"].dataProperties
.push("country_code");
1625 this.columns
["flags"].dataProperties
.push("flags_desc");
1626 this.initColumnsFunctions();
1629 initColumnsFunctions: function() {
1632 this.columns
["country"].updateTd = function(td
, row
) {
1633 const country
= this.getRowValue(row
, 0);
1634 const country_code
= this.getRowValue(row
, 1);
1636 let span
= td
.firstElementChild
;
1637 if (span
=== null) {
1638 span
= document
.createElement("span");
1639 span
.classList
.add("flags");
1643 span
.style
.backgroundImage
= `url('images/flags/${country_code ?? "xx"}.svg')`;
1644 span
.textContent
= country
;
1649 this.columns
["ip"].compareRows = function(row1
, row2
) {
1650 const ip1
= this.getRowValue(row1
);
1651 const ip2
= this.getRowValue(row2
);
1653 const a
= ip1
.split(".");
1654 const b
= ip2
.split(".");
1656 for (let i
= 0; i
< 4; ++i
) {
1665 this.columns
["flags"].updateTd = function(td
, row
) {
1666 td
.textContent
= this.getRowValue(row
, 0);
1667 td
.title
= this.getRowValue(row
, 1);
1671 this.columns
["progress"].updateTd = function(td
, row
) {
1672 const progress
= this.getRowValue(row
);
1673 let progressFormatted
= (progress
* 100).round(1);
1674 if ((progressFormatted
=== 100.0) && (progress
!== 1.0))
1675 progressFormatted
= 99.9;
1676 progressFormatted
+= "%";
1677 td
.textContent
= progressFormatted
;
1678 td
.title
= progressFormatted
;
1681 // dl_speed, up_speed
1682 this.columns
["dl_speed"].updateTd = function(td
, row
) {
1683 const speed
= this.getRowValue(row
);
1685 td
.textContent
= "";
1689 const formattedSpeed
= window
.qBittorrent
.Misc
.friendlyUnit(speed
, true);
1690 td
.textContent
= formattedSpeed
;
1691 td
.title
= formattedSpeed
;
1694 this.columns
["up_speed"].updateTd
= this.columns
["dl_speed"].updateTd
;
1696 // downloaded, uploaded
1697 this.columns
["downloaded"].updateTd = function(td
, row
) {
1698 const downloaded
= window
.qBittorrent
.Misc
.friendlyUnit(this.getRowValue(row
), false);
1699 td
.textContent
= downloaded
;
1700 td
.title
= downloaded
;
1702 this.columns
["uploaded"].updateTd
= this.columns
["downloaded"].updateTd
;
1705 this.columns
["relevance"].updateTd
= this.columns
["progress"].updateTd
;
1708 this.columns
["files"].updateTd = function(td
, row
) {
1709 const value
= this.getRowValue(row
, 0);
1710 td
.textContent
= value
.replace(/\n/g, ";");
1717 const SearchResultsTable
= new Class({
1718 Extends
: DynamicTable
,
1720 initColumns: function() {
1721 this.newColumn("fileName", "", "QBT_TR(Name)QBT_TR[CONTEXT=SearchResultsTable]", 500, true);
1722 this.newColumn("fileSize", "", "QBT_TR(Size)QBT_TR[CONTEXT=SearchResultsTable]", 100, true);
1723 this.newColumn("nbSeeders", "", "QBT_TR(Seeders)QBT_TR[CONTEXT=SearchResultsTable]", 100, true);
1724 this.newColumn("nbLeechers", "", "QBT_TR(Leechers)QBT_TR[CONTEXT=SearchResultsTable]", 100, true);
1725 this.newColumn("siteUrl", "", "QBT_TR(Search engine)QBT_TR[CONTEXT=SearchResultsTable]", 250, true);
1726 this.newColumn("pubDate", "", "QBT_TR(Published On)QBT_TR[CONTEXT=SearchResultsTable]", 200, true);
1728 this.initColumnsFunctions();
1731 initColumnsFunctions: function() {
1732 const displaySize = function(td
, row
) {
1733 const size
= window
.qBittorrent
.Misc
.friendlyUnit(this.getRowValue(row
), false);
1734 td
.textContent
= size
;
1737 const displayNum = function(td
, row
) {
1738 const value
= this.getRowValue(row
);
1739 const formattedValue
= (value
=== "-1") ? "Unknown" : value
;
1740 td
.textContent
= formattedValue
;
1741 td
.title
= formattedValue
;
1743 const displayDate = function(td
, row
) {
1744 const value
= this.getRowValue(row
) * 1000;
1745 const formattedValue
= (isNaN(value
) || (value
<= 0)) ? "" : (new Date(value
).toLocaleString());
1746 td
.textContent
= formattedValue
;
1747 td
.title
= formattedValue
;
1750 this.columns
["fileSize"].updateTd
= displaySize
;
1751 this.columns
["nbSeeders"].updateTd
= displayNum
;
1752 this.columns
["nbLeechers"].updateTd
= displayNum
;
1753 this.columns
["pubDate"].updateTd
= displayDate
;
1756 getFilteredAndSortedRows: function() {
1757 const getSizeFilters = function() {
1758 let minSize
= (window
.qBittorrent
.Search
.searchSizeFilter
.min
> 0.00) ? (window
.qBittorrent
.Search
.searchSizeFilter
.min
* Math
.pow(1024, window
.qBittorrent
.Search
.searchSizeFilter
.minUnit
)) : 0.00;
1759 let maxSize
= (window
.qBittorrent
.Search
.searchSizeFilter
.max
> 0.00) ? (window
.qBittorrent
.Search
.searchSizeFilter
.max
* Math
.pow(1024, window
.qBittorrent
.Search
.searchSizeFilter
.maxUnit
)) : 0.00;
1761 if ((minSize
> maxSize
) && (maxSize
> 0.00)) {
1762 const tmp
= minSize
;
1773 const getSeedsFilters = function() {
1774 let minSeeds
= (window
.qBittorrent
.Search
.searchSeedsFilter
.min
> 0) ? window
.qBittorrent
.Search
.searchSeedsFilter
.min
: 0;
1775 let maxSeeds
= (window
.qBittorrent
.Search
.searchSeedsFilter
.max
> 0) ? window
.qBittorrent
.Search
.searchSeedsFilter
.max
: 0;
1777 if ((minSeeds
> maxSeeds
) && (maxSeeds
> 0)) {
1778 const tmp
= minSeeds
;
1779 minSeeds
= maxSeeds
;
1789 let filteredRows
= [];
1790 const rows
= this.rows
.getValues();
1791 const searchTerms
= window
.qBittorrent
.Search
.searchText
.pattern
.toLowerCase().split(" ");
1792 const filterTerms
= window
.qBittorrent
.Search
.searchText
.filterPattern
.toLowerCase().split(" ");
1793 const sizeFilters
= getSizeFilters();
1794 const seedsFilters
= getSeedsFilters();
1795 const searchInTorrentName
= $("searchInTorrentName").value
=== "names";
1797 if (searchInTorrentName
|| (filterTerms
.length
> 0) || (window
.qBittorrent
.Search
.searchSizeFilter
.min
> 0.00) || (window
.qBittorrent
.Search
.searchSizeFilter
.max
> 0.00)) {
1798 for (let i
= 0; i
< rows
.length
; ++i
) {
1799 const row
= rows
[i
];
1801 if (searchInTorrentName
&& !window
.qBittorrent
.Misc
.containsAllTerms(row
.full_data
.fileName
, searchTerms
))
1803 if ((filterTerms
.length
> 0) && !window
.qBittorrent
.Misc
.containsAllTerms(row
.full_data
.fileName
, filterTerms
))
1805 if ((sizeFilters
.min
> 0.00) && (row
.full_data
.fileSize
< sizeFilters
.min
))
1807 if ((sizeFilters
.max
> 0.00) && (row
.full_data
.fileSize
> sizeFilters
.max
))
1809 if ((seedsFilters
.min
> 0) && (row
.full_data
.nbSeeders
< seedsFilters
.min
))
1811 if ((seedsFilters
.max
> 0) && (row
.full_data
.nbSeeders
> seedsFilters
.max
))
1814 filteredRows
.push(row
);
1818 filteredRows
= rows
;
1821 filteredRows
.sort((row1
, row2
) => {
1822 const column
= this.columns
[this.sortedColumn
];
1823 const res
= column
.compareRows(row1
, row2
);
1824 if (this.reverseSort
=== "0")
1830 return filteredRows
;
1833 setupTr: function(tr
) {
1834 tr
.addClass("searchTableRow");
1838 const SearchPluginsTable
= new Class({
1839 Extends
: DynamicTable
,
1841 initColumns: function() {
1842 this.newColumn("fullName", "", "QBT_TR(Name)QBT_TR[CONTEXT=SearchPluginsTable]", 175, true);
1843 this.newColumn("version", "", "QBT_TR(Version)QBT_TR[CONTEXT=SearchPluginsTable]", 100, true);
1844 this.newColumn("url", "", "QBT_TR(Url)QBT_TR[CONTEXT=SearchPluginsTable]", 175, true);
1845 this.newColumn("enabled", "", "QBT_TR(Enabled)QBT_TR[CONTEXT=SearchPluginsTable]", 100, true);
1847 this.initColumnsFunctions();
1850 initColumnsFunctions: function() {
1851 this.columns
["enabled"].updateTd = function(td
, row
) {
1852 const value
= this.getRowValue(row
);
1854 td
.textContent
= "QBT_TR(Yes)QBT_TR[CONTEXT=SearchPluginsTable]";
1855 td
.title
= "QBT_TR(Yes)QBT_TR[CONTEXT=SearchPluginsTable]";
1856 td
.getParent("tr").addClass("green");
1857 td
.getParent("tr").removeClass("red");
1860 td
.textContent
= "QBT_TR(No)QBT_TR[CONTEXT=SearchPluginsTable]";
1861 td
.title
= "QBT_TR(No)QBT_TR[CONTEXT=SearchPluginsTable]";
1862 td
.getParent("tr").addClass("red");
1863 td
.getParent("tr").removeClass("green");
1868 setupTr: function(tr
) {
1869 tr
.addClass("searchPluginsTableRow");
1873 const TorrentTrackersTable
= new Class({
1874 Extends
: DynamicTable
,
1876 initColumns: function() {
1877 this.newColumn("tier", "", "QBT_TR(Tier)QBT_TR[CONTEXT=TrackerListWidget]", 35, true);
1878 this.newColumn("url", "", "QBT_TR(URL)QBT_TR[CONTEXT=TrackerListWidget]", 250, true);
1879 this.newColumn("status", "", "QBT_TR(Status)QBT_TR[CONTEXT=TrackerListWidget]", 125, true);
1880 this.newColumn("peers", "", "QBT_TR(Peers)QBT_TR[CONTEXT=TrackerListWidget]", 75, true);
1881 this.newColumn("seeds", "", "QBT_TR(Seeds)QBT_TR[CONTEXT=TrackerListWidget]", 75, true);
1882 this.newColumn("leeches", "", "QBT_TR(Leeches)QBT_TR[CONTEXT=TrackerListWidget]", 75, true);
1883 this.newColumn("downloaded", "", "QBT_TR(Times Downloaded)QBT_TR[CONTEXT=TrackerListWidget]", 100, true);
1884 this.newColumn("message", "", "QBT_TR(Message)QBT_TR[CONTEXT=TrackerListWidget]", 250, true);
1888 const BulkRenameTorrentFilesTable
= new Class({
1889 Extends
: DynamicTable
,
1892 prevFilterTerms
: [],
1893 prevRowsString
: null,
1894 prevFilteredRows
: [],
1895 prevSortedColumn
: null,
1896 prevReverseSort
: null,
1897 fileTree
: new window
.qBittorrent
.FileTree
.FileTree(),
1899 populateTable: function(root
) {
1900 this.fileTree
.setRoot(root
);
1901 root
.children
.each((node
) => {
1902 this._addNodeToTable(node
, 0);
1906 _addNodeToTable: function(node
, depth
) {
1909 if (node
.isFolder
) {
1913 checked
: node
.checked
,
1915 original
: node
.original
,
1916 renamed
: node
.renamed
1920 node
.full_data
= data
;
1921 this.updateRowData(data
);
1924 node
.data
.rowId
= node
.rowId
;
1925 node
.full_data
= node
.data
;
1926 this.updateRowData(node
.data
);
1929 node
.children
.each((child
) => {
1930 this._addNodeToTable(child
, depth
+ 1);
1934 getRoot: function() {
1935 return this.fileTree
.getRoot();
1938 getNode: function(rowId
) {
1939 return this.fileTree
.getNode(rowId
);
1942 getRow: function(node
) {
1943 const rowId
= this.fileTree
.getRowId(node
);
1944 return this.rows
.get(rowId
);
1947 getSelectedRows: function() {
1948 const nodes
= this.fileTree
.toArray();
1950 return nodes
.filter(x
=> x
.checked
=== 0);
1953 initColumns: function() {
1954 // Blocks saving header width (because window width isn't saved)
1955 LocalPreferences
.remove("column_" + "checked" + "_width_" + this.dynamicTableDivId
);
1956 LocalPreferences
.remove("column_" + "original" + "_width_" + this.dynamicTableDivId
);
1957 LocalPreferences
.remove("column_" + "renamed" + "_width_" + this.dynamicTableDivId
);
1958 this.newColumn("checked", "", "", 50, true);
1959 this.newColumn("original", "", "QBT_TR(Original)QBT_TR[CONTEXT=TrackerListWidget]", 270, true);
1960 this.newColumn("renamed", "", "QBT_TR(Renamed)QBT_TR[CONTEXT=TrackerListWidget]", 220, true);
1962 this.initColumnsFunctions();
1966 * Toggles the global checkbox and all checkboxes underneath
1968 toggleGlobalCheckbox: function() {
1969 const checkbox
= $("rootMultiRename_cb");
1970 const checkboxes
= $$("input.RenamingCB");
1972 for (let i
= 0; i
< checkboxes
.length
; ++i
) {
1973 const node
= this.getNode(i
);
1975 if (checkbox
.checked
|| checkbox
.indeterminate
) {
1976 const cb
= checkboxes
[i
];
1978 cb
.indeterminate
= false;
1979 cb
.state
= "checked";
1981 node
.full_data
.checked
= node
.checked
;
1984 const cb
= checkboxes
[i
];
1986 cb
.indeterminate
= false;
1987 cb
.state
= "unchecked";
1989 node
.full_data
.checked
= node
.checked
;
1993 this.updateGlobalCheckbox();
1996 toggleNodeTreeCheckbox: function(rowId
, checkState
) {
1997 const node
= this.getNode(rowId
);
1998 node
.checked
= checkState
;
1999 node
.full_data
.checked
= checkState
;
2000 const checkbox
= $(`cbRename${rowId}`);
2001 checkbox
.checked
= node
.checked
=== 0;
2002 checkbox
.state
= checkbox
.checked
? "checked" : "unchecked";
2004 for (let i
= 0; i
< node
.children
.length
; ++i
)
2005 this.toggleNodeTreeCheckbox(node
.children
[i
].rowId
, checkState
);
2008 updateGlobalCheckbox: function() {
2009 const checkbox
= $("rootMultiRename_cb");
2010 const checkboxes
= $$("input.RenamingCB");
2011 const isAllChecked = function() {
2012 for (let i
= 0; i
< checkboxes
.length
; ++i
) {
2013 if (!checkboxes
[i
].checked
)
2018 const isAllUnchecked = function() {
2019 for (let i
= 0; i
< checkboxes
.length
; ++i
) {
2020 if (checkboxes
[i
].checked
)
2025 if (isAllChecked()) {
2026 checkbox
.state
= "checked";
2027 checkbox
.indeterminate
= false;
2028 checkbox
.checked
= true;
2030 else if (isAllUnchecked()) {
2031 checkbox
.state
= "unchecked";
2032 checkbox
.indeterminate
= false;
2033 checkbox
.checked
= false;
2036 checkbox
.state
= "partial";
2037 checkbox
.indeterminate
= true;
2038 checkbox
.checked
= false;
2042 initColumnsFunctions: function() {
2046 this.columns
["checked"].updateTd = function(td
, row
) {
2047 const id
= row
.rowId
;
2048 const value
= this.getRowValue(row
);
2050 const treeImg
= new Element("img", {
2051 src
: "images/L.gif",
2056 const checkbox
= new Element("input");
2057 checkbox
.type
= "checkbox";
2058 checkbox
.id
= "cbRename" + id
;
2059 checkbox
.setAttribute("data-id", id
);
2060 checkbox
.className
= "RenamingCB";
2061 checkbox
.addEventListener("click", (e
) => {
2062 const node
= that
.getNode(id
);
2063 node
.checked
= e
.target
.checked
? 0 : 1;
2064 node
.full_data
.checked
= node
.checked
;
2065 that
.updateGlobalCheckbox();
2066 that
.onRowSelectionChange(node
);
2067 e
.stopPropagation();
2069 checkbox
.checked
= (value
=== 0);
2070 checkbox
.state
= checkbox
.checked
? "checked" : "unchecked";
2071 checkbox
.indeterminate
= false;
2072 td
.adopt(treeImg
, checkbox
);
2076 this.columns
["original"].updateTd = function(td
, row
) {
2077 const id
= row
.rowId
;
2078 const fileNameId
= "filesTablefileName" + id
;
2079 const node
= that
.getNode(id
);
2081 if (node
.isFolder
) {
2082 const value
= this.getRowValue(row
);
2083 const dirImgId
= "renameTableDirImg" + id
;
2085 // just update file name
2086 $(fileNameId
).textContent
= value
;
2089 const span
= new Element("span", {
2093 const dirImg
= new Element("img", {
2094 src
: "images/directory.svg",
2098 "margin-bottom": -3,
2099 "margin-left": (node
.depth
* 20)
2103 td
.replaceChildren(dirImg
, span
);
2107 const value
= this.getRowValue(row
);
2108 const span
= new Element("span", {
2112 "margin-left": ((node
.depth
+ 1) * 20)
2115 td
.replaceChildren(span
);
2120 this.columns
["renamed"].updateTd = function(td
, row
) {
2121 const id
= row
.rowId
;
2122 const fileNameRenamedId
= "filesTablefileRenamed" + id
;
2123 const value
= this.getRowValue(row
);
2125 const span
= new Element("span", {
2127 id
: fileNameRenamedId
,
2129 td
.replaceChildren(span
);
2133 onRowSelectionChange: function(row
) {},
2135 selectRow: function() {
2139 reselectRows: function(rowIds
) {
2142 this.tableBody
.getElements("tr").each((tr
) => {
2143 if (rowIds
.includes(tr
.rowId
)) {
2144 const node
= that
.getNode(tr
.rowId
);
2146 node
.full_data
.checked
= 0;
2148 const checkbox
= tr
.children
[0].getElement("input");
2149 checkbox
.state
= "checked";
2150 checkbox
.indeterminate
= false;
2151 checkbox
.checked
= true;
2155 this.updateGlobalCheckbox();
2158 _sortNodesByColumn: function(nodes
, column
) {
2159 nodes
.sort((row1
, row2
) => {
2160 // list folders before files when sorting by name
2161 if (column
.name
=== "original") {
2162 const node1
= this.getNode(row1
.data
.rowId
);
2163 const node2
= this.getNode(row2
.data
.rowId
);
2164 if (node1
.isFolder
&& !node2
.isFolder
)
2166 if (node2
.isFolder
&& !node1
.isFolder
)
2170 const res
= column
.compareRows(row1
, row2
);
2171 return (this.reverseSort
=== "0") ? res
: -res
;
2174 nodes
.each((node
) => {
2175 if (node
.children
.length
> 0)
2176 this._sortNodesByColumn(node
.children
, column
);
2180 _filterNodes: function(node
, filterTerms
, filteredRows
) {
2181 if (node
.isFolder
) {
2182 const childAdded
= node
.children
.reduce((acc
, child
) => {
2183 // we must execute the function before ORing w/ acc or we'll stop checking child nodes after the first successful match
2184 return (this._filterNodes(child
, filterTerms
, filteredRows
) || acc
);
2188 const row
= this.getRow(node
);
2189 filteredRows
.push(row
);
2194 if (window
.qBittorrent
.Misc
.containsAllTerms(node
.original
, filterTerms
)) {
2195 const row
= this.getRow(node
);
2196 filteredRows
.push(row
);
2203 setFilter: function(text
) {
2204 const filterTerms
= text
.trim().toLowerCase().split(" ");
2205 if ((filterTerms
.length
=== 1) && (filterTerms
[0] === ""))
2206 this.filterTerms
= [];
2208 this.filterTerms
= filterTerms
;
2211 getFilteredAndSortedRows: function() {
2212 if (this.getRoot() === null)
2215 const generateRowsSignature = function(rows
) {
2216 const rowsData
= rows
.map((row
) => {
2217 return row
.full_data
;
2219 return JSON
.stringify(rowsData
);
2222 const getFilteredRows = function() {
2223 if (this.filterTerms
.length
=== 0) {
2224 const nodeArray
= this.fileTree
.toArray();
2225 const filteredRows
= nodeArray
.map((node
) => {
2226 return this.getRow(node
);
2228 return filteredRows
;
2231 const filteredRows
= [];
2232 this.getRoot().children
.each((child
) => {
2233 this._filterNodes(child
, this.filterTerms
, filteredRows
);
2235 filteredRows
.reverse();
2236 return filteredRows
;
2239 const hasRowsChanged = function(rowsString
, prevRowsStringString
) {
2240 const rowsChanged
= (rowsString
!== prevRowsStringString
);
2241 const isFilterTermsChanged
= this.filterTerms
.reduce((acc
, term
, index
) => {
2242 return (acc
|| (term
!== this.prevFilterTerms
[index
]));
2244 const isFilterChanged
= ((this.filterTerms
.length
!== this.prevFilterTerms
.length
)
2245 || ((this.filterTerms
.length
> 0) && isFilterTermsChanged
));
2246 const isSortedColumnChanged
= (this.prevSortedColumn
!== this.sortedColumn
);
2247 const isReverseSortChanged
= (this.prevReverseSort
!== this.reverseSort
);
2249 return (rowsChanged
|| isFilterChanged
|| isSortedColumnChanged
|| isReverseSortChanged
);
2252 const rowsString
= generateRowsSignature(this.rows
);
2253 if (!hasRowsChanged(rowsString
, this.prevRowsString
))
2254 return this.prevFilteredRows
;
2256 // sort, then filter
2257 const column
= this.columns
[this.sortedColumn
];
2258 this._sortNodesByColumn(this.getRoot().children
, column
);
2259 const filteredRows
= getFilteredRows();
2261 this.prevFilterTerms
= this.filterTerms
;
2262 this.prevRowsString
= rowsString
;
2263 this.prevFilteredRows
= filteredRows
;
2264 this.prevSortedColumn
= this.sortedColumn
;
2265 this.prevReverseSort
= this.reverseSort
;
2266 return filteredRows
;
2269 setIgnored: function(rowId
, ignore
) {
2270 const row
= this.rows
.get(rowId
);
2272 row
.full_data
.remaining
= 0;
2274 row
.full_data
.remaining
= (row
.full_data
.size
* (1.0 - (row
.full_data
.progress
/ 100)));
2277 setupTr: function(tr
) {
2278 tr
.addEventListener("keydown", function(event
) {
2279 switch (event
.key
) {
2281 qBittorrent
.PropFiles
.collapseFolder(this._this
.getSelectedRowId());
2284 qBittorrent
.PropFiles
.expandFolder(this._this
.getSelectedRowId());
2291 const TorrentFilesTable
= new Class({
2292 Extends
: DynamicTable
,
2295 prevFilterTerms
: [],
2296 prevRowsString
: null,
2297 prevFilteredRows
: [],
2298 prevSortedColumn
: null,
2299 prevReverseSort
: null,
2300 fileTree
: new window
.qBittorrent
.FileTree
.FileTree(),
2302 populateTable: function(root
) {
2303 this.fileTree
.setRoot(root
);
2304 root
.children
.each((node
) => {
2305 this._addNodeToTable(node
, 0);
2309 _addNodeToTable: function(node
, depth
) {
2312 if (node
.isFolder
) {
2316 checked
: node
.checked
,
2317 remaining
: node
.remaining
,
2318 progress
: node
.progress
,
2319 priority
: window
.qBittorrent
.PropFiles
.normalizePriority(node
.priority
),
2320 availability
: node
.availability
,
2326 node
.full_data
= data
;
2327 this.updateRowData(data
);
2330 node
.data
.rowId
= node
.rowId
;
2331 node
.full_data
= node
.data
;
2332 this.updateRowData(node
.data
);
2335 node
.children
.each((child
) => {
2336 this._addNodeToTable(child
, depth
+ 1);
2340 getRoot: function() {
2341 return this.fileTree
.getRoot();
2344 getNode: function(rowId
) {
2345 return this.fileTree
.getNode(rowId
);
2348 getRow: function(node
) {
2349 const rowId
= this.fileTree
.getRowId(node
);
2350 return this.rows
.get(rowId
);
2353 initColumns: function() {
2354 this.newColumn("checked", "", "", 50, true);
2355 this.newColumn("name", "", "QBT_TR(Name)QBT_TR[CONTEXT=TrackerListWidget]", 300, true);
2356 this.newColumn("size", "", "QBT_TR(Total Size)QBT_TR[CONTEXT=TrackerListWidget]", 75, true);
2357 this.newColumn("progress", "", "QBT_TR(Progress)QBT_TR[CONTEXT=TrackerListWidget]", 100, true);
2358 this.newColumn("priority", "", "QBT_TR(Download Priority)QBT_TR[CONTEXT=TrackerListWidget]", 150, true);
2359 this.newColumn("remaining", "", "QBT_TR(Remaining)QBT_TR[CONTEXT=TrackerListWidget]", 75, true);
2360 this.newColumn("availability", "", "QBT_TR(Availability)QBT_TR[CONTEXT=TrackerListWidget]", 75, true);
2362 this.initColumnsFunctions();
2365 initColumnsFunctions: function() {
2367 const displaySize = function(td
, row
) {
2368 const size
= window
.qBittorrent
.Misc
.friendlyUnit(this.getRowValue(row
), false);
2369 td
.textContent
= size
;
2372 const displayPercentage = function(td
, row
) {
2373 const value
= window
.qBittorrent
.Misc
.friendlyPercentage(this.getRowValue(row
));
2374 td
.textContent
= value
;
2379 this.columns
["checked"].updateTd = function(td
, row
) {
2380 const id
= row
.rowId
;
2381 const value
= this.getRowValue(row
);
2383 if (window
.qBittorrent
.PropFiles
.isDownloadCheckboxExists(id
)) {
2384 window
.qBittorrent
.PropFiles
.updateDownloadCheckbox(id
, value
);
2387 const treeImg
= new Element("img", {
2388 src
: "images/L.gif",
2393 td
.adopt(treeImg
, window
.qBittorrent
.PropFiles
.createDownloadCheckbox(id
, row
.full_data
.fileId
, value
));
2398 this.columns
["name"].updateTd = function(td
, row
) {
2399 const id
= row
.rowId
;
2400 const fileNameId
= "filesTablefileName" + id
;
2401 const node
= that
.getNode(id
);
2403 if (node
.isFolder
) {
2404 const value
= this.getRowValue(row
);
2405 const collapseIconId
= "filesTableCollapseIcon" + id
;
2406 const dirImgId
= "filesTableDirImg" + id
;
2408 // just update file name
2409 $(fileNameId
).textContent
= value
;
2412 const collapseIcon
= new Element("img", {
2413 src
: "images/go-down.svg",
2415 "margin-left": (node
.depth
* 20)
2417 class: "filesTableCollapseIcon",
2420 onclick
: "qBittorrent.PropFiles.collapseIconClicked(this)"
2422 const span
= new Element("span", {
2426 const dirImg
= new Element("img", {
2427 src
: "images/directory.svg",
2435 td
.replaceChildren(collapseIcon
, dirImg
, span
);
2439 const value
= this.getRowValue(row
);
2440 const span
= new Element("span", {
2444 "margin-left": ((node
.depth
+ 1) * 20)
2447 td
.replaceChildren(span
);
2452 this.columns
["size"].updateTd
= displaySize
;
2455 this.columns
["progress"].updateTd = function(td
, row
) {
2456 const id
= row
.rowId
;
2457 const value
= this.getRowValue(row
);
2459 const progressBar
= $("pbf_" + id
);
2460 if (progressBar
=== null) {
2461 td
.adopt(new window
.qBittorrent
.ProgressBar
.ProgressBar(value
.toFloat(), {
2467 progressBar
.setValue(value
.toFloat());
2472 this.columns
["priority"].updateTd = function(td
, row
) {
2473 const id
= row
.rowId
;
2474 const value
= this.getRowValue(row
);
2476 if (window
.qBittorrent
.PropFiles
.isPriorityComboExists(id
))
2477 window
.qBittorrent
.PropFiles
.updatePriorityCombo(id
, value
);
2479 td
.adopt(window
.qBittorrent
.PropFiles
.createPriorityCombo(id
, row
.full_data
.fileId
, value
));
2482 // remaining, availability
2483 this.columns
["remaining"].updateTd
= displaySize
;
2484 this.columns
["availability"].updateTd
= displayPercentage
;
2487 _sortNodesByColumn: function(nodes
, column
) {
2488 nodes
.sort((row1
, row2
) => {
2489 // list folders before files when sorting by name
2490 if (column
.name
=== "name") {
2491 const node1
= this.getNode(row1
.data
.rowId
);
2492 const node2
= this.getNode(row2
.data
.rowId
);
2493 if (node1
.isFolder
&& !node2
.isFolder
)
2495 if (node2
.isFolder
&& !node1
.isFolder
)
2499 const res
= column
.compareRows(row1
, row2
);
2500 return (this.reverseSort
=== "0") ? res
: -res
;
2503 nodes
.each((node
) => {
2504 if (node
.children
.length
> 0)
2505 this._sortNodesByColumn(node
.children
, column
);
2509 _filterNodes: function(node
, filterTerms
, filteredRows
) {
2510 if (node
.isFolder
) {
2511 const childAdded
= node
.children
.reduce((acc
, child
) => {
2512 // we must execute the function before ORing w/ acc or we'll stop checking child nodes after the first successful match
2513 return (this._filterNodes(child
, filterTerms
, filteredRows
) || acc
);
2517 const row
= this.getRow(node
);
2518 filteredRows
.push(row
);
2523 if (window
.qBittorrent
.Misc
.containsAllTerms(node
.name
, filterTerms
)) {
2524 const row
= this.getRow(node
);
2525 filteredRows
.push(row
);
2532 setFilter: function(text
) {
2533 const filterTerms
= text
.trim().toLowerCase().split(" ");
2534 if ((filterTerms
.length
=== 1) && (filterTerms
[0] === ""))
2535 this.filterTerms
= [];
2537 this.filterTerms
= filterTerms
;
2540 getFilteredAndSortedRows: function() {
2541 if (this.getRoot() === null)
2544 const generateRowsSignature = function(rows
) {
2545 const rowsData
= rows
.map((row
) => {
2546 return row
.full_data
;
2548 return JSON
.stringify(rowsData
);
2551 const getFilteredRows = function() {
2552 if (this.filterTerms
.length
=== 0) {
2553 const nodeArray
= this.fileTree
.toArray();
2554 const filteredRows
= nodeArray
.map((node
) => {
2555 return this.getRow(node
);
2557 return filteredRows
;
2560 const filteredRows
= [];
2561 this.getRoot().children
.each((child
) => {
2562 this._filterNodes(child
, this.filterTerms
, filteredRows
);
2564 filteredRows
.reverse();
2565 return filteredRows
;
2568 const hasRowsChanged = function(rowsString
, prevRowsStringString
) {
2569 const rowsChanged
= (rowsString
!== prevRowsStringString
);
2570 const isFilterTermsChanged
= this.filterTerms
.reduce((acc
, term
, index
) => {
2571 return (acc
|| (term
!== this.prevFilterTerms
[index
]));
2573 const isFilterChanged
= ((this.filterTerms
.length
!== this.prevFilterTerms
.length
)
2574 || ((this.filterTerms
.length
> 0) && isFilterTermsChanged
));
2575 const isSortedColumnChanged
= (this.prevSortedColumn
!== this.sortedColumn
);
2576 const isReverseSortChanged
= (this.prevReverseSort
!== this.reverseSort
);
2578 return (rowsChanged
|| isFilterChanged
|| isSortedColumnChanged
|| isReverseSortChanged
);
2581 const rowsString
= generateRowsSignature(this.rows
);
2582 if (!hasRowsChanged(rowsString
, this.prevRowsString
))
2583 return this.prevFilteredRows
;
2585 // sort, then filter
2586 const column
= this.columns
[this.sortedColumn
];
2587 this._sortNodesByColumn(this.getRoot().children
, column
);
2588 const filteredRows
= getFilteredRows();
2590 this.prevFilterTerms
= this.filterTerms
;
2591 this.prevRowsString
= rowsString
;
2592 this.prevFilteredRows
= filteredRows
;
2593 this.prevSortedColumn
= this.sortedColumn
;
2594 this.prevReverseSort
= this.reverseSort
;
2595 return filteredRows
;
2598 setIgnored: function(rowId
, ignore
) {
2599 const row
= this.rows
.get(rowId
);
2601 row
.full_data
.remaining
= 0;
2603 row
.full_data
.remaining
= (row
.full_data
.size
* (1.0 - (row
.full_data
.progress
/ 100)));
2606 setupTr: function(tr
) {
2607 tr
.addEventListener("keydown", function(event
) {
2608 switch (event
.key
) {
2610 qBittorrent
.PropFiles
.collapseFolder(this._this
.getSelectedRowId());
2613 qBittorrent
.PropFiles
.expandFolder(this._this
.getSelectedRowId());
2620 const RssFeedTable
= new Class({
2621 Extends
: DynamicTable
,
2622 initColumns: function() {
2623 this.newColumn("state_icon", "", "", 30, true);
2624 this.newColumn("name", "", "QBT_TR(RSS feeds)QBT_TR[CONTEXT=FeedListWidget]", -1, true);
2626 this.columns
["state_icon"].dataProperties
[0] = "";
2628 // map name row to "[name] ([unread])"
2629 this.columns
["name"].dataProperties
.push("unread");
2630 this.columns
["name"].updateTd = function(td
, row
) {
2631 const name
= this.getRowValue(row
, 0);
2632 const unreadCount
= this.getRowValue(row
, 1);
2633 const value
= name
+ " (" + unreadCount
+ ")";
2634 td
.textContent
= value
;
2638 setupHeaderMenu: function() {},
2639 setupHeaderEvents: function() {},
2640 getFilteredAndSortedRows: function() {
2641 return this.rows
.getValues();
2643 selectRow: function(rowId
) {
2644 this.selectedRows
.push(rowId
);
2646 this.onSelectedRowChanged();
2648 const rows
= this.rows
.getValues();
2650 for (let i
= 0; i
< rows
.length
; ++i
) {
2651 if (rows
[i
].rowId
=== rowId
) {
2652 path
= rows
[i
].full_data
.dataPath
;
2656 window
.qBittorrent
.Rss
.showRssFeed(path
);
2658 setupTr: function(tr
) {
2659 tr
.addEventListener("dblclick", function(e
) {
2660 if (this.rowId
!== 0) {
2661 window
.qBittorrent
.Rss
.moveItem(this._this
.rows
.get(this.rowId
).full_data
.dataPath
);
2666 updateRow: function(tr
, fullUpdate
) {
2667 const row
= this.rows
.get(tr
.rowId
);
2668 const data
= row
[fullUpdate
? "full_data" : "data"];
2670 const tds
= tr
.getElements("td");
2671 for (let i
= 0; i
< this.columns
.length
; ++i
) {
2672 if (Object
.hasOwn(data
, this.columns
[i
].dataProperties
[0]))
2673 this.columns
[i
].updateTd(tds
[i
], row
);
2676 tds
[0].style
.overflow
= "visible";
2677 const indentation
= row
.full_data
.indentation
;
2678 tds
[0].style
.paddingLeft
= (indentation
* 32 + 4) + "px";
2679 tds
[1].style
.paddingLeft
= (indentation
* 32 + 4) + "px";
2681 updateIcons: function() {
2683 this.rows
.each(row
=> {
2685 switch (row
.full_data
.status
) {
2687 img_path
= "images/application-rss.svg";
2690 img_path
= "images/task-reject.svg";
2693 img_path
= "images/spinner.gif";
2696 img_path
= "images/mail-inbox.svg";
2699 img_path
= "images/folder-documents.svg";
2703 for (let i
= 0; i
< this.tableBody
.rows
.length
; ++i
) {
2704 if (this.tableBody
.rows
[i
].rowId
=== row
.rowId
) {
2705 td
= this.tableBody
.rows
[i
].children
[0];
2709 if (td
.getChildren("img").length
> 0) {
2710 const img
= td
.getChildren("img")[0];
2711 if (!img
.src
.includes(img_path
)) {
2717 td
.adopt(new Element("img", {
2719 "class": "stateIcon",
2726 newColumn: function(name
, style
, caption
, defaultWidth
, defaultVisible
) {
2728 column
["name"] = name
;
2729 column
["title"] = name
;
2730 column
["visible"] = defaultVisible
;
2731 column
["force_hide"] = false;
2732 column
["caption"] = caption
;
2733 column
["style"] = style
;
2734 if (defaultWidth
!== -1)
2735 column
["width"] = defaultWidth
;
2737 column
["dataProperties"] = [name
];
2738 column
["getRowValue"] = function(row
, pos
) {
2739 if (pos
=== undefined)
2741 return row
["full_data"][this.dataProperties
[pos
]];
2743 column
["compareRows"] = function(row1
, row2
) {
2744 const value1
= this.getRowValue(row1
);
2745 const value2
= this.getRowValue(row2
);
2746 if ((typeof(value1
) === "number") && (typeof(value2
) === "number"))
2747 return compareNumbers(value1
, value2
);
2748 return window
.qBittorrent
.Misc
.naturalSortCollator
.compare(value1
, value2
);
2750 column
["updateTd"] = function(td
, row
) {
2751 const value
= this.getRowValue(row
);
2752 td
.textContent
= value
;
2755 column
["onResize"] = null;
2756 this.columns
.push(column
);
2757 this.columns
[name
] = column
;
2759 this.hiddenTableHeader
.appendChild(new Element("th"));
2760 this.fixedTableHeader
.appendChild(new Element("th"));
2764 const RssArticleTable
= new Class({
2765 Extends
: DynamicTable
,
2766 initColumns: function() {
2767 this.newColumn("name", "", "QBT_TR(Torrents: (double-click to download))QBT_TR[CONTEXT=RSSWidget]", -1, true);
2769 setupHeaderMenu: function() {},
2770 setupHeaderEvents: function() {},
2771 getFilteredAndSortedRows: function() {
2772 return this.rows
.getValues();
2774 selectRow: function(rowId
) {
2775 this.selectedRows
.push(rowId
);
2777 this.onSelectedRowChanged();
2779 const rows
= this.rows
.getValues();
2782 for (let i
= 0; i
< rows
.length
; ++i
) {
2783 if (rows
[i
].rowId
=== rowId
) {
2784 articleId
= rows
[i
].full_data
.dataId
;
2785 feedUid
= rows
[i
].full_data
.feedUid
;
2786 this.tableBody
.rows
[rows
[i
].rowId
].removeClass("unreadArticle");
2790 window
.qBittorrent
.Rss
.showDetails(feedUid
, articleId
);
2792 setupTr: function(tr
) {
2793 tr
.addEventListener("dblclick", function(e
) {
2794 showDownloadPage([this._this
.rows
.get(this.rowId
).full_data
.torrentURL
]);
2797 tr
.addClass("torrentsTableContextMenuTarget");
2799 updateRow: function(tr
, fullUpdate
) {
2800 const row
= this.rows
.get(tr
.rowId
);
2801 const data
= row
[fullUpdate
? "full_data" : "data"];
2802 if (!row
.full_data
.isRead
)
2803 tr
.addClass("unreadArticle");
2805 tr
.removeClass("unreadArticle");
2807 const tds
= tr
.getElements("td");
2808 for (let i
= 0; i
< this.columns
.length
; ++i
) {
2809 if (Object
.hasOwn(data
, this.columns
[i
].dataProperties
[0]))
2810 this.columns
[i
].updateTd(tds
[i
], row
);
2814 newColumn: function(name
, style
, caption
, defaultWidth
, defaultVisible
) {
2816 column
["name"] = name
;
2817 column
["title"] = name
;
2818 column
["visible"] = defaultVisible
;
2819 column
["force_hide"] = false;
2820 column
["caption"] = caption
;
2821 column
["style"] = style
;
2822 if (defaultWidth
!== -1)
2823 column
["width"] = defaultWidth
;
2825 column
["dataProperties"] = [name
];
2826 column
["getRowValue"] = function(row
, pos
) {
2827 if (pos
=== undefined)
2829 return row
["full_data"][this.dataProperties
[pos
]];
2831 column
["compareRows"] = function(row1
, row2
) {
2832 const value1
= this.getRowValue(row1
);
2833 const value2
= this.getRowValue(row2
);
2834 if ((typeof(value1
) === "number") && (typeof(value2
) === "number"))
2835 return compareNumbers(value1
, value2
);
2836 return window
.qBittorrent
.Misc
.naturalSortCollator
.compare(value1
, value2
);
2838 column
["updateTd"] = function(td
, row
) {
2839 const value
= this.getRowValue(row
);
2840 td
.textContent
= value
;
2843 column
["onResize"] = null;
2844 this.columns
.push(column
);
2845 this.columns
[name
] = column
;
2847 this.hiddenTableHeader
.appendChild(new Element("th"));
2848 this.fixedTableHeader
.appendChild(new Element("th"));
2852 const RssDownloaderRulesTable
= new Class({
2853 Extends
: DynamicTable
,
2854 initColumns: function() {
2855 this.newColumn("checked", "", "", 30, true);
2856 this.newColumn("name", "", "", -1, true);
2858 this.columns
["checked"].updateTd = function(td
, row
) {
2859 if ($("cbRssDlRule" + row
.rowId
) === null) {
2860 const checkbox
= new Element("input");
2861 checkbox
.type
= "checkbox";
2862 checkbox
.id
= "cbRssDlRule" + row
.rowId
;
2863 checkbox
.checked
= row
.full_data
.checked
;
2865 checkbox
.addEventListener("click", function(e
) {
2866 window
.qBittorrent
.RssDownloader
.rssDownloaderRulesTable
.updateRowData({
2868 checked
: this.checked
2870 window
.qBittorrent
.RssDownloader
.modifyRuleState(row
.full_data
.name
, "enabled", this.checked
);
2871 e
.stopPropagation();
2874 td
.append(checkbox
);
2877 $("cbRssDlRule" + row
.rowId
).checked
= row
.full_data
.checked
;
2881 setupHeaderMenu: function() {},
2882 setupHeaderEvents: function() {},
2883 getFilteredAndSortedRows: function() {
2884 return this.rows
.getValues();
2886 setupTr: function(tr
) {
2887 tr
.addEventListener("dblclick", function(e
) {
2888 window
.qBittorrent
.RssDownloader
.renameRule(this._this
.rows
.get(this.rowId
).full_data
.name
);
2892 newColumn: function(name
, style
, caption
, defaultWidth
, defaultVisible
) {
2894 column
["name"] = name
;
2895 column
["title"] = name
;
2896 column
["visible"] = defaultVisible
;
2897 column
["force_hide"] = false;
2898 column
["caption"] = caption
;
2899 column
["style"] = style
;
2900 if (defaultWidth
!== -1)
2901 column
["width"] = defaultWidth
;
2903 column
["dataProperties"] = [name
];
2904 column
["getRowValue"] = function(row
, pos
) {
2905 if (pos
=== undefined)
2907 return row
["full_data"][this.dataProperties
[pos
]];
2909 column
["compareRows"] = function(row1
, row2
) {
2910 const value1
= this.getRowValue(row1
);
2911 const value2
= this.getRowValue(row2
);
2912 if ((typeof(value1
) === "number") && (typeof(value2
) === "number"))
2913 return compareNumbers(value1
, value2
);
2914 return window
.qBittorrent
.Misc
.naturalSortCollator
.compare(value1
, value2
);
2916 column
["updateTd"] = function(td
, row
) {
2917 const value
= this.getRowValue(row
);
2918 td
.textContent
= value
;
2921 column
["onResize"] = null;
2922 this.columns
.push(column
);
2923 this.columns
[name
] = column
;
2925 this.hiddenTableHeader
.appendChild(new Element("th"));
2926 this.fixedTableHeader
.appendChild(new Element("th"));
2928 selectRow: function(rowId
) {
2929 this.selectedRows
.push(rowId
);
2931 this.onSelectedRowChanged();
2933 const rows
= this.rows
.getValues();
2935 for (let i
= 0; i
< rows
.length
; ++i
) {
2936 if (rows
[i
].rowId
=== rowId
) {
2937 name
= rows
[i
].full_data
.name
;
2941 window
.qBittorrent
.RssDownloader
.showRule(name
);
2945 const RssDownloaderFeedSelectionTable
= new Class({
2946 Extends
: DynamicTable
,
2947 initColumns: function() {
2948 this.newColumn("checked", "", "", 30, true);
2949 this.newColumn("name", "", "", -1, true);
2951 this.columns
["checked"].updateTd = function(td
, row
) {
2952 if ($("cbRssDlFeed" + row
.rowId
) === null) {
2953 const checkbox
= new Element("input");
2954 checkbox
.type
= "checkbox";
2955 checkbox
.id
= "cbRssDlFeed" + row
.rowId
;
2956 checkbox
.checked
= row
.full_data
.checked
;
2958 checkbox
.addEventListener("click", function(e
) {
2959 window
.qBittorrent
.RssDownloader
.rssDownloaderFeedSelectionTable
.updateRowData({
2961 checked
: this.checked
2963 e
.stopPropagation();
2966 td
.append(checkbox
);
2969 $("cbRssDlFeed" + row
.rowId
).checked
= row
.full_data
.checked
;
2973 setupHeaderMenu: function() {},
2974 setupHeaderEvents: function() {},
2975 getFilteredAndSortedRows: function() {
2976 return this.rows
.getValues();
2978 newColumn: function(name
, style
, caption
, defaultWidth
, defaultVisible
) {
2980 column
["name"] = name
;
2981 column
["title"] = name
;
2982 column
["visible"] = defaultVisible
;
2983 column
["force_hide"] = false;
2984 column
["caption"] = caption
;
2985 column
["style"] = style
;
2986 if (defaultWidth
!== -1)
2987 column
["width"] = defaultWidth
;
2989 column
["dataProperties"] = [name
];
2990 column
["getRowValue"] = function(row
, pos
) {
2991 if (pos
=== undefined)
2993 return row
["full_data"][this.dataProperties
[pos
]];
2995 column
["compareRows"] = function(row1
, row2
) {
2996 const value1
= this.getRowValue(row1
);
2997 const value2
= this.getRowValue(row2
);
2998 if ((typeof(value1
) === "number") && (typeof(value2
) === "number"))
2999 return compareNumbers(value1
, value2
);
3000 return window
.qBittorrent
.Misc
.naturalSortCollator
.compare(value1
, value2
);
3002 column
["updateTd"] = function(td
, row
) {
3003 const value
= this.getRowValue(row
);
3004 td
.textContent
= value
;
3007 column
["onResize"] = null;
3008 this.columns
.push(column
);
3009 this.columns
[name
] = column
;
3011 this.hiddenTableHeader
.appendChild(new Element("th"));
3012 this.fixedTableHeader
.appendChild(new Element("th"));
3014 selectRow: function() {}
3017 const RssDownloaderArticlesTable
= new Class({
3018 Extends
: DynamicTable
,
3019 initColumns: function() {
3020 this.newColumn("name", "", "", -1, true);
3022 setupHeaderMenu: function() {},
3023 setupHeaderEvents: function() {},
3024 getFilteredAndSortedRows: function() {
3025 return this.rows
.getValues();
3027 newColumn: function(name
, style
, caption
, defaultWidth
, defaultVisible
) {
3029 column
["name"] = name
;
3030 column
["title"] = name
;
3031 column
["visible"] = defaultVisible
;
3032 column
["force_hide"] = false;
3033 column
["caption"] = caption
;
3034 column
["style"] = style
;
3035 if (defaultWidth
!== -1)
3036 column
["width"] = defaultWidth
;
3038 column
["dataProperties"] = [name
];
3039 column
["getRowValue"] = function(row
, pos
) {
3040 if (pos
=== undefined)
3042 return row
["full_data"][this.dataProperties
[pos
]];
3044 column
["compareRows"] = function(row1
, row2
) {
3045 const value1
= this.getRowValue(row1
);
3046 const value2
= this.getRowValue(row2
);
3047 if ((typeof(value1
) === "number") && (typeof(value2
) === "number"))
3048 return compareNumbers(value1
, value2
);
3049 return window
.qBittorrent
.Misc
.naturalSortCollator
.compare(value1
, value2
);
3051 column
["updateTd"] = function(td
, row
) {
3052 const value
= this.getRowValue(row
);
3053 td
.textContent
= value
;
3056 column
["onResize"] = null;
3057 this.columns
.push(column
);
3058 this.columns
[name
] = column
;
3060 this.hiddenTableHeader
.appendChild(new Element("th"));
3061 this.fixedTableHeader
.appendChild(new Element("th"));
3063 selectRow: function() {},
3064 updateRow: function(tr
, fullUpdate
) {
3065 const row
= this.rows
.get(tr
.rowId
);
3066 const data
= row
[fullUpdate
? "full_data" : "data"];
3068 if (row
.full_data
.isFeed
) {
3069 tr
.addClass("articleTableFeed");
3070 tr
.removeClass("articleTableArticle");
3073 tr
.removeClass("articleTableFeed");
3074 tr
.addClass("articleTableArticle");
3077 const tds
= tr
.getElements("td");
3078 for (let i
= 0; i
< this.columns
.length
; ++i
) {
3079 if (Object
.hasOwn(data
, this.columns
[i
].dataProperties
[0]))
3080 this.columns
[i
].updateTd(tds
[i
], row
);
3086 const LogMessageTable
= new Class({
3087 Extends
: DynamicTable
,
3091 filteredLength: function() {
3092 return this.tableBody
.getElements("tr").length
;
3095 initColumns: function() {
3096 this.newColumn("rowId", "", "QBT_TR(ID)QBT_TR[CONTEXT=ExecutionLogWidget]", 50, true);
3097 this.newColumn("message", "", "QBT_TR(Message)QBT_TR[CONTEXT=ExecutionLogWidget]", 350, true);
3098 this.newColumn("timestamp", "", "QBT_TR(Timestamp)QBT_TR[CONTEXT=ExecutionLogWidget]", 150, true);
3099 this.newColumn("type", "", "QBT_TR(Log Type)QBT_TR[CONTEXT=ExecutionLogWidget]", 100, true);
3100 this.initColumnsFunctions();
3103 initColumnsFunctions: function() {
3104 this.columns
["timestamp"].updateTd = function(td
, row
) {
3105 const date
= new Date(this.getRowValue(row
) * 1000).toLocaleString();
3106 td
.set({ "text": date
, "title": date
});
3109 this.columns
["type"].updateTd = function(td
, row
) {
3110 // Type of the message: Log::NORMAL: 1, Log::INFO: 2, Log::WARNING: 4, Log::CRITICAL: 8
3111 let logLevel
, addClass
;
3112 switch (this.getRowValue(row
).toInt()) {
3114 logLevel
= "QBT_TR(Normal)QBT_TR[CONTEXT=ExecutionLogWidget]";
3115 addClass
= "logNormal";
3118 logLevel
= "QBT_TR(Info)QBT_TR[CONTEXT=ExecutionLogWidget]";
3119 addClass
= "logInfo";
3122 logLevel
= "QBT_TR(Warning)QBT_TR[CONTEXT=ExecutionLogWidget]";
3123 addClass
= "logWarning";
3126 logLevel
= "QBT_TR(Critical)QBT_TR[CONTEXT=ExecutionLogWidget]";
3127 addClass
= "logCritical";
3130 logLevel
= "QBT_TR(Unknown)QBT_TR[CONTEXT=ExecutionLogWidget]";
3131 addClass
= "logUnknown";
3134 td
.set({ "text": logLevel
, "title": logLevel
});
3135 td
.getParent("tr").className
= `logTableRow${addClass}`;
3139 getFilteredAndSortedRows: function() {
3140 let filteredRows
= [];
3141 const rows
= this.rows
.getValues();
3142 this.filterText
= window
.qBittorrent
.Log
.getFilterText();
3143 const filterTerms
= (this.filterText
.length
> 0) ? this.filterText
.toLowerCase().split(" ") : [];
3144 const logLevels
= window
.qBittorrent
.Log
.getSelectedLevels();
3145 if ((filterTerms
.length
> 0) || (logLevels
.length
< 4)) {
3146 for (let i
= 0; i
< rows
.length
; ++i
) {
3147 if (!logLevels
.includes(rows
[i
].full_data
.type
.toString()))
3150 if ((filterTerms
.length
> 0) && !window
.qBittorrent
.Misc
.containsAllTerms(rows
[i
].full_data
.message
, filterTerms
))
3153 filteredRows
.push(rows
[i
]);
3157 filteredRows
= rows
;
3160 filteredRows
.sort((row1
, row2
) => {
3161 const column
= this.columns
[this.sortedColumn
];
3162 const res
= column
.compareRows(row1
, row2
);
3163 return (this.reverseSort
=== "0") ? res
: -res
;
3166 return filteredRows
;
3169 setupCommonEvents: function() {},
3171 setupTr: function(tr
) {
3172 tr
.addClass("logTableRow");
3176 const LogPeerTable
= new Class({
3177 Extends
: LogMessageTable
,
3179 initColumns: function() {
3180 this.newColumn("rowId", "", "QBT_TR(ID)QBT_TR[CONTEXT=ExecutionLogWidget]", 50, true);
3181 this.newColumn("ip", "", "QBT_TR(IP)QBT_TR[CONTEXT=ExecutionLogWidget]", 150, true);
3182 this.newColumn("timestamp", "", "QBT_TR(Timestamp)QBT_TR[CONTEXT=ExecutionLogWidget]", 150, true);
3183 this.newColumn("blocked", "", "QBT_TR(Status)QBT_TR[CONTEXT=ExecutionLogWidget]", 150, true);
3184 this.newColumn("reason", "", "QBT_TR(Reason)QBT_TR[CONTEXT=ExecutionLogWidget]", 150, true);
3186 this.columns
["timestamp"].updateTd = function(td
, row
) {
3187 const date
= new Date(this.getRowValue(row
) * 1000).toLocaleString();
3188 td
.set({ "text": date
, "title": date
});
3191 this.columns
["blocked"].updateTd = function(td
, row
) {
3192 let status
, addClass
;
3193 if (this.getRowValue(row
)) {
3194 status
= "QBT_TR(Blocked)QBT_TR[CONTEXT=ExecutionLogWidget]";
3195 addClass
= "peerBlocked";
3198 status
= "QBT_TR(Banned)QBT_TR[CONTEXT=ExecutionLogWidget]";
3199 addClass
= "peerBanned";
3201 td
.set({ "text": status
, "title": status
});
3202 td
.getParent("tr").className
= `logTableRow${addClass}`;
3206 getFilteredAndSortedRows: function() {
3207 let filteredRows
= [];
3208 const rows
= this.rows
.getValues();
3209 this.filterText
= window
.qBittorrent
.Log
.getFilterText();
3210 const filterTerms
= (this.filterText
.length
> 0) ? this.filterText
.toLowerCase().split(" ") : [];
3211 if (filterTerms
.length
> 0) {
3212 for (let i
= 0; i
< rows
.length
; ++i
) {
3213 if ((filterTerms
.length
> 0) && !window
.qBittorrent
.Misc
.containsAllTerms(rows
[i
].full_data
.ip
, filterTerms
))
3216 filteredRows
.push(rows
[i
]);
3220 filteredRows
= rows
;
3223 filteredRows
.sort((row1
, row2
) => {
3224 const column
= this.columns
[this.sortedColumn
];
3225 const res
= column
.compareRows(row1
, row2
);
3226 return (this.reverseSort
=== "0") ? res
: -res
;
3229 return filteredRows
;
3235 Object
.freeze(window
.qBittorrent
.DynamicTable
);
3237 /*************************************************************/