Browse Source
Internally redesign tracker list widget using Qt Model/View architecture. Make tracker list sortable by any column. PR #19633. Closes #261.adaptive-webui-19844
Vladimir Golovnev
1 year ago
committed by
GitHub
30 changed files with 1784 additions and 1104 deletions
@ -1,813 +0,0 @@
@@ -1,813 +0,0 @@
|
||||
/*
|
||||
* Bittorrent Client using Qt and libtorrent. |
||||
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org> |
||||
* |
||||
* This program is free software; you can redistribute it and/or |
||||
* modify it under the terms of the GNU General Public License |
||||
* as published by the Free Software Foundation; either version 2 |
||||
* of the License, or (at your option) any later version. |
||||
* |
||||
* This program is distributed in the hope that it will be useful, |
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
* GNU General Public License for more details. |
||||
* |
||||
* You should have received a copy of the GNU General Public License |
||||
* along with this program; if not, write to the Free Software |
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
* |
||||
* In addition, as a special exception, the copyright holders give permission to |
||||
* link this program with the OpenSSL project's "OpenSSL" library (or with |
||||
* modified versions of it that use the same license as the "OpenSSL" library), |
||||
* and distribute the linked executables. You must obey the GNU General Public |
||||
* License in all respects for all of the code used other than "OpenSSL". If you |
||||
* modify file(s), you may extend this exception to your version of the file(s), |
||||
* but you are not obligated to do so. If you do not wish to do so, delete this |
||||
* exception statement from your version. |
||||
*/ |
||||
|
||||
#include "trackerlistwidget.h" |
||||
|
||||
#include <QAction> |
||||
#include <QApplication> |
||||
#include <QClipboard> |
||||
#include <QColor> |
||||
#include <QDebug> |
||||
#include <QHeaderView> |
||||
#include <QLocale> |
||||
#include <QMenu> |
||||
#include <QMessageBox> |
||||
#include <QPointer> |
||||
#include <QShortcut> |
||||
#include <QStringList> |
||||
#include <QTreeWidgetItem> |
||||
#include <QUrl> |
||||
#include <QVector> |
||||
#include <QWheelEvent> |
||||
|
||||
#include "base/bittorrent/peerinfo.h" |
||||
#include "base/bittorrent/session.h" |
||||
#include "base/bittorrent/torrent.h" |
||||
#include "base/bittorrent/trackerentry.h" |
||||
#include "base/global.h" |
||||
#include "base/preferences.h" |
||||
#include "base/utils/misc.h" |
||||
#include "gui/autoexpandabledialog.h" |
||||
#include "gui/uithememanager.h" |
||||
#include "propertieswidget.h" |
||||
#include "trackersadditiondialog.h" |
||||
|
||||
#define NB_STICKY_ITEM 3 |
||||
|
||||
TrackerListWidget::TrackerListWidget(PropertiesWidget *properties) |
||||
: m_properties(properties) |
||||
{ |
||||
#ifdef QBT_USES_LIBTORRENT2 |
||||
setColumnHidden(COL_PROTOCOL, true); // Must be set before calling loadSettings()
|
||||
#endif |
||||
|
||||
// Set header
|
||||
// Must be set before calling loadSettings() otherwise the header is reset on restart
|
||||
setHeaderLabels(headerLabels()); |
||||
// Load settings
|
||||
loadSettings(); |
||||
// Graphical settings
|
||||
setAllColumnsShowFocus(true); |
||||
setSelectionMode(QAbstractItemView::ExtendedSelection); |
||||
header()->setFirstSectionMovable(true); |
||||
header()->setStretchLastSection(false); // Must be set after loadSettings() in order to work
|
||||
header()->setTextElideMode(Qt::ElideRight); |
||||
// Ensure that at least one column is visible at all times
|
||||
if (visibleColumnsCount() == 0) |
||||
setColumnHidden(COL_URL, false); |
||||
// 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 (int i = 0; i < COL_COUNT; ++i) |
||||
{ |
||||
if ((columnWidth(i) <= 0) && !isColumnHidden(i)) |
||||
resizeColumnToContents(i); |
||||
} |
||||
// Context menu
|
||||
setContextMenuPolicy(Qt::CustomContextMenu); |
||||
connect(this, &QWidget::customContextMenuRequested, this, &TrackerListWidget::showTrackerListMenu); |
||||
// Header
|
||||
header()->setContextMenuPolicy(Qt::CustomContextMenu); |
||||
connect(header(), &QWidget::customContextMenuRequested, this, &TrackerListWidget::displayColumnHeaderMenu); |
||||
connect(header(), &QHeaderView::sectionMoved, this, &TrackerListWidget::saveSettings); |
||||
connect(header(), &QHeaderView::sectionResized, this, &TrackerListWidget::saveSettings); |
||||
connect(header(), &QHeaderView::sortIndicatorChanged, this, &TrackerListWidget::saveSettings); |
||||
|
||||
// Set DHT, PeX, LSD items
|
||||
m_DHTItem = new QTreeWidgetItem({ u"** [DHT] **"_s }); |
||||
insertTopLevelItem(0, m_DHTItem); |
||||
setRowColor(0, QColorConstants::Svg::grey); |
||||
m_PEXItem = new QTreeWidgetItem({ u"** [PeX] **"_s }); |
||||
insertTopLevelItem(1, m_PEXItem); |
||||
setRowColor(1, QColorConstants::Svg::grey); |
||||
m_LSDItem = new QTreeWidgetItem({ u"** [LSD] **"_s }); |
||||
insertTopLevelItem(2, m_LSDItem); |
||||
setRowColor(2, QColorConstants::Svg::grey); |
||||
|
||||
// Set static items alignment
|
||||
const Qt::Alignment alignment = (Qt::AlignRight | Qt::AlignVCenter); |
||||
m_DHTItem->setTextAlignment(COL_PEERS, alignment); |
||||
m_PEXItem->setTextAlignment(COL_PEERS, alignment); |
||||
m_LSDItem->setTextAlignment(COL_PEERS, alignment); |
||||
m_DHTItem->setTextAlignment(COL_SEEDS, alignment); |
||||
m_PEXItem->setTextAlignment(COL_SEEDS, alignment); |
||||
m_LSDItem->setTextAlignment(COL_SEEDS, alignment); |
||||
m_DHTItem->setTextAlignment(COL_LEECHES, alignment); |
||||
m_PEXItem->setTextAlignment(COL_LEECHES, alignment); |
||||
m_LSDItem->setTextAlignment(COL_LEECHES, alignment); |
||||
m_DHTItem->setTextAlignment(COL_TIMES_DOWNLOADED, alignment); |
||||
m_PEXItem->setTextAlignment(COL_TIMES_DOWNLOADED, alignment); |
||||
m_LSDItem->setTextAlignment(COL_TIMES_DOWNLOADED, alignment); |
||||
|
||||
// Set header alignment
|
||||
headerItem()->setTextAlignment(COL_TIER, alignment); |
||||
headerItem()->setTextAlignment(COL_PEERS, alignment); |
||||
headerItem()->setTextAlignment(COL_SEEDS, alignment); |
||||
headerItem()->setTextAlignment(COL_LEECHES, alignment); |
||||
headerItem()->setTextAlignment(COL_TIMES_DOWNLOADED, alignment); |
||||
headerItem()->setTextAlignment(COL_NEXT_ANNOUNCE, alignment); |
||||
headerItem()->setTextAlignment(COL_MIN_ANNOUNCE, alignment); |
||||
|
||||
// Set hotkeys
|
||||
const auto *editHotkey = new QShortcut(Qt::Key_F2, this, nullptr, nullptr, Qt::WidgetShortcut); |
||||
connect(editHotkey, &QShortcut::activated, this, &TrackerListWidget::editSelectedTracker); |
||||
const auto *deleteHotkey = new QShortcut(QKeySequence::Delete, this, nullptr, nullptr, Qt::WidgetShortcut); |
||||
connect(deleteHotkey, &QShortcut::activated, this, &TrackerListWidget::deleteSelectedTrackers); |
||||
const auto *copyHotkey = new QShortcut(QKeySequence::Copy, this, nullptr, nullptr, Qt::WidgetShortcut); |
||||
connect(copyHotkey, &QShortcut::activated, this, &TrackerListWidget::copyTrackerUrl); |
||||
|
||||
connect(this, &QAbstractItemView::doubleClicked, this, &TrackerListWidget::editSelectedTracker); |
||||
connect(this, &QTreeWidget::itemExpanded, this, [](QTreeWidgetItem *item) |
||||
{ |
||||
item->setText(COL_PEERS, QString()); |
||||
item->setText(COL_SEEDS, QString()); |
||||
item->setText(COL_LEECHES, QString()); |
||||
item->setText(COL_TIMES_DOWNLOADED, QString()); |
||||
item->setText(COL_MSG, QString()); |
||||
item->setText(COL_NEXT_ANNOUNCE, QString()); |
||||
item->setText(COL_MIN_ANNOUNCE, QString()); |
||||
}); |
||||
connect(this, &QTreeWidget::itemCollapsed, this, [](QTreeWidgetItem *item) |
||||
{ |
||||
item->setText(COL_PEERS, item->data(COL_PEERS, Qt::UserRole).toString()); |
||||
item->setText(COL_SEEDS, item->data(COL_SEEDS, Qt::UserRole).toString()); |
||||
item->setText(COL_LEECHES, item->data(COL_LEECHES, Qt::UserRole).toString()); |
||||
item->setText(COL_TIMES_DOWNLOADED, item->data(COL_TIMES_DOWNLOADED, Qt::UserRole).toString()); |
||||
item->setText(COL_MSG, item->data(COL_MSG, Qt::UserRole).toString()); |
||||
|
||||
const auto now = QDateTime::currentDateTime(); |
||||
const auto secsToNextAnnounce = now.secsTo(item->data(COL_NEXT_ANNOUNCE, Qt::UserRole).toDateTime()); |
||||
item->setText(COL_NEXT_ANNOUNCE, Utils::Misc::userFriendlyDuration(secsToNextAnnounce, -1, Utils::Misc::TimeResolution::Seconds)); |
||||
const auto secsToMinAnnounce = now.secsTo(item->data(COL_MIN_ANNOUNCE, Qt::UserRole).toDateTime()); |
||||
item->setText(COL_MIN_ANNOUNCE, Utils::Misc::userFriendlyDuration(secsToMinAnnounce, -1, Utils::Misc::TimeResolution::Seconds)); |
||||
}); |
||||
} |
||||
|
||||
TrackerListWidget::~TrackerListWidget() |
||||
{ |
||||
saveSettings(); |
||||
} |
||||
|
||||
QVector<QTreeWidgetItem *> TrackerListWidget::getSelectedTrackerItems() const |
||||
{ |
||||
const QList<QTreeWidgetItem *> selectedTrackerItems = selectedItems(); |
||||
QVector<QTreeWidgetItem *> selectedTrackers; |
||||
selectedTrackers.reserve(selectedTrackerItems.size()); |
||||
|
||||
for (QTreeWidgetItem *item : selectedTrackerItems) |
||||
{ |
||||
if (indexOfTopLevelItem(item) >= NB_STICKY_ITEM) // Ignore STICKY ITEMS
|
||||
selectedTrackers << item; |
||||
} |
||||
|
||||
return selectedTrackers; |
||||
} |
||||
|
||||
void TrackerListWidget::setRowColor(const int row, const QColor &color) |
||||
{ |
||||
const int nbColumns = columnCount(); |
||||
QTreeWidgetItem *item = topLevelItem(row); |
||||
for (int i = 0; i < nbColumns; ++i) |
||||
item->setData(i, Qt::ForegroundRole, color); |
||||
} |
||||
|
||||
void TrackerListWidget::moveSelectionUp() |
||||
{ |
||||
BitTorrent::Torrent *const torrent = m_properties->getCurrentTorrent(); |
||||
if (!torrent) |
||||
{ |
||||
clear(); |
||||
return; |
||||
} |
||||
const QVector<QTreeWidgetItem *> selectedTrackerItems = getSelectedTrackerItems(); |
||||
if (selectedTrackerItems.isEmpty()) return; |
||||
|
||||
bool change = false; |
||||
for (QTreeWidgetItem *item : selectedTrackerItems) |
||||
{ |
||||
int index = indexOfTopLevelItem(item); |
||||
if (index > NB_STICKY_ITEM) |
||||
{ |
||||
insertTopLevelItem(index - 1, takeTopLevelItem(index)); |
||||
change = true; |
||||
} |
||||
} |
||||
if (!change) return; |
||||
|
||||
// Restore selection
|
||||
QItemSelectionModel *selection = selectionModel(); |
||||
for (QTreeWidgetItem *item : selectedTrackerItems) |
||||
selection->select(indexFromItem(item), (QItemSelectionModel::Rows | QItemSelectionModel::Select)); |
||||
|
||||
setSelectionModel(selection); |
||||
// Update torrent trackers
|
||||
QVector<BitTorrent::TrackerEntry> trackers; |
||||
trackers.reserve(topLevelItemCount()); |
||||
for (int i = NB_STICKY_ITEM; i < topLevelItemCount(); ++i) |
||||
{ |
||||
const QString trackerURL = topLevelItem(i)->data(COL_URL, Qt::DisplayRole).toString(); |
||||
trackers.append({trackerURL, (i - NB_STICKY_ITEM)}); |
||||
} |
||||
|
||||
torrent->replaceTrackers(trackers); |
||||
// Reannounce
|
||||
if (!torrent->isPaused()) |
||||
torrent->forceReannounce(); |
||||
} |
||||
|
||||
void TrackerListWidget::moveSelectionDown() |
||||
{ |
||||
BitTorrent::Torrent *const torrent = m_properties->getCurrentTorrent(); |
||||
if (!torrent) |
||||
{ |
||||
clear(); |
||||
return; |
||||
} |
||||
const QVector<QTreeWidgetItem *> selectedTrackerItems = getSelectedTrackerItems(); |
||||
if (selectedTrackerItems.isEmpty()) return; |
||||
|
||||
bool change = false; |
||||
for (int i = selectedItems().size() - 1; i >= 0; --i) |
||||
{ |
||||
int index = indexOfTopLevelItem(selectedTrackerItems.at(i)); |
||||
if (index < (topLevelItemCount() - 1)) |
||||
{ |
||||
insertTopLevelItem(index + 1, takeTopLevelItem(index)); |
||||
change = true; |
||||
} |
||||
} |
||||
if (!change) return; |
||||
|
||||
// Restore selection
|
||||
QItemSelectionModel *selection = selectionModel(); |
||||
for (QTreeWidgetItem *item : selectedTrackerItems) |
||||
selection->select(indexFromItem(item), (QItemSelectionModel::Rows | QItemSelectionModel::Select)); |
||||
|
||||
setSelectionModel(selection); |
||||
// Update torrent trackers
|
||||
QVector<BitTorrent::TrackerEntry> trackers; |
||||
trackers.reserve(topLevelItemCount()); |
||||
for (int i = NB_STICKY_ITEM; i < topLevelItemCount(); ++i) |
||||
{ |
||||
const QString trackerURL = topLevelItem(i)->data(COL_URL, Qt::DisplayRole).toString(); |
||||
trackers.append({trackerURL, (i - NB_STICKY_ITEM)}); |
||||
} |
||||
|
||||
torrent->replaceTrackers(trackers); |
||||
// Reannounce
|
||||
if (!torrent->isPaused()) |
||||
torrent->forceReannounce(); |
||||
} |
||||
|
||||
void TrackerListWidget::clear() |
||||
{ |
||||
qDeleteAll(m_trackerItems); |
||||
m_trackerItems.clear(); |
||||
|
||||
m_DHTItem->setText(COL_STATUS, {}); |
||||
m_DHTItem->setText(COL_SEEDS, {}); |
||||
m_DHTItem->setText(COL_LEECHES, {}); |
||||
m_DHTItem->setText(COL_MSG, {}); |
||||
m_PEXItem->setText(COL_STATUS, {}); |
||||
m_PEXItem->setText(COL_SEEDS, {}); |
||||
m_PEXItem->setText(COL_LEECHES, {}); |
||||
m_PEXItem->setText(COL_MSG, {}); |
||||
m_LSDItem->setText(COL_STATUS, {}); |
||||
m_LSDItem->setText(COL_SEEDS, {}); |
||||
m_LSDItem->setText(COL_LEECHES, {}); |
||||
m_LSDItem->setText(COL_MSG, {}); |
||||
} |
||||
|
||||
void TrackerListWidget::loadStickyItems(const BitTorrent::Torrent *torrent) |
||||
{ |
||||
const QString working {tr("Working")}; |
||||
const QString disabled {tr("Disabled")}; |
||||
const QString torrentDisabled {tr("Disabled for this torrent")}; |
||||
const auto *session = BitTorrent::Session::instance(); |
||||
|
||||
// load DHT information
|
||||
if (!session->isDHTEnabled()) |
||||
m_DHTItem->setText(COL_STATUS, disabled); |
||||
else if (torrent->isPrivate() || torrent->isDHTDisabled()) |
||||
m_DHTItem->setText(COL_STATUS, torrentDisabled); |
||||
else |
||||
m_DHTItem->setText(COL_STATUS, working); |
||||
|
||||
// Load PeX Information
|
||||
if (!session->isPeXEnabled()) |
||||
m_PEXItem->setText(COL_STATUS, disabled); |
||||
else if (torrent->isPrivate() || torrent->isPEXDisabled()) |
||||
m_PEXItem->setText(COL_STATUS, torrentDisabled); |
||||
else |
||||
m_PEXItem->setText(COL_STATUS, working); |
||||
|
||||
// Load LSD Information
|
||||
if (!session->isLSDEnabled()) |
||||
m_LSDItem->setText(COL_STATUS, disabled); |
||||
else if (torrent->isPrivate() || torrent->isLSDDisabled()) |
||||
m_LSDItem->setText(COL_STATUS, torrentDisabled); |
||||
else |
||||
m_LSDItem->setText(COL_STATUS, working); |
||||
|
||||
if (torrent->isPrivate()) |
||||
{ |
||||
QString privateMsg = tr("This torrent is private"); |
||||
m_DHTItem->setText(COL_MSG, privateMsg); |
||||
m_PEXItem->setText(COL_MSG, privateMsg); |
||||
m_LSDItem->setText(COL_MSG, privateMsg); |
||||
} |
||||
|
||||
using TorrentPtr = QPointer<const BitTorrent::Torrent>; |
||||
torrent->fetchPeerInfo([this, torrent = TorrentPtr(torrent)](const QVector<BitTorrent::PeerInfo> &peers) |
||||
{ |
||||
if (torrent != m_properties->getCurrentTorrent()) |
||||
return; |
||||
|
||||
// XXX: libtorrent should provide this info...
|
||||
// Count peers from DHT, PeX, LSD
|
||||
uint seedsDHT = 0, seedsPeX = 0, seedsLSD = 0, peersDHT = 0, peersPeX = 0, peersLSD = 0; |
||||
for (const BitTorrent::PeerInfo &peer : peers) |
||||
{ |
||||
if (peer.isConnecting()) |
||||
continue; |
||||
|
||||
if (peer.fromDHT()) |
||||
{ |
||||
if (peer.isSeed()) |
||||
++seedsDHT; |
||||
else |
||||
++peersDHT; |
||||
} |
||||
if (peer.fromPeX()) |
||||
{ |
||||
if (peer.isSeed()) |
||||
++seedsPeX; |
||||
else |
||||
++peersPeX; |
||||
} |
||||
if (peer.fromLSD()) |
||||
{ |
||||
if (peer.isSeed()) |
||||
++seedsLSD; |
||||
else |
||||
++peersLSD; |
||||
} |
||||
} |
||||
|
||||
m_DHTItem->setText(COL_SEEDS, QString::number(seedsDHT)); |
||||
m_DHTItem->setText(COL_LEECHES, QString::number(peersDHT)); |
||||
m_PEXItem->setText(COL_SEEDS, QString::number(seedsPeX)); |
||||
m_PEXItem->setText(COL_LEECHES, QString::number(peersPeX)); |
||||
m_LSDItem->setText(COL_SEEDS, QString::number(seedsLSD)); |
||||
m_LSDItem->setText(COL_LEECHES, QString::number(peersLSD)); |
||||
}); |
||||
} |
||||
|
||||
void TrackerListWidget::loadTrackers() |
||||
{ |
||||
// Load trackers from torrent handle
|
||||
const BitTorrent::Torrent *torrent = m_properties->getCurrentTorrent(); |
||||
if (!torrent) return; |
||||
|
||||
loadStickyItems(torrent); |
||||
|
||||
const auto setAlignment = [](QTreeWidgetItem *item) |
||||
{ |
||||
for (const TrackerListColumn col : {COL_TIER, COL_PROTOCOL, COL_PEERS, COL_SEEDS |
||||
, COL_LEECHES, COL_TIMES_DOWNLOADED, COL_NEXT_ANNOUNCE, COL_MIN_ANNOUNCE}) |
||||
{ |
||||
item->setTextAlignment(col, (Qt::AlignRight | Qt::AlignVCenter)); |
||||
} |
||||
}; |
||||
|
||||
const auto prettyCount = [](const int val) |
||||
{ |
||||
return (val > -1) ? QString::number(val) : tr("N/A"); |
||||
}; |
||||
|
||||
const auto toString = [](const BitTorrent::TrackerEntry::Status status) |
||||
{ |
||||
switch (status) |
||||
{ |
||||
case BitTorrent::TrackerEntry::Status::Working: |
||||
return tr("Working"); |
||||
case BitTorrent::TrackerEntry::Status::Updating: |
||||
return tr("Updating..."); |
||||
case BitTorrent::TrackerEntry::Status::NotWorking: |
||||
return tr("Not working"); |
||||
case BitTorrent::TrackerEntry::Status::TrackerError: |
||||
return tr("Tracker error"); |
||||
case BitTorrent::TrackerEntry::Status::Unreachable: |
||||
return tr("Unreachable"); |
||||
case BitTorrent::TrackerEntry::Status::NotContacted: |
||||
return tr("Not contacted yet"); |
||||
} |
||||
return tr("Invalid status!"); |
||||
}; |
||||
|
||||
// Load actual trackers information
|
||||
QStringList oldTrackerURLs = m_trackerItems.keys(); |
||||
|
||||
for (const BitTorrent::TrackerEntry &entry : asConst(torrent->trackers())) |
||||
{ |
||||
const QString trackerURL = entry.url; |
||||
|
||||
QTreeWidgetItem *item = m_trackerItems.value(trackerURL, nullptr); |
||||
if (!item) |
||||
{ |
||||
item = new QTreeWidgetItem(); |
||||
item->setText(COL_URL, trackerURL); |
||||
item->setToolTip(COL_URL, trackerURL); |
||||
addTopLevelItem(item); |
||||
m_trackerItems[trackerURL] = item; |
||||
} |
||||
else |
||||
{ |
||||
oldTrackerURLs.removeOne(trackerURL); |
||||
} |
||||
|
||||
const auto now = QDateTime::currentDateTime(); |
||||
|
||||
int peersMax = -1; |
||||
int seedsMax = -1; |
||||
int leechesMax = -1; |
||||
int downloadedMax = -1; |
||||
QDateTime nextAnnounceTime; |
||||
QDateTime minAnnounceTime; |
||||
QString message; |
||||
|
||||
int index = 0; |
||||
for (const auto &endpoint : entry.stats) |
||||
{ |
||||
for (auto it = endpoint.cbegin(), end = endpoint.cend(); it != end; ++it) |
||||
{ |
||||
const int protocolVersion = it.key(); |
||||
const BitTorrent::TrackerEntry::EndpointStats &protocolStats = it.value(); |
||||
|
||||
peersMax = std::max(peersMax, protocolStats.numPeers); |
||||
seedsMax = std::max(seedsMax, protocolStats.numSeeds); |
||||
leechesMax = std::max(leechesMax, protocolStats.numLeeches); |
||||
downloadedMax = std::max(downloadedMax, protocolStats.numDownloaded); |
||||
|
||||
if (protocolStats.status == entry.status) |
||||
{ |
||||
if (!nextAnnounceTime.isValid() || (nextAnnounceTime > protocolStats.nextAnnounceTime)) |
||||
{ |
||||
nextAnnounceTime = protocolStats.nextAnnounceTime; |
||||
minAnnounceTime = protocolStats.minAnnounceTime; |
||||
if ((protocolStats.status != BitTorrent::TrackerEntry::Status::Working) |
||||
|| !protocolStats.message.isEmpty()) |
||||
{ |
||||
message = protocolStats.message; |
||||
} |
||||
} |
||||
|
||||
if (protocolStats.status == BitTorrent::TrackerEntry::Status::Working) |
||||
{ |
||||
if (message.isEmpty()) |
||||
message = protocolStats.message; |
||||
} |
||||
} |
||||
|
||||
QTreeWidgetItem *child = (index < item->childCount()) ? item->child(index) : new QTreeWidgetItem(item); |
||||
child->setText(COL_URL, protocolStats.name); |
||||
child->setText(COL_PROTOCOL, tr("v%1").arg(protocolVersion)); |
||||
child->setText(COL_STATUS, toString(protocolStats.status)); |
||||
child->setText(COL_PEERS, prettyCount(protocolStats.numPeers)); |
||||
child->setText(COL_SEEDS, prettyCount(protocolStats.numSeeds)); |
||||
child->setText(COL_LEECHES, prettyCount(protocolStats.numLeeches)); |
||||
child->setText(COL_TIMES_DOWNLOADED, prettyCount(protocolStats.numDownloaded)); |
||||
child->setText(COL_MSG, protocolStats.message); |
||||
child->setToolTip(COL_MSG, protocolStats.message); |
||||
child->setText(COL_NEXT_ANNOUNCE, Utils::Misc::userFriendlyDuration(now.secsTo(protocolStats.nextAnnounceTime), -1, Utils::Misc::TimeResolution::Seconds)); |
||||
child->setText(COL_MIN_ANNOUNCE, Utils::Misc::userFriendlyDuration(now.secsTo(protocolStats.minAnnounceTime), -1, Utils::Misc::TimeResolution::Seconds)); |
||||
setAlignment(child); |
||||
++index; |
||||
} |
||||
} |
||||
|
||||
while (item->childCount() != index) |
||||
delete item->takeChild(index); |
||||
|
||||
item->setText(COL_TIER, QString::number(entry.tier)); |
||||
item->setText(COL_STATUS, toString(entry.status)); |
||||
|
||||
item->setData(COL_PEERS, Qt::UserRole, prettyCount(peersMax)); |
||||
item->setData(COL_SEEDS, Qt::UserRole, prettyCount(seedsMax)); |
||||
item->setData(COL_LEECHES, Qt::UserRole, prettyCount(leechesMax)); |
||||
item->setData(COL_TIMES_DOWNLOADED, Qt::UserRole, prettyCount(downloadedMax)); |
||||
item->setData(COL_MSG, Qt::UserRole, message); |
||||
item->setData(COL_NEXT_ANNOUNCE, Qt::UserRole, nextAnnounceTime); |
||||
item->setData(COL_MIN_ANNOUNCE, Qt::UserRole, minAnnounceTime); |
||||
if (!item->isExpanded()) |
||||
{ |
||||
item->setText(COL_PEERS, item->data(COL_PEERS, Qt::UserRole).toString()); |
||||
item->setText(COL_SEEDS, item->data(COL_SEEDS, Qt::UserRole).toString()); |
||||
item->setText(COL_LEECHES, item->data(COL_LEECHES, Qt::UserRole).toString()); |
||||
item->setText(COL_TIMES_DOWNLOADED, item->data(COL_TIMES_DOWNLOADED, Qt::UserRole).toString()); |
||||
item->setText(COL_MSG, item->data(COL_MSG, Qt::UserRole).toString()); |
||||
const auto secsToNextAnnounce = now.secsTo(item->data(COL_NEXT_ANNOUNCE, Qt::UserRole).toDateTime()); |
||||
item->setText(COL_NEXT_ANNOUNCE, Utils::Misc::userFriendlyDuration(secsToNextAnnounce, -1, Utils::Misc::TimeResolution::Seconds)); |
||||
const auto secsToMinAnnounce = now.secsTo(item->data(COL_MIN_ANNOUNCE, Qt::UserRole).toDateTime()); |
||||
item->setText(COL_MIN_ANNOUNCE, Utils::Misc::userFriendlyDuration(secsToMinAnnounce, -1, Utils::Misc::TimeResolution::Seconds)); |
||||
} |
||||
setAlignment(item); |
||||
} |
||||
|
||||
// Remove old trackers
|
||||
for (const QString &tracker : asConst(oldTrackerURLs)) |
||||
delete m_trackerItems.take(tracker); |
||||
} |
||||
|
||||
void TrackerListWidget::openAddTrackersDialog() |
||||
{ |
||||
BitTorrent::Torrent *torrent = m_properties->getCurrentTorrent(); |
||||
if (!torrent) |
||||
return; |
||||
|
||||
auto *dialog = new TrackersAdditionDialog(this, torrent); |
||||
dialog->setAttribute(Qt::WA_DeleteOnClose); |
||||
dialog->open(); |
||||
} |
||||
|
||||
void TrackerListWidget::copyTrackerUrl() |
||||
{ |
||||
const QVector<QTreeWidgetItem *> selectedTrackerItems = getSelectedTrackerItems(); |
||||
if (selectedTrackerItems.isEmpty()) return; |
||||
|
||||
QStringList urlsToCopy; |
||||
for (const QTreeWidgetItem *item : selectedTrackerItems) |
||||
{ |
||||
QString trackerURL = item->data(COL_URL, Qt::DisplayRole).toString(); |
||||
qDebug() << "Copy:" << qUtf8Printable(trackerURL); |
||||
urlsToCopy << trackerURL; |
||||
} |
||||
QApplication::clipboard()->setText(urlsToCopy.join(u'\n')); |
||||
} |
||||
|
||||
|
||||
void TrackerListWidget::deleteSelectedTrackers() |
||||
{ |
||||
BitTorrent::Torrent *const torrent = m_properties->getCurrentTorrent(); |
||||
if (!torrent) |
||||
{ |
||||
clear(); |
||||
return; |
||||
} |
||||
|
||||
const QVector<QTreeWidgetItem *> selectedTrackerItems = getSelectedTrackerItems(); |
||||
if (selectedTrackerItems.isEmpty()) return; |
||||
|
||||
QStringList urlsToRemove; |
||||
for (const QTreeWidgetItem *item : selectedTrackerItems) |
||||
{ |
||||
QString trackerURL = item->data(COL_URL, Qt::DisplayRole).toString(); |
||||
urlsToRemove << trackerURL; |
||||
m_trackerItems.remove(trackerURL); |
||||
delete item; |
||||
} |
||||
|
||||
torrent->removeTrackers(urlsToRemove); |
||||
|
||||
if (!torrent->isPaused()) |
||||
torrent->forceReannounce(); |
||||
} |
||||
|
||||
void TrackerListWidget::editSelectedTracker() |
||||
{ |
||||
BitTorrent::Torrent *const torrent = m_properties->getCurrentTorrent(); |
||||
if (!torrent) return; |
||||
|
||||
const QVector<QTreeWidgetItem *> selectedTrackerItems = getSelectedTrackerItems(); |
||||
if (selectedTrackerItems.isEmpty()) return; |
||||
|
||||
// During multi-select only process item selected last
|
||||
const QUrl trackerURL = selectedTrackerItems.last()->text(COL_URL); |
||||
|
||||
bool ok = false; |
||||
const QUrl newTrackerURL = AutoExpandableDialog::getText(this, tr("Tracker editing"), tr("Tracker URL:"), |
||||
QLineEdit::Normal, trackerURL.toString(), &ok).trimmed(); |
||||
if (!ok) return; |
||||
|
||||
if (!newTrackerURL.isValid()) |
||||
{ |
||||
QMessageBox::warning(this, tr("Tracker editing failed"), tr("The tracker URL entered is invalid.")); |
||||
return; |
||||
} |
||||
if (newTrackerURL == trackerURL) return; |
||||
|
||||
QVector<BitTorrent::TrackerEntry> trackers = torrent->trackers(); |
||||
bool match = false; |
||||
for (BitTorrent::TrackerEntry &entry : trackers) |
||||
{ |
||||
if (newTrackerURL == QUrl(entry.url)) |
||||
{ |
||||
QMessageBox::warning(this, tr("Tracker editing failed"), tr("The tracker URL already exists.")); |
||||
return; |
||||
} |
||||
|
||||
if (!match && (trackerURL == QUrl(entry.url))) |
||||
{ |
||||
match = true; |
||||
entry.url = newTrackerURL.toString(); |
||||
} |
||||
} |
||||
|
||||
torrent->replaceTrackers(trackers); |
||||
|
||||
if (!torrent->isPaused()) |
||||
torrent->forceReannounce(); |
||||
} |
||||
|
||||
void TrackerListWidget::reannounceSelected() |
||||
{ |
||||
const QList<QTreeWidgetItem *> selItems = selectedItems(); |
||||
if (selItems.isEmpty()) return; |
||||
|
||||
BitTorrent::Torrent *const torrent = m_properties->getCurrentTorrent(); |
||||
if (!torrent) return; |
||||
|
||||
const QVector<BitTorrent::TrackerEntry> trackers = torrent->trackers(); |
||||
|
||||
for (const QTreeWidgetItem *item : selItems) |
||||
{ |
||||
// DHT case
|
||||
if (item == m_DHTItem) |
||||
{ |
||||
torrent->forceDHTAnnounce(); |
||||
continue; |
||||
} |
||||
|
||||
// Trackers case
|
||||
for (int i = 0; i < trackers.size(); ++i) |
||||
{ |
||||
if (item->text(COL_URL) == trackers[i].url) |
||||
{ |
||||
torrent->forceReannounce(i); |
||||
break; |
||||
} |
||||
} |
||||
} |
||||
|
||||
loadTrackers(); |
||||
} |
||||
|
||||
void TrackerListWidget::showTrackerListMenu() |
||||
{ |
||||
BitTorrent::Torrent *const torrent = m_properties->getCurrentTorrent(); |
||||
if (!torrent) return; |
||||
|
||||
QMenu *menu = new QMenu(this); |
||||
menu->setAttribute(Qt::WA_DeleteOnClose); |
||||
|
||||
// Add actions
|
||||
menu->addAction(UIThemeManager::instance()->getIcon(u"list-add"_s), tr("Add trackers...") |
||||
, this, &TrackerListWidget::openAddTrackersDialog); |
||||
|
||||
if (!getSelectedTrackerItems().isEmpty()) |
||||
{ |
||||
menu->addAction(UIThemeManager::instance()->getIcon(u"edit-rename"_s),tr("Edit tracker URL...") |
||||
, this, &TrackerListWidget::editSelectedTracker); |
||||
menu->addAction(UIThemeManager::instance()->getIcon(u"edit-clear"_s, u"list-remove"_s), tr("Remove tracker") |
||||
, this, &TrackerListWidget::deleteSelectedTrackers); |
||||
menu->addAction(UIThemeManager::instance()->getIcon(u"edit-copy"_s), tr("Copy tracker URL") |
||||
, this, &TrackerListWidget::copyTrackerUrl); |
||||
} |
||||
|
||||
if (!torrent->isPaused()) |
||||
{ |
||||
menu->addAction(UIThemeManager::instance()->getIcon(u"reannounce"_s, u"view-refresh"_s), tr("Force reannounce to selected trackers") |
||||
, this, &TrackerListWidget::reannounceSelected); |
||||
menu->addSeparator(); |
||||
menu->addAction(UIThemeManager::instance()->getIcon(u"reannounce"_s, u"view-refresh"_s), tr("Force reannounce to all trackers") |
||||
, this, [this]() |
||||
{ |
||||
BitTorrent::Torrent *h = m_properties->getCurrentTorrent(); |
||||
h->forceReannounce(); |
||||
h->forceDHTAnnounce(); |
||||
}); |
||||
} |
||||
|
||||
menu->popup(QCursor::pos()); |
||||
} |
||||
|
||||
void TrackerListWidget::loadSettings() |
||||
{ |
||||
header()->restoreState(Preferences::instance()->getPropTrackerListState()); |
||||
} |
||||
|
||||
void TrackerListWidget::saveSettings() const |
||||
{ |
||||
Preferences::instance()->setPropTrackerListState(header()->saveState()); |
||||
} |
||||
|
||||
QStringList TrackerListWidget::headerLabels() |
||||
{ |
||||
return |
||||
{ |
||||
tr("URL/Announce endpoint") |
||||
, tr("Tier") |
||||
, tr("Protocol") |
||||
, tr("Status") |
||||
, tr("Peers") |
||||
, tr("Seeds") |
||||
, tr("Leeches") |
||||
, tr("Times Downloaded") |
||||
, tr("Message") |
||||
, tr("Next announce") |
||||
, tr("Min announce") |
||||
}; |
||||
} |
||||
|
||||
int TrackerListWidget::visibleColumnsCount() const |
||||
{ |
||||
int count = 0; |
||||
for (int i = 0, iMax = header()->count(); i < iMax; ++i) |
||||
{ |
||||
if (!isColumnHidden(i)) |
||||
++count; |
||||
} |
||||
|
||||
return count; |
||||
} |
||||
|
||||
void TrackerListWidget::displayColumnHeaderMenu() |
||||
{ |
||||
QMenu *menu = new QMenu(this); |
||||
menu->setAttribute(Qt::WA_DeleteOnClose); |
||||
menu->setTitle(tr("Column visibility")); |
||||
menu->setToolTipsVisible(true); |
||||
|
||||
for (int i = 0; i < COL_COUNT; ++i) |
||||
{ |
||||
QAction *action = menu->addAction(headerLabels().at(i), this, [this, i](const bool checked) |
||||
{ |
||||
if (!checked && (visibleColumnsCount() <= 1)) |
||||
return; |
||||
|
||||
setColumnHidden(i, !checked); |
||||
|
||||
if (checked && (columnWidth(i) <= 5)) |
||||
resizeColumnToContents(i); |
||||
|
||||
saveSettings(); |
||||
}); |
||||
action->setCheckable(true); |
||||
action->setChecked(!isColumnHidden(i)); |
||||
} |
||||
|
||||
menu->addSeparator(); |
||||
QAction *resizeAction = menu->addAction(tr("Resize columns"), this, [this]() |
||||
{ |
||||
for (int i = 0, count = header()->count(); i < count; ++i) |
||||
{ |
||||
if (!isColumnHidden(i)) |
||||
resizeColumnToContents(i); |
||||
} |
||||
saveSettings(); |
||||
}); |
||||
resizeAction->setToolTip(tr("Resize all non-hidden columns to the size of their contents")); |
||||
|
||||
menu->popup(QCursor::pos()); |
||||
} |
||||
|
||||
void TrackerListWidget::wheelEvent(QWheelEvent *event) |
||||
{ |
||||
if (event->modifiers() & Qt::ShiftModifier) |
||||
{ |
||||
// Shift + scroll = horizontal scroll
|
||||
event->accept(); |
||||
QWheelEvent scrollHEvent {event->position(), event->globalPosition() |
||||
, event->pixelDelta(), event->angleDelta().transposed(), event->buttons() |
||||
, event->modifiers(), event->phase(), event->inverted(), event->source()}; |
||||
QTreeView::wheelEvent(&scrollHEvent); |
||||
return; |
||||
} |
||||
|
||||
QTreeView::wheelEvent(event); // event delegated to base class
|
||||
} |
@ -0,0 +1,65 @@
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Bittorrent Client using Qt and libtorrent. |
||||
* Copyright (C) 2023 Vladimir Golovnev <glassez@yandex.ru> |
||||
* |
||||
* This program is free software; you can redistribute it and/or |
||||
* modify it under the terms of the GNU General Public License |
||||
* as published by the Free Software Foundation; either version 2 |
||||
* of the License, or (at your option) any later version. |
||||
* |
||||
* This program is distributed in the hope that it will be useful, |
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
* GNU General Public License for more details. |
||||
* |
||||
* You should have received a copy of the GNU General Public License |
||||
* along with this program; if not, write to the Free Software |
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
* |
||||
* In addition, as a special exception, the copyright holders give permission to |
||||
* link this program with the OpenSSL project's "OpenSSL" library (or with |
||||
* modified versions of it that use the same license as the "OpenSSL" library), |
||||
* and distribute the linked executables. You must obey the GNU General Public |
||||
* License in all respects for all of the code used other than "OpenSSL". If you |
||||
* modify file(s), you may extend this exception to your version of the file(s), |
||||
* but you are not obligated to do so. If you do not wish to do so, delete this |
||||
* exception statement from your version. |
||||
*/ |
||||
|
||||
#include "trackerlistitemdelegate.h" |
||||
|
||||
#include <QModelIndex> |
||||
#include <QPainter> |
||||
|
||||
#include "trackerlistmodel.h" |
||||
#include "trackerlistwidget.h" |
||||
|
||||
TrackerListItemDelegate::TrackerListItemDelegate(TrackerListWidget *view) |
||||
: QStyledItemDelegate(view) |
||||
, m_view {view} |
||||
{ |
||||
Q_ASSERT(m_view); |
||||
} |
||||
|
||||
void TrackerListItemDelegate::initStyleOption(QStyleOptionViewItem *option, const QModelIndex &index) const |
||||
{ |
||||
QStyledItemDelegate::initStyleOption(option, index); |
||||
|
||||
if (index.parent().isValid() || !m_view->isExpanded(index.siblingAtColumn(0))) |
||||
return; |
||||
|
||||
switch (index.column()) |
||||
{ |
||||
case TrackerListModel::COL_PEERS: |
||||
case TrackerListModel::COL_SEEDS: |
||||
case TrackerListModel::COL_LEECHES: |
||||
case TrackerListModel::COL_TIMES_DOWNLOADED: |
||||
case TrackerListModel::COL_MSG: |
||||
case TrackerListModel::COL_NEXT_ANNOUNCE: |
||||
case TrackerListModel::COL_MIN_ANNOUNCE: |
||||
option->text.clear(); |
||||
break; |
||||
default: |
||||
break; |
||||
} |
||||
} |
@ -0,0 +1,49 @@
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Bittorrent Client using Qt and libtorrent. |
||||
* Copyright (C) 2023 Vladimir Golovnev <glassez@yandex.ru> |
||||
* |
||||
* This program is free software; you can redistribute it and/or |
||||
* modify it under the terms of the GNU General Public License |
||||
* as published by the Free Software Foundation; either version 2 |
||||
* of the License, or (at your option) any later version. |
||||
* |
||||
* This program is distributed in the hope that it will be useful, |
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
* GNU General Public License for more details. |
||||
* |
||||
* You should have received a copy of the GNU General Public License |
||||
* along with this program; if not, write to the Free Software |
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
* |
||||
* In addition, as a special exception, the copyright holders give permission to |
||||
* link this program with the OpenSSL project's "OpenSSL" library (or with |
||||
* modified versions of it that use the same license as the "OpenSSL" library), |
||||
* and distribute the linked executables. You must obey the GNU General Public |
||||
* License in all respects for all of the code used other than "OpenSSL". If you |
||||
* modify file(s), you may extend this exception to your version of the file(s), |
||||
* but you are not obligated to do so. If you do not wish to do so, delete this |
||||
* exception statement from your version. |
||||
*/ |
||||
|
||||
#pragma once |
||||
|
||||
#include <QStyledItemDelegate> |
||||
|
||||
//class QModelIndex;
|
||||
//class QStyleOptionViewItem;
|
||||
class TrackerListWidget; |
||||
|
||||
class TrackerListItemDelegate final : public QStyledItemDelegate |
||||
{ |
||||
Q_OBJECT |
||||
Q_DISABLE_COPY_MOVE(TrackerListItemDelegate) |
||||
|
||||
public: |
||||
explicit TrackerListItemDelegate(TrackerListWidget *view); |
||||
|
||||
void initStyleOption(QStyleOptionViewItem *option, const QModelIndex &index) const override; |
||||
|
||||
private: |
||||
TrackerListWidget *m_view = nullptr; |
||||
}; |
@ -0,0 +1,781 @@
@@ -0,0 +1,781 @@
|
||||
/*
|
||||
* Bittorrent Client using Qt and libtorrent. |
||||
* Copyright (C) 2023 Vladimir Golovnev <glassez@yandex.ru> |
||||
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org> |
||||
* |
||||
* This program is free software; you can redistribute it and/or |
||||
* modify it under the terms of the GNU General Public License |
||||
* as published by the Free Software Foundation; either version 2 |
||||
* of the License, or (at your option) any later version. |
||||
* |
||||
* This program is distributed in the hope that it will be useful, |
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
* GNU General Public License for more details. |
||||
* |
||||
* You should have received a copy of the GNU General Public License |
||||
* along with this program; if not, write to the Free Software |
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
* |
||||
* In addition, as a special exception, the copyright holders give permission to |
||||
* link this program with the OpenSSL project's "OpenSSL" library (or with |
||||
* modified versions of it that use the same license as the "OpenSSL" library), |
||||
* and distribute the linked executables. You must obey the GNU General Public |
||||
* License in all respects for all of the code used other than "OpenSSL". If you |
||||
* modify file(s), you may extend this exception to your version of the file(s), |
||||
* but you are not obligated to do so. If you do not wish to do so, delete this |
||||
* exception statement from your version. |
||||
*/ |
||||
|
||||
#include "trackerlistmodel.h" |
||||
|
||||
#include <chrono> |
||||
|
||||
#include <boost/multi_index_container.hpp> |
||||
#include <boost/multi_index/composite_key.hpp> |
||||
#include <boost/multi_index/hashed_index.hpp> |
||||
#include <boost/multi_index/indexed_by.hpp> |
||||
#include <boost/multi_index/member.hpp> |
||||
#include <boost/multi_index/random_access_index.hpp> |
||||
#include <boost/multi_index/tag.hpp> |
||||
|
||||
#include <QColor> |
||||
#include <QList> |
||||
#include <QPointer> |
||||
#include <QScopeGuard> |
||||
#include <QTimer> |
||||
|
||||
#include "base/bittorrent/peerinfo.h" |
||||
#include "base/bittorrent/session.h" |
||||
#include "base/bittorrent/torrent.h" |
||||
#include "base/global.h" |
||||
#include "base/utils/misc.h" |
||||
|
||||
using namespace std::chrono_literals; |
||||
using namespace boost::multi_index; |
||||
|
||||
const std::chrono::milliseconds ANNOUNCE_TIME_REFRESH_INTERVAL = 4s; |
||||
|
||||
namespace |
||||
{ |
||||
const QString STR_WORKING = TrackerListModel::tr("Working"); |
||||
const QString STR_DISABLED = TrackerListModel::tr("Disabled"); |
||||
const QString STR_TORRENT_DISABLED = TrackerListModel::tr("Disabled for this torrent"); |
||||
const QString STR_PRIVATE_MSG = TrackerListModel::tr("This torrent is private"); |
||||
|
||||
QString prettyCount(const int val) |
||||
{ |
||||
return (val > -1) ? QString::number(val) : TrackerListModel::tr("N/A"); |
||||
} |
||||
|
||||
QString toString(const BitTorrent::TrackerEntryStatus status) |
||||
{ |
||||
switch (status) |
||||
{ |
||||
case BitTorrent::TrackerEntryStatus::Working: |
||||
return TrackerListModel::tr("Working"); |
||||
case BitTorrent::TrackerEntryStatus::Updating: |
||||
return TrackerListModel::tr("Updating..."); |
||||
case BitTorrent::TrackerEntryStatus::NotWorking: |
||||
return TrackerListModel::tr("Not working"); |
||||
case BitTorrent::TrackerEntryStatus::TrackerError: |
||||
return TrackerListModel::tr("Tracker error"); |
||||
case BitTorrent::TrackerEntryStatus::Unreachable: |
||||
return TrackerListModel::tr("Unreachable"); |
||||
case BitTorrent::TrackerEntryStatus::NotContacted: |
||||
return TrackerListModel::tr("Not contacted yet"); |
||||
} |
||||
return TrackerListModel::tr("Invalid status!"); |
||||
} |
||||
|
||||
QString statusDHT(const BitTorrent::Torrent *torrent) |
||||
{ |
||||
if (!torrent->session()->isDHTEnabled()) |
||||
return STR_DISABLED; |
||||
|
||||
if (torrent->isPrivate() || torrent->isDHTDisabled()) |
||||
return STR_TORRENT_DISABLED; |
||||
|
||||
return STR_WORKING; |
||||
} |
||||
|
||||
QString statusPeX(const BitTorrent::Torrent *torrent) |
||||
{ |
||||
if (!torrent->session()->isPeXEnabled()) |
||||
return STR_DISABLED; |
||||
|
||||
if (torrent->isPrivate() || torrent->isPEXDisabled()) |
||||
return STR_TORRENT_DISABLED; |
||||
|
||||
return STR_WORKING; |
||||
} |
||||
|
||||
QString statusLSD(const BitTorrent::Torrent *torrent) |
||||
{ |
||||
if (!torrent->session()->isLSDEnabled()) |
||||
return STR_DISABLED; |
||||
|
||||
if (torrent->isPrivate() || torrent->isLSDDisabled()) |
||||
return STR_TORRENT_DISABLED; |
||||
|
||||
return STR_WORKING; |
||||
} |
||||
} |
||||
|
||||
std::size_t hash_value(const QString &string) |
||||
{ |
||||
return qHash(string); |
||||
} |
||||
|
||||
struct TrackerListModel::Item final |
||||
{ |
||||
QString name {}; |
||||
int tier = -1; |
||||
int btVersion = -1; |
||||
BitTorrent::TrackerEntryStatus status = BitTorrent::TrackerEntryStatus::NotContacted; |
||||
QString message {}; |
||||
|
||||
int numPeers = -1; |
||||
int numSeeds = -1; |
||||
int numLeeches = -1; |
||||
int numDownloaded = -1; |
||||
|
||||
QDateTime nextAnnounceTime {}; |
||||
QDateTime minAnnounceTime {}; |
||||
|
||||
qint64 secsToNextAnnounce = 0; |
||||
qint64 secsToMinAnnounce = 0; |
||||
QDateTime announceTimestamp; |
||||
|
||||
std::weak_ptr<Item> parentItem {}; |
||||
|
||||
multi_index_container<std::shared_ptr<Item>, indexed_by< |
||||
random_access<>, |
||||
hashed_unique<tag<struct ByID>, composite_key< |
||||
Item, |
||||
member<Item, QString, &Item::name>, |
||||
member<Item, int, &Item::btVersion> |
||||
>> |
||||
>> childItems {}; |
||||
|
||||
Item(QStringView name, QStringView message); |
||||
explicit Item(const BitTorrent::TrackerEntry &trackerEntry); |
||||
Item(const std::shared_ptr<Item> &parentItem, const BitTorrent::TrackerEndpointEntry &endpointEntry); |
||||
|
||||
void fillFrom(const BitTorrent::TrackerEntry &trackerEntry); |
||||
void fillFrom(const BitTorrent::TrackerEndpointEntry &endpointEntry); |
||||
}; |
||||
|
||||
class TrackerListModel::Items final : public multi_index_container< |
||||
std::shared_ptr<Item>, |
||||
indexed_by< |
||||
random_access<>, |
||||
hashed_unique<tag<struct ByName>, member<Item, QString, &Item::name>>>> |
||||
{ |
||||
}; |
||||
|
||||
TrackerListModel::Item::Item(const QStringView name, const QStringView message) |
||||
: name {name.toString()} |
||||
, message {message.toString()} |
||||
{ |
||||
} |
||||
|
||||
TrackerListModel::Item::Item(const BitTorrent::TrackerEntry &trackerEntry) |
||||
: name {trackerEntry.url} |
||||
{ |
||||
fillFrom(trackerEntry); |
||||
} |
||||
|
||||
TrackerListModel::Item::Item(const std::shared_ptr<Item> &parentItem, const BitTorrent::TrackerEndpointEntry &endpointEntry) |
||||
: name {endpointEntry.name} |
||||
, btVersion {endpointEntry.btVersion} |
||||
, parentItem {parentItem} |
||||
{ |
||||
fillFrom(endpointEntry); |
||||
} |
||||
|
||||
void TrackerListModel::Item::fillFrom(const BitTorrent::TrackerEntry &trackerEntry) |
||||
{ |
||||
Q_ASSERT(parentItem.expired()); |
||||
Q_ASSERT(trackerEntry.url == name); |
||||
|
||||
tier = trackerEntry.tier; |
||||
status = trackerEntry.status; |
||||
message = trackerEntry.message; |
||||
numPeers = trackerEntry.numPeers; |
||||
numSeeds = trackerEntry.numSeeds; |
||||
numLeeches = trackerEntry.numLeeches; |
||||
numDownloaded = trackerEntry.numDownloaded; |
||||
nextAnnounceTime = trackerEntry.nextAnnounceTime; |
||||
minAnnounceTime = trackerEntry.minAnnounceTime; |
||||
secsToNextAnnounce = 0; |
||||
secsToMinAnnounce = 0; |
||||
announceTimestamp = QDateTime(); |
||||
} |
||||
|
||||
void TrackerListModel::Item::fillFrom(const BitTorrent::TrackerEndpointEntry &endpointEntry) |
||||
{ |
||||
Q_ASSERT(!parentItem.expired()); |
||||
Q_ASSERT(endpointEntry.name == name); |
||||
Q_ASSERT(endpointEntry.btVersion == btVersion); |
||||
|
||||
status = endpointEntry.status; |
||||
message = endpointEntry.message; |
||||
numPeers = endpointEntry.numPeers; |
||||
numSeeds = endpointEntry.numSeeds; |
||||
numLeeches = endpointEntry.numLeeches; |
||||
numDownloaded = endpointEntry.numDownloaded; |
||||
nextAnnounceTime = endpointEntry.nextAnnounceTime; |
||||
minAnnounceTime = endpointEntry.minAnnounceTime; |
||||
secsToNextAnnounce = 0; |
||||
secsToMinAnnounce = 0; |
||||
announceTimestamp = QDateTime(); |
||||
} |
||||
|
||||
TrackerListModel::TrackerListModel(BitTorrent::Session *btSession, QObject *parent) |
||||
: QAbstractItemModel(parent) |
||||
, m_btSession {btSession} |
||||
, m_items {std::make_unique<Items>()} |
||||
, m_announceRefreshTimer {new QTimer(this)} |
||||
{ |
||||
Q_ASSERT(m_btSession); |
||||
|
||||
m_announceRefreshTimer->setSingleShot(true); |
||||
connect(m_announceRefreshTimer, &QTimer::timeout, this, &TrackerListModel::refreshAnnounceTimes); |
||||
|
||||
connect(m_btSession, &BitTorrent::Session::trackersAdded, this |
||||
, [this](BitTorrent::Torrent *torrent, const QList<BitTorrent::TrackerEntry> &newTrackers) |
||||
{ |
||||
if (torrent == m_torrent) |
||||
onTrackersAdded(newTrackers); |
||||
}); |
||||
connect(m_btSession, &BitTorrent::Session::trackersRemoved, this |
||||
, [this](BitTorrent::Torrent *torrent, const QStringList &deletedTrackers) |
||||
{ |
||||
if (torrent == m_torrent) |
||||
onTrackersRemoved(deletedTrackers); |
||||
}); |
||||
connect(m_btSession, &BitTorrent::Session::trackersChanged, this |
||||
, [this](BitTorrent::Torrent *torrent) |
||||
{ |
||||
if (torrent == m_torrent) |
||||
onTrackersChanged(); |
||||
}); |
||||
connect(m_btSession, &BitTorrent::Session::trackerEntriesUpdated, this |
||||
, [this](BitTorrent::Torrent *torrent, const QHash<QString, BitTorrent::TrackerEntry> &updatedTrackers) |
||||
{ |
||||
if (torrent == m_torrent) |
||||
onTrackersUpdated(updatedTrackers); |
||||
}); |
||||
} |
||||
|
||||
TrackerListModel::~TrackerListModel() = default; |
||||
|
||||
void TrackerListModel::setTorrent(BitTorrent::Torrent *torrent) |
||||
{ |
||||
beginResetModel(); |
||||
[[maybe_unused]] const auto modelResetGuard = qScopeGuard([this] { endResetModel(); }); |
||||
|
||||
if (m_torrent) |
||||
m_items->clear(); |
||||
|
||||
m_torrent = torrent; |
||||
|
||||
if (m_torrent) |
||||
populate(); |
||||
else |
||||
m_announceRefreshTimer->stop(); |
||||
} |
||||
|
||||
BitTorrent::Torrent *TrackerListModel::torrent() const |
||||
{ |
||||
return m_torrent; |
||||
} |
||||
|
||||
void TrackerListModel::populate() |
||||
{ |
||||
Q_ASSERT(m_torrent); |
||||
|
||||
const QList<BitTorrent::TrackerEntry> trackerEntries = m_torrent->trackers(); |
||||
m_items->reserve(trackerEntries.size() + STICKY_ROW_COUNT); |
||||
|
||||
const QString &privateTorrentMessage = m_torrent->isPrivate() ? STR_PRIVATE_MSG : u""_s; |
||||
m_items->emplace_back(std::make_shared<Item>(u"** [DHT] **", privateTorrentMessage)); |
||||
m_items->emplace_back(std::make_shared<Item>(u"** [PeX] **", privateTorrentMessage)); |
||||
m_items->emplace_back(std::make_shared<Item>(u"** [LSD] **", privateTorrentMessage)); |
||||
|
||||
using TorrentPtr = QPointer<const BitTorrent::Torrent>; |
||||
m_torrent->fetchPeerInfo([this, torrent = TorrentPtr(m_torrent)](const QList<BitTorrent::PeerInfo> &peers) |
||||
{ |
||||
if (torrent != m_torrent) |
||||
return; |
||||
|
||||
// XXX: libtorrent should provide this info...
|
||||
// Count peers from DHT, PeX, LSD
|
||||
uint seedsDHT = 0, seedsPeX = 0, seedsLSD = 0, peersDHT = 0, peersPeX = 0, peersLSD = 0; |
||||
for (const BitTorrent::PeerInfo &peer : peers) |
||||
{ |
||||
if (peer.isConnecting()) |
||||
continue; |
||||
|
||||
if (peer.isSeed()) |
||||
{ |
||||
if (peer.fromDHT()) |
||||
++seedsDHT; |
||||
|
||||
if (peer.fromPeX()) |
||||
++seedsPeX; |
||||
|
||||
if (peer.fromLSD()) |
||||
++seedsLSD; |
||||
} |
||||
else |
||||
{ |
||||
if (peer.fromDHT()) |
||||
++peersDHT; |
||||
|
||||
if (peer.fromPeX()) |
||||
++peersPeX; |
||||
|
||||
if (peer.fromLSD()) |
||||
++peersLSD; |
||||
} |
||||
} |
||||
|
||||
auto &itemsByPos = m_items->get<0>(); |
||||
itemsByPos.modify((itemsByPos.begin() + ROW_DHT), [&seedsDHT, &peersDHT](std::shared_ptr<Item> &item) |
||||
{ |
||||
item->numSeeds = seedsDHT; |
||||
item->numLeeches = peersDHT; |
||||
return true; |
||||
}); |
||||
itemsByPos.modify((itemsByPos.begin() + ROW_PEX), [&seedsPeX, &peersPeX](std::shared_ptr<Item> &item) |
||||
{ |
||||
item->numSeeds = seedsPeX; |
||||
item->numLeeches = peersPeX; |
||||
return true; |
||||
}); |
||||
itemsByPos.modify((itemsByPos.begin() + ROW_LSD), [&seedsLSD, &peersLSD](std::shared_ptr<Item> &item) |
||||
{ |
||||
item->numSeeds = seedsLSD; |
||||
item->numLeeches = peersLSD; |
||||
return true; |
||||
}); |
||||
|
||||
emit dataChanged(index(ROW_DHT, COL_SEEDS), index(ROW_LSD, COL_LEECHES)); |
||||
}); |
||||
|
||||
for (const BitTorrent::TrackerEntry &trackerEntry : trackerEntries) |
||||
addTrackerItem(trackerEntry); |
||||
|
||||
m_announceTimestamp = QDateTime::currentDateTime(); |
||||
m_announceRefreshTimer->start(ANNOUNCE_TIME_REFRESH_INTERVAL); |
||||
} |
||||
|
||||
std::shared_ptr<TrackerListModel::Item> TrackerListModel::createTrackerItem(const BitTorrent::TrackerEntry &trackerEntry) |
||||
{ |
||||
auto item = std::make_shared<Item>(trackerEntry); |
||||
for (const auto &[id, endpointEntry] : trackerEntry.endpointEntries.asKeyValueRange()) |
||||
{ |
||||
item->childItems.emplace_back(std::make_shared<Item>(item, endpointEntry)); |
||||
} |
||||
|
||||
return item; |
||||
} |
||||
|
||||
void TrackerListModel::addTrackerItem(const BitTorrent::TrackerEntry &trackerEntry) |
||||
{ |
||||
[[maybe_unused]] const auto &[iter, res] = m_items->emplace_back(createTrackerItem(trackerEntry)); |
||||
Q_ASSERT(res); |
||||
} |
||||
|
||||
void TrackerListModel::updateTrackerItem(const std::shared_ptr<Item> &item, const BitTorrent::TrackerEntry &trackerEntry) |
||||
{ |
||||
QSet<std::pair<QString, int>> endpointItemIDs; |
||||
QList<std::shared_ptr<Item>> newEndpointItems; |
||||
for (const auto &[id, endpointEntry] : trackerEntry.endpointEntries.asKeyValueRange()) |
||||
{ |
||||
endpointItemIDs.insert(id); |
||||
|
||||
auto &itemsByID = item->childItems.get<ByID>(); |
||||
if (const auto &iter = itemsByID.find(std::make_tuple(id.first, id.second)); iter != itemsByID.end()) |
||||
{ |
||||
(*iter)->fillFrom(endpointEntry); |
||||
} |
||||
else |
||||
{ |
||||
newEndpointItems.emplace_back(std::make_shared<Item>(item, endpointEntry)); |
||||
} |
||||
} |
||||
|
||||
const auto &itemsByPos = m_items->get<0>(); |
||||
const auto trackerRow = std::distance(itemsByPos.begin(), itemsByPos.iterator_to(item)); |
||||
const auto trackerIndex = index(trackerRow, 0); |
||||
|
||||
auto it = item->childItems.begin(); |
||||
while (it != item->childItems.end()) |
||||
{ |
||||
if (const auto endpointItemID = std::make_pair((*it)->name, (*it)->btVersion) |
||||
; endpointItemIDs.contains(endpointItemID)) |
||||
{ |
||||
++it; |
||||
} |
||||
else |
||||
{ |
||||
const auto row = std::distance(item->childItems.begin(), it); |
||||
beginRemoveRows(trackerIndex, row, row); |
||||
it = item->childItems.erase(it); |
||||
endRemoveRows(); |
||||
} |
||||
} |
||||
|
||||
const auto numRows = rowCount(trackerIndex); |
||||
emit dataChanged(index(0, 0, trackerIndex), index((numRows - 1), (columnCount(trackerIndex) - 1), trackerIndex)); |
||||
|
||||
if (!newEndpointItems.isEmpty()) |
||||
{ |
||||
beginInsertRows(trackerIndex, numRows, (numRows + newEndpointItems.size() - 1)); |
||||
for (const auto &newEndpointItem : asConst(newEndpointItems)) |
||||
item->childItems.get<0>().push_back(newEndpointItem); |
||||
endInsertRows(); |
||||
} |
||||
|
||||
item->fillFrom(trackerEntry); |
||||
emit dataChanged(trackerIndex, index(trackerRow, (columnCount() - 1))); |
||||
} |
||||
|
||||
void TrackerListModel::refreshAnnounceTimes() |
||||
{ |
||||
if (!m_torrent) |
||||
return; |
||||
|
||||
m_announceTimestamp = QDateTime::currentDateTime(); |
||||
emit dataChanged(index(0, COL_NEXT_ANNOUNCE), index((rowCount() - 1), COL_MIN_ANNOUNCE)); |
||||
for (int i = 0; i < rowCount(); ++i) |
||||
{ |
||||
const QModelIndex parentIndex = index(i, 0); |
||||
emit dataChanged(index(0, COL_NEXT_ANNOUNCE, parentIndex), index((rowCount(parentIndex) - 1), COL_MIN_ANNOUNCE, parentIndex)); |
||||
} |
||||
|
||||
m_announceRefreshTimer->start(ANNOUNCE_TIME_REFRESH_INTERVAL); |
||||
} |
||||
|
||||
int TrackerListModel::columnCount([[maybe_unused]] const QModelIndex &parent) const |
||||
{ |
||||
return COL_COUNT; |
||||
} |
||||
|
||||
int TrackerListModel::rowCount(const QModelIndex &parent) const |
||||
{ |
||||
if (!parent.isValid()) |
||||
return m_items->size(); |
||||
|
||||
const auto *item = static_cast<Item *>(parent.internalPointer()); |
||||
Q_ASSERT(item); |
||||
if (!item) [[unlikely]] |
||||
return 0; |
||||
|
||||
return item->childItems.size(); |
||||
} |
||||
|
||||
QVariant TrackerListModel::headerData(const int section, const Qt::Orientation orientation, const int role) const |
||||
{ |
||||
if (orientation != Qt::Horizontal) |
||||
return {}; |
||||
|
||||
switch (role) |
||||
{ |
||||
case Qt::DisplayRole: |
||||
switch (section) |
||||
{ |
||||
case COL_URL: |
||||
return tr("URL/Announce endpoint"); |
||||
case COL_TIER: |
||||
return tr("Tier"); |
||||
case COL_PROTOCOL: |
||||
return tr("Protocol"); |
||||
case COL_STATUS: |
||||
return tr("Status"); |
||||
case COL_PEERS: |
||||
return tr("Peers"); |
||||
case COL_SEEDS: |
||||
return tr("Seeds"); |
||||
case COL_LEECHES: |
||||
return tr("Leeches"); |
||||
case COL_TIMES_DOWNLOADED: |
||||
return tr("Times Downloaded"); |
||||
case COL_MSG: |
||||
return tr("Message"); |
||||
case COL_NEXT_ANNOUNCE: |
||||
return tr("Next announce"); |
||||
case COL_MIN_ANNOUNCE: |
||||
return tr("Min announce"); |
||||
default: |
||||
return {}; |
||||
} |
||||
|
||||
case Qt::TextAlignmentRole: |
||||
switch (section) |
||||
{ |
||||
case COL_TIER: |
||||
case COL_PEERS: |
||||
case COL_SEEDS: |
||||
case COL_LEECHES: |
||||
case COL_TIMES_DOWNLOADED: |
||||
case COL_NEXT_ANNOUNCE: |
||||
case COL_MIN_ANNOUNCE: |
||||
return QVariant {Qt::AlignRight | Qt::AlignVCenter}; |
||||
default: |
||||
return {}; |
||||
} |
||||
|
||||
default: |
||||
return {}; |
||||
} |
||||
} |
||||
|
||||
QVariant TrackerListModel::data(const QModelIndex &index, const int role) const |
||||
{ |
||||
if (!index.isValid()) |
||||
return {}; |
||||
|
||||
auto *itemPtr = static_cast<Item *>(index.internalPointer()); |
||||
Q_ASSERT(itemPtr); |
||||
if (!itemPtr) [[unlikely]] |
||||
return {}; |
||||
|
||||
if (itemPtr->announceTimestamp != m_announceTimestamp) |
||||
{ |
||||
itemPtr->secsToNextAnnounce = std::max<qint64>(0, m_announceTimestamp.secsTo(itemPtr->nextAnnounceTime)); |
||||
itemPtr->secsToMinAnnounce = std::max<qint64>(0, m_announceTimestamp.secsTo(itemPtr->minAnnounceTime)); |
||||
itemPtr->announceTimestamp = m_announceTimestamp; |
||||
} |
||||
|
||||
const bool isEndpoint = !itemPtr->parentItem.expired(); |
||||
|
||||
switch (role) |
||||
{ |
||||
case Qt::TextAlignmentRole: |
||||
switch (index.column()) |
||||
{ |
||||
case COL_TIER: |
||||
case COL_PROTOCOL: |
||||
case COL_PEERS: |
||||
case COL_SEEDS: |
||||
case COL_LEECHES: |
||||
case COL_TIMES_DOWNLOADED: |
||||
case COL_NEXT_ANNOUNCE: |
||||
case COL_MIN_ANNOUNCE: |
||||
return QVariant {Qt::AlignRight | Qt::AlignVCenter}; |
||||
default: |
||||
return {}; |
||||
} |
||||
|
||||
case Qt::ForegroundRole: |
||||
// TODO: Make me configurable via UI Theme
|
||||
if (!index.parent().isValid() && (index.row() < STICKY_ROW_COUNT)) |
||||
return QColorConstants::Svg::grey; |
||||
return {}; |
||||
|
||||
case Qt::DisplayRole: |
||||
case Qt::ToolTipRole: |
||||
switch (index.column()) |
||||
{ |
||||
case COL_URL: |
||||
return itemPtr->name; |
||||
case COL_TIER: |
||||
return (isEndpoint || (index.row() < STICKY_ROW_COUNT)) ? QString() : QString::number(itemPtr->tier); |
||||
case COL_PROTOCOL: |
||||
return isEndpoint ? tr("v%1").arg(itemPtr->btVersion) : QString(); |
||||
case COL_STATUS: |
||||
if (isEndpoint) |
||||
return toString(itemPtr->status); |
||||
if (index.row() == ROW_DHT) |
||||
return statusDHT(m_torrent); |
||||
if (index.row() == ROW_PEX) |
||||
return statusPeX(m_torrent); |
||||
if (index.row() == ROW_LSD) |
||||
return statusLSD(m_torrent); |
||||
return toString(itemPtr->status); |
||||
case COL_PEERS: |
||||
return prettyCount(itemPtr->numPeers); |
||||
case COL_SEEDS: |
||||
return prettyCount(itemPtr->numSeeds); |
||||
case COL_LEECHES: |
||||
return prettyCount(itemPtr->numLeeches); |
||||
case COL_TIMES_DOWNLOADED: |
||||
return prettyCount(itemPtr->numDownloaded); |
||||
case COL_MSG: |
||||
return itemPtr->message; |
||||
case COL_NEXT_ANNOUNCE: |
||||
return Utils::Misc::userFriendlyDuration(itemPtr->secsToNextAnnounce, -1, Utils::Misc::TimeResolution::Seconds); |
||||
case COL_MIN_ANNOUNCE: |
||||
return Utils::Misc::userFriendlyDuration(itemPtr->secsToMinAnnounce, -1, Utils::Misc::TimeResolution::Seconds); |
||||
default: |
||||
return {}; |
||||
} |
||||
|
||||
case SortRole: |
||||
switch (index.column()) |
||||
{ |
||||
case COL_URL: |
||||
return itemPtr->name; |
||||
case COL_TIER: |
||||
return isEndpoint ? -1 : itemPtr->tier; |
||||
case COL_PROTOCOL: |
||||
return isEndpoint ? itemPtr->btVersion : -1; |
||||
case COL_STATUS: |
||||
return toString(itemPtr->status); |
||||
case COL_PEERS: |
||||
return itemPtr->numPeers; |
||||
case COL_SEEDS: |
||||
return itemPtr->numSeeds; |
||||
case COL_LEECHES: |
||||
return itemPtr->numLeeches; |
||||
case COL_TIMES_DOWNLOADED: |
||||
return itemPtr->numDownloaded; |
||||
case COL_MSG: |
||||
return itemPtr->message; |
||||
case COL_NEXT_ANNOUNCE: |
||||
return itemPtr->secsToNextAnnounce; |
||||
case COL_MIN_ANNOUNCE: |
||||
return itemPtr->secsToMinAnnounce; |
||||
default: |
||||
return {}; |
||||
} |
||||
|
||||
default: |
||||
break; |
||||
} |
||||
|
||||
return {}; |
||||
} |
||||
|
||||
QModelIndex TrackerListModel::index(const int row, const int column, const QModelIndex &parent) const |
||||
{ |
||||
if ((column < 0) || (column >= columnCount())) |
||||
return {}; |
||||
|
||||
if ((row < 0) || (row >= rowCount(parent))) |
||||
return {}; |
||||
|
||||
const std::shared_ptr<Item> item = parent.isValid() |
||||
? m_items->at(static_cast<std::size_t>(parent.row()))->childItems.at(row) |
||||
: m_items->at(static_cast<std::size_t>(row)); |
||||
return createIndex(row, column, item.get()); |
||||
} |
||||
|
||||
QModelIndex TrackerListModel::parent(const QModelIndex &index) const |
||||
{ |
||||
if (!index.isValid()) |
||||
return {}; |
||||
|
||||
const auto *item = static_cast<Item *>(index.internalPointer()); |
||||
Q_ASSERT(item); |
||||
if (!item) [[unlikely]] |
||||
return {}; |
||||
|
||||
const std::shared_ptr<Item> parentItem = item->parentItem.lock(); |
||||
if (!parentItem) |
||||
return {}; |
||||
|
||||
const auto &itemsByName = m_items->get<ByName>(); |
||||
auto itemsByNameIter = itemsByName.find(parentItem->name); |
||||
Q_ASSERT(itemsByNameIter != itemsByName.end()); |
||||
if (itemsByNameIter == itemsByName.end()) [[unlikely]] |
||||
return {}; |
||||
|
||||
const auto &itemsByPosIter = m_items->project<0>(itemsByNameIter); |
||||
const auto row = std::distance(m_items->get<0>().begin(), itemsByPosIter); |
||||
|
||||
// From https://doc.qt.io/qt-6/qabstractitemmodel.html#parent:
|
||||
// A common convention used in models that expose tree data structures is that only items
|
||||
// in the first column have children. For that case, when reimplementing this function in
|
||||
// a subclass the column of the returned QModelIndex would be 0.
|
||||
return createIndex(row, 0, parentItem.get()); |
||||
} |
||||
|
||||
void TrackerListModel::onTrackersAdded(const QList<BitTorrent::TrackerEntry> &newTrackers) |
||||
{ |
||||
const auto row = rowCount(); |
||||
beginInsertRows({}, row, (row + newTrackers.size() - 1)); |
||||
for (const BitTorrent::TrackerEntry &trackerEntry : newTrackers) |
||||
addTrackerItem(trackerEntry); |
||||
endInsertRows(); |
||||
} |
||||
|
||||
void TrackerListModel::onTrackersRemoved(const QStringList &deletedTrackers) |
||||
{ |
||||
for (const QString &trackerURL : deletedTrackers) |
||||
{ |
||||
auto &itemsByName = m_items->get<ByName>(); |
||||
if (auto iter = itemsByName.find(trackerURL); iter != itemsByName.end()) |
||||
{ |
||||
const auto &iterByPos = m_items->project<0>(iter); |
||||
const auto row = std::distance(m_items->get<0>().begin(), iterByPos); |
||||
beginRemoveRows({}, row, row); |
||||
itemsByName.erase(iter); |
||||
endRemoveRows(); |
||||
} |
||||
} |
||||
} |
||||
|
||||
void TrackerListModel::onTrackersChanged() |
||||
{ |
||||
QSet<QString> trackerItemIDs; |
||||
for (int i = 0; i < STICKY_ROW_COUNT; ++i) |
||||
trackerItemIDs.insert(m_items->at(i)->name); |
||||
|
||||
QList<std::shared_ptr<Item>> newTrackerItems; |
||||
for (const BitTorrent::TrackerEntry &trackerEntry : m_torrent->trackers()) |
||||
{ |
||||
trackerItemIDs.insert(trackerEntry.url); |
||||
|
||||
auto &itemsByName = m_items->get<ByName>(); |
||||
if (const auto &iter = itemsByName.find(trackerEntry.url); iter != itemsByName.end()) |
||||
{ |
||||
updateTrackerItem(*iter, trackerEntry); |
||||
} |
||||
else |
||||
{ |
||||
newTrackerItems.emplace_back(createTrackerItem(trackerEntry)); |
||||
} |
||||
} |
||||
|
||||
auto it = m_items->begin(); |
||||
while (it != m_items->end()) |
||||
{ |
||||
if (trackerItemIDs.contains((*it)->name)) |
||||
{ |
||||
++it; |
||||
} |
||||
else |
||||
{ |
||||
const auto row = std::distance(m_items->begin(), it); |
||||
beginRemoveRows({}, row, row); |
||||
it = m_items->erase(it); |
||||
endRemoveRows(); |
||||
} |
||||
} |
||||
|
||||
if (!newTrackerItems.isEmpty()) |
||||
{ |
||||
const auto numRows = rowCount(); |
||||
beginInsertRows({}, numRows, (numRows + newTrackerItems.size() - 1)); |
||||
for (const auto &newTrackerItem : asConst(newTrackerItems)) |
||||
m_items->get<0>().push_back(newTrackerItem); |
||||
endInsertRows(); |
||||
} |
||||
} |
||||
|
||||
void TrackerListModel::onTrackersUpdated(const QHash<QString, BitTorrent::TrackerEntry> &updatedTrackers) |
||||
{ |
||||
for (const auto &[url, entry] : updatedTrackers.asKeyValueRange()) |
||||
{ |
||||
auto &itemsByName = m_items->get<ByName>(); |
||||
if (const auto &iter = itemsByName.find(entry.url); iter != itemsByName.end()) [[likely]] |
||||
{ |
||||
updateTrackerItem(*iter, entry); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,119 @@
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Bittorrent Client using Qt and libtorrent. |
||||
* Copyright (C) 2023 Vladimir Golovnev <glassez@yandex.ru> |
||||
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org> |
||||
* |
||||
* This program is free software; you can redistribute it and/or |
||||
* modify it under the terms of the GNU General Public License |
||||
* as published by the Free Software Foundation; either version 2 |
||||
* of the License, or (at your option) any later version. |
||||
* |
||||
* This program is distributed in the hope that it will be useful, |
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
* GNU General Public License for more details. |
||||
* |
||||
* You should have received a copy of the GNU General Public License |
||||
* along with this program; if not, write to the Free Software |
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
* |
||||
* In addition, as a special exception, the copyright holders give permission to |
||||
* link this program with the OpenSSL project's "OpenSSL" library (or with |
||||
* modified versions of it that use the same license as the "OpenSSL" library), |
||||
* and distribute the linked executables. You must obey the GNU General Public |
||||
* License in all respects for all of the code used other than "OpenSSL". If you |
||||
* modify file(s), you may extend this exception to your version of the file(s), |
||||
* but you are not obligated to do so. If you do not wish to do so, delete this |
||||
* exception statement from your version. |
||||
*/ |
||||
|
||||
#pragma once |
||||
|
||||
#include <memory> |
||||
|
||||
#include <QtContainerFwd> |
||||
#include <QAbstractItemModel> |
||||
#include <QDateTime> |
||||
|
||||
#include "base/bittorrent/trackerentry.h" |
||||
|
||||
class QTimer; |
||||
|
||||
namespace BitTorrent |
||||
{ |
||||
class Session; |
||||
class Torrent; |
||||
} |
||||
|
||||
class TrackerListModel final : public QAbstractItemModel |
||||
{ |
||||
Q_OBJECT |
||||
Q_DISABLE_COPY_MOVE(TrackerListModel) |
||||
|
||||
public: |
||||
enum TrackerListColumn |
||||
{ |
||||
COL_URL, |
||||
COL_TIER, |
||||
COL_PROTOCOL, |
||||
COL_STATUS, |
||||
COL_PEERS, |
||||
COL_SEEDS, |
||||
COL_LEECHES, |
||||
COL_TIMES_DOWNLOADED, |
||||
COL_MSG, |
||||
COL_NEXT_ANNOUNCE, |
||||
COL_MIN_ANNOUNCE, |
||||
|
||||
COL_COUNT |
||||
}; |
||||
|
||||
enum StickyRow |
||||
{ |
||||
ROW_DHT = 0, |
||||
ROW_PEX = 1, |
||||
ROW_LSD = 2, |
||||
|
||||
STICKY_ROW_COUNT |
||||
}; |
||||
|
||||
enum Roles |
||||
{ |
||||
SortRole = Qt::UserRole |
||||
}; |
||||
|
||||
explicit TrackerListModel(BitTorrent::Session *btSession, QObject *parent = nullptr); |
||||
~TrackerListModel() override; |
||||
|
||||
void setTorrent(BitTorrent::Torrent *torrent); |
||||
BitTorrent::Torrent *torrent() const; |
||||
|
||||
int columnCount(const QModelIndex &parent = {}) const override; |
||||
int rowCount(const QModelIndex &parent = {}) const override; |
||||
QVariant headerData(int section, Qt::Orientation orientation, int role) const override; |
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; |
||||
QModelIndex index(int row, int column, const QModelIndex &parent = {}) const override; |
||||
QModelIndex parent(const QModelIndex &index) const override; |
||||
|
||||
private: |
||||
struct Item; |
||||
|
||||
void populate(); |
||||
std::shared_ptr<Item> createTrackerItem(const BitTorrent::TrackerEntry &trackerEntry); |
||||
void addTrackerItem(const BitTorrent::TrackerEntry &trackerEntry); |
||||
void updateTrackerItem(const std::shared_ptr<Item> &item, const BitTorrent::TrackerEntry &trackerEntry); |
||||
void refreshAnnounceTimes(); |
||||
void onTrackersAdded(const QList<BitTorrent::TrackerEntry> &newTrackers); |
||||
void onTrackersRemoved(const QStringList &deletedTrackers); |
||||
void onTrackersChanged(); |
||||
void onTrackersUpdated(const QHash<QString, BitTorrent::TrackerEntry> &updatedTrackers); |
||||
|
||||
BitTorrent::Session *m_btSession = nullptr; |
||||
BitTorrent::Torrent *m_torrent = nullptr; |
||||
|
||||
class Items; |
||||
std::unique_ptr<Items> m_items; |
||||
|
||||
QDateTime m_announceTimestamp; |
||||
QTimer *m_announceRefreshTimer = nullptr; |
||||
}; |
@ -0,0 +1,56 @@
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Bittorrent Client using Qt and libtorrent. |
||||
* Copyright (C) 2023 Vladimir Golovnev <glassez@yandex.ru> |
||||
* |
||||
* This program is free software; you can redistribute it and/or |
||||
* modify it under the terms of the GNU General Public License |
||||
* as published by the Free Software Foundation; either version 2 |
||||
* of the License, or (at your option) any later version. |
||||
* |
||||
* This program is distributed in the hope that it will be useful, |
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
* GNU General Public License for more details. |
||||
* |
||||
* You should have received a copy of the GNU General Public License |
||||
* along with this program; if not, write to the Free Software |
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
* |
||||
* In addition, as a special exception, the copyright holders give permission to |
||||
* link this program with the OpenSSL project's "OpenSSL" library (or with |
||||
* modified versions of it that use the same license as the "OpenSSL" library), |
||||
* and distribute the linked executables. You must obey the GNU General Public |
||||
* License in all respects for all of the code used other than "OpenSSL". If you |
||||
* modify file(s), you may extend this exception to your version of the file(s), |
||||
* but you are not obligated to do so. If you do not wish to do so, delete this |
||||
* exception statement from your version. |
||||
*/ |
||||
|
||||
#include "trackerlistsortmodel.h" |
||||
|
||||
#include "trackerlistmodel.h" |
||||
|
||||
TrackerListSortModel::TrackerListSortModel(TrackerListModel *model, QObject *parent) |
||||
: QSortFilterProxyModel(parent) |
||||
{ |
||||
QSortFilterProxyModel::setSourceModel(model); |
||||
setDynamicSortFilter(true); |
||||
setSortCaseSensitivity(Qt::CaseInsensitive); |
||||
setSortRole(TrackerListModel::SortRole); |
||||
} |
||||
|
||||
void TrackerListSortModel::setSourceModel(TrackerListModel *model) |
||||
{ |
||||
QSortFilterProxyModel::setSourceModel(model); |
||||
} |
||||
|
||||
bool TrackerListSortModel::lessThan(const QModelIndex &left, const QModelIndex &right) const |
||||
{ |
||||
if (!left.parent().isValid() && !right.parent().isValid()) |
||||
{ |
||||
if ((left.row() < TrackerListModel::STICKY_ROW_COUNT) || (right.row() < TrackerListModel::STICKY_ROW_COUNT)) |
||||
return ((left.row() < right.row()) && (sortOrder() == Qt::AscendingOrder)); |
||||
} |
||||
|
||||
return QSortFilterProxyModel::lessThan(left, right); |
||||
} |
@ -0,0 +1,48 @@
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Bittorrent Client using Qt and libtorrent. |
||||
* Copyright (C) 2023 Vladimir Golovnev <glassez@yandex.ru> |
||||
* |
||||
* This program is free software; you can redistribute it and/or |
||||
* modify it under the terms of the GNU General Public License |
||||
* as published by the Free Software Foundation; either version 2 |
||||
* of the License, or (at your option) any later version. |
||||
* |
||||
* This program is distributed in the hope that it will be useful, |
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
* GNU General Public License for more details. |
||||
* |
||||
* You should have received a copy of the GNU General Public License |
||||
* along with this program; if not, write to the Free Software |
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
* |
||||
* In addition, as a special exception, the copyright holders give permission to |
||||
* link this program with the OpenSSL project's "OpenSSL" library (or with |
||||
* modified versions of it that use the same license as the "OpenSSL" library), |
||||
* and distribute the linked executables. You must obey the GNU General Public |
||||
* License in all respects for all of the code used other than "OpenSSL". If you |
||||
* modify file(s), you may extend this exception to your version of the file(s), |
||||
* but you are not obligated to do so. If you do not wish to do so, delete this |
||||
* exception statement from your version. |
||||
*/ |
||||
|
||||
#pragma once |
||||
|
||||
#include <QSortFilterProxyModel> |
||||
|
||||
class TrackerListModel; |
||||
|
||||
class TrackerListSortModel final : public QSortFilterProxyModel |
||||
{ |
||||
Q_OBJECT |
||||
Q_DISABLE_COPY_MOVE(TrackerListSortModel) |
||||
|
||||
public: |
||||
explicit TrackerListSortModel(TrackerListModel *model, QObject *parent = nullptr); |
||||
|
||||
void setSourceModel(TrackerListModel *model); |
||||
|
||||
private: |
||||
using QSortFilterProxyModel::setSourceModel; |
||||
bool lessThan(const QModelIndex &left, const QModelIndex &right) const override; |
||||
}; |
@ -0,0 +1,452 @@
@@ -0,0 +1,452 @@
|
||||
/*
|
||||
* Bittorrent Client using Qt and libtorrent. |
||||
* Copyright (C) 2023 Vladimir Golovnev <glassez@yandex.ru> |
||||
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org> |
||||
* |
||||
* This program is free software; you can redistribute it and/or |
||||
* modify it under the terms of the GNU General Public License |
||||
* as published by the Free Software Foundation; either version 2 |
||||
* of the License, or (at your option) any later version. |
||||
* |
||||
* This program is distributed in the hope that it will be useful, |
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
* GNU General Public License for more details. |
||||
* |
||||
* You should have received a copy of the GNU General Public License |
||||
* along with this program; if not, write to the Free Software |
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
* |
||||
* In addition, as a special exception, the copyright holders give permission to |
||||
* link this program with the OpenSSL project's "OpenSSL" library (or with |
||||
* modified versions of it that use the same license as the "OpenSSL" library), |
||||
* and distribute the linked executables. You must obey the GNU General Public |
||||
* License in all respects for all of the code used other than "OpenSSL". If you |
||||
* modify file(s), you may extend this exception to your version of the file(s), |
||||
* but you are not obligated to do so. If you do not wish to do so, delete this |
||||
* exception statement from your version. |
||||
*/ |
||||
|
||||
#include "trackerlistwidget.h" |
||||
|
||||
#include <QAction> |
||||
#include <QApplication> |
||||
#include <QClipboard> |
||||
#include <QColor> |
||||
#include <QDebug> |
||||
#include <QHeaderView> |
||||
#include <QLocale> |
||||
#include <QMenu> |
||||
#include <QMessageBox> |
||||
#include <QShortcut> |
||||
#include <QStringList> |
||||
#include <QTreeWidgetItem> |
||||
#include <QUrl> |
||||
#include <QVector> |
||||
#include <QWheelEvent> |
||||
|
||||
#include "base/bittorrent/session.h" |
||||
#include "base/bittorrent/torrent.h" |
||||
#include "base/bittorrent/trackerentry.h" |
||||
#include "base/global.h" |
||||
#include "base/preferences.h" |
||||
#include "gui/autoexpandabledialog.h" |
||||
#include "gui/trackersadditiondialog.h" |
||||
#include "gui/uithememanager.h" |
||||
#include "trackerlistitemdelegate.h" |
||||
#include "trackerlistmodel.h" |
||||
#include "trackerlistsortmodel.h" |
||||
|
||||
TrackerListWidget::TrackerListWidget(QWidget *parent) |
||||
: QTreeView(parent) |
||||
{ |
||||
#ifdef QBT_USES_LIBTORRENT2 |
||||
setColumnHidden(TrackerListModel::COL_PROTOCOL, true); // Must be set before calling loadSettings()
|
||||
#endif |
||||
|
||||
setExpandsOnDoubleClick(false); |
||||
setAllColumnsShowFocus(true); |
||||
setSelectionMode(QAbstractItemView::ExtendedSelection); |
||||
setSortingEnabled(true); |
||||
setUniformRowHeights(true); |
||||
setContextMenuPolicy(Qt::CustomContextMenu); |
||||
|
||||
header()->setSortIndicator(0, Qt::AscendingOrder); |
||||
header()->setFirstSectionMovable(true); |
||||
header()->setStretchLastSection(false); // Must be set after loadSettings() in order to work
|
||||
header()->setTextElideMode(Qt::ElideRight); |
||||
header()->setContextMenuPolicy(Qt::CustomContextMenu); |
||||
|
||||
m_model = new TrackerListModel(BitTorrent::Session::instance(), this); |
||||
auto *sortModel = new TrackerListSortModel(m_model, this); |
||||
QTreeView::setModel(sortModel); |
||||
|
||||
setItemDelegate(new TrackerListItemDelegate(this)); |
||||
|
||||
loadSettings(); |
||||
|
||||
// Ensure that at least one column is visible at all times
|
||||
if (visibleColumnsCount() == 0) |
||||
setColumnHidden(TrackerListModel::COL_URL, false); |
||||
// 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 (int i = 0; i < TrackerListModel::COL_COUNT; ++i) |
||||
{ |
||||
if ((columnWidth(i) <= 0) && !isColumnHidden(i)) |
||||
resizeColumnToContents(i); |
||||
} |
||||
|
||||
connect(this, &QWidget::customContextMenuRequested, this, &TrackerListWidget::showTrackerListMenu); |
||||
connect(header(), &QWidget::customContextMenuRequested, this, &TrackerListWidget::displayColumnHeaderMenu); |
||||
connect(header(), &QHeaderView::sectionMoved, this, &TrackerListWidget::saveSettings); |
||||
connect(header(), &QHeaderView::sectionResized, this, &TrackerListWidget::saveSettings); |
||||
connect(header(), &QHeaderView::sortIndicatorChanged, this, &TrackerListWidget::saveSettings); |
||||
|
||||
// Set hotkeys
|
||||
const auto *editHotkey = new QShortcut(Qt::Key_F2, this, nullptr, nullptr, Qt::WidgetShortcut); |
||||
connect(editHotkey, &QShortcut::activated, this, &TrackerListWidget::editSelectedTracker); |
||||
const auto *deleteHotkey = new QShortcut(QKeySequence::Delete, this, nullptr, nullptr, Qt::WidgetShortcut); |
||||
connect(deleteHotkey, &QShortcut::activated, this, &TrackerListWidget::deleteSelectedTrackers); |
||||
const auto *copyHotkey = new QShortcut(QKeySequence::Copy, this, nullptr, nullptr, Qt::WidgetShortcut); |
||||
connect(copyHotkey, &QShortcut::activated, this, &TrackerListWidget::copyTrackerUrl); |
||||
|
||||
connect(this, &QAbstractItemView::doubleClicked, this, &TrackerListWidget::editSelectedTracker); |
||||
} |
||||
|
||||
TrackerListWidget::~TrackerListWidget() |
||||
{ |
||||
saveSettings(); |
||||
} |
||||
|
||||
void TrackerListWidget::setTorrent(BitTorrent::Torrent *torrent) |
||||
{ |
||||
m_model->setTorrent(torrent); |
||||
} |
||||
|
||||
BitTorrent::Torrent *TrackerListWidget::torrent() const |
||||
{ |
||||
return m_model->torrent(); |
||||
} |
||||
|
||||
QModelIndexList TrackerListWidget::getSelectedTrackerRows() const |
||||
{ |
||||
QModelIndexList selectedItemIndexes = selectionModel()->selectedRows(); |
||||
selectedItemIndexes.removeIf([](const QModelIndex &index) |
||||
{ |
||||
return (index.parent().isValid() || (index.row() < TrackerListModel::STICKY_ROW_COUNT)); |
||||
}); |
||||
|
||||
return selectedItemIndexes; |
||||
} |
||||
|
||||
void TrackerListWidget::decreaseSelectedTrackerTiers() |
||||
{ |
||||
const auto &trackerIndexes = getSelectedTrackerRows(); |
||||
if (trackerIndexes.isEmpty()) |
||||
return; |
||||
|
||||
QSet<QString> trackerURLs; |
||||
for (const QModelIndex &index : trackerIndexes) |
||||
{ |
||||
trackerURLs.insert(index.siblingAtColumn(TrackerListModel::COL_URL).data().toString()); |
||||
} |
||||
|
||||
QList<BitTorrent::TrackerEntry> trackers = m_model->torrent()->trackers(); |
||||
for (BitTorrent::TrackerEntry &trackerEntry : trackers) |
||||
{ |
||||
if (trackerURLs.contains(trackerEntry.url)) |
||||
{ |
||||
if (trackerEntry.tier > 0) |
||||
--trackerEntry.tier; |
||||
} |
||||
} |
||||
|
||||
m_model->torrent()->replaceTrackers(trackers); |
||||
} |
||||
|
||||
void TrackerListWidget::increaseSelectedTrackerTiers() |
||||
{ |
||||
const auto &trackerIndexes = getSelectedTrackerRows(); |
||||
if (trackerIndexes.isEmpty()) |
||||
return; |
||||
|
||||
QSet<QString> trackerURLs; |
||||
for (const QModelIndex &index : trackerIndexes) |
||||
{ |
||||
trackerURLs.insert(index.siblingAtColumn(TrackerListModel::COL_URL).data().toString()); |
||||
} |
||||
|
||||
QList<BitTorrent::TrackerEntry> trackers = m_model->torrent()->trackers(); |
||||
for (BitTorrent::TrackerEntry &trackerEntry : trackers) |
||||
{ |
||||
if (trackerURLs.contains(trackerEntry.url)) |
||||
{ |
||||
if (trackerEntry.tier < std::numeric_limits<decltype(trackerEntry.tier)>::max()) |
||||
++trackerEntry.tier; |
||||
} |
||||
} |
||||
|
||||
m_model->torrent()->replaceTrackers(trackers); |
||||
} |
||||
|
||||
void TrackerListWidget::openAddTrackersDialog() |
||||
{ |
||||
if (!torrent()) |
||||
return; |
||||
|
||||
auto *dialog = new TrackersAdditionDialog(this, torrent()); |
||||
dialog->setAttribute(Qt::WA_DeleteOnClose); |
||||
dialog->open(); |
||||
} |
||||
|
||||
void TrackerListWidget::copyTrackerUrl() |
||||
{ |
||||
if (!torrent()) |
||||
return; |
||||
|
||||
const auto &selectedTrackerIndexes = getSelectedTrackerRows(); |
||||
if (selectedTrackerIndexes.isEmpty()) |
||||
return; |
||||
|
||||
QStringList urlsToCopy; |
||||
for (const QModelIndex &index : selectedTrackerIndexes) |
||||
{ |
||||
const QString &trackerURL = index.siblingAtColumn(TrackerListModel::COL_URL).data().toString(); |
||||
qDebug() << "Copy:" << qUtf8Printable(trackerURL); |
||||
urlsToCopy.append(trackerURL); |
||||
} |
||||
|
||||
QApplication::clipboard()->setText(urlsToCopy.join(u'\n')); |
||||
} |
||||
|
||||
|
||||
void TrackerListWidget::deleteSelectedTrackers() |
||||
{ |
||||
if (!torrent()) |
||||
return; |
||||
|
||||
const auto &selectedTrackerIndexes = getSelectedTrackerRows(); |
||||
if (selectedTrackerIndexes.isEmpty()) |
||||
return; |
||||
|
||||
QStringList urlsToRemove; |
||||
for (const QModelIndex &index : selectedTrackerIndexes) |
||||
{ |
||||
const QString trackerURL = index.siblingAtColumn(TrackerListModel::COL_URL).data().toString(); |
||||
urlsToRemove.append(trackerURL); |
||||
} |
||||
|
||||
torrent()->removeTrackers(urlsToRemove); |
||||
} |
||||
|
||||
void TrackerListWidget::editSelectedTracker() |
||||
{ |
||||
if (!torrent()) |
||||
return; |
||||
|
||||
const auto &selectedTrackerIndexes = getSelectedTrackerRows(); |
||||
if (selectedTrackerIndexes.isEmpty()) |
||||
return; |
||||
|
||||
// During multi-select only process item selected last
|
||||
const QUrl trackerURL = selectedTrackerIndexes.last().siblingAtColumn(TrackerListModel::COL_URL).data().toString(); |
||||
|
||||
bool ok = false; |
||||
const QUrl newTrackerURL = AutoExpandableDialog::getText(this |
||||
, tr("Tracker editing"), tr("Tracker URL:") |
||||
, QLineEdit::Normal, trackerURL.toString(), &ok).trimmed(); |
||||
if (!ok) |
||||
return; |
||||
|
||||
if (!newTrackerURL.isValid()) |
||||
{ |
||||
QMessageBox::warning(this, tr("Tracker editing failed"), tr("The tracker URL entered is invalid.")); |
||||
return; |
||||
} |
||||
|
||||
if (newTrackerURL == trackerURL) |
||||
return; |
||||
|
||||
QList<BitTorrent::TrackerEntry> trackers = torrent()->trackers(); |
||||
bool match = false; |
||||
for (BitTorrent::TrackerEntry &entry : trackers) |
||||
{ |
||||
if (newTrackerURL == QUrl(entry.url)) |
||||
{ |
||||
QMessageBox::warning(this, tr("Tracker editing failed"), tr("The tracker URL already exists.")); |
||||
return; |
||||
} |
||||
|
||||
if (!match && (trackerURL == QUrl(entry.url))) |
||||
{ |
||||
match = true; |
||||
entry.url = newTrackerURL.toString(); |
||||
} |
||||
} |
||||
|
||||
torrent()->replaceTrackers(trackers); |
||||
} |
||||
|
||||
void TrackerListWidget::reannounceSelected() |
||||
{ |
||||
if (!torrent()) |
||||
return; |
||||
|
||||
const auto &selectedItemIndexes = selectedIndexes(); |
||||
if (selectedItemIndexes.isEmpty()) |
||||
return; |
||||
|
||||
QSet<QString> trackerURLs; |
||||
for (const QModelIndex &index : selectedItemIndexes) |
||||
{ |
||||
if (index.parent().isValid()) |
||||
continue; |
||||
|
||||
if ((index.row() < TrackerListModel::STICKY_ROW_COUNT)) |
||||
{ |
||||
// DHT case
|
||||
if (index.row() == TrackerListModel::ROW_DHT) |
||||
torrent()->forceDHTAnnounce(); |
||||
|
||||
continue; |
||||
} |
||||
|
||||
trackerURLs.insert(index.siblingAtColumn(TrackerListModel::COL_URL).data().toString()); |
||||
} |
||||
|
||||
const QList<BitTorrent::TrackerEntry> &trackers = m_model->torrent()->trackers(); |
||||
for (qsizetype i = 0; i < trackers.size(); ++i) |
||||
{ |
||||
const BitTorrent::TrackerEntry &trackerEntry = trackers.at(i); |
||||
if (trackerURLs.contains(trackerEntry.url)) |
||||
{ |
||||
torrent()->forceReannounce(i); |
||||
} |
||||
} |
||||
} |
||||
|
||||
void TrackerListWidget::showTrackerListMenu() |
||||
{ |
||||
if (!torrent()) |
||||
return; |
||||
|
||||
QMenu *menu = new QMenu(this); |
||||
menu->setAttribute(Qt::WA_DeleteOnClose); |
||||
|
||||
// Add actions
|
||||
menu->addAction(UIThemeManager::instance()->getIcon(u"list-add"_s), tr("Add trackers...") |
||||
, this, &TrackerListWidget::openAddTrackersDialog); |
||||
|
||||
if (!getSelectedTrackerRows().isEmpty()) |
||||
{ |
||||
menu->addAction(UIThemeManager::instance()->getIcon(u"edit-rename"_s),tr("Edit tracker URL...") |
||||
, this, &TrackerListWidget::editSelectedTracker); |
||||
menu->addAction(UIThemeManager::instance()->getIcon(u"edit-clear"_s, u"list-remove"_s), tr("Remove tracker") |
||||
, this, &TrackerListWidget::deleteSelectedTrackers); |
||||
menu->addAction(UIThemeManager::instance()->getIcon(u"edit-copy"_s), tr("Copy tracker URL") |
||||
, this, &TrackerListWidget::copyTrackerUrl); |
||||
if (!torrent()->isPaused()) |
||||
{ |
||||
menu->addAction(UIThemeManager::instance()->getIcon(u"reannounce"_s, u"view-refresh"_s), tr("Force reannounce to selected trackers") |
||||
, this, &TrackerListWidget::reannounceSelected); |
||||
} |
||||
} |
||||
|
||||
if (!torrent()->isPaused()) |
||||
{ |
||||
menu->addSeparator(); |
||||
menu->addAction(UIThemeManager::instance()->getIcon(u"reannounce"_s, u"view-refresh"_s), tr("Force reannounce to all trackers") |
||||
, this, [this]() |
||||
{ |
||||
torrent()->forceReannounce(); |
||||
torrent()->forceDHTAnnounce(); |
||||
}); |
||||
} |
||||
|
||||
menu->popup(QCursor::pos()); |
||||
} |
||||
|
||||
void TrackerListWidget::setModel([[maybe_unused]] QAbstractItemModel *model) |
||||
{ |
||||
Q_ASSERT_X(false, Q_FUNC_INFO, "Changing the model of TrackerListWidget is not allowed."); |
||||
} |
||||
|
||||
void TrackerListWidget::loadSettings() |
||||
{ |
||||
header()->restoreState(Preferences::instance()->getTrackerListState()); |
||||
} |
||||
|
||||
void TrackerListWidget::saveSettings() const |
||||
{ |
||||
Preferences::instance()->setTrackerListState(header()->saveState()); |
||||
} |
||||
|
||||
int TrackerListWidget::visibleColumnsCount() const |
||||
{ |
||||
int count = 0; |
||||
for (int i = 0, iMax = header()->count(); i < iMax; ++i) |
||||
{ |
||||
if (!isColumnHidden(i)) |
||||
++count; |
||||
} |
||||
|
||||
return count; |
||||
} |
||||
|
||||
void TrackerListWidget::displayColumnHeaderMenu() |
||||
{ |
||||
QMenu *menu = new QMenu(this); |
||||
menu->setAttribute(Qt::WA_DeleteOnClose); |
||||
menu->setTitle(tr("Column visibility")); |
||||
menu->setToolTipsVisible(true); |
||||
|
||||
for (int i = 0; i < TrackerListModel::COL_COUNT; ++i) |
||||
{ |
||||
QAction *action = menu->addAction(model()->headerData(i, Qt::Horizontal).toString(), this |
||||
, [this, i](const bool checked) |
||||
{ |
||||
if (!checked && (visibleColumnsCount() <= 1)) |
||||
return; |
||||
|
||||
setColumnHidden(i, !checked); |
||||
|
||||
if (checked && (columnWidth(i) <= 5)) |
||||
resizeColumnToContents(i); |
||||
|
||||
saveSettings(); |
||||
}); |
||||
action->setCheckable(true); |
||||
action->setChecked(!isColumnHidden(i)); |
||||
} |
||||
|
||||
menu->addSeparator(); |
||||
QAction *resizeAction = menu->addAction(tr("Resize columns"), this, [this]() |
||||
{ |
||||
for (int i = 0, count = header()->count(); i < count; ++i) |
||||
{ |
||||
if (!isColumnHidden(i)) |
||||
resizeColumnToContents(i); |
||||
} |
||||
saveSettings(); |
||||
}); |
||||
resizeAction->setToolTip(tr("Resize all non-hidden columns to the size of their contents")); |
||||
|
||||
menu->popup(QCursor::pos()); |
||||
} |
||||
|
||||
void TrackerListWidget::wheelEvent(QWheelEvent *event) |
||||
{ |
||||
if (event->modifiers() & Qt::ShiftModifier) |
||||
{ |
||||
// Shift + scroll = horizontal scroll
|
||||
event->accept(); |
||||
QWheelEvent scrollHEvent {event->position(), event->globalPosition() |
||||
, event->pixelDelta(), event->angleDelta().transposed(), event->buttons() |
||||
, event->modifiers(), event->phase(), event->inverted(), event->source()}; |
||||
QTreeView::wheelEvent(&scrollHEvent); |
||||
return; |
||||
} |
||||
|
||||
QTreeView::wheelEvent(event); // event delegated to base class
|
||||
} |
Loading…
Reference in new issue