diff --git a/src/base/bittorrent/peerinfo.cpp b/src/base/bittorrent/peerinfo.cpp index abbe45df5..857424aee 100644 --- a/src/base/bittorrent/peerinfo.cpp +++ b/src/base/bittorrent/peerinfo.cpp @@ -29,6 +29,7 @@ #include "base/net/geoipmanager.h" #include "base/utils/string.h" #include "base/unicodestrings.h" +#include "base/bittorrent/torrenthandle.h" #include "peerinfo.h" namespace libt = libtorrent; @@ -49,9 +50,11 @@ PeerAddress::PeerAddress(QHostAddress ip, ushort port) // PeerInfo -PeerInfo::PeerInfo(const libt::peer_info &nativeInfo) +PeerInfo::PeerInfo(const TorrentHandle *torrent, const libt::peer_info &nativeInfo) : m_nativeInfo(nativeInfo) { + calcRelevance(torrent); + determineFlags(); } bool PeerInfo::fromDHT() const @@ -253,3 +256,155 @@ QString PeerInfo::connectionType() const return connection; } + +void PeerInfo::calcRelevance(const TorrentHandle *torrent) +{ + const QBitArray &allPieces = torrent->pieces(); + const QBitArray &peerPieces = pieces(); + + int localMissing = 0; + int remoteHaves = 0; + + for (int i = 0; i < allPieces.size(); ++i) { + if (!allPieces[i]) { + ++localMissing; + if (peerPieces[i]) + ++remoteHaves; + } + } + + if (localMissing == 0) + m_relevance = 0.0; + else + m_relevance = static_cast(remoteHaves) / localMissing; +} + +qreal PeerInfo::relevance() const +{ + return m_relevance; +} + +void PeerInfo::determineFlags() +{ + if (isInteresting()) { + //d = Your client wants to download, but peer doesn't want to send (interested and choked) + if (isRemoteChocked()) { + m_flags += "d "; + m_flagsDescription += tr("interested(local) and choked(peer)"); + m_flagsDescription += ", "; + } + else { + //D = Currently downloading (interested and not choked) + m_flags += "D "; + m_flagsDescription += tr("interested(local) and unchoked(peer)"); + m_flagsDescription += ", "; + } + } + + if (isRemoteInterested()) { + //u = Peer wants your client to upload, but your client doesn't want to (interested and choked) + if (isChocked()) { + m_flags += "u "; + m_flagsDescription += tr("interested(peer) and choked(local)"); + m_flagsDescription += ", "; + } + else { + //U = Currently uploading (interested and not choked) + m_flags += "U "; + m_flagsDescription += tr("interested(peer) and unchoked(local)"); + m_flagsDescription += ", "; + } + } + + //O = Optimistic unchoke + if (optimisticUnchoke()) { + m_flags += "O "; + m_flagsDescription += tr("optimistic unchoke"); + m_flagsDescription += ", "; + } + + //S = Peer is snubbed + if (isSnubbed()) { + m_flags += "S "; + m_flagsDescription += tr("peer snubbed"); + m_flagsDescription += ", "; + } + + //I = Peer is an incoming connection + if (!isLocalConnection()) { + m_flags += "I "; + m_flagsDescription += tr("incoming connection"); + m_flagsDescription += ", "; + } + + //K = Peer is unchoking your client, but your client is not interested + if (!isRemoteChocked() && !isInteresting()) { + m_flags += "K "; + m_flagsDescription += tr("not interested(local) and unchoked(peer)"); + m_flagsDescription += ", "; + } + + //? = Your client unchoked the peer but the peer is not interested + if (!isChocked() && !isRemoteInterested()) { + m_flags += "? "; + m_flagsDescription += tr("not interested(peer) and unchoked(local)"); + m_flagsDescription += ", "; + } + + //X = Peer was included in peerlists obtained through Peer Exchange (PEX) + if (fromPeX()) { + m_flags += "X "; + m_flagsDescription += tr("peer from PEX"); + m_flagsDescription += ", "; + } + + //H = Peer was obtained through DHT + if (fromDHT()) { + m_flags += "H "; + m_flagsDescription += tr("peer from DHT"); + m_flagsDescription += ", "; + } + + //E = Peer is using Protocol Encryption (all traffic) + if (isRC4Encrypted()) { + m_flags += "E "; + m_flagsDescription += tr("encrypted traffic"); + m_flagsDescription += ", "; + } + + //e = Peer is using Protocol Encryption (handshake) + if (isPlaintextEncrypted()) { + m_flags += "e "; + m_flagsDescription += tr("encrypted handshake"); + m_flagsDescription += ", "; + } + + //P = Peer is using uTorrent uTP + + if (useUTPSocket()) { + m_flags += "P "; + m_flagsDescription += QString::fromUtf8(C_UTP); + m_flagsDescription += ", "; + } + + //L = Peer is local + if (fromLSD()) { + m_flags += "L"; + m_flagsDescription += tr("peer from LSD"); + } + + m_flags = m_flags.trimmed(); + m_flagsDescription = m_flagsDescription.trimmed(); + if (m_flagsDescription.endsWith(',', Qt::CaseInsensitive)) + m_flagsDescription.chop(1); +} + +QString PeerInfo::flags() const +{ + return m_flags; +} + +QString PeerInfo::flagsDescription() const +{ + return m_flagsDescription; +} diff --git a/src/base/bittorrent/peerinfo.h b/src/base/bittorrent/peerinfo.h index 48922ec8e..ae63e2d54 100644 --- a/src/base/bittorrent/peerinfo.h +++ b/src/base/bittorrent/peerinfo.h @@ -33,9 +33,12 @@ #include #include +#include namespace BitTorrent { + class TorrentHandle; + struct PeerAddress { QHostAddress ip; @@ -47,8 +50,10 @@ namespace BitTorrent class PeerInfo { + Q_DECLARE_TR_FUNCTIONS(PeerInfo) + public: - PeerInfo(const libtorrent::peer_info &nativeInfo); + PeerInfo(const TorrentHandle *torrent, const libtorrent::peer_info &nativeInfo); bool fromDHT() const; bool fromPeX() const; @@ -89,12 +94,21 @@ namespace BitTorrent qlonglong totalDownload() const; QBitArray pieces() const; QString connectionType() const; + qreal relevance() const; + QString flags() const; + QString flagsDescription() const; #ifndef DISABLE_COUNTRIES_RESOLUTION QString country() const; #endif private: + void calcRelevance(const TorrentHandle *torrent); + void determineFlags(); + libtorrent::peer_info m_nativeInfo; + qreal m_relevance; + QString m_flags; + QString m_flagsDescription; }; } diff --git a/src/base/bittorrent/torrenthandle.cpp b/src/base/bittorrent/torrenthandle.cpp index b6e6f4e0e..de6ca9c5e 100644 --- a/src/base/bittorrent/torrenthandle.cpp +++ b/src/base/bittorrent/torrenthandle.cpp @@ -960,7 +960,7 @@ QList TorrentHandle::peers() const SAFE_CALL(get_peer_info, nativePeers); foreach (const libt::peer_info &peer, nativePeers) - peers << peer; + peers << PeerInfo(this, peer); return peers; } diff --git a/src/gui/properties/peerlistwidget.cpp b/src/gui/properties/peerlistwidget.cpp index c6a8360c8..14729cb38 100644 --- a/src/gui/properties/peerlistwidget.cpp +++ b/src/gui/properties/peerlistwidget.cpp @@ -51,464 +51,338 @@ #include "peerlistsortmodel.h" #include "peerlistwidget.h" -PeerListWidget::PeerListWidget(PropertiesWidget *parent): - QTreeView(parent), m_properties(parent), m_displayFlags(false) +PeerListWidget::PeerListWidget(PropertiesWidget *parent) + : QTreeView(parent) + , m_properties(parent) + , m_displayFlags(false) { - // Load settings - loadSettings(); - // Visual settings - setUniformRowHeights(true); - setRootIsDecorated(false); - setItemsExpandable(false); - setAllColumnsShowFocus(true); - setSelectionMode(QAbstractItemView::ExtendedSelection); - // List Model - m_listModel = new QStandardItemModel(0, PeerListDelegate::COL_COUNT); - m_listModel->setHeaderData(PeerListDelegate::COUNTRY, Qt::Horizontal, QVariant()); // Country flag column - m_listModel->setHeaderData(PeerListDelegate::IP, Qt::Horizontal, tr("IP")); - m_listModel->setHeaderData(PeerListDelegate::PORT, Qt::Horizontal, tr("Port")); - m_listModel->setHeaderData(PeerListDelegate::FLAGS, Qt::Horizontal, tr("Flags")); - m_listModel->setHeaderData(PeerListDelegate::CONNECTION, Qt::Horizontal, tr("Connection")); - m_listModel->setHeaderData(PeerListDelegate::CLIENT, Qt::Horizontal, tr("Client", "i.e.: Client application")); - m_listModel->setHeaderData(PeerListDelegate::PROGRESS, Qt::Horizontal, tr("Progress", "i.e: % downloaded")); - m_listModel->setHeaderData(PeerListDelegate::DOWN_SPEED, Qt::Horizontal, tr("Down Speed", "i.e: Download speed")); - m_listModel->setHeaderData(PeerListDelegate::UP_SPEED, Qt::Horizontal, tr("Up Speed", "i.e: Upload speed")); - m_listModel->setHeaderData(PeerListDelegate::TOT_DOWN, Qt::Horizontal, tr("Downloaded", "i.e: total data downloaded")); - m_listModel->setHeaderData(PeerListDelegate::TOT_UP, Qt::Horizontal, tr("Uploaded", "i.e: total data uploaded")); - m_listModel->setHeaderData(PeerListDelegate::RELEVANCE, Qt::Horizontal, tr("Relevance", "i.e: How relevant this peer is to us. How many pieces it has that we don't.")); - // Proxy model to support sorting without actually altering the underlying model - m_proxyModel = new PeerListSortModel(); - m_proxyModel->setDynamicSortFilter(true); - m_proxyModel->setSourceModel(m_listModel); - m_proxyModel->setSortCaseSensitivity(Qt::CaseInsensitive); - setModel(m_proxyModel); - //Explicitly set the column visibility. When columns are added/removed - //between versions this prevents some of them being hidden due to - //incorrect restoreState() being used. - for (unsigned int i=0; iresolvePeerCountries()) - hideColumn(PeerListDelegate::COUNTRY); - //To also mitigate the above issue, we have to resize each column when - //its size is 0, because explicitly 'showing' the column isn't enough - //in the above scenario. - for (unsigned int i=0; isortIndicatorSection()); - copyHotkey = new QShortcut(QKeySequence(Qt::ControlModifier + Qt::Key_C), this, SLOT(copySelectedPeers()), 0, Qt::WidgetShortcut); + // Load settings + loadSettings(); + // Visual settings + setUniformRowHeights(true); + setRootIsDecorated(false); + setItemsExpandable(false); + setAllColumnsShowFocus(true); + setSelectionMode(QAbstractItemView::ExtendedSelection); + // List Model + m_listModel = new QStandardItemModel(0, PeerListDelegate::COL_COUNT); + m_listModel->setHeaderData(PeerListDelegate::COUNTRY, Qt::Horizontal, QVariant()); // Country flag column + m_listModel->setHeaderData(PeerListDelegate::IP, Qt::Horizontal, tr("IP")); + m_listModel->setHeaderData(PeerListDelegate::PORT, Qt::Horizontal, tr("Port")); + m_listModel->setHeaderData(PeerListDelegate::FLAGS, Qt::Horizontal, tr("Flags")); + m_listModel->setHeaderData(PeerListDelegate::CONNECTION, Qt::Horizontal, tr("Connection")); + m_listModel->setHeaderData(PeerListDelegate::CLIENT, Qt::Horizontal, tr("Client", "i.e.: Client application")); + m_listModel->setHeaderData(PeerListDelegate::PROGRESS, Qt::Horizontal, tr("Progress", "i.e: % downloaded")); + m_listModel->setHeaderData(PeerListDelegate::DOWN_SPEED, Qt::Horizontal, tr("Down Speed", "i.e: Download speed")); + m_listModel->setHeaderData(PeerListDelegate::UP_SPEED, Qt::Horizontal, tr("Up Speed", "i.e: Upload speed")); + m_listModel->setHeaderData(PeerListDelegate::TOT_DOWN, Qt::Horizontal, tr("Downloaded", "i.e: total data downloaded")); + m_listModel->setHeaderData(PeerListDelegate::TOT_UP, Qt::Horizontal, tr("Uploaded", "i.e: total data uploaded")); + m_listModel->setHeaderData(PeerListDelegate::RELEVANCE, Qt::Horizontal, tr("Relevance", "i.e: How relevant this peer is to us. How many pieces it has that we don't.")); + // Proxy model to support sorting without actually altering the underlying model + m_proxyModel = new PeerListSortModel(); + m_proxyModel->setDynamicSortFilter(true); + m_proxyModel->setSourceModel(m_listModel); + m_proxyModel->setSortCaseSensitivity(Qt::CaseInsensitive); + setModel(m_proxyModel); + //Explicitly set the column visibility. When columns are added/removed + //between versions this prevents some of them being hidden due to + //incorrect restoreState() being used. + for (unsigned int i = 0; i < PeerListDelegate::IP_HIDDEN; i++) + showColumn(i); + hideColumn(PeerListDelegate::IP_HIDDEN); + hideColumn(PeerListDelegate::COL_COUNT); + if (!Preferences::instance()->resolvePeerCountries()) + hideColumn(PeerListDelegate::COUNTRY); + //To also mitigate the above issue, we have to resize each column when + //its size is 0, because explicitly 'showing' the column isn't enough + //in the above scenario. + for (unsigned int i = 0; i < PeerListDelegate::IP_HIDDEN; i++) + if (!columnWidth(i)) + resizeColumnToContents(i); + // Context menu + setContextMenuPolicy(Qt::CustomContextMenu); + connect(this, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(showPeerListMenu(QPoint))); + // List delegate + m_listDelegate = new PeerListDelegate(this); + setItemDelegate(m_listDelegate); + // Enable sorting + setSortingEnabled(true); + // IP to Hostname resolver + updatePeerHostNameResolutionState(); + // SIGNAL/SLOT + connect(header(), SIGNAL(sectionClicked(int)), SLOT(handleSortColumnChanged(int))); + handleSortColumnChanged(header()->sortIndicatorSection()); + m_copyHotkey = new QShortcut(QKeySequence(Qt::ControlModifier + Qt::Key_C), this, SLOT(copySelectedPeers()), 0, Qt::WidgetShortcut); } PeerListWidget::~PeerListWidget() { - saveSettings(); - delete m_proxyModel; - delete m_listModel; - delete m_listDelegate; - if (m_resolver) - delete m_resolver; - delete copyHotkey; + saveSettings(); + delete m_proxyModel; + delete m_listModel; + delete m_listDelegate; + if (m_resolver) + delete m_resolver; + delete m_copyHotkey; } void PeerListWidget::updatePeerHostNameResolutionState() { - if (Preferences::instance()->resolvePeerHostNames()) { - if (!m_resolver) { - m_resolver = new Net::ReverseResolution(this); - connect(m_resolver, SIGNAL(ipResolved(QString,QString)), SLOT(handleResolved(QString,QString))); - loadPeers(m_properties->getCurrentTorrent(), true); + if (Preferences::instance()->resolvePeerHostNames()) { + if (!m_resolver) { + m_resolver = new Net::ReverseResolution(this); + connect(m_resolver, SIGNAL(ipResolved(QString, QString)), SLOT(handleResolved(QString, QString))); + loadPeers(m_properties->getCurrentTorrent(), true); + } + } + else if (m_resolver) { + delete m_resolver; } - } else { - if (m_resolver) - delete m_resolver; - } } void PeerListWidget::updatePeerCountryResolutionState() { - if (Preferences::instance()->resolvePeerCountries() != m_displayFlags) { - m_displayFlags = !m_displayFlags; - if (m_displayFlags) { - loadPeers(m_properties->getCurrentTorrent()); - showColumn(PeerListDelegate::COUNTRY); - resizeColumnToContents(PeerListDelegate::COUNTRY); + if (Preferences::instance()->resolvePeerCountries() != m_displayFlags) { + m_displayFlags = !m_displayFlags; + if (m_displayFlags) { + loadPeers(m_properties->getCurrentTorrent()); + showColumn(PeerListDelegate::COUNTRY); + resizeColumnToContents(PeerListDelegate::COUNTRY); + } + else { + hideColumn(PeerListDelegate::COUNTRY); + } } - else { - hideColumn(PeerListDelegate::COUNTRY); - } - } } void PeerListWidget::showPeerListMenu(const QPoint&) { - QMenu menu; - bool empty_menu = true; - BitTorrent::TorrentHandle *const torrent = m_properties->getCurrentTorrent(); - if (!torrent) return; + QMenu menu; + bool emptyMenu = true; + BitTorrent::TorrentHandle *const torrent = m_properties->getCurrentTorrent(); + if (!torrent) return; - // Add Peer Action - QAction *addPeerAct = 0; - if (!torrent->isQueued() && !torrent->isChecking()) { - addPeerAct = menu.addAction(GuiIconProvider::instance()->getIcon("user-group-new"), tr("Add a new peer...")); - empty_menu = false; - } - QAction *banAct = 0; - QAction *copyPeerAct = 0; - if (!selectionModel()->selectedRows().isEmpty()) { - copyPeerAct = menu.addAction(GuiIconProvider::instance()->getIcon("edit-copy"), tr("Copy selected")); - menu.addSeparator(); - banAct = menu.addAction(GuiIconProvider::instance()->getIcon("user-group-delete"), tr("Ban peer permanently")); - empty_menu = false; - } - if (empty_menu) return; - QAction *act = menu.exec(QCursor::pos()); - if (act == 0) return; - if (act == addPeerAct) { - QList peersList = PeersAdditionDlg::askForPeers(); - int peerCount = 0; - foreach (const BitTorrent::PeerAddress &addr, peersList) { - if (torrent->connectPeer(addr)) { - qDebug("Adding peer %s...", qPrintable(addr.ip.toString())); - Logger::instance()->addMessage(tr("Manually adding peer '%1'...").arg(addr.ip.toString())); - peerCount++; - } - else { - Logger::instance()->addMessage(tr("The peer '%1' could not be added to this torrent.").arg(addr.ip.toString()), Log::WARNING); - } + // Add Peer Action + QAction *addPeerAct = 0; + if (!torrent->isQueued() && !torrent->isChecking()) { + addPeerAct = menu.addAction(GuiIconProvider::instance()->getIcon("user-group-new"), tr("Add a new peer...")); + emptyMenu = false; + } + QAction *banAct = 0; + QAction *copyPeerAct = 0; + if (!selectionModel()->selectedRows().isEmpty()) { + copyPeerAct = menu.addAction(GuiIconProvider::instance()->getIcon("edit-copy"), tr("Copy selected")); + menu.addSeparator(); + banAct = menu.addAction(GuiIconProvider::instance()->getIcon("user-group-delete"), tr("Ban peer permanently")); + emptyMenu = false; + } + if (emptyMenu) return; + QAction *act = menu.exec(QCursor::pos()); + if (act == 0) return; + if (act == addPeerAct) { + QList peersList = PeersAdditionDlg::askForPeers(); + int peerCount = 0; + foreach (const BitTorrent::PeerAddress &addr, peersList) { + if (torrent->connectPeer(addr)) { + qDebug("Adding peer %s...", qPrintable(addr.ip.toString())); + Logger::instance()->addMessage(tr("Manually adding peer '%1'...").arg(addr.ip.toString())); + peerCount++; + } + else { + Logger::instance()->addMessage(tr("The peer '%1' could not be added to this torrent.").arg(addr.ip.toString()), Log::WARNING); + } + } + if (peerCount < peersList.length()) + QMessageBox::information(0, tr("Peer addition"), tr("Some peers could not be added. Check the Log for details.")); + else if (peerCount > 0) + QMessageBox::information(0, tr("Peer addition"), tr("The peers were added to this torrent.")); + return; + } + if (act == banAct) { + banSelectedPeers(); + return; + } + if (act == copyPeerAct) { + copySelectedPeers(); + return; } - if (peerCount < peersList.length()) - QMessageBox::information(0, tr("Peer addition"), tr("Some peers could not be added. Check the Log for details.")); - else if (peerCount > 0) - QMessageBox::information(0, tr("Peer addition"), tr("The peers were added to this torrent.")); - return; - } - if (act == banAct) { - banSelectedPeers(); - return; - } - if (act == copyPeerAct) { - copySelectedPeers(); - return; - } } void PeerListWidget::banSelectedPeers() { - // Confirm first - int ret = QMessageBox::question(this, tr("Ban peer permanently"), tr("Are you sure you want to ban permanently the selected peers?"), - tr("&Yes"), tr("&No"), - QString(), 0, 1); - if (ret) - return; + // Confirm first + int ret = QMessageBox::question(this, tr("Ban peer permanently"), tr("Are you sure you want to ban permanently the selected peers?"), + tr("&Yes"), tr("&No"), + QString(), 0, 1); + if (ret) + return; - QModelIndexList selectedIndexes = selectionModel()->selectedRows(); - foreach (const QModelIndex &index, selectedIndexes) { - int row = m_proxyModel->mapToSource(index).row(); - QString ip = m_listModel->data(m_listModel->index(row, PeerListDelegate::IP_HIDDEN)).toString(); - qDebug("Banning peer %s...", ip.toLocal8Bit().data()); - Logger::instance()->addMessage(tr("Manually banning peer '%1'...").arg(ip)); - BitTorrent::Session::instance()->banIP(ip); - } - // Refresh list - loadPeers(m_properties->getCurrentTorrent()); + QModelIndexList selectedIndexes = selectionModel()->selectedRows(); + foreach (const QModelIndex &index, selectedIndexes) { + int row = m_proxyModel->mapToSource(index).row(); + QString ip = m_listModel->data(m_listModel->index(row, PeerListDelegate::IP_HIDDEN)).toString(); + qDebug("Banning peer %s...", ip.toLocal8Bit().data()); + Logger::instance()->addMessage(tr("Manually banning peer '%1'...").arg(ip)); + BitTorrent::Session::instance()->banIP(ip); + } + // Refresh list + loadPeers(m_properties->getCurrentTorrent()); } void PeerListWidget::copySelectedPeers() { - QModelIndexList selectedIndexes = selectionModel()->selectedRows(); - QStringList selectedPeers; - foreach (const QModelIndex &index, selectedIndexes) { - int row = m_proxyModel->mapToSource(index).row(); - QString ip = m_listModel->data(m_listModel->index(row, PeerListDelegate::IP_HIDDEN)).toString(); - QString myport = m_listModel->data(m_listModel->index(row, PeerListDelegate::PORT)).toString(); - if (ip.indexOf(".") == -1) // IPv6 - selectedPeers << "[" + ip + "]:" + myport; - else // IPv4 - selectedPeers << ip + ":" + myport; - } - QApplication::clipboard()->setText(selectedPeers.join("\n")); -} - -void PeerListWidget::clear() { - qDebug("clearing peer list"); - m_peerItems.clear(); - m_peerAddresses.clear(); - m_missingFlags.clear(); - int nbrows = m_listModel->rowCount(); - if (nbrows > 0) { - qDebug("Cleared %d peers", nbrows); - m_listModel->removeRows(0, nbrows); - } -} - -void PeerListWidget::loadSettings() { - header()->restoreState(Preferences::instance()->getPeerListState()); -} - -void PeerListWidget::saveSettings() const { - Preferences::instance()->setPeerListState(header()->saveState()); -} - -void PeerListWidget::loadPeers(BitTorrent::TorrentHandle *const torrent, bool force_hostname_resolution) { - if (!torrent) return; - - QList peers = torrent->peers(); - QSet old_peers_set = m_peerItems.keys().toSet(); - - foreach (const BitTorrent::PeerInfo &peer, peers) { - BitTorrent::PeerAddress addr = peer.address(); - if (addr.ip.isNull()) continue; - - QString peer_ip = addr.ip.toString(); - if (m_peerItems.contains(peer_ip)) { - // Update existing peer - updatePeer(peer_ip, torrent, peer); - old_peers_set.remove(peer_ip); - if (force_hostname_resolution && m_resolver) { - m_resolver->resolve(peer_ip); - } - } else { - // Add new peer - m_peerItems[peer_ip] = addPeer(peer_ip, torrent, peer); - m_peerAddresses[peer_ip] = addr; - // Resolve peer host name is asked - if (m_resolver) - m_resolver->resolve(peer_ip); + QModelIndexList selectedIndexes = selectionModel()->selectedRows(); + QStringList selectedPeers; + foreach (const QModelIndex &index, selectedIndexes) { + int row = m_proxyModel->mapToSource(index).row(); + QString ip = m_listModel->data(m_listModel->index(row, PeerListDelegate::IP_HIDDEN)).toString(); + QString myport = m_listModel->data(m_listModel->index(row, PeerListDelegate::PORT)).toString(); + if (ip.indexOf(".") == -1) // IPv6 + selectedPeers << "[" + ip + "]:" + myport; + else // IPv4 + selectedPeers << ip + ":" + myport; } - } - // Delete peers that are gone - QSetIterator it(old_peers_set); - while(it.hasNext()) { - const QString& ip = it.next(); - m_missingFlags.remove(ip); - m_peerAddresses.remove(ip); - QStandardItem *item = m_peerItems.take(ip); - m_listModel->removeRow(item->row()); - } + QApplication::clipboard()->setText(selectedPeers.join("\n")); } -QStandardItem* PeerListWidget::addPeer(const QString& ip, BitTorrent::TorrentHandle *const torrent, const BitTorrent::PeerInfo &peer) { - int row = m_listModel->rowCount(); - // Adding Peer to peer list - m_listModel->insertRow(row); - m_listModel->setData(m_listModel->index(row, PeerListDelegate::IP), ip); - m_listModel->setData(m_listModel->index(row, PeerListDelegate::IP), ip, Qt::ToolTipRole); - m_listModel->setData(m_listModel->index(row, PeerListDelegate::PORT), peer.address().port); - m_listModel->setData(m_listModel->index(row, PeerListDelegate::IP_HIDDEN), ip); - if (m_displayFlags) { - const QIcon ico = GuiIconProvider::instance()->getFlagIcon(peer.country()); - if (!ico.isNull()) { - m_listModel->setData(m_listModel->index(row, PeerListDelegate::COUNTRY), ico, Qt::DecorationRole); - const QString country_name = Net::GeoIPManager::CountryName(peer.country()); - m_listModel->setData(m_listModel->index(row, PeerListDelegate::COUNTRY), country_name, Qt::ToolTipRole); - } else { - m_missingFlags.insert(ip); +void PeerListWidget::clear() +{ + qDebug("clearing peer list"); + m_peerItems.clear(); + m_peerAddresses.clear(); + m_missingFlags.clear(); + int nbrows = m_listModel->rowCount(); + if (nbrows > 0) { + qDebug("Cleared %d peers", nbrows); + m_listModel->removeRows(0, nbrows); } - } - m_listModel->setData(m_listModel->index(row, PeerListDelegate::CONNECTION), peer.connectionType()); - QString flags, tooltip; - getFlags(peer, flags, tooltip); - m_listModel->setData(m_listModel->index(row, PeerListDelegate::FLAGS), flags); - m_listModel->setData(m_listModel->index(row, PeerListDelegate::FLAGS), tooltip, Qt::ToolTipRole); - m_listModel->setData(m_listModel->index(row, PeerListDelegate::CLIENT), peer.client()); - m_listModel->setData(m_listModel->index(row, PeerListDelegate::PROGRESS), peer.progress()); - m_listModel->setData(m_listModel->index(row, PeerListDelegate::DOWN_SPEED), peer.payloadDownSpeed()); - m_listModel->setData(m_listModel->index(row, PeerListDelegate::UP_SPEED), peer.payloadUpSpeed()); - m_listModel->setData(m_listModel->index(row, PeerListDelegate::TOT_DOWN), peer.totalDownload()); - m_listModel->setData(m_listModel->index(row, PeerListDelegate::TOT_UP), peer.totalUpload()); - m_listModel->setData(m_listModel->index(row, PeerListDelegate::RELEVANCE), getPeerRelevance(torrent->pieces(), peer.pieces())); - return m_listModel->item(row, PeerListDelegate::IP); } -void PeerListWidget::updatePeer(const QString &ip, BitTorrent::TorrentHandle *const torrent, const BitTorrent::PeerInfo &peer) { - QStandardItem *item = m_peerItems.value(ip); - int row = item->row(); - if (m_displayFlags) { - const QIcon ico = GuiIconProvider::instance()->getFlagIcon(peer.country()); - if (!ico.isNull()) { - m_listModel->setData(m_listModel->index(row, PeerListDelegate::COUNTRY), ico, Qt::DecorationRole); - const QString country_name = Net::GeoIPManager::CountryName(peer.country()); - m_listModel->setData(m_listModel->index(row, PeerListDelegate::COUNTRY), country_name, Qt::ToolTipRole); - m_missingFlags.remove(ip); +void PeerListWidget::loadSettings() +{ + header()->restoreState(Preferences::instance()->getPeerListState()); +} + +void PeerListWidget::saveSettings() const +{ + Preferences::instance()->setPeerListState(header()->saveState()); +} + +void PeerListWidget::loadPeers(BitTorrent::TorrentHandle *const torrent, bool forceHostnameResolution) +{ + if (!torrent) return; + + QList peers = torrent->peers(); + QSet oldeersSet = m_peerItems.keys().toSet(); + + foreach (const BitTorrent::PeerInfo &peer, peers) { + BitTorrent::PeerAddress addr = peer.address(); + if (addr.ip.isNull()) continue; + + QString peerIp = addr.ip.toString(); + if (m_peerItems.contains(peerIp)) { + // Update existing peer + updatePeer(peerIp, torrent, peer); + oldeersSet.remove(peerIp); + if (forceHostnameResolution && m_resolver) + m_resolver->resolve(peerIp); + } + else { + // Add new peer + m_peerItems[peerIp] = addPeer(peerIp, torrent, peer); + m_peerAddresses[peerIp] = addr; + // Resolve peer host name is asked + if (m_resolver) + m_resolver->resolve(peerIp); + } + } + // Delete peers that are gone + QSetIterator it(oldeersSet); + while (it.hasNext()) { + const QString& ip = it.next(); + m_missingFlags.remove(ip); + m_peerAddresses.remove(ip); + QStandardItem *item = m_peerItems.take(ip); + m_listModel->removeRow(item->row()); } - } - m_listModel->setData(m_listModel->index(row, PeerListDelegate::CONNECTION), peer.connectionType()); - QString flags, tooltip; - getFlags(peer, flags, tooltip); - m_listModel->setData(m_listModel->index(row, PeerListDelegate::PORT), peer.address().port); - m_listModel->setData(m_listModel->index(row, PeerListDelegate::FLAGS), flags); - m_listModel->setData(m_listModel->index(row, PeerListDelegate::FLAGS), tooltip, Qt::ToolTipRole); - m_listModel->setData(m_listModel->index(row, PeerListDelegate::CLIENT), peer.client()); - m_listModel->setData(m_listModel->index(row, PeerListDelegate::PROGRESS), peer.progress()); - m_listModel->setData(m_listModel->index(row, PeerListDelegate::DOWN_SPEED), peer.payloadDownSpeed()); - m_listModel->setData(m_listModel->index(row, PeerListDelegate::UP_SPEED), peer.payloadUpSpeed()); - m_listModel->setData(m_listModel->index(row, PeerListDelegate::TOT_DOWN), peer.totalDownload()); - m_listModel->setData(m_listModel->index(row, PeerListDelegate::TOT_UP), peer.totalUpload()); - m_listModel->setData(m_listModel->index(row, PeerListDelegate::RELEVANCE), getPeerRelevance(torrent->pieces(), peer.pieces())); } -void PeerListWidget::handleResolved(const QString &ip, const QString &hostname) { - QStandardItem *item = m_peerItems.value(ip, 0); - if (item) { - qDebug("Resolved %s -> %s", qPrintable(ip), qPrintable(hostname)); - item->setData(hostname, Qt::DisplayRole); - } +QStandardItem* PeerListWidget::addPeer(const QString& ip, BitTorrent::TorrentHandle *const torrent, const BitTorrent::PeerInfo &peer) +{ + int row = m_listModel->rowCount(); + // Adding Peer to peer list + m_listModel->insertRow(row); + m_listModel->setData(m_listModel->index(row, PeerListDelegate::IP), ip); + m_listModel->setData(m_listModel->index(row, PeerListDelegate::IP), ip, Qt::ToolTipRole); + m_listModel->setData(m_listModel->index(row, PeerListDelegate::PORT), peer.address().port); + m_listModel->setData(m_listModel->index(row, PeerListDelegate::IP_HIDDEN), ip); + if (m_displayFlags) { + const QIcon ico = GuiIconProvider::instance()->getFlagIcon(peer.country()); + if (!ico.isNull()) { + m_listModel->setData(m_listModel->index(row, PeerListDelegate::COUNTRY), ico, Qt::DecorationRole); + const QString countryName = Net::GeoIPManager::CountryName(peer.country()); + m_listModel->setData(m_listModel->index(row, PeerListDelegate::COUNTRY), countryName, Qt::ToolTipRole); + } + else { + m_missingFlags.insert(ip); + } + } + m_listModel->setData(m_listModel->index(row, PeerListDelegate::CONNECTION), peer.connectionType()); + m_listModel->setData(m_listModel->index(row, PeerListDelegate::FLAGS), peer.flags()); + m_listModel->setData(m_listModel->index(row, PeerListDelegate::FLAGS), peer.flagsDescription(), Qt::ToolTipRole); + m_listModel->setData(m_listModel->index(row, PeerListDelegate::CLIENT), peer.client()); + m_listModel->setData(m_listModel->index(row, PeerListDelegate::PROGRESS), peer.progress()); + m_listModel->setData(m_listModel->index(row, PeerListDelegate::DOWN_SPEED), peer.payloadDownSpeed()); + m_listModel->setData(m_listModel->index(row, PeerListDelegate::UP_SPEED), peer.payloadUpSpeed()); + m_listModel->setData(m_listModel->index(row, PeerListDelegate::TOT_DOWN), peer.totalDownload()); + m_listModel->setData(m_listModel->index(row, PeerListDelegate::TOT_UP), peer.totalUpload()); + m_listModel->setData(m_listModel->index(row, PeerListDelegate::RELEVANCE), peer.relevance()); + return m_listModel->item(row, PeerListDelegate::IP); +} + +void PeerListWidget::updatePeer(const QString &ip, BitTorrent::TorrentHandle *const torrent, const BitTorrent::PeerInfo &peer) +{ + QStandardItem *item = m_peerItems.value(ip); + int row = item->row(); + if (m_displayFlags) { + const QIcon ico = GuiIconProvider::instance()->getFlagIcon(peer.country()); + if (!ico.isNull()) { + m_listModel->setData(m_listModel->index(row, PeerListDelegate::COUNTRY), ico, Qt::DecorationRole); + const QString countryName = Net::GeoIPManager::CountryName(peer.country()); + m_listModel->setData(m_listModel->index(row, PeerListDelegate::COUNTRY), countryName, Qt::ToolTipRole); + m_missingFlags.remove(ip); + } + } + m_listModel->setData(m_listModel->index(row, PeerListDelegate::CONNECTION), peer.connectionType()); + m_listModel->setData(m_listModel->index(row, PeerListDelegate::PORT), peer.address().port); + m_listModel->setData(m_listModel->index(row, PeerListDelegate::FLAGS), peer.flags()); + m_listModel->setData(m_listModel->index(row, PeerListDelegate::FLAGS), peer.flagsDescription(), Qt::ToolTipRole); + m_listModel->setData(m_listModel->index(row, PeerListDelegate::CLIENT), peer.client()); + m_listModel->setData(m_listModel->index(row, PeerListDelegate::PROGRESS), peer.progress()); + m_listModel->setData(m_listModel->index(row, PeerListDelegate::DOWN_SPEED), peer.payloadDownSpeed()); + m_listModel->setData(m_listModel->index(row, PeerListDelegate::UP_SPEED), peer.payloadUpSpeed()); + m_listModel->setData(m_listModel->index(row, PeerListDelegate::TOT_DOWN), peer.totalDownload()); + m_listModel->setData(m_listModel->index(row, PeerListDelegate::TOT_UP), peer.totalUpload()); + m_listModel->setData(m_listModel->index(row, PeerListDelegate::RELEVANCE), peer.relevance()); +} + +void PeerListWidget::handleResolved(const QString &ip, const QString &hostname) +{ + QStandardItem *item = m_peerItems.value(ip, 0); + if (item) { + qDebug("Resolved %s -> %s", qPrintable(ip), qPrintable(hostname)); + item->setData(hostname, Qt::DisplayRole); + } } void PeerListWidget::handleSortColumnChanged(int col) { - if (col == PeerListDelegate::COUNTRY) { - qDebug("Sorting by decoration"); - m_proxyModel->setSortRole(Qt::ToolTipRole); - } else { - m_proxyModel->setSortRole(Qt::DisplayRole); - } -} - -void PeerListWidget::getFlags(const BitTorrent::PeerInfo &peer, QString& flags, QString& tooltip) -{ - if (peer.isInteresting()) { - //d = Your client wants to download, but peer doesn't want to send (interested and choked) - if (peer.isRemoteChocked()) { - flags += "d "; - tooltip += tr("interested(local) and choked(peer)"); - tooltip += ", "; + if (col == PeerListDelegate::COUNTRY) { + qDebug("Sorting by decoration"); + m_proxyModel->setSortRole(Qt::ToolTipRole); } else { - //D = Currently downloading (interested and not choked) - flags += "D "; - tooltip += tr("interested(local) and unchoked(peer)"); - tooltip += ", "; + m_proxyModel->setSortRole(Qt::DisplayRole); } - } - - if (peer.isRemoteInterested()) { - //u = Peer wants your client to upload, but your client doesn't want to (interested and choked) - if (peer.isChocked()) { - flags += "u "; - tooltip += tr("interested(peer) and choked(local)"); - tooltip += ", "; - } - else { - //U = Currently uploading (interested and not choked) - flags += "U "; - tooltip += tr("interested(peer) and unchoked(local)"); - tooltip += ", "; - } - } - - //O = Optimistic unchoke - if (peer.optimisticUnchoke()) { - flags += "O "; - tooltip += tr("optimistic unchoke"); - tooltip += ", "; - } - - //S = Peer is snubbed - if (peer.isSnubbed()) { - flags += "S "; - tooltip += tr("peer snubbed"); - tooltip += ", "; - } - - //I = Peer is an incoming connection - if (!peer.isLocalConnection()) { - flags += "I "; - tooltip += tr("incoming connection"); - tooltip += ", "; - } - - //K = Peer is unchoking your client, but your client is not interested - if (!peer.isRemoteChocked() && !peer.isInteresting()) { - flags += "K "; - tooltip += tr("not interested(local) and unchoked(peer)"); - tooltip += ", "; - } - - //? = Your client unchoked the peer but the peer is not interested - if (!peer.isChocked() && !peer.isRemoteInterested()) { - flags += "? "; - tooltip += tr("not interested(peer) and unchoked(local)"); - tooltip += ", "; - } - - //X = Peer was included in peerlists obtained through Peer Exchange (PEX) - if (peer.fromPeX()) { - flags += "X "; - tooltip += tr("peer from PEX"); - tooltip += ", "; - } - - //H = Peer was obtained through DHT - if (peer.fromDHT()) { - flags += "H "; - tooltip += tr("peer from DHT"); - tooltip += ", "; - } - - //E = Peer is using Protocol Encryption (all traffic) - if (peer.isRC4Encrypted()) { - flags += "E "; - tooltip += tr("encrypted traffic"); - tooltip += ", "; - } - - //e = Peer is using Protocol Encryption (handshake) - if (peer.isPlaintextEncrypted()) { - flags += "e "; - tooltip += tr("encrypted handshake"); - tooltip += ", "; - } - - //P = Peer is using uTorrent uTP - - if (peer.useUTPSocket()) { - flags += "P "; - tooltip += QString::fromUtf8(C_UTP); - tooltip += ", "; - } - - //L = Peer is local - if (peer.fromLSD()) { - flags += "L"; - tooltip += tr("peer from LSD"); - } - - flags = flags.trimmed(); - tooltip = tooltip.trimmed(); - if (tooltip.endsWith(',', Qt::CaseInsensitive)) - tooltip.chop(1); } -qreal PeerListWidget::getPeerRelevance(const QBitArray &allPieces, const QBitArray &peerPieces) -{ - int localMissing = 0; - int remoteHaves = 0; - - for (int i = 0; i < allPieces.size(); ++i) { - if (!allPieces[i]) { - ++localMissing; - if (peerPieces[i]) - ++remoteHaves; - } - } - - if (localMissing == 0) - return 0.0; - - return static_cast(remoteHaves) / localMissing; -} diff --git a/src/gui/properties/peerlistwidget.h b/src/gui/properties/peerlistwidget.h index 656f87998..c7ec81ad7 100644 --- a/src/gui/properties/peerlistwidget.h +++ b/src/gui/properties/peerlistwidget.h @@ -54,52 +54,46 @@ QT_END_NAMESPACE namespace BitTorrent { - -class TorrentHandle; -class PeerInfo; -struct PeerAddress; - + class TorrentHandle; + class PeerInfo; + struct PeerAddress; } -class PeerListWidget : public QTreeView { - Q_OBJECT +class PeerListWidget: public QTreeView +{ + Q_OBJECT public: - PeerListWidget(PropertiesWidget *parent); - ~PeerListWidget(); + explicit PeerListWidget(PropertiesWidget *parent); + ~PeerListWidget(); -public slots: - void loadPeers(BitTorrent::TorrentHandle *const torrent, bool force_hostname_resolution = false); - QStandardItem *addPeer(const QString &ip, BitTorrent::TorrentHandle *const torrent, const BitTorrent::PeerInfo &peer); - void updatePeer(const QString &ip, BitTorrent::TorrentHandle *const torrent, const BitTorrent::PeerInfo &peer); - void handleResolved(const QString &ip, const QString &hostname); - void updatePeerHostNameResolutionState(); - void updatePeerCountryResolutionState(); - void clear(); + void loadPeers(BitTorrent::TorrentHandle *const torrent, bool forceHostnameResolution = false); + QStandardItem *addPeer(const QString &ip, BitTorrent::TorrentHandle *const torrent, const BitTorrent::PeerInfo &peer); + void updatePeer(const QString &ip, BitTorrent::TorrentHandle *const torrent, const BitTorrent::PeerInfo &peer); + void handleResolved(const QString &ip, const QString &hostname); + void updatePeerHostNameResolutionState(); + void updatePeerCountryResolutionState(); + void clear(); -protected slots: - void loadSettings(); - void saveSettings() const; - void showPeerListMenu(const QPoint&); - void banSelectedPeers(); - void copySelectedPeers(); - void handleSortColumnChanged(int col); +private slots: + void loadSettings(); + void saveSettings() const; + void showPeerListMenu(const QPoint&); + void banSelectedPeers(); + void copySelectedPeers(); + void handleSortColumnChanged(int col); private: - static void getFlags(const BitTorrent::PeerInfo &peer, QString &flags, QString &tooltip); - qreal getPeerRelevance(const QBitArray &allPieces, const QBitArray &peerPieces); - -private: - QStandardItemModel *m_listModel; - PeerListDelegate *m_listDelegate; - PeerListSortModel *m_proxyModel; - QHash m_peerItems; - QHash m_peerAddresses; - QSet m_missingFlags; - QPointer m_resolver; - PropertiesWidget *m_properties; - bool m_displayFlags; - QShortcut *copyHotkey; + QStandardItemModel *m_listModel; + PeerListDelegate *m_listDelegate; + PeerListSortModel *m_proxyModel; + QHash m_peerItems; + QHash m_peerAddresses; + QSet m_missingFlags; + QPointer m_resolver; + PropertiesWidget *m_properties; + bool m_displayFlags; + QShortcut *m_copyHotkey; }; #endif // PEERLISTWIDGET_H diff --git a/src/webui/btjson.cpp b/src/webui/btjson.cpp index 51e779f37..0702f39d5 100644 --- a/src/webui/btjson.cpp +++ b/src/webui/btjson.cpp @@ -36,7 +36,9 @@ #include "base/bittorrent/sessionstatus.h" #include "base/bittorrent/torrenthandle.h" #include "base/bittorrent/trackerentry.h" +#include "base/bittorrent/peerinfo.h" #include "base/torrentfilter.h" +#include "base/net/geoipmanager.h" #include "jsonutils.h" #include @@ -105,6 +107,22 @@ static const char KEY_TORRENT_SUPER_SEEDING[] = "super_seeding"; static const char KEY_TORRENT_FORCE_START[] = "force_start"; static const char KEY_TORRENT_SAVE_PATH[] = "save_path"; +// Peer keys +static const char KEY_PEER_IP[] = "ip"; +static const char KEY_PEER_PORT[] = "port"; +static const char KEY_PEER_COUNTRY_CODE[] = "country_code"; +static const char KEY_PEER_COUNTRY[] = "country"; +static const char KEY_PEER_CLIENT[] = "client"; +static const char KEY_PEER_PROGRESS[] = "progress"; +static const char KEY_PEER_DOWN_SPEED[] = "dl_speed"; +static const char KEY_PEER_UP_SPEED[] = "up_speed"; +static const char KEY_PEER_TOT_DOWN[] = "downloaded"; +static const char KEY_PEER_TOT_UP[] = "uploaded"; +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"; + // Tracker keys static const char KEY_TRACKER_URL[] = "url"; static const char KEY_TRACKER_STATUS[] = "status"; @@ -171,6 +189,9 @@ static const char KEY_SYNC_MAINDATA_QUEUEING[] = "queueing"; static const char KEY_SYNC_MAINDATA_USE_ALT_SPEED_LIMITS[] = "use_alt_speed_limits"; static const char KEY_SYNC_MAINDATA_REFRESH_INTERVAL[] = "refresh_interval"; +// Sync torrent peers keys +static const char KEY_SYNC_TORRENT_PEERS_SHOW_FLAGS[] = "show_flags"; + static const char KEY_FULL_UPDATE[] = "full_update"; static const char KEY_RESPONSE_ID[] = "rid"; static const char KEY_SUFFIX_REMOVED[] = "_removed"; @@ -354,6 +375,54 @@ QByteArray btjson::getSyncMainData(int acceptedResponseId, QVariantMap &lastData return json::toJson(generateSyncData(acceptedResponseId, data, lastAcceptedData, lastData)); } +QByteArray btjson::getSyncTorrentPeersData(int acceptedResponseId, QString hash, QVariantMap &lastData, QVariantMap &lastAcceptedData) +{ + BitTorrent::TorrentHandle *const torrent = BitTorrent::Session::instance()->findTorrent(hash); + if (!torrent) { + qWarning() << Q_FUNC_INFO << "Invalid torrent " << qPrintable(hash); + return QByteArray(); + } + + QVariantMap data; + QVariantHash peers; + QList peersList = torrent->peers(); +#ifndef DISABLE_COUNTRIES_RESOLUTION + bool resolvePeerCountries = Preferences::instance()->resolvePeerCountries(); +#else + bool resolvePeerCountries = false; +#endif + + data[KEY_SYNC_TORRENT_PEERS_SHOW_FLAGS] = resolvePeerCountries; + + foreach (const BitTorrent::PeerInfo &pi, peersList) { + if (pi.address().ip.isNull()) continue; + QVariantMap peer; +#ifndef DISABLE_COUNTRIES_RESOLUTION + if (resolvePeerCountries) { + peer[KEY_PEER_COUNTRY_CODE] = pi.country().toLower(); + peer[KEY_PEER_COUNTRY] = Net::GeoIPManager::CountryName(pi.country()); + } +#endif + peer[KEY_PEER_IP] = pi.address().ip.toString(); + peer[KEY_PEER_PORT] = pi.address().port; + peer[KEY_PEER_CLIENT] = pi.client(); + peer[KEY_PEER_PROGRESS] = pi.progress(); + peer[KEY_PEER_DOWN_SPEED] = pi.payloadDownSpeed(); + peer[KEY_PEER_UP_SPEED] = pi.payloadUpSpeed(); + peer[KEY_PEER_TOT_DOWN] = pi.totalDownload(); + peer[KEY_PEER_TOT_UP] = pi.totalUpload(); + peer[KEY_PEER_CONNECTION_TYPE] = pi.connectionType(); + peer[KEY_PEER_FLAGS] = pi.flags(); + peer[KEY_PEER_FLAGS_DESCRIPTION] = pi.flagsDescription(); + peer[KEY_PEER_RELEVANCE] = pi.relevance(); + peers[pi.address().ip.toString() + ":" + QString::number(pi.address().port)] = peer; + } + + data["peers"] = peers; + + return json::toJson(generateSyncData(acceptedResponseId, data, lastAcceptedData, lastData)); +} + /** * Returns the trackers for a torrent in JSON format. * @@ -610,7 +679,7 @@ QByteArray btjson::getTorrentsRatesLimits(QStringList &hashes, bool downloadLimi map[hash] = limit; } - return json::toJson(map); + return json::toJson(map); } QVariantMap toMap(BitTorrent::TorrentHandle *const torrent) diff --git a/src/webui/btjson.h b/src/webui/btjson.h index 246d1eb86..bada11240 100644 --- a/src/webui/btjson.h +++ b/src/webui/btjson.h @@ -46,6 +46,7 @@ public: static QByteArray getTorrents(QString filter = "all", QString label = QString(), QString sortedColumn = "name", bool reverse = false, int limit = 0, int offset = 0); static QByteArray getSyncMainData(int acceptedResponseId, QVariantMap &lastData, QVariantMap &lastAcceptedData); + static QByteArray getSyncTorrentPeersData(int acceptedResponseId, QString hash, QVariantMap &lastData, QVariantMap &lastAcceptedData); static QByteArray getTrackersForTorrent(const QString& hash); static QByteArray getWebSeedsForTorrent(const QString& hash); static QByteArray getPropertiesForTorrent(const QString& hash); diff --git a/src/webui/webapplication.cpp b/src/webui/webapplication.cpp index d5a37a086..38e58e39d 100644 --- a/src/webui/webapplication.cpp +++ b/src/webui/webapplication.cpp @@ -82,6 +82,7 @@ QMap > WebApplication::initialize ADD_ACTION(query, propertiesWebSeeds); ADD_ACTION(query, propertiesFiles); ADD_ACTION(sync, maindata); + ADD_ACTION(sync, torrent_peers); ADD_ACTION(command, shutdown); ADD_ACTION(command, download); ADD_ACTION(command, upload); @@ -277,6 +278,19 @@ void WebApplication::action_sync_maindata() session()->syncMainDataLastAcceptedResponse), Http::CONTENT_TYPE_JSON); } +// GET param: +// - hash (string): torrent hash +// - rid (int): last response id +void WebApplication::action_sync_torrent_peers() +{ + CHECK_URI(0); + print(btjson::getSyncTorrentPeersData(request().gets["rid"].toInt(), + request().gets["hash"], + session()->syncTorrentPeersLastResponse, + session()->syncTorrentPeersLastAcceptedResponse), Http::CONTENT_TYPE_JSON); +} + + void WebApplication::action_version_api() { CHECK_URI(0); diff --git a/src/webui/webapplication.h b/src/webui/webapplication.h index 9696c8e09..8cf8fac8e 100644 --- a/src/webui/webapplication.h +++ b/src/webui/webapplication.h @@ -55,6 +55,7 @@ private: void action_query_propertiesWebSeeds(); void action_query_propertiesFiles(); void action_sync_maindata(); + void action_sync_torrent_peers(); void action_command_shutdown(); void action_command_download(); void action_command_upload(); diff --git a/src/webui/websessiondata.h b/src/webui/websessiondata.h index fe7b4eeec..01dd6ab57 100644 --- a/src/webui/websessiondata.h +++ b/src/webui/websessiondata.h @@ -35,6 +35,8 @@ struct WebSessionData { QVariantMap syncMainDataLastResponse; QVariantMap syncMainDataLastAcceptedResponse; + QVariantMap syncTorrentPeersLastResponse; + QVariantMap syncTorrentPeersLastAcceptedResponse; }; #endif // WEBSESSIONDATA diff --git a/src/webui/www/public/css/dynamicTable.css b/src/webui/www/public/css/dynamicTable.css index e4e282a1d..ccbc8bd98 100644 --- a/src/webui/www/public/css/dynamicTable.css +++ b/src/webui/www/public/css/dynamicTable.css @@ -29,7 +29,8 @@ } -#myTable tr:nth-child(even), +#torrentsTable tr:nth-child(even), +#torrentPeersTable tr:nth-child(even), #filesTable tr:nth-child(even), #properties #torrentFiles tr.alt, #properties #trackers tr.alt, @@ -50,8 +51,13 @@ background-color: #415A8D; color: #fff; } +#torrentPeersTable tr.selected { + background-color: #354158; + color: #fff; +} -#myTable tr:hover, +#torrentsTable tr:hover, +#torrentPeersTable tr:hover, #filesTable tr:hover, #properties #torrentFiles tr.over, #properties #trackers tr.over, @@ -60,7 +66,8 @@ color: #fff; } -#myTable tr:hover, +#torrentsTable tr:hover, +#torrentsTable tr:hover, #properties #torrentFiles tr.over, #properties #trackers tr.over, #transferList tr.over { diff --git a/src/webui/www/public/properties.html b/src/webui/www/public/properties.html index a9c04ecf0..a6369fc2e 100644 --- a/src/webui/www/public/properties.html +++ b/src/webui/www/public/properties.html @@ -2,6 +2,7 @@ diff --git a/src/webui/www/public/properties_content.html b/src/webui/www/public/properties_content.html index 2e906712a..a35ce4f31 100644 --- a/src/webui/www/public/properties_content.html +++ b/src/webui/www/public/properties_content.html @@ -63,6 +63,18 @@ + + + + \ No newline at end of file diff --git a/src/webui/www/public/scripts/client.js b/src/webui/www/public/scripts/client.js index 49374be4d..ba28bcfbb 100644 --- a/src/webui/www/public/scripts/client.js +++ b/src/webui/www/public/scripts/client.js @@ -22,9 +22,11 @@ * THE SOFTWARE. */ -myTable = new dynamicTable(); +torrentsTable = new TorrentsTable(); +torrentPeersTable = new TorrentPeersTable(); var updatePropertiesPanel = function(){}; +var updateTorrentPeersData = function(){}; var updateMainData = function(){}; var alternativeSpeedLimits = false; var queueing_enabled = true; @@ -99,7 +101,7 @@ window.addEvent('load', function () { selected_label = hash; localStorage.setItem('selected_label', selected_label); highlightSelectedLabel(); - if (typeof myTable.table != 'undefined') + if (typeof torrentsTable.table != 'undefined') updateMainData(); }; @@ -117,7 +119,7 @@ window.addEvent('load', function () { selected_filter = f; localStorage.setItem('selected_filter', f); // Reload torrents - if (typeof myTable.table != 'undefined') + if (typeof torrentsTable.table != 'undefined') updateMainData(); } @@ -230,9 +232,9 @@ window.addEvent('load', function () { return new Element('li', {id: hash, html: html}); }; - var all = myTable.getRowIds().length; + var all = torrentsTable.getRowIds().length; var unlabelled = 0; - Object.each(myTable.rows, function(row) { + Object.each(torrentsTable.rows, function(row) { if (row['full_data'].label.length === 0) unlabelled += 1; }); @@ -286,7 +288,7 @@ window.addEvent('load', function () { var update_labels = false; var full_update = (response['full_update'] == true); if (full_update) { - myTable.rows.erase(); + torrentsTable.clear(); label_list = {}; } if (response['rid']) { @@ -309,19 +311,20 @@ window.addEvent('load', function () { if (response['torrents']) { for (var key in response['torrents']) { response['torrents'][key]['hash'] = key; - myTable.updateRowData(response['torrents'][key]); + response['torrents'][key]['rowId'] = key; + torrentsTable.updateRowData(response['torrents'][key]); if (addTorrentToLabelList(response['torrents'][key])) update_labels = true; } } if (response['torrents_removed']) response['torrents_removed'].each(function (hash) { - myTable.removeRow(hash); + torrentsTable.removeRow(hash); removeTorrentFromLabelList(hash); update_labels = true; // Allways to update All label }); - myTable.updateTable(full_update); - myTable.altRow(); + torrentsTable.updateTable(full_update); + torrentsTable.altRow(); if (response['server_state']) { var tmp = response['server_state']; for(var key in tmp) @@ -340,7 +343,7 @@ window.addEvent('load', function () { }; updateMainData = function() { - myTable.updateTable(); + torrentsTable.updateTable(); clearTimeout(syncMainDataTimer); syncMainDataTimer = syncMainData.delay(100); } @@ -373,8 +376,8 @@ window.addEvent('load', function () { if (queueing_enabled != serverState.queueing) { queueing_enabled = serverState.queueing; - myTable.columns['priority'].force_hide = !queueing_enabled; - myTable.updateColumn('priority'); + torrentsTable.columns['priority'].force_hide = !queueing_enabled; + torrentsTable.updateColumn('priority'); if (queueing_enabled) { $('queueingLinks').removeClass('invisible'); $('queueingButtons').removeClass('invisible'); @@ -424,11 +427,6 @@ window.addEvent('load', function () { $('DlInfos').addEvent('click', globalDownloadLimitFN); $('UpInfos').addEvent('click', globalUploadLimitFN); - setSortedColumn = function (column) { - myTable.setSortedColumn(column); - updateMainData(); - }; - $('showTopToolbarLink').addEvent('click', function(e) { showTopToolbar = !showTopToolbar; localStorage.setItem('show_top_toolbar', showTopToolbar.toString()); @@ -489,7 +487,7 @@ window.addEvent('load', function () { }, contentURL : 'properties_content.html', require : { - css : ['css/Tabs.css'], + css : ['css/Tabs.css', 'css/dynamicTable.css'], js : ['scripts/prop-general.js', 'scripts/prop-trackers.js', 'scripts/prop-webseeds.js', 'scripts/prop-files.js'], }, tabsURL : 'properties.html', @@ -501,6 +499,8 @@ window.addEvent('load', function () { updateTorrentData(); else if (!$('prop_trackers').hasClass('invisible')) updateTrackersData(); + else if (!$('prop_peers').hasClass('invisible')) + updateTorrentPeersData(); else if (!$('prop_webseeds').hasClass('invisible')) updateWebSeedsData(); else if (!$('prop_files').hasClass('invisible')) @@ -512,6 +512,7 @@ window.addEvent('load', function () { $('prop_trackers').addClass("invisible"); $('prop_webseeds').addClass("invisible"); $('prop_files').addClass("invisible"); + $('prop_peers').addClass("invisible"); updatePropertiesPanel(); }); @@ -520,6 +521,16 @@ window.addEvent('load', function () { $('prop_general').addClass("invisible"); $('prop_webseeds').addClass("invisible"); $('prop_files').addClass("invisible"); + $('prop_peers').addClass("invisible"); + updatePropertiesPanel(); + }); + + $('PropPeersLink').addEvent('click', function(e){ + $('prop_peers').removeClass("invisible"); + $('prop_trackers').addClass("invisible"); + $('prop_general').addClass("invisible"); + $('prop_webseeds').addClass("invisible"); + $('prop_files').addClass("invisible"); updatePropertiesPanel(); }); @@ -528,6 +539,7 @@ window.addEvent('load', function () { $('prop_general').addClass("invisible"); $('prop_trackers').addClass("invisible"); $('prop_files').addClass("invisible"); + $('prop_peers').addClass("invisible"); updatePropertiesPanel(); }); @@ -536,6 +548,7 @@ window.addEvent('load', function () { $('prop_general').addClass("invisible"); $('prop_trackers').addClass("invisible"); $('prop_webseeds').addClass("invisible"); + $('prop_peers').addClass("invisible"); updatePropertiesPanel(); }); @@ -556,7 +569,7 @@ var keyboardEvents = new Keyboard({ defaultEventType: 'keydown', events: { 'ctrl+a': function(event) { - myTable.selectAll(); + torrentsTable.selectAll(); event.preventDefault(); }, 'delete': function(event) { @@ -567,3 +580,78 @@ var keyboardEvents = new Keyboard({ }); keyboardEvents.activate(); + +var loadTorrentPeersTimer; +var syncTorrentPeersLastResponseId = 0; +var show_flags = true; +var loadTorrentPeersData = function(){ + if ($('prop_peers').hasClass('invisible') || + $('propertiesPanel_collapseToggle').hasClass('panel-expand')) { + syncTorrentPeersLastResponseId = 0; + torrentPeersTable.clear(); + return; + } + var current_hash = torrentsTable.getCurrentTorrentHash(); + if (current_hash == "") { + syncTorrentPeersLastResponseId = 0; + torrentPeersTable.clear(); + clearTimeout(loadTorrentPeersTimer); + loadTorrentPeersTimer = loadTorrentPeersData.delay(syncMainDataTimerPeriod); + return; + } + var url = new URI('sync/torrent_peers'); + url.setData('rid', syncTorrentPeersLastResponseId); + url.setData('hash', current_hash); + var request = new Request.JSON({ + url: url, + noCache: true, + method: 'get', + onFailure: function() { + $('error_div').set('html', 'QBT_TR(qBittorrent client is not reachable)QBT_TR'); + clearTimeout(loadTorrentPeersTimer); + loadTorrentPeersTimer = loadTorrentPeersData.delay(5000); + }, + onSuccess: function(response) { + $('error_div').set('html', ''); + if (response) { + var full_update = (response['full_update'] == true); + if (full_update) { + torrentPeersTable.clear(); + } + if (response['rid']) { + syncTorrentPeersLastResponseId = response['rid']; + } + if (response['peers']) { + for (var key in response['peers']) { + response['peers'][key]['rowId'] = key; + torrentPeersTable.updateRowData(response['peers'][key]); + } + } + if (response['peers_removed']) + response['peers_removed'].each(function (hash) { + torrentPeersTable.removeRow(hash); + }); + torrentPeersTable.updateTable(full_update); + torrentPeersTable.altRow(); + + if (response['show_flags']) { + if (show_flags != response['show_flags']) { + show_flags = response['show_flags']; + torrentPeersTable.columns['country'].force_hide = !show_flags; + torrentPeersTable.updateColumn('country'); + } + } + } + else { + torrentPeersTable.clear(); + } + clearTimeout(loadTorrentPeersTimer); + loadTorrentPeersTimer = loadTorrentPeersData.delay(syncMainDataTimerPeriod); + } + }).send(); +}; + +updateTorrentPeersData = function(){ + clearTimeout(loadTorrentPeersTimer); + loadTorrentPeersData(); +}; \ No newline at end of file diff --git a/src/webui/www/public/scripts/contextmenu.js b/src/webui/www/public/scripts/contextmenu.js index 2387f340f..ff427a66c 100644 --- a/src/webui/www/public/scripts/contextmenu.js +++ b/src/webui/www/public/scripts/contextmenu.js @@ -140,9 +140,9 @@ var ContextMenu = new Class({ there_are_force_start = false; all_are_super_seeding = true; - var h = myTable.selectedIds(); + var h = torrentsTable.selectedRowsIds(); h.each(function(item, index){ - var data = myTable.rows.get(item).full_data; + var data = torrentsTable.rows.get(item).full_data; if (data['seq_dl'] != true) all_are_seq_dl = false; diff --git a/src/webui/www/public/scripts/dynamicTable.js b/src/webui/www/public/scripts/dynamicTable.js index 8b23bf29a..018ace08c 100644 --- a/src/webui/www/public/scripts/dynamicTable.js +++ b/src/webui/www/public/scripts/dynamicTable.js @@ -31,55 +31,35 @@ **************************************************************/ -var dynamicTable = new Class({ +var DynamicTable = new Class({ initialize : function () {}, - setup : function (table, context_menu) { - this.table = $(table); + setup : function (tableId, tableHeaderId, context_menu) { + this.tableId = tableId; + this.tableHeaderId = tableHeaderId; + this.table = $(tableId); this.rows = new Hash(); this.cur = new Array(); this.columns = new Array(); this.context_menu = context_menu; - this.sortedColumn = getLocalStorageItem('sorted_column', 'name'); - this.reverseSort = getLocalStorageItem('reverse_sort', '0'); + this.sortedColumn = getLocalStorageItem('sorted_column_' + this.tableId, 0); + this.reverseSort = getLocalStorageItem('reverse_sort_' + this.tableId, '0'); this.initColumns(); this.loadColumnsOrder(); this.updateHeader(); }, - initColumns : function () { - this.newColumn('priority', 'width: 30px; cursor: pointer', '#'); - this.newColumn('state_icon', 'width: 16px', ''); - this.newColumn('name', 'min-width: 200px; cursor: pointer', 'QBT_TR(Name)QBT_TR'); - this.newColumn('size', 'width: 100px; cursor: pointer', 'QBT_TR(Size)QBT_TR'); - this.newColumn('progress', 'width: 80px; cursor: pointer', 'QBT_TR(Done)QBT_TR'); - this.newColumn('num_seeds', 'width: 100px; cursor: pointer', 'QBT_TR(Seeds)QBT_TR'); - this.newColumn('num_leechs', 'width: 100px; cursor: pointer', 'QBT_TR(Peers)QBT_TR'); - this.newColumn('dlspeed', 'width: 100px; cursor: pointer', 'QBT_TR(Down Speed)QBT_TR'); - this.newColumn('upspeed', 'width: 100px; cursor: pointer', 'QBT_TR(Up Speed)QBT_TR'); - this.newColumn('eta', 'width: 100px; cursor: pointer', 'QBT_TR(ETA)QBT_TR'); - this.newColumn('ratio', 'width: 100px; cursor: pointer', 'QBT_TR(Ratio)QBT_TR'); - this.newColumn('label', 'width: 100px; cursor: pointer', 'QBT_TR(Label)QBT_TR'); - - 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.initColumnsFunctions(); - }, + initColumns : function () {}, newColumn : function (name, style, caption) { var column = {}; column['name'] = name; - column['visible'] = getLocalStorageItem('column_' + name + '_visible', '1'); + column['visible'] = getLocalStorageItem('column_' + name + '_visible_' + this.tableId, '1'); column['force_hide'] = false; column['caption'] = caption; column['style'] = style; - column['onclick'] = 'setSortedColumn(\'' + name + '\');'; + column['onclick'] = 'this._this.setSortedColumn(\'' + name + '\');'; column['dataProperties'] = [name]; column['getRowValue'] = function (row, pos) { if (pos == undefined) @@ -99,12 +79,12 @@ var dynamicTable = new Class({ this.columns.push(column); this.columns[name] = column; - $('torrentTableHeader').appendChild(new Element('th')); + $(this.tableHeaderId).appendChild(new Element('th')); }, loadColumnsOrder : function () { columnsOrder = ['state_icon']; // status icon column is always the first - val = localStorage.getItem('columns_order'); + val = localStorage.getItem('columns_order_' + this.tableId); if (val === null || val === undefined) return; val.split(',').forEach(function(v) { if ((v in this.columns) && (!columnsOrder.contains(v))) @@ -126,14 +106,15 @@ var dynamicTable = new Class({ val += ','; val += this.columns[i].name; } - localStorage.setItem('columns_order', val); + localStorage.setItem('columns_order_' + this.tableId, val); }, updateHeader : function () { - ths = $('torrentTableHeader').getElements('th'); + var ths = $(this.tableHeaderId).getElements('th'); for (var i = 0; i < ths.length; i++) { th = ths[i]; + th._this = this; th.setAttribute('onclick', this.columns[i].onclick); th.innerHTML = this.columns[i].caption; th.setAttribute('style', this.columns[i].style); @@ -154,7 +135,7 @@ var dynamicTable = new Class({ updateColumn : function (columnName) { var pos = this.getColumnPos(columnName); var visible = ((this.columns[pos].visible != '0') && !this.columns[pos].force_hide); - var ths = $('torrentTableHeader').getElements('th'); + var ths = $(this.tableHeaderId).getElements('th'); if (visible) ths[pos].removeClass('invisible'); else @@ -176,11 +157,12 @@ var dynamicTable = new Class({ // Toggle sort order this.reverseSort = this.reverseSort == '0' ? '1' : '0'; } - localStorage.setItem('sorted_column', column); - localStorage.setItem('reverse_sort', this.reverseSort); + localStorage.setItem('sorted_column_' + this.tableId, column); + localStorage.setItem('reverse_sort_' + this.tableId, this.reverseSort); + this.updateTable(false); }, - getCurrentTorrentHash : function () { + getSelectedRowId : function () { if (this.cur.length > 0) return this.cur[0]; return ''; @@ -206,19 +188,19 @@ var dynamicTable = new Class({ var trs = this.table.getElements('tr'); for (var i = 0; i < trs.length; i++) { var tr = trs[i]; - this.cur.push(tr.hash); + this.cur.push(tr.rowId); if (!tr.hasClass('selected')) tr.addClass('selected'); } }, - selectRow : function (hash) { + selectRow : function (rowId) { this.cur.empty(); - this.cur.push(hash); + this.cur.push(rowId); var trs = this.table.getElements('tr'); for (var i = 0; i < trs.length; i++) { var tr = trs[i]; - if (tr.hash == hash) { + if (tr.rowId == rowId) { if (!tr.hasClass('selected')) tr.addClass('selected'); } @@ -229,17 +211,17 @@ var dynamicTable = new Class({ }, updateRowData : function (data) { - var hash = data['hash']; + var rowId = data['rowId']; var row; - if (!this.rows.has(hash)) { + if (!this.rows.has(rowId)) { row = {}; - this.rows.set(hash, row); + this.rows.set(rowId, row); row['full_data'] = {}; - row['hash'] = hash; + row['rowId'] = rowId; } else - row = this.rows.get(hash); + row = this.rows.get(rowId); row['data'] = data; @@ -247,64 +229,19 @@ var dynamicTable = new Class({ row['full_data'][x] = data[x]; }, - applyFilter : function (row, filterName, labelName) { - var state = row['full_data'].state; - switch(filterName) { - case 'downloading': - if (state != 'downloading' && !~state.indexOf('DL')) - 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')) - return false; - break; - case 'paused': - if (state != 'pausedDL') - return false; - break; - case 'resumed': - if (~state.indexOf('paused')) - return false; - break; - case 'active': - if (state != 'downloading' && state != 'forcedDL' && state != 'uploading' && state != 'forcedUP') - return false; - break; - case 'inactive': - if (state == 'downloading' || state == 'forcedDL' || state == 'uploading' || state == 'forcedUP') - return false; - break; - } - - if (labelName == LABELS_ALL) - return true; - - if (labelName == LABELS_UNLABELLED && row['full_data'].label.length === 0) - return true; - - if (labelName != genHash( row['full_data'].label) ) - return false; - - return true; - }, - getFilteredAndSortedRows : function () { var filteredRows = new Array(); var rows = this.rows.getValues(); for (i = 0; i < rows.length; i++) - if (this.applyFilter(rows[i], selected_filter, selected_label)) { - filteredRows.push(rows[i]); - filteredRows[rows[i].hash] = rows[i]; - } + { + filteredRows.push(rows[i]); + filteredRows[rows[i].rowId] = rows[i]; + } filteredRows.sort(function (row1, row2) { - column = this.columns[this.sortedColumn]; + var column = this.columns[this.sortedColumn]; res = column.compareRows(row1, row2); if (this.reverseSort == '0') return res; @@ -314,10 +251,10 @@ var dynamicTable = new Class({ return filteredRows; }, - getTrByHash : function (hash) { + getTrByRowId : function (rowId) { trs = this.table.getElements('tr'); for (var i = 0; i < trs.length; i++) - if (trs[i].hash == hash) + if (trs[i].rowId == rowId) return trs[i]; return null; }, @@ -337,10 +274,10 @@ var dynamicTable = new Class({ var trs = this.table.getElements('tr'); for (var rowPos = 0; rowPos < rows.length; rowPos++) { - var hash = rows[rowPos]['hash']; + var rowId = rows[rowPos]['rowId']; tr_found = false; for (j = rowPos; j < trs.length; j++) - if (trs[j]['hash'] == hash) { + if (trs[j]['rowId'] == rowId) { trs[rowPos].removeClass('over'); tr_found = true; if (rowPos == j) @@ -357,59 +294,49 @@ var dynamicTable = new Class({ var tr = new Element('tr'); tr.addClass("menu-target"); - tr['hash'] = rows[rowPos]['hash']; + tr['rowId'] = rows[rowPos]['rowId']; + tr._this = this; tr.addEvent('contextmenu', function (e) { - if (!myTable.cur.contains(this.hash)) - myTable.selectRow(this.hash); - return true; - }); - tr.addEvent('dblclick', function (e) { - e.stop(); - myTable.selectRow(this.hash); - var row = myTable.rows.get(this.hash); - var state = row['full_data'].state; - if (~state.indexOf('paused')) - startFN(); - else - pauseFN(); + if (!this._this.cur.contains(this.rowId)) + this._this.selectRow(this.rowId); return true; }); tr.addEvent('click', function (e) { e.stop(); if (e.control) { // CTRL key was pressed - if (myTable.cur.contains(this.hash)) { + if (this._this.cur.contains(this.rowId)) { // remove it - myTable.cur.erase(this.hash); + this._this.cur.erase(this.rowId); // Remove selected style this.removeClass('selected'); } else { - myTable.cur.push(this.hash); + this._this.cur.push(this.rowId); // Add selected style this.addClass('selected'); } } else { - if (e.shift && myTable.cur.length == 1) { + if (e.shift && this._this.cur.length == 1) { // Shift key was pressed - var first_row_hash = myTable.cur[0]; - var last_row_hash = this.hash; - myTable.cur.empty(); - var trs = myTable.table.getElements('tr'); + var first_row_id = this._this.cur[0]; + var last_row_id = this.rowId; + this._this.cur.empty(); + var trs = this._this.table.getElements('tr'); var select = false; for (var i = 0; i < trs.length; i++) { var tr = trs[i]; - if ((tr.hash == first_row_hash) || (tr.hash == last_row_hash)) { - myTable.cur.push(tr.hash); + if ((tr.rowId == first_row_id) || (tr.rowId == last_row_id)) { + this._this.cur.push(tr.rowId); tr.addClass('selected'); select = !select; } else { if (select) { - myTable.cur.push(tr.hash); + this._this.cur.push(tr.rowId); tr.addClass('selected'); } else @@ -418,13 +345,15 @@ var dynamicTable = new Class({ } } else { // Simple selection - myTable.selectRow(this.hash); + this._this.selectRow(this.rowId); updatePropertiesPanel(); } } return false; }); + this.setupTrEvents(tr); + for (var j = 0 ; j < this.columns.length; j++) { var td = new Element('td'); if ((this.columns[j].visible == '0') || this.columns[j].force_hide) @@ -443,7 +372,8 @@ var dynamicTable = new Class({ } // Update context menu - this.context_menu.addTarget(tr); + if (this.context_menu) + this.context_menu.addTarget(tr); this.updateRow(tr, true); } @@ -457,8 +387,10 @@ var dynamicTable = new Class({ } }, + setupTrEvents : function (tr) {}, + updateRow : function (tr, fullUpdate) { - var row = this.rows.get(tr.hash); + var row = this.rows.get(tr.rowId); data = row[fullUpdate ? 'full_data' : 'data']; tds = tr.getElements('td'); @@ -467,43 +399,64 @@ var dynamicTable = new Class({ this.columns[i].updateTd(tds[i], row); } row['data'] = {}; - - /* - for(var prop in data) - for (var i = 0; i < this.columns.length; i++) - for (var j = 0; j < this.columns[i].dataProperties.length; j++) - if (this.columns[i].dataProperties[j] == prop) - this.columns[i].updateTd(tds[i], row); - - if (this.cur.contains(tr.hash)) { - if (!tr.hasClass('selected')) - tr.addClass('selected'); - } - else { - if (tr.hasClass('selected')) - tr.removeClass('selected'); - } - */ }, - removeRow : function (hash) { - this.cur.erase(hash); - var tr = this.getTrByHash(hash); + removeRow : function (rowId) { + this.cur.erase(rowId); + var tr = this.getTrByRowId(rowId); if (tr != null) { tr.dispose(); - this.rows.erase(hash); + this.rows.erase(rowId); return true; } return false; }, - selectedIds : function () { + clear : function () { + this.cur.empty(); + this.rows.empty(); + var trs = this.table.getElements('tr'); + while (trs.length > 0) { + trs[trs.length - 1].dispose(); + trs.pop(); + } + }, + + selectedRowsIds : function () { return this.cur; }, getRowIds : function () { return this.rows.getKeys(); }, + }); + +var TorrentsTable = new Class({ + Extends: DynamicTable, + + initColumns : function () { + this.newColumn('priority', 'width: 30px; cursor: pointer', '#'); + this.newColumn('state_icon', 'width: 16px', ''); + this.newColumn('name', 'min-width: 200px; cursor: pointer', 'QBT_TR(Name)QBT_TR'); + this.newColumn('size', 'width: 100px; cursor: pointer', 'QBT_TR(Size)QBT_TR'); + this.newColumn('progress', 'width: 80px; cursor: pointer', 'QBT_TR(Done)QBT_TR'); + this.newColumn('num_seeds', 'width: 100px; cursor: pointer', 'QBT_TR(Seeds)QBT_TR'); + this.newColumn('num_leechs', 'width: 100px; cursor: pointer', 'QBT_TR(Peers)QBT_TR'); + this.newColumn('dlspeed', 'width: 100px; cursor: pointer', 'QBT_TR(Down Speed)QBT_TR'); + this.newColumn('upspeed', 'width: 100px; cursor: pointer', 'QBT_TR(Up Speed)QBT_TR'); + this.newColumn('eta', 'width: 100px; cursor: pointer', 'QBT_TR(ETA)QBT_TR'); + this.newColumn('ratio', 'width: 100px; cursor: pointer', 'QBT_TR(Ratio)QBT_TR'); + this.newColumn('label', 'width: 100px; cursor: pointer', 'QBT_TR(Label)QBT_TR'); + + 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.initColumnsFunctions(); + }, initColumnsFunctions : function () { @@ -659,10 +612,205 @@ var dynamicTable = new Class({ html = (Math.floor(100 * ratio) / 100).toFixed(2); //Don't round up td.set('html', html); }; - } + }, + applyFilter : function (row, filterName, labelName) { + var state = row['full_data'].state; + switch(filterName) { + case 'downloading': + if (state != 'downloading' && !~state.indexOf('DL')) + 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')) + return false; + break; + case 'paused': + if (state != 'pausedDL') + return false; + break; + case 'resumed': + if (~state.indexOf('paused')) + return false; + break; + case 'active': + if (state != 'downloading' && state != 'forcedDL' && state != 'uploading' && state != 'forcedUP') + return false; + break; + case 'inactive': + if (state == 'downloading' || state == 'forcedDL' || state == 'uploading' || state == 'forcedUP') + return false; + break; + } + + if (labelName == LABELS_ALL) + return true; + + if (labelName == LABELS_UNLABELLED && row['full_data'].label.length === 0) + return true; + + if (labelName != genHash( row['full_data'].label) ) + return false; + + return true; + }, + + getFilteredAndSortedRows : function () { + var filteredRows = new Array(); + + var rows = this.rows.getValues(); + + for (i = 0; i < rows.length; i++) + if (this.applyFilter(rows[i], selected_filter, selected_label)) { + filteredRows.push(rows[i]); + filteredRows[rows[i].rowId] = rows[i]; + } + + filteredRows.sort(function (row1, row2) { + var column = this.columns[this.sortedColumn]; + res = column.compareRows(row1, row2); + if (this.reverseSort == '0') + return res; + else + return -res; + }.bind(this)); + return filteredRows; + }, + + setupTrEvents : function (tr) { + tr.addEvent('dblclick', function (e) { + e.stop(); + this._this.selectRow(this.rowId); + var row = this._this.rows.get(this.rowId); + var state = row['full_data'].state; + if (~state.indexOf('paused')) + startFN(); + else + pauseFN(); + return true; + }); + }, + + getCurrentTorrentHash : function () { + return this.getSelectedRowId(); + } + }); + +var TorrentPeersTable = new Class({ + Extends: DynamicTable, + + initColumns : function () { + this.newColumn('country', 'width: 4px', ''); + this.newColumn('ip', 'width: 80px', 'QBT_TR(IP)QBT_TR'); + this.newColumn('port', 'width: 35px', 'QBT_TR(Port)QBT_TR'); + this.newColumn('client', 'width: 110px', 'QBT_TR(Client)QBT_TR'); + this.newColumn('progress', 'width: 30px', 'QBT_TR(Progress)QBT_TR'); + this.newColumn('dl_speed', 'width: 30px', 'QBT_TR(Down Speed)QBT_TR'); + this.newColumn('up_speed', 'width: 30px', 'QBT_TR(Up Speed)QBT_TR'); + this.newColumn('downloaded', 'width: 30px', 'QBT_TR(Downloaded)QBT_TR'); + this.newColumn('uploaded', 'width: 30px', 'QBT_TR(Uploaded)QBT_TR'); + this.newColumn('connection', 'width: 30px', 'QBT_TR(Connection)QBT_TR'); + this.newColumn('flags', 'width: 30px', 'QBT_TR(Flags)QBT_TR'); + this.newColumn('relevance', 'min-width: 30px', 'QBT_TR(Relevance)QBT_TR'); + + 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) { + var country = this.getRowValue(row, 0); + var country_code = this.getRowValue(row, 1); + + if (!country_code) { + if (td.getChildren('img').length) + td.getChildren('img')[0].dispose(); + return; + } + + var img_path = 'images/flags/' + country_code + '.png'; + + if (td.getChildren('img').length) { + var img = td.getChildren('img')[0]; + img.set('src', img_path); + img.set('alt', country); + img.set('title', country); + } + else + td.adopt(new Element('img', { + 'src' : img_path, + 'alt' : country, + 'title' : country + })); + }; + + // ip + + this.columns['ip'].compareRows = function (row1, row2) { + var ip1 = this.getRowValue(row1); + var ip2 = this.getRowValue(row2); + + var a = ip1.split("."); + var b = ip2.split("."); + + for (var i = 0; i < 4; i++){ + if (a[i] != b[i]) + return a[i] - b[i]; + } + + return 0; + }; + + // progress, relevance + + this.columns['progress'].updateTd = function (td, row) { + var progress = this.getRowValue(row); + var progressFormated = (progress * 100).round(1); + if (progressFormated == 100.0 && progress != 1.0) + progressFormated = 99.9; + progressFormated += "%"; + td.set('html', progressFormated); + }; + + this.columns['relevance'].updateTd = this.columns['progress'].updateTd; + + // dl_speed, up_speed + + this.columns['dl_speed'].updateTd = function (td, row) { + var speed = this.getRowValue(row); + if (speed == 0) + td.set('html', ''); + else + td.set('html', friendlyUnit(speed, true)); + }; + + this.columns['up_speed'].updateTd = this.columns['dl_speed'].updateTd; + + // downloaded, uploaded + + this.columns['downloaded'].updateTd = function (td, row) { + var downloaded = this.getRowValue(row); + td.set('html', friendlyUnit(downloaded, false)); + }; + + this.columns['uploaded'].updateTd = this.columns['downloaded'].updateTd; + + // flags + + this.columns['flags'].updateTd = function (td, row) { + td.innerHTML = this.getRowValue(row, 0); + td.title = this.getRowValue(row, 1); + }; + + } }); -//dynamicTable.implement(new Options); -//dynamicTable.implement(new Events); /*************************************************************/ diff --git a/src/webui/www/public/scripts/mocha-init.js b/src/webui/www/public/scripts/mocha-init.js index b3732f2de..d5987a3c2 100644 --- a/src/webui/www/public/scripts/mocha-init.js +++ b/src/webui/www/public/scripts/mocha-init.js @@ -119,7 +119,7 @@ initializeWindows = function() { } uploadLimitFN = function() { - var h = myTable.selectedIds(); + var h = torrentsTable.selectedRowsIds(); if (h.length) { var hash = h[0]; new MochaUI.Window({ @@ -139,7 +139,7 @@ initializeWindows = function() { }; toggleSequentialDownloadFN = function() { - var h = myTable.selectedIds(); + var h = torrentsTable.selectedRowsIds(); if (h.length) { new Request({ url: 'command/toggleSequentialDownload', @@ -153,7 +153,7 @@ initializeWindows = function() { }; toggleFirstLastPiecePrioFN = function() { - var h = myTable.selectedIds(); + var h = torrentsTable.selectedRowsIds(); if (h.length) { new Request({ url: 'command/toggleFirstLastPiecePrio', @@ -167,7 +167,7 @@ initializeWindows = function() { }; setSuperSeedingFN = function(val) { - var h = myTable.selectedIds(); + var h = torrentsTable.selectedRowsIds(); if (h.length) { new Request({ url: 'command/setSuperSeeding', @@ -182,7 +182,7 @@ initializeWindows = function() { }; setForceStartFN = function() { - var h = myTable.selectedIds(); + var h = torrentsTable.selectedRowsIds(); if (h.length) { new Request({ url: 'command/setForceStart', @@ -213,7 +213,7 @@ initializeWindows = function() { } downloadLimitFN = function() { - var h = myTable.selectedIds(); + var h = torrentsTable.selectedRowsIds(); if (h.length) { var hash = h[0]; new MochaUI.Window({ @@ -233,7 +233,7 @@ initializeWindows = function() { }; deleteFN = function() { - var h = myTable.selectedIds(); + var h = torrentsTable.selectedRowsIds(); if (h.length) { new MochaUI.Window({ id: 'confirmDeletionPage', @@ -257,7 +257,7 @@ initializeWindows = function() { }); pauseFN = function() { - var h = myTable.selectedIds(); + var h = torrentsTable.selectedRowsIds(); if (h.length) { h.each(function(hash, index) { new Request({ @@ -273,7 +273,7 @@ initializeWindows = function() { }; startFN = function() { - var h = myTable.selectedIds(); + var h = torrentsTable.selectedRowsIds(); if (h.length) { h.each(function(hash, index) { new Request({ @@ -289,7 +289,7 @@ initializeWindows = function() { }; recheckFN = function() { - var h = myTable.selectedIds(); + var h = torrentsTable.selectedRowsIds(); if (h.length) { h.each(function(hash, index) { new Request({ @@ -305,7 +305,7 @@ initializeWindows = function() { }; newLabelFN = function () { - var h = myTable.selectedIds(); + var h = torrentsTable.selectedRowsIds(); if (h.length) { new MochaUI.Window({ id: 'newLabelPage', @@ -327,7 +327,7 @@ initializeWindows = function() { var labelName = ''; if (labelHash != 0) var labelName = label_list[labelHash].name; - var h = myTable.selectedIds(); + var h = torrentsTable.selectedRowsIds(); if (h.length) { new Request({ url: 'command/setLabel', @@ -353,7 +353,7 @@ initializeWindows = function() { ['pause', 'resume', 'recheck'].each(function(item) { addClickEvent(item, function(e) { new Event(e).stop(); - var h = myTable.selectedIds(); + var h = torrentsTable.selectedRowsIds(); if (h.length) { h.each(function(hash, index) { new Request({ @@ -377,7 +377,7 @@ initializeWindows = function() { }); setPriorityFN = function(cmd) { - var h = myTable.selectedIds(); + var h = torrentsTable.selectedRowsIds(); if (h.length) { new Request({ url: 'command/' + cmd, diff --git a/src/webui/www/public/scripts/prop-files.js b/src/webui/www/public/scripts/prop-files.js index 471e8bd4c..d5bc8f777 100644 --- a/src/webui/www/public/scripts/prop-files.js +++ b/src/webui/www/public/scripts/prop-files.js @@ -278,7 +278,7 @@ var loadTorrentFilesData = function() { // Tab changed, don't do anything return; } - var new_hash = myTable.getCurrentTorrentHash(); + var new_hash = torrentsTable.getCurrentTorrentHash(); if (new_hash == "") { fTable.removeAllRows(); clearTimeout(loadTorrentFilesDataTimer); diff --git a/src/webui/www/public/scripts/prop-general.js b/src/webui/www/public/scripts/prop-general.js index 4e80ee15d..e51ad9576 100644 --- a/src/webui/www/public/scripts/prop-general.js +++ b/src/webui/www/public/scripts/prop-general.js @@ -32,7 +32,7 @@ var loadTorrentData = function() { // Tab changed, don't do anything return; } - var current_hash = myTable.getCurrentTorrentHash(); + var current_hash = torrentsTable.getCurrentTorrentHash(); if (current_hash == "") { clearData(); clearTimeout(loadTorrentDataTimer); diff --git a/src/webui/www/public/scripts/prop-trackers.js b/src/webui/www/public/scripts/prop-trackers.js index 9bcb701da..d7d0a0066 100644 --- a/src/webui/www/public/scripts/prop-trackers.js +++ b/src/webui/www/public/scripts/prop-trackers.js @@ -59,7 +59,7 @@ var loadTrackersData = function() { // Tab changed, don't do anything return; } - var new_hash = myTable.getCurrentTorrentHash(); + var new_hash = torrentsTable.getCurrentTorrentHash(); if (new_hash == "") { tTable.removeAllRows(); clearTimeout(loadTrackersDataTimer); diff --git a/src/webui/www/public/scripts/prop-webseeds.js b/src/webui/www/public/scripts/prop-webseeds.js index 39a69421f..0d5903b6f 100644 --- a/src/webui/www/public/scripts/prop-webseeds.js +++ b/src/webui/www/public/scripts/prop-webseeds.js @@ -59,7 +59,7 @@ var loadWebSeedsData = function() { // Tab changed, don't do anything return; } - var new_hash = myTable.getCurrentTorrentHash(); + var new_hash = torrentsTable.getCurrentTorrentHash(); if (new_hash == "") { wsTable.removeAllRows(); clearTimeout(loadWebSeedsDataTimer); diff --git a/src/webui/www/public/transferlist.html b/src/webui/www/public/transferlist.html index dbfb707b6..61f776d01 100644 --- a/src/webui/www/public/transferlist.html +++ b/src/webui/www/public/transferlist.html @@ -1,15 +1,15 @@ - + - +