From c051ee9409a942145bfb38ab8d692786d6750ae3 Mon Sep 17 00:00:00 2001 From: Vladimir Golovnev Date: Tue, 3 Oct 2023 08:42:05 +0300 Subject: [PATCH] Revamp tracker list widget Internally redesign tracker list widget using Qt Model/View architecture. Make tracker list sortable by any column. PR #19633. Closes #261. --- src/base/bittorrent/sessionimpl.cpp | 4 +- src/base/bittorrent/sessionimpl.h | 4 +- src/base/bittorrent/torrent.h | 5 +- src/base/bittorrent/torrentimpl.cpp | 222 +++-- src/base/bittorrent/torrentimpl.h | 4 +- src/base/bittorrent/trackerentry.cpp | 7 +- src/base/bittorrent/trackerentry.h | 66 +- src/base/preferences.cpp | 6 +- src/base/preferences.h | 4 +- src/base/utils/misc.h | 2 +- src/gui/CMakeLists.txt | 16 +- src/gui/mainwindow.cpp | 3 +- src/gui/properties/propertieswidget.cpp | 18 +- src/gui/properties/propertieswidget.h | 1 - src/gui/properties/trackerlistwidget.cpp | 813 ------------------ .../trackerlist/trackerlistitemdelegate.cpp | 65 ++ src/gui/trackerlist/trackerlistitemdelegate.h | 49 ++ src/gui/trackerlist/trackerlistmodel.cpp | 781 +++++++++++++++++ src/gui/trackerlist/trackerlistmodel.h | 119 +++ src/gui/trackerlist/trackerlistsortmodel.cpp | 56 ++ src/gui/trackerlist/trackerlistsortmodel.h | 48 ++ src/gui/trackerlist/trackerlistwidget.cpp | 452 ++++++++++ .../trackerlistwidget.h | 62 +- .../trackersadditiondialog.cpp | 0 .../{properties => }/trackersadditiondialog.h | 0 .../trackersadditiondialog.ui | 0 .../categoryfiltermodel.cpp | 4 +- .../trackersfilterwidget.cpp | 17 +- src/webui/api/synccontroller.cpp | 2 + src/webui/api/torrentscontroller.cpp | 58 +- 30 files changed, 1784 insertions(+), 1104 deletions(-) delete mode 100644 src/gui/properties/trackerlistwidget.cpp create mode 100644 src/gui/trackerlist/trackerlistitemdelegate.cpp create mode 100644 src/gui/trackerlist/trackerlistitemdelegate.h create mode 100644 src/gui/trackerlist/trackerlistmodel.cpp create mode 100644 src/gui/trackerlist/trackerlistmodel.h create mode 100644 src/gui/trackerlist/trackerlistsortmodel.cpp create mode 100644 src/gui/trackerlist/trackerlistsortmodel.h create mode 100644 src/gui/trackerlist/trackerlistwidget.cpp rename src/gui/{properties => trackerlist}/trackerlistwidget.h (65%) rename src/gui/{properties => }/trackersadditiondialog.cpp (100%) rename src/gui/{properties => }/trackersadditiondialog.h (100%) rename src/gui/{properties => }/trackersadditiondialog.ui (100%) diff --git a/src/base/bittorrent/sessionimpl.cpp b/src/base/bittorrent/sessionimpl.cpp index 6b8d91ca2..c7e38eb37 100644 --- a/src/base/bittorrent/sessionimpl.cpp +++ b/src/base/bittorrent/sessionimpl.cpp @@ -4815,7 +4815,6 @@ void SessionImpl::handleTorrentTrackersAdded(TorrentImpl *const torrent, const Q for (const TrackerEntry &newTracker : newTrackers) LogMsg(tr("Added tracker to torrent. Torrent: \"%1\". Tracker: \"%2\"").arg(torrent->name(), newTracker.url)); emit trackersAdded(torrent, newTrackers); - emit trackersChanged(torrent); } void SessionImpl::handleTorrentTrackersRemoved(TorrentImpl *const torrent, const QStringList &deletedTrackers) @@ -4823,7 +4822,6 @@ void SessionImpl::handleTorrentTrackersRemoved(TorrentImpl *const torrent, const for (const QString &deletedTracker : deletedTrackers) LogMsg(tr("Removed tracker from torrent. Torrent: \"%1\". Tracker: \"%2\"").arg(torrent->name(), deletedTracker)); emit trackersRemoved(torrent, deletedTrackers); - emit trackersChanged(torrent); } void SessionImpl::handleTorrentTrackersChanged(TorrentImpl *const torrent) @@ -6057,7 +6055,7 @@ void SessionImpl::loadStatistics() m_previouslyUploaded = value[u"AlltimeUL"_s].toLongLong(); } -void SessionImpl::updateTrackerEntries(lt::torrent_handle torrentHandle, QHash>> updatedTrackers) +void SessionImpl::updateTrackerEntries(lt::torrent_handle torrentHandle, QHash>> updatedTrackers) { invokeAsync([this, torrentHandle = std::move(torrentHandle), updatedTrackers = std::move(updatedTrackers)]() mutable { diff --git a/src/base/bittorrent/sessionimpl.h b/src/base/bittorrent/sessionimpl.h index 51f772182..01374542c 100644 --- a/src/base/bittorrent/sessionimpl.h +++ b/src/base/bittorrent/sessionimpl.h @@ -576,7 +576,7 @@ namespace BitTorrent void saveStatistics() const; void loadStatistics(); - void updateTrackerEntries(lt::torrent_handle torrentHandle, QHash>> updatedTrackers); + void updateTrackerEntries(lt::torrent_handle torrentHandle, QHash>> updatedTrackers); // BitTorrent lt::session *m_nativeSession = nullptr; @@ -753,7 +753,7 @@ namespace BitTorrent // This field holds amounts of peers reported by trackers in their responses to announces // (torrent.tracker_name.tracker_local_endpoint.protocol_version.num_peers) - QHash>>> m_updatedTrackerEntries; + QHash>>> m_updatedTrackerEntries; // I/O errored torrents QSet m_recentErroredTorrents; diff --git a/src/base/bittorrent/torrent.h b/src/base/bittorrent/torrent.h index d7e690d67..e7cf4cde5 100644 --- a/src/base/bittorrent/torrent.h +++ b/src/base/bittorrent/torrent.h @@ -1,6 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2015-2022 Vladimir Golovnev + * Copyright (C) 2015-2023 Vladimir Golovnev * Copyright (C) 2006 Christophe Dumez * * This program is free software; you can redistribute it and/or @@ -49,6 +49,7 @@ namespace BitTorrent enum class DownloadPriority; class InfoHash; class PeerInfo; + class Session; class TorrentID; class TorrentInfo; struct PeerAddress; @@ -131,6 +132,8 @@ namespace BitTorrent using TorrentContentHandler::TorrentContentHandler; + virtual Session *session() const = 0; + virtual InfoHash infoHash() const = 0; virtual QString name() const = 0; virtual QDateTime creationDate() const = 0; diff --git a/src/base/bittorrent/torrentimpl.cpp b/src/base/bittorrent/torrentimpl.cpp index 57ccb5c41..61987b92f 100644 --- a/src/base/bittorrent/torrentimpl.cpp +++ b/src/base/bittorrent/torrentimpl.cpp @@ -87,190 +87,167 @@ namespace return qNow.addSecs(secsSinceNow); } -#ifdef QBT_USES_LIBTORRENT2 - void updateTrackerEntry(TrackerEntry &trackerEntry, const lt::announce_entry &nativeEntry - , const lt::info_hash_t &hashes, const QHash> &updateInfo) -#else + QString toString(const lt::tcp::endpoint <TCPEndpoint) + { + return QString::fromStdString((std::stringstream() << ltTCPEndpoint).str()); + } + void updateTrackerEntry(TrackerEntry &trackerEntry, const lt::announce_entry &nativeEntry - , const QHash> &updateInfo) -#endif + , const QSet &btProtocols, const QHash> &updateInfo) { Q_ASSERT(trackerEntry.url == QString::fromStdString(nativeEntry.url)); trackerEntry.tier = nativeEntry.tier; // remove outdated endpoints - trackerEntry.stats.removeIf([&nativeEntry](const decltype(trackerEntry.stats)::iterator &iter) + trackerEntry.endpointEntries.removeIf([&nativeEntry](const QHash, TrackerEndpointEntry>::iterator &iter) { return std::none_of(nativeEntry.endpoints.cbegin(), nativeEntry.endpoints.cend() - , [&endpoint = iter.key()](const auto &existingEndpoint) + , [&endpointName = std::get<0>(iter.key())](const auto &existingEndpoint) { - return (endpoint == existingEndpoint.local_endpoint); + return (endpointName == toString(existingEndpoint.local_endpoint)); }); }); + const auto numEndpoints = static_cast(nativeEntry.endpoints.size()) * btProtocols.size(); + int numUpdating = 0; int numWorking = 0; int numNotWorking = 0; int numTrackerError = 0; int numUnreachable = 0; -#ifdef QBT_USES_LIBTORRENT2 - const auto numEndpoints = static_cast(nativeEntry.endpoints.size()) * ((hashes.has_v1() && hashes.has_v2()) ? 2 : 1); - for (const lt::announce_endpoint &endpoint : nativeEntry.endpoints) + + for (const lt::announce_endpoint <AnnounceEndpoint : nativeEntry.endpoints) { - const auto endpointName = QString::fromStdString((std::stringstream() << endpoint.local_endpoint).str()); + const auto endpointName = toString(ltAnnounceEndpoint.local_endpoint); - for (const auto protocolVersion : {lt::protocol_version::V1, lt::protocol_version::V2}) + for (const auto protocolVersion : btProtocols) { - if (!hashes.has(protocolVersion)) - continue; - - const lt::announce_infohash &infoHash = endpoint.info_hashes[protocolVersion]; - - const int protocolVersionNum = (protocolVersion == lt::protocol_version::V1) ? 1 : 2; - const QMap &endpointUpdateInfo = updateInfo[endpoint.local_endpoint]; - TrackerEntry::EndpointStats &trackerEndpoint = trackerEntry.stats[endpoint.local_endpoint][protocolVersionNum]; - - trackerEndpoint.name = endpointName; - trackerEndpoint.numPeers = endpointUpdateInfo.value(protocolVersionNum, trackerEndpoint.numPeers); - trackerEndpoint.numSeeds = infoHash.scrape_complete; - trackerEndpoint.numLeeches = infoHash.scrape_incomplete; - trackerEndpoint.numDownloaded = infoHash.scrape_downloaded; - trackerEndpoint.nextAnnounceTime = fromLTTimePoint32(infoHash.next_announce); - trackerEndpoint.minAnnounceTime = fromLTTimePoint32(infoHash.min_announce); - - if (infoHash.updating) +#ifdef QBT_USES_LIBTORRENT2 + Q_ASSERT((protocolVersion == 1) || (protocolVersion == 2)); + const auto ltProtocolVersion = (protocolVersion == 1) ? lt::protocol_version::V1 : lt::protocol_version::V2; + const lt::announce_infohash <AnnounceInfo = ltAnnounceEndpoint.info_hashes[ltProtocolVersion]; +#else + Q_ASSERT(protocolVersion == 1); + const lt::announce_endpoint <AnnounceInfo = ltAnnounceEndpoint; +#endif + const QMap &endpointUpdateInfo = updateInfo[ltAnnounceEndpoint.local_endpoint]; + TrackerEndpointEntry &trackerEndpointEntry = trackerEntry.endpointEntries[std::make_pair(endpointName, protocolVersion)]; + + trackerEndpointEntry.name = endpointName; + trackerEndpointEntry.btVersion = protocolVersion; + trackerEndpointEntry.numPeers = endpointUpdateInfo.value(protocolVersion, trackerEndpointEntry.numPeers); + trackerEndpointEntry.numSeeds = ltAnnounceInfo.scrape_complete; + trackerEndpointEntry.numLeeches = ltAnnounceInfo.scrape_incomplete; + trackerEndpointEntry.numDownloaded = ltAnnounceInfo.scrape_downloaded; + trackerEndpointEntry.nextAnnounceTime = fromLTTimePoint32(ltAnnounceInfo.next_announce); + trackerEndpointEntry.minAnnounceTime = fromLTTimePoint32(ltAnnounceInfo.min_announce); + + if (ltAnnounceInfo.updating) { - trackerEndpoint.status = TrackerEntry::Updating; + trackerEndpointEntry.status = TrackerEntryStatus::Updating; ++numUpdating; } - else if (infoHash.fails > 0) + else if (ltAnnounceInfo.fails > 0) { - if (infoHash.last_error == lt::errors::tracker_failure) + if (ltAnnounceInfo.last_error == lt::errors::tracker_failure) { - trackerEndpoint.status = TrackerEntry::TrackerError; + trackerEndpointEntry.status = TrackerEntryStatus::TrackerError; ++numTrackerError; } - else if (infoHash.last_error == lt::errors::announce_skipped) + else if (ltAnnounceInfo.last_error == lt::errors::announce_skipped) { - trackerEndpoint.status = TrackerEntry::Unreachable; + trackerEndpointEntry.status = TrackerEntryStatus::Unreachable; ++numUnreachable; } else { - trackerEndpoint.status = TrackerEntry::NotWorking; + trackerEndpointEntry.status = TrackerEntryStatus::NotWorking; ++numNotWorking; } } else if (nativeEntry.verified) { - trackerEndpoint.status = TrackerEntry::Working; + trackerEndpointEntry.status = TrackerEntryStatus::Working; ++numWorking; } else { - trackerEndpoint.status = TrackerEntry::NotContacted; + trackerEndpointEntry.status = TrackerEntryStatus::NotContacted; } - if (!infoHash.message.empty()) - { - trackerEndpoint.message = QString::fromStdString(infoHash.message); - } - else if (infoHash.last_error) - { - trackerEndpoint.message = QString::fromLocal8Bit(infoHash.last_error.message()); - } - else - { - trackerEndpoint.message.clear(); - } - } - } -#else - const auto numEndpoints = static_cast(nativeEntry.endpoints.size()); - for (const lt::announce_endpoint &endpoint : nativeEntry.endpoints) - { - const int protocolVersionNum = 1; - const QMap &endpointUpdateInfo = updateInfo[endpoint.local_endpoint]; - TrackerEntry::EndpointStats &trackerEndpoint = trackerEntry.stats[endpoint.local_endpoint][protocolVersionNum]; - - trackerEndpoint.name = QString::fromStdString((std::stringstream() << endpoint.local_endpoint).str()); - trackerEndpoint.numPeers = endpointUpdateInfo.value(protocolVersionNum, trackerEndpoint.numPeers); - trackerEndpoint.numSeeds = endpoint.scrape_complete; - trackerEndpoint.numLeeches = endpoint.scrape_incomplete; - trackerEndpoint.numDownloaded = endpoint.scrape_downloaded; - trackerEndpoint.nextAnnounceTime = fromLTTimePoint32(endpoint.next_announce); - trackerEndpoint.minAnnounceTime = fromLTTimePoint32(endpoint.min_announce); - - if (endpoint.updating) - { - trackerEndpoint.status = TrackerEntry::Updating; - ++numUpdating; - } - else if (endpoint.fails > 0) - { - if (endpoint.last_error == lt::errors::tracker_failure) + if (!ltAnnounceInfo.message.empty()) { - trackerEndpoint.status = TrackerEntry::TrackerError; - ++numTrackerError; + trackerEndpointEntry.message = QString::fromStdString(ltAnnounceInfo.message); } - else if (endpoint.last_error == lt::errors::announce_skipped) + else if (ltAnnounceInfo.last_error) { - trackerEndpoint.status = TrackerEntry::Unreachable; - ++numUnreachable; + trackerEndpointEntry.message = QString::fromLocal8Bit(ltAnnounceInfo.last_error.message()); } else { - trackerEndpoint.status = TrackerEntry::NotWorking; - ++numNotWorking; + trackerEndpointEntry.message.clear(); } } - else if (nativeEntry.verified) - { - trackerEndpoint.status = TrackerEntry::Working; - ++numWorking; - } - else - { - trackerEndpoint.status = TrackerEntry::NotContacted; - } - - if (!endpoint.message.empty()) - { - trackerEndpoint.message = QString::fromStdString(endpoint.message); - } - else if (endpoint.last_error) - { - trackerEndpoint.message = QString::fromLocal8Bit(endpoint.last_error.message()); - } - else - { - trackerEndpoint.message.clear(); - } } -#endif if (numEndpoints > 0) { if (numUpdating > 0) { - trackerEntry.status = TrackerEntry::Updating; + trackerEntry.status = TrackerEntryStatus::Updating; } else if (numWorking > 0) { - trackerEntry.status = TrackerEntry::Working; + trackerEntry.status = TrackerEntryStatus::Working; } else if (numTrackerError > 0) { - trackerEntry.status = TrackerEntry::TrackerError; + trackerEntry.status = TrackerEntryStatus::TrackerError; } else if (numUnreachable == numEndpoints) { - trackerEntry.status = TrackerEntry::Unreachable; + trackerEntry.status = TrackerEntryStatus::Unreachable; } else if ((numUnreachable + numNotWorking) == numEndpoints) { - trackerEntry.status = TrackerEntry::NotWorking; + trackerEntry.status = TrackerEntryStatus::NotWorking; + } + } + + trackerEntry.numPeers = -1; + trackerEntry.numSeeds = -1; + trackerEntry.numLeeches = -1; + trackerEntry.numDownloaded = -1; + trackerEntry.nextAnnounceTime = QDateTime(); + trackerEntry.minAnnounceTime = QDateTime(); + trackerEntry.message.clear(); + + for (const TrackerEndpointEntry &endpointEntry : asConst(trackerEntry.endpointEntries)) + { + trackerEntry.numPeers = std::max(trackerEntry.numPeers, endpointEntry.numPeers); + trackerEntry.numSeeds = std::max(trackerEntry.numSeeds, endpointEntry.numSeeds); + trackerEntry.numLeeches = std::max(trackerEntry.numLeeches, endpointEntry.numLeeches); + trackerEntry.numDownloaded = std::max(trackerEntry.numDownloaded, endpointEntry.numDownloaded); + + if (endpointEntry.status == trackerEntry.status) + { + if (!trackerEntry.nextAnnounceTime.isValid() || (trackerEntry.nextAnnounceTime > endpointEntry.nextAnnounceTime)) + { + trackerEntry.nextAnnounceTime = endpointEntry.nextAnnounceTime; + trackerEntry.minAnnounceTime = endpointEntry.minAnnounceTime; + if ((endpointEntry.status != TrackerEntryStatus::Working) + || !endpointEntry.message.isEmpty()) + { + trackerEntry.message = endpointEntry.message; + } + } + + if (endpointEntry.status == TrackerEntryStatus::Working) + { + if (trackerEntry.message.isEmpty()) + trackerEntry.message = endpointEntry.message; + } } } } @@ -405,6 +382,11 @@ bool TorrentImpl::isValid() const return m_nativeHandle.is_valid(); } +Session *TorrentImpl::session() const +{ + return m_session; +} + InfoHash TorrentImpl::infoHash() const { return m_infoHash; @@ -1667,7 +1649,7 @@ void TorrentImpl::fileSearchFinished(const Path &savePath, const PathList &fileN endReceivedMetadataHandling(savePath, fileNames); } -TrackerEntry TorrentImpl::updateTrackerEntry(const lt::announce_entry &announceEntry, const QHash> &updateInfo) +TrackerEntry TorrentImpl::updateTrackerEntry(const lt::announce_entry &announceEntry, const QHash> &updateInfo) { const auto it = std::find_if(m_trackerEntries.begin(), m_trackerEntries.end() , [&announceEntry](const TrackerEntry &trackerEntry) @@ -1680,10 +1662,16 @@ TrackerEntry TorrentImpl::updateTrackerEntry(const lt::announce_entry &announceE return {}; #ifdef QBT_USES_LIBTORRENT2 - ::updateTrackerEntry(*it, announceEntry, nativeHandle().info_hashes(), updateInfo); + QSet btProtocols; + const auto &infoHashes = nativeHandle().info_hashes(); + if (infoHashes.has(lt::protocol_version::V1)) + btProtocols.insert(1); + if (infoHashes.has(lt::protocol_version::V2)) + btProtocols.insert(2); #else - ::updateTrackerEntry(*it, announceEntry, updateInfo); + const QSet btProtocols {1}; #endif + ::updateTrackerEntry(*it, announceEntry, btProtocols, updateInfo); return *it; } diff --git a/src/base/bittorrent/torrentimpl.h b/src/base/bittorrent/torrentimpl.h index b8a3b4bf1..78f33e414 100644 --- a/src/base/bittorrent/torrentimpl.h +++ b/src/base/bittorrent/torrentimpl.h @@ -99,6 +99,8 @@ namespace BitTorrent bool isValid() const; + Session *session() const override; + InfoHash infoHash() const override; QString name() const override; QDateTime creationDate() const override; @@ -264,7 +266,7 @@ namespace BitTorrent void saveResumeData(lt::resume_data_flags_t flags = {}); void handleMoveStorageJobFinished(const Path &path, MoveStorageContext context, bool hasOutstandingJob); void fileSearchFinished(const Path &savePath, const PathList &fileNames); - TrackerEntry updateTrackerEntry(const lt::announce_entry &announceEntry, const QHash> &updateInfo); + TrackerEntry updateTrackerEntry(const lt::announce_entry &announceEntry, const QHash> &updateInfo); void resetTrackerEntries(); private: diff --git a/src/base/bittorrent/trackerentry.cpp b/src/base/bittorrent/trackerentry.cpp index 776053911..f81e9c29e 100644 --- a/src/base/bittorrent/trackerentry.cpp +++ b/src/base/bittorrent/trackerentry.cpp @@ -1,6 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2015-2022 Vladimir Golovnev + * Copyright (C) 2015-2023 Vladimir Golovnev * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -29,13 +29,12 @@ #include "trackerentry.h" #include -#include -QVector BitTorrent::parseTrackerEntries(const QStringView str) +QList BitTorrent::parseTrackerEntries(const QStringView str) { const QList trackers = str.split(u'\n'); // keep the empty parts to track tracker tier - QVector entries; + QList entries; entries.reserve(trackers.size()); int trackerTier = 0; diff --git a/src/base/bittorrent/trackerentry.h b/src/base/bittorrent/trackerentry.h index e996c9c6e..33fce42f5 100644 --- a/src/base/bittorrent/trackerentry.h +++ b/src/base/bittorrent/trackerentry.h @@ -28,8 +28,6 @@ #pragma once -#include - #include #include #include @@ -38,44 +36,52 @@ namespace BitTorrent { - struct TrackerEntry + enum class TrackerEntryStatus { - using Endpoint = lt::tcp::endpoint; - - enum Status - { - NotContacted = 1, - Working = 2, - Updating = 3, - NotWorking = 4, - TrackerError = 5, - Unreachable = 6 - }; - - struct EndpointStats - { - QString name {}; + NotContacted = 1, + Working = 2, + Updating = 3, + NotWorking = 4, + TrackerError = 5, + Unreachable = 6 + }; + struct TrackerEndpointEntry + { + QString name {}; + int btVersion = 1; - Status status = NotContacted; - QString message {}; + TrackerEntryStatus status = TrackerEntryStatus::NotContacted; + QString message {}; - int numPeers = -1; - int numSeeds = -1; - int numLeeches = -1; - int numDownloaded = -1; + int numPeers = -1; + int numSeeds = -1; + int numLeeches = -1; + int numDownloaded = -1; - QDateTime nextAnnounceTime; - QDateTime minAnnounceTime; - }; + QDateTime nextAnnounceTime {}; + QDateTime minAnnounceTime {}; + }; + struct TrackerEntry + { QString url {}; int tier = 0; - Status status = NotContacted; - QHash> stats {}; + TrackerEntryStatus status = TrackerEntryStatus::NotContacted; + QString message {}; + + int numPeers = -1; + int numSeeds = -1; + int numLeeches = -1; + int numDownloaded = -1; + + QDateTime nextAnnounceTime {}; + QDateTime minAnnounceTime {}; + + QHash, TrackerEndpointEntry> endpointEntries {}; }; - QVector parseTrackerEntries(QStringView str); + QList parseTrackerEntries(QStringView str); bool operator==(const TrackerEntry &left, const TrackerEntry &right); std::size_t qHash(const TrackerEntry &key, std::size_t seed = 0); diff --git a/src/base/preferences.cpp b/src/base/preferences.cpp index 56d7057a6..678f499fb 100644 --- a/src/base/preferences.cpp +++ b/src/base/preferences.cpp @@ -1754,14 +1754,14 @@ void Preferences::setPropVisible(const bool visible) setValue(u"TorrentProperties/Visible"_s, visible); } -QByteArray Preferences::getPropTrackerListState() const +QByteArray Preferences::getTrackerListState() const { return value(u"GUI/Qt6/TorrentProperties/TrackerListState"_s); } -void Preferences::setPropTrackerListState(const QByteArray &state) +void Preferences::setTrackerListState(const QByteArray &state) { - if (state == getPropTrackerListState()) + if (state == getTrackerListState()) return; setValue(u"GUI/Qt6/TorrentProperties/TrackerListState"_s, state); diff --git a/src/base/preferences.h b/src/base/preferences.h index 44d357453..58f710330 100644 --- a/src/base/preferences.h +++ b/src/base/preferences.h @@ -370,8 +370,8 @@ public: void setPropCurTab(int tab); bool getPropVisible() const; void setPropVisible(bool visible); - QByteArray getPropTrackerListState() const; - void setPropTrackerListState(const QByteArray &state); + QByteArray getTrackerListState() const; + void setTrackerListState(const QByteArray &state); QStringList getRssOpenFolders() const; void setRssOpenFolders(const QStringList &folders); QByteArray getRssSideSplitterState() const; diff --git a/src/base/utils/misc.h b/src/base/utils/misc.h index 7f4f4fa2c..3418faeb9 100644 --- a/src/base/utils/misc.h +++ b/src/base/utils/misc.h @@ -60,7 +60,7 @@ namespace Utils::Misc // YobiByte, // 1024^8 }; -enum class TimeResolution + enum class TimeResolution { Seconds, Minutes diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 17cef5d1f..e8146f4d9 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -17,7 +17,6 @@ qt_wrap_ui(UI_HEADERS previewselectdialog.ui properties/peersadditiondialog.ui properties/propertieswidget.ui - properties/trackersadditiondialog.ui rss/automatedrssdownloader.ui rss/rsswidget.ui search/pluginselectdialog.ui @@ -32,6 +31,7 @@ qt_wrap_ui(UI_HEADERS torrentoptionsdialog.ui torrenttagsdialog.ui trackerentriesdialog.ui + trackersadditiondialog.ui uithemedialog.ui watchedfolderoptionsdialog.ui ) @@ -79,8 +79,6 @@ add_library(qbt_gui STATIC properties/proptabbar.h properties/speedplotview.h properties/speedwidget.h - properties/trackerlistwidget.h - properties/trackersadditiondialog.h raisedmessagebox.h rss/articlelistwidget.h rss/automatedrssdownloader.h @@ -108,6 +106,11 @@ add_library(qbt_gui STATIC torrentoptionsdialog.h torrenttagsdialog.h trackerentriesdialog.h + trackerlist/trackerlistitemdelegate.h + trackerlist/trackerlistmodel.h + trackerlist/trackerlistsortmodel.h + trackerlist/trackerlistwidget.h + trackersadditiondialog.h transferlistdelegate.h transferlistfilters/basefilterwidget.h transferlistfilters/categoryfiltermodel.h @@ -172,8 +175,6 @@ add_library(qbt_gui STATIC properties/proptabbar.cpp properties/speedplotview.cpp properties/speedwidget.cpp - properties/trackerlistwidget.cpp - properties/trackersadditiondialog.cpp raisedmessagebox.cpp rss/articlelistwidget.cpp rss/automatedrssdownloader.cpp @@ -201,6 +202,11 @@ add_library(qbt_gui STATIC torrentoptionsdialog.cpp torrenttagsdialog.cpp trackerentriesdialog.cpp + trackerlist/trackerlistitemdelegate.cpp + trackerlist/trackerlistmodel.cpp + trackerlist/trackerlistsortmodel.cpp + trackerlist/trackerlistwidget.cpp + trackersadditiondialog.cpp transferlistdelegate.cpp transferlistfilters/basefilterwidget.cpp transferlistfilters/categoryfiltermodel.cpp diff --git a/src/gui/mainwindow.cpp b/src/gui/mainwindow.cpp index 6aa40d65f..79520012e 100644 --- a/src/gui/mainwindow.cpp +++ b/src/gui/mainwindow.cpp @@ -87,13 +87,13 @@ #include "powermanagement/powermanagement.h" #include "properties/peerlistwidget.h" #include "properties/propertieswidget.h" -#include "properties/trackerlistwidget.h" #include "rss/rsswidget.h" #include "search/searchwidget.h" #include "speedlimitdialog.h" #include "statsdialog.h" #include "statusbar.h" #include "torrentcreatordialog.h" +#include "trackerlist/trackerlistwidget.h" #include "transferlistfilterswidget.h" #include "transferlistmodel.h" #include "transferlistwidget.h" @@ -245,7 +245,6 @@ MainWindow::MainWindow(IGUIApplication *app, WindowState initialState) connect(m_columnFilterEdit, &LineEdit::textChanged, this, &MainWindow::applyTransferListFilter); connect(hSplitter, &QSplitter::splitterMoved, this, &MainWindow::saveSettings); connect(m_splitter, &QSplitter::splitterMoved, this, &MainWindow::saveSplitterSettings); - connect(BitTorrent::Session::instance(), &BitTorrent::Session::trackersChanged, m_propertiesWidget, &PropertiesWidget::loadTrackers); #ifdef Q_OS_MACOS // Increase top spacing to avoid tab overlapping diff --git a/src/gui/properties/propertieswidget.cpp b/src/gui/properties/propertieswidget.cpp index 572da74d9..857c47f50 100644 --- a/src/gui/properties/propertieswidget.cpp +++ b/src/gui/properties/propertieswidget.cpp @@ -53,6 +53,7 @@ #include "base/utils/string.h" #include "gui/autoexpandabledialog.h" #include "gui/lineedit.h" +#include "gui/trackerlist/trackerlistwidget.h" #include "gui/uithememanager.h" #include "gui/utils.h" #include "downloadedpiecesbar.h" @@ -60,7 +61,6 @@ #include "pieceavailabilitybar.h" #include "proptabbar.h" #include "speedwidget.h" -#include "trackerlistwidget.h" #include "ui_propertieswidget.h" PropertiesWidget::PropertiesWidget(QWidget *parent) @@ -115,8 +115,8 @@ PropertiesWidget::PropertiesWidget(QWidget *parent) m_ui->trackerUpButton->setIconSize(Utils::Gui::smallIconSize()); m_ui->trackerDownButton->setIcon(UIThemeManager::instance()->getIcon(u"go-down"_s)); m_ui->trackerDownButton->setIconSize(Utils::Gui::smallIconSize()); - connect(m_ui->trackerUpButton, &QPushButton::clicked, m_trackerList, &TrackerListWidget::moveSelectionUp); - connect(m_ui->trackerDownButton, &QPushButton::clicked, m_trackerList, &TrackerListWidget::moveSelectionDown); + connect(m_ui->trackerUpButton, &QPushButton::clicked, m_trackerList, &TrackerListWidget::decreaseSelectedTrackerTiers); + connect(m_ui->trackerDownButton, &QPushButton::clicked, m_trackerList, &TrackerListWidget::increaseSelectedTrackerTiers); m_ui->hBoxLayoutTrackers->insertWidget(0, m_trackerList); // Peers list m_peerList = new PeerListWidget(this); @@ -230,7 +230,6 @@ void PropertiesWidget::clear() m_ui->labelLastSeenCompleteVal->clear(); m_ui->labelCreatedByVal->clear(); m_ui->labelAddedOnVal->clear(); - m_trackerList->clear(); m_downloadedPieces->clear(); m_piecesAvailability->clear(); m_peerList->clear(); @@ -263,12 +262,6 @@ void PropertiesWidget::updateSavePath(BitTorrent::Torrent *const torrent) m_ui->labelSavePathVal->setText(m_torrent->savePath().toString()); } -void PropertiesWidget::loadTrackers(BitTorrent::Torrent *const torrent) -{ - if (torrent == m_torrent) - m_trackerList->loadTrackers(); -} - void PropertiesWidget::updateTorrentInfos(BitTorrent::Torrent *const torrent) { if (torrent == m_torrent) @@ -281,6 +274,7 @@ void PropertiesWidget::loadTorrentInfos(BitTorrent::Torrent *const torrent) m_torrent = torrent; m_downloadedPieces->setTorrent(m_torrent); m_piecesAvailability->setTorrent(m_torrent); + m_trackerList->setTorrent(m_torrent); m_ui->filesList->setContentHandler(m_torrent); if (!m_torrent) return; @@ -466,10 +460,6 @@ void PropertiesWidget::loadDynamicData() } } break; - case PropTabBar::TrackersTab: - // Trackers - m_trackerList->loadTrackers(); - break; case PropTabBar::PeersTab: // Load peers m_peerList->loadPeers(m_torrent); diff --git a/src/gui/properties/propertieswidget.h b/src/gui/properties/propertieswidget.h index a8f0f9f95..f8dfc9eba 100644 --- a/src/gui/properties/propertieswidget.h +++ b/src/gui/properties/propertieswidget.h @@ -82,7 +82,6 @@ public slots: void readSettings(); void saveSettings(); void reloadPreferences(); - void loadTrackers(BitTorrent::Torrent *torrent); protected slots: void updateTorrentInfos(BitTorrent::Torrent *torrent); diff --git a/src/gui/properties/trackerlistwidget.cpp b/src/gui/properties/trackerlistwidget.cpp deleted file mode 100644 index 0624a792e..000000000 --- a/src/gui/properties/trackerlistwidget.cpp +++ /dev/null @@ -1,813 +0,0 @@ -/* - * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2006 Christophe Dumez - * - * 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 -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#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 TrackerListWidget::getSelectedTrackerItems() const -{ - const QList selectedTrackerItems = selectedItems(); - QVector 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 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 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 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 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; - torrent->fetchPeerInfo([this, torrent = TorrentPtr(torrent)](const QVector &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 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 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 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 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 selItems = selectedItems(); - if (selItems.isEmpty()) return; - - BitTorrent::Torrent *const torrent = m_properties->getCurrentTorrent(); - if (!torrent) return; - - const QVector 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 -} diff --git a/src/gui/trackerlist/trackerlistitemdelegate.cpp b/src/gui/trackerlist/trackerlistitemdelegate.cpp new file mode 100644 index 000000000..5aec4cb49 --- /dev/null +++ b/src/gui/trackerlist/trackerlistitemdelegate.cpp @@ -0,0 +1,65 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2023 Vladimir Golovnev + * + * 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 +#include + +#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; + } +} diff --git a/src/gui/trackerlist/trackerlistitemdelegate.h b/src/gui/trackerlist/trackerlistitemdelegate.h new file mode 100644 index 000000000..2cc6a16bf --- /dev/null +++ b/src/gui/trackerlist/trackerlistitemdelegate.h @@ -0,0 +1,49 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2023 Vladimir Golovnev + * + * 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 + +//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; +}; diff --git a/src/gui/trackerlist/trackerlistmodel.cpp b/src/gui/trackerlist/trackerlistmodel.cpp new file mode 100644 index 000000000..3ac377327 --- /dev/null +++ b/src/gui/trackerlist/trackerlistmodel.cpp @@ -0,0 +1,781 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2023 Vladimir Golovnev + * Copyright (C) 2006 Christophe Dumez + * + * 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 + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#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 parentItem {}; + + multi_index_container, indexed_by< + random_access<>, + hashed_unique, composite_key< + Item, + member, + member + >> + >> childItems {}; + + Item(QStringView name, QStringView message); + explicit Item(const BitTorrent::TrackerEntry &trackerEntry); + Item(const std::shared_ptr &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, + indexed_by< + random_access<>, + hashed_unique, member>>> +{ +}; + +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 &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()} + , 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 &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 &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 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(u"** [DHT] **", privateTorrentMessage)); + m_items->emplace_back(std::make_shared(u"** [PeX] **", privateTorrentMessage)); + m_items->emplace_back(std::make_shared(u"** [LSD] **", privateTorrentMessage)); + + using TorrentPtr = QPointer; + m_torrent->fetchPeerInfo([this, torrent = TorrentPtr(m_torrent)](const QList &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->numSeeds = seedsDHT; + item->numLeeches = peersDHT; + return true; + }); + itemsByPos.modify((itemsByPos.begin() + ROW_PEX), [&seedsPeX, &peersPeX](std::shared_ptr &item) + { + item->numSeeds = seedsPeX; + item->numLeeches = peersPeX; + return true; + }); + itemsByPos.modify((itemsByPos.begin() + ROW_LSD), [&seedsLSD, &peersLSD](std::shared_ptr &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::createTrackerItem(const BitTorrent::TrackerEntry &trackerEntry) +{ + auto item = std::make_shared(trackerEntry); + for (const auto &[id, endpointEntry] : trackerEntry.endpointEntries.asKeyValueRange()) + { + item->childItems.emplace_back(std::make_shared(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, const BitTorrent::TrackerEntry &trackerEntry) +{ + QSet> endpointItemIDs; + QList> newEndpointItems; + for (const auto &[id, endpointEntry] : trackerEntry.endpointEntries.asKeyValueRange()) + { + endpointItemIDs.insert(id); + + auto &itemsByID = item->childItems.get(); + 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, 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(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(index.internalPointer()); + Q_ASSERT(itemPtr); + if (!itemPtr) [[unlikely]] + return {}; + + if (itemPtr->announceTimestamp != m_announceTimestamp) + { + itemPtr->secsToNextAnnounce = std::max(0, m_announceTimestamp.secsTo(itemPtr->nextAnnounceTime)); + itemPtr->secsToMinAnnounce = std::max(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 = parent.isValid() + ? m_items->at(static_cast(parent.row()))->childItems.at(row) + : m_items->at(static_cast(row)); + return createIndex(row, column, item.get()); +} + +QModelIndex TrackerListModel::parent(const QModelIndex &index) const +{ + if (!index.isValid()) + return {}; + + const auto *item = static_cast(index.internalPointer()); + Q_ASSERT(item); + if (!item) [[unlikely]] + return {}; + + const std::shared_ptr parentItem = item->parentItem.lock(); + if (!parentItem) + return {}; + + const auto &itemsByName = m_items->get(); + 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 &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(); + 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 trackerItemIDs; + for (int i = 0; i < STICKY_ROW_COUNT; ++i) + trackerItemIDs.insert(m_items->at(i)->name); + + QList> newTrackerItems; + for (const BitTorrent::TrackerEntry &trackerEntry : m_torrent->trackers()) + { + trackerItemIDs.insert(trackerEntry.url); + + auto &itemsByName = m_items->get(); + 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 &updatedTrackers) +{ + for (const auto &[url, entry] : updatedTrackers.asKeyValueRange()) + { + auto &itemsByName = m_items->get(); + if (const auto &iter = itemsByName.find(entry.url); iter != itemsByName.end()) [[likely]] + { + updateTrackerItem(*iter, entry); + } + } +} diff --git a/src/gui/trackerlist/trackerlistmodel.h b/src/gui/trackerlist/trackerlistmodel.h new file mode 100644 index 000000000..2065a4eb2 --- /dev/null +++ b/src/gui/trackerlist/trackerlistmodel.h @@ -0,0 +1,119 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2023 Vladimir Golovnev + * Copyright (C) 2006 Christophe Dumez + * + * 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 + +#include +#include +#include + +#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 createTrackerItem(const BitTorrent::TrackerEntry &trackerEntry); + void addTrackerItem(const BitTorrent::TrackerEntry &trackerEntry); + void updateTrackerItem(const std::shared_ptr &item, const BitTorrent::TrackerEntry &trackerEntry); + void refreshAnnounceTimes(); + void onTrackersAdded(const QList &newTrackers); + void onTrackersRemoved(const QStringList &deletedTrackers); + void onTrackersChanged(); + void onTrackersUpdated(const QHash &updatedTrackers); + + BitTorrent::Session *m_btSession = nullptr; + BitTorrent::Torrent *m_torrent = nullptr; + + class Items; + std::unique_ptr m_items; + + QDateTime m_announceTimestamp; + QTimer *m_announceRefreshTimer = nullptr; +}; diff --git a/src/gui/trackerlist/trackerlistsortmodel.cpp b/src/gui/trackerlist/trackerlistsortmodel.cpp new file mode 100644 index 000000000..a3df7eec7 --- /dev/null +++ b/src/gui/trackerlist/trackerlistsortmodel.cpp @@ -0,0 +1,56 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2023 Vladimir Golovnev + * + * 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); +} diff --git a/src/gui/trackerlist/trackerlistsortmodel.h b/src/gui/trackerlist/trackerlistsortmodel.h new file mode 100644 index 000000000..b0400287a --- /dev/null +++ b/src/gui/trackerlist/trackerlistsortmodel.h @@ -0,0 +1,48 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2023 Vladimir Golovnev + * + * 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 + +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; +}; diff --git a/src/gui/trackerlist/trackerlistwidget.cpp b/src/gui/trackerlist/trackerlistwidget.cpp new file mode 100644 index 000000000..0cc73898d --- /dev/null +++ b/src/gui/trackerlist/trackerlistwidget.cpp @@ -0,0 +1,452 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2023 Vladimir Golovnev + * Copyright (C) 2006 Christophe Dumez + * + * 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 trackerURLs; + for (const QModelIndex &index : trackerIndexes) + { + trackerURLs.insert(index.siblingAtColumn(TrackerListModel::COL_URL).data().toString()); + } + + QList 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 trackerURLs; + for (const QModelIndex &index : trackerIndexes) + { + trackerURLs.insert(index.siblingAtColumn(TrackerListModel::COL_URL).data().toString()); + } + + QList trackers = m_model->torrent()->trackers(); + for (BitTorrent::TrackerEntry &trackerEntry : trackers) + { + if (trackerURLs.contains(trackerEntry.url)) + { + if (trackerEntry.tier < std::numeric_limits::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 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 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 &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 +} diff --git a/src/gui/properties/trackerlistwidget.h b/src/gui/trackerlist/trackerlistwidget.h similarity index 65% rename from src/gui/properties/trackerlistwidget.h rename to src/gui/trackerlist/trackerlistwidget.h index 71287a4f7..5131a516c 100644 --- a/src/gui/properties/trackerlistwidget.h +++ b/src/gui/trackerlist/trackerlistwidget.h @@ -1,5 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2023 Vladimir Golovnev * Copyright (C) 2006 Christophe Dumez * * This program is free software; you can redistribute it and/or @@ -28,75 +29,46 @@ #pragma once -#include #include +#include -class PropertiesWidget; +class TrackerListModel; namespace BitTorrent { class Torrent; } -class TrackerListWidget : public QTreeWidget +class TrackerListWidget : public QTreeView { Q_OBJECT Q_DISABLE_COPY_MOVE(TrackerListWidget) 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, + explicit TrackerListWidget(QWidget *parent = nullptr); + ~TrackerListWidget() override; - COL_COUNT - }; - - explicit TrackerListWidget(PropertiesWidget *properties); - ~TrackerListWidget(); + void setTorrent(BitTorrent::Torrent *torrent); + BitTorrent::Torrent *torrent() const; public slots: - void setRowColor(int row, const QColor &color); - - void moveSelectionUp(); - void moveSelectionDown(); - - void clear(); - void loadStickyItems(const BitTorrent::Torrent *torrent); - void loadTrackers(); + void decreaseSelectedTrackerTiers(); + void increaseSelectedTrackerTiers(); void copyTrackerUrl(); void reannounceSelected(); void deleteSelectedTrackers(); void editSelectedTracker(); void showTrackerListMenu(); + +private: + void setModel(QAbstractItemModel *model) override; + void wheelEvent(QWheelEvent *event) override; void loadSettings(); void saveSettings() const; - -protected: - QVector getSelectedTrackerItems() const; - -private slots: + QModelIndexList getSelectedTrackerRows() const; + int visibleColumnsCount() const; void openAddTrackersDialog(); void displayColumnHeaderMenu(); -private: - int visibleColumnsCount() const; - void wheelEvent(QWheelEvent *event) override; - - static QStringList headerLabels(); - - PropertiesWidget *m_properties = nullptr; - QHash m_trackerItems; - QTreeWidgetItem *m_DHTItem = nullptr; - QTreeWidgetItem *m_PEXItem = nullptr; - QTreeWidgetItem *m_LSDItem = nullptr; + TrackerListModel *m_model; }; diff --git a/src/gui/properties/trackersadditiondialog.cpp b/src/gui/trackersadditiondialog.cpp similarity index 100% rename from src/gui/properties/trackersadditiondialog.cpp rename to src/gui/trackersadditiondialog.cpp diff --git a/src/gui/properties/trackersadditiondialog.h b/src/gui/trackersadditiondialog.h similarity index 100% rename from src/gui/properties/trackersadditiondialog.h rename to src/gui/trackersadditiondialog.h diff --git a/src/gui/properties/trackersadditiondialog.ui b/src/gui/trackersadditiondialog.ui similarity index 100% rename from src/gui/properties/trackersadditiondialog.ui rename to src/gui/trackersadditiondialog.ui diff --git a/src/gui/transferlistfilters/categoryfiltermodel.cpp b/src/gui/transferlistfilters/categoryfiltermodel.cpp index 3ce7714c6..294966c31 100644 --- a/src/gui/transferlistfilters/categoryfiltermodel.cpp +++ b/src/gui/transferlistfilters/categoryfiltermodel.cpp @@ -310,7 +310,7 @@ void CategoryFilterModel::categoryAdded(const QString &categoryName) parent = findItem(expanded[expanded.count() - 2]); } - int row = parent->childCount(); + const int row = parent->childCount(); beginInsertRows(index(parent), row, row); new CategoryModelItem( parent, m_isSubcategoriesEnabled ? shortName(categoryName) : categoryName); @@ -322,7 +322,7 @@ void CategoryFilterModel::categoryRemoved(const QString &categoryName) auto *item = findItem(categoryName); if (item) { - QModelIndex i = index(item); + const QModelIndex i = index(item); beginRemoveRows(i.parent(), i.row(), i.row()); delete item; endRemoveRows(); diff --git a/src/gui/transferlistfilters/trackersfilterwidget.cpp b/src/gui/transferlistfilters/trackersfilterwidget.cpp index ff436cdb8..5c980d7c8 100644 --- a/src/gui/transferlistfilters/trackersfilterwidget.cpp +++ b/src/gui/transferlistfilters/trackersfilterwidget.cpp @@ -334,7 +334,7 @@ void TrackersFilterWidget::handleTrackerEntriesUpdated(const BitTorrent::Torrent for (const BitTorrent::TrackerEntry &trackerEntry : updatedTrackerEntries) { - if (trackerEntry.status == BitTorrent::TrackerEntry::Working) + if (trackerEntry.status == BitTorrent::TrackerEntryStatus::Working) { if (errorHashesIt != m_errors.end()) { @@ -342,13 +342,10 @@ void TrackersFilterWidget::handleTrackerEntriesUpdated(const BitTorrent::Torrent errored.remove(trackerEntry.url); } - const bool hasNoWarningMessages = std::all_of(trackerEntry.stats.cbegin(), trackerEntry.stats.cend(), [](const auto &endpoint) + const bool hasNoWarningMessages = std::all_of(trackerEntry.endpointEntries.cbegin(), trackerEntry.endpointEntries.cend() + , [](const BitTorrent::TrackerEndpointEntry &endpointEntry) { - return std::all_of(endpoint.cbegin(), endpoint.cend() - , [](const BitTorrent::TrackerEntry::EndpointStats &protocolStats) - { - return protocolStats.message.isEmpty() || (protocolStats.status != BitTorrent::TrackerEntry::Working); - }); + return endpointEntry.message.isEmpty() || (endpointEntry.status != BitTorrent::TrackerEntryStatus::Working); }); if (hasNoWarningMessages) { @@ -365,9 +362,9 @@ void TrackersFilterWidget::handleTrackerEntriesUpdated(const BitTorrent::Torrent warningHashesIt.value().insert(trackerEntry.url); } } - else if ((trackerEntry.status == BitTorrent::TrackerEntry::NotWorking) - || (trackerEntry.status == BitTorrent::TrackerEntry::TrackerError) - || (trackerEntry.status == BitTorrent::TrackerEntry::Unreachable)) + else if ((trackerEntry.status == BitTorrent::TrackerEntryStatus::NotWorking) + || (trackerEntry.status == BitTorrent::TrackerEntryStatus::TrackerError) + || (trackerEntry.status == BitTorrent::TrackerEntryStatus::Unreachable)) { if (errorHashesIt == m_errors.end()) errorHashesIt = m_errors.insert(id, {}); diff --git a/src/webui/api/synccontroller.cpp b/src/webui/api/synccontroller.cpp index de2af6563..0d25e03fa 100644 --- a/src/webui/api/synccontroller.cpp +++ b/src/webui/api/synccontroller.cpp @@ -482,6 +482,8 @@ void SyncController::maindataAction() connect(btSession, &BitTorrent::Session::torrentTagAdded, this, &SyncController::onTorrentTagAdded); connect(btSession, &BitTorrent::Session::torrentTagRemoved, this, &SyncController::onTorrentTagRemoved); connect(btSession, &BitTorrent::Session::torrentsUpdated, this, &SyncController::onTorrentsUpdated); + connect(btSession, &BitTorrent::Session::trackersAdded, this, &SyncController::onTorrentTrackersChanged); + connect(btSession, &BitTorrent::Session::trackersRemoved, this, &SyncController::onTorrentTrackersChanged); connect(btSession, &BitTorrent::Session::trackersChanged, this, &SyncController::onTorrentTrackersChanged); } diff --git a/src/webui/api/torrentscontroller.cpp b/src/webui/api/torrentscontroller.cpp index 5fa36a8b4..661a8d761 100644 --- a/src/webui/api/torrentscontroller.cpp +++ b/src/webui/api/torrentscontroller.cpp @@ -178,7 +178,7 @@ namespace } } - const int working = static_cast(BitTorrent::TrackerEntry::Working); + const int working = static_cast(BitTorrent::TrackerEntryStatus::Working); const int disabled = 0; const QString privateMsg {QCoreApplication::translate("TrackerListWidget", "This torrent is private")}; @@ -483,57 +483,19 @@ void TorrentsController::trackersAction() for (const BitTorrent::TrackerEntry &tracker : asConst(torrent->trackers())) { - int numPeers = -1; - int numSeeds = -1; - int numLeeches = -1; - int numDownloaded = -1; - QDateTime nextAnnounceTime; - QDateTime minAnnounceTime; - QString message; - for (const auto &endpoint : tracker.stats) - { - for (const auto &protocolStat : endpoint) - { - numPeers = std::max(numPeers, protocolStat.numPeers); - numSeeds = std::max(numSeeds, protocolStat.numSeeds); - numLeeches = std::max(numLeeches, protocolStat.numLeeches); - numDownloaded = std::max(numDownloaded, protocolStat.numDownloaded); - - if (protocolStat.status == tracker.status) - { - if (!nextAnnounceTime.isValid() || (nextAnnounceTime > protocolStat.nextAnnounceTime)) - { - nextAnnounceTime = protocolStat.nextAnnounceTime; - minAnnounceTime = protocolStat.minAnnounceTime; - if ((protocolStat.status != BitTorrent::TrackerEntry::Status::Working) - || !protocolStat.message.isEmpty()) - { - message = protocolStat.message; - } - } - - if (protocolStat.status == BitTorrent::TrackerEntry::Status::Working) - { - if (message.isEmpty()) - message = protocolStat.message; - } - } - } - } - - const bool isNotWorking = (tracker.status == BitTorrent::TrackerEntry::Status::NotWorking) - || (tracker.status == BitTorrent::TrackerEntry::Status::TrackerError) - || (tracker.status == BitTorrent::TrackerEntry::Status::Unreachable); + const bool isNotWorking = (tracker.status == BitTorrent::TrackerEntryStatus::NotWorking) + || (tracker.status == BitTorrent::TrackerEntryStatus::TrackerError) + || (tracker.status == BitTorrent::TrackerEntryStatus::Unreachable); trackerList << QJsonObject { {KEY_TRACKER_URL, tracker.url}, {KEY_TRACKER_TIER, tracker.tier}, - {KEY_TRACKER_STATUS, static_cast((isNotWorking ? BitTorrent::TrackerEntry::Status::NotWorking : tracker.status))}, - {KEY_TRACKER_MSG, message}, - {KEY_TRACKER_PEERS_COUNT, numPeers}, - {KEY_TRACKER_SEEDS_COUNT, numSeeds}, - {KEY_TRACKER_LEECHES_COUNT, numLeeches}, - {KEY_TRACKER_DOWNLOADED_COUNT, numDownloaded} + {KEY_TRACKER_STATUS, static_cast((isNotWorking ? BitTorrent::TrackerEntryStatus::NotWorking : tracker.status))}, + {KEY_TRACKER_MSG, tracker.message}, + {KEY_TRACKER_PEERS_COUNT, tracker.numPeers}, + {KEY_TRACKER_SEEDS_COUNT, tracker.numSeeds}, + {KEY_TRACKER_LEECHES_COUNT, tracker.numLeeches}, + {KEY_TRACKER_DOWNLOADED_COUNT, tracker.numDownloaded} }; }