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 setup: function(dynamicTableDivId, dynamicTableFixedHeaderDivId, contextMenu) {
902 this.parent(dynamicTableDivId, dynamicTableFixedHeaderDivId, contextMenu);
903 this.rows = new Map();
906 initColumns: function() {
907 this.newColumn("priority", "", "#", 30, true);
908 this.newColumn("state_icon", "cursor: default", "", 22, true);
909 this.newColumn("name", "", "QBT_TR(Name)QBT_TR[CONTEXT=TransferListModel]", 200, true);
910 this.newColumn("size", "", "QBT_TR(Size)QBT_TR[CONTEXT=TransferListModel]", 100, true);
911 this.newColumn("total_size", "", "QBT_TR(Total Size)QBT_TR[CONTEXT=TransferListModel]", 100, false);
912 this.newColumn("progress", "", "QBT_TR(Progress)QBT_TR[CONTEXT=TransferListModel]", 85, true);
913 this.newColumn("status", "", "QBT_TR(Status)QBT_TR[CONTEXT=TransferListModel]", 100, true);
914 this.newColumn("num_seeds", "", "QBT_TR(Seeds)QBT_TR[CONTEXT=TransferListModel]", 100, true);
915 this.newColumn("num_leechs", "", "QBT_TR(Peers)QBT_TR[CONTEXT=TransferListModel]", 100, true);
916 this.newColumn("dlspeed", "", "QBT_TR(Down Speed)QBT_TR[CONTEXT=TransferListModel]", 100, true);
917 this.newColumn("upspeed", "", "QBT_TR(Up Speed)QBT_TR[CONTEXT=TransferListModel]", 100, true);
918 this.newColumn("eta", "", "QBT_TR(ETA)QBT_TR[CONTEXT=TransferListModel]", 100, true);
919 this.newColumn("ratio", "", "QBT_TR(Ratio)QBT_TR[CONTEXT=TransferListModel]", 100, true);
920 this.newColumn("popularity", "", "QBT_TR(Popularity)QBT_TR[CONTEXT=TransferListModel]", 100, true);
921 this.newColumn("category", "", "QBT_TR(Category)QBT_TR[CONTEXT=TransferListModel]", 100, true);
922 this.newColumn("tags", "", "QBT_TR(Tags)QBT_TR[CONTEXT=TransferListModel]", 100, true);
923 this.newColumn("added_on", "", "QBT_TR(Added On)QBT_TR[CONTEXT=TransferListModel]", 100, true);
924 this.newColumn("completion_on", "", "QBT_TR(Completed On)QBT_TR[CONTEXT=TransferListModel]", 100, false);
925 this.newColumn("tracker", "", "QBT_TR(Tracker)QBT_TR[CONTEXT=TransferListModel]", 100, false);
926 this.newColumn("dl_limit", "", "QBT_TR(Down Limit)QBT_TR[CONTEXT=TransferListModel]", 100, false);
927 this.newColumn("up_limit", "", "QBT_TR(Up Limit)QBT_TR[CONTEXT=TransferListModel]", 100, false);
928 this.newColumn("downloaded", "", "QBT_TR(Downloaded)QBT_TR[CONTEXT=TransferListModel]", 100, false);
929 this.newColumn("uploaded", "", "QBT_TR(Uploaded)QBT_TR[CONTEXT=TransferListModel]", 100, false);
930 this.newColumn("downloaded_session", "", "QBT_TR(Session Download)QBT_TR[CONTEXT=TransferListModel]", 100, false);
931 this.newColumn("uploaded_session", "", "QBT_TR(Session Upload)QBT_TR[CONTEXT=TransferListModel]", 100, false);
932 this.newColumn("amount_left", "", "QBT_TR(Remaining)QBT_TR[CONTEXT=TransferListModel]", 100, false);
933 this.newColumn("time_active", "", "QBT_TR(Time Active)QBT_TR[CONTEXT=TransferListModel]", 100, false);
934 this.newColumn("save_path", "", "QBT_TR(Save path)QBT_TR[CONTEXT=TransferListModel]", 100, false);
935 this.newColumn("completed", "", "QBT_TR(Completed)QBT_TR[CONTEXT=TransferListModel]", 100, false);
936 this.newColumn("max_ratio", "", "QBT_TR(Ratio Limit)QBT_TR[CONTEXT=TransferListModel]", 100, false);
937 this.newColumn("seen_complete", "", "QBT_TR(Last Seen Complete)QBT_TR[CONTEXT=TransferListModel]", 100, false);
938 this.newColumn("last_activity", "", "QBT_TR(Last Activity)QBT_TR[CONTEXT=TransferListModel]", 100, false);
939 this.newColumn("availability", "", "QBT_TR(Availability)QBT_TR[CONTEXT=TransferListModel]", 100, false);
940 this.newColumn("download_path", "", "QBT_TR(Incomplete Save Path)QBT_TR[CONTEXT=TransferListModel]", 100, false);
941 this.newColumn("infohash_v1", "", "QBT_TR(Info Hash v1)QBT_TR[CONTEXT=TransferListModel]", 100, false);
942 this.newColumn("infohash_v2", "", "QBT_TR(Info Hash v2)QBT_TR[CONTEXT=TransferListModel]", 100, false);
943 this.newColumn("reannounce", "", "QBT_TR(Reannounce In)QBT_TR[CONTEXT=TransferListModel]", 100, false);
944 this.newColumn("private", "", "QBT_TR(Private)QBT_TR[CONTEXT=TransferListModel]", 100, false);
946 this.columns["state_icon"].onclick = "";
947 this.columns["state_icon"].dataProperties[0] = "state";
949 this.columns["num_seeds"].dataProperties.push("num_complete");
950 this.columns["num_leechs"].dataProperties.push("num_incomplete");
951 this.columns["time_active"].dataProperties.push("seeding_time");
953 this.initColumnsFunctions();
956 initColumnsFunctions: function() {
959 this.columns["state_icon"].updateTd = function(td, row) {
960 let state = this.getRowValue(row);
968 state = "downloading";
969 img_path = "images/downloading.svg";
974 img_path = "images/upload.svg";
978 img_path = "images/stalledUP.svg";
982 img_path = "images/stalledDL.svg";
985 state = "torrent-stop";
986 img_path = "images/stopped.svg";
989 state = "checked-completed";
990 img_path = "images/checked-completed.svg";
995 img_path = "images/queued.svg";
999 case "queuedForChecking":
1000 case "checkingResumeData":
1001 state = "force-recheck";
1002 img_path = "images/force-recheck.svg";
1006 img_path = "images/set-location.svg";
1010 case "missingFiles":
1012 img_path = "images/error.svg";
1015 break; // do nothing
1018 if (td.getChildren("img").length > 0) {
1019 const img = td.getChildren("img")[0];
1020 if (!img.src.includes(img_path)) {
1026 td.adopt(new Element("img", {
1028 "class": "stateIcon",
1035 this.columns["status"].updateTd = function(td, row) {
1036 const state = this.getRowValue(row);
1043 status = "QBT_TR(Downloading)QBT_TR[CONTEXT=TransferListDelegate]";
1046 status = "QBT_TR(Stalled)QBT_TR[CONTEXT=TransferListDelegate]";
1049 status = "QBT_TR(Downloading metadata)QBT_TR[CONTEXT=TransferListDelegate]";
1051 case "forcedMetaDL":
1052 status = "QBT_TR([F] Downloading metadata)QBT_TR[CONTEXT=TransferListDelegate]";
1055 status = "QBT_TR([F] Downloading)QBT_TR[CONTEXT=TransferListDelegate]";
1059 status = "QBT_TR(Seeding)QBT_TR[CONTEXT=TransferListDelegate]";
1062 status = "QBT_TR([F] Seeding)QBT_TR[CONTEXT=TransferListDelegate]";
1066 status = "QBT_TR(Queued)QBT_TR[CONTEXT=TransferListDelegate]";
1070 status = "QBT_TR(Checking)QBT_TR[CONTEXT=TransferListDelegate]";
1072 case "queuedForChecking":
1073 status = "QBT_TR(Queued for checking)QBT_TR[CONTEXT=TransferListDelegate]";
1075 case "checkingResumeData":
1076 status = "QBT_TR(Checking resume data)QBT_TR[CONTEXT=TransferListDelegate]";
1079 status = "QBT_TR(Stopped)QBT_TR[CONTEXT=TransferListDelegate]";
1082 status = "QBT_TR(Completed)QBT_TR[CONTEXT=TransferListDelegate]";
1085 status = "QBT_TR(Moving)QBT_TR[CONTEXT=TransferListDelegate]";
1087 case "missingFiles":
1088 status = "QBT_TR(Missing Files)QBT_TR[CONTEXT=TransferListDelegate]";
1091 status = "QBT_TR(Errored)QBT_TR[CONTEXT=TransferListDelegate]";
1094 status = "QBT_TR(Unknown)QBT_TR[CONTEXT=HttpServer]";
1097 td.textContent = status;
1102 this.columns["priority"].updateTd = function(td, row) {
1103 const queuePos = this.getRowValue(row);
1104 const formattedQueuePos = (queuePos < 1) ? "*" : queuePos;
1105 td.textContent = formattedQueuePos;
1106 td.title = formattedQueuePos;
1109 this.columns["priority"].compareRows = function(row1, row2) {
1110 let row1_val = this.getRowValue(row1);
1111 let row2_val = this.getRowValue(row2);
1116 return compareNumbers(row1_val, row2_val);
1119 // name, category, tags
1120 this.columns["name"].compareRows = function(row1, row2) {
1121 const row1Val = this.getRowValue(row1);
1122 const row2Val = this.getRowValue(row2);
1123 return row1Val.localeCompare(row2Val, undefined, { numeric: true, sensitivity: "base" });
1125 this.columns["category"].compareRows = this.columns["name"].compareRows;
1126 this.columns["tags"].compareRows = this.columns["name"].compareRows;
1129 this.columns["size"].updateTd = function(td, row) {
1130 const size = window.qBittorrent.Misc.friendlyUnit(this.getRowValue(row), false);
1131 td.textContent = size;
1134 this.columns["total_size"].updateTd = this.columns["size"].updateTd;
1137 this.columns["progress"].updateTd = function(td, row) {
1138 const progress = this.getRowValue(row);
1139 let progressFormatted = (progress * 100).round(1);
1140 if ((progressFormatted === 100.0) && (progress !== 1.0))
1141 progressFormatted = 99.9;
1143 if (td.getChildren("div").length > 0) {
1144 const div = td.getChildren("div")[0];
1147 div.setWidth(ProgressColumnWidth - 5);
1149 if (div.getValue() !== progressFormatted)
1150 div.setValue(progressFormatted);
1153 if (ProgressColumnWidth < 0)
1154 ProgressColumnWidth = td.offsetWidth;
1155 td.adopt(new window.qBittorrent.ProgressBar.ProgressBar(progressFormatted.toFloat(), {
1156 "width": ProgressColumnWidth - 5
1162 this.columns["progress"].onResize = function(columnName) {
1163 const pos = this.getColumnPos(columnName);
1164 const trs = this.tableBody.getElements("tr");
1165 ProgressColumnWidth = -1;
1166 for (let i = 0; i < trs.length; ++i) {
1167 const td = trs[i].getElements("td")[pos];
1168 if (ProgressColumnWidth < 0)
1169 ProgressColumnWidth = td.offsetWidth;
1171 this.columns[columnName].updateTd(td, this.rows.get(trs[i].rowId));
1176 this.columns["num_seeds"].updateTd = function(td, row) {
1177 const num_seeds = this.getRowValue(row, 0);
1178 const num_complete = this.getRowValue(row, 1);
1179 let value = num_seeds;
1180 if (num_complete !== -1)
1181 value += " (" + num_complete + ")";
1182 td.textContent = value;
1185 this.columns["num_seeds"].compareRows = function(row1, row2) {
1186 const num_seeds1 = this.getRowValue(row1, 0);
1187 const num_complete1 = this.getRowValue(row1, 1);
1189 const num_seeds2 = this.getRowValue(row2, 0);
1190 const num_complete2 = this.getRowValue(row2, 1);
1192 const result = compareNumbers(num_complete1, num_complete2);
1195 return compareNumbers(num_seeds1, num_seeds2);
1199 this.columns["num_leechs"].updateTd = this.columns["num_seeds"].updateTd;
1200 this.columns["num_leechs"].compareRows = this.columns["num_seeds"].compareRows;
1203 this.columns["dlspeed"].updateTd = function(td, row) {
1204 const speed = window.qBittorrent.Misc.friendlyUnit(this.getRowValue(row), true);
1205 td.textContent = speed;
1210 this.columns["upspeed"].updateTd = this.columns["dlspeed"].updateTd;
1213 this.columns["eta"].updateTd = function(td, row) {
1214 const eta = window.qBittorrent.Misc.friendlyDuration(this.getRowValue(row), window.qBittorrent.Misc.MAX_ETA);
1215 td.textContent = eta;
1220 this.columns["ratio"].updateTd = function(td, row) {
1221 const ratio = this.getRowValue(row);
1222 const string = (ratio === -1) ? "∞" : window.qBittorrent.Misc.toFixedPointString(ratio, 2);
1223 td.textContent = string;
1228 this.columns["popularity"].updateTd = function(td, row) {
1229 const value = this.getRowValue(row);
1230 const popularity = (value === -1) ? "∞" : window.qBittorrent.Misc.toFixedPointString(value, 2);
1231 td.textContent = popularity;
1232 td.title = popularity;
1236 this.columns["added_on"].updateTd = function(td, row) {
1237 const date = new Date(this.getRowValue(row) * 1000).toLocaleString();
1238 td.textContent = date;
1243 this.columns["completion_on"].updateTd = function(td, row) {
1244 const val = this.getRowValue(row);
1245 if ((val === 0xffffffff) || (val < 0)) {
1246 td.textContent = "";
1250 const date = new Date(this.getRowValue(row) * 1000).toLocaleString();
1251 td.textContent = date;
1257 this.columns["tracker"].updateTd = function(td, row) {
1258 const value = this.getRowValue(row);
1259 const tracker = displayFullURLTrackerColumn ? value : window.qBittorrent.Misc.getHost(value);
1260 td.textContent = tracker;
1264 // dl_limit, up_limit
1265 this.columns["dl_limit"].updateTd = function(td, row) {
1266 const speed = this.getRowValue(row);
1268 td.textContent = "∞";
1272 const formattedSpeed = window.qBittorrent.Misc.friendlyUnit(speed, true);
1273 td.textContent = formattedSpeed;
1274 td.title = formattedSpeed;
1278 this.columns["up_limit"].updateTd = this.columns["dl_limit"].updateTd;
1280 // downloaded, uploaded, downloaded_session, uploaded_session, amount_left
1281 this.columns["downloaded"].updateTd = this.columns["size"].updateTd;
1282 this.columns["uploaded"].updateTd = this.columns["size"].updateTd;
1283 this.columns["downloaded_session"].updateTd = this.columns["size"].updateTd;
1284 this.columns["uploaded_session"].updateTd = this.columns["size"].updateTd;
1285 this.columns["amount_left"].updateTd = this.columns["size"].updateTd;
1288 this.columns["time_active"].updateTd = function(td, row) {
1289 const activeTime = this.getRowValue(row, 0);
1290 const seedingTime = this.getRowValue(row, 1);
1291 const time = (seedingTime > 0)
1292 ? ("QBT_TR(%1 (seeded for %2))QBT_TR[CONTEXT=TransferListDelegate]"
1293 .replace("%1", window.qBittorrent.Misc.friendlyDuration(activeTime))
1294 .replace("%2", window.qBittorrent.Misc.friendlyDuration(seedingTime)))
1295 : window.qBittorrent.Misc.friendlyDuration(activeTime);
1296 td.textContent = time;
1301 this.columns["completed"].updateTd = this.columns["size"].updateTd;
1304 this.columns["max_ratio"].updateTd = this.columns["ratio"].updateTd;
1307 this.columns["seen_complete"].updateTd = this.columns["completion_on"].updateTd;
1310 this.columns["last_activity"].updateTd = function(td, row) {
1311 const val = this.getRowValue(row);
1313 td.textContent = "∞";
1317 const formattedVal = "QBT_TR(%1 ago)QBT_TR[CONTEXT=TransferListDelegate]".replace("%1", window.qBittorrent.Misc.friendlyDuration((new Date() / 1000) - val));
1318 td.textContent = formattedVal;
1319 td.title = formattedVal;
1324 this.columns["availability"].updateTd = function(td, row) {
1325 const value = window.qBittorrent.Misc.toFixedPointString(this.getRowValue(row), 3);
1326 td.textContent = value;
1331 this.columns["infohash_v1"].updateTd = function(td, row) {
1332 const sourceInfohashV1 = this.getRowValue(row);
1333 const infohashV1 = (sourceInfohashV1 !== "") ? sourceInfohashV1 : "QBT_TR(N/A)QBT_TR[CONTEXT=TransferListDelegate]";
1334 td.textContent = infohashV1;
1335 td.title = infohashV1;
1339 this.columns["infohash_v2"].updateTd = function(td, row) {
1340 const sourceInfohashV2 = this.getRowValue(row);
1341 const infohashV2 = (sourceInfohashV2 !== "") ? sourceInfohashV2 : "QBT_TR(N/A)QBT_TR[CONTEXT=TransferListDelegate]";
1342 td.textContent = infohashV2;
1343 td.title = infohashV2;
1347 this.columns["reannounce"].updateTd = function(td, row) {
1348 const time = window.qBittorrent.Misc.friendlyDuration(this.getRowValue(row));
1349 td.textContent = time;
1354 this.columns["private"].updateTd = function(td, row) {
1355 const hasMetadata = row["full_data"].has_metadata;
1356 const isPrivate = this.getRowValue(row);
1357 const string = hasMetadata
1359 ? "QBT_TR(Yes)QBT_TR[CONTEXT=PropertiesWidget]"
1360 : "QBT_TR(No)QBT_TR[CONTEXT=PropertiesWidget]")
1361 : "QBT_TR(N/A)QBT_TR[CONTEXT=PropertiesWidget]";
1362 td.textContent = string;
1367 applyFilter: function(row, filterName, categoryHash, tagHash, trackerHash, filterTerms) {
1368 const state = row["full_data"].state;
1369 let inactive = false;
1371 switch (filterName) {
1373 if ((state !== "downloading") && !state.includes("DL"))
1377 if ((state !== "uploading") && (state !== "forcedUP") && (state !== "stalledUP") && (state !== "queuedUP") && (state !== "checkingUP"))
1381 if ((state !== "uploading") && !state.includes("UP"))
1385 if (!state.includes("stopped"))
1389 if (state.includes("stopped"))
1393 if ((state !== "stalledUP") && (state !== "stalledDL"))
1396 case "stalled_uploading":
1397 if (state !== "stalledUP")
1400 case "stalled_downloading":
1401 if (state !== "stalledDL")
1409 if (state === "stalledDL")
1410 r = (row["full_data"].upspeed > 0);
1412 r = (state === "metaDL") || (state === "forcedMetaDL") || (state === "downloading") || (state === "forcedDL") || (state === "uploading") || (state === "forcedUP");
1418 if ((state !== "checkingUP") && (state !== "checkingDL") && (state !== "checkingResumeData"))
1422 if (state !== "moving")
1426 if ((state !== "error") && (state !== "unknown") && (state !== "missingFiles"))
1431 switch (categoryHash) {
1432 case CATEGORIES_ALL:
1433 break; // do nothing
1434 case CATEGORIES_UNCATEGORIZED:
1435 if (row["full_data"].category.length !== 0)
1437 break; // do nothing
1439 if (!useSubcategories) {
1440 if (categoryHash !== window.qBittorrent.Misc.genHash(row["full_data"].category))
1444 const selectedCategory = category_list.get(categoryHash);
1445 if (selectedCategory !== undefined) {
1446 const selectedCategoryName = selectedCategory.name + "/";
1447 const torrentCategoryName = row["full_data"].category + "/";
1448 if (!torrentCategoryName.startsWith(selectedCategoryName))
1457 break; // do nothing
1460 if (row["full_data"].tags.length !== 0)
1462 break; // do nothing
1465 const tagHashes = row["full_data"].tags.split(", ").map(tag => window.qBittorrent.Misc.genHash(tag));
1466 if (!tagHashes.contains(tagHash))
1472 switch (trackerHash) {
1474 break; // do nothing
1475 case TRACKERS_TRACKERLESS:
1476 if (row["full_data"].trackers_count !== 0)
1480 const tracker = trackerList.get(trackerHash);
1483 for (const torrents of tracker.trackerTorrentMap.values()) {
1484 if (torrents.includes(row["full_data"].rowId)) {
1496 if ((filterTerms !== undefined) && (filterTerms !== null)) {
1497 const filterBy = document.getElementById("torrentsFilterSelect").value;
1498 const textToSearch = row["full_data"][filterBy].toLowerCase();
1499 if (filterTerms instanceof RegExp) {
1500 if (!filterTerms.test(textToSearch))
1504 if ((filterTerms.length > 0) && !window.qBittorrent.Misc.containsAllTerms(textToSearch, filterTerms))
1512 removeRow: function(rowId) {
1513 this.selectedRows.erase(rowId);
1514 this.rows.delete(rowId);
1515 const tr = this.getTrByRowId(rowId);
1522 const trs = this.tableBody.getElements("tr");
1523 while (trs.length > 0)
1524 trs.pop().destroy();
1527 getRowIds: function() {
1528 return this.rows.keys();
1531 getFilteredTorrentsNumber: function(filterName, categoryHash, tagHash, trackerHash) {
1534 for (const row of this.rows.values()) {
1535 if (this.applyFilter(row, filterName, categoryHash, tagHash, trackerHash, null))
1541 getFilteredTorrentsHashes: function(filterName, categoryHash, tagHash, trackerHash) {
1542 const rowsHashes = [];
1544 for (const row of this.rows.values()) {
1545 if (this.applyFilter(row, filterName, categoryHash, tagHash, trackerHash, null))
1546 rowsHashes.push(row["rowId"]);
1552 getFilteredAndSortedRows: function() {
1553 const filteredRows = [];
1555 const useRegex = $("torrentsFilterRegexBox").checked;
1556 const filterText = $("torrentsFilterInput").value.trim().toLowerCase();
1559 filterTerms = (filterText.length > 0)
1560 ? (useRegex ? new RegExp(filterText) : filterText.split(" "))
1563 catch (e) { // SyntaxError: Invalid regex pattern
1564 return filteredRows;
1567 for (const row of this.rows.values()) {
1568 if (this.applyFilter(row, selectedStatus, selectedCategory, selectedTag, selectedTracker, filterTerms)) {
1569 filteredRows.push(row);
1570 filteredRows[row.rowId] = row;
1574 filteredRows.sort((row1, row2) => {
1575 const column = this.columns[this.sortedColumn];
1576 const res = column.compareRows(row1, row2);
1577 if (this.reverseSort === "0")
1582 return filteredRows;
1585 setupTr: function(tr) {
1586 tr.addEventListener("dblclick", function(e) {
1588 e.stopPropagation();
1590 this._this.deselectAll();
1591 this._this.selectRow(this.rowId);
1592 const row = this._this.rows.get(this.rowId);
1593 const state = row["full_data"].state;
1596 (state !== "uploading")
1597 && (state !== "stoppedUP")
1598 && (state !== "forcedUP")
1599 && (state !== "stalledUP")
1600 && (state !== "queuedUP")
1601 && (state !== "checkingUP")
1602 ? "dblclick_download"
1603 : "dblclick_complete";
1605 if (LocalPreferences.get(prefKey, "1") !== "1")
1608 if (state.includes("stopped"))
1614 tr.addClass("torrentsTableContextMenuTarget");
1617 getCurrentTorrentID: function() {
1618 return this.getSelectedRowId();
1621 onSelectedRowChanged: function() {
1622 updatePropertiesPanel();
1626 const TorrentPeersTable = new Class({
1627 Extends: DynamicTable,
1629 initColumns: function() {
1630 this.newColumn("country", "", "QBT_TR(Country/Region)QBT_TR[CONTEXT=PeerListWidget]", 22, true);
1631 this.newColumn("ip", "", "QBT_TR(IP)QBT_TR[CONTEXT=PeerListWidget]", 80, true);
1632 this.newColumn("port", "", "QBT_TR(Port)QBT_TR[CONTEXT=PeerListWidget]", 35, true);
1633 this.newColumn("connection", "", "QBT_TR(Connection)QBT_TR[CONTEXT=PeerListWidget]", 50, true);
1634 this.newColumn("flags", "", "QBT_TR(Flags)QBT_TR[CONTEXT=PeerListWidget]", 50, true);
1635 this.newColumn("client", "", "QBT_TR(Client)QBT_TR[CONTEXT=PeerListWidget]", 140, true);
1636 this.newColumn("peer_id_client", "", "QBT_TR(Peer ID Client)QBT_TR[CONTEXT=PeerListWidget]", 60, false);
1637 this.newColumn("progress", "", "QBT_TR(Progress)QBT_TR[CONTEXT=PeerListWidget]", 50, true);
1638 this.newColumn("dl_speed", "", "QBT_TR(Down Speed)QBT_TR[CONTEXT=PeerListWidget]", 50, true);
1639 this.newColumn("up_speed", "", "QBT_TR(Up Speed)QBT_TR[CONTEXT=PeerListWidget]", 50, true);
1640 this.newColumn("downloaded", "", "QBT_TR(Downloaded)QBT_TR[CONTEXT=PeerListWidget]", 50, true);
1641 this.newColumn("uploaded", "", "QBT_TR(Uploaded)QBT_TR[CONTEXT=PeerListWidget]", 50, true);
1642 this.newColumn("relevance", "", "QBT_TR(Relevance)QBT_TR[CONTEXT=PeerListWidget]", 30, true);
1643 this.newColumn("files", "", "QBT_TR(Files)QBT_TR[CONTEXT=PeerListWidget]", 100, true);
1645 this.columns["country"].dataProperties.push("country_code");
1646 this.columns["flags"].dataProperties.push("flags_desc");
1647 this.initColumnsFunctions();
1650 initColumnsFunctions: function() {
1653 this.columns["country"].updateTd = function(td, row) {
1654 const country = this.getRowValue(row, 0);
1655 const country_code = this.getRowValue(row, 1);
1657 let span = td.firstElementChild;
1658 if (span === null) {
1659 span = document.createElement("span");
1660 span.classList.add("flags");
1664 span.style.backgroundImage = `url('images/flags/${country_code ?? "xx"}.svg')`;
1665 span.textContent = country;
1670 this.columns["ip"].compareRows = function(row1, row2) {
1671 const ip1 = this.getRowValue(row1);
1672 const ip2 = this.getRowValue(row2);
1674 const a = ip1.split(".");
1675 const b = ip2.split(".");
1677 for (let i = 0; i < 4; ++i) {
1686 this.columns["flags"].updateTd = function(td, row) {
1687 td.textContent = this.getRowValue(row, 0);
1688 td.title = this.getRowValue(row, 1);
1692 this.columns["progress"].updateTd = function(td, row) {
1693 const progress = this.getRowValue(row);
1694 let progressFormatted = (progress * 100).round(1);
1695 if ((progressFormatted === 100.0) && (progress !== 1.0))
1696 progressFormatted = 99.9;
1697 progressFormatted += "%";
1698 td.textContent = progressFormatted;
1699 td.title = progressFormatted;
1702 // dl_speed, up_speed
1703 this.columns["dl_speed"].updateTd = function(td, row) {
1704 const speed = this.getRowValue(row);
1706 td.textContent = "";
1710 const formattedSpeed = window.qBittorrent.Misc.friendlyUnit(speed, true);
1711 td.textContent = formattedSpeed;
1712 td.title = formattedSpeed;
1715 this.columns["up_speed"].updateTd = this.columns["dl_speed"].updateTd;
1717 // downloaded, uploaded
1718 this.columns["downloaded"].updateTd = function(td, row) {
1719 const downloaded = window.qBittorrent.Misc.friendlyUnit(this.getRowValue(row), false);
1720 td.textContent = downloaded;
1721 td.title = downloaded;
1723 this.columns["uploaded"].updateTd = this.columns["downloaded"].updateTd;
1726 this.columns["relevance"].updateTd = this.columns["progress"].updateTd;
1729 this.columns["files"].updateTd = function(td, row) {
1730 const value = this.getRowValue(row, 0);
1731 td.textContent = value.replace(/\n/g, ";");
1738 const SearchResultsTable = new Class({
1739 Extends: DynamicTable,
1741 initColumns: function() {
1742 this.newColumn("fileName", "", "QBT_TR(Name)QBT_TR[CONTEXT=SearchResultsTable]", 500, true);
1743 this.newColumn("fileSize", "", "QBT_TR(Size)QBT_TR[CONTEXT=SearchResultsTable]", 100, true);
1744 this.newColumn("nbSeeders", "", "QBT_TR(Seeders)QBT_TR[CONTEXT=SearchResultsTable]", 100, true);
1745 this.newColumn("nbLeechers", "", "QBT_TR(Leechers)QBT_TR[CONTEXT=SearchResultsTable]", 100, true);
1746 this.newColumn("siteUrl", "", "QBT_TR(Search engine)QBT_TR[CONTEXT=SearchResultsTable]", 250, true);
1747 this.newColumn("pubDate", "", "QBT_TR(Published On)QBT_TR[CONTEXT=SearchResultsTable]", 200, true);
1749 this.initColumnsFunctions();
1752 initColumnsFunctions: function() {
1753 const displaySize = function(td, row) {
1754 const size = window.qBittorrent.Misc.friendlyUnit(this.getRowValue(row), false);
1755 td.textContent = size;
1758 const displayNum = function(td, row) {
1759 const value = this.getRowValue(row);
1760 const formattedValue = (value === "-1") ? "Unknown" : value;
1761 td.textContent = formattedValue;
1762 td.title = formattedValue;
1764 const displayDate = function(td, row) {
1765 const value = this.getRowValue(row) * 1000;
1766 const formattedValue = (isNaN(value) || (value <= 0)) ? "" : (new Date(value).toLocaleString());
1767 td.textContent = formattedValue;
1768 td.title = formattedValue;
1771 this.columns["fileSize"].updateTd = displaySize;
1772 this.columns["nbSeeders"].updateTd = displayNum;
1773 this.columns["nbLeechers"].updateTd = displayNum;
1774 this.columns["pubDate"].updateTd = displayDate;
1777 getFilteredAndSortedRows: function() {
1778 const getSizeFilters = function() {
1779 let minSize = (window.qBittorrent.Search.searchSizeFilter.min > 0.00) ? (window.qBittorrent.Search.searchSizeFilter.min * Math.pow(1024, window.qBittorrent.Search.searchSizeFilter.minUnit)) : 0.00;
1780 let maxSize = (window.qBittorrent.Search.searchSizeFilter.max > 0.00) ? (window.qBittorrent.Search.searchSizeFilter.max * Math.pow(1024, window.qBittorrent.Search.searchSizeFilter.maxUnit)) : 0.00;
1782 if ((minSize > maxSize) && (maxSize > 0.00)) {
1783 const tmp = minSize;
1794 const getSeedsFilters = function() {
1795 let minSeeds = (window.qBittorrent.Search.searchSeedsFilter.min > 0) ? window.qBittorrent.Search.searchSeedsFilter.min : 0;
1796 let maxSeeds = (window.qBittorrent.Search.searchSeedsFilter.max > 0) ? window.qBittorrent.Search.searchSeedsFilter.max : 0;
1798 if ((minSeeds > maxSeeds) && (maxSeeds > 0)) {
1799 const tmp = minSeeds;
1800 minSeeds = maxSeeds;
1810 let filteredRows = [];
1811 const rows = this.rows.getValues();
1812 const searchTerms = window.qBittorrent.Search.searchText.pattern.toLowerCase().split(" ");
1813 const filterTerms = window.qBittorrent.Search.searchText.filterPattern.toLowerCase().split(" ");
1814 const sizeFilters = getSizeFilters();
1815 const seedsFilters = getSeedsFilters();
1816 const searchInTorrentName = $("searchInTorrentName").value === "names";
1818 if (searchInTorrentName || (filterTerms.length > 0) || (window.qBittorrent.Search.searchSizeFilter.min > 0.00) || (window.qBittorrent.Search.searchSizeFilter.max > 0.00)) {
1819 for (let i = 0; i < rows.length; ++i) {
1820 const row = rows[i];
1822 if (searchInTorrentName && !window.qBittorrent.Misc.containsAllTerms(row.full_data.fileName, searchTerms))
1824 if ((filterTerms.length > 0) && !window.qBittorrent.Misc.containsAllTerms(row.full_data.fileName, filterTerms))
1826 if ((sizeFilters.min > 0.00) && (row.full_data.fileSize < sizeFilters.min))
1828 if ((sizeFilters.max > 0.00) && (row.full_data.fileSize > sizeFilters.max))
1830 if ((seedsFilters.min > 0) && (row.full_data.nbSeeders < seedsFilters.min))
1832 if ((seedsFilters.max > 0) && (row.full_data.nbSeeders > seedsFilters.max))
1835 filteredRows.push(row);
1839 filteredRows = rows;
1842 filteredRows.sort((row1, row2) => {
1843 const column = this.columns[this.sortedColumn];
1844 const res = column.compareRows(row1, row2);
1845 if (this.reverseSort === "0")
1851 return filteredRows;
1854 setupTr: function(tr) {
1855 tr.addClass("searchTableRow");
1859 const SearchPluginsTable = new Class({
1860 Extends: DynamicTable,
1862 initColumns: function() {
1863 this.newColumn("fullName", "", "QBT_TR(Name)QBT_TR[CONTEXT=SearchPluginsTable]", 175, true);
1864 this.newColumn("version", "", "QBT_TR(Version)QBT_TR[CONTEXT=SearchPluginsTable]", 100, true);
1865 this.newColumn("url", "", "QBT_TR(Url)QBT_TR[CONTEXT=SearchPluginsTable]", 175, true);
1866 this.newColumn("enabled", "", "QBT_TR(Enabled)QBT_TR[CONTEXT=SearchPluginsTable]", 100, true);
1868 this.initColumnsFunctions();
1871 initColumnsFunctions: function() {
1872 this.columns["enabled"].updateTd = function(td, row) {
1873 const value = this.getRowValue(row);
1875 td.textContent = "QBT_TR(Yes)QBT_TR[CONTEXT=SearchPluginsTable]";
1876 td.title = "QBT_TR(Yes)QBT_TR[CONTEXT=SearchPluginsTable]";
1877 td.getParent("tr").addClass("green");
1878 td.getParent("tr").removeClass("red");
1881 td.textContent = "QBT_TR(No)QBT_TR[CONTEXT=SearchPluginsTable]";
1882 td.title = "QBT_TR(No)QBT_TR[CONTEXT=SearchPluginsTable]";
1883 td.getParent("tr").addClass("red");
1884 td.getParent("tr").removeClass("green");
1889 setupTr: function(tr) {
1890 tr.addClass("searchPluginsTableRow");
1894 const TorrentTrackersTable = new Class({
1895 Extends: DynamicTable,
1897 initColumns: function() {
1898 this.newColumn("tier", "", "QBT_TR(Tier)QBT_TR[CONTEXT=TrackerListWidget]", 35, true);
1899 this.newColumn("url", "", "QBT_TR(URL)QBT_TR[CONTEXT=TrackerListWidget]", 250, true);
1900 this.newColumn("status", "", "QBT_TR(Status)QBT_TR[CONTEXT=TrackerListWidget]", 125, true);
1901 this.newColumn("peers", "", "QBT_TR(Peers)QBT_TR[CONTEXT=TrackerListWidget]", 75, true);
1902 this.newColumn("seeds", "", "QBT_TR(Seeds)QBT_TR[CONTEXT=TrackerListWidget]", 75, true);
1903 this.newColumn("leeches", "", "QBT_TR(Leeches)QBT_TR[CONTEXT=TrackerListWidget]", 75, true);
1904 this.newColumn("downloaded", "", "QBT_TR(Times Downloaded)QBT_TR[CONTEXT=TrackerListWidget]", 100, true);
1905 this.newColumn("message", "", "QBT_TR(Message)QBT_TR[CONTEXT=TrackerListWidget]", 250, true);
1909 const BulkRenameTorrentFilesTable = new Class({
1910 Extends: DynamicTable,
1913 prevFilterTerms: [],
1914 prevRowsString: null,
1915 prevFilteredRows: [],
1916 prevSortedColumn: null,
1917 prevReverseSort: null,
1918 fileTree: new window.qBittorrent.FileTree.FileTree(),
1920 populateTable: function(root) {
1921 this.fileTree.setRoot(root);
1922 root.children.each((node) => {
1923 this._addNodeToTable(node, 0);
1927 _addNodeToTable: function(node, depth) {
1930 if (node.isFolder) {
1934 checked: node.checked,
1936 original: node.original,
1937 renamed: node.renamed
1941 node.full_data = data;
1942 this.updateRowData(data);
1945 node.data.rowId = node.rowId;
1946 node.full_data = node.data;
1947 this.updateRowData(node.data);
1950 node.children.each((child) => {
1951 this._addNodeToTable(child, depth + 1);
1955 getRoot: function() {
1956 return this.fileTree.getRoot();
1959 getNode: function(rowId) {
1960 return this.fileTree.getNode(rowId);
1963 getRow: function(node) {
1964 const rowId = this.fileTree.getRowId(node);
1965 return this.rows.get(rowId);
1968 getSelectedRows: function() {
1969 const nodes = this.fileTree.toArray();
1971 return nodes.filter(x => x.checked === 0);
1974 initColumns: function() {
1975 // Blocks saving header width (because window width isn't saved)
1976 LocalPreferences.remove("column_" + "checked" + "_width_" + this.dynamicTableDivId);
1977 LocalPreferences.remove("column_" + "original" + "_width_" + this.dynamicTableDivId);
1978 LocalPreferences.remove("column_" + "renamed" + "_width_" + this.dynamicTableDivId);
1979 this.newColumn("checked", "", "", 50, true);
1980 this.newColumn("original", "", "QBT_TR(Original)QBT_TR[CONTEXT=TrackerListWidget]", 270, true);
1981 this.newColumn("renamed", "", "QBT_TR(Renamed)QBT_TR[CONTEXT=TrackerListWidget]", 220, true);
1983 this.initColumnsFunctions();
1987 * Toggles the global checkbox and all checkboxes underneath
1989 toggleGlobalCheckbox: function() {
1990 const checkbox = $("rootMultiRename_cb");
1991 const checkboxes = $$("input.RenamingCB");
1993 for (let i = 0; i < checkboxes.length; ++i) {
1994 const node = this.getNode(i);
1996 if (checkbox.checked || checkbox.indeterminate) {
1997 const cb = checkboxes[i];
1999 cb.indeterminate = false;
2000 cb.state = "checked";
2002 node.full_data.checked = node.checked;
2005 const cb = checkboxes[i];
2007 cb.indeterminate = false;
2008 cb.state = "unchecked";
2010 node.full_data.checked = node.checked;
2014 this.updateGlobalCheckbox();
2017 toggleNodeTreeCheckbox: function(rowId, checkState) {
2018 const node = this.getNode(rowId);
2019 node.checked = checkState;
2020 node.full_data.checked = checkState;
2021 const checkbox = $(`cbRename${rowId}`);
2022 checkbox.checked = node.checked === 0;
2023 checkbox.state = checkbox.checked ? "checked" : "unchecked";
2025 for (let i = 0; i < node.children.length; ++i)
2026 this.toggleNodeTreeCheckbox(node.children[i].rowId, checkState);
2029 updateGlobalCheckbox: function() {
2030 const checkbox = $("rootMultiRename_cb");
2031 const checkboxes = $$("input.RenamingCB");
2032 const isAllChecked = function() {
2033 for (let i = 0; i < checkboxes.length; ++i) {
2034 if (!checkboxes[i].checked)
2039 const isAllUnchecked = function() {
2040 for (let i = 0; i < checkboxes.length; ++i) {
2041 if (checkboxes[i].checked)
2046 if (isAllChecked()) {
2047 checkbox.state = "checked";
2048 checkbox.indeterminate = false;
2049 checkbox.checked = true;
2051 else if (isAllUnchecked()) {
2052 checkbox.state = "unchecked";
2053 checkbox.indeterminate = false;
2054 checkbox.checked = false;
2057 checkbox.state = "partial";
2058 checkbox.indeterminate = true;
2059 checkbox.checked = false;
2063 initColumnsFunctions: function() {
2067 this.columns["checked"].updateTd = function(td, row) {
2068 const id = row.rowId;
2069 const value = this.getRowValue(row);
2071 const treeImg = new Element("img", {
2072 src: "images/L.gif",
2077 const checkbox = new Element("input");
2078 checkbox.type = "checkbox";
2079 checkbox.id = "cbRename" + id;
2080 checkbox.setAttribute("data-id", id);
2081 checkbox.className = "RenamingCB";
2082 checkbox.addEventListener("click", (e) => {
2083 const node = that.getNode(id);
2084 node.checked = e.target.checked ? 0 : 1;
2085 node.full_data.checked = node.checked;
2086 that.updateGlobalCheckbox();
2087 that.onRowSelectionChange(node);
2088 e.stopPropagation();
2090 checkbox.checked = (value === 0);
2091 checkbox.state = checkbox.checked ? "checked" : "unchecked";
2092 checkbox.indeterminate = false;
2093 td.adopt(treeImg, checkbox);
2097 this.columns["original"].updateTd = function(td, row) {
2098 const id = row.rowId;
2099 const fileNameId = "filesTablefileName" + id;
2100 const node = that.getNode(id);
2102 if (node.isFolder) {
2103 const value = this.getRowValue(row);
2104 const dirImgId = "renameTableDirImg" + id;
2106 // just update file name
2107 $(fileNameId).textContent = value;
2110 const span = new Element("span", {
2114 const dirImg = new Element("img", {
2115 src: "images/directory.svg",
2119 "margin-bottom": -3,
2120 "margin-left": (node.depth * 20)
2124 td.replaceChildren(dirImg, span);
2128 const value = this.getRowValue(row);
2129 const span = new Element("span", {
2133 "margin-left": ((node.depth + 1) * 20)
2136 td.replaceChildren(span);
2141 this.columns["renamed"].updateTd = function(td, row) {
2142 const id = row.rowId;
2143 const fileNameRenamedId = "filesTablefileRenamed" + id;
2144 const value = this.getRowValue(row);
2146 const span = new Element("span", {
2148 id: fileNameRenamedId,
2150 td.replaceChildren(span);
2154 onRowSelectionChange: function(row) {},
2156 selectRow: function() {
2160 reselectRows: function(rowIds) {
2163 this.tableBody.getElements("tr").each((tr) => {
2164 if (rowIds.includes(tr.rowId)) {
2165 const node = that.getNode(tr.rowId);
2167 node.full_data.checked = 0;
2169 const checkbox = tr.children[0].getElement("input");
2170 checkbox.state = "checked";
2171 checkbox.indeterminate = false;
2172 checkbox.checked = true;
2176 this.updateGlobalCheckbox();
2179 _sortNodesByColumn: function(nodes, column) {
2180 nodes.sort((row1, row2) => {
2181 // list folders before files when sorting by name
2182 if (column.name === "original") {
2183 const node1 = this.getNode(row1.data.rowId);
2184 const node2 = this.getNode(row2.data.rowId);
2185 if (node1.isFolder && !node2.isFolder)
2187 if (node2.isFolder && !node1.isFolder)
2191 const res = column.compareRows(row1, row2);
2192 return (this.reverseSort === "0") ? res : -res;
2195 nodes.each((node) => {
2196 if (node.children.length > 0)
2197 this._sortNodesByColumn(node.children, column);
2201 _filterNodes: function(node, filterTerms, filteredRows) {
2202 if (node.isFolder) {
2203 const childAdded = node.children.reduce((acc, child) => {
2204 // we must execute the function before ORing w/ acc or we'll stop checking child nodes after the first successful match
2205 return (this._filterNodes(child, filterTerms, filteredRows) || acc);
2209 const row = this.getRow(node);
2210 filteredRows.push(row);
2215 if (window.qBittorrent.Misc.containsAllTerms(node.original, filterTerms)) {
2216 const row = this.getRow(node);
2217 filteredRows.push(row);
2224 setFilter: function(text) {
2225 const filterTerms = text.trim().toLowerCase().split(" ");
2226 if ((filterTerms.length === 1) && (filterTerms[0] === ""))
2227 this.filterTerms = [];
2229 this.filterTerms = filterTerms;
2232 getFilteredAndSortedRows: function() {
2233 if (this.getRoot() === null)
2236 const generateRowsSignature = function(rows) {
2237 const rowsData = rows.map((row) => {
2238 return row.full_data;
2240 return JSON.stringify(rowsData);
2243 const getFilteredRows = function() {
2244 if (this.filterTerms.length === 0) {
2245 const nodeArray = this.fileTree.toArray();
2246 const filteredRows = nodeArray.map((node) => {
2247 return this.getRow(node);
2249 return filteredRows;
2252 const filteredRows = [];
2253 this.getRoot().children.each((child) => {
2254 this._filterNodes(child, this.filterTerms, filteredRows);
2256 filteredRows.reverse();
2257 return filteredRows;
2260 const hasRowsChanged = function(rowsString, prevRowsStringString) {
2261 const rowsChanged = (rowsString !== prevRowsStringString);
2262 const isFilterTermsChanged = this.filterTerms.reduce((acc, term, index) => {
2263 return (acc || (term !== this.prevFilterTerms[index]));
2265 const isFilterChanged = ((this.filterTerms.length !== this.prevFilterTerms.length)
2266 || ((this.filterTerms.length > 0) && isFilterTermsChanged));
2267 const isSortedColumnChanged = (this.prevSortedColumn !== this.sortedColumn);
2268 const isReverseSortChanged = (this.prevReverseSort !== this.reverseSort);
2270 return (rowsChanged || isFilterChanged || isSortedColumnChanged || isReverseSortChanged);
2273 const rowsString = generateRowsSignature(this.rows);
2274 if (!hasRowsChanged(rowsString, this.prevRowsString))
2275 return this.prevFilteredRows;
2277 // sort, then filter
2278 const column = this.columns[this.sortedColumn];
2279 this._sortNodesByColumn(this.getRoot().children, column);
2280 const filteredRows = getFilteredRows();
2282 this.prevFilterTerms = this.filterTerms;
2283 this.prevRowsString = rowsString;
2284 this.prevFilteredRows = filteredRows;
2285 this.prevSortedColumn = this.sortedColumn;
2286 this.prevReverseSort = this.reverseSort;
2287 return filteredRows;
2290 setIgnored: function(rowId, ignore) {
2291 const row = this.rows.get(rowId);
2293 row.full_data.remaining = 0;
2295 row.full_data.remaining = (row.full_data.size * (1.0 - (row.full_data.progress / 100)));
2298 setupTr: function(tr) {
2299 tr.addEventListener("keydown", function(event) {
2300 switch (event.key) {
2302 qBittorrent.PropFiles.collapseFolder(this._this.getSelectedRowId());
2305 qBittorrent.PropFiles.expandFolder(this._this.getSelectedRowId());
2312 const TorrentFilesTable = new Class({
2313 Extends: DynamicTable,
2316 prevFilterTerms: [],
2317 prevRowsString: null,
2318 prevFilteredRows: [],
2319 prevSortedColumn: null,
2320 prevReverseSort: null,
2321 fileTree: new window.qBittorrent.FileTree.FileTree(),
2323 populateTable: function(root) {
2324 this.fileTree.setRoot(root);
2325 root.children.each((node) => {
2326 this._addNodeToTable(node, 0);
2330 _addNodeToTable: function(node, depth) {
2333 if (node.isFolder) {
2337 checked: node.checked,
2338 remaining: node.remaining,
2339 progress: node.progress,
2340 priority: window.qBittorrent.PropFiles.normalizePriority(node.priority),
2341 availability: node.availability,
2347 node.full_data = data;
2348 this.updateRowData(data);
2351 node.data.rowId = node.rowId;
2352 node.full_data = node.data;
2353 this.updateRowData(node.data);
2356 node.children.each((child) => {
2357 this._addNodeToTable(child, depth + 1);
2361 getRoot: function() {
2362 return this.fileTree.getRoot();
2365 getNode: function(rowId) {
2366 return this.fileTree.getNode(rowId);
2369 getRow: function(node) {
2370 const rowId = this.fileTree.getRowId(node);
2371 return this.rows.get(rowId);
2374 initColumns: function() {
2375 this.newColumn("checked", "", "", 50, true);
2376 this.newColumn("name", "", "QBT_TR(Name)QBT_TR[CONTEXT=TrackerListWidget]", 300, true);
2377 this.newColumn("size", "", "QBT_TR(Total Size)QBT_TR[CONTEXT=TrackerListWidget]", 75, true);
2378 this.newColumn("progress", "", "QBT_TR(Progress)QBT_TR[CONTEXT=TrackerListWidget]", 100, true);
2379 this.newColumn("priority", "", "QBT_TR(Download Priority)QBT_TR[CONTEXT=TrackerListWidget]", 150, true);
2380 this.newColumn("remaining", "", "QBT_TR(Remaining)QBT_TR[CONTEXT=TrackerListWidget]", 75, true);
2381 this.newColumn("availability", "", "QBT_TR(Availability)QBT_TR[CONTEXT=TrackerListWidget]", 75, true);
2383 this.initColumnsFunctions();
2386 initColumnsFunctions: function() {
2388 const displaySize = function(td, row) {
2389 const size = window.qBittorrent.Misc.friendlyUnit(this.getRowValue(row), false);
2390 td.textContent = size;
2393 const displayPercentage = function(td, row) {
2394 const value = window.qBittorrent.Misc.friendlyPercentage(this.getRowValue(row));
2395 td.textContent = value;
2400 this.columns["checked"].updateTd = function(td, row) {
2401 const id = row.rowId;
2402 const value = this.getRowValue(row);
2404 if (window.qBittorrent.PropFiles.isDownloadCheckboxExists(id)) {
2405 window.qBittorrent.PropFiles.updateDownloadCheckbox(id, value);
2408 const treeImg = new Element("img", {
2409 src: "images/L.gif",
2414 td.adopt(treeImg, window.qBittorrent.PropFiles.createDownloadCheckbox(id, row.full_data.fileId, value));
2419 this.columns["name"].updateTd = function(td, row) {
2420 const id = row.rowId;
2421 const fileNameId = "filesTablefileName" + id;
2422 const node = that.getNode(id);
2424 if (node.isFolder) {
2425 const value = this.getRowValue(row);
2426 const collapseIconId = "filesTableCollapseIcon" + id;
2427 const dirImgId = "filesTableDirImg" + id;
2429 // just update file name
2430 $(fileNameId).textContent = value;
2433 const collapseIcon = new Element("img", {
2434 src: "images/go-down.svg",
2436 "margin-left": (node.depth * 20)
2438 class: "filesTableCollapseIcon",
2441 onclick: "qBittorrent.PropFiles.collapseIconClicked(this)"
2443 const span = new Element("span", {
2447 const dirImg = new Element("img", {
2448 src: "images/directory.svg",
2456 td.replaceChildren(collapseIcon, dirImg, span);
2460 const value = this.getRowValue(row);
2461 const span = new Element("span", {
2465 "margin-left": ((node.depth + 1) * 20)
2468 td.replaceChildren(span);
2473 this.columns["size"].updateTd = displaySize;
2476 this.columns["progress"].updateTd = function(td, row) {
2477 const id = row.rowId;
2478 const value = this.getRowValue(row);
2480 const progressBar = $("pbf_" + id);
2481 if (progressBar === null) {
2482 td.adopt(new window.qBittorrent.ProgressBar.ProgressBar(value.toFloat(), {
2488 progressBar.setValue(value.toFloat());
2493 this.columns["priority"].updateTd = function(td, row) {
2494 const id = row.rowId;
2495 const value = this.getRowValue(row);
2497 if (window.qBittorrent.PropFiles.isPriorityComboExists(id))
2498 window.qBittorrent.PropFiles.updatePriorityCombo(id, value);
2500 td.adopt(window.qBittorrent.PropFiles.createPriorityCombo(id, row.full_data.fileId, value));
2503 // remaining, availability
2504 this.columns["remaining"].updateTd = displaySize;
2505 this.columns["availability"].updateTd = displayPercentage;
2508 _sortNodesByColumn: function(nodes, column) {
2509 nodes.sort((row1, row2) => {
2510 // list folders before files when sorting by name
2511 if (column.name === "name") {
2512 const node1 = this.getNode(row1.data.rowId);
2513 const node2 = this.getNode(row2.data.rowId);
2514 if (node1.isFolder && !node2.isFolder)
2516 if (node2.isFolder && !node1.isFolder)
2520 const res = column.compareRows(row1, row2);
2521 return (this.reverseSort === "0") ? res : -res;
2524 nodes.each((node) => {
2525 if (node.children.length > 0)
2526 this._sortNodesByColumn(node.children, column);
2530 _filterNodes: function(node, filterTerms, filteredRows) {
2531 if (node.isFolder) {
2532 const childAdded = node.children.reduce((acc, child) => {
2533 // we must execute the function before ORing w/ acc or we'll stop checking child nodes after the first successful match
2534 return (this._filterNodes(child, filterTerms, filteredRows) || acc);
2538 const row = this.getRow(node);
2539 filteredRows.push(row);
2544 if (window.qBittorrent.Misc.containsAllTerms(node.name, filterTerms)) {
2545 const row = this.getRow(node);
2546 filteredRows.push(row);
2553 setFilter: function(text) {
2554 const filterTerms = text.trim().toLowerCase().split(" ");
2555 if ((filterTerms.length === 1) && (filterTerms[0] === ""))
2556 this.filterTerms = [];
2558 this.filterTerms = filterTerms;
2561 getFilteredAndSortedRows: function() {
2562 if (this.getRoot() === null)
2565 const generateRowsSignature = function(rows) {
2566 const rowsData = rows.map((row) => {
2567 return row.full_data;
2569 return JSON.stringify(rowsData);
2572 const getFilteredRows = function() {
2573 if (this.filterTerms.length === 0) {
2574 const nodeArray = this.fileTree.toArray();
2575 const filteredRows = nodeArray.map((node) => {
2576 return this.getRow(node);
2578 return filteredRows;
2581 const filteredRows = [];
2582 this.getRoot().children.each((child) => {
2583 this._filterNodes(child, this.filterTerms, filteredRows);
2585 filteredRows.reverse();
2586 return filteredRows;
2589 const hasRowsChanged = function(rowsString, prevRowsStringString) {
2590 const rowsChanged = (rowsString !== prevRowsStringString);
2591 const isFilterTermsChanged = this.filterTerms.reduce((acc, term, index) => {
2592 return (acc || (term !== this.prevFilterTerms[index]));
2594 const isFilterChanged = ((this.filterTerms.length !== this.prevFilterTerms.length)
2595 || ((this.filterTerms.length > 0) && isFilterTermsChanged));
2596 const isSortedColumnChanged = (this.prevSortedColumn !== this.sortedColumn);
2597 const isReverseSortChanged = (this.prevReverseSort !== this.reverseSort);
2599 return (rowsChanged || isFilterChanged || isSortedColumnChanged || isReverseSortChanged);
2602 const rowsString = generateRowsSignature(this.rows);
2603 if (!hasRowsChanged(rowsString, this.prevRowsString))
2604 return this.prevFilteredRows;
2606 // sort, then filter
2607 const column = this.columns[this.sortedColumn];
2608 this._sortNodesByColumn(this.getRoot().children, column);
2609 const filteredRows = getFilteredRows();
2611 this.prevFilterTerms = this.filterTerms;
2612 this.prevRowsString = rowsString;
2613 this.prevFilteredRows = filteredRows;
2614 this.prevSortedColumn = this.sortedColumn;
2615 this.prevReverseSort = this.reverseSort;
2616 return filteredRows;
2619 setIgnored: function(rowId, ignore) {
2620 const row = this.rows.get(rowId);
2622 row.full_data.remaining = 0;
2624 row.full_data.remaining = (row.full_data.size * (1.0 - (row.full_data.progress / 100)));
2627 setupTr: function(tr) {
2628 tr.addEventListener("keydown", function(event) {
2629 switch (event.key) {
2631 qBittorrent.PropFiles.collapseFolder(this._this.getSelectedRowId());
2634 qBittorrent.PropFiles.expandFolder(this._this.getSelectedRowId());
2641 const RssFeedTable = new Class({
2642 Extends: DynamicTable,
2643 initColumns: function() {
2644 this.newColumn("state_icon", "", "", 30, true);
2645 this.newColumn("name", "", "QBT_TR(RSS feeds)QBT_TR[CONTEXT=FeedListWidget]", -1, true);
2647 this.columns["state_icon"].dataProperties[0] = "";
2649 // map name row to "[name] ([unread])"
2650 this.columns["name"].dataProperties.push("unread");
2651 this.columns["name"].updateTd = function(td, row) {
2652 const name = this.getRowValue(row, 0);
2653 const unreadCount = this.getRowValue(row, 1);
2654 const value = name + " (" + unreadCount + ")";
2655 td.textContent = value;
2659 setupHeaderMenu: function() {},
2660 setupHeaderEvents: function() {},
2661 getFilteredAndSortedRows: function() {
2662 return this.rows.getValues();
2664 selectRow: function(rowId) {
2665 this.selectedRows.push(rowId);
2667 this.onSelectedRowChanged();
2669 const rows = this.rows.getValues();
2671 for (let i = 0; i < rows.length; ++i) {
2672 if (rows[i].rowId === rowId) {
2673 path = rows[i].full_data.dataPath;
2677 window.qBittorrent.Rss.showRssFeed(path);
2679 setupTr: function(tr) {
2680 tr.addEventListener("dblclick", function(e) {
2681 if (this.rowId !== 0) {
2682 window.qBittorrent.Rss.moveItem(this._this.rows.get(this.rowId).full_data.dataPath);
2687 updateRow: function(tr, fullUpdate) {
2688 const row = this.rows.get(tr.rowId);
2689 const data = row[fullUpdate ? "full_data" : "data"];
2691 const tds = tr.getElements("td");
2692 for (let i = 0; i < this.columns.length; ++i) {
2693 if (Object.hasOwn(data, this.columns[i].dataProperties[0]))
2694 this.columns[i].updateTd(tds[i], row);
2697 tds[0].style.overflow = "visible";
2698 const indentation = row.full_data.indentation;
2699 tds[0].style.paddingLeft = (indentation * 32 + 4) + "px";
2700 tds[1].style.paddingLeft = (indentation * 32 + 4) + "px";
2702 updateIcons: function() {
2704 this.rows.each(row => {
2706 switch (row.full_data.status) {
2708 img_path = "images/application-rss.svg";
2711 img_path = "images/task-reject.svg";
2714 img_path = "images/spinner.gif";
2717 img_path = "images/mail-inbox.svg";
2720 img_path = "images/folder-documents.svg";
2724 for (let i = 0; i < this.tableBody.rows.length; ++i) {
2725 if (this.tableBody.rows[i].rowId === row.rowId) {
2726 td = this.tableBody.rows[i].children[0];
2730 if (td.getChildren("img").length > 0) {
2731 const img = td.getChildren("img")[0];
2732 if (!img.src.includes(img_path)) {
2738 td.adopt(new Element("img", {
2740 "class": "stateIcon",
2747 newColumn: function(name, style, caption, defaultWidth, defaultVisible) {
2749 column["name"] = name;
2750 column["title"] = name;
2751 column["visible"] = defaultVisible;
2752 column["force_hide"] = false;
2753 column["caption"] = caption;
2754 column["style"] = style;
2755 if (defaultWidth !== -1)
2756 column["width"] = defaultWidth;
2758 column["dataProperties"] = [name];
2759 column["getRowValue"] = function(row, pos) {
2760 if (pos === undefined)
2762 return row["full_data"][this.dataProperties[pos]];
2764 column["compareRows"] = function(row1, row2) {
2765 const value1 = this.getRowValue(row1);
2766 const value2 = this.getRowValue(row2);
2767 if ((typeof(value1) === "number") && (typeof(value2) === "number"))
2768 return compareNumbers(value1, value2);
2769 return window.qBittorrent.Misc.naturalSortCollator.compare(value1, value2);
2771 column["updateTd"] = function(td, row) {
2772 const value = this.getRowValue(row);
2773 td.textContent = value;
2776 column["onResize"] = null;
2777 this.columns.push(column);
2778 this.columns[name] = column;
2780 this.hiddenTableHeader.appendChild(new Element("th"));
2781 this.fixedTableHeader.appendChild(new Element("th"));
2785 const RssArticleTable = new Class({
2786 Extends: DynamicTable,
2787 initColumns: function() {
2788 this.newColumn("name", "", "QBT_TR(Torrents: (double-click to download))QBT_TR[CONTEXT=RSSWidget]", -1, true);
2790 setupHeaderMenu: function() {},
2791 setupHeaderEvents: function() {},
2792 getFilteredAndSortedRows: function() {
2793 return this.rows.getValues();
2795 selectRow: function(rowId) {
2796 this.selectedRows.push(rowId);
2798 this.onSelectedRowChanged();
2800 const rows = this.rows.getValues();
2803 for (let i = 0; i < rows.length; ++i) {
2804 if (rows[i].rowId === rowId) {
2805 articleId = rows[i].full_data.dataId;
2806 feedUid = rows[i].full_data.feedUid;
2807 this.tableBody.rows[rows[i].rowId].removeClass("unreadArticle");
2811 window.qBittorrent.Rss.showDetails(feedUid, articleId);
2813 setupTr: function(tr) {
2814 tr.addEventListener("dblclick", function(e) {
2815 showDownloadPage([this._this.rows.get(this.rowId).full_data.torrentURL]);
2818 tr.addClass("torrentsTableContextMenuTarget");
2820 updateRow: function(tr, fullUpdate) {
2821 const row = this.rows.get(tr.rowId);
2822 const data = row[fullUpdate ? "full_data" : "data"];
2823 if (!row.full_data.isRead)
2824 tr.addClass("unreadArticle");
2826 tr.removeClass("unreadArticle");
2828 const tds = tr.getElements("td");
2829 for (let i = 0; i < this.columns.length; ++i) {
2830 if (Object.hasOwn(data, this.columns[i].dataProperties[0]))
2831 this.columns[i].updateTd(tds[i], row);
2835 newColumn: function(name, style, caption, defaultWidth, defaultVisible) {
2837 column["name"] = name;
2838 column["title"] = name;
2839 column["visible"] = defaultVisible;
2840 column["force_hide"] = false;
2841 column["caption"] = caption;
2842 column["style"] = style;
2843 if (defaultWidth !== -1)
2844 column["width"] = defaultWidth;
2846 column["dataProperties"] = [name];
2847 column["getRowValue"] = function(row, pos) {
2848 if (pos === undefined)
2850 return row["full_data"][this.dataProperties[pos]];
2852 column["compareRows"] = function(row1, row2) {
2853 const value1 = this.getRowValue(row1);
2854 const value2 = this.getRowValue(row2);
2855 if ((typeof(value1) === "number") && (typeof(value2) === "number"))
2856 return compareNumbers(value1, value2);
2857 return window.qBittorrent.Misc.naturalSortCollator.compare(value1, value2);
2859 column["updateTd"] = function(td, row) {
2860 const value = this.getRowValue(row);
2861 td.textContent = value;
2864 column["onResize"] = null;
2865 this.columns.push(column);
2866 this.columns[name] = column;
2868 this.hiddenTableHeader.appendChild(new Element("th"));
2869 this.fixedTableHeader.appendChild(new Element("th"));
2873 const RssDownloaderRulesTable = new Class({
2874 Extends: DynamicTable,
2875 initColumns: function() {
2876 this.newColumn("checked", "", "", 30, true);
2877 this.newColumn("name", "", "", -1, true);
2879 this.columns["checked"].updateTd = function(td, row) {
2880 if ($("cbRssDlRule" + row.rowId) === null) {
2881 const checkbox = new Element("input");
2882 checkbox.type = "checkbox";
2883 checkbox.id = "cbRssDlRule" + row.rowId;
2884 checkbox.checked = row.full_data.checked;
2886 checkbox.addEventListener("click", function(e) {
2887 window.qBittorrent.RssDownloader.rssDownloaderRulesTable.updateRowData({
2889 checked: this.checked
2891 window.qBittorrent.RssDownloader.modifyRuleState(row.full_data.name, "enabled", this.checked);
2892 e.stopPropagation();
2895 td.append(checkbox);
2898 $("cbRssDlRule" + row.rowId).checked = row.full_data.checked;
2902 setupHeaderMenu: function() {},
2903 setupHeaderEvents: function() {},
2904 getFilteredAndSortedRows: function() {
2905 return this.rows.getValues();
2907 setupTr: function(tr) {
2908 tr.addEventListener("dblclick", function(e) {
2909 window.qBittorrent.RssDownloader.renameRule(this._this.rows.get(this.rowId).full_data.name);
2913 newColumn: function(name, style, caption, defaultWidth, defaultVisible) {
2915 column["name"] = name;
2916 column["title"] = name;
2917 column["visible"] = defaultVisible;
2918 column["force_hide"] = false;
2919 column["caption"] = caption;
2920 column["style"] = style;
2921 if (defaultWidth !== -1)
2922 column["width"] = defaultWidth;
2924 column["dataProperties"] = [name];
2925 column["getRowValue"] = function(row, pos) {
2926 if (pos === undefined)
2928 return row["full_data"][this.dataProperties[pos]];
2930 column["compareRows"] = function(row1, row2) {
2931 const value1 = this.getRowValue(row1);
2932 const value2 = this.getRowValue(row2);
2933 if ((typeof(value1) === "number") && (typeof(value2) === "number"))
2934 return compareNumbers(value1, value2);
2935 return window.qBittorrent.Misc.naturalSortCollator.compare(value1, value2);
2937 column["updateTd"] = function(td, row) {
2938 const value = this.getRowValue(row);
2939 td.textContent = value;
2942 column["onResize"] = null;
2943 this.columns.push(column);
2944 this.columns[name] = column;
2946 this.hiddenTableHeader.appendChild(new Element("th"));
2947 this.fixedTableHeader.appendChild(new Element("th"));
2949 selectRow: function(rowId) {
2950 this.selectedRows.push(rowId);
2952 this.onSelectedRowChanged();
2954 const rows = this.rows.getValues();
2956 for (let i = 0; i < rows.length; ++i) {
2957 if (rows[i].rowId === rowId) {
2958 name = rows[i].full_data.name;
2962 window.qBittorrent.RssDownloader.showRule(name);
2966 const RssDownloaderFeedSelectionTable = new Class({
2967 Extends: DynamicTable,
2968 initColumns: function() {
2969 this.newColumn("checked", "", "", 30, true);
2970 this.newColumn("name", "", "", -1, true);
2972 this.columns["checked"].updateTd = function(td, row) {
2973 if ($("cbRssDlFeed" + row.rowId) === null) {
2974 const checkbox = new Element("input");
2975 checkbox.type = "checkbox";
2976 checkbox.id = "cbRssDlFeed" + row.rowId;
2977 checkbox.checked = row.full_data.checked;
2979 checkbox.addEventListener("click", function(e) {
2980 window.qBittorrent.RssDownloader.rssDownloaderFeedSelectionTable.updateRowData({
2982 checked: this.checked
2984 e.stopPropagation();
2987 td.append(checkbox);
2990 $("cbRssDlFeed" + row.rowId).checked = row.full_data.checked;
2994 setupHeaderMenu: function() {},
2995 setupHeaderEvents: function() {},
2996 getFilteredAndSortedRows: function() {
2997 return this.rows.getValues();
2999 newColumn: function(name, style, caption, defaultWidth, defaultVisible) {
3001 column["name"] = name;
3002 column["title"] = name;
3003 column["visible"] = defaultVisible;
3004 column["force_hide"] = false;
3005 column["caption"] = caption;
3006 column["style"] = style;
3007 if (defaultWidth !== -1)
3008 column["width"] = defaultWidth;
3010 column["dataProperties"] = [name];
3011 column["getRowValue"] = function(row, pos) {
3012 if (pos === undefined)
3014 return row["full_data"][this.dataProperties[pos]];
3016 column["compareRows"] = function(row1, row2) {
3017 const value1 = this.getRowValue(row1);
3018 const value2 = this.getRowValue(row2);
3019 if ((typeof(value1) === "number") && (typeof(value2) === "number"))
3020 return compareNumbers(value1, value2);
3021 return window.qBittorrent.Misc.naturalSortCollator.compare(value1, value2);
3023 column["updateTd"] = function(td, row) {
3024 const value = this.getRowValue(row);
3025 td.textContent = value;
3028 column["onResize"] = null;
3029 this.columns.push(column);
3030 this.columns[name] = column;
3032 this.hiddenTableHeader.appendChild(new Element("th"));
3033 this.fixedTableHeader.appendChild(new Element("th"));
3035 selectRow: function() {}
3038 const RssDownloaderArticlesTable = new Class({
3039 Extends: DynamicTable,
3040 initColumns: function() {
3041 this.newColumn("name", "", "", -1, true);
3043 setupHeaderMenu: function() {},
3044 setupHeaderEvents: function() {},
3045 getFilteredAndSortedRows: function() {
3046 return this.rows.getValues();
3048 newColumn: function(name, style, caption, defaultWidth, defaultVisible) {
3050 column["name"] = name;
3051 column["title"] = name;
3052 column["visible"] = defaultVisible;
3053 column["force_hide"] = false;
3054 column["caption"] = caption;
3055 column["style"] = style;
3056 if (defaultWidth !== -1)
3057 column["width"] = defaultWidth;
3059 column["dataProperties"] = [name];
3060 column["getRowValue"] = function(row, pos) {
3061 if (pos === undefined)
3063 return row["full_data"][this.dataProperties[pos]];
3065 column["compareRows"] = function(row1, row2) {
3066 const value1 = this.getRowValue(row1);
3067 const value2 = this.getRowValue(row2);
3068 if ((typeof(value1) === "number") && (typeof(value2) === "number"))
3069 return compareNumbers(value1, value2);
3070 return window.qBittorrent.Misc.naturalSortCollator.compare(value1, value2);
3072 column["updateTd"] = function(td, row) {
3073 const value = this.getRowValue(row);
3074 td.textContent = value;
3077 column["onResize"] = null;
3078 this.columns.push(column);
3079 this.columns[name] = column;
3081 this.hiddenTableHeader.appendChild(new Element("th"));
3082 this.fixedTableHeader.appendChild(new Element("th"));
3084 selectRow: function() {},
3085 updateRow: function(tr, fullUpdate) {
3086 const row = this.rows.get(tr.rowId);
3087 const data = row[fullUpdate ? "full_data" : "data"];
3089 if (row.full_data.isFeed) {
3090 tr.addClass("articleTableFeed");
3091 tr.removeClass("articleTableArticle");
3094 tr.removeClass("articleTableFeed");
3095 tr.addClass("articleTableArticle");
3098 const tds = tr.getElements("td");
3099 for (let i = 0; i < this.columns.length; ++i) {
3100 if (Object.hasOwn(data, this.columns[i].dataProperties[0]))
3101 this.columns[i].updateTd(tds[i], row);
3107 const LogMessageTable = new Class({
3108 Extends: DynamicTable,
3112 filteredLength: function() {
3113 return this.tableBody.getElements("tr").length;
3116 initColumns: function() {
3117 this.newColumn("rowId", "", "QBT_TR(ID)QBT_TR[CONTEXT=ExecutionLogWidget]", 50, true);
3118 this.newColumn("message", "", "QBT_TR(Message)QBT_TR[CONTEXT=ExecutionLogWidget]", 350, true);
3119 this.newColumn("timestamp", "", "QBT_TR(Timestamp)QBT_TR[CONTEXT=ExecutionLogWidget]", 150, true);
3120 this.newColumn("type", "", "QBT_TR(Log Type)QBT_TR[CONTEXT=ExecutionLogWidget]", 100, true);
3121 this.initColumnsFunctions();
3124 initColumnsFunctions: function() {
3125 this.columns["timestamp"].updateTd = function(td, row) {
3126 const date = new Date(this.getRowValue(row) * 1000).toLocaleString();
3127 td.set({ "text": date, "title": date });
3130 this.columns["type"].updateTd = function(td, row) {
3131 // Type of the message: Log::NORMAL: 1, Log::INFO: 2, Log::WARNING: 4, Log::CRITICAL: 8
3132 let logLevel, addClass;
3133 switch (this.getRowValue(row).toInt()) {
3135 logLevel = "QBT_TR(Normal)QBT_TR[CONTEXT=ExecutionLogWidget]";
3136 addClass = "logNormal";
3139 logLevel = "QBT_TR(Info)QBT_TR[CONTEXT=ExecutionLogWidget]";
3140 addClass = "logInfo";
3143 logLevel = "QBT_TR(Warning)QBT_TR[CONTEXT=ExecutionLogWidget]";
3144 addClass = "logWarning";
3147 logLevel = "QBT_TR(Critical)QBT_TR[CONTEXT=ExecutionLogWidget]";
3148 addClass = "logCritical";
3151 logLevel = "QBT_TR(Unknown)QBT_TR[CONTEXT=ExecutionLogWidget]";
3152 addClass = "logUnknown";
3155 td.set({ "text": logLevel, "title": logLevel });
3156 td.getParent("tr").className = `logTableRow${addClass}`;
3160 getFilteredAndSortedRows: function() {
3161 let filteredRows = [];
3162 const rows = this.rows.getValues();
3163 this.filterText = window.qBittorrent.Log.getFilterText();
3164 const filterTerms = (this.filterText.length > 0) ? this.filterText.toLowerCase().split(" ") : [];
3165 const logLevels = window.qBittorrent.Log.getSelectedLevels();
3166 if ((filterTerms.length > 0) || (logLevels.length < 4)) {
3167 for (let i = 0; i < rows.length; ++i) {
3168 if (!logLevels.includes(rows[i].full_data.type.toString()))
3171 if ((filterTerms.length > 0) && !window.qBittorrent.Misc.containsAllTerms(rows[i].full_data.message, filterTerms))
3174 filteredRows.push(rows[i]);
3178 filteredRows = rows;
3181 filteredRows.sort((row1, row2) => {
3182 const column = this.columns[this.sortedColumn];
3183 const res = column.compareRows(row1, row2);
3184 return (this.reverseSort === "0") ? res : -res;
3187 return filteredRows;
3190 setupCommonEvents: function() {},
3192 setupTr: function(tr) {
3193 tr.addClass("logTableRow");
3197 const LogPeerTable = new Class({
3198 Extends: LogMessageTable,
3200 initColumns: function() {
3201 this.newColumn("rowId", "", "QBT_TR(ID)QBT_TR[CONTEXT=ExecutionLogWidget]", 50, true);
3202 this.newColumn("ip", "", "QBT_TR(IP)QBT_TR[CONTEXT=ExecutionLogWidget]", 150, true);
3203 this.newColumn("timestamp", "", "QBT_TR(Timestamp)QBT_TR[CONTEXT=ExecutionLogWidget]", 150, true);
3204 this.newColumn("blocked", "", "QBT_TR(Status)QBT_TR[CONTEXT=ExecutionLogWidget]", 150, true);
3205 this.newColumn("reason", "", "QBT_TR(Reason)QBT_TR[CONTEXT=ExecutionLogWidget]", 150, true);
3207 this.columns["timestamp"].updateTd = function(td, row) {
3208 const date = new Date(this.getRowValue(row) * 1000).toLocaleString();
3209 td.set({ "text": date, "title": date });
3212 this.columns["blocked"].updateTd = function(td, row) {
3213 let status, addClass;
3214 if (this.getRowValue(row)) {
3215 status = "QBT_TR(Blocked)QBT_TR[CONTEXT=ExecutionLogWidget]";
3216 addClass = "peerBlocked";
3219 status = "QBT_TR(Banned)QBT_TR[CONTEXT=ExecutionLogWidget]";
3220 addClass = "peerBanned";
3222 td.set({ "text": status, "title": status });
3223 td.getParent("tr").className = `logTableRow${addClass}`;
3227 getFilteredAndSortedRows: function() {
3228 let filteredRows = [];
3229 const rows = this.rows.getValues();
3230 this.filterText = window.qBittorrent.Log.getFilterText();
3231 const filterTerms = (this.filterText.length > 0) ? this.filterText.toLowerCase().split(" ") : [];
3232 if (filterTerms.length > 0) {
3233 for (let i = 0; i < rows.length; ++i) {
3234 if ((filterTerms.length > 0) && !window.qBittorrent.Misc.containsAllTerms(rows[i].full_data.ip, filterTerms))
3237 filteredRows.push(rows[i]);
3241 filteredRows = rows;
3244 filteredRows.sort((row1, row2) => {
3245 const column = this.columns[this.sortedColumn];
3246 const res = column.compareRows(row1, row2);
3247 return (this.reverseSort === "0") ? res : -res;
3250 return filteredRows;
3256 Object.freeze(window.qBittorrent.DynamicTable);
3258 /*************************************************************/