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