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,
54 TorrentWebseedsTable: TorrentWebseedsTable
58 const compareNumbers = (val1, val2) => {
66 let DynamicTableHeaderContextMenuClass = null;
67 let ProgressColumnWidth = -1;
69 const DynamicTable = new Class({
71 initialize: function() {},
73 setup: function(dynamicTableDivId, dynamicTableFixedHeaderDivId, contextMenu) {
74 this.dynamicTableDivId = dynamicTableDivId;
75 this.dynamicTableFixedHeaderDivId = dynamicTableFixedHeaderDivId;
76 this.fixedTableHeader = $(dynamicTableFixedHeaderDivId).getElements("tr")[0];
77 this.hiddenTableHeader = $(dynamicTableDivId).getElements("tr")[0];
78 this.tableBody = $(dynamicTableDivId).getElements("tbody")[0];
79 this.rows = new Map();
80 this.selectedRows = [];
82 this.contextMenu = contextMenu;
83 this.sortedColumn = LocalPreferences.get("sorted_column_" + this.dynamicTableDivId, 0);
84 this.reverseSort = LocalPreferences.get("reverse_sort_" + this.dynamicTableDivId, "0");
86 this.loadColumnsOrder();
87 this.updateTableHeaders();
88 this.setupCommonEvents();
89 this.setupHeaderEvents();
90 this.setupHeaderMenu();
91 this.setSortedColumnIcon(this.sortedColumn, null, (this.reverseSort === "1"));
95 setupCommonEvents: function() {
96 const tableDiv = $(this.dynamicTableDivId);
97 const tableFixedHeaderDiv = $(this.dynamicTableFixedHeaderDivId);
99 const tableElement = tableFixedHeaderDiv.querySelector("table");
100 tableDiv.addEventListener("scroll", () => {
101 tableElement.style.left = `${-tableDiv.scrollLeft}px`;
104 // if the table exists within a panel
105 const parentPanel = tableDiv.getParent(".panel");
107 const resizeFn = (entries) => {
108 const panel = entries[0].target;
109 let h = panel.getBoundingClientRect().height - tableFixedHeaderDiv.getBoundingClientRect().height;
110 tableDiv.style.height = `${h}px`;
112 // Workaround due to inaccurate calculation of elements heights by browser
115 // is panel vertical scrollbar visible or does panel content not fit?
116 while (((panel.clientWidth !== panel.offsetWidth) || (panel.clientHeight !== panel.scrollHeight)) && (n > 0)) {
119 tableDiv.style.height = `${h}px`;
123 const resizeDebouncer = window.qBittorrent.Misc.createDebounceHandler(100, (entries) => {
127 const resizeObserver = new ResizeObserver(resizeDebouncer);
128 resizeObserver.observe(parentPanel, { box: "border-box" });
132 setupHeaderEvents: function() {
133 this.currentHeaderAction = "";
134 this.canResize = false;
136 const resetElementBorderStyle = function(el, side) {
137 if ((side === "left") || (side !== "right"))
138 el.style.borderLeft = "";
139 if ((side === "right") || (side !== "left"))
140 el.style.borderRight = "";
143 const mouseMoveFn = function(e) {
144 const brect = e.target.getBoundingClientRect();
145 const mouseXRelative = e.clientX - brect.left;
146 if (this.currentHeaderAction === "") {
147 if ((brect.width - mouseXRelative) < 5) {
148 this.resizeTh = e.target;
149 this.canResize = true;
150 e.target.getParent("tr").style.cursor = "col-resize";
152 else if ((mouseXRelative < 5) && e.target.getPrevious('[class=""]')) {
153 this.resizeTh = e.target.getPrevious('[class=""]');
154 this.canResize = true;
155 e.target.getParent("tr").style.cursor = "col-resize";
158 this.canResize = false;
159 e.target.getParent("tr").style.cursor = "";
162 if (this.currentHeaderAction === "drag") {
163 const previousVisibleSibling = e.target.getPrevious('[class=""]');
164 let borderChangeElement = previousVisibleSibling;
165 let changeBorderSide = "right";
167 if (mouseXRelative > (brect.width / 2)) {
168 borderChangeElement = e.target;
169 this.dropSide = "right";
172 this.dropSide = "left";
175 e.target.getParent("tr").style.cursor = "move";
177 if (!previousVisibleSibling) { // right most column
178 borderChangeElement = e.target;
180 if (mouseXRelative <= (brect.width / 2))
181 changeBorderSide = "left";
184 const borderStyle = "initial solid #e60";
185 if (changeBorderSide === "left")
186 borderChangeElement.style.borderLeft = borderStyle;
188 borderChangeElement.style.borderRight = borderStyle;
190 resetElementBorderStyle(borderChangeElement, ((changeBorderSide === "right") ? "left" : "right"));
192 borderChangeElement.getSiblings('[class=""]').each((el) => {
193 resetElementBorderStyle(el);
196 this.lastHoverTh = e.target;
197 this.lastClientX = e.clientX;
200 const mouseOutFn = function(e) {
201 resetElementBorderStyle(e.target);
204 const onBeforeStart = function(el) {
206 this.currentHeaderAction = "start";
207 this.dragMovement = false;
208 this.dragStartX = this.lastClientX;
211 const onStart = function(el, event) {
212 if (this.canResize) {
213 this.currentHeaderAction = "resize";
214 this.startWidth = parseInt(this.resizeTh.style.width, 10);
217 this.currentHeaderAction = "drag";
218 el.style.backgroundColor = "#C1D5E7";
222 const onDrag = function(el, event) {
223 if (this.currentHeaderAction === "resize") {
224 let width = this.startWidth + (event.event.pageX - this.dragStartX);
227 this.columns[this.resizeTh.columnName].width = width;
228 this.updateColumn(this.resizeTh.columnName);
232 const onComplete = function(el, event) {
233 resetElementBorderStyle(this.lastHoverTh);
234 el.style.backgroundColor = "";
235 if (this.currentHeaderAction === "resize")
236 LocalPreferences.set("column_" + this.resizeTh.columnName + "_width_" + this.dynamicTableDivId, this.columns[this.resizeTh.columnName].width);
237 if ((this.currentHeaderAction === "drag") && (el !== this.lastHoverTh)) {
238 this.saveColumnsOrder();
239 const val = LocalPreferences.get("columns_order_" + this.dynamicTableDivId).split(",");
240 val.erase(el.columnName);
241 let pos = val.indexOf(this.lastHoverTh.columnName);
242 if (this.dropSide === "right")
244 val.splice(pos, 0, el.columnName);
245 LocalPreferences.set("columns_order_" + this.dynamicTableDivId, val.join(","));
246 this.loadColumnsOrder();
247 this.updateTableHeaders();
248 while (this.tableBody.firstChild)
249 this.tableBody.removeChild(this.tableBody.firstChild);
250 this.updateTable(true);
252 if (this.currentHeaderAction === "drag") {
253 resetElementBorderStyle(el);
254 el.getSiblings('[class=""]').each((el) => {
255 resetElementBorderStyle(el);
258 this.currentHeaderAction = "";
261 const onCancel = function(el) {
262 this.currentHeaderAction = "";
263 this.setSortedColumn(el.columnName);
266 const onTouch = function(e) {
267 const column = e.target.columnName;
268 this.currentHeaderAction = "";
269 this.setSortedColumn(column);
272 const ths = this.fixedTableHeader.getElements("th");
274 for (let i = 0; i < ths.length; ++i) {
276 th.addEventListener("mousemove", mouseMoveFn);
277 th.addEventListener("mouseout", mouseOutFn);
278 th.addEventListener("touchend", onTouch, { passive: true });
284 onBeforeStart: onBeforeStart,
287 onComplete: onComplete,
293 setupDynamicTableHeaderContextMenuClass: function() {
294 if (!DynamicTableHeaderContextMenuClass) {
295 DynamicTableHeaderContextMenuClass = new Class({
296 Extends: window.qBittorrent.ContextMenu.ContextMenu,
297 updateMenuItems: function() {
298 for (let i = 0; i < this.dynamicTable.columns.length; ++i) {
299 if (this.dynamicTable.columns[i].caption === "")
301 if (this.dynamicTable.columns[i].visible !== "0")
302 this.setItemChecked(this.dynamicTable.columns[i].name, true);
304 this.setItemChecked(this.dynamicTable.columns[i].name, false);
311 showColumn: function(columnName, show) {
312 this.columns[columnName].visible = show ? "1" : "0";
313 LocalPreferences.set("column_" + columnName + "_visible_" + this.dynamicTableDivId, show ? "1" : "0");
314 this.updateColumn(columnName);
317 setupHeaderMenu: function() {
318 this.setupDynamicTableHeaderContextMenuClass();
320 const menuId = this.dynamicTableDivId + "_headerMenu";
322 // reuse menu if already exists
323 const ul = $(menuId) ?? new Element("ul", {
325 class: "contextMenu scrollableMenu"
328 const createLi = function(columnName, text) {
329 const anchor = document.createElement("a");
330 anchor.href = `#${columnName}`;
331 anchor.textContent = text;
333 const img = document.createElement("img");
334 img.src = "images/checked-completed.svg";
337 const listItem = document.createElement("li");
338 listItem.appendChild(anchor);
345 const onMenuItemClicked = function(element, ref, action) {
346 this.showColumn(action, this.columns[action].visible === "0");
349 // recreate child nodes when reusing (enables the context menu to work correctly)
350 if (ul.hasChildNodes()) {
351 while (ul.firstChild)
352 ul.removeChild(ul.lastChild);
355 for (let i = 0; i < this.columns.length; ++i) {
356 const text = this.columns[i].caption;
359 ul.appendChild(createLi(this.columns[i].name, text));
360 actions[this.columns[i].name] = onMenuItemClicked;
363 ul.inject(document.body);
365 this.headerContextMenu = new DynamicTableHeaderContextMenuClass({
366 targets: "#" + this.dynamicTableFixedHeaderDivId + " tr",
375 this.headerContextMenu.dynamicTable = this;
378 initColumns: function() {},
380 newColumn: function(name, style, caption, defaultWidth, defaultVisible) {
382 column["name"] = name;
383 column["title"] = name;
384 column["visible"] = LocalPreferences.get("column_" + name + "_visible_" + this.dynamicTableDivId, defaultVisible ? "1" : "0");
385 column["force_hide"] = false;
386 column["caption"] = caption;
387 column["style"] = style;
388 column["width"] = LocalPreferences.get("column_" + name + "_width_" + this.dynamicTableDivId, defaultWidth);
389 column["dataProperties"] = [name];
390 column["getRowValue"] = function(row, pos) {
391 if (pos === undefined)
393 return row["full_data"][this.dataProperties[pos]];
395 column["compareRows"] = function(row1, row2) {
396 const value1 = this.getRowValue(row1);
397 const value2 = this.getRowValue(row2);
398 if ((typeof(value1) === "number") && (typeof(value2) === "number"))
399 return compareNumbers(value1, value2);
400 return window.qBittorrent.Misc.naturalSortCollator.compare(value1, value2);
402 column["updateTd"] = function(td, row) {
403 const value = this.getRowValue(row);
404 td.textContent = value;
407 column["onResize"] = null;
408 this.columns.push(column);
409 this.columns[name] = column;
411 this.hiddenTableHeader.appendChild(new Element("th"));
412 this.fixedTableHeader.appendChild(new Element("th"));
415 loadColumnsOrder: function() {
416 const columnsOrder = [];
417 const val = LocalPreferences.get("columns_order_" + this.dynamicTableDivId);
418 if ((val === null) || (val === undefined))
420 val.split(",").forEach((v) => {
421 if ((v in this.columns) && (!columnsOrder.contains(v)))
422 columnsOrder.push(v);
425 for (let i = 0; i < this.columns.length; ++i) {
426 if (!columnsOrder.contains(this.columns[i].name))
427 columnsOrder.push(this.columns[i].name);
430 for (let i = 0; i < this.columns.length; ++i)
431 this.columns[i] = this.columns[columnsOrder[i]];
434 saveColumnsOrder: function() {
436 for (let i = 0; i < this.columns.length; ++i) {
439 val += this.columns[i].name;
441 LocalPreferences.set("columns_order_" + this.dynamicTableDivId, val);
444 updateTableHeaders: function() {
445 this.updateHeader(this.hiddenTableHeader);
446 this.updateHeader(this.fixedTableHeader);
449 updateHeader: function(header) {
450 const ths = header.getElements("th");
452 for (let i = 0; i < ths.length; ++i) {
455 th.title = this.columns[i].caption;
456 th.textContent = this.columns[i].caption;
457 th.setAttribute("style", "width: " + this.columns[i].width + "px;" + this.columns[i].style);
458 th.columnName = this.columns[i].name;
459 th.addClass("column_" + th.columnName);
460 if ((this.columns[i].visible === "0") || this.columns[i].force_hide)
461 th.addClass("invisible");
463 th.removeClass("invisible");
467 getColumnPos: function(columnName) {
468 for (let i = 0; i < this.columns.length; ++i) {
469 if (this.columns[i].name === columnName)
475 updateColumn: function(columnName) {
476 const pos = this.getColumnPos(columnName);
477 const visible = ((this.columns[pos].visible !== "0") && !this.columns[pos].force_hide);
478 const ths = this.hiddenTableHeader.getElements("th");
479 const fths = this.fixedTableHeader.getElements("th");
480 const trs = this.tableBody.getElements("tr");
481 const style = "width: " + this.columns[pos].width + "px;" + this.columns[pos].style;
483 ths[pos].setAttribute("style", style);
484 fths[pos].setAttribute("style", style);
487 ths[pos].removeClass("invisible");
488 fths[pos].removeClass("invisible");
489 for (let i = 0; i < trs.length; ++i)
490 trs[i].getElements("td")[pos].removeClass("invisible");
493 ths[pos].addClass("invisible");
494 fths[pos].addClass("invisible");
495 for (let j = 0; j < trs.length; ++j)
496 trs[j].getElements("td")[pos].addClass("invisible");
498 if (this.columns[pos].onResize !== null)
499 this.columns[pos].onResize(columnName);
502 getSortedColumn: function() {
503 return LocalPreferences.get("sorted_column_" + this.dynamicTableDivId);
507 * @param {string} column name to sort by
508 * @param {string|null} reverse defaults to implementation-specific behavior when not specified. Should only be passed when restoring previous state.
510 setSortedColumn: function(column, reverse = null) {
511 if (column !== this.sortedColumn) {
512 const oldColumn = this.sortedColumn;
513 this.sortedColumn = column;
514 this.reverseSort = reverse ?? "0";
515 this.setSortedColumnIcon(column, oldColumn, false);
519 this.reverseSort = reverse ?? (this.reverseSort === "0" ? "1" : "0");
520 this.setSortedColumnIcon(column, null, (this.reverseSort === "1"));
522 LocalPreferences.set("sorted_column_" + this.dynamicTableDivId, column);
523 LocalPreferences.set("reverse_sort_" + this.dynamicTableDivId, this.reverseSort);
524 this.updateTable(false);
527 setSortedColumnIcon: function(newColumn, oldColumn, isReverse) {
528 const getCol = function(headerDivId, colName) {
529 const colElem = $$("#" + headerDivId + " .column_" + colName);
530 if (colElem.length === 1)
535 const colElem = getCol(this.dynamicTableFixedHeaderDivId, newColumn);
536 if (colElem !== null) {
537 colElem.addClass("sorted");
539 colElem.addClass("reverse");
541 colElem.removeClass("reverse");
543 const oldColElem = getCol(this.dynamicTableFixedHeaderDivId, oldColumn);
544 if (oldColElem !== null) {
545 oldColElem.removeClass("sorted");
546 oldColElem.removeClass("reverse");
550 getSelectedRowId: function() {
551 if (this.selectedRows.length > 0)
552 return this.selectedRows[0];
556 isRowSelected: function(rowId) {
557 return this.selectedRows.contains(rowId);
560 setupAltRow: function() {
561 const useAltRowColors = (LocalPreferences.get("use_alt_row_colors", "true") === "true");
563 document.getElementById(this.dynamicTableDivId).classList.add("altRowColors");
566 selectAll: function() {
569 const trs = this.tableBody.getElements("tr");
570 for (let i = 0; i < trs.length; ++i) {
572 this.selectedRows.push(tr.rowId);
573 if (!tr.hasClass("selected"))
574 tr.addClass("selected");
578 deselectAll: function() {
579 this.selectedRows.empty();
582 selectRow: function(rowId) {
583 this.selectedRows.push(rowId);
585 this.onSelectedRowChanged();
588 deselectRow: function(rowId) {
589 this.selectedRows.erase(rowId);
591 this.onSelectedRowChanged();
594 selectRows: function(rowId1, rowId2) {
596 if (rowId1 === rowId2) {
597 this.selectRow(rowId1);
603 this.tableBody.getElements("tr").each((tr) => {
604 if ((tr.rowId === rowId1) || (tr.rowId === rowId2)) {
606 that.selectedRows.push(tr.rowId);
609 that.selectedRows.push(tr.rowId);
613 this.onSelectedRowChanged();
616 reselectRows: function(rowIds) {
618 this.selectedRows = rowIds.slice();
619 this.tableBody.getElements("tr").each((tr) => {
620 if (rowIds.includes(tr.rowId))
621 tr.addClass("selected");
625 setRowClass: function() {
627 this.tableBody.getElements("tr").each((tr) => {
628 if (that.isRowSelected(tr.rowId))
629 tr.addClass("selected");
631 tr.removeClass("selected");
635 onSelectedRowChanged: function() {},
637 updateRowData: function(data) {
638 // ensure rowId is a string
639 const rowId = `${data["rowId"]}`;
642 if (!this.rows.has(rowId)) {
647 this.rows.set(rowId, row);
650 row = this.rows.get(rowId);
654 for (const x in data) {
655 if (!Object.hasOwn(data, x))
657 row["full_data"][x] = data[x];
661 getRow: function(rowId) {
662 return this.rows.get(rowId);
665 getFilteredAndSortedRows: function() {
666 const filteredRows = [];
668 for (const row of this.getRowValues()) {
669 filteredRows.push(row);
670 filteredRows[row.rowId] = row;
673 filteredRows.sort((row1, row2) => {
674 const column = this.columns[this.sortedColumn];
675 const res = column.compareRows(row1, row2);
676 if (this.reverseSort === "0")
684 getTrByRowId: function(rowId) {
685 const trs = this.tableBody.getElements("tr");
686 for (let i = 0; i < trs.length; ++i) {
687 if (trs[i].rowId === rowId)
693 updateTable: function(fullUpdate = false) {
694 const rows = this.getFilteredAndSortedRows();
696 for (let i = 0; i < this.selectedRows.length; ++i) {
697 if (!(this.selectedRows[i] in rows)) {
698 this.selectedRows.splice(i, 1);
703 const trs = this.tableBody.getElements("tr");
705 for (let rowPos = 0; rowPos < rows.length; ++rowPos) {
706 const rowId = rows[rowPos]["rowId"];
707 let tr_found = false;
708 for (let j = rowPos; j < trs.length; ++j) {
709 if (trs[j]["rowId"] === rowId) {
713 trs[j].inject(trs[rowPos], "before");
714 const tmpTr = trs[j];
716 trs.splice(rowPos, 0, tmpTr);
720 if (tr_found) { // row already exists in the table
721 this.updateRow(trs[rowPos], fullUpdate);
723 else { // else create a new row in the table
724 const tr = new Element("tr");
725 // set tabindex so element receives keydown events
726 // more info: https://developer.mozilla.org/en-US/docs/Web/API/Element/keydown_event
729 const rowId = rows[rowPos]["rowId"];
730 tr.setAttribute("data-row-id", rowId);
734 tr.addEventListener("contextmenu", function(e) {
735 if (!this._this.isRowSelected(this.rowId)) {
736 this._this.deselectAll();
737 this._this.selectRow(this.rowId);
741 tr.addEventListener("click", function(e) {
745 if (e.ctrlKey || e.metaKey) {
746 // CTRL/CMD ⌘ key was pressed
747 if (this._this.isRowSelected(this.rowId))
748 this._this.deselectRow(this.rowId);
750 this._this.selectRow(this.rowId);
752 else if (e.shiftKey && (this._this.selectedRows.length === 1)) {
753 // Shift key was pressed
754 this._this.selectRows(this._this.getSelectedRowId(), this.rowId);
758 this._this.deselectAll();
759 this._this.selectRow(this.rowId);
763 tr.addEventListener("touchstart", function(e) {
764 if (!this._this.isRowSelected(this.rowId)) {
765 this._this.deselectAll();
766 this._this.selectRow(this.rowId);
768 }, { passive: true });
769 tr.addEventListener("keydown", function(event) {
772 this._this.selectPreviousRow();
775 this._this.selectNextRow();
782 for (let k = 0; k < this.columns.length; ++k) {
783 const td = new Element("td");
784 if ((this.columns[k].visible === "0") || this.columns[k].force_hide)
785 td.addClass("invisible");
790 if (rowPos >= trs.length) {
791 tr.inject(this.tableBody);
795 tr.inject(trs[rowPos], "before");
796 trs.splice(rowPos, 0, tr);
799 // Update context menu
800 if (this.contextMenu)
801 this.contextMenu.addTarget(tr);
803 this.updateRow(tr, true);
807 const rowPos = rows.length;
809 while ((rowPos < trs.length) && (trs.length > 0))
813 setupTr: function(tr) {},
815 updateRow: function(tr, fullUpdate) {
816 const row = this.rows.get(tr.rowId);
817 const data = row[fullUpdate ? "full_data" : "data"];
819 const tds = tr.getElements("td");
820 for (let i = 0; i < this.columns.length; ++i) {
821 if (Object.hasOwn(data, this.columns[i].dataProperties[0]))
822 this.columns[i].updateTd(tds[i], row);
827 removeRow: function(rowId) {
828 this.selectedRows.erase(rowId);
829 this.rows.delete(rowId);
830 const tr = this.getTrByRowId(rowId);
837 const trs = this.tableBody.getElements("tr");
838 while (trs.length > 0)
842 selectedRowsIds: function() {
843 return this.selectedRows.slice();
846 getRowIds: function() {
847 return this.rows.keys();
850 getRowValues: function() {
851 return this.rows.values();
854 getRowItems: function() {
855 return this.rows.entries();
858 getRowSize: function() {
859 return this.rows.size;
862 selectNextRow: function() {
863 const visibleRows = $(this.dynamicTableDivId).getElements("tbody tr").filter(e => e.style.display !== "none");
864 const selectedRowId = this.getSelectedRowId();
866 let selectedIndex = -1;
867 for (let i = 0; i < visibleRows.length; ++i) {
868 const row = visibleRows[i];
869 if (row.getAttribute("data-row-id") === selectedRowId) {
875 const isLastRowSelected = (selectedIndex >= (visibleRows.length - 1));
876 if (!isLastRowSelected) {
879 const newRow = visibleRows[selectedIndex + 1];
880 this.selectRow(newRow.getAttribute("data-row-id"));
884 selectPreviousRow: function() {
885 const visibleRows = $(this.dynamicTableDivId).getElements("tbody tr").filter(e => e.style.display !== "none");
886 const selectedRowId = this.getSelectedRowId();
888 let selectedIndex = -1;
889 for (let i = 0; i < visibleRows.length; ++i) {
890 const row = visibleRows[i];
891 if (row.getAttribute("data-row-id") === selectedRowId) {
897 const isFirstRowSelected = selectedIndex <= 0;
898 if (!isFirstRowSelected) {
901 const newRow = visibleRows[selectedIndex - 1];
902 this.selectRow(newRow.getAttribute("data-row-id"));
907 const TorrentsTable = new Class({
908 Extends: DynamicTable,
910 initColumns: function() {
911 this.newColumn("priority", "", "#", 30, true);
912 this.newColumn("state_icon", "cursor: default", "", 22, true);
913 this.newColumn("name", "", "QBT_TR(Name)QBT_TR[CONTEXT=TransferListModel]", 200, true);
914 this.newColumn("size", "", "QBT_TR(Size)QBT_TR[CONTEXT=TransferListModel]", 100, true);
915 this.newColumn("total_size", "", "QBT_TR(Total Size)QBT_TR[CONTEXT=TransferListModel]", 100, false);
916 this.newColumn("progress", "", "QBT_TR(Progress)QBT_TR[CONTEXT=TransferListModel]", 85, true);
917 this.newColumn("status", "", "QBT_TR(Status)QBT_TR[CONTEXT=TransferListModel]", 100, true);
918 this.newColumn("num_seeds", "", "QBT_TR(Seeds)QBT_TR[CONTEXT=TransferListModel]", 100, true);
919 this.newColumn("num_leechs", "", "QBT_TR(Peers)QBT_TR[CONTEXT=TransferListModel]", 100, true);
920 this.newColumn("dlspeed", "", "QBT_TR(Down Speed)QBT_TR[CONTEXT=TransferListModel]", 100, true);
921 this.newColumn("upspeed", "", "QBT_TR(Up Speed)QBT_TR[CONTEXT=TransferListModel]", 100, true);
922 this.newColumn("eta", "", "QBT_TR(ETA)QBT_TR[CONTEXT=TransferListModel]", 100, true);
923 this.newColumn("ratio", "", "QBT_TR(Ratio)QBT_TR[CONTEXT=TransferListModel]", 100, true);
924 this.newColumn("popularity", "", "QBT_TR(Popularity)QBT_TR[CONTEXT=TransferListModel]", 100, true);
925 this.newColumn("category", "", "QBT_TR(Category)QBT_TR[CONTEXT=TransferListModel]", 100, true);
926 this.newColumn("tags", "", "QBT_TR(Tags)QBT_TR[CONTEXT=TransferListModel]", 100, true);
927 this.newColumn("added_on", "", "QBT_TR(Added On)QBT_TR[CONTEXT=TransferListModel]", 100, true);
928 this.newColumn("completion_on", "", "QBT_TR(Completed On)QBT_TR[CONTEXT=TransferListModel]", 100, false);
929 this.newColumn("tracker", "", "QBT_TR(Tracker)QBT_TR[CONTEXT=TransferListModel]", 100, false);
930 this.newColumn("dl_limit", "", "QBT_TR(Down Limit)QBT_TR[CONTEXT=TransferListModel]", 100, false);
931 this.newColumn("up_limit", "", "QBT_TR(Up Limit)QBT_TR[CONTEXT=TransferListModel]", 100, false);
932 this.newColumn("downloaded", "", "QBT_TR(Downloaded)QBT_TR[CONTEXT=TransferListModel]", 100, false);
933 this.newColumn("uploaded", "", "QBT_TR(Uploaded)QBT_TR[CONTEXT=TransferListModel]", 100, false);
934 this.newColumn("downloaded_session", "", "QBT_TR(Session Download)QBT_TR[CONTEXT=TransferListModel]", 100, false);
935 this.newColumn("uploaded_session", "", "QBT_TR(Session Upload)QBT_TR[CONTEXT=TransferListModel]", 100, false);
936 this.newColumn("amount_left", "", "QBT_TR(Remaining)QBT_TR[CONTEXT=TransferListModel]", 100, false);
937 this.newColumn("time_active", "", "QBT_TR(Time Active)QBT_TR[CONTEXT=TransferListModel]", 100, false);
938 this.newColumn("save_path", "", "QBT_TR(Save path)QBT_TR[CONTEXT=TransferListModel]", 100, false);
939 this.newColumn("completed", "", "QBT_TR(Completed)QBT_TR[CONTEXT=TransferListModel]", 100, false);
940 this.newColumn("max_ratio", "", "QBT_TR(Ratio Limit)QBT_TR[CONTEXT=TransferListModel]", 100, false);
941 this.newColumn("seen_complete", "", "QBT_TR(Last Seen Complete)QBT_TR[CONTEXT=TransferListModel]", 100, false);
942 this.newColumn("last_activity", "", "QBT_TR(Last Activity)QBT_TR[CONTEXT=TransferListModel]", 100, false);
943 this.newColumn("availability", "", "QBT_TR(Availability)QBT_TR[CONTEXT=TransferListModel]", 100, false);
944 this.newColumn("download_path", "", "QBT_TR(Incomplete Save Path)QBT_TR[CONTEXT=TransferListModel]", 100, false);
945 this.newColumn("infohash_v1", "", "QBT_TR(Info Hash v1)QBT_TR[CONTEXT=TransferListModel]", 100, false);
946 this.newColumn("infohash_v2", "", "QBT_TR(Info Hash v2)QBT_TR[CONTEXT=TransferListModel]", 100, false);
947 this.newColumn("reannounce", "", "QBT_TR(Reannounce In)QBT_TR[CONTEXT=TransferListModel]", 100, false);
948 this.newColumn("private", "", "QBT_TR(Private)QBT_TR[CONTEXT=TransferListModel]", 100, false);
950 this.columns["state_icon"].onclick = "";
951 this.columns["state_icon"].dataProperties[0] = "state";
953 this.columns["num_seeds"].dataProperties.push("num_complete");
954 this.columns["num_leechs"].dataProperties.push("num_incomplete");
955 this.columns["time_active"].dataProperties.push("seeding_time");
957 this.initColumnsFunctions();
960 initColumnsFunctions: function() {
963 this.columns["state_icon"].updateTd = function(td, row) {
964 let state = this.getRowValue(row);
972 state = "downloading";
973 img_path = "images/downloading.svg";
978 img_path = "images/upload.svg";
982 img_path = "images/stalledUP.svg";
986 img_path = "images/stalledDL.svg";
989 state = "torrent-stop";
990 img_path = "images/stopped.svg";
993 state = "checked-completed";
994 img_path = "images/checked-completed.svg";
999 img_path = "images/queued.svg";
1003 case "queuedForChecking":
1004 case "checkingResumeData":
1005 state = "force-recheck";
1006 img_path = "images/force-recheck.svg";
1010 img_path = "images/set-location.svg";
1014 case "missingFiles":
1016 img_path = "images/error.svg";
1019 break; // do nothing
1022 if (td.getChildren("img").length > 0) {
1023 const img = td.getChildren("img")[0];
1024 if (!img.src.includes(img_path)) {
1030 td.adopt(new Element("img", {
1032 "class": "stateIcon",
1039 this.columns["status"].updateTd = function(td, row) {
1040 const state = this.getRowValue(row);
1047 status = "QBT_TR(Downloading)QBT_TR[CONTEXT=TransferListDelegate]";
1050 status = "QBT_TR(Stalled)QBT_TR[CONTEXT=TransferListDelegate]";
1053 status = "QBT_TR(Downloading metadata)QBT_TR[CONTEXT=TransferListDelegate]";
1055 case "forcedMetaDL":
1056 status = "QBT_TR([F] Downloading metadata)QBT_TR[CONTEXT=TransferListDelegate]";
1059 status = "QBT_TR([F] Downloading)QBT_TR[CONTEXT=TransferListDelegate]";
1063 status = "QBT_TR(Seeding)QBT_TR[CONTEXT=TransferListDelegate]";
1066 status = "QBT_TR([F] Seeding)QBT_TR[CONTEXT=TransferListDelegate]";
1070 status = "QBT_TR(Queued)QBT_TR[CONTEXT=TransferListDelegate]";
1074 status = "QBT_TR(Checking)QBT_TR[CONTEXT=TransferListDelegate]";
1076 case "queuedForChecking":
1077 status = "QBT_TR(Queued for checking)QBT_TR[CONTEXT=TransferListDelegate]";
1079 case "checkingResumeData":
1080 status = "QBT_TR(Checking resume data)QBT_TR[CONTEXT=TransferListDelegate]";
1083 status = "QBT_TR(Stopped)QBT_TR[CONTEXT=TransferListDelegate]";
1086 status = "QBT_TR(Completed)QBT_TR[CONTEXT=TransferListDelegate]";
1089 status = "QBT_TR(Moving)QBT_TR[CONTEXT=TransferListDelegate]";
1091 case "missingFiles":
1092 status = "QBT_TR(Missing Files)QBT_TR[CONTEXT=TransferListDelegate]";
1095 status = "QBT_TR(Errored)QBT_TR[CONTEXT=TransferListDelegate]";
1098 status = "QBT_TR(Unknown)QBT_TR[CONTEXT=HttpServer]";
1101 td.textContent = status;
1105 this.columns["status"].compareRows = function(row1, row2) {
1106 return compareNumbers(row1.full_data._statusOrder, row2.full_data._statusOrder);
1110 this.columns["priority"].updateTd = function(td, row) {
1111 const queuePos = this.getRowValue(row);
1112 const formattedQueuePos = (queuePos < 1) ? "*" : queuePos;
1113 td.textContent = formattedQueuePos;
1114 td.title = formattedQueuePos;
1117 this.columns["priority"].compareRows = function(row1, row2) {
1118 let row1_val = this.getRowValue(row1);
1119 let row2_val = this.getRowValue(row2);
1124 return compareNumbers(row1_val, row2_val);
1127 // name, category, tags
1128 this.columns["name"].compareRows = function(row1, row2) {
1129 const row1Val = this.getRowValue(row1);
1130 const row2Val = this.getRowValue(row2);
1131 return row1Val.localeCompare(row2Val, undefined, { numeric: true, sensitivity: "base" });
1133 this.columns["category"].compareRows = this.columns["name"].compareRows;
1134 this.columns["tags"].compareRows = this.columns["name"].compareRows;
1137 this.columns["size"].updateTd = function(td, row) {
1138 const size = window.qBittorrent.Misc.friendlyUnit(this.getRowValue(row), false);
1139 td.textContent = size;
1142 this.columns["total_size"].updateTd = this.columns["size"].updateTd;
1145 this.columns["progress"].updateTd = function(td, row) {
1146 const progress = this.getRowValue(row);
1147 let progressFormatted = (progress * 100).round(1);
1148 if ((progressFormatted === 100.0) && (progress !== 1.0))
1149 progressFormatted = 99.9;
1151 if (td.getChildren("div").length > 0) {
1152 const div = td.getChildren("div")[0];
1155 div.setWidth(ProgressColumnWidth - 5);
1157 if (div.getValue() !== progressFormatted)
1158 div.setValue(progressFormatted);
1161 if (ProgressColumnWidth < 0)
1162 ProgressColumnWidth = td.offsetWidth;
1163 td.adopt(new window.qBittorrent.ProgressBar.ProgressBar(progressFormatted.toFloat(), {
1164 "width": ProgressColumnWidth - 5
1170 this.columns["progress"].onResize = function(columnName) {
1171 const pos = this.getColumnPos(columnName);
1172 const trs = this.tableBody.getElements("tr");
1173 ProgressColumnWidth = -1;
1174 for (let i = 0; i < trs.length; ++i) {
1175 const td = trs[i].getElements("td")[pos];
1176 if (ProgressColumnWidth < 0)
1177 ProgressColumnWidth = td.offsetWidth;
1179 this.columns[columnName].updateTd(td, this.rows.get(trs[i].rowId));
1184 this.columns["num_seeds"].updateTd = function(td, row) {
1185 const num_seeds = this.getRowValue(row, 0);
1186 const num_complete = this.getRowValue(row, 1);
1187 let value = num_seeds;
1188 if (num_complete !== -1)
1189 value += " (" + num_complete + ")";
1190 td.textContent = value;
1193 this.columns["num_seeds"].compareRows = function(row1, row2) {
1194 const num_seeds1 = this.getRowValue(row1, 0);
1195 const num_complete1 = this.getRowValue(row1, 1);
1197 const num_seeds2 = this.getRowValue(row2, 0);
1198 const num_complete2 = this.getRowValue(row2, 1);
1200 const result = compareNumbers(num_complete1, num_complete2);
1203 return compareNumbers(num_seeds1, num_seeds2);
1207 this.columns["num_leechs"].updateTd = this.columns["num_seeds"].updateTd;
1208 this.columns["num_leechs"].compareRows = this.columns["num_seeds"].compareRows;
1211 this.columns["dlspeed"].updateTd = function(td, row) {
1212 const speed = window.qBittorrent.Misc.friendlyUnit(this.getRowValue(row), true);
1213 td.textContent = speed;
1218 this.columns["upspeed"].updateTd = this.columns["dlspeed"].updateTd;
1221 this.columns["eta"].updateTd = function(td, row) {
1222 const eta = window.qBittorrent.Misc.friendlyDuration(this.getRowValue(row), window.qBittorrent.Misc.MAX_ETA);
1223 td.textContent = eta;
1228 this.columns["ratio"].updateTd = function(td, row) {
1229 const ratio = this.getRowValue(row);
1230 const string = (ratio === -1) ? "∞" : window.qBittorrent.Misc.toFixedPointString(ratio, 2);
1231 td.textContent = string;
1236 this.columns["popularity"].updateTd = function(td, row) {
1237 const value = this.getRowValue(row);
1238 const popularity = (value === -1) ? "∞" : window.qBittorrent.Misc.toFixedPointString(value, 2);
1239 td.textContent = popularity;
1240 td.title = popularity;
1244 this.columns["added_on"].updateTd = function(td, row) {
1245 const date = new Date(this.getRowValue(row) * 1000).toLocaleString();
1246 td.textContent = date;
1251 this.columns["completion_on"].updateTd = function(td, row) {
1252 const val = this.getRowValue(row);
1253 if ((val === 0xffffffff) || (val < 0)) {
1254 td.textContent = "";
1258 const date = new Date(this.getRowValue(row) * 1000).toLocaleString();
1259 td.textContent = date;
1265 this.columns["tracker"].updateTd = function(td, row) {
1266 const value = this.getRowValue(row);
1267 const tracker = displayFullURLTrackerColumn ? value : window.qBittorrent.Misc.getHost(value);
1268 td.textContent = tracker;
1272 // dl_limit, up_limit
1273 this.columns["dl_limit"].updateTd = function(td, row) {
1274 const speed = this.getRowValue(row);
1276 td.textContent = "∞";
1280 const formattedSpeed = window.qBittorrent.Misc.friendlyUnit(speed, true);
1281 td.textContent = formattedSpeed;
1282 td.title = formattedSpeed;
1286 this.columns["up_limit"].updateTd = this.columns["dl_limit"].updateTd;
1288 // downloaded, uploaded, downloaded_session, uploaded_session, amount_left
1289 this.columns["downloaded"].updateTd = this.columns["size"].updateTd;
1290 this.columns["uploaded"].updateTd = this.columns["size"].updateTd;
1291 this.columns["downloaded_session"].updateTd = this.columns["size"].updateTd;
1292 this.columns["uploaded_session"].updateTd = this.columns["size"].updateTd;
1293 this.columns["amount_left"].updateTd = this.columns["size"].updateTd;
1296 this.columns["time_active"].updateTd = function(td, row) {
1297 const activeTime = this.getRowValue(row, 0);
1298 const seedingTime = this.getRowValue(row, 1);
1299 const time = (seedingTime > 0)
1300 ? ("QBT_TR(%1 (seeded for %2))QBT_TR[CONTEXT=TransferListDelegate]"
1301 .replace("%1", window.qBittorrent.Misc.friendlyDuration(activeTime))
1302 .replace("%2", window.qBittorrent.Misc.friendlyDuration(seedingTime)))
1303 : window.qBittorrent.Misc.friendlyDuration(activeTime);
1304 td.textContent = time;
1309 this.columns["completed"].updateTd = this.columns["size"].updateTd;
1312 this.columns["max_ratio"].updateTd = this.columns["ratio"].updateTd;
1315 this.columns["seen_complete"].updateTd = this.columns["completion_on"].updateTd;
1318 this.columns["last_activity"].updateTd = function(td, row) {
1319 const val = this.getRowValue(row);
1321 td.textContent = "∞";
1325 const formattedVal = "QBT_TR(%1 ago)QBT_TR[CONTEXT=TransferListDelegate]".replace("%1", window.qBittorrent.Misc.friendlyDuration((new Date() / 1000) - val));
1326 td.textContent = formattedVal;
1327 td.title = formattedVal;
1332 this.columns["availability"].updateTd = function(td, row) {
1333 const value = window.qBittorrent.Misc.toFixedPointString(this.getRowValue(row), 3);
1334 td.textContent = value;
1339 this.columns["infohash_v1"].updateTd = function(td, row) {
1340 const sourceInfohashV1 = this.getRowValue(row);
1341 const infohashV1 = (sourceInfohashV1 !== "") ? sourceInfohashV1 : "QBT_TR(N/A)QBT_TR[CONTEXT=TransferListDelegate]";
1342 td.textContent = infohashV1;
1343 td.title = infohashV1;
1347 this.columns["infohash_v2"].updateTd = function(td, row) {
1348 const sourceInfohashV2 = this.getRowValue(row);
1349 const infohashV2 = (sourceInfohashV2 !== "") ? sourceInfohashV2 : "QBT_TR(N/A)QBT_TR[CONTEXT=TransferListDelegate]";
1350 td.textContent = infohashV2;
1351 td.title = infohashV2;
1355 this.columns["reannounce"].updateTd = function(td, row) {
1356 const time = window.qBittorrent.Misc.friendlyDuration(this.getRowValue(row));
1357 td.textContent = time;
1362 this.columns["private"].updateTd = function(td, row) {
1363 const hasMetadata = row["full_data"].has_metadata;
1364 const isPrivate = this.getRowValue(row);
1365 const string = hasMetadata
1367 ? "QBT_TR(Yes)QBT_TR[CONTEXT=PropertiesWidget]"
1368 : "QBT_TR(No)QBT_TR[CONTEXT=PropertiesWidget]")
1369 : "QBT_TR(N/A)QBT_TR[CONTEXT=PropertiesWidget]";
1370 td.textContent = string;
1375 applyFilter: function(row, filterName, categoryHash, tagHash, trackerHash, filterTerms) {
1376 const state = row["full_data"].state;
1377 let inactive = false;
1379 switch (filterName) {
1381 if ((state !== "downloading") && !state.includes("DL"))
1385 if ((state !== "uploading") && (state !== "forcedUP") && (state !== "stalledUP") && (state !== "queuedUP") && (state !== "checkingUP"))
1389 if ((state !== "uploading") && !state.includes("UP"))
1393 if (!state.includes("stopped"))
1397 if (state.includes("stopped"))
1401 if ((state !== "stalledUP") && (state !== "stalledDL"))
1404 case "stalled_uploading":
1405 if (state !== "stalledUP")
1408 case "stalled_downloading":
1409 if (state !== "stalledDL")
1417 if (state === "stalledDL")
1418 r = (row["full_data"].upspeed > 0);
1420 r = (state === "metaDL") || (state === "forcedMetaDL") || (state === "downloading") || (state === "forcedDL") || (state === "uploading") || (state === "forcedUP");
1426 if ((state !== "checkingUP") && (state !== "checkingDL") && (state !== "checkingResumeData"))
1430 if (state !== "moving")
1434 if ((state !== "error") && (state !== "unknown") && (state !== "missingFiles"))
1439 switch (categoryHash) {
1440 case CATEGORIES_ALL:
1441 break; // do nothing
1442 case CATEGORIES_UNCATEGORIZED:
1443 if (row["full_data"].category.length !== 0)
1445 break; // do nothing
1447 if (!useSubcategories) {
1448 if (categoryHash !== window.qBittorrent.Misc.genHash(row["full_data"].category))
1452 const selectedCategory = category_list.get(categoryHash);
1453 if (selectedCategory !== undefined) {
1454 const selectedCategoryName = selectedCategory.name + "/";
1455 const torrentCategoryName = row["full_data"].category + "/";
1456 if (!torrentCategoryName.startsWith(selectedCategoryName))
1465 break; // do nothing
1468 if (row["full_data"].tags.length !== 0)
1470 break; // do nothing
1473 const tagHashes = row["full_data"].tags.split(", ").map(tag => window.qBittorrent.Misc.genHash(tag));
1474 if (!tagHashes.contains(tagHash))
1480 switch (trackerHash) {
1482 break; // do nothing
1483 case TRACKERS_TRACKERLESS:
1484 if (row["full_data"].trackers_count !== 0)
1488 const tracker = trackerList.get(trackerHash);
1491 for (const torrents of tracker.trackerTorrentMap.values()) {
1492 if (torrents.has(row["full_data"].rowId)) {
1504 if ((filterTerms !== undefined) && (filterTerms !== null)) {
1505 const filterBy = document.getElementById("torrentsFilterSelect").value;
1506 const textToSearch = row["full_data"][filterBy].toLowerCase();
1507 if (filterTerms instanceof RegExp) {
1508 if (!filterTerms.test(textToSearch))
1512 if ((filterTerms.length > 0) && !window.qBittorrent.Misc.containsAllTerms(textToSearch, filterTerms))
1520 getFilteredTorrentsNumber: function(filterName, categoryHash, tagHash, trackerHash) {
1523 for (const row of this.rows.values()) {
1524 if (this.applyFilter(row, filterName, categoryHash, tagHash, trackerHash, null))
1530 getFilteredTorrentsHashes: function(filterName, categoryHash, tagHash, trackerHash) {
1531 const rowsHashes = [];
1532 const useRegex = document.getElementById("torrentsFilterRegexBox").checked;
1533 const filterText = document.getElementById("torrentsFilterInput").value.trim().toLowerCase();
1536 filterTerms = (filterText.length > 0)
1537 ? (useRegex ? new RegExp(filterText) : filterText.split(" "))
1540 catch (e) { // SyntaxError: Invalid regex pattern
1541 return filteredRows;
1544 for (const row of this.rows.values()) {
1545 if (this.applyFilter(row, filterName, categoryHash, tagHash, trackerHash, filterTerms))
1546 rowsHashes.push(row["rowId"]);
1552 getFilteredAndSortedRows: function() {
1553 const filteredRows = [];
1555 const useRegex = $("torrentsFilterRegexBox").checked;
1556 const filterText = $("torrentsFilterInput").value.trim().toLowerCase();
1559 filterTerms = (filterText.length > 0)
1560 ? (useRegex ? new RegExp(filterText) : filterText.split(" "))
1563 catch (e) { // SyntaxError: Invalid regex pattern
1564 return filteredRows;
1567 for (const row of this.rows.values()) {
1568 if (this.applyFilter(row, selectedStatus, selectedCategory, selectedTag, selectedTracker, filterTerms)) {
1569 filteredRows.push(row);
1570 filteredRows[row.rowId] = row;
1574 filteredRows.sort((row1, row2) => {
1575 const column = this.columns[this.sortedColumn];
1576 const res = column.compareRows(row1, row2);
1577 if (this.reverseSort === "0")
1582 return filteredRows;
1585 setupTr: function(tr) {
1586 tr.addEventListener("dblclick", function(e) {
1588 e.stopPropagation();
1590 this._this.deselectAll();
1591 this._this.selectRow(this.rowId);
1592 const row = this._this.rows.get(this.rowId);
1593 const state = row["full_data"].state;
1596 (state !== "uploading")
1597 && (state !== "stoppedUP")
1598 && (state !== "forcedUP")
1599 && (state !== "stalledUP")
1600 && (state !== "queuedUP")
1601 && (state !== "checkingUP")
1602 ? "dblclick_download"
1603 : "dblclick_complete";
1605 if (LocalPreferences.get(prefKey, "1") !== "1")
1608 if (state.includes("stopped"))
1614 tr.addClass("torrentsTableContextMenuTarget");
1617 getCurrentTorrentID: function() {
1618 return this.getSelectedRowId();
1621 onSelectedRowChanged: function() {
1622 updatePropertiesPanel();
1626 const TorrentPeersTable = new Class({
1627 Extends: DynamicTable,
1629 initColumns: function() {
1630 this.newColumn("country", "", "QBT_TR(Country/Region)QBT_TR[CONTEXT=PeerListWidget]", 22, true);
1631 this.newColumn("ip", "", "QBT_TR(IP)QBT_TR[CONTEXT=PeerListWidget]", 80, true);
1632 this.newColumn("port", "", "QBT_TR(Port)QBT_TR[CONTEXT=PeerListWidget]", 35, true);
1633 this.newColumn("connection", "", "QBT_TR(Connection)QBT_TR[CONTEXT=PeerListWidget]", 50, true);
1634 this.newColumn("flags", "", "QBT_TR(Flags)QBT_TR[CONTEXT=PeerListWidget]", 50, true);
1635 this.newColumn("client", "", "QBT_TR(Client)QBT_TR[CONTEXT=PeerListWidget]", 140, true);
1636 this.newColumn("peer_id_client", "", "QBT_TR(Peer ID Client)QBT_TR[CONTEXT=PeerListWidget]", 60, false);
1637 this.newColumn("progress", "", "QBT_TR(Progress)QBT_TR[CONTEXT=PeerListWidget]", 50, true);
1638 this.newColumn("dl_speed", "", "QBT_TR(Down Speed)QBT_TR[CONTEXT=PeerListWidget]", 50, true);
1639 this.newColumn("up_speed", "", "QBT_TR(Up Speed)QBT_TR[CONTEXT=PeerListWidget]", 50, true);
1640 this.newColumn("downloaded", "", "QBT_TR(Downloaded)QBT_TR[CONTEXT=PeerListWidget]", 50, true);
1641 this.newColumn("uploaded", "", "QBT_TR(Uploaded)QBT_TR[CONTEXT=PeerListWidget]", 50, true);
1642 this.newColumn("relevance", "", "QBT_TR(Relevance)QBT_TR[CONTEXT=PeerListWidget]", 30, true);
1643 this.newColumn("files", "", "QBT_TR(Files)QBT_TR[CONTEXT=PeerListWidget]", 100, true);
1645 this.columns["country"].dataProperties.push("country_code");
1646 this.columns["flags"].dataProperties.push("flags_desc");
1647 this.initColumnsFunctions();
1650 initColumnsFunctions: function() {
1653 this.columns["country"].updateTd = function(td, row) {
1654 const country = this.getRowValue(row, 0);
1655 const country_code = this.getRowValue(row, 1);
1657 let span = td.firstElementChild;
1658 if (span === null) {
1659 span = document.createElement("span");
1660 span.classList.add("flags");
1664 span.style.backgroundImage = `url('images/flags/${country_code ?? "xx"}.svg')`;
1665 span.textContent = country;
1670 this.columns["ip"].compareRows = function(row1, row2) {
1671 const ip1 = this.getRowValue(row1);
1672 const ip2 = this.getRowValue(row2);
1674 const a = ip1.split(".");
1675 const b = ip2.split(".");
1677 for (let i = 0; i < 4; ++i) {
1686 this.columns["flags"].updateTd = function(td, row) {
1687 td.textContent = this.getRowValue(row, 0);
1688 td.title = this.getRowValue(row, 1);
1692 this.columns["progress"].updateTd = function(td, row) {
1693 const progress = this.getRowValue(row);
1694 let progressFormatted = (progress * 100).round(1);
1695 if ((progressFormatted === 100.0) && (progress !== 1.0))
1696 progressFormatted = 99.9;
1697 progressFormatted += "%";
1698 td.textContent = progressFormatted;
1699 td.title = progressFormatted;
1702 // dl_speed, up_speed
1703 this.columns["dl_speed"].updateTd = function(td, row) {
1704 const speed = this.getRowValue(row);
1706 td.textContent = "";
1710 const formattedSpeed = window.qBittorrent.Misc.friendlyUnit(speed, true);
1711 td.textContent = formattedSpeed;
1712 td.title = formattedSpeed;
1715 this.columns["up_speed"].updateTd = this.columns["dl_speed"].updateTd;
1717 // downloaded, uploaded
1718 this.columns["downloaded"].updateTd = function(td, row) {
1719 const downloaded = window.qBittorrent.Misc.friendlyUnit(this.getRowValue(row), false);
1720 td.textContent = downloaded;
1721 td.title = downloaded;
1723 this.columns["uploaded"].updateTd = this.columns["downloaded"].updateTd;
1726 this.columns["relevance"].updateTd = this.columns["progress"].updateTd;
1729 this.columns["files"].updateTd = function(td, row) {
1730 const value = this.getRowValue(row, 0);
1731 td.textContent = value.replace(/\n/g, ";");
1738 const SearchResultsTable = new Class({
1739 Extends: DynamicTable,
1741 initColumns: function() {
1742 this.newColumn("fileName", "", "QBT_TR(Name)QBT_TR[CONTEXT=SearchResultsTable]", 500, true);
1743 this.newColumn("fileSize", "", "QBT_TR(Size)QBT_TR[CONTEXT=SearchResultsTable]", 100, true);
1744 this.newColumn("nbSeeders", "", "QBT_TR(Seeders)QBT_TR[CONTEXT=SearchResultsTable]", 100, true);
1745 this.newColumn("nbLeechers", "", "QBT_TR(Leechers)QBT_TR[CONTEXT=SearchResultsTable]", 100, true);
1746 this.newColumn("engineName", "", "QBT_TR(Engine)QBT_TR[CONTEXT=SearchResultsTable]", 100, true);
1747 this.newColumn("siteUrl", "", "QBT_TR(Engine URL)QBT_TR[CONTEXT=SearchResultsTable]", 250, true);
1748 this.newColumn("pubDate", "", "QBT_TR(Published On)QBT_TR[CONTEXT=SearchResultsTable]", 200, true);
1750 this.initColumnsFunctions();
1753 initColumnsFunctions: function() {
1754 const displaySize = function(td, row) {
1755 const size = window.qBittorrent.Misc.friendlyUnit(this.getRowValue(row), false);
1756 td.textContent = size;
1759 const displayNum = function(td, row) {
1760 const value = this.getRowValue(row);
1761 const formattedValue = (value === "-1") ? "Unknown" : value;
1762 td.textContent = formattedValue;
1763 td.title = formattedValue;
1765 const displayDate = function(td, row) {
1766 const value = this.getRowValue(row) * 1000;
1767 const formattedValue = (isNaN(value) || (value <= 0)) ? "" : (new Date(value).toLocaleString());
1768 td.textContent = formattedValue;
1769 td.title = formattedValue;
1772 this.columns["fileSize"].updateTd = displaySize;
1773 this.columns["nbSeeders"].updateTd = displayNum;
1774 this.columns["nbLeechers"].updateTd = displayNum;
1775 this.columns["pubDate"].updateTd = displayDate;
1778 getFilteredAndSortedRows: function() {
1779 const getSizeFilters = function() {
1780 let minSize = (window.qBittorrent.Search.searchSizeFilter.min > 0.00) ? (window.qBittorrent.Search.searchSizeFilter.min * Math.pow(1024, window.qBittorrent.Search.searchSizeFilter.minUnit)) : 0.00;
1781 let maxSize = (window.qBittorrent.Search.searchSizeFilter.max > 0.00) ? (window.qBittorrent.Search.searchSizeFilter.max * Math.pow(1024, window.qBittorrent.Search.searchSizeFilter.maxUnit)) : 0.00;
1783 if ((minSize > maxSize) && (maxSize > 0.00)) {
1784 const tmp = minSize;
1795 const getSeedsFilters = function() {
1796 let minSeeds = (window.qBittorrent.Search.searchSeedsFilter.min > 0) ? window.qBittorrent.Search.searchSeedsFilter.min : 0;
1797 let maxSeeds = (window.qBittorrent.Search.searchSeedsFilter.max > 0) ? window.qBittorrent.Search.searchSeedsFilter.max : 0;
1799 if ((minSeeds > maxSeeds) && (maxSeeds > 0)) {
1800 const tmp = minSeeds;
1801 minSeeds = maxSeeds;
1811 let filteredRows = [];
1812 const searchTerms = window.qBittorrent.Search.searchText.pattern.toLowerCase().split(" ");
1813 const filterTerms = window.qBittorrent.Search.searchText.filterPattern.toLowerCase().split(" ");
1814 const sizeFilters = getSizeFilters();
1815 const seedsFilters = getSeedsFilters();
1816 const searchInTorrentName = $("searchInTorrentName").value === "names";
1818 if (searchInTorrentName || (filterTerms.length > 0) || (window.qBittorrent.Search.searchSizeFilter.min > 0.00) || (window.qBittorrent.Search.searchSizeFilter.max > 0.00)) {
1819 for (const row of this.getRowValues()) {
1821 if (searchInTorrentName && !window.qBittorrent.Misc.containsAllTerms(row.full_data.fileName, searchTerms))
1823 if ((filterTerms.length > 0) && !window.qBittorrent.Misc.containsAllTerms(row.full_data.fileName, filterTerms))
1825 if ((sizeFilters.min > 0.00) && (row.full_data.fileSize < sizeFilters.min))
1827 if ((sizeFilters.max > 0.00) && (row.full_data.fileSize > sizeFilters.max))
1829 if ((seedsFilters.min > 0) && (row.full_data.nbSeeders < seedsFilters.min))
1831 if ((seedsFilters.max > 0) && (row.full_data.nbSeeders > seedsFilters.max))
1834 filteredRows.push(row);
1838 filteredRows = [...this.getRowValues()];
1841 filteredRows.sort((row1, row2) => {
1842 const column = this.columns[this.sortedColumn];
1843 const res = column.compareRows(row1, row2);
1844 if (this.reverseSort === "0")
1850 return filteredRows;
1853 setupTr: function(tr) {
1854 tr.addClass("searchTableRow");
1858 const SearchPluginsTable = new Class({
1859 Extends: DynamicTable,
1861 initColumns: function() {
1862 this.newColumn("fullName", "", "QBT_TR(Name)QBT_TR[CONTEXT=SearchPluginsTable]", 175, true);
1863 this.newColumn("version", "", "QBT_TR(Version)QBT_TR[CONTEXT=SearchPluginsTable]", 100, true);
1864 this.newColumn("url", "", "QBT_TR(Url)QBT_TR[CONTEXT=SearchPluginsTable]", 175, true);
1865 this.newColumn("enabled", "", "QBT_TR(Enabled)QBT_TR[CONTEXT=SearchPluginsTable]", 100, true);
1867 this.initColumnsFunctions();
1870 initColumnsFunctions: function() {
1871 this.columns["enabled"].updateTd = function(td, row) {
1872 const value = this.getRowValue(row);
1874 td.textContent = "QBT_TR(Yes)QBT_TR[CONTEXT=SearchPluginsTable]";
1875 td.title = "QBT_TR(Yes)QBT_TR[CONTEXT=SearchPluginsTable]";
1876 td.getParent("tr").addClass("green");
1877 td.getParent("tr").removeClass("red");
1880 td.textContent = "QBT_TR(No)QBT_TR[CONTEXT=SearchPluginsTable]";
1881 td.title = "QBT_TR(No)QBT_TR[CONTEXT=SearchPluginsTable]";
1882 td.getParent("tr").addClass("red");
1883 td.getParent("tr").removeClass("green");
1888 setupTr: function(tr) {
1889 tr.addClass("searchPluginsTableRow");
1893 const TorrentTrackersTable = new Class({
1894 Extends: DynamicTable,
1896 initColumns: function() {
1897 this.newColumn("tier", "", "QBT_TR(Tier)QBT_TR[CONTEXT=TrackerListWidget]", 35, true);
1898 this.newColumn("url", "", "QBT_TR(URL)QBT_TR[CONTEXT=TrackerListWidget]", 250, true);
1899 this.newColumn("status", "", "QBT_TR(Status)QBT_TR[CONTEXT=TrackerListWidget]", 125, true);
1900 this.newColumn("peers", "", "QBT_TR(Peers)QBT_TR[CONTEXT=TrackerListWidget]", 75, true);
1901 this.newColumn("seeds", "", "QBT_TR(Seeds)QBT_TR[CONTEXT=TrackerListWidget]", 75, true);
1902 this.newColumn("leeches", "", "QBT_TR(Leeches)QBT_TR[CONTEXT=TrackerListWidget]", 75, true);
1903 this.newColumn("downloaded", "", "QBT_TR(Times Downloaded)QBT_TR[CONTEXT=TrackerListWidget]", 100, true);
1904 this.newColumn("message", "", "QBT_TR(Message)QBT_TR[CONTEXT=TrackerListWidget]", 250, true);
1908 const BulkRenameTorrentFilesTable = new Class({
1909 Extends: DynamicTable,
1912 prevFilterTerms: [],
1913 prevRowsString: null,
1914 prevFilteredRows: [],
1915 prevSortedColumn: null,
1916 prevReverseSort: null,
1917 fileTree: new window.qBittorrent.FileTree.FileTree(),
1919 populateTable: function(root) {
1920 this.fileTree.setRoot(root);
1921 root.children.each((node) => {
1922 this._addNodeToTable(node, 0);
1926 _addNodeToTable: function(node, depth) {
1929 if (node.isFolder) {
1933 checked: node.checked,
1935 original: node.original,
1936 renamed: node.renamed
1940 node.full_data = data;
1941 this.updateRowData(data);
1944 node.data.rowId = node.rowId;
1945 node.full_data = node.data;
1946 this.updateRowData(node.data);
1949 node.children.each((child) => {
1950 this._addNodeToTable(child, depth + 1);
1954 getRoot: function() {
1955 return this.fileTree.getRoot();
1958 getNode: function(rowId) {
1959 return this.fileTree.getNode(rowId);
1962 getRow: function(node) {
1963 const rowId = this.fileTree.getRowId(node).toString();
1964 return this.rows.get(rowId);
1967 getSelectedRows: function() {
1968 const nodes = this.fileTree.toArray();
1970 return nodes.filter(x => x.checked === 0);
1973 initColumns: function() {
1974 // Blocks saving header width (because window width isn't saved)
1975 LocalPreferences.remove("column_" + "checked" + "_width_" + this.dynamicTableDivId);
1976 LocalPreferences.remove("column_" + "original" + "_width_" + this.dynamicTableDivId);
1977 LocalPreferences.remove("column_" + "renamed" + "_width_" + this.dynamicTableDivId);
1978 this.newColumn("checked", "", "", 50, true);
1979 this.newColumn("original", "", "QBT_TR(Original)QBT_TR[CONTEXT=TrackerListWidget]", 270, true);
1980 this.newColumn("renamed", "", "QBT_TR(Renamed)QBT_TR[CONTEXT=TrackerListWidget]", 220, true);
1982 this.initColumnsFunctions();
1986 * Toggles the global checkbox and all checkboxes underneath
1988 toggleGlobalCheckbox: function() {
1989 const checkbox = $("rootMultiRename_cb");
1990 const checkboxes = $$("input.RenamingCB");
1992 for (let i = 0; i < checkboxes.length; ++i) {
1993 const node = this.getNode(i);
1995 if (checkbox.checked || checkbox.indeterminate) {
1996 const cb = checkboxes[i];
1998 cb.indeterminate = false;
1999 cb.state = "checked";
2001 node.full_data.checked = node.checked;
2004 const cb = checkboxes[i];
2006 cb.indeterminate = false;
2007 cb.state = "unchecked";
2009 node.full_data.checked = node.checked;
2013 this.updateGlobalCheckbox();
2016 toggleNodeTreeCheckbox: function(rowId, checkState) {
2017 const node = this.getNode(rowId);
2018 node.checked = checkState;
2019 node.full_data.checked = checkState;
2020 const checkbox = $(`cbRename${rowId}`);
2021 checkbox.checked = node.checked === 0;
2022 checkbox.state = checkbox.checked ? "checked" : "unchecked";
2024 for (let i = 0; i < node.children.length; ++i)
2025 this.toggleNodeTreeCheckbox(node.children[i].rowId, checkState);
2028 updateGlobalCheckbox: function() {
2029 const checkbox = $("rootMultiRename_cb");
2030 const checkboxes = $$("input.RenamingCB");
2031 const isAllChecked = function() {
2032 for (let i = 0; i < checkboxes.length; ++i) {
2033 if (!checkboxes[i].checked)
2038 const isAllUnchecked = function() {
2039 for (let i = 0; i < checkboxes.length; ++i) {
2040 if (checkboxes[i].checked)
2045 if (isAllChecked()) {
2046 checkbox.state = "checked";
2047 checkbox.indeterminate = false;
2048 checkbox.checked = true;
2050 else if (isAllUnchecked()) {
2051 checkbox.state = "unchecked";
2052 checkbox.indeterminate = false;
2053 checkbox.checked = false;
2056 checkbox.state = "partial";
2057 checkbox.indeterminate = true;
2058 checkbox.checked = false;
2062 initColumnsFunctions: function() {
2066 this.columns["checked"].updateTd = function(td, row) {
2067 const id = row.rowId;
2068 const value = this.getRowValue(row);
2070 const treeImg = new Element("img", {
2071 src: "images/L.gif",
2076 const checkbox = new Element("input");
2077 checkbox.type = "checkbox";
2078 checkbox.id = "cbRename" + id;
2079 checkbox.setAttribute("data-id", id);
2080 checkbox.className = "RenamingCB";
2081 checkbox.addEventListener("click", (e) => {
2082 const node = that.getNode(id);
2083 node.checked = e.target.checked ? 0 : 1;
2084 node.full_data.checked = node.checked;
2085 that.updateGlobalCheckbox();
2086 that.onRowSelectionChange(node);
2087 e.stopPropagation();
2089 checkbox.checked = (value === 0);
2090 checkbox.state = checkbox.checked ? "checked" : "unchecked";
2091 checkbox.indeterminate = false;
2092 td.adopt(treeImg, checkbox);
2096 this.columns["original"].updateTd = function(td, row) {
2097 const id = row.rowId;
2098 const fileNameId = "filesTablefileName" + id;
2099 const node = that.getNode(id);
2101 if (node.isFolder) {
2102 const value = this.getRowValue(row);
2103 const dirImgId = "renameTableDirImg" + id;
2105 // just update file name
2106 $(fileNameId).textContent = value;
2109 const span = new Element("span", {
2113 const dirImg = new Element("img", {
2114 src: "images/directory.svg",
2118 "margin-bottom": -3,
2119 "margin-left": (node.depth * 20)
2123 td.replaceChildren(dirImg, span);
2127 const value = this.getRowValue(row);
2128 const span = new Element("span", {
2132 "margin-left": ((node.depth + 1) * 20)
2135 td.replaceChildren(span);
2140 this.columns["renamed"].updateTd = function(td, row) {
2141 const id = row.rowId;
2142 const fileNameRenamedId = "filesTablefileRenamed" + id;
2143 const value = this.getRowValue(row);
2145 const span = new Element("span", {
2147 id: fileNameRenamedId,
2149 td.replaceChildren(span);
2153 onRowSelectionChange: function(row) {},
2155 selectRow: function() {
2159 reselectRows: function(rowIds) {
2162 this.tableBody.getElements("tr").each((tr) => {
2163 if (rowIds.includes(tr.rowId)) {
2164 const node = that.getNode(tr.rowId);
2166 node.full_data.checked = 0;
2168 const checkbox = tr.children[0].getElement("input");
2169 checkbox.state = "checked";
2170 checkbox.indeterminate = false;
2171 checkbox.checked = true;
2175 this.updateGlobalCheckbox();
2178 _sortNodesByColumn: function(nodes, column) {
2179 nodes.sort((row1, row2) => {
2180 // list folders before files when sorting by name
2181 if (column.name === "original") {
2182 const node1 = this.getNode(row1.data.rowId);
2183 const node2 = this.getNode(row2.data.rowId);
2184 if (node1.isFolder && !node2.isFolder)
2186 if (node2.isFolder && !node1.isFolder)
2190 const res = column.compareRows(row1, row2);
2191 return (this.reverseSort === "0") ? res : -res;
2194 nodes.each((node) => {
2195 if (node.children.length > 0)
2196 this._sortNodesByColumn(node.children, column);
2200 _filterNodes: function(node, filterTerms, filteredRows) {
2201 if (node.isFolder) {
2202 const childAdded = node.children.reduce((acc, child) => {
2203 // we must execute the function before ORing w/ acc or we'll stop checking child nodes after the first successful match
2204 return (this._filterNodes(child, filterTerms, filteredRows) || acc);
2208 const row = this.getRow(node);
2209 filteredRows.push(row);
2214 if (window.qBittorrent.Misc.containsAllTerms(node.original, filterTerms)) {
2215 const row = this.getRow(node);
2216 filteredRows.push(row);
2223 setFilter: function(text) {
2224 const filterTerms = text.trim().toLowerCase().split(" ");
2225 if ((filterTerms.length === 1) && (filterTerms[0] === ""))
2226 this.filterTerms = [];
2228 this.filterTerms = filterTerms;
2231 getFilteredAndSortedRows: function() {
2232 if (this.getRoot() === null)
2235 const generateRowsSignature = () => {
2236 const rowsData = [];
2237 for (const { full_data } of this.getRowValues())
2238 rowsData.push(full_data);
2239 return JSON.stringify(rowsData);
2242 const getFilteredRows = function() {
2243 if (this.filterTerms.length === 0) {
2244 const nodeArray = this.fileTree.toArray();
2245 const filteredRows = nodeArray.map((node) => {
2246 return this.getRow(node);
2248 return filteredRows;
2251 const filteredRows = [];
2252 this.getRoot().children.each((child) => {
2253 this._filterNodes(child, this.filterTerms, filteredRows);
2255 filteredRows.reverse();
2256 return filteredRows;
2259 const hasRowsChanged = function(rowsString, prevRowsStringString) {
2260 const rowsChanged = (rowsString !== prevRowsStringString);
2261 const isFilterTermsChanged = this.filterTerms.reduce((acc, term, index) => {
2262 return (acc || (term !== this.prevFilterTerms[index]));
2264 const isFilterChanged = ((this.filterTerms.length !== this.prevFilterTerms.length)
2265 || ((this.filterTerms.length > 0) && isFilterTermsChanged));
2266 const isSortedColumnChanged = (this.prevSortedColumn !== this.sortedColumn);
2267 const isReverseSortChanged = (this.prevReverseSort !== this.reverseSort);
2269 return (rowsChanged || isFilterChanged || isSortedColumnChanged || isReverseSortChanged);
2272 const rowsString = generateRowsSignature();
2273 if (!hasRowsChanged(rowsString, this.prevRowsString))
2274 return this.prevFilteredRows;
2276 // sort, then filter
2277 const column = this.columns[this.sortedColumn];
2278 this._sortNodesByColumn(this.getRoot().children, column);
2279 const filteredRows = getFilteredRows();
2281 this.prevFilterTerms = this.filterTerms;
2282 this.prevRowsString = rowsString;
2283 this.prevFilteredRows = filteredRows;
2284 this.prevSortedColumn = this.sortedColumn;
2285 this.prevReverseSort = this.reverseSort;
2286 return filteredRows;
2289 setIgnored: function(rowId, ignore) {
2290 const row = this.rows.get(rowId);
2292 row.full_data.remaining = 0;
2294 row.full_data.remaining = (row.full_data.size * (1.0 - (row.full_data.progress / 100)));
2297 setupTr: function(tr) {
2298 tr.addEventListener("keydown", function(event) {
2299 switch (event.key) {
2301 qBittorrent.PropFiles.collapseFolder(this._this.getSelectedRowId());
2304 qBittorrent.PropFiles.expandFolder(this._this.getSelectedRowId());
2311 const TorrentFilesTable = new Class({
2312 Extends: DynamicTable,
2315 prevFilterTerms: [],
2316 prevRowsString: null,
2317 prevFilteredRows: [],
2318 prevSortedColumn: null,
2319 prevReverseSort: null,
2320 fileTree: new window.qBittorrent.FileTree.FileTree(),
2322 populateTable: function(root) {
2323 this.fileTree.setRoot(root);
2324 root.children.each((node) => {
2325 this._addNodeToTable(node, 0);
2329 _addNodeToTable: function(node, depth) {
2332 if (node.isFolder) {
2336 checked: node.checked,
2337 remaining: node.remaining,
2338 progress: node.progress,
2339 priority: window.qBittorrent.PropFiles.normalizePriority(node.priority),
2340 availability: node.availability,
2346 node.full_data = data;
2347 this.updateRowData(data);
2350 node.data.rowId = node.rowId;
2351 node.full_data = node.data;
2352 this.updateRowData(node.data);
2355 node.children.each((child) => {
2356 this._addNodeToTable(child, depth + 1);
2360 getRoot: function() {
2361 return this.fileTree.getRoot();
2364 getNode: function(rowId) {
2365 return this.fileTree.getNode(rowId);
2368 getRow: function(node) {
2369 const rowId = this.fileTree.getRowId(node).toString();
2370 return this.rows.get(rowId);
2373 initColumns: function() {
2374 this.newColumn("checked", "", "", 50, true);
2375 this.newColumn("name", "", "QBT_TR(Name)QBT_TR[CONTEXT=TrackerListWidget]", 300, true);
2376 this.newColumn("size", "", "QBT_TR(Total Size)QBT_TR[CONTEXT=TrackerListWidget]", 75, true);
2377 this.newColumn("progress", "", "QBT_TR(Progress)QBT_TR[CONTEXT=TrackerListWidget]", 100, true);
2378 this.newColumn("priority", "", "QBT_TR(Download Priority)QBT_TR[CONTEXT=TrackerListWidget]", 150, true);
2379 this.newColumn("remaining", "", "QBT_TR(Remaining)QBT_TR[CONTEXT=TrackerListWidget]", 75, true);
2380 this.newColumn("availability", "", "QBT_TR(Availability)QBT_TR[CONTEXT=TrackerListWidget]", 75, true);
2382 this.initColumnsFunctions();
2385 initColumnsFunctions: function() {
2387 const displaySize = function(td, row) {
2388 const size = window.qBittorrent.Misc.friendlyUnit(this.getRowValue(row), false);
2389 td.textContent = size;
2392 const displayPercentage = function(td, row) {
2393 const value = window.qBittorrent.Misc.friendlyPercentage(this.getRowValue(row));
2394 td.textContent = value;
2399 this.columns["checked"].updateTd = function(td, row) {
2400 const id = row.rowId;
2401 const value = this.getRowValue(row);
2403 if (window.qBittorrent.PropFiles.isDownloadCheckboxExists(id)) {
2404 window.qBittorrent.PropFiles.updateDownloadCheckbox(id, value);
2407 const treeImg = new Element("img", {
2408 src: "images/L.gif",
2413 td.adopt(treeImg, window.qBittorrent.PropFiles.createDownloadCheckbox(id, row.full_data.fileId, value));
2418 this.columns["name"].updateTd = function(td, row) {
2419 const id = row.rowId;
2420 const fileNameId = "filesTablefileName" + id;
2421 const node = that.getNode(id);
2423 if (node.isFolder) {
2424 const value = this.getRowValue(row);
2425 const collapseIconId = "filesTableCollapseIcon" + id;
2426 const dirImgId = "filesTableDirImg" + id;
2428 // just update file name
2429 $(fileNameId).textContent = value;
2432 const collapseIcon = new Element("img", {
2433 src: "images/go-down.svg",
2435 "margin-left": (node.depth * 20)
2437 class: "filesTableCollapseIcon",
2440 onclick: "qBittorrent.PropFiles.collapseIconClicked(this)"
2442 const span = new Element("span", {
2446 const dirImg = new Element("img", {
2447 src: "images/directory.svg",
2455 td.replaceChildren(collapseIcon, dirImg, span);
2459 const value = this.getRowValue(row);
2460 const span = new Element("span", {
2464 "margin-left": ((node.depth + 1) * 20)
2467 td.replaceChildren(span);
2472 this.columns["size"].updateTd = displaySize;
2475 this.columns["progress"].updateTd = function(td, row) {
2476 const id = row.rowId;
2477 const value = this.getRowValue(row);
2479 const progressBar = $("pbf_" + id);
2480 if (progressBar === null) {
2481 td.adopt(new window.qBittorrent.ProgressBar.ProgressBar(value.toFloat(), {
2487 progressBar.setValue(value.toFloat());
2492 this.columns["priority"].updateTd = function(td, row) {
2493 const id = row.rowId;
2494 const value = this.getRowValue(row);
2496 if (window.qBittorrent.PropFiles.isPriorityComboExists(id))
2497 window.qBittorrent.PropFiles.updatePriorityCombo(id, value);
2499 td.adopt(window.qBittorrent.PropFiles.createPriorityCombo(id, row.full_data.fileId, value));
2502 // remaining, availability
2503 this.columns["remaining"].updateTd = displaySize;
2504 this.columns["availability"].updateTd = displayPercentage;
2507 _sortNodesByColumn: function(nodes, column) {
2508 nodes.sort((row1, row2) => {
2509 // list folders before files when sorting by name
2510 if (column.name === "name") {
2511 const node1 = this.getNode(row1.data.rowId);
2512 const node2 = this.getNode(row2.data.rowId);
2513 if (node1.isFolder && !node2.isFolder)
2515 if (node2.isFolder && !node1.isFolder)
2519 const res = column.compareRows(row1, row2);
2520 return (this.reverseSort === "0") ? res : -res;
2523 nodes.each((node) => {
2524 if (node.children.length > 0)
2525 this._sortNodesByColumn(node.children, column);
2529 _filterNodes: function(node, filterTerms, filteredRows) {
2530 if (node.isFolder) {
2531 const childAdded = node.children.reduce((acc, child) => {
2532 // we must execute the function before ORing w/ acc or we'll stop checking child nodes after the first successful match
2533 return (this._filterNodes(child, filterTerms, filteredRows) || acc);
2537 const row = this.getRow(node);
2538 filteredRows.push(row);
2543 if (window.qBittorrent.Misc.containsAllTerms(node.name, filterTerms)) {
2544 const row = this.getRow(node);
2545 filteredRows.push(row);
2552 setFilter: function(text) {
2553 const filterTerms = text.trim().toLowerCase().split(" ");
2554 if ((filterTerms.length === 1) && (filterTerms[0] === ""))
2555 this.filterTerms = [];
2557 this.filterTerms = filterTerms;
2560 getFilteredAndSortedRows: function() {
2561 if (this.getRoot() === null)
2564 const generateRowsSignature = () => {
2565 const rowsData = [];
2566 for (const { full_data } of this.getRowValues())
2567 rowsData.push(full_data);
2568 return JSON.stringify(rowsData);
2571 const getFilteredRows = function() {
2572 if (this.filterTerms.length === 0) {
2573 const nodeArray = this.fileTree.toArray();
2574 const filteredRows = nodeArray.map((node) => {
2575 return this.getRow(node);
2577 return filteredRows;
2580 const filteredRows = [];
2581 this.getRoot().children.each((child) => {
2582 this._filterNodes(child, this.filterTerms, filteredRows);
2584 filteredRows.reverse();
2585 return filteredRows;
2588 const hasRowsChanged = function(rowsString, prevRowsStringString) {
2589 const rowsChanged = (rowsString !== prevRowsStringString);
2590 const isFilterTermsChanged = this.filterTerms.reduce((acc, term, index) => {
2591 return (acc || (term !== this.prevFilterTerms[index]));
2593 const isFilterChanged = ((this.filterTerms.length !== this.prevFilterTerms.length)
2594 || ((this.filterTerms.length > 0) && isFilterTermsChanged));
2595 const isSortedColumnChanged = (this.prevSortedColumn !== this.sortedColumn);
2596 const isReverseSortChanged = (this.prevReverseSort !== this.reverseSort);
2598 return (rowsChanged || isFilterChanged || isSortedColumnChanged || isReverseSortChanged);
2601 const rowsString = generateRowsSignature();
2602 if (!hasRowsChanged(rowsString, this.prevRowsString))
2603 return this.prevFilteredRows;
2605 // sort, then filter
2606 const column = this.columns[this.sortedColumn];
2607 this._sortNodesByColumn(this.getRoot().children, column);
2608 const filteredRows = getFilteredRows();
2610 this.prevFilterTerms = this.filterTerms;
2611 this.prevRowsString = rowsString;
2612 this.prevFilteredRows = filteredRows;
2613 this.prevSortedColumn = this.sortedColumn;
2614 this.prevReverseSort = this.reverseSort;
2615 return filteredRows;
2618 setIgnored: function(rowId, ignore) {
2619 const row = this.rows.get(rowId.toString());
2621 row.full_data.remaining = 0;
2623 row.full_data.remaining = (row.full_data.size * (1.0 - (row.full_data.progress / 100)));
2626 setupTr: function(tr) {
2627 tr.addEventListener("keydown", function(event) {
2628 switch (event.key) {
2630 qBittorrent.PropFiles.collapseFolder(this._this.getSelectedRowId());
2633 qBittorrent.PropFiles.expandFolder(this._this.getSelectedRowId());
2640 const RssFeedTable = new Class({
2641 Extends: DynamicTable,
2642 initColumns: function() {
2643 this.newColumn("state_icon", "", "", 30, true);
2644 this.newColumn("name", "", "QBT_TR(RSS feeds)QBT_TR[CONTEXT=FeedListWidget]", -1, true);
2646 this.columns["state_icon"].dataProperties[0] = "";
2648 // map name row to "[name] ([unread])"
2649 this.columns["name"].dataProperties.push("unread");
2650 this.columns["name"].updateTd = function(td, row) {
2651 const name = this.getRowValue(row, 0);
2652 const unreadCount = this.getRowValue(row, 1);
2653 const value = name + " (" + unreadCount + ")";
2654 td.textContent = value;
2658 setupHeaderMenu: function() {},
2659 setupHeaderEvents: function() {},
2660 getFilteredAndSortedRows: function() {
2661 return [...this.getRowValues()];
2663 selectRow: function(rowId) {
2664 this.selectedRows.push(rowId);
2666 this.onSelectedRowChanged();
2669 for (const row of this.getRowValues()) {
2670 if (row.rowId === rowId) {
2671 path = row.full_data.dataPath;
2675 window.qBittorrent.Rss.showRssFeed(path);
2677 setupTr: function(tr) {
2678 tr.addEventListener("dblclick", function(e) {
2679 if (this.rowId !== 0) {
2680 window.qBittorrent.Rss.moveItem(this._this.rows.get(this.rowId).full_data.dataPath);
2685 updateRow: function(tr, fullUpdate) {
2686 const row = this.rows.get(tr.rowId);
2687 const data = row[fullUpdate ? "full_data" : "data"];
2689 const tds = tr.getElements("td");
2690 for (let i = 0; i < this.columns.length; ++i) {
2691 if (Object.hasOwn(data, this.columns[i].dataProperties[0]))
2692 this.columns[i].updateTd(tds[i], row);
2695 tds[0].style.overflow = "visible";
2696 const indentation = row.full_data.indentation;
2697 tds[0].style.paddingLeft = (indentation * 32 + 4) + "px";
2698 tds[1].style.paddingLeft = (indentation * 32 + 4) + "px";
2700 updateIcons: function() {
2702 for (const row of this.getRowValues()) {
2704 switch (row.full_data.status) {
2706 img_path = "images/application-rss.svg";
2709 img_path = "images/task-reject.svg";
2712 img_path = "images/spinner.gif";
2715 img_path = "images/mail-inbox.svg";
2718 img_path = "images/folder-documents.svg";
2722 for (let i = 0; i < this.tableBody.rows.length; ++i) {
2723 if (this.tableBody.rows[i].rowId === row.rowId) {
2724 td = this.tableBody.rows[i].children[0];
2728 if (td.getChildren("img").length > 0) {
2729 const img = td.getChildren("img")[0];
2730 if (!img.src.includes(img_path)) {
2736 td.adopt(new Element("img", {
2738 "class": "stateIcon",
2745 newColumn: function(name, style, caption, defaultWidth, defaultVisible) {
2747 column["name"] = name;
2748 column["title"] = name;
2749 column["visible"] = defaultVisible;
2750 column["force_hide"] = false;
2751 column["caption"] = caption;
2752 column["style"] = style;
2753 if (defaultWidth !== -1)
2754 column["width"] = defaultWidth;
2756 column["dataProperties"] = [name];
2757 column["getRowValue"] = function(row, pos) {
2758 if (pos === undefined)
2760 return row["full_data"][this.dataProperties[pos]];
2762 column["compareRows"] = function(row1, row2) {
2763 const value1 = this.getRowValue(row1);
2764 const value2 = this.getRowValue(row2);
2765 if ((typeof(value1) === "number") && (typeof(value2) === "number"))
2766 return compareNumbers(value1, value2);
2767 return window.qBittorrent.Misc.naturalSortCollator.compare(value1, value2);
2769 column["updateTd"] = function(td, row) {
2770 const value = this.getRowValue(row);
2771 td.textContent = value;
2774 column["onResize"] = null;
2775 this.columns.push(column);
2776 this.columns[name] = column;
2778 this.hiddenTableHeader.appendChild(new Element("th"));
2779 this.fixedTableHeader.appendChild(new Element("th"));
2783 const RssArticleTable = new Class({
2784 Extends: DynamicTable,
2785 initColumns: function() {
2786 this.newColumn("name", "", "QBT_TR(Torrents: (double-click to download))QBT_TR[CONTEXT=RSSWidget]", -1, true);
2788 setupHeaderMenu: function() {},
2789 setupHeaderEvents: function() {},
2790 getFilteredAndSortedRows: function() {
2791 return [...this.getRowValues()];
2793 selectRow: function(rowId) {
2794 this.selectedRows.push(rowId);
2796 this.onSelectedRowChanged();
2800 for (const row of this.getRowValues()) {
2801 if (row.rowId === rowId) {
2802 articleId = row.full_data.dataId;
2803 feedUid = row.full_data.feedUid;
2804 this.tableBody.rows[row.rowId].removeClass("unreadArticle");
2808 window.qBittorrent.Rss.showDetails(feedUid, articleId);
2810 setupTr: function(tr) {
2811 tr.addEventListener("dblclick", function(e) {
2812 showDownloadPage([this._this.rows.get(this.rowId).full_data.torrentURL]);
2815 tr.addClass("torrentsTableContextMenuTarget");
2817 updateRow: function(tr, fullUpdate) {
2818 const row = this.rows.get(tr.rowId);
2819 const data = row[fullUpdate ? "full_data" : "data"];
2820 if (!row.full_data.isRead)
2821 tr.addClass("unreadArticle");
2823 tr.removeClass("unreadArticle");
2825 const tds = tr.getElements("td");
2826 for (let i = 0; i < this.columns.length; ++i) {
2827 if (Object.hasOwn(data, this.columns[i].dataProperties[0]))
2828 this.columns[i].updateTd(tds[i], row);
2832 newColumn: function(name, style, caption, defaultWidth, defaultVisible) {
2834 column["name"] = name;
2835 column["title"] = name;
2836 column["visible"] = defaultVisible;
2837 column["force_hide"] = false;
2838 column["caption"] = caption;
2839 column["style"] = style;
2840 if (defaultWidth !== -1)
2841 column["width"] = defaultWidth;
2843 column["dataProperties"] = [name];
2844 column["getRowValue"] = function(row, pos) {
2845 if (pos === undefined)
2847 return row["full_data"][this.dataProperties[pos]];
2849 column["compareRows"] = function(row1, row2) {
2850 const value1 = this.getRowValue(row1);
2851 const value2 = this.getRowValue(row2);
2852 if ((typeof(value1) === "number") && (typeof(value2) === "number"))
2853 return compareNumbers(value1, value2);
2854 return window.qBittorrent.Misc.naturalSortCollator.compare(value1, value2);
2856 column["updateTd"] = function(td, row) {
2857 const value = this.getRowValue(row);
2858 td.textContent = value;
2861 column["onResize"] = null;
2862 this.columns.push(column);
2863 this.columns[name] = column;
2865 this.hiddenTableHeader.appendChild(new Element("th"));
2866 this.fixedTableHeader.appendChild(new Element("th"));
2870 const RssDownloaderRulesTable = new Class({
2871 Extends: DynamicTable,
2872 initColumns: function() {
2873 this.newColumn("checked", "", "", 30, true);
2874 this.newColumn("name", "", "", -1, true);
2876 this.columns["checked"].updateTd = function(td, row) {
2877 if ($("cbRssDlRule" + row.rowId) === null) {
2878 const checkbox = new Element("input");
2879 checkbox.type = "checkbox";
2880 checkbox.id = "cbRssDlRule" + row.rowId;
2881 checkbox.checked = row.full_data.checked;
2883 checkbox.addEventListener("click", function(e) {
2884 window.qBittorrent.RssDownloader.rssDownloaderRulesTable.updateRowData({
2886 checked: this.checked
2888 window.qBittorrent.RssDownloader.modifyRuleState(row.full_data.name, "enabled", this.checked);
2889 e.stopPropagation();
2892 td.append(checkbox);
2895 $("cbRssDlRule" + row.rowId).checked = row.full_data.checked;
2899 setupHeaderMenu: function() {},
2900 setupHeaderEvents: function() {},
2901 getFilteredAndSortedRows: function() {
2902 return [...this.getRowValues()];
2904 setupTr: function(tr) {
2905 tr.addEventListener("dblclick", function(e) {
2906 window.qBittorrent.RssDownloader.renameRule(this._this.rows.get(this.rowId).full_data.name);
2910 newColumn: function(name, style, caption, defaultWidth, defaultVisible) {
2912 column["name"] = name;
2913 column["title"] = name;
2914 column["visible"] = defaultVisible;
2915 column["force_hide"] = false;
2916 column["caption"] = caption;
2917 column["style"] = style;
2918 if (defaultWidth !== -1)
2919 column["width"] = defaultWidth;
2921 column["dataProperties"] = [name];
2922 column["getRowValue"] = function(row, pos) {
2923 if (pos === undefined)
2925 return row["full_data"][this.dataProperties[pos]];
2927 column["compareRows"] = function(row1, row2) {
2928 const value1 = this.getRowValue(row1);
2929 const value2 = this.getRowValue(row2);
2930 if ((typeof(value1) === "number") && (typeof(value2) === "number"))
2931 return compareNumbers(value1, value2);
2932 return window.qBittorrent.Misc.naturalSortCollator.compare(value1, value2);
2934 column["updateTd"] = function(td, row) {
2935 const value = this.getRowValue(row);
2936 td.textContent = value;
2939 column["onResize"] = null;
2940 this.columns.push(column);
2941 this.columns[name] = column;
2943 this.hiddenTableHeader.appendChild(new Element("th"));
2944 this.fixedTableHeader.appendChild(new Element("th"));
2946 selectRow: function(rowId) {
2947 this.selectedRows.push(rowId);
2949 this.onSelectedRowChanged();
2952 for (const row of this.getRowValues()) {
2953 if (row.rowId === rowId) {
2954 name = row.full_data.name;
2958 window.qBittorrent.RssDownloader.showRule(name);
2962 const RssDownloaderFeedSelectionTable = new Class({
2963 Extends: DynamicTable,
2964 initColumns: function() {
2965 this.newColumn("checked", "", "", 30, true);
2966 this.newColumn("name", "", "", -1, true);
2968 this.columns["checked"].updateTd = function(td, row) {
2969 if ($("cbRssDlFeed" + row.rowId) === null) {
2970 const checkbox = new Element("input");
2971 checkbox.type = "checkbox";
2972 checkbox.id = "cbRssDlFeed" + row.rowId;
2973 checkbox.checked = row.full_data.checked;
2975 checkbox.addEventListener("click", function(e) {
2976 window.qBittorrent.RssDownloader.rssDownloaderFeedSelectionTable.updateRowData({
2978 checked: this.checked
2980 e.stopPropagation();
2983 td.append(checkbox);
2986 $("cbRssDlFeed" + row.rowId).checked = row.full_data.checked;
2990 setupHeaderMenu: function() {},
2991 setupHeaderEvents: function() {},
2992 getFilteredAndSortedRows: function() {
2993 return [...this.getRowValues()];
2995 newColumn: function(name, style, caption, defaultWidth, defaultVisible) {
2997 column["name"] = name;
2998 column["title"] = name;
2999 column["visible"] = defaultVisible;
3000 column["force_hide"] = false;
3001 column["caption"] = caption;
3002 column["style"] = style;
3003 if (defaultWidth !== -1)
3004 column["width"] = defaultWidth;
3006 column["dataProperties"] = [name];
3007 column["getRowValue"] = function(row, pos) {
3008 if (pos === undefined)
3010 return row["full_data"][this.dataProperties[pos]];
3012 column["compareRows"] = function(row1, row2) {
3013 const value1 = this.getRowValue(row1);
3014 const value2 = this.getRowValue(row2);
3015 if ((typeof(value1) === "number") && (typeof(value2) === "number"))
3016 return compareNumbers(value1, value2);
3017 return window.qBittorrent.Misc.naturalSortCollator.compare(value1, value2);
3019 column["updateTd"] = function(td, row) {
3020 const value = this.getRowValue(row);
3021 td.textContent = value;
3024 column["onResize"] = null;
3025 this.columns.push(column);
3026 this.columns[name] = column;
3028 this.hiddenTableHeader.appendChild(new Element("th"));
3029 this.fixedTableHeader.appendChild(new Element("th"));
3031 selectRow: function() {}
3034 const RssDownloaderArticlesTable = new Class({
3035 Extends: DynamicTable,
3036 initColumns: function() {
3037 this.newColumn("name", "", "", -1, true);
3039 setupHeaderMenu: function() {},
3040 setupHeaderEvents: function() {},
3041 getFilteredAndSortedRows: function() {
3042 return [...this.getRowValues()];
3044 newColumn: function(name, style, caption, defaultWidth, defaultVisible) {
3046 column["name"] = name;
3047 column["title"] = name;
3048 column["visible"] = defaultVisible;
3049 column["force_hide"] = false;
3050 column["caption"] = caption;
3051 column["style"] = style;
3052 if (defaultWidth !== -1)
3053 column["width"] = defaultWidth;
3055 column["dataProperties"] = [name];
3056 column["getRowValue"] = function(row, pos) {
3057 if (pos === undefined)
3059 return row["full_data"][this.dataProperties[pos]];
3061 column["compareRows"] = function(row1, row2) {
3062 const value1 = this.getRowValue(row1);
3063 const value2 = this.getRowValue(row2);
3064 if ((typeof(value1) === "number") && (typeof(value2) === "number"))
3065 return compareNumbers(value1, value2);
3066 return window.qBittorrent.Misc.naturalSortCollator.compare(value1, value2);
3068 column["updateTd"] = function(td, row) {
3069 const value = this.getRowValue(row);
3070 td.textContent = value;
3073 column["onResize"] = null;
3074 this.columns.push(column);
3075 this.columns[name] = column;
3077 this.hiddenTableHeader.appendChild(new Element("th"));
3078 this.fixedTableHeader.appendChild(new Element("th"));
3080 selectRow: function() {},
3081 updateRow: function(tr, fullUpdate) {
3082 const row = this.rows.get(tr.rowId);
3083 const data = row[fullUpdate ? "full_data" : "data"];
3085 if (row.full_data.isFeed) {
3086 tr.addClass("articleTableFeed");
3087 tr.removeClass("articleTableArticle");
3090 tr.removeClass("articleTableFeed");
3091 tr.addClass("articleTableArticle");
3094 const tds = tr.getElements("td");
3095 for (let i = 0; i < this.columns.length; ++i) {
3096 if (Object.hasOwn(data, this.columns[i].dataProperties[0]))
3097 this.columns[i].updateTd(tds[i], row);
3103 const LogMessageTable = new Class({
3104 Extends: DynamicTable,
3108 filteredLength: function() {
3109 return this.tableBody.getElements("tr").length;
3112 initColumns: function() {
3113 this.newColumn("rowId", "", "QBT_TR(ID)QBT_TR[CONTEXT=ExecutionLogWidget]", 50, true);
3114 this.newColumn("message", "", "QBT_TR(Message)QBT_TR[CONTEXT=ExecutionLogWidget]", 350, true);
3115 this.newColumn("timestamp", "", "QBT_TR(Timestamp)QBT_TR[CONTEXT=ExecutionLogWidget]", 150, true);
3116 this.newColumn("type", "", "QBT_TR(Log Type)QBT_TR[CONTEXT=ExecutionLogWidget]", 100, true);
3117 this.initColumnsFunctions();
3120 initColumnsFunctions: function() {
3121 this.columns["timestamp"].updateTd = function(td, row) {
3122 const date = new Date(this.getRowValue(row) * 1000).toLocaleString();
3123 td.set({ "text": date, "title": date });
3126 this.columns["type"].updateTd = function(td, row) {
3127 // Type of the message: Log::NORMAL: 1, Log::INFO: 2, Log::WARNING: 4, Log::CRITICAL: 8
3128 let logLevel, addClass;
3129 switch (this.getRowValue(row).toInt()) {
3131 logLevel = "QBT_TR(Normal)QBT_TR[CONTEXT=ExecutionLogWidget]";
3132 addClass = "logNormal";
3135 logLevel = "QBT_TR(Info)QBT_TR[CONTEXT=ExecutionLogWidget]";
3136 addClass = "logInfo";
3139 logLevel = "QBT_TR(Warning)QBT_TR[CONTEXT=ExecutionLogWidget]";
3140 addClass = "logWarning";
3143 logLevel = "QBT_TR(Critical)QBT_TR[CONTEXT=ExecutionLogWidget]";
3144 addClass = "logCritical";
3147 logLevel = "QBT_TR(Unknown)QBT_TR[CONTEXT=ExecutionLogWidget]";
3148 addClass = "logUnknown";
3151 td.set({ "text": logLevel, "title": logLevel });
3152 td.getParent("tr").className = `logTableRow${addClass}`;
3156 getFilteredAndSortedRows: function() {
3157 let filteredRows = [];
3158 this.filterText = window.qBittorrent.Log.getFilterText();
3159 const filterTerms = (this.filterText.length > 0) ? this.filterText.toLowerCase().split(" ") : [];
3160 const logLevels = window.qBittorrent.Log.getSelectedLevels();
3161 if ((filterTerms.length > 0) || (logLevels.length < 4)) {
3162 for (const row of this.getRowValues()) {
3163 if (!logLevels.includes(row.full_data.type.toString()))
3166 if ((filterTerms.length > 0) && !window.qBittorrent.Misc.containsAllTerms(row.full_data.message, filterTerms))
3169 filteredRows.push(row);
3173 filteredRows = [...this.getRowValues()];
3176 filteredRows.sort((row1, row2) => {
3177 const column = this.columns[this.sortedColumn];
3178 const res = column.compareRows(row1, row2);
3179 return (this.reverseSort === "0") ? res : -res;
3182 return filteredRows;
3185 setupCommonEvents: function() {},
3187 setupTr: function(tr) {
3188 tr.addClass("logTableRow");
3192 const LogPeerTable = new Class({
3193 Extends: LogMessageTable,
3195 initColumns: function() {
3196 this.newColumn("rowId", "", "QBT_TR(ID)QBT_TR[CONTEXT=ExecutionLogWidget]", 50, true);
3197 this.newColumn("ip", "", "QBT_TR(IP)QBT_TR[CONTEXT=ExecutionLogWidget]", 150, true);
3198 this.newColumn("timestamp", "", "QBT_TR(Timestamp)QBT_TR[CONTEXT=ExecutionLogWidget]", 150, true);
3199 this.newColumn("blocked", "", "QBT_TR(Status)QBT_TR[CONTEXT=ExecutionLogWidget]", 150, true);
3200 this.newColumn("reason", "", "QBT_TR(Reason)QBT_TR[CONTEXT=ExecutionLogWidget]", 150, true);
3202 this.columns["timestamp"].updateTd = function(td, row) {
3203 const date = new Date(this.getRowValue(row) * 1000).toLocaleString();
3204 td.set({ "text": date, "title": date });
3207 this.columns["blocked"].updateTd = function(td, row) {
3208 let status, addClass;
3209 if (this.getRowValue(row)) {
3210 status = "QBT_TR(Blocked)QBT_TR[CONTEXT=ExecutionLogWidget]";
3211 addClass = "peerBlocked";
3214 status = "QBT_TR(Banned)QBT_TR[CONTEXT=ExecutionLogWidget]";
3215 addClass = "peerBanned";
3217 td.set({ "text": status, "title": status });
3218 td.getParent("tr").className = `logTableRow${addClass}`;
3222 getFilteredAndSortedRows: function() {
3223 let filteredRows = [];
3224 this.filterText = window.qBittorrent.Log.getFilterText();
3225 const filterTerms = (this.filterText.length > 0) ? this.filterText.toLowerCase().split(" ") : [];
3226 if (filterTerms.length > 0) {
3227 for (const row of this.getRowValues()) {
3228 if ((filterTerms.length > 0) && !window.qBittorrent.Misc.containsAllTerms(row.full_data.ip, filterTerms))
3231 filteredRows.push(row);
3235 filteredRows = [...this.getRowValues()];
3238 filteredRows.sort((row1, row2) => {
3239 const column = this.columns[this.sortedColumn];
3240 const res = column.compareRows(row1, row2);
3241 return (this.reverseSort === "0") ? res : -res;
3244 return filteredRows;
3248 const TorrentWebseedsTable = new Class({
3249 Extends: DynamicTable,
3251 initColumns: function() {
3252 this.newColumn("url", "", "QBT_TR(URL)QBT_TR[CONTEXT=HttpServer]", 500, true);
3258 Object.freeze(window.qBittorrent.DynamicTable);
3260 /*************************************************************/