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 this.resizeDebounceTimer = -1;
123 const resizeDebouncer = (entries) => {
124 clearTimeout(this.resizeDebounceTimer);
125 this.resizeDebounceTimer = setTimeout(() => {
127 this.resizeDebounceTimer = -1;
131 const resizeObserver = new ResizeObserver(resizeDebouncer);
132 resizeObserver.observe(parentPanel, { box: "border-box" });
136 setupHeaderEvents: function() {
137 this.currentHeaderAction = "";
138 this.canResize = false;
140 const resetElementBorderStyle = function(el, side) {
141 if ((side === "left") || (side !== "right"))
142 el.style.borderLeft = "";
143 if ((side === "right") || (side !== "left"))
144 el.style.borderRight = "";
147 const mouseMoveFn = function(e) {
148 const brect = e.target.getBoundingClientRect();
149 const mouseXRelative = e.clientX - brect.left;
150 if (this.currentHeaderAction === "") {
151 if ((brect.width - mouseXRelative) < 5) {
152 this.resizeTh = e.target;
153 this.canResize = true;
154 e.target.getParent("tr").style.cursor = "col-resize";
156 else if ((mouseXRelative < 5) && e.target.getPrevious('[class=""]')) {
157 this.resizeTh = e.target.getPrevious('[class=""]');
158 this.canResize = true;
159 e.target.getParent("tr").style.cursor = "col-resize";
162 this.canResize = false;
163 e.target.getParent("tr").style.cursor = "";
166 if (this.currentHeaderAction === "drag") {
167 const previousVisibleSibling = e.target.getPrevious('[class=""]');
168 let borderChangeElement = previousVisibleSibling;
169 let changeBorderSide = "right";
171 if (mouseXRelative > (brect.width / 2)) {
172 borderChangeElement = e.target;
173 this.dropSide = "right";
176 this.dropSide = "left";
179 e.target.getParent("tr").style.cursor = "move";
181 if (!previousVisibleSibling) { // right most column
182 borderChangeElement = e.target;
184 if (mouseXRelative <= (brect.width / 2))
185 changeBorderSide = "left";
188 const borderStyle = "initial solid #e60";
189 if (changeBorderSide === "left")
190 borderChangeElement.style.borderLeft = borderStyle;
192 borderChangeElement.style.borderRight = borderStyle;
194 resetElementBorderStyle(borderChangeElement, ((changeBorderSide === "right") ? "left" : "right"));
196 borderChangeElement.getSiblings('[class=""]').each((el) => {
197 resetElementBorderStyle(el);
200 this.lastHoverTh = e.target;
201 this.lastClientX = e.clientX;
204 const mouseOutFn = function(e) {
205 resetElementBorderStyle(e.target);
208 const onBeforeStart = function(el) {
210 this.currentHeaderAction = "start";
211 this.dragMovement = false;
212 this.dragStartX = this.lastClientX;
215 const onStart = function(el, event) {
216 if (this.canResize) {
217 this.currentHeaderAction = "resize";
218 this.startWidth = parseInt(this.resizeTh.style.width, 10);
221 this.currentHeaderAction = "drag";
222 el.style.backgroundColor = "#C1D5E7";
226 const onDrag = function(el, event) {
227 if (this.currentHeaderAction === "resize") {
228 let width = this.startWidth + (event.event.pageX - this.dragStartX);
231 this.columns[this.resizeTh.columnName].width = width;
232 this.updateColumn(this.resizeTh.columnName);
236 const onComplete = function(el, event) {
237 resetElementBorderStyle(this.lastHoverTh);
238 el.style.backgroundColor = "";
239 if (this.currentHeaderAction === "resize")
240 LocalPreferences.set("column_" + this.resizeTh.columnName + "_width_" + this.dynamicTableDivId, this.columns[this.resizeTh.columnName].width);
241 if ((this.currentHeaderAction === "drag") && (el !== this.lastHoverTh)) {
242 this.saveColumnsOrder();
243 const val = LocalPreferences.get("columns_order_" + this.dynamicTableDivId).split(",");
244 val.erase(el.columnName);
245 let pos = val.indexOf(this.lastHoverTh.columnName);
246 if (this.dropSide === "right")
248 val.splice(pos, 0, el.columnName);
249 LocalPreferences.set("columns_order_" + this.dynamicTableDivId, val.join(","));
250 this.loadColumnsOrder();
251 this.updateTableHeaders();
252 while (this.tableBody.firstChild)
253 this.tableBody.removeChild(this.tableBody.firstChild);
254 this.updateTable(true);
256 if (this.currentHeaderAction === "drag") {
257 resetElementBorderStyle(el);
258 el.getSiblings('[class=""]').each((el) => {
259 resetElementBorderStyle(el);
262 this.currentHeaderAction = "";
265 const onCancel = function(el) {
266 this.currentHeaderAction = "";
267 this.setSortedColumn(el.columnName);
270 const onTouch = function(e) {
271 const column = e.target.columnName;
272 this.currentHeaderAction = "";
273 this.setSortedColumn(column);
276 const ths = this.fixedTableHeader.getElements("th");
278 for (let i = 0; i < ths.length; ++i) {
280 th.addEventListener("mousemove", mouseMoveFn);
281 th.addEventListener("mouseout", mouseOutFn);
282 th.addEventListener("touchend", onTouch);
288 onBeforeStart: onBeforeStart,
291 onComplete: onComplete,
297 setupDynamicTableHeaderContextMenuClass: function() {
298 if (!DynamicTableHeaderContextMenuClass) {
299 DynamicTableHeaderContextMenuClass = new Class({
300 Extends: window.qBittorrent.ContextMenu.ContextMenu,
301 updateMenuItems: function() {
302 for (let i = 0; i < this.dynamicTable.columns.length; ++i) {
303 if (this.dynamicTable.columns[i].caption === "")
305 if (this.dynamicTable.columns[i].visible !== "0")
306 this.setItemChecked(this.dynamicTable.columns[i].name, true);
308 this.setItemChecked(this.dynamicTable.columns[i].name, false);
315 showColumn: function(columnName, show) {
316 this.columns[columnName].visible = show ? "1" : "0";
317 LocalPreferences.set("column_" + columnName + "_visible_" + this.dynamicTableDivId, show ? "1" : "0");
318 this.updateColumn(columnName);
321 setupHeaderMenu: function() {
322 this.setupDynamicTableHeaderContextMenuClass();
324 const menuId = this.dynamicTableDivId + "_headerMenu";
326 // reuse menu if already exists
327 const ul = $(menuId) ?? new Element("ul", {
329 class: "contextMenu scrollableMenu"
332 const createLi = function(columnName, text) {
333 const anchor = document.createElement("a");
334 anchor.href = `#${columnName}`;
335 anchor.textContent = text;
337 const img = document.createElement("img");
338 img.src = "images/checked-completed.svg";
341 const listItem = document.createElement("li");
342 listItem.appendChild(anchor);
349 const onMenuItemClicked = function(element, ref, action) {
350 this.showColumn(action, this.columns[action].visible === "0");
353 // recreate child nodes when reusing (enables the context menu to work correctly)
354 if (ul.hasChildNodes()) {
355 while (ul.firstChild)
356 ul.removeChild(ul.lastChild);
359 for (let i = 0; i < this.columns.length; ++i) {
360 const text = this.columns[i].caption;
363 ul.appendChild(createLi(this.columns[i].name, text));
364 actions[this.columns[i].name] = onMenuItemClicked;
367 ul.inject(document.body);
369 this.headerContextMenu = new DynamicTableHeaderContextMenuClass({
370 targets: "#" + this.dynamicTableFixedHeaderDivId + " tr",
379 this.headerContextMenu.dynamicTable = this;
382 initColumns: function() {},
384 newColumn: function(name, style, caption, defaultWidth, defaultVisible) {
386 column["name"] = name;
387 column["title"] = name;
388 column["visible"] = LocalPreferences.get("column_" + name + "_visible_" + this.dynamicTableDivId, defaultVisible ? "1" : "0");
389 column["force_hide"] = false;
390 column["caption"] = caption;
391 column["style"] = style;
392 column["width"] = LocalPreferences.get("column_" + name + "_width_" + this.dynamicTableDivId, defaultWidth);
393 column["dataProperties"] = [name];
394 column["getRowValue"] = function(row, pos) {
395 if (pos === undefined)
397 return row["full_data"][this.dataProperties[pos]];
399 column["compareRows"] = function(row1, row2) {
400 const value1 = this.getRowValue(row1);
401 const value2 = this.getRowValue(row2);
402 if ((typeof(value1) === "number") && (typeof(value2) === "number"))
403 return compareNumbers(value1, value2);
404 return window.qBittorrent.Misc.naturalSortCollator.compare(value1, value2);
406 column["updateTd"] = function(td, row) {
407 const value = this.getRowValue(row);
408 td.textContent = value;
411 column["onResize"] = null;
412 this.columns.push(column);
413 this.columns[name] = column;
415 this.hiddenTableHeader.appendChild(new Element("th"));
416 this.fixedTableHeader.appendChild(new Element("th"));
419 loadColumnsOrder: function() {
420 const columnsOrder = [];
421 const val = LocalPreferences.get("columns_order_" + this.dynamicTableDivId);
422 if ((val === null) || (val === undefined))
424 val.split(",").forEach((v) => {
425 if ((v in this.columns) && (!columnsOrder.contains(v)))
426 columnsOrder.push(v);
429 for (let i = 0; i < this.columns.length; ++i) {
430 if (!columnsOrder.contains(this.columns[i].name))
431 columnsOrder.push(this.columns[i].name);
434 for (let i = 0; i < this.columns.length; ++i)
435 this.columns[i] = this.columns[columnsOrder[i]];
438 saveColumnsOrder: function() {
440 for (let i = 0; i < this.columns.length; ++i) {
443 val += this.columns[i].name;
445 LocalPreferences.set("columns_order_" + this.dynamicTableDivId, val);
448 updateTableHeaders: function() {
449 this.updateHeader(this.hiddenTableHeader);
450 this.updateHeader(this.fixedTableHeader);
453 updateHeader: function(header) {
454 const ths = header.getElements("th");
456 for (let i = 0; i < ths.length; ++i) {
459 th.title = this.columns[i].caption;
460 th.textContent = this.columns[i].caption;
461 th.setAttribute("style", "width: " + this.columns[i].width + "px;" + this.columns[i].style);
462 th.columnName = this.columns[i].name;
463 th.addClass("column_" + th.columnName);
464 if ((this.columns[i].visible === "0") || this.columns[i].force_hide)
465 th.addClass("invisible");
467 th.removeClass("invisible");
471 getColumnPos: function(columnName) {
472 for (let i = 0; i < this.columns.length; ++i) {
473 if (this.columns[i].name === columnName)
479 updateColumn: function(columnName) {
480 const pos = this.getColumnPos(columnName);
481 const visible = ((this.columns[pos].visible !== "0") && !this.columns[pos].force_hide);
482 const ths = this.hiddenTableHeader.getElements("th");
483 const fths = this.fixedTableHeader.getElements("th");
484 const trs = this.tableBody.getElements("tr");
485 const style = "width: " + this.columns[pos].width + "px;" + this.columns[pos].style;
487 ths[pos].setAttribute("style", style);
488 fths[pos].setAttribute("style", style);
491 ths[pos].removeClass("invisible");
492 fths[pos].removeClass("invisible");
493 for (let i = 0; i < trs.length; ++i)
494 trs[i].getElements("td")[pos].removeClass("invisible");
497 ths[pos].addClass("invisible");
498 fths[pos].addClass("invisible");
499 for (let j = 0; j < trs.length; ++j)
500 trs[j].getElements("td")[pos].addClass("invisible");
502 if (this.columns[pos].onResize !== null)
503 this.columns[pos].onResize(columnName);
506 getSortedColumn: function() {
507 return LocalPreferences.get("sorted_column_" + this.dynamicTableDivId);
511 * @param {string} column name to sort by
512 * @param {string|null} reverse defaults to implementation-specific behavior when not specified. Should only be passed when restoring previous state.
514 setSortedColumn: function(column, reverse = null) {
515 if (column !== this.sortedColumn) {
516 const oldColumn = this.sortedColumn;
517 this.sortedColumn = column;
518 this.reverseSort = reverse ?? "0";
519 this.setSortedColumnIcon(column, oldColumn, false);
523 this.reverseSort = reverse ?? (this.reverseSort === "0" ? "1" : "0");
524 this.setSortedColumnIcon(column, null, (this.reverseSort === "1"));
526 LocalPreferences.set("sorted_column_" + this.dynamicTableDivId, column);
527 LocalPreferences.set("reverse_sort_" + this.dynamicTableDivId, this.reverseSort);
528 this.updateTable(false);
531 setSortedColumnIcon: function(newColumn, oldColumn, isReverse) {
532 const getCol = function(headerDivId, colName) {
533 const colElem = $$("#" + headerDivId + " .column_" + colName);
534 if (colElem.length === 1)
539 const colElem = getCol(this.dynamicTableFixedHeaderDivId, newColumn);
540 if (colElem !== null) {
541 colElem.addClass("sorted");
543 colElem.addClass("reverse");
545 colElem.removeClass("reverse");
547 const oldColElem = getCol(this.dynamicTableFixedHeaderDivId, oldColumn);
548 if (oldColElem !== null) {
549 oldColElem.removeClass("sorted");
550 oldColElem.removeClass("reverse");
554 getSelectedRowId: function() {
555 if (this.selectedRows.length > 0)
556 return this.selectedRows[0];
560 isRowSelected: function(rowId) {
561 return this.selectedRows.contains(rowId);
564 setupAltRow: function() {
565 const useAltRowColors = (LocalPreferences.get("use_alt_row_colors", "true") === "true");
567 document.getElementById(this.dynamicTableDivId).classList.add("altRowColors");
570 selectAll: function() {
573 const trs = this.tableBody.getElements("tr");
574 for (let i = 0; i < trs.length; ++i) {
576 this.selectedRows.push(tr.rowId);
577 if (!tr.hasClass("selected"))
578 tr.addClass("selected");
582 deselectAll: function() {
583 this.selectedRows.empty();
586 selectRow: function(rowId) {
587 this.selectedRows.push(rowId);
589 this.onSelectedRowChanged();
592 deselectRow: function(rowId) {
593 this.selectedRows.erase(rowId);
595 this.onSelectedRowChanged();
598 selectRows: function(rowId1, rowId2) {
600 if (rowId1 === rowId2) {
601 this.selectRow(rowId1);
607 this.tableBody.getElements("tr").each((tr) => {
608 if ((tr.rowId === rowId1) || (tr.rowId === rowId2)) {
610 that.selectedRows.push(tr.rowId);
613 that.selectedRows.push(tr.rowId);
617 this.onSelectedRowChanged();
620 reselectRows: function(rowIds) {
622 this.selectedRows = rowIds.slice();
623 this.tableBody.getElements("tr").each((tr) => {
624 if (rowIds.includes(tr.rowId))
625 tr.addClass("selected");
629 setRowClass: function() {
631 this.tableBody.getElements("tr").each((tr) => {
632 if (that.isRowSelected(tr.rowId))
633 tr.addClass("selected");
635 tr.removeClass("selected");
639 onSelectedRowChanged: function() {},
641 updateRowData: function(data) {
642 // ensure rowId is a string
643 const rowId = `${data["rowId"]}`;
646 if (!this.rows.has(rowId)) {
651 this.rows.set(rowId, row);
654 row = this.rows.get(rowId);
658 for (const x in data) {
659 if (!Object.hasOwn(data, x))
661 row["full_data"][x] = data[x];
665 getFilteredAndSortedRows: function() {
666 const filteredRows = [];
668 const rows = this.rows.getValues();
670 for (let i = 0; i < rows.length; ++i) {
671 filteredRows.push(rows[i]);
672 filteredRows[rows[i].rowId] = rows[i];
675 filteredRows.sort((row1, row2) => {
676 const column = this.columns[this.sortedColumn];
677 const res = column.compareRows(row1, row2);
678 if (this.reverseSort === "0")
686 getTrByRowId: function(rowId) {
687 const trs = this.tableBody.getElements("tr");
688 for (let i = 0; i < trs.length; ++i) {
689 if (trs[i].rowId === rowId)
695 updateTable: function(fullUpdate = false) {
696 const rows = this.getFilteredAndSortedRows();
698 for (let i = 0; i < this.selectedRows.length; ++i) {
699 if (!(this.selectedRows[i] in rows)) {
700 this.selectedRows.splice(i, 1);
705 const trs = this.tableBody.getElements("tr");
707 for (let rowPos = 0; rowPos < rows.length; ++rowPos) {
708 const rowId = rows[rowPos]["rowId"];
709 let tr_found = false;
710 for (let j = rowPos; j < trs.length; ++j) {
711 if (trs[j]["rowId"] === rowId) {
715 trs[j].inject(trs[rowPos], "before");
716 const tmpTr = trs[j];
718 trs.splice(rowPos, 0, tmpTr);
722 if (tr_found) { // row already exists in the table
723 this.updateRow(trs[rowPos], fullUpdate);
725 else { // else create a new row in the table
726 const tr = new Element("tr");
727 // set tabindex so element receives keydown events
728 // more info: https://developer.mozilla.org/en-US/docs/Web/API/Element/keydown_event
731 const rowId = rows[rowPos]["rowId"];
732 tr.setAttribute("data-row-id", rowId);
736 tr.addEventListener("contextmenu", function(e) {
737 if (!this._this.isRowSelected(this.rowId)) {
738 this._this.deselectAll();
739 this._this.selectRow(this.rowId);
743 tr.addEventListener("click", function(e) {
747 if (e.ctrlKey || e.metaKey) {
748 // CTRL/CMD ⌘ key was pressed
749 if (this._this.isRowSelected(this.rowId))
750 this._this.deselectRow(this.rowId);
752 this._this.selectRow(this.rowId);
754 else if (e.shiftKey && (this._this.selectedRows.length === 1)) {
755 // Shift key was pressed
756 this._this.selectRows(this._this.getSelectedRowId(), this.rowId);
760 this._this.deselectAll();
761 this._this.selectRow(this.rowId);
765 tr.addEventListener("touchstart", function(e) {
766 if (!this._this.isRowSelected(this.rowId)) {
767 this._this.deselectAll();
768 this._this.selectRow(this.rowId);
771 tr.addEventListener("keydown", function(event) {
774 this._this.selectPreviousRow();
777 this._this.selectNextRow();
784 for (let k = 0; k < this.columns.length; ++k) {
785 const td = new Element("td");
786 if ((this.columns[k].visible === "0") || this.columns[k].force_hide)
787 td.addClass("invisible");
792 if (rowPos >= trs.length) {
793 tr.inject(this.tableBody);
797 tr.inject(trs[rowPos], "before");
798 trs.splice(rowPos, 0, tr);
801 // Update context menu
802 if (this.contextMenu)
803 this.contextMenu.addTarget(tr);
805 this.updateRow(tr, true);
809 const rowPos = rows.length;
811 while ((rowPos < trs.length) && (trs.length > 0))
815 setupTr: function(tr) {},
817 updateRow: function(tr, fullUpdate) {
818 const row = this.rows.get(tr.rowId);
819 const data = row[fullUpdate ? "full_data" : "data"];
821 const tds = tr.getElements("td");
822 for (let i = 0; i < this.columns.length; ++i) {
823 if (Object.hasOwn(data, this.columns[i].dataProperties[0]))
824 this.columns[i].updateTd(tds[i], row);
829 removeRow: function(rowId) {
830 this.selectedRows.erase(rowId);
831 if (this.rows.has(rowId))
832 this.rows.erase(rowId);
833 const tr = this.getTrByRowId(rowId);
841 const trs = this.tableBody.getElements("tr");
842 while (trs.length > 0)
846 selectedRowsIds: function() {
847 return this.selectedRows.slice();
850 getRowIds: function() {
851 return this.rows.getKeys();
854 selectNextRow: function() {
855 const visibleRows = $(this.dynamicTableDivId).getElements("tbody tr").filter(e => e.style.display !== "none");
856 const selectedRowId = this.getSelectedRowId();
858 let selectedIndex = -1;
859 for (let i = 0; i < visibleRows.length; ++i) {
860 const row = visibleRows[i];
861 if (row.getAttribute("data-row-id") === selectedRowId) {
867 const isLastRowSelected = (selectedIndex >= (visibleRows.length - 1));
868 if (!isLastRowSelected) {
871 const newRow = visibleRows[selectedIndex + 1];
872 this.selectRow(newRow.getAttribute("data-row-id"));
876 selectPreviousRow: function() {
877 const visibleRows = $(this.dynamicTableDivId).getElements("tbody tr").filter(e => e.style.display !== "none");
878 const selectedRowId = this.getSelectedRowId();
880 let selectedIndex = -1;
881 for (let i = 0; i < visibleRows.length; ++i) {
882 const row = visibleRows[i];
883 if (row.getAttribute("data-row-id") === selectedRowId) {
889 const isFirstRowSelected = selectedIndex <= 0;
890 if (!isFirstRowSelected) {
893 const newRow = visibleRows[selectedIndex - 1];
894 this.selectRow(newRow.getAttribute("data-row-id"));
899 const TorrentsTable = new Class({
900 Extends: DynamicTable,
902 initColumns: function() {
903 this.newColumn("priority", "", "#", 30, true);
904 this.newColumn("state_icon", "cursor: default", "", 22, true);
905 this.newColumn("name", "", "QBT_TR(Name)QBT_TR[CONTEXT=TransferListModel]", 200, true);
906 this.newColumn("size", "", "QBT_TR(Size)QBT_TR[CONTEXT=TransferListModel]", 100, true);
907 this.newColumn("total_size", "", "QBT_TR(Total Size)QBT_TR[CONTEXT=TransferListModel]", 100, false);
908 this.newColumn("progress", "", "QBT_TR(Progress)QBT_TR[CONTEXT=TransferListModel]", 85, true);
909 this.newColumn("status", "", "QBT_TR(Status)QBT_TR[CONTEXT=TransferListModel]", 100, true);
910 this.newColumn("num_seeds", "", "QBT_TR(Seeds)QBT_TR[CONTEXT=TransferListModel]", 100, true);
911 this.newColumn("num_leechs", "", "QBT_TR(Peers)QBT_TR[CONTEXT=TransferListModel]", 100, true);
912 this.newColumn("dlspeed", "", "QBT_TR(Down Speed)QBT_TR[CONTEXT=TransferListModel]", 100, true);
913 this.newColumn("upspeed", "", "QBT_TR(Up Speed)QBT_TR[CONTEXT=TransferListModel]", 100, true);
914 this.newColumn("eta", "", "QBT_TR(ETA)QBT_TR[CONTEXT=TransferListModel]", 100, true);
915 this.newColumn("ratio", "", "QBT_TR(Ratio)QBT_TR[CONTEXT=TransferListModel]", 100, true);
916 this.newColumn("popularity", "", "QBT_TR(Popularity)QBT_TR[CONTEXT=TransferListModel]", 100, true);
917 this.newColumn("category", "", "QBT_TR(Category)QBT_TR[CONTEXT=TransferListModel]", 100, true);
918 this.newColumn("tags", "", "QBT_TR(Tags)QBT_TR[CONTEXT=TransferListModel]", 100, true);
919 this.newColumn("added_on", "", "QBT_TR(Added On)QBT_TR[CONTEXT=TransferListModel]", 100, true);
920 this.newColumn("completion_on", "", "QBT_TR(Completed On)QBT_TR[CONTEXT=TransferListModel]", 100, false);
921 this.newColumn("tracker", "", "QBT_TR(Tracker)QBT_TR[CONTEXT=TransferListModel]", 100, false);
922 this.newColumn("dl_limit", "", "QBT_TR(Down Limit)QBT_TR[CONTEXT=TransferListModel]", 100, false);
923 this.newColumn("up_limit", "", "QBT_TR(Up Limit)QBT_TR[CONTEXT=TransferListModel]", 100, false);
924 this.newColumn("downloaded", "", "QBT_TR(Downloaded)QBT_TR[CONTEXT=TransferListModel]", 100, false);
925 this.newColumn("uploaded", "", "QBT_TR(Uploaded)QBT_TR[CONTEXT=TransferListModel]", 100, false);
926 this.newColumn("downloaded_session", "", "QBT_TR(Session Download)QBT_TR[CONTEXT=TransferListModel]", 100, false);
927 this.newColumn("uploaded_session", "", "QBT_TR(Session Upload)QBT_TR[CONTEXT=TransferListModel]", 100, false);
928 this.newColumn("amount_left", "", "QBT_TR(Remaining)QBT_TR[CONTEXT=TransferListModel]", 100, false);
929 this.newColumn("time_active", "", "QBT_TR(Time Active)QBT_TR[CONTEXT=TransferListModel]", 100, false);
930 this.newColumn("save_path", "", "QBT_TR(Save path)QBT_TR[CONTEXT=TransferListModel]", 100, false);
931 this.newColumn("completed", "", "QBT_TR(Completed)QBT_TR[CONTEXT=TransferListModel]", 100, false);
932 this.newColumn("max_ratio", "", "QBT_TR(Ratio Limit)QBT_TR[CONTEXT=TransferListModel]", 100, false);
933 this.newColumn("seen_complete", "", "QBT_TR(Last Seen Complete)QBT_TR[CONTEXT=TransferListModel]", 100, false);
934 this.newColumn("last_activity", "", "QBT_TR(Last Activity)QBT_TR[CONTEXT=TransferListModel]", 100, false);
935 this.newColumn("availability", "", "QBT_TR(Availability)QBT_TR[CONTEXT=TransferListModel]", 100, false);
936 this.newColumn("download_path", "", "QBT_TR(Incomplete Save Path)QBT_TR[CONTEXT=TransferListModel]", 100, false);
937 this.newColumn("infohash_v1", "", "QBT_TR(Info Hash v1)QBT_TR[CONTEXT=TransferListModel]", 100, false);
938 this.newColumn("infohash_v2", "", "QBT_TR(Info Hash v2)QBT_TR[CONTEXT=TransferListModel]", 100, false);
939 this.newColumn("reannounce", "", "QBT_TR(Reannounce In)QBT_TR[CONTEXT=TransferListModel]", 100, false);
940 this.newColumn("private", "", "QBT_TR(Private)QBT_TR[CONTEXT=TransferListModel]", 100, false);
942 this.columns["state_icon"].onclick = "";
943 this.columns["state_icon"].dataProperties[0] = "state";
945 this.columns["num_seeds"].dataProperties.push("num_complete");
946 this.columns["num_leechs"].dataProperties.push("num_incomplete");
947 this.columns["time_active"].dataProperties.push("seeding_time");
949 this.initColumnsFunctions();
952 initColumnsFunctions: function() {
955 this.columns["state_icon"].updateTd = function(td, row) {
956 let state = this.getRowValue(row);
964 state = "downloading";
965 img_path = "images/downloading.svg";
970 img_path = "images/upload.svg";
974 img_path = "images/stalledUP.svg";
978 img_path = "images/stalledDL.svg";
981 state = "torrent-stop";
982 img_path = "images/stopped.svg";
985 state = "checked-completed";
986 img_path = "images/checked-completed.svg";
991 img_path = "images/queued.svg";
995 case "queuedForChecking":
996 case "checkingResumeData":
997 state = "force-recheck";
998 img_path = "images/force-recheck.svg";
1002 img_path = "images/set-location.svg";
1006 case "missingFiles":
1008 img_path = "images/error.svg";
1011 break; // do nothing
1014 if (td.getChildren("img").length > 0) {
1015 const img = td.getChildren("img")[0];
1016 if (!img.src.includes(img_path)) {
1022 td.adopt(new Element("img", {
1024 "class": "stateIcon",
1031 this.columns["status"].updateTd = function(td, row) {
1032 const state = this.getRowValue(row);
1039 status = "QBT_TR(Downloading)QBT_TR[CONTEXT=TransferListDelegate]";
1042 status = "QBT_TR(Stalled)QBT_TR[CONTEXT=TransferListDelegate]";
1045 status = "QBT_TR(Downloading metadata)QBT_TR[CONTEXT=TransferListDelegate]";
1047 case "forcedMetaDL":
1048 status = "QBT_TR([F] Downloading metadata)QBT_TR[CONTEXT=TransferListDelegate]";
1051 status = "QBT_TR([F] Downloading)QBT_TR[CONTEXT=TransferListDelegate]";
1055 status = "QBT_TR(Seeding)QBT_TR[CONTEXT=TransferListDelegate]";
1058 status = "QBT_TR([F] Seeding)QBT_TR[CONTEXT=TransferListDelegate]";
1062 status = "QBT_TR(Queued)QBT_TR[CONTEXT=TransferListDelegate]";
1066 status = "QBT_TR(Checking)QBT_TR[CONTEXT=TransferListDelegate]";
1068 case "queuedForChecking":
1069 status = "QBT_TR(Queued for checking)QBT_TR[CONTEXT=TransferListDelegate]";
1071 case "checkingResumeData":
1072 status = "QBT_TR(Checking resume data)QBT_TR[CONTEXT=TransferListDelegate]";
1075 status = "QBT_TR(Stopped)QBT_TR[CONTEXT=TransferListDelegate]";
1078 status = "QBT_TR(Completed)QBT_TR[CONTEXT=TransferListDelegate]";
1081 status = "QBT_TR(Moving)QBT_TR[CONTEXT=TransferListDelegate]";
1083 case "missingFiles":
1084 status = "QBT_TR(Missing Files)QBT_TR[CONTEXT=TransferListDelegate]";
1087 status = "QBT_TR(Errored)QBT_TR[CONTEXT=TransferListDelegate]";
1090 status = "QBT_TR(Unknown)QBT_TR[CONTEXT=HttpServer]";
1093 td.textContent = status;
1098 this.columns["priority"].updateTd = function(td, row) {
1099 const queuePos = this.getRowValue(row);
1100 const formattedQueuePos = (queuePos < 1) ? "*" : queuePos;
1101 td.textContent = formattedQueuePos;
1102 td.title = formattedQueuePos;
1105 this.columns["priority"].compareRows = function(row1, row2) {
1106 let row1_val = this.getRowValue(row1);
1107 let row2_val = this.getRowValue(row2);
1112 return compareNumbers(row1_val, row2_val);
1115 // name, category, tags
1116 this.columns["name"].compareRows = function(row1, row2) {
1117 const row1Val = this.getRowValue(row1);
1118 const row2Val = this.getRowValue(row2);
1119 return row1Val.localeCompare(row2Val, undefined, { numeric: true, sensitivity: "base" });
1121 this.columns["category"].compareRows = this.columns["name"].compareRows;
1122 this.columns["tags"].compareRows = this.columns["name"].compareRows;
1125 this.columns["size"].updateTd = function(td, row) {
1126 const size = window.qBittorrent.Misc.friendlyUnit(this.getRowValue(row), false);
1127 td.textContent = size;
1130 this.columns["total_size"].updateTd = this.columns["size"].updateTd;
1133 this.columns["progress"].updateTd = function(td, row) {
1134 const progress = this.getRowValue(row);
1135 let progressFormatted = (progress * 100).round(1);
1136 if ((progressFormatted === 100.0) && (progress !== 1.0))
1137 progressFormatted = 99.9;
1139 if (td.getChildren("div").length > 0) {
1140 const div = td.getChildren("div")[0];
1143 div.setWidth(ProgressColumnWidth - 5);
1145 if (div.getValue() !== progressFormatted)
1146 div.setValue(progressFormatted);
1149 if (ProgressColumnWidth < 0)
1150 ProgressColumnWidth = td.offsetWidth;
1151 td.adopt(new window.qBittorrent.ProgressBar.ProgressBar(progressFormatted.toFloat(), {
1152 "width": ProgressColumnWidth - 5
1158 this.columns["progress"].onResize = function(columnName) {
1159 const pos = this.getColumnPos(columnName);
1160 const trs = this.tableBody.getElements("tr");
1161 ProgressColumnWidth = -1;
1162 for (let i = 0; i < trs.length; ++i) {
1163 const td = trs[i].getElements("td")[pos];
1164 if (ProgressColumnWidth < 0)
1165 ProgressColumnWidth = td.offsetWidth;
1167 this.columns[columnName].updateTd(td, this.rows.get(trs[i].rowId));
1172 this.columns["num_seeds"].updateTd = function(td, row) {
1173 const num_seeds = this.getRowValue(row, 0);
1174 const num_complete = this.getRowValue(row, 1);
1175 let value = num_seeds;
1176 if (num_complete !== -1)
1177 value += " (" + num_complete + ")";
1178 td.textContent = value;
1181 this.columns["num_seeds"].compareRows = function(row1, row2) {
1182 const num_seeds1 = this.getRowValue(row1, 0);
1183 const num_complete1 = this.getRowValue(row1, 1);
1185 const num_seeds2 = this.getRowValue(row2, 0);
1186 const num_complete2 = this.getRowValue(row2, 1);
1188 const result = compareNumbers(num_complete1, num_complete2);
1191 return compareNumbers(num_seeds1, num_seeds2);
1195 this.columns["num_leechs"].updateTd = this.columns["num_seeds"].updateTd;
1196 this.columns["num_leechs"].compareRows = this.columns["num_seeds"].compareRows;
1199 this.columns["dlspeed"].updateTd = function(td, row) {
1200 const speed = window.qBittorrent.Misc.friendlyUnit(this.getRowValue(row), true);
1201 td.textContent = speed;
1206 this.columns["upspeed"].updateTd = this.columns["dlspeed"].updateTd;
1209 this.columns["eta"].updateTd = function(td, row) {
1210 const eta = window.qBittorrent.Misc.friendlyDuration(this.getRowValue(row), window.qBittorrent.Misc.MAX_ETA);
1211 td.textContent = eta;
1216 this.columns["ratio"].updateTd = function(td, row) {
1217 const ratio = this.getRowValue(row);
1218 const string = (ratio === -1) ? "∞" : window.qBittorrent.Misc.toFixedPointString(ratio, 2);
1219 td.textContent = string;
1224 this.columns["popularity"].updateTd = function(td, row) {
1225 const value = this.getRowValue(row);
1226 const popularity = (value === -1) ? "∞" : window.qBittorrent.Misc.toFixedPointString(value, 2);
1227 td.textContent = popularity;
1228 td.title = popularity;
1232 this.columns["added_on"].updateTd = function(td, row) {
1233 const date = new Date(this.getRowValue(row) * 1000).toLocaleString();
1234 td.textContent = date;
1239 this.columns["completion_on"].updateTd = function(td, row) {
1240 const val = this.getRowValue(row);
1241 if ((val === 0xffffffff) || (val < 0)) {
1242 td.textContent = "";
1246 const date = new Date(this.getRowValue(row) * 1000).toLocaleString();
1247 td.textContent = date;
1252 // dl_limit, up_limit
1253 this.columns["dl_limit"].updateTd = function(td, row) {
1254 const speed = this.getRowValue(row);
1256 td.textContent = "∞";
1260 const formattedSpeed = window.qBittorrent.Misc.friendlyUnit(speed, true);
1261 td.textContent = formattedSpeed;
1262 td.title = formattedSpeed;
1266 this.columns["up_limit"].updateTd = this.columns["dl_limit"].updateTd;
1268 // downloaded, uploaded, downloaded_session, uploaded_session, amount_left
1269 this.columns["downloaded"].updateTd = this.columns["size"].updateTd;
1270 this.columns["uploaded"].updateTd = this.columns["size"].updateTd;
1271 this.columns["downloaded_session"].updateTd = this.columns["size"].updateTd;
1272 this.columns["uploaded_session"].updateTd = this.columns["size"].updateTd;
1273 this.columns["amount_left"].updateTd = this.columns["size"].updateTd;
1276 this.columns["time_active"].updateTd = function(td, row) {
1277 const activeTime = this.getRowValue(row, 0);
1278 const seedingTime = this.getRowValue(row, 1);
1279 const time = (seedingTime > 0)
1280 ? ("QBT_TR(%1 (seeded for %2))QBT_TR[CONTEXT=TransferListDelegate]"
1281 .replace("%1", window.qBittorrent.Misc.friendlyDuration(activeTime))
1282 .replace("%2", window.qBittorrent.Misc.friendlyDuration(seedingTime)))
1283 : window.qBittorrent.Misc.friendlyDuration(activeTime);
1284 td.textContent = time;
1289 this.columns["completed"].updateTd = this.columns["size"].updateTd;
1292 this.columns["max_ratio"].updateTd = this.columns["ratio"].updateTd;
1295 this.columns["seen_complete"].updateTd = this.columns["completion_on"].updateTd;
1298 this.columns["last_activity"].updateTd = function(td, row) {
1299 const val = this.getRowValue(row);
1301 td.textContent = "∞";
1305 const formattedVal = "QBT_TR(%1 ago)QBT_TR[CONTEXT=TransferListDelegate]".replace("%1", window.qBittorrent.Misc.friendlyDuration((new Date() / 1000) - val));
1306 td.textContent = formattedVal;
1307 td.title = formattedVal;
1312 this.columns["availability"].updateTd = function(td, row) {
1313 const value = window.qBittorrent.Misc.toFixedPointString(this.getRowValue(row), 3);
1314 td.textContent = value;
1319 this.columns["infohash_v1"].updateTd = function(td, row) {
1320 const sourceInfohashV1 = this.getRowValue(row);
1321 const infohashV1 = (sourceInfohashV1 !== "") ? sourceInfohashV1 : "QBT_TR(N/A)QBT_TR[CONTEXT=TransferListDelegate]";
1322 td.textContent = infohashV1;
1323 td.title = infohashV1;
1327 this.columns["infohash_v2"].updateTd = function(td, row) {
1328 const sourceInfohashV2 = this.getRowValue(row);
1329 const infohashV2 = (sourceInfohashV2 !== "") ? sourceInfohashV2 : "QBT_TR(N/A)QBT_TR[CONTEXT=TransferListDelegate]";
1330 td.textContent = infohashV2;
1331 td.title = infohashV2;
1335 this.columns["reannounce"].updateTd = function(td, row) {
1336 const time = window.qBittorrent.Misc.friendlyDuration(this.getRowValue(row));
1337 td.textContent = time;
1342 this.columns["private"].updateTd = function(td, row) {
1343 const hasMetadata = row["full_data"].has_metadata;
1344 const isPrivate = this.getRowValue(row);
1345 const string = hasMetadata
1347 ? "QBT_TR(Yes)QBT_TR[CONTEXT=PropertiesWidget]"
1348 : "QBT_TR(No)QBT_TR[CONTEXT=PropertiesWidget]")
1349 : "QBT_TR(N/A)QBT_TR[CONTEXT=PropertiesWidget]";
1350 td.textContent = string;
1355 applyFilter: function(row, filterName, categoryHash, tagHash, trackerHash, filterTerms) {
1356 const state = row["full_data"].state;
1357 let inactive = false;
1359 switch (filterName) {
1361 if ((state !== "downloading") && !state.includes("DL"))
1365 if ((state !== "uploading") && (state !== "forcedUP") && (state !== "stalledUP") && (state !== "queuedUP") && (state !== "checkingUP"))
1369 if ((state !== "uploading") && !state.includes("UP"))
1373 if (!state.includes("stopped"))
1377 if (state.includes("stopped"))
1381 if ((state !== "stalledUP") && (state !== "stalledDL"))
1384 case "stalled_uploading":
1385 if (state !== "stalledUP")
1388 case "stalled_downloading":
1389 if (state !== "stalledDL")
1397 if (state === "stalledDL")
1398 r = (row["full_data"].upspeed > 0);
1400 r = (state === "metaDL") || (state === "forcedMetaDL") || (state === "downloading") || (state === "forcedDL") || (state === "uploading") || (state === "forcedUP");
1406 if ((state !== "checkingUP") && (state !== "checkingDL") && (state !== "checkingResumeData"))
1410 if (state !== "moving")
1414 if ((state !== "error") && (state !== "unknown") && (state !== "missingFiles"))
1419 switch (categoryHash) {
1420 case CATEGORIES_ALL:
1421 break; // do nothing
1422 case CATEGORIES_UNCATEGORIZED:
1423 if (row["full_data"].category.length !== 0)
1425 break; // do nothing
1427 if (!useSubcategories) {
1428 if (categoryHash !== window.qBittorrent.Client.genHash(row["full_data"].category))
1432 const selectedCategory = category_list.get(categoryHash);
1433 if (selectedCategory !== undefined) {
1434 const selectedCategoryName = selectedCategory.name + "/";
1435 const torrentCategoryName = row["full_data"].category + "/";
1436 if (!torrentCategoryName.startsWith(selectedCategoryName))
1445 break; // do nothing
1448 if (row["full_data"].tags.length !== 0)
1450 break; // do nothing
1453 const tagHashes = row["full_data"].tags.split(", ").map(tag => window.qBittorrent.Client.genHash(tag));
1454 if (!tagHashes.contains(tagHash))
1460 const trackerHashInt = Number.parseInt(trackerHash, 10);
1461 switch (trackerHashInt) {
1463 break; // do nothing
1464 case TRACKERS_TRACKERLESS:
1465 if (row["full_data"].trackers_count !== 0)
1469 const tracker = trackerList.get(trackerHashInt);
1472 for (const torrents of tracker.trackerTorrentMap.values()) {
1473 if (torrents.includes(row["full_data"].rowId)) {
1485 if ((filterTerms !== undefined) && (filterTerms !== null)) {
1486 const filterBy = document.getElementById("torrentsFilterSelect").value;
1487 const textToSearch = row["full_data"][filterBy].toLowerCase();
1488 if (filterTerms instanceof RegExp) {
1489 if (!filterTerms.test(textToSearch))
1493 if ((filterTerms.length > 0) && !window.qBittorrent.Misc.containsAllTerms(textToSearch, filterTerms))
1501 getFilteredTorrentsNumber: function(filterName, categoryHash, tagHash, trackerHash) {
1503 const rows = this.rows.getValues();
1505 for (let i = 0; i < rows.length; ++i) {
1506 if (this.applyFilter(rows[i], filterName, categoryHash, tagHash, trackerHash, null))
1512 getFilteredTorrentsHashes: function(filterName, categoryHash, tagHash, trackerHash) {
1513 const rowsHashes = [];
1514 const rows = this.rows.getValues();
1516 for (let i = 0; i < rows.length; ++i) {
1517 if (this.applyFilter(rows[i], filterName, categoryHash, tagHash, trackerHash, null))
1518 rowsHashes.push(rows[i]["rowId"]);
1524 getFilteredAndSortedRows: function() {
1525 const filteredRows = [];
1527 const useRegex = $("torrentsFilterRegexBox").checked;
1528 const filterText = $("torrentsFilterInput").value.trim().toLowerCase();
1531 filterTerms = (filterText.length > 0)
1532 ? (useRegex ? new RegExp(filterText) : filterText.split(" "))
1535 catch (e) { // SyntaxError: Invalid regex pattern
1536 return filteredRows;
1539 const rows = this.rows.getValues();
1540 for (let i = 0; i < rows.length; ++i) {
1541 if (this.applyFilter(rows[i], selected_filter, selected_category, selectedTag, selectedTracker, filterTerms)) {
1542 filteredRows.push(rows[i]);
1543 filteredRows[rows[i].rowId] = rows[i];
1547 filteredRows.sort((row1, row2) => {
1548 const column = this.columns[this.sortedColumn];
1549 const res = column.compareRows(row1, row2);
1550 if (this.reverseSort === "0")
1555 return filteredRows;
1558 setupTr: function(tr) {
1559 tr.addEventListener("dblclick", function(e) {
1561 e.stopPropagation();
1563 this._this.deselectAll();
1564 this._this.selectRow(this.rowId);
1565 const row = this._this.rows.get(this.rowId);
1566 const state = row["full_data"].state;
1569 (state !== "uploading")
1570 && (state !== "stoppedUP")
1571 && (state !== "forcedUP")
1572 && (state !== "stalledUP")
1573 && (state !== "queuedUP")
1574 && (state !== "checkingUP")
1575 ? "dblclick_download"
1576 : "dblclick_complete";
1578 if (LocalPreferences.get(prefKey, "1") !== "1")
1581 if (state.includes("stopped"))
1587 tr.addClass("torrentsTableContextMenuTarget");
1590 getCurrentTorrentID: function() {
1591 return this.getSelectedRowId();
1594 onSelectedRowChanged: function() {
1595 updatePropertiesPanel();
1599 const TorrentPeersTable = new Class({
1600 Extends: DynamicTable,
1602 initColumns: function() {
1603 this.newColumn("country", "", "QBT_TR(Country/Region)QBT_TR[CONTEXT=PeerListWidget]", 22, true);
1604 this.newColumn("ip", "", "QBT_TR(IP)QBT_TR[CONTEXT=PeerListWidget]", 80, true);
1605 this.newColumn("port", "", "QBT_TR(Port)QBT_TR[CONTEXT=PeerListWidget]", 35, true);
1606 this.newColumn("connection", "", "QBT_TR(Connection)QBT_TR[CONTEXT=PeerListWidget]", 50, true);
1607 this.newColumn("flags", "", "QBT_TR(Flags)QBT_TR[CONTEXT=PeerListWidget]", 50, true);
1608 this.newColumn("client", "", "QBT_TR(Client)QBT_TR[CONTEXT=PeerListWidget]", 140, true);
1609 this.newColumn("peer_id_client", "", "QBT_TR(Peer ID Client)QBT_TR[CONTEXT=PeerListWidget]", 60, false);
1610 this.newColumn("progress", "", "QBT_TR(Progress)QBT_TR[CONTEXT=PeerListWidget]", 50, true);
1611 this.newColumn("dl_speed", "", "QBT_TR(Down Speed)QBT_TR[CONTEXT=PeerListWidget]", 50, true);
1612 this.newColumn("up_speed", "", "QBT_TR(Up Speed)QBT_TR[CONTEXT=PeerListWidget]", 50, true);
1613 this.newColumn("downloaded", "", "QBT_TR(Downloaded)QBT_TR[CONTEXT=PeerListWidget]", 50, true);
1614 this.newColumn("uploaded", "", "QBT_TR(Uploaded)QBT_TR[CONTEXT=PeerListWidget]", 50, true);
1615 this.newColumn("relevance", "", "QBT_TR(Relevance)QBT_TR[CONTEXT=PeerListWidget]", 30, true);
1616 this.newColumn("files", "", "QBT_TR(Files)QBT_TR[CONTEXT=PeerListWidget]", 100, true);
1618 this.columns["country"].dataProperties.push("country_code");
1619 this.columns["flags"].dataProperties.push("flags_desc");
1620 this.initColumnsFunctions();
1623 initColumnsFunctions: function() {
1626 this.columns["country"].updateTd = function(td, row) {
1627 const country = this.getRowValue(row, 0);
1628 const country_code = this.getRowValue(row, 1);
1630 if (!country_code) {
1631 if (td.getChildren("img").length > 0)
1632 td.getChildren("img")[0].destroy();
1636 const img_path = "images/flags/" + country_code + ".svg";
1638 if (td.getChildren("img").length > 0) {
1639 const img = td.getChildren("img")[0];
1641 img.className = "flags";
1643 img.title = country;
1646 td.adopt(new Element("img", {
1656 this.columns["ip"].compareRows = function(row1, row2) {
1657 const ip1 = this.getRowValue(row1);
1658 const ip2 = this.getRowValue(row2);
1660 const a = ip1.split(".");
1661 const b = ip2.split(".");
1663 for (let i = 0; i < 4; ++i) {
1672 this.columns["flags"].updateTd = function(td, row) {
1673 td.textContent = this.getRowValue(row, 0);
1674 td.title = this.getRowValue(row, 1);
1678 this.columns["progress"].updateTd = function(td, row) {
1679 const progress = this.getRowValue(row);
1680 let progressFormatted = (progress * 100).round(1);
1681 if ((progressFormatted === 100.0) && (progress !== 1.0))
1682 progressFormatted = 99.9;
1683 progressFormatted += "%";
1684 td.textContent = progressFormatted;
1685 td.title = progressFormatted;
1688 // dl_speed, up_speed
1689 this.columns["dl_speed"].updateTd = function(td, row) {
1690 const speed = this.getRowValue(row);
1692 td.textContent = "";
1696 const formattedSpeed = window.qBittorrent.Misc.friendlyUnit(speed, true);
1697 td.textContent = formattedSpeed;
1698 td.title = formattedSpeed;
1701 this.columns["up_speed"].updateTd = this.columns["dl_speed"].updateTd;
1703 // downloaded, uploaded
1704 this.columns["downloaded"].updateTd = function(td, row) {
1705 const downloaded = window.qBittorrent.Misc.friendlyUnit(this.getRowValue(row), false);
1706 td.textContent = downloaded;
1707 td.title = downloaded;
1709 this.columns["uploaded"].updateTd = this.columns["downloaded"].updateTd;
1712 this.columns["relevance"].updateTd = this.columns["progress"].updateTd;
1715 this.columns["files"].updateTd = function(td, row) {
1716 const value = this.getRowValue(row, 0);
1717 td.textContent = value.replace(/\n/g, ";");
1724 const SearchResultsTable = new Class({
1725 Extends: DynamicTable,
1727 initColumns: function() {
1728 this.newColumn("fileName", "", "QBT_TR(Name)QBT_TR[CONTEXT=SearchResultsTable]", 500, true);
1729 this.newColumn("fileSize", "", "QBT_TR(Size)QBT_TR[CONTEXT=SearchResultsTable]", 100, true);
1730 this.newColumn("nbSeeders", "", "QBT_TR(Seeders)QBT_TR[CONTEXT=SearchResultsTable]", 100, true);
1731 this.newColumn("nbLeechers", "", "QBT_TR(Leechers)QBT_TR[CONTEXT=SearchResultsTable]", 100, true);
1732 this.newColumn("siteUrl", "", "QBT_TR(Search engine)QBT_TR[CONTEXT=SearchResultsTable]", 250, true);
1733 this.newColumn("pubDate", "", "QBT_TR(Published On)QBT_TR[CONTEXT=SearchResultsTable]", 200, true);
1735 this.initColumnsFunctions();
1738 initColumnsFunctions: function() {
1739 const displaySize = function(td, row) {
1740 const size = window.qBittorrent.Misc.friendlyUnit(this.getRowValue(row), false);
1741 td.textContent = size;
1744 const displayNum = function(td, row) {
1745 const value = this.getRowValue(row);
1746 const formattedValue = (value === "-1") ? "Unknown" : value;
1747 td.textContent = formattedValue;
1748 td.title = formattedValue;
1750 const displayDate = function(td, row) {
1751 const value = this.getRowValue(row) * 1000;
1752 const formattedValue = (isNaN(value) || (value <= 0)) ? "" : (new Date(value).toLocaleString());
1753 td.textContent = formattedValue;
1754 td.title = formattedValue;
1757 this.columns["fileSize"].updateTd = displaySize;
1758 this.columns["nbSeeders"].updateTd = displayNum;
1759 this.columns["nbLeechers"].updateTd = displayNum;
1760 this.columns["pubDate"].updateTd = displayDate;
1763 getFilteredAndSortedRows: function() {
1764 const getSizeFilters = function() {
1765 let minSize = (window.qBittorrent.Search.searchSizeFilter.min > 0.00) ? (window.qBittorrent.Search.searchSizeFilter.min * Math.pow(1024, window.qBittorrent.Search.searchSizeFilter.minUnit)) : 0.00;
1766 let maxSize = (window.qBittorrent.Search.searchSizeFilter.max > 0.00) ? (window.qBittorrent.Search.searchSizeFilter.max * Math.pow(1024, window.qBittorrent.Search.searchSizeFilter.maxUnit)) : 0.00;
1768 if ((minSize > maxSize) && (maxSize > 0.00)) {
1769 const tmp = minSize;
1780 const getSeedsFilters = function() {
1781 let minSeeds = (window.qBittorrent.Search.searchSeedsFilter.min > 0) ? window.qBittorrent.Search.searchSeedsFilter.min : 0;
1782 let maxSeeds = (window.qBittorrent.Search.searchSeedsFilter.max > 0) ? window.qBittorrent.Search.searchSeedsFilter.max : 0;
1784 if ((minSeeds > maxSeeds) && (maxSeeds > 0)) {
1785 const tmp = minSeeds;
1786 minSeeds = maxSeeds;
1796 let filteredRows = [];
1797 const rows = this.rows.getValues();
1798 const searchTerms = window.qBittorrent.Search.searchText.pattern.toLowerCase().split(" ");
1799 const filterTerms = window.qBittorrent.Search.searchText.filterPattern.toLowerCase().split(" ");
1800 const sizeFilters = getSizeFilters();
1801 const seedsFilters = getSeedsFilters();
1802 const searchInTorrentName = $("searchInTorrentName").value === "names";
1804 if (searchInTorrentName || (filterTerms.length > 0) || (window.qBittorrent.Search.searchSizeFilter.min > 0.00) || (window.qBittorrent.Search.searchSizeFilter.max > 0.00)) {
1805 for (let i = 0; i < rows.length; ++i) {
1806 const row = rows[i];
1808 if (searchInTorrentName && !window.qBittorrent.Misc.containsAllTerms(row.full_data.fileName, searchTerms))
1810 if ((filterTerms.length > 0) && !window.qBittorrent.Misc.containsAllTerms(row.full_data.fileName, filterTerms))
1812 if ((sizeFilters.min > 0.00) && (row.full_data.fileSize < sizeFilters.min))
1814 if ((sizeFilters.max > 0.00) && (row.full_data.fileSize > sizeFilters.max))
1816 if ((seedsFilters.min > 0) && (row.full_data.nbSeeders < seedsFilters.min))
1818 if ((seedsFilters.max > 0) && (row.full_data.nbSeeders > seedsFilters.max))
1821 filteredRows.push(row);
1825 filteredRows = rows;
1828 filteredRows.sort((row1, row2) => {
1829 const column = this.columns[this.sortedColumn];
1830 const res = column.compareRows(row1, row2);
1831 if (this.reverseSort === "0")
1837 return filteredRows;
1840 setupTr: function(tr) {
1841 tr.addClass("searchTableRow");
1845 const SearchPluginsTable = new Class({
1846 Extends: DynamicTable,
1848 initColumns: function() {
1849 this.newColumn("fullName", "", "QBT_TR(Name)QBT_TR[CONTEXT=SearchPluginsTable]", 175, true);
1850 this.newColumn("version", "", "QBT_TR(Version)QBT_TR[CONTEXT=SearchPluginsTable]", 100, true);
1851 this.newColumn("url", "", "QBT_TR(Url)QBT_TR[CONTEXT=SearchPluginsTable]", 175, true);
1852 this.newColumn("enabled", "", "QBT_TR(Enabled)QBT_TR[CONTEXT=SearchPluginsTable]", 100, true);
1854 this.initColumnsFunctions();
1857 initColumnsFunctions: function() {
1858 this.columns["enabled"].updateTd = function(td, row) {
1859 const value = this.getRowValue(row);
1861 td.textContent = "QBT_TR(Yes)QBT_TR[CONTEXT=SearchPluginsTable]";
1862 td.title = "QBT_TR(Yes)QBT_TR[CONTEXT=SearchPluginsTable]";
1863 td.getParent("tr").addClass("green");
1864 td.getParent("tr").removeClass("red");
1867 td.textContent = "QBT_TR(No)QBT_TR[CONTEXT=SearchPluginsTable]";
1868 td.title = "QBT_TR(No)QBT_TR[CONTEXT=SearchPluginsTable]";
1869 td.getParent("tr").addClass("red");
1870 td.getParent("tr").removeClass("green");
1875 setupTr: function(tr) {
1876 tr.addClass("searchPluginsTableRow");
1880 const TorrentTrackersTable = new Class({
1881 Extends: DynamicTable,
1883 initColumns: function() {
1884 this.newColumn("tier", "", "QBT_TR(Tier)QBT_TR[CONTEXT=TrackerListWidget]", 35, true);
1885 this.newColumn("url", "", "QBT_TR(URL)QBT_TR[CONTEXT=TrackerListWidget]", 250, true);
1886 this.newColumn("status", "", "QBT_TR(Status)QBT_TR[CONTEXT=TrackerListWidget]", 125, true);
1887 this.newColumn("peers", "", "QBT_TR(Peers)QBT_TR[CONTEXT=TrackerListWidget]", 75, true);
1888 this.newColumn("seeds", "", "QBT_TR(Seeds)QBT_TR[CONTEXT=TrackerListWidget]", 75, true);
1889 this.newColumn("leeches", "", "QBT_TR(Leeches)QBT_TR[CONTEXT=TrackerListWidget]", 75, true);
1890 this.newColumn("downloaded", "", "QBT_TR(Times Downloaded)QBT_TR[CONTEXT=TrackerListWidget]", 100, true);
1891 this.newColumn("message", "", "QBT_TR(Message)QBT_TR[CONTEXT=TrackerListWidget]", 250, true);
1895 const BulkRenameTorrentFilesTable = new Class({
1896 Extends: DynamicTable,
1899 prevFilterTerms: [],
1900 prevRowsString: null,
1901 prevFilteredRows: [],
1902 prevSortedColumn: null,
1903 prevReverseSort: null,
1904 fileTree: new window.qBittorrent.FileTree.FileTree(),
1906 populateTable: function(root) {
1907 this.fileTree.setRoot(root);
1908 root.children.each((node) => {
1909 this._addNodeToTable(node, 0);
1913 _addNodeToTable: function(node, depth) {
1916 if (node.isFolder) {
1920 checked: node.checked,
1922 original: node.original,
1923 renamed: node.renamed
1927 node.full_data = data;
1928 this.updateRowData(data);
1931 node.data.rowId = node.rowId;
1932 node.full_data = node.data;
1933 this.updateRowData(node.data);
1936 node.children.each((child) => {
1937 this._addNodeToTable(child, depth + 1);
1941 getRoot: function() {
1942 return this.fileTree.getRoot();
1945 getNode: function(rowId) {
1946 return this.fileTree.getNode(rowId);
1949 getRow: function(node) {
1950 const rowId = this.fileTree.getRowId(node);
1951 return this.rows.get(rowId);
1954 getSelectedRows: function() {
1955 const nodes = this.fileTree.toArray();
1957 return nodes.filter(x => x.checked === 0);
1960 initColumns: function() {
1961 // Blocks saving header width (because window width isn't saved)
1962 LocalPreferences.remove("column_" + "checked" + "_width_" + this.dynamicTableDivId);
1963 LocalPreferences.remove("column_" + "original" + "_width_" + this.dynamicTableDivId);
1964 LocalPreferences.remove("column_" + "renamed" + "_width_" + this.dynamicTableDivId);
1965 this.newColumn("checked", "", "", 50, true);
1966 this.newColumn("original", "", "QBT_TR(Original)QBT_TR[CONTEXT=TrackerListWidget]", 270, true);
1967 this.newColumn("renamed", "", "QBT_TR(Renamed)QBT_TR[CONTEXT=TrackerListWidget]", 220, true);
1969 this.initColumnsFunctions();
1973 * Toggles the global checkbox and all checkboxes underneath
1975 toggleGlobalCheckbox: function() {
1976 const checkbox = $("rootMultiRename_cb");
1977 const checkboxes = $$("input.RenamingCB");
1979 for (let i = 0; i < checkboxes.length; ++i) {
1980 const node = this.getNode(i);
1982 if (checkbox.checked || checkbox.indeterminate) {
1983 const cb = checkboxes[i];
1985 cb.indeterminate = false;
1986 cb.state = "checked";
1988 node.full_data.checked = node.checked;
1991 const cb = checkboxes[i];
1993 cb.indeterminate = false;
1994 cb.state = "unchecked";
1996 node.full_data.checked = node.checked;
2000 this.updateGlobalCheckbox();
2003 toggleNodeTreeCheckbox: function(rowId, checkState) {
2004 const node = this.getNode(rowId);
2005 node.checked = checkState;
2006 node.full_data.checked = checkState;
2007 const checkbox = $(`cbRename${rowId}`);
2008 checkbox.checked = node.checked === 0;
2009 checkbox.state = checkbox.checked ? "checked" : "unchecked";
2011 for (let i = 0; i < node.children.length; ++i)
2012 this.toggleNodeTreeCheckbox(node.children[i].rowId, checkState);
2015 updateGlobalCheckbox: function() {
2016 const checkbox = $("rootMultiRename_cb");
2017 const checkboxes = $$("input.RenamingCB");
2018 const isAllChecked = function() {
2019 for (let i = 0; i < checkboxes.length; ++i) {
2020 if (!checkboxes[i].checked)
2025 const isAllUnchecked = function() {
2026 for (let i = 0; i < checkboxes.length; ++i) {
2027 if (checkboxes[i].checked)
2032 if (isAllChecked()) {
2033 checkbox.state = "checked";
2034 checkbox.indeterminate = false;
2035 checkbox.checked = true;
2037 else if (isAllUnchecked()) {
2038 checkbox.state = "unchecked";
2039 checkbox.indeterminate = false;
2040 checkbox.checked = false;
2043 checkbox.state = "partial";
2044 checkbox.indeterminate = true;
2045 checkbox.checked = false;
2049 initColumnsFunctions: function() {
2053 this.columns["checked"].updateTd = function(td, row) {
2054 const id = row.rowId;
2055 const value = this.getRowValue(row);
2057 const treeImg = new Element("img", {
2058 src: "images/L.gif",
2063 const checkbox = new Element("input");
2064 checkbox.type = "checkbox";
2065 checkbox.id = "cbRename" + id;
2066 checkbox.setAttribute("data-id", id);
2067 checkbox.className = "RenamingCB";
2068 checkbox.addEventListener("click", (e) => {
2069 const node = that.getNode(id);
2070 node.checked = e.target.checked ? 0 : 1;
2071 node.full_data.checked = node.checked;
2072 that.updateGlobalCheckbox();
2073 that.onRowSelectionChange(node);
2074 e.stopPropagation();
2076 checkbox.checked = (value === 0);
2077 checkbox.state = checkbox.checked ? "checked" : "unchecked";
2078 checkbox.indeterminate = false;
2079 td.adopt(treeImg, checkbox);
2083 this.columns["original"].updateTd = function(td, row) {
2084 const id = row.rowId;
2085 const fileNameId = "filesTablefileName" + id;
2086 const node = that.getNode(id);
2088 if (node.isFolder) {
2089 const value = this.getRowValue(row);
2090 const dirImgId = "renameTableDirImg" + id;
2092 // just update file name
2093 $(fileNameId).textContent = value;
2096 const span = new Element("span", {
2100 const dirImg = new Element("img", {
2101 src: "images/directory.svg",
2105 "margin-bottom": -3,
2106 "margin-left": (node.depth * 20)
2110 td.replaceChildren(dirImg, span);
2114 const value = this.getRowValue(row);
2115 const span = new Element("span", {
2119 "margin-left": ((node.depth + 1) * 20)
2122 td.replaceChildren(span);
2127 this.columns["renamed"].updateTd = function(td, row) {
2128 const id = row.rowId;
2129 const fileNameRenamedId = "filesTablefileRenamed" + id;
2130 const value = this.getRowValue(row);
2132 const span = new Element("span", {
2134 id: fileNameRenamedId,
2136 td.replaceChildren(span);
2140 onRowSelectionChange: function(row) {},
2142 selectRow: function() {
2146 reselectRows: function(rowIds) {
2149 this.tableBody.getElements("tr").each((tr) => {
2150 if (rowIds.includes(tr.rowId)) {
2151 const node = that.getNode(tr.rowId);
2153 node.full_data.checked = 0;
2155 const checkbox = tr.children[0].getElement("input");
2156 checkbox.state = "checked";
2157 checkbox.indeterminate = false;
2158 checkbox.checked = true;
2162 this.updateGlobalCheckbox();
2165 _sortNodesByColumn: function(nodes, column) {
2166 nodes.sort((row1, row2) => {
2167 // list folders before files when sorting by name
2168 if (column.name === "original") {
2169 const node1 = this.getNode(row1.data.rowId);
2170 const node2 = this.getNode(row2.data.rowId);
2171 if (node1.isFolder && !node2.isFolder)
2173 if (node2.isFolder && !node1.isFolder)
2177 const res = column.compareRows(row1, row2);
2178 return (this.reverseSort === "0") ? res : -res;
2181 nodes.each((node) => {
2182 if (node.children.length > 0)
2183 this._sortNodesByColumn(node.children, column);
2187 _filterNodes: function(node, filterTerms, filteredRows) {
2188 if (node.isFolder) {
2189 const childAdded = node.children.reduce((acc, child) => {
2190 // we must execute the function before ORing w/ acc or we'll stop checking child nodes after the first successful match
2191 return (this._filterNodes(child, filterTerms, filteredRows) || acc);
2195 const row = this.getRow(node);
2196 filteredRows.push(row);
2201 if (window.qBittorrent.Misc.containsAllTerms(node.original, filterTerms)) {
2202 const row = this.getRow(node);
2203 filteredRows.push(row);
2210 setFilter: function(text) {
2211 const filterTerms = text.trim().toLowerCase().split(" ");
2212 if ((filterTerms.length === 1) && (filterTerms[0] === ""))
2213 this.filterTerms = [];
2215 this.filterTerms = filterTerms;
2218 getFilteredAndSortedRows: function() {
2219 if (this.getRoot() === null)
2222 const generateRowsSignature = function(rows) {
2223 const rowsData = rows.map((row) => {
2224 return row.full_data;
2226 return JSON.stringify(rowsData);
2229 const getFilteredRows = function() {
2230 if (this.filterTerms.length === 0) {
2231 const nodeArray = this.fileTree.toArray();
2232 const filteredRows = nodeArray.map((node) => {
2233 return this.getRow(node);
2235 return filteredRows;
2238 const filteredRows = [];
2239 this.getRoot().children.each((child) => {
2240 this._filterNodes(child, this.filterTerms, filteredRows);
2242 filteredRows.reverse();
2243 return filteredRows;
2246 const hasRowsChanged = function(rowsString, prevRowsStringString) {
2247 const rowsChanged = (rowsString !== prevRowsStringString);
2248 const isFilterTermsChanged = this.filterTerms.reduce((acc, term, index) => {
2249 return (acc || (term !== this.prevFilterTerms[index]));
2251 const isFilterChanged = ((this.filterTerms.length !== this.prevFilterTerms.length)
2252 || ((this.filterTerms.length > 0) && isFilterTermsChanged));
2253 const isSortedColumnChanged = (this.prevSortedColumn !== this.sortedColumn);
2254 const isReverseSortChanged = (this.prevReverseSort !== this.reverseSort);
2256 return (rowsChanged || isFilterChanged || isSortedColumnChanged || isReverseSortChanged);
2259 const rowsString = generateRowsSignature(this.rows);
2260 if (!hasRowsChanged(rowsString, this.prevRowsString))
2261 return this.prevFilteredRows;
2263 // sort, then filter
2264 const column = this.columns[this.sortedColumn];
2265 this._sortNodesByColumn(this.getRoot().children, column);
2266 const filteredRows = getFilteredRows();
2268 this.prevFilterTerms = this.filterTerms;
2269 this.prevRowsString = rowsString;
2270 this.prevFilteredRows = filteredRows;
2271 this.prevSortedColumn = this.sortedColumn;
2272 this.prevReverseSort = this.reverseSort;
2273 return filteredRows;
2276 setIgnored: function(rowId, ignore) {
2277 const row = this.rows.get(rowId);
2279 row.full_data.remaining = 0;
2281 row.full_data.remaining = (row.full_data.size * (1.0 - (row.full_data.progress / 100)));
2284 setupTr: function(tr) {
2285 tr.addEventListener("keydown", function(event) {
2286 switch (event.key) {
2288 qBittorrent.PropFiles.collapseFolder(this._this.getSelectedRowId());
2291 qBittorrent.PropFiles.expandFolder(this._this.getSelectedRowId());
2298 const TorrentFilesTable = new Class({
2299 Extends: DynamicTable,
2302 prevFilterTerms: [],
2303 prevRowsString: null,
2304 prevFilteredRows: [],
2305 prevSortedColumn: null,
2306 prevReverseSort: null,
2307 fileTree: new window.qBittorrent.FileTree.FileTree(),
2309 populateTable: function(root) {
2310 this.fileTree.setRoot(root);
2311 root.children.each((node) => {
2312 this._addNodeToTable(node, 0);
2316 _addNodeToTable: function(node, depth) {
2319 if (node.isFolder) {
2323 checked: node.checked,
2324 remaining: node.remaining,
2325 progress: node.progress,
2326 priority: window.qBittorrent.PropFiles.normalizePriority(node.priority),
2327 availability: node.availability,
2333 node.full_data = data;
2334 this.updateRowData(data);
2337 node.data.rowId = node.rowId;
2338 node.full_data = node.data;
2339 this.updateRowData(node.data);
2342 node.children.each((child) => {
2343 this._addNodeToTable(child, depth + 1);
2347 getRoot: function() {
2348 return this.fileTree.getRoot();
2351 getNode: function(rowId) {
2352 return this.fileTree.getNode(rowId);
2355 getRow: function(node) {
2356 const rowId = this.fileTree.getRowId(node);
2357 return this.rows.get(rowId);
2360 initColumns: function() {
2361 this.newColumn("checked", "", "", 50, true);
2362 this.newColumn("name", "", "QBT_TR(Name)QBT_TR[CONTEXT=TrackerListWidget]", 300, true);
2363 this.newColumn("size", "", "QBT_TR(Total Size)QBT_TR[CONTEXT=TrackerListWidget]", 75, true);
2364 this.newColumn("progress", "", "QBT_TR(Progress)QBT_TR[CONTEXT=TrackerListWidget]", 100, true);
2365 this.newColumn("priority", "", "QBT_TR(Download Priority)QBT_TR[CONTEXT=TrackerListWidget]", 150, true);
2366 this.newColumn("remaining", "", "QBT_TR(Remaining)QBT_TR[CONTEXT=TrackerListWidget]", 75, true);
2367 this.newColumn("availability", "", "QBT_TR(Availability)QBT_TR[CONTEXT=TrackerListWidget]", 75, true);
2369 this.initColumnsFunctions();
2372 initColumnsFunctions: function() {
2374 const displaySize = function(td, row) {
2375 const size = window.qBittorrent.Misc.friendlyUnit(this.getRowValue(row), false);
2376 td.textContent = size;
2379 const displayPercentage = function(td, row) {
2380 const value = window.qBittorrent.Misc.friendlyPercentage(this.getRowValue(row));
2381 td.textContent = value;
2386 this.columns["checked"].updateTd = function(td, row) {
2387 const id = row.rowId;
2388 const value = this.getRowValue(row);
2390 if (window.qBittorrent.PropFiles.isDownloadCheckboxExists(id)) {
2391 window.qBittorrent.PropFiles.updateDownloadCheckbox(id, value);
2394 const treeImg = new Element("img", {
2395 src: "images/L.gif",
2400 td.adopt(treeImg, window.qBittorrent.PropFiles.createDownloadCheckbox(id, row.full_data.fileId, value));
2405 this.columns["name"].updateTd = function(td, row) {
2406 const id = row.rowId;
2407 const fileNameId = "filesTablefileName" + id;
2408 const node = that.getNode(id);
2410 if (node.isFolder) {
2411 const value = this.getRowValue(row);
2412 const collapseIconId = "filesTableCollapseIcon" + id;
2413 const dirImgId = "filesTableDirImg" + id;
2415 // just update file name
2416 $(fileNameId).textContent = value;
2419 const collapseIcon = new Element("img", {
2420 src: "images/go-down.svg",
2422 "margin-left": (node.depth * 20)
2424 class: "filesTableCollapseIcon",
2427 onclick: "qBittorrent.PropFiles.collapseIconClicked(this)"
2429 const span = new Element("span", {
2433 const dirImg = new Element("img", {
2434 src: "images/directory.svg",
2442 td.replaceChildren(collapseIcon, dirImg, span);
2446 const value = this.getRowValue(row);
2447 const span = new Element("span", {
2451 "margin-left": ((node.depth + 1) * 20)
2454 td.replaceChildren(span);
2459 this.columns["size"].updateTd = displaySize;
2462 this.columns["progress"].updateTd = function(td, row) {
2463 const id = row.rowId;
2464 const value = this.getRowValue(row);
2466 const progressBar = $("pbf_" + id);
2467 if (progressBar === null) {
2468 td.adopt(new window.qBittorrent.ProgressBar.ProgressBar(value.toFloat(), {
2474 progressBar.setValue(value.toFloat());
2479 this.columns["priority"].updateTd = function(td, row) {
2480 const id = row.rowId;
2481 const value = this.getRowValue(row);
2483 if (window.qBittorrent.PropFiles.isPriorityComboExists(id))
2484 window.qBittorrent.PropFiles.updatePriorityCombo(id, value);
2486 td.adopt(window.qBittorrent.PropFiles.createPriorityCombo(id, row.full_data.fileId, value));
2489 // remaining, availability
2490 this.columns["remaining"].updateTd = displaySize;
2491 this.columns["availability"].updateTd = displayPercentage;
2494 _sortNodesByColumn: function(nodes, column) {
2495 nodes.sort((row1, row2) => {
2496 // list folders before files when sorting by name
2497 if (column.name === "name") {
2498 const node1 = this.getNode(row1.data.rowId);
2499 const node2 = this.getNode(row2.data.rowId);
2500 if (node1.isFolder && !node2.isFolder)
2502 if (node2.isFolder && !node1.isFolder)
2506 const res = column.compareRows(row1, row2);
2507 return (this.reverseSort === "0") ? res : -res;
2510 nodes.each((node) => {
2511 if (node.children.length > 0)
2512 this._sortNodesByColumn(node.children, column);
2516 _filterNodes: function(node, filterTerms, filteredRows) {
2517 if (node.isFolder) {
2518 const childAdded = node.children.reduce((acc, child) => {
2519 // we must execute the function before ORing w/ acc or we'll stop checking child nodes after the first successful match
2520 return (this._filterNodes(child, filterTerms, filteredRows) || acc);
2524 const row = this.getRow(node);
2525 filteredRows.push(row);
2530 if (window.qBittorrent.Misc.containsAllTerms(node.name, filterTerms)) {
2531 const row = this.getRow(node);
2532 filteredRows.push(row);
2539 setFilter: function(text) {
2540 const filterTerms = text.trim().toLowerCase().split(" ");
2541 if ((filterTerms.length === 1) && (filterTerms[0] === ""))
2542 this.filterTerms = [];
2544 this.filterTerms = filterTerms;
2547 getFilteredAndSortedRows: function() {
2548 if (this.getRoot() === null)
2551 const generateRowsSignature = function(rows) {
2552 const rowsData = rows.map((row) => {
2553 return row.full_data;
2555 return JSON.stringify(rowsData);
2558 const getFilteredRows = function() {
2559 if (this.filterTerms.length === 0) {
2560 const nodeArray = this.fileTree.toArray();
2561 const filteredRows = nodeArray.map((node) => {
2562 return this.getRow(node);
2564 return filteredRows;
2567 const filteredRows = [];
2568 this.getRoot().children.each((child) => {
2569 this._filterNodes(child, this.filterTerms, filteredRows);
2571 filteredRows.reverse();
2572 return filteredRows;
2575 const hasRowsChanged = function(rowsString, prevRowsStringString) {
2576 const rowsChanged = (rowsString !== prevRowsStringString);
2577 const isFilterTermsChanged = this.filterTerms.reduce((acc, term, index) => {
2578 return (acc || (term !== this.prevFilterTerms[index]));
2580 const isFilterChanged = ((this.filterTerms.length !== this.prevFilterTerms.length)
2581 || ((this.filterTerms.length > 0) && isFilterTermsChanged));
2582 const isSortedColumnChanged = (this.prevSortedColumn !== this.sortedColumn);
2583 const isReverseSortChanged = (this.prevReverseSort !== this.reverseSort);
2585 return (rowsChanged || isFilterChanged || isSortedColumnChanged || isReverseSortChanged);
2588 const rowsString = generateRowsSignature(this.rows);
2589 if (!hasRowsChanged(rowsString, this.prevRowsString))
2590 return this.prevFilteredRows;
2592 // sort, then filter
2593 const column = this.columns[this.sortedColumn];
2594 this._sortNodesByColumn(this.getRoot().children, column);
2595 const filteredRows = getFilteredRows();
2597 this.prevFilterTerms = this.filterTerms;
2598 this.prevRowsString = rowsString;
2599 this.prevFilteredRows = filteredRows;
2600 this.prevSortedColumn = this.sortedColumn;
2601 this.prevReverseSort = this.reverseSort;
2602 return filteredRows;
2605 setIgnored: function(rowId, ignore) {
2606 const row = this.rows.get(rowId);
2608 row.full_data.remaining = 0;
2610 row.full_data.remaining = (row.full_data.size * (1.0 - (row.full_data.progress / 100)));
2613 setupTr: function(tr) {
2614 tr.addEventListener("keydown", function(event) {
2615 switch (event.key) {
2617 qBittorrent.PropFiles.collapseFolder(this._this.getSelectedRowId());
2620 qBittorrent.PropFiles.expandFolder(this._this.getSelectedRowId());
2627 const RssFeedTable = new Class({
2628 Extends: DynamicTable,
2629 initColumns: function() {
2630 this.newColumn("state_icon", "", "", 30, true);
2631 this.newColumn("name", "", "QBT_TR(RSS feeds)QBT_TR[CONTEXT=FeedListWidget]", -1, true);
2633 this.columns["state_icon"].dataProperties[0] = "";
2635 // map name row to "[name] ([unread])"
2636 this.columns["name"].dataProperties.push("unread");
2637 this.columns["name"].updateTd = function(td, row) {
2638 const name = this.getRowValue(row, 0);
2639 const unreadCount = this.getRowValue(row, 1);
2640 const value = name + " (" + unreadCount + ")";
2641 td.textContent = value;
2645 setupHeaderMenu: function() {},
2646 setupHeaderEvents: function() {},
2647 getFilteredAndSortedRows: function() {
2648 return this.rows.getValues();
2650 selectRow: function(rowId) {
2651 this.selectedRows.push(rowId);
2653 this.onSelectedRowChanged();
2655 const rows = this.rows.getValues();
2657 for (let i = 0; i < rows.length; ++i) {
2658 if (rows[i].rowId === rowId) {
2659 path = rows[i].full_data.dataPath;
2663 window.qBittorrent.Rss.showRssFeed(path);
2665 setupTr: function(tr) {
2666 tr.addEventListener("dblclick", function(e) {
2667 if (this.rowId !== 0) {
2668 window.qBittorrent.Rss.moveItem(this._this.rows.get(this.rowId).full_data.dataPath);
2673 updateRow: function(tr, fullUpdate) {
2674 const row = this.rows.get(tr.rowId);
2675 const data = row[fullUpdate ? "full_data" : "data"];
2677 const tds = tr.getElements("td");
2678 for (let i = 0; i < this.columns.length; ++i) {
2679 if (Object.hasOwn(data, this.columns[i].dataProperties[0]))
2680 this.columns[i].updateTd(tds[i], row);
2683 tds[0].style.overflow = "visible";
2684 const indentation = row.full_data.indentation;
2685 tds[0].style.paddingLeft = (indentation * 32 + 4) + "px";
2686 tds[1].style.paddingLeft = (indentation * 32 + 4) + "px";
2688 updateIcons: function() {
2690 this.rows.each(row => {
2692 switch (row.full_data.status) {
2694 img_path = "images/application-rss.svg";
2697 img_path = "images/task-reject.svg";
2700 img_path = "images/spinner.gif";
2703 img_path = "images/mail-inbox.svg";
2706 img_path = "images/folder-documents.svg";
2710 for (let i = 0; i < this.tableBody.rows.length; ++i) {
2711 if (this.tableBody.rows[i].rowId === row.rowId) {
2712 td = this.tableBody.rows[i].children[0];
2716 if (td.getChildren("img").length > 0) {
2717 const img = td.getChildren("img")[0];
2718 if (!img.src.includes(img_path)) {
2724 td.adopt(new Element("img", {
2726 "class": "stateIcon",
2733 newColumn: function(name, style, caption, defaultWidth, defaultVisible) {
2735 column["name"] = name;
2736 column["title"] = name;
2737 column["visible"] = defaultVisible;
2738 column["force_hide"] = false;
2739 column["caption"] = caption;
2740 column["style"] = style;
2741 if (defaultWidth !== -1)
2742 column["width"] = defaultWidth;
2744 column["dataProperties"] = [name];
2745 column["getRowValue"] = function(row, pos) {
2746 if (pos === undefined)
2748 return row["full_data"][this.dataProperties[pos]];
2750 column["compareRows"] = function(row1, row2) {
2751 const value1 = this.getRowValue(row1);
2752 const value2 = this.getRowValue(row2);
2753 if ((typeof(value1) === "number") && (typeof(value2) === "number"))
2754 return compareNumbers(value1, value2);
2755 return window.qBittorrent.Misc.naturalSortCollator.compare(value1, value2);
2757 column["updateTd"] = function(td, row) {
2758 const value = this.getRowValue(row);
2759 td.textContent = value;
2762 column["onResize"] = null;
2763 this.columns.push(column);
2764 this.columns[name] = column;
2766 this.hiddenTableHeader.appendChild(new Element("th"));
2767 this.fixedTableHeader.appendChild(new Element("th"));
2771 const RssArticleTable = new Class({
2772 Extends: DynamicTable,
2773 initColumns: function() {
2774 this.newColumn("name", "", "QBT_TR(Torrents: (double-click to download))QBT_TR[CONTEXT=RSSWidget]", -1, true);
2776 setupHeaderMenu: function() {},
2777 setupHeaderEvents: function() {},
2778 getFilteredAndSortedRows: function() {
2779 return this.rows.getValues();
2781 selectRow: function(rowId) {
2782 this.selectedRows.push(rowId);
2784 this.onSelectedRowChanged();
2786 const rows = this.rows.getValues();
2789 for (let i = 0; i < rows.length; ++i) {
2790 if (rows[i].rowId === rowId) {
2791 articleId = rows[i].full_data.dataId;
2792 feedUid = rows[i].full_data.feedUid;
2793 this.tableBody.rows[rows[i].rowId].removeClass("unreadArticle");
2797 window.qBittorrent.Rss.showDetails(feedUid, articleId);
2799 setupTr: function(tr) {
2800 tr.addEventListener("dblclick", function(e) {
2801 showDownloadPage([this._this.rows.get(this.rowId).full_data.torrentURL]);
2804 tr.addClass("torrentsTableContextMenuTarget");
2806 updateRow: function(tr, fullUpdate) {
2807 const row = this.rows.get(tr.rowId);
2808 const data = row[fullUpdate ? "full_data" : "data"];
2809 if (!row.full_data.isRead)
2810 tr.addClass("unreadArticle");
2812 tr.removeClass("unreadArticle");
2814 const tds = tr.getElements("td");
2815 for (let i = 0; i < this.columns.length; ++i) {
2816 if (Object.hasOwn(data, this.columns[i].dataProperties[0]))
2817 this.columns[i].updateTd(tds[i], row);
2821 newColumn: function(name, style, caption, defaultWidth, defaultVisible) {
2823 column["name"] = name;
2824 column["title"] = name;
2825 column["visible"] = defaultVisible;
2826 column["force_hide"] = false;
2827 column["caption"] = caption;
2828 column["style"] = style;
2829 if (defaultWidth !== -1)
2830 column["width"] = defaultWidth;
2832 column["dataProperties"] = [name];
2833 column["getRowValue"] = function(row, pos) {
2834 if (pos === undefined)
2836 return row["full_data"][this.dataProperties[pos]];
2838 column["compareRows"] = function(row1, row2) {
2839 const value1 = this.getRowValue(row1);
2840 const value2 = this.getRowValue(row2);
2841 if ((typeof(value1) === "number") && (typeof(value2) === "number"))
2842 return compareNumbers(value1, value2);
2843 return window.qBittorrent.Misc.naturalSortCollator.compare(value1, value2);
2845 column["updateTd"] = function(td, row) {
2846 const value = this.getRowValue(row);
2847 td.textContent = value;
2850 column["onResize"] = null;
2851 this.columns.push(column);
2852 this.columns[name] = column;
2854 this.hiddenTableHeader.appendChild(new Element("th"));
2855 this.fixedTableHeader.appendChild(new Element("th"));
2859 const RssDownloaderRulesTable = new Class({
2860 Extends: DynamicTable,
2861 initColumns: function() {
2862 this.newColumn("checked", "", "", 30, true);
2863 this.newColumn("name", "", "", -1, true);
2865 this.columns["checked"].updateTd = function(td, row) {
2866 if ($("cbRssDlRule" + row.rowId) === null) {
2867 const checkbox = new Element("input");
2868 checkbox.type = "checkbox";
2869 checkbox.id = "cbRssDlRule" + row.rowId;
2870 checkbox.checked = row.full_data.checked;
2872 checkbox.addEventListener("click", function(e) {
2873 window.qBittorrent.RssDownloader.rssDownloaderRulesTable.updateRowData({
2875 checked: this.checked
2877 window.qBittorrent.RssDownloader.modifyRuleState(row.full_data.name, "enabled", this.checked);
2878 e.stopPropagation();
2881 td.append(checkbox);
2884 $("cbRssDlRule" + row.rowId).checked = row.full_data.checked;
2888 setupHeaderMenu: function() {},
2889 setupHeaderEvents: function() {},
2890 getFilteredAndSortedRows: function() {
2891 return this.rows.getValues();
2893 setupTr: function(tr) {
2894 tr.addEventListener("dblclick", function(e) {
2895 window.qBittorrent.RssDownloader.renameRule(this._this.rows.get(this.rowId).full_data.name);
2899 newColumn: function(name, style, caption, defaultWidth, defaultVisible) {
2901 column["name"] = name;
2902 column["title"] = name;
2903 column["visible"] = defaultVisible;
2904 column["force_hide"] = false;
2905 column["caption"] = caption;
2906 column["style"] = style;
2907 if (defaultWidth !== -1)
2908 column["width"] = defaultWidth;
2910 column["dataProperties"] = [name];
2911 column["getRowValue"] = function(row, pos) {
2912 if (pos === undefined)
2914 return row["full_data"][this.dataProperties[pos]];
2916 column["compareRows"] = function(row1, row2) {
2917 const value1 = this.getRowValue(row1);
2918 const value2 = this.getRowValue(row2);
2919 if ((typeof(value1) === "number") && (typeof(value2) === "number"))
2920 return compareNumbers(value1, value2);
2921 return window.qBittorrent.Misc.naturalSortCollator.compare(value1, value2);
2923 column["updateTd"] = function(td, row) {
2924 const value = this.getRowValue(row);
2925 td.textContent = value;
2928 column["onResize"] = null;
2929 this.columns.push(column);
2930 this.columns[name] = column;
2932 this.hiddenTableHeader.appendChild(new Element("th"));
2933 this.fixedTableHeader.appendChild(new Element("th"));
2935 selectRow: function(rowId) {
2936 this.selectedRows.push(rowId);
2938 this.onSelectedRowChanged();
2940 const rows = this.rows.getValues();
2942 for (let i = 0; i < rows.length; ++i) {
2943 if (rows[i].rowId === rowId) {
2944 name = rows[i].full_data.name;
2948 window.qBittorrent.RssDownloader.showRule(name);
2952 const RssDownloaderFeedSelectionTable = new Class({
2953 Extends: DynamicTable,
2954 initColumns: function() {
2955 this.newColumn("checked", "", "", 30, true);
2956 this.newColumn("name", "", "", -1, true);
2958 this.columns["checked"].updateTd = function(td, row) {
2959 if ($("cbRssDlFeed" + row.rowId) === null) {
2960 const checkbox = new Element("input");
2961 checkbox.type = "checkbox";
2962 checkbox.id = "cbRssDlFeed" + row.rowId;
2963 checkbox.checked = row.full_data.checked;
2965 checkbox.addEventListener("click", function(e) {
2966 window.qBittorrent.RssDownloader.rssDownloaderFeedSelectionTable.updateRowData({
2968 checked: this.checked
2970 e.stopPropagation();
2973 td.append(checkbox);
2976 $("cbRssDlFeed" + row.rowId).checked = row.full_data.checked;
2980 setupHeaderMenu: function() {},
2981 setupHeaderEvents: function() {},
2982 getFilteredAndSortedRows: function() {
2983 return this.rows.getValues();
2985 newColumn: function(name, style, caption, defaultWidth, defaultVisible) {
2987 column["name"] = name;
2988 column["title"] = name;
2989 column["visible"] = defaultVisible;
2990 column["force_hide"] = false;
2991 column["caption"] = caption;
2992 column["style"] = style;
2993 if (defaultWidth !== -1)
2994 column["width"] = defaultWidth;
2996 column["dataProperties"] = [name];
2997 column["getRowValue"] = function(row, pos) {
2998 if (pos === undefined)
3000 return row["full_data"][this.dataProperties[pos]];
3002 column["compareRows"] = function(row1, row2) {
3003 const value1 = this.getRowValue(row1);
3004 const value2 = this.getRowValue(row2);
3005 if ((typeof(value1) === "number") && (typeof(value2) === "number"))
3006 return compareNumbers(value1, value2);
3007 return window.qBittorrent.Misc.naturalSortCollator.compare(value1, value2);
3009 column["updateTd"] = function(td, row) {
3010 const value = this.getRowValue(row);
3011 td.textContent = value;
3014 column["onResize"] = null;
3015 this.columns.push(column);
3016 this.columns[name] = column;
3018 this.hiddenTableHeader.appendChild(new Element("th"));
3019 this.fixedTableHeader.appendChild(new Element("th"));
3021 selectRow: function() {}
3024 const RssDownloaderArticlesTable = new Class({
3025 Extends: DynamicTable,
3026 initColumns: function() {
3027 this.newColumn("name", "", "", -1, true);
3029 setupHeaderMenu: function() {},
3030 setupHeaderEvents: function() {},
3031 getFilteredAndSortedRows: function() {
3032 return this.rows.getValues();
3034 newColumn: function(name, style, caption, defaultWidth, defaultVisible) {
3036 column["name"] = name;
3037 column["title"] = name;
3038 column["visible"] = defaultVisible;
3039 column["force_hide"] = false;
3040 column["caption"] = caption;
3041 column["style"] = style;
3042 if (defaultWidth !== -1)
3043 column["width"] = defaultWidth;
3045 column["dataProperties"] = [name];
3046 column["getRowValue"] = function(row, pos) {
3047 if (pos === undefined)
3049 return row["full_data"][this.dataProperties[pos]];
3051 column["compareRows"] = function(row1, row2) {
3052 const value1 = this.getRowValue(row1);
3053 const value2 = this.getRowValue(row2);
3054 if ((typeof(value1) === "number") && (typeof(value2) === "number"))
3055 return compareNumbers(value1, value2);
3056 return window.qBittorrent.Misc.naturalSortCollator.compare(value1, value2);
3058 column["updateTd"] = function(td, row) {
3059 const value = this.getRowValue(row);
3060 td.textContent = value;
3063 column["onResize"] = null;
3064 this.columns.push(column);
3065 this.columns[name] = column;
3067 this.hiddenTableHeader.appendChild(new Element("th"));
3068 this.fixedTableHeader.appendChild(new Element("th"));
3070 selectRow: function() {},
3071 updateRow: function(tr, fullUpdate) {
3072 const row = this.rows.get(tr.rowId);
3073 const data = row[fullUpdate ? "full_data" : "data"];
3075 if (row.full_data.isFeed) {
3076 tr.addClass("articleTableFeed");
3077 tr.removeClass("articleTableArticle");
3080 tr.removeClass("articleTableFeed");
3081 tr.addClass("articleTableArticle");
3084 const tds = tr.getElements("td");
3085 for (let i = 0; i < this.columns.length; ++i) {
3086 if (Object.hasOwn(data, this.columns[i].dataProperties[0]))
3087 this.columns[i].updateTd(tds[i], row);
3093 const LogMessageTable = new Class({
3094 Extends: DynamicTable,
3098 filteredLength: function() {
3099 return this.tableBody.getElements("tr").length;
3102 initColumns: function() {
3103 this.newColumn("rowId", "", "QBT_TR(ID)QBT_TR[CONTEXT=ExecutionLogWidget]", 50, true);
3104 this.newColumn("message", "", "QBT_TR(Message)QBT_TR[CONTEXT=ExecutionLogWidget]", 350, true);
3105 this.newColumn("timestamp", "", "QBT_TR(Timestamp)QBT_TR[CONTEXT=ExecutionLogWidget]", 150, true);
3106 this.newColumn("type", "", "QBT_TR(Log Type)QBT_TR[CONTEXT=ExecutionLogWidget]", 100, true);
3107 this.initColumnsFunctions();
3110 initColumnsFunctions: function() {
3111 this.columns["timestamp"].updateTd = function(td, row) {
3112 const date = new Date(this.getRowValue(row) * 1000).toLocaleString();
3113 td.set({ "text": date, "title": date });
3116 this.columns["type"].updateTd = function(td, row) {
3117 // Type of the message: Log::NORMAL: 1, Log::INFO: 2, Log::WARNING: 4, Log::CRITICAL: 8
3118 let logLevel, addClass;
3119 switch (this.getRowValue(row).toInt()) {
3121 logLevel = "QBT_TR(Normal)QBT_TR[CONTEXT=ExecutionLogWidget]";
3122 addClass = "logNormal";
3125 logLevel = "QBT_TR(Info)QBT_TR[CONTEXT=ExecutionLogWidget]";
3126 addClass = "logInfo";
3129 logLevel = "QBT_TR(Warning)QBT_TR[CONTEXT=ExecutionLogWidget]";
3130 addClass = "logWarning";
3133 logLevel = "QBT_TR(Critical)QBT_TR[CONTEXT=ExecutionLogWidget]";
3134 addClass = "logCritical";
3137 logLevel = "QBT_TR(Unknown)QBT_TR[CONTEXT=ExecutionLogWidget]";
3138 addClass = "logUnknown";
3141 td.set({ "text": logLevel, "title": logLevel });
3142 td.getParent("tr").className = `logTableRow${addClass}`;
3146 getFilteredAndSortedRows: function() {
3147 let filteredRows = [];
3148 const rows = this.rows.getValues();
3149 this.filterText = window.qBittorrent.Log.getFilterText();
3150 const filterTerms = (this.filterText.length > 0) ? this.filterText.toLowerCase().split(" ") : [];
3151 const logLevels = window.qBittorrent.Log.getSelectedLevels();
3152 if ((filterTerms.length > 0) || (logLevels.length < 4)) {
3153 for (let i = 0; i < rows.length; ++i) {
3154 if (!logLevels.includes(rows[i].full_data.type.toString()))
3157 if ((filterTerms.length > 0) && !window.qBittorrent.Misc.containsAllTerms(rows[i].full_data.message, filterTerms))
3160 filteredRows.push(rows[i]);
3164 filteredRows = rows;
3167 filteredRows.sort((row1, row2) => {
3168 const column = this.columns[this.sortedColumn];
3169 const res = column.compareRows(row1, row2);
3170 return (this.reverseSort === "0") ? res : -res;
3173 return filteredRows;
3176 setupCommonEvents: function() {},
3178 setupTr: function(tr) {
3179 tr.addClass("logTableRow");
3183 const LogPeerTable = new Class({
3184 Extends: LogMessageTable,
3186 initColumns: function() {
3187 this.newColumn("rowId", "", "QBT_TR(ID)QBT_TR[CONTEXT=ExecutionLogWidget]", 50, true);
3188 this.newColumn("ip", "", "QBT_TR(IP)QBT_TR[CONTEXT=ExecutionLogWidget]", 150, true);
3189 this.newColumn("timestamp", "", "QBT_TR(Timestamp)QBT_TR[CONTEXT=ExecutionLogWidget]", 150, true);
3190 this.newColumn("blocked", "", "QBT_TR(Status)QBT_TR[CONTEXT=ExecutionLogWidget]", 150, true);
3191 this.newColumn("reason", "", "QBT_TR(Reason)QBT_TR[CONTEXT=ExecutionLogWidget]", 150, true);
3193 this.columns["timestamp"].updateTd = function(td, row) {
3194 const date = new Date(this.getRowValue(row) * 1000).toLocaleString();
3195 td.set({ "text": date, "title": date });
3198 this.columns["blocked"].updateTd = function(td, row) {
3199 let status, addClass;
3200 if (this.getRowValue(row)) {
3201 status = "QBT_TR(Blocked)QBT_TR[CONTEXT=ExecutionLogWidget]";
3202 addClass = "peerBlocked";
3205 status = "QBT_TR(Banned)QBT_TR[CONTEXT=ExecutionLogWidget]";
3206 addClass = "peerBanned";
3208 td.set({ "text": status, "title": status });
3209 td.getParent("tr").className = `logTableRow${addClass}`;
3213 getFilteredAndSortedRows: function() {
3214 let filteredRows = [];
3215 const rows = this.rows.getValues();
3216 this.filterText = window.qBittorrent.Log.getFilterText();
3217 const filterTerms = (this.filterText.length > 0) ? this.filterText.toLowerCase().split(" ") : [];
3218 if (filterTerms.length > 0) {
3219 for (let i = 0; i < rows.length; ++i) {
3220 if ((filterTerms.length > 0) && !window.qBittorrent.Misc.containsAllTerms(rows[i].full_data.ip, filterTerms))
3223 filteredRows.push(rows[i]);
3227 filteredRows = rows;
3230 filteredRows.sort((row1, row2) => {
3231 const column = this.columns[this.sortedColumn];
3232 const res = column.compareRows(row1, row2);
3233 return (this.reverseSort === "0") ? res : -res;
3236 return filteredRows;
3242 Object.freeze(window.qBittorrent.DynamicTable);
3244 /*************************************************************/