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 if (window.qBittorrent === undefined)
37 window.qBittorrent = {};
39 window.qBittorrent.DynamicTable = (function() {
40 const exports = function() {
42 TorrentsTable: TorrentsTable,
43 TorrentPeersTable: TorrentPeersTable,
44 SearchResultsTable: SearchResultsTable,
45 SearchPluginsTable: SearchPluginsTable,
46 TorrentTrackersTable: TorrentTrackersTable,
47 BulkRenameTorrentFilesTable: BulkRenameTorrentFilesTable,
48 TorrentFilesTable: TorrentFilesTable,
49 LogMessageTable: LogMessageTable,
50 LogPeerTable: LogPeerTable,
51 RssFeedTable: RssFeedTable,
52 RssArticleTable: RssArticleTable,
53 RssDownloaderRulesTable: RssDownloaderRulesTable,
54 RssDownloaderFeedSelectionTable: RssDownloaderFeedSelectionTable,
55 RssDownloaderArticlesTable: RssDownloaderArticlesTable
59 const compareNumbers = (val1, val2) => {
67 let DynamicTableHeaderContextMenuClass = null;
68 let ProgressColumnWidth = -1;
70 const DynamicTable = new Class({
72 initialize: function() {},
74 setup: function(dynamicTableDivId, dynamicTableFixedHeaderDivId, contextMenu) {
75 this.dynamicTableDivId = dynamicTableDivId;
76 this.dynamicTableFixedHeaderDivId = dynamicTableFixedHeaderDivId;
77 this.fixedTableHeader = $(dynamicTableFixedHeaderDivId).getElements("tr")[0];
78 this.hiddenTableHeader = $(dynamicTableDivId).getElements("tr")[0];
79 this.tableBody = $(dynamicTableDivId).getElements("tbody")[0];
80 this.rows = new Hash();
81 this.selectedRows = [];
83 this.contextMenu = contextMenu;
84 this.sortedColumn = LocalPreferences.get("sorted_column_" + this.dynamicTableDivId, 0);
85 this.reverseSort = LocalPreferences.get("reverse_sort_" + this.dynamicTableDivId, "0");
87 this.loadColumnsOrder();
88 this.updateTableHeaders();
89 this.setupCommonEvents();
90 this.setupHeaderEvents();
91 this.setupHeaderMenu();
92 this.setSortedColumnIcon(this.sortedColumn, null, (this.reverseSort === "1"));
95 setupCommonEvents: function() {
96 const scrollFn = function() {
97 $(this.dynamicTableFixedHeaderDivId).getElements("table")[0].style.left = -$(this.dynamicTableDivId).scrollLeft + "px";
100 $(this.dynamicTableDivId).addEvent("scroll", scrollFn);
102 // if the table exists within a panel
103 if ($(this.dynamicTableDivId).getParent(".panel")) {
104 const resizeFn = function() {
105 const panel = $(this.dynamicTableDivId).getParent(".panel");
106 let h = panel.getBoundingClientRect().height - $(this.dynamicTableFixedHeaderDivId).getBoundingClientRect().height;
107 $(this.dynamicTableDivId).style.height = h + "px";
109 // Workaround due to inaccurate calculation of elements heights by browser
113 // is panel vertical scrollbar visible or does panel content not fit?
114 while (((panel.clientWidth !== panel.offsetWidth) || (panel.clientHeight !== panel.scrollHeight)) && (n > 0)) {
117 $(this.dynamicTableDivId).style.height = h + "px";
120 this.lastPanelHeight = panel.getBoundingClientRect().height;
123 $(this.dynamicTableDivId).getParent(".panel").addEvent("resize", resizeFn);
125 this.lastPanelHeight = 0;
127 // Workaround. Resize event is called not always (for example it isn't called when browser window changes it's size)
129 const checkResizeFn = function() {
130 const tableDiv = $(this.dynamicTableDivId);
132 // dynamicTableDivId is not visible on the UI
136 const panel = tableDiv.getParent(".panel");
137 if (this.lastPanelHeight !== panel.getBoundingClientRect().height) {
138 this.lastPanelHeight = panel.getBoundingClientRect().height;
139 panel.fireEvent("resize");
143 setInterval(checkResizeFn, 500);
147 setupHeaderEvents: function() {
148 this.currentHeaderAction = "";
149 this.canResize = false;
151 const resetElementBorderStyle = function(el, side) {
152 if ((side === "left") || (side !== "right")) {
153 el.setStyle("border-left-style", "");
154 el.setStyle("border-left-color", "");
155 el.setStyle("border-left-width", "");
157 if ((side === "right") || (side !== "left")) {
158 el.setStyle("border-right-style", "");
159 el.setStyle("border-right-color", "");
160 el.setStyle("border-right-width", "");
164 const mouseMoveFn = function(e) {
165 const brect = e.target.getBoundingClientRect();
166 const mouseXRelative = e.event.clientX - brect.left;
167 if (this.currentHeaderAction === "") {
168 if ((brect.width - mouseXRelative) < 5) {
169 this.resizeTh = e.target;
170 this.canResize = true;
171 e.target.getParent("tr").style.cursor = "col-resize";
173 else if ((mouseXRelative < 5) && e.target.getPrevious('[class=""]')) {
174 this.resizeTh = e.target.getPrevious('[class=""]');
175 this.canResize = true;
176 e.target.getParent("tr").style.cursor = "col-resize";
179 this.canResize = false;
180 e.target.getParent("tr").style.cursor = "";
183 if (this.currentHeaderAction === "drag") {
184 const previousVisibleSibling = e.target.getPrevious('[class=""]');
185 let borderChangeElement = previousVisibleSibling;
186 let changeBorderSide = "right";
188 if (mouseXRelative > (brect.width / 2)) {
189 borderChangeElement = e.target;
190 this.dropSide = "right";
193 this.dropSide = "left";
196 e.target.getParent("tr").style.cursor = "move";
198 if (!previousVisibleSibling) { // right most column
199 borderChangeElement = e.target;
201 if (mouseXRelative <= (brect.width / 2))
202 changeBorderSide = "left";
205 borderChangeElement.setStyle("border-" + changeBorderSide + "-style", "solid");
206 borderChangeElement.setStyle("border-" + changeBorderSide + "-color", "#e60");
207 borderChangeElement.setStyle("border-" + changeBorderSide + "-width", "initial");
209 resetElementBorderStyle(borderChangeElement, changeBorderSide === "right" ? "left" : "right");
211 borderChangeElement.getSiblings('[class=""]').each((el) => {
212 resetElementBorderStyle(el);
215 this.lastHoverTh = e.target;
216 this.lastClientX = e.event.clientX;
219 const mouseOutFn = function(e) {
220 resetElementBorderStyle(e.target);
223 const onBeforeStart = function(el) {
225 this.currentHeaderAction = "start";
226 this.dragMovement = false;
227 this.dragStartX = this.lastClientX;
230 const onStart = function(el, event) {
231 if (this.canResize) {
232 this.currentHeaderAction = "resize";
233 this.startWidth = this.resizeTh.getStyle("width").toFloat();
236 this.currentHeaderAction = "drag";
237 el.setStyle("background-color", "#C1D5E7");
241 const onDrag = function(el, event) {
242 if (this.currentHeaderAction === "resize") {
243 let width = this.startWidth + (event.page.x - this.dragStartX);
246 this.columns[this.resizeTh.columnName].width = width;
247 this.updateColumn(this.resizeTh.columnName);
251 const onComplete = function(el, event) {
252 resetElementBorderStyle(this.lastHoverTh);
253 el.setStyle("background-color", "");
254 if (this.currentHeaderAction === "resize")
255 LocalPreferences.set("column_" + this.resizeTh.columnName + "_width_" + this.dynamicTableDivId, this.columns[this.resizeTh.columnName].width);
256 if ((this.currentHeaderAction === "drag") && (el !== this.lastHoverTh)) {
257 this.saveColumnsOrder();
258 const val = LocalPreferences.get("columns_order_" + this.dynamicTableDivId).split(",");
259 val.erase(el.columnName);
260 let pos = val.indexOf(this.lastHoverTh.columnName);
261 if (this.dropSide === "right")
263 val.splice(pos, 0, el.columnName);
264 LocalPreferences.set("columns_order_" + this.dynamicTableDivId, val.join(","));
265 this.loadColumnsOrder();
266 this.updateTableHeaders();
267 while (this.tableBody.firstChild)
268 this.tableBody.removeChild(this.tableBody.firstChild);
269 this.updateTable(true);
271 if (this.currentHeaderAction === "drag") {
272 resetElementBorderStyle(el);
273 el.getSiblings('[class=""]').each((el) => {
274 resetElementBorderStyle(el);
277 this.currentHeaderAction = "";
280 const onCancel = function(el) {
281 this.currentHeaderAction = "";
282 this.setSortedColumn(el.columnName);
285 const onTouch = function(e) {
286 const column = e.target.columnName;
287 this.currentHeaderAction = "";
288 this.setSortedColumn(column);
291 const ths = this.fixedTableHeader.getElements("th");
293 for (let i = 0; i < ths.length; ++i) {
295 th.addEvent("mousemove", mouseMoveFn);
296 th.addEvent("mouseout", mouseOutFn);
297 th.addEvent("touchend", onTouch);
303 onBeforeStart: onBeforeStart,
306 onComplete: onComplete,
312 setupDynamicTableHeaderContextMenuClass: function() {
313 if (!DynamicTableHeaderContextMenuClass) {
314 DynamicTableHeaderContextMenuClass = new Class({
315 Extends: window.qBittorrent.ContextMenu.ContextMenu,
316 updateMenuItems: function() {
317 for (let i = 0; i < this.dynamicTable.columns.length; ++i) {
318 if (this.dynamicTable.columns[i].caption === "")
320 if (this.dynamicTable.columns[i].visible !== "0")
321 this.setItemChecked(this.dynamicTable.columns[i].name, true);
323 this.setItemChecked(this.dynamicTable.columns[i].name, false);
330 showColumn: function(columnName, show) {
331 this.columns[columnName].visible = show ? "1" : "0";
332 LocalPreferences.set("column_" + columnName + "_visible_" + this.dynamicTableDivId, show ? "1" : "0");
333 this.updateColumn(columnName);
336 setupHeaderMenu: function() {
337 this.setupDynamicTableHeaderContextMenuClass();
339 const menuId = this.dynamicTableDivId + "_headerMenu";
341 // reuse menu if already exists
342 const ul = $(menuId) ?? new Element("ul", {
344 class: "contextMenu scrollableMenu"
347 const createLi = function(columnName, text) {
348 const html = '<a href="#' + columnName + '" ><img src="images/checked-completed.svg"/>' + window.qBittorrent.Misc.escapeHtml(text) + "</a>";
349 return new Element("li", {
356 const onMenuItemClicked = function(element, ref, action) {
357 this.showColumn(action, this.columns[action].visible === "0");
360 // recreate child nodes when reusing (enables the context menu to work correctly)
361 if (ul.hasChildNodes()) {
362 while (ul.firstChild)
363 ul.removeChild(ul.lastChild);
366 for (let i = 0; i < this.columns.length; ++i) {
367 const text = this.columns[i].caption;
370 ul.appendChild(createLi(this.columns[i].name, text));
371 actions[this.columns[i].name] = onMenuItemClicked;
374 ul.inject(document.body);
376 this.headerContextMenu = new DynamicTableHeaderContextMenuClass({
377 targets: "#" + this.dynamicTableFixedHeaderDivId + " tr",
386 this.headerContextMenu.dynamicTable = this;
389 initColumns: function() {},
391 newColumn: function(name, style, caption, defaultWidth, defaultVisible) {
393 column["name"] = name;
394 column["title"] = name;
395 column["visible"] = LocalPreferences.get("column_" + name + "_visible_" + this.dynamicTableDivId, defaultVisible ? "1" : "0");
396 column["force_hide"] = false;
397 column["caption"] = caption;
398 column["style"] = style;
399 column["width"] = LocalPreferences.get("column_" + name + "_width_" + this.dynamicTableDivId, defaultWidth);
400 column["dataProperties"] = [name];
401 column["getRowValue"] = function(row, pos) {
402 if (pos === undefined)
404 return row["full_data"][this.dataProperties[pos]];
406 column["compareRows"] = function(row1, row2) {
407 const value1 = this.getRowValue(row1);
408 const value2 = this.getRowValue(row2);
409 if ((typeof(value1) === "number") && (typeof(value2) === "number"))
410 return compareNumbers(value1, value2);
411 return window.qBittorrent.Misc.naturalSortCollator.compare(value1, value2);
413 column["updateTd"] = function(td, row) {
414 const value = this.getRowValue(row);
415 td.set("text", value);
416 td.set("title", value);
418 column["onResize"] = null;
419 this.columns.push(column);
420 this.columns[name] = column;
422 this.hiddenTableHeader.appendChild(new Element("th"));
423 this.fixedTableHeader.appendChild(new Element("th"));
426 loadColumnsOrder: function() {
427 const columnsOrder = [];
428 const val = LocalPreferences.get("columns_order_" + this.dynamicTableDivId);
429 if ((val === null) || (val === undefined))
431 val.split(",").forEach((v) => {
432 if ((v in this.columns) && (!columnsOrder.contains(v)))
433 columnsOrder.push(v);
436 for (let i = 0; i < this.columns.length; ++i) {
437 if (!columnsOrder.contains(this.columns[i].name))
438 columnsOrder.push(this.columns[i].name);
441 for (let i = 0; i < this.columns.length; ++i)
442 this.columns[i] = this.columns[columnsOrder[i]];
445 saveColumnsOrder: function() {
447 for (let i = 0; i < this.columns.length; ++i) {
450 val += this.columns[i].name;
452 LocalPreferences.set("columns_order_" + this.dynamicTableDivId, val);
455 updateTableHeaders: function() {
456 this.updateHeader(this.hiddenTableHeader);
457 this.updateHeader(this.fixedTableHeader);
460 updateHeader: function(header) {
461 const ths = header.getElements("th");
463 for (let i = 0; i < ths.length; ++i) {
466 th.setAttribute("title", this.columns[i].caption);
467 th.set("text", this.columns[i].caption);
468 th.setAttribute("style", "width: " + this.columns[i].width + "px;" + this.columns[i].style);
469 th.columnName = this.columns[i].name;
470 th.addClass("column_" + th.columnName);
471 if ((this.columns[i].visible === "0") || this.columns[i].force_hide)
472 th.addClass("invisible");
474 th.removeClass("invisible");
478 getColumnPos: function(columnName) {
479 for (let i = 0; i < this.columns.length; ++i) {
480 if (this.columns[i].name === columnName)
486 updateColumn: function(columnName) {
487 const pos = this.getColumnPos(columnName);
488 const visible = ((this.columns[pos].visible !== "0") && !this.columns[pos].force_hide);
489 const ths = this.hiddenTableHeader.getElements("th");
490 const fths = this.fixedTableHeader.getElements("th");
491 const trs = this.tableBody.getElements("tr");
492 const style = "width: " + this.columns[pos].width + "px;" + this.columns[pos].style;
494 ths[pos].setAttribute("style", style);
495 fths[pos].setAttribute("style", style);
498 ths[pos].removeClass("invisible");
499 fths[pos].removeClass("invisible");
500 for (let i = 0; i < trs.length; ++i)
501 trs[i].getElements("td")[pos].removeClass("invisible");
504 ths[pos].addClass("invisible");
505 fths[pos].addClass("invisible");
506 for (let j = 0; j < trs.length; ++j)
507 trs[j].getElements("td")[pos].addClass("invisible");
509 if (this.columns[pos].onResize !== null)
510 this.columns[pos].onResize(columnName);
513 getSortedColumn: function() {
514 return LocalPreferences.get("sorted_column_" + this.dynamicTableDivId);
518 * @param {string} column name to sort by
519 * @param {string|null} reverse defaults to implementation-specific behavior when not specified. Should only be passed when restoring previous state.
521 setSortedColumn: function(column, reverse = null) {
522 if (column !== this.sortedColumn) {
523 const oldColumn = this.sortedColumn;
524 this.sortedColumn = column;
525 this.reverseSort = reverse ?? "0";
526 this.setSortedColumnIcon(column, oldColumn, false);
530 this.reverseSort = reverse ?? (this.reverseSort === "0" ? "1" : "0");
531 this.setSortedColumnIcon(column, null, (this.reverseSort === "1"));
533 LocalPreferences.set("sorted_column_" + this.dynamicTableDivId, column);
534 LocalPreferences.set("reverse_sort_" + this.dynamicTableDivId, this.reverseSort);
535 this.updateTable(false);
538 setSortedColumnIcon: function(newColumn, oldColumn, isReverse) {
539 const getCol = function(headerDivId, colName) {
540 const colElem = $$("#" + headerDivId + " .column_" + colName);
541 if (colElem.length === 1)
546 const colElem = getCol(this.dynamicTableFixedHeaderDivId, newColumn);
547 if (colElem !== null) {
548 colElem.addClass("sorted");
550 colElem.addClass("reverse");
552 colElem.removeClass("reverse");
554 const oldColElem = getCol(this.dynamicTableFixedHeaderDivId, oldColumn);
555 if (oldColElem !== null) {
556 oldColElem.removeClass("sorted");
557 oldColElem.removeClass("reverse");
561 getSelectedRowId: function() {
562 if (this.selectedRows.length > 0)
563 return this.selectedRows[0];
567 isRowSelected: function(rowId) {
568 return this.selectedRows.contains(rowId);
572 if (!MUI.ieLegacySupport)
575 const trs = this.tableBody.getElements("tr");
576 trs.each((el, i) => {
580 el.removeClass("alt");
584 selectAll: function() {
587 const trs = this.tableBody.getElements("tr");
588 for (let i = 0; i < trs.length; ++i) {
590 this.selectedRows.push(tr.rowId);
591 if (!tr.hasClass("selected"))
592 tr.addClass("selected");
596 deselectAll: function() {
597 this.selectedRows.empty();
600 selectRow: function(rowId) {
601 this.selectedRows.push(rowId);
603 this.onSelectedRowChanged();
606 deselectRow: function(rowId) {
607 this.selectedRows.erase(rowId);
609 this.onSelectedRowChanged();
612 selectRows: function(rowId1, rowId2) {
614 if (rowId1 === rowId2) {
615 this.selectRow(rowId1);
621 this.tableBody.getElements("tr").each((tr) => {
622 if ((tr.rowId === rowId1) || (tr.rowId === rowId2)) {
624 that.selectedRows.push(tr.rowId);
627 that.selectedRows.push(tr.rowId);
631 this.onSelectedRowChanged();
634 reselectRows: function(rowIds) {
636 this.selectedRows = rowIds.slice();
637 this.tableBody.getElements("tr").each((tr) => {
638 if (rowIds.includes(tr.rowId))
639 tr.addClass("selected");
643 setRowClass: function() {
645 this.tableBody.getElements("tr").each((tr) => {
646 if (that.isRowSelected(tr.rowId))
647 tr.addClass("selected");
649 tr.removeClass("selected");
653 onSelectedRowChanged: function() {},
655 updateRowData: function(data) {
656 // ensure rowId is a string
657 const rowId = `${data["rowId"]}`;
660 if (!this.rows.has(rowId)) {
665 this.rows.set(rowId, row);
668 row = this.rows.get(rowId);
672 for (const x in data) {
673 if (!Object.hasOwn(data, x))
675 row["full_data"][x] = data[x];
679 getFilteredAndSortedRows: function() {
680 const filteredRows = [];
682 const rows = this.rows.getValues();
684 for (let i = 0; i < rows.length; ++i) {
685 filteredRows.push(rows[i]);
686 filteredRows[rows[i].rowId] = rows[i];
689 filteredRows.sort((row1, row2) => {
690 const column = this.columns[this.sortedColumn];
691 const res = column.compareRows(row1, row2);
692 if (this.reverseSort === "0")
700 getTrByRowId: function(rowId) {
701 const trs = this.tableBody.getElements("tr");
702 for (let i = 0; i < trs.length; ++i) {
703 if (trs[i].rowId === rowId)
709 updateTable: function(fullUpdate = false) {
710 const rows = this.getFilteredAndSortedRows();
712 for (let i = 0; i < this.selectedRows.length; ++i) {
713 if (!(this.selectedRows[i] in rows)) {
714 this.selectedRows.splice(i, 1);
719 const trs = this.tableBody.getElements("tr");
721 for (let rowPos = 0; rowPos < rows.length; ++rowPos) {
722 const rowId = rows[rowPos]["rowId"];
723 let tr_found = false;
724 for (let j = rowPos; j < trs.length; ++j) {
725 if (trs[j]["rowId"] === rowId) {
729 trs[j].inject(trs[rowPos], "before");
730 const tmpTr = trs[j];
732 trs.splice(rowPos, 0, tmpTr);
736 if (tr_found) { // row already exists in the table
737 this.updateRow(trs[rowPos], fullUpdate);
739 else { // else create a new row in the table
740 const tr = new Element("tr");
741 // set tabindex so element receives keydown events
742 // more info: https://developer.mozilla.org/en-US/docs/Web/API/Element/keydown_event
743 tr.setProperty("tabindex", "-1");
745 const rowId = rows[rowPos]["rowId"];
746 tr.setProperty("data-row-id", rowId);
750 tr.addEvent("contextmenu", function(e) {
751 if (!this._this.isRowSelected(this.rowId)) {
752 this._this.deselectAll();
753 this._this.selectRow(this.rowId);
757 tr.addEvent("click", function(e) {
759 if (e.control || e.meta) {
760 // CTRL/CMD ⌘ key was pressed
761 if (this._this.isRowSelected(this.rowId))
762 this._this.deselectRow(this.rowId);
764 this._this.selectRow(this.rowId);
766 else if (e.shift && (this._this.selectedRows.length === 1)) {
767 // Shift key was pressed
768 this._this.selectRows(this._this.getSelectedRowId(), this.rowId);
772 this._this.deselectAll();
773 this._this.selectRow(this.rowId);
777 tr.addEvent("touchstart", function(e) {
778 if (!this._this.isRowSelected(this.rowId)) {
779 this._this.deselectAll();
780 this._this.selectRow(this.rowId);
783 tr.addEvent("keydown", function(event) {
786 this._this.selectPreviousRow();
789 this._this.selectNextRow();
796 for (let k = 0; k < this.columns.length; ++k) {
797 const td = new Element("td");
798 if ((this.columns[k].visible === "0") || this.columns[k].force_hide)
799 td.addClass("invisible");
804 if (rowPos >= trs.length) {
805 tr.inject(this.tableBody);
809 tr.inject(trs[rowPos], "before");
810 trs.splice(rowPos, 0, tr);
813 // Update context menu
814 if (this.contextMenu)
815 this.contextMenu.addTarget(tr);
817 this.updateRow(tr, true);
821 const rowPos = rows.length;
823 while ((rowPos < trs.length) && (trs.length > 0))
827 setupTr: function(tr) {},
829 updateRow: function(tr, fullUpdate) {
830 const row = this.rows.get(tr.rowId);
831 const data = row[fullUpdate ? "full_data" : "data"];
833 const tds = tr.getElements("td");
834 for (let i = 0; i < this.columns.length; ++i) {
835 if (Object.hasOwn(data, this.columns[i].dataProperties[0]))
836 this.columns[i].updateTd(tds[i], row);
841 removeRow: function(rowId) {
842 this.selectedRows.erase(rowId);
843 const tr = this.getTrByRowId(rowId);
846 this.rows.erase(rowId);
855 const trs = this.tableBody.getElements("tr");
856 while (trs.length > 0)
860 selectedRowsIds: function() {
861 return this.selectedRows.slice();
864 getRowIds: function() {
865 return this.rows.getKeys();
868 selectNextRow: function() {
869 const visibleRows = $(this.dynamicTableDivId).getElements("tbody tr").filter(e => e.getStyle("display") !== "none");
870 const selectedRowId = this.getSelectedRowId();
872 let selectedIndex = -1;
873 for (let i = 0; i < visibleRows.length; ++i) {
874 const row = visibleRows[i];
875 if (row.getProperty("data-row-id") === selectedRowId) {
881 const isLastRowSelected = (selectedIndex >= (visibleRows.length - 1));
882 if (!isLastRowSelected) {
885 const newRow = visibleRows[selectedIndex + 1];
886 this.selectRow(newRow.getProperty("data-row-id"));
890 selectPreviousRow: function() {
891 const visibleRows = $(this.dynamicTableDivId).getElements("tbody tr").filter(e => e.getStyle("display") !== "none");
892 const selectedRowId = this.getSelectedRowId();
894 let selectedIndex = -1;
895 for (let i = 0; i < visibleRows.length; ++i) {
896 const row = visibleRows[i];
897 if (row.getProperty("data-row-id") === selectedRowId) {
903 const isFirstRowSelected = selectedIndex <= 0;
904 if (!isFirstRowSelected) {
907 const newRow = visibleRows[selectedIndex - 1];
908 this.selectRow(newRow.getProperty("data-row-id"));
913 const TorrentsTable = new Class({
914 Extends: DynamicTable,
916 initColumns: function() {
917 this.newColumn("priority", "", "#", 30, true);
918 this.newColumn("state_icon", "cursor: default", "", 22, true);
919 this.newColumn("name", "", "QBT_TR(Name)QBT_TR[CONTEXT=TransferListModel]", 200, true);
920 this.newColumn("size", "", "QBT_TR(Size)QBT_TR[CONTEXT=TransferListModel]", 100, true);
921 this.newColumn("total_size", "", "QBT_TR(Total Size)QBT_TR[CONTEXT=TransferListModel]", 100, false);
922 this.newColumn("progress", "", "QBT_TR(Progress)QBT_TR[CONTEXT=TransferListModel]", 85, true);
923 this.newColumn("status", "", "QBT_TR(Status)QBT_TR[CONTEXT=TransferListModel]", 100, true);
924 this.newColumn("num_seeds", "", "QBT_TR(Seeds)QBT_TR[CONTEXT=TransferListModel]", 100, true);
925 this.newColumn("num_leechs", "", "QBT_TR(Peers)QBT_TR[CONTEXT=TransferListModel]", 100, true);
926 this.newColumn("dlspeed", "", "QBT_TR(Down Speed)QBT_TR[CONTEXT=TransferListModel]", 100, true);
927 this.newColumn("upspeed", "", "QBT_TR(Up Speed)QBT_TR[CONTEXT=TransferListModel]", 100, true);
928 this.newColumn("eta", "", "QBT_TR(ETA)QBT_TR[CONTEXT=TransferListModel]", 100, true);
929 this.newColumn("ratio", "", "QBT_TR(Ratio)QBT_TR[CONTEXT=TransferListModel]", 100, true);
930 this.newColumn("popularity", "", "QBT_TR(Popularity)QBT_TR[CONTEXT=TransferListModel]", 100, true);
931 this.newColumn("category", "", "QBT_TR(Category)QBT_TR[CONTEXT=TransferListModel]", 100, true);
932 this.newColumn("tags", "", "QBT_TR(Tags)QBT_TR[CONTEXT=TransferListModel]", 100, true);
933 this.newColumn("added_on", "", "QBT_TR(Added On)QBT_TR[CONTEXT=TransferListModel]", 100, true);
934 this.newColumn("completion_on", "", "QBT_TR(Completed On)QBT_TR[CONTEXT=TransferListModel]", 100, false);
935 this.newColumn("tracker", "", "QBT_TR(Tracker)QBT_TR[CONTEXT=TransferListModel]", 100, false);
936 this.newColumn("dl_limit", "", "QBT_TR(Down Limit)QBT_TR[CONTEXT=TransferListModel]", 100, false);
937 this.newColumn("up_limit", "", "QBT_TR(Up Limit)QBT_TR[CONTEXT=TransferListModel]", 100, false);
938 this.newColumn("downloaded", "", "QBT_TR(Downloaded)QBT_TR[CONTEXT=TransferListModel]", 100, false);
939 this.newColumn("uploaded", "", "QBT_TR(Uploaded)QBT_TR[CONTEXT=TransferListModel]", 100, false);
940 this.newColumn("downloaded_session", "", "QBT_TR(Session Download)QBT_TR[CONTEXT=TransferListModel]", 100, false);
941 this.newColumn("uploaded_session", "", "QBT_TR(Session Upload)QBT_TR[CONTEXT=TransferListModel]", 100, false);
942 this.newColumn("amount_left", "", "QBT_TR(Remaining)QBT_TR[CONTEXT=TransferListModel]", 100, false);
943 this.newColumn("time_active", "", "QBT_TR(Time Active)QBT_TR[CONTEXT=TransferListModel]", 100, false);
944 this.newColumn("save_path", "", "QBT_TR(Save path)QBT_TR[CONTEXT=TransferListModel]", 100, false);
945 this.newColumn("completed", "", "QBT_TR(Completed)QBT_TR[CONTEXT=TransferListModel]", 100, false);
946 this.newColumn("max_ratio", "", "QBT_TR(Ratio Limit)QBT_TR[CONTEXT=TransferListModel]", 100, false);
947 this.newColumn("seen_complete", "", "QBT_TR(Last Seen Complete)QBT_TR[CONTEXT=TransferListModel]", 100, false);
948 this.newColumn("last_activity", "", "QBT_TR(Last Activity)QBT_TR[CONTEXT=TransferListModel]", 100, false);
949 this.newColumn("availability", "", "QBT_TR(Availability)QBT_TR[CONTEXT=TransferListModel]", 100, false);
950 this.newColumn("reannounce", "", "QBT_TR(Reannounce In)QBT_TR[CONTEXT=TransferListModel]", 100, false);
952 this.columns["state_icon"].onclick = "";
953 this.columns["state_icon"].dataProperties[0] = "state";
955 this.columns["num_seeds"].dataProperties.push("num_complete");
956 this.columns["num_leechs"].dataProperties.push("num_incomplete");
957 this.columns["time_active"].dataProperties.push("seeding_time");
959 this.initColumnsFunctions();
962 initColumnsFunctions: function() {
965 this.columns["state_icon"].updateTd = function(td, row) {
966 let state = this.getRowValue(row);
974 state = "downloading";
975 img_path = "images/downloading.svg";
980 img_path = "images/upload.svg";
984 img_path = "images/stalledUP.svg";
988 img_path = "images/stalledDL.svg";
991 state = "torrent-stop";
992 img_path = "images/stopped.svg";
995 state = "checked-completed";
996 img_path = "images/checked-completed.svg";
1001 img_path = "images/queued.svg";
1005 case "queuedForChecking":
1006 case "checkingResumeData":
1007 state = "force-recheck";
1008 img_path = "images/force-recheck.svg";
1012 img_path = "images/set-location.svg";
1016 case "missingFiles":
1018 img_path = "images/error.svg";
1021 break; // do nothing
1024 if (td.getChildren("img").length > 0) {
1025 const img = td.getChildren("img")[0];
1026 if (!img.src.includes(img_path)) {
1027 img.set("src", img_path);
1028 img.set("title", state);
1032 td.adopt(new Element("img", {
1034 "class": "stateIcon",
1041 this.columns["status"].updateTd = function(td, row) {
1042 const state = this.getRowValue(row);
1049 status = "QBT_TR(Downloading)QBT_TR[CONTEXT=TransferListDelegate]";
1052 status = "QBT_TR(Stalled)QBT_TR[CONTEXT=TransferListDelegate]";
1055 status = "QBT_TR(Downloading metadata)QBT_TR[CONTEXT=TransferListDelegate]";
1057 case "forcedMetaDL":
1058 status = "QBT_TR([F] Downloading metadata)QBT_TR[CONTEXT=TransferListDelegate]";
1061 status = "QBT_TR([F] Downloading)QBT_TR[CONTEXT=TransferListDelegate]";
1065 status = "QBT_TR(Seeding)QBT_TR[CONTEXT=TransferListDelegate]";
1068 status = "QBT_TR([F] Seeding)QBT_TR[CONTEXT=TransferListDelegate]";
1072 status = "QBT_TR(Queued)QBT_TR[CONTEXT=TransferListDelegate]";
1076 status = "QBT_TR(Checking)QBT_TR[CONTEXT=TransferListDelegate]";
1078 case "queuedForChecking":
1079 status = "QBT_TR(Queued for checking)QBT_TR[CONTEXT=TransferListDelegate]";
1081 case "checkingResumeData":
1082 status = "QBT_TR(Checking resume data)QBT_TR[CONTEXT=TransferListDelegate]";
1085 status = "QBT_TR(Stopped)QBT_TR[CONTEXT=TransferListDelegate]";
1088 status = "QBT_TR(Completed)QBT_TR[CONTEXT=TransferListDelegate]";
1091 status = "QBT_TR(Moving)QBT_TR[CONTEXT=TransferListDelegate]";
1093 case "missingFiles":
1094 status = "QBT_TR(Missing Files)QBT_TR[CONTEXT=TransferListDelegate]";
1097 status = "QBT_TR(Errored)QBT_TR[CONTEXT=TransferListDelegate]";
1100 status = "QBT_TR(Unknown)QBT_TR[CONTEXT=HttpServer]";
1103 td.set("text", status);
1104 td.set("title", status);
1108 this.columns["priority"].updateTd = function(td, row) {
1109 const queuePos = this.getRowValue(row);
1110 const formattedQueuePos = (queuePos < 1) ? "*" : queuePos;
1111 td.set("text", formattedQueuePos);
1112 td.set("title", formattedQueuePos);
1115 this.columns["priority"].compareRows = function(row1, row2) {
1116 let row1_val = this.getRowValue(row1);
1117 let row2_val = this.getRowValue(row2);
1122 return compareNumbers(row1_val, row2_val);
1125 // name, category, tags
1126 this.columns["name"].compareRows = function(row1, row2) {
1127 const row1Val = this.getRowValue(row1);
1128 const row2Val = this.getRowValue(row2);
1129 return row1Val.localeCompare(row2Val, undefined, { numeric: true, sensitivity: "base" });
1131 this.columns["category"].compareRows = this.columns["name"].compareRows;
1132 this.columns["tags"].compareRows = this.columns["name"].compareRows;
1135 this.columns["size"].updateTd = function(td, row) {
1136 const size = window.qBittorrent.Misc.friendlyUnit(this.getRowValue(row), false);
1137 td.set("text", size);
1138 td.set("title", size);
1140 this.columns["total_size"].updateTd = this.columns["size"].updateTd;
1143 this.columns["progress"].updateTd = function(td, row) {
1144 const progress = this.getRowValue(row);
1145 let progressFormatted = (progress * 100).round(1);
1146 if ((progressFormatted === 100.0) && (progress !== 1.0))
1147 progressFormatted = 99.9;
1149 if (td.getChildren("div").length > 0) {
1150 const div = td.getChildren("div")[0];
1153 div.setWidth(ProgressColumnWidth - 5);
1155 if (div.getValue() !== progressFormatted)
1156 div.setValue(progressFormatted);
1159 if (ProgressColumnWidth < 0)
1160 ProgressColumnWidth = td.offsetWidth;
1161 td.adopt(new window.qBittorrent.ProgressBar.ProgressBar(progressFormatted.toFloat(), {
1162 "width": ProgressColumnWidth - 5
1168 this.columns["progress"].onResize = function(columnName) {
1169 const pos = this.getColumnPos(columnName);
1170 const trs = this.tableBody.getElements("tr");
1171 ProgressColumnWidth = -1;
1172 for (let i = 0; i < trs.length; ++i) {
1173 const td = trs[i].getElements("td")[pos];
1174 if (ProgressColumnWidth < 0)
1175 ProgressColumnWidth = td.offsetWidth;
1177 this.columns[columnName].updateTd(td, this.rows.get(trs[i].rowId));
1182 this.columns["num_seeds"].updateTd = function(td, row) {
1183 const num_seeds = this.getRowValue(row, 0);
1184 const num_complete = this.getRowValue(row, 1);
1185 let value = num_seeds;
1186 if (num_complete !== -1)
1187 value += " (" + num_complete + ")";
1188 td.set("text", value);
1189 td.set("title", value);
1191 this.columns["num_seeds"].compareRows = function(row1, row2) {
1192 const num_seeds1 = this.getRowValue(row1, 0);
1193 const num_complete1 = this.getRowValue(row1, 1);
1195 const num_seeds2 = this.getRowValue(row2, 0);
1196 const num_complete2 = this.getRowValue(row2, 1);
1198 const result = compareNumbers(num_complete1, num_complete2);
1201 return compareNumbers(num_seeds1, num_seeds2);
1205 this.columns["num_leechs"].updateTd = this.columns["num_seeds"].updateTd;
1206 this.columns["num_leechs"].compareRows = this.columns["num_seeds"].compareRows;
1209 this.columns["dlspeed"].updateTd = function(td, row) {
1210 const speed = window.qBittorrent.Misc.friendlyUnit(this.getRowValue(row), true);
1211 td.set("text", speed);
1212 td.set("title", speed);
1216 this.columns["upspeed"].updateTd = this.columns["dlspeed"].updateTd;
1219 this.columns["eta"].updateTd = function(td, row) {
1220 const eta = window.qBittorrent.Misc.friendlyDuration(this.getRowValue(row), window.qBittorrent.Misc.MAX_ETA);
1221 td.set("text", eta);
1222 td.set("title", eta);
1226 this.columns["ratio"].updateTd = function(td, row) {
1227 const ratio = this.getRowValue(row);
1228 const string = (ratio === -1) ? "∞" : window.qBittorrent.Misc.toFixedPointString(ratio, 2);
1229 td.set("text", string);
1230 td.set("title", string);
1234 this.columns["popularity"].updateTd = function(td, row) {
1235 const value = this.getRowValue(row);
1236 const popularity = (value === -1) ? "∞" : window.qBittorrent.Misc.toFixedPointString(value, 2);
1237 td.set("text", popularity);
1238 td.set("title", popularity);
1242 this.columns["added_on"].updateTd = function(td, row) {
1243 const date = new Date(this.getRowValue(row) * 1000).toLocaleString();
1244 td.set("text", date);
1245 td.set("title", date);
1249 this.columns["completion_on"].updateTd = function(td, row) {
1250 const val = this.getRowValue(row);
1251 if ((val === 0xffffffff) || (val < 0)) {
1253 td.set("title", "");
1256 const date = new Date(this.getRowValue(row) * 1000).toLocaleString();
1257 td.set("text", date);
1258 td.set("title", date);
1262 // dl_limit, up_limit
1263 this.columns["dl_limit"].updateTd = function(td, row) {
1264 const speed = this.getRowValue(row);
1266 td.set("text", "∞");
1267 td.set("title", "∞");
1270 const formattedSpeed = window.qBittorrent.Misc.friendlyUnit(speed, true);
1271 td.set("text", formattedSpeed);
1272 td.set("title", formattedSpeed);
1276 this.columns["up_limit"].updateTd = this.columns["dl_limit"].updateTd;
1278 // downloaded, uploaded, downloaded_session, uploaded_session, amount_left
1279 this.columns["downloaded"].updateTd = this.columns["size"].updateTd;
1280 this.columns["uploaded"].updateTd = this.columns["size"].updateTd;
1281 this.columns["downloaded_session"].updateTd = this.columns["size"].updateTd;
1282 this.columns["uploaded_session"].updateTd = this.columns["size"].updateTd;
1283 this.columns["amount_left"].updateTd = this.columns["size"].updateTd;
1286 this.columns["time_active"].updateTd = function(td, row) {
1287 const activeTime = this.getRowValue(row, 0);
1288 const seedingTime = this.getRowValue(row, 1);
1289 const time = (seedingTime > 0)
1290 ? ("QBT_TR(%1 (seeded for %2))QBT_TR[CONTEXT=TransferListDelegate]"
1291 .replace("%1", window.qBittorrent.Misc.friendlyDuration(activeTime))
1292 .replace("%2", window.qBittorrent.Misc.friendlyDuration(seedingTime)))
1293 : window.qBittorrent.Misc.friendlyDuration(activeTime);
1294 td.set("text", time);
1295 td.set("title", time);
1299 this.columns["completed"].updateTd = this.columns["size"].updateTd;
1302 this.columns["max_ratio"].updateTd = this.columns["ratio"].updateTd;
1305 this.columns["seen_complete"].updateTd = this.columns["completion_on"].updateTd;
1308 this.columns["last_activity"].updateTd = function(td, row) {
1309 const val = this.getRowValue(row);
1311 td.set("text", "∞");
1312 td.set("title", "∞");
1315 const formattedVal = "QBT_TR(%1 ago)QBT_TR[CONTEXT=TransferListDelegate]".replace("%1", window.qBittorrent.Misc.friendlyDuration((new Date() / 1000) - val));
1316 td.set("text", formattedVal);
1317 td.set("title", formattedVal);
1322 this.columns["availability"].updateTd = function(td, row) {
1323 const value = window.qBittorrent.Misc.toFixedPointString(this.getRowValue(row), 3);
1324 td.set("text", value);
1325 td.set("title", value);
1329 this.columns["reannounce"].updateTd = function(td, row) {
1330 const time = window.qBittorrent.Misc.friendlyDuration(this.getRowValue(row));
1331 td.set("text", time);
1332 td.set("title", time);
1336 applyFilter: function(row, filterName, categoryHash, tagHash, trackerHash, filterTerms) {
1337 const state = row["full_data"].state;
1338 const name = row["full_data"].name.toLowerCase();
1339 let inactive = false;
1341 switch (filterName) {
1343 if ((state !== "downloading") && !state.includes("DL"))
1347 if ((state !== "uploading") && (state !== "forcedUP") && (state !== "stalledUP") && (state !== "queuedUP") && (state !== "checkingUP"))
1351 if ((state !== "uploading") && !state.includes("UP"))
1355 if (!state.includes("stopped"))
1359 if (state.includes("stopped"))
1363 if ((state !== "stalledUP") && (state !== "stalledDL"))
1366 case "stalled_uploading":
1367 if (state !== "stalledUP")
1370 case "stalled_downloading":
1371 if (state !== "stalledDL")
1379 if (state === "stalledDL")
1380 r = (row["full_data"].upspeed > 0);
1382 r = (state === "metaDL") || (state === "forcedMetaDL") || (state === "downloading") || (state === "forcedDL") || (state === "uploading") || (state === "forcedUP");
1388 if ((state !== "checkingUP") && (state !== "checkingDL") && (state !== "checkingResumeData"))
1392 if (state !== "moving")
1396 if ((state !== "error") && (state !== "unknown") && (state !== "missingFiles"))
1401 switch (categoryHash) {
1402 case CATEGORIES_ALL:
1403 break; // do nothing
1404 case CATEGORIES_UNCATEGORIZED:
1405 if (row["full_data"].category.length !== 0)
1407 break; // do nothing
1409 if (!useSubcategories) {
1410 if (categoryHash !== window.qBittorrent.Client.genHash(row["full_data"].category))
1414 const selectedCategory = category_list.get(categoryHash);
1415 if (selectedCategory !== undefined) {
1416 const selectedCategoryName = selectedCategory.name + "/";
1417 const torrentCategoryName = row["full_data"].category + "/";
1418 if (!torrentCategoryName.startsWith(selectedCategoryName))
1427 break; // do nothing
1430 if (row["full_data"].tags.length !== 0)
1432 break; // do nothing
1435 const tagHashes = row["full_data"].tags.split(", ").map(tag => window.qBittorrent.Client.genHash(tag));
1436 if (!tagHashes.contains(tagHash))
1442 const trackerHashInt = Number.parseInt(trackerHash, 10);
1443 switch (trackerHashInt) {
1445 break; // do nothing
1446 case TRACKERS_TRACKERLESS:
1447 if (row["full_data"].trackers_count !== 0)
1451 const tracker = trackerList.get(trackerHashInt);
1454 for (const torrents of tracker.trackerTorrentMap.values()) {
1455 if (torrents.includes(row["full_data"].rowId)) {
1467 if ((filterTerms !== undefined) && (filterTerms !== null)) {
1468 if (filterTerms instanceof RegExp) {
1469 if (!filterTerms.test(name))
1473 if ((filterTerms.length > 0) && !window.qBittorrent.Misc.containsAllTerms(name, filterTerms))
1481 getFilteredTorrentsNumber: function(filterName, categoryHash, tagHash, trackerHash) {
1483 const rows = this.rows.getValues();
1485 for (let i = 0; i < rows.length; ++i) {
1486 if (this.applyFilter(rows[i], filterName, categoryHash, tagHash, trackerHash, null))
1492 getFilteredTorrentsHashes: function(filterName, categoryHash, tagHash, trackerHash) {
1493 const rowsHashes = [];
1494 const rows = this.rows.getValues();
1496 for (let i = 0; i < rows.length; ++i) {
1497 if (this.applyFilter(rows[i], filterName, categoryHash, tagHash, trackerHash, null))
1498 rowsHashes.push(rows[i]["rowId"]);
1504 getFilteredAndSortedRows: function() {
1505 const filteredRows = [];
1507 const rows = this.rows.getValues();
1508 const useRegex = $("torrentsFilterRegexBox").checked;
1509 const filterText = $("torrentsFilterInput").value.trim().toLowerCase();
1510 const filterTerms = (filterText.length > 0)
1511 ? (useRegex ? new RegExp(filterText) : filterText.split(" "))
1514 for (let i = 0; i < rows.length; ++i) {
1515 if (this.applyFilter(rows[i], selected_filter, selected_category, selectedTag, selectedTracker, filterTerms)) {
1516 filteredRows.push(rows[i]);
1517 filteredRows[rows[i].rowId] = rows[i];
1521 filteredRows.sort((row1, row2) => {
1522 const column = this.columns[this.sortedColumn];
1523 const res = column.compareRows(row1, row2);
1524 if (this.reverseSort === "0")
1529 return filteredRows;
1532 setupTr: function(tr) {
1533 tr.addEvent("dblclick", function(e) {
1535 this._this.deselectAll();
1536 this._this.selectRow(this.rowId);
1537 const row = this._this.rows.get(this.rowId);
1538 const state = row["full_data"].state;
1539 if (state.includes("stopped"))
1545 tr.addClass("torrentsTableContextMenuTarget");
1548 getCurrentTorrentID: function() {
1549 return this.getSelectedRowId();
1552 onSelectedRowChanged: function() {
1553 updatePropertiesPanel();
1557 const TorrentPeersTable = new Class({
1558 Extends: DynamicTable,
1560 initColumns: function() {
1561 this.newColumn("country", "", "QBT_TR(Country/Region)QBT_TR[CONTEXT=PeerListWidget]", 22, true);
1562 this.newColumn("ip", "", "QBT_TR(IP)QBT_TR[CONTEXT=PeerListWidget]", 80, true);
1563 this.newColumn("port", "", "QBT_TR(Port)QBT_TR[CONTEXT=PeerListWidget]", 35, true);
1564 this.newColumn("connection", "", "QBT_TR(Connection)QBT_TR[CONTEXT=PeerListWidget]", 50, true);
1565 this.newColumn("flags", "", "QBT_TR(Flags)QBT_TR[CONTEXT=PeerListWidget]", 50, true);
1566 this.newColumn("client", "", "QBT_TR(Client)QBT_TR[CONTEXT=PeerListWidget]", 140, true);
1567 this.newColumn("peer_id_client", "", "QBT_TR(Peer ID Client)QBT_TR[CONTEXT=PeerListWidget]", 60, false);
1568 this.newColumn("progress", "", "QBT_TR(Progress)QBT_TR[CONTEXT=PeerListWidget]", 50, true);
1569 this.newColumn("dl_speed", "", "QBT_TR(Down Speed)QBT_TR[CONTEXT=PeerListWidget]", 50, true);
1570 this.newColumn("up_speed", "", "QBT_TR(Up Speed)QBT_TR[CONTEXT=PeerListWidget]", 50, true);
1571 this.newColumn("downloaded", "", "QBT_TR(Downloaded)QBT_TR[CONTEXT=PeerListWidget]", 50, true);
1572 this.newColumn("uploaded", "", "QBT_TR(Uploaded)QBT_TR[CONTEXT=PeerListWidget]", 50, true);
1573 this.newColumn("relevance", "", "QBT_TR(Relevance)QBT_TR[CONTEXT=PeerListWidget]", 30, true);
1574 this.newColumn("files", "", "QBT_TR(Files)QBT_TR[CONTEXT=PeerListWidget]", 100, true);
1576 this.columns["country"].dataProperties.push("country_code");
1577 this.columns["flags"].dataProperties.push("flags_desc");
1578 this.initColumnsFunctions();
1581 initColumnsFunctions: function() {
1584 this.columns["country"].updateTd = function(td, row) {
1585 const country = this.getRowValue(row, 0);
1586 const country_code = this.getRowValue(row, 1);
1588 if (!country_code) {
1589 if (td.getChildren("img").length > 0)
1590 td.getChildren("img")[0].destroy();
1594 const img_path = "images/flags/" + country_code + ".svg";
1596 if (td.getChildren("img").length > 0) {
1597 const img = td.getChildren("img")[0];
1598 img.set("src", img_path);
1599 img.set("class", "flags");
1600 img.set("alt", country);
1601 img.set("title", country);
1604 td.adopt(new Element("img", {
1614 this.columns["ip"].compareRows = function(row1, row2) {
1615 const ip1 = this.getRowValue(row1);
1616 const ip2 = this.getRowValue(row2);
1618 const a = ip1.split(".");
1619 const b = ip2.split(".");
1621 for (let i = 0; i < 4; ++i) {
1630 this.columns["flags"].updateTd = function(td, row) {
1631 td.set("text", this.getRowValue(row, 0));
1632 td.set("title", this.getRowValue(row, 1));
1636 this.columns["progress"].updateTd = function(td, row) {
1637 const progress = this.getRowValue(row);
1638 let progressFormatted = (progress * 100).round(1);
1639 if ((progressFormatted === 100.0) && (progress !== 1.0))
1640 progressFormatted = 99.9;
1641 progressFormatted += "%";
1642 td.set("text", progressFormatted);
1643 td.set("title", progressFormatted);
1646 // dl_speed, up_speed
1647 this.columns["dl_speed"].updateTd = function(td, row) {
1648 const speed = this.getRowValue(row);
1651 td.set("title", "");
1654 const formattedSpeed = window.qBittorrent.Misc.friendlyUnit(speed, true);
1655 td.set("text", formattedSpeed);
1656 td.set("title", formattedSpeed);
1659 this.columns["up_speed"].updateTd = this.columns["dl_speed"].updateTd;
1661 // downloaded, uploaded
1662 this.columns["downloaded"].updateTd = function(td, row) {
1663 const downloaded = window.qBittorrent.Misc.friendlyUnit(this.getRowValue(row), false);
1664 td.set("text", downloaded);
1665 td.set("title", downloaded);
1667 this.columns["uploaded"].updateTd = this.columns["downloaded"].updateTd;
1670 this.columns["relevance"].updateTd = this.columns["progress"].updateTd;
1673 this.columns["files"].updateTd = function(td, row) {
1674 const value = this.getRowValue(row, 0);
1675 td.set("text", value.replace(/\n/g, ";"));
1676 td.set("title", value);
1682 const SearchResultsTable = new Class({
1683 Extends: DynamicTable,
1685 initColumns: function() {
1686 this.newColumn("fileName", "", "QBT_TR(Name)QBT_TR[CONTEXT=SearchResultsTable]", 500, true);
1687 this.newColumn("fileSize", "", "QBT_TR(Size)QBT_TR[CONTEXT=SearchResultsTable]", 100, true);
1688 this.newColumn("nbSeeders", "", "QBT_TR(Seeders)QBT_TR[CONTEXT=SearchResultsTable]", 100, true);
1689 this.newColumn("nbLeechers", "", "QBT_TR(Leechers)QBT_TR[CONTEXT=SearchResultsTable]", 100, true);
1690 this.newColumn("siteUrl", "", "QBT_TR(Search engine)QBT_TR[CONTEXT=SearchResultsTable]", 250, true);
1691 this.newColumn("pubDate", "", "QBT_TR(Published On)QBT_TR[CONTEXT=SearchResultsTable]", 200, true);
1693 this.initColumnsFunctions();
1696 initColumnsFunctions: function() {
1697 const displaySize = function(td, row) {
1698 const size = window.qBittorrent.Misc.friendlyUnit(this.getRowValue(row), false);
1699 td.set("text", size);
1700 td.set("title", size);
1702 const displayNum = function(td, row) {
1703 const value = this.getRowValue(row);
1704 const formattedValue = (value === "-1") ? "Unknown" : value;
1705 td.set("text", formattedValue);
1706 td.set("title", formattedValue);
1708 const displayDate = function(td, row) {
1709 const value = this.getRowValue(row) * 1000;
1710 const formattedValue = (isNaN(value) || (value <= 0)) ? "" : (new Date(value).toLocaleString());
1711 td.set("text", formattedValue);
1712 td.set("title", formattedValue);
1715 this.columns["fileSize"].updateTd = displaySize;
1716 this.columns["nbSeeders"].updateTd = displayNum;
1717 this.columns["nbLeechers"].updateTd = displayNum;
1718 this.columns["pubDate"].updateTd = displayDate;
1721 getFilteredAndSortedRows: function() {
1722 const getSizeFilters = function() {
1723 let minSize = (window.qBittorrent.Search.searchSizeFilter.min > 0.00) ? (window.qBittorrent.Search.searchSizeFilter.min * Math.pow(1024, window.qBittorrent.Search.searchSizeFilter.minUnit)) : 0.00;
1724 let maxSize = (window.qBittorrent.Search.searchSizeFilter.max > 0.00) ? (window.qBittorrent.Search.searchSizeFilter.max * Math.pow(1024, window.qBittorrent.Search.searchSizeFilter.maxUnit)) : 0.00;
1726 if ((minSize > maxSize) && (maxSize > 0.00)) {
1727 const tmp = minSize;
1738 const getSeedsFilters = function() {
1739 let minSeeds = (window.qBittorrent.Search.searchSeedsFilter.min > 0) ? window.qBittorrent.Search.searchSeedsFilter.min : 0;
1740 let maxSeeds = (window.qBittorrent.Search.searchSeedsFilter.max > 0) ? window.qBittorrent.Search.searchSeedsFilter.max : 0;
1742 if ((minSeeds > maxSeeds) && (maxSeeds > 0)) {
1743 const tmp = minSeeds;
1744 minSeeds = maxSeeds;
1754 let filteredRows = [];
1755 const rows = this.rows.getValues();
1756 const searchTerms = window.qBittorrent.Search.searchText.pattern.toLowerCase().split(" ");
1757 const filterTerms = window.qBittorrent.Search.searchText.filterPattern.toLowerCase().split(" ");
1758 const sizeFilters = getSizeFilters();
1759 const seedsFilters = getSeedsFilters();
1760 const searchInTorrentName = $("searchInTorrentName").get("value") === "names";
1762 if (searchInTorrentName || (filterTerms.length > 0) || (window.qBittorrent.Search.searchSizeFilter.min > 0.00) || (window.qBittorrent.Search.searchSizeFilter.max > 0.00)) {
1763 for (let i = 0; i < rows.length; ++i) {
1764 const row = rows[i];
1766 if (searchInTorrentName && !window.qBittorrent.Misc.containsAllTerms(row.full_data.fileName, searchTerms))
1768 if ((filterTerms.length > 0) && !window.qBittorrent.Misc.containsAllTerms(row.full_data.fileName, filterTerms))
1770 if ((sizeFilters.min > 0.00) && (row.full_data.fileSize < sizeFilters.min))
1772 if ((sizeFilters.max > 0.00) && (row.full_data.fileSize > sizeFilters.max))
1774 if ((seedsFilters.min > 0) && (row.full_data.nbSeeders < seedsFilters.min))
1776 if ((seedsFilters.max > 0) && (row.full_data.nbSeeders > seedsFilters.max))
1779 filteredRows.push(row);
1783 filteredRows = rows;
1786 filteredRows.sort((row1, row2) => {
1787 const column = this.columns[this.sortedColumn];
1788 const res = column.compareRows(row1, row2);
1789 if (this.reverseSort === "0")
1795 return filteredRows;
1798 setupTr: function(tr) {
1799 tr.addClass("searchTableRow");
1803 const SearchPluginsTable = new Class({
1804 Extends: DynamicTable,
1806 initColumns: function() {
1807 this.newColumn("fullName", "", "QBT_TR(Name)QBT_TR[CONTEXT=SearchPluginsTable]", 175, true);
1808 this.newColumn("version", "", "QBT_TR(Version)QBT_TR[CONTEXT=SearchPluginsTable]", 100, true);
1809 this.newColumn("url", "", "QBT_TR(Url)QBT_TR[CONTEXT=SearchPluginsTable]", 175, true);
1810 this.newColumn("enabled", "", "QBT_TR(Enabled)QBT_TR[CONTEXT=SearchPluginsTable]", 100, true);
1812 this.initColumnsFunctions();
1815 initColumnsFunctions: function() {
1816 this.columns["enabled"].updateTd = function(td, row) {
1817 const value = this.getRowValue(row);
1819 td.set("text", "QBT_TR(Yes)QBT_TR[CONTEXT=SearchPluginsTable]");
1820 td.set("title", "QBT_TR(Yes)QBT_TR[CONTEXT=SearchPluginsTable]");
1821 td.getParent("tr").addClass("green");
1822 td.getParent("tr").removeClass("red");
1825 td.set("text", "QBT_TR(No)QBT_TR[CONTEXT=SearchPluginsTable]");
1826 td.set("title", "QBT_TR(No)QBT_TR[CONTEXT=SearchPluginsTable]");
1827 td.getParent("tr").addClass("red");
1828 td.getParent("tr").removeClass("green");
1833 setupTr: function(tr) {
1834 tr.addClass("searchPluginsTableRow");
1838 const TorrentTrackersTable = new Class({
1839 Extends: DynamicTable,
1841 initColumns: function() {
1842 this.newColumn("tier", "", "QBT_TR(Tier)QBT_TR[CONTEXT=TrackerListWidget]", 35, true);
1843 this.newColumn("url", "", "QBT_TR(URL)QBT_TR[CONTEXT=TrackerListWidget]", 250, true);
1844 this.newColumn("status", "", "QBT_TR(Status)QBT_TR[CONTEXT=TrackerListWidget]", 125, true);
1845 this.newColumn("peers", "", "QBT_TR(Peers)QBT_TR[CONTEXT=TrackerListWidget]", 75, true);
1846 this.newColumn("seeds", "", "QBT_TR(Seeds)QBT_TR[CONTEXT=TrackerListWidget]", 75, true);
1847 this.newColumn("leeches", "", "QBT_TR(Leeches)QBT_TR[CONTEXT=TrackerListWidget]", 75, true);
1848 this.newColumn("downloaded", "", "QBT_TR(Times Downloaded)QBT_TR[CONTEXT=TrackerListWidget]", 100, true);
1849 this.newColumn("message", "", "QBT_TR(Message)QBT_TR[CONTEXT=TrackerListWidget]", 250, true);
1853 const BulkRenameTorrentFilesTable = new Class({
1854 Extends: DynamicTable,
1857 prevFilterTerms: [],
1858 prevRowsString: null,
1859 prevFilteredRows: [],
1860 prevSortedColumn: null,
1861 prevReverseSort: null,
1862 fileTree: new window.qBittorrent.FileTree.FileTree(),
1864 populateTable: function(root) {
1865 this.fileTree.setRoot(root);
1866 root.children.each((node) => {
1867 this._addNodeToTable(node, 0);
1871 _addNodeToTable: function(node, depth) {
1874 if (node.isFolder) {
1878 checked: node.checked,
1880 original: node.original,
1881 renamed: node.renamed
1885 node.full_data = data;
1886 this.updateRowData(data);
1889 node.data.rowId = node.rowId;
1890 node.full_data = node.data;
1891 this.updateRowData(node.data);
1894 node.children.each((child) => {
1895 this._addNodeToTable(child, depth + 1);
1899 getRoot: function() {
1900 return this.fileTree.getRoot();
1903 getNode: function(rowId) {
1904 return this.fileTree.getNode(rowId);
1907 getRow: function(node) {
1908 const rowId = this.fileTree.getRowId(node);
1909 return this.rows.get(rowId);
1912 getSelectedRows: function() {
1913 const nodes = this.fileTree.toArray();
1915 return nodes.filter(x => x.checked === 0);
1918 initColumns: function() {
1919 // Blocks saving header width (because window width isn't saved)
1920 LocalPreferences.remove("column_" + "checked" + "_width_" + this.dynamicTableDivId);
1921 LocalPreferences.remove("column_" + "original" + "_width_" + this.dynamicTableDivId);
1922 LocalPreferences.remove("column_" + "renamed" + "_width_" + this.dynamicTableDivId);
1923 this.newColumn("checked", "", "", 50, true);
1924 this.newColumn("original", "", "QBT_TR(Original)QBT_TR[CONTEXT=TrackerListWidget]", 270, true);
1925 this.newColumn("renamed", "", "QBT_TR(Renamed)QBT_TR[CONTEXT=TrackerListWidget]", 220, true);
1927 this.initColumnsFunctions();
1931 * Toggles the global checkbox and all checkboxes underneath
1933 toggleGlobalCheckbox: function() {
1934 const checkbox = $("rootMultiRename_cb");
1935 const checkboxes = $$("input.RenamingCB");
1937 for (let i = 0; i < checkboxes.length; ++i) {
1938 const node = this.getNode(i);
1940 if (checkbox.checked || checkbox.indeterminate) {
1941 const cb = checkboxes[i];
1943 cb.indeterminate = false;
1944 cb.state = "checked";
1946 node.full_data.checked = node.checked;
1949 const cb = checkboxes[i];
1951 cb.indeterminate = false;
1952 cb.state = "unchecked";
1954 node.full_data.checked = node.checked;
1958 this.updateGlobalCheckbox();
1961 toggleNodeTreeCheckbox: function(rowId, checkState) {
1962 const node = this.getNode(rowId);
1963 node.checked = checkState;
1964 node.full_data.checked = checkState;
1965 const checkbox = $(`cbRename${rowId}`);
1966 checkbox.checked = node.checked === 0;
1967 checkbox.state = checkbox.checked ? "checked" : "unchecked";
1969 for (let i = 0; i < node.children.length; ++i)
1970 this.toggleNodeTreeCheckbox(node.children[i].rowId, checkState);
1973 updateGlobalCheckbox: function() {
1974 const checkbox = $("rootMultiRename_cb");
1975 const checkboxes = $$("input.RenamingCB");
1976 const isAllChecked = function() {
1977 for (let i = 0; i < checkboxes.length; ++i) {
1978 if (!checkboxes[i].checked)
1983 const isAllUnchecked = function() {
1984 for (let i = 0; i < checkboxes.length; ++i) {
1985 if (checkboxes[i].checked)
1990 if (isAllChecked()) {
1991 checkbox.state = "checked";
1992 checkbox.indeterminate = false;
1993 checkbox.checked = true;
1995 else if (isAllUnchecked()) {
1996 checkbox.state = "unchecked";
1997 checkbox.indeterminate = false;
1998 checkbox.checked = false;
2001 checkbox.state = "partial";
2002 checkbox.indeterminate = true;
2003 checkbox.checked = false;
2007 initColumnsFunctions: function() {
2011 this.columns["checked"].updateTd = function(td, row) {
2012 const id = row.rowId;
2013 const value = this.getRowValue(row);
2015 const treeImg = new Element("img", {
2016 src: "images/L.gif",
2021 const checkbox = new Element("input");
2022 checkbox.set("type", "checkbox");
2023 checkbox.set("id", "cbRename" + id);
2024 checkbox.set("data-id", id);
2025 checkbox.set("class", "RenamingCB");
2026 checkbox.addEvent("click", (e) => {
2027 const node = that.getNode(id);
2028 node.checked = e.target.checked ? 0 : 1;
2029 node.full_data.checked = node.checked;
2030 that.updateGlobalCheckbox();
2031 that.onRowSelectionChange(node);
2032 e.stopPropagation();
2034 checkbox.checked = (value === 0);
2035 checkbox.state = checkbox.checked ? "checked" : "unchecked";
2036 checkbox.indeterminate = false;
2037 td.adopt(treeImg, checkbox);
2041 this.columns["original"].updateTd = function(td, row) {
2042 const id = row.rowId;
2043 const fileNameId = "filesTablefileName" + id;
2044 const node = that.getNode(id);
2046 if (node.isFolder) {
2047 const value = this.getRowValue(row);
2048 const dirImgId = "renameTableDirImg" + id;
2050 // just update file name
2051 $(fileNameId).set("text", value);
2054 const span = new Element("span", {
2058 const dirImg = new Element("img", {
2059 src: "images/directory.svg",
2063 "margin-bottom": -3,
2064 "margin-left": (node.depth * 20)
2068 const html = dirImg.outerHTML + span.outerHTML;
2069 td.set("html", html);
2073 const value = this.getRowValue(row);
2074 const span = new Element("span", {
2078 "margin-left": ((node.depth + 1) * 20)
2081 td.set("html", span.outerHTML);
2086 this.columns["renamed"].updateTd = function(td, row) {
2087 const id = row.rowId;
2088 const fileNameRenamedId = "filesTablefileRenamed" + id;
2089 const value = this.getRowValue(row);
2091 const span = new Element("span", {
2093 id: fileNameRenamedId,
2095 td.set("html", span.outerHTML);
2099 onRowSelectionChange: function(row) {},
2101 selectRow: function() {
2105 reselectRows: function(rowIds) {
2108 this.tableBody.getElements("tr").each((tr) => {
2109 if (rowIds.includes(tr.rowId)) {
2110 const node = that.getNode(tr.rowId);
2112 node.full_data.checked = 0;
2114 const checkbox = tr.children[0].getElement("input");
2115 checkbox.state = "checked";
2116 checkbox.indeterminate = false;
2117 checkbox.checked = true;
2121 this.updateGlobalCheckbox();
2124 altRow: function() {
2125 let addClass = false;
2126 const trs = this.tableBody.getElements("tr");
2128 if (tr.hasClass("invisible"))
2133 tr.removeClass("nonAlt");
2136 tr.removeClass("alt");
2137 tr.addClass("nonAlt");
2139 addClass = !addClass;
2143 _sortNodesByColumn: function(nodes, column) {
2144 nodes.sort((row1, row2) => {
2145 // list folders before files when sorting by name
2146 if (column.name === "original") {
2147 const node1 = this.getNode(row1.data.rowId);
2148 const node2 = this.getNode(row2.data.rowId);
2149 if (node1.isFolder && !node2.isFolder)
2151 if (node2.isFolder && !node1.isFolder)
2155 const res = column.compareRows(row1, row2);
2156 return (this.reverseSort === "0") ? res : -res;
2159 nodes.each((node) => {
2160 if (node.children.length > 0)
2161 this._sortNodesByColumn(node.children, column);
2165 _filterNodes: function(node, filterTerms, filteredRows) {
2166 if (node.isFolder) {
2167 const childAdded = node.children.reduce((acc, child) => {
2168 // we must execute the function before ORing w/ acc or we'll stop checking child nodes after the first successful match
2169 return (this._filterNodes(child, filterTerms, filteredRows) || acc);
2173 const row = this.getRow(node);
2174 filteredRows.push(row);
2179 if (window.qBittorrent.Misc.containsAllTerms(node.original, filterTerms)) {
2180 const row = this.getRow(node);
2181 filteredRows.push(row);
2188 setFilter: function(text) {
2189 const filterTerms = text.trim().toLowerCase().split(" ");
2190 if ((filterTerms.length === 1) && (filterTerms[0] === ""))
2191 this.filterTerms = [];
2193 this.filterTerms = filterTerms;
2196 getFilteredAndSortedRows: function() {
2197 if (this.getRoot() === null)
2200 const generateRowsSignature = function(rows) {
2201 const rowsData = rows.map((row) => {
2202 return row.full_data;
2204 return JSON.stringify(rowsData);
2207 const getFilteredRows = function() {
2208 if (this.filterTerms.length === 0) {
2209 const nodeArray = this.fileTree.toArray();
2210 const filteredRows = nodeArray.map((node) => {
2211 return this.getRow(node);
2213 return filteredRows;
2216 const filteredRows = [];
2217 this.getRoot().children.each((child) => {
2218 this._filterNodes(child, this.filterTerms, filteredRows);
2220 filteredRows.reverse();
2221 return filteredRows;
2224 const hasRowsChanged = function(rowsString, prevRowsStringString) {
2225 const rowsChanged = (rowsString !== prevRowsStringString);
2226 const isFilterTermsChanged = this.filterTerms.reduce((acc, term, index) => {
2227 return (acc || (term !== this.prevFilterTerms[index]));
2229 const isFilterChanged = ((this.filterTerms.length !== this.prevFilterTerms.length)
2230 || ((this.filterTerms.length > 0) && isFilterTermsChanged));
2231 const isSortedColumnChanged = (this.prevSortedColumn !== this.sortedColumn);
2232 const isReverseSortChanged = (this.prevReverseSort !== this.reverseSort);
2234 return (rowsChanged || isFilterChanged || isSortedColumnChanged || isReverseSortChanged);
2237 const rowsString = generateRowsSignature(this.rows);
2238 if (!hasRowsChanged(rowsString, this.prevRowsString))
2239 return this.prevFilteredRows;
2241 // sort, then filter
2242 const column = this.columns[this.sortedColumn];
2243 this._sortNodesByColumn(this.getRoot().children, column);
2244 const filteredRows = getFilteredRows();
2246 this.prevFilterTerms = this.filterTerms;
2247 this.prevRowsString = rowsString;
2248 this.prevFilteredRows = filteredRows;
2249 this.prevSortedColumn = this.sortedColumn;
2250 this.prevReverseSort = this.reverseSort;
2251 return filteredRows;
2254 setIgnored: function(rowId, ignore) {
2255 const row = this.rows.get(rowId);
2257 row.full_data.remaining = 0;
2259 row.full_data.remaining = (row.full_data.size * (1.0 - (row.full_data.progress / 100)));
2262 setupTr: function(tr) {
2263 tr.addEvent("keydown", function(event) {
2264 switch (event.key) {
2266 qBittorrent.PropFiles.collapseFolder(this._this.getSelectedRowId());
2269 qBittorrent.PropFiles.expandFolder(this._this.getSelectedRowId());
2276 const TorrentFilesTable = new Class({
2277 Extends: DynamicTable,
2280 prevFilterTerms: [],
2281 prevRowsString: null,
2282 prevFilteredRows: [],
2283 prevSortedColumn: null,
2284 prevReverseSort: null,
2285 fileTree: new window.qBittorrent.FileTree.FileTree(),
2287 populateTable: function(root) {
2288 this.fileTree.setRoot(root);
2289 root.children.each((node) => {
2290 this._addNodeToTable(node, 0);
2294 _addNodeToTable: function(node, depth) {
2297 if (node.isFolder) {
2301 checked: node.checked,
2302 remaining: node.remaining,
2303 progress: node.progress,
2304 priority: window.qBittorrent.PropFiles.normalizePriority(node.priority),
2305 availability: node.availability,
2311 node.full_data = data;
2312 this.updateRowData(data);
2315 node.data.rowId = node.rowId;
2316 node.full_data = node.data;
2317 this.updateRowData(node.data);
2320 node.children.each((child) => {
2321 this._addNodeToTable(child, depth + 1);
2325 getRoot: function() {
2326 return this.fileTree.getRoot();
2329 getNode: function(rowId) {
2330 return this.fileTree.getNode(rowId);
2333 getRow: function(node) {
2334 const rowId = this.fileTree.getRowId(node);
2335 return this.rows.get(rowId);
2338 initColumns: function() {
2339 this.newColumn("checked", "", "", 50, true);
2340 this.newColumn("name", "", "QBT_TR(Name)QBT_TR[CONTEXT=TrackerListWidget]", 300, true);
2341 this.newColumn("size", "", "QBT_TR(Total Size)QBT_TR[CONTEXT=TrackerListWidget]", 75, true);
2342 this.newColumn("progress", "", "QBT_TR(Progress)QBT_TR[CONTEXT=TrackerListWidget]", 100, true);
2343 this.newColumn("priority", "", "QBT_TR(Download Priority)QBT_TR[CONTEXT=TrackerListWidget]", 150, true);
2344 this.newColumn("remaining", "", "QBT_TR(Remaining)QBT_TR[CONTEXT=TrackerListWidget]", 75, true);
2345 this.newColumn("availability", "", "QBT_TR(Availability)QBT_TR[CONTEXT=TrackerListWidget]", 75, true);
2347 this.initColumnsFunctions();
2350 initColumnsFunctions: function() {
2352 const displaySize = function(td, row) {
2353 const size = window.qBittorrent.Misc.friendlyUnit(this.getRowValue(row), false);
2354 td.set("text", size);
2355 td.set("title", size);
2357 const displayPercentage = function(td, row) {
2358 const value = window.qBittorrent.Misc.friendlyPercentage(this.getRowValue(row));
2359 td.set("text", value);
2360 td.set("title", value);
2364 this.columns["checked"].updateTd = function(td, row) {
2365 const id = row.rowId;
2366 const value = this.getRowValue(row);
2368 if (window.qBittorrent.PropFiles.isDownloadCheckboxExists(id)) {
2369 window.qBittorrent.PropFiles.updateDownloadCheckbox(id, value);
2372 const treeImg = new Element("img", {
2373 src: "images/L.gif",
2378 td.adopt(treeImg, window.qBittorrent.PropFiles.createDownloadCheckbox(id, row.full_data.fileId, value));
2383 this.columns["name"].updateTd = function(td, row) {
2384 const id = row.rowId;
2385 const fileNameId = "filesTablefileName" + id;
2386 const node = that.getNode(id);
2388 if (node.isFolder) {
2389 const value = this.getRowValue(row);
2390 const collapseIconId = "filesTableCollapseIcon" + id;
2391 const dirImgId = "filesTableDirImg" + id;
2393 // just update file name
2394 $(fileNameId).set("text", value);
2397 const collapseIcon = new Element("img", {
2398 src: "images/go-down.svg",
2400 "margin-left": (node.depth * 20)
2402 class: "filesTableCollapseIcon",
2405 onclick: "qBittorrent.PropFiles.collapseIconClicked(this)"
2407 const span = new Element("span", {
2411 const dirImg = new Element("img", {
2412 src: "images/directory.svg",
2420 const html = collapseIcon.outerHTML + dirImg.outerHTML + span.outerHTML;
2421 td.set("html", html);
2425 const value = this.getRowValue(row);
2426 const span = new Element("span", {
2430 "margin-left": ((node.depth + 1) * 20)
2433 td.set("html", span.outerHTML);
2438 this.columns["size"].updateTd = displaySize;
2441 this.columns["progress"].updateTd = function(td, row) {
2442 const id = row.rowId;
2443 const value = this.getRowValue(row);
2445 const progressBar = $("pbf_" + id);
2446 if (progressBar === null) {
2447 td.adopt(new window.qBittorrent.ProgressBar.ProgressBar(value.toFloat(), {
2453 progressBar.setValue(value.toFloat());
2458 this.columns["priority"].updateTd = function(td, row) {
2459 const id = row.rowId;
2460 const value = this.getRowValue(row);
2462 if (window.qBittorrent.PropFiles.isPriorityComboExists(id))
2463 window.qBittorrent.PropFiles.updatePriorityCombo(id, value);
2465 td.adopt(window.qBittorrent.PropFiles.createPriorityCombo(id, row.full_data.fileId, value));
2468 // remaining, availability
2469 this.columns["remaining"].updateTd = displaySize;
2470 this.columns["availability"].updateTd = displayPercentage;
2473 altRow: function() {
2474 let addClass = false;
2475 const trs = this.tableBody.getElements("tr");
2477 if (tr.hasClass("invisible"))
2482 tr.removeClass("nonAlt");
2485 tr.removeClass("alt");
2486 tr.addClass("nonAlt");
2488 addClass = !addClass;
2492 _sortNodesByColumn: function(nodes, column) {
2493 nodes.sort((row1, row2) => {
2494 // list folders before files when sorting by name
2495 if (column.name === "name") {
2496 const node1 = this.getNode(row1.data.rowId);
2497 const node2 = this.getNode(row2.data.rowId);
2498 if (node1.isFolder && !node2.isFolder)
2500 if (node2.isFolder && !node1.isFolder)
2504 const res = column.compareRows(row1, row2);
2505 return (this.reverseSort === "0") ? res : -res;
2508 nodes.each((node) => {
2509 if (node.children.length > 0)
2510 this._sortNodesByColumn(node.children, column);
2514 _filterNodes: function(node, filterTerms, filteredRows) {
2515 if (node.isFolder) {
2516 const childAdded = node.children.reduce((acc, child) => {
2517 // we must execute the function before ORing w/ acc or we'll stop checking child nodes after the first successful match
2518 return (this._filterNodes(child, filterTerms, filteredRows) || acc);
2522 const row = this.getRow(node);
2523 filteredRows.push(row);
2528 if (window.qBittorrent.Misc.containsAllTerms(node.name, filterTerms)) {
2529 const row = this.getRow(node);
2530 filteredRows.push(row);
2537 setFilter: function(text) {
2538 const filterTerms = text.trim().toLowerCase().split(" ");
2539 if ((filterTerms.length === 1) && (filterTerms[0] === ""))
2540 this.filterTerms = [];
2542 this.filterTerms = filterTerms;
2545 getFilteredAndSortedRows: function() {
2546 if (this.getRoot() === null)
2549 const generateRowsSignature = function(rows) {
2550 const rowsData = rows.map((row) => {
2551 return row.full_data;
2553 return JSON.stringify(rowsData);
2556 const getFilteredRows = function() {
2557 if (this.filterTerms.length === 0) {
2558 const nodeArray = this.fileTree.toArray();
2559 const filteredRows = nodeArray.map((node) => {
2560 return this.getRow(node);
2562 return filteredRows;
2565 const filteredRows = [];
2566 this.getRoot().children.each((child) => {
2567 this._filterNodes(child, this.filterTerms, filteredRows);
2569 filteredRows.reverse();
2570 return filteredRows;
2573 const hasRowsChanged = function(rowsString, prevRowsStringString) {
2574 const rowsChanged = (rowsString !== prevRowsStringString);
2575 const isFilterTermsChanged = this.filterTerms.reduce((acc, term, index) => {
2576 return (acc || (term !== this.prevFilterTerms[index]));
2578 const isFilterChanged = ((this.filterTerms.length !== this.prevFilterTerms.length)
2579 || ((this.filterTerms.length > 0) && isFilterTermsChanged));
2580 const isSortedColumnChanged = (this.prevSortedColumn !== this.sortedColumn);
2581 const isReverseSortChanged = (this.prevReverseSort !== this.reverseSort);
2583 return (rowsChanged || isFilterChanged || isSortedColumnChanged || isReverseSortChanged);
2586 const rowsString = generateRowsSignature(this.rows);
2587 if (!hasRowsChanged(rowsString, this.prevRowsString))
2588 return this.prevFilteredRows;
2590 // sort, then filter
2591 const column = this.columns[this.sortedColumn];
2592 this._sortNodesByColumn(this.getRoot().children, column);
2593 const filteredRows = getFilteredRows();
2595 this.prevFilterTerms = this.filterTerms;
2596 this.prevRowsString = rowsString;
2597 this.prevFilteredRows = filteredRows;
2598 this.prevSortedColumn = this.sortedColumn;
2599 this.prevReverseSort = this.reverseSort;
2600 return filteredRows;
2603 setIgnored: function(rowId, ignore) {
2604 const row = this.rows.get(rowId);
2606 row.full_data.remaining = 0;
2608 row.full_data.remaining = (row.full_data.size * (1.0 - (row.full_data.progress / 100)));
2611 setupTr: function(tr) {
2612 tr.addEvent("keydown", function(event) {
2613 switch (event.key) {
2615 qBittorrent.PropFiles.collapseFolder(this._this.getSelectedRowId());
2618 qBittorrent.PropFiles.expandFolder(this._this.getSelectedRowId());
2625 const RssFeedTable = new Class({
2626 Extends: DynamicTable,
2627 initColumns: function() {
2628 this.newColumn("state_icon", "", "", 30, true);
2629 this.newColumn("name", "", "QBT_TR(RSS feeds)QBT_TR[CONTEXT=FeedListWidget]", -1, true);
2631 this.columns["state_icon"].dataProperties[0] = "";
2633 // map name row to "[name] ([unread])"
2634 this.columns["name"].dataProperties.push("unread");
2635 this.columns["name"].updateTd = function(td, row) {
2636 const name = this.getRowValue(row, 0);
2637 const unreadCount = this.getRowValue(row, 1);
2638 const value = name + " (" + unreadCount + ")";
2639 td.set("text", value);
2640 td.set("title", value);
2643 setupHeaderMenu: function() {},
2644 setupHeaderEvents: function() {},
2645 getFilteredAndSortedRows: function() {
2646 return this.rows.getValues();
2648 selectRow: function(rowId) {
2649 this.selectedRows.push(rowId);
2651 this.onSelectedRowChanged();
2653 const rows = this.rows.getValues();
2655 for (let i = 0; i < rows.length; ++i) {
2656 if (rows[i].rowId === rowId) {
2657 path = rows[i].full_data.dataPath;
2661 window.qBittorrent.Rss.showRssFeed(path);
2663 setupTr: function(tr) {
2664 tr.addEvent("dblclick", function(e) {
2665 if (this.rowId !== 0) {
2666 window.qBittorrent.Rss.moveItem(this._this.rows.get(this.rowId).full_data.dataPath);
2671 updateRow: function(tr, fullUpdate) {
2672 const row = this.rows.get(tr.rowId);
2673 const data = row[fullUpdate ? "full_data" : "data"];
2675 const tds = tr.getElements("td");
2676 for (let i = 0; i < this.columns.length; ++i) {
2677 if (Object.hasOwn(data, this.columns[i].dataProperties[0]))
2678 this.columns[i].updateTd(tds[i], row);
2681 tds[0].style.overflow = "visible";
2682 const indentation = row.full_data.indentation;
2683 tds[0].style.paddingLeft = (indentation * 32 + 4) + "px";
2684 tds[1].style.paddingLeft = (indentation * 32 + 4) + "px";
2686 updateIcons: function() {
2688 this.rows.each(row => {
2690 switch (row.full_data.status) {
2692 img_path = "images/application-rss.svg";
2695 img_path = "images/task-reject.svg";
2698 img_path = "images/spinner.gif";
2701 img_path = "images/mail-inbox.svg";
2704 img_path = "images/folder-documents.svg";
2708 for (let i = 0; i < this.tableBody.rows.length; ++i) {
2709 if (this.tableBody.rows[i].rowId === row.rowId) {
2710 td = this.tableBody.rows[i].children[0];
2714 if (td.getChildren("img").length > 0) {
2715 const img = td.getChildren("img")[0];
2716 if (!img.src.includes(img_path)) {
2717 img.set("src", img_path);
2718 img.set("title", status);
2722 td.adopt(new Element("img", {
2724 "class": "stateIcon",
2731 newColumn: function(name, style, caption, defaultWidth, defaultVisible) {
2733 column["name"] = name;
2734 column["title"] = name;
2735 column["visible"] = defaultVisible;
2736 column["force_hide"] = false;
2737 column["caption"] = caption;
2738 column["style"] = style;
2739 if (defaultWidth !== -1)
2740 column["width"] = defaultWidth;
2742 column["dataProperties"] = [name];
2743 column["getRowValue"] = function(row, pos) {
2744 if (pos === undefined)
2746 return row["full_data"][this.dataProperties[pos]];
2748 column["compareRows"] = function(row1, row2) {
2749 const value1 = this.getRowValue(row1);
2750 const value2 = this.getRowValue(row2);
2751 if ((typeof(value1) === "number") && (typeof(value2) === "number"))
2752 return compareNumbers(value1, value2);
2753 return window.qBittorrent.Misc.naturalSortCollator.compare(value1, value2);
2755 column["updateTd"] = function(td, row) {
2756 const value = this.getRowValue(row);
2757 td.set("text", value);
2758 td.set("title", value);
2760 column["onResize"] = null;
2761 this.columns.push(column);
2762 this.columns[name] = column;
2764 this.hiddenTableHeader.appendChild(new Element("th"));
2765 this.fixedTableHeader.appendChild(new Element("th"));
2767 setupCommonEvents: function() {
2768 const scrollFn = function() {
2769 $(this.dynamicTableFixedHeaderDivId).getElements("table")[0].style.left = -$(this.dynamicTableDivId).scrollLeft + "px";
2772 $(this.dynamicTableDivId).addEvent("scroll", scrollFn);
2776 const RssArticleTable = new Class({
2777 Extends: DynamicTable,
2778 initColumns: function() {
2779 this.newColumn("name", "", "QBT_TR(Torrents: (double-click to download))QBT_TR[CONTEXT=RSSWidget]", -1, true);
2781 setupHeaderMenu: function() {},
2782 setupHeaderEvents: function() {},
2783 getFilteredAndSortedRows: function() {
2784 return this.rows.getValues();
2786 selectRow: function(rowId) {
2787 this.selectedRows.push(rowId);
2789 this.onSelectedRowChanged();
2791 const rows = this.rows.getValues();
2794 for (let i = 0; i < rows.length; ++i) {
2795 if (rows[i].rowId === rowId) {
2796 articleId = rows[i].full_data.dataId;
2797 feedUid = rows[i].full_data.feedUid;
2798 this.tableBody.rows[rows[i].rowId].removeClass("unreadArticle");
2802 window.qBittorrent.Rss.showDetails(feedUid, articleId);
2804 setupTr: function(tr) {
2805 tr.addEvent("dblclick", function(e) {
2806 showDownloadPage([this._this.rows.get(this.rowId).full_data.torrentURL]);
2809 tr.addClass("torrentsTableContextMenuTarget");
2811 updateRow: function(tr, fullUpdate) {
2812 const row = this.rows.get(tr.rowId);
2813 const data = row[fullUpdate ? "full_data" : "data"];
2814 if (!row.full_data.isRead)
2815 tr.addClass("unreadArticle");
2817 tr.removeClass("unreadArticle");
2819 const tds = tr.getElements("td");
2820 for (let i = 0; i < this.columns.length; ++i) {
2821 if (Object.hasOwn(data, this.columns[i].dataProperties[0]))
2822 this.columns[i].updateTd(tds[i], row);
2826 newColumn: function(name, style, caption, defaultWidth, defaultVisible) {
2828 column["name"] = name;
2829 column["title"] = name;
2830 column["visible"] = defaultVisible;
2831 column["force_hide"] = false;
2832 column["caption"] = caption;
2833 column["style"] = style;
2834 if (defaultWidth !== -1)
2835 column["width"] = defaultWidth;
2837 column["dataProperties"] = [name];
2838 column["getRowValue"] = function(row, pos) {
2839 if (pos === undefined)
2841 return row["full_data"][this.dataProperties[pos]];
2843 column["compareRows"] = function(row1, row2) {
2844 const value1 = this.getRowValue(row1);
2845 const value2 = this.getRowValue(row2);
2846 if ((typeof(value1) === "number") && (typeof(value2) === "number"))
2847 return compareNumbers(value1, value2);
2848 return window.qBittorrent.Misc.naturalSortCollator.compare(value1, value2);
2850 column["updateTd"] = function(td, row) {
2851 const value = this.getRowValue(row);
2852 td.set("text", value);
2853 td.set("title", value);
2855 column["onResize"] = null;
2856 this.columns.push(column);
2857 this.columns[name] = column;
2859 this.hiddenTableHeader.appendChild(new Element("th"));
2860 this.fixedTableHeader.appendChild(new Element("th"));
2862 setupCommonEvents: function() {
2863 const scrollFn = function() {
2864 $(this.dynamicTableFixedHeaderDivId).getElements("table")[0].style.left = -$(this.dynamicTableDivId).scrollLeft + "px";
2867 $(this.dynamicTableDivId).addEvent("scroll", scrollFn);
2871 const RssDownloaderRulesTable = new Class({
2872 Extends: DynamicTable,
2873 initColumns: function() {
2874 this.newColumn("checked", "", "", 30, true);
2875 this.newColumn("name", "", "", -1, true);
2877 this.columns["checked"].updateTd = function(td, row) {
2878 if ($("cbRssDlRule" + row.rowId) === null) {
2879 const checkbox = new Element("input");
2880 checkbox.set("type", "checkbox");
2881 checkbox.set("id", "cbRssDlRule" + row.rowId);
2882 checkbox.checked = row.full_data.checked;
2884 checkbox.addEvent("click", function(e) {
2885 window.qBittorrent.RssDownloader.rssDownloaderRulesTable.updateRowData({
2887 checked: this.checked
2889 window.qBittorrent.RssDownloader.modifyRuleState(row.full_data.name, "enabled", this.checked);
2890 e.stopPropagation();
2893 td.append(checkbox);
2896 $("cbRssDlRule" + row.rowId).checked = row.full_data.checked;
2900 setupHeaderMenu: function() {},
2901 setupHeaderEvents: function() {},
2902 getFilteredAndSortedRows: function() {
2903 return this.rows.getValues();
2905 setupTr: function(tr) {
2906 tr.addEvent("dblclick", function(e) {
2907 window.qBittorrent.RssDownloader.renameRule(this._this.rows.get(this.rowId).full_data.name);
2911 newColumn: function(name, style, caption, defaultWidth, defaultVisible) {
2913 column["name"] = name;
2914 column["title"] = name;
2915 column["visible"] = defaultVisible;
2916 column["force_hide"] = false;
2917 column["caption"] = caption;
2918 column["style"] = style;
2919 if (defaultWidth !== -1)
2920 column["width"] = defaultWidth;
2922 column["dataProperties"] = [name];
2923 column["getRowValue"] = function(row, pos) {
2924 if (pos === undefined)
2926 return row["full_data"][this.dataProperties[pos]];
2928 column["compareRows"] = function(row1, row2) {
2929 const value1 = this.getRowValue(row1);
2930 const value2 = this.getRowValue(row2);
2931 if ((typeof(value1) === "number") && (typeof(value2) === "number"))
2932 return compareNumbers(value1, value2);
2933 return window.qBittorrent.Misc.naturalSortCollator.compare(value1, value2);
2935 column["updateTd"] = function(td, row) {
2936 const value = this.getRowValue(row);
2937 td.set("text", value);
2938 td.set("title", value);
2940 column["onResize"] = null;
2941 this.columns.push(column);
2942 this.columns[name] = column;
2944 this.hiddenTableHeader.appendChild(new Element("th"));
2945 this.fixedTableHeader.appendChild(new Element("th"));
2947 selectRow: function(rowId) {
2948 this.selectedRows.push(rowId);
2950 this.onSelectedRowChanged();
2952 const rows = this.rows.getValues();
2954 for (let i = 0; i < rows.length; ++i) {
2955 if (rows[i].rowId === rowId) {
2956 name = rows[i].full_data.name;
2960 window.qBittorrent.RssDownloader.showRule(name);
2964 const RssDownloaderFeedSelectionTable = new Class({
2965 Extends: DynamicTable,
2966 initColumns: function() {
2967 this.newColumn("checked", "", "", 30, true);
2968 this.newColumn("name", "", "", -1, true);
2970 this.columns["checked"].updateTd = function(td, row) {
2971 if ($("cbRssDlFeed" + row.rowId) === null) {
2972 const checkbox = new Element("input");
2973 checkbox.set("type", "checkbox");
2974 checkbox.set("id", "cbRssDlFeed" + row.rowId);
2975 checkbox.checked = row.full_data.checked;
2977 checkbox.addEvent("click", function(e) {
2978 window.qBittorrent.RssDownloader.rssDownloaderFeedSelectionTable.updateRowData({
2980 checked: this.checked
2982 e.stopPropagation();
2985 td.append(checkbox);
2988 $("cbRssDlFeed" + row.rowId).checked = row.full_data.checked;
2992 setupHeaderMenu: function() {},
2993 setupHeaderEvents: function() {},
2994 getFilteredAndSortedRows: function() {
2995 return this.rows.getValues();
2997 newColumn: function(name, style, caption, defaultWidth, defaultVisible) {
2999 column["name"] = name;
3000 column["title"] = name;
3001 column["visible"] = defaultVisible;
3002 column["force_hide"] = false;
3003 column["caption"] = caption;
3004 column["style"] = style;
3005 if (defaultWidth !== -1)
3006 column["width"] = defaultWidth;
3008 column["dataProperties"] = [name];
3009 column["getRowValue"] = function(row, pos) {
3010 if (pos === undefined)
3012 return row["full_data"][this.dataProperties[pos]];
3014 column["compareRows"] = function(row1, row2) {
3015 const value1 = this.getRowValue(row1);
3016 const value2 = this.getRowValue(row2);
3017 if ((typeof(value1) === "number") && (typeof(value2) === "number"))
3018 return compareNumbers(value1, value2);
3019 return window.qBittorrent.Misc.naturalSortCollator.compare(value1, value2);
3021 column["updateTd"] = function(td, row) {
3022 const value = this.getRowValue(row);
3023 td.set("text", value);
3024 td.set("title", value);
3026 column["onResize"] = null;
3027 this.columns.push(column);
3028 this.columns[name] = column;
3030 this.hiddenTableHeader.appendChild(new Element("th"));
3031 this.fixedTableHeader.appendChild(new Element("th"));
3033 selectRow: function() {}
3036 const RssDownloaderArticlesTable = new Class({
3037 Extends: DynamicTable,
3038 initColumns: function() {
3039 this.newColumn("name", "", "", -1, true);
3041 setupHeaderMenu: function() {},
3042 setupHeaderEvents: function() {},
3043 getFilteredAndSortedRows: function() {
3044 return this.rows.getValues();
3046 newColumn: function(name, style, caption, defaultWidth, defaultVisible) {
3048 column["name"] = name;
3049 column["title"] = name;
3050 column["visible"] = defaultVisible;
3051 column["force_hide"] = false;
3052 column["caption"] = caption;
3053 column["style"] = style;
3054 if (defaultWidth !== -1)
3055 column["width"] = defaultWidth;
3057 column["dataProperties"] = [name];
3058 column["getRowValue"] = function(row, pos) {
3059 if (pos === undefined)
3061 return row["full_data"][this.dataProperties[pos]];
3063 column["compareRows"] = function(row1, row2) {
3064 const value1 = this.getRowValue(row1);
3065 const value2 = this.getRowValue(row2);
3066 if ((typeof(value1) === "number") && (typeof(value2) === "number"))
3067 return compareNumbers(value1, value2);
3068 return window.qBittorrent.Misc.naturalSortCollator.compare(value1, value2);
3070 column["updateTd"] = function(td, row) {
3071 const value = this.getRowValue(row);
3072 td.set("text", value);
3073 td.set("title", value);
3075 column["onResize"] = null;
3076 this.columns.push(column);
3077 this.columns[name] = column;
3079 this.hiddenTableHeader.appendChild(new Element("th"));
3080 this.fixedTableHeader.appendChild(new Element("th"));
3082 selectRow: function() {},
3083 updateRow: function(tr, fullUpdate) {
3084 const row = this.rows.get(tr.rowId);
3085 const data = row[fullUpdate ? "full_data" : "data"];
3087 if (row.full_data.isFeed) {
3088 tr.addClass("articleTableFeed");
3089 tr.removeClass("articleTableArticle");
3092 tr.removeClass("articleTableFeed");
3093 tr.addClass("articleTableArticle");
3096 const tds = tr.getElements("td");
3097 for (let i = 0; i < this.columns.length; ++i) {
3098 if (Object.hasOwn(data, this.columns[i].dataProperties[0]))
3099 this.columns[i].updateTd(tds[i], row);
3105 const LogMessageTable = new Class({
3106 Extends: DynamicTable,
3110 filteredLength: function() {
3111 return this.tableBody.getElements("tr").length;
3114 initColumns: function() {
3115 this.newColumn("rowId", "", "QBT_TR(ID)QBT_TR[CONTEXT=ExecutionLogWidget]", 50, true);
3116 this.newColumn("message", "", "QBT_TR(Message)QBT_TR[CONTEXT=ExecutionLogWidget]", 350, true);
3117 this.newColumn("timestamp", "", "QBT_TR(Timestamp)QBT_TR[CONTEXT=ExecutionLogWidget]", 150, true);
3118 this.newColumn("type", "", "QBT_TR(Log Type)QBT_TR[CONTEXT=ExecutionLogWidget]", 100, true);
3119 this.initColumnsFunctions();
3122 initColumnsFunctions: function() {
3123 this.columns["timestamp"].updateTd = function(td, row) {
3124 const date = new Date(this.getRowValue(row) * 1000).toLocaleString();
3125 td.set({ "text": date, "title": date });
3128 this.columns["type"].updateTd = function(td, row) {
3129 // Type of the message: Log::NORMAL: 1, Log::INFO: 2, Log::WARNING: 4, Log::CRITICAL: 8
3130 let logLevel, addClass;
3131 switch (this.getRowValue(row).toInt()) {
3133 logLevel = "QBT_TR(Normal)QBT_TR[CONTEXT=ExecutionLogWidget]";
3134 addClass = "logNormal";
3137 logLevel = "QBT_TR(Info)QBT_TR[CONTEXT=ExecutionLogWidget]";
3138 addClass = "logInfo";
3141 logLevel = "QBT_TR(Warning)QBT_TR[CONTEXT=ExecutionLogWidget]";
3142 addClass = "logWarning";
3145 logLevel = "QBT_TR(Critical)QBT_TR[CONTEXT=ExecutionLogWidget]";
3146 addClass = "logCritical";
3149 logLevel = "QBT_TR(Unknown)QBT_TR[CONTEXT=ExecutionLogWidget]";
3150 addClass = "logUnknown";
3153 td.set({ "text": logLevel, "title": logLevel });
3154 td.getParent("tr").set("class", "logTableRow " + addClass);
3158 getFilteredAndSortedRows: function() {
3159 let filteredRows = [];
3160 const rows = this.rows.getValues();
3161 this.filterText = window.qBittorrent.Log.getFilterText();
3162 const filterTerms = (this.filterText.length > 0) ? this.filterText.toLowerCase().split(" ") : [];
3163 const logLevels = window.qBittorrent.Log.getSelectedLevels();
3164 if ((filterTerms.length > 0) || (logLevels.length < 4)) {
3165 for (let i = 0; i < rows.length; ++i) {
3166 if (!logLevels.includes(rows[i].full_data.type.toString()))
3169 if ((filterTerms.length > 0) && !window.qBittorrent.Misc.containsAllTerms(rows[i].full_data.message, filterTerms))
3172 filteredRows.push(rows[i]);
3176 filteredRows = rows;
3179 filteredRows.sort((row1, row2) => {
3180 const column = this.columns[this.sortedColumn];
3181 const res = column.compareRows(row1, row2);
3182 return (this.reverseSort === "0") ? res : -res;
3185 return filteredRows;
3188 setupCommonEvents: function() {},
3190 setupTr: function(tr) {
3191 tr.addClass("logTableRow");
3195 const LogPeerTable = new Class({
3196 Extends: LogMessageTable,
3198 initColumns: function() {
3199 this.newColumn("rowId", "", "QBT_TR(ID)QBT_TR[CONTEXT=ExecutionLogWidget]", 50, true);
3200 this.newColumn("ip", "", "QBT_TR(IP)QBT_TR[CONTEXT=ExecutionLogWidget]", 150, true);
3201 this.newColumn("timestamp", "", "QBT_TR(Timestamp)QBT_TR[CONTEXT=ExecutionLogWidget]", 150, true);
3202 this.newColumn("blocked", "", "QBT_TR(Status)QBT_TR[CONTEXT=ExecutionLogWidget]", 150, true);
3203 this.newColumn("reason", "", "QBT_TR(Reason)QBT_TR[CONTEXT=ExecutionLogWidget]", 150, true);
3205 this.columns["timestamp"].updateTd = function(td, row) {
3206 const date = new Date(this.getRowValue(row) * 1000).toLocaleString();
3207 td.set({ "text": date, "title": date });
3210 this.columns["blocked"].updateTd = function(td, row) {
3211 let status, addClass;
3212 if (this.getRowValue(row)) {
3213 status = "QBT_TR(Blocked)QBT_TR[CONTEXT=ExecutionLogWidget]";
3214 addClass = "peerBlocked";
3217 status = "QBT_TR(Banned)QBT_TR[CONTEXT=ExecutionLogWidget]";
3218 addClass = "peerBanned";
3220 td.set({ "text": status, "title": status });
3221 td.getParent("tr").set("class", "logTableRow " + addClass);
3225 getFilteredAndSortedRows: function() {
3226 let filteredRows = [];
3227 const rows = this.rows.getValues();
3228 this.filterText = window.qBittorrent.Log.getFilterText();
3229 const filterTerms = (this.filterText.length > 0) ? this.filterText.toLowerCase().split(" ") : [];
3230 if (filterTerms.length > 0) {
3231 for (let i = 0; i < rows.length; ++i) {
3232 if ((filterTerms.length > 0) && !window.qBittorrent.Misc.containsAllTerms(rows[i].full_data.ip, filterTerms))
3235 filteredRows.push(rows[i]);
3239 filteredRows = rows;
3242 filteredRows.sort((row1, row2) => {
3243 const column = this.columns[this.sortedColumn];
3244 const res = column.compareRows(row1, row2);
3245 return (this.reverseSort === "0") ? res : -res;
3248 return filteredRows;
3255 Object.freeze(window.qBittorrent.DynamicTable);
3257 /*************************************************************/