diff --git a/src/webui/abstractwebapplication.cpp b/src/webui/abstractwebapplication.cpp index 5b74f1bd8..b8f8c0575 100644 --- a/src/webui/abstractwebapplication.cpp +++ b/src/webui/abstractwebapplication.cpp @@ -244,7 +244,7 @@ void AbstractWebApplication::translateDocument(QString& data) "options_imp", "Preferences", "TrackersAdditionDlg", "ScanFoldersModel", "PropTabBar", "TorrentModel", "downloadFromURL", "MainWindow", "misc", "StatusBar", "AboutDlg", "about", "PeerListWidget", "StatusFiltersWidget", - "CategoryFiltersList" + "CategoryFiltersList", "TransferListDelegate" }; const size_t context_count = sizeof(contexts) / sizeof(contexts[0]); int i = 0; diff --git a/src/webui/btjson.cpp b/src/webui/btjson.cpp index 7fa3a9388..f221e15ac 100644 --- a/src/webui/btjson.cpp +++ b/src/webui/btjson.cpp @@ -109,6 +109,19 @@ static const char KEY_TORRENT_FORCE_START[] = "force_start"; static const char KEY_TORRENT_SAVE_PATH[] = "save_path"; static const char KEY_TORRENT_ADDED_ON[] = "added_on"; static const char KEY_TORRENT_COMPLETION_ON[] = "completion_on"; +static const char KEY_TORRENT_TRACKER[] = "tracker"; +static const char KEY_TORRENT_DL_LIMIT[] = "dl_limit"; +static const char KEY_TORRENT_UP_LIMIT[] = "up_limit"; +static const char KEY_TORRENT_AMOUNT_DOWNLOADED[] = "downloaded"; +static const char KEY_TORRENT_AMOUNT_UPLOADED[] = "uploaded"; +static const char KEY_TORRENT_AMOUNT_DOWNLOADED_SESSION[] = "downloaded_session"; +static const char KEY_TORRENT_AMOUNT_UPLOADED_SESSION[] = "uploaded_session"; +static const char KEY_TORRENT_AMOUNT_LEFT[] = "remaining"; +static const char KEY_TORRENT_AMOUNT_COMPLETED[] = "completed"; +static const char KEY_TORRENT_RATIO_LIMIT[] = "ratio_limit"; +static const char KEY_TORRENT_LAST_SEEN_COMPLETE_TIME[] = "seen_complete"; +static const char KEY_TORRENT_LAST_ACTIVITY_TIME[] = "last_activity"; +static const char KEY_TORRENT_TOTAL_SIZE[] = "total_size"; // Peer keys static const char KEY_PEER_IP[] = "ip"; @@ -125,6 +138,7 @@ static const char KEY_PEER_CONNECTION_TYPE[] = "connection"; static const char KEY_PEER_FLAGS[] = "flags"; static const char KEY_PEER_FLAGS_DESCRIPTION[] = "flags_desc"; static const char KEY_PEER_RELEVANCE[] = "relevance"; +static const char KEY_PEER_FILES[] = "files"; // Tracker keys static const char KEY_TRACKER_URL[] = "url"; @@ -347,6 +361,21 @@ QByteArray btjson::getTorrents(QString filter, QString category, * - "state": Torrent state * - "seq_dl": Torrent sequential download state * - "f_l_piece_prio": Torrent first last piece priority state + * - "completion_on": Torrent copletion time + * - "tracker": Torrent tracker + * - "dl_limit": Torrent download limit + * - "up_limit": Torrent upload limit + * - "downloaded": Amount of data downloaded + * - "uploaded": Amount of data uploaded + * - "downloaded_session": Amount of data downloaded since program open + * - "uploaded_session": Amount of data uploaded since program open + * - "amount_left": Amount of data left to download + * - "save_path": Torrent save path + * - "completed": Amount of data completed + * - "ratio_limit": Upload share ratio limit + * - "seen_complete": Indicates the time when the torrent was last seen complete/whole + * - "last_activity": Last time when a chunk was downloaded/uploaded + * - "total_size": Size including unwanted data * Server state map may contain the following keys: * - "connection_status": connection status * - "dht_nodes": DHT nodes count @@ -369,6 +398,16 @@ QByteArray btjson::getSyncMainData(int acceptedResponseId, QVariantMap &lastData foreach (BitTorrent::TorrentHandle *const torrent, session->torrents()) { QVariantMap map = toMap(torrent); map.remove(KEY_TORRENT_HASH); + + // Calculated last activity time can differ from actual value by up to 10 seconds (this is a libtorrent issue). + // So we don't need unnecessary updates of last activity time in response. + if (lastData.contains("torrents") && lastData["torrents"].toHash().contains(torrent->hash()) && + lastData["torrents"].toHash()[torrent->hash()].toMap().contains(KEY_TORRENT_LAST_ACTIVITY_TIME)) { + uint lastValue = lastData["torrents"].toHash()[torrent->hash()].toMap()[KEY_TORRENT_LAST_ACTIVITY_TIME].toUInt(); + if (qAbs((int)(lastValue - map[KEY_TORRENT_LAST_ACTIVITY_TIME].toUInt())) < 15) + map[KEY_TORRENT_LAST_ACTIVITY_TIME] = lastValue; + } + torrents[torrent->hash()] = map; } @@ -429,6 +468,8 @@ QByteArray btjson::getSyncTorrentPeersData(int acceptedResponseId, QString hash, peer[KEY_PEER_FLAGS] = pi.flags(); peer[KEY_PEER_FLAGS_DESCRIPTION] = pi.flagsDescription(); peer[KEY_PEER_RELEVANCE] = pi.relevance(); + peer[KEY_PEER_FILES] = torrent->info().filesForPiece(pi.downloadingPieceIndex()).join(QLatin1String("\n")); + peers[pi.address().ip.toString() + ":" + QString::number(pi.address().port)] = peer; } @@ -723,6 +764,27 @@ QVariantMap toMap(BitTorrent::TorrentHandle *const torrent) ret[KEY_TORRENT_SAVE_PATH] = Utils::Fs::toNativePath(torrent->savePath()); ret[KEY_TORRENT_ADDED_ON] = torrent->addedTime().toTime_t(); ret[KEY_TORRENT_COMPLETION_ON] = torrent->completedTime().toTime_t(); + ret[KEY_TORRENT_TRACKER] = torrent->currentTracker(); + ret[KEY_TORRENT_DL_LIMIT] = torrent->downloadLimit(); + ret[KEY_TORRENT_UP_LIMIT] = torrent->uploadLimit(); + ret[KEY_TORRENT_AMOUNT_DOWNLOADED] = torrent->totalDownload(); + ret[KEY_TORRENT_AMOUNT_UPLOADED] = torrent->totalUpload(); + ret[KEY_TORRENT_AMOUNT_DOWNLOADED_SESSION] = torrent->totalPayloadDownload(); + ret[KEY_TORRENT_AMOUNT_UPLOADED_SESSION] = torrent->totalPayloadUpload(); + ret[KEY_TORRENT_AMOUNT_LEFT] = torrent->incompletedSize(); + ret[KEY_TORRENT_AMOUNT_COMPLETED] = torrent->completedSize(); + ret[KEY_TORRENT_RATIO_LIMIT] = torrent->maxRatio(); + ret[KEY_TORRENT_LAST_SEEN_COMPLETE_TIME] = torrent->lastSeenComplete().toTime_t(); + + if (torrent->isPaused() || torrent->isChecking()) + ret[KEY_TORRENT_LAST_ACTIVITY_TIME] = 0; + else { + QDateTime dt = QDateTime::currentDateTime(); + dt = dt.addSecs(-torrent->timeSinceActivity()); + ret[KEY_TORRENT_LAST_ACTIVITY_TIME] = dt.toTime_t(); + } + + ret[KEY_TORRENT_TOTAL_SIZE] = torrent->totalSize(); return ret; } diff --git a/src/webui/www/private/index.html b/src/webui/www/private/index.html index 1b5eb46f8..4ed47a2c6 100644 --- a/src/webui/www/private/index.html +++ b/src/webui/www/private/index.html @@ -107,7 +107,7 @@
QBT_TR(URL)QBT_TR |
@@ -65,19 +65,27 @@
---|
QBT_TR(URL)QBT_TR | @@ -90,7 +98,7 @@
---|
@@ -106,6 +114,6 @@ diff --git a/src/webui/www/public/scripts/contextmenu.js b/src/webui/www/public/scripts/contextmenu.js index c8f83483e..11cfbf222 100644 --- a/src/webui/www/public/scripts/contextmenu.js +++ b/src/webui/www/public/scripts/contextmenu.js @@ -57,6 +57,11 @@ var ContextMenu = new Class({ adjustMenuPosition: function(e) { this.updateMenuItems(); + var scrollableMenuMaxHeight = document.documentElement.clientHeight * 0.75; + + if (this.menu.hasClass('scrollableMenu')) + this.menu.setStyle('max-height', scrollableMenuMaxHeight); + // draw the menu off-screen to know the menu dimentions this.menu.setStyles({ left: '-999em', @@ -69,7 +74,7 @@ var ContextMenu = new Class({ if (xPos + this.menu.offsetWidth > document.documentElement.clientWidth) xPos -= this.menu.offsetWidth; if (yPos + this.menu.offsetHeight > document.documentElement.clientHeight) - yPos -= this.menu.offsetHeight; + yPos = document.documentElement.clientHeight - this.menu.offsetHeight; if (xPos < 0) xPos = 0; if (yPos < 0) @@ -85,6 +90,8 @@ var ContextMenu = new Class({ var uls = this.menu.getElementsByTagName('ul'); for (var i = 0; i < uls.length; i++) { var ul = uls[i]; + if (ul.hasClass('scrollableMenu')) + ul.setStyle('max-height', scrollableMenuMaxHeight); var rectParent = ul.parentNode.getBoundingClientRect(); var xPosOrigin = rectParent.left; var yPosOrigin = rectParent.bottom; @@ -93,7 +100,7 @@ var ContextMenu = new Class({ if (xPos + ul.offsetWidth > document.documentElement.clientWidth) xPos -= (ul.offsetWidth + rectParent.width - 2); if (yPos + ul.offsetHeight > document.documentElement.clientHeight) - yPos -= (ul.offsetHeight - rectParent.height - 2); + yPos = document.documentElement.clientHeight - ul.offsetHeight; if (xPos < 0) xPos = 0; if (yPos < 0) @@ -228,7 +235,7 @@ var ContextMenu = new Class({ //execute an action execute: function (action, element) { if (this.options.actions[action]) { - this.options.actions[action](element, this); + this.options.actions[action](element, this, action); } return this; } diff --git a/src/webui/www/public/scripts/dynamicTable.js b/src/webui/www/public/scripts/dynamicTable.js index 15f30fb9c..09235f09a 100644 --- a/src/webui/www/public/scripts/dynamicTable.js +++ b/src/webui/www/public/scripts/dynamicTable.js @@ -31,35 +31,303 @@ **************************************************************/ +var DynamicTableHeaderContextMenuClass = null; + var DynamicTable = new Class({ initialize : function () {}, - setup : function (tableId, tableHeaderId, context_menu) { - this.tableId = tableId; - this.tableHeaderId = tableHeaderId; - this.table = $(tableId); + 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.cur = new Array(); + this.selectedRows = new Array(); this.columns = new Array(); - this.context_menu = context_menu; - this.sortedColumn = getLocalStorageItem('sorted_column_' + this.tableId, 0); - this.reverseSort = getLocalStorageItem('reverse_sort_' + this.tableId, '0'); + this.contextMenu = contextMenu; + this.sortedColumn = getLocalStorageItem('sorted_column_' + this.dynamicTableDivId, 0); + this.reverseSort = getLocalStorageItem('reverse_sort_' + this.dynamicTableDivId, '0'); this.initColumns(); this.loadColumnsOrder(); - this.updateHeader(); + this.updateTableHeaders(); + this.setupCommonEvents(); + this.setupHeaderEvents(); + this.setupHeaderMenu(); + }, + + setupCommonEvents : function () { + var scrollFn = function() { + $(this.dynamicTableFixedHeaderDivId).getElements('table')[0].style.left = + -$(this.dynamicTableDivId).scrollLeft + 'px'; + }.bind(this); + + $(this.dynamicTableDivId).addEvent('scroll', scrollFn); + + var resizeFn = function() { + var panel = $(this.dynamicTableDivId).getParent('.panel'); + var h = panel.getBoundingClientRect().height - $(this.dynamicTableFixedHeaderDivId).getBoundingClientRect().height; + $(this.dynamicTableDivId).style.height = h + 'px'; + + // Workaround due to inaccurate calculation of elements heights by browser + + var 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) + + var checkResizeFn = function() { + var panel = $(this.dynamicTableDivId).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; + + var 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', ''); + } + } + + var mouseMoveFn = function (e) { + var brect = e.target.getBoundingClientRect(); + var 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') { + var previousVisibleSibling = e.target.getPrevious('[class=""]'); + var borderChangeElement = previousVisibleSibling; + var 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); + + var mouseOutFn = function (e) { + resetElementBorderStyle(e.target); + }.bind(this); + + var onBeforeStart = function (el) { + this.clickedTh = el; + this.currentHeaderAction = 'start'; + this.dragMovement = false; + this.dragStartX = this.lastClientX; + }.bind(this); + + var 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); + + var onDrag = function (el, event) { + if (this.currentHeaderAction === 'resize') { + var 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); + + var onComplete = function (el, event) { + resetElementBorderStyle(this.lastHoverTh); + el.setStyle('background-color', ''); + if (this.currentHeaderAction === 'resize') + localStorage.setItem('column_' + this.resizeTh.columnName + '_width_' + this.dynamicTableDivId, this.columns[this.resizeTh.columnName].width); + if ((this.currentHeaderAction === 'drag') && (el !== this.lastHoverTh)) { + this.saveColumnsOrder(); + var val = localStorage.getItem('columns_order_' + this.dynamicTableDivId).split(','); + val.erase(el.columnName); + var pos = val.indexOf(this.lastHoverTh.columnName); + if (this.dropSide === 'right') pos++; + val.splice(pos, 0, el.columnName); + localStorage.setItem('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); + + var onCancel = function (el) { + this.currentHeaderAction = ''; + this.setSortedColumn(el.columnName); + }.bind(this); + + var ths = this.fixedTableHeader.getElements('th'); + + for (var i = 0; i < ths.length; i++) { + var th = ths[i]; + th.addEvent('mousemove', mouseMoveFn); + th.addEvent('mouseout', mouseOutFn); + th.makeResizable({ + modifiers : {x: '', y: ''}, + onBeforeStart : onBeforeStart, + onStart : onStart, + onDrag : onDrag, + onComplete : onComplete, + onCancel : onCancel + }) + } + }, + + setupDynamicTableHeaderContextMenuClass : function () { + if (!DynamicTableHeaderContextMenuClass) { + DynamicTableHeaderContextMenuClass = new Class({ + Extends: ContextMenu, + updateMenuItems: function () { + for (var 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'; + localStorage.setItem('column_' + columnName + '_visible_' + this.dynamicTableDivId, show ? '1' : '0'); + this.updateColumn(columnName); + }, + + setupHeaderMenu : function () { + this.setupDynamicTableHeaderContextMenuClass(); + + var menuId = this.dynamicTableDivId + '_headerMenu'; + + var ul = new Element('ul', {id: menuId, class: 'contextMenu scrollableMenu'}); + + var createLi = function(columnName, text) { + var html = ' |
---|