/* * MIT License * Copyright (c) 2008 Ishan Arora & Christophe Dumez * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /************************************************************** Script : Dynamic Table Version : 0.5 Authors : Ishan Arora & Christophe Dumez Desc : Programmable sortable table Licence : Open Source MIT Licence **************************************************************/ 'use strict'; if (window.qBittorrent === undefined) { window.qBittorrent = {}; } window.qBittorrent.DynamicTable = (function() { const exports = function() { return { TorrentsTable: TorrentsTable, TorrentPeersTable: TorrentPeersTable, SearchResultsTable: SearchResultsTable, SearchPluginsTable: SearchPluginsTable, TorrentTrackersTable: TorrentTrackersTable, BulkRenameTorrentFilesTable: BulkRenameTorrentFilesTable, TorrentFilesTable: TorrentFilesTable, LogMessageTable: LogMessageTable, LogPeerTable: LogPeerTable, RssFeedTable: RssFeedTable, RssArticleTable: RssArticleTable, RssDownloaderRulesTable: RssDownloaderRulesTable, RssDownloaderFeedSelectionTable: RssDownloaderFeedSelectionTable, RssDownloaderArticlesTable: RssDownloaderArticlesTable }; }; const compareNumbers = (val1, val2) => { if (val1 < val2) return -1; if (val1 > val2) return 1; return 0; }; let DynamicTableHeaderContextMenuClass = null; let ProgressColumnWidth = -1; const DynamicTable = new Class({ initialize: function() {}, setup: function(dynamicTableDivId, dynamicTableFixedHeaderDivId, contextMenu) { this.dynamicTableDivId = dynamicTableDivId; this.dynamicTableFixedHeaderDivId = dynamicTableFixedHeaderDivId; this.fixedTableHeader = $(dynamicTableFixedHeaderDivId).getElements('tr')[0]; this.hiddenTableHeader = $(dynamicTableDivId).getElements('tr')[0]; this.tableBody = $(dynamicTableDivId).getElements('tbody')[0]; this.rows = new Hash(); this.selectedRows = []; this.columns = []; this.contextMenu = contextMenu; this.sortedColumn = LocalPreferences.get('sorted_column_' + this.dynamicTableDivId, 0); this.reverseSort = LocalPreferences.get('reverse_sort_' + this.dynamicTableDivId, '0'); this.initColumns(); this.loadColumnsOrder(); this.updateTableHeaders(); this.setupCommonEvents(); this.setupHeaderEvents(); this.setupHeaderMenu(); this.setSortedColumnIcon(this.sortedColumn, null, (this.reverseSort === '1')); }, setupCommonEvents: function() { const scrollFn = function() { $(this.dynamicTableFixedHeaderDivId).getElements('table')[0].style.left = -$(this.dynamicTableDivId).scrollLeft + 'px'; }.bind(this); $(this.dynamicTableDivId).addEvent('scroll', scrollFn); // if the table exists within a panel if ($(this.dynamicTableDivId).getParent('.panel')) { const resizeFn = function() { const panel = $(this.dynamicTableDivId).getParent('.panel'); let h = panel.getBoundingClientRect().height - $(this.dynamicTableFixedHeaderDivId).getBoundingClientRect().height; $(this.dynamicTableDivId).style.height = h + 'px'; // Workaround due to inaccurate calculation of elements heights by browser let n = 2; while (panel.clientWidth != panel.offsetWidth && n > 0) { // is panel vertical scrollbar visible ? --n; h -= 0.5; $(this.dynamicTableDivId).style.height = h + 'px'; } this.lastPanelHeight = panel.getBoundingClientRect().height; }.bind(this); $(this.dynamicTableDivId).getParent('.panel').addEvent('resize', resizeFn); this.lastPanelHeight = 0; // Workaround. Resize event is called not always (for example it isn't called when browser window changes it's size) const checkResizeFn = function() { const tableDiv = $(this.dynamicTableDivId); // dynamicTableDivId is not visible on the UI if (!tableDiv) { return; } const panel = tableDiv.getParent('.panel'); if (this.lastPanelHeight != panel.getBoundingClientRect().height) { this.lastPanelHeight = panel.getBoundingClientRect().height; panel.fireEvent('resize'); } }.bind(this); setInterval(checkResizeFn, 500); } }, setupHeaderEvents: function() { this.currentHeaderAction = ''; this.canResize = false; const resetElementBorderStyle = function(el, side) { if (side === 'left' || side !== 'right') { el.setStyle('border-left-style', ''); el.setStyle('border-left-color', ''); el.setStyle('border-left-width', ''); } if (side === 'right' || side !== 'left') { el.setStyle('border-right-style', ''); el.setStyle('border-right-color', ''); el.setStyle('border-right-width', ''); } }; const mouseMoveFn = function(e) { const brect = e.target.getBoundingClientRect(); const mouseXRelative = e.event.clientX - brect.left; if (this.currentHeaderAction === '') { if (brect.width - mouseXRelative < 5) { this.resizeTh = e.target; this.canResize = true; e.target.getParent("tr").style.cursor = 'col-resize'; } else if ((mouseXRelative < 5) && e.target.getPrevious('[class=""]')) { this.resizeTh = e.target.getPrevious('[class=""]'); this.canResize = true; e.target.getParent("tr").style.cursor = 'col-resize'; } else { this.canResize = false; e.target.getParent("tr").style.cursor = ''; } } if (this.currentHeaderAction === 'drag') { const previousVisibleSibling = e.target.getPrevious('[class=""]'); let borderChangeElement = previousVisibleSibling; let changeBorderSide = 'right'; if (mouseXRelative > brect.width / 2) { borderChangeElement = e.target; this.dropSide = 'right'; } else { this.dropSide = 'left'; } e.target.getParent("tr").style.cursor = 'move'; if (!previousVisibleSibling) { // right most column borderChangeElement = e.target; if (mouseXRelative <= brect.width / 2) changeBorderSide = 'left'; } borderChangeElement.setStyle('border-' + changeBorderSide + '-style', 'solid'); borderChangeElement.setStyle('border-' + changeBorderSide + '-color', '#e60'); borderChangeElement.setStyle('border-' + changeBorderSide + '-width', 'initial'); resetElementBorderStyle(borderChangeElement, changeBorderSide === 'right' ? 'left' : 'right'); borderChangeElement.getSiblings('[class=""]').each(function(el) { resetElementBorderStyle(el); }); } this.lastHoverTh = e.target; this.lastClientX = e.event.clientX; }.bind(this); const mouseOutFn = function(e) { resetElementBorderStyle(e.target); }.bind(this); const onBeforeStart = function(el) { this.clickedTh = el; this.currentHeaderAction = 'start'; this.dragMovement = false; this.dragStartX = this.lastClientX; }.bind(this); const onStart = function(el, event) { if (this.canResize) { this.currentHeaderAction = 'resize'; this.startWidth = this.resizeTh.getStyle('width').toFloat(); } else { this.currentHeaderAction = 'drag'; el.setStyle('background-color', '#C1D5E7'); } }.bind(this); const onDrag = function(el, event) { if (this.currentHeaderAction === 'resize') { let width = this.startWidth + (event.page.x - this.dragStartX); if (width < 16) width = 16; this.columns[this.resizeTh.columnName].width = width; this.updateColumn(this.resizeTh.columnName); } }.bind(this); const onComplete = function(el, event) { resetElementBorderStyle(this.lastHoverTh); el.setStyle('background-color', ''); if (this.currentHeaderAction === 'resize') LocalPreferences.set('column_' + this.resizeTh.columnName + '_width_' + this.dynamicTableDivId, this.columns[this.resizeTh.columnName].width); if ((this.currentHeaderAction === 'drag') && (el !== this.lastHoverTh)) { this.saveColumnsOrder(); const val = LocalPreferences.get('columns_order_' + this.dynamicTableDivId).split(','); val.erase(el.columnName); let pos = val.indexOf(this.lastHoverTh.columnName); if (this.dropSide === 'right') ++pos; val.splice(pos, 0, el.columnName); LocalPreferences.set('columns_order_' + this.dynamicTableDivId, val.join(',')); this.loadColumnsOrder(); this.updateTableHeaders(); while (this.tableBody.firstChild) this.tableBody.removeChild(this.tableBody.firstChild); this.updateTable(true); } if (this.currentHeaderAction === 'drag') { resetElementBorderStyle(el); el.getSiblings('[class=""]').each(function(el) { resetElementBorderStyle(el); }); } this.currentHeaderAction = ''; }.bind(this); const onCancel = function(el) { this.currentHeaderAction = ''; this.setSortedColumn(el.columnName); }.bind(this); const onTouch = function(e) { const column = e.target.columnName; this.currentHeaderAction = ''; this.setSortedColumn(column); }.bind(this); const ths = this.fixedTableHeader.getElements('th'); for (let i = 0; i < ths.length; ++i) { const th = ths[i]; th.addEvent('mousemove', mouseMoveFn); th.addEvent('mouseout', mouseOutFn); th.addEvent('touchend', onTouch); th.makeResizable({ modifiers: { x: '', y: '' }, onBeforeStart: onBeforeStart, onStart: onStart, onDrag: onDrag, onComplete: onComplete, onCancel: onCancel }); } }, setupDynamicTableHeaderContextMenuClass: function() { if (!DynamicTableHeaderContextMenuClass) { DynamicTableHeaderContextMenuClass = new Class({ Extends: window.qBittorrent.ContextMenu.ContextMenu, updateMenuItems: function() { for (let i = 0; i < this.dynamicTable.columns.length; ++i) { if (this.dynamicTable.columns[i].caption === '') continue; if (this.dynamicTable.columns[i].visible !== '0') this.setItemChecked(this.dynamicTable.columns[i].name, true); else this.setItemChecked(this.dynamicTable.columns[i].name, false); } } }); } }, showColumn: function(columnName, show) { this.columns[columnName].visible = show ? '1' : '0'; LocalPreferences.set('column_' + columnName + '_visible_' + this.dynamicTableDivId, show ? '1' : '0'); this.updateColumn(columnName); }, setupHeaderMenu: function() { this.setupDynamicTableHeaderContextMenuClass(); const menuId = this.dynamicTableDivId + '_headerMenu'; // reuse menu if already exists const ul = $(menuId) ?? new Element('ul', { id: menuId, class: 'contextMenu scrollableMenu' }); const createLi = function(columnName, text) { const html = '' + window.qBittorrent.Misc.escapeHtml(text) + ''; return new Element('li', { html: html }); }; const actions = {}; const onMenuItemClicked = function(element, ref, action) { this.showColumn(action, this.columns[action].visible === '0'); }.bind(this); // recreate child nodes when reusing (enables the context menu to work correctly) if (ul.hasChildNodes()) { while (ul.firstChild) { ul.removeChild(ul.lastChild); } } for (let i = 0; i < this.columns.length; ++i) { const text = this.columns[i].caption; if (text === '') continue; ul.appendChild(createLi(this.columns[i].name, text)); actions[this.columns[i].name] = onMenuItemClicked; } ul.inject(document.body); this.headerContextMenu = new DynamicTableHeaderContextMenuClass({ targets: '#' + this.dynamicTableFixedHeaderDivId + ' tr', actions: actions, menu: menuId, offsets: { x: -15, y: 2 } }); this.headerContextMenu.dynamicTable = this; }, initColumns: function() {}, newColumn: function(name, style, caption, defaultWidth, defaultVisible) { const column = {}; column['name'] = name; column['title'] = name; column['visible'] = LocalPreferences.get('column_' + name + '_visible_' + this.dynamicTableDivId, defaultVisible ? '1' : '0'); column['force_hide'] = false; column['caption'] = caption; column['style'] = style; column['width'] = LocalPreferences.get('column_' + name + '_width_' + this.dynamicTableDivId, defaultWidth); column['dataProperties'] = [name]; column['getRowValue'] = function(row, pos) { if (pos === undefined) pos = 0; return row['full_data'][this.dataProperties[pos]]; }; column['compareRows'] = function(row1, row2) { const value1 = this.getRowValue(row1); const value2 = this.getRowValue(row2); if ((typeof(value1) === 'number') && (typeof(value2) === 'number')) return compareNumbers(value1, value2); return window.qBittorrent.Misc.naturalSortCollator.compare(value1, value2); }; column['updateTd'] = function(td, row) { const value = this.getRowValue(row); td.set('text', value); td.set('title', value); }; column['onResize'] = null; this.columns.push(column); this.columns[name] = column; this.hiddenTableHeader.appendChild(new Element('th')); this.fixedTableHeader.appendChild(new Element('th')); }, loadColumnsOrder: function() { const columnsOrder = []; const val = LocalPreferences.get('columns_order_' + this.dynamicTableDivId); if (val === null || val === undefined) return; val.split(',').forEach(function(v) { if ((v in this.columns) && (!columnsOrder.contains(v))) columnsOrder.push(v); }.bind(this)); for (let i = 0; i < this.columns.length; ++i) if (!columnsOrder.contains(this.columns[i].name)) columnsOrder.push(this.columns[i].name); for (let i = 0; i < this.columns.length; ++i) this.columns[i] = this.columns[columnsOrder[i]]; }, saveColumnsOrder: function() { let val = ''; for (let i = 0; i < this.columns.length; ++i) { if (i > 0) val += ','; val += this.columns[i].name; } LocalPreferences.set('columns_order_' + this.dynamicTableDivId, val); }, updateTableHeaders: function() { this.updateHeader(this.hiddenTableHeader); this.updateHeader(this.fixedTableHeader); }, updateHeader: function(header) { const ths = header.getElements('th'); for (let i = 0; i < ths.length; ++i) { const th = ths[i]; th._this = this; th.setAttribute('title', this.columns[i].caption); th.set('text', this.columns[i].caption); th.setAttribute('style', 'width: ' + this.columns[i].width + 'px;' + this.columns[i].style); th.columnName = this.columns[i].name; th.addClass('column_' + th.columnName); if ((this.columns[i].visible == '0') || this.columns[i].force_hide) th.addClass('invisible'); else th.removeClass('invisible'); } }, getColumnPos: function(columnName) { for (let i = 0; i < this.columns.length; ++i) if (this.columns[i].name == columnName) return i; return -1; }, updateColumn: function(columnName) { const pos = this.getColumnPos(columnName); const visible = ((this.columns[pos].visible != '0') && !this.columns[pos].force_hide); const ths = this.hiddenTableHeader.getElements('th'); const fths = this.fixedTableHeader.getElements('th'); const trs = this.tableBody.getElements('tr'); const style = 'width: ' + this.columns[pos].width + 'px;' + this.columns[pos].style; ths[pos].setAttribute('style', style); fths[pos].setAttribute('style', style); if (visible) { ths[pos].removeClass('invisible'); fths[pos].removeClass('invisible'); for (let i = 0; i < trs.length; ++i) trs[i].getElements('td')[pos].removeClass('invisible'); } else { ths[pos].addClass('invisible'); fths[pos].addClass('invisible'); for (let j = 0; j < trs.length; ++j) trs[j].getElements('td')[pos].addClass('invisible'); } if (this.columns[pos].onResize !== null) { this.columns[pos].onResize(columnName); } }, getSortedColumn: function() { return LocalPreferences.get('sorted_column_' + this.dynamicTableDivId); }, setSortedColumn: function(column) { if (column != this.sortedColumn) { const oldColumn = this.sortedColumn; this.sortedColumn = column; this.reverseSort = '0'; this.setSortedColumnIcon(column, oldColumn, false); } else { // Toggle sort order this.reverseSort = this.reverseSort === '0' ? '1' : '0'; this.setSortedColumnIcon(column, null, (this.reverseSort === '1')); } LocalPreferences.set('sorted_column_' + this.dynamicTableDivId, column); LocalPreferences.set('reverse_sort_' + this.dynamicTableDivId, this.reverseSort); this.updateTable(false); }, setSortedColumnIcon: function(newColumn, oldColumn, isReverse) { const getCol = function(headerDivId, colName) { const colElem = $$("#" + headerDivId + " .column_" + colName); if (colElem.length == 1) return colElem[0]; return null; }; const colElem = getCol(this.dynamicTableFixedHeaderDivId, newColumn); if (colElem !== null) { colElem.addClass('sorted'); if (isReverse) colElem.addClass('reverse'); else colElem.removeClass('reverse'); } const oldColElem = getCol(this.dynamicTableFixedHeaderDivId, oldColumn); if (oldColElem !== null) { oldColElem.removeClass('sorted'); oldColElem.removeClass('reverse'); } }, getSelectedRowId: function() { if (this.selectedRows.length > 0) return this.selectedRows[0]; return ''; }, isRowSelected: function(rowId) { return this.selectedRows.contains(rowId); }, altRow: function() { if (!MUI.ieLegacySupport) return; const trs = this.tableBody.getElements('tr'); trs.each(function(el, i) { if (i % 2) { el.addClass('alt'); } else { el.removeClass('alt'); } }.bind(this)); }, selectAll: function() { this.deselectAll(); const trs = this.tableBody.getElements('tr'); for (let i = 0; i < trs.length; ++i) { const tr = trs[i]; this.selectedRows.push(tr.rowId); if (!tr.hasClass('selected')) tr.addClass('selected'); } }, deselectAll: function() { this.selectedRows.empty(); }, selectRow: function(rowId) { this.selectedRows.push(rowId); this.setRowClass(); this.onSelectedRowChanged(); }, deselectRow: function(rowId) { this.selectedRows.erase(rowId); this.setRowClass(); this.onSelectedRowChanged(); }, selectRows: function(rowId1, rowId2) { this.deselectAll(); if (rowId1 === rowId2) { this.selectRow(rowId1); return; } let select = false; const that = this; this.tableBody.getElements('tr').each(function(tr) { if ((tr.rowId == rowId1) || (tr.rowId == rowId2)) { select = !select; that.selectedRows.push(tr.rowId); } else if (select) { that.selectedRows.push(tr.rowId); } }); this.setRowClass(); this.onSelectedRowChanged(); }, reselectRows: function(rowIds) { this.deselectAll(); this.selectedRows = rowIds.slice(); this.tableBody.getElements('tr').each(function(tr) { if (rowIds.indexOf(tr.rowId) > -1) tr.addClass('selected'); }); }, setRowClass: function() { const that = this; this.tableBody.getElements('tr').each(function(tr) { if (that.isRowSelected(tr.rowId)) tr.addClass('selected'); else tr.removeClass('selected'); }); }, onSelectedRowChanged: function() {}, updateRowData: function(data) { // ensure rowId is a string const rowId = `${data['rowId']}`; let row; if (!this.rows.has(rowId)) { row = { 'full_data': {}, 'rowId': rowId }; this.rows.set(rowId, row); } else row = this.rows.get(rowId); row['data'] = data; for (const x in data) row['full_data'][x] = data[x]; }, getFilteredAndSortedRows: function() { const filteredRows = []; const rows = this.rows.getValues(); for (let i = 0; i < rows.length; ++i) { filteredRows.push(rows[i]); filteredRows[rows[i].rowId] = rows[i]; } filteredRows.sort(function(row1, row2) { const column = this.columns[this.sortedColumn]; const res = column.compareRows(row1, row2); if (this.reverseSort === '0') return res; else return -res; }.bind(this)); return filteredRows; }, getTrByRowId: function(rowId) { const trs = this.tableBody.getElements('tr'); for (let i = 0; i < trs.length; ++i) if (trs[i].rowId == rowId) return trs[i]; return null; }, updateTable: function(fullUpdate) { if (fullUpdate === undefined) fullUpdate = false; const rows = this.getFilteredAndSortedRows(); for (let i = 0; i < this.selectedRows.length; ++i) if (!(this.selectedRows[i] in rows)) { this.selectedRows.splice(i, 1); --i; } const trs = this.tableBody.getElements('tr'); for (let rowPos = 0; rowPos < rows.length; ++rowPos) { const rowId = rows[rowPos]['rowId']; let tr_found = false; for (let j = rowPos; j < trs.length; ++j) if (trs[j]['rowId'] == rowId) { tr_found = true; if (rowPos == j) break; trs[j].inject(trs[rowPos], 'before'); const tmpTr = trs[j]; trs.splice(j, 1); trs.splice(rowPos, 0, tmpTr); break; } if (tr_found) // row already exists in the table this.updateRow(trs[rowPos], fullUpdate); else { // else create a new row in the table const tr = new Element('tr'); // set tabindex so element receives keydown events // more info: https://developer.mozilla.org/en-US/docs/Web/API/Element/keydown_event tr.setProperty("tabindex", "-1"); const rowId = rows[rowPos]['rowId']; tr.setProperty("data-row-id", rowId); tr['rowId'] = rowId; tr._this = this; tr.addEvent('contextmenu', function(e) { if (!this._this.isRowSelected(this.rowId)) { this._this.deselectAll(); this._this.selectRow(this.rowId); } return true; }); tr.addEvent('click', function(e) { e.stop(); if (e.control || e.meta) { // CTRL/CMD ⌘ key was pressed if (this._this.isRowSelected(this.rowId)) this._this.deselectRow(this.rowId); else this._this.selectRow(this.rowId); } else if (e.shift && (this._this.selectedRows.length == 1)) { // Shift key was pressed this._this.selectRows(this._this.getSelectedRowId(), this.rowId); } else { // Simple selection this._this.deselectAll(); this._this.selectRow(this.rowId); } return false; }); tr.addEvent('touchstart', function(e) { if (!this._this.isRowSelected(this.rowId)) { this._this.deselectAll(); this._this.selectRow(this.rowId); } return false; }); tr.addEvent('keydown', function(event) { switch (event.key) { case "up": this._this.selectPreviousRow(); return false; case "down": this._this.selectNextRow(); return false; } }); this.setupTr(tr); for (let k = 0; k < this.columns.length; ++k) { const td = new Element('td'); if ((this.columns[k].visible == '0') || this.columns[k].force_hide) td.addClass('invisible'); td.injectInside(tr); } // Insert if (rowPos >= trs.length) { tr.inject(this.tableBody); trs.push(tr); } else { tr.inject(trs[rowPos], 'before'); trs.splice(rowPos, 0, tr); } // Update context menu if (this.contextMenu) this.contextMenu.addTarget(tr); this.updateRow(tr, true); } } let rowPos = rows.length; while ((rowPos < trs.length) && (trs.length > 0)) { trs[trs.length - 1].dispose(); trs.pop(); } }, setupTr: function(tr) {}, updateRow: function(tr, fullUpdate) { const row = this.rows.get(tr.rowId); const data = row[fullUpdate ? 'full_data' : 'data']; const tds = tr.getElements('td'); for (let i = 0; i < this.columns.length; ++i) { if (Object.prototype.hasOwnProperty.call(data, this.columns[i].dataProperties[0])) this.columns[i].updateTd(tds[i], row); } row['data'] = {}; }, removeRow: function(rowId) { this.selectedRows.erase(rowId); const tr = this.getTrByRowId(rowId); if (tr !== null) { tr.dispose(); this.rows.erase(rowId); return true; } return false; }, clear: function() { this.deselectAll(); this.rows.empty(); const trs = this.tableBody.getElements('tr'); while (trs.length > 0) { trs[trs.length - 1].dispose(); trs.pop(); } }, selectedRowsIds: function() { return this.selectedRows.slice(); }, getRowIds: function() { return this.rows.getKeys(); }, selectNextRow: function() { const visibleRows = $(this.dynamicTableDivId).getElements("tbody tr").filter(e => e.getStyle("display") !== "none"); const selectedRowId = this.getSelectedRowId(); let selectedIndex = -1; for (let i = 0; i < visibleRows.length; ++i) { const row = visibleRows[i]; if (row.getProperty("data-row-id") === selectedRowId) { selectedIndex = i; break; } } const isLastRowSelected = (selectedIndex >= (visibleRows.length - 1)); if (!isLastRowSelected) { this.deselectAll(); const newRow = visibleRows[selectedIndex + 1]; this.selectRow(newRow.getProperty("data-row-id")); } }, selectPreviousRow: function() { const visibleRows = $(this.dynamicTableDivId).getElements("tbody tr").filter(e => e.getStyle("display") !== "none"); const selectedRowId = this.getSelectedRowId(); let selectedIndex = -1; for (let i = 0; i < visibleRows.length; ++i) { const row = visibleRows[i]; if (row.getProperty("data-row-id") === selectedRowId) { selectedIndex = i; break; } } const isFirstRowSelected = selectedIndex <= 0; if (!isFirstRowSelected) { this.deselectAll(); const newRow = visibleRows[selectedIndex - 1]; this.selectRow(newRow.getProperty("data-row-id")); } }, }); const TorrentsTable = new Class({ Extends: DynamicTable, initColumns: function() { this.newColumn('priority', '', '#', 30, true); this.newColumn('state_icon', 'cursor: default', '', 22, true); this.newColumn('name', '', 'Name', 200, true); this.newColumn('size', '', 'Size', 100, true); this.newColumn('total_size', '', 'Total Size', 100, false); this.newColumn('progress', '', 'Done', 85, true); this.newColumn('status', '', 'Status', 100, true); this.newColumn('num_seeds', '', 'Seeds', 100, true); this.newColumn('num_leechs', '', 'Peers', 100, true); this.newColumn('dlspeed', '', 'Down Speed', 100, true); this.newColumn('upspeed', '', 'Up Speed', 100, true); this.newColumn('eta', '', 'ETA', 100, true); this.newColumn('ratio', '', 'Ratio', 100, true); this.newColumn('category', '', 'Category', 100, true); this.newColumn('tags', '', 'Tags', 100, true); this.newColumn('added_on', '', 'Added On', 100, true); this.newColumn('completion_on', '', 'Completed On', 100, false); this.newColumn('tracker', '', 'Tracker', 100, false); this.newColumn('dl_limit', '', 'Down Limit', 100, false); this.newColumn('up_limit', '', 'Up Limit', 100, false); this.newColumn('downloaded', '', 'Downloaded', 100, false); this.newColumn('uploaded', '', 'Uploaded', 100, false); this.newColumn('downloaded_session', '', 'Session Download', 100, false); this.newColumn('uploaded_session', '', 'Session Upload', 100, false); this.newColumn('amount_left', '', 'Remaining', 100, false); this.newColumn('time_active', '', 'Time Active', 100, false); this.newColumn('save_path', '', 'Save path', 100, false); this.newColumn('completed', '', 'Completed', 100, false); this.newColumn('max_ratio', '', 'Ratio Limit', 100, false); this.newColumn('seen_complete', '', 'Last Seen Complete', 100, false); this.newColumn('last_activity', '', 'Last Activity', 100, false); this.newColumn('availability', '', 'Availability', 100, false); this.columns['state_icon'].onclick = ''; this.columns['state_icon'].dataProperties[0] = 'state'; this.columns['num_seeds'].dataProperties.push('num_complete'); this.columns['num_leechs'].dataProperties.push('num_incomplete'); this.columns['time_active'].dataProperties.push('seeding_time'); this.initColumnsFunctions(); }, initColumnsFunctions: function() { // state_icon this.columns['state_icon'].updateTd = function(td, row) { let state = this.getRowValue(row); let img_path; // normalize states switch (state) { case "forcedDL": case "metaDL": case "forcedMetaDL": case "downloading": state = "downloading"; img_path = "images/downloading.svg"; break; case "forcedUP": case "uploading": state = "uploading"; img_path = "images/upload.svg"; break; case "stalledUP": state = "stalledUP"; img_path = "images/stalledUP.svg"; break; case "stalledDL": state = "stalledDL"; img_path = "images/stalledDL.svg"; break; case "pausedDL": state = "torrent-stop"; img_path = "images/torrent-stop.svg"; break; case "pausedUP": state = "checked-completed"; img_path = "images/checked-completed.svg"; break; case "queuedDL": case "queuedUP": state = "queued"; img_path = "images/queued.svg"; break; case "checkingDL": case "checkingUP": case "queuedForChecking": case "checkingResumeData": case "moving": state = "force-recheck"; img_path = "images/force-recheck.svg"; break; case "unknown": case "missingFiles": state = "error"; img_path = "images/error.svg"; break; default: break; // do nothing } if (td.getChildren('img').length > 0) { const img = td.getChildren('img')[0]; if (img.src.indexOf(img_path) < 0) { img.set('src', img_path); img.set('title', state); } } else { td.adopt(new Element('img', { 'src': img_path, 'class': 'stateIcon', 'title': state })); } }; // status this.columns['status'].updateTd = function(td, row) { const state = this.getRowValue(row); if (!state) return; let status; switch (state) { case "downloading": status = "Downloading"; break; case "stalledDL": status = "Stalled"; break; case "metaDL": status = "Downloading metadata"; break; case "forcedMetaDL": status = "[F] Downloading metadata"; break; case "forcedDL": status = "[F] Downloading"; break; case "uploading": case "stalledUP": status = "Seeding"; break; case "forcedUP": status = "[F] Seeding"; break; case "queuedDL": case "queuedUP": status = "Queued"; break; case "checkingDL": case "checkingUP": status = "Checking"; break; case "queuedForChecking": status = "Queued for checking"; break; case "checkingResumeData": status = "Checking resume data"; break; case "pausedDL": status = "Paused"; break; case "pausedUP": status = "Completed"; break; case "moving": status = "Moving"; break; case "missingFiles": status = "Missing Files"; break; case "error": status = "Errored"; break; default: status = "Unknown"; } td.set('text', status); td.set('title', status); }; // priority this.columns['priority'].updateTd = function(td, row) { const queuePos = this.getRowValue(row); const formattedQueuePos = (queuePos < 1) ? '*' : queuePos; td.set('text', formattedQueuePos); td.set('title', formattedQueuePos); }; this.columns['priority'].compareRows = function(row1, row2) { let row1_val = this.getRowValue(row1); let row2_val = this.getRowValue(row2); if (row1_val < 1) row1_val = 1000000; if (row2_val < 1) row2_val = 1000000; return compareNumbers(row1_val, row2_val); }; // name, category, tags this.columns['name'].compareRows = function(row1, row2) { const row1Val = this.getRowValue(row1); const row2Val = this.getRowValue(row2); return row1Val.localeCompare(row2Val, undefined, { numeric: true, sensitivity: 'base' }); }; this.columns['category'].compareRows = this.columns['name'].compareRows; this.columns['tags'].compareRows = this.columns['name'].compareRows; // size, total_size this.columns['size'].updateTd = function(td, row) { const size = window.qBittorrent.Misc.friendlyUnit(this.getRowValue(row), false); td.set('text', size); td.set('title', size); }; this.columns['total_size'].updateTd = this.columns['size'].updateTd; // progress this.columns['progress'].updateTd = function(td, row) { const progress = this.getRowValue(row); let progressFormatted = (progress * 100).round(1); if (progressFormatted == 100.0 && progress != 1.0) progressFormatted = 99.9; if (td.getChildren('div').length > 0) { const div = td.getChildren('div')[0]; if (td.resized) { td.resized = false; div.setWidth(ProgressColumnWidth - 5); } if (div.getValue() != progressFormatted) div.setValue(progressFormatted); } else { if (ProgressColumnWidth < 0) ProgressColumnWidth = td.offsetWidth; td.adopt(new window.qBittorrent.ProgressBar.ProgressBar(progressFormatted.toFloat(), { 'width': ProgressColumnWidth - 5 })); td.resized = false; } }; this.columns['progress'].onResize = function(columnName) { const pos = this.getColumnPos(columnName); const trs = this.tableBody.getElements('tr'); ProgressColumnWidth = -1; for (let i = 0; i < trs.length; ++i) { const td = trs[i].getElements('td')[pos]; if (ProgressColumnWidth < 0) ProgressColumnWidth = td.offsetWidth; td.resized = true; this.columns[columnName].updateTd(td, this.rows.get(trs[i].rowId)); } }.bind(this); // num_seeds this.columns['num_seeds'].updateTd = function(td, row) { const num_seeds = this.getRowValue(row, 0); const num_complete = this.getRowValue(row, 1); let value = num_seeds; if (num_complete != -1) value += ' (' + num_complete + ')'; td.set('text', value); td.set('title', value); }; this.columns['num_seeds'].compareRows = function(row1, row2) { const num_seeds1 = this.getRowValue(row1, 0); const num_complete1 = this.getRowValue(row1, 1); const num_seeds2 = this.getRowValue(row2, 0); const num_complete2 = this.getRowValue(row2, 1); const result = compareNumbers(num_complete1, num_complete2); if (result !== 0) return result; return compareNumbers(num_seeds1, num_seeds2); }; // num_leechs this.columns['num_leechs'].updateTd = this.columns['num_seeds'].updateTd; this.columns['num_leechs'].compareRows = this.columns['num_seeds'].compareRows; // dlspeed this.columns['dlspeed'].updateTd = function(td, row) { const speed = window.qBittorrent.Misc.friendlyUnit(this.getRowValue(row), true); td.set('text', speed); td.set('title', speed); }; // upspeed this.columns['upspeed'].updateTd = this.columns['dlspeed'].updateTd; // eta this.columns['eta'].updateTd = function(td, row) { const eta = window.qBittorrent.Misc.friendlyDuration(this.getRowValue(row), window.qBittorrent.Misc.MAX_ETA); td.set('text', eta); td.set('title', eta); }; // ratio this.columns['ratio'].updateTd = function(td, row) { const ratio = this.getRowValue(row); const string = (ratio === -1) ? '∞' : window.qBittorrent.Misc.toFixedPointString(ratio, 2); td.set('text', string); td.set('title', string); td.classList.add('ratio'); td.set('class', 'ratio'); // color by percentage if (string === 0 || string === "0.00") { td.classList.add('highlight-darkred'); } else if (string > "0" && string <= "0.10") { td.classList.add('highlight-red'); } else if (string > "0.10" && string <= "0.20") { td.classList.add('highlight-orange'); } else if (string > "0.20" && string <= "0.40") { td.classList.add('highlight-yellow'); } else if (string > "0.40" && string <= "0.60") { td.classList.add('highlight-yellow'); } else if (string > "0.60" && string <= "0.80") { td.classList.add('highlight-green'); } else if (string > "0.80" && string <= "1.0" || string > "1.0") { td.classList.add('highlight-brightgreen'); } }; // added on this.columns['added_on'].updateTd = function(td, row) { const date = new Date(this.getRowValue(row) * 1000).toLocaleString(); td.set('text', date); td.set('title', date); }; // completion_on this.columns['completion_on'].updateTd = function(td, row) { const val = this.getRowValue(row); if ((val === 0xffffffff) || (val < 0)) { td.set('text', ''); td.set('title', ''); } else { const date = new Date(this.getRowValue(row) * 1000).toLocaleString(); td.set('text', date); td.set('title', date); } }; // dl_limit, up_limit this.columns['dl_limit'].updateTd = function(td, row) { const speed = this.getRowValue(row); if (speed === 0) { td.set('text', '∞'); td.set('title', '∞'); } else { const formattedSpeed = window.qBittorrent.Misc.friendlyUnit(speed, true); td.set('text', formattedSpeed); td.set('title', formattedSpeed); } }; this.columns['up_limit'].updateTd = this.columns['dl_limit'].updateTd; // downloaded, uploaded, downloaded_session, uploaded_session, amount_left this.columns['downloaded'].updateTd = this.columns['size'].updateTd; this.columns['uploaded'].updateTd = this.columns['size'].updateTd; this.columns['downloaded_session'].updateTd = this.columns['size'].updateTd; this.columns['uploaded_session'].updateTd = this.columns['size'].updateTd; this.columns['amount_left'].updateTd = this.columns['size'].updateTd; // time active this.columns['time_active'].updateTd = function(td, row) { const activeTime = this.getRowValue(row, 0); const seedingTime = this.getRowValue(row, 1); const time = (seedingTime > 0) ? ('%1 (seeded for %2)' .replace('%1', window.qBittorrent.Misc.friendlyDuration(activeTime)) .replace('%2', window.qBittorrent.Misc.friendlyDuration(seedingTime))) : window.qBittorrent.Misc.friendlyDuration(activeTime); td.set('text', time); td.set('title', time); }; // completed this.columns['completed'].updateTd = this.columns['size'].updateTd; // max_ratio this.columns['max_ratio'].updateTd = this.columns['ratio'].updateTd; // seen_complete this.columns['seen_complete'].updateTd = this.columns['completion_on'].updateTd; // last_activity this.columns['last_activity'].updateTd = function(td, row) { const val = this.getRowValue(row); if (val < 1) { td.set('text', '∞'); td.set('title', '∞'); } else { const formattedVal = '%1 ago'.replace('%1', window.qBittorrent.Misc.friendlyDuration((new Date()) / 1000 - val)); td.set('text', formattedVal); td.set('title', formattedVal); } }; // availability this.columns['availability'].updateTd = function(td, row) { const value = window.qBittorrent.Misc.toFixedPointString(this.getRowValue(row), 3); td.set('text', value); td.set('title', value); }; }, applyFilter: function(row, filterName, categoryHash, tagHash, trackerHash, filterTerms) { const state = row['full_data'].state; const name = row['full_data'].name.toLowerCase(); let inactive = false; let r; switch (filterName) { case 'downloading': if ((state != 'downloading') && (state.indexOf('DL') === -1)) return false; break; case 'seeding': if (state != 'uploading' && state != 'forcedUP' && state != 'stalledUP' && state != 'queuedUP' && state != 'checkingUP') return false; break; case 'completed': if ((state != 'uploading') && (state.indexOf('UP') === -1)) return false; break; case 'paused': if (state.indexOf('paused') === -1) return false; break; case 'resumed': if (state.indexOf('paused') > -1) return false; break; case 'stalled': if ((state != 'stalledUP') && (state != 'stalledDL')) return false; break; case 'stalled_uploading': if (state != 'stalledUP') return false; break; case 'stalled_downloading': if (state != 'stalledDL') return false; break; case 'inactive': inactive = true; // fallthrough case 'active': if (state == 'stalledDL') r = (row['full_data'].upspeed > 0); else r = state == 'metaDL' || state == 'forcedMetaDL' || state == 'downloading' || state == 'forcedDL' || state == 'uploading' || state == 'forcedUP'; if (r == inactive) return false; break; case 'checking': if (state !== 'checkingUP' && state !== 'checkingDL' && state !== 'checkingResumeData') return false; break; case 'errored': if (state != 'error' && state != "unknown" && state != "missingFiles") return false; break; } const categoryHashInt = parseInt(categoryHash); if (!isNaN(categoryHashInt)) { switch (categoryHashInt) { case CATEGORIES_ALL: break; // do nothing case CATEGORIES_UNCATEGORIZED: if (row['full_data'].category.length !== 0) return false; break; // do nothing default: if (!useSubcategories) { if (categoryHashInt !== genHash(row['full_data'].category)) return false; } else { const selectedCategoryName = category_list[categoryHash].name + "/"; const torrentCategoryName = row['full_data'].category + "/"; if (!torrentCategoryName.startsWith(selectedCategoryName)) return false; } } } const tagHashInt = parseInt(tagHash); const isNumber = !isNaN(tagHashInt); if (isNumber) { switch (tagHashInt) { case TAGS_ALL: break; // do nothing case TAGS_UNTAGGED: if (row['full_data'].tags.length !== 0) return false; break; // do nothing default: { let rowTags = row['full_data'].tags.split(', '); rowTags = rowTags.map(function(tag) { return genHash(tag); }); if (!rowTags.contains(tagHashInt)) return false; break; } } } const trackerHashInt = Number.parseInt(trackerHash, 10); switch (trackerHashInt) { case TRACKERS_ALL: break; // do nothing case TRACKERS_TRACKERLESS: if (row['full_data'].trackers_count !== 0) return false; break; default: { const tracker = trackerList.get(trackerHashInt); if (tracker && !tracker.torrents.includes(row['full_data'].rowId)) return false; break; } } if ((filterTerms !== undefined) && (filterTerms !== null) && (filterTerms.length > 0) && !window.qBittorrent.Misc.containsAllTerms(name, filterTerms)) return false; return true; }, getFilteredTorrentsNumber: function(filterName, categoryHash, tagHash, trackerHash) { let cnt = 0; const rows = this.rows.getValues(); for (let i = 0; i < rows.length; ++i) if (this.applyFilter(rows[i], filterName, categoryHash, tagHash, trackerHash, null)) ++cnt; return cnt; }, getFilteredTorrentsHashes: function(filterName, categoryHash, tagHash, trackerHash) { const rowsHashes = []; const rows = this.rows.getValues(); for (let i = 0; i < rows.length; ++i) if (this.applyFilter(rows[i], filterName, categoryHash, tagHash, trackerHash, null)) rowsHashes.push(rows[i]['rowId']); return rowsHashes; }, getFilteredAndSortedRows: function() { const filteredRows = []; const rows = this.rows.getValues(); const filterText = $('torrentsFilterInput').value.trim().toLowerCase(); const filterTerms = (filterText.length > 0) ? filterText.split(" ") : null; for (let i = 0; i < rows.length; ++i) { if (this.applyFilter(rows[i], selected_filter, selected_category, selectedTag, selectedTracker, filterTerms)) { filteredRows.push(rows[i]); filteredRows[rows[i].rowId] = rows[i]; } } filteredRows.sort(function(row1, row2) { const column = this.columns[this.sortedColumn]; const res = column.compareRows(row1, row2); if (this.reverseSort === '0') return res; else return -res; }.bind(this)); return filteredRows; }, setupTr: function(tr) { tr.addEvent('dblclick', function(e) { e.stop(); this._this.deselectAll(); this._this.selectRow(this.rowId); const row = this._this.rows.get(this.rowId); const state = row['full_data'].state; if (state.indexOf('paused') > -1) startFN(); else pauseFN(); return true; }); tr.addClass("torrentsTableContextMenuTarget"); }, getCurrentTorrentID: function() { return this.getSelectedRowId(); }, onSelectedRowChanged: function() { updatePropertiesPanel(); } }); const TorrentPeersTable = new Class({ Extends: DynamicTable, initColumns: function() { this.newColumn('country', '', 'Country/Region', 22, true); this.newColumn('ip', '', 'IP', 80, true); this.newColumn('port', '', 'Port', 35, true); this.newColumn('connection', '', 'Connection', 50, true); this.newColumn('flags', '', 'Flags', 50, true); this.newColumn('client', '', 'Client', 140, true); this.newColumn('peer_id_client', '', 'Peer ID Client', 60, false); this.newColumn('progress', '', 'Progress', 50, true); this.newColumn('dl_speed', '', 'Down Speed', 50, true); this.newColumn('up_speed', '', 'Up Speed', 50, true); this.newColumn('downloaded', '', 'Downloaded', 50, true); this.newColumn('uploaded', '', 'Uploaded', 50, true); this.newColumn('relevance', '', 'Relevance', 30, true); this.newColumn('files', '', 'Files', 100, true); this.columns['country'].dataProperties.push('country_code'); this.columns['flags'].dataProperties.push('flags_desc'); this.initColumnsFunctions(); }, initColumnsFunctions: function() { // country this.columns['country'].updateTd = function(td, row) { const country = this.getRowValue(row, 0); const country_code = this.getRowValue(row, 1); if (!country_code) { if (td.getChildren('img').length > 0) td.getChildren('img')[0].dispose(); return; } const img_path = 'images/flags/' + country_code + '.svg'; if (td.getChildren('img').length > 0) { const img = td.getChildren('img')[0]; img.set('src', img_path); img.set('class', 'flags'); img.set('alt', country); img.set('title', country); } else td.adopt(new Element('img', { 'src': img_path, 'class': 'flags', 'alt': country, 'title': country })); }; // ip this.columns['ip'].compareRows = function(row1, row2) { const ip1 = this.getRowValue(row1); const ip2 = this.getRowValue(row2); const a = ip1.split("."); const b = ip2.split("."); for (let i = 0; i < 4; ++i) { if (a[i] != b[i]) return a[i] - b[i]; } return 0; }; // flags this.columns['flags'].updateTd = function(td, row) { td.set('text', this.getRowValue(row, 0)); td.set('title', this.getRowValue(row, 1)); }; // progress this.columns['progress'].updateTd = function(td, row) { const progress = this.getRowValue(row); let progressFormatted = (progress * 100).round(1); if (progressFormatted == 100.0 && progress != 1.0) progressFormatted = 99.9; progressFormatted += "%"; td.set('text', progressFormatted); td.set('title', progressFormatted); }; // dl_speed, up_speed this.columns['dl_speed'].updateTd = function(td, row) { const speed = this.getRowValue(row); if (speed === 0) { td.set('text', ''); td.set('title', ''); } else { const formattedSpeed = window.qBittorrent.Misc.friendlyUnit(speed, true); td.set('text', formattedSpeed); td.set('title', formattedSpeed); } }; this.columns['up_speed'].updateTd = this.columns['dl_speed'].updateTd; // downloaded, uploaded this.columns['downloaded'].updateTd = function(td, row) { const downloaded = window.qBittorrent.Misc.friendlyUnit(this.getRowValue(row), false); td.set('text', downloaded); td.set('title', downloaded); }; this.columns['uploaded'].updateTd = this.columns['downloaded'].updateTd; // relevance this.columns['relevance'].updateTd = this.columns['progress'].updateTd; // files this.columns['files'].updateTd = function(td, row) { const value = this.getRowValue(row, 0); td.set('text', value.replace(/\n/g, ';')); td.set('title', value); }; } }); const SearchResultsTable = new Class({ Extends: DynamicTable, initColumns: function() { this.newColumn('fileName', '', 'Name', 500, true); this.newColumn('fileSize', '', 'Size', 100, true); this.newColumn('nbSeeders', '', 'Seeders', 100, true); this.newColumn('nbLeechers', '', 'Leechers', 100, true); this.newColumn('siteUrl', '', 'Search engine', 250, true); this.initColumnsFunctions(); }, initColumnsFunctions: function() { const displaySize = function(td, row) { const size = window.qBittorrent.Misc.friendlyUnit(this.getRowValue(row), false); td.set('text', size); td.set('title', size); }; const displayNum = function(td, row) { const value = this.getRowValue(row); const formattedValue = (value === "-1") ? "Unknown" : value; td.set('text', formattedValue); td.set('title', formattedValue); }; this.columns['fileSize'].updateTd = displaySize; this.columns['nbSeeders'].updateTd = displayNum; this.columns['nbLeechers'].updateTd = displayNum; }, getFilteredAndSortedRows: function() { const getSizeFilters = function() { let minSize = (window.qBittorrent.Search.searchSizeFilter.min > 0.00) ? (window.qBittorrent.Search.searchSizeFilter.min * Math.pow(1024, window.qBittorrent.Search.searchSizeFilter.minUnit)) : 0.00; let maxSize = (window.qBittorrent.Search.searchSizeFilter.max > 0.00) ? (window.qBittorrent.Search.searchSizeFilter.max * Math.pow(1024, window.qBittorrent.Search.searchSizeFilter.maxUnit)) : 0.00; if ((minSize > maxSize) && (maxSize > 0.00)) { const tmp = minSize; minSize = maxSize; maxSize = tmp; } return { min: minSize, max: maxSize }; }; const getSeedsFilters = function() { let minSeeds = (window.qBittorrent.Search.searchSeedsFilter.min > 0) ? window.qBittorrent.Search.searchSeedsFilter.min : 0; let maxSeeds = (window.qBittorrent.Search.searchSeedsFilter.max > 0) ? window.qBittorrent.Search.searchSeedsFilter.max : 0; if ((minSeeds > maxSeeds) && (maxSeeds > 0)) { const tmp = minSeeds; minSeeds = maxSeeds; maxSeeds = tmp; } return { min: minSeeds, max: maxSeeds }; }; let filteredRows = []; const rows = this.rows.getValues(); const searchTerms = window.qBittorrent.Search.searchText.pattern.toLowerCase().split(" "); const filterTerms = window.qBittorrent.Search.searchText.filterPattern.toLowerCase().split(" "); const sizeFilters = getSizeFilters(); const seedsFilters = getSeedsFilters(); const searchInTorrentName = $('searchInTorrentName').get('value') === "names"; if (searchInTorrentName || (filterTerms.length > 0) || (window.qBittorrent.Search.searchSizeFilter.min > 0.00) || (window.qBittorrent.Search.searchSizeFilter.max > 0.00)) { for (let i = 0; i < rows.length; ++i) { const row = rows[i]; if (searchInTorrentName && !window.qBittorrent.Misc.containsAllTerms(row.full_data.fileName, searchTerms)) continue; if ((filterTerms.length > 0) && !window.qBittorrent.Misc.containsAllTerms(row.full_data.fileName, filterTerms)) continue; if ((sizeFilters.min > 0.00) && (row.full_data.fileSize < sizeFilters.min)) continue; if ((sizeFilters.max > 0.00) && (row.full_data.fileSize > sizeFilters.max)) continue; if ((seedsFilters.min > 0) && (row.full_data.nbSeeders < seedsFilters.min)) continue; if ((seedsFilters.max > 0) && (row.full_data.nbSeeders > seedsFilters.max)) continue; filteredRows.push(row); } } else { filteredRows = rows; } filteredRows.sort(function(row1, row2) { const column = this.columns[this.sortedColumn]; const res = column.compareRows(row1, row2); if (this.reverseSort === '0') return res; else return -res; }.bind(this)); return filteredRows; }, setupTr: function(tr) { tr.addClass("searchTableRow"); } }); const SearchPluginsTable = new Class({ Extends: DynamicTable, initColumns: function() { this.newColumn('fullName', '', 'Name', 175, true); this.newColumn('version', '', 'Version', 100, true); this.newColumn('url', '', 'Url', 175, true); this.newColumn('enabled', '', 'Enabled', 100, true); this.initColumnsFunctions(); }, initColumnsFunctions: function() { this.columns['enabled'].updateTd = function(td, row) { const value = this.getRowValue(row); if (value) { td.set('text', 'Yes'); td.set('title', 'Yes'); td.getParent("tr").addClass("green"); td.getParent("tr").removeClass("red"); } else { td.set('text', 'No'); td.set('title', 'No'); td.getParent("tr").addClass("red"); td.getParent("tr").removeClass("green"); } }; }, setupTr: function(tr) { tr.addClass("searchPluginsTableRow"); } }); const TorrentTrackersTable = new Class({ Extends: DynamicTable, initColumns: function() { this.newColumn('tier', '', 'Tier', 35, true); this.newColumn('url', '', 'URL', 250, true); this.newColumn('status', '', 'Status', 125, true); this.newColumn('peers', '', 'Peers', 75, true); this.newColumn('seeds', '', 'Seeds', 75, true); this.newColumn('leeches', '', 'Leeches', 75, true); this.newColumn('downloaded', '', 'Times Downloaded', 100, true); this.newColumn('message', '', 'Message', 250, true); }, }); const BulkRenameTorrentFilesTable = new Class({ Extends: DynamicTable, filterTerms: [], prevFilterTerms: [], prevRowsString: null, prevFilteredRows: [], prevSortedColumn: null, prevReverseSort: null, fileTree: new window.qBittorrent.FileTree.FileTree(), populateTable: function(root) { this.fileTree.setRoot(root); root.children.each(function(node) { this._addNodeToTable(node, 0); }.bind(this)); }, _addNodeToTable: function(node, depth) { node.depth = depth; if (node.isFolder) { const data = { rowId: node.rowId, fileId: -1, checked: node.checked, path: node.path, original: node.original, renamed: node.renamed }; node.data = data; node.full_data = data; this.updateRowData(data); } else { node.data.rowId = node.rowId; node.full_data = node.data; this.updateRowData(node.data); } node.children.each(function(child) { this._addNodeToTable(child, depth + 1); }.bind(this)); }, getRoot: function() { return this.fileTree.getRoot(); }, getNode: function(rowId) { return this.fileTree.getNode(rowId); }, getRow: function(node) { const rowId = this.fileTree.getRowId(node); return this.rows.get(rowId); }, getSelectedRows: function() { const nodes = this.fileTree.toArray(); return nodes.filter(x => x.checked == 0); }, initColumns: function() { // Blocks saving header width (because window width isn't saved) LocalPreferences.remove('column_' + "checked" + '_width_' + this.dynamicTableDivId); LocalPreferences.remove('column_' + "original" + '_width_' + this.dynamicTableDivId); LocalPreferences.remove('column_' + "renamed" + '_width_' + this.dynamicTableDivId); this.newColumn('checked', '', '', 50, true); this.newColumn('original', '', 'Original', 270, true); this.newColumn('renamed', '', 'Renamed', 220, true); this.initColumnsFunctions(); }, /** * Toggles the global checkbox and all checkboxes underneath */ toggleGlobalCheckbox: function() { const checkbox = $('rootMultiRename_cb'); const checkboxes = $$('input.RenamingCB'); for (let i = 0; i < checkboxes.length; ++i) { const node = this.getNode(i); if (checkbox.checked || checkbox.indeterminate) { let cb = checkboxes[i]; cb.checked = true; cb.indeterminate = false; cb.state = "checked"; node.checked = 0; node.full_data.checked = node.checked; } else { let cb = checkboxes[i]; cb.checked = false; cb.indeterminate = false; cb.state = "unchecked"; node.checked = 1; node.full_data.checked = node.checked; } } this.updateGlobalCheckbox(); }, toggleNodeTreeCheckbox: function(rowId, checkState) { const node = this.getNode(rowId); node.checked = checkState; node.full_data.checked = checkState; const checkbox = $(`cbRename${rowId}`); checkbox.checked = node.checked == 0; checkbox.state = checkbox.checked ? "checked" : "unchecked"; for (let i = 0; i < node.children.length; ++i) { this.toggleNodeTreeCheckbox(node.children[i].rowId, checkState); } }, updateGlobalCheckbox: function() { const checkbox = $('rootMultiRename_cb'); const checkboxes = $$('input.RenamingCB'); const isAllChecked = function() { for (let i = 0; i < checkboxes.length; ++i) { if (!checkboxes[i].checked) return false; } return true; }; const isAllUnchecked = function() { for (let i = 0; i < checkboxes.length; ++i) { if (checkboxes[i].checked) return false; } return true; }; if (isAllChecked()) { checkbox.state = "checked"; checkbox.indeterminate = false; checkbox.checked = true; } else if (isAllUnchecked()) { checkbox.state = "unchecked"; checkbox.indeterminate = false; checkbox.checked = false; } else { checkbox.state = "partial"; checkbox.indeterminate = true; checkbox.checked = false; } }, initColumnsFunctions: function() { const that = this; // checked this.columns['checked'].updateTd = function(td, row) { const id = row.rowId; const value = this.getRowValue(row); const treeImg = new Element('img', { src: 'images/L.gif', styles: { 'margin-bottom': -2 } }); const checkbox = new Element('input'); checkbox.set('type', 'checkbox'); checkbox.set('id', 'cbRename' + id); checkbox.set('data-id', id); checkbox.set('class', 'RenamingCB'); checkbox.addEvent('click', function(e) { const node = that.getNode(id); node.checked = e.target.checked ? 0 : 1; node.full_data.checked = node.checked; that.updateGlobalCheckbox(); that.onRowSelectionChange(node); e.stopPropagation(); }); checkbox.checked = value == 0; checkbox.state = checkbox.checked ? "checked" : "unchecked"; checkbox.indeterminate = false; td.adopt(treeImg, checkbox); }; // original this.columns['original'].updateTd = function(td, row) { const id = row.rowId; const fileNameId = 'filesTablefileName' + id; const node = that.getNode(id); if (node.isFolder) { const value = this.getRowValue(row); const dirImgId = 'renameTableDirImg' + id; if ($(dirImgId)) { // just update file name $(fileNameId).set('text', value); } else { const span = new Element('span', { text: value, id: fileNameId }); const dirImg = new Element('img', { src: 'images/directory.svg', styles: { 'width': 15, 'padding-right': 5, 'margin-bottom': -3, 'margin-left': (node.depth * 20) }, id: dirImgId }); const html = dirImg.outerHTML + span.outerHTML; td.set('html', html); } } else { // is file const value = this.getRowValue(row); const span = new Element('span', { text: value, id: fileNameId, styles: { 'margin-left': ((node.depth + 1) * 20) } }); td.set('html', span.outerHTML); } }; // renamed this.columns['renamed'].updateTd = function(td, row) { const id = row.rowId; const fileNameRenamedId = 'filesTablefileRenamed' + id; const value = this.getRowValue(row); const span = new Element('span', { text: value, id: fileNameRenamedId, }); td.set('html', span.outerHTML); }; }, onRowSelectionChange: function(row) {}, selectRow: function() { return; }, reselectRows: function(rowIds) { const that = this; this.deselectAll(); this.tableBody.getElements('tr').each(function(tr) { if (rowIds.indexOf(tr.rowId) > -1) { const node = that.getNode(tr.rowId); node.checked = 0; node.full_data.checked = 0; const checkbox = tr.children[0].getElement('input'); checkbox.state = "checked"; checkbox.indeterminate = false; checkbox.checked = true; } }); this.updateGlobalCheckbox(); }, altRow: function() { let addClass = false; const trs = this.tableBody.getElements('tr'); trs.each(function(tr) { if (tr.hasClass("invisible")) return; if (addClass) { tr.addClass("alt"); tr.removeClass("nonAlt"); } else { tr.removeClass("alt"); tr.addClass("nonAlt"); } addClass = !addClass; }.bind(this)); }, _sortNodesByColumn: function(nodes, column) { nodes.sort(function(row1, row2) { // list folders before files when sorting by name if (column.name === "original") { const node1 = this.getNode(row1.data.rowId); const node2 = this.getNode(row2.data.rowId); if (node1.isFolder && !node2.isFolder) return -1; if (node2.isFolder && !node1.isFolder) return 1; } const res = column.compareRows(row1, row2); return (this.reverseSort === '0') ? res : -res; }.bind(this)); nodes.each(function(node) { if (node.children.length > 0) this._sortNodesByColumn(node.children, column); }.bind(this)); }, _filterNodes: function(node, filterTerms, filteredRows) { if (node.isFolder) { const childAdded = node.children.reduce(function(acc, child) { // we must execute the function before ORing w/ acc or we'll stop checking child nodes after the first successful match return (this._filterNodes(child, filterTerms, filteredRows) || acc); }.bind(this), false); if (childAdded) { const row = this.getRow(node); filteredRows.push(row); return true; } } if (window.qBittorrent.Misc.containsAllTerms(node.original, filterTerms)) { const row = this.getRow(node); filteredRows.push(row); return true; } return false; }, setFilter: function(text) { const filterTerms = text.trim().toLowerCase().split(' '); if ((filterTerms.length === 1) && (filterTerms[0] === '')) this.filterTerms = []; else this.filterTerms = filterTerms; }, getFilteredAndSortedRows: function() { if (this.getRoot() === null) return []; const generateRowsSignature = function(rows) { const rowsData = rows.map(function(row) { return row.full_data; }); return JSON.stringify(rowsData); }; const getFilteredRows = function() { if (this.filterTerms.length === 0) { const nodeArray = this.fileTree.toArray(); const filteredRows = nodeArray.map(function(node) { return this.getRow(node); }.bind(this)); return filteredRows; } const filteredRows = []; this.getRoot().children.each(function(child) { this._filterNodes(child, this.filterTerms, filteredRows); }.bind(this)); filteredRows.reverse(); return filteredRows; }.bind(this); const hasRowsChanged = function(rowsString, prevRowsStringString) { const rowsChanged = (rowsString !== prevRowsStringString); const isFilterTermsChanged = this.filterTerms.reduce(function(acc, term, index) { return (acc || (term !== this.prevFilterTerms[index])); }.bind(this), false); const isFilterChanged = ((this.filterTerms.length !== this.prevFilterTerms.length) || ((this.filterTerms.length > 0) && isFilterTermsChanged)); const isSortedColumnChanged = (this.prevSortedColumn !== this.sortedColumn); const isReverseSortChanged = (this.prevReverseSort !== this.reverseSort); return (rowsChanged || isFilterChanged || isSortedColumnChanged || isReverseSortChanged); }.bind(this); const rowsString = generateRowsSignature(this.rows); if (!hasRowsChanged(rowsString, this.prevRowsString)) { return this.prevFilteredRows; } // sort, then filter const column = this.columns[this.sortedColumn]; this._sortNodesByColumn(this.getRoot().children, column); const filteredRows = getFilteredRows(); this.prevFilterTerms = this.filterTerms; this.prevRowsString = rowsString; this.prevFilteredRows = filteredRows; this.prevSortedColumn = this.sortedColumn; this.prevReverseSort = this.reverseSort; return filteredRows; }, setIgnored: function(rowId, ignore) { const row = this.rows.get(rowId); if (ignore) row.full_data.remaining = 0; else row.full_data.remaining = (row.full_data.size * (1.0 - (row.full_data.progress / 100))); }, setupTr: function(tr) { tr.addEvent('keydown', function(event) { switch (event.key) { case "left": qBittorrent.PropFiles.collapseFolder(this._this.getSelectedRowId()); return false; case "right": qBittorrent.PropFiles.expandFolder(this._this.getSelectedRowId()); return false; } }); } }); const TorrentFilesTable = new Class({ Extends: DynamicTable, filterTerms: [], prevFilterTerms: [], prevRowsString: null, prevFilteredRows: [], prevSortedColumn: null, prevReverseSort: null, fileTree: new window.qBittorrent.FileTree.FileTree(), populateTable: function(root) { this.fileTree.setRoot(root); root.children.each(function(node) { this._addNodeToTable(node, 0); }.bind(this)); }, _addNodeToTable: function(node, depth) { node.depth = depth; if (node.isFolder) { const data = { rowId: node.rowId, size: node.size, checked: node.checked, remaining: node.remaining, progress: node.progress, priority: window.qBittorrent.PropFiles.normalizePriority(node.priority), availability: node.availability, fileId: -1, name: node.name }; node.data = data; node.full_data = data; this.updateRowData(data); } else { node.data.rowId = node.rowId; node.full_data = node.data; this.updateRowData(node.data); } node.children.each(function(child) { this._addNodeToTable(child, depth + 1); }.bind(this)); }, getRoot: function() { return this.fileTree.getRoot(); }, getNode: function(rowId) { return this.fileTree.getNode(rowId); }, getRow: function(node) { const rowId = this.fileTree.getRowId(node); return this.rows.get(rowId); }, initColumns: function() { this.newColumn('checked', '', '', 50, true); this.newColumn('name', '', 'Name', 300, true); this.newColumn('size', '', 'Total Size', 75, true); this.newColumn('progress', '', 'Progress', 100, true); this.newColumn('priority', '', 'Download Priority', 150, true); this.newColumn('remaining', '', 'Remaining', 75, true); this.newColumn('availability', '', 'Availability', 75, true); this.initColumnsFunctions(); }, initColumnsFunctions: function() { const that = this; const displaySize = function(td, row) { const size = window.qBittorrent.Misc.friendlyUnit(this.getRowValue(row), false); td.set('text', size); td.set('title', size); }; const displayPercentage = function(td, row) { const value = window.qBittorrent.Misc.friendlyPercentage(this.getRowValue(row)); td.set('text', value); td.set('title', value); }; // checked this.columns['checked'].updateTd = function(td, row) { const id = row.rowId; const value = this.getRowValue(row); if (window.qBittorrent.PropFiles.isDownloadCheckboxExists(id)) { window.qBittorrent.PropFiles.updateDownloadCheckbox(id, value); } else { const treeImg = new Element('img', { src: 'images/L.gif', styles: { 'margin-bottom': -2 } }); td.adopt(treeImg, window.qBittorrent.PropFiles.createDownloadCheckbox(id, row.full_data.fileId, value)); } }; // name this.columns['name'].updateTd = function(td, row) { const id = row.rowId; const fileNameId = 'filesTablefileName' + id; const node = that.getNode(id); if (node.isFolder) { const value = this.getRowValue(row); const collapseIconId = 'filesTableCollapseIcon' + id; const dirImgId = 'filesTableDirImg' + id; if ($(dirImgId)) { // just update file name $(fileNameId).set('text', value); } else { const collapseIcon = new Element('img', { src: 'images/go-down.svg', styles: { 'margin-left': (node.depth * 20) }, class: "filesTableCollapseIcon", id: collapseIconId, "data-id": id, onclick: "qBittorrent.PropFiles.collapseIconClicked(this)" }); const span = new Element('span', { text: value, id: fileNameId }); const dirImg = new Element('img', { src: 'images/directory.svg', styles: { 'width': 15, 'padding-right': 5, 'margin-bottom': -3 }, id: dirImgId }); const html = collapseIcon.outerHTML + dirImg.outerHTML + span.outerHTML; td.set('html', html); } } else { const value = this.getRowValue(row); const span = new Element('span', { text: value, id: fileNameId, styles: { 'margin-left': ((node.depth + 1) * 20) } }); td.set('html', span.outerHTML); } }; // size this.columns['size'].updateTd = displaySize; // progress this.columns['progress'].updateTd = function(td, row) { const id = row.rowId; const value = this.getRowValue(row); const progressBar = $('pbf_' + id); if (progressBar === null) { td.adopt(new window.qBittorrent.ProgressBar.ProgressBar(value.toFloat(), { id: 'pbf_' + id, width: 80 })); } else { progressBar.setValue(value.toFloat()); } }; // priority this.columns['priority'].updateTd = function(td, row) { const id = row.rowId; const value = this.getRowValue(row); if (window.qBittorrent.PropFiles.isPriorityComboExists(id)) window.qBittorrent.PropFiles.updatePriorityCombo(id, value); else td.adopt(window.qBittorrent.PropFiles.createPriorityCombo(id, row.full_data.fileId, value)); }; // remaining, availability this.columns['remaining'].updateTd = displaySize; this.columns['availability'].updateTd = displayPercentage; }, altRow: function() { let addClass = false; const trs = this.tableBody.getElements('tr'); trs.each(function(tr) { if (tr.hasClass("invisible")) return; if (addClass) { tr.addClass("alt"); tr.removeClass("nonAlt"); } else { tr.removeClass("alt"); tr.addClass("nonAlt"); } addClass = !addClass; }.bind(this)); }, _sortNodesByColumn: function(nodes, column) { nodes.sort(function(row1, row2) { // list folders before files when sorting by name if (column.name === "name") { const node1 = this.getNode(row1.data.rowId); const node2 = this.getNode(row2.data.rowId); if (node1.isFolder && !node2.isFolder) return -1; if (node2.isFolder && !node1.isFolder) return 1; } const res = column.compareRows(row1, row2); return (this.reverseSort === '0') ? res : -res; }.bind(this)); nodes.each(function(node) { if (node.children.length > 0) this._sortNodesByColumn(node.children, column); }.bind(this)); }, _filterNodes: function(node, filterTerms, filteredRows) { if (node.isFolder) { const childAdded = node.children.reduce(function(acc, child) { // we must execute the function before ORing w/ acc or we'll stop checking child nodes after the first successful match return (this._filterNodes(child, filterTerms, filteredRows) || acc); }.bind(this), false); if (childAdded) { const row = this.getRow(node); filteredRows.push(row); return true; } } if (window.qBittorrent.Misc.containsAllTerms(node.name, filterTerms)) { const row = this.getRow(node); filteredRows.push(row); return true; } return false; }, setFilter: function(text) { const filterTerms = text.trim().toLowerCase().split(' '); if ((filterTerms.length === 1) && (filterTerms[0] === '')) this.filterTerms = []; else this.filterTerms = filterTerms; }, getFilteredAndSortedRows: function() { if (this.getRoot() === null) return []; const generateRowsSignature = function(rows) { const rowsData = rows.map(function(row) { return row.full_data; }); return JSON.stringify(rowsData); }; const getFilteredRows = function() { if (this.filterTerms.length === 0) { const nodeArray = this.fileTree.toArray(); const filteredRows = nodeArray.map(function(node) { return this.getRow(node); }.bind(this)); return filteredRows; } const filteredRows = []; this.getRoot().children.each(function(child) { this._filterNodes(child, this.filterTerms, filteredRows); }.bind(this)); filteredRows.reverse(); return filteredRows; }.bind(this); const hasRowsChanged = function(rowsString, prevRowsStringString) { const rowsChanged = (rowsString !== prevRowsStringString); const isFilterTermsChanged = this.filterTerms.reduce(function(acc, term, index) { return (acc || (term !== this.prevFilterTerms[index])); }.bind(this), false); const isFilterChanged = ((this.filterTerms.length !== this.prevFilterTerms.length) || ((this.filterTerms.length > 0) && isFilterTermsChanged)); const isSortedColumnChanged = (this.prevSortedColumn !== this.sortedColumn); const isReverseSortChanged = (this.prevReverseSort !== this.reverseSort); return (rowsChanged || isFilterChanged || isSortedColumnChanged || isReverseSortChanged); }.bind(this); const rowsString = generateRowsSignature(this.rows); if (!hasRowsChanged(rowsString, this.prevRowsString)) { return this.prevFilteredRows; } // sort, then filter const column = this.columns[this.sortedColumn]; this._sortNodesByColumn(this.getRoot().children, column); const filteredRows = getFilteredRows(); this.prevFilterTerms = this.filterTerms; this.prevRowsString = rowsString; this.prevFilteredRows = filteredRows; this.prevSortedColumn = this.sortedColumn; this.prevReverseSort = this.reverseSort; return filteredRows; }, setIgnored: function(rowId, ignore) { const row = this.rows.get(rowId); if (ignore) row.full_data.remaining = 0; else row.full_data.remaining = (row.full_data.size * (1.0 - (row.full_data.progress / 100))); }, setupTr: function(tr) { tr.addEvent('keydown', function(event) { switch (event.key) { case "left": qBittorrent.PropFiles.collapseFolder(this._this.getSelectedRowId()); return false; case "right": qBittorrent.PropFiles.expandFolder(this._this.getSelectedRowId()); return false; } }); } }); const RssFeedTable = new Class({ Extends: DynamicTable, initColumns: function() { this.newColumn('state_icon', '', '', 30, true); this.newColumn('name', '', 'RSS feeds', -1, true); this.columns['state_icon'].dataProperties[0] = ''; // map name row to "[name] ([unread])" this.columns['name'].dataProperties.push('unread'); this.columns['name'].updateTd = function(td, row) { const name = this.getRowValue(row, 0); const unreadCount = this.getRowValue(row, 1); let value = name + ' (' + unreadCount + ')'; td.set('text', value); td.set('title', value); }; }, setupHeaderMenu: function() {}, setupHeaderEvents: function() {}, getFilteredAndSortedRows: function() { return this.rows.getValues(); }, selectRow: function(rowId) { this.selectedRows.push(rowId); this.setRowClass(); this.onSelectedRowChanged(); const rows = this.rows.getValues(); let path = ''; for (let i = 0; i < rows.length; ++i) { if (rows[i].rowId === rowId) { path = rows[i].full_data.dataPath; break; } } window.qBittorrent.Rss.showRssFeed(path); }, setupTr: function(tr) { tr.addEvent('dblclick', function(e) { if (this.rowId !== 0) { window.qBittorrent.Rss.moveItem(this._this.rows.get(this.rowId).full_data.dataPath); return true; } }); }, updateRow: function(tr, fullUpdate) { const row = this.rows.get(tr.rowId); const data = row[fullUpdate ? 'full_data' : 'data']; const tds = tr.getElements('td'); for (let i = 0; i < this.columns.length; ++i) { if (Object.prototype.hasOwnProperty.call(data, this.columns[i].dataProperties[0])) this.columns[i].updateTd(tds[i], row); } row['data'] = {}; tds[0].style.overflow = 'visible'; let indentation = row.full_data.indentation; tds[0].style.paddingLeft = (indentation * 32 + 4) + 'px'; tds[1].style.paddingLeft = (indentation * 32 + 4) + 'px'; }, updateIcons: function() { // state_icon this.rows.each(row => { let img_path; switch (row.full_data.status) { case 'default': img_path = 'images/application-rss.svg'; break; case 'hasError': img_path = 'images/task-reject.svg'; break; case 'isLoading': img_path = 'images/spinner.gif'; break; case 'unread': img_path = 'images/mail-inbox.svg'; break; case 'isFolder': img_path = 'images/folder-documents.svg'; break; } let td; for (let i = 0; i < this.tableBody.rows.length; ++i) { if (this.tableBody.rows[i].rowId === row.rowId) { td = this.tableBody.rows[i].children[0]; break; } } if (td.getChildren('img').length > 0) { const img = td.getChildren('img')[0]; if (img.src.indexOf(img_path) < 0) { img.set('src', img_path); img.set('title', status); } } else { td.adopt(new Element('img', { 'src': img_path, 'class': 'stateIcon', 'height': '22px', 'width': '22px' })); } }); }, newColumn: function(name, style, caption, defaultWidth, defaultVisible) { const column = {}; column['name'] = name; column['title'] = name; column['visible'] = defaultVisible; column['force_hide'] = false; column['caption'] = caption; column['style'] = style; if (defaultWidth !== -1) { column['width'] = defaultWidth; } column['dataProperties'] = [name]; column['getRowValue'] = function(row, pos) { if (pos === undefined) pos = 0; return row['full_data'][this.dataProperties[pos]]; }; column['compareRows'] = function(row1, row2) { const value1 = this.getRowValue(row1); const value2 = this.getRowValue(row2); if ((typeof(value1) === 'number') && (typeof(value2) === 'number')) return compareNumbers(value1, value2); return window.qBittorrent.Misc.naturalSortCollator.compare(value1, value2); }; column['updateTd'] = function(td, row) { const value = this.getRowValue(row); td.set('text', value); td.set('title', value); }; column['onResize'] = null; this.columns.push(column); this.columns[name] = column; this.hiddenTableHeader.appendChild(new Element('th')); this.fixedTableHeader.appendChild(new Element('th')); }, setupCommonEvents: function() { const scrollFn = function() { $(this.dynamicTableFixedHeaderDivId).getElements('table')[0].style.left = -$(this.dynamicTableDivId).scrollLeft + 'px'; }.bind(this); $(this.dynamicTableDivId).addEvent('scroll', scrollFn); } }); const RssArticleTable = new Class({ Extends: DynamicTable, initColumns: function() { this.newColumn('name', '', 'Torrents: (double-click to download)', -1, true); }, setupHeaderMenu: function() {}, setupHeaderEvents: function() {}, getFilteredAndSortedRows: function() { return this.rows.getValues(); }, selectRow: function(rowId) { this.selectedRows.push(rowId); this.setRowClass(); this.onSelectedRowChanged(); const rows = this.rows.getValues(); let articleId = ''; let feedUid = ''; for (let i = 0; i < rows.length; ++i) { if (rows[i].rowId === rowId) { articleId = rows[i].full_data.dataId; feedUid = rows[i].full_data.feedUid; this.tableBody.rows[rows[i].rowId].removeClass('unreadArticle'); break; } } window.qBittorrent.Rss.showDetails(feedUid, articleId); }, setupTr: function(tr) { tr.addEvent('dblclick', function(e) { showDownloadPage([this._this.rows.get(this.rowId).full_data.torrentURL]); return true; }); tr.addClass('torrentsTableContextMenuTarget'); }, updateRow: function(tr, fullUpdate) { const row = this.rows.get(tr.rowId); const data = row[fullUpdate ? 'full_data' : 'data']; if (!row.full_data.isRead) tr.addClass('unreadArticle'); else tr.removeClass('unreadArticle'); const tds = tr.getElements('td'); for (let i = 0; i < this.columns.length; ++i) { if (Object.prototype.hasOwnProperty.call(data, this.columns[i].dataProperties[0])) this.columns[i].updateTd(tds[i], row); } row['data'] = {}; }, newColumn: function(name, style, caption, defaultWidth, defaultVisible) { const column = {}; column['name'] = name; column['title'] = name; column['visible'] = defaultVisible; column['force_hide'] = false; column['caption'] = caption; column['style'] = style; if (defaultWidth !== -1) { column['width'] = defaultWidth; } column['dataProperties'] = [name]; column['getRowValue'] = function(row, pos) { if (pos === undefined) pos = 0; return row['full_data'][this.dataProperties[pos]]; }; column['compareRows'] = function(row1, row2) { const value1 = this.getRowValue(row1); const value2 = this.getRowValue(row2); if ((typeof(value1) === 'number') && (typeof(value2) === 'number')) return compareNumbers(value1, value2); return window.qBittorrent.Misc.naturalSortCollator.compare(value1, value2); }; column['updateTd'] = function(td, row) { const value = this.getRowValue(row); td.set('text', value); td.set('title', value); }; column['onResize'] = null; this.columns.push(column); this.columns[name] = column; this.hiddenTableHeader.appendChild(new Element('th')); this.fixedTableHeader.appendChild(new Element('th')); }, setupCommonEvents: function() { const scrollFn = function() { $(this.dynamicTableFixedHeaderDivId).getElements('table')[0].style.left = -$(this.dynamicTableDivId).scrollLeft + 'px'; }.bind(this); $(this.dynamicTableDivId).addEvent('scroll', scrollFn); } }); const RssDownloaderRulesTable = new Class({ Extends: DynamicTable, initColumns: function() { this.newColumn('checked', '', '', 30, true); this.newColumn('name', '', '', -1, true); this.columns['checked'].updateTd = function(td, row) { if ($('cbRssDlRule' + row.rowId) === null) { const checkbox = new Element('input'); checkbox.set('type', 'checkbox'); checkbox.set('id', 'cbRssDlRule' + row.rowId); checkbox.checked = row.full_data.checked; checkbox.addEvent('click', function(e) { window.qBittorrent.RssDownloader.rssDownloaderRulesTable.updateRowData({ rowId: row.rowId, checked: this.checked }); window.qBittorrent.RssDownloader.modifyRuleState(row.full_data.name, 'enabled', this.checked); e.stopPropagation(); }); td.append(checkbox); } else { $('cbRssDlRule' + row.rowId).checked = row.full_data.checked; } }; }, setupHeaderMenu: function() {}, setupHeaderEvents: function() {}, getFilteredAndSortedRows: function() { return this.rows.getValues(); }, setupTr: function(tr) { tr.addEvent('dblclick', function(e) { window.qBittorrent.RssDownloader.renameRule(this._this.rows.get(this.rowId).full_data.name); return true; }); }, newColumn: function(name, style, caption, defaultWidth, defaultVisible) { const column = {}; column['name'] = name; column['title'] = name; column['visible'] = defaultVisible; column['force_hide'] = false; column['caption'] = caption; column['style'] = style; if (defaultWidth !== -1) { column['width'] = defaultWidth; } column['dataProperties'] = [name]; column['getRowValue'] = function(row, pos) { if (pos === undefined) pos = 0; return row['full_data'][this.dataProperties[pos]]; }; column['compareRows'] = function(row1, row2) { const value1 = this.getRowValue(row1); const value2 = this.getRowValue(row2); if ((typeof(value1) === 'number') && (typeof(value2) === 'number')) return compareNumbers(value1, value2); return window.qBittorrent.Misc.naturalSortCollator.compare(value1, value2); }; column['updateTd'] = function(td, row) { const value = this.getRowValue(row); td.set('text', value); td.set('title', value); }; column['onResize'] = null; this.columns.push(column); this.columns[name] = column; this.hiddenTableHeader.appendChild(new Element('th')); this.fixedTableHeader.appendChild(new Element('th')); }, selectRow: function(rowId) { this.selectedRows.push(rowId); this.setRowClass(); this.onSelectedRowChanged(); const rows = this.rows.getValues(); let name = ''; for (let i = 0; i < rows.length; ++i) { if (rows[i].rowId === rowId) { name = rows[i].full_data.name; break; } } window.qBittorrent.RssDownloader.showRule(name); } }); const RssDownloaderFeedSelectionTable = new Class({ Extends: DynamicTable, initColumns: function() { this.newColumn('checked', '', '', 30, true); this.newColumn('name', '', '', -1, true); this.columns['checked'].updateTd = function(td, row) { if ($('cbRssDlFeed' + row.rowId) === null) { const checkbox = new Element('input'); checkbox.set('type', 'checkbox'); checkbox.set('id', 'cbRssDlFeed' + row.rowId); checkbox.checked = row.full_data.checked; checkbox.addEvent('click', function(e) { window.qBittorrent.RssDownloader.rssDownloaderFeedSelectionTable.updateRowData({ rowId: row.rowId, checked: this.checked }); e.stopPropagation(); }); td.append(checkbox); } else { $('cbRssDlFeed' + row.rowId).checked = row.full_data.checked; } }; }, setupHeaderMenu: function() {}, setupHeaderEvents: function() {}, getFilteredAndSortedRows: function() { return this.rows.getValues(); }, newColumn: function(name, style, caption, defaultWidth, defaultVisible) { const column = {}; column['name'] = name; column['title'] = name; column['visible'] = defaultVisible; column['force_hide'] = false; column['caption'] = caption; column['style'] = style; if (defaultWidth !== -1) { column['width'] = defaultWidth; } column['dataProperties'] = [name]; column['getRowValue'] = function(row, pos) { if (pos === undefined) pos = 0; return row['full_data'][this.dataProperties[pos]]; }; column['compareRows'] = function(row1, row2) { const value1 = this.getRowValue(row1); const value2 = this.getRowValue(row2); if ((typeof(value1) === 'number') && (typeof(value2) === 'number')) return compareNumbers(value1, value2); return window.qBittorrent.Misc.naturalSortCollator.compare(value1, value2); }; column['updateTd'] = function(td, row) { const value = this.getRowValue(row); td.set('text', value); td.set('title', value); }; column['onResize'] = null; this.columns.push(column); this.columns[name] = column; this.hiddenTableHeader.appendChild(new Element('th')); this.fixedTableHeader.appendChild(new Element('th')); }, selectRow: function() {} }); const RssDownloaderArticlesTable = new Class({ Extends: DynamicTable, initColumns: function() { this.newColumn('name', '', '', -1, true); }, setupHeaderMenu: function() {}, setupHeaderEvents: function() {}, getFilteredAndSortedRows: function() { return this.rows.getValues(); }, newColumn: function(name, style, caption, defaultWidth, defaultVisible) { const column = {}; column['name'] = name; column['title'] = name; column['visible'] = defaultVisible; column['force_hide'] = false; column['caption'] = caption; column['style'] = style; if (defaultWidth !== -1) { column['width'] = defaultWidth; } column['dataProperties'] = [name]; column['getRowValue'] = function(row, pos) { if (pos === undefined) pos = 0; return row['full_data'][this.dataProperties[pos]]; }; column['compareRows'] = function(row1, row2) { const value1 = this.getRowValue(row1); const value2 = this.getRowValue(row2); if ((typeof(value1) === 'number') && (typeof(value2) === 'number')) return compareNumbers(value1, value2); return window.qBittorrent.Misc.naturalSortCollator.compare(value1, value2); }; column['updateTd'] = function(td, row) { const value = this.getRowValue(row); td.set('text', value); td.set('title', value); }; column['onResize'] = null; this.columns.push(column); this.columns[name] = column; this.hiddenTableHeader.appendChild(new Element('th')); this.fixedTableHeader.appendChild(new Element('th')); }, selectRow: function() {}, updateRow: function(tr, fullUpdate) { const row = this.rows.get(tr.rowId); const data = row[fullUpdate ? 'full_data' : 'data']; if (row.full_data.isFeed) { tr.addClass('articleTableFeed'); tr.removeClass('articleTableArticle'); } else { tr.removeClass('articleTableFeed'); tr.addClass('articleTableArticle'); } const tds = tr.getElements('td'); for (let i = 0; i < this.columns.length; ++i) { if (Object.prototype.hasOwnProperty.call(data, this.columns[i].dataProperties[0])) this.columns[i].updateTd(tds[i], row); } row['data'] = {}; } }); const LogMessageTable = new Class({ Extends: DynamicTable, filterText: '', filteredLength: function() { return this.tableBody.getElements('tr').length; }, initColumns: function() { this.newColumn('rowId', '', 'ID', 50, true); this.newColumn('message', '', 'Message', 350, true); this.newColumn('timestamp', '', 'Timestamp', 150, true); this.newColumn('type', '', 'Log Type', 100, true); this.initColumnsFunctions(); }, initColumnsFunctions: function() { this.columns['timestamp'].updateTd = function(td, row) { const date = new Date(this.getRowValue(row) * 1000).toLocaleString(); td.set({ 'text': date, 'title': date }); }; this.columns['type'].updateTd = function(td, row) { //Type of the message: Log::NORMAL: 1, Log::INFO: 2, Log::WARNING: 4, Log::CRITICAL: 8 let logLevel, addClass; switch (this.getRowValue(row).toInt()) { case 1: logLevel = 'Normal'; addClass = 'logNormal'; break; case 2: logLevel = 'Info'; addClass = 'logInfo'; break; case 4: logLevel = 'Warning'; addClass = 'logWarning'; break; case 8: logLevel = 'Critical'; addClass = 'logCritical'; break; default: logLevel = 'Unknown'; addClass = 'logUnknown'; break; } td.set({ 'text': logLevel, 'title': logLevel }); td.getParent('tr').set('class', 'logTableRow ' + addClass); }; }, getFilteredAndSortedRows: function() { let filteredRows = []; const rows = this.rows.getValues(); this.filterText = window.qBittorrent.Log.getFilterText(); const filterTerms = (this.filterText.length > 0) ? this.filterText.toLowerCase().split(' ') : []; const logLevels = window.qBittorrent.Log.getSelectedLevels(); if (filterTerms.length > 0 || logLevels.length < 4) { for (let i = 0; i < rows.length; ++i) { if (logLevels.indexOf(rows[i].full_data.type.toString()) == -1) continue; if (filterTerms.length > 0 && !window.qBittorrent.Misc.containsAllTerms(rows[i].full_data.message, filterTerms)) continue; filteredRows.push(rows[i]); } } else { filteredRows = rows; } filteredRows.sort(function(row1, row2) { const column = this.columns[this.sortedColumn]; const res = column.compareRows(row1, row2); return (this.reverseSort == '0') ? res : -res; }.bind(this)); return filteredRows; }, setupCommonEvents: function() {}, setupTr: function(tr) { tr.addClass('logTableRow'); } }); const LogPeerTable = new Class({ Extends: LogMessageTable, initColumns: function() { this.newColumn('rowId', '', 'ID', 50, true); this.newColumn('ip', '', 'IP', 150, true); this.newColumn('timestamp', '', 'Timestamp', 150, true); this.newColumn('blocked', '', 'Status', 150, true); this.newColumn('reason', '', 'Reason', 150, true); this.columns['timestamp'].updateTd = function(td, row) { const date = new Date(this.getRowValue(row) * 1000).toLocaleString(); td.set({ 'text': date, 'title': date }); }; this.columns['blocked'].updateTd = function(td, row) { let status, addClass; if (this.getRowValue(row)) { status = 'Blocked'; addClass = 'peerBlocked'; } else { status = 'Banned'; addClass = 'peerBanned'; } td.set({ 'text': status, 'title': status }); td.getParent('tr').set('class', 'logTableRow ' + addClass); }; }, getFilteredAndSortedRows: function() { let filteredRows = []; const rows = this.rows.getValues(); this.filterText = window.qBittorrent.Log.getFilterText(); const filterTerms = (this.filterText.length > 0) ? this.filterText.toLowerCase().split(' ') : []; if (filterTerms.length > 0) { for (let i = 0; i < rows.length; ++i) { if (filterTerms.length > 0 && !window.qBittorrent.Misc.containsAllTerms(rows[i].full_data.ip, filterTerms)) continue; filteredRows.push(rows[i]); } } else { filteredRows = rows; } filteredRows.sort(function(row1, row2) { const column = this.columns[this.sortedColumn]; const res = column.compareRows(row1, row2); return (this.reverseSort == '0') ? res : -res; }.bind(this)); return filteredRows; } }); return exports(); })(); Object.freeze(window.qBittorrent.DynamicTable); /*************************************************************/